By Isaac Muthui, CEO, Tamalaki Business Network Ltd. Originally published at factoraerp.com.
In most inventory systems, packaging is a boring item: a SKU, a unit of measure, a reorder level. In a food factory that is a modelling mistake. The film, bottle or tub that touches the food is closer to an ingredient than to stationery. It needs a specification, evidence that it is safe for contact with this food, an intake check, a lot number, and a link to every batch it was used on.
This post walks through how we think about modelling food-contact packaging in an ERP for food and agro manufacturers in East Africa. The operational checklist behind it is in the original article.
What the domain rules say
The requirements read like a spec:
- Codex CXC 1-1969: packaging "should be safe and suitable for food use" and should not pose a threat to the safety and suitability of food "under the specified conditions of storage and use"; reusable packaging should be "suitably durable, easy to clean and, where necessary, to disinfect" (13.2.9); packaging materials need separate storage from non-food chemicals (9.2.7). Source
- EAS 39:2001: food-contact packaging "shall be free of contamination, shall not taint the food" (4.3.10), and packaging "shall not be used as a waste receptacle" (10.8.4). Source
- Kenya Food Hygiene Regulations (Cap. 254): packaging processes and materials must not transmit contaminants to the products (reg. 14(f)), and each container must be marked to identify the producing factory and the lot (reg. 14(d)). Source
- EU Regulation 1935/2004, Article 17 (a benchmark, not Kenyan law): businesses must be able to identify the businesses from which and to which food contact materials are supplied. Source
Read as a developer, that is four requirements: conditions of use are part of the item's definition, suitability evidence is data with a scope, packaging is lot-tracked, and packaging lots belong in batch genealogy.
1. "Conditions of use" belong on a versioned specification
A packaging item is not suitable in general. It is suitable for a food, at a fill temperature, for a shelf-life. So the specification is a first-class, versioned record, and the item points at the approved version.
create table packaging_spec (
id bigserial primary key,
item_id bigint not null references item(id),
version int not null,
material text not null, -- e.g. 'PET', 'LDPE film 60 micron'
food_contact boolean not null, -- primary packaging?
intended_use jsonb not null, -- {"foods":["juice"],"max_fill_c":85,"shelf_life_days":180}
status text not null check (status in ('draft','approved','superseded')),
approved_by bigint references app_user(id),
approved_at timestamptz,
unique (item_id, version)
);
A new supplier or material is a new version, not an edit. The old version stays, because batches made last month were packed against it.
2. Suitability evidence is data with a scope and a date
The supplier's food-contact statement is not a PDF in someone's inbox. It is a record linked to the supplier, the item and the spec version it covers, with the conditions it covers and an expiry or review date.
create table packaging_evidence (
id bigserial primary key,
supplier_id bigint not null references supplier(id),
spec_id bigint not null references packaging_spec(id),
kind text not null, -- 'food_contact_statement','test_report'
covers jsonb not null, -- foods / temperatures the document actually covers
document_url text not null,
valid_until date
);
The useful check is a comparison, not a checkbox: does evidence.covers include everything in spec.intended_use? If the spec says hot fill at 85 °C and the statement only covers ambient use, that is a gap the system can show before the first delivery, not after the first complaint.
3. Receiving creates a lot, and the lot starts on hold
Packaging is received exactly like an ingredient: the supplier's lot number is mandatory, the intake inspection is a record, and the lot cannot be issued until it passes.
type PackagingLot = {
id: string;
itemId: string;
specId: string; // the spec version in force at receipt
supplierId: string;
supplierLot: string; // required: no supplier lot, no receipt
qty: number;
status: 'on_hold' | 'released' | 'rejected';
};
function canIssue(lot: PackagingLot, evidenceOk: boolean): boolean {
return lot.status === 'released' && evidenceOk;
}
Making supplierLot required is the cheapest traceability control you will ever ship. Making on_hold the default status means a forgotten inspection fails safe.
4. Packaging lots are inputs in batch genealogy
When a roll of film or a pallet of bottles is issued to the line, the issue is a consumption against the production batch, the same table you use for ingredient lots.
-- Which batches did a faulty film lot go on, and where were they shipped?
select b.batch_code, d.customer_id, d.shipped_at
from batch_input bi
join batch b on b.id = bi.batch_id
left join dispatch d on d.batch_id = b.id
where bi.input_lot_id = :packaging_lot_id;
If packaging is not in batch_input, this query has no answer and the factory holds every batch from the period instead of the affected ones.
5. Reconciliation is a cheap anomaly detector
Issued minus used minus wasted minus returned should be close to zero for each run. Store the four numbers per batch and flag the gap. A large positive gap means packaging went somewhere it should not: into another product, into the bin, or into someone's car. A negative gap means a count or an issue was missed.
Design notes
- Do not let packaging be a "consumable" type. Consumables are often excluded from lot tracking by default. Food-contact packaging must not be.
- Model reusable containers separately. Crates and returnable bottles are assets that cycle: they need a cleaning event and an inspection before each reuse, not a receipt.
- Keep the spec version on the lot. When the spec changes, lots received under the old version still need to be judged against what was approved when they arrived.
If you build software for factories, how do you treat packaging in your data model: as stock, as an ingredient, or as something else?
Top comments (0)