Skip to content
VC
Case 14 of 33 · E-commerce / Operations

Shipping-partner file exchange: day file out, shipping cost and tracking back

Every day the warehouse of a US outdoor-gear brand hands its shipping partner a file with the day's batch and gets it back with tracking numbers and shipping cost per parcel. The file now builds itself and looks like the working book, and the partner's reply is parsed and lands in the right rows of a live spreadsheet.

Industry
E-commerce, US outdoor-gear brand
Stack
Python · openpyxl · Apps Script
Timeline
~3 weeks to live cost writes
Outcome
shipping cost: hunted by eye → lands in its row
01 · Pain Point

The partner's file was built by hand, and the reply was keyed in by eye

Same brand, same two warehouse books and same receiver as in the Paid Shopify orders → warehouse spreadsheets case. There, orders arrive in the book from two stores. Here both the data source and the recipient are different: an outside shipping partner who works in an xlsx file.

Every evening the warehouse operator stamped the batch's send date in the book, exported the whole book to Excel, deleted everything except today's rows, drew the borders, fixed the multi-line addresses Excel had mashed into one line, and sent the file to the partner in chat. The partner sent the same file back with tracking numbers and shipping cost for every parcel.

Tracking numbers went into the store by hand, and from there the pipeline placed them in the book on its own. Shipping cost stayed manual: for every row of the file the operator hunted through a book thousands of rows long for the right order and the right product. Bundles added a risk of their own: an order made of a tent, a stove and a floor ships as three parcels, and rows with no tracking number went back to the partner for re-shipment while the goods were already on their way.

What the warehouse operator asked for

"I fill in the cost myself, and I realised just how awkward it is: every time I have to hunt by eye for the right product and the right order number, just so I don't put it on the wrong item."

02 · Solution

Two halves: the day file out, cost and tracking back

The outbound half builds the day file from a date the operator stamps in the book and reproduces the book's colouring. The inbound half parses the partner's file, carries shipping cost into the live book and lays bundle tracking numbers across the sheet's rows. Writes into the book go through the same Apps Script receiver as the main pipeline; this page covers only what happens around the partner.

01
Send date in the book

The operator stamps the batch date in the "Send date" column. A watcher checks every 5 minutes, spots a new date and waits for the rows to stop growing

02
Day file

The day's rows with borders, wrapping and data-driven widths. The book's colour rules are recomputed per row; the xlsx carries genuine conditional formatting

03
The partner fills it in

Tracking numbers and shipping cost per parcel go into the same file, which comes back to chat: the group topic or a direct message

04
Parsing the inbound xlsx

Key: order number plus row suffix. Cost lands in its own row, written only over the untouched original formula

05
Tracking and the detector

Bundle tracking numbers spread across sheet rows when the counts match; a separate detector finds orders fulfilled in full that still have rows with no tracking number

The day file builds itself from a date the operator sets

The operator stamps the send date once a day, right before sending the batch, and that date became both the filter and the trigger. A watcher reads the book through the receiver in read-only mode, spots a new date and drops the file into the group topic 10 minutes after the rows for that date stop growing. The pause protects the partner from an incomplete file: without it the export would go out while the operator is still stamping dates. If orders keep coming in later, a second version follows, up to three per date. Fixed send times were dropped at the operator's request: they sent the file before the batch was assembled.

The first grid was every 20 minutes, and with the pause and the next tick the wait for a finished file ran to roughly 50 minutes. The grid is now 5 minutes, the wait is down to about 15, and for the "we sat down just to send the table" moment there is a chat command that builds the file on the spot. Read load was measured first: one run reads four columns of the book page by page, two reads in total, and the five-minute grid inside the working window comes to about 380 reads a day with plenty of headroom against the quotas.

The book's colouring: rules are read and computed on our side

The owner asked that the file "lose nothing" of what the manual export carried: the book has colour-coded products, red for "no tracking number", orange for "shipped". The book's colours live in conditional formatting: a rule looks at the text in the product column and paints the cell, while the cell itself stays white. Reading a cell's colour is therefore useless; the rules themselves are what to read. The receiver could already return them through a service action, so the colouring was reproduced with zero edits to the Apps Script and no deployment, which would have been costly here.

Rules that depend on the row itself are computed row by row: a text match, blank or filled, above or below a number. The first match wins, exactly as in the spreadsheet. COUNTIF rules, which highlight duplicate order and tracking numbers, are skipped on purpose: they count across the whole column of the book, a day file holds about thirty rows, and "no duplicates" there would be a lie: the partner would see a clean table where the book shows a warning. The number of skipped rules is printed out loud, because silent partial coverage is worse than none.

A run on a live file: 148 cells coloured, 2 rules skipped. Checked colour by colour against a screenshot of the book: a match. The rule engine is pinned by five tests: rule order, blank and filled cells, numeric thresholds in the book's "$13,02" format, the COUNTIF skip, and a text-only rule yielding to the next rule with a fill.

Highlighting that goes out the moment data is typed in

