Payment-gated delivery¶
For prepayment sales flows — a proforma invoice, a down payment, or any other “pay first, ship later” arrangement — goods must not physically leave the warehouse before the money has actually arrived. Payment-gated delivery holds the outgoing delivery of a sale order for as long as the order is unpaid: it blocks both the stock reservation and the Validate button, then releases automatically the moment every invoice is settled. A privileged user can still override the hold and ship early, and the override is fully traced in the chatter.
The reusable “On Hold” primitive¶
Payment-gated delivery is built on top of a generic hold mechanism that lives in the base
eyssen_stock module and is available on every transfer, independent of sales or payments:
a Hold toggle and a read-only Hold Reason field on stock.picking;
a red On Hold ribbon on the transfer form whenever Hold is ticked, plus On Hold / Off Hold filters in the transfers list search panel; and
the methods
action_set_hold(reason)andaction_release_hold(), which set or clear Hold and Hold Reason together.
Setting Hold immediately unreserves the transfer’s stock moves; releasing it re-triggers
reservation (_action_assign) so the transfer picks back up available stock automatically. Any
module — not only sale_stock_payment_gate — can put a transfer on hold this way and expect
reservation to stay off until it is released.
Note
By itself, the generic Hold only stops reservation. It does not block Validate — that additional, stricter guard is specific to the payment gate described below.
The payment-term flag¶
Whether an order is gated at all is decided by its payment term. The Require payment before delivery checkbox, added to the payment term form, marks a term as “pay before you ship.” Orders using a payment term with the checkbox off — for example a cash-on-delivery term — are never held. Orders with no payment term at all are likewise never held.
Holding the delivery on order confirmation¶
When a sale order is confirmed, and its payment term requires payment before delivery, every not-yet-done, not-yet-cancelled outgoing picking of that order is put on hold in one step: Hold is ticked, Hold Reason is set to “Awaiting payment for delivery,” and the gate’s own Held for payment field is set as well. If the order is already fully paid at confirmation time (for example, a proforma or down payment was settled before confirmation), no hold is applied and the delivery proceeds normally.
The hard gate: no reservation, no validation¶
A payment-held delivery is blocked twice over, so it cannot slip through either the automatic or the manual path:
No reservation. The gate overrides stock-move reservation so that any move belonging to a payment-held picking is skipped, both when Odoo auto-assigns stock on confirmation and when a user clicks Check Availability. This check is on the gate’s own Held for payment flag, so it holds even if the generic Hold toggle is separately cleared.
No validation. Clicking Validate on a payment-held delivery raises an error instead of shipping the goods, pointing the user to the Release for delivery button described below.
Important
Directly writing Held for payment = unchecked on a picking is blocked for any user who is not in the Deliver without payment group. Only that group’s Release for delivery button (or a privileged/system write) can lift the gate — a plain write() from another flow cannot silently bypass it.
Automatic release when the order is paid¶
An order counts as delivery-paid once every posted customer invoice linked to it reaches payment
status Paid or In Payment. A base.automation rule watches the
Payment Status field on customer invoices (account.move, out_invoice) and, on
every create or write that touches it, re-evaluates all outgoing, not-done pickings of the related
orders:
if the order is now fully paid, any picking still on payment hold is released — Held for payment, Hold and Hold Reason are all cleared, and reservation runs again automatically; and
if the order is not fully paid and a picking is not currently held (and was not manually released — see below), it is re-held with the reason “Payment reversed - delivery re-blocked.”
Note
In Payment counts as paid because it represents a full receipt for which reconciliation with the bank statement is still pending. A partial payment never releases the hold. The gate assumes the linked invoice(s) cover the full order amount — with a native partial down payment, a fully-paid partial invoice would release the whole delivery early; that scenario is explicitly out of scope.
Re-blocking on payment reversal¶
Because the same automation re-evaluates the order on every change to Payment Status, reversing a payment (for example, unreconciling or cancelling a payment so the invoice drops back to Not Paid) automatically re-applies the hold to any not-done delivery of that order — as long as the delivery was not manually released by a privileged user.
Tip
The gate reads only the standard account.move / payment_state fields on posted customer
invoices. It does not depend on any localization: the same logic releases the delivery whether the
order was settled through a Hungarian NAV proforma / díjbekérő invoice, a regular down-payment
invoice, or any other module that eventually posts and pays a customer invoice against the order.
Manual override: releasing a delivery before payment¶
Sometimes a delivery genuinely needs to ship before the order is fully paid (goodwill shipment, trusted customer, manual arrangement). The Release for delivery button on the transfer’s header does this:
it is visible only when the picking is on payment hold, and only to users in the Deliver without payment security group;
clicking it asks for confirmation, then clears Held for payment, Hold and Hold Reason, and sets Manually released for delivery to keep track of the override; and
a chatter message records who released the delivery and when.
A delivery flagged Manually released for delivery is not re-held by the automatic sync described above, even if the order later turns out to be unpaid again — the manual decision is respected until someone re-applies a hold by hand.
Backorders inherit the hold¶
If a payment-held (or previously released) delivery is only partially shipped and Odoo creates a backorder for the remainder, the backorder is re-evaluated against the same rule as the original delivery: when the order still requires payment before delivery and is not yet fully paid, the new backorder transfer is put on hold again, with the same “Awaiting payment for delivery” reason — even if the original transfer had been manually released.
Order-level visibility¶
The sale order form shows a warning banner — “Delivery is held until this order is paid.” — above the order sheet whenever any of the order’s deliveries is currently on payment hold. This is driven by the computed Delivery held for payment field, so sales staff see the gated status at a glance without opening the transfer.
Configuration¶
Make sure
sale_stock_payment_gateis installed. It automatically pulls ineyssen_stock,sale_stock,accountandbase_automation.Go to , open (or create) the payment term used for prepaid/proforma sales, and tick Require payment before delivery. Leave the checkbox unticked on terms such as cash-on-delivery.
Grant the Deliver without payment group (category Inventory) to the users who are allowed to override the gate — for example warehouse supervisors — via . This group also needs ordinary inventory read/write access to actually see and release the transfer.
No further setup is required: the base.automation rule and the hold/release logic are active as
soon as the module is installed and at least one payment term has the flag enabled.
Usage¶
Confirm a sale order that uses a gated payment term. Its outgoing delivery is created and immediately put on hold, unless the order is already fully paid.
The order form shows the Delivery is held until this order is paid. banner, and the delivery transfer shows the On Hold ribbon with Hold Reason = “Awaiting payment for delivery.” Check Availability does not reserve stock, and Validate refuses to run.
Create and post the customer invoice (proforma, down payment, or regular invoice) for the order, then register the full payment. As soon as the invoice’s Payment Status reaches Paid or In Payment, the automation releases the delivery: the hold is cleared and stock is reserved again.
Validate the now-released delivery normally.
If early shipment is unavoidable, a user with the Deliver without payment group can open the held transfer and click Release for delivery instead of waiting for payment. The override is logged in the chatter and survives later payment-status changes; if a backorder is later created for the remaining quantity on that same order, it is re-held automatically unless the order has become fully paid by then.
Scope and modules¶
The feature is delivered by two modules:
eyssen_stock— the generic, reusable Hold / Hold Reason primitive on transfers (ribbon, toggle, search filters, and the reservation-blocking/re-reserval logic), used here as the base building block.sale_stock_payment_gate— the payment gate itself: the Require payment before delivery payment-term flag, the hold-on-confirm logic, the hard reservation/validation block, thebase.automation-driven auto-release and re-block on payment changes, the Deliver without payment group with its Release for delivery button and chatter trail, the backorder re-hold, and the order-level warning banner.