pelagia-portal/App/lib/po-money.ts
Hardik 78afcb610b
Some checks failed
PR checks / checks (pull_request) Failing after 35s
PR checks / integration (pull_request) Successful in 33s
feat(po): TCS & Discount below GST (#133)
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>
2026-06-29 14:50:34 +05:30

67 lines
2.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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;
}