GWM : tax calculation : using taxable amount column now

This commit is contained in:
Gowtham M 2026-07-20 09:48:31 +05:30
parent 6ba17a3a6f
commit 67b2c32884
6 changed files with 37 additions and 13 deletions

View File

@ -247,6 +247,8 @@ Each sub-master supports: `GET /` (list), `GET /:id`, `POST /`, `PUT /:id`, `DEL
| [x] | GET | `/purchase-orders/:poId/attachments/:attachmentId/download` | view | Download file (authenticated) | | [x] | GET | `/purchase-orders/:poId/attachments/:attachmentId/download` | view | Download file (authenticated) |
| [x] | DELETE | `/purchase-orders/:poId/attachments/:attachmentId` | delete | Delete attachment + file | | [x] | DELETE | `/purchase-orders/:poId/attachments/:attachmentId` | delete | Delete attachment + file |
**Totals:** `taxable_amount = sub_total + freight + other discount`; GST (`tax_total` / CGST+SGST / IGST) is calculated on `taxable_amount` (not on `sub_total`). Column persisted via `scripts/patch-po-taxable-amount.sql`.
--- ---
### GRN (`/grn`) — module: `GRN` ### GRN (`/grn`) — module: `GRN`

View File

@ -698,6 +698,7 @@ model purchase_orders {
expected_delivery_date DateTime? @db.Date expected_delivery_date DateTime? @db.Date
sub_total Decimal @default(0) @db.Decimal(15, 4) sub_total Decimal @default(0) @db.Decimal(15, 4)
discount_amount Decimal @default(0) @db.Decimal(15, 4) discount_amount Decimal @default(0) @db.Decimal(15, 4)
taxable_amount Decimal @default(0) @db.Decimal(15, 4)
tax_total Decimal @default(0) @db.Decimal(15, 4) tax_total Decimal @default(0) @db.Decimal(15, 4)
cgst Decimal @default(0) @db.Decimal(15, 4) cgst Decimal @default(0) @db.Decimal(15, 4)
sgst Decimal @default(0) @db.Decimal(15, 4) sgst Decimal @default(0) @db.Decimal(15, 4)

View File

@ -0,0 +1,21 @@
-- Persist PO header taxable amount and support GST-on-taxable calc
-- taxable_amount = sub_total + freight_charges + other_charges - discount_amount
ALTER TABLE purchase_orders
ADD COLUMN IF NOT EXISTS taxable_amount NUMERIC(15, 4) NOT NULL DEFAULT 0;
UPDATE purchase_orders
SET taxable_amount = ROUND(
COALESCE(sub_total, 0)
+ COALESCE(freight_charges, 0)
+ COALESCE(other_charges, 0)
- COALESCE(discount_amount, 0),
4
)
WHERE taxable_amount = 0
AND (
COALESCE(sub_total, 0) <> 0
OR COALESCE(freight_charges, 0) <> 0
OR COALESCE(other_charges, 0) <> 0
OR COALESCE(discount_amount, 0) <> 0
);

View File

@ -172,6 +172,10 @@ paths:
get: get:
tags: [Purchase Orders] tags: [Purchase Orders]
summary: Get purchase order detail summary: Get purchase order detail
description: >
Includes line items and amount summary. Header taxable_amount =
sub_total + freight_charges + other_charges - discount_amount.
tax_total / cgst+sgst / igst are calculated on taxable_amount.
parameters: parameters:
- { name: id, in: path, required: true, schema: { type: string, example: '1' } } - { name: id, in: path, required: true, schema: { type: string, example: '1' } }
responses: responses:

View File

@ -33,7 +33,7 @@ const computeHeaderTotals = (
) => { ) => {
// Sub total = sum of line taxable amounts (after line-level discounts). // Sub total = sum of line taxable amounts (after line-level discounts).
const subTotal = round4(lines.reduce((sum, line) => sum + toNum(line.taxable_amount), 0)); const subTotal = round4(lines.reduce((sum, line) => sum + toNum(line.taxable_amount), 0));
const taxTotal = round4(lines.reduce((sum, line) => sum + toNum(line.tax_amount), 0)); const lineTaxTotal = round4(lines.reduce((sum, line) => sum + toNum(line.tax_amount), 0));
const headerDiscountAmount = round4(headerDiscount); const headerDiscountAmount = round4(headerDiscount);
const freightCharges = round4(freight); const freightCharges = round4(freight);
const otherCharges = round4(other); const otherCharges = round4(other);
@ -41,6 +41,12 @@ const computeHeaderTotals = (
// Taxable amount = Sub Total + Freight + Other Discount (header-level). // Taxable amount = Sub Total + Freight + Other Discount (header-level).
const taxableAmount = round4(subTotal + freightCharges + otherCharges - headerDiscountAmount); const taxableAmount = round4(subTotal + freightCharges + otherCharges - headerDiscountAmount);
// GST is applied on header taxable amount (not on sub_total alone).
// Scale line taxes proportionally so multi-rate POs stay correct:
// each line's share of freight/other/discount is taxed at that line's GST %.
const taxTotal =
subTotal > 0 ? round4((lineTaxTotal * taxableAmount) / subTotal) : 0;
// TDS is calculated on sub total (line taxable), not on header taxable. // TDS is calculated on sub total (line taxable), not on header taxable.
const tdsAmount = tdsApplicable ? round4((subTotal * toNum(tdsSectionPct)) / 100) : 0; const tdsAmount = tdsApplicable ? round4((subTotal * toNum(tdsSectionPct)) / 100) : 0;
const adjustmentAmount = round4(adjustment); const adjustmentAmount = round4(adjustment);

View File

@ -21,7 +21,6 @@ const {
computeHeaderTotals, computeHeaderTotals,
splitGst, splitGst,
toNum, toNum,
round4,
} = require('./purchase-orders.calculations'); } = require('./purchase-orders.calculations');
const repository = require('./purchase-orders.repository'); const repository = require('./purchase-orders.repository');
const { assertAnyLocation } = require('../../utils/locations'); const { assertAnyLocation } = require('../../utils/locations');
@ -144,13 +143,6 @@ const sanitizePo = (po) => {
return { return {
...rest, ...rest,
// Header taxable = sub_total + freight + other discount (not a DB column).
taxable_amount: round4(
toNum(rest.sub_total) +
toNum(rest.freight_charges) +
toNum(rest.other_charges) -
toNum(rest.discount_amount)
),
vendor: vendors || null, vendor: vendors || null,
billing: billing_location || null, billing: billing_location || null,
shipping: shipping_location || null, shipping: shipping_location || null,
@ -320,10 +312,8 @@ const buildHeaderData = async (payload, builtItems, userId, { applyDefaultTerms
adjustment: payload.adjustment, adjustment: payload.adjustment,
} }
); );
// taxable_amount is derived for API/PDF; not a persisted PO column.
const { taxable_amount: _taxableAmount, ...persistedTotals } = totals;
const gstSplit = splitGst(persistedTotals.tax_total, isInterStateSupply(billing, vendor)); const gstSplit = splitGst(totals.tax_total, isInterStateSupply(billing, vendor));
let termsAndConditions = let termsAndConditions =
payload.terms_and_conditions !== undefined && payload.terms_and_conditions !== null payload.terms_and_conditions !== undefined && payload.terms_and_conditions !== null
@ -349,7 +339,7 @@ const buildHeaderData = async (payload, builtItems, userId, { applyDefaultTerms
remarks: payload.remarks || null, remarks: payload.remarks || null,
tds_applicable: tdsApplicable, tds_applicable: tdsApplicable,
tds_section_pct: tdsApplicable ? tdsSectionPct : null, tds_section_pct: tdsApplicable ? tdsSectionPct : null,
...persistedTotals, ...totals,
...gstSplit, ...gstSplit,
updated_by: userId ? BigInt(userId) : null, updated_by: userId ? BigInt(userId) : null,
}; };