Skip to content

Rework the acquisition order-line editing model (commitment vs. live target) + order claims/reminders #4152

Description

@PascalRepond

Rework the acquisition order-line editing model (commitment vs. live target) + order claims/reminders

Pre-requisites:

  • Confirm the workflow with the acquisition module users
  • Develop in pair

Context

AcqOrderLine.extended_validation (and the DisallowedByOrderStatus permission) gate line editing on the aggregate order status. This has two weaknesses:

  1. It ignores the line's own received state. While the order is partially_received, a line that is itself already fully received can still be edited. The guard only ever looks at the order status, never the line's received_quantity.
  2. There is no quantity >= received_quantity invariant. Nothing prevents editing the ordered quantity below what has already been received. When that happens, unreceived_quantity goes negative and the line can never compute back to received — a stuck partially_received.

This surfaced while fixing a related bug (cancelling the last non-received line of a partially received order — see the accompanying fix(acquisition): allow cancelling the last non-received order line). That fix is a targeted patch; the underlying model needs a decision.

The modelling question

There's a legitimate need behind the looseness: e.g. "ordered 3 copies, received 1, the book is now unavailable — finalize the line with 1." Two coherent ways to support it, mutually exclusive:

Model A — order line is a live target, editable to match reality

Editing the ordered quantity down to what was received reconciles the line and lets the order close.

  • Pros: lighter to build (mostly add the quantity >= received_quantity invariant + gate editing on the line's own receipts instead of the order status).
  • Cons: rewrites what was actually ordered (loses audit trail), and editing the amount after receipts churns the encumbrance/expenditure math.

Model B — order line is a commitment; receipts are the actuals; short lines are closed (recommended)

  • Order line quantity = what was ordered, effectively frozen once the order is sent.
  • Receipt lines = what actually came.
  • A new terminal state closed/complete, distinct from cancelled (nothing received / abandoned) and received (everything received), meaning "no more expected." The 3-ordered/1-received line becomes closed; the order can reach received; the trail keeps "ordered 3, received 1, closed the remaining 2".
  • A librarian action "close line / close order" (a.k.a. "cancel remaining copies").

Recommendation: Model B. It preserves history, keeps the encumbrance math honest (no retro-editing of a committed amount), and matches how mature acquisition modules behave.

Proposed permission/validation redesign (Model B)

Gate on the two axes that actually matter, in one place, instead of the aggregate order status:

  • Editing line fields (quantity/amount/document): allowed only while the order is pending (not yet sent).
  • Cancel / un-cancel / close: gated on the line's own received_quantity, not the order status:
    • received_quantity == 0 → may cancel/un-cancel;
    • 0 < received_quantity < quantity → may close (finalize the shortfall);
    • fully received → terminal.
  • A dedicated cancel/close endpoint or action rather than routing through the generic update (avoids the double-commit/re-validate footgun that generated the recent bug series).

Dependency: order claims / reminders

A future order-reminder ("claim") feature — remind the vendor about unreceived items — depends on this model:

  • A claim is the query "which lines are still outstanding and overdue": status ordered/partially_received, not cancelled, not closed, with quantity − received_quantity > 0, and order_date older than a claim delay.
  • Model B's closed state is exactly what removes abandoned shortfalls from the claimable set, so reminders don't nag the vendor for items the library has given up on. With a frozen ordered quantity, "outstanding since date X" is a stable fact.
  • Model A would need a closed-like concept reintroduced just to make claims sane.

Additions the claim feature needs (orthogonal to A/B, easier on B):

  • an expected/claim date on the order, or a per-vendor claim-delay config;
  • a new NotificationType (e.g. ACQUISITION_ORDER_CLAIM) dispatched like the existing send_order notification, listing outstanding lines;
  • claim tracking (count + dates), mirroring the existing serial-issue claim_notifications pattern at the order-line level.

Suggested sequencing

  1. Decide Model A vs. B (recommend B).
  2. Implement the editing/cancel/close permission+validation redesign + the quantity >= received_quantity invariant.
  3. Build order claims/reminders on top, using closed to scope the outstanding set.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    Inbox

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions