@classytic/ledger 0.10.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridges/index.d.mts +1 -1
- package/dist/constants/index.d.mts +1 -1
- package/dist/constants/index.mjs +2 -2
- package/dist/{core-DwjkrRkJ.d.mts → core-B7uVjqGS.d.mts} +25 -0
- package/dist/country/index.d.mts +1 -1
- package/dist/events/index.d.mts +1 -1
- package/dist/exports/index.d.mts +1 -1
- package/dist/exports/index.mjs +1 -1
- package/dist/{fx-realization.plugin-Dzqzi3u0.mjs → fx-realization.plugin-DY3pPxIi.mjs} +70 -1
- package/dist/{index-ClLwzNRF.d.mts → index-BFPFihTF.d.mts} +8 -0
- package/dist/{index-08IpHhrU.d.mts → index-Dd7HknPP.d.mts} +1 -1
- package/dist/index.d.mts +120 -24
- package/dist/index.mjs +375 -165
- package/dist/{journals-DUpWwFt1.d.mts → journals-CTrAuzdk.d.mts} +1 -1
- package/dist/{partner-ledger-CR0geilx.mjs → partner-ledger-B0eym6Ss.mjs} +951 -213
- package/dist/plugins/index.d.mts +1 -1
- package/dist/plugins/index.mjs +1 -1
- package/dist/reports/index.d.mts +2 -2
- package/dist/reports/index.mjs +2 -2
- package/dist/{trial-balance-DyNm5bFu.d.mts → trial-balance-UXV2PN6x.d.mts} +280 -75
- package/package.json +8 -20
- package/dist/opening-balance-1cixYh6Y.mjs +0 -60
- package/dist/sync/index.d.mts +0 -324
- package/dist/sync/index.mjs +0 -530
- package/dist/sync-JvchM3FO.d.mts +0 -152
- /package/dist/{categories-FJlrvzcl.mjs → categories-CclX7Q94.mjs} +0 -0
- /package/dist/{currencies-Jo5oaM_4.mjs → currencies-OuPHPyS2.mjs} +0 -0
- /package/dist/{exports-C30yRapf.mjs → exports-B3whucXe.mjs} +0 -0
- /package/dist/{index-Bl0gP9lD.d.mts → index-DygMrab0.d.mts} +0 -0
- /package/dist/{index-J-XIbXH-.d.mts → index-pRW5cZhF.d.mts} +0 -0
- /package/dist/{outbox-store-BcCiHMPw.d.mts → outbox-store-CPLeocPg.d.mts} +0 -0
|
@@ -1,6 +1,123 @@
|
|
|
1
1
|
import { i as Errors } from "./errors-vXd932rB.mjs";
|
|
2
|
-
import { i as extractMainType } from "./categories-
|
|
2
|
+
import { i as extractMainType } from "./categories-CclX7Q94.mjs";
|
|
3
3
|
import mongoose from "mongoose";
|
|
4
|
+
//#region src/utils/period-columns.ts
|
|
5
|
+
const MONTH_LABELS = [
|
|
6
|
+
"Jan",
|
|
7
|
+
"Feb",
|
|
8
|
+
"Mar",
|
|
9
|
+
"Apr",
|
|
10
|
+
"May",
|
|
11
|
+
"Jun",
|
|
12
|
+
"Jul",
|
|
13
|
+
"Aug",
|
|
14
|
+
"Sep",
|
|
15
|
+
"Oct",
|
|
16
|
+
"Nov",
|
|
17
|
+
"Dec"
|
|
18
|
+
];
|
|
19
|
+
function isoDate(d) {
|
|
20
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
21
|
+
}
|
|
22
|
+
function endOfMonth(year, month) {
|
|
23
|
+
return new Date(year, month + 1, 0, 23, 59, 59, 999);
|
|
24
|
+
}
|
|
25
|
+
function maxDate(a, b) {
|
|
26
|
+
return a > b ? a : b;
|
|
27
|
+
}
|
|
28
|
+
function minDate(a, b) {
|
|
29
|
+
return a < b ? a : b;
|
|
30
|
+
}
|
|
31
|
+
function pushClampedPeriod(periods, column, rawStart, rawEnd, outerStart, outerEnd) {
|
|
32
|
+
const start = maxDate(rawStart, outerStart);
|
|
33
|
+
const end = minDate(rawEnd, outerEnd);
|
|
34
|
+
if (start > end) return;
|
|
35
|
+
periods.push({
|
|
36
|
+
column: {
|
|
37
|
+
...column,
|
|
38
|
+
startDate: isoDate(start),
|
|
39
|
+
endDate: isoDate(end)
|
|
40
|
+
},
|
|
41
|
+
start,
|
|
42
|
+
end
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function buildPeriodColumns(outerStart, outerEnd, comparative) {
|
|
46
|
+
if (!comparative) return [{
|
|
47
|
+
column: {
|
|
48
|
+
key: "total",
|
|
49
|
+
label: `${outerStart.toLocaleDateString("en-US", {
|
|
50
|
+
month: "short",
|
|
51
|
+
day: "numeric"
|
|
52
|
+
})} - ${outerEnd.toLocaleDateString("en-US", {
|
|
53
|
+
year: "numeric",
|
|
54
|
+
month: "short",
|
|
55
|
+
day: "numeric"
|
|
56
|
+
})}`,
|
|
57
|
+
startDate: isoDate(outerStart),
|
|
58
|
+
endDate: isoDate(outerEnd)
|
|
59
|
+
},
|
|
60
|
+
start: outerStart,
|
|
61
|
+
end: outerEnd
|
|
62
|
+
}];
|
|
63
|
+
const periods = [];
|
|
64
|
+
const startYear = outerStart.getFullYear();
|
|
65
|
+
const startMonth = outerStart.getMonth();
|
|
66
|
+
const endYear = outerEnd.getFullYear();
|
|
67
|
+
const endMonth = outerEnd.getMonth();
|
|
68
|
+
if (comparative === "monthly") {
|
|
69
|
+
let y = startYear;
|
|
70
|
+
let m = startMonth;
|
|
71
|
+
while (y < endYear || y === endYear && m <= endMonth) {
|
|
72
|
+
pushClampedPeriod(periods, {
|
|
73
|
+
key: `${y}-${String(m + 1).padStart(2, "0")}`,
|
|
74
|
+
label: `${MONTH_LABELS[m]} ${y}`
|
|
75
|
+
}, new Date(y, m, 1), endOfMonth(y, m), outerStart, outerEnd);
|
|
76
|
+
m += 1;
|
|
77
|
+
if (m === 12) {
|
|
78
|
+
m = 0;
|
|
79
|
+
y += 1;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
let y = startYear;
|
|
84
|
+
let q = Math.floor(startMonth / 3);
|
|
85
|
+
const endQ = Math.floor(endMonth / 3);
|
|
86
|
+
while (y < endYear || y === endYear && q <= endQ) {
|
|
87
|
+
pushClampedPeriod(periods, {
|
|
88
|
+
key: `${y}-Q${q + 1}`,
|
|
89
|
+
label: `Q${q + 1} ${y}`
|
|
90
|
+
}, new Date(y, q * 3, 1), endOfMonth(y, q * 3 + 2), outerStart, outerEnd);
|
|
91
|
+
q += 1;
|
|
92
|
+
if (q === 4) {
|
|
93
|
+
q = 0;
|
|
94
|
+
y += 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
periods.push({
|
|
99
|
+
column: {
|
|
100
|
+
key: "total",
|
|
101
|
+
label: `Total ${endYear === startYear ? endYear : `${startYear}-${endYear}`}`,
|
|
102
|
+
startDate: isoDate(outerStart),
|
|
103
|
+
endDate: isoDate(outerEnd),
|
|
104
|
+
isTotal: true
|
|
105
|
+
},
|
|
106
|
+
start: outerStart,
|
|
107
|
+
end: outerEnd
|
|
108
|
+
});
|
|
109
|
+
return periods;
|
|
110
|
+
}
|
|
111
|
+
function buildAgeBucketColumns(buckets, asOfDate) {
|
|
112
|
+
return buckets.map((bucket) => ({
|
|
113
|
+
key: bucket.label,
|
|
114
|
+
label: bucket.label,
|
|
115
|
+
startDate: "",
|
|
116
|
+
endDate: "",
|
|
117
|
+
isAgeBucket: true
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
4
121
|
//#region src/utils/tenant-guard.ts
|
|
5
122
|
/**
|
|
6
123
|
* Multi-tenant scope guard.
|
|
@@ -40,6 +157,7 @@ async function generateAgedBalance(opts, params) {
|
|
|
40
157
|
requireOrgScope(orgField, params.organizationId);
|
|
41
158
|
const asOfDate = params.asOfDate ?? /* @__PURE__ */ new Date();
|
|
42
159
|
const buckets = params.buckets ?? DEFAULT_BUCKETS;
|
|
160
|
+
const periods = buildAgeBucketColumns(buckets, asOfDate);
|
|
43
161
|
const bucketLabels = buckets.map((b) => b.label);
|
|
44
162
|
const dueDateField = params.dueDateField ?? "journalItems.dueDate";
|
|
45
163
|
const contactField = params.contactField;
|
|
@@ -61,7 +179,7 @@ async function generateAgedBalance(opts, params) {
|
|
|
61
179
|
asOfDate: asOfDate.toISOString().split("T")[0],
|
|
62
180
|
type: params.type
|
|
63
181
|
},
|
|
64
|
-
|
|
182
|
+
periods,
|
|
65
183
|
rows: [],
|
|
66
184
|
totals: Object.fromEntries(bucketLabels.map((l) => [l, 0])),
|
|
67
185
|
grandTotal: 0
|
|
@@ -114,18 +232,19 @@ async function generateAgedBalance(opts, params) {
|
|
|
114
232
|
accountCode: acc?.accountNumber ?? "",
|
|
115
233
|
...contactField ? { contactId: r._id.contact } : {},
|
|
116
234
|
total: 0,
|
|
117
|
-
|
|
235
|
+
amounts: Object.fromEntries(bucketLabels.map((l) => [l, 0]))
|
|
118
236
|
});
|
|
119
237
|
}
|
|
120
238
|
const row = rowMap.get(key);
|
|
121
|
-
if (row
|
|
239
|
+
if (!row) continue;
|
|
240
|
+
if (row.amounts[r._id.bucket] !== void 0) row.amounts[r._id.bucket] += r.amount;
|
|
122
241
|
row.total += r.amount;
|
|
123
242
|
}
|
|
124
243
|
const rows = Array.from(rowMap.values()).sort((a, b) => a.accountCode.localeCompare(b.accountCode, void 0, { numeric: true }));
|
|
125
244
|
const totals = Object.fromEntries(bucketLabels.map((l) => [l, 0]));
|
|
126
245
|
let grandTotal = 0;
|
|
127
246
|
for (const row of rows) {
|
|
128
|
-
for (const label of bucketLabels) totals[label] += row.
|
|
247
|
+
for (const label of bucketLabels) totals[label] += row.amounts[label];
|
|
129
248
|
grandTotal += row.total;
|
|
130
249
|
}
|
|
131
250
|
return {
|
|
@@ -134,7 +253,7 @@ async function generateAgedBalance(opts, params) {
|
|
|
134
253
|
asOfDate: asOfDate.toISOString().split("T")[0],
|
|
135
254
|
type: params.type
|
|
136
255
|
},
|
|
137
|
-
|
|
256
|
+
periods,
|
|
138
257
|
rows,
|
|
139
258
|
totals,
|
|
140
259
|
grandTotal
|
|
@@ -312,7 +431,8 @@ function buildItemFilters(filters) {
|
|
|
312
431
|
async function generateBalanceSheet(opts, params) {
|
|
313
432
|
const { AccountModel, JournalEntryModel, country, orgField, fiscalYearStartMonth = 1, retainedEarningsAccountCode = country.retainedEarningsAccountCode, retainedEarningsDisplayCode = country.retainedEarningsDisplayCode ?? retainedEarningsAccountCode, currentYearEarningsCode = country.currentYearEarningsCode ?? "3680" } = opts;
|
|
314
433
|
requireOrgScope(orgField, params.organizationId);
|
|
315
|
-
const { endDate } = getDateRange(params.dateOption, params.dateValue);
|
|
434
|
+
const { startDate, endDate } = getDateRange(params.dateOption, params.dateValue);
|
|
435
|
+
const periods = buildPeriodColumns(startDate, endDate, params.comparative ?? null);
|
|
316
436
|
const fiscalYearStart = getFiscalYearStart(endDate, fiscalYearStartMonth);
|
|
317
437
|
const itemFilters = buildItemFilters(params.filters);
|
|
318
438
|
const q = { active: true };
|
|
@@ -495,17 +615,7 @@ async function generateBalanceSheet(opts, params) {
|
|
|
495
615
|
liabilities.total = liabilities.groups.reduce((s, g) => s + g.total, 0);
|
|
496
616
|
equity.total = equity.groups.reduce((s, g) => s + g.total, 0);
|
|
497
617
|
const liabilitiesAndEquity = liabilities.total + equity.total;
|
|
498
|
-
|
|
499
|
-
metadata: {
|
|
500
|
-
businessName: params.businessName,
|
|
501
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
502
|
-
asOfDate: endDate.toISOString().split("T")[0],
|
|
503
|
-
displayDate: `As of ${endDate.toLocaleDateString("en-US", {
|
|
504
|
-
year: "numeric",
|
|
505
|
-
month: "long",
|
|
506
|
-
day: "numeric"
|
|
507
|
-
})}`
|
|
508
|
-
},
|
|
618
|
+
const currentSnapshot = {
|
|
509
619
|
assets,
|
|
510
620
|
liabilities,
|
|
511
621
|
equity,
|
|
@@ -518,6 +628,126 @@ async function generateBalanceSheet(opts, params) {
|
|
|
518
628
|
isBalanced: assets.total === liabilitiesAndEquity
|
|
519
629
|
}
|
|
520
630
|
};
|
|
631
|
+
const summaryByPeriod = {
|
|
632
|
+
totalAssets: {},
|
|
633
|
+
totalLiabilities: {},
|
|
634
|
+
totalEquity: {},
|
|
635
|
+
liabilitiesAndEquity: {},
|
|
636
|
+
difference: {},
|
|
637
|
+
isBalanced: {}
|
|
638
|
+
};
|
|
639
|
+
const snapshotSections = /* @__PURE__ */ new Map();
|
|
640
|
+
for (const period of periods) {
|
|
641
|
+
if (period.column.key === "total") {
|
|
642
|
+
summaryByPeriod.totalAssets[period.column.key] = currentSnapshot.summary.totalAssets;
|
|
643
|
+
summaryByPeriod.totalLiabilities[period.column.key] = currentSnapshot.summary.totalLiabilities;
|
|
644
|
+
summaryByPeriod.totalEquity[period.column.key] = currentSnapshot.summary.totalEquity;
|
|
645
|
+
summaryByPeriod.liabilitiesAndEquity[period.column.key] = currentSnapshot.summary.liabilitiesAndEquity;
|
|
646
|
+
summaryByPeriod.difference[period.column.key] = currentSnapshot.summary.difference;
|
|
647
|
+
summaryByPeriod.isBalanced[period.column.key] = currentSnapshot.summary.isBalanced;
|
|
648
|
+
snapshotSections.set(period.column.key, {
|
|
649
|
+
assetsSection: buildBalanceSheetSectionFromCategory("assets", period.column.key, assets),
|
|
650
|
+
liabilitiesSection: buildBalanceSheetSectionFromCategory("liabilities", period.column.key, liabilities),
|
|
651
|
+
equitySection: buildBalanceSheetSectionFromCategory("equity", period.column.key, equity)
|
|
652
|
+
});
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
const snapshot = await generateBalanceSheet(opts, {
|
|
656
|
+
...params,
|
|
657
|
+
dateOption: "custom",
|
|
658
|
+
dateValue: {
|
|
659
|
+
startDate: period.start,
|
|
660
|
+
endDate: period.end
|
|
661
|
+
},
|
|
662
|
+
comparative: null
|
|
663
|
+
});
|
|
664
|
+
summaryByPeriod.totalAssets[period.column.key] = snapshot.summaryByPeriod.totalAssets.total ?? 0;
|
|
665
|
+
summaryByPeriod.totalLiabilities[period.column.key] = snapshot.summaryByPeriod.totalLiabilities.total ?? 0;
|
|
666
|
+
summaryByPeriod.totalEquity[period.column.key] = snapshot.summaryByPeriod.totalEquity.total ?? 0;
|
|
667
|
+
summaryByPeriod.liabilitiesAndEquity[period.column.key] = snapshot.summaryByPeriod.liabilitiesAndEquity.total ?? 0;
|
|
668
|
+
summaryByPeriod.difference[period.column.key] = snapshot.summaryByPeriod.difference.total ?? 0;
|
|
669
|
+
summaryByPeriod.isBalanced[period.column.key] = snapshot.summaryByPeriod.isBalanced.total ?? false;
|
|
670
|
+
snapshotSections.set(period.column.key, {
|
|
671
|
+
assetsSection: snapshot.assetsSection,
|
|
672
|
+
liabilitiesSection: snapshot.liabilitiesSection,
|
|
673
|
+
equitySection: snapshot.equitySection
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
return {
|
|
677
|
+
metadata: {
|
|
678
|
+
businessName: params.businessName,
|
|
679
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
680
|
+
asOfDate: isoDate(endDate),
|
|
681
|
+
displayDate: `As of ${endDate.toLocaleDateString("en-US", {
|
|
682
|
+
year: "numeric",
|
|
683
|
+
month: "long",
|
|
684
|
+
day: "numeric"
|
|
685
|
+
})}`,
|
|
686
|
+
comparative: params.comparative ?? null,
|
|
687
|
+
labels: {
|
|
688
|
+
assets: labels.assets,
|
|
689
|
+
liabilities: labels.liabilities,
|
|
690
|
+
equity: labels.equity
|
|
691
|
+
}
|
|
692
|
+
},
|
|
693
|
+
periods: periods.map((p) => p.column),
|
|
694
|
+
summaryByPeriod,
|
|
695
|
+
assetsSection: mergeBalanceSheetSections("assetsSection", periods, snapshotSections),
|
|
696
|
+
liabilitiesSection: mergeBalanceSheetSections("liabilitiesSection", periods, snapshotSections),
|
|
697
|
+
equitySection: mergeBalanceSheetSections("equitySection", periods, snapshotSections)
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
function buildBalanceSheetSectionFromCategory(section, periodKey, category) {
|
|
701
|
+
const totals = { [periodKey]: category.total };
|
|
702
|
+
const lines = [];
|
|
703
|
+
for (const group of category.groups) for (const account of group.accounts) {
|
|
704
|
+
const accountId = String(account.id);
|
|
705
|
+
lines.push({
|
|
706
|
+
label: account.name,
|
|
707
|
+
code: account.code,
|
|
708
|
+
amounts: { [periodKey]: account.balance },
|
|
709
|
+
source: account.isCalculated ? {
|
|
710
|
+
kind: "calculated",
|
|
711
|
+
accountId,
|
|
712
|
+
group: group.name,
|
|
713
|
+
section: "equity"
|
|
714
|
+
} : {
|
|
715
|
+
kind: "account",
|
|
716
|
+
accountId,
|
|
717
|
+
group: group.name,
|
|
718
|
+
section
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
return {
|
|
723
|
+
totals,
|
|
724
|
+
lines: lines.sort((a, b) => a.code.localeCompare(b.code, void 0, { numeric: true }))
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
function mergeBalanceSheetSections(key, periods, snapshots) {
|
|
728
|
+
const totals = Object.fromEntries(periods.map((p) => [p.column.key, 0]));
|
|
729
|
+
const lines = /* @__PURE__ */ new Map();
|
|
730
|
+
for (const period of periods) {
|
|
731
|
+
const snapshot = snapshots.get(period.column.key);
|
|
732
|
+
if (!snapshot) continue;
|
|
733
|
+
const section = snapshot[key];
|
|
734
|
+
totals[period.column.key] = section.totals.total ?? section.totals[period.column.key] ?? 0;
|
|
735
|
+
for (const sourceLine of section.lines) {
|
|
736
|
+
const lineKey = "accountId" in sourceLine.source ? String(sourceLine.source.accountId) : `${sourceLine.code}:${sourceLine.label}`;
|
|
737
|
+
const line = lines.get(lineKey) ?? {
|
|
738
|
+
label: sourceLine.label,
|
|
739
|
+
code: sourceLine.code,
|
|
740
|
+
amounts: Object.fromEntries(periods.map((p) => [p.column.key, 0])),
|
|
741
|
+
source: sourceLine.source
|
|
742
|
+
};
|
|
743
|
+
line.amounts[period.column.key] = sourceLine.amounts.total ?? sourceLine.amounts[period.column.key] ?? 0;
|
|
744
|
+
lines.set(lineKey, line);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return {
|
|
748
|
+
totals,
|
|
749
|
+
lines: [...lines.values()].sort((a, b) => a.code.localeCompare(b.code, void 0, { numeric: true }))
|
|
750
|
+
};
|
|
521
751
|
}
|
|
522
752
|
//#endregion
|
|
523
753
|
//#region src/reports/budget-vs-actual.ts
|
|
@@ -626,29 +856,86 @@ async function generateBudgetVsActual(opts, params) {
|
|
|
626
856
|
}
|
|
627
857
|
//#endregion
|
|
628
858
|
//#region src/reports/cash-flow.ts
|
|
859
|
+
const TAG_DISPLAY_NAMES = {
|
|
860
|
+
depreciation: "Depreciation",
|
|
861
|
+
amortization: "Amortization",
|
|
862
|
+
impairment: "Impairment",
|
|
863
|
+
gain_on_disposal: "Gain on disposal",
|
|
864
|
+
loss_on_disposal: "Loss on disposal",
|
|
865
|
+
unrealized_fx: "Unrealized FX",
|
|
866
|
+
stock_based_compensation: "Stock-based compensation"
|
|
867
|
+
};
|
|
868
|
+
/**
|
|
869
|
+
* Country-pack convention: account codes 1111-1130 are cash & near-cash.
|
|
870
|
+
* The per-Account `isCashAccount: true` flag is the authoritative override.
|
|
871
|
+
*/
|
|
872
|
+
function isCashAccount(meta) {
|
|
873
|
+
if (meta.isCashAccount === true) return true;
|
|
874
|
+
return /^11(1[0-9]|2[0-9]|30)$/.test(meta.accountTypeCode ?? "");
|
|
875
|
+
}
|
|
876
|
+
function resolveSection(meta) {
|
|
877
|
+
if (meta.cashflowSection) return meta.cashflowSection;
|
|
878
|
+
if (!meta.isBalanceSheet) return "excluded";
|
|
879
|
+
if (meta.isCash) return "cash";
|
|
880
|
+
switch (meta.cashFlowCategory) {
|
|
881
|
+
case "Operating": return "operating";
|
|
882
|
+
case "Investing": return "investing";
|
|
883
|
+
case "Financing": return "financing";
|
|
884
|
+
default: return "excluded";
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
function tagDisplayName(tag) {
|
|
888
|
+
return TAG_DISPLAY_NAMES[tag] ?? tag.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
889
|
+
}
|
|
629
890
|
async function generateCashFlow(opts, params) {
|
|
630
891
|
const { AccountModel, JournalEntryModel, country, orgField } = opts;
|
|
631
892
|
requireOrgScope(orgField, params.organizationId);
|
|
632
|
-
const { startDate, endDate } = getDateRange(params.dateOption, params.dateValue);
|
|
893
|
+
const { startDate: outerStart, endDate: outerEnd } = getDateRange(params.dateOption, params.dateValue);
|
|
633
894
|
const itemFilters = buildItemFilters(params.filters);
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
const
|
|
638
|
-
const
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
const at = country.getAccountType(acc.accountTypeCode);
|
|
895
|
+
const periods = buildPeriodColumns(outerStart, outerEnd, params.comparative ?? null);
|
|
896
|
+
const accountQuery = { active: true };
|
|
897
|
+
if (orgField && params.organizationId) accountQuery[orgField] = params.organizationId;
|
|
898
|
+
const accounts = await AccountModel.find(accountQuery).lean();
|
|
899
|
+
const metas = /* @__PURE__ */ new Map();
|
|
900
|
+
for (const acc of accounts) {
|
|
901
|
+
const at = country.getAccountType(acc.accountTypeCode ?? "");
|
|
642
902
|
if (!at || at.isGroup || at.isTotal) continue;
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
const
|
|
646
|
-
|
|
903
|
+
const isBalanceSheet = at.category.startsWith("Balance Sheet-");
|
|
904
|
+
const isCash = isCashAccount(acc);
|
|
905
|
+
const meta = {
|
|
906
|
+
...acc,
|
|
647
907
|
category: at.category,
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
908
|
+
cashFlowCategory: at.cashFlowCategory ?? null,
|
|
909
|
+
nonCashAdjustmentTag: at.nonCashAdjustmentTag ?? null,
|
|
910
|
+
isBalanceSheet,
|
|
911
|
+
isCash,
|
|
912
|
+
section: "excluded"
|
|
913
|
+
};
|
|
914
|
+
meta.section = resolveSection(meta);
|
|
915
|
+
metas.set(String(acc._id), meta);
|
|
651
916
|
}
|
|
917
|
+
if (metas.size === 0) return emptyReport(periods, params);
|
|
918
|
+
const columnComputations = /* @__PURE__ */ new Map();
|
|
919
|
+
for (const period of periods) columnComputations.set(period.column.key, await computeColumn({
|
|
920
|
+
JournalEntryModel,
|
|
921
|
+
metas,
|
|
922
|
+
startDate: period.start,
|
|
923
|
+
endDate: period.end,
|
|
924
|
+
orgField,
|
|
925
|
+
organizationId: params.organizationId,
|
|
926
|
+
itemFilters
|
|
927
|
+
}));
|
|
928
|
+
return assembleReport({
|
|
929
|
+
periods: periods.map((p) => p.column),
|
|
930
|
+
columnComputations,
|
|
931
|
+
metas,
|
|
932
|
+
outerStart,
|
|
933
|
+
outerEnd,
|
|
934
|
+
params
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
async function computeColumn(args) {
|
|
938
|
+
const { JournalEntryModel, metas, startDate, endDate, orgField, organizationId, itemFilters } = args;
|
|
652
939
|
const baseMatch = {
|
|
653
940
|
state: "posted",
|
|
654
941
|
date: {
|
|
@@ -656,12 +943,13 @@ async function generateCashFlow(opts, params) {
|
|
|
656
943
|
$lte: endDate
|
|
657
944
|
}
|
|
658
945
|
};
|
|
659
|
-
if (orgField &&
|
|
660
|
-
const
|
|
946
|
+
if (orgField && organizationId) baseMatch[orgField] = organizationId;
|
|
947
|
+
const accountIds = [...metas.values()].map((m) => m._id);
|
|
948
|
+
const periodRows = await JournalEntryModel.aggregate([
|
|
661
949
|
{ $match: baseMatch },
|
|
662
950
|
{ $unwind: "$journalItems" },
|
|
663
951
|
{ $match: {
|
|
664
|
-
"journalItems.account": { $in:
|
|
952
|
+
"journalItems.account": { $in: accountIds },
|
|
665
953
|
...itemFilters
|
|
666
954
|
} },
|
|
667
955
|
{ $group: {
|
|
@@ -669,41 +957,261 @@ async function generateCashFlow(opts, params) {
|
|
|
669
957
|
d: { $sum: "$journalItems.debit" },
|
|
670
958
|
c: { $sum: "$journalItems.credit" }
|
|
671
959
|
} }
|
|
672
|
-
])
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
960
|
+
]);
|
|
961
|
+
const movements = /* @__PURE__ */ new Map();
|
|
962
|
+
for (const row of periodRows) movements.set(String(row._id), {
|
|
963
|
+
d: row.d,
|
|
964
|
+
c: row.c
|
|
965
|
+
});
|
|
966
|
+
let netIncome = 0;
|
|
967
|
+
for (const meta of metas.values()) {
|
|
968
|
+
if (meta.isBalanceSheet) continue;
|
|
969
|
+
const m = movements.get(String(meta._id));
|
|
970
|
+
if (!m) continue;
|
|
971
|
+
if (meta.category === "Income Statement-Income") netIncome += m.c - m.d;
|
|
972
|
+
else if (meta.category === "Income Statement-Expense") netIncome -= m.d - m.c;
|
|
973
|
+
}
|
|
974
|
+
const nonCashByTag = /* @__PURE__ */ new Map();
|
|
975
|
+
for (const meta of metas.values()) {
|
|
976
|
+
if (!meta.nonCashAdjustmentTag) continue;
|
|
977
|
+
const m = movements.get(String(meta._id));
|
|
978
|
+
if (!m) continue;
|
|
979
|
+
const addback = meta.category === "Income Statement-Expense" ? m.d - m.c : -(m.c - m.d);
|
|
980
|
+
const tag = meta.nonCashAdjustmentTag;
|
|
981
|
+
const bucket = nonCashByTag.get(tag) ?? {
|
|
982
|
+
amount: 0,
|
|
983
|
+
labels: []
|
|
984
|
+
};
|
|
985
|
+
bucket.amount += addback;
|
|
986
|
+
if (!bucket.labels.includes(meta.name ?? meta.accountTypeCode ?? "")) bucket.labels.push(meta.name ?? meta.accountTypeCode ?? "");
|
|
987
|
+
nonCashByTag.set(tag, bucket);
|
|
988
|
+
}
|
|
989
|
+
const bsMovements = /* @__PURE__ */ new Map();
|
|
990
|
+
for (const meta of metas.values()) {
|
|
991
|
+
if (!meta.isBalanceSheet) continue;
|
|
992
|
+
if (meta.section === "cash" || meta.section === "excluded") continue;
|
|
993
|
+
const m = movements.get(String(meta._id));
|
|
994
|
+
if (!m || m.d === 0 && m.c === 0) continue;
|
|
995
|
+
bsMovements.set(String(meta._id), {
|
|
996
|
+
section: meta.section,
|
|
997
|
+
cashEffect: -(m.d - m.c),
|
|
998
|
+
meta
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
const { openingCash, closingCash } = await computeCashBalances({
|
|
1002
|
+
JournalEntryModel,
|
|
1003
|
+
cashIds: [...metas.values()].filter((m) => m.isCash).map((m) => m._id),
|
|
1004
|
+
startDate,
|
|
1005
|
+
endDate,
|
|
1006
|
+
orgField,
|
|
1007
|
+
organizationId
|
|
1008
|
+
});
|
|
1009
|
+
return {
|
|
1010
|
+
netIncome,
|
|
1011
|
+
nonCashByTag,
|
|
1012
|
+
bsMovements,
|
|
1013
|
+
openingCash,
|
|
1014
|
+
closingCash,
|
|
1015
|
+
cashDelta: closingCash - openingCash
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
function assembleReport(args) {
|
|
1019
|
+
const { periods, columnComputations, metas, outerStart, outerEnd, params } = args;
|
|
1020
|
+
const tagsSeen = /* @__PURE__ */ new Set();
|
|
1021
|
+
const tagLabels = /* @__PURE__ */ new Map();
|
|
1022
|
+
const bsAccountIds = /* @__PURE__ */ new Set();
|
|
1023
|
+
for (const comp of columnComputations.values()) {
|
|
1024
|
+
for (const tag of comp.nonCashByTag.keys()) tagsSeen.add(tag);
|
|
1025
|
+
for (const [tag, b] of comp.nonCashByTag.entries()) {
|
|
1026
|
+
const label = b.labels.length === 1 ? b.labels[0] : `${tagDisplayName(tag)} (${b.labels.length} accounts)`;
|
|
1027
|
+
tagLabels.set(tag, label);
|
|
685
1028
|
}
|
|
1029
|
+
for (const id of comp.bsMovements.keys()) bsAccountIds.add(id);
|
|
1030
|
+
}
|
|
1031
|
+
const sortedTags = [...tagsSeen].sort((a, b) => tagDisplayName(a).localeCompare(tagDisplayName(b)));
|
|
1032
|
+
const sortedBs = [...bsAccountIds].map((id) => ({
|
|
1033
|
+
id,
|
|
1034
|
+
meta: metas.get(id)
|
|
1035
|
+
})).filter((x) => !!x.meta).sort((a, b) => {
|
|
1036
|
+
const codeA = a.meta.accountNumber ?? a.meta.accountTypeCode ?? "";
|
|
1037
|
+
const codeB = b.meta.accountNumber ?? b.meta.accountTypeCode ?? "";
|
|
1038
|
+
return codeA.localeCompare(codeB, void 0, { numeric: true });
|
|
1039
|
+
});
|
|
1040
|
+
const netIncomeAmounts = {};
|
|
1041
|
+
const nonCashAmounts = {};
|
|
1042
|
+
const bsAmounts = {};
|
|
1043
|
+
const fxByCol = {};
|
|
1044
|
+
const operatingTotals = {};
|
|
1045
|
+
const investingTotals = {};
|
|
1046
|
+
const financingTotals = {};
|
|
1047
|
+
const netCashByCol = {};
|
|
1048
|
+
const reconByCol = {};
|
|
1049
|
+
for (const tag of sortedTags) nonCashAmounts[tag] = {};
|
|
1050
|
+
for (const x of sortedBs) bsAmounts[x.id] = {};
|
|
1051
|
+
for (const col of periods) {
|
|
1052
|
+
const comp = columnComputations.get(col.key);
|
|
1053
|
+
if (!comp) continue;
|
|
1054
|
+
netIncomeAmounts[col.key] = comp.netIncome;
|
|
1055
|
+
fxByCol[col.key] = 0;
|
|
1056
|
+
let opTotal = comp.netIncome;
|
|
1057
|
+
let invTotal = 0;
|
|
1058
|
+
let finTotal = 0;
|
|
1059
|
+
for (const tag of sortedTags) {
|
|
1060
|
+
const amt = comp.nonCashByTag.get(tag)?.amount ?? 0;
|
|
1061
|
+
nonCashAmounts[tag][col.key] = amt;
|
|
1062
|
+
opTotal += amt;
|
|
1063
|
+
}
|
|
1064
|
+
for (const x of sortedBs) {
|
|
1065
|
+
const mv = comp.bsMovements.get(x.id);
|
|
1066
|
+
const amt = mv?.cashEffect ?? 0;
|
|
1067
|
+
bsAmounts[x.id][col.key] = amt;
|
|
1068
|
+
if (mv) {
|
|
1069
|
+
if (mv.section === "operating") opTotal += amt;
|
|
1070
|
+
else if (mv.section === "investing") invTotal += amt;
|
|
1071
|
+
else if (mv.section === "financing") finTotal += amt;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
operatingTotals[col.key] = opTotal;
|
|
1075
|
+
investingTotals[col.key] = invTotal;
|
|
1076
|
+
financingTotals[col.key] = finTotal;
|
|
1077
|
+
netCashByCol[col.key] = opTotal + invTotal + finTotal + fxByCol[col.key];
|
|
1078
|
+
const calculated = comp.openingCash + netCashByCol[col.key];
|
|
1079
|
+
reconByCol[col.key] = {
|
|
1080
|
+
openingCash: comp.openingCash,
|
|
1081
|
+
closingCash: comp.closingCash,
|
|
1082
|
+
calculated,
|
|
1083
|
+
tieOutOk: Math.abs(comp.closingCash - calculated) <= 1
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
const operating = {
|
|
1087
|
+
totals: operatingTotals,
|
|
1088
|
+
lines: [
|
|
1089
|
+
{
|
|
1090
|
+
label: "Net Income",
|
|
1091
|
+
code: "",
|
|
1092
|
+
amounts: { ...netIncomeAmounts },
|
|
1093
|
+
source: { kind: "netIncome" }
|
|
1094
|
+
},
|
|
1095
|
+
...sortedTags.map((tag) => ({
|
|
1096
|
+
label: tagLabels.get(tag) ?? tagDisplayName(tag),
|
|
1097
|
+
code: "",
|
|
1098
|
+
amounts: { ...nonCashAmounts[tag] },
|
|
1099
|
+
source: {
|
|
1100
|
+
kind: "nonCashAdjustment",
|
|
1101
|
+
tag
|
|
1102
|
+
}
|
|
1103
|
+
})),
|
|
1104
|
+
...sortedBs.filter((x) => x.meta.section === "operating").map((x) => ({
|
|
1105
|
+
label: x.meta.name ?? x.meta.accountTypeCode ?? "",
|
|
1106
|
+
code: x.meta.accountNumber ?? x.meta.accountTypeCode ?? "",
|
|
1107
|
+
amounts: { ...bsAmounts[x.id] },
|
|
1108
|
+
source: {
|
|
1109
|
+
kind: "workingCapital",
|
|
1110
|
+
accountId: x.id
|
|
1111
|
+
}
|
|
1112
|
+
}))
|
|
1113
|
+
]
|
|
686
1114
|
};
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
1115
|
+
const investing = {
|
|
1116
|
+
totals: investingTotals,
|
|
1117
|
+
lines: sortedBs.filter((x) => x.meta.section === "investing").map((x) => ({
|
|
1118
|
+
label: x.meta.name ?? x.meta.accountTypeCode ?? "",
|
|
1119
|
+
code: x.meta.accountNumber ?? x.meta.accountTypeCode ?? "",
|
|
1120
|
+
amounts: { ...bsAmounts[x.id] },
|
|
1121
|
+
source: {
|
|
1122
|
+
kind: "directMovement",
|
|
1123
|
+
accountId: x.id
|
|
1124
|
+
}
|
|
1125
|
+
}))
|
|
1126
|
+
};
|
|
1127
|
+
const financing = {
|
|
1128
|
+
totals: financingTotals,
|
|
1129
|
+
lines: sortedBs.filter((x) => x.meta.section === "financing").map((x) => ({
|
|
1130
|
+
label: x.meta.name ?? x.meta.accountTypeCode ?? "",
|
|
1131
|
+
code: x.meta.accountNumber ?? x.meta.accountTypeCode ?? "",
|
|
1132
|
+
amounts: { ...bsAmounts[x.id] },
|
|
1133
|
+
source: {
|
|
1134
|
+
kind: "directMovement",
|
|
1135
|
+
accountId: x.id
|
|
1136
|
+
}
|
|
1137
|
+
}))
|
|
1138
|
+
};
|
|
1139
|
+
const periodDisplay = `${outerStart.toLocaleDateString("en-US", {
|
|
1140
|
+
month: "short",
|
|
1141
|
+
day: "numeric"
|
|
1142
|
+
})} – ${outerEnd.toLocaleDateString("en-US", {
|
|
1143
|
+
year: "numeric",
|
|
1144
|
+
month: "short",
|
|
1145
|
+
day: "numeric"
|
|
1146
|
+
})}`;
|
|
1147
|
+
return {
|
|
1148
|
+
metadata: {
|
|
1149
|
+
businessName: params.businessName,
|
|
1150
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1151
|
+
periodStart: isoDate(outerStart),
|
|
1152
|
+
periodEnd: isoDate(outerEnd),
|
|
1153
|
+
displayPeriod: periodDisplay,
|
|
1154
|
+
...params.currency ? { currency: params.currency } : {},
|
|
1155
|
+
comparative: params.comparative ?? null
|
|
1156
|
+
},
|
|
1157
|
+
periods,
|
|
1158
|
+
operating,
|
|
1159
|
+
investing,
|
|
1160
|
+
financing,
|
|
1161
|
+
fxEffect: fxByCol,
|
|
1162
|
+
netCashFlow: netCashByCol,
|
|
1163
|
+
cashReconciliation: reconByCol
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
async function computeCashBalances(args) {
|
|
1167
|
+
const { JournalEntryModel, cashIds, startDate, endDate, orgField, organizationId } = args;
|
|
1168
|
+
if (cashIds.length === 0) return {
|
|
1169
|
+
openingCash: 0,
|
|
1170
|
+
closingCash: 0
|
|
1171
|
+
};
|
|
1172
|
+
const match = { state: "posted" };
|
|
1173
|
+
if (orgField && organizationId) match[orgField] = organizationId;
|
|
1174
|
+
const rows = await JournalEntryModel.aggregate([
|
|
1175
|
+
{ $match: match },
|
|
1176
|
+
{ $unwind: "$journalItems" },
|
|
1177
|
+
{ $match: { "journalItems.account": { $in: cashIds } } },
|
|
1178
|
+
{ $group: {
|
|
1179
|
+
_id: null,
|
|
1180
|
+
opening: { $sum: { $cond: [
|
|
1181
|
+
{ $lt: ["$date", startDate] },
|
|
1182
|
+
{ $subtract: ["$journalItems.debit", "$journalItems.credit"] },
|
|
1183
|
+
0
|
|
1184
|
+
] } },
|
|
1185
|
+
closing: { $sum: { $cond: [
|
|
1186
|
+
{ $lte: ["$date", endDate] },
|
|
1187
|
+
{ $subtract: ["$journalItems.debit", "$journalItems.credit"] },
|
|
1188
|
+
0
|
|
1189
|
+
] } }
|
|
1190
|
+
} }
|
|
1191
|
+
]);
|
|
1192
|
+
return {
|
|
1193
|
+
openingCash: rows[0]?.opening ?? 0,
|
|
1194
|
+
closingCash: rows[0]?.closing ?? 0
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
function emptyReport(periods, params) {
|
|
1198
|
+
const zeros = {};
|
|
1199
|
+
const recon = {};
|
|
1200
|
+
for (const p of periods) {
|
|
1201
|
+
zeros[p.column.key] = 0;
|
|
1202
|
+
recon[p.column.key] = {
|
|
1203
|
+
openingCash: 0,
|
|
1204
|
+
closingCash: 0,
|
|
1205
|
+
calculated: 0,
|
|
1206
|
+
tieOutOk: true
|
|
1207
|
+
};
|
|
700
1208
|
}
|
|
701
|
-
|
|
702
|
-
const
|
|
703
|
-
const periodDisplay = `${
|
|
1209
|
+
const outerStart = periods[0].start;
|
|
1210
|
+
const outerEnd = periods[periods.length - 1].end;
|
|
1211
|
+
const periodDisplay = `${outerStart.toLocaleDateString("en-US", {
|
|
704
1212
|
month: "short",
|
|
705
1213
|
day: "numeric"
|
|
706
|
-
})} – ${
|
|
1214
|
+
})} – ${outerEnd.toLocaleDateString("en-US", {
|
|
707
1215
|
year: "numeric",
|
|
708
1216
|
month: "short",
|
|
709
1217
|
day: "numeric"
|
|
@@ -712,14 +1220,115 @@ async function generateCashFlow(opts, params) {
|
|
|
712
1220
|
metadata: {
|
|
713
1221
|
businessName: params.businessName,
|
|
714
1222
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
715
|
-
periodStart:
|
|
716
|
-
periodEnd:
|
|
717
|
-
displayPeriod: periodDisplay
|
|
1223
|
+
periodStart: isoDate(outerStart),
|
|
1224
|
+
periodEnd: isoDate(outerEnd),
|
|
1225
|
+
displayPeriod: periodDisplay,
|
|
1226
|
+
...params.currency ? { currency: params.currency } : {},
|
|
1227
|
+
comparative: params.comparative ?? null
|
|
1228
|
+
},
|
|
1229
|
+
periods: periods.map((p) => p.column),
|
|
1230
|
+
operating: {
|
|
1231
|
+
totals: { ...zeros },
|
|
1232
|
+
lines: [{
|
|
1233
|
+
label: "Net Income",
|
|
1234
|
+
code: "",
|
|
1235
|
+
amounts: { ...zeros },
|
|
1236
|
+
source: { kind: "netIncome" }
|
|
1237
|
+
}]
|
|
718
1238
|
},
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
1239
|
+
investing: {
|
|
1240
|
+
totals: { ...zeros },
|
|
1241
|
+
lines: []
|
|
1242
|
+
},
|
|
1243
|
+
financing: {
|
|
1244
|
+
totals: { ...zeros },
|
|
1245
|
+
lines: []
|
|
1246
|
+
},
|
|
1247
|
+
fxEffect: { ...zeros },
|
|
1248
|
+
netCashFlow: { ...zeros },
|
|
1249
|
+
cashReconciliation: recon
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
//#endregion
|
|
1253
|
+
//#region src/reports/daybook.ts
|
|
1254
|
+
const DEFAULT_LIMIT = 5e3;
|
|
1255
|
+
const MAX_LIMIT = 5e4;
|
|
1256
|
+
async function generateDaybook(opts, params) {
|
|
1257
|
+
const { JournalEntryModel, orgField } = opts;
|
|
1258
|
+
const { startDate, endDate, state = "posted", accountId, journalType, partnerId, partnerField = "partnerId" } = params;
|
|
1259
|
+
requireOrgScope(orgField, params.organizationId);
|
|
1260
|
+
const limit = Math.min(params.limit ?? DEFAULT_LIMIT, MAX_LIMIT);
|
|
1261
|
+
const baseMatch = { date: {
|
|
1262
|
+
$gte: startDate,
|
|
1263
|
+
$lte: endDate
|
|
1264
|
+
} };
|
|
1265
|
+
if (state !== "all") baseMatch.state = state;
|
|
1266
|
+
if (orgField && params.organizationId) baseMatch[orgField] = params.organizationId;
|
|
1267
|
+
if (journalType) baseMatch.journalType = journalType;
|
|
1268
|
+
const itemMatch = {};
|
|
1269
|
+
if (accountId) itemMatch["journalItems.account"] = accountId;
|
|
1270
|
+
if (partnerId !== void 0 && partnerId !== null) itemMatch[`journalItems.${partnerField}`] = partnerId;
|
|
1271
|
+
const pipeline = [
|
|
1272
|
+
{ $match: baseMatch },
|
|
1273
|
+
{ $addFields: { journalItems: { $map: {
|
|
1274
|
+
input: { $range: [0, { $size: "$journalItems" }] },
|
|
1275
|
+
as: "idx",
|
|
1276
|
+
in: { $mergeObjects: [{ $arrayElemAt: ["$journalItems", "$$idx"] }, { _itemIndex: "$$idx" }] }
|
|
1277
|
+
} } } },
|
|
1278
|
+
{ $unwind: "$journalItems" },
|
|
1279
|
+
...Object.keys(itemMatch).length > 0 ? [{ $match: itemMatch }] : [],
|
|
1280
|
+
{ $project: {
|
|
1281
|
+
_id: 0,
|
|
1282
|
+
entryId: "$_id",
|
|
1283
|
+
itemIndex: "$journalItems._itemIndex",
|
|
1284
|
+
date: { $ifNull: ["$journalItems.date", "$date"] },
|
|
1285
|
+
referenceNumber: "$referenceNumber",
|
|
1286
|
+
journalType: "$journalType",
|
|
1287
|
+
entryLabel: "$label",
|
|
1288
|
+
itemLabel: "$journalItems.label",
|
|
1289
|
+
state: "$state",
|
|
1290
|
+
accountId: "$journalItems.account",
|
|
1291
|
+
debit: { $ifNull: ["$journalItems.debit", 0] },
|
|
1292
|
+
credit: { $ifNull: ["$journalItems.credit", 0] },
|
|
1293
|
+
partnerId: `$journalItems.${partnerField}`,
|
|
1294
|
+
matchingNumber: "$journalItems.matchingNumber"
|
|
1295
|
+
} },
|
|
1296
|
+
{ $sort: {
|
|
1297
|
+
date: 1,
|
|
1298
|
+
entryId: 1,
|
|
1299
|
+
itemIndex: 1
|
|
1300
|
+
} },
|
|
1301
|
+
{ $limit: limit + 1 }
|
|
1302
|
+
];
|
|
1303
|
+
const rows = await JournalEntryModel.aggregate(pipeline);
|
|
1304
|
+
const truncated = rows.length > limit;
|
|
1305
|
+
const lines = truncated ? rows.slice(0, limit) : rows;
|
|
1306
|
+
let totalDebit = 0;
|
|
1307
|
+
let totalCredit = 0;
|
|
1308
|
+
for (const r of lines) {
|
|
1309
|
+
totalDebit += r.debit ?? 0;
|
|
1310
|
+
totalCredit += r.credit ?? 0;
|
|
1311
|
+
}
|
|
1312
|
+
return {
|
|
1313
|
+
metadata: {
|
|
1314
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1315
|
+
period: {
|
|
1316
|
+
startDate: startDate.toISOString().split("T")[0],
|
|
1317
|
+
endDate: endDate.toISOString().split("T")[0]
|
|
1318
|
+
},
|
|
1319
|
+
state,
|
|
1320
|
+
filters: {
|
|
1321
|
+
...accountId ? { accountId } : {},
|
|
1322
|
+
...journalType ? { journalType } : {},
|
|
1323
|
+
...partnerId !== void 0 && partnerId !== null ? { partnerId } : {}
|
|
1324
|
+
},
|
|
1325
|
+
truncated,
|
|
1326
|
+
rowCount: lines.length
|
|
1327
|
+
},
|
|
1328
|
+
lines,
|
|
1329
|
+
totalDebit,
|
|
1330
|
+
totalCredit,
|
|
1331
|
+
netDelta: totalDebit - totalCredit
|
|
723
1332
|
};
|
|
724
1333
|
}
|
|
725
1334
|
//#endregion
|
|
@@ -993,6 +1602,7 @@ async function generateIncomeStatement(opts, params) {
|
|
|
993
1602
|
requireOrgScope(orgField, params.organizationId);
|
|
994
1603
|
const { startDate, endDate } = getDateRange(params.dateOption, params.dateValue);
|
|
995
1604
|
const itemFilters = buildItemFilters(params.filters);
|
|
1605
|
+
const periods = buildPeriodColumns(startDate, endDate, params.comparative ?? null);
|
|
996
1606
|
const q = { active: true };
|
|
997
1607
|
if (orgField && params.organizationId) q[orgField] = params.organizationId;
|
|
998
1608
|
const allAccounts = await AccountModel.find(q).lean();
|
|
@@ -1000,30 +1610,9 @@ async function generateIncomeStatement(opts, params) {
|
|
|
1000
1610
|
const at = country.getAccountType(a.accountTypeCode);
|
|
1001
1611
|
return at && !at.isGroup && !at.isTotal && at.category.startsWith("Income Statement");
|
|
1002
1612
|
}).map((a) => a._id);
|
|
1003
|
-
const baseMatch = {
|
|
1004
|
-
state: "posted",
|
|
1005
|
-
date: {
|
|
1006
|
-
$gte: startDate,
|
|
1007
|
-
$lte: endDate
|
|
1008
|
-
}
|
|
1009
|
-
};
|
|
1613
|
+
const baseMatch = { state: "posted" };
|
|
1010
1614
|
if (orgField && params.organizationId) baseMatch[orgField] = params.organizationId;
|
|
1011
|
-
const results = await JournalEntryModel.aggregate([
|
|
1012
|
-
{ $match: baseMatch },
|
|
1013
|
-
{ $unwind: "$journalItems" },
|
|
1014
|
-
{ $match: {
|
|
1015
|
-
"journalItems.account": { $in: isIds },
|
|
1016
|
-
...itemFilters
|
|
1017
|
-
} },
|
|
1018
|
-
{ $group: {
|
|
1019
|
-
_id: "$journalItems.account",
|
|
1020
|
-
d: { $sum: "$journalItems.debit" },
|
|
1021
|
-
c: { $sum: "$journalItems.credit" }
|
|
1022
|
-
} }
|
|
1023
|
-
]);
|
|
1024
1615
|
const accountMap = new Map(allAccounts.map((a) => [String(a._id), a]));
|
|
1025
|
-
const revenueGroups = {};
|
|
1026
|
-
const expenseGroups = {};
|
|
1027
1616
|
const resolveGroupName = (at) => {
|
|
1028
1617
|
const visited = /* @__PURE__ */ new Set();
|
|
1029
1618
|
let current = at.parentCode ? country.getAccountType(at.parentCode) : void 0;
|
|
@@ -1034,56 +1623,157 @@ async function generateIncomeStatement(opts, params) {
|
|
|
1034
1623
|
}
|
|
1035
1624
|
return at.name;
|
|
1036
1625
|
};
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1626
|
+
const cogsCode = country.cogsGroupCode;
|
|
1627
|
+
const isCogs = (name) => cogsCode ? name === cogsCode : name === "Cost of Sales" || name === "Cost of Goods Sold";
|
|
1628
|
+
const periodResults = /* @__PURE__ */ new Map();
|
|
1629
|
+
for (const period of periods) {
|
|
1630
|
+
const results = await JournalEntryModel.aggregate([
|
|
1631
|
+
{ $match: {
|
|
1632
|
+
...baseMatch,
|
|
1633
|
+
date: {
|
|
1634
|
+
$gte: period.start,
|
|
1635
|
+
$lte: period.end
|
|
1636
|
+
}
|
|
1637
|
+
} },
|
|
1638
|
+
{ $unwind: "$journalItems" },
|
|
1639
|
+
{ $match: {
|
|
1640
|
+
"journalItems.account": { $in: isIds },
|
|
1641
|
+
...itemFilters
|
|
1642
|
+
} },
|
|
1643
|
+
{ $group: {
|
|
1644
|
+
_id: "$journalItems.account",
|
|
1645
|
+
d: { $sum: "$journalItems.debit" },
|
|
1646
|
+
c: { $sum: "$journalItems.credit" }
|
|
1647
|
+
} }
|
|
1648
|
+
]);
|
|
1649
|
+
periodResults.set(period.column.key, results);
|
|
1059
1650
|
}
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1651
|
+
const revenueTotals = {};
|
|
1652
|
+
const expenseTotals = {};
|
|
1653
|
+
const costOfSalesByPeriod = {};
|
|
1654
|
+
const grossProfitByPeriod = {};
|
|
1655
|
+
const operatingIncomeByPeriod = {};
|
|
1656
|
+
const netIncomeByPeriod = {};
|
|
1657
|
+
const revenueLines = /* @__PURE__ */ new Map();
|
|
1658
|
+
const expenseLines = /* @__PURE__ */ new Map();
|
|
1659
|
+
for (const period of periods) {
|
|
1660
|
+
const revenuePeriodGroups = {};
|
|
1661
|
+
const expensePeriodGroups = {};
|
|
1662
|
+
for (const r of periodResults.get(period.column.key) ?? []) {
|
|
1663
|
+
const acc = accountMap.get(String(r._id));
|
|
1664
|
+
if (!acc) continue;
|
|
1665
|
+
const at = country.getAccountType(acc.accountTypeCode);
|
|
1666
|
+
if (!at) continue;
|
|
1667
|
+
const mainType = extractMainType(at.category);
|
|
1668
|
+
const netAmount = mainType === "Income" ? r.c - r.d : r.d - r.c;
|
|
1669
|
+
if (netAmount === 0) continue;
|
|
1670
|
+
const groupName = resolveGroupName(at);
|
|
1671
|
+
const targetGroups = mainType === "Income" ? revenuePeriodGroups : expensePeriodGroups;
|
|
1672
|
+
if (!(groupName in targetGroups)) targetGroups[groupName] = {
|
|
1673
|
+
name: groupName,
|
|
1674
|
+
total: 0,
|
|
1675
|
+
accounts: []
|
|
1676
|
+
};
|
|
1677
|
+
targetGroups[groupName].accounts.push({
|
|
1678
|
+
id: acc._id,
|
|
1679
|
+
name: acc.name ?? at.name,
|
|
1680
|
+
code: acc.accountNumber ?? at.code,
|
|
1681
|
+
balance: netAmount
|
|
1682
|
+
});
|
|
1683
|
+
targetGroups[groupName].total += netAmount;
|
|
1684
|
+
const lineMap = mainType === "Income" ? revenueLines : expenseLines;
|
|
1685
|
+
const key = String(acc._id);
|
|
1686
|
+
const line = lineMap.get(key) ?? {
|
|
1687
|
+
label: acc.name ?? at.name,
|
|
1688
|
+
code: acc.accountNumber ?? at.code,
|
|
1689
|
+
group: groupName,
|
|
1690
|
+
amounts: Object.fromEntries(periods.map((p) => [p.column.key, 0]))
|
|
1691
|
+
};
|
|
1692
|
+
line.amounts[period.column.key] = netAmount;
|
|
1693
|
+
lineMap.set(key, line);
|
|
1694
|
+
}
|
|
1695
|
+
const periodRevenue = Object.values(revenuePeriodGroups).reduce((s, g) => s + g.total, 0);
|
|
1696
|
+
const periodExpenses = Object.values(expensePeriodGroups).reduce((s, g) => s + g.total, 0);
|
|
1697
|
+
const periodCostOfSales = Object.values(expensePeriodGroups).filter((g) => isCogs(g.name)).reduce((s, g) => s + g.total, 0);
|
|
1698
|
+
const periodGrossProfit = periodRevenue - periodCostOfSales;
|
|
1699
|
+
const periodOperatingExpenses = Object.values(expensePeriodGroups).filter((g) => !isCogs(g.name)).reduce((s, g) => s + g.total, 0);
|
|
1700
|
+
revenueTotals[period.column.key] = periodRevenue;
|
|
1701
|
+
expenseTotals[period.column.key] = periodExpenses;
|
|
1702
|
+
costOfSalesByPeriod[period.column.key] = periodCostOfSales;
|
|
1703
|
+
grossProfitByPeriod[period.column.key] = periodGrossProfit;
|
|
1704
|
+
operatingIncomeByPeriod[period.column.key] = periodGrossProfit - periodOperatingExpenses;
|
|
1705
|
+
netIncomeByPeriod[period.column.key] = periodRevenue - periodExpenses;
|
|
1706
|
+
}
|
|
1707
|
+
const sortLines = (lines) => lines.sort((a, b) => a.code.localeCompare(b.code, void 0, { numeric: true }));
|
|
1708
|
+
const revenueSection = {
|
|
1709
|
+
totals: revenueTotals,
|
|
1710
|
+
lines: sortLines([...revenueLines.entries()].map(([accountId, line]) => ({
|
|
1711
|
+
label: line.label,
|
|
1712
|
+
code: line.code,
|
|
1713
|
+
amounts: line.amounts,
|
|
1714
|
+
source: {
|
|
1715
|
+
kind: "account",
|
|
1716
|
+
accountId,
|
|
1717
|
+
group: line.group,
|
|
1718
|
+
statementType: "revenue"
|
|
1719
|
+
}
|
|
1720
|
+
})))
|
|
1069
1721
|
};
|
|
1070
|
-
const
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1722
|
+
const expensesSection = {
|
|
1723
|
+
totals: expenseTotals,
|
|
1724
|
+
lines: sortLines([...expenseLines.entries()].map(([accountId, line]) => ({
|
|
1725
|
+
label: line.label,
|
|
1726
|
+
code: line.code,
|
|
1727
|
+
amounts: line.amounts,
|
|
1728
|
+
source: {
|
|
1729
|
+
kind: "account",
|
|
1730
|
+
accountId,
|
|
1731
|
+
group: line.group,
|
|
1732
|
+
statementType: "expense"
|
|
1733
|
+
}
|
|
1734
|
+
})))
|
|
1075
1735
|
};
|
|
1076
|
-
const
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1736
|
+
const summarySection = {
|
|
1737
|
+
totals: netIncomeByPeriod,
|
|
1738
|
+
lines: [
|
|
1739
|
+
{
|
|
1740
|
+
label: "Cost of Sales",
|
|
1741
|
+
code: "",
|
|
1742
|
+
amounts: costOfSalesByPeriod,
|
|
1743
|
+
source: {
|
|
1744
|
+
kind: "aggregate",
|
|
1745
|
+
name: "costOfSales"
|
|
1746
|
+
}
|
|
1747
|
+
},
|
|
1748
|
+
{
|
|
1749
|
+
label: "Gross Profit",
|
|
1750
|
+
code: "",
|
|
1751
|
+
amounts: grossProfitByPeriod,
|
|
1752
|
+
source: {
|
|
1753
|
+
kind: "aggregate",
|
|
1754
|
+
name: "grossProfit"
|
|
1755
|
+
}
|
|
1756
|
+
},
|
|
1757
|
+
{
|
|
1758
|
+
label: "Operating Income",
|
|
1759
|
+
code: "",
|
|
1760
|
+
amounts: operatingIncomeByPeriod,
|
|
1761
|
+
source: {
|
|
1762
|
+
kind: "aggregate",
|
|
1763
|
+
name: "operatingIncome"
|
|
1764
|
+
}
|
|
1765
|
+
},
|
|
1766
|
+
{
|
|
1767
|
+
label: "Net Income",
|
|
1768
|
+
code: "",
|
|
1769
|
+
amounts: netIncomeByPeriod,
|
|
1770
|
+
source: {
|
|
1771
|
+
kind: "aggregate",
|
|
1772
|
+
name: "netIncome"
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
]
|
|
1080
1776
|
};
|
|
1081
|
-
const cogsCode = country.cogsGroupCode;
|
|
1082
|
-
const isCogs = (name) => cogsCode ? name === cogsCode : name === "Cost of Sales" || name === "Cost of Goods Sold";
|
|
1083
|
-
const costOfSales = expenses.groups.find((g) => isCogs(g.name))?.total ?? 0;
|
|
1084
|
-
const grossProfit = revenue.total - costOfSales;
|
|
1085
|
-
const operatingIncome = grossProfit - expenses.groups.filter((g) => !isCogs(g.name)).reduce((s, g) => s + g.total, 0);
|
|
1086
|
-
const netIncome = revenue.total - expenses.total;
|
|
1087
1777
|
const periodDisplay = params.dateOption === "year" ? `For the year ended ${endDate.toLocaleDateString("en-US", {
|
|
1088
1778
|
year: "numeric",
|
|
1089
1779
|
month: "long",
|
|
@@ -1096,20 +1786,28 @@ async function generateIncomeStatement(opts, params) {
|
|
|
1096
1786
|
month: "short",
|
|
1097
1787
|
day: "numeric"
|
|
1098
1788
|
})}`;
|
|
1789
|
+
const labels = country.reportLabels ?? {};
|
|
1099
1790
|
return {
|
|
1100
1791
|
metadata: {
|
|
1101
1792
|
businessName: params.businessName,
|
|
1102
1793
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1103
|
-
periodStart: startDate
|
|
1104
|
-
periodEnd: endDate
|
|
1105
|
-
displayPeriod: periodDisplay
|
|
1794
|
+
periodStart: isoDate(startDate),
|
|
1795
|
+
periodEnd: isoDate(endDate),
|
|
1796
|
+
displayPeriod: periodDisplay,
|
|
1797
|
+
comparative: params.comparative ?? null,
|
|
1798
|
+
labels: {
|
|
1799
|
+
revenue: labels.revenue,
|
|
1800
|
+
expenses: labels.expenses
|
|
1801
|
+
}
|
|
1106
1802
|
},
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1803
|
+
periods: periods.map((p) => p.column),
|
|
1804
|
+
revenueSection,
|
|
1805
|
+
expensesSection,
|
|
1806
|
+
summarySection,
|
|
1807
|
+
costOfSalesByPeriod,
|
|
1808
|
+
grossProfitByPeriod,
|
|
1809
|
+
operatingIncomeByPeriod,
|
|
1810
|
+
netIncomeByPeriod
|
|
1113
1811
|
};
|
|
1114
1812
|
}
|
|
1115
1813
|
//#endregion
|
|
@@ -1316,7 +2014,7 @@ async function generateTrialBalance(opts, params) {
|
|
|
1316
2014
|
const { AccountModel, JournalEntryModel, country, orgField, fiscalYearStartMonth = 1 } = opts;
|
|
1317
2015
|
requireOrgScope(orgField, params.organizationId);
|
|
1318
2016
|
const { startDate, endDate } = getDateRange(params.dateOption, params.dateValue);
|
|
1319
|
-
const
|
|
2017
|
+
const periods = buildPeriodColumns(startDate, endDate, params.comparative ?? null);
|
|
1320
2018
|
const itemFilters = buildItemFilters(params.filters);
|
|
1321
2019
|
const accountQuery = { active: true };
|
|
1322
2020
|
if (orgField && params.organizationId) accountQuery[orgField] = params.organizationId;
|
|
@@ -1332,6 +2030,7 @@ async function generateTrialBalance(opts, params) {
|
|
|
1332
2030
|
const baseMatch = { state: "posted" };
|
|
1333
2031
|
if (orgField && params.organizationId) baseMatch[orgField] = params.organizationId;
|
|
1334
2032
|
const accountFilter = params.accountId ? { "journalItems.account": params.accountId } : {};
|
|
2033
|
+
const accountLookup = new Map(allAccounts.map((a) => [String(a._id), a]));
|
|
1335
2034
|
const buildPipeline = (ids, dateFrom, dateTo) => [
|
|
1336
2035
|
{ $match: {
|
|
1337
2036
|
...baseMatch,
|
|
@@ -1352,58 +2051,95 @@ async function generateTrialBalance(opts, params) {
|
|
|
1352
2051
|
c: { $sum: "$journalItems.credit" }
|
|
1353
2052
|
} }
|
|
1354
2053
|
];
|
|
1355
|
-
const
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
const
|
|
1361
|
-
|
|
1362
|
-
const
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
2054
|
+
const sortRows = (rows) => rows.sort((a, b) => {
|
|
2055
|
+
const codeA = a.account?.accountNumber ?? a.account?.accountTypeCode ?? "";
|
|
2056
|
+
const codeB = b.account?.accountNumber ?? b.account?.accountTypeCode ?? "";
|
|
2057
|
+
return codeA.localeCompare(codeB, void 0, { numeric: true });
|
|
2058
|
+
});
|
|
2059
|
+
const computeRowsForRange = async (rangeStart, rangeEnd) => {
|
|
2060
|
+
const fiscalYearStart = getFiscalYearStart(rangeStart, fiscalYearStartMonth);
|
|
2061
|
+
const [bsInitial, isInitial, current] = await Promise.all([
|
|
2062
|
+
bsIds.length ? JournalEntryModel.aggregate(buildPipeline(bsIds, /* @__PURE__ */ new Date(0), rangeStart)) : [],
|
|
2063
|
+
isIds.length ? JournalEntryModel.aggregate(buildPipeline(isIds, fiscalYearStart, rangeStart)) : [],
|
|
2064
|
+
JournalEntryModel.aggregate(buildPipeline([...bsIds, ...isIds], rangeStart, new Date(rangeEnd.getTime() + 1)))
|
|
2065
|
+
]);
|
|
2066
|
+
const map = /* @__PURE__ */ new Map();
|
|
2067
|
+
for (const r of [...bsInitial, ...isInitial]) {
|
|
2068
|
+
const key = String(r._id);
|
|
2069
|
+
map.set(key, {
|
|
2070
|
+
iD: r.d,
|
|
2071
|
+
iC: r.c,
|
|
2072
|
+
cD: 0,
|
|
2073
|
+
cC: 0
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
for (const r of current) {
|
|
2077
|
+
const key = String(r._id);
|
|
2078
|
+
const existing = map.get(key) ?? {
|
|
2079
|
+
iD: 0,
|
|
2080
|
+
iC: 0,
|
|
2081
|
+
cD: 0,
|
|
2082
|
+
cC: 0
|
|
2083
|
+
};
|
|
2084
|
+
existing.cD = r.d;
|
|
2085
|
+
existing.cC = r.c;
|
|
2086
|
+
map.set(key, existing);
|
|
2087
|
+
}
|
|
2088
|
+
const rows = [];
|
|
2089
|
+
for (const [id, bal] of map) {
|
|
2090
|
+
const acc = accountLookup.get(id);
|
|
2091
|
+
const net = bal.iD + bal.cD - (bal.iC + bal.cC);
|
|
2092
|
+
rows.push({
|
|
2093
|
+
account: acc ?? id,
|
|
2094
|
+
initial: {
|
|
2095
|
+
debit: bal.iD,
|
|
2096
|
+
credit: bal.iC
|
|
2097
|
+
},
|
|
2098
|
+
current: {
|
|
2099
|
+
debit: bal.cD,
|
|
2100
|
+
credit: bal.cC
|
|
2101
|
+
},
|
|
2102
|
+
ending: net >= 0 ? {
|
|
2103
|
+
debit: net,
|
|
2104
|
+
credit: 0
|
|
2105
|
+
} : {
|
|
2106
|
+
debit: 0,
|
|
2107
|
+
credit: Math.abs(net)
|
|
2108
|
+
}
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
return sortRows(rows);
|
|
2112
|
+
};
|
|
2113
|
+
const columnarRowsByAccount = /* @__PURE__ */ new Map();
|
|
2114
|
+
for (const period of periods) {
|
|
2115
|
+
const periodRows = await computeRowsForRange(period.start, period.end);
|
|
2116
|
+
for (const row of periodRows) {
|
|
2117
|
+
const id = String(row.account?._id ?? row.account);
|
|
2118
|
+
const columnar = columnarRowsByAccount.get(id) ?? {
|
|
2119
|
+
account: row.account,
|
|
2120
|
+
initial: {
|
|
2121
|
+
debit: Object.fromEntries(periods.map((p) => [p.column.key, 0])),
|
|
2122
|
+
credit: Object.fromEntries(periods.map((p) => [p.column.key, 0]))
|
|
2123
|
+
},
|
|
2124
|
+
current: {
|
|
2125
|
+
debit: Object.fromEntries(periods.map((p) => [p.column.key, 0])),
|
|
2126
|
+
credit: Object.fromEntries(periods.map((p) => [p.column.key, 0]))
|
|
2127
|
+
},
|
|
2128
|
+
ending: {
|
|
2129
|
+
debit: Object.fromEntries(periods.map((p) => [p.column.key, 0])),
|
|
2130
|
+
credit: Object.fromEntries(periods.map((p) => [p.column.key, 0]))
|
|
2131
|
+
}
|
|
2132
|
+
};
|
|
2133
|
+
columnar.initial.debit[period.column.key] = row.initial.debit;
|
|
2134
|
+
columnar.initial.credit[period.column.key] = row.initial.credit;
|
|
2135
|
+
columnar.current.debit[period.column.key] = row.current.debit;
|
|
2136
|
+
columnar.current.credit[period.column.key] = row.current.credit;
|
|
2137
|
+
columnar.ending.debit[period.column.key] = row.ending.debit;
|
|
2138
|
+
columnar.ending.credit[period.column.key] = row.ending.credit;
|
|
2139
|
+
columnarRowsByAccount.set(id, columnar);
|
|
2140
|
+
}
|
|
1405
2141
|
}
|
|
1406
|
-
|
|
2142
|
+
const columnarRows = [...columnarRowsByAccount.values()].sort((a, b) => {
|
|
1407
2143
|
const codeA = a.account?.accountNumber ?? a.account?.accountTypeCode ?? "";
|
|
1408
2144
|
const codeB = b.account?.accountNumber ?? b.account?.accountTypeCode ?? "";
|
|
1409
2145
|
return codeA.localeCompare(codeB, void 0, { numeric: true });
|
|
@@ -1424,15 +2160,17 @@ async function generateTrialBalance(opts, params) {
|
|
|
1424
2160
|
metadata: {
|
|
1425
2161
|
businessName: params.businessName,
|
|
1426
2162
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1427
|
-
periodStart: startDate
|
|
1428
|
-
periodEnd: endDate
|
|
1429
|
-
displayPeriod: periodDisplay
|
|
2163
|
+
periodStart: isoDate(startDate),
|
|
2164
|
+
periodEnd: isoDate(endDate),
|
|
2165
|
+
displayPeriod: periodDisplay,
|
|
2166
|
+
comparative: params.comparative ?? null
|
|
1430
2167
|
},
|
|
1431
|
-
rows,
|
|
1432
2168
|
period: {
|
|
1433
2169
|
startDate,
|
|
1434
2170
|
endDate
|
|
1435
|
-
}
|
|
2171
|
+
},
|
|
2172
|
+
periods: periods.map((p) => p.column),
|
|
2173
|
+
columnarRows
|
|
1436
2174
|
};
|
|
1437
2175
|
}
|
|
1438
2176
|
//#endregion
|
|
@@ -1598,12 +2336,12 @@ async function closeFiscalPeriod(opts, params) {
|
|
|
1598
2336
|
closingEntryId = closingEntry._id;
|
|
1599
2337
|
}
|
|
1600
2338
|
const closedAt = /* @__PURE__ */ new Date();
|
|
1601
|
-
await FiscalPeriodModel.findOneAndUpdate(periodQuery, {
|
|
2339
|
+
await FiscalPeriodModel.findOneAndUpdate(periodQuery, { $set: {
|
|
1602
2340
|
closed: true,
|
|
1603
2341
|
closedAt,
|
|
1604
2342
|
closedBy: closedBy ?? null,
|
|
1605
2343
|
closingEntryId
|
|
1606
|
-
}, queryOpts);
|
|
2344
|
+
} }, queryOpts);
|
|
1607
2345
|
const result = {
|
|
1608
2346
|
periodId,
|
|
1609
2347
|
netIncome,
|
|
@@ -1640,14 +2378,14 @@ async function reopenFiscalPeriod(opts, params) {
|
|
|
1640
2378
|
const closingEntryId = period.closingEntryId ?? null;
|
|
1641
2379
|
if (closingEntryId) await JournalEntryModel.findByIdAndDelete(closingEntryId, queryOpts);
|
|
1642
2380
|
const reopenedAt = /* @__PURE__ */ new Date();
|
|
1643
|
-
await FiscalPeriodModel.findOneAndUpdate(periodQuery, {
|
|
2381
|
+
await FiscalPeriodModel.findOneAndUpdate(periodQuery, { $set: {
|
|
1644
2382
|
closed: false,
|
|
1645
2383
|
closedAt: null,
|
|
1646
2384
|
closedBy: null,
|
|
1647
2385
|
closingEntryId: null,
|
|
1648
2386
|
reopenedAt,
|
|
1649
2387
|
reopenedBy: reopenedBy ?? null
|
|
1650
|
-
}, queryOpts);
|
|
2388
|
+
} }, queryOpts);
|
|
1651
2389
|
const result = {
|
|
1652
2390
|
periodId,
|
|
1653
2391
|
deletedEntryId: closingEntryId,
|
|
@@ -1797,4 +2535,4 @@ async function generatePartnerLedger(opts, params) {
|
|
|
1797
2535
|
};
|
|
1798
2536
|
}
|
|
1799
2537
|
//#endregion
|
|
1800
|
-
export {
|
|
2538
|
+
export { computeEndingBalance as C, requireOrgScope as D, generateAgedBalance as E, calculateTotal as S, DEFAULT_BUCKETS as T, generateBalanceSheet as _, finalizeSession as a, getFiscalYearStart as b, generateRevaluation as c, generateIncomeStatement as d, generateGeneralLedger as f, generateBudgetVsActual as g, generateCashFlow as h, acquireSession as i, buildRevaluationEntry as l, generateDaybook as m, closeFiscalPeriod as n, defaultLogger as o, generateDimensionBreakdown as p, reopenFiscalPeriod as r, generateTrialBalance as s, generatePartnerLedger as t, computeRevaluation as u, buildItemFilters as v, isVirtualTaxAccount as w, buildAccountTypeMap as x, getDateRange as y };
|