diff --git a/client/src/components/dms-allocations-summary/rr-dms-allocations-summary.component.jsx b/client/src/components/dms-allocations-summary/rr-dms-allocations-summary.component.jsx
index 4cffcfd85..a367caee6 100644
--- a/client/src/components/dms-allocations-summary/rr-dms-allocations-summary.component.jsx
+++ b/client/src/components/dms-allocations-summary/rr-dms-allocations-summary.component.jsx
@@ -64,7 +64,7 @@ function normalizeJobAllocations(ack) {
* RR-specific DMS Allocations Summary
* Focused on what we actually send to RR:
* - ROGOG (split by taxable / non-taxable segments)
- * - ROLABOR shell
+ * - ROLABOR labor rows with bill hours / rates
*
* The heavy lifting (ROGOG/ROLABOR split, cost allocation, tax flags)
* is now done on the backend via buildRogogFromAllocations/buildRolaborFromRogog.
@@ -181,21 +181,30 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat
const rolaborRows = useMemo(() => {
if (!rolaborPreview || !Array.isArray(rolaborPreview.ops)) return [];
- return rolaborPreview.ops.map((op, idx) => {
- const rowOpCode = opCode || op.opCode;
+ return rolaborPreview.ops
+ .filter((op) =>
+ [op.bill?.jobTotalHrs, op.bill?.billTime, op.bill?.billRate, op.amount?.custPrice, op.amount?.totalAmt]
+ .map((value) => Number.parseFloat(value ?? "0"))
+ .some((value) => !Number.isNaN(value) && value !== 0)
+ )
+ .map((op, idx) => {
+ const rowOpCode = opCode || op.opCode;
- return {
- key: `${op.jobNo}-${idx}`,
- opCode: rowOpCode,
- jobNo: op.jobNo,
- custPayTypeFlag: op.custPayTypeFlag,
- custTxblNtxblFlag: op.custTxblNtxblFlag,
- payType: op.bill?.payType,
- amtType: op.amount?.amtType,
- custPrice: op.amount?.custPrice,
- totalAmt: op.amount?.totalAmt
- };
- });
+ return {
+ key: `${op.jobNo}-${idx}`,
+ opCode: rowOpCode,
+ jobNo: op.jobNo,
+ custPayTypeFlag: op.custPayTypeFlag,
+ custTxblNtxblFlag: op.custTxblNtxblFlag,
+ payType: op.bill?.payType,
+ jobTotalHrs: op.bill?.jobTotalHrs,
+ billTime: op.bill?.billTime,
+ billRate: op.bill?.billRate,
+ amtType: op.amount?.amtType,
+ custPrice: op.amount?.custPrice,
+ totalAmt: op.amount?.totalAmt
+ };
+ });
}, [rolaborPreview, opCode]);
// Totals for ROGOG (sum custPrice + dlrCost over all lines)
@@ -245,6 +254,9 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat
{ title: "CustPayType", dataIndex: "custPayTypeFlag", key: "custPayTypeFlag" },
{ title: "CustTxblFlag", dataIndex: "custTxblNtxblFlag", key: "custTxblNtxblFlag" },
{ title: "PayType", dataIndex: "payType", key: "payType" },
+ { title: "JobTotalHrs", dataIndex: "jobTotalHrs", key: "jobTotalHrs" },
+ { title: "BillTime", dataIndex: "billTime", key: "billTime" },
+ { title: "BillRate", dataIndex: "billRate", key: "billRate" },
{ title: "AmtType", dataIndex: "amtType", key: "amtType" },
{ title: "CustPrice", dataIndex: "custPrice", key: "custPrice" },
{ title: "TotalAmt", dataIndex: "totalAmt", key: "totalAmt" }
@@ -317,12 +329,13 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat
children: (
<>
- This mirrors the shell that would be sent for ROLABOR when all financials are carried in GOG.
+ This mirrors the labor rows RR will receive, including weighted bill hours and rates derived from the
+ job's labor lines.
cost: summarizeMoney(a.cost)
}));
+const toFiniteNumber = (value) => {
+ const parsed = Number.parseFloat(value);
+ return Number.isFinite(parsed) ? parsed : 0;
+};
+
/**
* Internal per-center bucket shape for *sales*.
* We keep separate buckets for RR so we can split
@@ -62,6 +67,8 @@ function emptyCenterBucket() {
// Labor
laborTaxableSale: zero, // labor that should be taxed in RR
laborNonTaxableSale: zero, // labor that should NOT be taxed in RR
+ laborTaxableHours: 0,
+ laborNonTaxableHours: 0,
// Extras (MAPA/MASH/towing/PAO/etc)
extrasSale: zero, // total extras (taxable + non-taxable)
@@ -453,6 +460,7 @@ function buildProfitCenterHash(job, debugLog, taxContext) {
const rateKey = `rate_${val.mod_lbr_ty.toLowerCase()}`;
const rate = job[rateKey];
+ const lineHours = toFiniteNumber(val.mod_lb_hrs);
const laborAmount = Dinero({
amount: Math.round(rate * 100)
@@ -460,8 +468,10 @@ function buildProfitCenterHash(job, debugLog, taxContext) {
if (isLaborTaxable(val, taxContext)) {
bucket.laborTaxableSale = bucket.laborTaxableSale.add(laborAmount);
+ bucket.laborTaxableHours += lineHours;
} else {
bucket.laborNonTaxableSale = bucket.laborNonTaxableSale.add(laborAmount);
+ bucket.laborNonTaxableHours += lineHours;
}
}
@@ -478,6 +488,8 @@ function buildProfitCenterHash(job, debugLog, taxContext) {
partsNonTaxable: summarizeMoney(b.partsNonTaxableSale),
laborTaxable: summarizeMoney(b.laborTaxableSale),
laborNonTaxable: summarizeMoney(b.laborNonTaxableSale),
+ laborTaxableHours: b.laborTaxableHours,
+ laborNonTaxableHours: b.laborNonTaxableHours,
extras: summarizeMoney(b.extrasSale),
extrasTaxable: summarizeMoney(b.extrasTaxableSale),
extrasNonTaxable: summarizeMoney(b.extrasNonTaxableSale)
@@ -916,6 +928,8 @@ function buildJobAllocations(bodyshop, profitCenterHash, costCenterHash, debugLo
// Labor
laborTaxableSale: bucket.laborTaxableSale,
laborNonTaxableSale: bucket.laborNonTaxableSale,
+ laborTaxableHours: bucket.laborTaxableHours,
+ laborNonTaxableHours: bucket.laborNonTaxableHours,
// Extras
extrasSale,
diff --git a/server/rr/rr-job-export.js b/server/rr/rr-job-export.js
index 5eefbf19a..15e2f2a34 100644
--- a/server/rr/rr-job-export.js
+++ b/server/rr/rr-job-export.js
@@ -1,4 +1,4 @@
-const { buildRRRepairOrderPayload } = require("./rr-job-helpers");
+const { buildRRRepairOrderPayload, buildMinimalRolaborFromJob } = require("./rr-job-helpers");
const { buildClientAndOpts } = require("./rr-lookup");
const CreateRRLogEvent = require("./rr-logger-event");
const { withRRRequestXml } = require("./rr-log-xml");
@@ -56,6 +56,27 @@ const deriveRRStatus = (rrRes = {}) => {
};
};
+const resolveRROpCode = (bodyshop, txEnvelope = {}) => {
+ const resolvedBaseOpCode = resolveRROpCodeFromBodyshop(bodyshop);
+ let opCodeOverride = txEnvelope?.opCode || txEnvelope?.opcode || txEnvelope?.op_code || null;
+
+ if (!opCodeOverride) {
+ const opPrefix = txEnvelope?.opPrefix ?? txEnvelope?.op_prefix ?? null;
+ const opBase = txEnvelope?.opBase ?? txEnvelope?.op_base ?? null;
+ const opSuffix = txEnvelope?.opSuffix ?? txEnvelope?.op_suffix ?? null;
+
+ if (opPrefix || opBase || opSuffix) {
+ const combined = `${opPrefix || ""}${opBase || ""}${opSuffix || ""}`.trim();
+ if (combined) {
+ opCodeOverride = combined;
+ }
+ }
+ }
+
+ if (!opCodeOverride && !resolvedBaseOpCode) return null;
+ return String(opCodeOverride || resolvedBaseOpCode).trim() || null;
+};
+
/**
* Early RO Creation: Create a minimal RR Repair Order with basic info (customer, advisor, mileage, story).
* Used when creating RO from convert button or admin page before full job export.
@@ -93,7 +114,9 @@ const createMinimalRRRepairOrder = async (args) => {
const story = txEnvelope?.story ? String(txEnvelope.story).trim() : null;
const makeOverride = txEnvelope?.makeOverride ? String(txEnvelope.makeOverride).trim() : null;
- // Build minimal RO payload - just header, no allocations/parts/labor
+ // Build minimal RO payload for early review mode.
+ // We keep it lightweight, but include a single labor row when we can so Ignite
+ // exposes the labor subsection for editing.
const cleanVin =
(job?.v_vin || "")
.toString()
@@ -116,6 +139,12 @@ const createMinimalRRRepairOrder = async (args) => {
resolvedMileageIn: mileageIn
});
+ const earlyRoOpCode = resolveRROpCode(bodyshop, txEnvelope);
+ const earlyRoLabor = buildMinimalRolaborFromJob(job, {
+ opCode: earlyRoOpCode,
+ payType: "Cust"
+ });
+
const payload = {
customerNo: String(selected),
advisorNo: String(advisorNo),
@@ -141,9 +170,14 @@ const createMinimalRRRepairOrder = async (args) => {
if (makeOverride) {
payload.makeOverride = makeOverride;
}
+ if (earlyRoLabor) {
+ payload.rolabor = earlyRoLabor;
+ }
CreateRRLogEvent(socket, "INFO", "Creating minimal RR Repair Order (early creation)", {
- payload
+ payload,
+ earlyRoOpCode,
+ hasRolabor: !!earlyRoLabor
});
const response = await client.createRepairOrder(payload, finalOpts);
@@ -221,15 +255,10 @@ const updateRRRepairOrderWithFullData = async (args) => {
const story = txEnvelope?.story ? String(txEnvelope.story).trim() : null;
const makeOverride = txEnvelope?.makeOverride ? String(txEnvelope.makeOverride).trim() : null;
- // Optional RR OpCode segments coming from the FE (RRPostForm)
- const opPrefix = txEnvelope?.opPrefix ?? txEnvelope?.op_prefix ?? null;
- const opBase = txEnvelope?.opBase ?? txEnvelope?.op_base ?? null;
- const opSuffix = txEnvelope?.opSuffix ?? txEnvelope?.op_suffix ?? null;
-
// RR-only extras
let rrCentersConfig = null;
let allocations = null;
- let opCode = null;
+ const opCode = resolveRROpCode(bodyshop, txEnvelope);
// 1) Responsibility center config (for visibility / debugging)
try {
@@ -280,28 +309,9 @@ const updateRRRepairOrderWithFullData = async (args) => {
allocations = [];
}
- const resolvedBaseOpCode = resolveRROpCodeFromBodyshop(bodyshop);
-
- let opCodeOverride = txEnvelope?.opCode || txEnvelope?.opcode || txEnvelope?.op_code || null;
-
- // If the FE only sends segments, combine them here.
- if (!opCodeOverride && (opPrefix || opBase || opSuffix)) {
- const combined = `${opPrefix || ""}${opBase || ""}${opSuffix || ""}`.trim();
- if (combined) {
- opCodeOverride = combined;
- }
- }
-
- if (opCodeOverride || resolvedBaseOpCode) {
- opCode = String(opCodeOverride || resolvedBaseOpCode).trim() || null;
- }
-
CreateRRLogEvent(socket, "SILLY", "RR OP config resolved", {
opCode,
- baseFromConfig: resolvedBaseOpCode,
- opPrefix,
- opBase,
- opSuffix
+ baseFromConfig: resolveRROpCodeFromBodyshop(bodyshop)
});
// Build full RO payload for update with allocations
@@ -426,15 +436,10 @@ const exportJobToRR = async (args) => {
const story = txEnvelope?.story ? String(txEnvelope.story).trim() : null;
const makeOverride = txEnvelope?.makeOverride ? String(txEnvelope.makeOverride).trim() : null;
- // Optional RR OpCode segments coming from the FE (RRPostForm)
- const opPrefix = txEnvelope?.opPrefix ?? txEnvelope?.op_prefix ?? null;
- const opBase = txEnvelope?.opBase ?? txEnvelope?.op_base ?? null;
- const opSuffix = txEnvelope?.opSuffix ?? txEnvelope?.op_suffix ?? null;
-
// RR-only extras
let rrCentersConfig = null;
let allocations = null;
- let opCode = null;
+ const opCode = resolveRROpCode(bodyshop, txEnvelope);
// 1) Responsibility center config (for visibility / debugging)
try {
@@ -477,28 +482,9 @@ const exportJobToRR = async (args) => {
allocations = [];
}
- const resolvedBaseOpCode = resolveRROpCodeFromBodyshop(bodyshop);
-
- let opCodeOverride = txEnvelope?.opCode || txEnvelope?.opcode || txEnvelope?.op_code || null;
-
- // If the FE only sends segments, combine them here.
- if (!opCodeOverride && (opPrefix || opBase || opSuffix)) {
- const combined = `${opPrefix || ""}${opBase || ""}${opSuffix || ""}`.trim();
- if (combined) {
- opCodeOverride = combined;
- }
- }
-
- if (opCodeOverride || resolvedBaseOpCode) {
- opCode = String(opCodeOverride || resolvedBaseOpCode).trim() || null;
- }
-
CreateRRLogEvent(socket, "SILLY", "RR OP config resolved", {
opCode,
- baseFromConfig: resolvedBaseOpCode,
- opPrefix,
- opBase,
- opSuffix
+ baseFromConfig: resolveRROpCodeFromBodyshop(bodyshop)
});
// Build RO payload for create.
diff --git a/server/rr/rr-job-helpers.js b/server/rr/rr-job-helpers.js
index 13ca83fac..23d003ccf 100644
--- a/server/rr/rr-job-helpers.js
+++ b/server/rr/rr-job-helpers.js
@@ -52,6 +52,19 @@ const asN2 = (dineroLike) => {
return amount.toFixed(2);
};
+const toFiniteNumber = (value) => {
+ if (typeof value === "number") {
+ return Number.isFinite(value) ? value : 0;
+ }
+
+ if (typeof value === "string") {
+ const parsed = Number.parseFloat(value);
+ return Number.isFinite(parsed) ? parsed : 0;
+ }
+
+ return 0;
+};
+
/**
* Normalize various "money-like" shapes to integer cents.
* Supports:
@@ -100,6 +113,100 @@ const toMoneyCents = (value) => {
const asN2FromCents = (cents) => asN2({ amount: Number.isFinite(cents) ? cents : 0, precision: 2 });
+const formatDecimal = (value, maxDecimals = 2) => {
+ const factor = Math.pow(10, maxDecimals);
+ const rounded = Math.round(Math.max(0, toFiniteNumber(value)) * factor) / factor;
+ if (!Number.isFinite(rounded)) return "0";
+ return rounded.toFixed(maxDecimals).replace(/\.?0+$/, "") || "0";
+};
+
+const buildRolaborBillFields = ({ amountUnits = 0, hours = 0, rate = 0 } = {}) => {
+ const normalizedAmount = toFiniteNumber(amountUnits);
+
+ if (normalizedAmount <= 0) {
+ return {
+ jobTotalHrs: "0",
+ billTime: "0",
+ billRate: "0"
+ };
+ }
+
+ let resolvedHours = toFiniteNumber(hours);
+ let resolvedRate = toFiniteNumber(rate);
+
+ if (resolvedHours > 0 && resolvedRate <= 0) {
+ resolvedRate = normalizedAmount / resolvedHours;
+ } else if (resolvedRate > 0 && resolvedHours <= 0) {
+ resolvedHours = normalizedAmount / resolvedRate;
+ } else if (resolvedHours <= 0 && resolvedRate <= 0) {
+ // Keep the math internally consistent even if the source job has dollars but no usable hours.
+ resolvedHours = 1;
+ resolvedRate = normalizedAmount;
+ }
+
+ return {
+ jobTotalHrs: formatDecimal(resolvedHours),
+ billTime: formatDecimal(resolvedHours),
+ billRate: resolvedRate.toFixed(2)
+ };
+};
+
+const buildMinimalRolaborFromJob = (job, { opCode, payType = "Cust" } = {}) => {
+ const trimmedOpCode = opCode != null ? String(opCode).trim() : "";
+ if (!job || !trimmedOpCode) return null;
+
+ let totalHours = 0;
+ let totalAmountUnits = 0;
+
+ for (const line of job?.joblines || []) {
+ const laborType = typeof line?.mod_lbr_ty === "string" ? line.mod_lbr_ty.trim() : "";
+ if (!laborType) continue;
+
+ const lineHours = toFiniteNumber(line?.mod_lb_hrs ?? line?.db_hrs);
+ const configuredRate = toFiniteNumber(job?.[`rate_${laborType.toLowerCase()}`]);
+ let lineAmountUnits = toFiniteNumber(line?.lbr_amt);
+
+ if (lineAmountUnits <= 0 && lineHours > 0 && configuredRate > 0) {
+ lineAmountUnits = lineHours * configuredRate;
+ }
+
+ if (lineAmountUnits <= 0 && lineHours <= 0) continue;
+
+ totalHours += lineHours;
+ totalAmountUnits += lineAmountUnits;
+ }
+
+ if (totalAmountUnits <= 0 && totalHours <= 0) return null;
+
+ const bill = buildRolaborBillFields({
+ amountUnits: totalAmountUnits,
+ hours: totalHours,
+ rate: totalHours > 0 ? totalAmountUnits / totalHours : 0
+ });
+ const formattedAmount = totalAmountUnits.toFixed(2);
+
+ return {
+ ops: [
+ {
+ opCode: trimmedOpCode,
+ jobNo: "1",
+ custPayTypeFlag: "C",
+ custTxblNtxblFlag: toFiniteNumber(job?.tax_lbr_rt) > 0 ? "T" : "N",
+ bill: {
+ payType,
+ ...bill
+ },
+ amount: {
+ payType,
+ amtType: "Job",
+ custPrice: formattedAmount,
+ totalAmt: formattedAmount
+ }
+ }
+ ]
+ };
+};
+
/**
* Build RR estimate block from allocation totals.
* @param {Array} allocations
@@ -326,6 +433,13 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo
// Each segment becomes its own op / JobNo with a single line
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 segmentHours = isLaborSegment
+ ? seg.kind === "laborTaxable"
+ ? toFiniteNumber(alloc.laborTaxableHours)
+ : toFiniteNumber(alloc.laborNonTaxableHours)
+ : 0;
+ const segmentBillRate = isLaborSegment && segmentHours > 0 ? seg.saleCents / 100 / segmentHours : 0;
const line = {
breakOut,
@@ -349,7 +463,9 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo
// Extra metadata for UI / debugging
segmentKind: seg.kind,
segmentIndex: idx,
- segmentCount
+ segmentCount,
+ segmentHours,
+ segmentBillRate
});
});
}
@@ -368,9 +484,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-specific hours/rate remain zeroed out, but actual labor
- * sale amounts are mirrored into ROLABOR for labor segments so RR receives
- * the expected labor pricing on updates. Non-labor ops remain zeroed.
+ * 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.
*
* @param {Object} rogg - result of buildRogogFromAllocations
* @param {Object} opts
@@ -391,6 +507,17 @@ 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 laborBill = isLaborSegment
+ ? buildRolaborBillFields({
+ amountUnits: laborAmount,
+ hours: op.segmentHours,
+ rate: op.segmentBillRate
+ })
+ : {
+ jobTotalHrs: "0",
+ billTime: "0",
+ billRate: "0"
+ };
return {
opCode: op.opCode,
@@ -399,9 +526,7 @@ const buildRolaborFromRogog = (rogg, { payType = "Cust" } = {}) => {
custTxblNtxblFlag: txFlag,
bill: {
payType,
- jobTotalHrs: "0",
- billTime: "0",
- billRate: "0"
+ ...laborBill
},
amount: {
payType,
@@ -686,5 +811,6 @@ module.exports = {
normalizeCustomerCandidates,
normalizeVehicleCandidates,
buildRogogFromAllocations,
- buildRolaborFromRogog
+ buildRolaborFromRogog,
+ buildMinimalRolaborFromJob
};
diff --git a/server/rr/rr-job-helpers.test.js b/server/rr/rr-job-helpers.test.js
new file mode 100644
index 000000000..c3a6ee5f3
--- /dev/null
+++ b/server/rr/rr-job-helpers.test.js
@@ -0,0 +1,118 @@
+import { afterEach, describe, expect, it } from "vitest";
+import { createRequire } from "node:module";
+
+const require = createRequire(import.meta.url);
+const mock = require("mock-require");
+
+const graphClientModuleId = require.resolve("../graphql-client/graphql-client");
+const queriesModuleId = require.resolve("../graphql-client/queries");
+const helpersModuleId = require.resolve("./rr-job-helpers");
+
+const loadHelpers = () => {
+ mock.stopAll();
+ mock(graphClientModuleId, { client: { request: async () => ({}) } });
+ mock(queriesModuleId, { GET_JOB_BY_PK: "GET_JOB_BY_PK" });
+ delete require.cache[helpersModuleId];
+ return require(helpersModuleId);
+};
+
+afterEach(() => {
+ mock.stopAll();
+ delete require.cache[helpersModuleId];
+});
+
+describe("server/rr/rr-job-helpers", () => {
+ it("builds a single early-RO labor row from aggregated job labor", () => {
+ const { buildMinimalRolaborFromJob } = loadHelpers();
+
+ const rolabor = buildMinimalRolaborFromJob(
+ {
+ tax_lbr_rt: 13,
+ joblines: [
+ { mod_lbr_ty: "LAB", mod_lb_hrs: 2, lbr_amt: 200 },
+ { mod_lbr_ty: "LAD", mod_lb_hrs: 1.5, lbr_amt: 180 }
+ ]
+ },
+ { opCode: "51DOZ" }
+ );
+
+ expect(rolabor).toEqual({
+ ops: [
+ {
+ opCode: "51DOZ",
+ jobNo: "1",
+ custPayTypeFlag: "C",
+ custTxblNtxblFlag: "T",
+ bill: {
+ payType: "Cust",
+ jobTotalHrs: "3.5",
+ billTime: "3.5",
+ billRate: "108.57"
+ },
+ amount: {
+ payType: "Cust",
+ amtType: "Job",
+ custPrice: "380.00",
+ totalAmt: "380.00"
+ }
+ }
+ ]
+ });
+ });
+
+ it("populates labor bill fields from allocation hours on the full RR payload", () => {
+ const { buildRRRepairOrderPayload } = loadHelpers();
+
+ const payload = buildRRRepairOrderPayload({
+ job: {
+ id: "job-1",
+ ro_number: "RO-123",
+ v_vin: "1HGBH41JXMN109186"
+ },
+ selectedCustomer: { customerNo: "1134485" },
+ advisorNo: "70754",
+ allocations: [
+ {
+ center: "Body Labor",
+ partsSale: { amount: 0, precision: 2 },
+ laborTaxableSale: { amount: 24000, precision: 2 },
+ laborNonTaxableSale: { amount: 0, precision: 2 },
+ extrasSale: { amount: 0, precision: 2 },
+ totalSale: { amount: 24000, precision: 2 },
+ cost: { amount: 12000, precision: 2 },
+ laborTaxableHours: 2,
+ laborNonTaxableHours: 0,
+ profitCenter: {
+ rr_gogcode: "BL",
+ rr_item_type: "G",
+ accountdesc: "BODY LABOR"
+ }
+ }
+ ],
+ opCode: "51DOZ"
+ });
+
+ expect(payload.rolabor).toEqual({
+ ops: [
+ {
+ opCode: "51DOZ",
+ jobNo: "1",
+ custPayTypeFlag: "C",
+ custTxblNtxblFlag: "T",
+ bill: {
+ payType: "Cust",
+ jobTotalHrs: "2",
+ billTime: "2",
+ billRate: "120.00"
+ },
+ amount: {
+ payType: "Cust",
+ amtType: "Job",
+ custPrice: "240.00",
+ totalAmt: "240.00"
+ }
+ }
+ ]
+ });
+ });
+});