RrScratch3 - Tax / Extras Improvements
This commit is contained in:
@@ -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 & 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
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user