This commit is contained in:
Dave
2025-12-04 14:12:11 -05:00
parent a6b3bd573e
commit e92bab0455
2 changed files with 88 additions and 29 deletions

View File

@@ -1,7 +1,7 @@
/** /**
* THIS IS A COPY of CDKCalculateAllocations, modified to: * THIS IS A COPY of CDKCalculateAllocations, modified to:
* - Only calculate allocations needed for Reynolds & RR exports * - 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 * - Add extra logging for easier debugging
* *
* Original comments follow. * Original comments follow.
@@ -49,14 +49,21 @@ const summarizeAllocationsArray = (arr) =>
/** /**
* Internal per-center bucket shape for *sales*. * Internal per-center bucket shape for *sales*.
* We keep separate buckets for RR so we can split * 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() { function emptyCenterBucket() {
const zero = Dinero(); const zero = Dinero();
return { 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 laborTaxableSale: zero, // labor that should be taxed in RR
laborNonTaxableSale: zero, // labor that should NOT be taxed in RR laborNonTaxableSale: zero, // labor that should NOT be taxed in RR
// Extras
extrasSale: zero // MAPA/MASH/towing/storage/PAO/etc extrasSale: zero // MAPA/MASH/towing/storage/PAO/etc
}; };
} }
@@ -161,6 +168,15 @@ function isLaborTaxable(line) {
return line.tax_part; 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. * Build profitCenterHash from joblines (parts + labor) and detect MAPA/MASH presence.
* Now stores *buckets* instead of a single Dinero per center. * Now stores *buckets* instead of a single Dinero per center.
@@ -215,6 +231,15 @@ function buildProfitCenterHash(job, debugLog) {
amount = amount.add(discount); 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); bucket.partsSale = bucket.partsSale.add(amount);
} }
@@ -245,6 +270,8 @@ function buildProfitCenterHash(job, debugLog) {
centers: Object.entries(profitCenterHash).map(([center, b]) => ({ centers: Object.entries(profitCenterHash).map(([center, b]) => ({
center, center,
parts: summarizeMoney(b.partsSale), parts: summarizeMoney(b.partsSale),
partsTaxable: summarizeMoney(b.partsTaxableSale),
partsNonTaxable: summarizeMoney(b.partsNonTaxableSale),
laborTaxable: summarizeMoney(b.laborTaxableSale), laborTaxable: summarizeMoney(b.laborTaxableSale),
laborNonTaxable: summarizeMoney(b.laborNonTaxableSale), laborNonTaxable: summarizeMoney(b.laborNonTaxableSale),
extras: summarizeMoney(b.extrasSale) extras: summarizeMoney(b.extrasSale)
@@ -521,14 +548,6 @@ function applyExtras({ job, bodyshop, selectedDmsAllocationConfig, profitCenterH
return { profitCenterHash, taxAllocations }; 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). * Apply Rome-specific profile adjustments (parts + rates).
* These also feed into the *sales* buckets. * These also feed into the *sales* buckets.
@@ -627,6 +646,8 @@ function applyRomeProfileAdjustments({
* { * {
* center, * center,
* partsSale, * partsSale,
* partsTaxableSale,
* partsNonTaxableSale,
* laborTaxableSale, * laborTaxableSale,
* laborNonTaxableSale, * laborNonTaxableSale,
* extrasSale, * extrasSale,
@@ -653,10 +674,18 @@ function buildJobAllocations(bodyshop, profitCenterHash, costCenterHash, debugLo
return { return {
center, center,
// Parts
partsSale: bucket.partsSale, partsSale: bucket.partsSale,
partsTaxableSale: bucket.partsTaxableSale,
partsNonTaxableSale: bucket.partsNonTaxableSale,
// Labor
laborTaxableSale: bucket.laborTaxableSale, laborTaxableSale: bucket.laborTaxableSale,
laborNonTaxableSale: bucket.laborNonTaxableSale, laborNonTaxableSale: bucket.laborNonTaxableSale,
// Extras
extrasSale: bucket.extrasSale, extrasSale: bucket.extrasSale,
totalSale, totalSale,
cost: costCenterHash[center] || Dinero(), cost: costCenterHash[center] || Dinero(),
@@ -671,6 +700,8 @@ function buildJobAllocations(bodyshop, profitCenterHash, costCenterHash, debugLo
jobAllocations.map((row) => ({ jobAllocations.map((row) => ({
center: row.center, center: row.center,
parts: summarizeMoney(row.partsSale), parts: summarizeMoney(row.partsSale),
partsTaxable: summarizeMoney(row.partsTaxableSale),
partsNonTaxable: summarizeMoney(row.partsNonTaxableSale),
laborTaxable: summarizeMoney(row.laborTaxableSale), laborTaxable: summarizeMoney(row.laborTaxableSale),
laborNonTaxable: summarizeMoney(row.laborNonTaxableSale), laborNonTaxable: summarizeMoney(row.laborNonTaxableSale),
extras: summarizeMoney(row.extrasSale), extras: summarizeMoney(row.extrasSale),
@@ -847,6 +878,8 @@ function calculateAllocations(connectionData, job) {
centers: Object.entries(profitCenterHash || {}).map(([center, b]) => ({ centers: Object.entries(profitCenterHash || {}).map(([center, b]) => ({
center, center,
parts: summarizeMoney(b.partsSale), parts: summarizeMoney(b.partsSale),
partsTaxable: summarizeMoney(b.partsTaxableSale),
partsNonTaxable: summarizeMoney(b.partsNonTaxableSale),
laborTaxable: summarizeMoney(b.laborTaxableSale), laborTaxable: summarizeMoney(b.laborTaxableSale),
laborNonTaxable: summarizeMoney(b.laborNonTaxableSale), laborNonTaxable: summarizeMoney(b.laborNonTaxableSale),
extras: summarizeMoney(b.extrasSale) extras: summarizeMoney(b.extrasSale)

View File

@@ -60,6 +60,8 @@ const asN2 = (dineroLike) => {
* { * {
* center, * center,
* partsSale, * partsSale,
* partsTaxableSale,
* partsNonTaxableSale,
* laborTaxableSale, * laborTaxableSale,
* laborNonTaxableSale, * laborNonTaxableSale,
* extrasSale, * extrasSale,
@@ -69,19 +71,21 @@ const asN2 = (dineroLike) => {
* costCenter * costCenter
* } * }
* *
* For each center, we can emit up to 3 GOG *segments*: * For each center, we can emit up to 5 GOG *segments*:
* - parts+extras (uses profitCenter.rr_cust_txbl_flag) * - taxable parts (CustTxblNTxblFlag="T")
* - taxable labor (CustTxblNTxblFlag="T") * - non-taxable parts (CustTxblNTxblFlag="N")
* - non-tax labor (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 * 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:
* - <AllGogOpCodeInfo> (ROGOG) * - <AllGogOpCodeInfo> (ROGOG)
* - <OpCodeLaborInfo> (ROLABOR) * - <OpCodeLaborInfo> (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. * for UI/debug purposes. The XML templates can safely ignore these.
* *
* @param {Array} allocations * @param {Array} allocations
@@ -147,27 +151,43 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo
// Only centers configured for RR GOG are included // Only centers configured for RR GOG are included
if (!breakOut || !itemType) continue; 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 extrasCents = toCents(alloc.extrasSale);
const laborTaxableCents = toCents(alloc.laborTaxableSale); const laborTaxableCents = toCents(alloc.laborTaxableSale);
const laborNonTaxableCents = toCents(alloc.laborNonTaxableSale); const laborNonTaxableCents = toCents(alloc.laborNonTaxableSale);
const costCents = toCents(alloc.cost); const costCents = toCents(alloc.cost);
// Parts + extras share a single segment
const partsExtrasCents = partsCents + extrasCents;
const segments = []; const segments = [];
// 1) Parts + extras segment (respect center's default tax flag) // 1) Taxable parts segment -> "T"
if (partsExtrasCents !== 0) { if (partsTaxableCents !== 0) {
segments.push({ segments.push({
kind: "partsExtras", kind: "partsTaxable",
saleCents: partsExtrasCents, 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" txFlag: pc.rr_cust_txbl_flag || "N"
}); });
} }
// 2) Taxable labor segment -> "T" // 4) Taxable labor segment -> "T"
if (laborTaxableCents !== 0) { if (laborTaxableCents !== 0) {
segments.push({ segments.push({
kind: "laborTaxable", 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) { if (laborNonTaxableCents !== 0) {
segments.push({ segments.push({
kind: "laborNonTaxable", kind: "laborNonTaxable",
@@ -254,6 +274,12 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo
/** /**
* Build RO.ROLABOR structure for the reynolds-rome-client `createRepairOrder` payload * Build RO.ROLABOR structure for the reynolds-rome-client `createRepairOrder` payload
* from an already-built RO.GOG structure. * 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} rogg - result of buildRogogFromAllocations
* @param {Object} opts * @param {Object} opts
* @param {string} [opts.payType="Cust"] * @param {string} [opts.payType="Cust"]