Skip to content

Allow this site to count your visits with Yandex Metrica? Metrica will store cookies in your browser to do this. How the site uses cookies

VC
Case 8 of 22 · E-commerce · Operations

File exchange with a shipping partner: the file goes out by itself, and costs and tracking numbers come back into the sheet

Every day the warehouse of a US outdoor-gear brand sends its shipping partner a file with the day's batch. The partner sends it back with tracking numbers and the shipping cost of each parcel. Now the file builds itself and looks just like the working sheet. The partner's reply is read automatically, and the data lands in the right rows.

Industry
E-commerce, US outdoor-gear brand
Technology
Python · openpyxl · Apps Script
Timeline
About 3 weeks until costs were written to the sheet
Outcome
Shipping cost lands in its row by itself
01 · Problem

The partner's file was put together by hand, and the reply was copied back by eye

This is the same brand, with the same two warehouse sheets and the same script that writes to them, as in the case Paid Shopify orders → warehouse spreadsheets. There, orders come into the sheet from two stores. Here the data comes from somewhere else and goes to someone else: an outside shipping partner who works in Excel files (xlsx).

Every evening the warehouse operator entered the batch's shipping date in the sheet. Then they exported the whole sheet to Excel, deleted everything except that day's rows, drew the borders and fixed the multi-line addresses that Excel had squashed into one line. The finished file went to the partner in a chat. The partner sent the same file back with tracking numbers and the shipping cost of each parcel.

The operators entered tracking numbers in the store, and from there the automation put them into the sheet. Shipping costs were still manual work. For every row of the file, the operator searched by eye for the right order and the right product in a sheet thousands of rows long.

Bundles added risk. An order of a tent, a stove and a floor ships as three parcels. Rows without a tracking number went back to the partner to be shipped again, even though the goods were already on their way.

What the warehouse operator asked for

"I fill in the cost myself, and I've realised 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 parts: the day's file goes out, costs and tracking numbers come back

The first part builds the day's file from the date the operator enters in the sheet and copies the sheet's colours. The second part reads the partner's file, carries the shipping costs into the working sheet and spreads the tracking numbers of bundles across their rows. Everything is written to the sheet by the same Apps Script receiver as in the main order transfer. Apps Script is code that runs inside the Google Sheet itself. This page covers only the part that involves the partner.

01
Shipping date in the sheet

The operator enters the batch date in the "Shipping date" column. A watcher script checks every 5 minutes, spots the new date and waits until rows stop being added

02
The day's file

The day's rows with borders, text wrapping and column widths that fit the data. The sheet's colour rules are worked out for each row, and the xlsx carries real conditional formatting

03
The partner fills it in

The partner adds tracking numbers and the shipping cost of each parcel to the same file and sends it back to the chat: to the group topic or as a direct message

04
Reading the reply

The key is the order number plus the row suffix. The cost lands in its own row, and only over the untouched original formula

05
Tracking numbers and a shipping check

A bundle's tracking numbers are spread across its rows only if there are exactly as many numbers as rows. A separate check looks for orders shipped in full that still have rows without a tracking number

The day's file builds itself from the date the operator enters

The operator enters the shipping date once a day, just before sending the batch. That date became both the filter for the rows and the signal to start. The watcher reads the sheet through the receiver in read-only mode. When it sees a new date, it waits until rows for that date stop being added and, 10 minutes later, posts the file to the group topic.

The pause protects the partner from an incomplete file: without it the file would go out while the operator was still entering dates. If orders turn up later, a new version of the file follows, up to three per date. Sending on a fixed schedule was dropped at the operator's request, because the file used to arrive before the batch was ready.

At first the watcher ran every 20 minutes. With the pause and the wait for the next run, the finished file arrived after about 50 minutes. The interval was cut to 5 minutes, and the wait dropped to about 15. For the "we need to send the table right now" moment there is a chat command that builds the file on the spot. The load on the sheet was measured in advance. One run reads four columns in parts, which is two read requests. Running every five minutes during working hours comes to about 380 requests a day, well within the limits.

