diff --git a/BACKEND_TASKS.md b/BACKEND_TASKS.md index c28eace..a8f0ed7 100644 --- a/BACKEND_TASKS.md +++ b/BACKEND_TASKS.md @@ -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] | 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` diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5b8baf2..18f1b07 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -698,6 +698,7 @@ model purchase_orders { expected_delivery_date DateTime? @db.Date sub_total 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) cgst Decimal @default(0) @db.Decimal(15, 4) sgst Decimal @default(0) @db.Decimal(15, 4) diff --git a/scripts/patch-po-taxable-amount.sql b/scripts/patch-po-taxable-amount.sql new file mode 100644 index 0000000..a855a9d --- /dev/null +++ b/scripts/patch-po-taxable-amount.sql @@ -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 + ); diff --git a/src/docs/purchase-orders-routes.yaml b/src/docs/purchase-orders-routes.yaml index a7211a2..fdc2b1d 100644 --- a/src/docs/purchase-orders-routes.yaml +++ b/src/docs/purchase-orders-routes.yaml @@ -172,6 +172,10 @@ paths: get: tags: [Purchase Orders] 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: - { name: id, in: path, required: true, schema: { type: string, example: '1' } } responses: diff --git a/src/modules/purchase-orders/purchase-orders.calculations.js b/src/modules/purchase-orders/purchase-orders.calculations.js index 5bfcd1a..d3f0142 100644 --- a/src/modules/purchase-orders/purchase-orders.calculations.js +++ b/src/modules/purchase-orders/purchase-orders.calculations.js @@ -33,7 +33,7 @@ const computeHeaderTotals = ( ) => { // 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 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 freightCharges = round4(freight); const otherCharges = round4(other); @@ -41,6 +41,12 @@ const computeHeaderTotals = ( // Taxable amount = Sub Total + Freight + Other − Discount (header-level). 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. const tdsAmount = tdsApplicable ? round4((subTotal * toNum(tdsSectionPct)) / 100) : 0; const adjustmentAmount = round4(adjustment); diff --git a/src/modules/purchase-orders/purchase-orders.service.js b/src/modules/purchase-orders/purchase-orders.service.js index 506e598..a387011 100644 --- a/src/modules/purchase-orders/purchase-orders.service.js +++ b/src/modules/purchase-orders/purchase-orders.service.js @@ -21,7 +21,6 @@ const { computeHeaderTotals, splitGst, toNum, - round4, } = require('./purchase-orders.calculations'); const repository = require('./purchase-orders.repository'); const { assertAnyLocation } = require('../../utils/locations'); @@ -144,13 +143,6 @@ const sanitizePo = (po) => { return { ...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, billing: billing_location || null, shipping: shipping_location || null, @@ -320,10 +312,8 @@ const buildHeaderData = async (payload, builtItems, userId, { applyDefaultTerms 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 = 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, tds_applicable: tdsApplicable, tds_section_pct: tdsApplicable ? tdsSectionPct : null, - ...persistedTotals, + ...totals, ...gstSplit, updated_by: userId ? BigInt(userId) : null, };