Documents · Returns processing

Return paperwork the receiving system accepts first time

A return that arrives without a matching document is not a return yet. It is a pallet on a dock and an argument about whose paperwork was wrong. The document that prevents that argument is a workbook with a hundred and one columns, of which the builder sets about ten — and the answer to whether it was any good arrives after the upload, on someone else's results page.

101 columns in the import template
21 allow-listed editable fields
4 upload states per document
Confidential client & operation

Built for the returns operation of a contract-logistics site, producing the advance shipment notices a customer-owned warehouse system requires before returned equipment can be received against: one link in its returns and RMA flows.

Return paperwork built from the check-in and tracked to accepted or failedA receiving scan checks the return in and writes its lines to the tracking tables, where a manager screen edits twenty-one allow-listed fields; a typed form covers returns that never came through that route. The document builder groups the lines by product code and sums them, filters them against the customer's item master, writes about ten of the hundred and one columns in the import template and takes the rest from the template's seeded example row, copied onto every line, resets the index so dropped lines leave no gap, writes quantities as five-decimal strings and stores the workbook as a blob in the database before queueing it. The upload drives the warehouse system's import screen in a browser and moves each document through pending, uploading and uploaded or failed, with a reason led by the exception class so it is never blank. It fails closed: an error on the results page propagates, and a results page that cannot be read is a failure rather than an assumption.RETURNS · RECEIVINGCheck-inthe receiving scan builds thedocument directlyLine detailtracking tables, edited in place21 allow-listed fieldsTyped form, for the restsame store, same queue, sameuploaderWHAT WE BUILTDocument builderGroup lines by product code and sum them,converting text quantities to numbers firstFilter against the item master: a staticexport on the automatic path, a live queryfrom the typed form; 'could not validate'is never reported as 'invalid'Write about 10 of the 101 columns; the restcome from the seeded example row, copiedonto every line, not only the firstReset the index: dropped lines leave no gapQuantities go out as five-decimal stringsStore the workbook as a blob, then queue it4 UPLOAD STATES PER DOCUMENTpendinguploadinguploadedfailedfailed keeps its reason, led by the exception class, never blanka failed document is downloadable for a hand submissionIMPORT TEMPLATE · 101 COLUMNSheaderheaderseededline 1line 2sheet 1sheet 2sheet 3about 10 columns, set by the builderthe rest: the seeded example row, on every line3 sheets · 2 header rows · 1 seeded example rowWarehouse system · import screenresults pagean error here propagates to the documenta page that cannot be read is a failuredecides the stateThe related document flow retries once, after a delaychosen to outlast the one-time-passcode window; a passthat completes kills any browser still running.The upload drives the import screen in a browser and fails closed: a detected error on the results page propagates, a results page that cannot be readis a failure rather than an assumption, and a document whose stored file cannot be retrieved raises instead of returning quietly. One process in the fleetruns the pending sweep, under a database lock, and a problem recording the outcome never fails an upload that worked.
The return announcement is built from the check-in itself, checked against the customer's item master, written into the 101-column template and tracked through four upload states to accepted or failed, with the reason kept.

How do you get return ASNs accepted by a warehouse system with no write API?

Build the notice from the return check-in, in the exact format the import expects, and track whether the system took it. In this build the check-in path fills a 101-column vendor workbook, drops lines the customer's item master does not know, writes quantities as fixed five-decimal strings, and records each upload as pending, uploading, uploaded or failed.

The problem

Every unit coming back had to be announced to the warehouse system before it could be received against, which is how ASN receiving works. The announcement is a workbook: three sheets, two rows of headers, a hundred and one columns, and one seeded example row carrying the standard defaults. The builder sets about ten of those columns. The rest are left exactly as the template has them.

Filling that in by hand is slow, and slow is the least of it. Three things in particular have to be right, and none of them looks important: a quantity written as a fixed five-decimal string rather than a plain number, a product code the customer's item master actually knows, and no gap in the middle of the rows where a filtered line used to be. Whether they were right is answered after the upload, on a results page, in front of whoever is standing there.

  • Line detail arrived as a spreadsheet and had to be loaded into the tracking tables before anything could be built from it
  • The only record of an upload was a boolean, and it said uploaded whether or not the import had been accepted
  • A document whose stored file could not be retrieved left its row at not-uploaded forever, with no trace anywhere
  • A refusal appeared on the portal's results page and nowhere else

What we built

The document is now a by-product of checking the return in rather than a task that follows it — the check-in path builds it directly. Detail lines are read from the tracking tables, grouped by product code and summed, filtered against the customer's item master, and written into the template. The finished workbook is stored in the database as a blob and queued for upload, so nothing depends on a file staying put on a particular machine.

Alongside it sits a manager screen over the line detail, because the data that feeds the document has to be right before the document is built. A second path builds the same document from a typed form for the returns that never came through the automatic route, using the same store, the same queue and the same uploader.

  • Every field edit is its own request against an allow-listed column map — twenty-one fields, and anything not on the list is refused
  • Edits apply immediately and roll back the individual control when the server rejects them, then quietly re-read from the database
  • A new detail line gets its database row lazily, and the creation is memoised so a double-click cannot create two
  • Expanding a return twice issues one fetch, not two
  • Clearing the received date unwinds the lines under it — quantity received, audit flag, audit date, reconciliation — and writes each of those back rather than only to the screen
  • The source workbook can be dropped on the page; sheets are matched on the headers they carry, with position used only as a fallback, and re-importing replaces those returns instead of duplicating them