The colours: the script reads the sheet's own rules and applies them

The owner asked that the file "lose nothing" compared with the manual export. The sheet has colour-coded products, red for "no tracking number" and orange for "shipped". These colours come from conditional formatting: a rule looks at the text in the product column and colours the cell, while the cell itself stays white. So reading a cell's colour tells you nothing; you have to read the rules themselves.

The receiver could already return those rules with a separate command. So the colouring was reproduced without a single change to the Apps Script and without releasing a new version of it, which would have been costly here.

Rules that depend on the row itself are checked row by row: a text match, empty or filled, above or below a number. The first matching rule wins, just as in the sheet. I skipped the COUNTIF rules on purpose. They highlight repeated order and tracking numbers and count across the whole column of the sheet, while the day's file has about thirty rows. There, "no repeats" would be untrue: the partner would see a clean table while the main sheet shows a warning. The script says plainly how many rules it skipped, so that partial colouring isn't taken for complete.

A test on a live file: 148 cells coloured, 2 rules skipped. I compared the result with a screenshot of the sheet colour by colour, and everything matched. The rule logic is covered by five tests: rule order, empty and filled cells, number thresholds in the sheet's "$13,02" format, skipping COUNTIF, and a rule that only colours the text and lets the next rule add a fill.

The highlighting goes away as soon as the partner enters the data

The operator's main requirement for the highlighting was that it should disappear once the partner fills in a cell. A plain fill can't do that. A red "no tracking number" cell would stay red after the number was entered, and the file would mislead people at the very moment they were using it.

So the "empty or filled" rules and the number thresholds went into the xlsx as real conditional formatting: ISBLANK on the tracking number, the tracking date and the "shipped" mark, and thresholds on the cost. Excel recalculates them by itself. Text rules, such as "return" and "replacement", stayed as fills: they don't depend on the partner's data and must always be visible.

At the operator's request the product colours were removed: they matter in the big sheet and only clutter the day's file. The file got the borders that used to be drawn by hand before sending. The columns now run up to "Notes", and the address arrives on several lines and centred, as in the sheet. Column widths used to follow the header, so the 26-character heading "Tracking number received on" stretched a column meant for ten-character values. Now the width follows the data and the header wraps. The tracking-date column shrank from 28 to 9 characters, the cost columns from 28–31 to 9, and the whole table from 412 to 217, almost half. Three more columns the warehouse never fills in are hidden without being deleted. The visible width came down to 191, and the data stays in the file, one click away.

Shipping cost: an exact key, and the original formula marks a free cell

The partner returns the same table that was sent to them. So there's no need to match rows by product name, which is where one-letter mistakes used to happen. The key is exact: the order number with the row suffix, for example 12345, 12345.2, 12345.3. The partner deletes about 10% of the rows, because those orders ship from other warehouses. So a row is looked up by number across the whole sheet. A search within a fixed range would miss one day, and the miss would look like "this order isn't in the sheet".

Of the six money columns the partner fills in only one, and it's never empty. It holds either a number the operator typed in by hand or the original division formula. So the usual safeguard, "write only into an empty cell", would protect nothing here. A cell counts as free while it still holds the text of the original formula. The receiver checks this at the moment of writing, because the operator can edit the sheet at any minute, and a check against a minute-old export could overwrite their work.

The file had the "big tent quirk" the operator had warned about. A tent's row includes the cost of its accessories, which ship as separate parcels, and the accessory rows keep their own cost as well. Only parts of the same product are added up; different products in one order are counted separately. I worked the rule out from the operator's manual work, and the operator confirmed it word for word.

Zeros are never written: for the partner, $0 means "no cost in this parcel", and the warehouse leaves those rows with the formula. The script never creates a row that isn't in the sheet. If the number matches and the product differs, the row is skipped and shown to the operator. A row with a tracking number and no cost is listed as a question for the partner.

