Production
ENRU
Case study - custom ERP

Production stopped being planned by eye.

A womenswear seller with its own sewing factory: four seller accounts on Wildberries and Ozon, two major e-commerce marketplaces, its own finished goods warehouse and a sewing floor of two teams under their foremen. The system answers the three questions of every week: what to ship from the own warehouse to the marketplaces, what to sew and on which sewing line, and how the week is going in fact. Next to that: money that matches the client's accounting system to the kopeck, seasonality from the seller's own sales history, and an item card with two cost prices.

FastAPIPostgreSQL 16CeleryReact 18 TypeScriptWildberries and Ozon APIs1C via filesGoogle Sheets
77
data tables in 23 backend modules
148
API endpoints, every module with its own README
1,673+551
backend and frontend autotests
4
accounts on two marketplaces in one loop
2
sewing lines, a batch goes whole to one
3 / 11 wk
sewing horizon and the weeks the run sees
01 Context and pain

The factory runs, but the week was assembled from five spreadsheets

The client has its own sewing factory, its own finished goods warehouse and four storefronts on two marketplaces. Every week the planner put the answers together by hand: what to ship to the marketplaces, what to put into sewing, whether there are enough people. Money was counted in two places and did not match.

Restock from five reports

Twice a week for every account: the marketplace catalog, the 1C stock, the marketplace stock, the seller warehouse and the order feed. The order feed has no barcode, so the key was glued from article and size, and lookup errors were replaced with zeros by hand. The reserve between accounts was zeroed, and the same unit could go to two accounts.

Demand in four accounts, one factory

One article sells from four storefronts. How much of it to sew shows only in the sum of orders across all accounts. And apparel buyout is far from a hundred percent: part of the orders come back to the warehouse and sell again.

Factory load lived in someone's head

Hours per garment and how many people a foreman has. Piece rates in roubles existed, but turning them into hours through an average rate is too rough. The client's requirement was blunt: the plan needs hours, not money.

Margin did not match 1C

The totals matched to the rouble, but the base of display was different. The accounting system counts revenue by buyer receipts, while the portal divided margin by the seller price, which is 1.6 to 2.4 times higher than the receipts. The portal margin came out several times lower than in 1C.

Cost price in two places

The factory price list for reports and the technologist's costing sheets, where coefficients and a hand-typed number follow the total. Which figure was right depended on who was asking.

Seasonality lived in someone's head

Seasonality was not set up either in the accounting system or in Excel. Every article has its own season: suit trousers and linen trousers of one category sell in different months. And the marketplace gives no more than a year of history.

02 Architecture

Four accounts and 1C files in, the factory week out

At night Celery pulls four accounts on two marketplaces into PostgreSQL: daily orders, the sales funnel, the marketplace size report, stock, accruals and advertising. Warehouse stock, hours from the work-norming department and bills of materials arrive as files. The modules compute restock, the plan, demand and money on the fly from the latest snapshots. People work in a React portal, managers see the same truth in a Google Sheet, and deviations gather on the Signals page.

NIGHTLY CONVEYOR · MOSCOW TIME Wildberries 2 seller accounts daily orders · funnel sizes · stock · reports 12 nightly sync steps Ozon 2 seller accounts orders · daily accruals advertising · stock Client files 1C stock per warehouse work-norming hours BOMs via a template Sync layer Celery Beat, run journal idempotent import a requested report is kept across restarts rate limit per account Moscow day boundaries accounts are read only PostgreSQL 16 77 tables · 50 migrations Redis 7: cache, broker, queues backup before every release Calculation modules restock and a live workbook advisor and exact search sewing lines and weekly fact buyout, sizes, season receipt money, expense articles items and two cost prices computed on the fly, not stored Portal Restock · Plan · Dashboard Items · Analytics · Pulse 5 color schemes Google Sheets showcase the same truth for managers rendered at 07:35, before the pull Signals 21 deviation types a portal page and a sheet in the Google table IRON RULE The system writes nothing into the marketplace accounts or the accounting system. Supplies, prices, fabric orders and 1C documents stay with people: the system computes, explains and prepares the file, the decision stays human.
23backend modules, each with its own README
50schema migrations, additive releases only
07:35Sheets render, before the pull
0write methods into accounts and 1C
03 Restock

