diff --git a/assets/pasted-20260220-114014-3aea48e0.png b/assets/pasted-20260220-114014-3aea48e0.png new file mode 100644 index 0000000..e281797 Binary files /dev/null and b/assets/pasted-20260220-114014-3aea48e0.png differ diff --git a/check_schema.php b/check_schema.php new file mode 100644 index 0000000..fdcea91 --- /dev/null +++ b/check_schema.php @@ -0,0 +1,14 @@ +query("DESCRIBE $t"); + while ($r = $res->fetch(PDO::FETCH_ASSOC)) { + echo " {$r['Field']} ({$r['Type']})\n"; + } + } catch (Exception $e) { + echo " Error: " . $e->getMessage() . "\n"; + } +} diff --git a/db/migrations/20260220_unify_pos_sales.php b/db/migrations/20260220_unify_pos_sales.php new file mode 100644 index 0000000..56a8ec2 --- /dev/null +++ b/db/migrations/20260220_unify_pos_sales.php @@ -0,0 +1,23 @@ +exec($q); + echo "Executed: $q\n"; + } catch (Exception $e) { + echo "Error executing $q: " . $e->getMessage() . "\n"; + } +} diff --git a/db/migrations/20260220_unify_pos_sales.sql b/db/migrations/20260220_unify_pos_sales.sql new file mode 100644 index 0000000..ccd9e9b --- /dev/null +++ b/db/migrations/20260220_unify_pos_sales.sql @@ -0,0 +1,13 @@ +-- Unify POS and Sales Invoices +-- Date: 2026-02-20 + +ALTER TABLE invoices ADD COLUMN IF NOT EXISTS transaction_no VARCHAR(50) DEFAULT NULL AFTER id; +ALTER TABLE invoices ADD COLUMN IF NOT EXISTS is_pos TINYINT(1) DEFAULT 0; +ALTER TABLE invoices ADD COLUMN IF NOT EXISTS discount_amount DECIMAL(15,3) DEFAULT 0; +ALTER TABLE invoices ADD COLUMN IF NOT EXISTS loyalty_points_earned DECIMAL(15,3) DEFAULT 0; +ALTER TABLE invoices ADD COLUMN IF NOT EXISTS loyalty_points_redeemed DECIMAL(15,3) DEFAULT 0; +ALTER TABLE invoices ADD COLUMN IF NOT EXISTS created_by INT(11) DEFAULT NULL; + +ALTER TABLE payments ADD COLUMN IF NOT EXISTS transaction_id INT(11) DEFAULT NULL; + +ALTER TABLE invoices MODIFY COLUMN status ENUM('paid','unpaid','partially_paid','refunded','cancelled') DEFAULT 'unpaid'; diff --git a/db/migrations/fix_lpo_foreign_key.sql b/db/migrations/fix_lpo_foreign_key.sql new file mode 100644 index 0000000..89c5334 --- /dev/null +++ b/db/migrations/fix_lpo_foreign_key.sql @@ -0,0 +1,2 @@ +ALTER TABLE lpos DROP FOREIGN KEY lpos_ibfk_1; +ALTER TABLE lpos ADD CONSTRAINT lpos_ibfk_1 FOREIGN KEY (supplier_id) REFERENCES suppliers(id); diff --git a/describe_tables.php b/describe_tables.php new file mode 100644 index 0000000..e17c954 --- /dev/null +++ b/describe_tables.php @@ -0,0 +1,13 @@ +query("DESCRIBE $t"); + while($r = $res->fetch(PDO::FETCH_ASSOC)) { + echo " {$r['Field']} ({$r['Type']})\n"; + } + } catch (Exception $e) { + echo " Error: " . $e->getMessage() . "\n"; + } +} diff --git a/includes/lang.php b/includes/lang.php index 72e1d15..9c93b55 100644 --- a/includes/lang.php +++ b/includes/lang.php @@ -14,6 +14,7 @@ $translations = [ 'sales_returns' => 'Sales Returns', 'purchases' => 'Purchases', 'purchase_returns' => 'Purchase Returns', + 'lpos' => 'Local Purchase Orders (LPO)', 'quotations' => 'Quotations', 'expenses' => 'Expenses', 'expense_categories' => 'Expense Categories', @@ -107,6 +108,7 @@ $translations = [ 'sales_returns' => 'مرتجعات المبيعات', 'purchases' => 'المشتريات', 'purchase_returns' => 'مرتجعات المشتريات', + 'lpos' => 'أوامر الشراء المحلية (LPO)', 'quotations' => 'عروض الأسعار', 'expenses' => 'المصاريف', 'expense_categories' => 'فئات المصاريف', diff --git a/index.php b/index.php index 1786d2a..0e4b261 100644 --- a/index.php +++ b/index.php @@ -362,27 +362,34 @@ if (isset($_GET['action']) || isset($_POST['action'])) { } } - // Insert Transaction - $stmt = $db->prepare("INSERT INTO pos_transactions (transaction_no, customer_id, total_amount, tax_amount, discount_code_id, discount_amount, loyalty_points_redeemed, net_amount, register_session_id, created_by, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'completed')"); - $stmt->execute([$transaction_no, $customer_id, $total_amount, $tax_amount, $discount_code_id, $discount_amount, $loyalty_redeemed, $net_amount, $session_id, $_SESSION['user_id']]); + // Insert into unified Invoice table + $items_for_journal = []; + foreach ($items as $item) { + $items_for_journal[] = ['id' => $item['id'], 'qty' => $item['qty']]; + } + + $stmt = $db->prepare("INSERT INTO invoices (transaction_no, customer_id, invoice_date, payment_type, total_amount, vat_amount, total_with_vat, paid_amount, status, register_session_id, is_pos, discount_amount, loyalty_points_redeemed, created_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'paid', ?, 1, ?, ?, ?)"); + $stmt->execute([$transaction_no, $customer_id, date('Y-m-d'), 'pos', $total_amount, $tax_amount, $net_amount, $net_amount, $session_id, $discount_amount, $loyalty_redeemed, $_SESSION['user_id']]); $transaction_id = (int)$db->lastInsertId(); // Insert Items & Update Stock - $stmtItem = $db->prepare("INSERT INTO pos_items (transaction_id, product_id, quantity, unit_price, vat_rate, vat_amount, subtotal) VALUES (?, ?, ?, ?, ?, ?, ?)"); + $stmtItem = $db->prepare("INSERT INTO invoice_items (invoice_id, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)"); $stmtStock = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?"); foreach ($items as $item) { $sub = (float)$item['price'] * (float)$item['qty']; - $vr = (float)($item['vat_rate'] ?? 0); $va = (float)($item['vat_amount'] ?? 0); - $stmtItem->execute([$transaction_id, $item['id'], $item['qty'], $item['price'], $vr, $va, $sub]); + $stmtItem->execute([$transaction_id, $item['id'], $item['qty'], $item['price'], $va, $sub]); $stmtStock->execute([$item['qty'], $item['id']]); } // Insert Payments - $stmtPay = $db->prepare("INSERT INTO pos_payments (transaction_id, payment_method, amount) VALUES (?, ?, ?)"); + require_once 'includes/accounting_helper.php'; + $stmtPay = $db->prepare("INSERT INTO payments (invoice_id, amount, payment_date, payment_method, notes) VALUES (?, ?, ?, ?, ?)"); foreach ($payments as $p) { - $stmtPay->execute([$transaction_id, $p['method'], $p['amount']]); + $stmtPay->execute([$transaction_id, $p['amount'], date('Y-m-d'), $p['method'], 'POS Transaction']); + $payment_id = $db->lastInsertId(); + recordPaymentReceivedJournal((int)$payment_id, $p['amount'], date('Y-m-d'), $p['method']); } // Update Loyalty Points if customer exists @@ -402,10 +409,13 @@ if (isset($_GET['action']) || isset($_POST['action'])) { ->execute([$customer_id, $transaction_id, -$loyalty_redeemed * 100, "Redeemed for POS order #$transaction_no"]); } - // Update transaction with points earned - $db->prepare("UPDATE pos_transactions SET loyalty_points_earned = ? WHERE id = ?")->execute([$points_earned, $transaction_id]); + // Update invoice with points earned + $db->prepare("UPDATE invoices SET loyalty_points_earned = ? WHERE id = ?")->execute([$points_earned, $transaction_id]); } + // Record Sale Journal for POS + recordSaleJournal($transaction_id, $net_amount, date('Y-m-d'), $items_for_journal, $tax_amount); + $db->commit(); echo json_encode(['success' => true, 'invoice_id' => $transaction_id, 'transaction_no' => $transaction_no]); } catch (Exception $e) { @@ -512,7 +522,7 @@ if (isset($_GET['action']) || isset($_POST['action'])) { } // Redirect to login if not authenticated -if (false && !isset($_SESSION['user_id'])) { +if (!isset($_SESSION['user_id'])) { ?> @@ -753,69 +763,11 @@ function getPromotionalPrice($item) { $pay_type = $_POST['payment_type'] ?? 'cash'; $items = $_POST['item_ids'] ?? []; - $qtys = $_POST['quantities'] ?? []; - $prices = $_POST['prices'] ?? []; - - $total_subtotal = 0; - $total_vat = 0; - - foreach ($items as $i => $item_id) { - if (!$item_id) continue; - $qty = (float)$qtys[$i]; - $price = (float)$prices[$i]; - $subtotal = $qty * $price; - - $stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?"); - $stmtVat->execute([$item_id]); - $vatRate = (float)$stmtVat->fetchColumn(); - - $vatAmount = $subtotal * ($vatRate / 100); - $total_subtotal += $subtotal; - $total_vat += $vatAmount; + if (empty($items)) { + throw new Exception("Please add at least one item."); } - - $total_with_vat = $total_subtotal + $total_vat; - $paid = (float)($_POST['paid_amount'] ?? 0); - if ($status === 'paid') $paid = $total_with_vat; - - $stmt = $db->prepare("INSERT INTO $table ($cust_supplier_col, invoice_date, due_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"); - $stmt->execute([$cust_id, $inv_date, $due_date, $status, $pay_type, $total_subtotal, $total_vat, $total_with_vat, $paid]); - $inv_id = $db->lastInsertId(); - - $items_for_journal = []; - foreach ($items as $i => $item_id) { - if (!$item_id) continue; - $qty = (float)$qtys[$i]; - $price = (float)$prices[$i]; - $subtotal = $qty * $price; - - $stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?"); - $stmtVat->execute([$item_id]); - $vatRate = (float)$stmtVat->fetchColumn(); - $vatAmount = $subtotal * ($vatRate / 100); - - $db->prepare("INSERT INTO $item_table (" . (($type === 'purchase') ? 'purchase_id' : 'invoice_id') . ", item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$inv_id, $item_id, $qty, $price, $vatAmount, $subtotal]); - $change = ($type === 'sale') ? -$qty : $qty; - $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?")->execute([$change, $item_id]); - $items_for_journal[] = ['id' => $item_id, 'qty' => $qty]; - } - if ($type === 'sale') recordSaleJournal((int)$inv_id, $total_with_vat, $inv_date, $items_for_journal, $total_vat); - else recordPurchaseJournal((int)$inv_id, $total_with_vat, $inv_date, $items_for_journal, $total_vat); - $db->commit(); - $message = ($type === 'purchase' ? "Purchase" : "Invoice") . " #$inv_id created!"; - } catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); } - } - - if (isset($_POST['add_quotation'])) { - $db = db(); - try { - $db->beginTransaction(); - $cust_id = (int)$_POST['customer_id']; - $quot_date = $_POST['quotation_date'] ?: date('Y-m-d'); - $valid_until = $_POST['valid_until'] ?: null; - - $items = $_POST['item_ids'] ?? []; $qtys = $_POST['quantities'] ?? []; + $prices = $_POST['prices'] ?? []; $total_subtotal = 0; @@ -921,10 +873,134 @@ function getPromotionalPrice($item) { if (isset($_POST['delete_quotation'])) { $id = (int)$_POST['id']; db()->prepare("DELETE FROM quotations WHERE id = ?")->execute([$id]); - db()->prepare("DELETE FROM quotation_items WHERE quotation_id = ?")->execute([$id]); $message = "Quotation deleted!"; } + if (isset($_POST['add_lpo'])) { + $db = db(); + try { + $db->beginTransaction(); + $supp_id = (int)$_POST['supplier_id']; + $lpo_date = $_POST['lpo_date'] ?: date('Y-m-d'); + $delivery_date = $_POST['delivery_date'] ?: null; + $terms = $_POST['terms_conditions'] ?? ''; + + $items = $_POST['item_ids'] ?? []; + if (empty($items)) { + throw new Exception("Please add at least one item."); + } + $qtys = $_POST['quantities'] ?? []; + $prices = $_POST['prices'] ?? []; + + $total_subtotal = 0; + $total_vat = 0; + + foreach ($items as $i => $item_id) { + if (!$item_id) continue; + $qty = (float)$qtys[$i]; + $price = (float)$prices[$i]; + $subtotal = $qty * $price; + + $stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?"); + $stmtVat->execute([$item_id]); + $vatRate = (float)$stmtVat->fetchColumn(); + + $vatAmount = $subtotal * ($vatRate / 100); + $total_subtotal += $subtotal; + $total_vat += $vatAmount; + } + + $total_with_vat = $total_subtotal + $total_vat; + + $stmt = $db->prepare("INSERT INTO lpos (supplier_id, lpo_date, delivery_date, status, total_amount, vat_amount, total_with_vat, terms_conditions) VALUES (?, ?, ?, 'pending', ?, ?, ?, ?)"); + $stmt->execute([$supp_id, $lpo_date, $delivery_date, $total_subtotal, $total_vat, $total_with_vat, $terms]); + $lpo_id = $db->lastInsertId(); + + foreach ($items as $i => $item_id) { + if (!$item_id) continue; + $qty = (float)$qtys[$i]; + $price = (float)$prices[$i]; + $subtotal = $qty * $price; + + $stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?"); + $stmtVat->execute([$item_id]); + $vatRate = (float)$stmtVat->fetchColumn(); + $vatAmount = $subtotal * ($vatRate / 100); + + $db->prepare("INSERT INTO lpo_items (lpo_id, item_id, quantity, unit_price, vat_percentage, vat_amount, total_amount) VALUES (?, ?, ?, ?, ?, ?, ?)")->execute([$lpo_id, $item_id, $qty, $price, $vatRate, $vatAmount, $subtotal]); + } + $db->commit(); + $message = "LPO #$lpo_id created!"; + } catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); } + } + + if (isset($_POST['edit_lpo'])) { + $db = db(); + try { + $db->beginTransaction(); + $lpo_id = (int)$_POST['lpo_id']; + $supp_id = (int)$_POST['supplier_id']; + $lpo_date = $_POST['lpo_date']; + $delivery_date = $_POST['delivery_date'] ?: null; + $status = $_POST['status'] ?? 'pending'; + $terms = $_POST['terms_conditions'] ?? ''; + + $items = $_POST['item_ids'] ?? []; + if (empty($items)) { + throw new Exception("Please add at least one item."); + } + $qtys = $_POST['quantities'] ?? []; + $prices = $_POST['prices'] ?? []; + + $total_subtotal = 0; + $total_vat = 0; + + foreach ($items as $i => $item_id) { + if (!$item_id) continue; + $qty = (float)$qtys[$i]; + $price = (float)$prices[$i]; + $subtotal = $qty * $price; + + $stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?"); + $stmtVat->execute([$item_id]); + $vatRate = (float)$stmtVat->fetchColumn(); + + $vatAmount = $subtotal * ($vatRate / 100); + $total_subtotal += $subtotal; + $total_vat += $vatAmount; + } + + $total_with_vat = $total_subtotal + $total_vat; + + $stmt = $db->prepare("UPDATE lpos SET supplier_id = ?, lpo_date = ?, delivery_date = ?, status = ?, total_amount = ?, vat_amount = ?, total_with_vat = ?, terms_conditions = ? WHERE id = ?"); + $stmt->execute([$supp_id, $lpo_date, $delivery_date, $status, $total_subtotal, $total_vat, $total_with_vat, $terms, $lpo_id]); + + $db->prepare("DELETE FROM lpo_items WHERE lpo_id = ?")->execute([$lpo_id]); + + foreach ($items as $i => $item_id) { + if (!$item_id) continue; + $qty = (float)$qtys[$i]; + $price = (float)$prices[$i]; + $subtotal = $qty * $price; + + $stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?"); + $stmtVat->execute([$item_id]); + $vatRate = (float)$stmtVat->fetchColumn(); + $vatAmount = $subtotal * ($vatRate / 100); + + $db->prepare("INSERT INTO lpo_items (lpo_id, item_id, quantity, unit_price, vat_percentage, vat_amount, total_amount) VALUES (?, ?, ?, ?, ?, ?, ?)")->execute([$lpo_id, $item_id, $qty, $price, $vatRate, $vatAmount, $subtotal]); + } + $db->commit(); + $message = "LPO #$lpo_id updated!"; + } catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); } + } + + if (isset($_POST['delete_lpo'])) { + $id = (int)$_POST['id']; + db()->prepare("DELETE FROM lpos WHERE id = ?")->execute([$id]); + $message = "LPO deleted!"; + } + if (isset($_POST['convert_to_invoice'])) { $db = db(); try { @@ -1763,7 +1839,7 @@ if (isset($_POST['add_hr_department'])) { $session->execute([$session_id]); $opening = (float)$session->fetchColumn(); - $sales = db()->prepare("SELECT SUM(p.amount) FROM pos_payments p JOIN pos_transactions t ON p.transaction_id = t.id WHERE t.register_session_id = ? AND t.status = 'completed' AND p.payment_method = 'cash'"); + $sales = db()->prepare("SELECT SUM(p.amount) FROM payments p JOIN invoices i ON p.invoice_id = i.id WHERE i.register_session_id = ? AND i.status = 'paid' AND i.is_pos = 1 AND p.payment_method = 'cash'"); $sales->execute([$session_id]); $cash_sales = (float)$sales->fetchColumn(); @@ -1791,6 +1867,7 @@ $page_permissions = [ 'purchases' => 'purchases_view', 'purchase_returns' => 'purchase_returns_view', 'quotations' => 'quotations_view', + 'lpos' => 'lpos_view', 'accounting' => 'accounting_view', 'expense_categories' => 'expense_categories_view', 'expenses' => 'expenses_view', @@ -1873,6 +1950,7 @@ $permission_groups = [ ], 'Purchases' => [ 'purchases' => 'Purchases', + 'lpos' => 'LPOs', 'purchase_returns' => 'Purchase Returns' ], 'Expenses' => [ @@ -2102,18 +2180,50 @@ switch ($page) { } $whereSql = implode(" AND ", $where); $stmt = db()->prepare("SELECT q.*, c.name as customer_name - FROM quotations q - LEFT JOIN customers c ON q.customer_id = c.id - WHERE $whereSql - ORDER BY q.id DESC"); + FROM quotations q + JOIN customers c ON q.customer_id = c.id + WHERE $whereSql + ORDER BY q.id DESC"); $stmt->execute($params); $data['quotations'] = $stmt->fetchAll(); - $items_list_raw = db()->query("SELECT id, name_en, name_ar, sale_price, purchase_price, stock_quantity, vat_rate, is_promotion, promotion_start, promotion_end, promotion_percent FROM stock_items ORDER BY name_en ASC")->fetchAll(PDO::FETCH_ASSOC); - foreach ($items_list_raw as &$item) { - $item['sale_price'] = getPromotionalPrice($item); + break; + case 'lpos': + $where = ["1=1"]; + $params = []; + if (!empty($_GET['search'])) { + $s = $_GET['search']; + $clean_id = preg_replace('/[^0-9]/', '', $s); + if ($clean_id !== '') { + $where[] = "(q.id LIKE ? OR s.name LIKE ? OR q.id = ?)"; + $params[] = "%$s%"; + $params[] = "%$s%"; + $params[] = $clean_id; + } else { + $where[] = "(q.id LIKE ? OR s.name LIKE ?)"; + $params[] = "%$s%"; + $params[] = "%$s%"; + } } - $data['items_list'] = $items_list_raw; - $data['customers_list'] = db()->query("SELECT id, name FROM customers ORDER BY name ASC")->fetchAll(); + if (!empty($_GET['supplier_id'])) { + $where[] = "q.supplier_id = ?"; + $params[] = $_GET['supplier_id']; + } + if (!empty($_GET['start_date'])) { + $where[] = "q.lpo_date >= ?"; + $params[] = $_GET['start_date']; + } + if (!empty($_GET['end_date'])) { + $where[] = "q.lpo_date <= ?"; + $params[] = $_GET['end_date']; + } + $whereSql = implode(" AND ", $where); + $stmt = db()->prepare("SELECT q.*, s.name as supplier_name + FROM lpos q + JOIN suppliers s ON q.supplier_id = s.id + WHERE $whereSql + ORDER BY q.id DESC"); + $stmt->execute($params); + $data['lpos'] = $stmt->fetchAll(); break; case 'payment_methods': $data['payment_methods'] = db()->query("SELECT * FROM payment_methods ORDER BY id DESC")->fetchAll(); @@ -2565,8 +2675,8 @@ switch ($page) { $data['stats'] = [ 'total_customers' => db()->query("SELECT COUNT(*) FROM customers")->fetchColumn(), 'total_items' => db()->query("SELECT COUNT(*) FROM stock_items")->fetchColumn(), - 'total_sales' => db()->query("SELECT SUM(total_with_vat) FROM invoices")->fetchColumn() ?: 0, - 'total_received' => db()->query("SELECT SUM(amount) FROM payments")->fetchColumn() ?: 0, + 'total_sales' => (db()->query("SELECT SUM(total_with_vat) FROM invoices")->fetchColumn() ?: 0) + (db()->query("SELECT SUM(net_amount) FROM pos_transactions WHERE status = 'completed'")->fetchColumn() ?: 0), + 'total_received' => (db()->query("SELECT SUM(amount) FROM payments")->fetchColumn() ?: 0) + (db()->query("SELECT SUM(amount) FROM pos_payments")->fetchColumn() ?: 0), 'total_purchases' => db()->query("SELECT SUM(total_with_vat) FROM purchases")->fetchColumn() ?: 0, 'total_paid' => db()->query("SELECT SUM(amount) FROM purchase_payments")->fetchColumn() ?: 0, 'expired_items' => db()->query("SELECT COUNT(*) FROM stock_items WHERE expiry_date IS NOT NULL AND expiry_date <= CURDATE()")->fetchColumn(), @@ -2720,17 +2830,22 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; - - - +
Cash Reconciliation
@@ -8218,14 +8442,15 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; Expected Cash: OMR
+
Actual Cash: - OMR + OMR
0 ? 'text-info' : 'text-success'); + $shortage = (float)$s['cash_in_hand'] - $expected_cash_total; + $shortage_class = $shortage < 0 ? 'text-danger' : 'text-success'; ?>
Balance: @@ -8252,6 +8477,30 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; + prepare("SELECT i.*, c.name as customer_name, GROUP_CONCAT(p.payment_method SEPARATOR ', ') as methods FROM invoices i LEFT JOIN payments p ON i.id = p.invoice_id LEFT JOIN customers c ON i.customer_id = c.id WHERE i.register_session_id = ? AND i.status = 'paid' AND i.is_pos = 1 GROUP BY i.id ORDER BY i.created_at DESC"); + $txs_stmt->execute([$s['id']]); + $txs = $txs_stmt->fetchAll(); + foreach ($txs as $tx): + ?> + + + + + + prepare("SELECT si.name_en, ii.quantity FROM invoice_items ii JOIN stock_items si ON ii.item_id = si.id WHERE ii.invoice_id = ?"); + $items_stmt->execute([$tx['id']]); + $items = $items_stmt->fetchAll(); + foreach ($items as $item) { + echo "" . htmlspecialchars($item['name_en']) . " x " . (float)$item['quantity'] . ""; + } + ?> + + + + + prepare("SELECT t.*, c.name as customer_name, GROUP_CONCAT(p.payment_method SEPARATOR ', ') as methods FROM pos_transactions t LEFT JOIN pos_payments p ON t.id = p.transaction_id LEFT JOIN customers c ON t.customer_id = c.id WHERE t.register_session_id = ? AND t.status = 'completed' GROUP BY t.id ORDER BY t.created_at DESC"); $txs_stmt->execute([$s['id']]); @@ -8354,8 +8603,13 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
prepare("SELECT p.payment_method, SUM(p.amount) as total FROM pos_payments p JOIN pos_transactions t ON p.transaction_id = t.id WHERE t.register_session_id = ? AND t.status = 'completed' GROUP BY p.payment_method"); - $curBreakdown->execute([$session['id']]); + // Unified breakdown for closing + $curBreakdown = db()->prepare("SELECT payment_method, SUM(amount) as total FROM ( + SELECT p.payment_method, p.amount FROM pos_payments p JOIN pos_transactions t ON p.transaction_id = t.id WHERE t.register_session_id = ? AND t.status = 'completed' + UNION ALL + SELECT p.payment_method, p.amount FROM payments p JOIN invoices i ON p.invoice_id = i.id WHERE i.register_session_id = ? AND i.status = 'paid' AND i.is_pos = 1 + ) as combined GROUP BY payment_method"); + $curBreakdown->execute([$session['id'], $session['id']]); $curMethods = $curBreakdown->fetchAll(); $cash_sales = 0; @@ -9284,7 +9538,7 @@ document.addEventListener('DOMContentLoaded', function() { document.getElementById('receiptNo').textContent = 'RCP-' + data.id.toString().padStart(5, '0'); document.getElementById('receiptDate').textContent = data.payment_date; document.getElementById('receiptCustomer').textContent = data.customer_name || '---'; - document.getElementById('receiptInvNo').textContent = 'INV-' + data.inv_id.toString().padStart(5, '0'); + document.getElementById('receiptInvNo').textContent = (data.inv_type === 'purchase' ? 'PUR-' : 'INV-') + data.inv_id.toString().padStart(5, '0'); document.getElementById('receiptMethod').textContent = data.payment_method; document.getElementById('receiptAmount').textContent = parseFloat(data.amount).toFixed(3); document.getElementById('receiptAmountWords').textContent = data.amount_words; @@ -9549,10 +9803,188 @@ document.addEventListener('DOMContentLoaded', function() { }); } - const invoiceType = ''; + const invoiceType = ''; initInvoiceForm('productSearchInput', 'searchSuggestions', 'invoiceItemsTableBody', 'grandTotal', 'subtotal', 'totalVat'); initInvoiceForm('editProductSearchInput', 'editSearchSuggestions', 'editInvoiceItemsTableBody', 'edit_grandTotal', 'edit_subtotal', 'edit_totalVat'); + // LPO Form Logic + initInvoiceForm('lpoProductSearchInput', 'lpoSearchSuggestions', 'lpoItemsTableBody', 'lpo_grand_display', 'lpo_subtotal_display', 'lpo_vat_display'); + initInvoiceForm('editLpoProductSearchInput', 'editLpoSearchSuggestions', 'editLpoItemsTableBody', 'edit_lpo_grand_display', 'edit_lpo_subtotal_display', 'edit_lpo_vat_display'); + + document.querySelectorAll('.edit-lpo-btn').forEach(btn => { + btn.addEventListener('click', function() { + const data = JSON.parse(this.dataset.json); + document.getElementById('edit_lpo_id').value = data.id; + const supplierSelect = document.getElementById('edit_lpo_supplier_id'); + supplierSelect.value = data.supplier_id; + if (window.jQuery && $(supplierSelect).data('select2')) { + $(supplierSelect).trigger('change'); + } + document.getElementById('edit_lpo_date').value = data.lpo_date; + document.getElementById('edit_lpo_delivery_date').value = data.delivery_date || ''; + document.getElementById('edit_lpo_status').value = data.status || 'pending'; + document.getElementById('edit_lpo_terms').value = data.terms_conditions || ''; + + const tableBody = document.getElementById('editLpoItemsTableBody'); + tableBody.innerHTML = ''; + + data.items.forEach(item => { + addItemToTable({ + id: item.item_id, + name_en: item.name_en, + name_ar: item.name_ar, + sku: '', + vat_rate: item.vat_rate || 0 + }, tableBody, null, null, + document.getElementById('edit_lpo_grand_display'), + document.getElementById('edit_lpo_subtotal_display'), + document.getElementById('edit_lpo_vat_display'), + { quantity: item.quantity, unit_price: item.unit_price }); + }); + }); + }); + + document.querySelectorAll('.view-lpo-btn').forEach(btn => { + btn.addEventListener('click', function() { + const data = JSON.parse(this.dataset.json); + window.viewAndPrintLPO(data); + }); + }); + + window.viewAndPrintLPO = function(data) { + const modal = new bootstrap.Modal(document.getElementById('viewLpoModal')); + const content = document.getElementById('lpoDetailsContent'); + + const logoUrl = companySettings.company_logo || ''; + const companyHeader = ` +
+
+ ${logoUrl ? `Logo` : ''} +

