/* © 2026 K2B NETWORK SOLUTIONS LLC. All rights reserved. */ /* eslint-disable */ // ImpScreens — RC25 (imp), the imports pack screens: // window.ImpWorkbook detect(sheets) — is this the old system's multi-sheet workbook? + the per-sheet column rules // window.ImpWorkbookFlow the whole workbook as ONE job: every sheet to its destination in the README's load order, one plan with // counts per sheet, run in order with per-sheet results (opened from the import wizard's file step) // window.ImpReconcileWindow Accounting › Reconcile inventory to GL: per schedule (new / used / parts / body shop / WIP / service A/R) // the GL balance as of a date next to the imported list, the difference, the items behind it, and the two // postings — always both offered, with plain words on when each is right (the SERVER posts; this screen asks) // window.ImpRoHistory Service › Old ROs: the repair-order history imported from the old system, looked up on the server // window.ImpTbSchedules a panel under Classic Reports › Trial Balance: which inventory accounts tie to a schedule, last reconciled when // RC26 (inv): window.ImpVehicleHistory / ImpVinHistoryButton — every deal, trade and old + K2B repair order on a VIN with the owner at the time // (GET /api/import/vehicle-history; Sales sees no cost); window.ImpCustomerOldRos — the customer's old repair orders (linked by // phone / VIN, or waiting with the phone as customer #) and vehicles (GET /api/import/customer-history); the Old ROs tab shows // the links and the office's "Link old ROs to customers" button (POST /api/import/link-ros); Reconcile Inventory leaves off // non-inventory accounts (238 SAW MILL LOT: dealership property) and lets the office exclude others on the schedule map. // Everything money is integer cents from the server; the rules are ImpCalc.jsx.txt (== the server's imp-calc.js). (function () { const D = () => window.DMS; const C = () => window.ImpCalc; const money = (c) => (C() ? C().fmt(Math.round(+c || 0)) : String((+c || 0) / 100)); const box = { background: "#FFF", border: "1px solid #C9D6E3", borderRadius: 4 }; const cell = { padding: "4px 8px", borderTop: "1px solid #EEE", verticalAlign: "top", fontSize: 11.5 }; const hcell = { padding: "5px 8px", textAlign: "left", background: "#F0F3F7", fontWeight: 700, borderBottom: "1px solid #D6DEE7", fontSize: 11.5, whiteSpace: "nowrap" }; const num = { textAlign: "right", fontFamily: "'Courier Prime', monospace", whiteSpace: "nowrap" }; const btn = (primary, extra) => ({ fontWeight: 700, padding: "5px 14px", background: primary ? "#FFE9B0" : undefined, ...(extra || {}) }); const toast = (t, k, ms) => { try { window.k2bToast && window.k2bToast(t, k || "info", ms || 4200); } catch (e) {} }; const todayIso = () => { const d = new Date(); return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0"); }; const errText = (e) => (e && e.body && e.body.error) || (e && e.message) || String(e || "unknown"); const sidebar = (route) => { const el = document.querySelector('.jb-sidebar [data-route="' + route + '"]'); if (el) el.click(); }; const goTo = (go, route) => { if (typeof go === "function") go(route); else sidebar(route); }; /* ═══════════════════════════════ 1. the old system's workbook ═══════════════════════════════ */ // sig: the columns that identify the sheet when it was renamed (RC25 fix round, attack W1: a sheet renamed "ROs 2026" is recognised by // its columns; a sheet recognised neither way is LISTED as not imported, never silently left out) const WB_SHEETS = [ { name: "Chart_of_Accounts", dest: "glChart", why: "the chart first — every other list looks its accounts up here", sig: { all: ["account", "description"], none: ["balance", "asof", "rowtype", "mtdamount", "ytdamount"] } }, { name: "GL_Opening_Balances", dest: "glTrial", why: "ONE balanced opening-balances entry dated the sheet's as-of date (accounts missing from the chart are added from the sheet's description)", sig: { all: ["asof", "account", "balance"] } }, { name: "Customers", dest: "customers", why: "customers before the deals and repair orders that mention them (RC30 (cn): the customer # is the phone without the area code — a customer_number column is used as is, a row with a phone and no number is numbered from the phone, a row with neither gets a number K2B issues)", sig: { any: [["customerid", "name", "homephone"], ["customernumber", "name"], ["name", "homephone"]] } }, // RC30 merge: cn's two signatures + imp2's (name + home_phone) { name: "Vehicles", dest: "vehicleMaster", why: "every VIN the old system ever saw — a lookup list on the server for the repair-order history (NOT units in stock)", sig: { all: ["vin", "sources"] } }, { name: "Inventory", dest: "inventory", why: "the units in stock, with the old ledger's GL Cost and GL Account per unit (nothing is posted — the approval screen ties them to the ledger)", sig: { all: ["stock", "glcost"] } }, { name: "Sales_Deals", dest: "deals", why: "closed-deal history (records only; RC36: the x_* / unconfirmed_* columns are kept as “from the old system — unconfirmed” information)", sig: { all: ["dealnumber", "saleprice"] } }, { name: "Repair_Orders", dest: "roHistory", why: "repair-order history in batches to the server; the RO_Lines sheet supplies the job lines (closed ROs = history, open ROs = on the board, no journal)", linesSheet: "RO_Lines", sig: { all: ["recordid", "paytypeflag", "rototal"] } }, { name: "RO_Lines", partOf: "Repair_Orders", why: "the job lines of the repair orders (imported together with Repair_Orders)", sig: { all: ["recordid", "lineno", "text1"], none: ["rototal"] } }, // RC33 (svi): the service side — names for the customer #s, the A/R aging, open warranty claims, body-shop jobs in progress (each sheet optional; the // service customer list is recognised by its NAME only — its columns would also match the Customers sheet) { name: "Service_Customers", dest: "svcCustomers", why: "the names (and addresses) of the service customers, by their 7-digit customer # — a customer is created at approval for a number not on file, a customer on file only gets its blank fields filled, another name is listed for the office" }, { name: "AR_Aging", dest: "arAging", why: "the A/R aging by customer (current / 31-60 / 61-90 / 91-120 / over 120 / total) — the customers' A/R accounts, tied to 220 (and 220A) on the review", sig: { all: ["customernumber", "current", "total"] } }, { name: "Warranty_Claims", dest: "warrantyClaims", why: "the open warranty claims (claim #, RO #, amount) — tied to 263 WARRANTY CLAIMS on the review", sig: { all: ["claimnumber", "ronumber", "amount"] } }, { name: "Body_Shop_WIP", dest: "bodyWip", why: "the body-shop jobs still in progress (their cost) — tied to 245 BODY SHOP INVENTORY on the review", sig: { all: ["ronumber", "materialscost"] } }, // RC36 (fpd36): the lender's floorplan statement (optional) — compared with the floored units, 310 / 314 on the review; kept on file at approval { name: "Floorplan_Statement", dest: "floorplanStmt", why: "the lender's floorplan statement (stock # / VIN, floor date, amount floored, curtailments, balance owed) — compared unit by unit with the floored units and 310 / 314 on the review; nothing is posted", sig: { any: [["stock", "balanceowed"], ["vin", "balanceowed"]] } }, // RC30 (imp2): the owner's template adds the parts list and the Tie_Out sheet (which GL account each list must agree with) { name: "Parts", dest: "partsMaster", why: "the parts on hand (part #, bin, on hand, cost) — tied to the parts inventory account on approval", sig: { all: ["part", "onhand"] } }, // RC36 (imp36): a template sheet for every import — vendors (old balances → A/P), the check register, journal entries, aged schedules { name: "Vendors", dest: "vendors", why: "the vendors (old balance owed for reference — tied to A/P 300 on the review)", sig: { all: ["vendorname", "terms"] } }, { name: "Check_Register", dest: "checks", why: "the check register (register records only — tied to the bank account's credits on the review)", sig: { all: ["checknumber", "payee", "amount"] } }, { name: "Journal_Entries", dest: "glJournal", why: "journal entries (posted at approval — compared with what the books already carry for the same days)", sig: { all: ["entryref", "glaccount", "debit", "credit"] } }, { name: "Aged_Schedules", dest: "schedule", why: "aged schedules by GL account (control #, balance) — each account tied to its ledger balance on the review", sig: { all: ["control", "balance", "glaccount"] } }, { name: "Summary_by_Year", reference: true, why: "the deal file's own totals by year — read back on the review of the deals (deals / new / used / total sales / financed)" }, { name: "Tie_Out", tieOut: true, why: "which GL account each list must agree with (Inventory / Parts / Customer A/R / WIP labor / Sublet / Warranty / Body shop / Parts on open ROs / Information / Exclude) — read by the approval screen; the defaults are used when this sheet is missing", sig: { all: ["glaccount", "tiesto"] } }, { name: "Trial_Balance_Report", reference: true, why: "kept for reference — compare it with Classic Reports › Trial Balance after the opening balances post", sig: { all: ["account", "rowtype"] } }, { name: "PL_Report", reference: true, why: "kept for reference — compare it with Profit & Loss" }, { name: "README", skip: true, why: "the old system's notes" }, { name: "START_HERE", skip: true, why: "the template's instructions (not data)" }, { name: "Column_Guide", skip: true, why: "the template's column guide (not data)" }, ]; const nkey = (s) => String(s || "").toLowerCase().replace(/[^a-z0-9]/g, ""); const sigHit = (w, sh) => { if (!w.sig || !sh || !Array.isArray(sh.headers)) return false; const hs = new Set(sh.headers.map(nkey)); const alls = w.sig.any || [w.sig.all || []]; return alls.some((all) => all.every((h) => hs.has(h))) && !(w.sig.none || []).some((h) => hs.has(h)); }; // RC30 (cn): sig.any = several column signatures (the ACS export and the K2B template) function detect(sheets) { const found = {}, byColumns = {}, taken = new Set(); (sheets || []).forEach((sh) => { const k = nkey(sh && sh.name); WB_SHEETS.forEach((w) => { if (!found[w.name] && nkey(w.name) === k) { found[w.name] = sh; taken.add(sh); } }); }); (sheets || []).forEach((sh) => { if (taken.has(sh)) return; const w = WB_SHEETS.find((x) => !found[x.name] && sigHit(x, sh)); if (w) { found[w.name] = sh; byColumns[w.name] = sh.name; taken.add(sh); } }); const importable = WB_SHEETS.filter((w) => w.dest && found[w.name]); const unknown = (sheets || []).filter((sh) => sh && !taken.has(sh)).map((sh) => ({ name: sh.name, rows: (sh.rows || []).length })); const missing = WB_SHEETS.filter((w) => (w.dest || w.partOf) && !found[w.name] && w.name !== "Parts").map((w) => w.name); // RC30: the ACS export has no Parts sheet — not "missing" return { ok: importable.length >= 2, recognised: Object.keys(found).length, names: WB_SHEETS.filter((w) => found[w.name]).map((w) => w.name), found, importable: importable.map((w) => w.name), byColumns, unknown, missing, tieOut: found.Tie_Out || null }; } const typeFromAcct = (a) => { const s = String(a || "").trim().toUpperCase(); if (/^0\d{3}/.test(s)) return "Expense"; const d = (s.match(/^\d+/) || [""])[0]; return ({ 1: "Asset", 2: "Asset", 3: "Liability", 4: "Sales", 5: "Sales", 6: "Cost", 7: "Expense", 8: "Income", 9: "Income" })[d[0]] || "Expense"; }; const ymm = (y, mk, md) => [y, mk, md].map((x) => String(x == null ? "" : x).trim()).filter(Boolean).join(" "); const X_WHY = "not imported (meaning unconfirmed)", SSN_WHY = "never imported (sensitive)", NOFIELD = "no K2B field"; const WB_COLS = { Chart_of_Accounts: { map: { Account: "acct", Description: "name", "Type (from the account number)": "type" }, virtual: (r) => ({ "Type (from the account number)": typeFromAcct(r.Account) }), note: "The old chart has no account type column: the type is taken from the account number (0xxx expense · 1xx / 2xx asset · 3xx liability and equity · 4xx / 5xx sales · 6xx cost · 7xx expense · 8xx / 9xx income)." }, GL_Opening_Balances: { map: { Account: "acct", Description: "name", Balance: "ytd" }, skip: { "As Of": "used as the as-of date of the opening balances" }, opts: (rows) => ({ asOf: rows[0] && rows[0]["As Of"] ? String(rows[0]["As Of"]).slice(0, 10) : "", periodMode: "asOf", addMissingAccounts: true }) }, Customers: { map: { customer_number: "custNum", name: "fullName", address: "street", city: "city", state: "state", zip: "postal", home_phone: "phone", work_phone: "workPhone", email: "email", customer_id: "oldCustNo" }, // RC30 (cn): customer_number = THE customer # (7 digits), work_phone and email imported; RC26 (inv) fix round: customer_id kept as the old system's customer id — an old deal names its buyer by it skip: { employer: NOFIELD, county: NOFIELD, ssn_last4: SSN_WHY, last_deal_date: NOFIELD } }, Vehicles: { map: { vin: "vin", year: "year", make: "make", model: "model", color: "color", sources: "sources" }, skip: { customer_number: "the vehicle's last owner — linked through the repair orders and deals, not stored on the VIN list" } }, Inventory: { guess: "inventory", skip: { "Mileage Code": NOFIELD } }, Parts: { map: { "Part #": "part", Description: "desc", Bin: "bin", "On hand": "onHand", Cost: "cost", List: "list", Trade: "trade", Source: "source", "Core charge": "core", "GL Account": "glAcct" } }, Sales_Deals: { // RC36 (imp36): the deal history complete — year / make / model, the trade, finance terms, the second salesperson, the GL accounts, county, employer, the // stock-in date, purchased from, the open items; the x_* / unconfirmed_* columns are kept as "from the old system — unconfirmed" information map: { year: "vehYear", make: "vehMake", model: "vehModel", body: "vehBody", color: "vehColor", sale_gl_account: "saleAcct", inventory_gl_account: "invAcct", salesperson2_code: "salesperson2", stock_in_date: "stockInDate", purchased_from: "purchasedFrom", buyer_county: "county", buyer_employer: "employer", buyer_work_phone: "workPhone", cobuyer_relation: "cobuyerRelation", sales_tax: "salesTax", net_trade: "netTrade", total_down: "totalDown", total_of_payments: "totalOfPayments", total_sale_price: "totalSalePrice", first_payment_date: "firstPayDate", trade1_year: "tradeYear", trade1_make: "tradeMake", trade1_model: "tradeModel", trade1_body: "tradeBody", trade1_color: "tradeColor", funded: "funded", payoff_paid: "payoffPaid", tax_remitted: "taxRemitted", contract_remitted: "contractRemitted", x_doc_fee_or_pack: "ucDocFee", x_total_cost: "ucTotalCost", x_trade_acv: "ucTradeAcv", x_front_gross: "ucFrontGross", x_back_gross: "ucBackGross", x_created_date: "ucCreated", x_modified_date: "ucModified", unconfirmed_doc_fee_or_pack: "ucDocFee", unconfirmed_total_cost: "ucTotalCost", unconfirmed_trade_acv: "ucTradeAcv", unconfirmed_front_gross: "ucFrontGross", unconfirmed_back_gross: "ucBackGross", unconfirmed_created_date: "ucCreated", unconfirmed_modified_date: "ucModified", deal_number: "dealNo", deal_date: "date", lender_code: "lender", salesperson_code: "salesperson", stock_number: "stock", vin: "vin", "Vehicle (year make model)": "vehicle", odometer: "miles", new_used: "type", buyer_name: "buyer", buyer_address: "street", buyer_city: "city", buyer_state: "state", buyer_zip: "postal", buyer_home_phone: "phone", customer_number: "custNum", customer_id: "oldCustNo", cobuyer_name: "cobuyer", vehicle_cost: "invoicePrice", // RC30 (cn): customer_number = the buyer's customer # trade1_stock_number: "tradeStock", "Trade (year make model)": "tradeDesc", trade1_vin: "tradeVin", trade1_odometer: "tradeMiles", trade1_lienholder: "payoffLender", trade_payoff: "payoff1", sale_price: "salePrice", cash_down: "cashReceived", trade_allowance: "tradeAllow", service_contract: "vscSale", apr: "contractRate", term_months: "term", payment_amount: "payment", amount_financed: "amtFinanced", finance_charge: "financeCharge" }, virtual: (r) => ({ "Vehicle (year make model)": ymm(r.year, r.make, r.model), "Trade (year make model)": ymm(r.trade1_year, r.trade1_make, r.trade1_model) }), historyOnly: Object.fromEntries(["x_doc_fee_or_pack", "x_total_cost", "x_trade_acv", "x_front_gross", "x_back_gross", "x_created_date", "x_modified_date", "unconfirmed_doc_fee_or_pack", "unconfirmed_total_cost", "unconfirmed_trade_acv", "unconfirmed_front_gross", "unconfirmed_back_gross", "unconfirmed_created_date", "unconfirmed_modified_date"].map((c) => [c, "kept on the deal as “from the old system — unconfirmed” (information only, decides nothing)"])), // RC36 (imp36): listed on the plan as unconfirmed information skip: { buyer_ssn: SSN_WHY, cobuyer_ssn: SSN_WHY }, note: "RC36: every column is read. The x_* / unconfirmed_* columns are kept on the deal as “from the old system — unconfirmed” (information only — the old export says their meaning is unconfirmed; they decide nothing in K2B). The SSN columns are never imported. The review ties the deals to the general ledger (open items vs 205 / 303 / 324A / 302, sales and cost of sales year to date)." }, Vendors: { guess: "vendors" }, Check_Register: { guess: "checks" }, Journal_Entries: { guess: "glJournal" }, Aged_Schedules: { guess: "schedule" }, // RC36 (imp36): the template's column names are the fields' own synonyms Repair_Orders: { map: { record_id: "roNo", customer_account: "customerAccount", customer_number: "customerAccount", ro_date: "date", pay_type_flag: "flag", vin: "vin", stock_number: "stock", year: "year", make: "make", model: "model", color: "color", odometer: "odometer", technician_code: "tech", advisor_code: "advisor", // RC30 (imp2): template customer_number customer_name: "customerName", customer_last_name: "custLast", customer_first_name: "custFirst", address: "custStreet", city: "custCity", state: "custState", zip: "custPostal", phone: "custPhone", email: "custEmail", status: "status", // RC33 (svi): the template v3's customer columns (the ACS export has none) parts_sale: "partsSale", parts_cost: "partsCost", labor_sale: "laborSale", labor_cost: "laborCost", sublet_sale: "subletSale", sublet_cost: "subletCost", shop_supplies: "supplies", tax_rate_pct: "taxRatePct", sales_tax: "salesTax", ro_total: "total", x_customer_pay_total: "customerPay", x_warranty_or_internal_total: "otherPay" }, historyOnly: { x_customer_pay_total: "kept on the RO history exactly as printed — meaning unconfirmed, decides nothing", x_warranty_or_internal_total: "kept on the RO history exactly as printed — meaning unconfirmed, decides nothing" }, note: "The two x_ columns add up to ro_total on every row, but the old export says their meaning is unconfirmed (on an open RO the customer pay is 0 until it is invoiced): they are kept on the repair-order HISTORY as printed and decide nothing in K2B — an open RO goes on the board as Customer pay, and they never count as A/R. pay_type_flag: I = invoiced (closed), O = open, H = held, W = warranty (closed); a VOID customer account = a voided RO. RC33: a customer_name column (template v3) makes the customer of a 7-digit customer # at approval (or fills the blank fields of the one on file; another name on file is listed for the office)." }, // RC33 (svi): the service lists of the template v3 Service_Customers: { map: { customer_number: "custNum", name: "customerName", last_name: "custLast", first_name: "custFirst", address: "custStreet", city: "custCity", state: "custState", zip: "custPostal", phone: "custPhone", work_phone: "custWorkPhone", email: "custEmail" } }, AR_Aging: { map: { customer_number: "control", name: "name", current: "current", days_31_60: "d31_60", days_61_90: "d61_90", days_91_120: "d91_120", over_120: "over120", total: "total", gl_account: "gl", as_of: "asOf" }, opts: (rows) => ({ asOf: rows[0] && rows[0].as_of ? String(rows[0].as_of).slice(0, 10) : "" }) }, // RC36 (fpd36): the K2B template's floorplan statement columns (any other header is guessed from the destination's names) Floorplan_Statement: { guess: "floorplanStmt", map: { stock_number: "stock", vin: "vin", description: "desc", lender: "lender", floor_date: "floorDate", amount_floored: "advance", curtailments_paid: "curtail", balance_owed: "balance", statement_date: "stmtDate" } }, Warranty_Claims: { map: { claim_number: "claim", ro_number: "ro", claim_date: "date", amount: "amount", amount_paid: "paid", status: "status" } }, Body_Shop_WIP: { map: { ro_number: "roNo", date: "date", customer_number: "custNum", labor_cost: "laborCost", parts_cost: "partsCost", materials_cost: "materialsCost", sublet_cost: "subletCost", status: "status" } }, }; function buildJob(w, sh, fileName, allSheets) { const spec = WB_COLS[w.name] || {}; const Dm = D(); const SKIP = Dm.IMPORT_SKIP || "__skip"; const rows = spec.virtual ? sh.rows.map((r) => Object.assign({}, r, spec.virtual(r))) : sh.rows; const headers = spec.virtual ? sh.headers.concat(Object.keys(spec.virtual(sh.rows[0] || {})).filter((h) => !sh.headers.includes(h))) : sh.headers.slice(); let mapping = {}; if (spec.guess) { const g = Dm.importGuessMapping(spec.guess, headers, rows); mapping = Object.assign({}, g.mapping); } const notImported = []; headers.forEach((h) => { if (spec.map && spec.map[h]) { mapping[h] = spec.map[h]; return; } if (spec.skip && spec.skip[h]) { mapping[h] = SKIP; notImported.push({ col: h, why: spec.skip[h] }); return; } if (!mapping[h]) { mapping[h] = SKIP; notImported.push({ col: h, why: /^x_/i.test(h) ? X_WHY : /ssn/i.test(h) ? SSN_WHY : NOFIELD }); } }); const used = {}; headers.forEach((h) => { const f = mapping[h]; if (f && f !== SKIP) { if (used[f]) { mapping[h] = SKIP; notImported.push({ col: h, why: "“" + used[f] + "” already fills that field" }); } else used[f] = h; } }); const opts = Object.assign({ headers, fileName, sheetName: sh.name, allSheets }, spec.opts ? spec.opts(rows) : {}); const historyOnly = Object.keys(spec.historyOnly || {}).filter((h) => headers.includes(h) && mapping[h] && mapping[h] !== SKIP).map((h) => ({ col: h, why: spec.historyOnly[h] })); return { w, sh, rows, headers, mapping, notImported, historyOnly, opts, dest: w.dest, def: Dm.importDest(w.dest), note: spec.note || "" }; } const planJob = (job) => { try { return D().importPlan(job.dest, job.rows, job.mapping, job.opts); } catch (e) { return { error: "Preview failed: " + ((e && e.message) || e) }; } }; const countsText = (c) => c ? `${c.added} added · ${c.updated} updated · ${c.skipped} skipped · ${c.rejected} rejected` : ""; window.ImpWorkbook = { SHEETS: WB_SHEETS, COLS: WB_COLS, detect, buildJob, planJob, typeFromAcct }; const ImpWorkbookFlow = ({ file, go, onBack, onClose }) => { const Dm = D(); const det = React.useMemo(() => detect(file.sheets), [file]); const jobs = React.useMemo(() => WB_SHEETS.filter((w) => w.dest && det.found[w.name]).map((w) => buildJob(w, det.found[w.name], file.name, file.sheets)), [file, det]); const extras = WB_SHEETS.filter((w) => !w.dest && det.found[w.name]); const sheetName = (w) => det.byColumns[w.name] ? <>{w.name}
sheet “{det.byColumns[w.name]}”, recognised by its columns
: w.name; const noOpening = !det.found.GL_Opening_Balances; const [plans, setPlans] = React.useState({}); const [results, setResults] = React.useState({}); const [running, setRunning] = React.useState(false); const [cur, setCur] = React.useState(""); const [progress, setProgress] = React.useState(null); const [stopped, setStopped] = React.useState(""); const [staged, setStaged] = React.useState(null); // RC30 (imp2): the ONE run every sheet went into, once it waits for approval const [, tick] = React.useReducer((n) => n + 1, 0); const doneAll = !running && jobs.length > 0 && jobs.every((j) => results[j.w.name]); const asOf = (jobs.find((j) => j.w.name === "GL_Opening_Balances") || { opts: {} }).opts.asOf || ""; React.useEffect(() => { const p = {}; jobs.forEach((j) => { p[j.w.name] = planJob(j); }); setPlans(p); }, [jobs]); const allowed = (j) => !j.def || !Dm.can || !j.def.perm || Dm.can(j.def.perm); const openReview = (runId) => { try { if (runId) localStorage.setItem("k2b-imp-review-run", runId); } catch (e) {} goTo(go, "importreview"); }; // RC30 (imp2): the whole workbook is ONE staged run, one part per sheet — nothing is written or posted here; the Tie_Out sheet travels // with the run (the approval screen's tie-out); a blocked sheet (e.g. opening balances that do not net to zero) is staged as a problem // part so the owner sees why, instead of stopping the whole workbook. const runAll = async () => { if (running) return; if (!Dm.importStage || !Dm.importStageStart) { toast("This DMS page is older than the server — reload it (Ctrl+F5).", "warn", 6000); return; } setRunning(true); setStopped(""); setResults({}); setStaged(null); const tieRows = det.tieOut ? det.tieOut.rows.map((r) => ({ glAccount: r["GL Account"], tiesTo: r["Ties to"], note: r.Note })) : undefined; let runId = ""; try { const st = await Dm.importStageStart({ fileName: file.name, source: "Old system workbook", parts: jobs.map((j) => ({ part: C().partName(j.w.name), dest: j.dest, sheet: j.sh.name })), asOf, tieOut: tieRows, totalRows: jobs.reduce((s, j) => s + j.rows.length, 0) }); runId = st.runId; } catch (e) { setStopped("start"); setResults({ start: { status: "failed", msg: errText(e) } }); setRunning(false); toast(errText(e), "warn", 7000); return; } let failedAt = ""; for (const j of jobs) { setCur(j.w.name); setProgress(null); if (!allowed(j)) { setResults((r) => ({ ...r, [j.w.name]: { status: "failed", msg: `Your role can't import into ${j.def ? j.def.short : j.dest}.` } })); failedAt = j.w.name; break; } const plan = planJob(j); setPlans((p) => ({ ...p, [j.w.name]: plan })); if (plan.error) { setResults((r) => ({ ...r, [j.w.name]: { status: "failed", msg: plan.error } })); failedAt = j.w.name; break; } const meta = { file: file.name, sheet: j.sh.name, source: "Old system workbook" }; let res; try { res = await Dm.importStage(j.dest, plan, meta, (p) => setProgress(p), { runId, part: C().partName(j.w.name), noFinish: true, allowBlocked: true }); } catch (e) { res = { ok: false, msg: errText(e) }; } setProgress(null); if (!res || !res.ok) { setResults((r) => ({ ...r, [j.w.name]: { status: "failed", msg: (res && res.msg) || "failed", plan } })); failedAt = j.w.name; break; } setResults((r) => ({ ...r, [j.w.name]: { status: "done", msg: res.msg, counts: res.counts, histId: res.id, server: res.server, plan, blocked: plan.blocked ? plan.blockers[0].title : "" } })); tick(); } let fin = null; try { fin = await Dm.importStageFinish(runId, {}); } catch (e) { toast(errText(e), "warn", 7000); } setStaged({ runId, run: fin && fin.run ? fin.run : null, review: fin && fin.review ? fin.review : null }); if (failedAt) setStopped(failedAt); setCur(""); setRunning(false); }; const skipAndContinue = () => { setStopped(""); }; return (
📚 The whole old-system workbook — {file.name}
{jobs.length} sheets to import in the README's load order{extras.length ? ` · ${extras.length} kept for reference` : ""}{det.unknown.length ? ` · ${det.unknown.length} not recognised` : ""}{det.missing.length ? ` · ${det.missing.length} missing` : ""}{asOf ? ` · opening balances as of ${asOf}` : ""}
How this works. Each sheet is planned exactly as if you had imported it alone (the same column rules, the same preview rules) and every sheet goes into ONE import run on the server, in the order below. Nothing is saved and nothing is posted to the General Ledger — not even the opening-balances entry — until the owner approves the run on Accounting › Import review & approval, where every problem row is explained and the lists are tied to the ledger (the Tie_Out sheet, or the defaults). A sheet with a problem (opening balances that do not net to zero, say) does not stop the others: it is shown as a problem on the review screen.
{jobs.map((j, i) => { const p = plans[j.w.name], r = results[j.w.name]; const mapped = j.headers.filter((h) => j.mapping[h] && j.mapping[h] !== (Dm.IMPORT_SKIP || "__skip")).length; return ( ); })} {extras.map((w) => { const orphan = w.partOf && !det.found[w.partOf]; return ; })} {det.unknown.map((u) => )} {det.missing.map((n) => )}
#SheetRowsGoes toColumnsPlan (before anything runs)Result
{i + 1} {sheetName(j.w)}{j.w.linesSheet && det.found[j.w.linesSheet] ?
+ {j.w.linesSheet} ({det.found[j.w.linesSheet].rows.length} lines)
: null}
{j.rows.length.toLocaleString("en-US")} {j.def ? j.def.label : j.dest}
{j.w.why}
{!allowed(j) &&
Your role can't import here.
}
{mapped} imported{j.notImported.length ? `, ${j.notImported.length} not` : ""}
{j.notImported.length > 0 &&
not imported
    {j.notImported.map((x) =>
  • {x.col} — {x.why}
  • )}
}{j.historyOnly && j.historyOnly.length > 0 &&
{j.historyOnly.length} kept as history only (meaning unconfirmed): {j.historyOnly.map((x) => {x.col})}
}{j.note &&
{j.note}
}
{!p ? "…" : p.error ? {p.error} : <>
{countsText(p.counts)}
{p.blocked &&
⚠ {p.blockers[0].title}{j.w.name !== "Chart_of_Accounts" && /account/i.test(p.blockers[0].title) ? " — checked again after the chart is imported" : ""}
}{p.summary && p.summary.kind === "trialBalance" && p.summary.entry &&
one entry {p.summary.entry.ref} · {p.summary.entry.lines} lines · {money(Math.round(p.summary.entry.debits * 100))}
}{p.summary && p.summary.kind === "roHistory" &&
closed {p.summary.byStatus.closed} · open {p.summary.byStatus.open} · held {p.summary.byStatus.held} · void {p.summary.byStatus.void} · {p.summary.lines} job lines · {p.summary.batches} batches
}{p.counts && p.counts.rejected > 0 && j.dest === "customers" &&
rejected rows: a customer # that does not match the phone, a code word in the number column, or the same 7 digits on another area code (the office adds those in the CRM)
}}
{cur === j.w.name ? ⏳ sending…{progress && progress.total ? ` batch ${progress.done} of ${progress.total}` : ""} : r ? (r.status === "done" ? {r.blocked ? "⚠ staged with a problem: " + r.blocked : "✓ staged"}{r.counts ? ` — ${r.counts.staged || 0} ready · ${r.counts.rejected || 0} problems · ${r.counts.skipped || 0} skipped` : ""} : ✕ {r.msg}) : waiting}
{sheetName(w)}{det.found[w.name].rows.length.toLocaleString("en-US")}{orphan ? `NOT imported: its ${w.partOf} sheet is not in this file (or was not recognised) — job lines need their repair orders. Import the repair orders with them, or import this sheet later into Service — repair orders once the repair orders are on the server.` : <>{w.partOf ? "imported together with " + w.partOf : w.skip ? "not imported" : "kept for reference — not imported"}: {w.why}}
{u.name}{u.rows.toLocaleString("en-US")}Not recognised — NOT imported. Its name and columns match none of the old export's sheets ({WB_SHEETS.filter((w) => !w.skip).map((w) => w.name).join(", ")}). Rename it to the sheet it is, or import it on its own with the wizard (choose its destination first).
{n}{n === "GL_Opening_Balances" ? "Not in this file: NO opening balances are posted. The inventory accounts will carry no balance, so on Reconcile inventory to GL the right posting is (b) “Post the inventory to the books at cost” — unless the opening balances reach the ledger another way first (then (a))." : "Not in this file — nothing is imported for it."}
{progress && progress.total > 0 &&
{cur}: batch {progress.done} of {progress.total} — {(progress.rows || 0).toLocaleString("en-US")} of {(progress.rowsTotal || 0).toLocaleString("en-US")} rows
} {stopped &&
The run stopped at {stopped}: {results[stopped] && results[stopped].msg} — the sheets before it are on the server waiting for approval. Fix the file (or the books) and run again (a new run).
} {staged && !running && (
Every sheet is on the server — import run {staged.runId} is {staged.run ? staged.run.statusWord : "waiting for approval"}. Nothing is saved or posted yet. {staged.run ? `${staged.run.totals.ready} rows ready · ${staged.run.totals.problems} with problems · ${staged.run.totals.skipped} skipped.` : ""} The owner reviews it on Import review & approval: every problem row with what it most likely means, the lists tied to the ledger as of {asOf || "the as-of date"}{noOpening ? " (this file had NO opening balances — the accounts carry no balance yet)" : ""}, and the reconciling entries to tick.{det.unknown.length ? ` ${det.unknown.length} sheet${det.unknown.length === 1 ? " was" : "s were"} not recognised and not imported.` : ""}
)}
{!staged && }
); }; /* ═══════════════════════════════ 2. Reconcile inventory to GL ═══════════════════════════════ */ // RC25 fix round: the account that takes an adjusting entry is never guessed into a GM-program account, and when the imported units // carry no account (the old Inventory sheet has none) the office picks it — the default is the only account, else the account the // assigned items sit on with the largest difference (never a PROGRAM one), else nothing. const isProgram = (a) => /PROGRAM/i.test(String((a && a.name) || "")); const defaultPick = (s) => { if (!s.accounts.length) return ""; if (s.accounts.length === 1) return s.accounts[0].acct; const withItems = s.accounts.filter((a) => a.items > 0 && !isProgram(a)).sort((a, b) => Math.abs(b.diffCents) - Math.abs(a.diffCents)); return s.unassignedItems === 0 && withItems.length ? withItems[0].acct : ""; }; const ScheduleCard = ({ s, data, conv, may, onPost, busy, acctSel, setAcctSel, complete, setComplete, perAcct, setPerAcct }) => { const [open, setOpen] = React.useState(false); const diffColor = s.diffCents === 0 ? "#176A38" : "#A02234"; const pick = acctSel[s.key] !== undefined ? acctSel[s.key] : defaultPick(s); const unit = s.unit; const confirmed = !s.partial || !!complete[s.key]; const blockedWhy = !may ? "The office posts this (Admin / F&I, Office Manager, Accounting)." : !conv ? "Pick the conversion / clearing account first." : s.laterEntry && s.laterEntry.future ? "An entry on these accounts is dated in the future — correct its date in Accounting first." : s.laterEntry ? "An entry on these accounts is dated after this day — pick a later as-of date." : !confirmed ? "Tick “the list is complete” first (only when the accountant confirms it)." : ""; const ready = may && conv && !busy && confirmed; return (
{s.label}
{s.detail}
{s.partial && information}
{s.hasDetail ? (s.diffCents === 0 ? "✓ agrees to the cent" : (s.diffCents > 0 ? "list higher by " : "ledger higher by ") + money(Math.abs(s.diffCents))) : "no imported list yet"}
{s.accounts.map((a) => )} {!s.accounts.length && } {s.unassignedItems > 0 && }
GL accountNameLedger balanceOn the listDifference{unit}s
{a.acct}{a.name}{money(a.glCents)}{a.items ? money(a.subCents) : —}{a.items ? money(a.diffCents) : ""}{a.items || ""}
No general-ledger account is mapped to this schedule — open “Schedule map” and pick the account(s).
{s.unassignedItems} {unit}{s.unassignedItems === 1 ? "" : "s"} without a GL account on the record{s.accounts.length > 1 ? " — the schedule total is what ties; the list cannot be split by account" : ""}{money(s.unassignedCents)}{s.unassignedItems}
Total{money(s.glCents)}{s.hasDetail ? money(s.subledgerCents) : "—"}{s.hasDetail ? money(s.diffCents) : ""}{s.itemCount || ""}
{s.words.state}
{s.importRef &&
The list comes from {s.importRef.kind}{s.importRef.file ? " “" + s.importRef.file + "”" : ""}{s.importRef.at ? ", " + String(s.importRef.at).slice(0, 10) : ""} — a posting is linked to it ({s.importRef.id}).
} {s.problemItems > 0 &&
⚠ {s.problemItems} {unit}{s.problemItems === 1 ? " has a cost" : "s have a cost"} that was rounded to the cent or could not be read — see “Show the {unit}s” (the note on each); fix the record before posting.
} {/* RC28 (fx, the reviewer's finding 1): a cost / quantity that can't be read blocks every posting of the schedule (never counted as $0.00) */} {s.unreadableItems > 0 &&
⛔ {s.words.unreadable} {(s.unreadable || []).slice(0, 8).map((u) => (u.missing ? `${u.id} (no cost entered)` : `${u.id} (${u.field === "onHand" ? "on hand" : u.field === "costBooked" ? "GL cost" : u.field} "${u.value}")`)).join(", ")}{s.unreadableItems > 8 ? " …" : ""}
} {s.laterEntry &&
⚠ {s.words.later}
} {s.partial && (
{s.words.partial} {s.hasDetail && s.diffCents !== 0 && may && }
)} {s.hasDetail && s.itemCount > 0 &&
} {open && (
{(s.key === "parts" ? ["Part #", "Description", "Bin", "On hand", "Cost", "Value", "Account", "Note"] : s.key === "wip" ? ["RO #", "Date", "Customer", "Vehicle", "Labor cost", "On the board"] : s.key === "ar" ? ["Who", "A/R control #", "GL account", "Amount"] : SVC_KEYS[s.key] ? ["What", "RO / claim / core", "Date", "Customer", "Amount", "Note"] : ["Stock #", "VIN", "Vehicle", "N/U", "Account", "GL cost", "Note"]).map((h) => )}{(s.items || []).map((it, i) => ( s.key === "parts" ? : s.key === "wip" ? : SVC_KEYS[s.key] ? : s.key === "ar" ? : ))}{s.itemsHidden > 0 && }
{h}
{it.part}{it.desc}{it.bin}{it.onHand}{money(it.costCents)}{money(it.cents)}{it.acct || ""}{it.note || ""}
{it.roNo || it.part}{it.date || (it.kind === "tank" ? "bulk oil" : it.status === "held" ? "held" : "open")}{it.customer || it.desc || ""}{it.vehicle || ""}{it.costHidden ? "—" : money(it.cents)}{it.k2bRoId || "—"}
{SVC_ITEM_WORD[it.kind] || it.kind}{it.claim || it.roNo || it.part || ""}{it.date || ""}{it.customer || it.desc || ""}{it.costHidden ? "—" : money(it.cents)}{it.note || (it.imported ? "imported" : "")}
{it.customer}{it.control}{it.acct || ""}{money(it.cents)}
{it.stock}{it.vin}{it.desc}{it.type}{it.acct || none{it.guessedAcct ? ` (the import guessed ${it.guessedAcct} from the model — not used)` : ""}}{money(it.cents)}{it.note || ""}
… and {s.itemsHidden} more (all are counted).
)} {s.posts && s.posts.length > 0 &&
Earlier: {s.posts.slice(0, 3).map((p) => `${p.kind === "checked" ? "marked reconciled" : p.kind === "adjust" ? "adjusting entry " + p.journalId + " " + money(p.diffCents) : "posted at cost " + p.journalId + " " + money(p.diffCents)} as of ${p.asOf} (${p.by || ""}${p.runId ? ", import " + p.runId : ""})`).join(" · ")}
} {/* the two actions — ALWAYS both offered, with the words that say when each is the right one */}
(a) Post one adjusting entry for the difference
{s.words.adjust}
{s.accounts.length > 1 && } {s.accounts.length > 1 && s.unassignedItems === 0 && }
(b) Post the inventory to the books at cost
{s.words.atCost}
{s.accounts.length > 1 && s.unassignedItems > 0 && }
{s.canMark && may &&
}
); }; // RC33 (svi): the service lists' items on Reconcile Inventory const SVC_KEYS = { sublet: 1, warranty: 1, body: 1 }; const SVC_ITEM_WORD = { ro: "old RO", k2bro: "K2B RO (sublet)", core: "core on the shelf", claim: "warranty claim", chargeback: "chargeback of a paid claim (GM takes it next)", job: "body-shop job", tank: "bulk oil" }; // chargeback: RC33 merge (rc33m) const ImpReconcileWindow = ({ onClose, go, embedded, asOf: asOf0 }) => { const Dm = D(); const [asOf, setAsOf] = React.useState(asOf0 || todayIso()); const [data, setData] = React.useState(null); const [settings, setSettings] = React.useState(null); const [err, setErr] = React.useState(""); const [loading, setLoading] = React.useState(false); const [conv, setConv] = React.useState(""); const [acctSel, setAcctSel] = React.useState({}); const [perAcct, setPerAcct] = React.useState({}); // RC28 (fx, finding 4): one adjusting entry per account const [complete, setComplete] = React.useState({}); // RC25 fix round: "the list is complete" per partial schedule (WIP / A/R) const [busy, setBusy] = React.useState(""); const [msg, setMsg] = React.useState(null); const [confirm, setConfirm] = React.useState(null); const [mapOpen, setMapOpen] = React.useState(false); const [mapDraft, setMapDraft] = React.useState({}); const [exDraft, setExDraft] = React.useState([]); // RC26 (inv): the office's extra non-inventory accounts const may = !!(data && data.may && data.may.post); const load = React.useCallback(async (date) => { if (!Dm || typeof Dm._apiGet !== "function") { setErr("This DMS page is older than the server — reload it (Ctrl+F5)."); return; } setLoading(true); setErr(""); try { const d = await Dm._apiGet("/import/reconcile?asOf=" + encodeURIComponent(date || asOf)); setData(d); setConv((c) => (c && (d.conversion.choices || []).some((a) => a.acct === c)) ? c : (d.conversion.acct || "")); if (!settings) { try { setSettings(await Dm._apiGet("/import/reconcile/settings")); } catch (e) {} } } catch (e) { setErr(errText(e)); } setLoading(false); }, [asOf, settings]); React.useEffect(() => { load(asOf); }, []); // eslint-disable-line const onPost = (kind, s, pick) => { if (kind === "mark") return doPost(kind, s, pick); const Cc = C(); let lines = []; // RC28 (fx, finding 4): "only this account, to its own list" — the account's own difference (one entry per account) const one = kind === "adjust" && (perAcct[s.key] !== undefined ? perAcct[s.key] : s.needsPerAccount) ? (s.accounts.find((a) => a.acct === pick) || null) : null; // RC28 fix round (audit FU2) if (kind === "adjust") lines = Cc ? Cc.adjustLines(pick, conv, one ? one.diffCents : s.diffCents) : []; else { const by = {}; s.accounts.forEach((a) => { if (a.subCents) by[a.acct] = (by[a.acct] || 0) + a.subCents; }); if (s.unassignedCents) by[pick || (s.accounts[0] || {}).acct] = (by[pick || (s.accounts[0] || {}).acct] || 0) + s.unassignedCents; lines = Cc ? Cc.atCostLines(by, conv) : []; } setConfirm({ kind, s, pick, lines }); }; const doPost = async (kind, s, pick) => { setConfirm(null); setMsg(null); const key = Dm._newIdemKey(kind === "adjust" ? "ia" : kind === "at-cost" ? "ic" : "im"); // RC25 fix round: the posting names the import it reconciles (runId) and, for a partial list, the office's "the list is complete" const extra = Object.assign({}, s.importRef && s.importRef.id ? { runId: s.importRef.id } : {}, s.partial && complete[s.key] ? { listComplete: true } : {}); const one = kind === "adjust" && (perAcct[s.key] !== undefined ? perAcct[s.key] : s.needsPerAccount) ? (s.accounts.find((a) => a.acct === pick) || null) : null; // RC28 fix round (audit FU2) const body = kind === "adjust" ? Object.assign({ schedule: s.key, account: pick, asOf, conversionAcct: conv, expectedDiffCents: one ? undefined : s.diffCents }, one ? { perAccount: true } : {}, extra) : kind === "at-cost" ? Object.assign({ schedule: s.key, asOf, conversionAcct: conv, expectedCents: s.subledgerCents }, pick ? { account: pick } : {}, extra) : { schedule: s.key, asOf }; setBusy(s.key + kind); const r = await Dm._command("/import/reconcile/" + kind, body, key, "/import/reconcile/op/"); setBusy(""); if (!r.ok) { const b = (r.err && r.err.body) || {}; const text = r.unknown ? `The server did not answer. Look at the ledger before trying again (the office can look up ${key}).` : b.error || (r.err && r.err.message) || "The server refused this posting."; setMsg({ level: "error", text, code: b.code, journalId: b.journalId }); toast(text, "warn", 7000); return; } const res = r.res || {}; setMsg({ level: "ok", text: res.message || "Posted.", journalId: res.journalId, replayed: !!res.replayed }); toast(res.message || "Posted.", "ok", 6000); if (res.conversionAcct) setConv(res.conversionAcct); load(asOf); }; const saveConv = async () => { try { await Dm._apiPost("/import/reconcile/settings", { conversionAcct: conv }); toast("Saved as the default conversion / clearing account.", "ok"); } catch (e) { toast(errText(e), "warn", 6000); } }; const openMap = () => { const d = {}; (data ? data.schedules : []).forEach((s) => { d[s.key] = s.accounts.map((a) => a.acct); }); setMapDraft(d); setExDraft(((data && data.excluded) || []).filter((x) => !x.fixed).map((x) => x.acct)); setMapOpen(true); }; const saveMap = async () => { try { const m = {}; Object.keys(mapDraft).forEach((k) => { m[k] = (mapDraft[k] || []).filter((a) => !(exDraft || []).includes(a)); }); await Dm._apiPost("/import/reconcile/settings", { map: m, exclude: exDraft || [] }); setMapOpen(false); setSettings(null); toast("Schedule map saved — it is now the default for everyone.", "ok"); load(asOf); } catch (e) { toast(errText(e), "warn", 6000); } }; const chart = (settings && settings.chart) || []; const invRange = chart.filter((a) => /^(22|23|24)\d/.test(a.acct)); const suggested = data ? data.conversion.suggested : []; // RC25 fix round (audit H3): only balance-sheet accounts outside the inventories / receivables can be the other side; shown with their type const choices = (data && data.conversion.choices) || []; const sugSet = new Set(suggested.map((a) => a.acct)); const groups = [["Suggested (the name says conversion / clearing / suspense / opening balance)", choices.filter((a) => sugSet.has(a.acct))], ["Equity and liabilities (3xx)", choices.filter((a) => !sugSet.has(a.acct) && /^3/.test(a.acct))], ["Other balance-sheet accounts", choices.filter((a) => !sugSet.has(a.acct) && !/^3/.test(a.acct))]].filter((g) => g[1].length); const convRow = choices.find((a) => a.acct === conv); const body = (
⚖ Reconcile inventory to the General Ledger
what the imported lists say is on the lot / shelf / in the shop, next to what the ledger carries, per inventory account
{err &&
⚠ {err}
} {msg &&
{msg.level === "ok" ? "✓ " : "✕ "}{msg.text}{msg.journalId && msg.level === "ok" ? <> — entry {msg.journalId} : null}
} {data && (
Conversion / clearing account — the other side of every posting from this screen. It must be a balance-sheet account (asset, liability or equity) that is on no schedule: a sales / cost account would put the difference into this month's earnings. {suggested.length ? <>Suggested from the chart (its name says conversion / clearing / suspense / opening balance): {suggested.map((a) => a.acct + " " + a.name).join(", ")}. : No balance-sheet account in the chart is named conversion / clearing / suspense / opening balance — the office must pick one (ask the accountant which equity or clearing account the conversion goes to; it is remembered as the default once used).}{data.conversion.savedRefused ? The saved default {data.conversion.savedRefused} is not allowed any more (not a balance-sheet account outside the inventories) — pick another. : null}
{convRow && {convRow.type} · balance sheet} as of {data.asOf} · {data.counts.entries} ledger entries · {data.counts.unitsOnLot} units on the lot · {data.counts.partsOnHand} parts with stock · {data.counts.openRos} open / {data.counts.heldRos} held imported ROs{data.openMonth && data.openMonth.open ? ` · open month ${data.openMonth.open}` : ""}{!may ? " · view only (the office posts)" : ""}
)} {data && data.schedules.map((s) => )} {data && (data.excluded || []).length > 0 && (
Not inventory — left off this screen
{data.excluded.map((a) => )}
{a.acct}{a.name}{a.note}
)} {data && window.ImpGlLive ? : null}{/* RC36 (imp36): every import's tie-out, read from the live books the same way the review reads it */} {data && data.other.length > 0 && (
Other inventory-type accounts (no imported list to compare with)
{data.other.map((a) => )}
{a.acct}{a.name}{money(a.glCents)}
Tires, body-shop materials, sublets, cores and the lots have no list in the old export; they stay as the opening balances say. Add one to a schedule with “Schedule map” if a list for it is imported later.
)}
{confirm && ( setConfirm(null)} buttons={<> setConfirm(null)}>Cancel}>
{confirm.s.label} as of {asOf}: {confirm.kind === "adjust" ? `the ledger carries ${money(confirm.s.glCents)}, the list says ${money(confirm.s.subledgerCents)} — the entry moves the ledger by ${money(Math.abs(confirm.s.diffCents))}.` : `the list says ${money(confirm.s.subledgerCents)} and the accounts are empty — the entry books every ${confirm.s.unit} at cost.`}
{confirm.lines.map((l, i) => )}
{l.acct}{l.dr ? "DR " + money(Math.round(l.dr * 100)) : ""}{l.cr ? "CR " + money(Math.round(l.cr * 100)) : ""}
The other side: {conv} {convRow ? convRow.name + " (" + convRow.type + ", balance sheet)" : ""}.{confirm.s.partial ? " You confirmed the list is complete." : ""}{confirm.s.importRef ? ` Linked to the import ${confirm.s.importRef.id}.` : ""}
Dated {asOf}, source Import, written by the server once (a second click says it is already posted). It goes through the ledger's rules — balanced, chart accounts, open month (a closed month needs the owner's override). It can only be corrected by a reversing entry in Accounting.
)} {mapOpen && ( setMapOpen(false)} buttons={<> setMapOpen(false)}>Cancel}>
The map was taken from the account NAMES in the chart (inventory-range accounts 220 / 230–249). Tick the accounts each list should be compared with; an account can be on one schedule only.
Not inventory — leave off this screen
A lot, a building or other property the dealership owns is not inventory. 238 SAW MILL LOT is always left off (dealership property).
{invRange.map((a) => { const fixed = (data.excluded || []).some((x) => x.acct === a.acct && x.fixed); const on = fixed || (exDraft || []).includes(a.acct); return ( ); })}
{(data ? data.schedules : []).map((s) => (
{s.label}
{invRange.map((a) => { const on = (mapDraft[s.key] || []).includes(a.acct); const elsewhere = !on && Object.keys(mapDraft).some((k) => k !== s.key && (mapDraft[k] || []).includes(a.acct)); return ( ); })}
))}
)}
); if (embedded) return body; return (
{}} onMax={() => {}} /> {body}
); }; /* ═══════════════════════════════ 3. Old ROs (the imported repair-order history) ═══════════════════════════════ */ const ImpRoHistory = ({ embedded, go }) => { const Dm = D(); const [q, setQ] = React.useState({ ro: "", vin: "", customer: "", stock: "", q: "" }); const [rows, setRows] = React.useState(null); const [status, setStatus] = React.useState(null); const [sel, setSel] = React.useState(null); const [err, setErr] = React.useState(""); const [loading, setLoading] = React.useState(false); const [links, setLinks] = React.useState(null); // RC26 (inv): the links to K2B customers const [linkBusy, setLinkBusy] = React.useState(false); const [linkMsg, setLinkMsg] = React.useState(null); const [vinOpen, setVinOpen] = React.useState(""); const loadLinks = async () => { try { setLinks(await Dm._apiGet("/import/link-status")); } catch (e) { setLinks(null); } }; React.useEffect(() => { (async () => { try { setStatus(await Dm._apiGet("/import/status")); } catch (e) { setErr(errText(e)); } })(); loadLinks(); }, []); // eslint-disable-line const linkAll = async () => { setLinkBusy(true); setLinkMsg(null); const r = await Dm._command("/import/link-ros", {}, Dm._newIdemKey("lnk"), "/ops/op/"); setLinkBusy(false); const m = r.ok ? { ok: true, text: r.res.message } : { ok: false, text: (r.err && r.err.body && r.err.body.error) || (r.unknown ? "The server did not answer — look at the counts before pressing again." : "The server refused.") }; setLinkMsg(m); toast(m.text, m.ok ? "ok" : "warn", 8000); loadLinks(); }; const custWord = (r) => r.linkHow === "phone" || r.linkHow === "vin" ? `${r.customerNum} (by ${r.linkHow === "phone" ? "customer #" : "VIN"})` : r.linkHow === "waiting" ? `${r.customerNum} — waiting for a customer with this number` : r.linkHow === "ambiguous" ? `${r.possible.length} possible matches` : ""; // RC30 (cn): the account IS the customer #, no area code const search = async () => { setLoading(true); setErr(""); setSel(null); try { const ps = Object.keys(q).filter((k) => q[k].trim()).map((k) => k + "=" + encodeURIComponent(q[k].trim())); const r = await Dm._apiGet("/import/ro-history?" + ps.concat(["limit=200"]).join("&")); setRows(r.rows); } catch (e) { setErr(errText(e)); } setLoading(false); }; const openOne = async (roNo) => { try { const r = await Dm._apiGet("/import/ro-history/" + encodeURIComponent(roNo)); setSel(r.ro); } catch (e) { setErr(errText(e)); } }; const by = (status && status.ros && status.ros.byStatus) || {}; const body = (
Repair orders imported from the old system
{links && links.counts && Linked to customers: by phone {links.counts.phone} · by VIN {links.counts.vin} · waiting for a customer {links.counts.waiting} · several possible {links.counts.ambiguous} · no number / owner {links.counts.none}{links.counts.notLinked ? " · not linked yet " + links.counts.notLinked : ""}} {links && links.may && links.may.run && } {linkMsg && {linkMsg.text}} {status ? `${(status.ros.count || 0).toLocaleString("en-US")} on the server — closed ${(by.closed || {}).count || 0} · open ${(by.open || {}).count || 0} · held ${(by.held || {}).count || 0} · void ${(by.void || {}).count || 0} · ${(status.vehicles || 0).toLocaleString("en-US")} VINs in the lookup list` : "…"}
{[["ro", "RO #"], ["vin", "VIN"], ["customer", "Customer account"], ["stock", "Stock #"], ["q", "Anything"]].map(([k, l]) => )}
{err &&
⚠ {err}
} {rows && (
{["RO #", "Date", "Status", "Account", "Customer #", "Vehicle", "VIN", "Payer", "Total"].map((h) => )}{rows.map((r) => openOne(r.roNo)} style={{ cursor: "pointer" }}>)}{!rows.length && }
{h}
{r.roNo}{r.date || (r.status === "open" ? "open" : "")}{r.void ? "VOID" : r.status}{r.k2bRoId ? " · " + r.k2bRoId : ""}{r.customerAccount}{custWord(r)}{[r.year, r.make, r.model].filter(Boolean).join(" ")}{r.vin}{r.payerWord}{money(r.cents.total)}
Nothing found.
)} {sel && (
RO {sel.roNo} — {sel.void ? "VOID" : sel.flagWord} {sel.date ? "· " + sel.date : ""} {sel.k2bRoId ? : null}
K2B customer: {sel.linkHow === "phone" || sel.linkHow === "vin" || sel.linkHow === "waiting" ? sel.customerNum : "—"} {sel.linkNote ? — {sel.linkNote} : null}{sel.possible && sel.possible.length ? · possible matches: {sel.possible.map((p) => p.custNum + (p.name ? " " + p.name : "")).join("; ")} : null}{sel.vin ? : null}
Old account {sel.customerAccount || "—"} · Vehicle {[sel.year, sel.make, sel.model, sel.color].filter(Boolean).join(" ")} · VIN {sel.vin || "—"}{sel.stock ? ` · stock ${sel.stock}` : ""} · {sel.odometer != null ? sel.odometer.toLocaleString("en-US") + " mi" : ""} · tech {sel.tech || "—"} · advisor {sel.advisor || "—"}
{["Parts", "Labor", "Sublet", "Supplies", "Tax", "Total", "Customer pay *", "Warranty / internal *"].map((h) => )}{["partsSale", "laborSale", "subletSale", "supplies", "salesTax", "total", "customerPay", "otherPay"].map((k) => )}{sel.costHidden ? : ["partsCost", "laborCost", "subletCost"].map((k) => )}
{h}
{money(sel.cents[k])}
cost: not shown to your rolecost {money(sel.cents[k])}payer: {sel.payerWord}{sel.taxRatePct != null ? ` · tax rate ${sel.taxRatePct}%` : ""}
{sel.lines && sel.lines.length > 0 &&
Job lines
    {sel.lines.map((l, i) =>
  1. {l.text}{l.op ? ({l.op}) : null}
  2. )}
} {sel.note &&
{sel.note}
}
History from the old system (import run {sel.runId}). No journal entry was posted for it in K2B. * the old export's x_customer_pay_total / x_warranty_or_internal_total, shown as printed: their meaning is not confirmed, so they decide nothing in K2B. The old account is the old service system's number — the customer's phone without the area code, which IS the K2B Customer # (RC30); the link above is by that number first, then by the VIN's owner at the RO date.
)}
); const withVin = <>{body}{vinOpen && setVinOpen("")} />}; if (embedded) return withVin; return
{}} />{withVin}
; }; /* ═══════════════════════════════ RC26 (inv): the history of a vehicle by VIN, the customer's old ROs ═══════════════════════════════ */ const KIND_WORD = { deal: "Sold", trade: "Traded in", "ro-old": "Old RO", ro: "K2B RO", body: "Body shop" }; const ImpVehicleHistory = ({ vin, onClose }) => { const Dm = D(); const [d, setD] = React.useState(null); const [err, setErr] = React.useState(""); React.useEffect(() => { (async () => { try { setD(await Dm._apiGet("/import/vehicle-history?vin=" + encodeURIComponent(vin))); } catch (e) { setErr(errText(e)); } })(); }, [vin]); // eslint-disable-line return ( Close}>
{err &&
⚠ {err}
} {!d && !err &&
Loading…
} {d &&
{d.vehicle || "Vehicle"} · VIN {d.vin}{d.inStock ? ` · in inventory as ${d.inStock.stock} (${d.inStock.status || "—"})` : ""} · {d.counts.deals} sale{d.counts.deals === 1 ? "" : "s"}, {d.counts.trades} trade{d.counts.trades === 1 ? "" : "s"}, {d.counts.oldRos} old RO{d.counts.oldRos === 1 ? "" : "s"}, {d.counts.k2bRos} K2B RO{d.counts.k2bRos === 1 ? "" : "s"}
} {d && ( {["Date", "What", "Number", "Owner at the time", "Odometer", "Total"].concat(d.costShown ? ["Cost"] : []).map((h) => )}{d.events.map((e, i) => ( {d.costShown && } ))} {!d.events.length && }
{h}
{e.date || "—"} {KIND_WORD[e.kind] || e.kind}{e.status && e.kind !== "deal" && e.kind !== "trade" ? " · " + e.status : ""} {e.roNo || e.roId || e.dealId || ""} {e.owner ? (e.owner.name || "—") + (e.owner.custNum ? " · " + e.owner.custNum : "") + (e.kind === "ro-old" && e.linkHow === "waiting" ? " (waiting for a customer)" : "") : (e.kind === "ro-old" && e.possible && e.possible.length ? e.possible.length + " possible customers" : "—")} {e.odometer != null && e.odometer !== "" ? Number(e.odometer).toLocaleString("en-US") : ""} {e.totalCents != null ? money(e.totalCents) : ""}{e.costCents != null ? money(e.costCents) : ""}
Nothing on file for this VIN.
)}
The owner of an old RO is its linked K2B customer, else the vehicle's owner at that date from the deals (the latest sale before it, or the customer who traded it in later).
); }; const ImpVinHistoryButton = ({ vin, label, small }) => { const [open, setOpen] = React.useState(false); const v = String(vin || "").trim(); if (v.length < 11) return null; return <>{open && setOpen(false)} />}; }; const ImpCustomerOldRos = ({ custNum }) => { const Dm = D(); const [d, setD] = React.useState(null); React.useEffect(() => { let live = true; (async () => { try { const r = await Dm._apiGet("/import/customer-history?custNum=" + encodeURIComponent(custNum)); if (live) setD(r); } catch (e) { if (live) setD({ ok: false }); } })(); return () => { live = false; }; }, [custNum]); // eslint-disable-line if (!d || d.ok === false || (!d.ros.length && !d.vehicles.length)) return null; return (
Old repair orders — the old system ({d.ros.length}){d.counts.waiting ? ` · ${d.counts.waiting} matched by this Customer #` : ""}
{d.ros.slice(0, 200).map((r) => (
{r.roNo}{r.date}{r.vehicle}{money(r.totalCents)}{r.linkHow === "phone" ? "by phone" : r.linkHow === "vin" ? "by VIN" : r.linkHow === "waiting" ? "by customer #" : r.linkHow}
))}
{d.vehicles.length > 0 &&
{d.vehicles.map((v) =>
{v.vehicle || "Vehicle"}{v.vin}{v.oldRos} old RO{v.oldRos === 1 ? "" : "s"}{v.deals ? " · bought" : ""}{v.trades ? " · traded in" : ""}
)}
}
); }; /* ═══════════════════════════════ 4. the panel under the Trial Balance ═══════════════════════════════ */ const ImpTbSchedules = () => { const Dm = D(); const [sum, setSum] = React.useState(null); React.useEffect(() => { (async () => { try { setSum(await Dm._apiGet("/import/reconcile/summary")); } catch (e) { setSum({ error: errText(e) }); } })(); }, []); // eslint-disable-line if (!sum || sum.error || !sum.accounts || !sum.accounts.length) return null; const fmtAt = (iso) => { const d = new Date(iso); return isNaN(d) ? "" : d.toLocaleDateString("en-US"); }; return (
Inventory accounts tied to a schedule(the imported lists on Reconcile inventory to GL)
{sum.accounts.map((a) => )}
{a.acct}{a.name}{a.scheduleLabel}{a.lastReconciled ? `last reconciled as of ${a.lastReconciled.asOf} (${a.lastReconciled.kind === "checked" ? "at zero" : a.lastReconciled.kind === "adjust" ? "adjusting entry " + a.lastReconciled.journalId : "posted at cost " + a.lastReconciled.journalId}, ${fmtAt(a.lastReconciled.at)})` : "not reconciled yet"}
); }; /* ═══════════════════════════════ 5. RC30 (imp2): Import review & approval ═══════════════════════════════ Every import waits here. Per run: the parts (sheets) with rows ready / with problems / skipped; every problem row with "what this most likely means" and the fix (the server's rule table — ImpCalc.RULES, the same words on every station); the GL tie-out as if approved (list total vs the ledger balance on the as-of date per account, the difference, the ranked likely causes); the reconciling entries to tick; the conversion clearing account; the typed confirmation. The SERVER approves (one command, one transaction), rejects or undoes. */ const LEVEL_WORD = { reject: "not imported", warn: "imported, with a note", ready: "ready", skip: "skipped" }; // RC33 (svi): the review's words for the lists an account ties to const TIE_WORD = { inventory: "Inventory", parts: "Parts", ar: "Customer A/R", wip: "WIP labor (open ROs)", sublet: "Sublet (open ROs)", warranty: "Warranty claims", body: "Body shop jobs", info: "Information", roparts: "Parts on open ROs" }; const TIE_UNIT = { wip: " items", sublet: " items", warranty: " claims", body: " jobs", info: "", roparts: "" }; const ImpReviewWindow = ({ onClose, go, embedded }) => { const Dm = D(); const [runs, setRuns] = React.useState(null); const [may, setMay] = React.useState({ approve: false }); const [sel, setSel] = React.useState(() => { try { return localStorage.getItem("k2b-imp-review-run") || ""; } catch (e) { return ""; } }); const [rv, setRv] = React.useState(null); const [err, setErr] = React.useState(""); const [loading, setLoading] = React.useState(false); const [asOf, setAsOf] = React.useState(""); const [ticks, setTicks] = React.useState({}); const [complete, setComplete] = React.useState({}); // RC33 (svi): "the list is complete" per account (a partial list posts only with it) const [glPost, setGlPost] = React.useState([]); // RC36 (imp36): the ticked entries of the tie-out by import (ImpGl.jsx.txt) const rvNarrow = typeof window !== "undefined" && window.innerWidth < 760; // RC36 (imp36): phones const [conv, setConv] = React.useState(""); const [confirm, setConfirm] = React.useState(""); const [reason, setReason] = React.useState(""); const [busy, setBusy] = React.useState(""); const [msg, setMsg] = React.useState(null); const [openPart, setOpenPart] = React.useState({}); const [filter, setFilter] = React.useState("all"); const loadRuns = React.useCallback(async () => { if (!Dm || typeof Dm._apiGet !== "function") { setErr("This DMS page is older than the server — reload it (Ctrl+F5)."); return; } try { const r = await Dm._apiGet("/import/stage/runs?limit=100"); setRuns(r.runs || []); setMay(r.may || {}); if (!sel) { const w = (r.runs || []).find((x) => x.status === "waiting") || (r.runs || [])[0]; if (w) setSel(w.runId); } } catch (e) { setErr(errText(e)); } }, [sel]); const load = React.useCallback(async (runId, date, keepMsg) => { if (!runId) { setRv(null); return; } setLoading(true); setErr(""); if (!keepMsg) setMsg(null); try { const d = await Dm._apiGet("/import/stage/runs/" + encodeURIComponent(runId) + "/review" + (date ? "?asOf=" + encodeURIComponent(date) : "")); setRv(d); setAsOf(d.tieOut ? d.tieOut.asOf : (d.run.asOf || "")); setConv((c) => (d.tieOut && d.tieOut.conversion ? (c && (d.tieOut.conversion.choices || []).some((a) => a.acct === c) ? c : d.tieOut.conversion.acct || "") : "")); setTicks({}); setComplete({}); setConfirm(""); } catch (e) { setErr(errText(e)); setRv(null); } setLoading(false); }, []); React.useEffect(() => { loadRuns(); }, []); // eslint-disable-line React.useEffect(() => { load(sel, ""); try { if (sel) localStorage.setItem("k2b-imp-review-run", sel); } catch (e) {} }, [sel]); // eslint-disable-line const run = rv && rv.run; const tie = rv && rv.tieOut; const postings = tie ? tie.accounts.filter((a) => a.posting && ticks[a.acct] && (!a.posting.needsConfirm || complete[a.acct])) : []; const act = async (kind) => { if (!run) return; setMsg(null); setBusy(kind); const key = Dm._newIdemKey(kind === "approve" ? "iap" : kind === "reject" ? "irj" : "iun"); const body = kind === "approve" ? { confirm, postings: postings.map((a) => Object.assign({ kind: "adjust", schedule: a.schedule, account: a.acct, perAccount: true, expectedDiffCents: a.diffCents }, a.posting.needsConfirm ? { listComplete: true } : {})).concat(glPost.map((g) => ({ kind: "gl-tie", dest: g.dest, account: g.account, expectedDiffCents: g.expectedDiffCents, listComplete: g.listComplete }))), asOf, conversionAcct: conv || undefined } : kind === "reject" ? { reason } : { confirm }; const r = await Dm._command("/import/stage/runs/" + encodeURIComponent(run.runId) + "/" + kind, body, key, "/import/stage/op/"); setBusy(""); let m; if (!r.ok) { const b = (r.err && r.err.body) || {}; const text = r.unknown ? `The server did not answer. Look at the run's status before trying again (the office can look up ${key}).` : b.error || (r.err && r.err.message) || "The server refused."; m = { level: "error", text, code: b.code, used: b.used }; toast(text, "warn", 8000); } else { const res = r.res || {}; m = { level: "ok", text: res.message || "Done.", code: kind, journals: res.journals, replayed: !!res.replayed }; toast(res.message || "Done.", "ok", 7000); setConfirm(""); setReason(""); } await loadRuns(); await load(run.runId, asOf, true); // the refreshed run keeps the command's answer on the screen setMsg(m); }; const partRows = (p) => (p.problems || []).filter((x) => filter === "all" || x.level === filter); const body = (
✔ Import review & approval
every import waits here — nothing is saved or posted until the owner approves it
{may.approve ? "You may approve" : "View only — the owner (or a login the owner granted) approves"}
{/* RC36 (imp36): a phone stacks the run list above the review */}
{!runs &&
Loading…
} {runs && !runs.length &&
No import runs yet. Import a file (Data & Admin › Import / Export) — it shows up here, waiting for approval.
} {(runs || []).map((r) => (
setSel(r.runId)} style={{ ...box, padding: "6px 9px", cursor: "pointer", borderColor: sel === r.runId ? "#1B66A8" : "#C9D6E3", background: sel === r.runId ? "#EEF5FC" : r.status === "waiting" ? "#FFF8E6" : "#FFF" }}>
{r.runId}{r.statusWord}
{r.fileName || r.kind}{r.source ? " · " + r.source : ""}
{r.totals.ready} ready · {r.totals.problems} problems · {r.totals.skipped} skipped · {(r.parts || []).length} part{(r.parts || []).length === 1 ? "" : "s"}{r.asOf ? " · as of " + r.asOf : ""}
))}
{err &&
⚠ {err}
} {msg &&
{msg.level === "ok" ? "✓ " : "✕ "}{msg.text}{msg.journals && msg.journals.length ?
Posted: {msg.journals.map((j) => j.id).join(", ")}
: null}{msg.used && msg.used.length ?
    {msg.used.slice(0, 20).map((u, i) =>
  • {u.collection} {u.id} — {u.why}
  • )}
: null}
} {!run && !err &&
Pick an import run on the left.
} {run && ( <>
{run.runId} — {run.statusWord}
{run.fileName || run.kind}{run.source ? " · " + run.source : ""} sent by {run.createdByName || run.createdBy} {run.finishedAt ? "on " + new Date(run.finishedAt).toLocaleString("en-US") : ""} {run.approvedAt && approved by {run.approvedByName || run.approvedBy} on {new Date(run.approvedAt).toLocaleString("en-US")}{run.journals && run.journals.length ? " · posted " + run.journals.map((j) => j.id).join(", ") : " · nothing posted"}} {run.rejectedAt && rejected: {run.rejectReason}} {run.undoneAt && undone on {new Date(run.undoneAt).toLocaleString("en-US")}} {run.totals.ready} rows ready · {run.totals.problems} with problems · {run.totals.skipped} skipped{run.totals.warnings ? ` · ${run.totals.warnings} with a note` : ""}
{(rv.checks || []).map((c, i) => (
{c.part}: {c.label}. {c.text}
What this most likely means:
    {(c.means || []).map((m, k) =>
  1. {m}
  2. )}
The fix: {c.fix}
))}
{(rv.parts || []).map((p) => ( ))}
Sheet / partGoes toRowsReadyWith problemsSkippedRead fromNotes
{p.part}{p.label}{p.counts.rows}{p.counts.ready}{p.counts.problems}{p.counts.skipped} {Object.keys(p.origins || {}).map((o) => {o === "text" ? "file" : o === "pdf-text" ? "read from PDF — check" : "scanned page (AI) — check"} {p.origins[o]})} {p.blocked ? ⚠ not written: {(p.blockers || []).map((b) => b.title).join("; ")} : null}{p.entry ? opening balances {p.entry.ref || p.entry.id} · {p.entry.lines} lines · dated {p.entry.date} : null}{p.alreadyPosted ? already posted as {p.alreadyPosted.id} — not posted again : null}{(p.metaWarnings || []).map((w, i) =>
{w.title}{w.detail ? ": " + w.detail : ""}
)}
{/* RC33 (svi): what the names on the service lists do at approval */} {(() => { const nm = rv.names || ((rv.parts || []).find((p) => p.names) || {}).names; if (!nm) return null; return (
Customers from the service lists ({nm.numbers} customer #{nm.numbers === 1 ? "" : "s"} with a name{nm.sources ? ` — repair orders ${nm.sources.roHistory || 0}, service list ${nm.sources.svcCustomers || 0}, A/R aging ${nm.sources.arAging || 0} rows` : ""}): {nm.create} created at approval · {nm.fill} on file, blank fields filled (nothing the office typed is overwritten) · {nm.same || 0} already complete · {nm.differs} another name on file — listed for the office, nothing changed · {(nm.two || 0) + (nm.mismatch || 0) + (nm.used || 0)} not created (two names / a phone of another number / a retired number){nm.others ? ` · ${nm.others} other name${nm.others === 1 ? "" : "s"} on older rows of ${nm.othersNums || nm.others} customer #${(nm.othersNums || nm.others) === 1 ? "" : "s"} — listed for the office (same household / a typo / a different person), nothing written for ${nm.others === 1 ? "it" : "them"}` : ""}{nm.noNumber ? ` · ${nm.noNumber} row${nm.noNumber === 1 ? "" : "s"} with a name on an account that is no customer # (no customer made)` : ""}.
); })()} {(rv.parts || []).filter((p) => (p.problems || []).length).map((p) => (
{p.part} — {p.problems.length} row{p.problems.length === 1 ? "" : "s"} to look at(rows "not imported" are left out when the run is approved; rows "with a note" are imported)
{openPart[p.part] !== false && ( {partRows(p).map((x) => ( ))}
RowRecordResultWhat is wrongWhat this most likely means · the fix
{(x.srcRows || []).join(" ") || x.rowNo} {x.key}{x.label ?
{x.label}
: null}{x.originWord ?
{x.originWord}{x.pageRef ? " · " + x.pageRef : ""}
: null}
{LEVEL_WORD[x.level] || x.level} {(x.reasons || []).map((r, i) =>
{r.field ? {r.field}: : null}{r.reason}
)}{(x.warnings || []).map((r, i) =>
{r.field ? {r.field}: : null}{r.reason}
)}{(x.rules || []).filter((r) => r.text && !(x.reasons || []).some((q) => q.reason === r.text)).map((r, i) =>
{r.text}
)}
{Array.from(new Map((x.rules || []).map((r) => [r.rule, r])).values()).map((r) =>
{r.label}{r.means && r.means.length ?
    {r.means.map((m, k) =>
  1. {m}
  2. )}
: null}{r.fix ?
Fix: {r.fix}
: null}
)}
)} {p.problems.length >= 300 &&
The first 300 rows are shown; every row is counted.
}
))} {/* RC36 (fpd36): the floorplan tie-out of an Inventory / Floorplan statement part (Floorplan.jsx.txt; the server answers run.floorplan) */} {run.floorplan && window.FpReviewTie ? : null} {tie && (
GL tie-out as if approved — the list total next to the ledger balance per account, as of setAsOf(e.target.value)} style={{ font: "inherit", padding: "2px 5px" }} />
accounts from {tie.source}{tie.obEntry ? ` · the staged opening-balances entry ${tie.obEntry.ref || tie.obEntry.id} (${tie.obEntry.lines} lines) counts on the ledger side` : ""} · {tie.counts.unitsOnLot} units on the lot after approval
{/* RC31 (fx31, RC30 acceptance F5 — owner decision): a file dated BEFORE entries already on the books is said FIRST; no entry is offered for those accounts */} {tie.dated &&
⚠ {(tie.dated.words || "").split(" — ")[0]}{(tie.dated.words || "").indexOf(" — ") !== -1 ? " — " + (tie.dated.words || "").split(" — ").slice(1).join(" — ") : ""}
} {/* RC31 fix round (fx31 checkers): a file dated after today, and items with no GL account of their own (an entry per account would write them off) — said FIRST */} {tie.future &&
⚠ {tie.future.words}
} {tie.noAccount &&
⚠ {tie.noAccount.words}
} {tie.badTieRows && tie.badTieRows.length > 0 &&
Tie_Out rows not understood: {tie.badTieRows.map((b) => `row ${b.row}${b.acct ? " (" + b.acct + ")" : ""}: ${b.why}`).join("; ")}
} {tie.accounts.map((a) => ( ))}
GL accountTies toList totalLedger {asOf}DifferenceWhat this most likely means (most likely first)Post on approval
{a.acct}
{a.name}
{TIE_WORD[a.tiesTo] || a.tiesTo}{a.note ?
{a.note}
: null}{a.source === "default" ?
default
: null}
{a.items ? money(a.listCents) : —}
{a.items ? a.items + (a.tiesTo === "ar" ? " list" : a.tiesTo === "parts" ? " parts" : TIE_UNIT[a.tiesTo] || " units") : ""}
{money(a.glCents)}{a.obCents ?
incl. staged OB {money(a.obCents)}
: null}
{a.noList ? "—" : a.items || a.glCents ? money(a.diffCents) : ""}
{a.state}
{(a.causes || []).map((c, i) =>
{c.label}
    {c.means.map((m, k) =>
  1. {m}
  2. )}
Fix: {c.fix}
)}
{a.info ? information — never posted from the import : a.posting ?
{a.posting.needsConfirm ? : null}{a.posting.holdWords ?
{a.posting.holdWords}
: null}
: a.noList ? no list imported for this account yet — nothing to compare, nothing to post (the balance stays as the sheet says) : (a.causes || [])[0] && (a.causes[0].rule === "as_of_future" || a.causes[0].rule === "entry_dated_future") ? nothing offered — {a.causes[0].rule === "as_of_future" ? "the file is dated after today (set the as-of date)" : (a.laterEntry ? a.laterEntry.id : "an entry") + " is dated in the future (correct its date in Accounting)"} : a.laterEntry ? nothing offered — the books already have {a.laterEntry.id} dated after this file (re-export today's list or set the as-of date) : (a.causes || [])[0] && ["tie_row_mismatch", "tie_not_on_schedule", "items_without_account", "unreadable_money"].indexOf(a.causes[0].rule) !== -1 ? nothing offered — {a.causes[0].rule === "tie_row_mismatch" ? "the Tie_Out sheet and the books tie this account to different lists" : a.causes[0].rule === "tie_not_on_schedule" ? "the account is on no schedule of the books" : a.causes[0].rule === "items_without_account" ? "items on this list carry no GL account of their own" : "a cost or quantity on this list cannot be read"} (see the reason) : a.diffCents ? {a.tiesTo === "ar" ? "A/R: correct with a journal entry after approval" : "no schedule for this account — post it on Reconcile Inventory after approval"} : nothing to post}
{tie.unassigned && tie.unassigned.units > 0 &&
{tie.unassigned.note}
} {tie.unassigned && tie.unassigned.partsNote &&
{tie.unassigned.partsNote}
} {tie.excluded && tie.excluded.length > 0 &&
Left off as property, not inventory: {tie.excluded.map((e) => `${e.acct} ${e.name} (${money(e.glCents)})`).join(" · ")}
}
Conversion clearing account — the other side of every reconciling entry (the owner's choice: 390 CONVERSION CLEARING). It must be a balance-sheet account on no schedule; a profit & loss account is refused.{tie.conversion.problem ?
{tie.conversion.problem}
:
Set: {tie.conversion.acct} {tie.conversion.name}
}
)} {rv.glTie && window.ImpGlTie ? : null}{/* RC36 (imp36): every import ties to the GL */} {run.status === "waiting" && (
Approve writes the {run.totals.ready} ready row{run.totals.ready === 1 ? "" : "s"} to the records{tie && tie.obEntry ? `, posts the opening-balances entry ${tie.obEntry.ref || tie.obEntry.id}` : ""}{postings.length ? `, and posts ${postings.length} adjusting entr${postings.length === 1 ? "y" : "ies"} (${postings.map((a) => a.acct + " " + money(Math.abs(a.diffCents))).join(", ")}) against the conversion clearing account` : ""}{glPost.length ? `, and posts ${glPost.length} tie-out entr${glPost.length === 1 ? "y" : "ies"} by import (${glPost.map((g) => g.words).join(", ")}) against the conversion clearing account` : ""} — in ONE step, audited. Rows with problems are left out. It can be undone while nothing has used the data.
setReason(e.target.value)} placeholder="why it is rejected" style={{ font: "inherit", padding: "3px 6px", width: 220 }} />
{!may.approve &&
Only the dealership owner (or a login the owner gave "approve imports" in the Owner panel) can approve. You can look and reject.
}
)} {run.status === "approved" && (
Undo reverses the entries this run posted (RV- entries, never deleted) and removes the records it wrote — refused with the list when anything has used or changed them since.
)}
The explanations come from a fixed rule table (ImpCalc RULES — the same words on every station). If a rule is wrong, send us the rule code shown with the row (for example cust_not_7), the row or account it fired on, and what the right answer was.
)}
); if (embedded) return body; return (
{}} onMax={() => {}} /> {body}
); }; Object.assign(window, { ImpWorkbookFlow, ImpReconcileWindow, ImpRoHistory, ImpTbSchedules, ImpVehicleHistory, ImpVinHistoryButton, ImpCustomerOldRos, ImpReviewWindow }); })();