Distribution_GL Initialization

This commit is contained in:
2026-10-20 13:07:07 +05:30
commit 3f16e0cb44
194 changed files with 35659 additions and 0 deletions
@@ -0,0 +1,76 @@
using General_Ledger_Service.DTOs.Reports;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace General_Ledger_Service.Services.PdfDocuments
{
// 05_LKAS_Report_PDF_Templates.md Section 3 (LKAS 1) - retrofit 2026-07-31, classified
// presentation (Non-Current/Current split) driven by BalanceSheetReportDto's sections,
// rather than the old rollup-by-depth-1 grouping. Each section is already leaf rows + a
// server-computed Total (Accounting_System_Design_LKAS.md Section 11.3), so the renderer
// just lists lines and prints the given totals - no rendering-layer arithmetic.
public class BalanceSheetPdfDocument(string companyName, string currencyCode, DateOnly asOfDate, BalanceSheetReportDto data) : IDocument
{
public DocumentMetadata GetMetadata() => DocumentMetadata.Default;
public void Compose(IDocumentContainer container)
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
ReportPageHeader.Compose(page, companyName, "Statement of Financial Position", ReportPageHeader.PointInTimePeriodLine(asOfDate), currencyCode);
page.Content().PaddingTop(10).Column(column =>
{
void RenderSection(string title, BalanceSheetSectionDto section, string totalLabel)
{
if (section.Lines.Count == 0) return;
column.Item().PaddingTop(8).Text(title).Bold();
foreach (var line in section.Lines)
{
column.Item().Row(r =>
{
r.RelativeItem(4).Text(line.AccountName).FontFamily(Fonts.Consolas);
r.RelativeItem(1).AlignRight().Text(PdfFormatting.FormatAmount(line.Balance));
});
}
column.Item().PaddingTop(2).LineHorizontal(0.75f);
column.Item().Row(r =>
{
r.RelativeItem(4).Text(totalLabel).Bold();
r.RelativeItem(1).AlignRight().Text(PdfFormatting.FormatAmount(section.Total)).Bold();
});
}
column.Item().Text("ASSETS").Bold().FontSize(12);
RenderSection("Non-Current Assets", data.NonCurrentAssets, "Total Non-Current Assets");
RenderSection("Current Assets", data.CurrentAssets, "Total Current Assets");
RenderSection("Unclassified Assets", data.UnclassifiedAssets, "Total Unclassified Assets");
column.Item().PaddingTop(3).LineHorizontal(1.5f);
column.Item().Row(r =>
{
r.RelativeItem(4).Text("TOTAL ASSETS").Bold();
r.RelativeItem(1).AlignRight().Text(PdfFormatting.FormatAmount(data.TotalAssets)).Bold();
});
column.Item().PaddingTop(15).Text("EQUITY AND LIABILITIES").Bold().FontSize(12);
RenderSection("Equity", data.Equity, "Total Equity");
RenderSection("Non-Current Liabilities", data.NonCurrentLiabilities, "Total Non-Current Liabilities");
RenderSection("Current Liabilities", data.CurrentLiabilities, "Total Current Liabilities");
RenderSection("Unclassified Liabilities", data.UnclassifiedLiabilities, "Total Unclassified Liabilities");
column.Item().PaddingTop(3).LineHorizontal(1.5f);
column.Item().Row(r =>
{
r.RelativeItem(4).Text("TOTAL EQUITY AND LIABILITIES").Bold();
r.RelativeItem(1).AlignRight().Text(PdfFormatting.FormatAmount(data.TotalEquityAndLiabilities)).Bold();
});
});
});
}
}
}
@@ -0,0 +1,67 @@
using General_Ledger_Service.DTOs.Reports;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace General_Ledger_Service.Services.PdfDocuments
{
// 05_LKAS_Report_PDF_Templates.md Section 7 - not an LKAS statement, a management report.
// Flat table including a Period column, since a budget can span multiple periods (Section 7's
// own open question - grouping by period is a possible future enhancement, not required here).
public class BudgetVsActualPdfDocument(string companyName, string currencyCode, long budgetId, List<BudgetVsActualRowDto> rows) : IDocument
{
public DocumentMetadata GetMetadata() => DocumentMetadata.Default;
public void Compose(IDocumentContainer container)
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
ReportPageHeader.Compose(page, companyName, "Budget vs Actual Report", $"Budget ID: {budgetId}", currencyCode);
page.Content().PaddingTop(10).Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.RelativeColumn(2);
columns.RelativeColumn(4);
columns.RelativeColumn(1);
columns.RelativeColumn(2);
columns.RelativeColumn(2);
columns.RelativeColumn(2);
});
table.Header(header =>
{
header.Cell().Text("Account Code").Bold();
header.Cell().Text("Account Name").Bold();
header.Cell().AlignRight().Text("Period").Bold();
header.Cell().AlignRight().Text("Budgeted").Bold();
header.Cell().AlignRight().Text("Actual").Bold();
header.Cell().AlignRight().Text("Variance").Bold();
header.Cell().ColumnSpan(6).PaddingTop(3).LineHorizontal(1);
});
foreach (var row in rows.OrderBy(r => r.PeriodId).ThenBy(r => r.AccountCode))
{
table.Cell().Text(row.AccountCode);
table.Cell().Text(row.AccountName);
table.Cell().AlignRight().Text(row.PeriodId.ToString());
table.Cell().AlignRight().Text(PdfFormatting.FormatAmount(row.BudgetedAmount));
table.Cell().AlignRight().Text(PdfFormatting.FormatAmount(row.ActualAmount));
table.Cell().AlignRight().Text(PdfFormatting.FormatAmount(row.Variance));
}
table.Cell().ColumnSpan(6).PaddingTop(3).LineHorizontal(1);
table.Cell().Text("");
table.Cell().Text("Total").Bold();
table.Cell().Text("");
table.Cell().AlignRight().Text(PdfFormatting.FormatAmount(rows.Sum(r => r.BudgetedAmount))).Bold();
table.Cell().AlignRight().Text(PdfFormatting.FormatAmount(rows.Sum(r => r.ActualAmount))).Bold();
table.Cell().AlignRight().Text(PdfFormatting.FormatAmount(rows.Sum(r => r.Variance))).Bold();
});
});
}
}
}
@@ -0,0 +1,131 @@
using General_Ledger_Service.DTOs.Reports;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace General_Ledger_Service.Services.PdfDocuments
{
// 05_LKAS_Report_PDF_Templates.md Section 5 (LKAS 7, full statement). Retrofit 2026-07-22:
// no printed opening/closing cash balance (still in the Json/Csv data for the reconciliation
// check, just not displayed here, matching the reference template); nonCashAdjustments and
// workingCapitalChanges are combined and re-bucketed by sign at render time into "Additions
// to Cash"/"Subtractions From Cash" rather than kept as two separate lists. Working-capital
// lines now use the account's own Direction ("Increase"/"Decrease") for their wording, since
// that's no longer derivable from ChangeAmount's sign alone (see the DTO's comment).
public class CashFlowPdfDocument(string companyName, string currencyCode, CashFlowReportDto data) : IDocument
{
private record CashMovementLine(string Description, decimal Amount);
public DocumentMetadata GetMetadata() => DocumentMetadata.Default;
public void Compose(IDocumentContainer container)
{
var operating = data.OperatingActivities;
var operatingProfitBeforeWorkingCapital = operating.ProfitForPeriod + operating.NonCashAdjustments.Sum(a => a.Amount);
var movements = operating.NonCashAdjustments
.Select(a => new CashMovementLine(a.Description, a.Amount))
.Concat(operating.WorkingCapitalChanges.Select(c => new CashMovementLine($"{c.Direction} in {c.AccountName}", c.ChangeAmount)))
.ToList();
var additions = movements.Where(m => m.Amount > 0).ToList();
var subtractions = movements.Where(m => m.Amount <= 0).ToList();
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
ReportPageHeader.Compose(page, companyName, "Statement of Cash Flows", ReportPageHeader.RangePeriodLine(data.PeriodStart, data.PeriodEnd), currencyCode);
page.Content().PaddingTop(10).Column(column =>
{
column.Item().Text("Cash Flows from Operating Activities").Bold();
column.Item().Row(r =>
{
r.RelativeItem(4).Text("Profit for the period");
r.RelativeItem(1).AlignRight().Text(PdfFormatting.FormatAmount(operating.ProfitForPeriod));
});
column.Item().Row(r =>
{
r.RelativeItem(4).Text("Operating profit before working capital changes");
r.RelativeItem(1).AlignRight().Text(PdfFormatting.FormatAmount(operatingProfitBeforeWorkingCapital));
});
if (additions.Count > 0)
{
column.Item().PaddingTop(5).Text("Additions to Cash");
foreach (var line in additions)
{
column.Item().PaddingLeft(10).Row(r =>
{
r.RelativeItem(4).Text(line.Description);
r.RelativeItem(1).AlignRight().Text(PdfFormatting.FormatAmount(line.Amount));
});
}
}
if (subtractions.Count > 0)
{
column.Item().PaddingTop(5).Text("Subtractions From Cash");
foreach (var line in subtractions)
{
column.Item().PaddingLeft(10).Row(r =>
{
r.RelativeItem(4).Text(line.Description);
r.RelativeItem(1).AlignRight().Text(PdfFormatting.FormatAmount(line.Amount));
});
}
}
column.Item().PaddingTop(3).LineHorizontal(1);
column.Item().Row(r =>
{
r.RelativeItem(4).Text("Net Cash from Operating Activities").Bold();
r.RelativeItem(1).AlignRight().Text(PdfFormatting.FormatAmount(operating.NetCashFromOperatingActivities)).Bold();
});
column.Item().PaddingTop(15).Text("Cash Flows from Investing Activities").Bold();
foreach (var line in data.InvestingActivities.Lines)
{
column.Item().Row(r =>
{
r.RelativeItem(4).Text(line.AccountName);
r.RelativeItem(1).AlignRight().Text(PdfFormatting.FormatAmount(line.Amount));
});
}
column.Item().PaddingTop(3).LineHorizontal(1);
column.Item().Row(r =>
{
var label = data.InvestingActivities.NetCashFromInvestingActivities < 0 ? "Net Cash used in Investing Activities" : "Net Cash from Investing Activities";
r.RelativeItem(4).Text(label).Bold();
r.RelativeItem(1).AlignRight().Text(PdfFormatting.FormatAmount(data.InvestingActivities.NetCashFromInvestingActivities)).Bold();
});
column.Item().PaddingTop(15).Text("Cash Flows from Financing Activities").Bold();
foreach (var line in data.FinancingActivities.Lines)
{
column.Item().Row(r =>
{
r.RelativeItem(4).Text(line.AccountName);
r.RelativeItem(1).AlignRight().Text(PdfFormatting.FormatAmount(line.Amount));
});
}
column.Item().PaddingTop(3).LineHorizontal(1);
column.Item().Row(r =>
{
var label = data.FinancingActivities.NetCashFromFinancingActivities < 0 ? "Net Cash used in Financing Activities" : "Net Cash from Financing Activities";
r.RelativeItem(4).Text(label).Bold();
r.RelativeItem(1).AlignRight().Text(PdfFormatting.FormatAmount(data.FinancingActivities.NetCashFromFinancingActivities)).Bold();
});
column.Item().PaddingTop(15).LineHorizontal(1.5f);
column.Item().Row(r =>
{
var label = data.NetIncreaseDecreaseInCash < 0 ? "Net Decrease in Cash and Cash Equivalents" : "Net Increase in Cash and Cash Equivalents";
r.RelativeItem(4).Text(label).Bold();
r.RelativeItem(1).AlignRight().Text(PdfFormatting.FormatAmount(data.NetIncreaseDecreaseInCash)).Bold();
});
});
});
}
}
}
@@ -0,0 +1,69 @@
using General_Ledger_Service.DTOs.Reports;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace General_Ledger_Service.Services.PdfDocuments
{
// 05_LKAS_Report_PDF_Templates.md Section 6 - one continuous table (not separate boxed
// sections per account), account boundaries marked by an inline full-width group-header row.
// Response is already sorted by accountCode then entryDate - no re-sorting needed here.
public class GeneralLedgerPdfDocument(string companyName, string currencyCode, DateOnly periodStart, DateOnly periodEnd, List<GeneralLedgerLineDto> rows) : IDocument
{
public DocumentMetadata GetMetadata() => DocumentMetadata.Default;
public void Compose(IDocumentContainer container)
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
ReportPageHeader.Compose(page, companyName, "General Ledger", ReportPageHeader.RangePeriodLine(periodStart, periodEnd), currencyCode);
page.Content().PaddingTop(10).Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.RelativeColumn(2);
columns.RelativeColumn(4);
columns.RelativeColumn(3);
columns.RelativeColumn(2);
columns.RelativeColumn(2);
columns.RelativeColumn(2);
});
table.Header(header =>
{
header.Cell().Text("Date").Bold();
header.Cell().Text("Particulars").Bold();
header.Cell().Text("Description/Reference (J.R.)").Bold();
header.Cell().AlignRight().Text("Debit").Bold();
header.Cell().AlignRight().Text("Credit").Bold();
header.Cell().AlignRight().Text("Balance").Bold();
header.Cell().ColumnSpan(6).PaddingTop(3).LineHorizontal(1);
});
string? currentAccountCode = null;
foreach (var row in rows)
{
if (row.AccountCode != currentAccountCode)
{
currentAccountCode = row.AccountCode;
table.Cell().ColumnSpan(6).Background(Colors.Grey.Lighten3).Padding(3)
.Text($"Account: {row.AccountCode} — {row.AccountName}").Bold();
}
table.Cell().Text(row.EntryDate.ToString("yyyy-MM-dd"));
table.Cell().Text(row.Narration ?? string.Empty);
table.Cell().Text(row.JournalNo);
table.Cell().AlignRight().Text(PdfFormatting.FormatAmountOrBlank(row.DebitAmount));
table.Cell().AlignRight().Text(PdfFormatting.FormatAmountOrBlank(row.CreditAmount));
table.Cell().AlignRight().Text(PdfFormatting.FormatAmount(row.RunningBalance));
}
table.Cell().ColumnSpan(6).PaddingTop(3).LineHorizontal(1);
});
});
}
}
}
@@ -0,0 +1,83 @@
using General_Ledger_Service.DTOs.Reports;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace General_Ledger_Service.Services.PdfDocuments
{
// 05_LKAS_Report_PDF_Templates.md Section 8, redesigned 2026-07-22 - Accounting_System_Design_LKAS.md
// Section 11.7. A single Profit-Before-Tax-to-Balance-Tax-Payable computation (current tax
// only, no deferred tax) - the standard corporate income tax return shape, replacing the
// 2026-07-21 VAT/WHT/NBT per-tax_code summary entirely. Uses the fuller company identity
// block (address/TIN/BRN) since a tax computation conventionally carries those.
public class IncomeTaxComputationPdfDocument(string companyName, string companyAddress, string companyTin, string companyBrn, string currencyCode, IncomeTaxComputationDto data) : IDocument
{
public DocumentMetadata GetMetadata() => DocumentMetadata.Default;
public void Compose(IDocumentContainer container)
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
ReportPageHeader.ComposeWithEntityDetails(
page, companyName, companyAddress, companyTin, companyBrn,
"Income Tax Computation", ReportPageHeader.RangePeriodLine(data.PeriodStart, data.PeriodEnd), currencyCode);
page.Content().PaddingTop(10).Column(column =>
{
Line(column, "Profit Before Tax", data.ProfitBeforeTax);
Line(column, "Add: Non-Deductible Expenses", data.NonDeductibleExpenses);
Line(column, "Less: Allowable Deductions", -data.AllowableDeductions);
Total(column, "Adjusted Business Profit", data.AdjustedBusinessProfit);
Line(column, "Add: Other Taxable Income", data.OtherTaxableIncome);
Total(column, "Assessable Income", data.AssessableIncome);
Line(column, "Less: Qualifying Payments / Reliefs", -data.QualifyingPaymentsReliefs);
Total(column, "Taxable Income", data.TaxableIncome);
column.Item().PaddingTop(10).Row(r =>
{
r.RelativeItem(4).Text($"Corporate Income Tax @ {data.TaxRatePercent:0.00}%");
r.RelativeItem(1).AlignRight().Text(PdfFormatting.FormatAmount(data.CorporateIncomeTax));
});
Line(column, "Add: Surcharge / Education Levy", data.SurchargeAmount);
Total(column, "Gross Tax Liability", data.GrossTaxLiability);
Line(column, "Less: APIT Credit", -data.ApitCredit);
Line(column, "Less: WHT Credit", -data.WhtCredit);
Line(column, "Less: Quarterly Tax Payments", -data.QuarterlyTaxPayments);
column.Item().PaddingTop(10).LineHorizontal(1.5f);
column.Item().Row(r =>
{
var label = data.BalanceTaxPayable < 0 ? "BALANCE TAX REFUNDABLE" : "BALANCE TAX PAYABLE";
r.RelativeItem(4).Text(label).Bold();
r.RelativeItem(1).AlignRight().Text(PdfFormatting.FormatAmount(data.BalanceTaxPayable)).Bold();
});
});
});
}
private static void Line(ColumnDescriptor column, string label, decimal amount)
{
column.Item().Row(r =>
{
r.RelativeItem(4).Text(label);
r.RelativeItem(1).AlignRight().Text(PdfFormatting.FormatAmount(amount));
});
}
private static void Total(ColumnDescriptor column, string label, decimal amount)
{
column.Item().PaddingTop(3).LineHorizontal(1);
column.Item().Row(r =>
{
r.RelativeItem(4).Text(label).Bold();
r.RelativeItem(1).AlignRight().Text(PdfFormatting.FormatAmount(amount)).Bold();
});
column.Item().PaddingTop(5);
}
}
}
+22
View File
@@ -0,0 +1,22 @@
namespace General_Ledger_Service.Services.PdfDocuments
{
// Shared amount-formatting rules for every report PDF (05_LKAS_Report_PDF_Templates.md
// Section 9): thousands separator, 2 decimal places, round-half-up, negatives in
// parentheses. Same rounding rule as CLAUDE.md Rule 13's FormatCellValue.
public static class PdfFormatting
{
public static string FormatAmount(decimal value)
{
var rounded = Math.Round(value, 2, MidpointRounding.AwayFromZero);
return rounded < 0 ? $"({Math.Abs(rounded):N2})" : rounded.ToString("N2");
}
// Blank instead of "0.00" - Trial Balance's Debit/Credit split (Section 2).
public static string FormatAmountOrBlank(decimal value) =>
value == 0 ? string.Empty : FormatAmount(value);
// "—" instead of "0.00" - Tax Summary's Output/Input Tax columns (Section 8).
public static string FormatAmountOrDash(decimal value) =>
value == 0 ? "—" : FormatAmount(value);
}
}
@@ -0,0 +1,85 @@
using General_Ledger_Service.DTOs.Reports;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace General_Ledger_Service.Services.PdfDocuments
{
// 05_LKAS_Report_PDF_Templates.md Section 4 (LKAS 1). Retrofit 2026-07-22: restructured
// around accounts.income_statement_section - Sales/Cost of Sales -> Gross Profit, then
// Other Income and four expense groups -> Net Profit for the Period. Both GrossProfit and
// NetProfitForPeriod are already computed server-side (ReportService.BuildProfitAndLossReportAsync).
public class ProfitAndLossPdfDocument(string companyName, string currencyCode, ProfitAndLossReportDto data) : IDocument
{
public DocumentMetadata GetMetadata() => DocumentMetadata.Default;
public void Compose(IDocumentContainer container)
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
ReportPageHeader.Compose(page, companyName, "Statement of Profit or Loss", ReportPageHeader.RangePeriodLine(data.PeriodStart, data.PeriodEnd), currencyCode);
page.Content().PaddingTop(10).Column(column =>
{
ComposeSection(column, "Sales", data.Sales);
ComposeSection(column, "Cost of Sales", data.CostOfSales);
ComposeTotal(column, "GROSS PROFIT", data.GrossProfit, topRule: true);
ComposeSection(column, "Other Income", data.OtherIncome);
ComposeSection(column, "Distribution Expenses", data.DistributionExpenses);
ComposeSection(column, "Administration Expenses", data.AdministrationExpenses);
ComposeSection(column, "Other Expenses", data.OtherExpenses);
ComposeSection(column, "Financial Expenses", data.FinancialExpenses);
if (data.Unclassified.Lines.Count > 0)
{
ComposeSection(column, "Unclassified", data.Unclassified);
}
ComposeTotal(column, "NET PROFIT FOR THE PERIOD", data.NetProfitForPeriod, topRule: true, doubleRule: true);
});
});
}
private static void ComposeSection(ColumnDescriptor column, string title, ProfitAndLossSectionDto section)
{
if (section.Lines.Count == 0)
{
return;
}
column.Item().PaddingTop(10).Text(title).Bold();
foreach (var line in section.Lines)
{
column.Item().Row(r =>
{
r.RelativeItem(4).Text(line.AccountName);
r.RelativeItem(1).AlignRight().Text(PdfFormatting.FormatAmount(line.Amount));
});
}
column.Item().PaddingTop(3).LineHorizontal(1);
column.Item().Row(r =>
{
r.RelativeItem(4).Text($"Total {title}").Bold();
r.RelativeItem(1).AlignRight().Text(PdfFormatting.FormatAmount(section.Total)).Bold();
});
}
private static void ComposeTotal(ColumnDescriptor column, string label, decimal amount, bool topRule, bool doubleRule = false)
{
if (topRule)
{
column.Item().PaddingTop(10).LineHorizontal(doubleRule ? 1.5f : 1);
}
column.Item().Row(r =>
{
r.RelativeItem(4).Text(label).Bold();
r.RelativeItem(1).AlignRight().Text(PdfFormatting.FormatAmount(amount)).Bold();
});
}
}
}
+67
View File
@@ -0,0 +1,67 @@
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
namespace General_Ledger_Service.Services.PdfDocuments
{
// Shared header/footer block for every report PDF (05_LKAS_Report_PDF_Templates.md Section 1)
// - satisfies LKAS 1.51's minimum for every statement: entity name, report title, period
// covered, presentation currency. Applied to the non-LKAS reports too, for consistency.
public static class ReportPageHeader
{
public static void Compose(PageDescriptor page, string companyName, string reportTitle, string periodLine, string currencyCode)
{
page.Header().Column(column =>
{
column.Item().Text(companyName).FontSize(14).Bold();
column.Item().Text(reportTitle).FontSize(12).Bold();
column.Item().Text(periodLine).FontSize(10);
column.Item().Text($"(Amounts in {currencyCode})").FontSize(9).Italic();
column.Item().PaddingTop(5).LineHorizontal(1);
});
ComposeFooter(page);
}
// Fuller identity block for the Income Tax Computation report (02_Phase_By_Phase_Development_Plan.md
// Section 1.9) - a corporate tax computation conventionally carries the filer's address
// and tax reference numbers, which the other six reports have no need for.
public static void ComposeWithEntityDetails(PageDescriptor page, string companyName, string address, string tin, string brn, string reportTitle, string periodLine, string currencyCode)
{
page.Header().Column(column =>
{
column.Item().Text(companyName).FontSize(14).Bold();
column.Item().Text(address).FontSize(9);
column.Item().Text($"TIN: {tin} BRN: {brn}").FontSize(9);
column.Item().Text(reportTitle).FontSize(12).Bold();
column.Item().Text(periodLine).FontSize(10);
column.Item().Text($"(Amounts in {currencyCode})").FontSize(9).Italic();
column.Item().PaddingTop(5).LineHorizontal(1);
});
ComposeFooter(page);
}
private static void ComposeFooter(PageDescriptor page)
{
page.Footer().Row(row =>
{
row.RelativeItem().Text(text =>
{
text.Span("Generated ").FontSize(8);
text.Span(DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm") + " UTC").FontSize(8);
});
row.RelativeItem().AlignRight().Text(text =>
{
text.Span("Page ").FontSize(8);
text.CurrentPageNumber().FontSize(8);
text.Span(" of ").FontSize(8);
text.TotalPages().FontSize(8);
});
});
}
// Two period-line conventions (Section 1): point-in-time reports vs period reports.
public static string PointInTimePeriodLine(DateOnly asOfDate) => $"As at {asOfDate:yyyy-MM-dd}";
public static string RangePeriodLine(DateOnly periodStart, DateOnly periodEnd) => $"For the period {periodStart:yyyy-MM-dd} to {periodEnd:yyyy-MM-dd}";
}
}
@@ -0,0 +1,62 @@
using General_Ledger_Service.DTOs.Reports;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace General_Ledger_Service.Services.PdfDocuments
{
// 05_LKAS_Report_PDF_Templates.md Section 2 - not an LKAS statement, a working paper.
// Retrofit 2026-07-22: flat list (no depth/indentation) - portrait, matching the
// reference template's plain Account name | Debit | Credit table.
public class TrialBalancePdfDocument(string companyName, string currencyCode, DateOnly asOfDate, List<TrialBalanceRowDto> rows) : IDocument
{
public DocumentMetadata GetMetadata() => DocumentMetadata.Default;
public void Compose(IDocumentContainer container)
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
ReportPageHeader.Compose(page, companyName, "Trial Balance", ReportPageHeader.PointInTimePeriodLine(asOfDate), currencyCode);
page.Content().PaddingTop(10).Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.RelativeColumn(2);
columns.RelativeColumn(5);
columns.RelativeColumn(2);
columns.RelativeColumn(2);
});
table.Header(header =>
{
header.Cell().Text("Account Code").Bold();
header.Cell().Text("Account Name").Bold();
header.Cell().AlignRight().Text("Debit").Bold();
header.Cell().AlignRight().Text("Credit").Bold();
header.Cell().ColumnSpan(4).PaddingTop(3).LineHorizontal(1);
});
foreach (var row in rows)
{
table.Cell().Text(row.AccountCode);
table.Cell().Text(row.AccountName);
table.Cell().AlignRight().Text(PdfFormatting.FormatAmountOrBlank(row.Debit));
table.Cell().AlignRight().Text(PdfFormatting.FormatAmountOrBlank(row.Credit));
}
var totalDebit = rows.Sum(r => r.Debit);
var totalCredit = rows.Sum(r => r.Credit);
table.Cell().ColumnSpan(4).PaddingTop(3).LineHorizontal(1);
table.Cell().Text("");
table.Cell().Text("TOTAL").Bold();
table.Cell().AlignRight().Text(PdfFormatting.FormatAmount(totalDebit)).Bold();
table.Cell().AlignRight().Text(PdfFormatting.FormatAmount(totalCredit)).Bold();
});
});
}
}
}