Spec (docs/reviewed/document-lifecycle.md): - C1: Correct GlManager::delete() description — reversal entry, not hard delete - C3: Clarify PO status 2/3 are derived (receipt_status), never stored - C4: Add invoice status=2 (Paid/Settled) to status table - C5: Add full Receipt/Payment Billing Note lifecycle section - C6: Add Quotation status table and transition rules - C7: Document soft-delete tombstone mechanism (company_id negation) Code (InvoiceManager.php): - C2: voidInvoice() now blocks on active credit notes, posted receipt billing notes, and posted payment billing notes in addition to the existing receipt/payment checks - M9: softDelete() skips assertPostingWindow for draft invoices (status=0) since drafts have no GL entry and no accounting impact Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7.4 KiB
Document Lifecycle And Status
Status Values
Document status is stored as an integer column on each transaction table. The meaning is consistent across most document types but not guaranteed to be identical everywhere — always read the manager before assuming.
Sales and Purchase Invoices (td_invoice, all doc_type values)
| Status | Meaning |
|---|---|
0 |
Draft |
1 |
Issued / Active |
2 |
Paid / Settled (fully allocated) |
4 |
Void |
doc_type values on td_invoice: invoice, credit_note, purchase_invoice, supplier_credit_note.
Voiding is handled by a shared status endpoint. When a document is voided, InvoiceManager calls GlManager::delete() to remove any posted GL entry. Void is blocked if settlement documents (receipts or payments) are still allocated to the invoice — those must be voided first.
Receipts and Payments (td_receipt, td_payment)
| Status | Meaning |
|---|---|
1 |
Active / posted business document |
4 |
Void |
Void calls GlManager::delete() through ReceiptManager and PaymentManager respectively. Posting window is enforced on both save and void.
Receipt Billing Notes and Payment Billing Notes (td_receipt_billing, td_payment_billing)
Billing notes group one or more invoices into a single collection document sent to a customer (receipt billing) or supplier (payment billing). Each line in td_receipt_billing_item / td_payment_billing_item references a td_invoice.id and carries an allocated amount.
| Status | Meaning |
|---|---|
1 |
Open — no receipts / payments posted against it yet |
2 |
Fully received / paid — allocated receipts or payments equal the billing total |
3 |
Partially received / paid — some but not all receipts or payments have been posted |
4 |
Void |
Status is never set manually — ReceiptBillingManager::refreshBillingStatus() and PaymentBillingManager::refreshBillingStatus() derive and write the correct value after each receipt or payment change.
Void is blocked if any posted receipt (td_receipt.status = 1) or payment (td_payment.status = 1) is allocated to the billing note — those must be voided first. Voiding a billing note does not void the invoices it references. Invoices cannot be voided while an active billing note (status ∈ {1, 2, 3}) references them.
Quotations (td_quotation)
| Status | Meaning |
|---|---|
-1 |
Cancelled |
0 |
Draft |
1 |
Sent |
2 |
Accepted |
3 |
Rejected |
5 |
Converted |
Status transitions are enforced by QuotationManager::updateStatus():
0 → 1(send),1 → 2(accept),1 → 3(reject)1 / 2 / 3 → 0(reopen) — blocked if any item already hasconverted_qty > 00 / 1 → -1(cancel)
status = 5 is never set via updateStatus(). It is written automatically by QuotationManager::incrementConvertedQty() once all line items are fully converted to a sales order. OrderManager::linkQuotationToOrder() calls this after a successful order creation. When the linked order is later cancelled or soft-deleted, OrderManager reverts the quotation back to status = 2 (Accepted).
Sales Orders (td_order)
| Status | Meaning |
|---|---|
-2 |
Pending warehouse selection or source completion |
-1 |
Cancelled |
0 |
Draft |
1 |
Confirmed |
Confirmation can create stock-out rows. Cancellation reverses stock-out rows where required and is blocked when active invoices or returns exist.
Purchase Orders (td_purchase_order)
| Status | Meaning |
|---|---|
-2 |
Pending warehouse selection or source completion |
-1 |
Cancelled |
0 |
Draft |
1 |
Confirmed |
td_purchase_order.status only ever holds the values above. Partially received and fully received are not stored statuses — they are derived on the fly by PurchaseOrderManager::deriveReceiptStatus() into a virtual receipt_status field returned alongside the PO row. This derived value reflects the ratio of approved stock-in rows to ordered quantities and is never written back to td_purchase_order.status.
Receiving creates stock-in rows. Close and cancel rules apply based on receiving progress and approved stock-in rows.
Purchase Requests (td_purchase_request)
| Status | Meaning |
|---|---|
-1 |
Cancelled |
0 |
Draft |
1 |
Submitted |
2 |
Approved / reopened after conversion rollback |
3 |
Rejected |
5 |
Converted |
Converted requests have a linked purchase order.
Returns and Supplier Returns (td_return, td_supplier_return)
| Status | Meaning |
|---|---|
-1 |
Cancelled |
0 |
Draft |
1 |
Confirmed |
Confirmation creates stock-in (sales return) or stock-out (supplier return) rows. Conversion creates a credit note or supplier credit note document via InvoiceManager.
Stock Entries (td_stock_<warehouse_id>)
Draft stock entries await approval. Approved entries update warehouse balances. Approved entries cannot be deleted without reversal; editing and deletion are also blocked outside the posting window.
Void And Soft Delete Strategy
The current strategy (as of 2026-05-19):
- Draft or unposted documents may be soft-deleted or cancelled freely.
- Posted or issued documents use a void workflow. The source document is preserved in audit history. Void removes the linked GL entry through
GlManager::delete()and enforces the posting window. - Master data is soft-deleted by setting
status = 0. Deletion is blocked when active documents reference the record.
Void convention uses status = 4 for td_invoice document types (covers invoice, credit_note, purchase_invoice, supplier_credit_note). Credit notes are considered inactive at status = 4, not status = -1.
Soft-delete tombstone mechanism
Draft transaction documents (invoices, receipts, payments, billing notes, orders, quotations, returns, etc.) are soft-deleted by negating company_id on the header row and all child item rows:
UPDATE td_invoice SET company_id = company_id * -1 WHERE id = :id AND company_id = :cid
UPDATE td_invoice_item SET company_id = company_id * -1 WHERE invoice_id = :id AND company_id = :cid
This is the tombstone convention used by every softDelete() method across all managers. Because every read query filters WHERE company_id = :cid (positive), tombstoned rows are automatically excluded without any additional flag. Any query that omits the company_id filter will expose tombstoned rows — treat this filter as mandatory.
deleted_at, deleted_by, and delete_reason audit columns are not yet implemented.
GL Versioning
When a source document is posted to the GL more than once (re-post after edit), GlManager::replace() is called instead of GlManager::post(). The replace path:
- Reads the current
td_glandtd_gl_itemrows. - Appends a snapshot of the current lines as JSON into the
td_gl.historycolumn. - Increments
td_gl.current_version. - Writes the new lines into
td_gl_item.
This means the full posting history for a document is preserved in td_gl.history as a versioned JSON array. GlManager::delete() creates a reversal journal entry (debits and credits swapped, lines prefixed with VOID:) linked to the original source_id, then tombstones the original td_gl row. No rows are physically deleted — the full audit trail is preserved in the database.
Manual journal entries use GlManager::postManual() and GlManager::replaceManual() which follow the same versioning pattern.