/* ──────────────────────────────────────────────────────────────────────────── K2B DMS — parts & inventory money: ONE calculation for the count adjustment, the PO receipt, the core bank, the bulk fluids and the counter RETURN (RC26, lane inv — the last money actions a station still posted by itself). The SAME file is used twice, byte for byte: server src/lib/parts-calc.js — the commands of src/routes/parts.js compute the committed quantities, values and journal lines with it (the server's answer wins) browser PartsCalc.jsx.txt — window.PartsCalc, for the PREVIEW only (Physical Inventory, Receive PO, Core Bank, Bulk Fluids, the Return dialog), so what a station shows before it submits is what the server will commit. Money rules (the RC20 rules of fixedops-calc, plus two of our own): • every amount is an INTEGER number of cents; a quantity is an integer number of thousandths (4.5 qt = 4500); • a line value = qty × unit, rounded half-up to the cent; • STRICT amounts (pitfall of the lost first attempt): a money figure a PERSON typed is accepted only as plain dollars ("5", "5.00", "$1,234.50", "1234.5"); a comma that is not a thousands separator ("5,00", "1,5", "12,34") is AMBIGUOUS and refused — never read as 500. A quantity is plain digits with at most 3 decimals ("4.5"), never "4,5", never "1e3"; • the return of part of a sale reverses the sale, the tax and the cost PROPORTIONALLY, in integer cents, so that returning every line of a sale reverses exactly the sale's tax (cumulative rounding — the last return gets the remainder). Pure functions only — no database, no window, no Date. Node 20 CommonJS / browser global. ──────────────────────────────────────────────────────────────────────── */ (function (root, factory) { var api = factory(); if (typeof module === "object" && module && module.exports) module.exports = api; if (root) root.PartsCalc = api; })(typeof window !== "undefined" ? window : null, function () { "use strict"; var CALC_VERSION = "inv-26"; /* ── the accounts: exactly the ones the station posts today (DMSCore.jsx.txt adjustInventory / receivePO / collectCore / creditCore / returnCore / receiveBulkOil, and the RC20 parts ticket map for the return). Nothing here is new. ── */ var ACCT = { invParts: "242", // INVENTORY - PARTS (what a count, a receipt and a restock move) partsAdjust: "688", // ADJUSTMENT - PARTS & ACCESS (the station's adjustInventory offset; the posting-template table says 682 — the CODE posts 688) apTrade: "300", // A/P TRADE (a PO receipt / a bulk delivery puts the vendor's bill here; the legacy core return CO- debits it) coreShelf: "246", // the station parks a returned core here ("Core inventory (on shelf)") — SUBLET REPAIRS - INV in the primary store's built-in chart cashBank: "202", // BANK OF NEW MADRID — the station refunds the core deposit from here and books the GM core credit here; a card refund cashOnHand: "201", // CASH ON HAND — the parts till (the counter sale's cash debit; a cash refund comes back out of it) arCustomer: "220", // A/R SERVICE & PARTS — a charged ticket, a refund back to the account, a refund check's pass-through bulkInv: "247", // the station's bulk oil inventory account (247 is "WORK IN PROCESS - LABOR" in the primary store's built-in chart; 244 is its oil account — see the owner decisions) bulkInvAlt: "244", // GAS, OIL, GREASE - INV — the one other account the office may pick for bulk deliveries (Parts settings) salesTaxPay: "324A", partsCounter: "482", partsWholesale: "483", costPartsCounter: "682", costPartsWholesale: "683", }; var MAX_CENTS = 1000000000; // $10,000,000 var MAX_QTY_MILLI = 1000000 * 1000; // 1,000,000 units /* ── numbers (strict) ── */ function mulDivRound(a, b, div) { var neg = (a < 0) !== (b < 0) && a !== 0 && b !== 0; var n = Math.abs(a) * Math.abs(b); var q = Math.floor(n / div), r = n - q * div; if (r * 2 >= div) q += 1; return neg ? -q : q; } /** A money figure typed by a person or sent by a station → integer cents. NaN = not a number; 'ambiguous' = a comma that is not a thousands separator ("5,00"); 'decimals' = more than two decimals. Numbers must be finite with at most 2 decimals. */ function strictMoney(v) { if (v === undefined || v === null || v === "") return NaN; if (typeof v === "number") { if (!isFinite(v)) return NaN; var c = Math.round(v * 100); if (Math.abs(v * 100 - c) > 1e-6) return "decimals"; return c; } if (typeof v !== "string") return NaN; var s = v.trim(); if (!s) return NaN; var m = /^(-)?\$?\s*(-)?(\d{1,3}(?:,\d{3})+|\d+)(?:\.(\d*))?$/.exec(s); if (!m) { if (/,/.test(s) && /^-?\$?\s*-?[\d,]+(\.\d*)?$/.test(s)) return "ambiguous"; return NaN; } var frac = m[4] || ""; if (frac.length > 2) return "decimals"; var cents = Number(m[3].replace(/,/g, "")) * 100 + Number((frac + "00").slice(0, 2)); if (!isFinite(cents)) return NaN; return (m[1] || m[2]) && cents !== 0 ? -cents : cents; } /** A quantity typed by a person or sent by a station → integer thousandths. Plain digits, at most 3 decimals, never negative, never a comma or an exponent. NaN when not accepted. */ function strictQty(v) { if (v === undefined || v === null || v === "") return NaN; var s; if (typeof v === "number") { if (!isFinite(v) || v < 0) return NaN; s = String(v); if (!/^\d+(\.\d+)?$/.test(s)) return NaN; } else if (typeof v === "string") s = v.trim(); else return NaN; var m = /^(\d{1,7})(?:\.(\d{1,3}))?$/.exec(s); if (!m) return NaN; var n = Number(m[1]) * 1000 + Number(((m[2] || "") + "000").slice(0, 3)); return n > MAX_QTY_MILLI ? NaN : n; } /** A whole quantity (parts on a PO / a ticket): digits only, 1 … 1,000,000 (0 allowed when opts.zero). */ function wholeQty(v, opts) { var m = strictQty(v); if (!isFinite(m) || m % 1000 !== 0) return NaN; var n = m / 1000; if (n === 0 && !(opts && opts.zero)) return NaN; return n; } /** Any stored quantity (the parts file's onHand, possibly a float like 2.5) → thousandths. 0 when not a number. */ function milliOf(v) { if (v === undefined || v === null || v === "") return 0; var n = Number(v); if (!isFinite(n)) return 0; return Math.round(n * 1000); } /** A stored dollar figure (the parts file's cost / list) → cents, half-up. 0 when not a number. */ function centsOf(v) { if (v === undefined || v === null || v === "") return 0; var n = typeof v === "string" ? Number(v.replace(/[,$\s]/g, "")) : Number(v); if (!isFinite(n)) return 0; return Math.round(n * 100); } function qtyOf(milli) { return milli / 1000; } function extCents(qtyMilli, unitCents) { return mulDivRound(qtyMilli, unitCents, 1000); } function dollars(c) { return Math.round(c) / 100; } function fmt(c) { var neg = c < 0; c = Math.abs(Math.round(c)); var s = String(Math.floor(c / 100)).replace(/\B(?=(\d{3})+(?!\d))/g, ","); return (neg ? "-$" : "$") + s + "." + String(c % 100).padStart(2, "0"); } function fmtQty(milli) { var s = (milli / 1000).toFixed(3).replace(/\.?0+$/, ""); return s === "" || s === "-" ? "0" : s; } function sumSides(lines) { var d = 0, c = 0; (lines || []).forEach(function (l) { d += l.dr || 0; c += l.cr || 0; }); return { dr: d, cr: c, balanced: d === c }; } function toJournalLines(lines) { return (lines || []).filter(function (l) { return l.dr || l.cr; }).map(function (l) { var o = { acct: l.acct, dr: dollars(l.dr), cr: dollars(l.cr) }; if (l.desc) o.desc = String(l.desc).slice(0, 120); return o; }); } /** the money problem of a typed amount, in plain words, or "" */ function moneyProblem(v, what) { var c = strictMoney(v); if (c === "ambiguous") return what + " \"" + String(v).slice(0, 20) + "\" is not clear — write dollars and cents like 12.50 (a comma only as a thousands separator, like 1,250.00)."; if (c === "decimals") return what + " has more than two decimals."; if (!isFinite(c) || c < 0) return what + " must be $0.00 or more."; if (c > MAX_CENTS) return what + " is larger than the server allows ($10,000,000)."; return ""; } /* ── COUNT ADJUSTMENT (Physical Inventory): the variance at the part's cost on file. DR 242 / CR 688 when the count is above the system, DR 688 / CR 242 when below (the station's adjustInventory). ── */ function countAdjust(input) { var onHand = milliOf(input.onHand), counted = typeof input.countedMilli === "number" ? input.countedMilli : strictQty(input.counted); var cost = isFinite(input.costCents) ? input.costCents : centsOf(input.cost); if (!isFinite(counted)) return { ok: false, problem: "The counted quantity must be plain digits with at most 3 decimals (like 12 or 4.5) — never below zero." }; var variance = counted - onHand; var value = extCents(Math.abs(variance), cost); var lines = []; if (variance > 0 && value) { lines.push({ acct: ACCT.invParts, dr: value, cr: 0, desc: "Inventory count — up" }); lines.push({ acct: ACCT.partsAdjust, dr: 0, cr: value, desc: "Parts adjustment" }); } if (variance < 0 && value) { lines.push({ acct: ACCT.partsAdjust, dr: value, cr: 0, desc: "Parts adjustment" }); lines.push({ acct: ACCT.invParts, dr: 0, cr: value, desc: "Inventory count — down" }); } return { ok: true, calc: CALC_VERSION, onHandMilli: onHand, countedMilli: counted, varianceMilli: variance, costCents: cost, valueCents: variance < 0 ? -value : value, absValueCents: value, lines: lines }; } /* ── PO RECEIPT: each line qty × unit cost, DR 242 / CR 300 for the total (the station's receivePO). lines [{ part, desc, qty (whole), unitCents, ordered, received }] → per line + the total + the over-receive problems ── */ function poReceipt(lines) { var out = []; var total = 0; var problems = []; (lines || []).forEach(function (l, i) { var qty = typeof l.qty === "number" && Number.isInteger(l.qty) ? l.qty : wholeQty(l.qty); var unit = isFinite(l.unitCents) ? l.unitCents : strictMoney(l.unitCost); var tag = "Line " + (i + 1) + " (" + (l.part || "?") + ")"; if (!isFinite(qty) || qty < 1) { problems.push({ code: "bad_qty", part: l.part, msg: tag + ": the quantity received must be a whole number of 1 or more." }); return; } if (typeof l.ordered === "number") { var open = Math.max(0, l.ordered - (Number(l.received) || 0)); if (qty > open) { problems.push({ code: "over_receive", part: l.part, open: open, msg: open <= 0 ? "Part " + l.part + " is fully received (" + l.ordered + " ordered, " + (Number(l.received) || 0) + " received) — nothing is open on that line." : "Part " + l.part + ": only " + open + " " + (open === 1 ? "is" : "are") + " still open (" + l.ordered + " ordered, " + (Number(l.received) || 0) + " already received) — this receipt asks for " + qty + ". Receive at most " + open + "." }); return; } } if (unit === "ambiguous") { problems.push({ code: "bad_amount", part: l.part, msg: tag + ": the unit cost \"" + l.unitCost + "\" is not clear — write dollars and cents like 12.50." }); return; } if (unit === "decimals") { problems.push({ code: "bad_amount", part: l.part, msg: tag + ": the unit cost has more than two decimals." }); return; } if (!isFinite(unit) || unit < 0 || unit > MAX_CENTS) { problems.push({ code: "bad_amount", part: l.part, msg: tag + ": the unit cost must be $0.00 or more." }); return; } var ext = extCents(qty * 1000, unit); total += ext; out.push({ part: l.part, desc: l.desc || "", qty: qty, unitCents: unit, extCents: ext }); }); var jl = total ? [{ acct: ACCT.invParts, dr: total, cr: 0, desc: "Parts into inventory at cost" }, { acct: ACCT.apTrade, dr: 0, cr: total, desc: "Accounts payable — vendor" }] : []; return { calc: CALC_VERSION, lines: out, totalCents: total, journal: jl, problems: problems }; } /* ── CORE BANK: the station's entries. receive from the customer COC- DR 246 core shelf / CR 202 bank (the deposit refunded; "cash" = the parts till 201) GM core credit received COG- DR 202 bank / CR 246 core shelf legacy core return CO- DR 300 A/P / CR 242 (catch-up only — the old single-step "Core return — credit received") ── */ function coreReceive(amountCents, refundMethod, opts) { var a = refundMethod === "cash" ? ACCT.cashOnHand : refundMethod === "ar" ? ACCT.arCustomer : ACCT.cashBank; // RC28 (fx, reviewer gate 2c): a core whose deposit was COLLECTED on the ticket (opts.depositAcct — the core-deposit liability) — the // deposit is refunded FROM that liability (DR it / CR the till, the bank or the customer's A/R), and the core goes on the shelf at its // value out of cost of sales (DR 246 / CR 682 counter | 683 wholesale — what a part returned as a core does). A core logged before RC28 // (no deposit collected) keeps the RC26 entry: DR 246 / CR the bank. if (opts && opts.depositAcct) return [ { acct: String(opts.depositAcct), dr: amountCents, cr: 0, desc: "Core deposit refunded from the deposit held" }, { acct: a, dr: 0, cr: amountCents, desc: "Deposit refunded to customer" }, { acct: ACCT.coreShelf, dr: amountCents, cr: 0, desc: "Core inventory (on shelf)" }, { acct: opts.wholesale ? ACCT.costPartsWholesale : ACCT.costPartsCounter, dr: 0, cr: amountCents, desc: "Core value out of cost of sales" }]; return [{ acct: ACCT.coreShelf, dr: amountCents, cr: 0, desc: "Core inventory (on shelf)" }, { acct: a, dr: 0, cr: amountCents, desc: "Deposit refunded to customer" }]; } function coreCredit(amountCents) { return [{ acct: ACCT.cashBank, dr: amountCents, cr: 0, desc: "GM core credit received" }, { acct: ACCT.coreShelf, dr: 0, cr: amountCents, desc: "Clear core inventory" }]; } var CORE_STEPS = ["Due from Customer", "On Core Shelf", "Shipped to Mfr", "Credited"]; /* RC26 fix round (audit F3): a core closed because the sold part itself came back on a counter return — nothing due, no money */ var CORE_RETURNED = "Part Returned"; /** the core bank per part: { part: { due, shelf, shipped, credited, dueCents, shelfCents, shippedCents } } — counts never negative (a core moves one step at a time, each step once). */ function coreBank(cores) { var by = {}; (cores || []).forEach(function (c) { if (!c) return; var k = String(c.part || c.desc || "?"); var b = by[k] || (by[k] = { part: k, due: 0, shelf: 0, shipped: 0, credited: 0, closed: 0, dueCents: 0, shelfCents: 0, shippedCents: 0, creditedCents: 0 }); var a = centsOf(c.amount); var st = String(c.status || CORE_STEPS[0]); if (st === "On Core Shelf") { b.shelf++; b.shelfCents += a; } else if (st === "Shipped to Mfr") { b.shipped++; b.shippedCents += a; } else if (st === "Credited") { b.credited++; b.creditedCents += a; } else if (st === CORE_RETURNED) { b.closed++; } else { b.due++; b.dueCents += a; } }); return by; } /* ── BULK FLUIDS: a delivery = qty × unit cost, DR 247 (or 244 when the office picked it) / CR 300 and the vendor bill. ── */ function bulkReceipt(input) { var qty = typeof input.qtyMilli === "number" ? input.qtyMilli : strictQty(input.qty); var unit = isFinite(input.unitCents) ? input.unitCents : strictMoney(input.unitCost); var acct = input.acct === ACCT.bulkInvAlt ? ACCT.bulkInvAlt : ACCT.bulkInv; if (!isFinite(qty) || qty <= 0) return { ok: false, problem: "The quantity received must be plain digits with at most 3 decimals (like 110 or 55.5), more than zero." }; if (unit === "ambiguous") return { ok: false, problem: "The cost per unit \"" + input.unitCost + "\" is not clear — write dollars and cents like 2.40." }; if (unit === "decimals") return { ok: false, problem: "The cost per unit has more than two decimals." }; if (!isFinite(unit) || unit < 0 || unit > MAX_CENTS) return { ok: false, problem: "The cost per unit must be $0.00 or more." }; var amt = extCents(qty, unit); var lines = amt ? [{ acct: acct, dr: amt, cr: 0, desc: "Bulk oil / fluid into inventory" }, { acct: ACCT.apTrade, dr: 0, cr: amt, desc: "Accounts payable — vendor" }] : []; return { ok: true, calc: CALC_VERSION, qtyMilli: qty, unitCents: unit, amountCents: amt, acct: acct, lines: lines }; } /* ── RC28 fix round (audit FU5 / FU7 — the reviewer's gate 2a): a bulk tank's oil is kept in DELIVERY LAYERS, oldest first: { acct: the account the delivery was received into (247 / 244), milli: what is left of it, cents: its value left, unitCents, ref }. Receive delivery adds a layer; Dispense takes the oldest oil first (FIFO) and stamps the RO line with the layers it took (bulkLayers); the RO close relieves each layer's value from ITS account. Before, every delivery overwrote the tank's account and cost: oil still in the tank from a 247 delivery was relieved from 244 after the setting changed, and every quart was relieved at the LAST delivery's cost, so 247 / 244 drifted from the value of the oil left in the tank. A tank from before RC28 (no layers) starts as ONE layer: what is in it at its cost, in its account. ── */ function bulkLayersOf(f, fallbackAcct) { var onHand = milliOf(f && f.onHand); var acctOf = function (a) { return a === ACCT.bulkInvAlt ? ACCT.bulkInvAlt : (a === ACCT.bulkInv ? ACCT.bulkInv : (fallbackAcct === ACCT.bulkInvAlt ? ACCT.bulkInvAlt : ACCT.bulkInv)); }; var src = f && Array.isArray(f.layers) ? f.layers : null; if (src) { var ok = true, sum = 0, out = []; src.forEach(function (l) { if (!l || typeof l !== "object" || !isFinite(l.milli) || l.milli < 0 || Math.floor(l.milli) !== l.milli || !isFinite(l.cents) || l.cents < 0 || Math.floor(l.cents) !== l.cents) { ok = false; return; } if (l.milli > 0) { out.push({ acct: acctOf(l.acct), milli: l.milli, cents: l.cents, unitCents: isFinite(l.unitCents) ? l.unitCents : (l.milli ? Math.round(l.cents * 1000 / l.milli) : 0), ref: l.ref || "", at: l.at || "" }); sum += l.milli; } }); if (ok && isFinite(onHand) && sum === onHand) return { layers: out, legacy: false }; } if (!isFinite(onHand) || onHand <= 0) return { layers: [], legacy: !src, mismatch: !!src && isFinite(onHand) && onHand > 0 }; var unit = centsOf(f && f.cost); if (!isFinite(unit) || unit < 0) unit = 0; return { layers: [{ acct: acctOf(f && f.invAcct), milli: onHand, cents: extCents(onHand, unit), unitCents: unit, ref: "before RC28", at: "" }], legacy: true, mismatch: !!src }; } /** take qtyMilli from the oldest layers → { ok, taken: [{ acct, milli, cents }], left: [layers], cents } — the value taken from a layer is its share (rounded half-up), and the LAST of a layer takes exactly what is left of its value, so an account never keeps a cent of oil that is gone */ function bulkTake(layers, qtyMilli) { var left = (layers || []).map(function (l) { return Object.assign({}, l); }), taken = [], need = qtyMilli, cents = 0; while (need > 0 && left.length) { var l = left[0], q = Math.min(need, l.milli); var c = q === l.milli ? l.cents : mulDivRound(l.cents, q, l.milli); var t = taken.length && taken[taken.length - 1].acct === l.acct ? taken[taken.length - 1] : null; if (t) { t.milli += q; t.cents += c; } else taken.push({ acct: l.acct, milli: q, cents: c }); l.milli -= q; l.cents -= c; need -= q; cents += c; if (l.milli === 0) left.shift(); } return { ok: need === 0, taken: taken, left: left, cents: cents }; } /** put a line's layers back on the FRONT of the tank (the oil is the oldest again) */ function bulkPutBack(layers, taken) { var back = (taken || []).filter(function (t) { return t && t.milli > 0; }).map(function (t) { return { acct: t.acct, milli: t.milli, cents: t.cents, unitCents: t.milli ? Math.round(t.cents * 1000 / t.milli) : 0, ref: "returned from an RO", at: "" }; }); return back.concat((layers || []).map(function (l) { return Object.assign({}, l); })); } /** a bulk RO line's own layers (Dispense stamped them) → [{ acct, milli, cents }] or null (a line dispensed before RC28) */ function lineBulkLayers(p) { if (!p || !Array.isArray(p.bulkLayers) || !p.bulkLayers.length) return null; var out = []; for (var i = 0; i < p.bulkLayers.length; i++) { var l = p.bulkLayers[i]; if (!l || !isFinite(l.milli) || !isFinite(l.cents) || l.milli <= 0 || l.cents < 0 || Math.floor(l.cents) !== l.cents) return null; out.push({ acct: l.acct === ACCT.bulkInvAlt ? ACCT.bulkInvAlt : ACCT.bulkInv, milli: l.milli, cents: l.cents }); } return out; } /* ── COUNTER RETURN against a stored RC20 parts invoice. input: { invoice: { kind, lines:[{part, qty, unitCents, extCents, coreCents}], taxableCents, taxCents, subtotalCents }, returned: { : qty already returned }, taxBackSoFarCents, extBackSoFarCents, lines: [{ lineIndex, qty, disposition restock|defective|core, costCents (the unit cost at the time of the sale) }], refundMethod cash|card|ar|check } → the lines, the sale / tax / cost reversed, the refund, and the journal: DR 482|483 sale · DR 324A tax · CR the refund account (cash 201 | card 202 | ar / check 220) restock: DR 242 / CR 682|683 — the cost back on the shelf defective: nothing (the cost stays in cost of sales — the part is scrapped / sent back by hand) core: DR 246 / CR 682|683 for the core value (the line's core charge × qty, never more than its cost) — the core sits on the core shelf until the vendor credits it (Core Bank › Ship / Post Credit clears 246) ── */ function partsReturn(input) { var inv = input.invoice || {}; var src = inv.lines || []; var returned = input.returned || {}; var problems = []; var out = []; var ext = 0; var cost = 0; var restockCost = 0; var coreShelf = 0; var seen = {}; (input.lines || []).forEach(function (l, i) { var ix = typeof l.lineIndex === "number" ? l.lineIndex : Number(l.lineIndex); var sl = src[ix]; if (!sl || !Number.isInteger(ix)) { problems.push({ code: "bad_line", lineIndex: ix, msg: "Return line " + (i + 1) + ": the ticket has no line " + (isFinite(ix) ? ix + 1 : String(l.lineIndex)) + "." }); return; } if (seen[ix]) { problems.push({ code: "bad_line", lineIndex: ix, msg: "Line " + (ix + 1) + " (" + sl.part + ") is on the return twice — put it on one line." }); return; } seen[ix] = true; var qty = typeof l.qty === "number" && Number.isInteger(l.qty) ? l.qty : wholeQty(l.qty); var sold = Number(sl.qty) || 0; var done = Number(returned[ix]) || 0; var left = sold - done; if (!isFinite(qty) || qty < 1) { problems.push({ code: "bad_qty", part: sl.part, lineIndex: ix, msg: "Return line " + (i + 1) + " (" + sl.part + "): the quantity must be a whole number of 1 or more." }); return; } if (qty > left) { problems.push({ code: "return_over", part: sl.part, lineIndex: ix, left: Math.max(0, left), sold: sold, returned: done, asked: qty, msg: (left <= 0 ? "Every one of part " + sl.part + " on this ticket was already returned (" + sold + " sold, " + done + " returned) — nothing is left to return." : "Only " + left + " of part " + sl.part + " can still be returned (" + sold + " sold, " + done + " already returned) — the return asks for " + qty + ".") }); return; } var disp = String(l.disposition || "restock").toLowerCase(); if (["restock", "defective", "core"].indexOf(disp) === -1) { problems.push({ code: "bad_disposition", part: sl.part, lineIndex: ix, msg: "Return line " + (i + 1) + " (" + sl.part + "): say what happens to the part — restock, defective or core." }); return; } var e = extCents(qty * 1000, Number(sl.unitCents) || 0); var uc = isFinite(l.costCents) ? l.costCents : 0; var c = extCents(qty * 1000, uc); var coreVal = 0; if (disp === "core") coreVal = Math.min(c, extCents(qty * 1000, Number(sl.coreCents) || 0)); ext += e; cost += c; if (disp === "restock") restockCost += c; if (disp === "core") coreShelf += coreVal; out.push({ lineIndex: ix, part: sl.part, desc: sl.desc || "", qty: qty, sold: sold, returnedBefore: done, leftAfter: left - qty, unitCents: Number(sl.unitCents) || 0, extCents: e, disposition: disp, unitCostCents: uc, costCents: c, coreCents: Number(sl.coreCents) || 0, coreShelfCents: coreVal }); }); // tax back: the cumulative share of the sale's tax, so the returns of a whole ticket add up to exactly its tax var taxable = Number(inv.taxableCents) || 0, saleTax = Number(inv.taxCents) || 0, subtotal = Number(inv.subtotalCents) || 0; var before = Number(input.extBackSoFarCents) || 0, taxBefore = Number(input.taxBackSoFarCents) || 0; var tax = 0; if (taxable > 0 && saleTax > 0 && subtotal > 0) { var cumAfter = before + ext >= subtotal ? saleTax : mulDivRound(saleTax, before + ext, subtotal); tax = Math.max(0, Math.min(saleTax - taxBefore, cumAfter - taxBefore)); } // RC28 (fx, gate 2c): the core deposit collected on the ticket for the parts that came back (the core is no longer due) goes back too var depBack = Math.max(0, Math.round(Number(input.coreDepositBackCents) || 0)); var refund = ext + tax + depBack; var whs = inv.kind === "wholesale"; var method = String(input.refundMethod || "cash").toLowerCase(); var refAcct = method === "card" ? ACCT.cashBank : (method === "ar" || method === "check") ? ACCT.arCustomer : ACCT.cashOnHand; var cogs = whs ? ACCT.costPartsWholesale : ACCT.costPartsCounter; var jl = []; if (ext) jl.push({ acct: whs ? ACCT.partsWholesale : ACCT.partsCounter, dr: ext, cr: 0, desc: "Parts returned — sale reversed" }); if (tax) jl.push({ acct: ACCT.salesTaxPay, dr: tax, cr: 0, desc: "Sales tax refunded" }); if (depBack) jl.push({ acct: String(input.coreDepositAcct || "331"), dr: depBack, cr: 0, desc: "Core deposit refunded — the part came back" }); if (refund) jl.push({ acct: refAcct, dr: 0, cr: refund, desc: method === "check" ? "Refund due — paid by check" : method === "ar" ? "Refund credited to the customer's account" : method === "card" ? "Card refund" : "Cash refund" }); if (restockCost) { jl.push({ acct: ACCT.invParts, dr: restockCost, cr: 0, desc: "Parts back into inventory" }); jl.push({ acct: cogs, dr: 0, cr: restockCost, desc: "Cost of parts sold reversed" }); } if (coreShelf) { jl.push({ acct: ACCT.coreShelf, dr: coreShelf, cr: 0, desc: "Core returned — on the core shelf" }); jl.push({ acct: cogs, dr: 0, cr: coreShelf, desc: "Core value out of cost of sales" }); } return { calc: CALC_VERSION, lines: out, subtotalCents: ext, taxCents: tax, costCents: cost, restockCostCents: restockCost, coreShelfCents: coreShelf, coreDepositBackCents: depBack, refundCents: refund, refundMethod: method, refundAcct: refAcct, journal: jl, problems: problems }; } return { CALC_VERSION: CALC_VERSION, ACCT: ACCT, MAX_CENTS: MAX_CENTS, CORE_STEPS: CORE_STEPS, CORE_RETURNED: CORE_RETURNED, strictMoney: strictMoney, strictQty: strictQty, wholeQty: wholeQty, milliOf: milliOf, centsOf: centsOf, qtyOf: qtyOf, extCents: extCents, mulDivRound: mulDivRound, bulkLayersOf: bulkLayersOf, bulkTake: bulkTake, bulkPutBack: bulkPutBack, lineBulkLayers: lineBulkLayers, // RC28 fix round (audit FU5 / FU7) dollars: dollars, fmt: fmt, fmtQty: fmtQty, sumSides: sumSides, toJournalLines: toJournalLines, moneyProblem: moneyProblem, countAdjust: countAdjust, poReceipt: poReceipt, coreReceive: coreReceive, coreCredit: coreCredit, coreBank: coreBank, bulkReceipt: bulkReceipt, partsReturn: partsReturn, }; });