---
name: tomago_exo_import
description: "Tomago Industrial Supplies EXO→Evolution invoice import tool — project scope, decisions, status"
metadata: 
  node_type: memory
  type: project
  originSessionId: d960bc1e-a472-45f8-b83d-4eff8e8cafd6
  modified: 2026-07-26T10:58:29.100Z
---

New client **Tomago Industrial Supplies** (prod tenant DB `304-1779936994`; also a **Test** tenant `304-1781836545`) is migrating off **MYOB EXO**. While running both systems in parallel, we sync EXO invoices into Evolution every couple of days.

Building an admin web tool (planned `?page=invoiceimport`, auto-routed via web-root `.inc`) that: browses/uploads EXO CSV exports, resolves refs to existing Evolution data, and injects `invoices` + `invoiceitems`. Reuse the `library/upload.php` upload UX (as `importer.php` does) but **drive the `invoice` class** (`newInvoice`→`addLines`→`recomputeBalance`), NOT the generic single-table `importer.php` (it's single-table + interpolates table name into SQL).

**Decisions (2026-07-18):**
- Unmatched stock code → post against **MISC** item (`inventory.id 28103`), keep original EXO code+desc in line description. Never skip lines.
- Import as **committed** (`commited=1`, GL-posted via `commitInvoice`) — NB this also **decrements inventory SOH** (COGS/asset postings for stocked items). Flag SOH movement in dry-run.
- **Let imported invoices sync to Xero** (tenant has `xerosync=1`); treat like normal invoices.
- Build/validate against the **Test tenant `304-1781836545`** (full read+write there); shift to live `304-1779936994` only later to test Xero integration. Writes need an INSERT grant for the `claude` user on the target DB.
- Gate page on `mod_ar==2 || administrator=='1'`; add menu entry manually in `app/templates/menu.php`.

**Build status (2026-07-18):** core built + resolver validated (PHP dry-run against real Test data). Files: `library/exoimport.php` (class exoImport: parse/resolve/dryRun/import — drives `invoice` class then `new accounts;->commitInvoice`), `invoiceimport.inc` (page, gated mod_ar==2||admin), `invoiceimport.php` (upload/dryrun/import ajax), `assets/js/modules/invoiceimport.js`, menu entry in `app/templates/menu.php` (admin section, "EXO INVOICE IMPORT"). NOT yet done: in-browser test on deployed app logged into Test tenant (dev code-server app can't reach prod Test DB, so can't click-test locally); release notes + docs per [[feature_release_docs_policy]]; validate column positions/link-key vs a FULL real export (samples were 2-3 rows). Open risks: EXO taxId map only confirms 10→GST (else amount-based fallback); EXO paid/balance NOT imported (lands unpaid — payments = possible phase 2); import has no txn wrapper (MyISAM), idempotency covers re-runs.

**2026-07-26 — the 15 `accname cannot be null` errors, and what they really meant.** The Test tenant's `preferences` row had `accRec = 0` / `accPay = 0` (live has 11 / 43 — the sandbox copy never brought the preferences row across). So **all 1,772 imported invoices committed with NO receivable leg** — see [[evolution_gl_control_account_validation]] for the mechanism. The echoed PDO error also landed mid-JSON, which is what made the UI say "Import request failed." on both runs — **not** the response timeout the handoff previously assumed (handoff corrected). Shane's call: **no GL backfill — resync Test from live and rerun the import** (needed for the bug-4 credit-note cutoff anyway); `projects/tis/reports/fix_test_tenant_control_accounts.sql` is now a verify-only step post-resync.

**Company-target guard (2026-07-26):** the tool ran against whatever `$_SESSION['userdb']` happened to be, so switching company in another tab then running the import could commit a whole load into the wrong tenant. Added a target-company `<select>` (populated from `evolution.companies` × `users_access` for the logged-in user) that is **verify-only** — it must match the current session or both run buttons disable with a warning; it does not switch tenant, because company switching is a full re-login (`login2save.php` reloads permissions/companyid/xero_auth_version/companyplugins). Confirm dialog names the company. Also: `invoiceimport.php` now wraps both the dry-run and import in `exoJsonRespond()` (output-buffered, `Throwable`→`{"error":…}`, stray echoes moved to a `warnings` field) so a legacy `echo` can never corrupt the JSON body again, and `library/exoimport.php` catches a failed `commitInvoice()` and reports the invoice as a **draft** instead of counting it as created.

Data-model resolution keys + CSV gotchas live in [[tomago_exo_data_mapping]]. Release notes done 2026-07-26 (prod `releasenotes` 143/144, `status=0` awaiting approve after docs deploy) + docs page `docs/admin/control-accounts.php` per [[feature_release_docs_policy]] — both cover the GL fixes, not the bespoke import tool itself.
