Skip to content

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 ordered qty across one or many invoices — OrderLineOverInvoicedError rolls the transaction back.
  • Direct quote → invoice traceability: invoice lines now carry quote_detail_id when the conversion skipped the order step.

Added

Sales safety, Tier 1 (feature page)

  • order_details.quantity_invoiced (DOUBLE PRECISION, defaults 0). Maintained by InvoiceEventHandler._recompute_quantities_invoiced on every invoice insert / update / delete; equals the sum of linked invoice-line quantities.
  • invoice_details.quote_detail_id nullable FK to quote_details.id. Populated automatically by InvoiceFactory.from_quote for direct quote→invoice conversions.
  • OrderLineOverInvoicedError in app/core/exceptions.py; raised when a recompute would push a line past order_detail.quantity.
  • InvoiceEventDTO.order_id field, surfaced by InvoiceFactory.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_insert and post_update:
  • Pass reservation_source=("order", order_id) to update_quantity when 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. Raises OrderLineOverInvoicedError and rolls back the transaction if any order line would be over-invoiced.
  • InvoiceEventHandler.post_delete (previously a no-op) now recomputes quantity_invoiced so 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_source exclusion fixes this.

Migrations

Run on top of 1.1.0:

  • add_sales_safety_tier_1 — adds order_details.quantity_invoiced and invoice_details.quote_detail_id plus 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.quantityInvoiced next to ordered qty so reps see remaining-to-invoice at a glance. Disable the "convert to invoice" CTA when quantityInvoiced == quantity.
  • Invoice creation error handling — handle OrderLineOverInvoicedError as a per-line validation; the error message includes the order line and the remaining-to-invoice amount.
  • Quote → invoice path — the new quoteDetailId is 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.