diff --git a/app/assets/js/custom.js b/app/assets/js/custom.js index 3d7aea4..a86a945 100644 --- a/app/assets/js/custom.js +++ b/app/assets/js/custom.js @@ -581,9 +581,19 @@ function load_formula_options(select_id, document_type, selected_id, onLoaded) { }); } +// Line tax rate; derived from tax_amount / total_price when only the amount was stored +function line_tax_rate(item) { + var rate = parseFloat(item.tax_rate) || 0; + var amount = parseFloat(item.tax_amount) || 0; + var total = parseFloat(item.total_price) || 0; + if (rate === 0 && amount > 0 && total > 0) rate = round_dp(amount / total * 100, 2); + return rate; +} + +// Returns the request promise so callers can await the options before selecting a value function load_departments(select_id, selected_id) { var ctx = document.getElementById('session-context'); - ajax_request({ + return ajax_request({ url: server_url + 'accounting/api/engine/department.php', action: 'get', queueLock: false, diff --git a/app/assets/utils/classes/OrderManager.php b/app/assets/utils/classes/OrderManager.php index 37df2ec..2fdfeb5 100644 --- a/app/assets/utils/classes/OrderManager.php +++ b/app/assets/utils/classes/OrderManager.php @@ -260,7 +260,10 @@ class OrderManager { { $sth = $this->pdo->prepare( "SELECT o.*, - COALESCE(c.contact_name, '') AS contact_name + COALESCE(c.contact_name, '') AS contact_name, + (SELECT COUNT(*) FROM td_order_item i + WHERE i.company_id = o.company_id + AND i.order_id = o.id) AS item_count FROM td_order o LEFT JOIN md_contact c ON c.company_id = o.company_id @@ -493,7 +496,7 @@ class OrderManager { // Fetch existing row to check status and load log $sth = $this->pdo->prepare( - "SELECT status, `log` FROM td_order + "SELECT status, department_id, `log` FROM td_order WHERE company_id = :company_id AND id = :id" ); $sth->execute([':company_id' => $this->company_id, ':id' => $id]); @@ -502,6 +505,11 @@ class OrderManager { if (!$row) { throw new Exception("Order not found."); } + + // Pages without a department field (Revenue SO) must not wipe the stored one + $department_id = array_key_exists('department_id', $data) + ? (int)$data['department_id'] + : (int)$row['department_id']; $cur_status = (int)$row['status']; if ($cur_status !== 0 && $cur_status !== -2) { throw new Exception("Only draft or pending orders can be edited."); @@ -541,7 +549,7 @@ class OrderManager { WHERE id = :id AND company_id = :company_id" )->execute([ ':contact_id' => (int)($data['contact_id'] ?? 0), - ':department_id' => (int)($data['department_id'] ?? 0), + ':department_id' => $department_id, ':order_date' => $data['order_date'] ?? date('Y-m-d'), ':subtotal' => $subtotal, ':discount' => $discount, diff --git a/app/assets/utils/classes/PurchaseOrderManager.php b/app/assets/utils/classes/PurchaseOrderManager.php index f4d4918..43da110 100644 --- a/app/assets/utils/classes/PurchaseOrderManager.php +++ b/app/assets/utils/classes/PurchaseOrderManager.php @@ -194,7 +194,10 @@ class PurchaseOrderManager { { $sth = $this->pdo->prepare( "SELECT p.*, - COALESCE(c.contact_name, '') AS contact_name + COALESCE(c.contact_name, '') AS contact_name, + (SELECT COUNT(*) FROM td_purchase_order_item i + WHERE i.company_id = p.company_id + AND i.order_id = p.id) AS item_count FROM td_purchase_order p LEFT JOIN md_contact c ON c.company_id = p.company_id diff --git a/app/assets/utils/classes/PurchaseRequestManager.php b/app/assets/utils/classes/PurchaseRequestManager.php index 597d407..13a0a30 100644 --- a/app/assets/utils/classes/PurchaseRequestManager.php +++ b/app/assets/utils/classes/PurchaseRequestManager.php @@ -94,7 +94,10 @@ class PurchaseRequestManager WHERE p.company_id = r.company_id AND p.source = 'purchase_request' AND p.source_id = r.id - AND p.status != -1) AS linked_po_count + AND p.status != -1) AS linked_po_count, + (SELECT COUNT(*) FROM td_purchase_request_item i + WHERE i.company_id = r.company_id + AND i.request_id = r.id) AS item_count FROM td_purchase_request r LEFT JOIN md_contact c ON c.company_id = r.company_id AND c.id = r.contact_id @@ -166,6 +169,12 @@ class PurchaseRequestManager if (empty($items)) throw new Exception('At least one item is required.'); + $request_date = (string)($data['request_date'] ?? ''); + $required_date = (string)($data['required_date'] ?? ''); + if ($request_date !== '' && $required_date !== '' && $required_date < $request_date) { + throw new Exception('Required date cannot be earlier than the request date.'); + } + [$subtotal, $tax, $grand] = $this->computeTotals($items, $discount, $shipping_fee); if ($id === 0) { diff --git a/app/assets/utils/classes/QuotationManager.php b/app/assets/utils/classes/QuotationManager.php index 7dd4690..58e7dd6 100644 --- a/app/assets/utils/classes/QuotationManager.php +++ b/app/assets/utils/classes/QuotationManager.php @@ -89,7 +89,10 @@ class QuotationManager public function getList(): array { $sth = $this->pdo->prepare( - "SELECT q.*, COALESCE(c.contact_name, '') AS contact_name + "SELECT q.*, COALESCE(c.contact_name, '') AS contact_name, + (SELECT COUNT(*) FROM td_quotation_item i + WHERE i.company_id = q.company_id + AND i.quotation_id = q.id) AS item_count FROM td_quotation q LEFT JOIN md_contact c ON c.id = q.contact_id AND c.company_id = q.company_id @@ -173,6 +176,24 @@ class QuotationManager $items = $data['items'] ?? []; $discount = (float)($data['discount'] ?? 0); + $quotation_date = (string)($data['quotation_date'] ?? ''); + $valid_until = (string)($data['valid_until'] ?? ''); + if ((int)($data['contact_id'] ?? 0) <= 0) { + throw new Exception('Contact is required.'); + } + if ($quotation_date === '') { + throw new Exception('Quotation date is required.'); + } + if ($valid_until !== '' && $valid_until < $quotation_date) { + throw new Exception('Valid until cannot be earlier than the quotation date.'); + } + if ((int)($data['department_id'] ?? 0) <= 0) { + throw new Exception('Department is required.'); + } + if (empty($items)) { + throw new Exception('At least one line item is required.'); + } + [$subtotal, $tax, $grand] = $this->computeTotals($items, $discount); if ($id === 0) { diff --git a/app/assets/utils/classes/ReturnManager.php b/app/assets/utils/classes/ReturnManager.php index e68b98a..7d5029f 100644 --- a/app/assets/utils/classes/ReturnManager.php +++ b/app/assets/utils/classes/ReturnManager.php @@ -169,7 +169,10 @@ class ReturnManager { $sth = $this->pdo->prepare( "SELECT r.*, COALESCE(c.contact_name, '') AS contact_name, - o.order_number + o.order_number, + (SELECT COUNT(*) FROM td_return_item i + WHERE i.company_id = r.company_id + AND i.return_id = r.id) AS item_count FROM td_return r LEFT JOIN md_contact c ON c.company_id = r.company_id diff --git a/app/assets/utils/classes/SupplierReturnManager.php b/app/assets/utils/classes/SupplierReturnManager.php index 5538fcc..0b809d8 100644 --- a/app/assets/utils/classes/SupplierReturnManager.php +++ b/app/assets/utils/classes/SupplierReturnManager.php @@ -154,7 +154,10 @@ class SupplierReturnManager { $sth = $this->pdo->prepare( "SELECT r.*, COALESCE(c.contact_name, '') AS contact_name, - p.po_number + p.po_number, + (SELECT COUNT(*) FROM td_supplier_return_item i + WHERE i.company_id = r.company_id + AND i.return_id = r.id) AS item_count FROM td_supplier_return r LEFT JOIN md_contact c ON c.company_id = r.company_id diff --git a/app/expense/api/engine/convert_purchase_request.php b/app/expense/api/engine/convert_purchase_request.php index 8ba4d99..b225d69 100644 --- a/app/expense/api/engine/convert_purchase_request.php +++ b/app/expense/api/engine/convert_purchase_request.php @@ -15,10 +15,6 @@ if (!$request_id) { $answer['message'] = 'Purchase request ID is required.'; exit(json_encode($answer)); } -if (!$contact_id) { - $answer['message'] = 'Supplier (contact_id) is required for the PO.'; - exit(json_encode($answer)); -} $prm = new PurchaseRequestManager($pdo2, $company_id); @@ -29,6 +25,15 @@ if (!$pr || (int)$pr['status'] !== 2) { exit(json_encode($answer)); } +// Default to the PR's preferred supplier +if (!$contact_id) { + $contact_id = (int)($pr['contact_id'] ?? 0); +} +if (!$contact_id) { + $answer['message'] = 'Set a Preferred Supplier on this purchase request before converting it to a PO.'; + exit(json_encode($answer)); +} + $pr_items = $pr['items']; if (empty($pr_items)) { $answer['message'] = 'Purchase request has no items.'; @@ -92,6 +97,7 @@ foreach ($convert_items as $ci) { 'unit_price' => $unit_price, 'total_price' => $total_price, 'tax_amount' => $tax_amount, + 'tax_rate' => (float)($pi['tax_rate'] ?? 0), 'received_qty' => 0, 'stock_in_id' => 0, ]; diff --git a/app/expense/manage_purchase_order.php b/app/expense/manage_purchase_order.php index 86986ca..1e6d522 100644 --- a/app/expense/manage_purchase_order.php +++ b/app/expense/manage_purchase_order.php @@ -194,7 +194,7 @@ - + ${format_number((item.quantity || 1) * (item.unit_price || item.price || 0), 2)} @@ -371,8 +371,8 @@ recalc_totals(); }); - $(function() { - load_departments('department_id'); + $(async function() { + await Promise.resolve(load_departments('department_id')).catch(function() {}); flatpickr('#po_date', { dateFormat: 'd/m/Y', allowInput: true }); flatpickr('#expected_date', { dateFormat: 'd/m/Y', allowInput: true }); if (po_id) { diff --git a/app/expense/manage_purchase_request.php b/app/expense/manage_purchase_request.php index 08f4db4..4d9a607 100644 --- a/app/expense/manage_purchase_request.php +++ b/app/expense/manage_purchase_request.php @@ -370,8 +370,9 @@ recalc_totals(); }); - $(function() { - load_departments('department_id'); + $(async function() { + // Options must exist before retrieve_request() selects the saved department + await Promise.resolve(load_departments('department_id')).catch(function() {}); flatpickr('#request_date', { dateFormat: 'd/m/Y', allowInput: true }); flatpickr('#required_date', { dateFormat: 'd/m/Y', allowInput: true }); if (request_id) { diff --git a/app/expense/purchase_request.php b/app/expense/purchase_request.php index 2b18169..f5a3c39 100644 --- a/app/expense/purchase_request.php +++ b/app/expense/purchase_request.php @@ -180,7 +180,7 @@ return; } $.each(rows, function(i, r) { - var items = typeof r.items === 'string' ? JSON.parse(r.items || '[]') : (r.items || []); + var item_count = parseInt(r.item_count) || 0; var po_link = r.po_id > 0 ? `${escape_html(r.po_number || ('PO #' + r.po_id))}` : '—'; @@ -190,7 +190,7 @@ ${r.required_date ? format_date(r.required_date) : '—'} ${escape_html(r.contact_name || '—')} ${get_dept_label(r.department_id)} - ${items.length} item${items.length !== 1 ? 's' : ''} + ${item_count} item${item_count !== 1 ? 's' : ''} ${format_number(r.grand_total, 2)} ${status_badge[String(r.status)] || r.status} ${po_link} diff --git a/app/include_header.php b/app/include_header.php index 46ca22e..65ed825 100644 --- a/app/include_header.php +++ b/app/include_header.php @@ -85,7 +85,7 @@ header('Referrer-Policy: strict-origin-when-cross-origin'); - +