Hand-filled template or a document built from the check-in

StepHand-filled (before)Built from the check-in (after)
Building the documentSomeone fills a 101-column vendor workbook by handThe check-in path writes it directly
Product codesFound wrong only when the import refused themFiltered against the customer's item master before writing
QuantitiesHave to be typed as fixed five-decimal stringsConverted from text, summed, written back as five-decimal strings
Upload recordOne boolean that said uploaded either wayPending, uploading, uploaded or failed, with a one-line reason
A refusalShown on the portal's results page and nowhere elseKept on the document's row, which can be downloaded and resubmitted

Getting the 101-column template accepted

Most of the work in this build is in the columns nobody looks at.

The template ships with one seeded example row, and that row carries values in a good many more columns than the builder writes. Overwrite it in place and the first generated line is fine; the second lands on a blank row, missing values the import expects. So the fields that vary per line — owner, pack key, line number — are written on every generated row rather than left to the template's first row, and the sibling builder for a related document goes further, copying the template's default row onto each line before overwriting only the fields it owns. That is the sort of bug that first appears on the day a return has more than one product code on it.

Filtering has a related trap. Lines whose product code is not in the item master are dropped before writing, and dropping rows from a table without re-indexing leaves the survivors carrying their original positions — which, in a fixed-layout sheet, means blank rows in the middle and lines written past the end. The index is reset deliberately for that reason. Quantities are stored as text in the source, so they are converted to numbers before they are summed; otherwise the totals are digits concatenated rather than added. They go back out as fixed five-decimal strings, because that is the format the import wants.

  • Sheet names are resolved case- and plural-insensitively, and a missing sheet raises an error naming the sheets the file actually contained
  • The automatic path filters against a static export of the item master; the typed-form path queries the item master live, for the owner account chosen on the form
  • A failure to reach the item master is reported as 'could not validate', never as 'invalid' — the two have different fixes
  • A reference that already has a document on file is refused rather than duplicated, because the uploader matches rows by reference alone

What happens when the upload fails?

There is no write interface to the warehouse system, so the upload drives its own import screen in a browser. Everything about that arrangement is fragile, which is why the failure handling is where the design effort went.

Every document carries an explicit state — pending, uploading, uploaded, failed — with a one-line reason attached to the failure, which is what queues that report what failed and why look like in practice. The reason is assembled deliberately: a browser-automation timeout stringifies to an empty message, which used to store a blank reason on the row, so the exception class is always led with and the message line kept only when there is one.

The upload fails closed. An earlier version raised its error inside a block that caught and discarded it, so the caller always saw success. Now a detected error on the results page propagates, and a results page that cannot be read at all is treated as a failure rather than an assumption. A document whose stored file cannot be retrieved raises instead of returning quietly. The sweep that picks up pending documents runs inside a background loop that holds a database-level lock, so exactly one process in the fleet runs it no matter how many servers are up — and the status writes themselves are best-effort and never raise, because a problem recording the outcome must not fail an upload that actually worked.

  • Documents live in the database, so a failed upload can be downloaded and submitted by hand without hunting for a file
  • The related document flow retries once, after a delay chosen to outlast the one-time-passcode window — retrying inside it just re-sends the code that was already rejected
  • Status columns are added idempotently at runtime, and rows created before status tracking existed fall back to the old boolean rather than showing blank
  • A pass that completes kills any browser processes still running afterwards

The result

The document is generated from the check-in, the line detail behind it is editable in one place by the people who know what changed, and every edit is written straight through rather than staged in a spreadsheet somebody has to remember to send.

The question that mattered most now has an answer. Not 'did we build the document' but 'did the receiving system take it' — every document carries its own state and, when it failed, the reason it failed. The narrow lesson we took away is that when a system's only interface is a file format, the format is the API, and the parts that look like decoration are load-bearing: the seeded example row, the duplicated header, the trailing decimals, the empty columns you are supposed to leave alone. Retail compliance teaches the same lesson the expensive way, which is why our guide to tracing a chargeback back to the file starts with the document.

Questions about this build

Is a workbook ASN the same as an X12 856?

It does the same job in a different envelope: it tells the receiving system what is coming before it arrives. Among the X12 transaction sets, including the 856, that job belongs to the 856 Ship Notice/Manifest, which travels between trading partners as an EDI file, and a 997 Functional Acknowledgment comes back to confirm it arrived. This notice was a 101-column workbook uploaded through the warehouse system's own import screen, so acceptance was read off its results page instead.

Why report 'could not validate' separately from 'invalid'?

Because the two have different fixes. 'Invalid' means the item master answered and does not know the product code, so the line itself needs correcting. 'Could not validate' means the item master could not be reached, so the line may be fine and the check has to run again. Reporting one as the other sends people to repair data that was never broken, or to resubmit data that was.

Why are quantities written as five-decimal strings?

Because that is the format the import wants. Quantities are stored as text in the source, so they are converted to numbers before they are summed; otherwise the totals are digits concatenated rather than added. They go back out as fixed five-decimal strings. That format is one of three easy-to-miss details, with a product code the item master knows and no gap in the rows, that decide whether the file is accepted.

Built with

  • Python
  • Flask
  • SQL Server
  • pandas
  • openpyxl
  • Selenium WebDriver
  • SheetJS

On the details we left out

Client, location, system names and volumes are withheld deliberately. 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

Got a receiving system that rejects your paperwork?

Send the column headings of the template your receiving system wants and one refusal from its results page. 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.