The operator's key requirement for the highlighting: it has to disappear when the partner enters the data. A plain fill cannot do that: a red "no tracking number" cell would stay red after the number is typed in, and the file would start lying at the exact moment it is being used. The blank-or-filled rules and the numeric thresholds are written into the xlsx as real conditional formatting: ISBLANK on the tracking number, the tracking date and the "shipped" mark, thresholds on cost. Excel recalculates them itself. Text-based rules such as "return" and "replacement" stay as fills: they never depend on the partner's input and must always be visible.

At the operator's request the product colours were dropped: they matter in the big book and clutter the day file. The borders that used to be drawn by hand before sending are in. Columns run through "Notes", and the address arrives multi-line and centred, as in the book. Column widths were driven by the header, so a 26-character heading like "Tracking number received on" stretched a column under ten-character values. Widths now follow the data and the header wraps: the tracking-date column went 28 → 9, cost 28–31 → 9, the table's total width 412 → 217 characters, close to half. Three more columns the warehouse never fills are hidden without being deleted: visible width reached 191, and the data stays in the file one click away.

Shipping cost: an exact key, and a formula as the sign of a free cell

The partner returns the very table that was exported to them, which removes matching by product name, the source of earlier one-letter mistakes. The key is exact: order number plus row suffix, say 12345, 12345.2, 12345.3. The partner deletes about 10% of the rows, orders shipping from other warehouses, so a row is looked up by number across the whole book; a row window would eventually miss, and the miss would look like "no such order in the book".

Of six money columns the partner fills exactly one, and it is never empty: it holds either a number the operator typed by hand or the original division formula. The usual "write only into an empty cell" guard would protect nothing here. The sign of a free cell is the text of the original formula, and the receiver checks it at the moment of writing. The operator edits the book by hand in real time, and a check against a minute-old export would mean overwriting their work.

The file held the "big tents subtlety" the operator had warned about: a tent's row absorbs the cost of its accessory rows, which ship as separate parcels, while the accessory rows keep their cost as well. Only parts of one product are summed; different products in one order stand on their own. The rule was derived from the operator's manual work and confirmed word for word afterwards. Zeros are never written: a $0 from the partner means "no cost in this parcel", and the warehouse leaves such rows with the formula. A row missing from the book is never created; a matching number with a different product is skipped and shown; a row with a tracking number and no cost is called out as a question for the partner.

Checked against the operator's manual work: the algorithm reproduced 34 of 34 hand-filled rows verbatim, including all 7 sums for big items. One divergence across the file's 50 rows: the hand-typed number differed from the partner's, and it went to the operator as a question. For the first six days the module ran in preview mode on a separate page of the book. After two confirmations of "correct", preview mode closed and writes went into the working rows: 13 and 43 rows on live files, 0 skipped. The preview stays alongside the write as a change log: it shows exactly what was entered.

Money in a different column

A day after live writes began, the operator reported that the cost from the latest file was missing from the book. The file turned out to have the money in a different money column, with the usual one empty throughout. The module read only the usual column and reported, truthfully, "tracking present, cost missing". Money is never moved on a guess: the column was selected explicitly, a preview was built, and only after the operator confirmed the partner had mixed up the columns were 92 rows written in one day, 0 skipped. The list of affected rows was saved before writing so the change could be rolled back. Three rows stayed untouched: they held hand-typed numbers, and the receiver walks around those by its own rule.

To keep this from repeating in silence, an auto-pickup with a narrow boundary went in: if the usual column is empty across the whole file while the neighbouring one is filled, the neighbour is taken and it is said out loud. If the usual column has even one number, behaviour is unchanged. The inbox also learned to pick files up from direct messages, because people send a file where they are talking. A report goes to the group topic only when something was written, a repeat run leaves the same files alone, and memory is by message id. The inbox polls the chat every 2 minutes within a 07:00–22:00 MSK window; a poll is one Telegram call and never touches the spreadsheets.

Bundle tracking numbers spread across the sheet's rows

In the store a bundle is one line item: "tent + stove + floor". In the book, at the operator's request, it is three rows. Tracking numbers were grouped by the line item's title, the receiver found only the tent row by it, and the second and third numbers were dropped. The stove and floor rows stayed empty, looked unshipped and went back to the partner for re-shipment.

The layout order now follows the sheet's rows. Laying out by the parts of the title would have put the stove's number on the floor: in the title the stove comes before the floor, while on the sheet tents print first and the rest follows the name order the operator set. The hypothesis was tested on an independent order the operator had filled by hand and that was never seen during design: the bot's proposal matched the manual entry on all three tracking numbers, digit for digit.

The operator's rule: as many tracking numbers as products shipped, and products never travel together under one number. So the layout happens only when the counts are equal: exactly as many numbers as rows. A live counter-example showed the price of guessing: on an order with two numbers for three rows the operator had put the second number on the stove, sheet order would have handed it to the floor, and one number would have landed on two products. Caught by a state check before writing, it never reached the book, and it is pinned by a test. When the counts differ, the book stays untouched and the group topic gets a message with the tracking numbers and the bundle's contents, so the manual layout takes a minute.

An A/B of the old and new layouts on 970 live orders: exactly 2 orders affected, 0 changes under the same key. The tracking lane has 110 green tests, four of them new, including the reference order and the ban on guessing.

