Files
wms-app/docs/reviewed/document-lifecycle.md
T
Thanakorn SandClaude Sonnet 4.6 cb36d3b8fd Document lifecycle review: spec updates and C2/M9 code fixes
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>
2026-05-26 16:23:46 +07:00

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 has converted_qty > 0
  • 0 / 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:

  1. Reads the current td_gl and td_gl_item rows.
  2. Appends a snapshot of the current lines as JSON into the td_gl.history column.
  3. Increments td_gl.current_version.
  4. 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.