What to ship from the own warehouse to the marketplaces this week

Restock became the first tab of production: it is a daily question, not a monthly table. A row is one barcode of one size in one account. The table is not stored anywhere: it is computed on the fly from the latest stock and order snapshots and repeats the planner's Excel calculation formula for formula.

forecast = orders per day over 3 days × 7 days
marketplace stock = marketplace warehouse + seller warehouse (FBS)
shortfall = marketplace stock − forecast
available = max(0, 1C stock − reserve of open documents of other accounts)
restock = max(0, min(safety stock 3 − shortfall, available))

In words: ship enough to cover a week of sales with three more units left on the shelf, but no more than the warehouse holds. Every number on the screen has its substitution: 6 orders in 3 days, 2.0 a day × 7 days = forecast 14; stock 6 - 14 = shortfall -8; safety 3 - (-8) = 11; warehouse 53 - reserve 0 = 53; restock min(11, 53) = 11.

Restock by barcode: daily orders, marketplace and warehouse stock, shortfall and restock with a how-it-is-counted card
Restock by barcode: the Item, Orders, Marketplace, Main warehouse and Restock groups. The restock figure can be edited by hand, and a click on it opens the substitution by the formula.

A workbook with live formulas

The calculation file is not an export of numbers: eight sheets match the planner's file sheet for sheet, row formulas match character for character, reconciliation columns against the portal sit on the right, plus a legend sheet. A test evaluates the workbook formulas as Excel does and demands a match with the portal. A person checks the system in a familiar layout and spots an error either way.

Orders by size from the marketplace report

The funnel matches the account but knows no sizes. The order list knows sizes but is smaller than the funnel. The answer: the marketplace size report day by day with a daily reconciliation against the funnel. On a day without a report each size takes its own listed orders, and only the difference is split by shares. A test on 300 random cases holds the invariants.

Whoever issued the document took the stock

The Create a list for the warehouse button issues a document that is open at once. An open document reserves its units from other accounts and closes itself when a 1C snapshot newer than its date arrives. One unit no longer goes to two accounts.

The file is built in the background

A background job with a status and a percentage, a lock against a double click, and a build stuck for 10 minutes reads as an error with a Retry button. Fresh marketplace stock is taken before the build; a marketplace refusal does not break it, the file is built from the last snapshot and the reason is said in words. 5,500 rows build in 6.6 seconds.

1C stock replaces only its own warehouse

Before applying, the window says what will happen: this many sizes will be applied, this many sizes of the previous snapshot will become zero. Replacing it whole is deliberate: the accounting export lists only what is there. A file of another warehouse is refused, an empty upload is not applied.

Archived does not mean not selling

Archived articles are included in restock by default. Archived means no longer sewn, while the stock is still being sold and has to be shipped: almost half of the orders fall on models already out of production.

What the calculation does not see is written right on the screen: supplies to marketplace warehouses, goods on their way to a buyer, the second account of the same marketplace. Without a marketplace snapshot the stock shows as a dash with a note, while the arithmetic takes it as zero and says accepted as 0: otherwise such a row could never be shipped. A row with a shortfall and an empty warehouse gets the nothing to cover note. It is a candidate for sewing and the bridge to the plan.

4settings: window, horizon, safety stock, in transit
12kinds of row notes
9workbook sheets, 13 traffic light rules
230+91autotests of the module
04 Sewing plan

The plan takes what a person ticked as sew

