================================================================================ MPUMALANGA PVC - INVOICES MODULE ================================================================================ Last Updated: 2026-03-26 Path: /app/invoices/ ================================================================================ WHAT THIS SECTION DOES ----------------------- Manages tax invoices issued to clients. An invoice is either created manually (add_invoices.php) or converted from a quote (convert_to_invoice.php). Invoices can be further actioned: converted to a work schedule, converted to supplier orders, or exported as a PDF. Credit notes linked to an invoice are visible inline on the edit page. DATABASE TABLES --------------- invoices: record_id int - Primary key (NO AUTO_INCREMENT) client_id int - FK to clients order_type text - SUPPLY / SUPPLY & INSTALL / SUPPLY & DELIVERY user_id int - FK to users status text - e.g. PENDING PAYMENT, OPENED date_time_created varchar(50) - Timestamp terms text subject text notes text area text quote_id int - FK to quotes (0 if created directly) invoice_number int - Incrementing number additional_delivery_details text invoice_list: record_id int stock_id int - FK to stock qty text price text - Currency string invoice_id int - FK to invoices size_m text pannels text ================================================================================ FILES ================================================================================ home.php -------- Lists all invoices with an Edit button per row. Uses: new table("invoices") + add_action_button("edit_invoices.php") Note: Title says "JOB CARDS" - minor bug. -------------------------------------------------------------------------------- add_invoices.php ---------------- What it does: Manual invoice creation form. Same structure as add_quotes.php but creates an invoice directly (not from a quote). How it works: - Loads the last invoice to calculate the next invoice_number. - get_stock_datalist("stock_list") and get_clients_datalist("clients_list"). - Notes and terms fields pre-fill with standard invoice text. - Same dynamic line items table as quotes. - Saves to save_invoices.php. JS Functions: Same as add_quotes.php (get_client_name, add_row, delete_row, get_unit_of_measure, calculateRow, calculateTotals, save, change_to_currency). Note: VAT row is hidden (display:none) but code still calculates it. Net total effectively equals subtotal because VAT multiplier is 0 in calculateTotals. -------------------------------------------------------------------------------- save_invoices.php ----------------- What it does: Inserts a new invoice header and all line items, redirects to home.php. How it works: - Sanitises subject, note, terms, area (strips quotes and double-quotes). - Gets last invoice_number + 1. - Inserts into invoices with quote_id=0 (not from a quote). - Loops over $_POST['stock_code'], looks up each by code, inserts invoice_list. - Redirects to home.php. -------------------------------------------------------------------------------- edit_invoices.php ----------------- What it does: Full edit form for an existing invoice. Shows all fields, line items, and any credit notes linked to this invoice. How it works: - Loads invoice by $_GET['record_id']. - If invoice has a linked quote (quote_id != 0), shows a button linking back to the quote's edit page. - Loads all invoice_list rows and renders them as editable table rows. - After the invoice line items, loads any credit_notes linked to this invoice and renders each as a read-only row showing the credit note total. Each credit note row has an OPEN button to go to edit_c_notes.php with a from_invoice=1 flag so SAVE returns here. - Has a USER CREATED dropdown and DATE CREATED readonly field. - Saves to update_invoices.php. - Shows SAVE button. Additional functions available (called from quick bar or invoice detail actions): go_to(url) - Simple window.location.href redirect. JS Functions (same as add_invoices.php plus calculateRow on load for all rows). -------------------------------------------------------------------------------- update_invoices.php ------------------- What it does: Updates invoice header, deletes all invoice_list rows, re-inserts from POST. How it works: - Sanitises text fields. - Updates invoices table. - DELETEs all invoice_list rows for this invoice_id. - Loops over $_POST['stock_code'], skips empty entries, inserts back. - Redirects to home.php. -------------------------------------------------------------------------------- get_unit_of_measure.ajax.php ---------------------------- AJAX endpoint. Receives stock record_id via POST. Returns: "unit_of_measure,retail_price" Used by: edit_invoices.php and edit_quotes.php when switching stock items. -------------------------------------------------------------------------------- invoice.pdf.php --------------- What it does: Generates a full Tax Invoice PDF for a given invoice (?record_id=X). PDF contents: - Company logo (left) and secondary logo (right). - "TAX INVOICE {invoice_number}" title. - Client details (left) and company details (right). - Date, order type. - Subject block. - Area. - Line items table: Code, Description, UOM, Qty, Size, Panels, Amount, Total. - Any credit notes linked to this invoice are shown as deduction rows. - Subtotal and Net Total. - Payments Received, Balance Outstanding, Amount Due. - Bank details block. - Notes section. Note: VAT is 0 (commented out). Balance outstanding shown as R 0.00 (hardcoded bug - it should show invoice total minus payments, but instead shows 0). The "Amount Due" line does correctly calculate net_total - payments. -------------------------------------------------------------------------------- delivery.pdf.php ---------------- What it does: Generates a delivery note PDF directly from the invoice_list items. This is separate from the delivery_note module's PDF (which uses delivery_note_list). This one is a quick delivery note based purely on what is on the invoice. PDF contents: - "DELIVERY NOTE" title (not a tax invoice). - Client and company details. - Invoice number reference. - Delivery details block (if filled). - Line items table: Code, Description, Size, Panels. NOTE: Skips items with code "DLVR". - "RECEIVED IN GOOD CONDITION" statement with signature lines. -------------------------------------------------------------------------------- convert_to_order.php -------------------- What it does: Converts an invoice into a work schedule (order) record, then redirects to the work schedule edit page. Called via: convert_to_order.php?record_id={invoice_id} How it works: Step 1 - Check if order already exists: Queries orders WHERE invoice_id = record_id. If found, uses existing order_id. Step 2 - Create order if not found: Inserts into orders with date_created=NOW() and the invoice_id. Because orders table has no AUTO_INCREMENT, immediately SELECTs back: SELECT record_id FROM orders WHERE invoice_id = X ORDER BY record_id DESC LIMIT 1 This gives the real order_id. Step 3 - Rebuild order_list: DELETEs all existing order_list rows for this order. Re-inserts from invoice_list. Step 4 - Redirect: Goes to ../work_schedule/edit_work_schedule.php?record_id={order_id} KNOWN ISSUE (fixed 2026-03): Originally redirected to ../orders/edit_orders.php which does not exist. Fixed to go to the correct work_schedule path. -------------------------------------------------------------------------------- convert_to_supplier_orders.php ------------------------------- What it does: Converts an invoice into one or more supplier orders - one order per supplier of items on the invoice. Redirects to supplier_orders/home.php. Called via: convert_to_supplier_orders.php?record_id={invoice_id} How it works: Step 1 - Get distinct suppliers: Queries invoice_list JOIN stock to get DISTINCT supplier_id values. Step 2 - For each supplier: Checks if a supplier_order already exists for this invoice + supplier combo. If yes: uses the existing supplier_order_id. If no: Gets last order for this supplier and increments the number. Format: SUPPLIERCODE-N (e.g. EDPVC-3) Inserts into supplier_orders. Step 3 - Rebuild supplier_order_list: DELETEs existing list rows for this supplier_order. Re-inserts from invoice_list JOIN stock, filtered to this supplier, using stock.cost as the price. Step 4 - Redirect to supplier_orders/home.php. KNOWN BUG (fixed 2026-03): Original code used str_replace() + 1 on the order number string, which fails in PHP 8 because str_replace returns a string and string + int throws a TypeError. Fixed with (int) cast. NOTE ON SEPARATOR: This file uses dash (-) for new orders. The get_supplier_order.ajax.php file (used by the manual add form) uses underscore (_). This is an inconsistency - orders created via convert use dashes, orders created manually use underscores. ================================================================================ KNOWN ISSUES SUMMARY ================================================================================ 1. home.php title says "JOB CARDS". 2. invoice.pdf.php has hardcoded "R 0.00" for Balance Outstanding. 3. convert_to_supplier_orders uses dash (-) but manual add uses underscore (_) for order number format - inconsistency that can cause numbering to break. 4. No invoice status update flow (status stays as whatever it was set to). 5. SQL injection throughout. ================================================================================