import { Add, Divide, Multiply, Subtract } from "../shared/math-operations"; import { GetNumber } from "../shared/util"; /** * Safely handles null/undefined arrays and returns empty array */ function safeArray(arr: any[]): any[] { return Array.isArray(arr) ? arr : []; } function getLineNetAmount(line: any): number { return Subtract(GetNumber(line.UnAmt || line.Amt), Add(GetNumber(line.Disc), GetNumber(line.RecDisc))); } function getTaxCodeById(taxCodes: any[], taxCodeId: any) { const normalizedTaxCodeId = Number(taxCodeId); if (!normalizedTaxCodeId) { return null; } return taxCodes.find((taxCode: any) => Number(taxCode._id) === normalizedTaxCodeId) || null; } function getTaxComponentRate(taxCode: any, code: string): number { const component = safeArray(taxCode?.Components).find((item: any) => item?.Code === code); if (component) { return GetNumber(component.Rate); } return GetNumber(taxCode?.[code]); } function buildTaxesFromFlatFields(line: any, taxCode: any, netAmount: number): any[] { const flatTaxes = [ { Code: "CGST", Amt: GetNumber(line.CGST) }, { Code: "SGST", Amt: GetNumber(line.SGST) }, { Code: "IGST", Amt: GetNumber(line.IGST) } ]; return flatTaxes .filter((tax) => tax.Amt > 0) .map((tax) => ({ Code: tax.Code, Amt: tax.Amt, Rate: getTaxComponentRate(taxCode, tax.Code) || (netAmount > 0 ? Multiply(Divide(tax.Amt, netAmount), 100) : 0), TaxCodeId: GetNumber(line.TCode) || GetNumber(taxCode?._id) })); } function buildTaxesFromTaxCode(line: any, taxCode: any, netAmount: number): any[] { if (!taxCode) { return []; } const taxComponents = safeArray(taxCode.Components); if (taxComponents.length > 0) { return taxComponents .filter((component: any) => ["CGST", "SGST", "IGST"].includes(component?.Code)) .map((component: any) => ({ Code: component.Code, Amt: Multiply(netAmount, Divide(GetNumber(component.Rate), 100)), Rate: GetNumber(component.Rate), TaxCodeId: GetNumber(taxCode._id) })) .filter((tax: any) => tax.Amt > 0); } const legacyTaxes = [ { Code: "CGST", Rate: GetNumber(taxCode.CGST) }, { Code: "SGST", Rate: GetNumber(taxCode.SGST) }, { Code: "IGST", Rate: GetNumber(taxCode.IGST) } ]; return legacyTaxes .filter((tax) => tax.Rate > 0) .map((tax) => ({ Code: tax.Code, Amt: Multiply(netAmount, Divide(tax.Rate, 100)), Rate: tax.Rate, TaxCodeId: GetNumber(taxCode._id) })); } function getResolvedTaxes(line: any, taxCodes: any[] = []): any[] { const lineTaxes = safeArray(line?.Taxes); if (lineTaxes.length > 0) { return lineTaxes.map((tax: any) => ({ ...tax, Code: tax?.Code, Amt: GetNumber(tax?.Amt), Rate: GetNumber(tax?.Rate), TaxCodeId: GetNumber(tax?.TaxCodeId) })).filter((tax: any) => !!tax.Code); } const netAmount = getLineNetAmount(line); const taxCode = getTaxCodeById(taxCodes, line?.TCode); const taxesFromFlatFields = buildTaxesFromFlatFields(line, taxCode, netAmount); if (taxesFromFlatFields.length > 0) { return taxesFromFlatFields; } return buildTaxesFromTaxCode(line, taxCode, netAmount); } function getTaxAmountByCode(taxes: any[], code: string): number { return taxes .filter((tax: any) => tax.Code === code) .reduce((sum: number, tax: any) => Add(sum, GetNumber(tax.Amt)), 0); } export function TotalCalculations(items: any[] = [], ops: any[] = [], isTaxable: boolean = false, taxCodes: any[] = [], Adjust: number = 0) { // Handle null/undefined arrays const safeItems = safeArray(items); const safeOps = safeArray(ops); const safeTaxCodes = safeArray(taxCodes); // Initialize all totals let subtotalOnItems: number = 0; let totalTaxOnItems: number = 0; let totalDiscountOnItems: number = 0; let subtotalOnLabor: number = 0; let totalTaxOnLabor: number = 0; let totalDiscountOnLabor: number = 0; let total: number = 0; let subtotal: number = 0; let totalTax: number = 0; let totalDiscount: number = 0; let roundOff: number = 0; let totalAfterRounded: number = 0; // Returns totals let returnSubtotal: number = 0; let nonReturnSubTotal: number = 0; let returnTax: number = 0; let nonReturnTax: number = 0; let returnTotal: number = 0; let nonReturnTotal: number = 0; let returnTotalDiscount: number = 0; let nonReturnTotalDiscount: number = 0; // Tax breakdown totals - only calculate if taxable let totalSGST: number = 0; let totalCGST: number = 0; let totalIGST: number = 0; let totalSGSTOnItems: number = 0; let totalCGSTOnItems: number = 0; let totalIGSTOnItems: number = 0; let totalSGSTOnLabor: number = 0; let totalCGSTOnLabor: number = 0; let totalIGSTOnLabor: number = 0; // Calculate items totals with null safety safeItems.forEach((item: any) => { const itemDisc = GetNumber(item.Disc); const itemRecDisc = GetNumber(item.RecDisc); const itemUnAmt = GetNumber(item.UnAmt); // Calculate return total for items with Ret = true (explicitly check for true) if (item.Ret === true) { const itemNetAmount = Subtract(itemUnAmt, Add(itemDisc, itemRecDisc)); returnTotalDiscount = Add(returnTotalDiscount, itemDisc, itemRecDisc); // Add to return subtotal (net amount after discounts) returnSubtotal = Add(returnSubtotal, itemUnAmt); if (isTaxable) { const resolvedItemTaxes = getResolvedTaxes(item, safeTaxCodes); const itemSGST = getTaxAmountByCode(resolvedItemTaxes, "SGST"); const itemCGST = getTaxAmountByCode(resolvedItemTaxes, "CGST"); const itemIGST = getTaxAmountByCode(resolvedItemTaxes, "IGST"); const itemTotalTax = Add(itemSGST, itemCGST, itemIGST); // Add to return tax returnTax = Add(returnTax, itemTotalTax); // Return total includes net amount + tax returnTotal = Add(returnTotal, itemNetAmount, itemTotalTax); } else { // Return total without tax (same as returnSubtotal when not taxable) returnTotal = Add(returnTotal, itemNetAmount); } } else { // Process normal items (Ret is false, undefined, or null - all treated as normal items) totalDiscountOnItems = Add(totalDiscountOnItems, itemDisc, itemRecDisc); subtotalOnItems = Add(subtotalOnItems, itemUnAmt); // Only calculate tax if taxable if (isTaxable) { const resolvedItemTaxes = getResolvedTaxes(item, safeTaxCodes); const itemSGST = getTaxAmountByCode(resolvedItemTaxes, "SGST"); const itemCGST = getTaxAmountByCode(resolvedItemTaxes, "CGST"); const itemIGST = getTaxAmountByCode(resolvedItemTaxes, "IGST"); // Add individual tax amounts for items totalSGSTOnItems = Add(totalSGSTOnItems, itemSGST); totalCGSTOnItems = Add(totalCGSTOnItems, itemCGST); totalIGSTOnItems = Add(totalIGSTOnItems, itemIGST); totalTaxOnItems = Add(totalTaxOnItems, itemSGST, itemCGST, itemIGST); } } }); // Calculate operations/labor totals with null safety safeOps.forEach((op: any) => { const opDisc = GetNumber(op.Disc); const opRecDisc = GetNumber(op.RecDisc); const opAmt = GetNumber(op.Amt); totalDiscountOnLabor = Add(totalDiscountOnLabor, opDisc, opRecDisc); subtotalOnLabor = Add(subtotalOnLabor, opAmt); // Only calculate tax if taxable if (isTaxable) { const resolvedOpTaxes = getResolvedTaxes(op, safeTaxCodes); const opSGST = getTaxAmountByCode(resolvedOpTaxes, "SGST"); const opCGST = getTaxAmountByCode(resolvedOpTaxes, "CGST"); const opIGST = getTaxAmountByCode(resolvedOpTaxes, "IGST"); // Add individual tax amounts for operations/labor totalSGSTOnLabor = Add(totalSGSTOnLabor, opSGST); totalCGSTOnLabor = Add(totalCGSTOnLabor, opCGST); totalIGSTOnLabor = Add(totalIGSTOnLabor, opIGST); totalTaxOnLabor = Add(totalTaxOnLabor, opSGST, opCGST, opIGST); } }); // Calculate combined totals totalDiscount = Add(totalDiscountOnItems, totalDiscountOnLabor); totalTax = Add(totalTaxOnItems, totalTaxOnLabor); subtotal = Add(subtotalOnItems, subtotalOnLabor); nonReturnSubTotal = subtotal; nonReturnTax = totalTax; nonReturnTotalDiscount = totalDiscount; // Step 2: Add tax if taxable nonReturnTotal = isTaxable ? Add(Subtract(subtotal, totalDiscount), totalTax) : Subtract(subtotal, totalDiscount); // Subtract return subtotal from subtotal subtotal = Subtract(subtotal, returnSubtotal); // Subtract return tax from total tax totalTax = Subtract(totalTax, returnTax); // Subtract return Discount from total discount totalDiscount = Subtract(totalDiscount, returnTotalDiscount); // Calculate combined tax breakdown totals only if taxable if (isTaxable) { totalSGST = Add(totalSGSTOnItems, totalSGSTOnLabor); totalCGST = Add(totalCGSTOnItems, totalCGSTOnLabor); totalIGST = Add(totalIGSTOnItems, totalIGSTOnLabor); } // FIXED CALCULATION: Correct order of operations // Step 1: Calculate net amount after discount const netAmountAfterDiscount = Subtract(subtotal, totalDiscount); // Step 2: Add tax if taxable total = isTaxable ? Add(netAmountAfterDiscount, totalTax) : netAmountAfterDiscount; // Step 3: Add adjustments total = Add(total, GetNumber(Adjust)); // // Step 4: Subtract return total from the final total // total = Subtract(total, returnTotal); // FIXED ROUNDING: Use Math.round instead of Math.floor const roundedTotal = Math.round(total); roundOff = Subtract(roundedTotal, total); // Update total to rounded value totalAfterRounded = roundedTotal; // Base return object const result: any = { total, totalAfterRounded, subtotal, totalDiscount, roundOff, subtotalOnItems, subtotalOnLabor, totalDiscountOnItems, totalDiscountOnLabor, netAmountAfterDiscount, returnSubtotal, returnTax, returnTotal, returnTotalDiscount, nonReturnSubTotal, nonReturnTax, nonReturnTotal, nonReturnTotalDiscount }; // Only include tax fields and taxSummary if taxable if (isTaxable) { result.totalTax = totalTax; result.totalTaxOnItems = totalTaxOnItems; result.totalTaxOnLabor = totalTaxOnLabor; result.totalSGST = totalSGST; result.totalCGST = totalCGST; result.totalIGST = totalIGST; result.totalSGSTOnItems = totalSGSTOnItems; result.totalCGSTOnItems = totalCGSTOnItems; result.totalIGSTOnItems = totalIGSTOnItems; result.totalSGSTOnLabor = totalSGSTOnLabor; result.totalCGSTOnLabor = totalCGSTOnLabor; result.totalIGSTOnLabor = totalIGSTOnLabor; // Calculate tax summary for both new Taxes[] and legacy tax-code based records // Filter out items where Ret is true (only include non-return items) const nonReturnItems = safeItems.filter((item: any) => item.Ret !== true); result.itemsTaxSummary = CalculateTaxSummary(nonReturnItems, safeTaxCodes); result.opsTaxSummary = CalculateTaxSummary(safeOps, safeTaxCodes); } return result; } /** * Enhanced version with decimal places control */ export function TotalCalculationsWithDecimals( items: any[] = [], ops: any[] = [], isTaxable: boolean = false, taxCodes: any[] = [], Adjust: number, decimalPlaces: number = 2, ) { const result = TotalCalculations(items, ops, isTaxable, taxCodes, Adjust); // Round all values to specified decimal places const roundToDecimals = (num: number) => Math.round(num * Math.pow(10, decimalPlaces)) / Math.pow(10, decimalPlaces); // Base rounded result const roundedResult: any = { total: roundToDecimals(result.total), totalAfterRounded: roundToDecimals(result.totalAfterRounded), subtotal: roundToDecimals(result.subtotal), totalDiscount: roundToDecimals(result.totalDiscount), roundOff: roundToDecimals(result.roundOff), subtotalOnItems: roundToDecimals(result.subtotalOnItems), subtotalOnLabor: roundToDecimals(result.subtotalOnLabor), totalDiscountOnItems: roundToDecimals(result.totalDiscountOnItems), totalDiscountOnLabor: roundToDecimals(result.totalDiscountOnLabor), // returned related fields netAmountAfterDiscount: roundToDecimals(result.netAmountAfterDiscount), returnSubtotal: roundToDecimals(result.returnSubtotal), returnTax: roundToDecimals(result.returnTax), returnTotal: roundToDecimals(result.returnTotal), returnTotalDiscount: roundToDecimals(result.returnTotalDiscount), nonReturnSubTotal: roundToDecimals(result.nonReturnSubTotal), nonReturnTax: roundToDecimals(result.nonReturnTax), nonReturnTotal: roundToDecimals(result.nonReturnTotal), nonReturnTotalDiscount: roundToDecimals(result.nonReturnTotalDiscount) }; // Only include tax fields and taxSummary if taxable if (isTaxable) { roundedResult.totalTax = roundToDecimals(result.totalTax); roundedResult.totalTaxOnItems = roundToDecimals(result.totalTaxOnItems); roundedResult.totalTaxOnLabor = roundToDecimals(result.totalTaxOnLabor); roundedResult.totalSGST = roundToDecimals(result.totalSGST); roundedResult.totalCGST = roundToDecimals(result.totalCGST); roundedResult.totalIGST = roundToDecimals(result.totalIGST); roundedResult.totalSGSTOnItems = roundToDecimals(result.totalSGSTOnItems); roundedResult.totalCGSTOnItems = roundToDecimals(result.totalCGSTOnItems); roundedResult.totalIGSTOnItems = roundToDecimals(result.totalIGSTOnItems); roundedResult.totalSGSTOnLabor = roundToDecimals(result.totalSGSTOnLabor); roundedResult.totalCGSTOnLabor = roundToDecimals(result.totalCGSTOnLabor); roundedResult.totalIGSTOnLabor = roundToDecimals(result.totalIGSTOnLabor); // Round tax summary only if it exists if (result.itemsTaxSummary) { roundedResult.itemsTaxSummary = result.itemsTaxSummary.map((tax: any) => ({ ...tax, TaxableAmount: roundToDecimals(tax.TaxableAmount), TaxAmount: roundToDecimals(tax.TaxAmount), CGSTAmount: roundToDecimals(tax.CGSTAmount), SGSTAmount: roundToDecimals(tax.SGSTAmount), IGSTAmount: roundToDecimals(tax.IGSTAmount) })); } if (result.opsTaxSummary) { roundedResult.opsTaxSummary = result.opsTaxSummary.map((tax: any) => ({ ...tax, TaxableAmount: roundToDecimals(tax.TaxableAmount), TaxAmount: roundToDecimals(tax.TaxAmount), CGSTAmount: roundToDecimals(tax.CGSTAmount), SGSTAmount: roundToDecimals(tax.SGSTAmount), IGSTAmount: roundToDecimals(tax.IGSTAmount) })); } } return roundedResult; } export function CalculateTaxSummary(items: any[], taxCodes: any[]) { return Object.values( items.reduce((acc: any, item: any) => { const taxes = getResolvedTaxes(item, taxCodes); if (taxes.length === 0) return acc; const percent = (taxes.reduce((sum: number, tax: any) => Add(sum, GetNumber(tax.Rate)), 0) + "%"); const taxable = getLineNetAmount(item); const cgstAmount = getTaxAmountByCode(taxes, "CGST"); const sgstAmount = getTaxAmountByCode(taxes, "SGST"); const igstAmount = getTaxAmountByCode(taxes, "IGST"); const totalTaxAmount = Add(cgstAmount, sgstAmount, igstAmount); if (!acc[percent]) { acc[percent] = { Tax: percent, TaxableAmount: 0, TaxAmount: 0, CGSTAmount: 0, SGSTAmount: 0, IGSTAmount: 0 }; } acc[percent].TaxableAmount = Add(acc[percent].TaxableAmount, taxable); acc[percent].TaxAmount = Add(acc[percent].TaxAmount, totalTaxAmount); acc[percent].CGSTAmount = Add(acc[percent].CGSTAmount, cgstAmount); acc[percent].SGSTAmount = Add(acc[percent].SGSTAmount, sgstAmount); acc[percent].IGSTAmount = Add(acc[percent].IGSTAmount, igstAmount); return acc; }, {}) ); }