Whether to sew is decided by a person, by money and by the traits of an article. The plan's job is to lay out a decision already taken across weeks and sewing lines, size the batches, set the dates, count the fabric shortage and go to the factory as a file.

  1. A catalog with traits. Articles with movement: buyout, cost price, margin per piece, hours per piece, season, suggested quantity and a sew-no-later-than date. Badges prompt the decision: a black mark for a low buyout, test batch conditions for novelties. The sew tick is saved at once.
  2. An advisor by urgency. The Build the plan with the advisor button lays out everything ticked: first what is already late, then this week, then later; inside that, by the date the stock runs out; novelties earlier. It takes seconds. The exact calculation with branch and bound stays as the second button.
  3. A batch from demand after arrival. The batch size is the uncovered demand over the cover window after the batch arrives, rounded up to the step and no smaller than the minimum batch. Every batch has real dates: cutting, sewing, at the warehouse, at the marketplace.
  4. An edit is a new version. Pieces, week, line, delete, add: every edit creates a manual version linked to its parent with a before and after line. Undo the edit means going back to the parent.
  5. Make it current and export. Batches go into the working plan with their sewing line and size split, and their sew ticks are cleared in the same transaction. The export is a three-sheet workbook without a single money figure.
Sewing plan: a money-free summary, weekly load of two sewing lines and the batches of the run
The plan: five money-free cards, a weekly load strip for two sewing lines, weeks past the sewing horizon in grey, and the batches of the run with size chips below.
Plan by week: a gantt of batches with cutting, sewing, the road to the warehouse and fabric lanes
The weekly view: cutting, sewing in the color of the line, the road to the warehouse and delivery. Fabric lanes below the batches carry diamonds for when to place the order.

The sewing horizon is separate from the run weeks

A batch sewn in the last week of a three-week horizon reaches the marketplace past the edge of the run, and the engine saw zero demand after arrival. So run weeks = horizon + arrival lag + cover window = 3 + 2 + 6 = 11, and batches are placed only in the first three. The exact search sped up on the way: 1,141 variants per item instead of 20,489.

A money-free plan is a filter, not a second engine

The exact search needs a criterion to compare variants, so the engine still counts profit, while the answer goes out as a copy without money keys at any depth. The why texts are rewritten in pieces, hours and dates. The workbook has a second lock: a test walks every cell and lets no words about roubles, profit or margin through.

A human pick beats advice

A ticked article is always placed, even when by its stock it is too early to sew: then it goes to the first week of the horizon with the words sewn by a human decision. The only refusal is a batch that does not fit the horizon capacity.

Versions with an input snapshot

Every run keeps the whole engine input, and an edit is recomputed from that same input without a trip to the database. Two people editing one run get two branches: nobody overwrites anybody.

Bit for bit with the reference

The Python engine is checked against the prototype engine on six items: the same layout, the same batches, the same number of search nodes. Even JavaScript rounding is reproduced as floor(x + 0.5).

Total hours are computed once

In the workbook the total is computed from exact hours rather than by adding rounded rows. Otherwise the file would argue with the screen over a tenth of an hour.

Honest limits. The run does not yet see batches in progress either as arrivals or as busy hours, so their ticks are cleared and the In progress column warns about a repeated tick. There is no fabric order calendar with statuses yet: the fabric shortage and the order-by date are computed and shown as diamonds on the gantt, while the order itself is placed by people.

3 / 11weeks: sewing horizon / run
30%of weekly hours to novelties first
40 / 120 slimits of the exact run
226autotests of the plan engine
05 Sewing lines

Two sewing lines instead of five workshops

After a meeting with the factory only sewing is planned, in two lines under their foremen. Cutting, quality control, ironing and packing are switched off the load, while their roubles still go into the cost price. Scheduling every workshop rigidly does harm: the factory loses room to manoeuvre.

line capacity, h per week = people × shift hours × shifts per week × (1 − reserve) − hours for outside orders
hours per piece = work-norming hours from the BOM, otherwise the sewing rate / conversion rate
batch hours = pieces × hours per piece
load, % = hours of the week's batches / capacity × 100 over 100 red, from 85 amber

