Reconciliation · B2B trading partners

Inventory variances that only close when they're actually gone

Every B2B trading relationship eventually produces a variance report, the starting point of any reconciliation: a list of parts where your number and their number do not match. It usually arrives as an attachment, gets worked in the attachment, and is discarded when the next one lands. Which means nobody can say how long any single disagreement has been open, or who decided three weeks ago that it was fine.

An estimated $2,383 a year in labour time saved, by the site's own tracker. Where the figure comes from

7 server routes
4 record lifecycle states
12 change-logged fields
Confidential client & operation

What it saves

$2,383 a year, estimated

The site's efficiency-savings tracker timed this task by hand and again with the software. The labour time saved comes to an estimated $2,383 a year.

TaskSaved a year
Working the trading-partner variance report$2,383

Estimated by the site's own efficiency-savings tracker: each task timed by hand and again with the software, the time saved valued at a fully loaded labour rate over a 252-day working year. Labour time only, to the whole dollar.

Built for a contract-logistics operation reconciling on-hand inventory quantities against a trading partner's system of record, across two inventory organizations.

A variance report that remembers, and that only the system can closeOur on-hand extract and the trading partner's system of record both feed a variance workbook that arrives as a recurring email attachment. A loader pulls it from the mailbox, or takes a manual upload, and upserts it into a persistent variance register: a line matches on part, organization and disposition against anything not already closed, quantities update in place and the original reported and occurrence dates are never touched, so re-running the same report ten times resets no clock and creates no duplicates. The register shows each part's quantities on both sides, the variance, the days open and a state. A person may set open, investigating or solved; closed is set only by the system, when the line leaves the partner's report, and a solved line that reappears with a real difference is reopened under the system's name. Every edit of the twelve allow-listed fields is logged with the old value, the new value, the user and the time; quantities and identifiers are not editable.TWO SYSTEMS, ONE WORKBOOKOur on-hand extractwritten to its own table onevery runPartner's recordtheir system of record, asreportedVariance workbookemailed, recurringWHAT WE BUILT · 7 ROUTESLoaderAn upsert, not an import: a line matches onpart, organization and disposition, againstanything not already closedQuantities update in place; the originalreported and occurrence dates stay put:the same report loaded ten times resetsno clock and creates no duplicateTwo ways ina mailbox pull or a manualupload land in the same folderand run the same loader over thenewest workbook. The mailboxcredential reads and sends only,so the source message is neveraltered or deleted.VARIANCE REGISTER · 4 LIFECYCLE STATES · SAMPLE VALUESupsert, then sweepPARTOURSTHEIRSVARIANCEDAYSSTATENOTEP-10421120118−214OpenP-088733640+431Investigatingticket on their sideP-077105247−53Opensystem reopened itP-05118210205−540Solvednext report decidesP-065027570−522Closedsystem onlyDays open accrues from the original occurrence date, in the database, only while a line is still being worked.CHANGE LOG · 12 ALLOW-LISTED FIELDSFIELDOLD → NEWBYstateopen → investigatinga personroot causeblank → classifieda persontheir ticketblank → addeda personstatesolved → openthe systemstateopen → closedthe systemQuantities, the computed variance, organizationand part identifier are not on the list: nobody editsthe numbers the report produced. System movesland in the same log, under the system's name.Only the system closes a variance. Closed means the line is no longer on the partner's report: theAPI rejects a hand close and the screen shows a closed record with no control on it. The sweepthat closes absent lines runs only after a load that succeeded, and a solved line that comes backwith a real difference is reopened under the system's name.
The recurring variance workbook is loaded as an upsert into a register that keeps each disagreement's age and history; a person may set three of the four states, and only the system closes a line. Values are illustrative.

Why can't an emailed variance report say how old a disagreement is?

An emailed variance report cannot date a disagreement because each workbook is a snapshot with no memory: a resolved line returns looking new, and the root cause lives in whichever copy someone saved. We load every report into a register by upsert, keep the original occurrence date, change-log twelve fields, and let only the system close a line.

There was no feed, and no trading-partner integration to build one on. The reconciliation between the two systems was produced by somebody else's process and delivered as a workbook attached to a recurring email — a summary sheet plus the raw on-hand extract from each side. Whoever opened it worked it in place.

