A complete back office for a seller of home textiles on Wildberries, a major e-commerce marketplace: 763 product cards, 4 brands, about 6,000 orders a day, production in Uzbekistan with a ~60-day cycle. The system covers restock planning on TOC buffers, a daily pulse board, 7 alert types, an economics dashboard with a month-end forecast, set combinatorics, ABC analysis, a producer order in one click and a warehouse register. On the warehouse floor runs the FBS module: supplies are built right in the ERP, the marking code is checked at the moment of the scan, urgent orders go first, missing stock is reordered from suppliers in MoySklad, and the owner sees picker output and acceptance per supplier.
A fast-growing seller of towels, bathrobes and textile sets. The numbers lived in three disconnected services. The most important decision of the business, what to restock and when, was made by hand.
The marketplace cabinet held orders and ads. MoySklad (an inventory SaaS) held warehouses and set recipes. TrueStats (a seller analytics service) held unit economics. Plus manual file exports. Nobody saw one picture.
Which SKU to move to the marketplace warehouse, how much and when: the owner decided this daily, in a personal Excel, across 763 product cards in ~27 categories. The process did not scale and did not survive a vacation.
Production takes about 60 days, plus ~10 days of delivery, plus domestic legs. Order too late and you miss the season. Order too early and you freeze cash in stock. The margin for error is small and shrinks in season.
Stockouts, order drops, negative reviews and data mismatches were noticed when someone happened to look. With 6,000 orders a day, "happened to look" is not a control system.
Every night Celery jobs pull the marketplace, the inventory SaaS and the analytics service into PostgreSQL. Calculation modules turn raw rows into buffers, alerts and forecasts. People work in a React SPA or in a Google Sheets showcase that the backend renders for them. In the daytime, during the warehouse shift, the FBS module polls the marketplace every minute and writes back exactly what it is told to: supplies into the marketplace account and purchase orders into MoySklad, every write with a log entry.
Contracts between modules mean nothing is computed twice: sufficiency comes from one function, the 33/67 traffic light from one settings row, ABC classes from one service. The pulse board, the Google showcase and the alerts all consume the same numbers.
The client asked to verify his own restock formulas and suggest improvements. His classic coverage math stayed in the table as visible columns. On top of it the system runs Dynamic Buffer Management (DBM): every SKU gets a target buffer, the buffer splits into traffic-light zones, and the buffer corrects itself from actual consumption. This is textbook TOC, implemented as code and tests.
The ×1.1 is the client's paranoia margin on every buffer, in the spirit of Goldratt. M is the DBM multiplier: it starts at 1.00 and moves in steps of 1/3.
The buffer is full enough. Do nothing. Twelve green days in a row will suggest shrinking the buffer: capital should not sleep in stock.
Plan replenishment. The SKU enters the restock table with a computed quantity, not a guess.
Order now. Red days accumulate in the 12-day history; a red streak triggers a buffer increase recommendation.
Stock is zero while sales run. Lost revenue every day. Black counts as red in the streaks and screams at the top of the alert feed.
| Constant | Value | Reasoning |
|---|---|---|
| DBM window | 12 days | Three times the replenishment leg to the marketplace warehouse (4 days). A shorter window reacts to noise, a longer one reacts too late. |
| Increase trigger | ≥80% red of 12 | A buffer that lives in red is not a buffer. Recommendation: +1/3 of the target. |
| Decrease trigger | 12 green in a row | A buffer that never leaves green is frozen cash. Recommendation: -1/3, and only a full unbroken streak counts. |
| Floor | M ≥ 1/3 | The multiplier never drops below one third. If a step would break the floor, the decrease is not even suggested. |
| Cooldown | 12 days | After any correction the system stays silent for a full window, so one decision is measured before the next one is proposed. |
| Paranoia | ×1.1 | The client's rule: +10% on every buffer. Encoded as a named constant, covered by tests. |
| Supply legs | 4 / 14 / 74 days | To the marketplace warehouse / from the producer's stock / from production (60-day cycle + 10 delivery + domestic legs). |
| TOP rule | 90% | The client's rule: an A-class SKU below 90% buffer always jumps to the top of the restock table, above any C-class SKU, and restocks to at least 90%. |
Each category has a season window and a coefficient. A manual coefficient set by the client always overrides the automatic hint. The hints come from last year's orders: weekly sums, 3-week smoothing, season = the segment above 50% of the peak.
Out of season the speed formula is ignored. The category holds a fixed stock in units, distributed across SKUs by their share of orders over 90 days. No sales history at all: split evenly.
In the closing window of the season the buffer ramps down linearly to a planned deficit. The season ends with empty shelves by design, not with a year of leftovers in stock.
Auto-apply of buffer corrections ships OFF by default. The daily job only writes a recommendation into the row: a ±1/3 button with a tooltip explaining why. The manager clicks, the change is logged, the cooldown starts. That is Goldratt's own discipline: buffers adapt, humans approve.
The pulse board is the daily per-SKU control panel: sales funnel, ad economics, stock and prices, about 60 columns per SKU, virtualized so 763 rows scroll like silk. The dashboard answers the owner's question: how does the month end if we keep moving like this?
763 rows × ~60 columns with sticky SKU column and configurable traffic lights. A card mode for working by exception. Each SKU opens into a card: sales, ads, stock, prices and comments in 7 / 14 / 30-day windows.
The marketplace funnel export lands by drag-and-drop. Columns are matched by fuzzy mapping; a renamed column triggers a confirm dialog instead of a silent misread. Broken rows are rejected individually, never the whole file.
TrueStats and marketplace revenue are compared daily. A gap over 5% raises a banner. In production this caught a real 13.31% mismatch on the first battle run.
Cabinet and category economics from file, API or both merged per SKU. A month-end forecast from the 15-day trend. Expense metrics invert their coloring: growing ad spend is red, not green. Response: 61 ms cached, 101 ms cold.
The backend renders showcase tabs into the familiar spreadsheet: main board, season view,
distribution, price proposals, stock control, alerts. Writes are RAW only, so barcodes do
not turn into 2.05E+12. Manual edits flow back by a diff-pull every 10 minutes,
and the render never overwrites a human's cell. A button in the sheet posts new prices back
through a token-checked webhook.
Problems should find people, not wait to be found. Seven deterministic alert types watch the business; a health page with traffic lights watches the system itself.
| Alert | Trigger | Detail |
|---|---|---|
| Urgent restock | ≤100 units left on the marketplace | Always red, always pinned to the top of the feed. |
| Low sufficiency | coverage below threshold | Uses the same sufficiency contract as the restock table. One formula, one truth. |
| Orders drop | 3-day speed vs 10-day speed, ≥30% down | Catches a dying SKU in days, not weeks. |
| Orders spike | 3-day speed sharply up | A spike is a restock signal too: sell it while it flies. |
| Negative reviews | new 1-3 star reviews | Popup with the review texts, server-side pagination. 1,447 negatives were on file at acceptance. |
| Production start | <90 days of runway | Counts the 60-day production cycle backwards from the projected stockout, softened off-season. |
| Data mismatch | TrueStats vs WB gap >5% | The system distrusts its own inputs before it lets you trust its outputs. |
An alert is an episode: active, resolved, reactivated same-day if the condition returns. Zero duplicates by design. Acknowledgements are per user, so one manager's "seen" does not hide the problem from another.
If marketplace data is older than 24 hours, the whole alert recalculation is skipped. No alerts computed on stale data: silence is more honest than noise.
The first production run surfaced 388 active alert episodes. That is the backlog of problems that used to be invisible. The feed sorted them; urgent restock came first.
A cron job re-runs every autotest, 1,286 backend and 440 frontend, every morning at 05:00 in a clean, separate database. The result lands in a table and shows on the Health page, next to traffic lights for containers, memory, disk and backup freshness. The ERP proves to its owner daily that it still works. Not a promise, a green light.
Towels sell alone and in sets. The same raw item can go out a dozen ways, each with its own price per unit, margin and stock cap. The client used to hold this combinatorics in his head. Now the system computes it, and explains every suggestion in a "Why" column.
For each raw towel: all its sell variants, single or inside any of 349 set recipes pulled from MoySklad tech cards. Per variant: price per unit, margin, cap by current stock, the limiting component, and a greedy allocation plan. 294 raw items × 349 recipes = 1,813 variants, computed in 3.0 seconds and verified by hand against SQL on production data.
The client's merchandising rule, encoded: pink does not sell alone, but alongside grey it flies. The engine parses the model base from the article, pairs a top color with a slow mover (same base), crosses bases, and estimates the economics of each candidate from analogs.
Every candidate carries a human-readable rationale: which color pulls, which stock it unfreezes, what the expected per-unit economics are. Optimization is deliberately greedy, not an LP solver: the client can follow every step, and trust beats the last percent of optimality.
A floating ABC classification by three criteria at once: profit, revenue and orders, with configurable 80/95 thresholds guarded by a database-level check.
The first production run put numbers on the intuition: class A is 134 SKUs, 17.6% of the catalog, holding 80.2% of the profit. The classes are cross-checked against the analytics service's own ABC, and SKUs that are loss-making or lack data are shown in explicit buckets with the reason broken down, not silently dropped.
Classes are computed once and served by contract: the pulse board and the restock table pull the letter without recomputing anything. The TOP rule of the restock table feeds on exactly these classes.
An A-class SKU below 90% of its buffer jumps to the top of the supplier order, above any C-class SKU with a worse percentage. The business protects the 134 SKUs that pay for everything else. That priority is a tested rule, not a habit in someone's head.
The buffer says what is missing. The order module says where to get it and by when, then produces the exact Excel file the producer in Uzbekistan expects.
Q(L) = max(0, ceil(V_plan × (N + L) × 1.1 - total stock)). Pass one checks the producer's warehouse: enough there means mode "from stock", lead 14 days. Otherwise mode "production", lead 74 days (60 production + 10 delivery + domestic legs), with a note when part of the quantity is available now.The warehouse packed orders by hand: the marketplace account, a pick list, Excel, a scanner into Excel, a manual replacement of hidden separators and an upload of the template back. The real loss of time started after that upload: the marketplace checks every code against the national traceability registry and rejects some of them, and people go looking for those items through the boxes. The module moves the check to the moment of the scan: the picker learns about a bad code while the item is still in their hands. The supplies themselves followed into the ERP: how they are built from the queue is the next section.
| Verdict | What the picker sees and does next |
|---|---|
| Code accepted | green, the order is closed, the label goes to the printer |
| Accepted unchecked | yellow: the registry did not answer, the code is accepted by setting and rechecked at night |
| Owned by another company | put it aside, take another item |
| Already sold | the code has left circulation: put it aside, take another item |
| Not in circulation | the code was never put into circulation: send it to quarantine |
| Blocked | quarantine |
| Unknown to the registry | quarantine |
| Duplicate | red, with the number of the owning order: the label can be reprinted from there |
| Extra unit | every order of this size in the supply is already packed: the scanner names the product, the size and the time of the last pack, and asks to put the unit aside |
| Another product | the scanner names the product and the size and says where its orders wait: in this supply, in another batch or in another supply |
| Code not read | the code was read only in part: scan again. A cut code tail is never reported as a foreign code |
The order of the checks matters. A foreign sold code has several reasons to fail, and the picker needs to hear one: the one they can act on.
The server classifies the input itself, barcode, marking code or label, and moves the state of the order. A person holding an item has nothing to switch modes with.
Every press carries its own identifier, and a repeated request returns the same answer instead of a recomputed one. Otherwise the picker would see "this code is already scanned" in response to their own scan.
A repeated import refreshes the marketplace fields and never touches ours: status, batch, scans and codes. The nightly run does not erase what people did.
An order with a bad code goes to quarantine with a reason rather than disappearing from the list. The warehouse lead sees how many orders are stuck and why.
The module admin right allows adding and disabling pickers without the portal administrator. Disabling only, never deletion: scans and shift reports hang on the person.
A shift is a day in Moscow time. Per employee it shows what was picked, which codes were accepted, what is done, how many problems, how many rejected scans, and the first and last scan. The rejected scan count opens a list: time, product, what was scanned and what the scanner answered.
A scanner types its characters within milliseconds, so 300 ms of silence after something that looks like a finished code already means the end of input. The scan is caught anywhere on the page: a click on a tab or a button no longer swallows it.
A scanner presses keys as if the layout were English, so with a Russian layout the code arrives in Cyrillic with the wrong symbols. The repair map runs in one pass and only when the string has Cyrillic in it: a dot and a comma are legal in a serial number.
A marking code finds its order by the order barcode first, then by the size barcode in the marketplace card, and only then by links learned on the warehouse floor. Whether a product needs a code is also taken from its card. An extra code request costs one scan, a missed one would ship the item without a code.
Silent degradation is unacceptable here. If the registry token dies, the module raises a system alert and keeps working in "accepted unchecked" mode, marking such codes honestly. It is exactly a silent failure that would cost the rejection at the marketplace this whole module exists to avoid.
Supplies used to be created in the marketplace account: checkboxes down the list of new orders from the top. One supply ended up with a hundred different articles, and the picker walked the whole warehouse with it. Orders from legal entities carry no mark in the account: you tick a hundred orders, the supply cannot be created, and you hunt for the one that blocks it. Now the queue lives in the ERP, and a supply is made of what is convenient to pick in one trip to the shelf.
Before this module the ERP wrote nothing into external systems. The exception is narrow: create a supply, add orders, set the shipping parameters and hand it over to delivery. Every write lands in a log with its request and response, visible on the supply tab. If the marketplace loses a supply, there is something to show its support. Marking codes still go through a template that a person uploads: that is an honest limit.
Orders are added in batches of 100. If the marketplace rejects a batch, it is split in half until the exact rejected orders are found. They stay in the queue with a reason, the rest get through. One bad order does not bring the supply down.
The supply row is saved right after the supply is created at the marketplace, before the orders are added. If anything fails later, the ERP knows about the empty supply and deletes it. A lock guards against a double click, and queue orders are locked so a parallel poll cannot move them.
The system splits a mixed selection itself: oversized goods apart, legal entities apart, and the plan row says so. Nobody has to remember the marketplace rules to avoid a rejection.
If a supply is disbanded on the marketplace side, the ERP notices it on the next poll. Its orders return to the queue, what was packed stays packed, and an empty ERP supply is deleted.
The scanner works by batch. Orders the marketplace moved to another supply detach themselves from the old batch on every poll, and a banner offers to merge batches if there are several after all.
New orders of the same article can be added to an open supply from the selection bar or from the supply page. They join the same batch, and the picker carries on from where they were.
Sometimes the warehouse has to pick by the age of an order rather than by product type. The account shows the oldest order, while the queue used to show neither a date nor an age. Now the queue has two tabs: the whole queue and hot orders. The hot tab carries a ladder of orders waiting longer than 20, 24, 36, 48 and 72 hours with counters, and the Waiting column shows the age of the oldest order: amber past the threshold, red past 48 hours.
The tree arrives already narrowed, so selection, the plan and supply creation know nothing about urgency and stay the same. Tick a category on the hot tab, and only the hot orders of that category go into the supply, while fresh ones stay in the queue.
At a 20 hour threshold half the queue is hot on a weekend. The manager takes the oldest with the 48 hour step rather than by editing a setting. The tab lives in the page address, so it can be shared as a link.
Orders the marketplace has already given a final status to, but still lists as new, are left out of the age count. Otherwise they would stay the oldest forever and hide the really urgent ones.
Once supplies and scans live in the ERP, the owner gets answers to questions that used to be settled by eye: what a picker was busy with, which products cause scanner rejections, what is missing on the shelf, and how much of a supplier's goods the marketplace actually accepted.
Up to four pickers work in parallel. The system has no shifts or quotas, so everything is counted from scans. Presence runs from the first to the last scan of the day. Active time adds up the pauses between neighbouring scans that are shorter than a threshold: 5, 10 or 15 minutes to choose from, anything longer is idle time, lunch or other work. Speed is pieces per active hour, load is active time over presence. Efficiency compares a person with the warehouse average for the same period in one number, and speed and load next to it explain why it is what it is. Only the owner sees the dynamics per person.
The statistics for a day must match the shift report, and a test enforces it. There are no aggregate tables, everything is computed on the fly, and the migration added only indexes, built without locking the hot tables in the middle of a shift.
Two thirds of rejections have no order attached: the scanner refused earlier. So the product is recovered from the code itself, along the same chain the scanner uses. This shows which supplier products cause rejections, and that can be taken to the supplier.
An account with no packed piece and fewer than ten scans stays out of the statistics: it is a scanner check or a test login. Marked goods take longer to pack, and the screen says plainly that comparing people on different goods is approximate.
A picker used to spot missing stock by eye and by lunchtime wrote it into the supply name. The list went to the supplier, who spent a few more hours collecting it and delivered the next day: a day was lost, and a late pick hurts the seller rating. Now a button in the queue splits unpacked orders by supplier from the MoySklad product cards and creates a purchase order for each. Sets are broken into components by the tech card, packaging is not ordered. This is a different function from the producer order in section 08: that one follows the TOC buffer, this one follows concrete FBS orders.
100 were ordered, 7 more arrived, and the next order is for 7, not 107. A partial unique index in the database holds that. If an order is deleted or cancelled in MoySklad, its orders go back to be reordered: statuses are reconciled before every preview.
A log row with an external code is saved before the request. On a dropped connection or a timeout the document is first looked up by that code, and only if it is not there are the orders released. Otherwise a retry would create a second purchase order.
The order is created unposted and warehouse stock is not deducted yet: people strike out the extra in MoySklad. That is a deliberate first iteration. After creation the document is reread and the number of lines and pieces is reconciled.
One MoySklad token serves the nightly sync and order creation, and only a superuser can change it. Product cards are read at the moment of the click: a manager fixes a supplier or a barcode during the day, and the button sees the fix at once.
Some suppliers work as fulfilment: they deliver goods to the marketplace warehouse themselves and get paid for what was accepted. 600 handed in, 599 accepted, 599 paid. The ledger shows how much of each supplier's goods went out in FBS supplies and how much of it was accepted, by acceptance date or by handover date, split by day and by product, with a separate tab for what was handed in and is still not accepted.
The supplier is fixed at the moment of handover, and the accepted mark is sticky. An order cancelled by the buyer after the supply was scanned in at the marketplace warehouse still counts as accepted: the supplier delivered, the warehouse received.
The acceptance date comes from the supply scan at the marketplace warehouse. Without a scan it is the moment the ERP saw the acceptance, marked as approximate and refined later.
One field on the supplies screen searches by number, article, barcode, name, order and label in every supply, closed ones included. About 155 thousand orders are scanned in 150-270 ms as measured on the production database, so an index and a migration that locks a hot table mid-shift are not needed yet.
There was no warehouse register in the system: the warehouse name lived as a bare string inside the daily stock snapshots, and there was nothing to switch a warehouse off with. The client kept a noticeable share of goods in warehouses that no longer exist, while replenishment, turnover and the daily pulse counted those goods as live stock.
The client asked to state from which day a warehouse is off and to recompute the history from that day. Stock snapshots older than the date stay as they were: the past is not rewritten.
Two fields on the warehouse row only answer "who switched it off right now". The client's question was a different one, and the history answers it: who, when, from which date and for what reason.
Saving the same values again adds nothing to the log. Otherwise in a week it fills with noise and stops answering the original question.
Pseudo-warehouses for goods in transit and the drop-off points cannot be excluded: they hold no stock, they mark its movement. An attempt to switch such a row off is rejected by the server, not only by a guard on the front end.
Business analysis first. Every stage started as a self-contained written spec with data contracts, formulas and acceptance criteria. Implementation went step by step under iron rules written down after the previous ERP project. Each change lands as a small reviewable step.
The automatic seasonal coefficient currently equals 1: the marketplace API keeps only about 6 rolling months of history, so year-over-year seasonality is physically unavailable. The system says "not enough history" and accumulates its own. The Google service key was not yet handed over at acceptance, so that module soft-skips instead of failing. Competitor parsing is coded but waits for a proxy. All three are written in the acceptance report, not discovered later.
| Area | Before | After |
|---|---|---|
| Restock decisions | The owner's personal Excel, daily, by feel. Held on one person. | TOC buffers with traffic-light zones per SKU, self-correcting by ±1/3, every change confirmed by a human. |
| Data | Three services plus manual exports. No single picture. | One PostgreSQL, a nightly conveyor with freshness gates and idempotent imports. |
| Problem discovery | By eye, when someone happened to look. | 7 alert types; 388 previously invisible episodes surfaced on day one, urgent restock always on top. |
| Month economics | Clear after the month ended. | A dashboard with a month-end forecast from the 15-day trend, in 61 ms. |
| Set planning | Combinatorics in the owner's head. | 1,813 computed variants in 3.0 s, each new candidate with a written "Why". |
| Producer order | Assembled by hand for every order. | Checkboxes plus one button: the producer's exact Excel format, with history and snapshots. |
| FBS supplies | Checkboxes in the marketplace account from the top down, a hundred different articles in one supply. | A queue per warehouse and a suggested build: one article per supply, urgent orders first, handover to delivery from the ERP with a log. |
| Missing stock | By lunchtime a picker wrote what was missing into the supply name, and the list went to the supplier. | A purchase order in MoySklad straight from the FBS queue, sets split by the tech card, every order reordered only once. |
| Warehouse control | Who was busy with what and which products caused rejections was invisible. | Picker statistics from scans and acceptance per supplier: handed in, accepted by the marketplace, waiting. |
| Trust | "It should be working." | 1,286+440 autotests re-run every morning in a clean database, result visible to the client on the Health page. |
Business analyst and systems builder. I start with the business process and the data contract, then automate. This ERP went from agreed spec to production, and it runs the client's restock on Goldratt's buffers instead of gut feel.