How do you turn emailed Excel transfer requests into warehouse orders?
Read the attachment defensively and check every line before an order exists. In this build a scheduled pipeline reads the mailbox, opens each attachment as a workbook or, failing that, as an HTML table, checks every line against live available balance, groups and splits the survivors into reference-numbered orders and uploads them, recording what happened to each sheet.
Requests to move inventory — between owners, between dispositions, out to a repair line, back off a lease — arrived the way inter-company work usually arrives: a spreadsheet attached to an email. Someone opened it, read the lines, and built the matching order by hand, one part number at a time, whether it was inventory moved between owners or stock changing disposition on the same shelf.
The format was the first difficulty. Every requesting team exported from something different, so the same template came back with columns bolted on the end, with two columns carrying the same header, and — often enough to matter — as an HTML table saved with a spreadsheet extension, which opens perfectly well by hand and defeats any reader that trusts the file name.
The second difficulty was that nothing was checked before the order existed. A request against stock that had already moved became an order nobody could fill, found at the far end of the process by somebody with no idea where it came from.
- No two requesting teams sent the same file, and the shape changed without notice
- Availability was confirmed after the order was built, if at all
- Reference numbers came off a shared running list — fine until two people work at once
- Nothing connected a finished order back to the message that asked for it
One page with a tab per request type, and behind it a pipeline that runs on a schedule and on demand: read the mailbox, pull the attachments, check every line, build the order file, upload it, record what happened. In short, emailed forms turned into structured records, and then into orders.
The reading end assumes nothing about the sender, because the sender is not going to change.
- An attachment is read as a workbook first and as an HTML table second — the fallback triggers on the parse failure, never on the file extension
- Columns are taken by position, since the template carries two columns under the same name; trailing columns nobody asked for are ignored, and a file with too few columns is an error rather than a guess
- Every line is checked against live available balance at its source disposition before it can become an order; lines that don't balance are dropped and reported, and the lines that do balance still go
- Balanced lines are grouped by owner, source disposition, destination and request type, then split so every reference lands inside the unit band the receiving process accepts — a group slightly over the ceiling becomes two even halves, not one full reference and one nearly empty one
- A line whose quantity straddles a split is divided across the two references rather than rounded onto either side of it
- Each reference number is reserved in the database before it is used, so two runs on the same day cannot issue the same one, and line numbers are renumbered inside each reference
Keyed by hand or built from the attachment
| Step | Keyed by hand (before) | Built from the attachment (after) |
| Reading the request | Someone opened each attachment and read the lines | Read as a workbook, or as an HTML table when that parse fails |
| Checking stock | After the order was built, if at all | Every line against live available balance first |
| Reference numbers | Taken from a shared running list | Reserved in the database before use |
| Tracing an order | Nothing linked it to the message that asked for it | Message, sheet, references and dropped lines on record |
The expensive failure in a pipeline like this is not a crash. It is a partial success that looks finished — the same orders created twice because a run was repeated, or a sheet sent up carrying a reference somebody else is also using.
So the order of operations is deliberate, because the goal is orders that are safe to retry. A message is marked processed before anything is uploaded: if the upload dies, the sheet is left visibly unfinished for a person to push through, which is a smaller problem than a second copy of the same order appearing downstream. Each attachment is processed in isolation, so one unreadable file cannot cost the run the work it has already done.
Where the build cannot be completed correctly it is not completed at all. If a reference can't be generated for a group — an unrecognized request type, a daily sequence run out — that group keeps the requester's original reference, unsplit, and the whole sheet is marked not-uploadable with the reason attached. Nothing invents a number to keep moving.
- One lock is shared by the scheduled run and the page's check-now button, so pressing it mid-run cannot process the same message twice or open a second portal session on one account
- The whole flow has a single off switch that reads no mail, builds nothing and uploads nothing — and if the configuration it reads fails to load, it falls back to the environment rather than defaulting to on
- Every sheet carries its own upload state and reason, so the page shows what happened instead of implying it
- A failed upload never records an empty reason: the exception class is always kept, because blank is indistinguishable from never attempted
- Any run that processed an attachment emails a summary with the dropped lines and the built workbook, whether or not something failed
A request goes from an inbox to a live order without anyone retyping it, and the people who used to key them spend their attention on the exceptions the checks surface — which is the part that needed a person all along. The balance check is the simplest form of multi-node inventory and allocation rules: nothing becomes an order unless the stock is there.
The audit trail is the quieter win. For any order the system built you can name the message it came from, the sheet it was written to, the references it was split into, and every line dropped along the way with the balance that dropped it. When a requester asks why their transfer came back short, the answer is on the page rather than in somebody's mail folder. Requests that arrive as PDFs or scans rather than workbooks are the case for LLM document and email automation; these arrived as spreadsheets, so a plain parser was the right tool.
Questions about this build
Why read a spreadsheet attachment as an HTML table?
Because requesting teams often send an HTML table saved with a spreadsheet extension. It opens perfectly well by hand and defeats any reader that trusts the file name. So each attachment is read as a workbook first and as an HTML table second, and the fallback triggers on the parse failure, never on the file extension.
What happens to transfer lines that fail the balance check?
They are dropped and reported, and the lines that do balance still go. Every line is checked against live available balance at its source disposition before it can become an order. Any run that processed an attachment emails a summary with the dropped lines and the built workbook, and the page keeps the balance that dropped each line, so why a transfer came back short is on record.
How are duplicate orders prevented when a run repeats?
By the order of operations. A message is marked processed before anything is uploaded, so a failed upload leaves the sheet visibly unfinished rather than creating a second copy downstream. Each reference number is reserved in the database before use, and one lock is shared by the scheduled run and the page's check-now button.