A check against the operator's manual work: the algorithm reproduced 34 of 34 hand-filled rows exactly, including all 7 sums for large items. Across the file's 50 rows there was one difference: the number typed by hand differed from the one in the partner's file, and it went to the operator as a question. For the first six days the module ran in preview mode: it showed its result on a separate sheet and wrote nothing to the working rows. After the operator confirmed "correct" twice, writing was switched on: 13 and 43 rows on live files, with 0 skipped. The preview sheet stayed as a change log that shows exactly what was entered.

The money in a different column

A day after writing was switched on, the operator reported that the costs from the latest file were missing from the sheet. It turned out that in this file the partner had filled in a different money column, and the usual one was empty throughout. The module read only the usual column and reported, correctly, "tracking number present, cost missing".

Money is never moved on a guess. The column was chosen explicitly, and the result was first shown in preview mode. Once the operator confirmed that the partner had mixed up the columns, 92 rows were written in one day, with 0 skipped. The list of affected rows was saved before writing, so the change could be rolled back. Three rows were left as they were: they held numbers typed in by hand, and the receiver doesn't touch those, by its own rule.

So that this can't happen again unnoticed, I added a narrow rule. If the usual column is empty throughout the file and the next one is filled, the next one is used, and the bot says so in the chat. If the usual column has even one number, everything works as before. At the same time the inbox learned to pick up files from direct messages too, since people send a file wherever they're talking. A report goes to the group topic only when something was written. A repeat run doesn't touch files it has already handled, because processed messages are remembered by their ID. The inbox checks the chat every 2 minutes from 07:00 to 22:00 Moscow time. One check is one request to Telegram, and it doesn't touch the sheets.

A bundle's tracking numbers are spread across its rows

In the store a bundle is one line item: "tent + stove + floor". In the sheet, at the operator's request, it is three rows. Tracking numbers used to be grouped by the line item's title. The receiver found only the tent's row by that title and dropped the second and third numbers. The stove and floor rows stayed empty, looked unshipped and went back to the partner to be shipped again.

Now the order follows the rows on the sheet. Spreading the numbers 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 the order is different, with tents first and the rest in the order the operator chose. I tested this on another order the operator had filled in by hand, one that wasn't used during development. The bot's suggestion matched the manual entry on all three tracking numbers, digit for digit.

The operator has a rule: as many tracking numbers as products shipped, and products never travel together under one number. So the numbers are spread only when the counts match, with exactly as many numbers as rows. A live example showed why guessing is dangerous. On an order with two tracking numbers for three rows, the operator had put the second number on the stove. The sheet order would have given it to the floor, and one number would have ended up on two products. The check before writing caught this, so the error never reached the sheet, and a test now covers the case. If the counts differ, the sheet stays untouched, and the group topic gets a message with the tracking numbers and the contents of the bundle. Sorting them out by hand takes a minute.

A comparison of the old and new methods on 970 live orders: exactly 2 orders were affected, and the rest had 0 changes. This part has 110 passing tests, four of them new, including the reference order and the ban on guessing.

A second sign: the order shipped in full, and rows without a tracking number remain

A missed mistake here costs a second shipment to the customer and paying for delivery twice. Until now only the operator's attention caught it: in the operator's words, it was "some kind of instinct" that made them look closer at two orders. Now a separate module finds such rows and lists them without writing anything to the sheet.

There are two signs. 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 sheet and nothing is lost, and yet two rows look unshipped. The second sign is independent of the first: the order is shipped in full in the store, and in the sheet it still has rows with no tracking number. First I checked that the sign actually fires: 726 of 924 active orders count as "shipped in full". A run over 60 days and 924 active orders found one case: that very order with a lost tracking number and two bundle rows.

03 · Technology

The standard library reads the incoming file, openpyxl builds the outgoing one

Python, standard library

Reads the incoming xlsx as a zip archive of XML files, with no third-party libraries; requests to the receiver go through urllib

openpyxl

Builds the day's file: borders, text wrapping, widths that fit the data, conditional formatting through FormulaRule and CellIsRule

