feature/IO-3672-Reynolds-Adjustments-V3 - Make sure there is never a scenario where a ROGOG does not have a ROLABOR
This commit is contained in:
@@ -331,16 +331,12 @@ const updateRRRepairOrderWithFullData = async (args) => {
|
||||
payload.roNo = String(roNo);
|
||||
payload.outsdRoNo = job?.ro_number || job?.id || undefined;
|
||||
|
||||
// RR update rejects placeholder non-labor ROLABOR rows with zero labor prices.
|
||||
// Keep only the actual labor jobs in ROLABOR and let ROGOG carry parts/extras.
|
||||
// RR update needs a ROLABOR row for every ROGOG JobNo, but rejects zero-price placeholders.
|
||||
// buildRolaborFromRogog mirrors the GOG price into each row, so keep the full 1:1 set.
|
||||
if (payload.rolabor?.ops?.length && payload.rogg?.ops?.length) {
|
||||
const laborJobNos = new Set(
|
||||
payload.rogg.ops
|
||||
.filter((op) => op?.segmentKind === "laborTaxable" || op?.segmentKind === "laborNonTaxable")
|
||||
.map((op) => String(op.jobNo))
|
||||
);
|
||||
const roggJobNos = new Set(payload.rogg.ops.map((op) => String(op.jobNo)));
|
||||
|
||||
payload.rolabor.ops = payload.rolabor.ops.filter((op) => laborJobNos.has(String(op?.jobNo)));
|
||||
payload.rolabor.ops = payload.rolabor.ops.filter((op) => roggJobNos.has(String(op?.jobNo)));
|
||||
|
||||
if (!payload.rolabor.ops.length) {
|
||||
delete payload.rolabor;
|
||||
|
||||
@@ -120,6 +120,22 @@ const formatDecimal = (value, maxDecimals = 2) => {
|
||||
return rounded.toFixed(maxDecimals).replace(/\.?0+$/, "") || "0";
|
||||
};
|
||||
|
||||
const isLaborSideProfitCenter = (alloc = {}) => {
|
||||
const pc = alloc?.profitCenter || {};
|
||||
|
||||
if (
|
||||
pc.rr_requires_rolabor ||
|
||||
pc.rr_force_rolabor ||
|
||||
pc.rr_labor_side ||
|
||||
pc.rr_is_labor ||
|
||||
pc.is_labor
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return [alloc.center, pc.name, pc.accountdesc, pc.accountname].some((value) => /\blabou?r\b/i.test(String(value || "")));
|
||||
};
|
||||
|
||||
const buildRolaborBillFields = ({ amountUnits = 0, hours = 0, rate = 0 } = {}) => {
|
||||
const normalizedAmount = toFiniteNumber(amountUnits);
|
||||
|
||||
@@ -335,6 +351,7 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo
|
||||
const pc = alloc?.profitCenter || {};
|
||||
const breakOut = pc.rr_gogcode;
|
||||
const itemType = pc.rr_item_type;
|
||||
const laborSideProfitCenter = isLaborSideProfitCenter(alloc);
|
||||
|
||||
// Only centers configured for RR GOG are included
|
||||
if (!breakOut || !itemType) continue;
|
||||
@@ -434,6 +451,8 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo
|
||||
segments.forEach((seg, idx) => {
|
||||
const jobNo = String(ops.length + 1); // global, 1-based JobNo across all centers/segments
|
||||
const isLaborSegment = seg.kind === "laborTaxable" || seg.kind === "laborNonTaxable";
|
||||
const isPartsSegment = seg.kind === "partsTaxable" || seg.kind === "partsNonTaxable";
|
||||
const rolaborRequired = isLaborSegment || (isPartsSegment && laborSideProfitCenter);
|
||||
const segmentHours = isLaborSegment
|
||||
? seg.kind === "laborTaxable"
|
||||
? toFiniteNumber(alloc.laborTaxableHours)
|
||||
@@ -465,7 +484,8 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo
|
||||
segmentIndex: idx,
|
||||
segmentCount,
|
||||
segmentHours,
|
||||
segmentBillRate
|
||||
segmentBillRate,
|
||||
rolaborRequired
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -484,9 +504,9 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo
|
||||
*
|
||||
* 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 sale amounts are mirrored into ROLABOR and, when hours
|
||||
* are available from allocations, weighted bill hours/rates are also
|
||||
* populated so the labor subsection is editable in Ignite.
|
||||
* GOG line. Sale amounts are mirrored into ROLABOR so Reynolds has a
|
||||
* non-zero job anchor for every ROGOG JobNo; when labor hours are available
|
||||
* from allocations, weighted bill hours/rates are also populated.
|
||||
*
|
||||
* @param {Object} rogg - result of buildRogogFromAllocations
|
||||
* @param {Object} opts
|
||||
@@ -506,7 +526,7 @@ const buildRolaborFromRogog = (rogg, { payType = "Cust" } = {}) => {
|
||||
|
||||
const linePayType = firstLine.custPayTypeFlag || "C";
|
||||
const isLaborSegment = op.segmentKind === "laborTaxable" || op.segmentKind === "laborNonTaxable";
|
||||
const laborAmount = isLaborSegment ? String(firstLine?.amount?.custPrice ?? "0") : "0";
|
||||
const laborAmount = String(firstLine?.amount?.custPrice ?? "0");
|
||||
const laborBill = isLaborSegment
|
||||
? buildRolaborBillFields({
|
||||
amountUnits: laborAmount,
|
||||
|
||||
@@ -115,4 +115,146 @@ describe("server/rr/rr-job-helpers", () => {
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it("mirrors parts assigned to a labor-side RR profit center into ROLABOR", () => {
|
||||
const { buildRRRepairOrderPayload } = loadHelpers();
|
||||
|
||||
const payload = buildRRRepairOrderPayload({
|
||||
job: {
|
||||
id: "job-2",
|
||||
ro_number: "RO-456",
|
||||
v_vin: "3GCUKHEL3TG292014"
|
||||
},
|
||||
selectedCustomer: { customerNo: "411588" },
|
||||
advisorNo: "70754",
|
||||
allocations: [
|
||||
{
|
||||
center: "Customer Pay CV Labor",
|
||||
partsSale: { amount: 15000, precision: 2 },
|
||||
partsTaxableSale: { amount: 0, precision: 2 },
|
||||
partsNonTaxableSale: { amount: 15000, precision: 2 },
|
||||
laborTaxableSale: { amount: 0, precision: 2 },
|
||||
laborNonTaxableSale: { amount: 0, precision: 2 },
|
||||
extrasSale: { amount: 0, precision: 2 },
|
||||
extrasTaxableSale: { amount: 0, precision: 2 },
|
||||
extrasNonTaxableSale: { amount: 0, precision: 2 },
|
||||
totalSale: { amount: 15000, precision: 2 },
|
||||
cost: { amount: 0, precision: 2 },
|
||||
profitCenter: {
|
||||
rr_gogcode: "VL",
|
||||
rr_item_type: "P",
|
||||
accountdesc: "Customer Pay CV Labor"
|
||||
}
|
||||
}
|
||||
],
|
||||
opCode: "30CVZBDY"
|
||||
});
|
||||
|
||||
expect(payload.rogg.ops[0]).toMatchObject({
|
||||
opCode: "30CVZBDY",
|
||||
jobNo: "1",
|
||||
segmentKind: "partsNonTaxable",
|
||||
rolaborRequired: true,
|
||||
lines: [
|
||||
{
|
||||
breakOut: "VL",
|
||||
itemType: "P",
|
||||
itemDesc: "Customer Pay CV Labor",
|
||||
custTxblNtxblFlag: "N",
|
||||
amount: {
|
||||
custPrice: "150.00",
|
||||
dlrCost: "0.00"
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
expect(payload.rolabor).toEqual({
|
||||
ops: [
|
||||
{
|
||||
opCode: "30CVZBDY",
|
||||
jobNo: "1",
|
||||
custPayTypeFlag: "C",
|
||||
custTxblNtxblFlag: "N",
|
||||
bill: {
|
||||
payType: "Cust",
|
||||
jobTotalHrs: "0",
|
||||
billTime: "0",
|
||||
billRate: "0"
|
||||
},
|
||||
amount: {
|
||||
payType: "Cust",
|
||||
amtType: "Job",
|
||||
custPrice: "150.00",
|
||||
totalAmt: "150.00"
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it("mirrors regular ROGOG parts into ROLABOR so Reynolds can find the JobNo", () => {
|
||||
const { buildRRRepairOrderPayload } = loadHelpers();
|
||||
|
||||
const payload = buildRRRepairOrderPayload({
|
||||
job: {
|
||||
id: "job-3",
|
||||
ro_number: "CDK10131",
|
||||
v_vin: "3TMLU4EN1AM044343"
|
||||
},
|
||||
selectedCustomer: { customerNo: "69158" },
|
||||
advisorNo: "6224",
|
||||
allocations: [
|
||||
{
|
||||
center: "B/S PARTS",
|
||||
partsSale: { amount: 15000, precision: 2 },
|
||||
partsTaxableSale: { amount: 15000, precision: 2 },
|
||||
partsNonTaxableSale: { amount: 0, precision: 2 },
|
||||
laborTaxableSale: { amount: 0, precision: 2 },
|
||||
laborNonTaxableSale: { amount: 0, precision: 2 },
|
||||
extrasSale: { amount: 0, precision: 2 },
|
||||
extrasTaxableSale: { amount: 0, precision: 2 },
|
||||
extrasNonTaxableSale: { amount: 0, precision: 2 },
|
||||
totalSale: { amount: 15000, precision: 2 },
|
||||
cost: { amount: 0, precision: 2 },
|
||||
profitCenter: {
|
||||
rr_gogcode: "FR",
|
||||
rr_item_type: "G",
|
||||
accountdesc: "B/S PARTS"
|
||||
}
|
||||
}
|
||||
],
|
||||
opCode: "60GMZ"
|
||||
});
|
||||
|
||||
expect(payload.rogg.ops[0]).toMatchObject({
|
||||
opCode: "60GMZ",
|
||||
jobNo: "1",
|
||||
segmentKind: "partsTaxable",
|
||||
rolaborRequired: false
|
||||
});
|
||||
|
||||
expect(payload.rolabor).toEqual({
|
||||
ops: [
|
||||
{
|
||||
opCode: "60GMZ",
|
||||
jobNo: "1",
|
||||
custPayTypeFlag: "C",
|
||||
custTxblNtxblFlag: "T",
|
||||
bill: {
|
||||
payType: "Cust",
|
||||
jobTotalHrs: "0",
|
||||
billTime: "0",
|
||||
billRate: "0"
|
||||
},
|
||||
amount: {
|
||||
payType: "Cust",
|
||||
amtType: "Job",
|
||||
custPrice: "150.00",
|
||||
totalAmt: "150.00"
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user