From cba04fc6abc63ba02f7b0be092ce7119310145eb Mon Sep 17 00:00:00 2001 From: mabaega Date: Wed, 19 Aug 2026 21:24:37 +0700 Subject: [PATCH 1/2] Fix custom theme not applied to transactions generated from recurring templates Recurring transaction models (RecurringPurchaseInvoice, RecurringSalesInvoice, RecurringPurchaseOrder, RecurringSalesQuote, RecurringSalesOrder) each expose their own uniquely-named custom theme fields (e.g. HasPurchaseInvoiceCustomTheme / PurchaseInvoiceCustomTheme), while the generated transactions expose CustomTheme / CustomThemeId via IHasCustomTheme. Since BusinessTemplate.Copy() maps members by name, these never matched, so the selected theme was silently dropped whenever a transaction was generated from a recurring template. Bridge the two via an explicit IHasCustomTheme implementation on each recurring model, and copy the value explicitly in NakedObjectsOfPendingRecurringTransactions right after Copy(), mirroring how Date is already handled there. Co-Authored-By: Claude Sonnet 5 --- .../NakedObjectsOfPendingRecurringTransactions.cs | 9 +++++++++ ManagerServer/Model/RecurringPurchaseInvoice.cs | 5 ++++- ManagerServer/Model/RecurringPurchaseOrder.cs | 5 ++++- ManagerServer/Model/RecurringSalesInvoice.cs | 5 ++++- ManagerServer/Model/RecurringSalesOrder.cs | 5 ++++- ManagerServer/Model/RecurringSalesQuote.cs | 5 ++++- 6 files changed, 29 insertions(+), 5 deletions(-) diff --git a/ManagerServer/HttpHandlers/Businesses/Business/NakedObjectsOfPendingRecurringTransactions.cs b/ManagerServer/HttpHandlers/Businesses/Business/NakedObjectsOfPendingRecurringTransactions.cs index 056623d..12898cf 100644 --- a/ManagerServer/HttpHandlers/Businesses/Business/NakedObjectsOfPendingRecurringTransactions.cs +++ b/ManagerServer/HttpHandlers/Businesses/Business/NakedObjectsOfPendingRecurringTransactions.cs @@ -65,6 +65,15 @@ protected override async Task InnerPost() Copy(recurringTransaction, transaction); transaction.Date = recurringTransaction.NextIssueDate.Value; + // Copy() only matches members by name, but each recurring transaction type exposes its own + // uniquely-named custom theme fields (e.g. HasPurchaseInvoiceCustomTheme), so this must be + // bridged explicitly via the shared IHasCustomTheme interface. + if (recurringTransaction is ManagerServer.Model.IHasCustomTheme recurringCustomTheme && transaction is ManagerServer.Model.IHasCustomTheme transactionCustomTheme) + { + transactionCustomTheme.CustomTheme = recurringCustomTheme.CustomTheme; + transactionCustomTheme.CustomThemeId = recurringCustomTheme.CustomThemeId; + } + ((ManagerServer.Model.Object)transaction).Key = Guid.CreateVersion7(); list.Add((ManagerServer.Model.Object)transaction); diff --git a/ManagerServer/Model/RecurringPurchaseInvoice.cs b/ManagerServer/Model/RecurringPurchaseInvoice.cs index 53eee9c..352fd39 100644 --- a/ManagerServer/Model/RecurringPurchaseInvoice.cs +++ b/ManagerServer/Model/RecurringPurchaseInvoice.cs @@ -15,7 +15,7 @@ namespace ManagerServer.Model [ProtoContract] [Guid("11de04ac-c448-4665-b206-8aa631e63532")] [Currency(nameof(Supplier))] - public sealed class RecurringPurchaseInvoice : Object, IRecurringTransactionFor, ICustomFields + public sealed class RecurringPurchaseInvoice : Object, IRecurringTransactionFor, ICustomFields, IHasCustomTheme { [Guide("The date when the next purchase invoice will be automatically created. This date advances automatically based on your frequency settings.")] [Guide("Set this to match when you expect to receive the supplier's invoice. The system checks daily for invoices due to be created.")] @@ -80,6 +80,9 @@ public sealed class RecurringPurchaseInvoice : Object, IRecurringTransactionFor< MonthDay IRecurringTransaction.MonthDay => MonthDay; + bool IHasCustomTheme.CustomTheme { get => HasPurchaseInvoiceCustomTheme; set => HasPurchaseInvoiceCustomTheme = value; } + Guid? IHasCustomTheme.CustomThemeId { get => PurchaseInvoiceCustomTheme; set => PurchaseInvoiceCustomTheme = value; } + /* [ProtoContract] public sealed class Line diff --git a/ManagerServer/Model/RecurringPurchaseOrder.cs b/ManagerServer/Model/RecurringPurchaseOrder.cs index 6cc22b7..0f33957 100644 --- a/ManagerServer/Model/RecurringPurchaseOrder.cs +++ b/ManagerServer/Model/RecurringPurchaseOrder.cs @@ -15,7 +15,7 @@ namespace ManagerServer.Model [ProtoContract] [Guid("3be38758-7bf2-46f1-84a5-34e8748cade0")] [Currency(nameof(Supplier))] - public sealed class RecurringPurchaseOrder : Object, IRecurringTransactionFor, ICustomFields + public sealed class RecurringPurchaseOrder : Object, IRecurringTransactionFor, ICustomFields, IHasCustomTheme { [Guide("The date when the next purchase order will be automatically created. This date advances automatically based on the interval and period type after each order is generated.")] [ProtoMember(1), NoWrap] public DateTime? NextIssueDate { get; set; } @@ -66,5 +66,8 @@ public sealed class RecurringPurchaseOrder : Object, IRecurringTransactionFor CustomFields2; MonthDay IRecurringTransaction.MonthDay => MonthDay; + + bool IHasCustomTheme.CustomTheme { get => HasPurchaseOrderCustomTheme; set => HasPurchaseOrderCustomTheme = value; } + Guid? IHasCustomTheme.CustomThemeId { get => PurchaseOrderCustomTheme; set => PurchaseOrderCustomTheme = value; } } } \ No newline at end of file diff --git a/ManagerServer/Model/RecurringSalesInvoice.cs b/ManagerServer/Model/RecurringSalesInvoice.cs index 2cbed6c..ccfeee4 100644 --- a/ManagerServer/Model/RecurringSalesInvoice.cs +++ b/ManagerServer/Model/RecurringSalesInvoice.cs @@ -16,7 +16,7 @@ namespace ManagerServer.Model [ProtoContract] [Guid("81385989-81e5-48c7-a819-c344324c1c01")] [Currency(nameof(Customer))] - public sealed class RecurringSalesInvoice : Object, IRecurringTransactionFor, ICustomFields + public sealed class RecurringSalesInvoice : Object, IRecurringTransactionFor, ICustomFields, IHasCustomTheme { [Guide("The date when the next sales invoice will be automatically created. This date advances automatically based on your frequency settings.")] [Guide("The system checks daily for recurring transactions due to be created. Set this to today or earlier to create the first invoice immediately.")] @@ -96,6 +96,9 @@ public sealed class RecurringSalesInvoice : Object, IRecurringTransactionFor MonthDay; + bool IHasCustomTheme.CustomTheme { get => HasSalesInvoiceCustomTheme; set => HasSalesInvoiceCustomTheme = value; } + Guid? IHasCustomTheme.CustomThemeId { get => SalesInvoiceCustomTheme; set => SalesInvoiceCustomTheme = value; } + /* [ProtoContract] public sealed class Line diff --git a/ManagerServer/Model/RecurringSalesOrder.cs b/ManagerServer/Model/RecurringSalesOrder.cs index 02a389a..e55cecd 100644 --- a/ManagerServer/Model/RecurringSalesOrder.cs +++ b/ManagerServer/Model/RecurringSalesOrder.cs @@ -15,7 +15,7 @@ namespace ManagerServer.Model [ProtoContract] [Guid("dd7d5b17-c4be-4369-b0f5-79361525f3c2")] [Currency(nameof(Customer))] - public sealed class RecurringSalesOrder : ManagerServer.Model.Object, IRecurringTransactionFor, ICustomFields + public sealed class RecurringSalesOrder : ManagerServer.Model.Object, IRecurringTransactionFor, ICustomFields, IHasCustomTheme { [Guide("The date when the next sales order will be automatically created. This date advances automatically based on the interval and period type after each order is generated.")] [ProtoMember(1), NoWrap] public DateTime? NextIssueDate { get; set; } @@ -70,5 +70,8 @@ public sealed class RecurringSalesOrder : ManagerServer.Model.Object, IRecurring CustomFields ICustomFields.CustomFields => CustomFields2; MonthDay IRecurringTransaction.MonthDay => MonthDay; + + bool IHasCustomTheme.CustomTheme { get => HasSalesOrderCustomTheme; set => HasSalesOrderCustomTheme = value; } + Guid? IHasCustomTheme.CustomThemeId { get => SalesOrderCustomTheme; set => SalesOrderCustomTheme = value; } } } \ No newline at end of file diff --git a/ManagerServer/Model/RecurringSalesQuote.cs b/ManagerServer/Model/RecurringSalesQuote.cs index 9e5f9d8..b496e23 100644 --- a/ManagerServer/Model/RecurringSalesQuote.cs +++ b/ManagerServer/Model/RecurringSalesQuote.cs @@ -15,7 +15,7 @@ namespace ManagerServer.Model [ProtoContract] [Guid("1ca6ee3a-3583-41d8-83b1-74ac9129e1c1")] [Currency(nameof(Customer))] - public sealed class RecurringSalesQuote : ManagerServer.Model.Object, IRecurringTransactionFor, ICustomFields + public sealed class RecurringSalesQuote : ManagerServer.Model.Object, IRecurringTransactionFor, ICustomFields, IHasCustomTheme { [Guide("The date when the next sales quote will be automatically created. This date advances automatically based on the interval and period type after each quote is generated.")] [ProtoMember(1), NoWrap] public DateTime? NextIssueDate { get; set; } @@ -72,5 +72,8 @@ public sealed class RecurringSalesQuote : ManagerServer.Model.Object, IRecurring CustomFields ICustomFields.CustomFields => CustomFields2; MonthDay IRecurringTransaction.MonthDay => MonthDay; + + bool IHasCustomTheme.CustomTheme { get => HasSalesQuoteCustomTheme; set => HasSalesQuoteCustomTheme = value; } + Guid? IHasCustomTheme.CustomThemeId { get => SalesQuoteCustomTheme; set => SalesQuoteCustomTheme = value; } } } From 37142ecfd8eec5ae7cd6f3072dfc7f5534947e69 Mon Sep 17 00:00:00 2001 From: mabaega Date: Wed, 19 Aug 2026 21:52:57 +0700 Subject: [PATCH 2/2] Fix custom theme lost when copying a transaction to a recurring template The reverse direction has the same root cause as the forward-generation fix: "Copy to -> Recurring" (PurchaseInvoiceView etc. -> Recurring*Form) calls Copy(transaction, form), which only matches members by name. The transaction exposes CustomTheme/CustomThemeId while the recurring form exposes its own uniquely-named fields, so the theme was silently dropped. Bridge it explicitly in each Recurring*Form.OnSource(), right after Copy(), using the IHasCustomTheme interface now implemented by the recurring models. Co-Authored-By: Claude Sonnet 5 --- .../RecurringPurchaseInvoiceForm.cs | 8 ++++++++ .../RecurringPurchaseOrders/RecurringPurchaseOrderForm.cs | 8 ++++++++ .../RecurringSalesInvoices/RecurringSalesInvoiceForm.cs | 8 ++++++++ .../RecurringSalesOrders/RecurringSalesOrderForm.cs | 8 ++++++++ .../RecurringSalesQuotes/RecurringSalesQuoteForm.cs | 8 ++++++++ 5 files changed, 40 insertions(+) diff --git a/ManagerServer/HttpHandlers/Businesses/Business/Settings/RecurringTransactions/RecurringPurchaseInvoices/RecurringPurchaseInvoiceForm.cs b/ManagerServer/HttpHandlers/Businesses/Business/Settings/RecurringTransactions/RecurringPurchaseInvoices/RecurringPurchaseInvoiceForm.cs index a2ecdc2..fb58b0d 100644 --- a/ManagerServer/HttpHandlers/Businesses/Business/Settings/RecurringTransactions/RecurringPurchaseInvoices/RecurringPurchaseInvoiceForm.cs +++ b/ManagerServer/HttpHandlers/Businesses/Business/Settings/RecurringTransactions/RecurringPurchaseInvoices/RecurringPurchaseInvoiceForm.cs @@ -17,6 +17,14 @@ protected override void OnSource(RecurringPurchaseInvoice form, ManagerServer.Mo if (source is PurchaseInvoice purchaseInvoice) { Copy(purchaseInvoice, form); + + // Copy() only matches members by name; PurchaseInvoice exposes CustomTheme/CustomThemeId + // while this form exposes its own uniquely-named fields, so bridge via IHasCustomTheme. + if (purchaseInvoice is IHasCustomTheme sourceCustomTheme) + { + form.HasPurchaseInvoiceCustomTheme = sourceCustomTheme.CustomTheme; + form.PurchaseInvoiceCustomTheme = sourceCustomTheme.CustomThemeId; + } } } } diff --git a/ManagerServer/HttpHandlers/Businesses/Business/Settings/RecurringTransactions/RecurringPurchaseOrders/RecurringPurchaseOrderForm.cs b/ManagerServer/HttpHandlers/Businesses/Business/Settings/RecurringTransactions/RecurringPurchaseOrders/RecurringPurchaseOrderForm.cs index 83b10f8..83af490 100644 --- a/ManagerServer/HttpHandlers/Businesses/Business/Settings/RecurringTransactions/RecurringPurchaseOrders/RecurringPurchaseOrderForm.cs +++ b/ManagerServer/HttpHandlers/Businesses/Business/Settings/RecurringTransactions/RecurringPurchaseOrders/RecurringPurchaseOrderForm.cs @@ -16,6 +16,14 @@ protected override void OnSource(ManagerServer.Model.RecurringPurchaseOrder form if (source is ManagerServer.Model.PurchaseOrder purchaseOrder) { Copy(purchaseOrder, form); + + // Copy() only matches members by name; PurchaseOrder exposes CustomTheme/CustomThemeId + // while this form exposes its own uniquely-named fields, so bridge via IHasCustomTheme. + if (purchaseOrder is ManagerServer.Model.IHasCustomTheme sourceCustomTheme) + { + form.HasPurchaseOrderCustomTheme = sourceCustomTheme.CustomTheme; + form.PurchaseOrderCustomTheme = sourceCustomTheme.CustomThemeId; + } } } } diff --git a/ManagerServer/HttpHandlers/Businesses/Business/Settings/RecurringTransactions/RecurringSalesInvoices/RecurringSalesInvoiceForm.cs b/ManagerServer/HttpHandlers/Businesses/Business/Settings/RecurringTransactions/RecurringSalesInvoices/RecurringSalesInvoiceForm.cs index 0ffce4c..f5d3e59 100644 --- a/ManagerServer/HttpHandlers/Businesses/Business/Settings/RecurringTransactions/RecurringSalesInvoices/RecurringSalesInvoiceForm.cs +++ b/ManagerServer/HttpHandlers/Businesses/Business/Settings/RecurringTransactions/RecurringSalesInvoices/RecurringSalesInvoiceForm.cs @@ -17,6 +17,14 @@ protected override void OnSource(RecurringSalesInvoice form, ManagerServer.Model if (source is SalesInvoice salesInvoice) { Copy(salesInvoice, form); + + // Copy() only matches members by name; SalesInvoice exposes CustomTheme/CustomThemeId + // while this form exposes its own uniquely-named fields, so bridge via IHasCustomTheme. + if (salesInvoice is IHasCustomTheme sourceCustomTheme) + { + form.HasSalesInvoiceCustomTheme = sourceCustomTheme.CustomTheme; + form.SalesInvoiceCustomTheme = sourceCustomTheme.CustomThemeId; + } } } } diff --git a/ManagerServer/HttpHandlers/Businesses/Business/Settings/RecurringTransactions/RecurringSalesOrders/RecurringSalesOrderForm.cs b/ManagerServer/HttpHandlers/Businesses/Business/Settings/RecurringTransactions/RecurringSalesOrders/RecurringSalesOrderForm.cs index 4844598..7fd0902 100644 --- a/ManagerServer/HttpHandlers/Businesses/Business/Settings/RecurringTransactions/RecurringSalesOrders/RecurringSalesOrderForm.cs +++ b/ManagerServer/HttpHandlers/Businesses/Business/Settings/RecurringTransactions/RecurringSalesOrders/RecurringSalesOrderForm.cs @@ -16,6 +16,14 @@ protected override void OnSource(ManagerServer.Model.RecurringSalesOrder form, M if (source is ManagerServer.Model.SalesOrder salesOrder) { Copy(salesOrder, form); + + // Copy() only matches members by name; SalesOrder exposes CustomTheme/CustomThemeId + // while this form exposes its own uniquely-named fields, so bridge via IHasCustomTheme. + if (salesOrder is ManagerServer.Model.IHasCustomTheme sourceCustomTheme) + { + form.HasSalesOrderCustomTheme = sourceCustomTheme.CustomTheme; + form.SalesOrderCustomTheme = sourceCustomTheme.CustomThemeId; + } } } } diff --git a/ManagerServer/HttpHandlers/Businesses/Business/Settings/RecurringTransactions/RecurringSalesQuotes/RecurringSalesQuoteForm.cs b/ManagerServer/HttpHandlers/Businesses/Business/Settings/RecurringTransactions/RecurringSalesQuotes/RecurringSalesQuoteForm.cs index 6ead293..66f0636 100644 --- a/ManagerServer/HttpHandlers/Businesses/Business/Settings/RecurringTransactions/RecurringSalesQuotes/RecurringSalesQuoteForm.cs +++ b/ManagerServer/HttpHandlers/Businesses/Business/Settings/RecurringTransactions/RecurringSalesQuotes/RecurringSalesQuoteForm.cs @@ -21,6 +21,14 @@ protected override void OnSource(ManagerServer.Model.RecurringSalesQuote form, M if (source is ManagerServer.Model.SalesQuote salesQuote) { Copy(salesQuote, form); + + // Copy() only matches members by name; SalesQuote exposes CustomTheme/CustomThemeId + // while this form exposes its own uniquely-named fields, so bridge via IHasCustomTheme. + if (salesQuote is ManagerServer.Model.IHasCustomTheme sourceCustomTheme) + { + form.HasSalesQuoteCustomTheme = sourceCustomTheme.CustomTheme; + form.SalesQuoteCustomTheme = sourceCustomTheme.CustomThemeId; + } } } }