Adds two PO-level charges shown below GST, per issue #133 ask 2. - Stored as ABSOLUTE rupee amounts on PurchaseOrder.tcsAmount / discountAmount (Decimal?, default 0; null/0 on historical & imported POs). Migration added. - Discount is applied post-GST. totalAmount folds the charges in (net payable = subtotal + GST + TCS − Discount), so payments / reports / advance all use the true amount due. lib/po-money.ts is the single source of truth. - Forms (create + edit) render a shared TcsDiscountFields with a % control bidirectionally linked to the rupee value (percentage is convenience only, taken against the GST-inclusive total; only the absolute amount is persisted). - createPo / updatePo store & compute; both manager-edit actions PRESERVE the PO's TCS/Discount when recomputing the total; import leaves them at 0. - PO detail shows TCS / Discount / Net payable below GST; PDF + XLSX export show the same breakdown and a corrected grand total. Tests: lib/po-money unit tests; po-tcs-discount integration test (create / edit / manager-line-edit preservation). Docs: CLAUDE.md GST section + wiki Purchase Orders (TCS/Discount + a full "what import sets vs. not" field-mapping table). Full unit (360) + integration (305) suites green; tsc clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
67 lines
2.2 KiB
TypeScript
67 lines
2.2 KiB
TypeScript
/**
|
||
* Single source of truth for PO money math (issue #133).
|
||
*
|
||
* A PO's value is built up as:
|
||
*
|
||
* taxable = Σ qty · unitPrice (ex-GST)
|
||
* gst = Σ qty · unitPrice · gstRate
|
||
* inclGst = taxable + gst (the line-items total)
|
||
* netPayable = inclGst + tcs − discount (PO-level charges, below GST)
|
||
*
|
||
* `netPayable` is what is stored in `PurchaseOrder.totalAmount`, so payments,
|
||
* reports, and the advance-payment slider all operate on the true amount due.
|
||
*
|
||
* TCS and Discount are **absolute** rupee amounts (the UI's % control is only a
|
||
* convenience that writes back the rupee value). Discount is applied **post-GST**.
|
||
* The percentage shown for either is taken against `inclGst` (the GST-inclusive
|
||
* line-items total) — see `amountToPercent` / `percentToAmount`.
|
||
*/
|
||
|
||
export interface MoneyItem {
|
||
quantity: number;
|
||
unitPrice: number;
|
||
gstRate?: number | null;
|
||
}
|
||
|
||
export const DEFAULT_GST_RATE = 0.18;
|
||
|
||
export interface PoMoney {
|
||
taxable: number;
|
||
gst: number;
|
||
inclGst: number;
|
||
tcs: number;
|
||
discount: number;
|
||
netPayable: number;
|
||
}
|
||
|
||
export function computePoMoney(
|
||
items: MoneyItem[],
|
||
tcs = 0,
|
||
discount = 0
|
||
): PoMoney {
|
||
const taxable = items.reduce((s, i) => s + i.quantity * i.unitPrice, 0);
|
||
const gst = items.reduce(
|
||
(s, i) => s + i.quantity * i.unitPrice * (i.gstRate ?? DEFAULT_GST_RATE),
|
||
0
|
||
);
|
||
const inclGst = taxable + gst;
|
||
const t = tcs || 0;
|
||
const d = discount || 0;
|
||
return { taxable, gst, inclGst, tcs: t, discount: d, netPayable: inclGst + t - d };
|
||
}
|
||
|
||
/** Net payable (PO totalAmount) for a set of line items plus PO-level charges. */
|
||
export function poNetPayable(items: MoneyItem[], tcs = 0, discount = 0): number {
|
||
return computePoMoney(items, tcs, discount).netPayable;
|
||
}
|
||
|
||
/** Convert an absolute charge to its percentage of a base (0 when base is 0). */
|
||
export function amountToPercent(amount: number, base: number): number {
|
||
if (!base) return 0;
|
||
return (amount / base) * 100;
|
||
}
|
||
|
||
/** Convert a percentage of a base back to an absolute charge. */
|
||
export function percentToAmount(percent: number, base: number): number {
|
||
return (percent / 100) * base;
|
||
}
|