1.2.0¶
Released: staging (target promotion: TBD)
Compared to 1.1.0.
Highlights¶
- Sales safety, Tier 1: invoices created from an order now consume that order's stock reservations and skip them in the negative-stock check, closing the "own reservation blocks own invoice" loophole from the inventory tier.
- Partial-fulfillment guard: an order line can no longer be invoiced beyond
orderedqty across one or many invoices —OrderLineOverInvoicedErrorrolls the transaction back. - Direct quote → invoice traceability: invoice lines now carry
quote_detail_idwhen the conversion skipped the order step.
Added¶
Sales safety, Tier 1 (feature page)¶
order_details.quantity_invoiced(DOUBLE PRECISION, defaults0). Maintained byInvoiceEventHandler._recompute_quantities_invoicedon every invoice insert / update / delete; equals the sum of linked invoice-line quantities.invoice_details.quote_detail_idnullable FK toquote_details.id. Populated automatically byInvoiceFactory.from_quotefor direct quote→invoice conversions.OrderLineOverInvoicedErrorinapp/core/exceptions.py; raised when a recompute would push a line pastorder_detail.quantity.InvoiceEventDTO.order_idfield, surfaced byInvoiceFactory.to_event, so the handler can target the right order's reservations without an extra DB hit.
Schema (selected)¶
InvoiceDetail.quoteDetailId: UUID,InvoiceDetailInput.quoteDetailId: UUID— direct quote line traceability.OrderDetail.quantityInvoiced: Decimal!— surfaces the running total for UI / reporting.
Changed¶
InvoiceEventHandler.post_insertandpost_update:- Pass
reservation_source=("order", order_id)toupdate_quantitywhen the invoice is order-backed, so the negative-stock guard excludes that order's own active reservations from the availability check. - Call
StockReservationRepository.consume_for_source("order", order_id)after the deduction loop to flip the order's ACTIVE reservations to CONSUMED. - Call
_recompute_quantities_invoiced(event)to enforce the partial-fulfillment guard. RaisesOrderLineOverInvoicedErrorand rolls back the transaction if any order line would be over-invoiced. InvoiceEventHandler.post_delete(previously a no-op) now recomputesquantity_invoicedso deleted invoices release their share of the order line.
Fixed¶
- An invoice generated from an order with active stock reservations would previously be blocked by the negative-stock guard because the order's own reservation counted against availability. The new
reservation_sourceexclusion fixes this.
Migrations¶
Run on top of 1.1.0:
add_sales_safety_tier_1— addsorder_details.quantity_invoicedandinvoice_details.quote_detail_idplus its FK.
Apply with task migrate-dev against staging, or task migrate-prod once promoted. No backfill required — quantity_invoiced defaults to 0 (consistent with current state) and the partial-fulfillment guard only kicks in on new invoice inserts/updates.
Frontend impact¶
- Order line UI — surface
orderDetail.quantityInvoicednext to ordered qty so reps see remaining-to-invoice at a glance. Disable the "convert to invoice" CTA whenquantityInvoiced == quantity. - Invoice creation error handling — handle
OrderLineOverInvoicedErroras a per-line validation; the error message includes the order line and the remaining-to-invoice amount. - Quote → invoice path — the new
quoteDetailIdis populated automatically. UI can use it to show "from quote line X" badges on invoice details, or leave it server-side only. - No breaking changes — all schema additions are nullable / defaulted.
Versioning notes¶
- MINOR bump: new feature, additive GraphQL schema, no breaking changes.
- Bumped via
uv run python scripts/bump_version.py 1.2.0 --changelog-stub. - Deploy order: migration first, then app. The partial-fulfillment guard activates on the first invoice mutation after deploy; historical data is untouched.