An example on made-up numbers: 20 people at 8 hours, 5 shifts and a 3% reserve give 20 × 8 × 5 × 0.97 = 776 hours a week. A garment with a norm of 2.03 hours in a batch of 180 takes 365 hours, almost half the line's week.

Settings: two sewing lines with capacity in hours and the switched-off workshops
Workshops: two sewing lines with the capacity formula and a Sewing total row. Cutting, quality control, ironing and packing are switched off and grey at the bottom, outside the load.

One hours function for every screen

Hours per piece come from one function for the engine, the advisor, the dashboard, the item card, the BOMs and the catalog. There is no second way to count hours in the system, and every figure names its source: from the BOM or converted by the rate.

A batch goes whole to one line

The week layout keeps pinned batches in place and sends the rest, by descending hours, to the line that will have more free hours left after it. The advisor runs the same function: a batch that does not fit is trimmed by the step with the reason written out; when even the minimum batch does not fit, it moves to the next week.

The first sewing mark pins the line

Otherwise a new batch of the week would move one already started to the other line and rewrite the history of actual hours. A line can also be pinned with a button or by making the run the working plan.

The work-norming file loads as it is

The sheet is found by its header, the column order is free, hours come with a decimal comma, a model without a color applies to every color in the catalog. A novelty without hours goes to the without hours list, not to errors. The pay column is not read at all.

Empty is more honest than zero. If a line has no headcount, it has no capacity: the cell is grey with the reason, not zero and not 100%, and the line stays out of the Sewing total, which is said in words. A workshop whose rate is under review is counted, but the figure is grey and not used in decisions.

2 + 4lines and switched-off workshops
>100%red cell: the week will not fit
85-100%amber: no room for rework
196autotests of the production module
06 Plan vs fact

The production dashboard: how the week is going

A plan has to be followed: what is cut and sewn, what is late, how many hours actually went. It also works the other way: the factory is already sewing something the plan did not have, and that is entered too, as a list pasted straight from Excel.

Production dashboard: plan against fact by sewing line and week, batch stages
A lines by weeks grid: each cell shows plan against capacity, plan and fact in hours, and seven day dots with marks. Below, batches with stages, lag in words and a Mark button.

The status is derived from the marks

It is not stored separately: one truth. A stage can have as many marks as needed, cutting can go in parts. More pieces than the stage has left or a date in the future is refused in words.

Lag in words

Every batch says where it is now and how far behind: sewing planned by 13.09, 40 of 100 done. A click on a week cell opens the batches its hours came from.

Sizes with a live sum

The size split is edited in the expanded batch, the sum 230 of 240 sits next to it, and Save stays disabled until the sum matches the batch.

4stages: cutting, sewing, ready, shipping
6statuses derived from marks
4 / 8 / 12weeks in the dashboard window
07 Demand model

Buyout, sizes and season come from a ladder of sources

Demand is assembled from the actual orders of all four accounts by item key. Three multipliers act on it: buyout, size shares and season. Each comes from a ladder: manual beats automatic, the item beats the category, and when there is no data, the system says exactly that.

Buyout: six steps, the first one that applies

  1. Manual on the article. A human decision beats any statistics.
  2. The article's own buyout, if the window has at least 20 orders.
  3. Manual on the category.
  4. The category buyout by the sum of pieces of all its articles, not an average of percentages: otherwise an item with three orders would weigh as much as one with three thousand.
  5. The buyout of the whole catalog.
  6. The value from the settings as the last step.

A window with a gap, and zero as no data

The marketplace funnel keeps adding buyouts for about two more weeks, so the last 14 days stay out of the window: otherwise the freshest days would always look like a slump. Zero buyouts with live orders means no data, not 0%: otherwise the engine would call every batch of the article a loss.

Only what was bought leaves the warehouse

Units that were not bought come back to the warehouse after three weeks and sell again. On a textbook batch of 240 at a 50% buyout this correction flips the sign of the batch profit: without it the advisor would place loss-making batches for everyone.