Google Apps Script receiver

Read-only export, a separate command that returns the sheet's conditional-formatting rules, and cost writing only over the untouched original formula

Telegram Bot API

Day files to the group topic, a chat command to build the file, incoming xlsx picked up from the topic and from direct messages

systemd timers

Shipping-date watcher every 5 minutes, 09:00–24:00 Moscow time; inbox every 2 minutes, 07:00–22:00 Moscow time

pytest

8 tests on reading costs, 5 on the colour rules, 110 in the tracking-number part

PythonopenpyxlApps ScriptGoogle SheetsTelegram Bot APIsystemdpytest
04 · Results

What changed on both sides

Wait for the day's file
~50 min ~15 min

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

Match with manual entry
34 / 34

cost rows reproduced exactly, including all 7 sums for large items

Risk of a second shipment
1

case found among 924 active orders over 60 days; the check covers 726 of them

The file goes to the partner by itself, with borders, text wrapping, addresses and highlighting that fades as the cells are filled in. Nobody has to finish it by hand before sending, or export the whole sheet and delete the extra rows.

Nobody has to hunt for shipping costs by eye any more. On live files 13, 43 and 92 rows were written, with 0 skipped. The numbers the operator typed by hand stayed untouched, because writing happens only over the original formula. The preview sheet shows exactly what was entered, and the list of affected rows saved before a large change makes it possible to roll it back.

A bundle's tracking numbers land on their own rows. When the counts don't match, the operator gets a ready message with the numbers and the order's contents. The risk of a second shipment now shows up as a list, and the very first run found the order that until then only the operator's instinct had caught.

Checks you can show the client

Each part of the exchange was checked against people's manual work before it was trusted with the working sheet.

  • Colours. 148 cells checked against a screenshot of the sheet, colour by colour; the script names the 2 rules it skipped.
  • Costs. 34 of 34 rows matched exactly; one difference across 50 rows, which went to the operator as a question.
  • Tracking numbers. A test order matched on all three numbers; comparing the old and new methods on 970 orders affected 2, with 0 changes for the rest.
  • Shipping check. 726 of 924 orders count as "shipped in full", so the single case it found reflects real coverage.

Timeline: about three weeks from the first day's file to writing costs into the working rows. For six of those days the cost module ran in preview mode while the operator confirmed the result twice.

05 · Where it fits

Where else the same approach works

Behind this case is a common task: "give a contractor data in their format and take their reply back into the working sheet". The contractor works in Excel, and the business has its own sheet with formulas, manual edits and its own row order. The same setup works for exchanges like these:

  • → Carriers and outside warehouses → the shipment register: the batch file goes out, and tracking numbers and rates come back into the rows
  • → Outsourced bookkeeping → a register of transactions sent for review, with corrected amounts and comments coming back into the same rows
  • → Suppliers and purchasing → a request in the supplier's template, with confirmed prices, lead times and stock coming back
  • → Copywriters and translators → product cards exported to xlsx, with filled-in columns coming back and no rows shifted
  • → Any "our file → someone else's hands → our file" exchange where the reply has to land in the right row and people's manual entries have to stay untouched
What I reuse in later projects
  • The sheet's colours worked out by the script from the sheet's own rules, and real conditional formatting in the xlsx that fades as cells are filled in
  • The exact "number plus row suffix" key, with a search across the whole sheet and no matching by product name
  • The text of the original formula as the sign of a free cell, for columns that are never empty
  • The rule "spread the numbers only when the counts match, otherwise report the facts plainly"
  • A check with two independent signs: first confirm that it fires, then trust its result
  • Preview mode before writing, and a saved list of affected rows before any large change
Similar challenge?

Does someone on your team copy a contractor's reply into a spreadsheet by hand? That can be automated

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

Ready to start?

Start with the 5,000 ₽ audit: a written report and a payback estimate

I'll tell you what to automate first, when it will pay for itself and whether your task needs AI at all. Sometimes it doesn't.

Or just ask a question. During business hours I reply within 2 hours