diff --git a/server/rr/rr-calculate-allocations.js b/server/rr/rr-calculate-allocations.js index fed7c54b7..0afdc8005 100644 --- a/server/rr/rr-calculate-allocations.js +++ b/server/rr/rr-calculate-allocations.js @@ -1,7 +1,7 @@ /** * THIS IS A COPY of CDKCalculateAllocations, modified to: * - Only calculate allocations needed for Reynolds & RR exports - * - Keep sales broken down into buckets (parts, taxable labor, non-taxable labor, extras) + * - Keep sales broken down into buckets (parts, taxable / non-taxable parts, taxable labor, non-taxable labor, extras) * - Add extra logging for easier debugging * * Original comments follow. @@ -49,14 +49,21 @@ const summarizeAllocationsArray = (arr) => /** * Internal per-center bucket shape for *sales*. * We keep separate buckets for RR so we can split - * taxable vs non-taxable labor lines later. + * taxable vs non-taxable parts and labor lines later. */ function emptyCenterBucket() { const zero = Dinero(); return { - partsSale: zero, // parts sale + // Parts + partsSale: zero, // total parts (taxable + non-taxable) + partsTaxableSale: zero, // parts that should be taxed in RR + partsNonTaxableSale: zero, // parts that should NOT be taxed in RR + + // Labor laborTaxableSale: zero, // labor that should be taxed in RR laborNonTaxableSale: zero, // labor that should NOT be taxed in RR + + // Extras extrasSale: zero // MAPA/MASH/towing/storage/PAO/etc }; } @@ -161,6 +168,15 @@ function isLaborTaxable(line) { return line.tax_part; } +/** + * Decide if a *part* line is taxable vs non-taxable for RR. + * For now we mirror the same flag; this can be extended with CAD-specific + * logic (federal_tax_rate, parts_tax_rate, prt_tax_in, etc.) later. + */ +function isPartTaxable(line) { + return line.tax_part; +} + /** * Build profitCenterHash from joblines (parts + labor) and detect MAPA/MASH presence. * Now stores *buckets* instead of a single Dinero per center. @@ -215,6 +231,15 @@ function buildProfitCenterHash(job, debugLog) { amount = amount.add(discount); } + const taxable = isPartTaxable(val); + + if (taxable) { + bucket.partsTaxableSale = bucket.partsTaxableSale.add(amount); + } else { + bucket.partsNonTaxableSale = bucket.partsNonTaxableSale.add(amount); + } + + // Keep total parts for compatibility / convenience bucket.partsSale = bucket.partsSale.add(amount); } @@ -245,6 +270,8 @@ function buildProfitCenterHash(job, debugLog) { centers: Object.entries(profitCenterHash).map(([center, b]) => ({ center, parts: summarizeMoney(b.partsSale), + partsTaxable: summarizeMoney(b.partsTaxableSale), + partsNonTaxable: summarizeMoney(b.partsNonTaxableSale), laborTaxable: summarizeMoney(b.laborTaxableSale), laborNonTaxable: summarizeMoney(b.laborNonTaxableSale), extras: summarizeMoney(b.extrasSale) @@ -521,14 +548,6 @@ function applyExtras({ job, bodyshop, selectedDmsAllocationConfig, profitCenterH return { profitCenterHash, taxAllocations }; } -/** - * Apply Rome-specific profile adjustments (parts + rates). - * These also feed into the *sales* buckets. - */ -/** - * Apply Rome-specific profile adjustments (parts + rates). - * These also feed into the *sales* buckets. - */ /** * Apply Rome-specific profile adjustments (parts + rates). * These also feed into the *sales* buckets. @@ -627,6 +646,8 @@ function applyRomeProfileAdjustments({ * { * center, * partsSale, + * partsTaxableSale, + * partsNonTaxableSale, * laborTaxableSale, * laborNonTaxableSale, * extrasSale, @@ -653,10 +674,18 @@ function buildJobAllocations(bodyshop, profitCenterHash, costCenterHash, debugLo return { center, + // Parts partsSale: bucket.partsSale, + partsTaxableSale: bucket.partsTaxableSale, + partsNonTaxableSale: bucket.partsNonTaxableSale, + + // Labor laborTaxableSale: bucket.laborTaxableSale, laborNonTaxableSale: bucket.laborNonTaxableSale, + + // Extras extrasSale: bucket.extrasSale, + totalSale, cost: costCenterHash[center] || Dinero(), @@ -671,6 +700,8 @@ function buildJobAllocations(bodyshop, profitCenterHash, costCenterHash, debugLo jobAllocations.map((row) => ({ center: row.center, parts: summarizeMoney(row.partsSale), + partsTaxable: summarizeMoney(row.partsTaxableSale), + partsNonTaxable: summarizeMoney(row.partsNonTaxableSale), laborTaxable: summarizeMoney(row.laborTaxableSale), laborNonTaxable: summarizeMoney(row.laborNonTaxableSale), extras: summarizeMoney(row.extrasSale), @@ -847,6 +878,8 @@ function calculateAllocations(connectionData, job) { centers: Object.entries(profitCenterHash || {}).map(([center, b]) => ({ center, parts: summarizeMoney(b.partsSale), + partsTaxable: summarizeMoney(b.partsTaxableSale), + partsNonTaxable: summarizeMoney(b.partsNonTaxableSale), laborTaxable: summarizeMoney(b.laborTaxableSale), laborNonTaxable: summarizeMoney(b.laborNonTaxableSale), extras: summarizeMoney(b.extrasSale) diff --git a/server/rr/rr-job-helpers.js b/server/rr/rr-job-helpers.js index c85e50ea0..d9fc523a8 100644 --- a/server/rr/rr-job-helpers.js +++ b/server/rr/rr-job-helpers.js @@ -60,6 +60,8 @@ const asN2 = (dineroLike) => { * { * center, * partsSale, + * partsTaxableSale, + * partsNonTaxableSale, * laborTaxableSale, * laborNonTaxableSale, * extrasSale, @@ -69,19 +71,21 @@ const asN2 = (dineroLike) => { * costCenter * } * - * For each center, we can emit up to 3 GOG *segments*: - * - parts+extras (uses profitCenter.rr_cust_txbl_flag) - * - taxable labor (CustTxblNTxblFlag="T") - * - non-tax labor (CustTxblNTxblFlag="N") + * For each center, we can emit up to 5 GOG *segments*: + * - taxable parts (CustTxblNTxblFlag="T") + * - non-taxable parts (CustTxblNTxblFlag="N") + * - extras (uses profitCenter.rr_cust_txbl_flag) + * - taxable labor (CustTxblNTxblFlag="T") + * - non-tax labor (CustTxblNTxblFlag="N") * - * IMPORTANT CHANGE: + * IMPORTANT: * Each segment becomes its OWN JobNo / AllGogOpCodeInfo, with exactly one - * AllGogLineItmInfo inside. This makes the count of: + * AllGogLineItmInfo inside. This keeps a clean 1:1 mapping between: * - (ROGOG) * - (ROLABOR) - * match 1:1, and ensures taxable/non-taxable flags line up by JobNo. + * and ensures taxable/non-taxable flags line up by JobNo. * - * We now also attach segmentKind/segmentIndex/segmentCount metadata on each op + * We attach segmentKind/segmentIndex/segmentCount metadata on each op * for UI/debug purposes. The XML templates can safely ignore these. * * @param {Array} allocations @@ -147,27 +151,43 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo // Only centers configured for RR GOG are included if (!breakOut || !itemType) continue; - const partsCents = toCents(alloc.partsSale); + const partsTaxableCents = toCents(alloc.partsTaxableSale); + const partsNonTaxableCents = toCents(alloc.partsNonTaxableSale); const extrasCents = toCents(alloc.extrasSale); const laborTaxableCents = toCents(alloc.laborTaxableSale); const laborNonTaxableCents = toCents(alloc.laborNonTaxableSale); const costCents = toCents(alloc.cost); - // Parts + extras share a single segment - const partsExtrasCents = partsCents + extrasCents; - const segments = []; - // 1) Parts + extras segment (respect center's default tax flag) - if (partsExtrasCents !== 0) { + // 1) Taxable parts segment -> "T" + if (partsTaxableCents !== 0) { segments.push({ - kind: "partsExtras", - saleCents: partsExtrasCents, + kind: "partsTaxable", + saleCents: partsTaxableCents, + txFlag: "T" + }); + } + + // 2) Non-taxable parts segment -> "N" + if (partsNonTaxableCents !== 0) { + segments.push({ + kind: "partsNonTaxable", + saleCents: partsNonTaxableCents, + txFlag: "N" + }); + } + + // 3) Extras segment (respect center's default tax flag) + if (extrasCents !== 0) { + segments.push({ + kind: "extras", + saleCents: extrasCents, txFlag: pc.rr_cust_txbl_flag || "N" }); } - // 2) Taxable labor segment -> "T" + // 4) Taxable labor segment -> "T" if (laborTaxableCents !== 0) { segments.push({ kind: "laborTaxable", @@ -176,7 +196,7 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo }); } - // 3) Non-taxable labor segment -> "N" + // 5) Non-taxable labor segment -> "N" if (laborNonTaxableCents !== 0) { segments.push({ kind: "laborNonTaxable", @@ -254,6 +274,12 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo /** * Build RO.ROLABOR structure for the reynolds-rome-client `createRepairOrder` payload * from an already-built RO.GOG structure. + * + * We still keep a 1:1 mapping with GOG ops: each op gets a corresponding + * OpCodeLaborInfo entry using the same JobNo and the same tax flag as its + * GOG line. Labor-specific details (hrs/rate) remain zeroed out, and the + * DMS can ignore non-labor ops by virtue of the zero hours/amounts. + * * @param {Object} rogg - result of buildRogogFromAllocations * @param {Object} opts * @param {string} [opts.payType="Cust"]