Distribution_GL Initialization
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user