Size shares and the batch split

Shares come from a manual setting, the article's own orders over 180 days, its own sales, the category profile or an even split. A batch is split by size: first the shortage of each size in the month of arrival is covered, the rest goes by shares in multiples of 10, and the sum always equals the batch. A shortage of size 48 is never covered by a surplus of size 54.

A golden test against the client's Excel

The port of the monthly demand calculation is checked on the inputs and expectations of the workbook itself: 13,786 size-month pairs in a full run. Rounding happens once and only half up, as ROUND in Excel. The monthly screen later gave way to the daily restock, and the engine lives on in the plan, the batch size split and the item card.

Season from the seller's own history

History collects itself every night: a year of marketplace analytics arrives as one report in seconds instead of hours of requests, and old months are never deleted, so a second round of the season appears after a year. A month coefficient is the daily pace of the month over the average daily pace of full months: by totals, February would lose three days to March. Fewer than six full months means too little history, not an invented profile. A novelty never takes its own history: the category gives the shape of the season, not the level of demand.

Seasonality: a category chart by month with articles overlaid as a percentage of their own average
Analytics, Seasonality: slices by month, week, day and year, and up to five articles over the category line on the right axis as a percentage of their own average.

A category twin without AI

Ozon categories find their Wildberries pair by a rule on word stems: doubled letters collapse, endings are stripped, and a candidate must be shorter than the original name. A qualifier can be dropped but never invented: a top does not become a sports top. 23 categories of 29 find a pair, a person picks the donor for the rest.

The shape of the season, not the pieces

Articles sit on the right axis as a percentage of their own average: in pieces an article line would lie flat near zero next to its category, and it is the shape of the season that people compare. Year over year runs on a twelve-month axis: January against January.

A flat year is not saved

Twelve ones would block the way to the category history, so a deliberate ignore the season is a separate button. Coefficients are never written for all categories at once: noisy categories need a human look.

6buyout steps
5size share steps
13,786pairs in the golden run
23 of 29categories find a twin
08 Money

Money matches the client's books to the kopeck

The display was rebuilt the way the client's accounting system counts: one ladder for both marketplaces on the dashboard, in analytics and in the pulse board. Profit and the money paid out did not change by a kopeck: the base of the percentages changed, not the money.

sales at seller prices − marketplace discount to buyers = revenue by receipts
− commission above the marketplace discount − logistics, storage, returns, fines, acquiring
− cost of goods sold − advertising in real money = profit
margin = profit / revenue by receipts; ad share = advertising / revenue by receipts
Portal dashboard: the money ladder and a breakdown of a row into marketplace expense articles
The dashboard: seven cards, a summary along the money ladder and the What it is made of window. Marketplace articles with amounts and shares, a new article marked with a chip, and a window total equal to the dashboard row.

A breakdown window per article

A summary row, a category or a pie slice opens the marketplace articles the amount is made of. A new article is marked with a dated chip and raises a signal. An administrator assigns an article to the right row, and the move applies to the whole history.

The window total equals the row by construction

Both figures come from one column of the showcase, and the frontend compares them in whole kopecks: in floating point a one-kopeck difference would simply vanish.

Every marketplace report is reconciled

Ten totals and the total-to-pay formula are checked in whole kopecks, and a mismatch is a red signal naming the field. The paid-out amount is collected by the operation day of the report, so a week's sum equals the marketplace total to the kopeck.

A move is recomputed once

A queue with 120 seconds of quiet, each move pushes the deadline, and the recount runs through the same functions as the nightly build: no second profit formula appears in the system.

Kopecks are not lost in division

Even and proportional splits run in whole kopecks: minus 5 kopecks in three parts is 1, 2 and 2 kopecks, and the parts always add up to the whole.

The same days of the previous month

On September 7 the comparison is with August 1-7, not with the whole of August: otherwise at the start of a month every card would show minus 75%.

7dashboard cards
31summary rows, each with a hint
11checks per marketplace report
21signal types
09 Items

