- An online-only roaster is the simplest shape in coffee: one location, national shipping, some subscription revenue, no branches and no wholesale.
- Freshness is the constraint that makes it different. Roast date is a stock attribute, not a note on a label.
- The store dashboard holds the sale. It does not hold roast date, roast loss, batch cost or a subscription as recurring revenue.
- Picking oldest first is the single change that stops you shipping a fresh bag while older stock quietly ages out.
- A small roaster selling a few hundred bags a month usually does not need any of this yet, and that is a fair answer.
The model, in one paragraph
Green beans in, roast to order or to a forecast, pack, ship nationally, and take some of the revenue as a monthly subscription. No branches, no wholesale accounts, no counter. It is the simplest of the coffee models, which makes it the right place to start, and the one where the gap between what the store dashboard does and what the business needs is easiest to see. If you are still deciding whether the question applies to you at all, does your store need an ERP covers it from the start.
What the store dashboard does not hold
Your storefront is good at selling coffee. It records that a customer bought a 250g bag of a named coffee, took the payment, and produced a shipping label. Four things it does not hold, and all four matter more in coffee than in most categories.
Roast date as a property of the stock. Not a line on the label, but an attribute the system can sort and pick by.
Cost from green to roasted. The bag you ship is lighter than the beans you bought, and the difference has to land somewhere.
A subscription as recurring revenue, rather than the same customer placing a similar order twelve times.
Returns on something perishable, where the question is not only whether to refund but whether the bag can go back into sellable stock at all.
Inventory by roast date
This is the section that matters most, and it is the one thing a general stock count cannot do for you.
Each roast becomes a lot, carrying its own roast date. Odoo's expiry tracking then gives that lot up to four dates: a best before date, an end of life date, an alert date that warns you in advance, and a removal date, which is the one the picking rule reads. Set the freshness window you sell to, say thirty days from roast, and the removal date follows from the roast date automatically.
Then switch the removal strategy to first expiry, first out. From that point the system picks the lot closest to its limit rather than whatever is nearest the door. That one setting is the difference between a warehouse that rotates itself and one that quietly ages out its oldest stock while shipping the freshest bag to a customer who would not have minded either way.
What a merchant sees looks like this.
| Lot | Roasted | Window used | Bags | Status |
|---|---|---|---|---|
| R-0901 | 24 days ago | 24 of 30 days | 8 | Near limit |
| R-0910 | 12 days ago | 12 of 30 days | 28 | Next |
| R-0918 | 4 days ago | 4 of 30 days | 48 | Freshest |
Three lots of the same coffee, 84 bags in total, sold on a thirty day window. R-0901 ships first because it has six days left, and the alert date is what tells you that in time to do something about it, whether that is prioritising it, bundling it, or putting it in subscription boxes going out this week. Without lots, those 84 bags are one number and the decision is invisible. Odoo's inventory application is where this lives.
From order to invoice
The full path for a single bag, once the store and the system are connected.
- Store order. Customer, items and payment.
- Into the system. A sales order is created and stock is reserved.
- Roast batch. The lot number and roast date are recorded against what was produced.
- Packing. The oldest lot is picked first.
- Courier. Shipment raised and the tracking number attached.
- E-invoice. Issued and reported to ZATCA from the same record.
Nobody retypes anything at any step, and the lot number stays attached from the roast to the invoice, which is what makes a quality complaint traceable to a batch rather than to a guess.
Batch cost, from green to roasted
This is where most roasters are quietly wrong, and it is arithmetic rather than accounting.
Take an illustrative example. You buy a 60kg green lot. Roasting drives off moisture, so at a typical loss of around 16 percent you are left with roughly 50kg of roasted coffee, which is about 201 bags at 250g. The cost that belongs on one bag is its share of the green lot, plus the roast, plus packaging and labour, divided across 201 bags rather than across the 240 bags the green weight would have suggested.
Cost from the green weight and every bag is understated by the loss. Do it from the roasted weight, per lot, and the margin you read is the margin you have. If roast batches are tracked as production, manufacturing is what carries the bill of materials and the loss.
Subscriptions
A monthly subscription is not twelve orders. It is one agreement that generates a delivery and an invoice on a schedule, and the distinction shows up in your numbers immediately: recurring revenue you can forecast, churn you can see, and a roast plan you can build against next month's committed volume rather than last month's guess.
Odoo's subscriptions application holds the agreement and raises the recurring invoice. Whether the subscriber manages their own plan from the storefront depends on how the store and the system are connected, and that is worth settling during setup rather than after the first renewal.
What you actually need to switch on
Less than most people expect. Sales, inventory with lot and expiry tracking, accounting, and purchasing for green buying. Add manufacturing only if you want roast batches tracked as production with a bill of materials and a recorded loss. Add subscriptions when subscription revenue is material rather than experimental.
If your store is on one of the Saudi platforms, the connection itself is covered separately: connecting a Salla store and connecting a Zid store each set out what moves and how the setup runs.