shareneus 1.6.22 → 1.6.23
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.
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TransactionCalculationEngine } from "./transaction-calculation-engine";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TransactionCalculationEngine = void 0;
|
|
4
|
+
var transaction_calculation_engine_1 = require("./transaction-calculation-engine");
|
|
5
|
+
Object.defineProperty(exports, "TransactionCalculationEngine", { enumerable: true, get: function () { return transaction_calculation_engine_1.TransactionCalculationEngine; } });
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
export interface ReverseCalculationInput {
|
|
2
|
+
total: number;
|
|
3
|
+
qty: number;
|
|
4
|
+
discount: number;
|
|
5
|
+
overallDiscount: number;
|
|
6
|
+
totalTaxRate: number;
|
|
7
|
+
isIndependentTax: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface TaxTotalOptions {
|
|
10
|
+
includeTaxInTotal: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare class TransactionCalculationEngine {
|
|
13
|
+
/**
|
|
14
|
+
* Summarizes a taxes array.
|
|
15
|
+
*
|
|
16
|
+
* Supported row shapes (case-tolerant):
|
|
17
|
+
* - `{ Amt: number }` (preferred for totals)
|
|
18
|
+
* - `{ Rate: number }` (preferred for reverse calc if Amt not present)
|
|
19
|
+
*/
|
|
20
|
+
static getTaxesSummary(taxes: any): {
|
|
21
|
+
taxAmount: number;
|
|
22
|
+
taxRate: number;
|
|
23
|
+
};
|
|
24
|
+
static sumTaxAmountFromRows(rows: unknown): number;
|
|
25
|
+
static calculateTotalFromTaxRows(input: {
|
|
26
|
+
baseAmount: number;
|
|
27
|
+
taxRows: unknown;
|
|
28
|
+
options: TaxTotalOptions;
|
|
29
|
+
}): number;
|
|
30
|
+
static calculateSubtotalFromTotalUsingTaxes(input: {
|
|
31
|
+
total: number;
|
|
32
|
+
taxRows: unknown;
|
|
33
|
+
includeTaxInTotal: boolean;
|
|
34
|
+
}): number;
|
|
35
|
+
static reverseCalculateAmountAndUnitPriceFromTotal(input: {
|
|
36
|
+
qty: number;
|
|
37
|
+
total: number;
|
|
38
|
+
disc: number;
|
|
39
|
+
recDisc: number;
|
|
40
|
+
noDisc: boolean;
|
|
41
|
+
taxRows: unknown;
|
|
42
|
+
includeTaxInTotal: boolean;
|
|
43
|
+
}): {
|
|
44
|
+
amount: number;
|
|
45
|
+
unitPrice: number;
|
|
46
|
+
};
|
|
47
|
+
static calculatePriceFromTotal(input: ReverseCalculationInput): number;
|
|
48
|
+
/**
|
|
49
|
+
* Returns the sum of all component rates for a tax code id.
|
|
50
|
+
*
|
|
51
|
+
* Safe for missing inputs:
|
|
52
|
+
* - empty TaxCodes
|
|
53
|
+
* - TCode not found
|
|
54
|
+
* - empty Components
|
|
55
|
+
*/
|
|
56
|
+
static getGSTRateSumByTaxCode(TCode: any, TaxCodes: any[]): number;
|
|
57
|
+
/**
|
|
58
|
+
* Calculates the raw base amount for an item:
|
|
59
|
+
* unAmt = qty * unpr
|
|
60
|
+
*
|
|
61
|
+
* Returns a shallow copy of the item with `unAmt` set.
|
|
62
|
+
*/
|
|
63
|
+
static calculateItemAmount(item: any): any;
|
|
64
|
+
/**
|
|
65
|
+
* Calculates the item-level (record-level) discount applied to a single item.
|
|
66
|
+
*
|
|
67
|
+
* Rules:
|
|
68
|
+
* - If `NoDisc` is true → recDisc = 0
|
|
69
|
+
* - Else if `perc` exists → recDisc = qty * unpr * (perc / 100)
|
|
70
|
+
* - Else → recDisc = disc (flat amount)
|
|
71
|
+
*
|
|
72
|
+
* Returns a shallow copy of the item with `recDisc` set.
|
|
73
|
+
*/
|
|
74
|
+
static calculateItemDiscount(item: any): any;
|
|
75
|
+
/**
|
|
76
|
+
* Calculates subtotal, tax amount, and total for an item using forward logic
|
|
77
|
+
* (price/qty → total).
|
|
78
|
+
*
|
|
79
|
+
* subtotal = (qty * unpr) − recDisc
|
|
80
|
+
* taxAmount = subtotal * (tax / 100)
|
|
81
|
+
* total = subtotal + taxAmount
|
|
82
|
+
*
|
|
83
|
+
* Returns a shallow copy of the item with `subtotal`, `taxAmount`, and
|
|
84
|
+
* `total` set.
|
|
85
|
+
*/
|
|
86
|
+
static calculateItemForwardTotal(item: any): any;
|
|
87
|
+
/**
|
|
88
|
+
* Calculates subtotal, taxAmount, and total for an item using a `Taxes` array.
|
|
89
|
+
*
|
|
90
|
+
* Rules:
|
|
91
|
+
* - subtotal = (qty * unpr) − recDisc
|
|
92
|
+
* - taxAmount = Σ Taxes[].Amt (if missing/0, falls back to Σ Taxes[].Rate %)
|
|
93
|
+
* - total = subtotal + taxAmount (when includeTaxInTotal = true)
|
|
94
|
+
*
|
|
95
|
+
* Notes:
|
|
96
|
+
* - If you want a "pre-tax total" line, pass includeTaxInTotal = false.
|
|
97
|
+
*/
|
|
98
|
+
static calculateItemForwardTotalUsingTaxes(item: any, options?: {
|
|
99
|
+
includeTaxInTotal?: boolean;
|
|
100
|
+
}): any;
|
|
101
|
+
/**
|
|
102
|
+
* Reverse-calculates `unpr` from a manually entered `total`.
|
|
103
|
+
*
|
|
104
|
+
* Steps:
|
|
105
|
+
* subtotal = total / (1 + tax / 100) [strips tax]
|
|
106
|
+
* baseAmt = subtotal + recDisc [adds back discount]
|
|
107
|
+
* unpr = baseAmt / qty [per-unit price]
|
|
108
|
+
*
|
|
109
|
+
* Guards:
|
|
110
|
+
* - If `qty` is 0 the calculation is skipped and the item is returned unchanged.
|
|
111
|
+
* - If `tax` is 0 the divisor reduces to 1 (safe).
|
|
112
|
+
*
|
|
113
|
+
* Returns a shallow copy of the item with `unpr` (and derived `unAmt`) updated.
|
|
114
|
+
*/
|
|
115
|
+
static calculateReverseFromTotal(item: any): any;
|
|
116
|
+
/**
|
|
117
|
+
* Reverse-calculates `unpr` from a manually entered `total`, supporting the
|
|
118
|
+
* same `disc`/`perc` semantics as `calculateItemDiscount`.
|
|
119
|
+
*
|
|
120
|
+
* This is intentionally tolerant of existing calling conventions:
|
|
121
|
+
* - `item.extraDisc` can be provided to represent an additional discount
|
|
122
|
+
* amount that should be added on top of the row discount (e.g. overall /
|
|
123
|
+
* record-level discount allocation). It is ignored when `NoDisc` is true.
|
|
124
|
+
* - When `tax` is 0, tax stripping becomes a no-op.
|
|
125
|
+
*
|
|
126
|
+
* Returns a shallow copy of the item with `unpr`, `unAmt`, and `recDisc` set.
|
|
127
|
+
*/
|
|
128
|
+
static calculateReverseFromTotalUsingDiscPerc(item: any): any;
|
|
129
|
+
/**
|
|
130
|
+
* Reverse-calculates `unpr` from a manually entered `total` using a `Taxes` array.
|
|
131
|
+
*
|
|
132
|
+
* When taxes contain `Amt` rows, the reverse "strips tax" by subtracting the
|
|
133
|
+
* summed tax amount. If tax `Amt` is 0/missing, it falls back to stripping by
|
|
134
|
+
* summed tax `Rate` (same as `calculateReverseFromTotalUsingDiscPerc`).
|
|
135
|
+
*
|
|
136
|
+
* Inputs:
|
|
137
|
+
* - qty, total, disc, perc, extraDisc, NoDisc
|
|
138
|
+
* - Taxes: array of `{ Amt?, Rate? }`
|
|
139
|
+
*/
|
|
140
|
+
static calculateReverseFromTotalUsingDiscPercAndTaxes(item: any, options?: {
|
|
141
|
+
includeTaxInTotal?: boolean;
|
|
142
|
+
}): any;
|
|
143
|
+
/**
|
|
144
|
+
* Applies an item-only discount (`PDisc` / `PPerc`) to an array of items.
|
|
145
|
+
*
|
|
146
|
+
* Rules:
|
|
147
|
+
* - Items with `NoDisc = true` are skipped.
|
|
148
|
+
* - If `PPerc` is provided: each item's share = its netAmt * (PPerc / 100)
|
|
149
|
+
* - Otherwise: `PDisc` is distributed proportionally by each item's `unAmt`.
|
|
150
|
+
*
|
|
151
|
+
* The `recDisc` on each item is **added to** any existing item-level discount
|
|
152
|
+
* so that both record-level and overall PDisc coexist correctly.
|
|
153
|
+
*
|
|
154
|
+
* Returns a new array of updated item objects.
|
|
155
|
+
*/
|
|
156
|
+
static applyPDiscountToItems(items: any[], PDisc: any, PPerc: any): any[];
|
|
157
|
+
/**
|
|
158
|
+
* Applies an ops-only discount (`LDisc` / `LPerc`) to an array of ops.
|
|
159
|
+
*
|
|
160
|
+
* Rules:
|
|
161
|
+
* - If `LPerc` is provided: each op's share = its `amount` * (LPerc / 100)
|
|
162
|
+
* - Otherwise: `LDisc` is distributed proportionally by each op's `amount`.
|
|
163
|
+
*
|
|
164
|
+
* Returns a new array of updated ops objects.
|
|
165
|
+
*/
|
|
166
|
+
static applyLDiscountToOps(ops: any[], LDisc: any, LPerc: any): any[];
|
|
167
|
+
/**
|
|
168
|
+
* Applies a cross-line discount (`Disc` / `Prec`) proportionally across
|
|
169
|
+
* both items and ops.
|
|
170
|
+
*
|
|
171
|
+
* Rules:
|
|
172
|
+
* - Items with `NoDisc = true` are excluded from the base but returned unchanged.
|
|
173
|
+
* - If `Prec` is provided: share = row's base * (Prec / 100)
|
|
174
|
+
* - Otherwise: `Disc` is distributed proportionally across the combined base
|
|
175
|
+
* (items by `unAmt`, ops by `amount`).
|
|
176
|
+
*
|
|
177
|
+
* Returns `{ items, ops }` with updated `recDisc` values.
|
|
178
|
+
*/
|
|
179
|
+
static applyCommonDiscount(items: any[], ops: any[], Disc: any, Prec: any): {
|
|
180
|
+
items: any[];
|
|
181
|
+
ops: any[];
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* Orchestration method that applies all transaction-level discounts in the
|
|
185
|
+
* correct order:
|
|
186
|
+
*
|
|
187
|
+
* 1. applyPDiscountToItems (PDisc / PPerc)
|
|
188
|
+
* 2. applyLDiscountToOps (LDisc / LPerc)
|
|
189
|
+
* 3. applyCommonDiscount (Disc / Prec)
|
|
190
|
+
*
|
|
191
|
+
* The `recDisc` on each line starts fresh from its item-level discount
|
|
192
|
+
* (already computed by `calculateItemDiscount`) before overall discounts
|
|
193
|
+
* are stacked on top.
|
|
194
|
+
*
|
|
195
|
+
* Returns `{ items, ops }`.
|
|
196
|
+
*/
|
|
197
|
+
static distributeRecordDiscount(items: any[], ops: any[], transaction: any): {
|
|
198
|
+
items: any[];
|
|
199
|
+
ops: any[];
|
|
200
|
+
};
|
|
201
|
+
/**
|
|
202
|
+
* Master recalculation method.
|
|
203
|
+
*
|
|
204
|
+
* Call whenever any value in the transaction changes:
|
|
205
|
+
* qty | unpr | disc | perc | tax | PDisc | PPerc | LDisc | LPerc | Disc | Prec
|
|
206
|
+
* or when items / ops are added or removed.
|
|
207
|
+
*
|
|
208
|
+
* Processing pipeline:
|
|
209
|
+
* For each item:
|
|
210
|
+
* a. calculateItemAmount → unAmt
|
|
211
|
+
* b. calculateItemDiscount → item-level recDisc
|
|
212
|
+
* → distributeRecordDiscount → stacks overall discounts onto recDisc
|
|
213
|
+
* For each item:
|
|
214
|
+
* c. calculateItemForwardTotal → subtotal, taxAmount, total
|
|
215
|
+
* For each op:
|
|
216
|
+
* d. recalculate op total → amount − recDisc
|
|
217
|
+
* → computeTransactionTotals → subTotal, taxTotal, grandTotal
|
|
218
|
+
*
|
|
219
|
+
* Returns a new transaction object (shallow-copied) with updated
|
|
220
|
+
* `Items`, `Ops`, and summary totals (`SubTotal`, `TaxTotal`, `Total`).
|
|
221
|
+
*
|
|
222
|
+
* The original transaction object is never mutated.
|
|
223
|
+
*
|
|
224
|
+
* ⚠️ When writing results back to Reactive Form controls always use
|
|
225
|
+
* `{ emitEvent: false }` to prevent triggering another recalculation.
|
|
226
|
+
*/
|
|
227
|
+
static recalculateTransaction(transaction: any): any;
|
|
228
|
+
/**
|
|
229
|
+
* Computes summary totals from already-calculated items and ops.
|
|
230
|
+
*
|
|
231
|
+
* subTotal = Σ item.subtotal + Σ op.total
|
|
232
|
+
* taxTotal = Σ item.taxAmount
|
|
233
|
+
* grandTotal = subTotal + taxTotal
|
|
234
|
+
*
|
|
235
|
+
* Returns `{ subTotal, taxTotal, grandTotal }`.
|
|
236
|
+
*/
|
|
237
|
+
static computeTransactionTotals(items: any[], ops: any[]): {
|
|
238
|
+
subTotal: number;
|
|
239
|
+
taxTotal: number;
|
|
240
|
+
grandTotal: number;
|
|
241
|
+
};
|
|
242
|
+
}
|
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TransactionCalculationEngine = void 0;
|
|
4
|
+
const math_operations_1 = require("../shared/math-operations");
|
|
5
|
+
/**
|
|
6
|
+
* TransactionCalculationEngine
|
|
7
|
+
*
|
|
8
|
+
* Reusable pure-calculation module for ERP-style transactions:
|
|
9
|
+
* Invoice | Bill | Sales Order | Delivery Challan | Purchase Order
|
|
10
|
+
*
|
|
11
|
+
* All methods are static and side-effect-free.
|
|
12
|
+
* Callers are responsible for writing results back to Reactive Form controls
|
|
13
|
+
* using { emitEvent: false } to prevent infinite loops.
|
|
14
|
+
*
|
|
15
|
+
* Monetary values are not rounded by this engine.
|
|
16
|
+
*
|
|
17
|
+
* Item fields used:
|
|
18
|
+
* qty, unpr, unAmt, disc, perc, recDisc, tax, total, NoDisc
|
|
19
|
+
*
|
|
20
|
+
* Ops fields used:
|
|
21
|
+
* amount, recDisc
|
|
22
|
+
*
|
|
23
|
+
* Transaction-level discount fields:
|
|
24
|
+
* PDisc / PPerc → items only
|
|
25
|
+
* LDisc / LPerc → ops only
|
|
26
|
+
* Disc / Prec → items + ops proportionally
|
|
27
|
+
*/
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Internal helpers
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
function toNum(value) {
|
|
32
|
+
const n = Number(value);
|
|
33
|
+
return Number.isFinite(n) ? n : 0;
|
|
34
|
+
}
|
|
35
|
+
function isNullOrEmpty(value) {
|
|
36
|
+
return (value === null || value === undefined || value === '' || value === 'null');
|
|
37
|
+
}
|
|
38
|
+
function getTaxesArray(value) {
|
|
39
|
+
var _a, _b;
|
|
40
|
+
const taxes = (_b = (_a = value === null || value === void 0 ? void 0 : value.Taxes) !== null && _a !== void 0 ? _a : value === null || value === void 0 ? void 0 : value.taxes) !== null && _b !== void 0 ? _b : value;
|
|
41
|
+
return Array.isArray(taxes) ? taxes : [];
|
|
42
|
+
}
|
|
43
|
+
function sumTaxAmountFromTaxes(taxes) {
|
|
44
|
+
const rows = getTaxesArray(taxes);
|
|
45
|
+
return rows.reduce((sum, row) => { var _a; return (0, math_operations_1.Add)(sum, toNum((_a = row === null || row === void 0 ? void 0 : row.Amt) !== null && _a !== void 0 ? _a : row === null || row === void 0 ? void 0 : row.amt)); }, 0);
|
|
46
|
+
}
|
|
47
|
+
function sumTaxRateFromTaxes(taxes) {
|
|
48
|
+
const rows = getTaxesArray(taxes);
|
|
49
|
+
return rows.reduce((sum, row) => { var _a; return (0, math_operations_1.Add)(sum, toNum((_a = row === null || row === void 0 ? void 0 : row.Rate) !== null && _a !== void 0 ? _a : row === null || row === void 0 ? void 0 : row.rate)); }, 0);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Distribute a flat discount amount across a set of rows proportionally
|
|
53
|
+
* by each row's `baseKey` value. Returns an array of share amounts aligned
|
|
54
|
+
* with the input rows.
|
|
55
|
+
*
|
|
56
|
+
* The last row absorbs any remainder so the sum always equals discAmt.
|
|
57
|
+
*/
|
|
58
|
+
function distributeProportionally(rows, discAmt, baseKey) {
|
|
59
|
+
const total = rows.reduce((sum, row) => (0, math_operations_1.Add)(sum, toNum(row[baseKey])), 0);
|
|
60
|
+
if (total === 0) {
|
|
61
|
+
return rows.map(() => 0);
|
|
62
|
+
}
|
|
63
|
+
const shares = [];
|
|
64
|
+
let allocated = 0;
|
|
65
|
+
rows.forEach((row, index) => {
|
|
66
|
+
if (index === rows.length - 1) {
|
|
67
|
+
// Last row: give the remainder to avoid floating-point drift
|
|
68
|
+
shares.push((0, math_operations_1.Subtract)(discAmt, allocated));
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const share = (0, math_operations_1.Multiply)((0, math_operations_1.Divide)(toNum(row[baseKey]), total), discAmt);
|
|
72
|
+
shares.push(share);
|
|
73
|
+
allocated = (0, math_operations_1.Add)(allocated, share);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
return shares;
|
|
77
|
+
}
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Public engine
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
class TransactionCalculationEngine {
|
|
82
|
+
// -------------------------------------------------------------------------
|
|
83
|
+
// Taxes helpers (Amt-based + Rate-based)
|
|
84
|
+
// -------------------------------------------------------------------------
|
|
85
|
+
/**
|
|
86
|
+
* Summarizes a taxes array.
|
|
87
|
+
*
|
|
88
|
+
* Supported row shapes (case-tolerant):
|
|
89
|
+
* - `{ Amt: number }` (preferred for totals)
|
|
90
|
+
* - `{ Rate: number }` (preferred for reverse calc if Amt not present)
|
|
91
|
+
*/
|
|
92
|
+
static getTaxesSummary(taxes) {
|
|
93
|
+
return {
|
|
94
|
+
taxAmount: sumTaxAmountFromTaxes(taxes),
|
|
95
|
+
taxRate: sumTaxRateFromTaxes(taxes),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
static sumTaxAmountFromRows(rows) {
|
|
99
|
+
if (!Array.isArray(rows)) {
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
return rows.reduce((sum, row) => {
|
|
103
|
+
return (0, math_operations_1.Add)(sum, toNum(row === null || row === void 0 ? void 0 : row.Amt));
|
|
104
|
+
}, 0);
|
|
105
|
+
}
|
|
106
|
+
static calculateTotalFromTaxRows(input) {
|
|
107
|
+
const baseAmount = toNum(input.baseAmount);
|
|
108
|
+
const taxAmount = TransactionCalculationEngine.sumTaxAmountFromRows(input.taxRows);
|
|
109
|
+
return input.options.includeTaxInTotal
|
|
110
|
+
? (0, math_operations_1.Add)(baseAmount, taxAmount)
|
|
111
|
+
: baseAmount;
|
|
112
|
+
}
|
|
113
|
+
static calculateSubtotalFromTotalUsingTaxes(input) {
|
|
114
|
+
const total = toNum(input.total);
|
|
115
|
+
if (!input.includeTaxInTotal) {
|
|
116
|
+
return total;
|
|
117
|
+
}
|
|
118
|
+
const taxSummary = TransactionCalculationEngine.getTaxesSummary(input.taxRows);
|
|
119
|
+
if (taxSummary.taxRate) {
|
|
120
|
+
const divisor = (0, math_operations_1.Add)(1, (0, math_operations_1.Divide)(taxSummary.taxRate, 100));
|
|
121
|
+
return divisor === 0 ? total : (0, math_operations_1.Divide)(total, divisor);
|
|
122
|
+
}
|
|
123
|
+
return (0, math_operations_1.Subtract)(total, taxSummary.taxAmount);
|
|
124
|
+
}
|
|
125
|
+
static reverseCalculateAmountAndUnitPriceFromTotal(input) {
|
|
126
|
+
const qty = toNum(input.qty) || 1;
|
|
127
|
+
const subtotal = TransactionCalculationEngine.calculateSubtotalFromTotalUsingTaxes({
|
|
128
|
+
total: input.total,
|
|
129
|
+
taxRows: input.taxRows,
|
|
130
|
+
includeTaxInTotal: input.includeTaxInTotal,
|
|
131
|
+
});
|
|
132
|
+
const disc = toNum(input.disc);
|
|
133
|
+
const recDisc = toNum(input.recDisc);
|
|
134
|
+
const amount = input.noDisc
|
|
135
|
+
? subtotal
|
|
136
|
+
: (0, math_operations_1.Add)(subtotal, (0, math_operations_1.Add)(disc, recDisc));
|
|
137
|
+
const unitPrice = (0, math_operations_1.Divide)(amount, qty);
|
|
138
|
+
return { amount, unitPrice };
|
|
139
|
+
}
|
|
140
|
+
// Shared reverse-calculation formula. Components only need to supply their own field mapping.
|
|
141
|
+
static calculatePriceFromTotal(input) {
|
|
142
|
+
const qty = toNum(input.qty) || 1;
|
|
143
|
+
const total = toNum(input.total);
|
|
144
|
+
const discount = toNum(input.discount);
|
|
145
|
+
const overallDiscount = toNum(input.overallDiscount);
|
|
146
|
+
const totalTaxRate = toNum(input.totalTaxRate);
|
|
147
|
+
const taxableBase = input.isIndependentTax
|
|
148
|
+
? (0, math_operations_1.Divide)(total, (0, math_operations_1.Add)(1, (0, math_operations_1.Divide)(totalTaxRate, 100)))
|
|
149
|
+
: total;
|
|
150
|
+
return (0, math_operations_1.Divide)((0, math_operations_1.Add)((0, math_operations_1.Add)(taxableBase, discount), overallDiscount), qty);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Returns the sum of all component rates for a tax code id.
|
|
154
|
+
*
|
|
155
|
+
* Safe for missing inputs:
|
|
156
|
+
* - empty TaxCodes
|
|
157
|
+
* - TCode not found
|
|
158
|
+
* - empty Components
|
|
159
|
+
*/
|
|
160
|
+
static getGSTRateSumByTaxCode(TCode, TaxCodes) {
|
|
161
|
+
if (isNullOrEmpty(TCode) || !Array.isArray(TaxCodes)) {
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
const taxCode = TaxCodes.find((code) => (code === null || code === void 0 ? void 0 : code._id) === TCode);
|
|
165
|
+
const components = Array.isArray(taxCode === null || taxCode === void 0 ? void 0 : taxCode.Components)
|
|
166
|
+
? taxCode.Components
|
|
167
|
+
: [];
|
|
168
|
+
return components.reduce((sum, comp) => (0, math_operations_1.Add)(sum, toNum(comp === null || comp === void 0 ? void 0 : comp.Rate)), 0);
|
|
169
|
+
}
|
|
170
|
+
// -------------------------------------------------------------------------
|
|
171
|
+
// 1. calculateItemAmount
|
|
172
|
+
// -------------------------------------------------------------------------
|
|
173
|
+
/**
|
|
174
|
+
* Calculates the raw base amount for an item:
|
|
175
|
+
* unAmt = qty * unpr
|
|
176
|
+
*
|
|
177
|
+
* Returns a shallow copy of the item with `unAmt` set.
|
|
178
|
+
*/
|
|
179
|
+
static calculateItemAmount(item) {
|
|
180
|
+
const qty = toNum(item.qty);
|
|
181
|
+
const unpr = toNum(item.unpr);
|
|
182
|
+
return Object.assign(Object.assign({}, item), { unAmt: (0, math_operations_1.Multiply)(qty, unpr) });
|
|
183
|
+
}
|
|
184
|
+
// -------------------------------------------------------------------------
|
|
185
|
+
// 2. calculateItemDiscount
|
|
186
|
+
// -------------------------------------------------------------------------
|
|
187
|
+
/**
|
|
188
|
+
* Calculates the item-level (record-level) discount applied to a single item.
|
|
189
|
+
*
|
|
190
|
+
* Rules:
|
|
191
|
+
* - If `NoDisc` is true → recDisc = 0
|
|
192
|
+
* - Else if `perc` exists → recDisc = qty * unpr * (perc / 100)
|
|
193
|
+
* - Else → recDisc = disc (flat amount)
|
|
194
|
+
*
|
|
195
|
+
* Returns a shallow copy of the item with `recDisc` set.
|
|
196
|
+
*/
|
|
197
|
+
static calculateItemDiscount(item) {
|
|
198
|
+
if (item.NoDisc) {
|
|
199
|
+
return Object.assign(Object.assign({}, item), { recDisc: 0 });
|
|
200
|
+
}
|
|
201
|
+
const qty = toNum(item.qty);
|
|
202
|
+
const unpr = toNum(item.unpr);
|
|
203
|
+
const perc = toNum(item.perc);
|
|
204
|
+
const disc = toNum(item.disc);
|
|
205
|
+
const recDisc = !isNullOrEmpty(item.perc) && perc !== 0
|
|
206
|
+
? (0, math_operations_1.Multiply)((0, math_operations_1.Multiply)(qty, unpr), (0, math_operations_1.Divide)(perc, 100))
|
|
207
|
+
: disc;
|
|
208
|
+
return Object.assign(Object.assign({}, item), { recDisc });
|
|
209
|
+
}
|
|
210
|
+
// -------------------------------------------------------------------------
|
|
211
|
+
// 3. calculateItemForwardTotal
|
|
212
|
+
// -------------------------------------------------------------------------
|
|
213
|
+
/**
|
|
214
|
+
* Calculates subtotal, tax amount, and total for an item using forward logic
|
|
215
|
+
* (price/qty → total).
|
|
216
|
+
*
|
|
217
|
+
* subtotal = (qty * unpr) − recDisc
|
|
218
|
+
* taxAmount = subtotal * (tax / 100)
|
|
219
|
+
* total = subtotal + taxAmount
|
|
220
|
+
*
|
|
221
|
+
* Returns a shallow copy of the item with `subtotal`, `taxAmount`, and
|
|
222
|
+
* `total` set.
|
|
223
|
+
*/
|
|
224
|
+
static calculateItemForwardTotal(item) {
|
|
225
|
+
const qty = toNum(item.qty);
|
|
226
|
+
const unpr = toNum(item.unpr);
|
|
227
|
+
const recDisc = toNum(item.recDisc);
|
|
228
|
+
const tax = toNum(item.tax);
|
|
229
|
+
const subtotal = (0, math_operations_1.Subtract)((0, math_operations_1.Multiply)(qty, unpr), recDisc);
|
|
230
|
+
const taxAmount = (0, math_operations_1.Divide)((0, math_operations_1.Multiply)(subtotal, tax), 100);
|
|
231
|
+
const total = (0, math_operations_1.Add)(subtotal, taxAmount);
|
|
232
|
+
return Object.assign(Object.assign({}, item), { subtotal, taxAmount, total });
|
|
233
|
+
}
|
|
234
|
+
// -------------------------------------------------------------------------
|
|
235
|
+
// 3b. calculateItemForwardTotalUsingTaxes
|
|
236
|
+
// -------------------------------------------------------------------------
|
|
237
|
+
/**
|
|
238
|
+
* Calculates subtotal, taxAmount, and total for an item using a `Taxes` array.
|
|
239
|
+
*
|
|
240
|
+
* Rules:
|
|
241
|
+
* - subtotal = (qty * unpr) − recDisc
|
|
242
|
+
* - taxAmount = Σ Taxes[].Amt (if missing/0, falls back to Σ Taxes[].Rate %)
|
|
243
|
+
* - total = subtotal + taxAmount (when includeTaxInTotal = true)
|
|
244
|
+
*
|
|
245
|
+
* Notes:
|
|
246
|
+
* - If you want a "pre-tax total" line, pass includeTaxInTotal = false.
|
|
247
|
+
*/
|
|
248
|
+
static calculateItemForwardTotalUsingTaxes(item, options) {
|
|
249
|
+
var _a;
|
|
250
|
+
const includeTaxInTotal = (options === null || options === void 0 ? void 0 : options.includeTaxInTotal) !== false;
|
|
251
|
+
const qty = toNum(item.qty);
|
|
252
|
+
const unpr = toNum(item.unpr);
|
|
253
|
+
const recDisc = toNum(item.recDisc);
|
|
254
|
+
const subtotal = (0, math_operations_1.Subtract)((0, math_operations_1.Multiply)(qty, unpr), recDisc);
|
|
255
|
+
const taxes = (_a = item === null || item === void 0 ? void 0 : item.Taxes) !== null && _a !== void 0 ? _a : item === null || item === void 0 ? void 0 : item.taxes;
|
|
256
|
+
const taxSummary = TransactionCalculationEngine.getTaxesSummary(taxes);
|
|
257
|
+
const taxAmount = taxSummary.taxAmount !== 0
|
|
258
|
+
? taxSummary.taxAmount
|
|
259
|
+
: (0, math_operations_1.Divide)((0, math_operations_1.Multiply)(subtotal, taxSummary.taxRate), 100);
|
|
260
|
+
const total = (0, math_operations_1.Add)(subtotal, includeTaxInTotal ? taxAmount : 0);
|
|
261
|
+
return Object.assign(Object.assign({}, item), { subtotal, taxAmount, total });
|
|
262
|
+
}
|
|
263
|
+
// -------------------------------------------------------------------------
|
|
264
|
+
// 4. calculateReverseFromTotal
|
|
265
|
+
// -------------------------------------------------------------------------
|
|
266
|
+
/**
|
|
267
|
+
* Reverse-calculates `unpr` from a manually entered `total`.
|
|
268
|
+
*
|
|
269
|
+
* Steps:
|
|
270
|
+
* subtotal = total / (1 + tax / 100) [strips tax]
|
|
271
|
+
* baseAmt = subtotal + recDisc [adds back discount]
|
|
272
|
+
* unpr = baseAmt / qty [per-unit price]
|
|
273
|
+
*
|
|
274
|
+
* Guards:
|
|
275
|
+
* - If `qty` is 0 the calculation is skipped and the item is returned unchanged.
|
|
276
|
+
* - If `tax` is 0 the divisor reduces to 1 (safe).
|
|
277
|
+
*
|
|
278
|
+
* Returns a shallow copy of the item with `unpr` (and derived `unAmt`) updated.
|
|
279
|
+
*/
|
|
280
|
+
static calculateReverseFromTotal(item) {
|
|
281
|
+
const qty = toNum(item.qty);
|
|
282
|
+
const total = toNum(item.total);
|
|
283
|
+
const recDisc = toNum(item.recDisc);
|
|
284
|
+
const tax = toNum(item.tax);
|
|
285
|
+
if (qty === 0) {
|
|
286
|
+
// Cannot reverse without a quantity – return item unchanged
|
|
287
|
+
return Object.assign({}, item);
|
|
288
|
+
}
|
|
289
|
+
const divisor = (0, math_operations_1.Add)(1, (0, math_operations_1.Divide)(tax, 100));
|
|
290
|
+
const subtotal = (0, math_operations_1.Divide)(total, divisor === 0 ? 1 : divisor);
|
|
291
|
+
const baseAmt = (0, math_operations_1.Add)(subtotal, recDisc);
|
|
292
|
+
const unpr = (0, math_operations_1.Divide)(baseAmt, qty);
|
|
293
|
+
const unAmt = (0, math_operations_1.Multiply)(qty, unpr);
|
|
294
|
+
return Object.assign(Object.assign({}, item), { unpr, unAmt });
|
|
295
|
+
}
|
|
296
|
+
// -------------------------------------------------------------------------
|
|
297
|
+
// 4b. calculateReverseFromTotalUsingDiscPerc
|
|
298
|
+
// -------------------------------------------------------------------------
|
|
299
|
+
/**
|
|
300
|
+
* Reverse-calculates `unpr` from a manually entered `total`, supporting the
|
|
301
|
+
* same `disc`/`perc` semantics as `calculateItemDiscount`.
|
|
302
|
+
*
|
|
303
|
+
* This is intentionally tolerant of existing calling conventions:
|
|
304
|
+
* - `item.extraDisc` can be provided to represent an additional discount
|
|
305
|
+
* amount that should be added on top of the row discount (e.g. overall /
|
|
306
|
+
* record-level discount allocation). It is ignored when `NoDisc` is true.
|
|
307
|
+
* - When `tax` is 0, tax stripping becomes a no-op.
|
|
308
|
+
*
|
|
309
|
+
* Returns a shallow copy of the item with `unpr`, `unAmt`, and `recDisc` set.
|
|
310
|
+
*/
|
|
311
|
+
static calculateReverseFromTotalUsingDiscPerc(item) {
|
|
312
|
+
const qty = toNum(item.qty);
|
|
313
|
+
const total = toNum(item.total);
|
|
314
|
+
const tax = toNum(item.tax);
|
|
315
|
+
const disc = toNum(item.disc);
|
|
316
|
+
const perc = toNum(item.perc);
|
|
317
|
+
const extraDisc = toNum(item.extraDisc);
|
|
318
|
+
if (qty === 0) {
|
|
319
|
+
return Object.assign({}, item);
|
|
320
|
+
}
|
|
321
|
+
const divisor = (0, math_operations_1.Add)(1, (0, math_operations_1.Divide)(tax, 100));
|
|
322
|
+
const subtotal = (0, math_operations_1.Divide)(total, divisor === 0 ? 1 : divisor);
|
|
323
|
+
if (item.NoDisc) {
|
|
324
|
+
const unpr = (0, math_operations_1.Divide)(subtotal, qty);
|
|
325
|
+
const unAmt = (0, math_operations_1.Multiply)(qty, unpr);
|
|
326
|
+
return Object.assign(Object.assign({}, item), { unpr, unAmt, recDisc: 0 });
|
|
327
|
+
}
|
|
328
|
+
// Percentage path: solve for unpr algebraically because recDisc depends on unpr
|
|
329
|
+
if (!isNullOrEmpty(item.perc) && perc !== 0) {
|
|
330
|
+
const discountRate = (0, math_operations_1.Divide)(perc, 100);
|
|
331
|
+
const priceFactor = (0, math_operations_1.Subtract)(1, discountRate);
|
|
332
|
+
if (priceFactor === 0) {
|
|
333
|
+
return Object.assign({}, item);
|
|
334
|
+
}
|
|
335
|
+
const unpr = (0, math_operations_1.Divide)((0, math_operations_1.Add)(subtotal, extraDisc), (0, math_operations_1.Multiply)(qty, priceFactor));
|
|
336
|
+
const unAmt = (0, math_operations_1.Multiply)(qty, unpr);
|
|
337
|
+
const rowDisc = (0, math_operations_1.Multiply)(unAmt, discountRate);
|
|
338
|
+
const recDisc = (0, math_operations_1.Add)(rowDisc, extraDisc);
|
|
339
|
+
return Object.assign(Object.assign({}, item), { unpr, unAmt, recDisc });
|
|
340
|
+
}
|
|
341
|
+
// Flat amount path
|
|
342
|
+
const recDisc = (0, math_operations_1.Add)(disc, extraDisc);
|
|
343
|
+
const unpr = (0, math_operations_1.Divide)((0, math_operations_1.Add)(subtotal, recDisc), qty);
|
|
344
|
+
const unAmt = (0, math_operations_1.Multiply)(qty, unpr);
|
|
345
|
+
return Object.assign(Object.assign({}, item), { unpr, unAmt, recDisc });
|
|
346
|
+
}
|
|
347
|
+
// -------------------------------------------------------------------------
|
|
348
|
+
// 4c. calculateReverseFromTotalUsingDiscPercAndTaxes
|
|
349
|
+
// -------------------------------------------------------------------------
|
|
350
|
+
/**
|
|
351
|
+
* Reverse-calculates `unpr` from a manually entered `total` using a `Taxes` array.
|
|
352
|
+
*
|
|
353
|
+
* When taxes contain `Amt` rows, the reverse "strips tax" by subtracting the
|
|
354
|
+
* summed tax amount. If tax `Amt` is 0/missing, it falls back to stripping by
|
|
355
|
+
* summed tax `Rate` (same as `calculateReverseFromTotalUsingDiscPerc`).
|
|
356
|
+
*
|
|
357
|
+
* Inputs:
|
|
358
|
+
* - qty, total, disc, perc, extraDisc, NoDisc
|
|
359
|
+
* - Taxes: array of `{ Amt?, Rate? }`
|
|
360
|
+
*/
|
|
361
|
+
static calculateReverseFromTotalUsingDiscPercAndTaxes(item, options) {
|
|
362
|
+
var _a;
|
|
363
|
+
const includeTaxInTotal = (options === null || options === void 0 ? void 0 : options.includeTaxInTotal) !== false;
|
|
364
|
+
const qty = toNum(item.qty);
|
|
365
|
+
const total = toNum(item.total);
|
|
366
|
+
const disc = toNum(item.disc);
|
|
367
|
+
const perc = toNum(item.perc);
|
|
368
|
+
const extraDisc = toNum(item.extraDisc);
|
|
369
|
+
if (qty === 0) {
|
|
370
|
+
return Object.assign({}, item);
|
|
371
|
+
}
|
|
372
|
+
const taxSummary = TransactionCalculationEngine.getTaxesSummary((_a = item === null || item === void 0 ? void 0 : item.Taxes) !== null && _a !== void 0 ? _a : item === null || item === void 0 ? void 0 : item.taxes);
|
|
373
|
+
// If total doesn't include tax, treat it as "already stripped".
|
|
374
|
+
const subtotal = includeTaxInTotal
|
|
375
|
+
? taxSummary.taxAmount !== 0
|
|
376
|
+
? (0, math_operations_1.Subtract)(total, taxSummary.taxAmount)
|
|
377
|
+
: (() => {
|
|
378
|
+
const divisor = (0, math_operations_1.Add)(1, (0, math_operations_1.Divide)(taxSummary.taxRate, 100));
|
|
379
|
+
return (0, math_operations_1.Divide)(total, divisor === 0 ? 1 : divisor);
|
|
380
|
+
})()
|
|
381
|
+
: total;
|
|
382
|
+
if (item.NoDisc) {
|
|
383
|
+
const unpr = (0, math_operations_1.Divide)(subtotal, qty);
|
|
384
|
+
const unAmt = (0, math_operations_1.Multiply)(qty, unpr);
|
|
385
|
+
return Object.assign(Object.assign({}, item), { unpr, unAmt, recDisc: 0 });
|
|
386
|
+
}
|
|
387
|
+
// Percentage path: solve for unpr algebraically because row discount depends on unpr.
|
|
388
|
+
if (!isNullOrEmpty(item.perc) && perc !== 0) {
|
|
389
|
+
const discountRate = (0, math_operations_1.Divide)(perc, 100);
|
|
390
|
+
const priceFactor = (0, math_operations_1.Subtract)(1, discountRate);
|
|
391
|
+
if (priceFactor === 0) {
|
|
392
|
+
return Object.assign({}, item);
|
|
393
|
+
}
|
|
394
|
+
const unpr = (0, math_operations_1.Divide)((0, math_operations_1.Add)(subtotal, extraDisc), (0, math_operations_1.Multiply)(qty, priceFactor));
|
|
395
|
+
const unAmt = (0, math_operations_1.Multiply)(qty, unpr);
|
|
396
|
+
const rowDisc = (0, math_operations_1.Multiply)(unAmt, discountRate);
|
|
397
|
+
const recDisc = (0, math_operations_1.Add)(rowDisc, extraDisc);
|
|
398
|
+
return Object.assign(Object.assign({}, item), { unpr, unAmt, recDisc });
|
|
399
|
+
}
|
|
400
|
+
const recDisc = (0, math_operations_1.Add)(disc, extraDisc);
|
|
401
|
+
const unpr = (0, math_operations_1.Divide)((0, math_operations_1.Add)(subtotal, recDisc), qty);
|
|
402
|
+
const unAmt = (0, math_operations_1.Multiply)(qty, unpr);
|
|
403
|
+
return Object.assign(Object.assign({}, item), { unpr, unAmt, recDisc });
|
|
404
|
+
}
|
|
405
|
+
// -------------------------------------------------------------------------
|
|
406
|
+
// 5. applyPDiscountToItems
|
|
407
|
+
// -------------------------------------------------------------------------
|
|
408
|
+
/**
|
|
409
|
+
* Applies an item-only discount (`PDisc` / `PPerc`) to an array of items.
|
|
410
|
+
*
|
|
411
|
+
* Rules:
|
|
412
|
+
* - Items with `NoDisc = true` are skipped.
|
|
413
|
+
* - If `PPerc` is provided: each item's share = its netAmt * (PPerc / 100)
|
|
414
|
+
* - Otherwise: `PDisc` is distributed proportionally by each item's `unAmt`.
|
|
415
|
+
*
|
|
416
|
+
* The `recDisc` on each item is **added to** any existing item-level discount
|
|
417
|
+
* so that both record-level and overall PDisc coexist correctly.
|
|
418
|
+
*
|
|
419
|
+
* Returns a new array of updated item objects.
|
|
420
|
+
*/
|
|
421
|
+
static applyPDiscountToItems(items, PDisc, PPerc) {
|
|
422
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
423
|
+
return items !== null && items !== void 0 ? items : [];
|
|
424
|
+
}
|
|
425
|
+
const discountable = items.filter((i) => !i.NoDisc);
|
|
426
|
+
// Percentage path
|
|
427
|
+
if (!isNullOrEmpty(PPerc) && toNum(PPerc) !== 0) {
|
|
428
|
+
const pperc = toNum(PPerc);
|
|
429
|
+
return items.map((item) => {
|
|
430
|
+
if (item.NoDisc)
|
|
431
|
+
return Object.assign({}, item);
|
|
432
|
+
const netAmt = (0, math_operations_1.Subtract)(toNum(item.unAmt), toNum(item.recDisc));
|
|
433
|
+
const share = (0, math_operations_1.Divide)((0, math_operations_1.Multiply)(netAmt, pperc), 100);
|
|
434
|
+
return Object.assign(Object.assign({}, item), { recDisc: (0, math_operations_1.Add)(toNum(item.recDisc), share) });
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
// Flat-amount proportional path
|
|
438
|
+
const pdisc = toNum(PDisc);
|
|
439
|
+
if (pdisc === 0 || discountable.length === 0) {
|
|
440
|
+
return items.map((i) => (Object.assign({}, i)));
|
|
441
|
+
}
|
|
442
|
+
const shares = distributeProportionally(discountable, pdisc, 'unAmt');
|
|
443
|
+
let shareIdx = 0;
|
|
444
|
+
return items.map((item) => {
|
|
445
|
+
var _a;
|
|
446
|
+
if (item.NoDisc)
|
|
447
|
+
return Object.assign({}, item);
|
|
448
|
+
const share = (_a = shares[shareIdx++]) !== null && _a !== void 0 ? _a : 0;
|
|
449
|
+
return Object.assign(Object.assign({}, item), { recDisc: (0, math_operations_1.Add)(toNum(item.recDisc), share) });
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
// -------------------------------------------------------------------------
|
|
453
|
+
// 6. applyLDiscountToOps
|
|
454
|
+
// -------------------------------------------------------------------------
|
|
455
|
+
/**
|
|
456
|
+
* Applies an ops-only discount (`LDisc` / `LPerc`) to an array of ops.
|
|
457
|
+
*
|
|
458
|
+
* Rules:
|
|
459
|
+
* - If `LPerc` is provided: each op's share = its `amount` * (LPerc / 100)
|
|
460
|
+
* - Otherwise: `LDisc` is distributed proportionally by each op's `amount`.
|
|
461
|
+
*
|
|
462
|
+
* Returns a new array of updated ops objects.
|
|
463
|
+
*/
|
|
464
|
+
static applyLDiscountToOps(ops, LDisc, LPerc) {
|
|
465
|
+
if (!Array.isArray(ops) || ops.length === 0) {
|
|
466
|
+
return ops !== null && ops !== void 0 ? ops : [];
|
|
467
|
+
}
|
|
468
|
+
// Percentage path
|
|
469
|
+
if (!isNullOrEmpty(LPerc) && toNum(LPerc) !== 0) {
|
|
470
|
+
const lperc = toNum(LPerc);
|
|
471
|
+
return ops.map((op) => {
|
|
472
|
+
const share = (0, math_operations_1.Divide)((0, math_operations_1.Multiply)(toNum(op.amount), lperc), 100);
|
|
473
|
+
return Object.assign(Object.assign({}, op), { recDisc: (0, math_operations_1.Add)(toNum(op.recDisc), share) });
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
// Flat-amount proportional path
|
|
477
|
+
const ldisc = toNum(LDisc);
|
|
478
|
+
if (ldisc === 0) {
|
|
479
|
+
return ops.map((o) => (Object.assign({}, o)));
|
|
480
|
+
}
|
|
481
|
+
const shares = distributeProportionally(ops, ldisc, 'amount');
|
|
482
|
+
return ops.map((op, i) => {
|
|
483
|
+
var _a;
|
|
484
|
+
return (Object.assign(Object.assign({}, op), { recDisc: (0, math_operations_1.Add)(toNum(op.recDisc), (_a = shares[i]) !== null && _a !== void 0 ? _a : 0) }));
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
// -------------------------------------------------------------------------
|
|
488
|
+
// 7. applyCommonDiscount
|
|
489
|
+
// -------------------------------------------------------------------------
|
|
490
|
+
/**
|
|
491
|
+
* Applies a cross-line discount (`Disc` / `Prec`) proportionally across
|
|
492
|
+
* both items and ops.
|
|
493
|
+
*
|
|
494
|
+
* Rules:
|
|
495
|
+
* - Items with `NoDisc = true` are excluded from the base but returned unchanged.
|
|
496
|
+
* - If `Prec` is provided: share = row's base * (Prec / 100)
|
|
497
|
+
* - Otherwise: `Disc` is distributed proportionally across the combined base
|
|
498
|
+
* (items by `unAmt`, ops by `amount`).
|
|
499
|
+
*
|
|
500
|
+
* Returns `{ items, ops }` with updated `recDisc` values.
|
|
501
|
+
*/
|
|
502
|
+
static applyCommonDiscount(items, ops, Disc, Prec) {
|
|
503
|
+
const safeItems = Array.isArray(items) ? items : [];
|
|
504
|
+
const safeOps = Array.isArray(ops) ? ops : [];
|
|
505
|
+
const discountableItems = safeItems.filter((i) => !i.NoDisc);
|
|
506
|
+
// Percentage path
|
|
507
|
+
if (!isNullOrEmpty(Prec) && toNum(Prec) !== 0) {
|
|
508
|
+
const prec = toNum(Prec);
|
|
509
|
+
const updatedItems = safeItems.map((item) => {
|
|
510
|
+
if (item.NoDisc)
|
|
511
|
+
return Object.assign({}, item);
|
|
512
|
+
// Apply common % discount on the net base after any prior discounts
|
|
513
|
+
// (item-level discount + PDisc), so discounts stack sequentially.
|
|
514
|
+
const base = Math.max(0, (0, math_operations_1.Subtract)(toNum(item.unAmt), toNum(item.recDisc)));
|
|
515
|
+
const share = (0, math_operations_1.Divide)((0, math_operations_1.Multiply)(base, prec), 100);
|
|
516
|
+
console.log('Disc share', { id: item.id, base, share });
|
|
517
|
+
return Object.assign(Object.assign({}, item), { recDisc: (0, math_operations_1.Add)(toNum(item.recDisc), share) });
|
|
518
|
+
});
|
|
519
|
+
const updatedOps = safeOps.map((op) => {
|
|
520
|
+
// Apply common % discount on the net base after LDisc.
|
|
521
|
+
const base = Math.max(0, (0, math_operations_1.Subtract)(toNum(op.amount), toNum(op.recDisc)));
|
|
522
|
+
const share = (0, math_operations_1.Divide)((0, math_operations_1.Multiply)(base, prec), 100);
|
|
523
|
+
return Object.assign(Object.assign({}, op), { recDisc: (0, math_operations_1.Add)(toNum(op.recDisc), share) });
|
|
524
|
+
});
|
|
525
|
+
return { items: updatedItems, ops: updatedOps };
|
|
526
|
+
}
|
|
527
|
+
// Flat-amount proportional path
|
|
528
|
+
const disc = toNum(Disc);
|
|
529
|
+
if (disc === 0) {
|
|
530
|
+
return {
|
|
531
|
+
items: safeItems.map((i) => (Object.assign({}, i))),
|
|
532
|
+
ops: safeOps.map((o) => (Object.assign({}, o))),
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
// Build a unified list for proportional distribution (no-disc items excluded)
|
|
536
|
+
const rows = [
|
|
537
|
+
...discountableItems.map((i) => ({
|
|
538
|
+
_key: 'item',
|
|
539
|
+
_ref: i,
|
|
540
|
+
base: toNum(i.unAmt),
|
|
541
|
+
})),
|
|
542
|
+
...safeOps.map((o) => ({ _key: 'op', _ref: o, base: toNum(o.amount) })),
|
|
543
|
+
];
|
|
544
|
+
const totalBase = rows.reduce((sum, r) => (0, math_operations_1.Add)(sum, r.base), 0);
|
|
545
|
+
if (totalBase === 0) {
|
|
546
|
+
return {
|
|
547
|
+
items: safeItems.map((i) => (Object.assign({}, i))),
|
|
548
|
+
ops: safeOps.map((o) => (Object.assign({}, o))),
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
// Assign shares
|
|
552
|
+
let allocated = 0;
|
|
553
|
+
rows.forEach((row, index) => {
|
|
554
|
+
let share;
|
|
555
|
+
if (index === rows.length - 1) {
|
|
556
|
+
share = (0, math_operations_1.Subtract)(disc, allocated);
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
share = (0, math_operations_1.Multiply)((0, math_operations_1.Divide)(row.base, totalBase), disc);
|
|
560
|
+
allocated = (0, math_operations_1.Add)(allocated, share);
|
|
561
|
+
}
|
|
562
|
+
row._share = share;
|
|
563
|
+
});
|
|
564
|
+
// Build result maps
|
|
565
|
+
const itemShareMap = new Map();
|
|
566
|
+
const opShareMap = new Map();
|
|
567
|
+
rows.forEach((row) => {
|
|
568
|
+
if (row._key === 'item')
|
|
569
|
+
itemShareMap.set(row._ref, row._share);
|
|
570
|
+
else
|
|
571
|
+
opShareMap.set(row._ref, row._share);
|
|
572
|
+
});
|
|
573
|
+
const updatedItems = safeItems.map((item) => {
|
|
574
|
+
var _a;
|
|
575
|
+
if (item.NoDisc)
|
|
576
|
+
return Object.assign({}, item);
|
|
577
|
+
const share = (_a = itemShareMap.get(item)) !== null && _a !== void 0 ? _a : 0;
|
|
578
|
+
return Object.assign(Object.assign({}, item), { recDisc: (0, math_operations_1.Add)(toNum(item.recDisc), share) });
|
|
579
|
+
});
|
|
580
|
+
const updatedOps = safeOps.map((op) => {
|
|
581
|
+
var _a;
|
|
582
|
+
const share = (_a = opShareMap.get(op)) !== null && _a !== void 0 ? _a : 0;
|
|
583
|
+
return Object.assign(Object.assign({}, op), { recDisc: (0, math_operations_1.Add)(toNum(op.recDisc), share) });
|
|
584
|
+
});
|
|
585
|
+
return { items: updatedItems, ops: updatedOps };
|
|
586
|
+
}
|
|
587
|
+
// -------------------------------------------------------------------------
|
|
588
|
+
// 8. distributeRecordDiscount
|
|
589
|
+
// -------------------------------------------------------------------------
|
|
590
|
+
/**
|
|
591
|
+
* Orchestration method that applies all transaction-level discounts in the
|
|
592
|
+
* correct order:
|
|
593
|
+
*
|
|
594
|
+
* 1. applyPDiscountToItems (PDisc / PPerc)
|
|
595
|
+
* 2. applyLDiscountToOps (LDisc / LPerc)
|
|
596
|
+
* 3. applyCommonDiscount (Disc / Prec)
|
|
597
|
+
*
|
|
598
|
+
* The `recDisc` on each line starts fresh from its item-level discount
|
|
599
|
+
* (already computed by `calculateItemDiscount`) before overall discounts
|
|
600
|
+
* are stacked on top.
|
|
601
|
+
*
|
|
602
|
+
* Returns `{ items, ops }`.
|
|
603
|
+
*/
|
|
604
|
+
static distributeRecordDiscount(items, ops, transaction) {
|
|
605
|
+
const { PDisc, PPerc, LDisc, LPerc, Disc, Prec } = transaction !== null && transaction !== void 0 ? transaction : {};
|
|
606
|
+
let updatedItems = Array.isArray(items)
|
|
607
|
+
? items.map((i) => (Object.assign({}, i)))
|
|
608
|
+
: [];
|
|
609
|
+
let updatedOps = Array.isArray(ops)
|
|
610
|
+
? ops.map((o) => (Object.assign({}, o)))
|
|
611
|
+
: [];
|
|
612
|
+
// Step 1 – items-only discount
|
|
613
|
+
updatedItems = TransactionCalculationEngine.applyPDiscountToItems(updatedItems, PDisc, PPerc);
|
|
614
|
+
// Step 2 – ops-only discount
|
|
615
|
+
updatedOps = TransactionCalculationEngine.applyLDiscountToOps(updatedOps, LDisc, LPerc);
|
|
616
|
+
// Step 3 – common discount across both
|
|
617
|
+
const result = TransactionCalculationEngine.applyCommonDiscount(updatedItems, updatedOps, Disc, Prec);
|
|
618
|
+
return { items: result.items, ops: result.ops };
|
|
619
|
+
}
|
|
620
|
+
// -------------------------------------------------------------------------
|
|
621
|
+
// 9. recalculateTransaction
|
|
622
|
+
// -------------------------------------------------------------------------
|
|
623
|
+
/**
|
|
624
|
+
* Master recalculation method.
|
|
625
|
+
*
|
|
626
|
+
* Call whenever any value in the transaction changes:
|
|
627
|
+
* qty | unpr | disc | perc | tax | PDisc | PPerc | LDisc | LPerc | Disc | Prec
|
|
628
|
+
* or when items / ops are added or removed.
|
|
629
|
+
*
|
|
630
|
+
* Processing pipeline:
|
|
631
|
+
* For each item:
|
|
632
|
+
* a. calculateItemAmount → unAmt
|
|
633
|
+
* b. calculateItemDiscount → item-level recDisc
|
|
634
|
+
* → distributeRecordDiscount → stacks overall discounts onto recDisc
|
|
635
|
+
* For each item:
|
|
636
|
+
* c. calculateItemForwardTotal → subtotal, taxAmount, total
|
|
637
|
+
* For each op:
|
|
638
|
+
* d. recalculate op total → amount − recDisc
|
|
639
|
+
* → computeTransactionTotals → subTotal, taxTotal, grandTotal
|
|
640
|
+
*
|
|
641
|
+
* Returns a new transaction object (shallow-copied) with updated
|
|
642
|
+
* `Items`, `Ops`, and summary totals (`SubTotal`, `TaxTotal`, `Total`).
|
|
643
|
+
*
|
|
644
|
+
* The original transaction object is never mutated.
|
|
645
|
+
*
|
|
646
|
+
* ⚠️ When writing results back to Reactive Form controls always use
|
|
647
|
+
* `{ emitEvent: false }` to prevent triggering another recalculation.
|
|
648
|
+
*/
|
|
649
|
+
static recalculateTransaction(transaction) {
|
|
650
|
+
if (!transaction)
|
|
651
|
+
return transaction;
|
|
652
|
+
const rawItems = Array.isArray(transaction.Items)
|
|
653
|
+
? transaction.Items
|
|
654
|
+
: [];
|
|
655
|
+
const rawOps = Array.isArray(transaction.Ops) ? transaction.Ops : [];
|
|
656
|
+
// ── Step A & B: item-level amounts and item-level discounts ──────────────
|
|
657
|
+
let items = rawItems
|
|
658
|
+
.map((item) => TransactionCalculationEngine.calculateItemAmount(item))
|
|
659
|
+
.map((item) => TransactionCalculationEngine.calculateItemDiscount(item));
|
|
660
|
+
// Ops: ensure `amount` exists before distribution (carry existing value)
|
|
661
|
+
let ops = rawOps.map((op) => (Object.assign({}, op)));
|
|
662
|
+
// ── Step C: distribute transaction-level discounts ────────────────────────
|
|
663
|
+
const distributed = TransactionCalculationEngine.distributeRecordDiscount(items, ops, transaction);
|
|
664
|
+
items = distributed.items;
|
|
665
|
+
ops = distributed.ops;
|
|
666
|
+
// ── Step D: forward totals per item ──────────────────────────────────────
|
|
667
|
+
items = items.map((item) => TransactionCalculationEngine.calculateItemForwardTotal(item));
|
|
668
|
+
// ── Step E: op totals (amount already known; subtract recDisc) ───────────
|
|
669
|
+
ops = ops.map((op) => (Object.assign(Object.assign({}, op), { total: (0, math_operations_1.Subtract)(toNum(op.amount), toNum(op.recDisc)) })));
|
|
670
|
+
// ── Step F: transaction-level summary ────────────────────────────────────
|
|
671
|
+
const totals = TransactionCalculationEngine.computeTransactionTotals(items, ops);
|
|
672
|
+
return Object.assign(Object.assign({}, transaction), { Items: items, Ops: ops, SubTotal: totals.subTotal, TaxTotal: totals.taxTotal, Total: totals.grandTotal });
|
|
673
|
+
}
|
|
674
|
+
// -------------------------------------------------------------------------
|
|
675
|
+
// computeTransactionTotals (utility – exposed for standalone use)
|
|
676
|
+
// -------------------------------------------------------------------------
|
|
677
|
+
/**
|
|
678
|
+
* Computes summary totals from already-calculated items and ops.
|
|
679
|
+
*
|
|
680
|
+
* subTotal = Σ item.subtotal + Σ op.total
|
|
681
|
+
* taxTotal = Σ item.taxAmount
|
|
682
|
+
* grandTotal = subTotal + taxTotal
|
|
683
|
+
*
|
|
684
|
+
* Returns `{ subTotal, taxTotal, grandTotal }`.
|
|
685
|
+
*/
|
|
686
|
+
static computeTransactionTotals(items, ops) {
|
|
687
|
+
const safeItems = Array.isArray(items) ? items : [];
|
|
688
|
+
const safeOps = Array.isArray(ops) ? ops : [];
|
|
689
|
+
const itemSubTotal = safeItems.reduce((sum, i) => (0, math_operations_1.Add)(sum, toNum(i.subtotal)), 0);
|
|
690
|
+
const taxTotal = safeItems.reduce((sum, i) => (0, math_operations_1.Add)(sum, toNum(i.taxAmount)), 0);
|
|
691
|
+
const opsTotal = safeOps.reduce((sum, o) => (0, math_operations_1.Add)(sum, toNum(o.total)), 0);
|
|
692
|
+
const subTotal = (0, math_operations_1.Add)(itemSubTotal, opsTotal);
|
|
693
|
+
const grandTotal = (0, math_operations_1.Add)(subTotal, taxTotal);
|
|
694
|
+
return {
|
|
695
|
+
subTotal,
|
|
696
|
+
taxTotal,
|
|
697
|
+
grandTotal,
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
exports.TransactionCalculationEngine = TransactionCalculationEngine;
|