A second signal: the order is fulfilled in full, yet rows have no tracking number

The cost of a missed defect equals re-shipping the goods to the customer and paying for delivery twice. Until now it was caught only by the operator's attention: two orders noticed, in the operator's words, "by some instinct". A separate module lists such rows and writes nothing to the book.

There are two signals. The narrow one, "a tracking number got lost on the way", would miss a bundle with one number on three rows: the number is in the book, nothing is lost, yet two rows still look unshipped. The second signal is independent of the first: the order is fulfilled in full in the store, and the book still has rows for it with an empty tracking cell. Coverage was checked so the signal could be trusted: 726 of 924 active orders qualify as "fulfilled in full". A run over 60 days and 924 active orders produced one finding: the very order with a lost number and two bundle rows.

03 · Stack

The standard library where it can, openpyxl where it must

Python, standard library

Parses the inbound xlsx as zip plus XML with no third-party dependency; HTTP to the receiver on urllib

openpyxl

Builds the day file: borders, wrapping, data-driven widths, conditional formatting via FormulaRule and CellIsRule

Google Apps Script receiver

Read-only export, a service action that returns the book's conditional-formatting rules, and a cost-write action that writes only over the untouched original formula

Telegram Bot API

Day files into the group topic, a build-the-file chat command, inbound xlsx picked up from the topic and direct messages via getFile

systemd timers

Send-date watcher every 5 minutes, 09:00–24:00 MSK; inbox every 2 minutes, 07:00–22:00 MSK

pytest

8 tests on cost parsing, 5 on the colour-rule engine, 110 in the tracking lane

PythonopenpyxlApps ScriptGoogle SheetsTelegram Bot APIsystemdpytest
04 · Results

What changed on both sides

Wait for the day file
~50 min ~15 min

watcher every 5 minutes; a chat command builds the file on the spot

Match with manual entry
34 / 34

cost rows reproduced verbatim, including all 7 sums for big items

Re-shipment risk
1

finding across 924 active orders over 60 days; the signal covers 726 of them

The file goes to the partner on its own, with borders, wrapping, addresses and highlighting that fades as the cells fill up. The hand-finishing before sending is gone, and so is exporting the whole book and deleting the rest.

Shipping cost stopped being a hunt-by-eye task: 13, 43 and 92 rows written on live files, 0 skipped, with the operator's hand-typed numbers untouched under the "write only over the original formula" rule. The preview beside each write shows what went in; the saved row list before a large write gives a rollback.

Bundle tracking numbers land on their own rows, and when the counts differ the operator gets a ready message with the numbers and the order's contents. Re-shipment risk is caught as a list, and the first run found the order that until then only the operator's instinct had noticed.

Checks you can show the client

Each half of the exchange was checked against people's manual work before it was trusted with the live book.

  • Colouring. 148 cells checked colour by colour against a screenshot of the book; the 2 skipped rules are named out loud.
  • Cost. 34 of 34 rows verbatim; one divergence across 50 rows, and it went to the operator as a question.
  • Tracking. An independent order matched on all three numbers; the A/B on 970 orders touched 2, with 0 changes under the same key.
  • Detector. 726 of 924 orders qualify as "fulfilled in full", so the single finding speaks of real coverage.

Timeline: about three weeks from the first day file to cost writes into the working rows. Six of those days the cost module spent in preview mode while the operator confirmed the parse twice.

05 · Where it fits

Where else the same methodology applies

Behind this case sits the standard problem of "hand data to a contractor in their format and take their reply back into a live spreadsheet". The contractor works in Excel, and the spreadsheet on our side is alive: formulas, manual edits, its own row order. The same loop fits:

  • Carriers and fulfilment → the shipments register: the batch file out, tracking numbers and rates back into the rows
  • Outsourced bookkeeping → a transactions register for review, corrected amounts and comments back into the same rows
  • Suppliers and purchasing → a request in the supplier's template, confirmed prices, lead times and stock back
  • Content and translation contractors → product cards exported to xlsx, filled columns back with no row drift
  • Any "our file → someone else's hands → our file" exchange where the reply must land in the right row while people's manual entries stay untouched
What's reused on subsequent projects
  • The book's colouring computed on our side from its own rules, and genuine conditional formatting in the xlsx that fades on input
  • The exact "number plus row suffix" key with lookup across the whole book; matching by product name is out
  • A free-cell sign based on the original formula's text, for columns that are never empty
  • The "lay out only when the counts match, otherwise say so with the facts" rule
  • A detector with two independent signals and a coverage check before its result is trusted
  • Preview mode before writing, and a saved list of affected rows before any large write
Similar challenge?

If someone on your team keys a contractor's reply into a spreadsheet by hand — that can go away

The contractor keeps working in their own file, and the spreadsheet stays yours, with its formulas and manual edits. It starts with one live file: what the key is, which columns actually get filled, what counts as a free cell.

Ready to start?

The 9,900 ₽ audit — with a concrete report and quote

I'll tell you what to deploy in your business first, what the payback looks like, and whether you need AI for the task at all (sometimes you don't).

Or just send your question — I reply within 2 hours