diff --git a/QuickBooksDesktop.slnx b/QuickBooksDesktop.slnx
index 9686701..ae06a54 100644
--- a/QuickBooksDesktop.slnx
+++ b/QuickBooksDesktop.slnx
@@ -8,6 +8,7 @@
+
diff --git a/src/Core/QBD.Application/DTOs/RegisterEntryDto.cs b/src/Core/QBD.Application/DTOs/RegisterEntryDto.cs
index e9441be..0927165 100644
--- a/src/Core/QBD.Application/DTOs/RegisterEntryDto.cs
+++ b/src/Core/QBD.Application/DTOs/RegisterEntryDto.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Enums;
namespace QBD.Application.ViewModels;
diff --git a/src/Core/QBD.Application/DTOs/ReportColumnDto.cs b/src/Core/QBD.Application/DTOs/ReportColumnDto.cs
index 736c52b..b4ad81d 100644
--- a/src/Core/QBD.Application/DTOs/ReportColumnDto.cs
+++ b/src/Core/QBD.Application/DTOs/ReportColumnDto.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
namespace QBD.Application.ViewModels;
public class ReportColumnDto
diff --git a/src/Core/QBD.Application/DTOs/ReportRowDto.cs b/src/Core/QBD.Application/DTOs/ReportRowDto.cs
index 7d80bd5..d53585f 100644
--- a/src/Core/QBD.Application/DTOs/ReportRowDto.cs
+++ b/src/Core/QBD.Application/DTOs/ReportRowDto.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
namespace QBD.Application.ViewModels;
public class ReportRowDto
diff --git a/src/Core/QBD.Application/DTOs/TransactionSummaryDto.cs b/src/Core/QBD.Application/DTOs/TransactionSummaryDto.cs
index 2a36e5c..08286ca 100644
--- a/src/Core/QBD.Application/DTOs/TransactionSummaryDto.cs
+++ b/src/Core/QBD.Application/DTOs/TransactionSummaryDto.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Enums;
namespace QBD.Application.ViewModels;
diff --git a/src/Core/QBD.Application/Interfaces/IAuditService.cs b/src/Core/QBD.Application/Interfaces/IAuditService.cs
index b5f58c1..8892212 100644
--- a/src/Core/QBD.Application/Interfaces/IAuditService.cs
+++ b/src/Core/QBD.Application/Interfaces/IAuditService.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
namespace QBD.Application.Interfaces;
public interface IAuditService
diff --git a/src/Core/QBD.Application/Interfaces/IFileDialogService.cs b/src/Core/QBD.Application/Interfaces/IFileDialogService.cs
new file mode 100644
index 0000000..890dcec
--- /dev/null
+++ b/src/Core/QBD.Application/Interfaces/IFileDialogService.cs
@@ -0,0 +1,10 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
+namespace QBD.Application.Interfaces
+{
+ public interface IFileDialogService
+ {
+ string? ShowSaveFileDialog(string fileName, string defaultExt, string filter);
+ }
+}
diff --git a/src/Core/QBD.Application/Interfaces/INavigationService.cs b/src/Core/QBD.Application/Interfaces/INavigationService.cs
index 389ec9a..b3999f7 100644
--- a/src/Core/QBD.Application/Interfaces/INavigationService.cs
+++ b/src/Core/QBD.Application/Interfaces/INavigationService.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
namespace QBD.Application.Interfaces;
public interface INavigationService
diff --git a/src/Core/QBD.Application/Interfaces/INumberSequenceService.cs b/src/Core/QBD.Application/Interfaces/INumberSequenceService.cs
index 169c6a1..3f92028 100644
--- a/src/Core/QBD.Application/Interfaces/INumberSequenceService.cs
+++ b/src/Core/QBD.Application/Interfaces/INumberSequenceService.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
namespace QBD.Application.Interfaces;
public interface INumberSequenceService
diff --git a/src/Core/QBD.Application/Interfaces/IPdfExportService.cs b/src/Core/QBD.Application/Interfaces/IPdfExportService.cs
new file mode 100644
index 0000000..a98dfa1
--- /dev/null
+++ b/src/Core/QBD.Application/Interfaces/IPdfExportService.cs
@@ -0,0 +1,15 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
+using System.Threading.Tasks;
+using QBD.Domain.Entities.Customers;
+
+namespace QBD.Application.Interfaces
+{
+ public interface IPdfExportService
+ {
+ Task ExportInvoiceToPdfAsync(Invoice invoice, string filePath);
+
+ Task ExportReportToPdfAsync(string reportTitle, object reportData, string filePath);
+ }
+}
\ No newline at end of file
diff --git a/src/Core/QBD.Application/Interfaces/IRepository.cs b/src/Core/QBD.Application/Interfaces/IRepository.cs
index 25506a4..281c6c5 100644
--- a/src/Core/QBD.Application/Interfaces/IRepository.cs
+++ b/src/Core/QBD.Application/Interfaces/IRepository.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Linq.Expressions;
using QBD.Domain.Common;
diff --git a/src/Core/QBD.Application/Interfaces/ITransactionPostingService.cs b/src/Core/QBD.Application/Interfaces/ITransactionPostingService.cs
index 21c9140..571b715 100644
--- a/src/Core/QBD.Application/Interfaces/ITransactionPostingService.cs
+++ b/src/Core/QBD.Application/Interfaces/ITransactionPostingService.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Enums;
namespace QBD.Application.Interfaces;
diff --git a/src/Core/QBD.Application/Interfaces/IUnitOfWork.cs b/src/Core/QBD.Application/Interfaces/IUnitOfWork.cs
index 08f5d44..6582487 100644
--- a/src/Core/QBD.Application/Interfaces/IUnitOfWork.cs
+++ b/src/Core/QBD.Application/Interfaces/IUnitOfWork.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
namespace QBD.Application.Interfaces;
public interface IUnitOfWork : IDisposable
diff --git a/src/Core/QBD.Application/ViewModels/CenterViewModelBase.cs b/src/Core/QBD.Application/ViewModels/CenterViewModelBase.cs
index 6be9fe8..8fc529a 100644
--- a/src/Core/QBD.Application/ViewModels/CenterViewModelBase.cs
+++ b/src/Core/QBD.Application/ViewModels/CenterViewModelBase.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
diff --git a/src/Core/QBD.Application/ViewModels/HomePageViewModel.cs b/src/Core/QBD.Application/ViewModels/HomePageViewModel.cs
index c44051f..0421f23 100644
--- a/src/Core/QBD.Application/ViewModels/HomePageViewModel.cs
+++ b/src/Core/QBD.Application/ViewModels/HomePageViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using CommunityToolkit.Mvvm.Input;
using QBD.Application.Interfaces;
diff --git a/src/Core/QBD.Application/ViewModels/ListViewModelBase.cs b/src/Core/QBD.Application/ViewModels/ListViewModelBase.cs
index aa31762..c195924 100644
--- a/src/Core/QBD.Application/ViewModels/ListViewModelBase.cs
+++ b/src/Core/QBD.Application/ViewModels/ListViewModelBase.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
diff --git a/src/Core/QBD.Application/ViewModels/RegisterViewModelBase.cs b/src/Core/QBD.Application/ViewModels/RegisterViewModelBase.cs
index efa2777..b8e6e4b 100644
--- a/src/Core/QBD.Application/ViewModels/RegisterViewModelBase.cs
+++ b/src/Core/QBD.Application/ViewModels/RegisterViewModelBase.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
diff --git a/src/Core/QBD.Application/ViewModels/ReportViewModelBase.cs b/src/Core/QBD.Application/ViewModels/ReportViewModelBase.cs
index 1809b04..b854e20 100644
--- a/src/Core/QBD.Application/ViewModels/ReportViewModelBase.cs
+++ b/src/Core/QBD.Application/ViewModels/ReportViewModelBase.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
diff --git a/src/Core/QBD.Application/ViewModels/TransactionFormViewModelBase.cs b/src/Core/QBD.Application/ViewModels/TransactionFormViewModelBase.cs
index fdb2a29..9c76efe 100644
--- a/src/Core/QBD.Application/ViewModels/TransactionFormViewModelBase.cs
+++ b/src/Core/QBD.Application/ViewModels/TransactionFormViewModelBase.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
diff --git a/src/Core/QBD.Application/ViewModels/ViewModelBase.cs b/src/Core/QBD.Application/ViewModels/ViewModelBase.cs
index 06bb58b..60a2eed 100644
--- a/src/Core/QBD.Application/ViewModels/ViewModelBase.cs
+++ b/src/Core/QBD.Application/ViewModels/ViewModelBase.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
diff --git a/src/Core/QBD.Domain/Common/AuditLogEntry.cs b/src/Core/QBD.Domain/Common/AuditLogEntry.cs
index 7b549e0..eda8cd3 100644
--- a/src/Core/QBD.Domain/Common/AuditLogEntry.cs
+++ b/src/Core/QBD.Domain/Common/AuditLogEntry.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
namespace QBD.Domain.Common;
public class AuditLogEntry
diff --git a/src/Core/QBD.Domain/Common/BaseEntity.cs b/src/Core/QBD.Domain/Common/BaseEntity.cs
index 73ff742..67fa221 100644
--- a/src/Core/QBD.Domain/Common/BaseEntity.cs
+++ b/src/Core/QBD.Domain/Common/BaseEntity.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.ComponentModel.DataAnnotations;
namespace QBD.Domain.Common;
diff --git a/src/Core/QBD.Domain/Common/NumberSequence.cs b/src/Core/QBD.Domain/Common/NumberSequence.cs
index c86c079..4850dec 100644
--- a/src/Core/QBD.Domain/Common/NumberSequence.cs
+++ b/src/Core/QBD.Domain/Common/NumberSequence.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
namespace QBD.Domain.Common;
public class NumberSequence : BaseEntity
diff --git a/src/Core/QBD.Domain/Entities/Accounting/Account.cs b/src/Core/QBD.Domain/Entities/Accounting/Account.cs
index 702517f..c041577 100644
--- a/src/Core/QBD.Domain/Entities/Accounting/Account.cs
+++ b/src/Core/QBD.Domain/Entities/Accounting/Account.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Enums;
diff --git a/src/Core/QBD.Domain/Entities/Accounting/Class.cs b/src/Core/QBD.Domain/Entities/Accounting/Class.cs
index bb7976c..d46bd60 100644
--- a/src/Core/QBD.Domain/Entities/Accounting/Class.cs
+++ b/src/Core/QBD.Domain/Entities/Accounting/Class.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
namespace QBD.Domain.Entities.Accounting;
diff --git a/src/Core/QBD.Domain/Entities/Accounting/FiscalPeriod.cs b/src/Core/QBD.Domain/Entities/Accounting/FiscalPeriod.cs
index a6956a4..736ddba 100644
--- a/src/Core/QBD.Domain/Entities/Accounting/FiscalPeriod.cs
+++ b/src/Core/QBD.Domain/Entities/Accounting/FiscalPeriod.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
namespace QBD.Domain.Entities.Accounting;
diff --git a/src/Core/QBD.Domain/Entities/Accounting/FiscalYear.cs b/src/Core/QBD.Domain/Entities/Accounting/FiscalYear.cs
index 8f432b7..0694623 100644
--- a/src/Core/QBD.Domain/Entities/Accounting/FiscalYear.cs
+++ b/src/Core/QBD.Domain/Entities/Accounting/FiscalYear.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
namespace QBD.Domain.Entities.Accounting;
diff --git a/src/Core/QBD.Domain/Entities/Accounting/GLEntry.cs b/src/Core/QBD.Domain/Entities/Accounting/GLEntry.cs
index ad26031..dfda5bd 100644
--- a/src/Core/QBD.Domain/Entities/Accounting/GLEntry.cs
+++ b/src/Core/QBD.Domain/Entities/Accounting/GLEntry.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Enums;
diff --git a/src/Core/QBD.Domain/Entities/Accounting/JournalEntry.cs b/src/Core/QBD.Domain/Entities/Accounting/JournalEntry.cs
index c230bc3..2a62f95 100644
--- a/src/Core/QBD.Domain/Entities/Accounting/JournalEntry.cs
+++ b/src/Core/QBD.Domain/Entities/Accounting/JournalEntry.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Enums;
diff --git a/src/Core/QBD.Domain/Entities/Accounting/JournalEntryLine.cs b/src/Core/QBD.Domain/Entities/Accounting/JournalEntryLine.cs
index a075686..75304c6 100644
--- a/src/Core/QBD.Domain/Entities/Accounting/JournalEntryLine.cs
+++ b/src/Core/QBD.Domain/Entities/Accounting/JournalEntryLine.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
namespace QBD.Domain.Entities.Accounting;
diff --git a/src/Core/QBD.Domain/Entities/Accounting/Location.cs b/src/Core/QBD.Domain/Entities/Accounting/Location.cs
index f74566b..08eeec7 100644
--- a/src/Core/QBD.Domain/Entities/Accounting/Location.cs
+++ b/src/Core/QBD.Domain/Entities/Accounting/Location.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
namespace QBD.Domain.Entities.Accounting;
diff --git a/src/Core/QBD.Domain/Entities/Accounting/PaymentMethod.cs b/src/Core/QBD.Domain/Entities/Accounting/PaymentMethod.cs
index decfdcd..055cb56 100644
--- a/src/Core/QBD.Domain/Entities/Accounting/PaymentMethod.cs
+++ b/src/Core/QBD.Domain/Entities/Accounting/PaymentMethod.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
namespace QBD.Domain.Entities.Accounting;
diff --git a/src/Core/QBD.Domain/Entities/Accounting/TaxCode.cs b/src/Core/QBD.Domain/Entities/Accounting/TaxCode.cs
index 2afb440..75ba0ad 100644
--- a/src/Core/QBD.Domain/Entities/Accounting/TaxCode.cs
+++ b/src/Core/QBD.Domain/Entities/Accounting/TaxCode.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
namespace QBD.Domain.Entities.Accounting;
diff --git a/src/Core/QBD.Domain/Entities/Accounting/Terms.cs b/src/Core/QBD.Domain/Entities/Accounting/Terms.cs
index 3b5c0c3..505d2dc 100644
--- a/src/Core/QBD.Domain/Entities/Accounting/Terms.cs
+++ b/src/Core/QBD.Domain/Entities/Accounting/Terms.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
namespace QBD.Domain.Entities.Accounting;
diff --git a/src/Core/QBD.Domain/Entities/Banking/Check.cs b/src/Core/QBD.Domain/Entities/Banking/Check.cs
index 72bc7a4..bf5cc0d 100644
--- a/src/Core/QBD.Domain/Entities/Banking/Check.cs
+++ b/src/Core/QBD.Domain/Entities/Banking/Check.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Entities.Accounting;
using QBD.Domain.Enums;
diff --git a/src/Core/QBD.Domain/Entities/Banking/CheckExpenseLine.cs b/src/Core/QBD.Domain/Entities/Banking/CheckExpenseLine.cs
index c43aff9..a7ee55f 100644
--- a/src/Core/QBD.Domain/Entities/Banking/CheckExpenseLine.cs
+++ b/src/Core/QBD.Domain/Entities/Banking/CheckExpenseLine.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Entities.Accounting;
using QBD.Domain.Entities.Customers;
diff --git a/src/Core/QBD.Domain/Entities/Banking/CheckItemLine.cs b/src/Core/QBD.Domain/Entities/Banking/CheckItemLine.cs
index 00d0432..e070fd1 100644
--- a/src/Core/QBD.Domain/Entities/Banking/CheckItemLine.cs
+++ b/src/Core/QBD.Domain/Entities/Banking/CheckItemLine.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Entities.Items;
diff --git a/src/Core/QBD.Domain/Entities/Banking/Deposit.cs b/src/Core/QBD.Domain/Entities/Banking/Deposit.cs
index 175e946..899fee7 100644
--- a/src/Core/QBD.Domain/Entities/Banking/Deposit.cs
+++ b/src/Core/QBD.Domain/Entities/Banking/Deposit.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Entities.Accounting;
using QBD.Domain.Enums;
diff --git a/src/Core/QBD.Domain/Entities/Banking/DepositLine.cs b/src/Core/QBD.Domain/Entities/Banking/DepositLine.cs
index f4b5b07..e4e9ecc 100644
--- a/src/Core/QBD.Domain/Entities/Banking/DepositLine.cs
+++ b/src/Core/QBD.Domain/Entities/Banking/DepositLine.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Entities.Accounting;
diff --git a/src/Core/QBD.Domain/Entities/Banking/Reconciliation.cs b/src/Core/QBD.Domain/Entities/Banking/Reconciliation.cs
index b6fdd7d..26ce143 100644
--- a/src/Core/QBD.Domain/Entities/Banking/Reconciliation.cs
+++ b/src/Core/QBD.Domain/Entities/Banking/Reconciliation.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Entities.Accounting;
diff --git a/src/Core/QBD.Domain/Entities/Banking/ReconciliationLine.cs b/src/Core/QBD.Domain/Entities/Banking/ReconciliationLine.cs
index 9ce09b3..fce3271 100644
--- a/src/Core/QBD.Domain/Entities/Banking/ReconciliationLine.cs
+++ b/src/Core/QBD.Domain/Entities/Banking/ReconciliationLine.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Enums;
diff --git a/src/Core/QBD.Domain/Entities/Banking/Transfer.cs b/src/Core/QBD.Domain/Entities/Banking/Transfer.cs
index 795940f..6876608 100644
--- a/src/Core/QBD.Domain/Entities/Banking/Transfer.cs
+++ b/src/Core/QBD.Domain/Entities/Banking/Transfer.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Entities.Accounting;
using QBD.Domain.Enums;
diff --git a/src/Core/QBD.Domain/Entities/Company/CompanyInfo.cs b/src/Core/QBD.Domain/Entities/Company/CompanyInfo.cs
index 8d50050..7413041 100644
--- a/src/Core/QBD.Domain/Entities/Company/CompanyInfo.cs
+++ b/src/Core/QBD.Domain/Entities/Company/CompanyInfo.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
namespace QBD.Domain.Entities.Company;
diff --git a/src/Core/QBD.Domain/Entities/Company/Preference.cs b/src/Core/QBD.Domain/Entities/Company/Preference.cs
index 45ec70e..6bcade0 100644
--- a/src/Core/QBD.Domain/Entities/Company/Preference.cs
+++ b/src/Core/QBD.Domain/Entities/Company/Preference.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
namespace QBD.Domain.Entities.Company;
diff --git a/src/Core/QBD.Domain/Entities/Company/User.cs b/src/Core/QBD.Domain/Entities/Company/User.cs
index 7ad76c7..2b64528 100644
--- a/src/Core/QBD.Domain/Entities/Company/User.cs
+++ b/src/Core/QBD.Domain/Entities/Company/User.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
namespace QBD.Domain.Entities.Company;
diff --git a/src/Core/QBD.Domain/Entities/Customers/CreditMemo.cs b/src/Core/QBD.Domain/Entities/Customers/CreditMemo.cs
index 510b4e2..e047049 100644
--- a/src/Core/QBD.Domain/Entities/Customers/CreditMemo.cs
+++ b/src/Core/QBD.Domain/Entities/Customers/CreditMemo.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Enums;
diff --git a/src/Core/QBD.Domain/Entities/Customers/CreditMemoLine.cs b/src/Core/QBD.Domain/Entities/Customers/CreditMemoLine.cs
index 3fc1e0a..8951c7c 100644
--- a/src/Core/QBD.Domain/Entities/Customers/CreditMemoLine.cs
+++ b/src/Core/QBD.Domain/Entities/Customers/CreditMemoLine.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Entities.Accounting;
using QBD.Domain.Entities.Items;
diff --git a/src/Core/QBD.Domain/Entities/Customers/Customer.cs b/src/Core/QBD.Domain/Entities/Customers/Customer.cs
index bea1819..94e0e12 100644
--- a/src/Core/QBD.Domain/Entities/Customers/Customer.cs
+++ b/src/Core/QBD.Domain/Entities/Customers/Customer.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Entities.Accounting;
diff --git a/src/Core/QBD.Domain/Entities/Customers/Estimate.cs b/src/Core/QBD.Domain/Entities/Customers/Estimate.cs
index 624fa96..b931097 100644
--- a/src/Core/QBD.Domain/Entities/Customers/Estimate.cs
+++ b/src/Core/QBD.Domain/Entities/Customers/Estimate.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Enums;
diff --git a/src/Core/QBD.Domain/Entities/Customers/EstimateLine.cs b/src/Core/QBD.Domain/Entities/Customers/EstimateLine.cs
index e7b1979..368f7fe 100644
--- a/src/Core/QBD.Domain/Entities/Customers/EstimateLine.cs
+++ b/src/Core/QBD.Domain/Entities/Customers/EstimateLine.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Entities.Accounting;
using QBD.Domain.Entities.Items;
diff --git a/src/Core/QBD.Domain/Entities/Customers/Invoice.cs b/src/Core/QBD.Domain/Entities/Customers/Invoice.cs
index b101dda..47ffb16 100644
--- a/src/Core/QBD.Domain/Entities/Customers/Invoice.cs
+++ b/src/Core/QBD.Domain/Entities/Customers/Invoice.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Entities.Accounting;
using QBD.Domain.Enums;
diff --git a/src/Core/QBD.Domain/Entities/Customers/InvoiceLine.cs b/src/Core/QBD.Domain/Entities/Customers/InvoiceLine.cs
index 2724d85..2e9b6fa 100644
--- a/src/Core/QBD.Domain/Entities/Customers/InvoiceLine.cs
+++ b/src/Core/QBD.Domain/Entities/Customers/InvoiceLine.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Entities.Accounting;
using QBD.Domain.Entities.Items;
diff --git a/src/Core/QBD.Domain/Entities/Customers/Job.cs b/src/Core/QBD.Domain/Entities/Customers/Job.cs
index 28cf311..8563304 100644
--- a/src/Core/QBD.Domain/Entities/Customers/Job.cs
+++ b/src/Core/QBD.Domain/Entities/Customers/Job.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
namespace QBD.Domain.Entities.Customers;
diff --git a/src/Core/QBD.Domain/Entities/Customers/PaymentApplication.cs b/src/Core/QBD.Domain/Entities/Customers/PaymentApplication.cs
index 85094fa..7882615 100644
--- a/src/Core/QBD.Domain/Entities/Customers/PaymentApplication.cs
+++ b/src/Core/QBD.Domain/Entities/Customers/PaymentApplication.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
namespace QBD.Domain.Entities.Customers;
diff --git a/src/Core/QBD.Domain/Entities/Customers/ReceivePayment.cs b/src/Core/QBD.Domain/Entities/Customers/ReceivePayment.cs
index 447ec1e..e8f02ac 100644
--- a/src/Core/QBD.Domain/Entities/Customers/ReceivePayment.cs
+++ b/src/Core/QBD.Domain/Entities/Customers/ReceivePayment.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Entities.Accounting;
using QBD.Domain.Enums;
diff --git a/src/Core/QBD.Domain/Entities/Customers/SalesReceipt.cs b/src/Core/QBD.Domain/Entities/Customers/SalesReceipt.cs
index 36c82c1..725317d 100644
--- a/src/Core/QBD.Domain/Entities/Customers/SalesReceipt.cs
+++ b/src/Core/QBD.Domain/Entities/Customers/SalesReceipt.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Entities.Accounting;
using QBD.Domain.Enums;
diff --git a/src/Core/QBD.Domain/Entities/Customers/SalesReceiptLine.cs b/src/Core/QBD.Domain/Entities/Customers/SalesReceiptLine.cs
index 3fb1e20..db52a80 100644
--- a/src/Core/QBD.Domain/Entities/Customers/SalesReceiptLine.cs
+++ b/src/Core/QBD.Domain/Entities/Customers/SalesReceiptLine.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Entities.Accounting;
using QBD.Domain.Entities.Items;
diff --git a/src/Core/QBD.Domain/Entities/Items/Item.cs b/src/Core/QBD.Domain/Entities/Items/Item.cs
index 1dbf460..2aa710a 100644
--- a/src/Core/QBD.Domain/Entities/Items/Item.cs
+++ b/src/Core/QBD.Domain/Entities/Items/Item.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Entities.Accounting;
using QBD.Domain.Enums;
diff --git a/src/Core/QBD.Domain/Entities/Vendors/Bill.cs b/src/Core/QBD.Domain/Entities/Vendors/Bill.cs
index 0c637a2..8be5328 100644
--- a/src/Core/QBD.Domain/Entities/Vendors/Bill.cs
+++ b/src/Core/QBD.Domain/Entities/Vendors/Bill.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Entities.Accounting;
using QBD.Domain.Enums;
diff --git a/src/Core/QBD.Domain/Entities/Vendors/BillExpenseLine.cs b/src/Core/QBD.Domain/Entities/Vendors/BillExpenseLine.cs
index 10c370b..55c60f6 100644
--- a/src/Core/QBD.Domain/Entities/Vendors/BillExpenseLine.cs
+++ b/src/Core/QBD.Domain/Entities/Vendors/BillExpenseLine.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Entities.Accounting;
using QBD.Domain.Entities.Customers;
diff --git a/src/Core/QBD.Domain/Entities/Vendors/BillItemLine.cs b/src/Core/QBD.Domain/Entities/Vendors/BillItemLine.cs
index 8e16d5a..e6d66e2 100644
--- a/src/Core/QBD.Domain/Entities/Vendors/BillItemLine.cs
+++ b/src/Core/QBD.Domain/Entities/Vendors/BillItemLine.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Entities.Accounting;
using QBD.Domain.Entities.Customers;
diff --git a/src/Core/QBD.Domain/Entities/Vendors/BillPayment.cs b/src/Core/QBD.Domain/Entities/Vendors/BillPayment.cs
index 1181ac5..e81c1d1 100644
--- a/src/Core/QBD.Domain/Entities/Vendors/BillPayment.cs
+++ b/src/Core/QBD.Domain/Entities/Vendors/BillPayment.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Entities.Accounting;
using QBD.Domain.Enums;
diff --git a/src/Core/QBD.Domain/Entities/Vendors/BillPaymentApplication.cs b/src/Core/QBD.Domain/Entities/Vendors/BillPaymentApplication.cs
index 5e3c381..08cf797 100644
--- a/src/Core/QBD.Domain/Entities/Vendors/BillPaymentApplication.cs
+++ b/src/Core/QBD.Domain/Entities/Vendors/BillPaymentApplication.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Entities.Accounting;
diff --git a/src/Core/QBD.Domain/Entities/Vendors/PurchaseOrder.cs b/src/Core/QBD.Domain/Entities/Vendors/PurchaseOrder.cs
index bed7979..02127d4 100644
--- a/src/Core/QBD.Domain/Entities/Vendors/PurchaseOrder.cs
+++ b/src/Core/QBD.Domain/Entities/Vendors/PurchaseOrder.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Enums;
diff --git a/src/Core/QBD.Domain/Entities/Vendors/PurchaseOrderLine.cs b/src/Core/QBD.Domain/Entities/Vendors/PurchaseOrderLine.cs
index 2394276..ff511b3 100644
--- a/src/Core/QBD.Domain/Entities/Vendors/PurchaseOrderLine.cs
+++ b/src/Core/QBD.Domain/Entities/Vendors/PurchaseOrderLine.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Entities.Items;
diff --git a/src/Core/QBD.Domain/Entities/Vendors/Vendor.cs b/src/Core/QBD.Domain/Entities/Vendors/Vendor.cs
index dc94334..d1c4146 100644
--- a/src/Core/QBD.Domain/Entities/Vendors/Vendor.cs
+++ b/src/Core/QBD.Domain/Entities/Vendors/Vendor.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Entities.Accounting;
diff --git a/src/Core/QBD.Domain/Entities/Vendors/VendorCredit.cs b/src/Core/QBD.Domain/Entities/Vendors/VendorCredit.cs
index 08818a7..a84f018 100644
--- a/src/Core/QBD.Domain/Entities/Vendors/VendorCredit.cs
+++ b/src/Core/QBD.Domain/Entities/Vendors/VendorCredit.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Enums;
diff --git a/src/Core/QBD.Domain/Entities/Vendors/VendorCreditLine.cs b/src/Core/QBD.Domain/Entities/Vendors/VendorCreditLine.cs
index a9b8236..58c3955 100644
--- a/src/Core/QBD.Domain/Entities/Vendors/VendorCreditLine.cs
+++ b/src/Core/QBD.Domain/Entities/Vendors/VendorCreditLine.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Domain.Common;
using QBD.Domain.Entities.Accounting;
using QBD.Domain.Entities.Items;
diff --git a/src/Core/QBD.Domain/Enums/AccountType.cs b/src/Core/QBD.Domain/Enums/AccountType.cs
index f8bbae1..3d3032c 100644
--- a/src/Core/QBD.Domain/Enums/AccountType.cs
+++ b/src/Core/QBD.Domain/Enums/AccountType.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
namespace QBD.Domain.Enums;
public enum AccountType
diff --git a/src/Core/QBD.Domain/Enums/DocStatus.cs b/src/Core/QBD.Domain/Enums/DocStatus.cs
index c3b500f..2dc6d2d 100644
--- a/src/Core/QBD.Domain/Enums/DocStatus.cs
+++ b/src/Core/QBD.Domain/Enums/DocStatus.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
namespace QBD.Domain.Enums;
public enum DocStatus
diff --git a/src/Core/QBD.Domain/Enums/ItemType.cs b/src/Core/QBD.Domain/Enums/ItemType.cs
index 7057692..05356d8 100644
--- a/src/Core/QBD.Domain/Enums/ItemType.cs
+++ b/src/Core/QBD.Domain/Enums/ItemType.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
namespace QBD.Domain.Enums;
public enum ItemType
diff --git a/src/Core/QBD.Domain/Enums/TransactionType.cs b/src/Core/QBD.Domain/Enums/TransactionType.cs
index af64a08..196fa32 100644
--- a/src/Core/QBD.Domain/Enums/TransactionType.cs
+++ b/src/Core/QBD.Domain/Enums/TransactionType.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
namespace QBD.Domain.Enums;
public enum TransactionType
diff --git a/src/Infrastructure/QBD.Infrastructure/Data/DatabaseSeeder.cs b/src/Infrastructure/QBD.Infrastructure/Data/DatabaseSeeder.cs
index b4d6d8e..471dc6d 100644
--- a/src/Infrastructure/QBD.Infrastructure/Data/DatabaseSeeder.cs
+++ b/src/Infrastructure/QBD.Infrastructure/Data/DatabaseSeeder.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using Microsoft.EntityFrameworkCore;
using QBD.Domain.Common;
using QBD.Domain.Entities.Accounting;
diff --git a/src/Infrastructure/QBD.Infrastructure/Data/QBDesktopDbContext.cs b/src/Infrastructure/QBD.Infrastructure/Data/QBDesktopDbContext.cs
index ad9b67c..0092624 100644
--- a/src/Infrastructure/QBD.Infrastructure/Data/QBDesktopDbContext.cs
+++ b/src/Infrastructure/QBD.Infrastructure/Data/QBDesktopDbContext.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using Microsoft.EntityFrameworkCore;
using QBD.Domain.Common;
using QBD.Domain.Entities.Accounting;
diff --git a/src/Infrastructure/QBD.Infrastructure/QBD.Infrastructure.csproj b/src/Infrastructure/QBD.Infrastructure/QBD.Infrastructure.csproj
index 8bd0377..49011dd 100644
--- a/src/Infrastructure/QBD.Infrastructure/QBD.Infrastructure.csproj
+++ b/src/Infrastructure/QBD.Infrastructure/QBD.Infrastructure.csproj
@@ -13,6 +13,7 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/src/Infrastructure/QBD.Infrastructure/Repositories/Repository.cs b/src/Infrastructure/QBD.Infrastructure/Repositories/Repository.cs
index 4947c94..83f5a44 100644
--- a/src/Infrastructure/QBD.Infrastructure/Repositories/Repository.cs
+++ b/src/Infrastructure/QBD.Infrastructure/Repositories/Repository.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using QBD.Application.Interfaces;
diff --git a/src/Infrastructure/QBD.Infrastructure/Repositories/UnitOfWork.cs b/src/Infrastructure/QBD.Infrastructure/Repositories/UnitOfWork.cs
index 4b27aa7..6e9348a 100644
--- a/src/Infrastructure/QBD.Infrastructure/Repositories/UnitOfWork.cs
+++ b/src/Infrastructure/QBD.Infrastructure/Repositories/UnitOfWork.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using Microsoft.EntityFrameworkCore.Storage;
using QBD.Application.Interfaces;
using QBD.Infrastructure.Data;
diff --git a/src/Infrastructure/QBD.Infrastructure/Services/AuditService.cs b/src/Infrastructure/QBD.Infrastructure/Services/AuditService.cs
index 9b663ab..284bf53 100644
--- a/src/Infrastructure/QBD.Infrastructure/Services/AuditService.cs
+++ b/src/Infrastructure/QBD.Infrastructure/Services/AuditService.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Application.Interfaces;
using QBD.Domain.Common;
using QBD.Infrastructure.Data;
diff --git a/src/Infrastructure/QBD.Infrastructure/Services/NumberSequenceService.cs b/src/Infrastructure/QBD.Infrastructure/Services/NumberSequenceService.cs
index 7646c79..9146c55 100644
--- a/src/Infrastructure/QBD.Infrastructure/Services/NumberSequenceService.cs
+++ b/src/Infrastructure/QBD.Infrastructure/Services/NumberSequenceService.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using Microsoft.EntityFrameworkCore;
using QBD.Application.Interfaces;
using QBD.Domain.Common;
diff --git a/src/Infrastructure/QBD.Infrastructure/Services/PdfExportService.cs b/src/Infrastructure/QBD.Infrastructure/Services/PdfExportService.cs
new file mode 100644
index 0000000..a05c499
--- /dev/null
+++ b/src/Infrastructure/QBD.Infrastructure/Services/PdfExportService.cs
@@ -0,0 +1,137 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using QBD.Application.Interfaces;
+using QBD.Domain.Entities.Customers;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QBD.Infrastructure.Services
+{
+ public class PdfExportService : IPdfExportService
+ {
+ static PdfExportService()
+ {
+ QuestPDF.Settings.License = LicenseType.Community;
+ }
+
+ public async Task ExportInvoiceToPdfAsync(Invoice invoice, string filePath)
+ {
+ ArgumentNullException.ThrowIfNull(invoice);
+
+ await Task.Run(() =>
+ {
+ Document.Create(container =>
+ {
+ container.Page(page =>
+ {
+ page.Size(PageSizes.A4);
+ page.Margin(2, Unit.Centimetre);
+ page.PageColor(Colors.White);
+ page.DefaultTextStyle(x => x.FontSize(11).FontFamily("Arial"));
+
+ page.Header().Row(row =>
+ {
+ row.RelativeItem().Column(column =>
+ {
+ column.Item().Text("INVOICE").FontSize(28).SemiBold().FontColor(Colors.Blue.Darken2);
+ column.Item().Text($"Date: {invoice.Date:d}").FontSize(12);
+ column.Item().Text($"Invoice #: {invoice.InvoiceNumber ?? "N/A"}").FontSize(12);
+ });
+
+ row.ConstantItem(100).Height(50).Placeholder();
+ });
+
+ page.Content().PaddingVertical(1, Unit.Centimetre).Column(column =>
+ {
+ column.Spacing(5);
+ column.Item().Text("Billed To:").SemiBold();
+
+ var customerName = invoice.Customer?.CustomerName ?? "No Customer Selected";
+ var address = invoice.BillToAddress ?? "No Address Provided";
+ column.Item().Text($"{customerName}\n{address}");
+
+ column.Item().PaddingTop(25).Table(table =>
+ {
+ table.ColumnsDefinition(columns =>
+ {
+ columns.ConstantColumn(25); // #
+ columns.RelativeColumn(3); // Item
+ columns.RelativeColumn(); // Qty
+ columns.RelativeColumn(); // Rate
+ columns.RelativeColumn(); // Amount
+ });
+
+ table.Header(header =>
+ {
+ header.Cell().BorderBottom(1).Text("#").SemiBold();
+ header.Cell().BorderBottom(1).Text("Item").SemiBold();
+ header.Cell().BorderBottom(1).AlignRight().Text("Qty").SemiBold();
+ header.Cell().BorderBottom(1).AlignRight().Text("Rate").SemiBold();
+ header.Cell().BorderBottom(1).AlignRight().Text("Amount").SemiBold();
+ });
+
+ int index = 1;
+ foreach (var line in invoice.Lines ?? new List())
+ {
+ table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2).PaddingVertical(5).Text(index++.ToString());
+ table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2).PaddingVertical(5).Text(line.Item?.ItemName ?? line.Description ?? "Item");
+
+ table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2).PaddingVertical(5).AlignRight().Text(line.Qty.ToString());
+
+ table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2).PaddingVertical(5).AlignRight().Text($"{line.Rate:C}");
+ table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2).PaddingVertical(5).AlignRight().Text($"{line.Amount:C}");
+ }
+ });
+
+ column.Item().PaddingTop(10).AlignRight().Text($"Total: {invoice.Total:C}").FontSize(16).SemiBold();
+ });
+
+ page.Footer().AlignCenter().Text(x =>
+ {
+ x.Span("Page ");
+ x.CurrentPageNumber();
+ x.Span(" of ");
+ x.TotalPages();
+ });
+ });
+ })
+ .GeneratePdf(filePath);
+ });
+ }
+
+ public async Task ExportReportToPdfAsync(string reportTitle, object reportData, string filePath)
+ {
+ await Task.Run(() =>
+ {
+ Document.Create(container =>
+ {
+ container.Page(page =>
+ {
+ page.Size(PageSizes.A4.Landscape());
+ page.Margin(1.5f, Unit.Centimetre);
+ page.PageColor(Colors.White);
+
+ page.Header().Text(reportTitle).SemiBold().FontSize(20).FontColor(Colors.Blue.Darken2);
+ page.Content().PaddingVertical(1, Unit.Centimetre).Text("Report data export is currently in development...");
+
+ page.Footer().AlignCenter().Text(x =>
+ {
+ x.Span("Page ");
+ x.CurrentPageNumber();
+ x.Span(" / ");
+ x.TotalPages();
+ });
+ });
+ })
+ .GeneratePdf(filePath);
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Infrastructure/QBD.Infrastructure/Services/TransactionPostingService.cs b/src/Infrastructure/QBD.Infrastructure/Services/TransactionPostingService.cs
index f321267..4d9f2b0 100644
--- a/src/Infrastructure/QBD.Infrastructure/Services/TransactionPostingService.cs
+++ b/src/Infrastructure/QBD.Infrastructure/Services/TransactionPostingService.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using Microsoft.EntityFrameworkCore;
using QBD.Application.Interfaces;
using QBD.Domain.Entities.Accounting;
diff --git a/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/BankRegisterViewModel.cs b/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/BankRegisterViewModel.cs
index 4d3925c..cf4f21b 100644
--- a/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/BankRegisterViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/BankRegisterViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.EntityFrameworkCore;
diff --git a/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/MakeDepositsFormViewModel.cs b/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/MakeDepositsFormViewModel.cs
index 8e64c60..7974715 100644
--- a/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/MakeDepositsFormViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/MakeDepositsFormViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.EntityFrameworkCore;
diff --git a/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/ReconcileViewModel.cs b/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/ReconcileViewModel.cs
index 047d38c..0d9ac22 100644
--- a/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/ReconcileViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/ReconcileViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
diff --git a/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/TransferFundsFormViewModel.cs b/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/TransferFundsFormViewModel.cs
index 6a4644b..1d43963 100644
--- a/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/TransferFundsFormViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/TransferFundsFormViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
diff --git a/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/WriteChecksFormViewModel.cs b/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/WriteChecksFormViewModel.cs
index f812b16..a75f543 100644
--- a/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/WriteChecksFormViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/WriteChecksFormViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.EntityFrameworkCore;
diff --git a/src/Presentation/Modules/QBD.Modules.Company/ViewModels/ChartOfAccountsViewModel.cs b/src/Presentation/Modules/QBD.Modules.Company/ViewModels/ChartOfAccountsViewModel.cs
index cc69cbb..b0407ff 100644
--- a/src/Presentation/Modules/QBD.Modules.Company/ViewModels/ChartOfAccountsViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Company/ViewModels/ChartOfAccountsViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using CommunityToolkit.Mvvm.Input;
using Microsoft.EntityFrameworkCore;
using QBD.Application.Interfaces;
diff --git a/src/Presentation/Modules/QBD.Modules.Company/ViewModels/ClassListViewModel.cs b/src/Presentation/Modules/QBD.Modules.Company/ViewModels/ClassListViewModel.cs
index 285ede9..92bae99 100644
--- a/src/Presentation/Modules/QBD.Modules.Company/ViewModels/ClassListViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Company/ViewModels/ClassListViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using Microsoft.EntityFrameworkCore;
using QBD.Application.Interfaces;
using QBD.Application.ViewModels;
diff --git a/src/Presentation/Modules/QBD.Modules.Company/ViewModels/CompanyInfoFormViewModel.cs b/src/Presentation/Modules/QBD.Modules.Company/ViewModels/CompanyInfoFormViewModel.cs
index 7d85bec..f8f597b 100644
--- a/src/Presentation/Modules/QBD.Modules.Company/ViewModels/CompanyInfoFormViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Company/ViewModels/CompanyInfoFormViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using QBD.Application.Interfaces;
diff --git a/src/Presentation/Modules/QBD.Modules.Company/ViewModels/ItemListViewModel.cs b/src/Presentation/Modules/QBD.Modules.Company/ViewModels/ItemListViewModel.cs
index 05b4900..93978af 100644
--- a/src/Presentation/Modules/QBD.Modules.Company/ViewModels/ItemListViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Company/ViewModels/ItemListViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using Microsoft.EntityFrameworkCore;
using QBD.Application.Interfaces;
using QBD.Application.ViewModels;
diff --git a/src/Presentation/Modules/QBD.Modules.Company/ViewModels/PaymentMethodListViewModel.cs b/src/Presentation/Modules/QBD.Modules.Company/ViewModels/PaymentMethodListViewModel.cs
index d55ead7..61fcd8b 100644
--- a/src/Presentation/Modules/QBD.Modules.Company/ViewModels/PaymentMethodListViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Company/ViewModels/PaymentMethodListViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using Microsoft.EntityFrameworkCore;
using QBD.Application.Interfaces;
using QBD.Application.ViewModels;
diff --git a/src/Presentation/Modules/QBD.Modules.Company/ViewModels/PreferencesFormViewModel.cs b/src/Presentation/Modules/QBD.Modules.Company/ViewModels/PreferencesFormViewModel.cs
index 8a17e04..7843d7e 100644
--- a/src/Presentation/Modules/QBD.Modules.Company/ViewModels/PreferencesFormViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Company/ViewModels/PreferencesFormViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
diff --git a/src/Presentation/Modules/QBD.Modules.Company/ViewModels/TermsListViewModel.cs b/src/Presentation/Modules/QBD.Modules.Company/ViewModels/TermsListViewModel.cs
index f6df260..e67dc33 100644
--- a/src/Presentation/Modules/QBD.Modules.Company/ViewModels/TermsListViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Company/ViewModels/TermsListViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using Microsoft.EntityFrameworkCore;
using QBD.Application.Interfaces;
using QBD.Application.ViewModels;
diff --git a/src/Presentation/Modules/QBD.Modules.Customers/QBD.Modules.Customers.csproj b/src/Presentation/Modules/QBD.Modules.Customers/QBD.Modules.Customers.csproj
index c5be2c4..3fe1447 100644
--- a/src/Presentation/Modules/QBD.Modules.Customers/QBD.Modules.Customers.csproj
+++ b/src/Presentation/Modules/QBD.Modules.Customers/QBD.Modules.Customers.csproj
@@ -16,4 +16,4 @@
-
+
\ No newline at end of file
diff --git a/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/CreditMemoFormViewModel.cs b/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/CreditMemoFormViewModel.cs
index ac06954..43ab50b 100644
--- a/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/CreditMemoFormViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/CreditMemoFormViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.EntityFrameworkCore;
diff --git a/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/CustomerCenterViewModel.cs b/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/CustomerCenterViewModel.cs
index 34afe94..e373f32 100644
--- a/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/CustomerCenterViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/CustomerCenterViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.EntityFrameworkCore;
diff --git a/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/EstimateFormViewModel.cs b/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/EstimateFormViewModel.cs
index 24a0a60..da24c0d 100644
--- a/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/EstimateFormViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/EstimateFormViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.EntityFrameworkCore;
diff --git a/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/InvoiceFormViewModel.cs b/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/InvoiceFormViewModel.cs
index a98c92f..e06980e 100644
--- a/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/InvoiceFormViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/InvoiceFormViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.EntityFrameworkCore;
@@ -7,6 +10,7 @@
using QBD.Domain.Entities.Customers;
using QBD.Domain.Entities.Items;
using QBD.Domain.Enums;
+using CommunityToolkit.Mvvm.Input;
namespace QBD.Modules.Customers.ViewModels;
@@ -17,6 +21,8 @@ public partial class InvoiceFormViewModel : TransactionFormViewModelBase _itemRepository;
private readonly IRepository _termsRepository;
private readonly INumberSequenceService _numberSequenceService;
+ private readonly IPdfExportService _pdfExportService;
+ private readonly IFileDialogService _fileDialogService;
[ObservableProperty] private ObservableCollection _customers = new();
[ObservableProperty] private ObservableCollection- _items = new();
@@ -32,13 +38,17 @@ public InvoiceFormViewModel(
IRepository customerRepository,
IRepository
- itemRepository,
IRepository termsRepository,
- INumberSequenceService numberSequenceService) : base(unitOfWork, postingService, navigationService)
+ INumberSequenceService numberSequenceService,
+ IPdfExportService pdfExportService,
+ IFileDialogService fileDialogService) : base(unitOfWork, postingService, navigationService)
{
_invoiceRepository = invoiceRepository;
_customerRepository = customerRepository;
_itemRepository = itemRepository;
_termsRepository = termsRepository;
_numberSequenceService = numberSequenceService;
+ _pdfExportService = pdfExportService;
+ _fileDialogService = fileDialogService;
Title = "Create Invoice";
}
@@ -62,6 +72,7 @@ partial void OnSelectedCustomerChanged(Customer? value)
{
if (value != null)
{
+ Header.Customer = value;
Header.CustomerId = value.Id;
Header.BillToAddress = value.BillToAddress;
if (value.TermsId.HasValue)
@@ -140,4 +151,35 @@ protected override async Task VoidAsync()
}
catch (Exception ex) { SetError(ex.Message); }
}
-}
+
+ [RelayCommand]
+ private async Task ExportToPdfAsync()
+ {
+ if (Header == null) return;
+
+ var filePath = _fileDialogService.ShowSaveFileDialog(
+ $"Invoice_{Header.InvoiceNumber ?? "New"}.pdf",
+ ".pdf",
+ "PDF Documents (.pdf)|*.pdf");
+
+ if (filePath != null)
+ {
+ IsBusy = true;
+ try
+ {
+ Header.Lines = Lines.Where(l => l.Amount != 0).ToList();
+ RecalculateTotals();
+ await _pdfExportService.ExportInvoiceToPdfAsync(Header, filePath);
+ SetStatus($"Invoice exported to {filePath}");
+ }
+ catch (Exception ex)
+ {
+ SetError($"Export failed: {ex.Message}");
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/ReceivePaymentFormViewModel.cs b/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/ReceivePaymentFormViewModel.cs
index 9c99889..8f6211c 100644
--- a/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/ReceivePaymentFormViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/ReceivePaymentFormViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.EntityFrameworkCore;
diff --git a/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/SalesReceiptFormViewModel.cs b/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/SalesReceiptFormViewModel.cs
index c817378..6f1e4d1 100644
--- a/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/SalesReceiptFormViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/SalesReceiptFormViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.EntityFrameworkCore;
diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/APAgingDetailReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/APAgingDetailReportViewModel.cs
index be413fc..57ed0c9 100644
--- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/APAgingDetailReportViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/APAgingDetailReportViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using Microsoft.EntityFrameworkCore;
using QBD.Application.Interfaces;
diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/APAgingSummaryReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/APAgingSummaryReportViewModel.cs
index 40cd9ca..d24547a 100644
--- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/APAgingSummaryReportViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/APAgingSummaryReportViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using Microsoft.EntityFrameworkCore;
using QBD.Application.Interfaces;
diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ARAgingDetailReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ARAgingDetailReportViewModel.cs
index 5d35f15..711a27b 100644
--- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ARAgingDetailReportViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ARAgingDetailReportViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using Microsoft.EntityFrameworkCore;
using QBD.Application.Interfaces;
diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ARAgingSummaryReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ARAgingSummaryReportViewModel.cs
index c36d1fd..cbc2689 100644
--- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ARAgingSummaryReportViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ARAgingSummaryReportViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using Microsoft.EntityFrameworkCore;
using QBD.Application.Interfaces;
diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/BalanceSheetReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/BalanceSheetReportViewModel.cs
index 77028f7..c2a200b 100644
--- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/BalanceSheetReportViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/BalanceSheetReportViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using Microsoft.EntityFrameworkCore;
using QBD.Application.Interfaces;
diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/CashFlowsReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/CashFlowsReportViewModel.cs
index 3e11c4d..05d413b 100644
--- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/CashFlowsReportViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/CashFlowsReportViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using Microsoft.EntityFrameworkCore;
using QBD.Application.Interfaces;
diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/CustomerBalanceReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/CustomerBalanceReportViewModel.cs
index 5ff7424..8638686 100644
--- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/CustomerBalanceReportViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/CustomerBalanceReportViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using Microsoft.EntityFrameworkCore;
using QBD.Application.Interfaces;
diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/DepositDetailReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/DepositDetailReportViewModel.cs
index c7a80a6..bc1497a 100644
--- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/DepositDetailReportViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/DepositDetailReportViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using Microsoft.EntityFrameworkCore;
using QBD.Application.Interfaces;
diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/GeneralLedgerReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/GeneralLedgerReportViewModel.cs
index 052518a..32400a9 100644
--- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/GeneralLedgerReportViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/GeneralLedgerReportViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using Microsoft.EntityFrameworkCore;
using QBD.Application.Interfaces;
diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/OpenInvoicesReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/OpenInvoicesReportViewModel.cs
index 226d0b3..22195f7 100644
--- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/OpenInvoicesReportViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/OpenInvoicesReportViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using Microsoft.EntityFrameworkCore;
using QBD.Application.Interfaces;
diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ProfitLossReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ProfitLossReportViewModel.cs
index 09a52e7..65d4aa0 100644
--- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ProfitLossReportViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ProfitLossReportViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using Microsoft.EntityFrameworkCore;
using QBD.Application.Interfaces;
diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ReportCenterViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ReportCenterViewModel.cs
index e441682..ae0a076 100644
--- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ReportCenterViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ReportCenterViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Application.Interfaces;
using QBD.Application.ViewModels;
using CommunityToolkit.Mvvm.Input;
diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/TransactionListReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/TransactionListReportViewModel.cs
index 4bb481d..3576854 100644
--- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/TransactionListReportViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/TransactionListReportViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using Microsoft.EntityFrameworkCore;
using QBD.Application.Interfaces;
diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/TrialBalanceReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/TrialBalanceReportViewModel.cs
index 31a6846..d08aa0f 100644
--- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/TrialBalanceReportViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/TrialBalanceReportViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using Microsoft.EntityFrameworkCore;
using QBD.Application.Interfaces;
diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/UnpaidBillsReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/UnpaidBillsReportViewModel.cs
index ecd9d2f..6a2195b 100644
--- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/UnpaidBillsReportViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/UnpaidBillsReportViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using Microsoft.EntityFrameworkCore;
using QBD.Application.Interfaces;
diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/VendorBalanceReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/VendorBalanceReportViewModel.cs
index f354fde..16cb715 100644
--- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/VendorBalanceReportViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/VendorBalanceReportViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using Microsoft.EntityFrameworkCore;
using QBD.Application.Interfaces;
diff --git a/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/BillFormViewModel.cs b/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/BillFormViewModel.cs
index 6c13c1f..b56b7d8 100644
--- a/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/BillFormViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/BillFormViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.EntityFrameworkCore;
diff --git a/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/PayBillsFormViewModel.cs b/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/PayBillsFormViewModel.cs
index 5eaf1f3..444f891 100644
--- a/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/PayBillsFormViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/PayBillsFormViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.EntityFrameworkCore;
diff --git a/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/PurchaseOrderFormViewModel.cs b/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/PurchaseOrderFormViewModel.cs
index cf3ede2..911f870 100644
--- a/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/PurchaseOrderFormViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/PurchaseOrderFormViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.EntityFrameworkCore;
diff --git a/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/VendorCenterViewModel.cs b/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/VendorCenterViewModel.cs
index e8b7ffb..5125cd0 100644
--- a/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/VendorCenterViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/VendorCenterViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.EntityFrameworkCore;
diff --git a/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/VendorCreditFormViewModel.cs b/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/VendorCreditFormViewModel.cs
index b132771..8223f12 100644
--- a/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/VendorCreditFormViewModel.cs
+++ b/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/VendorCreditFormViewModel.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.EntityFrameworkCore;
diff --git a/src/Presentation/QBD.API/Controllers/AccountsController.cs b/src/Presentation/QBD.API/Controllers/AccountsController.cs
new file mode 100644
index 0000000..443364e
--- /dev/null
+++ b/src/Presentation/QBD.API/Controllers/AccountsController.cs
@@ -0,0 +1,99 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using QBD.Application.Interfaces;
+using QBD.Domain.Entities.Accounting;
+using QBD.Domain.Enums;
+
+namespace QBD.API.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class AccountsController : ControllerBase
+{
+ private readonly IRepository _repo;
+ private readonly IRepository _glRepo;
+ private readonly IUnitOfWork _uow;
+
+ public AccountsController(IRepository repo, IRepository glRepo, IUnitOfWork uow)
+ {
+ _repo = repo;
+ _glRepo = glRepo;
+ _uow = uow;
+ }
+
+ [HttpGet]
+ public async Task GetAll([FromQuery] AccountType? type, [FromQuery] bool? activeOnly)
+ {
+ var query = _repo.Query();
+ if (type.HasValue) query = query.Where(a => a.AccountType == type.Value);
+ if (activeOnly == true) query = query.Where(a => a.IsActive);
+ var accounts = await query.OrderBy(a => a.SortOrder).ToListAsync();
+ return Ok(accounts);
+ }
+
+ [HttpGet("{id}")]
+ public async Task GetById(int id)
+ {
+ var account = await _repo.Query()
+ .Include(a => a.SubAccounts)
+ .FirstOrDefaultAsync(a => a.Id == id);
+ if (account == null) return NotFound();
+ return Ok(account);
+ }
+
+ [HttpPost]
+ public async Task Create([FromBody] Account account)
+ {
+ account.IsActive = true;
+ var created = await _repo.AddAsync(account);
+ await _uow.SaveChangesAsync();
+ return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
+ }
+
+ [HttpPut("{id}")]
+ public async Task Update(int id, [FromBody] Account account)
+ {
+ var existing = await _repo.GetByIdAsync(id);
+ if (existing == null) return NotFound();
+
+ existing.Name = account.Name;
+ existing.Number = account.Number;
+ existing.AccountType = account.AccountType;
+ existing.Description = account.Description;
+ existing.IsActive = account.IsActive;
+ existing.ParentId = account.ParentId;
+
+ await _repo.UpdateAsync(existing);
+ await _uow.SaveChangesAsync();
+ return Ok(existing);
+ }
+
+ [HttpDelete("{id}")]
+ public async Task Delete(int id)
+ {
+ var account = await _repo.GetByIdAsync(id);
+ if (account == null) return NotFound();
+ if (account.IsSystemAccount) return BadRequest("Cannot delete system accounts.");
+
+ await _repo.DeleteAsync(account);
+ await _uow.SaveChangesAsync();
+ return NoContent();
+ }
+
+ [HttpGet("{id}/ledger")]
+ public async Task GetLedger(int id, [FromQuery] DateTime? from, [FromQuery] DateTime? to)
+ {
+ var account = await _repo.GetByIdAsync(id);
+ if (account == null) return NotFound();
+
+ var query = _glRepo.Query().Where(g => g.AccountId == id && !g.IsVoid);
+ if (from.HasValue) query = query.Where(g => g.PostingDate >= from.Value);
+ if (to.HasValue) query = query.Where(g => g.PostingDate <= to.Value);
+
+ var entries = await query.OrderBy(g => g.PostingDate).ThenBy(g => g.Id).ToListAsync();
+ return Ok(entries);
+ }
+}
diff --git a/src/Presentation/QBD.API/Controllers/AuditController.cs b/src/Presentation/QBD.API/Controllers/AuditController.cs
new file mode 100644
index 0000000..84c901d
--- /dev/null
+++ b/src/Presentation/QBD.API/Controllers/AuditController.cs
@@ -0,0 +1,41 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using QBD.Infrastructure.Data;
+
+namespace QBD.API.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class AuditController : ControllerBase
+{
+ private readonly QBDesktopDbContext _context;
+
+ public AuditController(QBDesktopDbContext context)
+ {
+ _context = context;
+ }
+
+ [HttpGet]
+ public async Task GetAll(
+ [FromQuery] string? entityType,
+ [FromQuery] int? entityId,
+ [FromQuery] DateTime? from,
+ [FromQuery] DateTime? to,
+ [FromQuery] int page = 1,
+ [FromQuery] int pageSize = 50)
+ {
+ var query = _context.AuditLogEntries.AsQueryable();
+ if (!string.IsNullOrEmpty(entityType)) query = query.Where(a => a.EntityType == entityType);
+ if (entityId.HasValue) query = query.Where(a => a.EntityId == entityId.Value);
+ if (from.HasValue) query = query.Where(a => a.Timestamp >= from.Value);
+ if (to.HasValue) query = query.Where(a => a.Timestamp <= to.Value);
+
+ var total = await query.CountAsync();
+ var items = await query.OrderByDescending(a => a.Timestamp)
+ .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
+ return Ok(new { items, total, page, pageSize });
+ }
+}
diff --git a/src/Presentation/QBD.API/Controllers/BillPaymentsController.cs b/src/Presentation/QBD.API/Controllers/BillPaymentsController.cs
new file mode 100644
index 0000000..f6cd3cb
--- /dev/null
+++ b/src/Presentation/QBD.API/Controllers/BillPaymentsController.cs
@@ -0,0 +1,90 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using QBD.Application.Interfaces;
+using QBD.Domain.Entities.Vendors;
+using QBD.Domain.Enums;
+
+namespace QBD.API.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class BillPaymentsController : ControllerBase
+{
+ private readonly IRepository _repo;
+ private readonly IUnitOfWork _uow;
+ private readonly ITransactionPostingService _posting;
+
+ public BillPaymentsController(
+ IRepository repo,
+ IUnitOfWork uow,
+ ITransactionPostingService posting)
+ {
+ _repo = repo;
+ _uow = uow;
+ _posting = posting;
+ }
+
+ [HttpGet]
+ public async Task GetAll([FromQuery] int page = 1, [FromQuery] int pageSize = 50)
+ {
+ var query = _repo.Query()
+ .Include(bp => bp.PaymentAccount);
+
+ var total = await query.CountAsync();
+ var items = await query.OrderByDescending(bp => bp.Date)
+ .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
+ return Ok(new { items, total, page, pageSize });
+ }
+
+ [HttpGet("{id}")]
+ public async Task GetById(int id)
+ {
+ var payment = await _repo.Query()
+ .Include(bp => bp.PaymentAccount)
+ .Include(bp => bp.PaymentMethod)
+ .Include(bp => bp.Applications).ThenInclude(a => a.Bill).ThenInclude(b => b.Vendor)
+ .FirstOrDefaultAsync(bp => bp.Id == id);
+ if (payment == null) return NotFound();
+ return Ok(payment);
+ }
+
+ [HttpPost]
+ public async Task Create([FromBody] BillPayment payment)
+ {
+ payment.Status = DocStatus.Draft;
+ payment.Amount = payment.Applications.Sum(a => a.AmountApplied);
+
+ var created = await _repo.AddAsync(payment);
+ await _uow.SaveChangesAsync();
+ return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
+ }
+
+ [HttpPost("{id}/post")]
+ public async Task Post(int id)
+ {
+ var payment = await _repo.GetByIdAsync(id);
+ if (payment == null) return NotFound();
+ if (payment.Status != DocStatus.Draft) return BadRequest("Only draft bill payments can be posted.");
+
+ await _posting.PostTransactionAsync(TransactionType.BillPayment, id);
+ payment.Status = DocStatus.Posted;
+ await _repo.UpdateAsync(payment);
+ await _uow.SaveChangesAsync();
+ return Ok(payment);
+ }
+
+ [HttpDelete("{id}")]
+ public async Task Delete(int id)
+ {
+ var payment = await _repo.GetByIdAsync(id);
+ if (payment == null) return NotFound();
+ if (payment.Status == DocStatus.Posted) return BadRequest("Cannot delete posted bill payments.");
+
+ await _repo.DeleteAsync(payment);
+ await _uow.SaveChangesAsync();
+ return NoContent();
+ }
+}
diff --git a/src/Presentation/QBD.API/Controllers/BillsController.cs b/src/Presentation/QBD.API/Controllers/BillsController.cs
new file mode 100644
index 0000000..76cb4bd
--- /dev/null
+++ b/src/Presentation/QBD.API/Controllers/BillsController.cs
@@ -0,0 +1,112 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using QBD.Application.Interfaces;
+using QBD.Domain.Entities.Vendors;
+using QBD.Domain.Enums;
+
+namespace QBD.API.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class BillsController : ControllerBase
+{
+ private readonly IRepository _repo;
+ private readonly IUnitOfWork _uow;
+ private readonly INumberSequenceService _numberSeq;
+ private readonly ITransactionPostingService _posting;
+
+ public BillsController(
+ IRepository repo,
+ IUnitOfWork uow,
+ INumberSequenceService numberSeq,
+ ITransactionPostingService posting)
+ {
+ _repo = repo;
+ _uow = uow;
+ _numberSeq = numberSeq;
+ _posting = posting;
+ }
+
+ [HttpGet]
+ public async Task GetAll([FromQuery] int? vendorId, [FromQuery] DocStatus? status,
+ [FromQuery] int page = 1, [FromQuery] int pageSize = 50)
+ {
+ var query = _repo.Query().Include(b => b.Vendor).AsQueryable();
+ if (vendorId.HasValue) query = query.Where(b => b.VendorId == vendorId.Value);
+ if (status.HasValue) query = query.Where(b => b.Status == status.Value);
+
+ var total = await query.CountAsync();
+ var items = await query.OrderByDescending(b => b.Date)
+ .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
+ return Ok(new { items, total, page, pageSize });
+ }
+
+ [HttpGet("{id}")]
+ public async Task GetById(int id)
+ {
+ var bill = await _repo.Query()
+ .Include(b => b.Vendor)
+ .Include(b => b.ExpenseLines).ThenInclude(l => l.Account)
+ .Include(b => b.ItemLines).ThenInclude(l => l.Item)
+ .Include(b => b.Terms)
+ .FirstOrDefaultAsync(b => b.Id == id);
+ if (bill == null) return NotFound();
+ return Ok(bill);
+ }
+
+ [HttpPost]
+ public async Task Create([FromBody] Bill bill)
+ {
+ bill.BillNumber = await _numberSeq.GetNextNumberAsync("Bill");
+ bill.Status = DocStatus.Draft;
+ bill.AmountDue = bill.ExpenseLines.Sum(l => l.Amount) + bill.ItemLines.Sum(l => l.Amount);
+ bill.BalanceDue = bill.AmountDue;
+
+ var created = await _repo.AddAsync(bill);
+ await _uow.SaveChangesAsync();
+ return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
+ }
+
+ [HttpPost("{id}/post")]
+ public async Task Post(int id)
+ {
+ var bill = await _repo.GetByIdAsync(id);
+ if (bill == null) return NotFound();
+ if (bill.Status != DocStatus.Draft) return BadRequest("Only draft bills can be posted.");
+
+ await _posting.PostTransactionAsync(TransactionType.Bill, id);
+ bill.Status = DocStatus.Posted;
+ await _repo.UpdateAsync(bill);
+ await _uow.SaveChangesAsync();
+ return Ok(bill);
+ }
+
+ [HttpPost("{id}/void")]
+ public async Task Void(int id)
+ {
+ var bill = await _repo.GetByIdAsync(id);
+ if (bill == null) return NotFound();
+ if (bill.Status != DocStatus.Posted) return BadRequest("Only posted bills can be voided.");
+
+ await _posting.VoidTransactionAsync(TransactionType.Bill, id);
+ bill.Status = DocStatus.Voided;
+ await _repo.UpdateAsync(bill);
+ await _uow.SaveChangesAsync();
+ return Ok(bill);
+ }
+
+ [HttpDelete("{id}")]
+ public async Task Delete(int id)
+ {
+ var bill = await _repo.GetByIdAsync(id);
+ if (bill == null) return NotFound();
+ if (bill.Status == DocStatus.Posted) return BadRequest("Cannot delete posted bills. Void first.");
+
+ await _repo.DeleteAsync(bill);
+ await _uow.SaveChangesAsync();
+ return NoContent();
+ }
+}
diff --git a/src/Presentation/QBD.API/Controllers/ChecksController.cs b/src/Presentation/QBD.API/Controllers/ChecksController.cs
new file mode 100644
index 0000000..3317027
--- /dev/null
+++ b/src/Presentation/QBD.API/Controllers/ChecksController.cs
@@ -0,0 +1,108 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using QBD.Application.Interfaces;
+using QBD.Domain.Entities.Banking;
+using QBD.Domain.Enums;
+
+namespace QBD.API.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class ChecksController : ControllerBase
+{
+ private readonly IRepository _repo;
+ private readonly IUnitOfWork _uow;
+ private readonly INumberSequenceService _numberSeq;
+ private readonly ITransactionPostingService _posting;
+
+ public ChecksController(
+ IRepository repo,
+ IUnitOfWork uow,
+ INumberSequenceService numberSeq,
+ ITransactionPostingService posting)
+ {
+ _repo = repo;
+ _uow = uow;
+ _numberSeq = numberSeq;
+ _posting = posting;
+ }
+
+ [HttpGet]
+ public async Task GetAll([FromQuery] int? bankAccountId, [FromQuery] int page = 1, [FromQuery] int pageSize = 50)
+ {
+ var query = _repo.Query().Include(c => c.BankAccount).AsQueryable();
+ if (bankAccountId.HasValue) query = query.Where(c => c.BankAccountId == bankAccountId.Value);
+
+ var total = await query.CountAsync();
+ var items = await query.OrderByDescending(c => c.Date)
+ .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
+ return Ok(new { items, total, page, pageSize });
+ }
+
+ [HttpGet("{id}")]
+ public async Task GetById(int id)
+ {
+ var check = await _repo.Query()
+ .Include(c => c.BankAccount)
+ .Include(c => c.ExpenseLines).ThenInclude(l => l.Account)
+ .Include(c => c.ItemLines).ThenInclude(l => l.Item)
+ .FirstOrDefaultAsync(c => c.Id == id);
+ if (check == null) return NotFound();
+ return Ok(check);
+ }
+
+ [HttpPost]
+ public async Task Create([FromBody] Check check)
+ {
+ check.CheckNumber = await _numberSeq.GetNextNumberAsync("Check");
+ check.Status = DocStatus.Draft;
+ check.Amount = check.ExpenseLines.Sum(l => l.Amount) + check.ItemLines.Sum(l => l.Amount);
+
+ var created = await _repo.AddAsync(check);
+ await _uow.SaveChangesAsync();
+ return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
+ }
+
+ [HttpPost("{id}/post")]
+ public async Task Post(int id)
+ {
+ var check = await _repo.GetByIdAsync(id);
+ if (check == null) return NotFound();
+ if (check.Status != DocStatus.Draft) return BadRequest("Only draft checks can be posted.");
+
+ await _posting.PostTransactionAsync(TransactionType.Check, id);
+ check.Status = DocStatus.Posted;
+ await _repo.UpdateAsync(check);
+ await _uow.SaveChangesAsync();
+ return Ok(check);
+ }
+
+ [HttpPost("{id}/void")]
+ public async Task Void(int id)
+ {
+ var check = await _repo.GetByIdAsync(id);
+ if (check == null) return NotFound();
+ if (check.Status != DocStatus.Posted) return BadRequest("Only posted checks can be voided.");
+
+ await _posting.VoidTransactionAsync(TransactionType.Check, id);
+ check.Status = DocStatus.Voided;
+ await _repo.UpdateAsync(check);
+ await _uow.SaveChangesAsync();
+ return Ok(check);
+ }
+
+ [HttpDelete("{id}")]
+ public async Task Delete(int id)
+ {
+ var check = await _repo.GetByIdAsync(id);
+ if (check == null) return NotFound();
+ if (check.Status == DocStatus.Posted) return BadRequest("Cannot delete posted checks.");
+
+ await _repo.DeleteAsync(check);
+ await _uow.SaveChangesAsync();
+ return NoContent();
+ }
+}
diff --git a/src/Presentation/QBD.API/Controllers/CompanyController.cs b/src/Presentation/QBD.API/Controllers/CompanyController.cs
new file mode 100644
index 0000000..6f10721
--- /dev/null
+++ b/src/Presentation/QBD.API/Controllers/CompanyController.cs
@@ -0,0 +1,169 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using QBD.Application.Interfaces;
+using QBD.Domain.Entities.Accounting;
+using QBD.Domain.Entities.Company;
+
+namespace QBD.API.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class CompanyController : ControllerBase
+{
+ private readonly IRepository _companyRepo;
+ private readonly IRepository _termsRepo;
+ private readonly IRepository _paymentMethodRepo;
+ private readonly IRepository _taxCodeRepo;
+ private readonly IRepository _classRepo;
+ private readonly IRepository _locationRepo;
+ private readonly IUnitOfWork _uow;
+
+ public CompanyController(
+ IRepository companyRepo,
+ IRepository termsRepo,
+ IRepository paymentMethodRepo,
+ IRepository taxCodeRepo,
+ IRepository classRepo,
+ IRepository locationRepo,
+ IUnitOfWork uow)
+ {
+ _companyRepo = companyRepo;
+ _termsRepo = termsRepo;
+ _paymentMethodRepo = paymentMethodRepo;
+ _taxCodeRepo = taxCodeRepo;
+ _classRepo = classRepo;
+ _locationRepo = locationRepo;
+ _uow = uow;
+ }
+
+ // === Company Info ===
+
+ [HttpGet("info")]
+ public async Task GetCompanyInfo()
+ {
+ var info = await _companyRepo.Query().FirstOrDefaultAsync();
+ if (info == null) return NotFound();
+ return Ok(info);
+ }
+
+ [HttpPut("info")]
+ public async Task UpdateCompanyInfo([FromBody] CompanyInfo info)
+ {
+ var existing = await _companyRepo.Query().FirstOrDefaultAsync();
+ if (existing == null) return NotFound();
+
+ existing.Name = info.Name;
+ existing.LegalName = info.LegalName;
+ existing.Address = info.Address;
+ existing.City = info.City;
+ existing.State = info.State;
+ existing.Zip = info.Zip;
+ existing.Phone = info.Phone;
+ existing.Email = info.Email;
+ existing.EIN = info.EIN;
+ existing.FiscalYearStartMonth = info.FiscalYearStartMonth;
+
+ await _companyRepo.UpdateAsync(existing);
+ await _uow.SaveChangesAsync();
+ return Ok(existing);
+ }
+
+ // === Terms ===
+
+ [HttpGet("terms")]
+ public async Task GetTerms()
+ {
+ var terms = await _termsRepo.GetAllAsync();
+ return Ok(terms);
+ }
+
+ [HttpPost("terms")]
+ public async Task CreateTerms([FromBody] Terms terms)
+ {
+ terms.IsActive = true;
+ var created = await _termsRepo.AddAsync(terms);
+ await _uow.SaveChangesAsync();
+ return Ok(created);
+ }
+
+ // === Payment Methods ===
+
+ [HttpGet("payment-methods")]
+ public async Task GetPaymentMethods()
+ {
+ var methods = await _paymentMethodRepo.GetAllAsync();
+ return Ok(methods);
+ }
+
+ [HttpPost("payment-methods")]
+ public async Task CreatePaymentMethod([FromBody] PaymentMethod method)
+ {
+ method.IsActive = true;
+ var created = await _paymentMethodRepo.AddAsync(method);
+ await _uow.SaveChangesAsync();
+ return Ok(created);
+ }
+
+ // === Tax Codes ===
+
+ [HttpGet("tax-codes")]
+ public async Task GetTaxCodes()
+ {
+ var codes = await _taxCodeRepo.GetAllAsync();
+ return Ok(codes);
+ }
+
+ [HttpPost("tax-codes")]
+ public async Task CreateTaxCode([FromBody] TaxCode code)
+ {
+ code.IsActive = true;
+ var created = await _taxCodeRepo.AddAsync(code);
+ await _uow.SaveChangesAsync();
+ return Ok(created);
+ }
+
+ // === Classes ===
+
+ [HttpGet("classes")]
+ public async Task GetClasses()
+ {
+ var classes = await _classRepo.Query()
+ .Include(c => c.SubClasses)
+ .Where(c => c.ParentId == null)
+ .ToListAsync();
+ return Ok(classes);
+ }
+
+ [HttpPost("classes")]
+ public async Task CreateClass([FromBody] Class cls)
+ {
+ cls.IsActive = true;
+ var created = await _classRepo.AddAsync(cls);
+ await _uow.SaveChangesAsync();
+ return Ok(created);
+ }
+
+ // === Locations ===
+
+ [HttpGet("locations")]
+ public async Task GetLocations()
+ {
+ var locations = await _locationRepo.Query()
+ .Include(l => l.SubLocations)
+ .Where(l => l.ParentId == null)
+ .ToListAsync();
+ return Ok(locations);
+ }
+
+ [HttpPost("locations")]
+ public async Task CreateLocation([FromBody] Location location)
+ {
+ location.IsActive = true;
+ var created = await _locationRepo.AddAsync(location);
+ await _uow.SaveChangesAsync();
+ return Ok(created);
+ }
+}
diff --git a/src/Presentation/QBD.API/Controllers/CreditMemosController.cs b/src/Presentation/QBD.API/Controllers/CreditMemosController.cs
new file mode 100644
index 0000000..79c407a
--- /dev/null
+++ b/src/Presentation/QBD.API/Controllers/CreditMemosController.cs
@@ -0,0 +1,95 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using QBD.Application.Interfaces;
+using QBD.Domain.Entities.Customers;
+using QBD.Domain.Enums;
+
+namespace QBD.API.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class CreditMemosController : ControllerBase
+{
+ private readonly IRepository _repo;
+ private readonly IUnitOfWork _uow;
+ private readonly INumberSequenceService _numberSeq;
+ private readonly ITransactionPostingService _posting;
+
+ public CreditMemosController(
+ IRepository repo,
+ IUnitOfWork uow,
+ INumberSequenceService numberSeq,
+ ITransactionPostingService posting)
+ {
+ _repo = repo;
+ _uow = uow;
+ _numberSeq = numberSeq;
+ _posting = posting;
+ }
+
+ [HttpGet]
+ public async Task GetAll([FromQuery] int? customerId, [FromQuery] int page = 1, [FromQuery] int pageSize = 50)
+ {
+ var query = _repo.Query().Include(c => c.Customer).AsQueryable();
+ if (customerId.HasValue) query = query.Where(c => c.CustomerId == customerId.Value);
+
+ var total = await query.CountAsync();
+ var items = await query.OrderByDescending(c => c.Date)
+ .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
+ return Ok(new { items, total, page, pageSize });
+ }
+
+ [HttpGet("{id}")]
+ public async Task GetById(int id)
+ {
+ var memo = await _repo.Query()
+ .Include(c => c.Customer)
+ .Include(c => c.Lines).ThenInclude(l => l.Item)
+ .FirstOrDefaultAsync(c => c.Id == id);
+ if (memo == null) return NotFound();
+ return Ok(memo);
+ }
+
+ [HttpPost]
+ public async Task Create([FromBody] CreditMemo memo)
+ {
+ memo.CreditNumber = await _numberSeq.GetNextNumberAsync("CreditMemo");
+ memo.Status = DocStatus.Draft;
+ memo.Subtotal = memo.Lines.Sum(l => l.Amount);
+ memo.Total = memo.Subtotal + memo.Tax;
+ memo.BalanceRemaining = memo.Total;
+
+ var created = await _repo.AddAsync(memo);
+ await _uow.SaveChangesAsync();
+ return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
+ }
+
+ [HttpPost("{id}/post")]
+ public async Task Post(int id)
+ {
+ var memo = await _repo.GetByIdAsync(id);
+ if (memo == null) return NotFound();
+ if (memo.Status != DocStatus.Draft) return BadRequest("Only draft credit memos can be posted.");
+
+ await _posting.PostTransactionAsync(TransactionType.CreditMemo, id);
+ memo.Status = DocStatus.Posted;
+ await _repo.UpdateAsync(memo);
+ await _uow.SaveChangesAsync();
+ return Ok(memo);
+ }
+
+ [HttpDelete("{id}")]
+ public async Task Delete(int id)
+ {
+ var memo = await _repo.GetByIdAsync(id);
+ if (memo == null) return NotFound();
+ if (memo.Status == DocStatus.Posted) return BadRequest("Cannot delete posted credit memos.");
+
+ await _repo.DeleteAsync(memo);
+ await _uow.SaveChangesAsync();
+ return NoContent();
+ }
+}
diff --git a/src/Presentation/QBD.API/Controllers/CustomersController.cs b/src/Presentation/QBD.API/Controllers/CustomersController.cs
new file mode 100644
index 0000000..b4df80c
--- /dev/null
+++ b/src/Presentation/QBD.API/Controllers/CustomersController.cs
@@ -0,0 +1,86 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using QBD.Application.Interfaces;
+using QBD.Domain.Entities.Customers;
+
+namespace QBD.API.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class CustomersController : ControllerBase
+{
+ private readonly IRepository _repo;
+ private readonly IUnitOfWork _uow;
+
+ public CustomersController(IRepository repo, IUnitOfWork uow)
+ {
+ _repo = repo;
+ _uow = uow;
+ }
+
+ [HttpGet]
+ public async Task GetAll([FromQuery] bool? activeOnly, [FromQuery] int page = 1, [FromQuery] int pageSize = 50)
+ {
+ var (items, total) = await _repo.GetPagedAsync(page, pageSize,
+ filter: activeOnly == true ? c => c.IsActive : null,
+ orderBy: c => c.CustomerName);
+ return Ok(new { items, total, page, pageSize });
+ }
+
+ [HttpGet("{id}")]
+ public async Task GetById(int id)
+ {
+ var customer = await _repo.Query()
+ .Include(c => c.Jobs)
+ .Include(c => c.Invoices)
+ .Include(c => c.Terms)
+ .FirstOrDefaultAsync(c => c.Id == id);
+ if (customer == null) return NotFound();
+ return Ok(customer);
+ }
+
+ [HttpPost]
+ public async Task Create([FromBody] Customer customer)
+ {
+ customer.IsActive = true;
+ var created = await _repo.AddAsync(customer);
+ await _uow.SaveChangesAsync();
+ return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
+ }
+
+ [HttpPut("{id}")]
+ public async Task Update(int id, [FromBody] Customer customer)
+ {
+ var existing = await _repo.GetByIdAsync(id);
+ if (existing == null) return NotFound();
+
+ existing.CustomerName = customer.CustomerName;
+ existing.Company = customer.Company;
+ existing.BillToAddress = customer.BillToAddress;
+ existing.ShipToAddress = customer.ShipToAddress;
+ existing.Phone = customer.Phone;
+ existing.Email = customer.Email;
+ existing.TermsId = customer.TermsId;
+ existing.CreditLimit = customer.CreditLimit;
+ existing.TaxCodeId = customer.TaxCodeId;
+ existing.IsActive = customer.IsActive;
+
+ await _repo.UpdateAsync(existing);
+ await _uow.SaveChangesAsync();
+ return Ok(existing);
+ }
+
+ [HttpDelete("{id}")]
+ public async Task Delete(int id)
+ {
+ var customer = await _repo.GetByIdAsync(id);
+ if (customer == null) return NotFound();
+
+ await _repo.DeleteAsync(customer);
+ await _uow.SaveChangesAsync();
+ return NoContent();
+ }
+}
diff --git a/src/Presentation/QBD.API/Controllers/DepositsController.cs b/src/Presentation/QBD.API/Controllers/DepositsController.cs
new file mode 100644
index 0000000..bf23a2e
--- /dev/null
+++ b/src/Presentation/QBD.API/Controllers/DepositsController.cs
@@ -0,0 +1,90 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using QBD.Application.Interfaces;
+using QBD.Domain.Entities.Banking;
+using QBD.Domain.Enums;
+
+namespace QBD.API.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class DepositsController : ControllerBase
+{
+ private readonly IRepository _repo;
+ private readonly IUnitOfWork _uow;
+ private readonly ITransactionPostingService _posting;
+
+ public DepositsController(
+ IRepository repo,
+ IUnitOfWork uow,
+ ITransactionPostingService posting)
+ {
+ _repo = repo;
+ _uow = uow;
+ _posting = posting;
+ }
+
+ [HttpGet]
+ public async Task GetAll([FromQuery] int? bankAccountId, [FromQuery] int page = 1, [FromQuery] int pageSize = 50)
+ {
+ var query = _repo.Query().Include(d => d.BankAccount).AsQueryable();
+ if (bankAccountId.HasValue) query = query.Where(d => d.BankAccountId == bankAccountId.Value);
+
+ var total = await query.CountAsync();
+ var items = await query.OrderByDescending(d => d.Date)
+ .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
+ return Ok(new { items, total, page, pageSize });
+ }
+
+ [HttpGet("{id}")]
+ public async Task GetById(int id)
+ {
+ var deposit = await _repo.Query()
+ .Include(d => d.BankAccount)
+ .Include(d => d.Lines).ThenInclude(l => l.FromAccount)
+ .Include(d => d.Lines).ThenInclude(l => l.PaymentMethod)
+ .FirstOrDefaultAsync(d => d.Id == id);
+ if (deposit == null) return NotFound();
+ return Ok(deposit);
+ }
+
+ [HttpPost]
+ public async Task Create([FromBody] Deposit deposit)
+ {
+ deposit.Status = DocStatus.Draft;
+ deposit.Total = deposit.Lines.Sum(l => l.Amount);
+
+ var created = await _repo.AddAsync(deposit);
+ await _uow.SaveChangesAsync();
+ return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
+ }
+
+ [HttpPost("{id}/post")]
+ public async Task Post(int id)
+ {
+ var deposit = await _repo.GetByIdAsync(id);
+ if (deposit == null) return NotFound();
+ if (deposit.Status != DocStatus.Draft) return BadRequest("Only draft deposits can be posted.");
+
+ await _posting.PostTransactionAsync(TransactionType.Deposit, id);
+ deposit.Status = DocStatus.Posted;
+ await _repo.UpdateAsync(deposit);
+ await _uow.SaveChangesAsync();
+ return Ok(deposit);
+ }
+
+ [HttpDelete("{id}")]
+ public async Task Delete(int id)
+ {
+ var deposit = await _repo.GetByIdAsync(id);
+ if (deposit == null) return NotFound();
+ if (deposit.Status == DocStatus.Posted) return BadRequest("Cannot delete posted deposits.");
+
+ await _repo.DeleteAsync(deposit);
+ await _uow.SaveChangesAsync();
+ return NoContent();
+ }
+}
diff --git a/src/Presentation/QBD.API/Controllers/EstimatesController.cs b/src/Presentation/QBD.API/Controllers/EstimatesController.cs
new file mode 100644
index 0000000..9ebfd74
--- /dev/null
+++ b/src/Presentation/QBD.API/Controllers/EstimatesController.cs
@@ -0,0 +1,73 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using QBD.Application.Interfaces;
+using QBD.Domain.Entities.Customers;
+using QBD.Domain.Enums;
+
+namespace QBD.API.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class EstimatesController : ControllerBase
+{
+ private readonly IRepository _repo;
+ private readonly IUnitOfWork _uow;
+ private readonly INumberSequenceService _numberSeq;
+
+ public EstimatesController(IRepository repo, IUnitOfWork uow, INumberSequenceService numberSeq)
+ {
+ _repo = repo;
+ _uow = uow;
+ _numberSeq = numberSeq;
+ }
+
+ [HttpGet]
+ public async Task GetAll([FromQuery] int? customerId, [FromQuery] int page = 1, [FromQuery] int pageSize = 50)
+ {
+ var query = _repo.Query().Include(e => e.Customer).AsQueryable();
+ if (customerId.HasValue) query = query.Where(e => e.CustomerId == customerId.Value);
+
+ var total = await query.CountAsync();
+ var items = await query.OrderByDescending(e => e.Date)
+ .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
+ return Ok(new { items, total, page, pageSize });
+ }
+
+ [HttpGet("{id}")]
+ public async Task GetById(int id)
+ {
+ var estimate = await _repo.Query()
+ .Include(e => e.Customer)
+ .Include(e => e.Lines).ThenInclude(l => l.Item)
+ .FirstOrDefaultAsync(e => e.Id == id);
+ if (estimate == null) return NotFound();
+ return Ok(estimate);
+ }
+
+ [HttpPost]
+ public async Task Create([FromBody] Estimate estimate)
+ {
+ estimate.EstimateNumber = await _numberSeq.GetNextNumberAsync("Estimate");
+ estimate.Status = DocStatus.Draft;
+ estimate.Subtotal = estimate.Lines.Sum(l => l.Amount);
+ estimate.Total = estimate.Subtotal + estimate.Tax;
+
+ var created = await _repo.AddAsync(estimate);
+ await _uow.SaveChangesAsync();
+ return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
+ }
+
+ [HttpDelete("{id}")]
+ public async Task Delete(int id)
+ {
+ var estimate = await _repo.GetByIdAsync(id);
+ if (estimate == null) return NotFound();
+
+ await _repo.DeleteAsync(estimate);
+ await _uow.SaveChangesAsync();
+ return NoContent();
+ }
+}
diff --git a/src/Presentation/QBD.API/Controllers/InvoicesController.cs b/src/Presentation/QBD.API/Controllers/InvoicesController.cs
new file mode 100644
index 0000000..10880b6
--- /dev/null
+++ b/src/Presentation/QBD.API/Controllers/InvoicesController.cs
@@ -0,0 +1,157 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using QBD.Application.Interfaces;
+using QBD.Domain.Entities.Customers;
+using QBD.Domain.Enums;
+
+namespace QBD.API.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class InvoicesController : ControllerBase
+{
+ private readonly IRepository _repo;
+ private readonly IUnitOfWork _uow;
+ private readonly INumberSequenceService _numberSeq;
+ private readonly ITransactionPostingService _posting;
+
+ public InvoicesController(
+ IRepository repo,
+ IUnitOfWork uow,
+ INumberSequenceService numberSeq,
+ ITransactionPostingService posting)
+ {
+ _repo = repo;
+ _uow = uow;
+ _numberSeq = numberSeq;
+ _posting = posting;
+ }
+
+ [HttpGet]
+ public async Task GetAll([FromQuery] int? customerId, [FromQuery] DocStatus? status,
+ [FromQuery] int page = 1, [FromQuery] int pageSize = 50)
+ {
+ var query = _repo.Query().Include(i => i.Customer).AsQueryable();
+ if (customerId.HasValue) query = query.Where(i => i.CustomerId == customerId.Value);
+ if (status.HasValue) query = query.Where(i => i.Status == status.Value);
+
+ var total = await query.CountAsync();
+ var items = await query.OrderByDescending(i => i.Date)
+ .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
+ return Ok(new { items, total, page, pageSize });
+ }
+
+ [HttpGet("{id}")]
+ public async Task GetById(int id)
+ {
+ var invoice = await _repo.Query()
+ .Include(i => i.Customer)
+ .Include(i => i.Lines).ThenInclude(l => l.Item)
+ .Include(i => i.Terms)
+ .FirstOrDefaultAsync(i => i.Id == id);
+ if (invoice == null) return NotFound();
+ return Ok(invoice);
+ }
+
+ [HttpPost]
+ public async Task Create([FromBody] Invoice invoice)
+ {
+ invoice.InvoiceNumber = await _numberSeq.GetNextNumberAsync("Invoice");
+ invoice.Status = DocStatus.Draft;
+
+ // Calculate totals
+ invoice.Subtotal = invoice.Lines.Sum(l => l.Amount);
+ invoice.Total = invoice.Subtotal + invoice.TaxTotal;
+ invoice.BalanceDue = invoice.Total;
+
+ var created = await _repo.AddAsync(invoice);
+ await _uow.SaveChangesAsync();
+ return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
+ }
+
+ [HttpPut("{id}")]
+ public async Task Update(int id, [FromBody] Invoice invoice)
+ {
+ var existing = await _repo.Query()
+ .Include(i => i.Lines)
+ .FirstOrDefaultAsync(i => i.Id == id);
+ if (existing == null) return NotFound();
+ if (existing.Status != DocStatus.Draft) return BadRequest("Only draft invoices can be edited.");
+
+ existing.CustomerId = invoice.CustomerId;
+ existing.Date = invoice.Date;
+ existing.DueDate = invoice.DueDate;
+ existing.TermsId = invoice.TermsId;
+ existing.Memo = invoice.Memo;
+ existing.BillToAddress = invoice.BillToAddress;
+ existing.ShipToAddress = invoice.ShipToAddress;
+
+ // Replace lines
+ existing.Lines.Clear();
+ foreach (var line in invoice.Lines)
+ {
+ existing.Lines.Add(new InvoiceLine
+ {
+ ItemId = line.ItemId,
+ Description = line.Description,
+ Qty = line.Qty,
+ Rate = line.Rate,
+ Amount = line.Amount,
+ TaxCodeId = line.TaxCodeId,
+ ClassId = line.ClassId
+ });
+ }
+
+ existing.Subtotal = existing.Lines.Sum(l => l.Amount);
+ existing.TaxTotal = invoice.TaxTotal;
+ existing.Total = existing.Subtotal + existing.TaxTotal;
+ existing.BalanceDue = existing.Total - existing.AmountPaid;
+
+ await _repo.UpdateAsync(existing);
+ await _uow.SaveChangesAsync();
+ return Ok(existing);
+ }
+
+ [HttpPost("{id}/post")]
+ public async Task Post(int id)
+ {
+ var invoice = await _repo.GetByIdAsync(id);
+ if (invoice == null) return NotFound();
+ if (invoice.Status != DocStatus.Draft) return BadRequest("Only draft invoices can be posted.");
+
+ await _posting.PostTransactionAsync(TransactionType.Invoice, id);
+ invoice.Status = DocStatus.Posted;
+ await _repo.UpdateAsync(invoice);
+ await _uow.SaveChangesAsync();
+ return Ok(invoice);
+ }
+
+ [HttpPost("{id}/void")]
+ public async Task Void(int id)
+ {
+ var invoice = await _repo.GetByIdAsync(id);
+ if (invoice == null) return NotFound();
+ if (invoice.Status != DocStatus.Posted) return BadRequest("Only posted invoices can be voided.");
+
+ await _posting.VoidTransactionAsync(TransactionType.Invoice, id);
+ invoice.Status = DocStatus.Voided;
+ await _repo.UpdateAsync(invoice);
+ await _uow.SaveChangesAsync();
+ return Ok(invoice);
+ }
+
+ [HttpDelete("{id}")]
+ public async Task Delete(int id)
+ {
+ var invoice = await _repo.GetByIdAsync(id);
+ if (invoice == null) return NotFound();
+ if (invoice.Status == DocStatus.Posted) return BadRequest("Cannot delete posted invoices. Void first.");
+
+ await _repo.DeleteAsync(invoice);
+ await _uow.SaveChangesAsync();
+ return NoContent();
+ }
+}
diff --git a/src/Presentation/QBD.API/Controllers/ItemsController.cs b/src/Presentation/QBD.API/Controllers/ItemsController.cs
new file mode 100644
index 0000000..0a1ede4
--- /dev/null
+++ b/src/Presentation/QBD.API/Controllers/ItemsController.cs
@@ -0,0 +1,90 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using QBD.Application.Interfaces;
+using QBD.Domain.Entities.Items;
+using QBD.Domain.Enums;
+
+namespace QBD.API.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class ItemsController : ControllerBase
+{
+ private readonly IRepository
- _repo;
+ private readonly IUnitOfWork _uow;
+
+ public ItemsController(IRepository
- repo, IUnitOfWork uow)
+ {
+ _repo = repo;
+ _uow = uow;
+ }
+
+ [HttpGet]
+ public async Task GetAll([FromQuery] ItemType? type, [FromQuery] bool? activeOnly)
+ {
+ var query = _repo.Query();
+ if (type.HasValue) query = query.Where(i => i.ItemType == type.Value);
+ if (activeOnly == true) query = query.Where(i => i.IsActive);
+ var items = await query.OrderBy(i => i.ItemName).ToListAsync();
+ return Ok(items);
+ }
+
+ [HttpGet("{id}")]
+ public async Task GetById(int id)
+ {
+ var item = await _repo.Query()
+ .Include(i => i.IncomeAccount)
+ .Include(i => i.ExpenseAccount)
+ .Include(i => i.AssetAccount)
+ .Include(i => i.SubItems)
+ .FirstOrDefaultAsync(i => i.Id == id);
+ if (item == null) return NotFound();
+ return Ok(item);
+ }
+
+ [HttpPost]
+ public async Task Create([FromBody] Item item)
+ {
+ item.IsActive = true;
+ var created = await _repo.AddAsync(item);
+ await _uow.SaveChangesAsync();
+ return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
+ }
+
+ [HttpPut("{id}")]
+ public async Task Update(int id, [FromBody] Item item)
+ {
+ var existing = await _repo.GetByIdAsync(id);
+ if (existing == null) return NotFound();
+
+ existing.ItemName = item.ItemName;
+ existing.ItemType = item.ItemType;
+ existing.Description = item.Description;
+ existing.SalesPrice = item.SalesPrice;
+ existing.PurchaseCost = item.PurchaseCost;
+ existing.IncomeAccountId = item.IncomeAccountId;
+ existing.ExpenseAccountId = item.ExpenseAccountId;
+ existing.AssetAccountId = item.AssetAccountId;
+ existing.ReorderPoint = item.ReorderPoint;
+ existing.IsActive = item.IsActive;
+ existing.TaxCodeId = item.TaxCodeId;
+
+ await _repo.UpdateAsync(existing);
+ await _uow.SaveChangesAsync();
+ return Ok(existing);
+ }
+
+ [HttpDelete("{id}")]
+ public async Task Delete(int id)
+ {
+ var item = await _repo.GetByIdAsync(id);
+ if (item == null) return NotFound();
+
+ await _repo.DeleteAsync(item);
+ await _uow.SaveChangesAsync();
+ return NoContent();
+ }
+}
diff --git a/src/Presentation/QBD.API/Controllers/JournalEntriesController.cs b/src/Presentation/QBD.API/Controllers/JournalEntriesController.cs
new file mode 100644
index 0000000..c29f14d
--- /dev/null
+++ b/src/Presentation/QBD.API/Controllers/JournalEntriesController.cs
@@ -0,0 +1,109 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using QBD.Application.Interfaces;
+using QBD.Domain.Entities.Accounting;
+using QBD.Domain.Enums;
+
+namespace QBD.API.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class JournalEntriesController : ControllerBase
+{
+ private readonly IRepository _repo;
+ private readonly IUnitOfWork _uow;
+ private readonly INumberSequenceService _numberSeq;
+ private readonly ITransactionPostingService _posting;
+
+ public JournalEntriesController(
+ IRepository repo,
+ IUnitOfWork uow,
+ INumberSequenceService numberSeq,
+ ITransactionPostingService posting)
+ {
+ _repo = repo;
+ _uow = uow;
+ _numberSeq = numberSeq;
+ _posting = posting;
+ }
+
+ [HttpGet]
+ public async Task GetAll([FromQuery] int page = 1, [FromQuery] int pageSize = 50)
+ {
+ var total = await _repo.Query().CountAsync();
+ var items = await _repo.Query()
+ .OrderByDescending(j => j.PostingDate)
+ .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
+ return Ok(new { items, total, page, pageSize });
+ }
+
+ [HttpGet("{id}")]
+ public async Task GetById(int id)
+ {
+ var entry = await _repo.Query()
+ .Include(j => j.Lines).ThenInclude(l => l.Account)
+ .FirstOrDefaultAsync(j => j.Id == id);
+ if (entry == null) return NotFound();
+ return Ok(entry);
+ }
+
+ [HttpPost]
+ public async Task Create([FromBody] JournalEntry entry)
+ {
+ // Validate balanced
+ var totalDebits = entry.Lines.Sum(l => l.DebitAmount);
+ var totalCredits = entry.Lines.Sum(l => l.CreditAmount);
+ if (totalDebits != totalCredits)
+ return BadRequest($"Debits ({totalDebits:C}) must equal Credits ({totalCredits:C}).");
+
+ entry.EntryNumber = await _numberSeq.GetNextNumberAsync("JournalEntry");
+ entry.Status = DocStatus.Draft;
+
+ var created = await _repo.AddAsync(entry);
+ await _uow.SaveChangesAsync();
+ return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
+ }
+
+ [HttpPost("{id}/post")]
+ public async Task Post(int id)
+ {
+ var entry = await _repo.GetByIdAsync(id);
+ if (entry == null) return NotFound();
+ if (entry.Status != DocStatus.Draft) return BadRequest("Only draft entries can be posted.");
+
+ await _posting.PostTransactionAsync(TransactionType.JournalEntry, id);
+ entry.Status = DocStatus.Posted;
+ await _repo.UpdateAsync(entry);
+ await _uow.SaveChangesAsync();
+ return Ok(entry);
+ }
+
+ [HttpPost("{id}/void")]
+ public async Task Void(int id)
+ {
+ var entry = await _repo.GetByIdAsync(id);
+ if (entry == null) return NotFound();
+ if (entry.Status != DocStatus.Posted) return BadRequest("Only posted entries can be voided.");
+
+ await _posting.VoidTransactionAsync(TransactionType.JournalEntry, id);
+ entry.Status = DocStatus.Voided;
+ await _repo.UpdateAsync(entry);
+ await _uow.SaveChangesAsync();
+ return Ok(entry);
+ }
+
+ [HttpDelete("{id}")]
+ public async Task Delete(int id)
+ {
+ var entry = await _repo.GetByIdAsync(id);
+ if (entry == null) return NotFound();
+ if (entry.Status == DocStatus.Posted) return BadRequest("Cannot delete posted journal entries.");
+
+ await _repo.DeleteAsync(entry);
+ await _uow.SaveChangesAsync();
+ return NoContent();
+ }
+}
diff --git a/src/Presentation/QBD.API/Controllers/PurchaseOrdersController.cs b/src/Presentation/QBD.API/Controllers/PurchaseOrdersController.cs
new file mode 100644
index 0000000..1766830
--- /dev/null
+++ b/src/Presentation/QBD.API/Controllers/PurchaseOrdersController.cs
@@ -0,0 +1,73 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using QBD.Application.Interfaces;
+using QBD.Domain.Entities.Vendors;
+using QBD.Domain.Enums;
+
+namespace QBD.API.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class PurchaseOrdersController : ControllerBase
+{
+ private readonly IRepository _repo;
+ private readonly IUnitOfWork _uow;
+ private readonly INumberSequenceService _numberSeq;
+
+ public PurchaseOrdersController(IRepository repo, IUnitOfWork uow, INumberSequenceService numberSeq)
+ {
+ _repo = repo;
+ _uow = uow;
+ _numberSeq = numberSeq;
+ }
+
+ [HttpGet]
+ public async Task GetAll([FromQuery] int? vendorId, [FromQuery] int page = 1, [FromQuery] int pageSize = 50)
+ {
+ var query = _repo.Query().Include(p => p.Vendor).AsQueryable();
+ if (vendorId.HasValue) query = query.Where(p => p.VendorId == vendorId.Value);
+
+ var total = await query.CountAsync();
+ var items = await query.OrderByDescending(p => p.Date)
+ .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
+ return Ok(new { items, total, page, pageSize });
+ }
+
+ [HttpGet("{id}")]
+ public async Task GetById(int id)
+ {
+ var po = await _repo.Query()
+ .Include(p => p.Vendor)
+ .Include(p => p.Lines).ThenInclude(l => l.Item)
+ .FirstOrDefaultAsync(p => p.Id == id);
+ if (po == null) return NotFound();
+ return Ok(po);
+ }
+
+ [HttpPost]
+ public async Task Create([FromBody] PurchaseOrder po)
+ {
+ po.PONumber = await _numberSeq.GetNextNumberAsync("PurchaseOrder");
+ po.Status = DocStatus.Draft;
+ po.Subtotal = po.Lines.Sum(l => l.Amount);
+ po.Total = po.Subtotal;
+
+ var created = await _repo.AddAsync(po);
+ await _uow.SaveChangesAsync();
+ return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
+ }
+
+ [HttpDelete("{id}")]
+ public async Task Delete(int id)
+ {
+ var po = await _repo.GetByIdAsync(id);
+ if (po == null) return NotFound();
+
+ await _repo.DeleteAsync(po);
+ await _uow.SaveChangesAsync();
+ return NoContent();
+ }
+}
diff --git a/src/Presentation/QBD.API/Controllers/ReceivePaymentsController.cs b/src/Presentation/QBD.API/Controllers/ReceivePaymentsController.cs
new file mode 100644
index 0000000..10781c0
--- /dev/null
+++ b/src/Presentation/QBD.API/Controllers/ReceivePaymentsController.cs
@@ -0,0 +1,89 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using QBD.Application.Interfaces;
+using QBD.Domain.Entities.Customers;
+using QBD.Domain.Enums;
+
+namespace QBD.API.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class ReceivePaymentsController : ControllerBase
+{
+ private readonly IRepository _repo;
+ private readonly IUnitOfWork _uow;
+ private readonly ITransactionPostingService _posting;
+
+ public ReceivePaymentsController(
+ IRepository repo,
+ IUnitOfWork uow,
+ ITransactionPostingService posting)
+ {
+ _repo = repo;
+ _uow = uow;
+ _posting = posting;
+ }
+
+ [HttpGet]
+ public async Task GetAll([FromQuery] int? customerId, [FromQuery] int page = 1, [FromQuery] int pageSize = 50)
+ {
+ var query = _repo.Query().Include(r => r.Customer).AsQueryable();
+ if (customerId.HasValue) query = query.Where(r => r.CustomerId == customerId.Value);
+
+ var total = await query.CountAsync();
+ var items = await query.OrderByDescending(r => r.PaymentDate)
+ .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
+ return Ok(new { items, total, page, pageSize });
+ }
+
+ [HttpGet("{id}")]
+ public async Task GetById(int id)
+ {
+ var payment = await _repo.Query()
+ .Include(r => r.Customer)
+ .Include(r => r.Applications).ThenInclude(a => a.Invoice)
+ .Include(r => r.PaymentMethod)
+ .Include(r => r.DepositToAccount)
+ .FirstOrDefaultAsync(r => r.Id == id);
+ if (payment == null) return NotFound();
+ return Ok(payment);
+ }
+
+ [HttpPost]
+ public async Task Create([FromBody] ReceivePayment payment)
+ {
+ payment.Status = DocStatus.Draft;
+ var created = await _repo.AddAsync(payment);
+ await _uow.SaveChangesAsync();
+ return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
+ }
+
+ [HttpPost("{id}/post")]
+ public async Task Post(int id)
+ {
+ var payment = await _repo.GetByIdAsync(id);
+ if (payment == null) return NotFound();
+ if (payment.Status != DocStatus.Draft) return BadRequest("Only draft payments can be posted.");
+
+ await _posting.PostTransactionAsync(TransactionType.ReceivePayment, id);
+ payment.Status = DocStatus.Posted;
+ await _repo.UpdateAsync(payment);
+ await _uow.SaveChangesAsync();
+ return Ok(payment);
+ }
+
+ [HttpDelete("{id}")]
+ public async Task Delete(int id)
+ {
+ var payment = await _repo.GetByIdAsync(id);
+ if (payment == null) return NotFound();
+ if (payment.Status == DocStatus.Posted) return BadRequest("Cannot delete posted payments.");
+
+ await _repo.DeleteAsync(payment);
+ await _uow.SaveChangesAsync();
+ return NoContent();
+ }
+}
diff --git a/src/Presentation/QBD.API/Controllers/SalesReceiptsController.cs b/src/Presentation/QBD.API/Controllers/SalesReceiptsController.cs
new file mode 100644
index 0000000..225e8bd
--- /dev/null
+++ b/src/Presentation/QBD.API/Controllers/SalesReceiptsController.cs
@@ -0,0 +1,93 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using QBD.Application.Interfaces;
+using QBD.Domain.Entities.Customers;
+using QBD.Domain.Enums;
+
+namespace QBD.API.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class SalesReceiptsController : ControllerBase
+{
+ private readonly IRepository _repo;
+ private readonly IUnitOfWork _uow;
+ private readonly INumberSequenceService _numberSeq;
+ private readonly ITransactionPostingService _posting;
+
+ public SalesReceiptsController(
+ IRepository repo,
+ IUnitOfWork uow,
+ INumberSequenceService numberSeq,
+ ITransactionPostingService posting)
+ {
+ _repo = repo;
+ _uow = uow;
+ _numberSeq = numberSeq;
+ _posting = posting;
+ }
+
+ [HttpGet]
+ public async Task GetAll([FromQuery] int page = 1, [FromQuery] int pageSize = 50)
+ {
+ var query = _repo.Query().Include(s => s.Customer);
+ var total = await query.CountAsync();
+ var items = await query.OrderByDescending(s => s.Date)
+ .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
+ return Ok(new { items, total, page, pageSize });
+ }
+
+ [HttpGet("{id}")]
+ public async Task GetById(int id)
+ {
+ var receipt = await _repo.Query()
+ .Include(s => s.Customer)
+ .Include(s => s.Lines).ThenInclude(l => l.Item)
+ .Include(s => s.PaymentMethod)
+ .FirstOrDefaultAsync(s => s.Id == id);
+ if (receipt == null) return NotFound();
+ return Ok(receipt);
+ }
+
+ [HttpPost]
+ public async Task Create([FromBody] SalesReceipt receipt)
+ {
+ receipt.SalesReceiptNumber = await _numberSeq.GetNextNumberAsync("SalesReceipt");
+ receipt.Status = DocStatus.Draft;
+ receipt.Subtotal = receipt.Lines.Sum(l => l.Amount);
+ receipt.Total = receipt.Subtotal + receipt.Tax;
+
+ var created = await _repo.AddAsync(receipt);
+ await _uow.SaveChangesAsync();
+ return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
+ }
+
+ [HttpPost("{id}/post")]
+ public async Task Post(int id)
+ {
+ var receipt = await _repo.GetByIdAsync(id);
+ if (receipt == null) return NotFound();
+ if (receipt.Status != DocStatus.Draft) return BadRequest("Only draft sales receipts can be posted.");
+
+ await _posting.PostTransactionAsync(TransactionType.SalesReceipt, id);
+ receipt.Status = DocStatus.Posted;
+ await _repo.UpdateAsync(receipt);
+ await _uow.SaveChangesAsync();
+ return Ok(receipt);
+ }
+
+ [HttpDelete("{id}")]
+ public async Task Delete(int id)
+ {
+ var receipt = await _repo.GetByIdAsync(id);
+ if (receipt == null) return NotFound();
+ if (receipt.Status == DocStatus.Posted) return BadRequest("Cannot delete posted sales receipts.");
+
+ await _repo.DeleteAsync(receipt);
+ await _uow.SaveChangesAsync();
+ return NoContent();
+ }
+}
diff --git a/src/Presentation/QBD.API/Controllers/TransfersController.cs b/src/Presentation/QBD.API/Controllers/TransfersController.cs
new file mode 100644
index 0000000..b045a73
--- /dev/null
+++ b/src/Presentation/QBD.API/Controllers/TransfersController.cs
@@ -0,0 +1,88 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using QBD.Application.Interfaces;
+using QBD.Domain.Entities.Banking;
+using QBD.Domain.Enums;
+
+namespace QBD.API.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class TransfersController : ControllerBase
+{
+ private readonly IRepository _repo;
+ private readonly IUnitOfWork _uow;
+ private readonly ITransactionPostingService _posting;
+
+ public TransfersController(
+ IRepository repo,
+ IUnitOfWork uow,
+ ITransactionPostingService posting)
+ {
+ _repo = repo;
+ _uow = uow;
+ _posting = posting;
+ }
+
+ [HttpGet]
+ public async Task GetAll([FromQuery] int page = 1, [FromQuery] int pageSize = 50)
+ {
+ var query = _repo.Query()
+ .Include(t => t.FromAccount)
+ .Include(t => t.ToAccount);
+
+ var total = await query.CountAsync();
+ var items = await query.OrderByDescending(t => t.Date)
+ .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
+ return Ok(new { items, total, page, pageSize });
+ }
+
+ [HttpGet("{id}")]
+ public async Task GetById(int id)
+ {
+ var transfer = await _repo.Query()
+ .Include(t => t.FromAccount)
+ .Include(t => t.ToAccount)
+ .FirstOrDefaultAsync(t => t.Id == id);
+ if (transfer == null) return NotFound();
+ return Ok(transfer);
+ }
+
+ [HttpPost]
+ public async Task Create([FromBody] Transfer transfer)
+ {
+ transfer.Status = DocStatus.Draft;
+ var created = await _repo.AddAsync(transfer);
+ await _uow.SaveChangesAsync();
+ return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
+ }
+
+ [HttpPost("{id}/post")]
+ public async Task Post(int id)
+ {
+ var transfer = await _repo.GetByIdAsync(id);
+ if (transfer == null) return NotFound();
+ if (transfer.Status != DocStatus.Draft) return BadRequest("Only draft transfers can be posted.");
+
+ await _posting.PostTransactionAsync(TransactionType.Transfer, id);
+ transfer.Status = DocStatus.Posted;
+ await _repo.UpdateAsync(transfer);
+ await _uow.SaveChangesAsync();
+ return Ok(transfer);
+ }
+
+ [HttpDelete("{id}")]
+ public async Task Delete(int id)
+ {
+ var transfer = await _repo.GetByIdAsync(id);
+ if (transfer == null) return NotFound();
+ if (transfer.Status == DocStatus.Posted) return BadRequest("Cannot delete posted transfers.");
+
+ await _repo.DeleteAsync(transfer);
+ await _uow.SaveChangesAsync();
+ return NoContent();
+ }
+}
diff --git a/src/Presentation/QBD.API/Controllers/VendorCreditsController.cs b/src/Presentation/QBD.API/Controllers/VendorCreditsController.cs
new file mode 100644
index 0000000..0065c51
--- /dev/null
+++ b/src/Presentation/QBD.API/Controllers/VendorCreditsController.cs
@@ -0,0 +1,95 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using QBD.Application.Interfaces;
+using QBD.Domain.Entities.Vendors;
+using QBD.Domain.Enums;
+
+namespace QBD.API.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class VendorCreditsController : ControllerBase
+{
+ private readonly IRepository _repo;
+ private readonly IUnitOfWork _uow;
+ private readonly INumberSequenceService _numberSeq;
+ private readonly ITransactionPostingService _posting;
+
+ public VendorCreditsController(
+ IRepository repo,
+ IUnitOfWork uow,
+ INumberSequenceService numberSeq,
+ ITransactionPostingService posting)
+ {
+ _repo = repo;
+ _uow = uow;
+ _numberSeq = numberSeq;
+ _posting = posting;
+ }
+
+ [HttpGet]
+ public async Task GetAll([FromQuery] int? vendorId, [FromQuery] int page = 1, [FromQuery] int pageSize = 50)
+ {
+ var query = _repo.Query().Include(v => v.Vendor).AsQueryable();
+ if (vendorId.HasValue) query = query.Where(v => v.VendorId == vendorId.Value);
+
+ var total = await query.CountAsync();
+ var items = await query.OrderByDescending(v => v.Date)
+ .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
+ return Ok(new { items, total, page, pageSize });
+ }
+
+ [HttpGet("{id}")]
+ public async Task GetById(int id)
+ {
+ var credit = await _repo.Query()
+ .Include(v => v.Vendor)
+ .Include(v => v.Lines).ThenInclude(l => l.Account)
+ .Include(v => v.Lines).ThenInclude(l => l.Item)
+ .FirstOrDefaultAsync(v => v.Id == id);
+ if (credit == null) return NotFound();
+ return Ok(credit);
+ }
+
+ [HttpPost]
+ public async Task Create([FromBody] VendorCredit credit)
+ {
+ credit.RefNo = await _numberSeq.GetNextNumberAsync("VendorCredit");
+ credit.Status = DocStatus.Draft;
+ credit.Total = credit.Lines.Sum(l => l.Amount);
+ credit.BalanceRemaining = credit.Total;
+
+ var created = await _repo.AddAsync(credit);
+ await _uow.SaveChangesAsync();
+ return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
+ }
+
+ [HttpPost("{id}/post")]
+ public async Task Post(int id)
+ {
+ var credit = await _repo.GetByIdAsync(id);
+ if (credit == null) return NotFound();
+ if (credit.Status != DocStatus.Draft) return BadRequest("Only draft vendor credits can be posted.");
+
+ await _posting.PostTransactionAsync(TransactionType.VendorCredit, id);
+ credit.Status = DocStatus.Posted;
+ await _repo.UpdateAsync(credit);
+ await _uow.SaveChangesAsync();
+ return Ok(credit);
+ }
+
+ [HttpDelete("{id}")]
+ public async Task Delete(int id)
+ {
+ var credit = await _repo.GetByIdAsync(id);
+ if (credit == null) return NotFound();
+ if (credit.Status == DocStatus.Posted) return BadRequest("Cannot delete posted vendor credits.");
+
+ await _repo.DeleteAsync(credit);
+ await _uow.SaveChangesAsync();
+ return NoContent();
+ }
+}
diff --git a/src/Presentation/QBD.API/Controllers/VendorsController.cs b/src/Presentation/QBD.API/Controllers/VendorsController.cs
new file mode 100644
index 0000000..fa0db77
--- /dev/null
+++ b/src/Presentation/QBD.API/Controllers/VendorsController.cs
@@ -0,0 +1,85 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using QBD.Application.Interfaces;
+using QBD.Domain.Entities.Vendors;
+
+namespace QBD.API.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class VendorsController : ControllerBase
+{
+ private readonly IRepository _repo;
+ private readonly IUnitOfWork _uow;
+
+ public VendorsController(IRepository repo, IUnitOfWork uow)
+ {
+ _repo = repo;
+ _uow = uow;
+ }
+
+ [HttpGet]
+ public async Task GetAll([FromQuery] bool? activeOnly, [FromQuery] int page = 1, [FromQuery] int pageSize = 50)
+ {
+ var (items, total) = await _repo.GetPagedAsync(page, pageSize,
+ filter: activeOnly == true ? v => v.IsActive : null,
+ orderBy: v => v.VendorName);
+ return Ok(new { items, total, page, pageSize });
+ }
+
+ [HttpGet("{id}")]
+ public async Task GetById(int id)
+ {
+ var vendor = await _repo.Query()
+ .Include(v => v.Bills)
+ .Include(v => v.Terms)
+ .FirstOrDefaultAsync(v => v.Id == id);
+ if (vendor == null) return NotFound();
+ return Ok(vendor);
+ }
+
+ [HttpPost]
+ public async Task Create([FromBody] Vendor vendor)
+ {
+ vendor.IsActive = true;
+ var created = await _repo.AddAsync(vendor);
+ await _uow.SaveChangesAsync();
+ return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
+ }
+
+ [HttpPut("{id}")]
+ public async Task Update(int id, [FromBody] Vendor vendor)
+ {
+ var existing = await _repo.GetByIdAsync(id);
+ if (existing == null) return NotFound();
+
+ existing.VendorName = vendor.VendorName;
+ existing.Company = vendor.Company;
+ existing.Address = vendor.Address;
+ existing.Phone = vendor.Phone;
+ existing.Email = vendor.Email;
+ existing.TermsId = vendor.TermsId;
+ existing.CreditLimit = vendor.CreditLimit;
+ existing.TaxId = vendor.TaxId;
+ existing.Is1099 = vendor.Is1099;
+ existing.IsActive = vendor.IsActive;
+
+ await _repo.UpdateAsync(existing);
+ await _uow.SaveChangesAsync();
+ return Ok(existing);
+ }
+
+ [HttpDelete("{id}")]
+ public async Task Delete(int id)
+ {
+ var vendor = await _repo.GetByIdAsync(id);
+ if (vendor == null) return NotFound();
+
+ await _repo.DeleteAsync(vendor);
+ await _uow.SaveChangesAsync();
+ return NoContent();
+ }
+}
diff --git a/src/Presentation/QBD.API/Program.cs b/src/Presentation/QBD.API/Program.cs
new file mode 100644
index 0000000..0c83430
--- /dev/null
+++ b/src/Presentation/QBD.API/Program.cs
@@ -0,0 +1,58 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
+using Microsoft.EntityFrameworkCore;
+using QBD.Application.Interfaces;
+using QBD.Infrastructure.Data;
+using QBD.Infrastructure.Repositories;
+using QBD.Infrastructure.Services;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Database - SQLite for cross-platform support
+builder.Services.AddDbContext(options =>
+ options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")
+ ?? "Data Source=QuickBooksDesktop.db"));
+
+// Repositories
+builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
+builder.Services.AddScoped();
+
+// Services
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+
+// Seeder
+builder.Services.AddScoped();
+
+builder.Services.AddControllers()
+ .AddJsonOptions(options =>
+ {
+ options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles;
+ });
+
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen(c =>
+{
+ c.SwaggerDoc("v1", new() { Title = "QuickBooks Desktop API", Version = "v1" });
+});
+
+var app = builder.Build();
+
+// Ensure database is created and seeded
+using (var scope = app.Services.CreateScope())
+{
+ var context = scope.ServiceProvider.GetRequiredService();
+ await context.Database.EnsureCreatedAsync();
+
+ var seeder = scope.ServiceProvider.GetRequiredService();
+ await seeder.SeedAsync();
+}
+
+app.UseSwagger();
+app.UseSwaggerUI();
+
+app.MapControllers();
+
+app.Run();
diff --git a/src/Presentation/QBD.API/Properties/launchSettings.json b/src/Presentation/QBD.API/Properties/launchSettings.json
new file mode 100644
index 0000000..ca55329
--- /dev/null
+++ b/src/Presentation/QBD.API/Properties/launchSettings.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:5081",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "https://localhost:7196;http://localhost:5081",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/src/Presentation/QBD.API/QBD.API.csproj b/src/Presentation/QBD.API/QBD.API.csproj
new file mode 100644
index 0000000..9c59a09
--- /dev/null
+++ b/src/Presentation/QBD.API/QBD.API.csproj
@@ -0,0 +1,21 @@
+
+
+
+ net8.0
+ enable
+ enable
+ QBD.API
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Presentation/QBD.API/QuickBooksDesktop.db b/src/Presentation/QBD.API/QuickBooksDesktop.db
new file mode 100644
index 0000000..724fe81
Binary files /dev/null and b/src/Presentation/QBD.API/QuickBooksDesktop.db differ
diff --git a/src/Presentation/QBD.API/appsettings.Development.json b/src/Presentation/QBD.API/appsettings.Development.json
new file mode 100644
index 0000000..ff66ba6
--- /dev/null
+++ b/src/Presentation/QBD.API/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/src/Presentation/QBD.API/appsettings.json b/src/Presentation/QBD.API/appsettings.json
new file mode 100644
index 0000000..f203ae4
--- /dev/null
+++ b/src/Presentation/QBD.API/appsettings.json
@@ -0,0 +1,12 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "ConnectionStrings": {
+ "DefaultConnection": "Data Source=QuickBooksDesktop.db"
+ }
+}
diff --git a/src/Presentation/QBD.WPF/App.xaml b/src/Presentation/QBD.WPF/App.xaml
index 89033b4..ef6edb9 100644
--- a/src/Presentation/QBD.WPF/App.xaml
+++ b/src/Presentation/QBD.WPF/App.xaml
@@ -1,3 +1,6 @@
+
+
+
diff --git a/src/Presentation/QBD.WPF/App.xaml.cs b/src/Presentation/QBD.WPF/App.xaml.cs
index 92b5722..905cb29 100644
--- a/src/Presentation/QBD.WPF/App.xaml.cs
+++ b/src/Presentation/QBD.WPF/App.xaml.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Windows;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
@@ -33,6 +36,8 @@ public App()
services.AddScoped();
services.AddScoped();
services.AddScoped();
+ services.AddTransient();
+ services.AddSingleton();
// Navigation
services.AddSingleton();
diff --git a/src/Presentation/QBD.WPF/AssemblyInfo.cs b/src/Presentation/QBD.WPF/AssemblyInfo.cs
index cc29e7f..0eb371b 100644
--- a/src/Presentation/QBD.WPF/AssemblyInfo.cs
+++ b/src/Presentation/QBD.WPF/AssemblyInfo.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Windows;
[assembly:ThemeInfo(
diff --git a/src/Presentation/QBD.WPF/Controls/ViewModelTemplateSelector.cs b/src/Presentation/QBD.WPF/Controls/ViewModelTemplateSelector.cs
index 6679cc5..5a8f71d 100644
--- a/src/Presentation/QBD.WPF/Controls/ViewModelTemplateSelector.cs
+++ b/src/Presentation/QBD.WPF/Controls/ViewModelTemplateSelector.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System.Windows;
using System.Windows.Controls;
using QBD.Application.ViewModels;
diff --git a/src/Presentation/QBD.WPF/MainWindow.xaml b/src/Presentation/QBD.WPF/MainWindow.xaml
index 38ac16e..f64035f 100644
--- a/src/Presentation/QBD.WPF/MainWindow.xaml
+++ b/src/Presentation/QBD.WPF/MainWindow.xaml
@@ -1,3 +1,6 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Presentation/QBD.WPF/MainWindow.xaml.cs b/src/Presentation/QBD.WPF/MainWindow.xaml.cs
index 03fe618..1ae1e09 100644
--- a/src/Presentation/QBD.WPF/MainWindow.xaml.cs
+++ b/src/Presentation/QBD.WPF/MainWindow.xaml.cs
@@ -1,7 +1,12 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using System;
using System.IO;
+using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
+using System.Windows.Input;
using QBD.Application.Interfaces;
using QBD.Application.ViewModels;
@@ -13,18 +18,32 @@ public partial class MainWindow : Window
private bool _isDarkMode = false;
private string _themeConfigFile = string.Empty;
+ public ICommand HomePageCommand { get; }
+ public ICommand FocusSearchCommand { get; }
+ public ICommand CloseCurrentTabCommand { get; }
+ public ICommand SaveCommand { get; }
+ public ICommand NewCommand { get; }
+ public ICommand ShowKeyboardShortcutsCommand { get; }
+
public MainWindow(INavigationService navigationService, HomePageViewModel homePageViewModel)
{
- InitializeComponent();
_navigationService = navigationService;
+ HomePageCommand = new ActionCommand(() => _navigationService.OpenHomePage());
+ FocusSearchCommand = new ActionCommand(() => FocusSearch());
+ CloseCurrentTabCommand = new ActionCommand(() => CloseCurrentTab());
+ SaveCommand = new ActionCommand(() => ExecuteGlobalSave());
+ NewCommand = new ActionCommand(() => ExecuteGlobalNew());
+ ShowKeyboardShortcutsCommand = new ActionCommand(() => ShowKeyboardShortcuts());
+
+ InitializeComponent();
+
+ this.DataContext = this;
+
try
{
var appDataDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "QBD", "QBD.WPF");
- if (!Directory.Exists(appDataDirectory))
- {
- Directory.CreateDirectory(appDataDirectory);
- }
+ if (!Directory.Exists(appDataDirectory)) Directory.CreateDirectory(appDataDirectory);
_themeConfigFile = Path.Combine(appDataDirectory, "theme.cfg");
@@ -51,8 +70,7 @@ public void OpenTab(ViewModelBase viewModel)
{
foreach (var item in WorkspaceTabs.Items)
{
- if (item is ViewModelBase existing && existing.GetType() == viewModel.GetType()
- && existing.Title == viewModel.Title)
+ if (item is ViewModelBase existing && existing.GetType() == viewModel.GetType() && existing.Title == viewModel.Title)
{
WorkspaceTabs.SelectedItem = item;
return;
@@ -62,15 +80,8 @@ public void OpenTab(ViewModelBase viewModel)
WorkspaceTabs.SelectedItem = viewModel;
}
- public void CloseTab(object viewModel)
- {
- WorkspaceTabs.Items.Remove(viewModel);
- }
-
- public void CloseAllTabs()
- {
- WorkspaceTabs.Items.Clear();
- }
+ public void CloseTab(object viewModel) => WorkspaceTabs.Items.Remove(viewModel);
+ public void CloseAllTabs() => WorkspaceTabs.Items.Clear();
private void Exit_Click(object sender, RoutedEventArgs e) => System.Windows.Application.Current.Shutdown();
private void HomePage_Click(object sender, RoutedEventArgs e) => _navigationService.OpenHomePage();
@@ -116,36 +127,243 @@ public void CloseAllTabs()
private void CloseTab_Click(object sender, RoutedEventArgs e)
{
- if (sender is Button btn && btn.DataContext is ViewModelBase vm)
- {
- CloseTab(vm);
- }
+ if (sender is Button btn && btn.DataContext is ViewModelBase vm) CloseTab(vm);
}
-
private void CloseAllTabs_Click(object sender, RoutedEventArgs e) => CloseAllTabs();
+ private void KeyboardShortcuts_Click(object sender, RoutedEventArgs e) => ShowKeyboardShortcuts();
+
+ private void ShowKeyboardShortcuts()
+ {
+ var shortcuts = new Window
+ {
+ Title = "Keyboard Shortcuts",
+ Width = 450,
+ Height = 480,
+ WindowStartupLocation = WindowStartupLocation.CenterOwner,
+ Owner = this,
+ ResizeMode = ResizeMode.NoResize,
+ Background = (System.Windows.Media.Brush)FindResource("ThemeWindowBackground")
+ };
+
+ var grid = new Grid { Margin = new Thickness(20) };
+ grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+ grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
+ grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+
+ var header = new TextBlock
+ {
+ Text = "Keyboard Shortcuts",
+ FontSize = 18,
+ FontWeight = FontWeights.Bold,
+ Margin = new Thickness(0, 0, 0, 12),
+ Foreground = (System.Windows.Media.Brush)FindResource("ThemeForeground")
+ };
+ Grid.SetRow(header, 0);
+
+ var items = new[]
+ {
+ ("F1", "Show this help dialog"),
+ ("F5", "Go to Home Page"),
+ ("Ctrl+N", "New transaction (context-aware)"),
+ ("Ctrl+S", "Save current form"),
+ ("Ctrl+F", "Focus search box"),
+ ("Ctrl+A", "Chart of Accounts"),
+ ("Esc", "Close current tab"),
+ ("Tab", "Move to next field"),
+ ("Shift+Tab", "Move to previous field"),
+ ("Alt+S", "Save (in forms)"),
+ ("Alt+P", "Save & Post (in forms)"),
+ ("Alt+L", "Clear (in forms)"),
+ ("Alt+V", "Void (in forms)"),
+ ("Alt+R", "Print (in forms)"),
+ ("Alt+N", "New (in lists)"),
+ ("Alt+E", "Edit (in lists)"),
+ ("Alt+D", "Delete (in lists)"),
+ };
+
+ var listView = new ListView
+ {
+ BorderThickness = new Thickness(1),
+ BorderBrush = (System.Windows.Media.Brush)FindResource("ThemeBorderBrush"),
+ Background = (System.Windows.Media.Brush)FindResource("ThemeControlBackground")
+ };
+
+ var gridView = new GridView();
+ gridView.Columns.Add(new GridViewColumn
+ {
+ Header = "Shortcut",
+ Width = 120,
+ DisplayMemberBinding = new System.Windows.Data.Binding("Item1")
+ });
+ gridView.Columns.Add(new GridViewColumn
+ {
+ Header = "Action",
+ Width = 280,
+ DisplayMemberBinding = new System.Windows.Data.Binding("Item2")
+ });
+ listView.View = gridView;
+
+ foreach (var item in items)
+ listView.Items.Add(item);
+
+ Grid.SetRow(listView, 1);
+
+ var closeBtn = new Button
+ {
+ Content = "Close",
+ Padding = new Thickness(20, 6, 20, 6),
+ HorizontalAlignment = HorizontalAlignment.Right,
+ Margin = new Thickness(0, 12, 0, 0)
+ };
+ closeBtn.Click += (_, _) => shortcuts.Close();
+ Grid.SetRow(closeBtn, 2);
+
+ grid.Children.Add(header);
+ grid.Children.Add(listView);
+ grid.Children.Add(closeBtn);
+ shortcuts.Content = grid;
+ shortcuts.ShowDialog();
+ }
+
private void About_Click(object sender, RoutedEventArgs e)
{
- MessageBox.Show("QuickBooks Desktop Enterprise Clone\nVersion 1.0\n\nA full-featured accounting application.",
- "About", MessageBoxButton.OK, MessageBoxImage.Information);
+ MessageBox.Show("QuickBooks Desktop Enterprise Clone\nVersion 1.0\n\nA full-featured accounting application.", "About", MessageBoxButton.OK, MessageBoxImage.Information);
}
private void ToggleDarkMode_Click(object sender, RoutedEventArgs e)
{
_isDarkMode = !_isDarkMode;
- var app = (App)System.Windows.Application.Current;
- app.ChangeTheme(_isDarkMode);
+ ((App)System.Windows.Application.Current).ChangeTheme(_isDarkMode);
+ try { if (!string.IsNullOrEmpty(_themeConfigFile)) File.WriteAllText(_themeConfigFile, _isDarkMode.ToString()); }
+ catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"Error saving theme: {ex.Message}"); }
+ }
- try
+ private void CloseCurrentTab()
+ {
+ if (WorkspaceTabs.SelectedItem != null) CloseTab(WorkspaceTabs.SelectedItem);
+ }
+
+ private void FocusSearch()
+ {
+ var contentPresenter = FindVisualChild(WorkspaceTabs);
+ var searchRoot = contentPresenter ?? (DependencyObject)WorkspaceTabs;
+ var searchBox = FindSearchTextBox(searchRoot);
+
+ if (searchBox != null)
+ {
+ searchBox.Focus();
+ Keyboard.Focus(searchBox);
+ StatusText.Text = "Search box focused (Ctrl+F).";
+ }
+ else
+ {
+ StatusText.Text = "No search box found in this view.";
+ }
+ }
+
+ private void ExecuteGlobalNew()
+ {
+ var selectedItem = WorkspaceTabs.SelectedItem;
+ if (selectedItem == null) return;
+
+ StatusText.Text = "Opening new entry form...";
+
+ var itemType = selectedItem.GetType();
+ string[] commandNames = { "NewEntityCommand", "NewItemCommand", "NewCommand" };
+
+ foreach (var name in commandNames)
{
- if (!string.IsNullOrEmpty(_themeConfigFile))
+ var prop = itemType.GetProperty(name);
+ if (prop != null && prop.GetValue(selectedItem) is ICommand command && command.CanExecute(null))
{
- File.WriteAllText(_themeConfigFile, _isDarkMode.ToString());
+ command.Execute(null);
+ (WorkspaceTabs.SelectedContent as FrameworkElement)?.Focus();
+ return;
}
}
- catch (Exception ex)
+ StatusText.Text = "New entry not available here.";
+ }
+
+ private async void ExecuteGlobalSave()
+ {
+ var selectedItem = WorkspaceTabs.SelectedItem;
+ if (selectedItem == null) return;
+
+ var prop = selectedItem.GetType().GetProperty("SaveCommand");
+ if (prop != null && prop.GetValue(selectedItem) is ICommand command && command.CanExecute(null))
{
- System.Diagnostics.Debug.WriteLine($"Error saving theme: {ex.Message}");
+ StatusText.Text = "Saving...";
+ try
+ {
+ var asyncMethod = command.GetType().GetMethod("ExecuteAsync");
+ if (asyncMethod != null && asyncMethod.Invoke(command, new object?[] { null }) is Task task)
+ {
+ await task;
+ }
+ else
+ {
+ command.Execute(null);
+ }
+ StatusText.Text = "Transaction saved successfully (Ctrl+S).";
+ }
+ catch (Exception ex)
+ {
+ StatusText.Text = "Save failed.";
+ System.Diagnostics.Debug.WriteLine($"Save error: {ex.Message}");
+ }
+
+ (WorkspaceTabs.SelectedContent as FrameworkElement)?.Focus();
+ }
+ else
+ {
+ StatusText.Text = "Save not supported in this view.";
+ }
+ }
+
+ private TextBox? FindSearchTextBox(DependencyObject? obj)
+ {
+ if (obj == null) return null;
+
+ for (int i = 0; i < System.Windows.Media.VisualTreeHelper.GetChildrenCount(obj); i++)
+ {
+ var child = System.Windows.Media.VisualTreeHelper.GetChild(obj, i);
+
+ if (child is TextBox tb)
+ {
+ var binding = tb.GetBindingExpression(TextBox.TextProperty);
+ var path = binding?.ParentBinding?.Path?.Path;
+ if (path is "FilterText" or "SearchText")
+ return tb;
+ }
+
+ var result = FindSearchTextBox(child);
+ if (result != null) return result;
}
+ return null;
}
+
+ private T? FindVisualChild(DependencyObject? obj) where T : DependencyObject
+ {
+ if (obj == null) return null;
+
+ for (int i = 0; i < System.Windows.Media.VisualTreeHelper.GetChildrenCount(obj); i++)
+ {
+ var child = System.Windows.Media.VisualTreeHelper.GetChild(obj, i);
+ if (child is T match) return match;
+
+ var result = FindVisualChild(child);
+ if (result != null) return result;
+ }
+ return null;
+ }
+}
+
+public class ActionCommand : ICommand
+{
+ private readonly Action _execute;
+ public ActionCommand(Action execute) => _execute = execute;
+ public bool CanExecute(object? parameter) => true;
+ public void Execute(object? parameter) => _execute();
+ public event EventHandler? CanExecuteChanged { add { } remove { } }
}
\ No newline at end of file
diff --git a/src/Presentation/QBD.WPF/QBD.WPF.csproj b/src/Presentation/QBD.WPF/QBD.WPF.csproj
index d392753..7ed91dc 100644
--- a/src/Presentation/QBD.WPF/QBD.WPF.csproj
+++ b/src/Presentation/QBD.WPF/QBD.WPF.csproj
@@ -13,7 +13,6 @@
-
diff --git a/src/Presentation/QBD.WPF/Services/NavigationService.cs b/src/Presentation/QBD.WPF/Services/NavigationService.cs
index 4441a55..5ea665d 100644
--- a/src/Presentation/QBD.WPF/Services/NavigationService.cs
+++ b/src/Presentation/QBD.WPF/Services/NavigationService.cs
@@ -1,3 +1,6 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
using QBD.Application.Interfaces;
using QBD.Application.ViewModels;
diff --git a/src/Presentation/QBD.WPF/Services/WpfFileDialogService.cs b/src/Presentation/QBD.WPF/Services/WpfFileDialogService.cs
new file mode 100644
index 0000000..3f9b902
--- /dev/null
+++ b/src/Presentation/QBD.WPF/Services/WpfFileDialogService.cs
@@ -0,0 +1,21 @@
+// Copyright (c) 2026, Ravindu Gajanayaka
+// Licensed under GPLv3. See LICENSE
+
+using QBD.Application.Interfaces;
+
+namespace QBD.WPF.Services;
+
+public class WpfFileDialogService : IFileDialogService
+{
+ public string? ShowSaveFileDialog(string fileName, string defaultExt, string filter)
+ {
+ var dialog = new Microsoft.Win32.SaveFileDialog
+ {
+ FileName = fileName,
+ DefaultExt = defaultExt,
+ Filter = filter
+ };
+
+ return dialog.ShowDialog() == true ? dialog.FileName : null;
+ }
+}
diff --git a/src/Presentation/QBD.WPF/Themes/ClassicTheme.xaml b/src/Presentation/QBD.WPF/Themes/ClassicTheme.xaml
index 696d3c8..e7d4dc2 100644
--- a/src/Presentation/QBD.WPF/Themes/ClassicTheme.xaml
+++ b/src/Presentation/QBD.WPF/Themes/ClassicTheme.xaml
@@ -1,3 +1,6 @@
+
+
+
@@ -22,7 +25,7 @@
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4" Padding="{TemplateBinding Padding}">
-
+
@@ -55,7 +58,7 @@
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="3" Padding="{TemplateBinding Padding}">
-
+
diff --git a/src/Presentation/QBD.WPF/Themes/DarkColors.xaml b/src/Presentation/QBD.WPF/Themes/DarkColors.xaml
index 46ebe98..acbe065 100644
--- a/src/Presentation/QBD.WPF/Themes/DarkColors.xaml
+++ b/src/Presentation/QBD.WPF/Themes/DarkColors.xaml
@@ -1,3 +1,6 @@
+
+
+
diff --git a/src/Presentation/QBD.WPF/Themes/DataTemplates.xaml b/src/Presentation/QBD.WPF/Themes/DataTemplates.xaml
index a2a5563..333eb25 100644
--- a/src/Presentation/QBD.WPF/Themes/DataTemplates.xaml
+++ b/src/Presentation/QBD.WPF/Themes/DataTemplates.xaml
@@ -1,3 +1,6 @@
+
+
+
@@ -107,19 +110,19 @@
-
+ Width="160" Margin="0,0,4,0" VerticalContentAlignment="Center" TabIndex="1"/>
+
-
-
+
+
+ Margin="4" BorderThickness="1" BorderBrush="{DynamicResource ThemeBorderBrush}" TabIndex="3"/>
@@ -189,13 +192,25 @@
-
-
-
-
+
+
+
+
-
+
+
+
+
@@ -241,11 +256,14 @@
+
-
+
+
-
-
+
+
+
@@ -300,14 +318,19 @@
-
-
-
+
+
+
+
+
+
-
+ VerticalContentAlignment="Center" TabIndex="1"/>
+
+
@@ -349,20 +372,24 @@
+
+
+
-
-
-
-
+
+
+
diff --git a/src/Presentation/QBD.WPF/Themes/LightColors.xaml b/src/Presentation/QBD.WPF/Themes/LightColors.xaml
index 2c1eca9..231779d 100644
--- a/src/Presentation/QBD.WPF/Themes/LightColors.xaml
+++ b/src/Presentation/QBD.WPF/Themes/LightColors.xaml
@@ -1,3 +1,6 @@
+
+
+