Key takeaways
MRO purchasing runs on four records, and each asserts something the other three cannot. A requisition says somebody asked, a purchase order says the company committed, a goods receipt says goods arrived, an invoice says a supplier wants money.
Four people write them at four moments, and that separation is the point. The model earns its keep when the four disagree, because it can say which one is wrong.
| Record | What it asserts |
|---|---|
| Requisition | Somebody inside the plant asked to buy something |
| Purchase order | The company committed to a named supplier |
| Goods receipt | Goods physically reached the storeroom |
| Invoice | The supplier says you owe this amount |
The storeroom is where maintenance meets finance, and this join is the commonest place a plant loses the true cost of a repair. A part paid for but never tied to the job that needed it lands on the storeroom, not on the machine.
Most setups collapse at least one pair to save a screen.
| Collapse | What you lose |
|---|---|
| Requisition into order | Refused and cancelled asks leave no row at all |
| Order into receipt | No open balance, so partial deliveries cannot be tracked |
| Receipt into invoice | Stock is booked from the bill, not from what arrived |
| Invoice into order | Every invoice matches by construction, so price drift is invisible |
Match an invoice against the order alone and a short delivery is paid in full, because the order is exactly what the supplier billed against.
On the three lines worked through below, the supplier billed €2,802.00 against orders worth €2,732.00, while goods supported only €2,640.00. That is €162.00 of over-billing, 6.1% above the payable amount.
The conceptual difference between the two documents belongs to our guide on work order vs purchase order. The storeroom itself, the part master and the stock policy sit in benefits of spare parts management.
What a missing part costs in downtime minutes is worked out in spare parts availability and downtime, and is not recomputed here. Reorder point and safety stock arithmetic belongs to safety stock vs reorder point, and is not re-derived here either.
This page adds the records themselves: the fields, the tolerance fields that decide whether an invoice is paid, and the two clocks that measure a supplier. The wider record set around it sits in our CMMS data model, and the ask that starts many purchases in the work request data model.
A requisition is an internal ask with no supplier attached yet. It exists so the decision to spend is recorded separately from the decision about who to spend it with.
Keep it as its own record and refused asks survive. Fold it into the purchase order and every rejected line disappears, because an order never raised leaves nothing behind.
| Field | Type | Why |
|---|---|---|
| requisition_ | id | Mandatory. Own key, quotable before a supplier exists |
| storeroom_ | id | Mandatory. Which store will hold or issue the parts |
| requested_ | id | Mandatory. Employee record, never a typed name |
| requested_ | timestamp | Mandatory. Server time, starts the purchasing clock |
| need_ | date | Mandatory. When the plant needs it in its hand |
| purpose | enum | Mandatory. Stock, work order, project, emergency |
| work_ | id | Mandatory when purpose is work order |
| cost_ | id | Mandatory. Who carries the spend if nothing is issued |
| approval_ | enum | Mandatory. Draft, submitted, approved, rejected, cancelled |
| approved_ | id | Mandatory on approval. A person, never a role |
| approved_ | timestamp | Mandatory on approval. Stops the approval clock |
| currency | char3 | Mandatory. Fixed at the header, never per line |
need_by_date is the date the plant needs the part in its hand. It is not a delivery date, which is a supplier's opinion about shipping.
work_order_id is what makes the true cost of a repair possible. Parts are one half of that cost and labour is the other, specified in our maintenance labour data model.
Lines are approved and refused one at a time, so each needs its own key and status. A requisition with nine lines can be approved on seven, and a header status cannot express that.
| Field | Type | Why |
|---|---|---|
| req_ | id | Mandatory. Own key, because lines are decided one by one |
| part_ | id | Optional. Points at the catalogue row when the part exists |
| free_ | text | Mandatory when part_ |
| requested_ | decimal | Mandatory. Counted in the ordering unit |
| requested_ | enum | Mandatory. The unit the number above is counted in |
| est_ | decimal | Optional. What the requester expects to pay |
| suggested_ | id | Optional. A hint for the buyer, never a commitment |
| line_ | date | Optional. Overrides the header date for this line |
| line_ | enum | Mandatory. Open, converted, rejected, cancelled |
| po_ | id | Optional. Filled when the line becomes an order line |
| rejection_ | enum | Mandatory when the line is rejected |
A purchase order is the first record a third party can hold you to. Everything on it is a promise, so everything on it is frozen when it is sent.
| Field | Type | Why |
|---|---|---|
| po_ | id | Mandatory. The number the supplier quotes back to you |
| supplier_ | id | Mandatory. A supplier record, never typed text |
| po_ | enum | Mandatory. Draft, sent, acknowledged, part received, closed |
| ordered_ | id | Mandatory. The buyer who committed the company |
| po_ | timestamp | Mandatory. Starts the measured lead time |
| currency | char3 | Mandatory. Copied onto the order, never read live |
| payment_ | enum | Mandatory. Frozen at the moment the order is sent |
| delivery_ | id | Mandatory. The site and the door, not the head office |
| incoterm | enum | Optional. Decides who pays freight and who carries risk |
| freight_ | decimal | Optional. On the order, so the invoice can match it |
po_sent_at makes lead time measurable and is the field most often left empty. An order created one day and sent three days later carries a three day hole nobody will see again.
| Field | Type | Why |
|---|---|---|
| po_ | id | Mandatory. Own key, the unit of matching and receiving |
| part_ | id | Mandatory once the line may be received into stock |
| order_ | decimal | Mandatory. Counted in order_ |
| order_ | enum | Mandatory. Box, drum, reel, metre, piece |
| unit_ | decimal | Mandatory. Price per order_ |
| conversion_ | decimal | Mandatory. Stock units inside one order unit |
| requested_ | date | Mandatory. The date you asked the supplier for |
| confirmed_ | date | Mandatory once acknowledged. The date they promised |
| received_ | decimal | Mandatory. A running total, written only by receipts |
| invoiced_ | decimal | Mandatory. A running total, written only by invoices |
| line_ | enum | Mandatory. Open, part received, received, closed, cancelled |
| req_ | id | Optional. Points back at the ask that caused the line |
received_qty and invoiced_qty are totals maintained by the receipt and invoice rows, never typed. If a person can edit either, the match is a formality.
confirmed_date is the supplier's answer and requested_date is your ask. Storing only one is the commonest reason two honest people report different on-time figures from the same data.
The receipt is the only record written by somebody who touched the goods. That makes it the anchor, and it is why an invoice should never be matched without one.
| Field | Type | Why |
|---|---|---|
| receipt_ | id | Mandatory. Own key, one row per delivery per line |
| po_ | id | Mandatory. Which commitment this delivery answers |
| received_ | decimal | Mandatory. What was counted, not what the note claims |
| receipt_ | enum | Mandatory. The unit counted at the door |
| received_ | timestamp | Mandatory. Server time, stops the lead time clock |
| received_ | id | Mandatory. A person, never a store or a department |
| delivery_ | text | Mandatory. The supplier's own reference for the drop |
| condition_ | enum | Mandatory. Good, damaged, wrong item, short packed |
| batch_ | text | Optional. Mandatory for parts with a shelf life |
| put_ | id | Mandatory. The bin, so the part can be found again |
| over_ | boolean | Mandatory. Set when the line balance is exceeded |
| receipt_ | enum | Mandatory. Posted, held, returned, reversed |
condition_code earns its place the first time a pallet arrives damaged. Received and rejected is a different fact from not received, and only one of them leaves the supplier still owing you goods.
The supplier record holds two kinds of field. One set is policy copied onto an order, the other is history you measure the supplier by.
| Field | Type | Why |
|---|---|---|
| supplier_ | id | Mandatory. Own key, stable across name changes |
| supplier_ | text | Mandatory. The legal name, not the rep's name |
| supplier_ | enum | Mandatory. Approved, on hold, blocked, retired |
| category | enum | Mandatory. Bearings, electrical, lubricants, consumables |
| default_ | char3 | Mandatory. Copied onto an order, never read live |
| payment_ | enum | Mandatory. The default the buyer may override |
| quoted_ | int | Mandatory. What they claim, beside what you measure |
| min_ | decimal | Optional. Why small asks get batched into one order |
| tax_ | text | Mandatory. Needed before any invoice can be posted |
| approved_ | id | Mandatory. Who let this supplier into the plant |
| approved_ | date | Optional. Forces a review rather than silent permanence |
| contact_ | text | Optional. Where the buyer finds the account manager |
quoted_lead_days is a claim, not a measurement. It belongs on the record next to the measured figure, so the gap is visible to anyone who opens the page.
The match is two comparisons, not one. Quantity is matched receipt against invoice, price is matched order against invoice, and a system doing only one of them passes half the errors you care about.
Tolerances exist because an exact match is too brittle to run a plant on.
| Field | What it controls |
|---|---|
| price_ | How far the invoiced unit price may drift, as a percentage |
| price_ | A hard cap in money, so a percentage cannot pass a large sum |
| over_ | How much more than ordered the store may accept |
| invoice_ | How far the invoiced quantity may exceed the received quantity |
| tolerance_ | Whether the test runs per line or per invoice |
| variance_ | Where an accepted difference posts instead of blocking |
The pair of price tolerances matters more than either alone. A drift of 3.0% is trivial on a €40.00 line and is €900.00 on a €30,000.00 line, so the percentage needs a money cap beside it.
Set the rule so a variance passes only inside both limits. tolerance_scope is the quiet one: testing per invoice lets a credit on one line hide an overcharge on another.
Three purchase order lines from one week, with what was ordered and what was billed. Money is rounded to the nearest cent, half up, and every percentage to one decimal place.
| Line | Ordered | Invoiced |
|---|---|---|
| A bearings | 40 at €18.50 | 40 at €18.50 |
| B filters | 12 at €46.00 | 12 at €46.00 |
| C hose | 200 at €7.20 | 200 at €7.55 |
| Line | Received | Outcome |
|---|---|---|
| A bearings | 40 | Passes, €740.00 released |
| B filters | 10 | Blocked on quantity |
| C hose | 200 | Blocked on price |
Line A passes. Ordered 40 at €18.50 is €740.00, received 40, invoiced 40 at the same price, so both tests return zero and €740.00 is released.
Line B is a short delivery billed in full. Ten cartridges arrived and twelve were invoiced, so the invoice exceeds the receipt by 2 units worth €92.00.
With invoice_qty_tolerance at zero units the line blocks, and the supportable amount is 10 at €46.00 = €460.00. The €92.00 gap is 16.7% of the €552.00 ordered value, paid for goods not in the building.
Line C is a price variance. The order says €7.20 per metre and the invoice says €7.55, a drift of €0.35 per metre, which is 4.9%.
Across 200 metres that is €70.00. Against a price_tolerance_pct of 3.0% and a price_tolerance_abs of €50.00 the line fails both tests and blocks.
Change one number and the same machinery pays. At €7.35 per metre the drift is €0.15, which is 2.1% and €30.00 across the line, inside both limits.
It passes, and the €30.00 posts to the variance account instead of stopping the payment run.
Totalled, the three lines were ordered at €2,732.00 and invoiced at €2,802.00. Priced at the ordered price, the receipts support €2,640.00, so €162.00 of that invoice run has nothing behind it.
An MRO line rarely arrives once. The line carries a running balance, and that balance has to survive a delivery that overshoots it.
Take PO‑2026‑0412 line 10: 100 pieces of a V belt at €12.40 each, an ordered value of €1,240.00. Four deliveries arrived against it.
| Receipt | Qty | Running balance |
|---|---|---|
| GR‑0781 | 40 | 40 received, 60 open |
| GR‑0834 | 35 | 75 received, 25 open |
| GR‑0902 | 30 | 105 received, 5 over |
| GR‑0955 | 6 | 111 received, 11 over |
The third receipt takes the cumulative quantity past the ordered 100 to 105. One field decides whether the store may take it.
With over_receipt_pct at 5.0%, the allowance on a 100 piece line is 5 pieces, so 105 sits exactly at the cap and the receipt is accepted with the over-receipt recorded.
The fourth receipt breaks the rule. Six more pieces take the cumulative to 111, an over-receipt of 11 against an allowance of 5, so 6 pieces fall outside the tolerance and the receipt is held.
| Action | When it applies |
|---|---|
| Accept | Inside over_ |
| Accept and amend | Buyer raises order_ |
| Return | Outside tolerance, supplier collects at their own cost |
| Hold | Outside tolerance, goods quarantined with no stock value |
Now delete the rule and watch the money move. All 111 pieces post to stock at €12.40, which is €1,376.40 behind an order worth €1,240.00.
That is €136.40, or 11.0%, of stock with no order line under it. If the supplier bills for it you pay a quantity nobody approved, and if it never does, your stock account and your goods received not invoiced account disagree by that amount.
The quieter cost lasts longer. Those extra pieces make the item look better stocked than it was ever bought to be, which is a stock policy problem covered in spare parts inventory management.
Storerooms buy in boxes, drums and reels and issue in pieces, litres and metres. The conversion is a field, and when it is wrong the storeroom loses count without anyone making a mistake.
| Field | Why |
|---|---|
| order_ | The unit on the order and on the supplier price list |
| stock_ | The unit the balance is held in, and the one stock value uses |
| issue_ | The unit a work order consumes in |
| conversion_ | How many stock units sit inside one order unit |
| factor_ | The date it started, so old receipts keep their own factor |
| factor_ | Who set it: the supplier catalogue, a buyer or a stocktake |
factor_valid_from is the field nobody specifies until a supplier first changes a pack size. Without it, correcting today's factor silently rewrites the quantity of every receipt ever taken against that part.
A line covers 20 boxes of gloves at €54.00 a box, an ordered value of €1,080.00. A box holds 50 pairs, so the correct receipt is 1,000 pairs at €1,080.00 divided by 1,000 = €1.08 per pair.
The catalogue carries a conversion factor of 100 instead of 50. The receipt posts 20 times 100 = 2,000 pairs where 1,000 physically arrived.
| Figure | Correct | Booked |
|---|---|---|
| Pairs received | 1,000 | 2,000 |
| Value at €1.08 | €1,080.00 | €2,160.00 |
| Pairs on the shelf | 1,000 | 1,000 |
Valued at a standard cost of €1.08 per pair, the receipt books €2,160.00 against a purchase worth €1,080.00. The error invents €1,080.00, an overstatement of 100.0%.
A stocktake finds 1,000 pairs where the book says 2,000, a shortfall of 1,000 pairs and 50.0% of the book quantity. At 40 pairs a week, the phantom half is 25.0 weeks of cover that does not exist.
The min level never trips, nobody buys gloves, and the first person to notice is an operator at an empty shelf. Setting those levels properly is answered in safety stock vs reorder point.
Every supplier record carries a quoted lead time and almost none carry the measured one. They are different fields and both belong on the page.
| Measure | Formula |
|---|---|
| Requested lead time | requested_ |
| Promised lead time | confirmed_ |
| Actual lead time | final receipt date minus po_ |
| Variance to promise | final receipt date minus confirmed_ |
| Variance to request | final receipt date minus requested_ |
Each needs its denominator printed beside it, because the population is a choice and the choice moves the answer.
| Measure | Population it runs on |
|---|---|
| Actual lead time | Lines fully received in the period, dated at final receipt |
| On time to promise | The same lines, restricted to those with a confirmed date |
| On time to request | The same lines, restricted to those with a requested date |
| Open line age | Lines not yet fully received, measured to today |
Walk one line. PO‑2026‑0412 was sent on 2026‑03‑02, the plant asked for 2026‑03‑16, the supplier confirmed 2026‑03‑23, and the last piece landed on 2026‑03‑27.
Requested lead time is 14 days and promised lead time is 21 days, because the supplier answered with a date of its own. Actual lead time is 25 days.
Against the promise the line is 4 days late. Against the request it is 11 days late, and the supplier's catalogue lead time of 14 days was never once tested.
Storeroom STORE‑01, one calendar quarter, one plant. Every figure below is computed from the rows in these tables and nothing else.
| Outcome | Requisition lines |
|---|---|
| Converted to an order line | 196 |
| Rejected at approval | 18 |
| Cancelled by the requester | 12 |
| Still open at quarter end | 14 |
| Total raised | 240 |
Check the arithmetic first: 196 + 18 + 12 + 14 = 240. A quarter that does not reconcile to the raw count has rows in a state nobody agreed on.
Those 196 converted lines produced 180 purchase order lines, because 16 were added to orders that already existed. That is 196 divided by 180 = 1.09 requisition lines per order line.
Of the 180 order lines, 162 were fully received inside the quarter and 18 were still open. Those 162 lines took 219 goods receipts, which is 219 divided by 162 = 1.35 deliveries per line.
The invoice side closed 168 invoice lines: 141 matched clean, 14 blocked on quantity and 13 blocked on price. That is 83.9% clean, with 27 lines or 16.1% stopped by a tolerance.
The 162 received lines were ordered at €211,950.00 and received at €213,860.00. The €1,910.00 difference is over-receipts accepted inside tolerance, 0.9% of the ordered value.
Of that received value, €148,300.00 was issued against work orders inside the quarter and €65,560.00 was still on the shelf. That is 69.3% consumed and 30.7% held.
The 162 received lines came from five suppliers. Both methods run on these same rows and the same definition of on time: received on or before the confirmed date.
| Supplier | Lines | On time |
|---|---|---|
| S1 bearings | 46 | 41 |
| S2 filters | 52 | 44 |
| S3 electrical | 24 | 15 |
| S4 drives | 18 | 11 |
| S5 consumables | 22 | 13 |
| Total | 162 | 124 |
| Supplier | Value € | On time € |
|---|---|---|
| S1 bearings | 38,400.00 | 34,100.00 |
| S2 filters | 22,750.00 | 19,300.00 |
| S3 electrical | 61,200.00 | 28,900.00 |
| S4 drives | 74,500.00 | 39,600.00 |
| S5 consumables | 17,010.00 | 9,700.00 |
| Total | 213,860.00 | 131,600.00 |
Method A counts lines. 124 of 162 arrived on or before the confirmed date, so on-time delivery is 76.5%.
Method B counts money. €131,600.00 of €213,860.00 of received value arrived on time, so on-time delivery is 61.5%.
The gap is 15.0 percentage points out of the same 162 rows. In counts, the 38 late lines are 23.5% of the lines and carry €82,260.00, which is 38.5% of the value.
| Supplier | By line | By value |
|---|---|---|
| S1 bearings | 89.1% | 88.8% |
| S2 filters | 84.6% | 84.8% |
| S3 electrical | 62.5% | 47.2% |
| S4 drives | 61.1% | 53.2% |
| S5 consumables | 59.1% | 57.0% |
S2, the filter supplier, looks excellent and is cheap line for line. It carries 52 of the 162 lines, 32.1% of them, and only 10.6% of the value.
S3, the electrical supplier, is the case a line count hides. It runs 62.5% on time by line and 47.2% by value, a drop of 15.3 percentage points inside its own rows.
That gap says its late deliveries are its expensive ones. A supplier punctual with fuses and late with a drive is a different risk from one uniformly 62.5% on time.
A simple average makes it worse again. The mean of the five by-line percentages is 71.3%, while the true combined figure is 124 divided by 162 = 76.5%.
Every figure so far measured each supplier against its own confirmed date. Re-measure the same 162 lines against the date the plant asked for and the 124 on-time lines become 103.
That is 63.6%, which is 12.9 percentage points below the 76.5% figure, on 21 lines where the supplier confirmed a later date than the one requested and then kept its own promise.
A supplier confirming three weeks against your two week ask is on time in its own terms and late in yours. Only storing both dates lets you say so.
The defence is one line in the report header naming the population and the clock. "On-time delivery, 162 lines fully received in the quarter, measured against the confirmed date" is reproducible, and "supplier on-time delivery 76.5%" is not.
Question five separates the tools. Any system shows an on-time percentage, and very few print the population and date field behind it (equipment maintenance software).
Fabrico is an OEE platform with a full CMMS built in, and its inventory module covers the storeroom half of this page rather than the purchasing half.
It holds a parts catalogue with min and max levels, records deliveries into the store, ties consumption to the work order that used the part, and supports stock-takes and a supplier listing.
Your team scans QR codes on parts and machines from the iOS, Android or web app, so what arrives and what is issued is recorded where it happens.
Approval workflows and the audit log record who approved what, and the analytics export to Excel, with a REST API, webhooks and SAP and other ERP connectors when the numbers have to meet your finance system.
Now the part that matters for this page, stated plainly. Fabrico records what arrives and what gets consumed, and the purchase order itself lives in your ERP or purchasing system.
Fabrico does not raise requisitions or purchase orders, does not run a three-way match against supplier invoices, and does not send anything to a supplier. The supplier listing is a list, with no messaging attached to it.
It does not reorder parts on its own, it does not plan or schedule production, and it has no predictive maintenance product. A work order is always created and confirmed by your team.
Everything else on this page is a specification to test a shortlist against rather than a feature list.
Want to see how received parts and work order consumption would look on your own storeroom? Book a 30 minute demo with a Fabrico consultant, no commitment, or contact us with your questions.
A requisition records the ask, a purchase order the commitment to a supplier, a goods receipt what arrived, and an invoice what the supplier says you owe. Four different people write them, which is what makes a disagreement detectable.
It is two comparisons, not one: invoiced quantity against received quantity, and invoiced price against the ordered price. A line passes only when both sit inside the tolerance fields, and otherwise it blocks before payment.
price_tolerance_pct and price_tolerance_abs govern price drift, invoice_qty_tolerance governs billing more than arrived, and tolerance_scope decides whether the test runs per line or per invoice. A variance should pass only inside both price limits.
Store an over_receipt_pct on the line and an action of accept, accept and amend, return or hold. Without that rule, 111 pieces against a 100 piece line at €12.40 book €1,376.40 of stock value behind a €1,240.00 order.
Actual lead time is the final receipt date minus po_sent_at, compared against the confirmed date for the supplier's promise and the requested date for what the plant needed. Print the population beside it, because excluding open lines flatters every supplier.