Most of the samples a lab handles pass through the same story:
booked, received, tested, verified, published. What happens next
was, until now, either invented per-lab or written down outside
of SENAITE. Two recent PRs in senaite.core add proper endings.
A dispose workflow, opt-in
senaite.core#2970
introduces a real dispose transition and a disposed state.
Turn on the Dispose workflow toggle in the setup
(dispose_workflow_enabled, off by default) and every
post-receipt state gets a Dispose action: sample_received,
to_be_verified, to_be_preserved, verified, published,
rejected, invalid, and even cancelled. So the sample that
sat cancelled in the fridge for a year can still be recorded as
disposed of, with actor, date, and reason.
The dispose_samples form asks for a comment, pre-filled with a
sensible default. An alert viewlet on the sample view carries the
disposal metadata forever. A guard blocks disposal while an
analysis is still on a worksheet. Partition-primary rules apply:
dispose a primary and its partitions follow; dispose the last
partition and the primary auto-disposes.
A new permission, senaite.core: Transition: Dispose Sample, is
granted to LabManager and Manager. An upgrade step registers the
permission, state, and transition; nothing is user-visible until
the toggle flips.
Dispatch, now optional and locking
senaite.core#2971
does the parallel move for the existing dispatch flow. The
dispatch transition and the dispatched review-state tab are
gated by Enable the Sample Dispatch workflow. For existing
installs an upgrade step turns dispatch on so nothing breaks; new
installs start it off.
More importantly, dispatching now locks the sample’s analyses. Before this PR, a dispatched sample was recognisable only through its review state; the JSON API could still write results on it because analysis edit permissions are governed by the analysis state, not the sample state. That gap is closed.
The reusable locking mechanism
The interesting part is what both features share. Instead of a
one-off “dispatched analyses are locked” rule, the PR introduces
a marker interface ILockingState and a new analysis state
called locked. IDisposed and IDispatched both inherit from
ILockingState. The analysis lock guard reads:
ILockingState.providedBy(sample)
That’s it. Any future sample state that wants the same behavior,
say a hypothetical IStored in senaite.storage, just inherits
ILockingState and gets analysis locking for free.
Locking is careful about what it touches. Analyses already in a
terminal state (cancelled, rejected, retracted) are left
alone, so disposing a cancelled sample never resurrects its
analyses into locked and their is_active status stays
truthful. The locked state has a single hidden, automatic
unlock transition; its after_unlock event rolls each analysis
back to the exact status it held before the lock. Restore a
sample and the analyses come back with their original state
intact, no bookkeeping in the caller.
What this means for a lab
Two immediate wins.
First, the end of a sample’s life is now first-class. If your retention policy says “hold for two years then dispose”, the disposal is a transition with an actor, a date, and a reason, on the sample itself. Auditors get one place to look.
Second, downstream integrations can trust a sample’s terminal state. Once dispatched or disposed, no adapter, no admin script, no JSON API call will silently mutate an analysis result. The locking is enforced in the workflow layer, above whatever code tries to write.
Both toggles ship off by default. If your lab does not need them, the day-to-day flow does not change. If it does, they slot in without touching custom code.