Purchasing
Read Conventions first.
Document chain: Purchase Request → Purchase Order → GRN (Goods Receipt PO) → A/P Invoice, and separately
Goods Return Request → Goods Return → A/P Debit Memo. Each consuming line carries a typed source_*_line_id
FK (ON DELETE RESTRICT) and bumps a counter + row_status on the source line.
purchase_requests
Near-clone of inventory_transfer_requests, with no pricing.
id,business_id,branch_idNOT NULL,warehouse_id→ warehouses NULLdocument_no- UNIQUE (business_id, document_no)request_datedate DEFAULT current_date;remarksstatusDEFAULT ‘open’ - CHECK IN (‘open’,‘closed’,‘cancelled’)closed_at/closed_by,cancelled_at/cancelled_by,created_by,created_at
purchase_request_lines
id, purchase_request_id CASCADE, item_id → items NOT NULL; quantity numeric(14,3) NOT NULL; uom_id;
required_date NULL; ordered_quantity numeric(14,3) DEFAULT 0 (consumed by POs); row_status CHECK IN
(‘open’,‘closed’); created_at
purchase_orders
Pricing + tax + snapshotted supplier fields + persisted rollup totals.
id,business_id,document_no- UNIQUE (business_id, document_no)branch_idNOT NULL,warehouse_idNOT NULL,supplier_id→ business_partners NOT NULLsupplier_name,supplier_address,supplier_tax_id,supplier_contact_person- snapshots at creationorder_date,due_date,delivery_date- datepayment_term_id→ payment_terms;price_list_id→ price_listsline_typeDEFAULT ‘item’ - CHECK IN (‘item’,‘service’)header_discount_typeCHECK IN (‘percentage’,‘fixed’);header_discount_valuenumeric(14,2)convenience_fee_id→ convenience_feessscl_liableboolean;sscl_amountnumeric(14,2)- Totals (numeric(14,2) DEFAULT 0):
items_subtotal,header_discount_amount,taxable_value(pre-tax base after header discount),tax_total(recomputed on that reduced base),charges_total,charges_tax_amount,convenience_fee_amount,convenience_fee_tax_amount,grand_total statusDEFAULT ‘draft’ - CHECK IN (‘draft’,‘pending_approval’,‘posted’,‘closed’,‘cancelled’); auto-transitions to ‘closed’ when every line’srow_status= ‘closed’remarks,posted_at/posted_by,cancelled_at/cancelled_by,created_by,created_at
purchase_order_lines
id,purchase_order_idCASCADEitem_id→ items NULL (service lines have none);description- CHECK (item_id IS NOT NULL OR description <> ‘’)quantitynumeric(14,3) NOT NULL;uom_id;unit_pricenumeric(14,2)discount_typeCHECK IN (‘percentage’,‘fixed’);discount_valuenumeric(14,2)tax_group_id/tax_component_id- CHECK: never both settax_amount,line_totalnumeric(14,2) -line_totalis tax-inclusivedelivery_datesource_request_line_id→ purchase_request_lines RESTRICTreceived_quantitynumeric(14,3) DEFAULT 0 - written by GRN postingrow_statusCHECK IN (‘open’,‘closed’,‘cancelled’);created_at
purchase_order_charges
Ad-hoc additional charges (freight, delivery…), repeatable per PO.
id,purchase_order_idCASCADE,descriptionNOT NULL,amountnumeric(14,2)is_taxableboolean DEFAULT false (deliberate default-off)tax_group_id/tax_component_id- CHECK: not both;tax_amountnumeric(14,2);created_at
goods_receipt_notes
“Goods Receipt PO” - receiving specifically against a Purchase Order. Distinct from the plain goods_receipts inventory document.
id,business_id,document_no- UNIQUE (business_id, document_no)branch_idNOT NULL,warehouse_idNOT NULLpurchase_order_id→ purchase_orders NULLreceipt_date,reference,notesstatusCHECK IN (‘draft’,‘posted’,‘cancelled’)items_subtotal,discount_total,tax_total,grand_totalnumeric(14,2)posted_at/posted_by,cancelled_at/cancelled_by,created_by,created_at
goods_receipt_note_lines
Carries the PO’s pricing/tax/discount as a read-only snapshot at copy time - never live-recomputed.
id,goods_receipt_note_idCASCADE,item_idNOT NULL,gl_account_idNOT NULLquantitynumeric(14,3),unit_costnumeric(12,2)discount_typeDEFAULT ‘percentage’,discount_valuenumeric(14,2)tax_group_id,tax_component_id,tax_code,tax_amountnumeric(14,2)net_amountnumeric(14,2) - tax-exclusive; this is what GL/stock costing usesline_totalnumeric(14,2) - tax-inclusive (net_amount + tax_amount)batch_number,expiry_date,serial_numbers,admission_date- batch/serial capturebin_location_id→ bin_locations,uom_id,cost_center_id→ cost_centerssource_po_line_id→ purchase_order_lines RESTRICTreturned_quantitynumeric(14,3) DEFAULT 0;created_at
Goods Return chain
- goods_return_requests -
id,business_id,document_noUNIQUE (business_id, document_no);branch_idNOT NULL,warehouse_idNULL,request_date,remarks;statusCHECK IN (‘open’,‘closed’,‘cancelled’); closed/cancelled stamps,created_by,created_at - goods_return_request_lines -
id,goods_return_request_idCASCADE;source_receipt_line_id→ goods_receipt_note_lines RESTRICT nullable (a return with no GRN history);item_idNOT NULL,quantity,uom_id,reason;returned_quantitynumeric(14,3) DEFAULT 0;row_statusCHECK IN (‘open’,‘closed’);created_at - goods_returns - posting one reverses the GRN’s stock/GL movement.
id,business_id,document_noUNIQUE (business_id, document_no);branch_idNOT NULL,warehouse_idNOT NULL;goods_return_request_idNULL;supplier_idNULL,supplier_namesnapshot;return_date,reference,notes;statusCHECK IN (‘draft’,‘posted’,‘cancelled’); posted/cancelled stamps,created_by,created_at - goods_return_lines -
id,goods_return_idCASCADE,item_idNOT NULL,gl_account_idNOT NULL;quantity,unit_cost;discount_type,discount_valuenumeric(14,4),tax_group_id,tax_component_id,tax_code,tax_amount,net_amount,line_total;uom_id,cost_center_id;source_request_line_id→ goods_return_request_lines RESTRICT;created_at. Note:batch_number/expiry_date/serial_numberswere dropped here - batch resolution goes viastock_movement_batch_allocations
ap_invoices
A/P Invoice and A/P Down Payment Invoice, both copied from a posted GRN - this is what records the real payable (a GRN only moves stock + a GR-IR clearing balance).
id,business_id,document_no- UNIQUE (business_id, document_no)document_subtypeNOT NULL - CHECK IN (‘invoice’,‘down_payment’)branch_idNOT NULL,warehouse_idnullable (expense-only invoices)source_grn_id→ goods_receipt_notes NULLsupplier_id,supplier_namesnapshotinvoice_date,reference,notespayment_term_id→ payment_termsdown_payment_percentnumeric(5,2) NULL;down_payment_amountnumeric(14,2)statusDEFAULT ‘initiated’ - CHECK IN (‘initiated’,‘pending_approval’,‘posted’,‘cancelled’)items_subtotal,discount_total,tax_total,grand_totalnumeric(14,2)paid_amountnumeric(14,2) DEFAULT 0;payment_statusCHECK IN (‘unpaid’,‘partial’,‘paid’)- Multi-currency:
currency_id,exchange_rate,foreign_grand_total,foreign_paid_amountDEFAULT 0 - existing amount columns stay base-currency terminal_id→ pos_terminals NULL - only POS-originated rows set itposted_at/posted_by,cancelled_at/cancelled_by,created_by,created_at- Index
(business_id, invoice_date)
ap_invoice_lines
id, ap_invoice_id CASCADE; item_id NULL (expense lines); description DEFAULT ‘’; gl_account_id
NOT NULL, quantity, unit_cost; discount_type, discount_value, tax_group_id, tax_component_id,
tax_code, tax_amount, net_amount, line_total; uom_id, cost_center_id; source_grn_line_id →
goods_receipt_note_lines RESTRICT; created_at
A/P Debit Memo (from Goods Return)
- ap_debit_memos - copied from a posted Goods Return.
id,business_id,document_noUNIQUE (business_id, document_no);branch_idNOT NULL,warehouse_idNOT NULL;source_goods_return_idNULL;supplier_id,supplier_name,memo_date,reference,notes;statusCHECK IN (‘draft’,‘posted’,‘cancelled’); posted/cancelled stamps,created_by,created_at - ap_debit_memo_lines -
id,ap_debit_memo_idCASCADE,item_idNOT NULL,gl_account_idNOT NULL;quantity,unit_cost,discount_type,discount_value,tax_group_id,tax_component_id,tax_code,tax_amount,net_amount,line_total;uom_id,cost_center_id;source_goods_return_line_id→ goods_return_lines RESTRICT;created_at
Landed Costs
Freight/duty allocated onto a posted GRN’s lines, raising unit cost.
- landed_costs -
id,business_id,document_noUNIQUE (business_id, document_no);branch_idNOT NULL,grn_id→ goods_receipt_notes NOT NULL;cost_dateNOT NULL;allocation_methodDEFAULT ‘value’ CHECK IN (‘value’,‘quantity’);statusDEFAULT ‘posted’ CHECK IN (‘posted’,‘cancelled’);remarks,cancelled_at/cancelled_by,created_byNOT NULL,created_at - landed_cost_charges -
id,landed_cost_idCASCADE,descriptionNOT NULL,amountnumeric(14,2) NOT NULL,source_account_id→ gl_accounts NOT NULL - landed_cost_allocations -
id,landed_cost_idCASCADE,grn_line_id→ goods_receipt_note_lines NOT NULL,item_idNOT NULL,gl_account_idNOT NULL,quantity,allocated_amountnumeric(14,2),unit_cost_increasenumeric(14,4)