${companySettings.company_name || 'Your Company'}

+

+ ${companySettings.company_address || ''}
+ Phone: ${companySettings.company_phone || ''} | Email: ${companySettings.company_email || ''} + ${companySettings.tax_number ? `
TRN: ${companySettings.tax_number}` : ''} +

+
+
+

LOCAL PURCHASE ORDER

+

LPO-${data.id.toString().padStart(5, '0')}

+
+
+ `; + + let itemsHtml = ''; + data.items.forEach((item, index) => { + itemsHtml += ` + + ${index + 1} + ${item.name_en}
${item.name_ar} + ${item.quantity} + ${parseFloat(item.unit_price).toFixed(3)} + ${item.vat_rate}% + ${parseFloat(item.total_amount).toFixed(3)} + + `; + }); + + content.innerHTML = ` + ${companyHeader} +
+
+
+
Supplier
+

${data.supplier_name}

+

+ ${data.supplier_phone ? `Phone: ${data.supplier_phone}` : ''} +

+
+
+
Details
+
+ Date: + ${data.lpo_date} +
+
+ Delivery: + ${data.delivery_date || '---'} +
+
+ Status: + ${data.status.toUpperCase()} +
+
+
+
+ + + + + + + + + + + + ${itemsHtml} + + + + + + + + + + + + + + +
#DescriptionQtyUnit PriceVATTotal
SubtotalOMR ${parseFloat(data.total_amount).toFixed(3)}
VAT AmountOMR ${parseFloat(data.vat_amount).toFixed(3)}
Grand TotalOMR ${parseFloat(data.total_with_vat).toFixed(3)}
+
+ ${data.terms_conditions ? ` +
+
+
Terms & Conditions
+

${data.terms_conditions.replace(/\n/g, '
')}

+
+
` : ''} + +
+
+
+

Prepared By

+
+
+
+
+
+

Authorized Signature

+
+
+
+ `; + + window.printLPO = function() { + const printWindow = window.open('', '_blank'); + printWindow.document.write('LPO-' + data.id + ''); + printWindow.document.write(''); + printWindow.document.write(''); + printWindow.document.write(''); + printWindow.document.write(content.innerHTML); + printWindow.document.write(''); + printWindow.document.close(); + setTimeout(() => { + printWindow.print(); + printWindow.close(); + }, 500); + }; + + modal.show(); + }; + // Quotation Form Logic initInvoiceForm('quotProductSearchInput', 'quotSearchSuggestions', 'quotItemsTableBody', 'quot_grand_display', 'quot_subtotal_display', 'quot_vat_display'); initInvoiceForm('editQuotProductSearchInput', 'editQuotSearchSuggestions', 'editQuotItemsTableBody', 'edit_quot_grand_display', 'edit_quot_subtotal_display', 'edit_quot_vat_display'); @@ -10593,6 +11025,200 @@ document.addEventListener('DOMContentLoaded', function() {
+ + + + + + + + +