A snapshot has no memory, and that is the whole difficulty. Two consecutive workbooks look identical whether a disagreement is new or months old. A line somebody investigated and resolved comes back on the next report looking exactly like a new one. And the decision — the root cause, the ticket reference, the person who made the call — lived in whichever copy of the file you happened to be holding. The 940 and 945 guide explains why receipts and adjustments end up in a spreadsheet emailed on Friday in the first place.

  • The reconciliation arrived as an email attachment, not an interface
  • Each workbook was a snapshot with no relationship to the last one
  • Nothing recorded how long a given disagreement had been open
  • Classifications and resolution notes did not survive to the next report

What we built

A loader that pulls the workbook out of the mailbox and a persistent variance register behind it, with a per-record change log and an editable screen for the columns that require human judgment.

The load is an upsert, not an import. Records match on part identifier, organization and the disposition that arrives in the same cell, against anything not already closed, and a match updates the quantities in place while leaving the original reported and occurrence dates alone. That single decision is what makes the age of a disagreement meaningful — re-running the same report ten times does not reset any clock, and does not create ten records. The previous variance is kept alongside the new one, so the screen can show which way a line moved since the last report rather than only where it stands.

  • Rows with no quantity difference never become records at all
  • Days outstanding is computed in the database from the original occurrence date, and only accrues while a record is still being worked
  • Both sides' raw on-hand extracts are written to their own tables on every run — truncated and repopulated, so the figures on screen can always be traced back to the rows of the current report
  • Two ticket references per record — one for each side of the trading relationship, split out of a single earlier column by a migration that runs itself
  • Two ways in — a mailbox pull and a manual upload — landing in the same folder and running the same loader over the newest workbook
  • Four charts and a filterable, sortable table over the whole register, paged at a hundred rows

Reading a spreadsheet you don't control

The input is generated by a process nobody on this side owns, which means the parser has to survive it changing shape. It tries the expected layout first and falls back to one with two rows of preamble before giving up. Three workbook formats are accepted and the read engine is chosen per file extension. Attachments that are not spreadsheets are skipped rather than optimistically parsed.

Two traps in that file were worth the code they cost. Blank numeric cells arrive as NaN, and NaN is truthy in Python — the obvious idiom for defaulting a missing number silently keeps the blank instead of zeroing it, so every quantity is coerced explicitly and non-numeric values are mapped to zero. And the identifier column carries two values in one cell separated by a slash, while some identifiers legitimately contain a slash themselves; the split is on the spaced form of the delimiter, not the character, because the naive version quietly truncates real part numbers.

The mail side is deliberately narrow. The mailbox credential is scoped to read and send only, so the loader can never alter or delete the source message — the evidence for any reconciliation stays where it arrived. When a stored credential cannot be refreshed, the mail helper logs and returns nothing rather than raising, and every caller checks for a null service and gives up; the load runs on a thread spawned by a web request, and a failure there should surface as no new data, not as a traceback nobody is watching. A run that fails to load anything also never advances the last-updated stamp the screen displays, so a stale dashboard looks stale instead of looking current.

Only the system closes a variance

A record moves through four states, and a person may set three of them. The fourth cannot be set by hand: the API rejects the attempt with an explicit message, and the screen renders a closed record as static text with no control on it. Closed means exactly one thing — the disagreement is no longer on the partner's report — and it is not an opinion anyone is allowed to hold.

After each successful load, a sweep closes every record for that organization that was in the register but absent from the new report, writing a change-log row for each. The sweep is gated on the load returning success, so an unreadable workbook or an empty mailbox skips it rather than closing an organization's open list against a file that never arrived.

The rule runs the other way too. A line somebody marked solved that reappears on the next report with a real difference is reopened automatically, its solved date cleared, and the reopen written to the change log under the system's name. Optimism about a fix does not survive contact with the next report.

  • Field edits are validated against an allowlist of twelve columns before the column name reaches SQL
  • Quantities, computed variance, organization and part identifier are not on that list — nobody edits the numbers the report produced
  • Every change records the old value, the new value, the user and the time, viewable per record
  • System-driven transitions land in the same log as human edits, attributed to the system
  • Bulk edits fan out as individual writes, so each affected record gets its own log entry
  • Classification is a closed vocabulary — seven transaction error types and a fixed root-cause list — so the same problem is named the same way twice
  • All seven routes require an authenticated session; the query and edit endpoints run on a bounded worker pool and return a timeout rather than holding a request thread