One article, one card, two cost prices

Learning about one article used to take five tabs. The card stores nothing: it gathers figures from the source modules on every request. Cards, the cost chain, labour, batch and advice, season, 12 weeks of movement, sizes and batches.

current cost = factory price list, versioned by date → dashboard, analytics, pulse board
operations = sewing × (1 + premium) + cutting + quality control + ironing + packing
production cost = materials + operations
approved cost = approved price, otherwise production cost × K1 × K2 → advisor, plan
gap, % = |approved − current| / current × 100 highlighted above 15%
Item card: two cost prices, labour, batch and advice, movement and sizes
The item card: six cards, the cost chain, labour, batch and advice, season, 12 weeks of movement and sizes.
Bills of materials: sewing hours with their source, coefficients and the cost chain
Bills of materials: sewing hours with a source chip, premium, coefficients and three cost prices. An expanded row shows the chain from materials to the approved price.

No auto-sync between the two costs

Only an explicit button on one article writes into the current cost. The two figures serve their own purposes, and the gap between them is highlighted.

Profit history is not rewritten

The first cost version applies to the whole history, later ones only forward. A zero or a jump of more than 10 times is held for review: a zero would turn a loss-making item into a star. A new price recomputes the showcases by itself, and the header shows the progress.

Archive with one button

An archived article leaves planning, while its stock still counts in the economics and is shipped by restock. Who archived it, when and from where is kept in the journal.

10 Interface language

No buffers, drums or bottlenecks on the screen

Underneath runs ordinary theory of constraints and honest combinatorics. None of their terms appear on the screen: people on the factory floor speak differently, and there is no point in teaching them a vocabulary for the sake of an interface.

In the code and the specOn the screen
item key, item_keyArticle
bill of materials, BOMWhat we sew from
consumption normFabric per piece, m
logistics lead timeFabric in transit, days
reorder pointOrder by
workshop capacityHours per week
launch decisionthe sew tick
greedy layout by urgencythe Build the plan with the advisor button
branch and boundthe Exact calculation button
uncovered shortfallthe nothing to cover note

How it is counted, on every figure

A figure with a dotted underline opens the formula in words and the substitution line. A figure that cannot be explained in ten seconds counts as unfinished in this interface.

A filter on every column

A funnel in the header of every column, and for paged tables the selection runs on the server. A guard test breaks the build if a table without filters appears in the system.

The factory's order

Articles everywhere follow the factory order: dresses, sundresses, skirts, trousers, suits, then model number and color. One function on the frontend and the server. Tabs became addresses, the first column is sticky, and on a phone tables turn into cards.

11 Results

What changed for the client

BeforeAfter
Restock for every account from five reports and lookups, the reserve between accounts zeroed. Restock on the fly for every barcode, a workbook with the planner's live formulas and a reconciliation against the portal, the reserve between accounts held by a document.
Demand counted per account, buyout as one constant, season in someone's head. Orders of four accounts by item key, buyout, sizes and season from a ladder of sources, history collected automatically.
What to sew and when was decided with a spreadsheet. A person ticks what to sew, the advisor lays it out by week and line, and the plan goes to the factory as a money-free file.
Factory load lived in someone's head. Two sewing lines with capacity in hours, a batch goes whole to one, and plan against fact by week on the dashboard.
Portal margin several times lower than in 1C. A money ladder as in 1C: revenue by receipts, expense articles with a breakdown, every marketplace report reconciled to the kopeck.
Cost price in two places and arguments about the figures. Two cost prices by purpose in one item card, the gap highlighted.
Nobody could explain where a figure came from. Every figure carries how it is counted with a substitution, and the plan has a Why block and a list of what did not fit.

Kirill Sedov

Business analyst and systems builder. I start from the business process and the data contract, then automate. Here the weekly assembly from five spreadsheets was replaced by a system that computes restock, the plan and the money and can explain every figure.

Get in touch on LinkedIn