RrScratch3 - Tax / Extras Improvements

This commit is contained in:
Dave
2025-12-05 13:17:25 -05:00
parent 56738f800c
commit 288c8e6347
3 changed files with 377 additions and 80 deletions

View File

@@ -18,7 +18,21 @@ export default connect(mapStateToProps, mapDispatchToProps)(RrAllocationsSummary
/**
* Normalize job allocations into a flat list for display / preview building.
* @param ack
* @returns {{center: *, sale, partsSale, laborTaxableSale, laborNonTaxableSale, extrasSale, cost, profitCenter, costCenter}[]|*[]}
* @returns {{
* center: *,
* sale: *,
* partsSale: *,
* partsTaxableSale: *,
* partsNonTaxableSale: *,
* laborTaxableSale: *,
* laborNonTaxableSale: *,
* extrasSale: *,
* extrasTaxableSale: *,
* extrasNonTaxableSale: *,
* cost: *,
* profitCenter: *,
* costCenter: *
* }[]|*[]}
*/
function normalizeJobAllocations(ack) {
if (!ack || !Array.isArray(ack.jobAllocations)) return [];
@@ -31,9 +45,13 @@ function normalizeJobAllocations(ack) {
// bucketed sales used to build split ROGOG/ROLABOR
partsSale: row.partsSale || null,
partsTaxableSale: row.partsTaxableSale || null,
partsNonTaxableSale: row.partsNonTaxableSale || null,
laborTaxableSale: row.laborTaxableSale || null,
laborNonTaxableSale: row.laborNonTaxableSale || null,
extrasSale: row.extrasSale || null,
extrasTaxableSale: row.extrasTaxableSale || null,
extrasNonTaxableSale: row.extrasNonTaxableSale || null,
cost: row.cost || null,
profitCenter: row.profitCenter || null,
@@ -111,9 +129,12 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat
}, [fetchAllocations]);
const segmentLabelMap = {
partsExtras: "Parts/Extras",
laborTaxable: "Taxable Labor",
laborNonTaxable: "Non-Taxable Labor"
partsTaxable: "Parts Taxable",
partsNonTaxable: "Parts Non-Taxable",
extrasTaxable: "Extras Taxable",
extrasNonTaxable: "Extras Non-Taxable",
laborTaxable: "Labor Taxable",
laborNonTaxable: "Labor Non-Taxable"
};
const roggRows = useMemo(() => {
@@ -149,7 +170,7 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat
});
});
return rows;
}, [roggPreview, opCode]);
}, [roggPreview, opCode, segmentLabelMap]);
const rolaborRows = useMemo(() => {
if (!rolaborPreview || !Array.isArray(rolaborPreview.ops)) return [];
@@ -231,7 +252,8 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat
<>
<Typography.Paragraph type="secondary" style={{ marginBottom: 8 }}>
OpCode: <strong>{effectiveOpCode}</strong>. Only centers with RR GOG mapping (rr_gogcode &amp; rr_item_type)
are included. Totals below reflect exactly what will be sent in ROGOG.
are included. Totals below reflect exactly what will be sent in ROGOG, with parts, extras, and labor split
into taxable / non-taxable segments.
</Typography.Paragraph>
<Table

View File

@@ -63,8 +63,10 @@ function emptyCenterBucket() {
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
// Extras (MAPA/MASH/towing/PAO/etc)
extrasSale: zero, // total extras (taxable + non-taxable)
extrasTaxableSale: zero,
extrasNonTaxableSale: zero
};
}
@@ -162,26 +164,228 @@ function buildTaxAllocations(bodyshop, job) {
}
/**
* Decide if a labor line is taxable vs non-taxable for RR.
* ============================
* Tax Context & Helpers
* ============================
*/
function isLaborTaxable(line) {
return line.tax_part;
/**
* Build a small "tax context" object from the job + current instance.
*
* This centralises all of the "is this category taxable?" logic so that
* the rest of the allocation code just asks simple yes/no questions.
*
* IMPORTANT: we are **not** calculating any tax **amounts** here that is
* still handled by job-costing. We only need to know if a given sale bucket
* should be treated as taxable vs non-taxable for RR (CustTxblNTxblFlag).
*/
function buildTaxContext(job = {}) {
const isImex = !!InstanceManager({ imex: true }); // Canada
const isRome = !!InstanceManager({ rome: true }); // US
const toNumber = (v) => (v == null ? 0 : Number(v) || 0);
const federalTaxRate = toNumber(job.federal_tax_rate);
const stateTaxRate = toNumber(job.state_tax_rate);
const localTaxRate = toNumber(job.local_tax_rate);
const hasFederalRate = federalTaxRate > 0;
const hasState = stateTaxRate > 0;
const hasLocal = localTaxRate > 0;
// "hasFederal" kept for backwards compatibility / logging (Canada only)
const hasFederal = isImex && hasFederalRate;
// Canada: if ANY of federal / state / local > 0, treat the job as
// "everything taxable by default", then let line-level flags override
// for parts where applicable.
const globalAllTaxCanada = isImex && (hasFederalRate || hasState || hasLocal);
const hasAnySalesTax = hasFederalRate || hasState || hasLocal;
// Parts tax rate map (PAA/PAC/…)
let partTaxRates = job.part_tax_rates || job.parts_tax_rates || {};
if (typeof partTaxRates === "string") {
try {
partTaxRates = JSON.parse(partTaxRates);
} catch {
partTaxRates = {};
}
}
if (!partTaxRates || typeof partTaxRates !== "object") {
partTaxRates = {};
}
const tax_lbr_rt = toNumber(job.tax_lbr_rt); // labour
const tax_paint_mat_rt = toNumber(job.tax_paint_mat_rt || job.tax_paint_mt_rate); // MAPA
const tax_shop_mat_rt = toNumber(job.tax_shop_mat_rt); // MASH
const tax_tow_rt = toNumber(job.tax_tow_rt); // towing
const tax_sub_rt = toNumber(job.tax_sub_rt); // sublet (rarely used directly)
const hasAnyPartsWithTax = Object.values(partTaxRates).some(
(entry) => entry && entry.prt_tax_in && toNumber(entry.prt_tax_rt) > 0
);
const hasAnyTax =
hasAnySalesTax ||
tax_lbr_rt > 0 ||
tax_paint_mat_rt > 0 ||
tax_shop_mat_rt > 0 ||
tax_tow_rt > 0 ||
tax_sub_rt > 0 ||
hasAnyPartsWithTax;
return {
isImex,
isRome,
federalTaxRate,
stateTaxRate,
localTaxRate,
hasFederal,
hasState,
hasLocal,
hasAnySalesTax,
globalAllTaxCanada,
partTaxRates,
tax_lbr_rt,
tax_paint_mat_rt,
tax_shop_mat_rt,
tax_tow_rt,
tax_sub_rt,
hasAnyPartsWithTax,
hasAnyTax
};
}
/**
* Resolve the "PA" / part-type code (PAA/PAC/…) from a job line.
*/
function resolvePartType(line = {}) {
return line.part_type || line.partType || line.pa_code || line.pa || null;
}
/**
* 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.
*
* Rules:
* - Canada (IMEX):
* - If ANY of federal_tax_rate / state_tax_rate / local_tax_rate > 0
* => everything is taxable by default (globalAllTaxCanada),
* unless tax_part is explicitly false.
* - Otherwise, use part_tax_rates[part_type] (prt_tax_in && prt_tax_rt > 0),
* with tax_part as final override.
* - US (ROME):
* - Use part_tax_rates[part_type] (prt_tax_in && prt_tax_rt > 0),
* with tax_part as final override.
*
* - line.tax_part is treated as the *final* check:
* - tax_part === false => always non-taxable.
* - tax_part === true => always taxable, even if we have no table entry.
*/
function isPartTaxable(line) {
return line.tax_part;
function isPartTaxable(line = {}, taxCtx) {
if (!taxCtx) return !!line.tax_part;
const { globalAllTaxCanada, partTaxRates } = taxCtx;
// Explicit per-line override to *not* tax.
if (typeof line.tax_part === "boolean" && line.tax_part === false) {
return false;
}
// Canada: any federal/state/local tax rate set => all parts taxable,
// unless explicitly turned off above.
if (globalAllTaxCanada) {
return true;
}
let taxable = false;
const partType = resolvePartType(line);
if (partType && partTaxRates && partTaxRates[partType]) {
const entry = partTaxRates[partType];
const rate = Number(entry?.prt_tax_rt || 0);
const indicator = !!entry?.prt_tax_in;
taxable = indicator && rate > 0;
}
// tax_part === true is treated as "final yes" even if we didn't find
// a matching part_tax_rate entry.
if (typeof line.tax_part === "boolean" && line.tax_part === true) {
taxable = true;
}
return taxable;
}
/**
* Decide if *labour* for this job is taxable.
*
* - Canada (IMEX):
* - If ANY of federal_tax_rate / state_tax_rate / local_tax_rate > 0
* (globalAllTaxCanada) => all labour is taxable.
* - Else if tax_lbr_rt > 0 => labour taxable.
* - Else => non-taxable.
* - US (ROME):
* - tax_lbr_rt > 0 => labour taxable, otherwise not.
*/
function isLaborTaxable(_line, taxCtx) {
if (!taxCtx) return false;
const { isImex, globalAllTaxCanada, tax_lbr_rt } = taxCtx;
if (isImex && globalAllTaxCanada) return true;
return tax_lbr_rt > 0;
}
/**
* Taxability helpers for "extras" buckets.
* These are all job-level decisions; there are no per-line flags for them
* in the data we currently work with.
*
* Canada: if globalAllTaxCanada is true, we treat these as taxable.
*/
function isMapaTaxable(taxCtx) {
if (!taxCtx) return false;
const { isImex, globalAllTaxCanada, tax_paint_mat_rt } = taxCtx;
if (isImex && globalAllTaxCanada) return true;
return tax_paint_mat_rt > 0;
}
function isMashTaxable(taxCtx) {
if (!taxCtx) return false;
const { isImex, globalAllTaxCanada, tax_shop_mat_rt } = taxCtx;
if (isImex && globalAllTaxCanada) return true;
return tax_shop_mat_rt > 0;
}
function isTowTaxable(taxCtx) {
if (!taxCtx) return false;
const { isImex, globalAllTaxCanada, tax_tow_rt } = taxCtx;
if (isImex && globalAllTaxCanada) return true;
return tax_tow_rt > 0;
}
/**
* Helper to push an "extra" (MAPA/MASH/towing/PAO/etc) amount into the
* appropriate taxable / non-taxable buckets for a given center.
*/
function addExtras(bucket, dineroAmount, isTaxable) {
if (!bucket || !dineroAmount || typeof dineroAmount.add !== "function") return;
bucket.extrasSale = bucket.extrasSale.add(dineroAmount);
if (isTaxable) {
bucket.extrasTaxableSale = bucket.extrasTaxableSale.add(dineroAmount);
} else {
bucket.extrasNonTaxableSale = bucket.extrasNonTaxableSale.add(dineroAmount);
}
}
/**
* Build profitCenterHash from joblines (parts + labor) and detect MAPA/MASH presence.
* Now stores *buckets* instead of a single Dinero per center.
*/
function buildProfitCenterHash(job, debugLog) {
function buildProfitCenterHash(job, debugLog, taxContext) {
let hasMapaLine = false;
let hasMashLine = false;
@@ -231,7 +435,7 @@ function buildProfitCenterHash(job, debugLog) {
amount = amount.add(discount);
}
const taxable = isPartTaxable(val);
const taxable = isPartTaxable(val, taxContext);
if (taxable) {
bucket.partsTaxableSale = bucket.partsTaxableSale.add(amount);
@@ -254,7 +458,7 @@ function buildProfitCenterHash(job, debugLog) {
amount: Math.round(rate * 100)
}).multiply(val.mod_lb_hrs);
if (isLaborTaxable(val)) {
if (isLaborTaxable(val, taxContext)) {
bucket.laborTaxableSale = bucket.laborTaxableSale.add(laborAmount);
} else {
bucket.laborNonTaxableSale = bucket.laborNonTaxableSale.add(laborAmount);
@@ -274,7 +478,9 @@ function buildProfitCenterHash(job, debugLog) {
partsNonTaxable: summarizeMoney(b.partsNonTaxableSale),
laborTaxable: summarizeMoney(b.laborTaxableSale),
laborNonTaxable: summarizeMoney(b.laborNonTaxableSale),
extras: summarizeMoney(b.extrasSale)
extras: summarizeMoney(b.extrasSale),
extrasTaxable: summarizeMoney(b.extrasTaxableSale),
extrasNonTaxable: summarizeMoney(b.extrasNonTaxableSale)
}))
});
@@ -353,39 +559,48 @@ function applyMapaMashManualLines({
profitCenterHash,
hasMapaLine,
hasMashLine,
debugLog
debugLog,
taxContext
}) {
// MAPA
// MAPA (paint materials)
if (!hasMapaLine && job.job_totals.rates.mapa.total.amount > 0) {
const mapaAccountName = selectedDmsAllocationConfig.profits.MAPA;
const mapaAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === mapaAccountName);
if (mapaAccount) {
const amount = Dinero(job.job_totals.rates.mapa.total);
const taxable = isMapaTaxable(taxContext);
debugLog("Adding MAPA Line Manually", {
mapaAccountName,
amount: summarizeMoney(Dinero(job.job_totals.rates.mapa.total))
amount: summarizeMoney(amount),
taxable
});
const bucket = ensureCenterBucket(profitCenterHash, mapaAccountName);
bucket.extrasSale = bucket.extrasSale.add(Dinero(job.job_totals.rates.mapa.total));
addExtras(bucket, amount, taxable);
} else {
debugLog("NO MAPA ACCOUNT FOUND!!", { mapaAccountName });
}
}
// MASH
// MASH (shop materials)
if (!hasMashLine && job.job_totals.rates.mash.total.amount > 0) {
const mashAccountName = selectedDmsAllocationConfig.profits.MASH;
const mashAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === mashAccountName);
if (mashAccount) {
const amount = Dinero(job.job_totals.rates.mash.total);
const taxable = isMashTaxable(taxContext);
debugLog("Adding MASH Line Manually", {
mashAccountName,
amount: summarizeMoney(Dinero(job.job_totals.rates.mash.total))
amount: summarizeMoney(amount),
taxable
});
const bucket = ensureCenterBucket(profitCenterHash, mashAccountName);
bucket.extrasSale = bucket.extrasSale.add(Dinero(job.job_totals.rates.mash.total));
addExtras(bucket, amount, taxable);
} else {
debugLog("NO MASH ACCOUNT FOUND!!", { mashAccountName });
}
@@ -467,9 +682,17 @@ function applyMaterialsCosting({ job, bodyshop, selectedDmsAllocationConfig, cos
/**
* Apply non-tax extras (PVRT, towing, storage, PAO).
* Extras go into the extrasSale bucket.
* Extras go into the extrasSale bucket (split taxable / non-taxable).
*/
function applyExtras({ job, bodyshop, selectedDmsAllocationConfig, profitCenterHash, taxAllocations, debugLog }) {
function applyExtras({
job,
bodyshop,
selectedDmsAllocationConfig,
profitCenterHash,
taxAllocations,
debugLog,
taxContext
}) {
const { ca_bc_pvrt } = job;
// BC PVRT -> state tax
@@ -485,17 +708,19 @@ function applyExtras({ job, bodyshop, selectedDmsAllocationConfig, profitCenterH
const towAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === towAccountName);
if (towAccount) {
const amount = Dinero({
amount: Math.round((job.towing_payable || 0) * 100)
});
const taxable = isTowTaxable(taxContext);
debugLog("Adding towing_payable to TOW account", {
towAccountName,
towing_payable: job.towing_payable
towing_payable: job.towing_payable,
taxable
});
const bucket = ensureCenterBucket(profitCenterHash, towAccountName);
bucket.extrasSale = bucket.extrasSale.add(
Dinero({
amount: Math.round((job.towing_payable || 0) * 100)
})
);
addExtras(bucket, amount, taxable);
} else {
debugLog("NO TOW ACCOUNT FOUND!!", { towAccountName });
}
@@ -507,17 +732,19 @@ function applyExtras({ job, bodyshop, selectedDmsAllocationConfig, profitCenterH
const towAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === storageAccountName);
if (towAccount) {
const amount = Dinero({
amount: Math.round((job.storage_payable || 0) * 100)
});
const taxable = isTowTaxable(taxContext);
debugLog("Adding storage_payable to TOW account", {
storageAccountName,
storage_payable: job.storage_payable
storage_payable: job.storage_payable,
taxable
});
const bucket = ensureCenterBucket(profitCenterHash, storageAccountName);
bucket.extrasSale = bucket.extrasSale.add(
Dinero({
amount: Math.round((job.storage_payable || 0) * 100)
})
);
addExtras(bucket, amount, taxable);
} else {
debugLog("NO STORAGE/TOW ACCOUNT FOUND!!", { storageAccountName });
}
@@ -529,17 +756,19 @@ function applyExtras({ job, bodyshop, selectedDmsAllocationConfig, profitCenterH
const otherAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === otherAccountName);
if (otherAccount) {
const amount = Dinero({
amount: Math.round((job.adjustment_bottom_line || 0) * 100)
});
const taxable = !!(taxContext && taxContext.hasAnyTax);
debugLog("Adding adjustment_bottom_line to PAO", {
otherAccountName,
adjustment_bottom_line: job.adjustment_bottom_line
adjustment_bottom_line: job.adjustment_bottom_line,
taxable
});
const bucket = ensureCenterBucket(profitCenterHash, otherAccountName);
bucket.extrasSale = bucket.extrasSale.add(
Dinero({
amount: Math.round((job.adjustment_bottom_line || 0) * 100)
})
);
addExtras(bucket, amount, taxable);
} else {
debugLog("NO PAO ACCOUNT FOUND!!", { otherAccountName });
}
@@ -558,7 +787,8 @@ function applyRomeProfileAdjustments({
selectedDmsAllocationConfig,
profitCenterHash,
debugLog,
connectionData
connectionData,
taxContext
}) {
// Only relevant for Rome instances
if (!InstanceManager({ rome: true })) return profitCenterHash;
@@ -576,6 +806,8 @@ function applyRomeProfileAdjustments({
rateKeys: Object.keys(rateMap)
});
const extrasTaxable = !!(taxContext && taxContext.hasAnyTax);
// Parts adjustments
Object.keys(partsAdjustments).forEach((key) => {
const accountName = selectedDmsAllocationConfig.profits[key];
@@ -585,12 +817,13 @@ function applyRomeProfileAdjustments({
const bucket = ensureCenterBucket(profitCenterHash, accountName);
const adjMoney = Dinero(partsAdjustments[key]);
bucket.extrasSale = bucket.extrasSale.add(adjMoney);
addExtras(bucket, adjMoney, extrasTaxable);
debugLog("Added parts adjustment", {
key,
accountName,
adjustment: summarizeMoney(adjMoney)
adjustment: summarizeMoney(adjMoney),
taxable: extrasTaxable
});
} else {
CreateRRLogEvent(
@@ -619,12 +852,13 @@ function applyRomeProfileAdjustments({
// Note: we intentionally use `rate.adjustments` here to mirror CDK behaviour
const adjMoney = Dinero(rate.adjustments);
bucket.extrasSale = bucket.extrasSale.add(adjMoney);
addExtras(bucket, adjMoney, extrasTaxable);
debugLog("Added rate adjustment", {
key,
accountName,
adjustment: summarizeMoney(adjMoney)
adjustment: summarizeMoney(adjMoney),
taxable: extrasTaxable
});
} else {
CreateRRLogEvent(
@@ -651,6 +885,8 @@ function applyRomeProfileAdjustments({
* laborTaxableSale,
* laborNonTaxableSale,
* extrasSale,
* extrasTaxableSale,
* extrasNonTaxableSale,
* totalSale,
* cost,
* profitCenter,
@@ -663,10 +899,8 @@ function buildJobAllocations(bodyshop, profitCenterHash, costCenterHash, debugLo
const jobAllocations = centers.map((center) => {
const bucket = profitCenterHash[center] || emptyCenterBucket();
const totalSale = bucket.partsSale
.add(bucket.laborTaxableSale)
.add(bucket.laborNonTaxableSale)
.add(bucket.extrasSale);
const extrasSale = bucket.extrasSale;
const totalSale = bucket.partsSale.add(bucket.laborTaxableSale).add(bucket.laborNonTaxableSale).add(extrasSale);
const profitCenter = bodyshop.md_responsibility_centers.profits.find((c) => c.name === center);
const costCenter = bodyshop.md_responsibility_centers.costs.find((c) => c.name === center);
@@ -684,7 +918,9 @@ function buildJobAllocations(bodyshop, profitCenterHash, costCenterHash, debugLo
laborNonTaxableSale: bucket.laborNonTaxableSale,
// Extras
extrasSale: bucket.extrasSale,
extrasSale,
extrasTaxableSale: bucket.extrasTaxableSale,
extrasNonTaxableSale: bucket.extrasNonTaxableSale,
totalSale,
@@ -705,6 +941,8 @@ function buildJobAllocations(bodyshop, profitCenterHash, costCenterHash, debugLo
laborTaxable: summarizeMoney(row.laborTaxableSale),
laborNonTaxable: summarizeMoney(row.laborNonTaxableSale),
extras: summarizeMoney(row.extrasSale),
extrasTaxable: summarizeMoney(row.extrasTaxableSale),
extrasNonTaxable: summarizeMoney(row.extrasNonTaxableSale),
totalSale: summarizeMoney(row.totalSale),
cost: summarizeMoney(row.cost)
}))
@@ -812,12 +1050,35 @@ function calculateAllocations(connectionData, job) {
timetickets: Array.isArray(job.timetickets) ? job.timetickets.length : 0
});
const taxContext = buildTaxContext(job);
debugLog("Tax context initialised", {
isImex: taxContext.isImex,
isRome: taxContext.isRome,
federalTaxRate: taxContext.federalTaxRate,
stateTaxRate: taxContext.stateTaxRate,
localTaxRate: taxContext.localTaxRate,
hasFederal: taxContext.hasFederal,
hasState: taxContext.hasState,
hasLocal: taxContext.hasLocal,
globalAllTaxCanada: taxContext.globalAllTaxCanada,
tax_lbr_rt: taxContext.tax_lbr_rt,
tax_paint_mat_rt: taxContext.tax_paint_mat_rt,
tax_shop_mat_rt: taxContext.tax_shop_mat_rt,
tax_tow_rt: taxContext.tax_tow_rt,
hasAnyPartsWithTax: taxContext.hasAnyPartsWithTax,
hasAnyTax: taxContext.hasAnyTax
});
// 1) Tax allocations
let taxAllocations = buildTaxAllocations(bodyshop, job);
debugLog("Initial taxAllocations", summarizeTaxAllocations(taxAllocations));
// 2) Profit centers from job lines + MAPA/MASH detection
const { profitCenterHash: initialProfitHash, hasMapaLine, hasMashLine } = buildProfitCenterHash(job, debugLog);
const {
profitCenterHash: initialProfitHash,
hasMapaLine,
hasMashLine
} = buildProfitCenterHash(job, debugLog, taxContext);
// 3) DMS allocation config
const selectedDmsAllocationConfig =
@@ -842,7 +1103,8 @@ function calculateAllocations(connectionData, job) {
profitCenterHash: initialProfitHash,
hasMapaLine,
hasMashLine,
debugLog
debugLog,
taxContext
});
// 6) Materials costing (MAPA/MASH cost side)
@@ -861,7 +1123,8 @@ function calculateAllocations(connectionData, job) {
selectedDmsAllocationConfig,
profitCenterHash,
taxAllocations,
debugLog
debugLog,
taxContext
}));
// 8) Rome-only profile-level adjustments
@@ -871,7 +1134,8 @@ function calculateAllocations(connectionData, job) {
selectedDmsAllocationConfig,
profitCenterHash,
debugLog,
connectionData
connectionData,
taxContext
});
debugLog("profitCenterHash before jobAllocations build", {
@@ -882,7 +1146,9 @@ function calculateAllocations(connectionData, job) {
partsNonTaxable: summarizeMoney(b.partsNonTaxableSale),
laborTaxable: summarizeMoney(b.laborTaxableSale),
laborNonTaxable: summarizeMoney(b.laborNonTaxableSale),
extras: summarizeMoney(b.extrasSale)
extras: summarizeMoney(b.extrasSale),
extrasTaxable: summarizeMoney(b.extrasTaxableSale),
extrasNonTaxable: summarizeMoney(b.extrasNonTaxableSale)
}))
});
debugLog("costCenterHash before jobAllocations build", {

View File

@@ -56,7 +56,7 @@ const asN2 = (dineroLike) => {
* Build RO.GOG structure for the reynolds-rome-client `createRepairOrder` payload
* from allocations.
*
* Supports the new allocation shape:
* Supports the allocation shape:
* {
* center,
* partsSale,
@@ -65,18 +65,21 @@ const asN2 = (dineroLike) => {
* laborTaxableSale,
* laborNonTaxableSale,
* extrasSale,
* extrasTaxableSale,
* extrasNonTaxableSale,
* totalSale,
* cost,
* profitCenter,
* costCenter
* }
*
* 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")
* For each center, we can emit up to 6 GOG *segments*:
* - taxable parts (CustTxblNTxblFlag="T")
* - non-taxable parts (CustTxblNTxblFlag="N")
* - taxable extras (CustTxblNTxblFlag="T")
* - non-taxable extras (CustTxblNTxblFlag="N")
* - taxable labor (CustTxblNTxblFlag="T")
* - non-taxable labor (CustTxblNTxblFlag="N")
*
* IMPORTANT:
* Each segment becomes its OWN JobNo / AllGogOpCodeInfo, with exactly one
@@ -153,7 +156,8 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo
const partsTaxableCents = toCents(alloc.partsTaxableSale);
const partsNonTaxableCents = toCents(alloc.partsNonTaxableSale);
const extrasCents = toCents(alloc.extrasSale);
const extrasTaxableCents = toCents(alloc.extrasTaxableSale);
const extrasNonTaxableCents = toCents(alloc.extrasNonTaxableSale);
const laborTaxableCents = toCents(alloc.laborTaxableSale);
const laborNonTaxableCents = toCents(alloc.laborNonTaxableSale);
const costCents = toCents(alloc.cost);
@@ -178,16 +182,25 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo
});
}
// 3) Extras segment (respect center's default tax flag)
if (extrasCents !== 0) {
// 3) Taxable extras -> "T"
if (extrasTaxableCents !== 0) {
segments.push({
kind: "extras",
saleCents: extrasCents,
txFlag: pc.rr_cust_txbl_flag || "N"
kind: "extrasTaxable",
saleCents: extrasTaxableCents,
txFlag: "T"
});
}
// 4) Taxable labor segment -> "T"
// 4) Non-taxable extras -> "N"
if (extrasNonTaxableCents !== 0) {
segments.push({
kind: "extrasNonTaxable",
saleCents: extrasNonTaxableCents,
txFlag: "N"
});
}
// 5) Taxable labor segment -> "T"
if (laborTaxableCents !== 0) {
segments.push({
kind: "laborTaxable",
@@ -196,7 +209,7 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo
});
}
// 5) Non-taxable labor segment -> "N"
// 6) Non-tax labor segment -> "N"
if (laborNonTaxableCents !== 0) {
segments.push({
kind: "laborNonTaxable",
@@ -356,10 +369,8 @@ const QueryJobData = async (ctx = {}, jobId) => {
* @param advisorNo
* @param story
* @param makeOverride
* @param bodyshop
* @param allocations
* @param {string} [opCode] - RR OpCode for this RO (global default / override)
* @param {string} [taxCode] - RR tax code for header tax (e.g. state/prov code)
* @returns {Object}
*/
const buildRRRepairOrderPayload = ({
@@ -370,7 +381,6 @@ const buildRRRepairOrderPayload = ({
makeOverride,
allocations,
opCode
// taxCode
} = {}) => {
const customerNo = selectedCustomer?.customerNo
? String(selectedCustomer.customerNo).trim()
@@ -421,7 +431,6 @@ const buildRRRepairOrderPayload = ({
if (haveAllocations) {
const effectiveOpCode = (opCode && String(opCode).trim()) || null;
// const effectiveTaxCode = (taxCode && String(taxCode).trim()) || null;
if (effectiveOpCode) {
// Build RO.GOG and RO.LABOR in the new normalized shape