By Isaac Muthui, CEO, Tamalaki Business Network Ltd. Originally published at factoraerp.com.
Most manufacturing systems store a pack's declared quantity as a label field and nothing more. Whether the packs actually contain that quantity lives on a clipboard next to the filler, or in a checkweigher that nobody exports from.
For food and agro manufacturers that gap matters. In Kenya, the Weights and Measures (Sale and Labelling of Goods) Rules deem a quantity declaration correct only if two things hold for a tested sample (rule 14): no pack is deficient by more than a set tolerance (10% for bottles of 250 ml or 250 g or less, 3% of the marked quantity otherwise), and the sample average is at least the marked quantity. The international reference, OIML R 87:2016, is built on the same two ideas: an average requirement plus limits on individual short packs.
If your data model only stores "net weight: 500 g", you cannot prove either test passed, and you cannot see what overfill is costing. This piece is for developers modelling quality and production in manufacturing ERPs or MES tools. The factory-floor checklist behind it is on the FactoraERP blog (canonical link above). It is a modelling sketch, not legal advice.
The domain objects you need
-
SkuQuantitySpec:
sku_id,declared_qty,uom(g|kg|ml|l),basis(weight|capacity),tolerance_rule(pct_3|bottle_small_pct_10),fill_target,effective_from. Version it; never edit in place. -
TareProfile:
sku_id,packaging_lot_id(film roll, bag grade, bottle mould),empty_pack_weights[],avg_tare,measured_by,measured_at. The Rules' Tenth Schedule describes averaging ten empty containers; enforcecount(empty_pack_weights) >= 10. -
ScaleCheck:
scale_id,test_weight_nominal,reading,passed,checked_by,checked_at. A sample taken on a scale without a passing check for the shift is invalid. -
LineBatch:
line_id,sku_id,production_lot_id,window_start,window_end. The Rules define a batch at the end of a packing line as the line's maximum hourly output, so a one-hour window is a natural default. -
WeightSample:
line_batch_id,filler_head,gross_weight,tare_profile_id,net_weight(derived),sampled_at,sampled_by,scale_id. -
BatchVerdict:
line_batch_id,n,avg_net,min_net,avg_passed,individual_passed,verdict(pass|fail),spec_version.
Invariants worth enforcing
-
Net is always derived.
net_weight = gross_weight - tare_profile.avg_tare. Never accept a typed-in net figure; you lose the ability to recompute when a tare turns out wrong. -
Tare follows packaging lots. When a new
packaging_lot_idis issued to the line, block sampling until aTareProfileexists for it. -
No sample without a scale check.
exists(ScaleCheck where scale_id = s and passed and checked_at >= shift_start). -
Two tests, both stored.
avg_passed = avg_net >= declared_qtyindividual_passed = min_net >= declared_qty * (1 - tolerance)-
verdict = passonly if both are true. Store both booleans, not only the verdict, so reports can show which test failed.
-
Every head is sampled. For multi-head fillers, a
BatchVerdictcannot be computed until eachfiller_headhas at least one sample in the window. - Spec version is frozen on the verdict. If the fill target or tolerance changes mid-shift, earlier verdicts keep the spec they were judged against.
-
Fail means hold. A
failverdict automatically creates a hold on the production lot output since the last passingLineBatch. Release requires a re-check record.
A minimal state machine for a LineBatch
LineBatch: open -> sampling -> judged(pass) -> closed
|
+-> judged(fail) -> held -> resorted -> rechecked -> closed
-
open -> sampling: window starts; scale check and tare profile present. -
sampling -> judged: minimum sample count reached across all heads; computeBatchVerdict. -
judged(fail) -> held: create hold on affected output, notify QA. -
held -> resorted: 100% check-weigh results recorded, rejects reworked or downgraded. -
resorted -> rechecked -> closed: a fresh passing sample after the fix.
Schema sketch
create table weight_sample (
id bigserial primary key,
line_batch_id bigint not null references line_batch(id),
filler_head smallint not null,
scale_id bigint not null,
tare_profile_id bigint not null references tare_profile(id),
tare numeric(12,3) not null, -- copied from tare_profile at insert
gross_weight numeric(12,3) not null,
net_weight numeric(12,3) generated always as (gross_weight - tare) stored,
sampled_by bigint not null,
sampled_at timestamptz not null
);
create table batch_verdict (
line_batch_id bigint primary key references line_batch(id),
spec_version int not null,
n int not null,
avg_net numeric(12,3) not null,
min_net numeric(12,3) not null,
avg_passed boolean not null,
individual_passed boolean not null,
verdict text not null check (verdict in ('pass','fail'))
);
(A Postgres generated column cannot reference another table, which is why tare is copied onto the row at insert time. That is a feature, not a workaround: it freezes the tare that was actually used for that sample.)
Checkweigher integration
If the line has an inline checkweigher, treat its stream as a second source rather than the only one:
- Ingest per-pack gross weights with a timestamp and head ID, aggregate them into the same
LineBatchwindows, and run the same two tests. - Keep manual hourly samples as an independent check. A checkweigher drifting out of calibration should show up as a disagreement between the two sources.
- Store raw readings for a limited period and aggregates for longer; the verdicts and their inputs are the records people will ask for.
Reporting that pays off
Once samples persist with a spec version, two reports become trivial:
- Compliance: verdicts by line, SKU and shift, with which test failed.
-
Give-away:
avg_net - declared_qtyby SKU, line and shift, multiplied by output and material cost. That number usually gets a production manager's attention faster than any compliance argument.
Records
Section 47 of Kenya's Weights and Measures Act provides a defence for a person who proves they took all reasonable precautions and exercised all due diligence. Whatever your users' legal position, design these tables for retention and audit trails rather than updates and deletes: a verdict that can be silently edited is not evidence of anything.
The full operations checklist, with FAQ, is here: Net Quantity Checklist.
Top comments (0)