DEV Community

Isaac Mwendwa Muthui
Isaac Mwendwa Muthui

Posted on Originally published at factoraerp.com

Model Pack-Weight Checks So Short Fills Cannot Hide: SkuQuantitySpec, TareProfile, WeightSample and the Two-Test Rule

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; enforce count(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

  1. 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.
  2. Tare follows packaging lots. When a new packaging_lot_id is issued to the line, block sampling until a TareProfile exists for it.
  3. No sample without a scale check. exists(ScaleCheck where scale_id = s and passed and checked_at >= shift_start).
  4. Two tests, both stored.
    • avg_passed = avg_net >= declared_qty
    • individual_passed = min_net >= declared_qty * (1 - tolerance)
    • verdict = pass only if both are true. Store both booleans, not only the verdict, so reports can show which test failed.
  5. Every head is sampled. For multi-head fillers, a BatchVerdict cannot be computed until each filler_head has at least one sample in the window.
  6. 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.
  7. Fail means hold. A fail verdict automatically creates a hold on the production lot output since the last passing LineBatch. 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
Enter fullscreen mode Exit fullscreen mode
  • open -> sampling: window starts; scale check and tare profile present.
  • sampling -> judged: minimum sample count reached across all heads; compute BatchVerdict.
  • 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'))
);
Enter fullscreen mode Exit fullscreen mode

(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 LineBatch windows, 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_qty by 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)