Margin fell by 12 points in a single week. Nothing in the system had been changed, prices were the same, sales were the same. The cause turned out to be three weeks old — a vendor return that everybody treated as a corrected mistake.
The nastiest accounting mistakes are the ones that fire with a delay. The document was posted in July, the error was spotted the same week, corrected, forgotten. And in August the owner looks at a report and asks why margin sagged across the whole company.
I worked through this case a few days ago, and it is a good one because there is no software bug here and no one's negligence. There is an ordinary data entry mistake, a perfectly reasonable attempt to correct it — and a feature of inventory valuation that few people remember at the moment of correcting.
Everything described below is done with a mouse in standard Odoo reports. No database access and no queries required.
The symptom
The weekly margin report shows 36% instead of the usual 44–47%. The drop looks general: it is visible in the total across all sales, and the owner quite logically concludes that “something broke in the system” or “vendors raised their prices”.
The first thing worth doing in this situation is not to look for a systemic cause, but to check whether it is systemic at all. Break margin down by product. If one product produces the dip, look there rather than in the settings.
In my case the difference was striking: 36.5% across the whole range and 48.3% without one product. In other words, the rest of the catalogue performed even better than the previous week.
What actually happened
The wrong product ended up on a purchase order. The vendor sells both cheap red caviar (about ₴102 apiece) and premium black (₴1,406 apiece). The cheap product was picked on the order, while the price was left over from the expensive one. Twenty units entered stock at a price inflated almost fourteen times.
The mistake was noticed a few days later. And it was corrected in the most obvious way — a vendor return was issued for all twenty units. Quantity balanced out: no extra stock on hand, the on-hand figures were right. Everyone relaxed.
The trickiest part here is the delay. Three weeks passed between the mistake and the symptom. All that time the on-hand quantities were correct, the cost of goods sold was correct, the reports looked normal. The error was waiting for the queue to reach its layer.
How to find this in your own system: four screens
Screen 1. Which product exactly ruined the margin
There is no need to exclude products one by one by hand — a single pivot table is enough.
Sales → Reporting → Sales, switch to Pivot view. Then:
- Measures: “Margin” and “Untaxed Total”. The margin field appears in the report if the
sale_marginmodule is installed — and it almost always is. - Rows: Product.
- Columns: Order Date, grouped by week.
Now click the header of the problem week's column — the table will sort by margin. The product with the anomaly will end up at the edge, and its margin will usually be negative, which does not happen in normal operation. In my case one product showed ₴−12,256 of margin on ₴3,829 of turnover.
The same thing can be opened through Sales → Reporting → Products — it is the same report, already grouped by product.
There is also the “Product Margins” report (the product_margin module): you select products in the list and call it from the actions menu. It is handy as a general overview, but it computes cost from standard_price and from accounts rather than from the layers actually consumed — for this task it is too coarse.
Screen 2. Make sure it is about cost, not about price
Open any sales order from the problem week. In the order lines table click the column settings icon (the small toggle to the right above the table) and enable the Cost and Margin columns.
If the selling price is normal while the cost is several times higher than usual, the question is not about sales but about the price at which the goods entered stock.
Screen 3. Find the layer itself
Inventory → Reporting → Valuation. This is a list of all layers: every receipt and every write-off on its own line.
By default not all the columns you need are shown here. Click the column settings icon to the right above the list and enable:
- Unit Value — the price at which the layer came in;
- Remaining Quantity — how much of that layer is still in stock;
- Remaining Value — for what amount.
Filter by the product in question and look at the “Unit Value” column. A layer with a price that stands out from the rest is visible immediately — nothing needs to be calculated.
The “Valuation” menu is only visible with inventory valuation rights. If it is not there, ask an administrator or an accountant to look at it together with you.
Screen 4. See where the system has already put the difference
This is the least obvious screen — and it is exactly the one that saves you from correcting twice.
When you post a vendor return, Odoo sees that the goods went back at one price while stock was relieved at another. It does not leave the difference hanging: it creates a separate, third journal entry that closes the balance with the vendor and throws the difference into cost of goods sold as a reduction. In other words, that month's expenses become artificially smaller.
You can look at it in Accounting → Reporting → General Ledger, filtered by the return date. You will see three entries instead of two: the receipt, the return, and a third, automatic one for the difference.
How to fix it
After the fourth screen it becomes clear that there is less work than it seems at first glance.
| Where it is stuck | What to do about it |
|---|---|
| What is still in stock | A valuation adjustment on that specific layer |
| What has already been sold | Nothing — the system levels this out itself as the goods are sold |
| The balance with the vendor | Nothing — closed automatically at the time of the return |
| The margin report | Rewrite the cost on the order lines |
Step 1. Adjusting the remaining stock
In the same Inventory → Reporting → Valuation list find the line of the erroneous receipt and tick the checkbox on that exact line. The Actions menu will appear at the top — choose Adjust Valuation.
In the dialog fill in:
- Added value — the negative difference: how much excess value has to be taken off what is still in stock. That is the remaining quantity multiplied by the difference between the wrong price and the correct one.
- Counterpart Account — the cost of goods sold account.
- Journal — the inventory journal, filled in automatically.
What not to do
The most natural reaction is to see August's inflated expenses and “correct” them with another manual entry. Do not do this. Those expenses are already balanced by the reduction of July's expenses that the system posted on its own (screen 4). A second entry will make the correction twice, and an amount with no basis will hang on the variance account.
It is easy to check: add up July's decrease and August's increase for this product. If the total comes out roughly equal to what is still in stock, everything is already balanced and no extra entries are needed.
Step 2. Cost on the sales orders
The last one — and purely a reporting matter. On every order line the cost is stored as of the moment of delivery, and it will not recalculate itself after the valuation adjustment. Which means the margin report will keep showing the old figures.
It is corrected by hand: open the order, enable the Cost column (as on screen 2) and type in the correct value. If there are a dozen such lines, it is five minutes of work. If there are hundreds, it is easier to ask a developer to update them in bulk.
This action has no effect on accounting at all: it changes only what the margin report shows.
How to avoid it
An erroneous receipt must not be corrected with a vendor return. The quantity will balance out, while the difference in value will stay in stock and surface weeks later.
What to do in reality depends on how far the paperwork has gone.
The mistake was spotted before the receipt was validated
The simplest case: correct the price on the purchase order and only then validate the receipt. The layer will be created with the correct value straight away, and there will be nothing to fix.
The receipt is validated, the vendor bill is not yet
This is the best of the “late” scenarios, and it is the one people forget about most often. Just post the vendor bill with the correct price. Odoo will create a correction layer for the difference and fix the valuation itself — and it will split it correctly: what is still in stock gets recalculated in the remaining value, and what has already been sold goes to the expense account.
This is a standard mechanism that does all the work for you. Nothing has to be calculated by hand — it is enough to enter the price the vendor actually charged into the bill.
The bill is already posted or cancelled
Then there will be no automation, and what is left is what is described above: a valuation adjustment on the remaining stock. This is exactly the situation my case fell into — the bill had been cancelled as a duplicate, and the fix had to be done by hand.
Frequently asked questions
Is this an Odoo bug?
No. This is FIFO logic by design: removal goes from the oldest layer, and a vendor return is no exception. Other systems with layered valuation behave the same way.
Will a physical inventory help?
No. A physical inventory levels quantity, and here the quantity is already correct. The problem is purely in the value of the layer.
How quickly does it surface?
It depends on turnover. The slower the product sells, the longer the erroneous layer waits its turn — and the harder it is afterwards to connect a margin dip with an event three months old.
What if the product has already sold out completely?
Then nothing at all is needed. The decrease in expenses in the month of the return and the increase in the months of the sales have fully cancelled each other out — the books are already correct. The only thing left is perhaps to rewrite the cost on the order lines so that the margin report for past weeks is not misleading.
Can this be automated?
Yes. Checking the whole database for anomalous layers can be set up as a separate report that emails a list of suspicious receipts once a week. That is a job for a developer, but a small one.
The moral here is not about Odoo. Inventory valuation works this way in any system: the cost queue lives its own life and does not remember which particular unit came in at which price. When you correct a mistake with a stock move, you correct the quantity. Value has to be corrected separately — and that is exactly what is easiest to forget.
Margin dropped and nothing had changed: the FIFO return trap