What the register knows that the workbook did not

The register carries what the workbook could not: how long each disagreement has been open, which direction it moved since the last report, who classified it and when, and which side's ticket it is sitting behind. Items leave the list when the underlying difference clears, rather than accumulating until people stop reading it. It is a reconciliation report for when two systems disagree, kept as a register instead of a file.

The schema maintains itself on the way in — tables are created if absent, new columns are added idempotently behind existence checks, and the older single-ticket field was backfilled into the first of its two successors on first run — so the register could grow new fields without a migration window on a system people use daily.

The honest observation from this build is that the arithmetic was never the hard part. Subtraction is subtraction. The work was deciding which facts belong to the machine and which belong to the person, the same discipline as naming the system of record before building the interface, writing that boundary into the API rather than into a policy document, and then refusing to let either side overwrite the other.

Emailed workbook or variance register

What you need to knowEmailed workbookVariance register
How long has this line been open?Unknown: every copy looks newDays outstanding from the original occurrence date
Which way did it move since the last report?Two consecutive workbooks look identicalThe previous variance kept beside the new one
Who classified it, and when?Nobody can say who made the callChange log: old value, new value, user, time
Is it really resolved?A resolved line returns looking newClosed only when it leaves the partner's report
Whose ticket is it behind?In whichever copy of the file you holdTwo ticket references, one per side

Questions about this build

Why can't a person mark a variance closed?

Because closed means one fact: the line is no longer on the partner's report. A person may set three of the four states, and the API refuses the fourth with an explicit message. After each successful load a sweep closes every record missing from the new report and logs each one, and a line marked solved that comes back with a real difference is reopened under the system's name.

What happens when the source workbook changes layout?

The parser tries the expected layout first, then one with two rows of preamble, before giving up. It accepts three workbook formats with the read engine picked per extension, skips attachments that are not spreadsheets, coerces blank numeric cells explicitly so a NaN never survives as a quantity, and splits the two-value identifier cell on the spaced delimiter so part numbers containing a slash stay whole.

Built with

  • Python
  • Flask
  • SQL Server
  • pandas
  • openpyxl / pyxlsb
  • Gmail API (OAuth2)
  • Chart.js

On the details we left out

Client, location, system names and volumes are withheld deliberately; the savings above are the tracker's dollar estimates, with the minutes, rates and headcount behind them left out. We're happy to go deeper on architecture and approach on a call.

Services this draws on

System Integration

Connect aging systems to modern APIs, data platforms, and SaaS tools.

See System Integration →

3PL & Logistics Systems

WMS, TMS, and warehouse-floor workflows for 3PLs, brokers, and distributors.

See 3PL & Logistics Systems →

B2B EDI & Trading-Partner Integration

Onboard retail and customer partners over EDI or API — mapped, tested, and monitored.

See B2B EDI & Trading-Partner Integration →

More projects

Integration · Inbound logistics

Knowing where inbound freight actually is

Inbound shipments were tracked by opening carrier websites one tab at a time. We replaced the tabs with live carrier integrations and a workflow that routes exceptions to the person who can fix them.

Read the story →
Automation · Warehouse systems

Automating a system that has no API

The warehouse platform offered no write interface, so every transaction went in by hand. We automated the screens themselves — carefully enough to trust with live inventory.

Read the story →
Automation · Inventory transfers

The order builds itself from the attachment

Inventory transfer requests arrived as spreadsheets attached to email, in as many shapes as there were requesters, and were keyed in line by line. Now the attachment becomes a validated, reference-numbered order on its own.

Read the story →
Get in touch

Is your variance report a weekly attachment?

Forward one, names removed, and an engineer will say what a register would keep. Free assessment.

Prefer to talk?

Call 832-598-8234 or email msco@stoneagesoftware.com. Houston, Texas — serving Houston, The Woodlands, Conroe, Sugar Land, Katy, Pearland, and the Greater Houston metro.

What happens next

We read your message, an engineer replies with questions or a straight answer, and if a call would help, we book one. From the first reply on, you're talking to the people who'd do the work.

No obligation — we reply within one business day. This form sends your details to Stone Age Software LLC so we can respond to you. See our privacy policy for how we handle them. This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.