@classytic/ledger 0.5.1 → 0.6.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/country/index.d.mts +2 -2
- package/dist/{errors-BmRjW38t.mjs → errors-CSDQPNyt.mjs} +1 -1
- package/dist/fx-realization.plugin-CgQFDGv2.mjs +459 -0
- package/dist/index-BthGypsI.d.mts +228 -0
- package/dist/index.d.mts +50 -105
- package/dist/index.mjs +570 -104
- package/dist/{fiscal-close-Dk3yRT9i.mjs → partner-ledger-D9H5hegI.mjs} +143 -6
- package/dist/plugins/index.d.mts +2 -24
- package/dist/plugins/index.mjs +2 -2
- package/dist/reports/index.d.mts +2 -2
- package/dist/reports/index.mjs +2 -2
- package/dist/tax-hooks-BnVenul5.d.mts +513 -0
- package/dist/{trial-balance-BZ7yOOFD.d.mts → trial-balance-s92GEvRR.d.mts} +75 -2
- package/package.json +6 -5
- package/dist/date-lock.plugin-B6WyvqNG.mjs +0 -254
- package/dist/idempotency.plugin-WcQLZU9n.d.mts +0 -98
- package/dist/index-GmfEFxVn.d.mts +0 -119
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { a as JOURNAL_CODES, c as getCustomJournalTypes, d as isValidJournalType, f as registerJournalType, i as isValidCurrency, l as getJournalType, n as getCurrency, o as JOURNAL_TYPES, r as getMinorUnit, s as _freezeJournalTypes, t as CURRENCIES, u as getJournalTypeCodes } from "./currencies-CsuBGfgs.mjs";
|
|
2
2
|
import { Money, add, allocate, format, formatPlain, fromDecimal, multiply, parseCents, percentage, splitTaxExclusive, splitTaxInclusive, subtract, toDecimal } from "./money.mjs";
|
|
3
|
-
import { n as Errors, t as AccountingError } from "./errors-
|
|
4
|
-
import { C as
|
|
3
|
+
import { n as Errors, t as AccountingError } from "./errors-CSDQPNyt.mjs";
|
|
4
|
+
import { C as isVirtualTaxAccount, E as requireOrgScope, S as computeEndingBalance, T as generateAgedBalance, _ as buildItemFilters, a as finalizeSession, b as buildAccountTypeMap, c as generateRevaluation, d as generateIncomeStatement, f as generateGeneralLedger, g as generateBalanceSheet, h as generateBudgetVsActual, i as acquireSession, l as buildRevaluationEntry, m as generateCashFlow, n as closeFiscalPeriod, o as defaultLogger, p as generateDimensionBreakdown, r as reopenFiscalPeriod, s as generateTrialBalance, t as generatePartnerLedger, u as computeRevaluation, v as getDateRange, w as DEFAULT_BUCKETS, x as calculateTotal, y as getFiscalYearStart } from "./partner-ledger-D9H5hegI.mjs";
|
|
5
5
|
import { c as getNormalBalance, d as isValidCategory, l as isBalanceSheet, n as CATEGORY_KEYS, t as CATEGORIES, u as isIncomeStatement } from "./categories-BkKdv16V.mjs";
|
|
6
|
-
import { i as
|
|
6
|
+
import { a as taxLockPlugin, c as createLockPlugin, i as fiscalLockPlugin, l as idempotencyPlugin, n as creditLimitPlugin, o as watermarkResolver, r as dailyLockPlugin, s as periodResolver, t as fxRealizationPlugin, u as doubleEntryPlugin } from "./fx-realization.plugin-CgQFDGv2.mjs";
|
|
7
7
|
import { defineCountryPack } from "./country/index.mjs";
|
|
8
8
|
import { a as exportToCsv, i as quickbooksFieldMap, r as universalFieldMap, t as flattenJournalEntries } from "./exports-BP-0Ni5W.mjs";
|
|
9
9
|
import mongoose, { Schema } from "mongoose";
|
|
@@ -273,6 +273,121 @@ function createFiscalPeriodSchema(config, options = {}) {
|
|
|
273
273
|
return schema;
|
|
274
274
|
}
|
|
275
275
|
//#endregion
|
|
276
|
+
//#region src/schemas/journal.schema.ts
|
|
277
|
+
/**
|
|
278
|
+
* Journal Schema Factory (0.6.0 — first-class Journal resource)
|
|
279
|
+
*
|
|
280
|
+
* A Journal is an organization-owned posting channel with its own
|
|
281
|
+
* reference-number sequence, permitted payment methods, optional default
|
|
282
|
+
* accounts, and source configuration for bank/statement imports.
|
|
283
|
+
*
|
|
284
|
+
* Journals are **optional** — a consumer that never seeds journals keeps
|
|
285
|
+
* the 0.5.x enum-only flow on journal entries. Consumers that do seed
|
|
286
|
+
* journals gain:
|
|
287
|
+
*
|
|
288
|
+
* - Per-journal reference-number prefix (e.g. `INV/2026/03/0042`)
|
|
289
|
+
* - Per-journal lock wiring (sale-lock on the sales journal, not globally)
|
|
290
|
+
* - Payment method restrictions
|
|
291
|
+
* - Bank-statement source binding for automated imports (0.7+)
|
|
292
|
+
*
|
|
293
|
+
* The schema is additive: existing journal entries without a `journal` ref
|
|
294
|
+
* keep working unchanged, and seed-from-pack is opt-in via
|
|
295
|
+
* `engine.repositories.journals.seedDefaults(orgId)`.
|
|
296
|
+
*/
|
|
297
|
+
function createJournalSchema(config, accountModelName, options = {}) {
|
|
298
|
+
const { multiTenant } = config;
|
|
299
|
+
const { indexes = true, extraFields = {}, extraIndexes = [] } = options;
|
|
300
|
+
const fields = {
|
|
301
|
+
code: {
|
|
302
|
+
type: String,
|
|
303
|
+
required: true,
|
|
304
|
+
trim: true
|
|
305
|
+
},
|
|
306
|
+
name: {
|
|
307
|
+
type: String,
|
|
308
|
+
required: true,
|
|
309
|
+
trim: true
|
|
310
|
+
},
|
|
311
|
+
journalType: {
|
|
312
|
+
type: String,
|
|
313
|
+
required: true
|
|
314
|
+
},
|
|
315
|
+
kind: {
|
|
316
|
+
type: String,
|
|
317
|
+
enum: [
|
|
318
|
+
"general",
|
|
319
|
+
"sale",
|
|
320
|
+
"purchase",
|
|
321
|
+
"bank",
|
|
322
|
+
"cash",
|
|
323
|
+
"misc"
|
|
324
|
+
],
|
|
325
|
+
default: "general",
|
|
326
|
+
required: true
|
|
327
|
+
},
|
|
328
|
+
sequencePrefix: {
|
|
329
|
+
type: String,
|
|
330
|
+
default: null
|
|
331
|
+
},
|
|
332
|
+
sequenceNextNum: {
|
|
333
|
+
type: Number,
|
|
334
|
+
default: 1,
|
|
335
|
+
min: 1
|
|
336
|
+
},
|
|
337
|
+
defaultDebitAccount: {
|
|
338
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
339
|
+
ref: accountModelName,
|
|
340
|
+
default: null
|
|
341
|
+
},
|
|
342
|
+
defaultCreditAccount: {
|
|
343
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
344
|
+
ref: accountModelName,
|
|
345
|
+
default: null
|
|
346
|
+
},
|
|
347
|
+
allowedPaymentMethods: {
|
|
348
|
+
type: [String],
|
|
349
|
+
default: []
|
|
350
|
+
},
|
|
351
|
+
source: {
|
|
352
|
+
type: String,
|
|
353
|
+
default: "manual"
|
|
354
|
+
},
|
|
355
|
+
active: {
|
|
356
|
+
type: Boolean,
|
|
357
|
+
default: true
|
|
358
|
+
},
|
|
359
|
+
...extraFields
|
|
360
|
+
};
|
|
361
|
+
if (multiTenant) fields[multiTenant.orgField] = {
|
|
362
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
363
|
+
ref: multiTenant.orgRef,
|
|
364
|
+
required: true
|
|
365
|
+
};
|
|
366
|
+
const schema = new mongoose.Schema(fields, { timestamps: true });
|
|
367
|
+
if (indexes) {
|
|
368
|
+
const org = multiTenant?.orgField;
|
|
369
|
+
if (org) {
|
|
370
|
+
schema.index({
|
|
371
|
+
[org]: 1,
|
|
372
|
+
code: 1
|
|
373
|
+
}, { unique: true });
|
|
374
|
+
schema.index({
|
|
375
|
+
[org]: 1,
|
|
376
|
+
kind: 1,
|
|
377
|
+
active: 1
|
|
378
|
+
});
|
|
379
|
+
} else {
|
|
380
|
+
schema.index({ code: 1 }, { unique: true });
|
|
381
|
+
schema.index({
|
|
382
|
+
kind: 1,
|
|
383
|
+
active: 1
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
for (const idx of extraIndexes) schema.index(idx.fields, idx.options);
|
|
388
|
+
return schema;
|
|
389
|
+
}
|
|
390
|
+
//#endregion
|
|
276
391
|
//#region src/schemas/journal-entry.schema.ts
|
|
277
392
|
/**
|
|
278
393
|
* Journal Entry Schema Factory
|
|
@@ -308,17 +423,19 @@ function createJournalEntrySchema(config, accountModelName, options = {}) {
|
|
|
308
423
|
message: "exchangeRate must be greater than zero when set, got {VALUE}"
|
|
309
424
|
}
|
|
310
425
|
};
|
|
426
|
+
const originalAmountValidator = {
|
|
427
|
+
validator: (v) => v === null || v === void 0 || Number.isInteger(v) && v >= 0,
|
|
428
|
+
message: "{PATH} must be a non-negative integer (cents), got {VALUE}"
|
|
429
|
+
};
|
|
311
430
|
currencyItemFields.originalDebit = {
|
|
312
431
|
type: Number,
|
|
313
432
|
default: null,
|
|
314
|
-
|
|
315
|
-
validate: amountValidator
|
|
433
|
+
validate: originalAmountValidator
|
|
316
434
|
};
|
|
317
435
|
currencyItemFields.originalCredit = {
|
|
318
436
|
type: Number,
|
|
319
437
|
default: null,
|
|
320
|
-
|
|
321
|
-
validate: amountValidator
|
|
438
|
+
validate: originalAmountValidator
|
|
322
439
|
};
|
|
323
440
|
}
|
|
324
441
|
const JournalItemSchema = new mongoose.Schema({
|
|
@@ -345,6 +462,14 @@ function createJournalEntrySchema(config, accountModelName, options = {}) {
|
|
|
345
462
|
type: [TaxDetailSchema],
|
|
346
463
|
default: []
|
|
347
464
|
},
|
|
465
|
+
matchingNumber: {
|
|
466
|
+
type: String,
|
|
467
|
+
default: null
|
|
468
|
+
},
|
|
469
|
+
maturityDate: {
|
|
470
|
+
type: Date,
|
|
471
|
+
default: null
|
|
472
|
+
},
|
|
348
473
|
...currencyItemFields,
|
|
349
474
|
...extraItemFields
|
|
350
475
|
}, { _id: false });
|
|
@@ -356,6 +481,10 @@ function createJournalEntrySchema(config, accountModelName, options = {}) {
|
|
|
356
481
|
default: JOURNAL_CODES.MISC,
|
|
357
482
|
required: true
|
|
358
483
|
},
|
|
484
|
+
journal: {
|
|
485
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
486
|
+
default: null
|
|
487
|
+
},
|
|
359
488
|
referenceNumber: { type: String },
|
|
360
489
|
label: { type: String },
|
|
361
490
|
date: {
|
|
@@ -572,6 +701,11 @@ function createJournalEntrySchema(config, accountModelName, options = {}) {
|
|
|
572
701
|
});
|
|
573
702
|
}
|
|
574
703
|
schema.index({ reversed: 1 });
|
|
704
|
+
if (org) schema.index({
|
|
705
|
+
[org]: 1,
|
|
706
|
+
"journalItems.matchingNumber": 1
|
|
707
|
+
});
|
|
708
|
+
else schema.index({ "journalItems.matchingNumber": 1 });
|
|
575
709
|
if (config.idempotency) {
|
|
576
710
|
const idempotencyIdx = {};
|
|
577
711
|
if (org) idempotencyIdx[org] = 1;
|
|
@@ -601,50 +735,112 @@ function createJournalEntrySchema(config, accountModelName, options = {}) {
|
|
|
601
735
|
//#endregion
|
|
602
736
|
//#region src/schemas/reconciliation.schema.ts
|
|
603
737
|
/**
|
|
604
|
-
* Reconciliation Schema Factory
|
|
738
|
+
* Reconciliation Schema Factory (0.6.0 — item-level open-item matching)
|
|
739
|
+
*
|
|
740
|
+
* A reconciliation is a **matching group** of journal items that together
|
|
741
|
+
* settle each other in whole or in part. Each group carries a stable
|
|
742
|
+
* `matchingNumber` string that is stamped onto every referenced journal
|
|
743
|
+
* item. A group is `isFullReconcile = true` iff debit/credit totals
|
|
744
|
+
* balance to zero — partial matches (a cheque paying 2 of 3 invoices) are
|
|
745
|
+
* represented by isFullReconcile=false.
|
|
605
746
|
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
747
|
+
* This replaces 0.5.x entry-level reconciliation, which could not represent
|
|
748
|
+
* the canonical AR/AP flow where one cheque covers multiple invoices or
|
|
749
|
+
* one invoice is paid by multiple cheques.
|
|
750
|
+
*
|
|
751
|
+
* A dedicated collection exists so that match/unmatch can atomically stamp
|
|
752
|
+
* `journalItems[i].matchingNumber`, update totals, and trigger the
|
|
753
|
+
* fxRealizationPlugin via mongokit hooks — without bumping the entries'
|
|
754
|
+
* `updatedAt` timestamps.
|
|
609
755
|
*/
|
|
610
756
|
function createReconciliationSchema(config, accountModelName, journalEntryModelName, options = {}) {
|
|
611
757
|
const { multiTenant } = config;
|
|
612
758
|
const { indexes = true, extraFields = {}, extraIndexes = [] } = options;
|
|
759
|
+
const MatchedItemRefSchema = new mongoose.Schema({
|
|
760
|
+
entry: {
|
|
761
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
762
|
+
ref: journalEntryModelName,
|
|
763
|
+
required: true
|
|
764
|
+
},
|
|
765
|
+
itemIndex: {
|
|
766
|
+
type: Number,
|
|
767
|
+
required: true,
|
|
768
|
+
min: 0,
|
|
769
|
+
validate: {
|
|
770
|
+
validator: Number.isInteger,
|
|
771
|
+
message: "itemIndex must be a non-negative integer"
|
|
772
|
+
}
|
|
773
|
+
},
|
|
774
|
+
debit: {
|
|
775
|
+
type: Number,
|
|
776
|
+
default: 0,
|
|
777
|
+
min: 0
|
|
778
|
+
},
|
|
779
|
+
credit: {
|
|
780
|
+
type: Number,
|
|
781
|
+
default: 0,
|
|
782
|
+
min: 0
|
|
783
|
+
},
|
|
784
|
+
amountCurrency: {
|
|
785
|
+
type: Number,
|
|
786
|
+
default: null
|
|
787
|
+
},
|
|
788
|
+
exchangeRate: {
|
|
789
|
+
type: Number,
|
|
790
|
+
default: null
|
|
791
|
+
}
|
|
792
|
+
}, { _id: false });
|
|
613
793
|
const fields = {
|
|
794
|
+
matchingNumber: {
|
|
795
|
+
type: String,
|
|
796
|
+
required: true
|
|
797
|
+
},
|
|
614
798
|
account: {
|
|
615
799
|
type: mongoose.Schema.Types.ObjectId,
|
|
616
800
|
ref: accountModelName,
|
|
617
801
|
required: true
|
|
618
802
|
},
|
|
619
|
-
|
|
620
|
-
type: [
|
|
621
|
-
type: mongoose.Schema.Types.ObjectId,
|
|
622
|
-
ref: journalEntryModelName
|
|
623
|
-
}],
|
|
803
|
+
items: {
|
|
804
|
+
type: [MatchedItemRefSchema],
|
|
624
805
|
required: true,
|
|
625
806
|
validate: {
|
|
626
|
-
validator: (v) => Array.isArray(v) && v.length
|
|
627
|
-
message: "
|
|
807
|
+
validator: (v) => Array.isArray(v) && v.length >= 2,
|
|
808
|
+
message: "a reconciliation must reference at least two items"
|
|
628
809
|
}
|
|
629
810
|
},
|
|
630
811
|
debitTotal: {
|
|
631
812
|
type: Number,
|
|
632
|
-
required: true
|
|
813
|
+
required: true,
|
|
814
|
+
min: 0
|
|
633
815
|
},
|
|
634
816
|
creditTotal: {
|
|
635
817
|
type: Number,
|
|
636
|
-
required: true
|
|
818
|
+
required: true,
|
|
819
|
+
min: 0
|
|
637
820
|
},
|
|
638
821
|
difference: {
|
|
639
822
|
type: Number,
|
|
640
823
|
default: 0
|
|
641
824
|
},
|
|
825
|
+
isFullReconcile: {
|
|
826
|
+
type: Boolean,
|
|
827
|
+
default: false
|
|
828
|
+
},
|
|
829
|
+
currency: {
|
|
830
|
+
type: String,
|
|
831
|
+
default: null
|
|
832
|
+
},
|
|
642
833
|
note: { type: String },
|
|
643
834
|
reconciledBy: { type: String },
|
|
644
835
|
reconciledAt: {
|
|
645
836
|
type: Date,
|
|
646
837
|
default: Date.now
|
|
647
838
|
},
|
|
839
|
+
fxRealizationEntry: {
|
|
840
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
841
|
+
ref: journalEntryModelName,
|
|
842
|
+
default: null
|
|
843
|
+
},
|
|
648
844
|
...extraFields
|
|
649
845
|
};
|
|
650
846
|
if (multiTenant) fields[multiTenant.orgField] = {
|
|
@@ -654,18 +850,31 @@ function createReconciliationSchema(config, accountModelName, journalEntryModelN
|
|
|
654
850
|
};
|
|
655
851
|
const schema = new mongoose.Schema(fields, { timestamps: true });
|
|
656
852
|
if (indexes) {
|
|
657
|
-
|
|
658
|
-
|
|
853
|
+
const org = multiTenant?.orgField;
|
|
854
|
+
if (org) {
|
|
855
|
+
schema.index({
|
|
856
|
+
[org]: 1,
|
|
857
|
+
matchingNumber: 1
|
|
858
|
+
}, { unique: true });
|
|
659
859
|
schema.index({
|
|
660
860
|
[org]: 1,
|
|
661
861
|
account: 1,
|
|
862
|
+
isFullReconcile: 1,
|
|
662
863
|
reconciledAt: 1
|
|
663
864
|
});
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
865
|
+
schema.index({
|
|
866
|
+
[org]: 1,
|
|
867
|
+
"items.entry": 1
|
|
868
|
+
});
|
|
869
|
+
} else {
|
|
870
|
+
schema.index({ matchingNumber: 1 }, { unique: true });
|
|
871
|
+
schema.index({
|
|
872
|
+
account: 1,
|
|
873
|
+
isFullReconcile: 1,
|
|
874
|
+
reconciledAt: 1
|
|
875
|
+
});
|
|
876
|
+
schema.index({ "items.entry": 1 });
|
|
877
|
+
}
|
|
669
878
|
}
|
|
670
879
|
for (const idx of extraIndexes) schema.index(idx.fields, idx.options);
|
|
671
880
|
return schema;
|
|
@@ -678,33 +887,29 @@ function resolveModelNames(overrides) {
|
|
|
678
887
|
journalEntry: overrides?.journalEntry ?? "JournalEntry",
|
|
679
888
|
fiscalPeriod: overrides?.fiscalPeriod ?? "FiscalPeriod",
|
|
680
889
|
budget: overrides?.budget ?? "Budget",
|
|
681
|
-
reconciliation: overrides?.reconciliation ?? "Reconciliation"
|
|
890
|
+
reconciliation: overrides?.reconciliation ?? "Reconciliation",
|
|
891
|
+
journal: overrides?.journal ?? "Journal"
|
|
682
892
|
};
|
|
683
893
|
}
|
|
684
|
-
/**
|
|
685
|
-
* Create (or reuse) all ledger models on the given connection.
|
|
686
|
-
*
|
|
687
|
-
* If a model with the same name is already registered on the connection,
|
|
688
|
-
* the existing model is reused — this allows multiple engine instances
|
|
689
|
-
* to share models and prevents "OverwriteModelError".
|
|
690
|
-
*/
|
|
691
894
|
function createModels(connection, config) {
|
|
692
895
|
const names = resolveModelNames(config.modelNames);
|
|
693
896
|
const so = config.schemaOptions ?? {};
|
|
694
897
|
const existing = connection.models;
|
|
695
|
-
if (existing[names.account] && existing[names.journalEntry] && existing[names.fiscalPeriod] && existing[names.budget] && existing[names.reconciliation]) return {
|
|
898
|
+
if (existing[names.account] && existing[names.journalEntry] && existing[names.fiscalPeriod] && existing[names.budget] && existing[names.reconciliation] && existing[names.journal]) return {
|
|
696
899
|
Account: existing[names.account],
|
|
697
900
|
JournalEntry: existing[names.journalEntry],
|
|
698
901
|
FiscalPeriod: existing[names.fiscalPeriod],
|
|
699
902
|
Budget: existing[names.budget],
|
|
700
|
-
Reconciliation: existing[names.reconciliation]
|
|
903
|
+
Reconciliation: existing[names.reconciliation],
|
|
904
|
+
Journal: existing[names.journal]
|
|
701
905
|
};
|
|
702
906
|
return {
|
|
703
907
|
Account: existing[names.account] ?? connection.model(names.account, createAccountSchema(config, so.account)),
|
|
704
908
|
JournalEntry: existing[names.journalEntry] ?? connection.model(names.journalEntry, createJournalEntrySchema(config, names.account, so.journalEntry)),
|
|
705
909
|
FiscalPeriod: existing[names.fiscalPeriod] ?? connection.model(names.fiscalPeriod, createFiscalPeriodSchema(config, so.fiscalPeriod)),
|
|
706
910
|
Budget: existing[names.budget] ?? connection.model(names.budget, createBudgetSchema(config, so.budget)),
|
|
707
|
-
Reconciliation: existing[names.reconciliation] ?? connection.model(names.reconciliation, createReconciliationSchema(config, names.account, names.journalEntry, so.reconciliation))
|
|
911
|
+
Reconciliation: existing[names.reconciliation] ?? connection.model(names.reconciliation, createReconciliationSchema(config, names.account, names.journalEntry, so.reconciliation)),
|
|
912
|
+
Journal: existing[names.journal] ?? connection.model(names.journal, createJournalSchema(config, names.account, so.journal))
|
|
708
913
|
};
|
|
709
914
|
}
|
|
710
915
|
//#endregion
|
|
@@ -901,6 +1106,104 @@ function wireAccountMethods(repository, AccountModel, country, orgField) {
|
|
|
901
1106
|
return repository;
|
|
902
1107
|
}
|
|
903
1108
|
//#endregion
|
|
1109
|
+
//#region src/repositories/journal.repository.ts
|
|
1110
|
+
/**
|
|
1111
|
+
* Lean default set used when a country pack doesn't provide
|
|
1112
|
+
* `journalTemplates`. Covers the Stripe/QuickBooks/Xero baseline.
|
|
1113
|
+
*/
|
|
1114
|
+
const DEFAULT_TEMPLATES = [
|
|
1115
|
+
{
|
|
1116
|
+
code: "SALES",
|
|
1117
|
+
name: "Sales",
|
|
1118
|
+
journalType: "SALES",
|
|
1119
|
+
kind: "sale",
|
|
1120
|
+
sequencePrefix: "INV"
|
|
1121
|
+
},
|
|
1122
|
+
{
|
|
1123
|
+
code: "PURCHASE",
|
|
1124
|
+
name: "Purchases",
|
|
1125
|
+
journalType: "PURCHASES",
|
|
1126
|
+
kind: "purchase",
|
|
1127
|
+
sequencePrefix: "BILL"
|
|
1128
|
+
},
|
|
1129
|
+
{
|
|
1130
|
+
code: "BANK",
|
|
1131
|
+
name: "Bank",
|
|
1132
|
+
journalType: "CASH_RECEIPTS",
|
|
1133
|
+
kind: "bank",
|
|
1134
|
+
sequencePrefix: "BNK"
|
|
1135
|
+
},
|
|
1136
|
+
{
|
|
1137
|
+
code: "CASH",
|
|
1138
|
+
name: "Cash",
|
|
1139
|
+
journalType: "CASH_PAYMENTS",
|
|
1140
|
+
kind: "cash",
|
|
1141
|
+
sequencePrefix: "CSH"
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
code: "MISC",
|
|
1145
|
+
name: "Miscellaneous",
|
|
1146
|
+
journalType: "MISC",
|
|
1147
|
+
kind: "general",
|
|
1148
|
+
sequencePrefix: "JE"
|
|
1149
|
+
}
|
|
1150
|
+
];
|
|
1151
|
+
function wireJournalMethods(repository, country, orgField) {
|
|
1152
|
+
const create = repository.create.bind(repository);
|
|
1153
|
+
const exists = repository.exists.bind(repository);
|
|
1154
|
+
repository.seedDefaults = async (orgId) => {
|
|
1155
|
+
requireOrgScope(orgField, orgId);
|
|
1156
|
+
const templates = country.journalTemplates ?? DEFAULT_TEMPLATES;
|
|
1157
|
+
let created = 0;
|
|
1158
|
+
let skipped = 0;
|
|
1159
|
+
for (const tpl of templates) {
|
|
1160
|
+
const existingQuery = { code: tpl.code };
|
|
1161
|
+
if (orgField && orgId != null) existingQuery[orgField] = orgId;
|
|
1162
|
+
if (await exists(existingQuery)) {
|
|
1163
|
+
skipped += 1;
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
const data = {
|
|
1167
|
+
code: tpl.code,
|
|
1168
|
+
name: tpl.name,
|
|
1169
|
+
journalType: tpl.journalType,
|
|
1170
|
+
kind: tpl.kind ?? "general",
|
|
1171
|
+
sequencePrefix: tpl.sequencePrefix ?? tpl.code,
|
|
1172
|
+
sequenceNextNum: tpl.sequenceStartNum ?? 1,
|
|
1173
|
+
active: true
|
|
1174
|
+
};
|
|
1175
|
+
if (orgField && orgId != null) data[orgField] = orgId;
|
|
1176
|
+
await create(data);
|
|
1177
|
+
created += 1;
|
|
1178
|
+
}
|
|
1179
|
+
return {
|
|
1180
|
+
created,
|
|
1181
|
+
skipped
|
|
1182
|
+
};
|
|
1183
|
+
};
|
|
1184
|
+
repository.nextSequenceNumber = async (journalId, orgId) => {
|
|
1185
|
+
requireOrgScope(orgField, orgId);
|
|
1186
|
+
const query = { _id: journalId };
|
|
1187
|
+
if (orgField && orgId != null) query[orgField] = orgId;
|
|
1188
|
+
const updated = await repository._executeQuery(async (Model) => Model.findOneAndUpdate(query, { $inc: { sequenceNextNum: 1 } }, { returnDocument: "after" }).lean());
|
|
1189
|
+
if (!updated) throw Errors.notFound(`Journal ${String(journalId)} not found`);
|
|
1190
|
+
const next = (updated.sequenceNextNum ?? 1) - 1;
|
|
1191
|
+
const prefix = updated.sequencePrefix ?? updated.code ?? "JE";
|
|
1192
|
+
const now = /* @__PURE__ */ new Date();
|
|
1193
|
+
return `${prefix}/${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, "0")}/${String(next).padStart(4, "0")}`;
|
|
1194
|
+
};
|
|
1195
|
+
if (typeof repository.registerMethod === "function") for (const name of ["seedDefaults", "nextSequenceNumber"]) {
|
|
1196
|
+
const fn = repository[name];
|
|
1197
|
+
try {
|
|
1198
|
+
delete repository[name];
|
|
1199
|
+
repository.registerMethod(name, fn);
|
|
1200
|
+
} catch {
|
|
1201
|
+
repository[name] = fn;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
return repository;
|
|
1205
|
+
}
|
|
1206
|
+
//#endregion
|
|
904
1207
|
//#region src/repositories/journal-entry.repository.ts
|
|
905
1208
|
/** Keys that are either handled explicitly or must not be copied */
|
|
906
1209
|
const ITEM_CORE_KEYS = new Set([
|
|
@@ -1204,91 +1507,186 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1204
1507
|
//#endregion
|
|
1205
1508
|
//#region src/repositories/reconciliation.repository.ts
|
|
1206
1509
|
/**
|
|
1207
|
-
*
|
|
1208
|
-
*
|
|
1209
|
-
*
|
|
1210
|
-
*
|
|
1211
|
-
* - Cross-repo reads (JournalEntryModel) use direct Model access (acceptable)
|
|
1510
|
+
* Default matching-number generator — atomic counter stored in the
|
|
1511
|
+
* reconciliation collection. Uses a dedicated sentinel document keyed
|
|
1512
|
+
* `{ matchingNumber: '__counter__', [org]: orgId }` so each org has its
|
|
1513
|
+
* own counter, safe under concurrent match calls.
|
|
1212
1514
|
*/
|
|
1213
|
-
function
|
|
1515
|
+
async function nextMatchingNumber(ReconciliationModel, orgField, orgId, session) {
|
|
1516
|
+
const counterQuery = { matchingNumber: "__counter__" };
|
|
1517
|
+
if (orgField && orgId != null) counterQuery[orgField] = orgId;
|
|
1518
|
+
const seq = (await ReconciliationModel.findOneAndUpdate(counterQuery, {
|
|
1519
|
+
$inc: { seq: 1 },
|
|
1520
|
+
$setOnInsert: {
|
|
1521
|
+
account: null,
|
|
1522
|
+
items: [{
|
|
1523
|
+
entry: null,
|
|
1524
|
+
itemIndex: 0
|
|
1525
|
+
}, {
|
|
1526
|
+
entry: null,
|
|
1527
|
+
itemIndex: 1
|
|
1528
|
+
}],
|
|
1529
|
+
debitTotal: 0,
|
|
1530
|
+
creditTotal: 0
|
|
1531
|
+
}
|
|
1532
|
+
}, {
|
|
1533
|
+
new: true,
|
|
1534
|
+
upsert: true,
|
|
1535
|
+
session,
|
|
1536
|
+
strict: false
|
|
1537
|
+
}).lean())?.seq ?? 1;
|
|
1538
|
+
return `RECN-${String(seq).padStart(6, "0")}`;
|
|
1539
|
+
}
|
|
1540
|
+
function wireReconciliationMethods(repository, ReconciliationModel, JournalEntryModel, orgField) {
|
|
1214
1541
|
const create = repository.create.bind(repository);
|
|
1215
1542
|
const deleteById = repository.delete.bind(repository);
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
repository.reconcile = async (input) => {
|
|
1221
|
-
const { account, journalEntryIds, note, reconciledBy, organizationId } = input;
|
|
1543
|
+
const repoInstance = repository;
|
|
1544
|
+
repository.match = async (input) => {
|
|
1545
|
+
const { account, items, note, reconciledBy, organizationId, session = null } = input;
|
|
1546
|
+
let { matchingNumber } = input;
|
|
1222
1547
|
requireOrgScope(orgField, organizationId);
|
|
1223
|
-
if (!
|
|
1224
|
-
const
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1548
|
+
if (!Array.isArray(items) || items.length < 2) throw Errors.validation("match() requires at least two items");
|
|
1549
|
+
const entryIds = Array.from(new Set(items.map((i) => String(i.entry))));
|
|
1550
|
+
const entryQuery = { _id: { $in: entryIds } };
|
|
1551
|
+
if (orgField && organizationId != null) entryQuery[orgField] = organizationId;
|
|
1552
|
+
const entries = await JournalEntryModel.find(entryQuery).session(session).lean();
|
|
1553
|
+
if (entries.length !== entryIds.length) throw Errors.notFound(`Expected ${entryIds.length} entries but found ${entries.length}. Some do not exist or belong to a different organization.`);
|
|
1554
|
+
const entryMap = /* @__PURE__ */ new Map();
|
|
1555
|
+
for (const e of entries) entryMap.set(String(e._id), e);
|
|
1230
1556
|
const accountStr = String(account);
|
|
1231
|
-
for (const entry of entries) if (!entry.journalItems.some((item) => String(item.account) === accountStr)) throw Errors.validation(`Entry ${entry._id} does not contain any items for account ${account}.`);
|
|
1232
1557
|
let debitTotal = 0;
|
|
1233
1558
|
let creditTotal = 0;
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1559
|
+
const currencies = /* @__PURE__ */ new Set();
|
|
1560
|
+
const itemSnapshots = [];
|
|
1561
|
+
for (const ref of items) {
|
|
1562
|
+
const entry = entryMap.get(String(ref.entry));
|
|
1563
|
+
if (!entry) throw Errors.notFound(`Entry ${String(ref.entry)} not found in match input`);
|
|
1564
|
+
if (entry.state !== "posted") throw Errors.validation(`Entry ${String(entry._id)} is not posted — only posted entries can be matched`);
|
|
1565
|
+
const item = entry.journalItems[ref.itemIndex];
|
|
1566
|
+
if (!item) throw Errors.validation(`Entry ${String(entry._id)} has no item at index ${ref.itemIndex}`);
|
|
1567
|
+
if (String(item.account) !== accountStr) throw Errors.validation(`Item ${String(entry._id)}[${ref.itemIndex}] is on a different account than the match`);
|
|
1568
|
+
if (item.matchingNumber) throw Errors.conflict(`Item ${String(entry._id)}[${ref.itemIndex}] is already matched (${item.matchingNumber})`);
|
|
1569
|
+
const debit = item.debit ?? 0;
|
|
1570
|
+
const credit = item.credit ?? 0;
|
|
1571
|
+
debitTotal += debit;
|
|
1572
|
+
creditTotal += credit;
|
|
1573
|
+
if (item.currency) currencies.add(item.currency);
|
|
1574
|
+
const amountCurrency = item.originalDebit != null || item.originalCredit != null ? (item.originalDebit ?? 0) - (item.originalCredit ?? 0) : null;
|
|
1575
|
+
itemSnapshots.push({
|
|
1576
|
+
entry: entry._id,
|
|
1577
|
+
itemIndex: ref.itemIndex,
|
|
1578
|
+
debit,
|
|
1579
|
+
credit,
|
|
1580
|
+
amountCurrency,
|
|
1581
|
+
exchangeRate: item.exchangeRate ?? null
|
|
1582
|
+
});
|
|
1237
1583
|
}
|
|
1584
|
+
const difference = debitTotal - creditTotal;
|
|
1585
|
+
const isFullReconcile = difference === 0;
|
|
1586
|
+
const sharedCurrency = currencies.size === 1 ? Array.from(currencies)[0] : null;
|
|
1587
|
+
if (!matchingNumber) matchingNumber = await nextMatchingNumber(ReconciliationModel, orgField, organizationId, session);
|
|
1588
|
+
const bulkOps = itemSnapshots.map((snap) => ({ updateOne: {
|
|
1589
|
+
filter: { _id: snap.entry },
|
|
1590
|
+
update: { $set: { [`journalItems.${snap.itemIndex}.matchingNumber`]: matchingNumber } }
|
|
1591
|
+
} }));
|
|
1592
|
+
await JournalEntryModel.bulkWrite(bulkOps, { session: session ?? void 0 });
|
|
1238
1593
|
const reconciliationData = {
|
|
1594
|
+
matchingNumber,
|
|
1239
1595
|
account,
|
|
1240
|
-
|
|
1596
|
+
items: itemSnapshots.map((s) => ({
|
|
1597
|
+
entry: s.entry,
|
|
1598
|
+
itemIndex: s.itemIndex,
|
|
1599
|
+
debit: s.debit,
|
|
1600
|
+
credit: s.credit,
|
|
1601
|
+
amountCurrency: s.amountCurrency,
|
|
1602
|
+
exchangeRate: s.exchangeRate
|
|
1603
|
+
})),
|
|
1241
1604
|
debitTotal,
|
|
1242
1605
|
creditTotal,
|
|
1243
|
-
difference
|
|
1606
|
+
difference,
|
|
1607
|
+
isFullReconcile,
|
|
1608
|
+
currency: sharedCurrency,
|
|
1244
1609
|
note,
|
|
1245
1610
|
reconciledBy,
|
|
1246
1611
|
reconciledAt: /* @__PURE__ */ new Date()
|
|
1247
1612
|
};
|
|
1248
1613
|
if (orgField && organizationId != null) reconciliationData[orgField] = organizationId;
|
|
1249
|
-
|
|
1614
|
+
const record = await create(reconciliationData);
|
|
1615
|
+
const emit = repoInstance._emitHook;
|
|
1616
|
+
if (emit) await emit.call(repoInstance, "after:match", {
|
|
1617
|
+
reconciliation: record,
|
|
1618
|
+
items: itemSnapshots,
|
|
1619
|
+
sharedCurrency,
|
|
1620
|
+
session
|
|
1621
|
+
});
|
|
1622
|
+
return record;
|
|
1250
1623
|
};
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
*/
|
|
1254
|
-
repository.unreconcile = async (input) => {
|
|
1255
|
-
const { reconciliationId, organizationId } = input;
|
|
1624
|
+
repository.unmatch = async (input) => {
|
|
1625
|
+
const { matchingNumber, organizationId, session = null } = input;
|
|
1256
1626
|
requireOrgScope(orgField, organizationId);
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1627
|
+
const query = { matchingNumber };
|
|
1628
|
+
if (orgField && organizationId != null) query[orgField] = organizationId;
|
|
1629
|
+
const existing = await ReconciliationModel.findOne(query).session(session).lean();
|
|
1630
|
+
if (!existing) throw Errors.notFound(`Reconciliation ${matchingNumber} not found`);
|
|
1631
|
+
const bulkOps = (existing.items ?? []).map((it) => ({ updateOne: {
|
|
1632
|
+
filter: { _id: it.entry },
|
|
1633
|
+
update: { $set: { [`journalItems.${it.itemIndex}.matchingNumber`]: null } }
|
|
1634
|
+
} }));
|
|
1635
|
+
if (bulkOps.length > 0) await JournalEntryModel.bulkWrite(bulkOps, { session: session ?? void 0 });
|
|
1636
|
+
const result = await deleteById(String(existing._id));
|
|
1637
|
+
if (!result.success) throw Errors.notFound("Failed to delete reconciliation record");
|
|
1265
1638
|
return result;
|
|
1266
1639
|
};
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
* Uses repository.getAll() for reconciliation lookups (hooks fire),
|
|
1270
|
-
* and direct JournalEntryModel for cross-repo reads (acceptable).
|
|
1271
|
-
*/
|
|
1272
|
-
repository.getUnreconciled = async (input) => {
|
|
1273
|
-
const { accountId, organizationId, limit = 100, skip = 0 } = input;
|
|
1640
|
+
repository.getOpenItems = async (params) => {
|
|
1641
|
+
const { accountId, organizationId, filter, asOfDate, limit = 100, skip = 0 } = params;
|
|
1274
1642
|
requireOrgScope(orgField, organizationId);
|
|
1275
|
-
const
|
|
1276
|
-
if (orgField && organizationId != null)
|
|
1277
|
-
|
|
1278
|
-
const
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1643
|
+
const match = { state: "posted" };
|
|
1644
|
+
if (orgField && organizationId != null) match[orgField] = organizationId;
|
|
1645
|
+
if (asOfDate) match.date = { $lte: asOfDate };
|
|
1646
|
+
const pipeline = [
|
|
1647
|
+
{ $match: match },
|
|
1648
|
+
{ $project: {
|
|
1649
|
+
_id: 1,
|
|
1650
|
+
date: 1,
|
|
1651
|
+
referenceNumber: 1,
|
|
1652
|
+
journalItems: 1
|
|
1653
|
+
} },
|
|
1654
|
+
{ $addFields: { journalItems: { $map: {
|
|
1655
|
+
input: { $range: [0, { $size: "$journalItems" }] },
|
|
1656
|
+
as: "idx",
|
|
1657
|
+
in: { $mergeObjects: [{ $arrayElemAt: ["$journalItems", "$$idx"] }, { _itemIndex: "$$idx" }] }
|
|
1658
|
+
} } } },
|
|
1659
|
+
{ $unwind: "$journalItems" },
|
|
1660
|
+
{ $match: {
|
|
1661
|
+
"journalItems.account": accountId,
|
|
1662
|
+
$or: [{ "journalItems.matchingNumber": null }, { "journalItems.matchingNumber": { $exists: false } }],
|
|
1663
|
+
...filter ? Object.fromEntries(Object.entries(filter).map(([k, v]) => [`journalItems.${k}`, v])) : {}
|
|
1664
|
+
} },
|
|
1665
|
+
{ $project: {
|
|
1666
|
+
_id: 0,
|
|
1667
|
+
entry: "$_id",
|
|
1668
|
+
itemIndex: "$journalItems._itemIndex",
|
|
1669
|
+
debit: { $ifNull: ["$journalItems.debit", 0] },
|
|
1670
|
+
credit: { $ifNull: ["$journalItems.credit", 0] },
|
|
1671
|
+
date: { $ifNull: ["$journalItems.date", "$date"] },
|
|
1672
|
+
maturityDate: "$journalItems.maturityDate",
|
|
1673
|
+
account: "$journalItems.account",
|
|
1674
|
+
currency: "$journalItems.currency",
|
|
1675
|
+
exchangeRate: "$journalItems.exchangeRate",
|
|
1676
|
+
label: "$journalItems.label",
|
|
1677
|
+
referenceNumber: 1,
|
|
1678
|
+
item: "$journalItems"
|
|
1679
|
+
} },
|
|
1680
|
+
{ $sort: { date: 1 } },
|
|
1681
|
+
{ $skip: skip },
|
|
1682
|
+
{ $limit: limit }
|
|
1683
|
+
];
|
|
1684
|
+
return await JournalEntryModel.aggregate(pipeline);
|
|
1287
1685
|
};
|
|
1288
1686
|
if (typeof repository.registerMethod === "function") for (const name of [
|
|
1289
|
-
"
|
|
1290
|
-
"
|
|
1291
|
-
"
|
|
1687
|
+
"match",
|
|
1688
|
+
"unmatch",
|
|
1689
|
+
"getOpenItems"
|
|
1292
1690
|
]) {
|
|
1293
1691
|
const fn = repository[name];
|
|
1294
1692
|
try {
|
|
@@ -1347,12 +1745,18 @@ function createRepositories(models, config, plugins = {}, pagination = {}) {
|
|
|
1347
1745
|
JournalEntryModel: models.JournalEntry,
|
|
1348
1746
|
orgField
|
|
1349
1747
|
}));
|
|
1748
|
+
const journalEntries = wireJournalEntryMethods(new Repository(models.JournalEntry, jePlugins, jePagination), models.JournalEntry, orgField, strictness);
|
|
1749
|
+
const fiscalPeriods = new Repository(models.FiscalPeriod, plugins.fiscalPeriod ?? [], fpPagination);
|
|
1750
|
+
const budgets = new Repository(models.Budget, plugins.budget ?? [], budgetPagination);
|
|
1751
|
+
const reconciliations = wireReconciliationMethods(new Repository(models.Reconciliation, plugins.reconciliation ?? [], reconPagination), models.Reconciliation, models.JournalEntry, orgField);
|
|
1752
|
+
const journalPagination = pagination.journal ?? {};
|
|
1350
1753
|
return {
|
|
1351
1754
|
accounts,
|
|
1352
|
-
journalEntries
|
|
1353
|
-
fiscalPeriods
|
|
1354
|
-
budgets
|
|
1355
|
-
reconciliations
|
|
1755
|
+
journalEntries,
|
|
1756
|
+
fiscalPeriods,
|
|
1757
|
+
budgets,
|
|
1758
|
+
reconciliations,
|
|
1759
|
+
journals: wireJournalMethods(new Repository(models.Journal, plugins.journal ?? [], journalPagination), country, orgField)
|
|
1356
1760
|
};
|
|
1357
1761
|
}
|
|
1358
1762
|
//#endregion
|
|
@@ -2046,6 +2450,68 @@ function createAccountingEngine(config) {
|
|
|
2046
2450
|
return new AccountingEngine(config);
|
|
2047
2451
|
}
|
|
2048
2452
|
//#endregion
|
|
2453
|
+
//#region src/utils/repartition-tax.ts
|
|
2454
|
+
/**
|
|
2455
|
+
* Default role resolver used when the country pack doesn't override.
|
|
2456
|
+
* Walks `taxCodes` to find a code whose `direction` matches the role.
|
|
2457
|
+
*/
|
|
2458
|
+
function defaultResolveRoleCode(role, _tax, country) {
|
|
2459
|
+
const direction = role === "collected" ? "collected" : role === "recoverable" ? "recoverable" : null;
|
|
2460
|
+
if (!direction) return void 0;
|
|
2461
|
+
for (const tc of Object.values(country.taxCodes)) if (tc.direction === direction) return tc.code;
|
|
2462
|
+
}
|
|
2463
|
+
/**
|
|
2464
|
+
* Build a `TaxLineGenerator` that expands each hit `taxCode` into one
|
|
2465
|
+
* journal item per repartition line. Taxes without `repartition` fall
|
|
2466
|
+
* back to a single-line generator using the direction-implied account.
|
|
2467
|
+
*/
|
|
2468
|
+
function createRepartitionTaxGenerator(options) {
|
|
2469
|
+
const { country, resolveAccount, documentType = "invoice" } = options;
|
|
2470
|
+
return { generateTaxLines(input) {
|
|
2471
|
+
const code = input.taxCode;
|
|
2472
|
+
if (!code) return [];
|
|
2473
|
+
const tax = country.taxCodes[code];
|
|
2474
|
+
if (!tax) return [];
|
|
2475
|
+
const baseTax = Math.round(input.amount * tax.rate / 100);
|
|
2476
|
+
if (baseTax === 0) return [];
|
|
2477
|
+
const lines = tax.repartition && tax.repartition.length > 0 ? tax.repartition.filter((line) => !line.documentTypes || line.documentTypes.includes(documentType)) : [{
|
|
2478
|
+
factor: 1,
|
|
2479
|
+
accountRole: tax.direction === "recoverable" ? "recoverable" : "collected",
|
|
2480
|
+
gridCode: tax.reportLines?.[0]
|
|
2481
|
+
}];
|
|
2482
|
+
const generated = [];
|
|
2483
|
+
for (const rep of lines) {
|
|
2484
|
+
const signed = Math.round(baseTax * rep.factor);
|
|
2485
|
+
if (signed === 0) continue;
|
|
2486
|
+
const account = resolveAccount(rep.accountRole, tax, input);
|
|
2487
|
+
if (!account) throw new Error(`repartitionTax: cannot resolve account for role "${rep.accountRole}" on tax "${tax.code}"`);
|
|
2488
|
+
const absAmount = Math.abs(signed);
|
|
2489
|
+
let onCredit = rep.accountRole === "collected" || rep.accountRole === "transition";
|
|
2490
|
+
if (signed < 0) onCredit = !onCredit;
|
|
2491
|
+
generated.push({
|
|
2492
|
+
account,
|
|
2493
|
+
debit: onCredit ? 0 : absAmount,
|
|
2494
|
+
credit: onCredit ? absAmount : 0,
|
|
2495
|
+
label: rep.label ?? `${tax.name} ${rep.accountRole}`,
|
|
2496
|
+
taxDetails: [{
|
|
2497
|
+
taxCode: tax.code,
|
|
2498
|
+
taxName: tax.name,
|
|
2499
|
+
...rep.gridCode != null ? { gridCode: String(rep.gridCode) } : {}
|
|
2500
|
+
}]
|
|
2501
|
+
});
|
|
2502
|
+
}
|
|
2503
|
+
return generated;
|
|
2504
|
+
} };
|
|
2505
|
+
}
|
|
2506
|
+
/**
|
|
2507
|
+
* Helper for packs that want the standard "role → account-type code"
|
|
2508
|
+
* mapping without writing their own resolver. Returns the function you
|
|
2509
|
+
* stuff into `CountryPackInput.resolveTaxRepartitionAccountCode`.
|
|
2510
|
+
*/
|
|
2511
|
+
function defaultResolveTaxRepartitionAccountCode(country) {
|
|
2512
|
+
return (role, tax) => defaultResolveRoleCode(role, tax, country);
|
|
2513
|
+
}
|
|
2514
|
+
//#endregion
|
|
2049
2515
|
//#region src/utils/dimensions.ts
|
|
2050
2516
|
/**
|
|
2051
2517
|
* Analytic Dimensions — Helpers for defining analytic dimensions
|
|
@@ -2105,4 +2571,4 @@ function buildDimensionIndexes(dimensions, orgField) {
|
|
|
2105
2571
|
});
|
|
2106
2572
|
}
|
|
2107
2573
|
//#endregion
|
|
2108
|
-
export { AccountingEngine, AccountingError, CATEGORIES, CATEGORY_KEYS, CURRENCIES, DEFAULT_BUCKETS, Errors, JOURNAL_CODES, JOURNAL_TYPES, Money, acquireSession, add, allocate, buildAccountTypeMap, buildDimensionFields, buildDimensionIndexes, buildItemFilters, buildRevaluationEntry, calculateTotal, closeFiscalPeriod, computeEndingBalance, computeRevaluation, createAccountingEngine, createModels, createRepositories,
|
|
2574
|
+
export { AccountingEngine, AccountingError, CATEGORIES, CATEGORY_KEYS, CURRENCIES, DEFAULT_BUCKETS, Errors, JOURNAL_CODES, JOURNAL_TYPES, Money, acquireSession, add, allocate, buildAccountTypeMap, buildDimensionFields, buildDimensionIndexes, buildItemFilters, buildRevaluationEntry, calculateTotal, closeFiscalPeriod, computeEndingBalance, computeRevaluation, createAccountingEngine, createLockPlugin, createModels, createRepartitionTaxGenerator, createRepositories, creditLimitPlugin, dailyLockPlugin, defaultLogger, defaultResolveTaxRepartitionAccountCode, defineCountryPack, doubleEntryPlugin, exportToCsv, finalizeSession, fiscalLockPlugin, flattenJournalEntries, format, formatPlain, fromDecimal, fxRealizationPlugin, generateAgedBalance, generateBalanceSheet, generateBudgetVsActual, generateCashFlow, generateDimensionBreakdown, generateGeneralLedger, generateIncomeStatement, generatePartnerLedger, generateRevaluation, generateTrialBalance, getCurrency, getCustomJournalTypes, getDateRange, getFiscalYearStart, getJournalType, getJournalTypeCodes, getMinorUnit, getNormalBalance, idempotencyPlugin, isBalanceSheet, isIncomeStatement, isValidCategory, isValidCurrency, isValidJournalType, isVirtualTaxAccount, multiply, parseCents, percentage, periodResolver, quickbooksFieldMap, registerJournalType, reopenFiscalPeriod, resolveModelNames, splitTaxExclusive, splitTaxInclusive, subtract, taxLockPlugin, toDecimal, universalFieldMap, watermarkResolver };
|