diff --git a/edit_sale.php b/edit_sale.php index 0155786..c82a02b 100644 --- a/edit_sale.php +++ b/edit_sale.php @@ -9,6 +9,9 @@ if ($editSaleId > 0) { $stmt->execute([':id' => $editSaleId]); $editSale = $stmt->fetch(); } +if ($editSale) { + $editSale['items'] = json_decode((string) ($editSale['items_json'] ?? '[]'), true) ?: []; +} if (!$editSale) { die(tr('الفاتورة غير موجودة.', 'Invoice not found.')); } @@ -96,42 +99,62 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($error === '') { $cashierName = current_lang() === 'ar' ? $user['name_ar'] : $user['name_en']; - $stmt = db()->prepare('UPDATE sales_orders SET - branch_code = :branch_code, - customer_id = :customer_id, - customer_name = :customer_name, - payment_method = :payment_method, - payment_status = :payment_status, - paid_amount = :paid_amount, - due_amount = :due_amount, - items_json = :items_json, - item_count = :item_count, - subtotal = :subtotal, - vat_amount = :vat_amount, - total_amount = :total_amount, - status = :status, - notes = :notes - WHERE id = :id'); - $stmt->execute([ - ':branch_code' => $branchCode, - ':customer_id' => $customerId, - ':customer_name' => $customerName !== '' ? $customerName : null, - ':payment_method' => $paymentMethod, - ':payment_status' => $paymentMeta['payment_status'], - ':paid_amount' => $paymentMeta['paid_amount'], - ':due_amount' => $paymentMeta['due_amount'], - ':items_json' => json_encode($normalized, JSON_UNESCAPED_UNICODE), - ':item_count' => $itemCount, - ':subtotal' => $subtotal, - ':vat_amount' => $totalVat, - ':total_amount' => $totalAmount, - ':status' => $saleStatus, - ':notes' => $notes !== '' ? $notes : null, - ':id' => $editSaleId, - ]); - set_flash('success', tr('تم تحديث الفاتورة بنجاح.', 'Invoice updated successfully.')); - redirect_to('sale.php', ['id' => $editSaleId]); + db()->beginTransaction(); + try { + sync_order_stock_reservation( + $editSale['items'] ?? [], + (string) ($editSale['status'] ?? 'completed'), + $normalized, + $saleStatus + ); + + $stmt = db()->prepare('UPDATE sales_orders SET + branch_code = :branch_code, + customer_id = :customer_id, + customer_name = :customer_name, + payment_method = :payment_method, + payment_status = :payment_status, + paid_amount = :paid_amount, + due_amount = :due_amount, + items_json = :items_json, + item_count = :item_count, + subtotal = :subtotal, + vat_amount = :vat_amount, + total_amount = :total_amount, + status = :status, + notes = :notes + WHERE id = :id'); + $stmt->execute([ + ':branch_code' => $branchCode, + ':customer_id' => $customerId, + ':customer_name' => $customerName !== '' ? $customerName : null, + ':payment_method' => $paymentMethod, + ':payment_status' => $paymentMeta['payment_status'], + ':paid_amount' => $paymentMeta['paid_amount'], + ':due_amount' => $paymentMeta['due_amount'], + ':items_json' => json_encode($normalized, JSON_UNESCAPED_UNICODE), + ':item_count' => $itemCount, + ':subtotal' => $subtotal, + ':vat_amount' => $totalVat, + ':total_amount' => $totalAmount, + ':status' => $saleStatus, + ':notes' => $notes !== '' ? $notes : null, + ':id' => $editSaleId, + ]); + + db()->commit(); + } catch (Throwable $e) { + if (db()->inTransaction()) { + db()->rollBack(); + } + $error = tr('تعذر تحديث الفاتورة.', 'Could not update the invoice.'); + } + + if ($error === '') { + set_flash('success', tr('تم تحديث الفاتورة بنجاح.', 'Invoice updated successfully.')); + redirect_to('sale.php', ['id' => $editSaleId]); + } } } } diff --git a/includes/app.php b/includes/app.php index 569693c..275a9ce 100644 --- a/includes/app.php +++ b/includes/app.php @@ -60,6 +60,35 @@ try { ('wablas_daily_auto_last_date', '')"); @file_put_contents($flagFileV5, '1'); } + + $flagFileV6 = sys_get_temp_dir() . '/.schema_migrated_v6_' . md5(__DIR__); + if (!file_exists($flagFileV6)) { + $pdo = db(); + $orderItemsStmt = $pdo->query("SELECT items_json FROM sales_orders WHERE status = 'order'"); + $reservedBySku = []; + foreach ($orderItemsStmt->fetchAll(PDO::FETCH_ASSOC) as $orderRow) { + $orderItems = json_decode((string) ($orderRow['items_json'] ?? '[]'), true) ?: []; + foreach ($orderItems as $item) { + $sku = (string) ($item['sku'] ?? ''); + $qty = (int) ($item['qty'] ?? 0); + if ($sku === '' || $qty <= 0) { + continue; + } + $reservedBySku[$sku] = ($reservedBySku[$sku] ?? 0) + $qty; + } + } + + if ($reservedBySku !== []) { + $adjustStmt = $pdo->prepare("UPDATE items SET base_stock = base_stock - :qty WHERE sku = :sku"); + foreach ($reservedBySku as $sku => $qty) { + $adjustStmt->bindValue(':qty', $qty, PDO::PARAM_INT); + $adjustStmt->bindValue(':sku', $sku); + $adjustStmt->execute(); + } + } + + @file_put_contents($flagFileV6, '1'); + } } catch (\Throwable $e) {} @@ -349,7 +378,7 @@ function require_auth(): array return $user; } -function get_app_modules(): array { return ["pos" => ["name_ar" => "نقاط البيع", "name_en" => "POS", "actions" => ["show", "add"]], "normal_sale" => ["name_ar" => "بيع عادي", "name_en" => "Normal Sale", "actions" => ["show", "add"]], "sales" => ["name_ar" => "المبيعات", "name_en" => "Sales", "actions" => ["show", "edit", "del"]], "purchases" => ["name_ar" => "المشتريات", "name_en" => "Purchases", "actions" => ["show", "add", "edit", "del"]], "stock" => ["name_ar" => "المخزون", "name_en" => "Stock", "actions" => ["show", "add", "edit", "del"]], "reports" => ["name_ar" => "التقارير", "name_en" => "Reports", "actions" => ["show"]], "customers" => ["name_ar" => "العملاء", "name_en" => "Customers", "actions" => ["show", "add", "edit", "del"]], "suppliers" => ["name_ar" => "الموردين", "name_en" => "Suppliers", "actions" => ["show", "add", "edit", "del"]], "categories" => ["name_ar" => "التصنيفات", "name_en" => "Categories", "actions" => ["show", "add", "edit", "del"]], "units" => ["name_ar" => "الوحدات", "name_en" => "Units", "actions" => ["show", "add", "edit", "del"]], "users" => ["name_ar" => "المستخدمين", "name_en" => "Users", "actions" => ["show", "add", "edit", "del"]], "settings" => ["name_ar" => "الإعدادات", "name_en" => "Settings", "actions" => ["show", "edit"]], "expense_categories" => ["name_ar" => "تصنيفات المصروفات", "name_en" => "Expense Categories", "actions" => ["show", "add", "edit", "del"]], "expenses" => ["name_ar" => "المصروفات", "name_en" => "Expenses", "actions" => ["show", "add", "edit", "del"]]]; } function has_permission(string $m, string $a = "show"): bool { $u = current_user(); if (!$u) return false; if ($u["role"] === "owner") return true; $p = !empty($u["permissions"]) ? (is_array($u["permissions"]) ? $u["permissions"] : json_decode($u["permissions"], true)) : []; return !empty($p[$m][$a]); } function require_permission(string $m, string $a = "show"): array { $u = require_auth(); if (!has_permission($m, $a)) { set_flash("warning", tr("ليس لديك صلاحية.", "You do not have permission.")); redirect_to("index.php"); } return $u; } +function get_app_modules(): array { return ["pos" => ["name_ar" => "نقاط البيع", "name_en" => "POS", "actions" => ["show", "add"]], "normal_sale" => ["name_ar" => "فاتورة", "name_en" => "Invoice", "actions" => ["show", "add"]], "sales" => ["name_ar" => "المبيعات", "name_en" => "Sales", "actions" => ["show", "edit", "del"]], "purchases" => ["name_ar" => "المشتريات", "name_en" => "Purchases", "actions" => ["show", "add", "edit", "del"]], "stock" => ["name_ar" => "المخزون", "name_en" => "Stock", "actions" => ["show", "add", "edit", "del"]], "reports" => ["name_ar" => "التقارير", "name_en" => "Reports", "actions" => ["show"]], "customers" => ["name_ar" => "العملاء", "name_en" => "Customers", "actions" => ["show", "add", "edit", "del"]], "suppliers" => ["name_ar" => "الموردين", "name_en" => "Suppliers", "actions" => ["show", "add", "edit", "del"]], "categories" => ["name_ar" => "التصنيفات", "name_en" => "Categories", "actions" => ["show", "add", "edit", "del"]], "units" => ["name_ar" => "الوحدات", "name_en" => "Units", "actions" => ["show", "add", "edit", "del"]], "users" => ["name_ar" => "المستخدمين", "name_en" => "Users", "actions" => ["show", "add", "edit", "del"]], "settings" => ["name_ar" => "الإعدادات", "name_en" => "Settings", "actions" => ["show", "edit"]], "expense_categories" => ["name_ar" => "تصنيفات المصروفات", "name_en" => "Expense Categories", "actions" => ["show", "add", "edit", "del"]], "expenses" => ["name_ar" => "المصروفات", "name_en" => "Expenses", "actions" => ["show", "add", "edit", "del"]]]; } function has_permission(string $m, string $a = "show"): bool { $u = current_user(); if (!$u) return false; if ($u["role"] === "owner") return true; $p = !empty($u["permissions"]) ? (is_array($u["permissions"]) ? $u["permissions"] : json_decode($u["permissions"], true)) : []; return !empty($p[$m][$a]); } function require_permission(string $m, string $a = "show"): array { $u = require_auth(); if (!has_permission($m, $a)) { set_flash("warning", tr("ليس لديك صلاحية.", "You do not have permission.")); redirect_to("index.php"); } return $u; } function require_roles(array $roles): array { $user = require_auth(); @@ -455,7 +484,7 @@ function currency(float $amount): string function sale_mode_label(string $mode): string { - return $mode === 'normal' ? tr('بيع عادي', 'Normal Sale') : tr('بيع نقاط البيع', 'POS Sale'); + return $mode === 'normal' ? tr('فاتورة', 'Invoice') : tr('بيع نقاط البيع', 'POS Sale'); } function round_money(float $amount): float @@ -556,19 +585,32 @@ function apply_sale_payment(int $saleId, float $paymentAmount, bool $completeOrd $newPaidAmount = round_money($summary['paid_amount'] + $appliedAmount); $newDueAmount = max(0.0, round_money((float) $sale['total_amount'] - $newPaidAmount)); $newPaymentStatus = $newDueAmount <= 0.0005 ? 'paid' : 'partial'; - $newSaleStatus = (string) ($sale['status'] ?? 'completed'); + $oldSaleStatus = (string) ($sale['status'] ?? 'completed'); + $newSaleStatus = $oldSaleStatus; if ($completeOrderWhenPaid && $newDueAmount <= 0.0005 && $newSaleStatus === 'order') { $newSaleStatus = 'completed'; } - $stmt = db()->prepare('UPDATE sales_orders SET paid_amount = :paid_amount, due_amount = :due_amount, payment_status = :payment_status, status = :status WHERE id = :id'); - $stmt->execute([ - ':paid_amount' => $newPaidAmount, - ':due_amount' => $newDueAmount, - ':payment_status' => $newPaymentStatus, - ':status' => $newSaleStatus, - ':id' => $saleId, - ]); + db()->beginTransaction(); + try { + sync_order_stock_reservation($sale['items'] ?? [], $oldSaleStatus, $sale['items'] ?? [], $newSaleStatus); + + $stmt = db()->prepare('UPDATE sales_orders SET paid_amount = :paid_amount, due_amount = :due_amount, payment_status = :payment_status, status = :status WHERE id = :id'); + $stmt->execute([ + ':paid_amount' => $newPaidAmount, + ':due_amount' => $newDueAmount, + ':payment_status' => $newPaymentStatus, + ':status' => $newSaleStatus, + ':id' => $saleId, + ]); + + db()->commit(); + } catch (Throwable $e) { + if (db()->inTransaction()) { + db()->rollBack(); + } + throw $e; + } return [ 'applied_amount' => $appliedAmount, @@ -1191,33 +1233,93 @@ function create_sale(array $data): int { ensure_sales_table(); - $stmt = db()->prepare('INSERT INTO sales_orders - (receipt_no, sale_mode, branch_code, cashier_username, cashier_name, role_name, customer_id, customer_name, payment_method, payment_status, items_json, item_count, subtotal, vat_amount, total_amount, paid_amount, due_amount, status, notes, sale_date) - VALUES - (:receipt_no, :sale_mode, :branch_code, :cashier_username, :cashier_name, :role_name, :customer_id, :customer_name, :payment_method, :payment_status, :items_json, :item_count, :subtotal, :vat_amount, :total_amount, :paid_amount, :due_amount, :status, :notes, NOW())'); + db()->beginTransaction(); + try { + $stmt = db()->prepare('INSERT INTO sales_orders + (receipt_no, sale_mode, branch_code, cashier_username, cashier_name, role_name, customer_id, customer_name, payment_method, payment_status, items_json, item_count, subtotal, vat_amount, total_amount, paid_amount, due_amount, status, notes, sale_date) + VALUES + (:receipt_no, :sale_mode, :branch_code, :cashier_username, :cashier_name, :role_name, :customer_id, :customer_name, :payment_method, :payment_status, :items_json, :item_count, :subtotal, :vat_amount, :total_amount, :paid_amount, :due_amount, :status, :notes, NOW())'); - $stmt->bindValue(':receipt_no', $data['receipt_no']); - $stmt->bindValue(':sale_mode', $data['sale_mode']); - $stmt->bindValue(':branch_code', $data['branch_code']); - $stmt->bindValue(':cashier_username', $data['cashier_username']); - $stmt->bindValue(':cashier_name', $data['cashier_name']); - $stmt->bindValue(':role_name', $data['role_name']); - $stmt->bindValue(':customer_id', $data['customer_id'] ?? null, PDO::PARAM_INT); - $stmt->bindValue(':customer_name', $data['customer_name']); - $stmt->bindValue(':payment_method', $data['payment_method']); - $stmt->bindValue(':payment_status', $data['payment_status'] ?? 'paid'); - $stmt->bindValue(':items_json', json_encode($data['items'], JSON_UNESCAPED_UNICODE)); - $stmt->bindValue(':item_count', $data['item_count'], PDO::PARAM_INT); - $stmt->bindValue(':subtotal', $data['subtotal']); - $stmt->bindValue(':vat_amount', $data['vat_amount'] ?? 0.0); - $stmt->bindValue(':total_amount', $data['total_amount']); - $stmt->bindValue(':paid_amount', $data['paid_amount'] ?? $data['total_amount']); - $stmt->bindValue(':due_amount', $data['due_amount'] ?? 0.0); - $stmt->bindValue(':status', $data['status'] ?? 'completed'); - $stmt->bindValue(':notes', $data['notes']); - $stmt->execute(); + $stmt->bindValue(':receipt_no', $data['receipt_no']); + $stmt->bindValue(':sale_mode', $data['sale_mode']); + $stmt->bindValue(':branch_code', $data['branch_code']); + $stmt->bindValue(':cashier_username', $data['cashier_username']); + $stmt->bindValue(':cashier_name', $data['cashier_name']); + $stmt->bindValue(':role_name', $data['role_name']); + $stmt->bindValue(':customer_id', $data['customer_id'] ?? null, PDO::PARAM_INT); + $stmt->bindValue(':customer_name', $data['customer_name']); + $stmt->bindValue(':payment_method', $data['payment_method']); + $stmt->bindValue(':payment_status', $data['payment_status'] ?? 'paid'); + $stmt->bindValue(':items_json', json_encode($data['items'], JSON_UNESCAPED_UNICODE)); + $stmt->bindValue(':item_count', $data['item_count'], PDO::PARAM_INT); + $stmt->bindValue(':subtotal', $data['subtotal']); + $stmt->bindValue(':vat_amount', $data['vat_amount'] ?? 0.0); + $stmt->bindValue(':total_amount', $data['total_amount']); + $stmt->bindValue(':paid_amount', $data['paid_amount'] ?? $data['total_amount']); + $stmt->bindValue(':due_amount', $data['due_amount'] ?? 0.0); + $stmt->bindValue(':status', $data['status'] ?? 'completed'); + $stmt->bindValue(':notes', $data['notes']); + $stmt->execute(); - return (int) db()->lastInsertId(); + $saleId = (int) db()->lastInsertId(); + sync_order_stock_reservation([], 'completed', $data['items'] ?? [], (string) ($data['status'] ?? 'completed')); + + db()->commit(); + return $saleId; + } catch (Throwable $e) { + if (db()->inTransaction()) { + db()->rollBack(); + } + throw $e; + } +} + +function sale_item_quantities(array $items): array +{ + $quantities = []; + foreach ($items as $item) { + $sku = (string) ($item['sku'] ?? ''); + $qty = (int) ($item['qty'] ?? 0); + if ($sku === '' || $qty <= 0) { + continue; + } + $quantities[$sku] = ($quantities[$sku] ?? 0) + $qty; + } + + return $quantities; +} + +function adjust_item_base_stock(array $stockDeltaBySku): void +{ + if ($stockDeltaBySku === []) { + return; + } + + $stmt = db()->prepare('UPDATE items SET base_stock = base_stock + :stock_delta WHERE sku = :sku'); + foreach ($stockDeltaBySku as $sku => $delta) { + if ($sku === '' || $delta === 0) { + continue; + } + $stmt->bindValue(':stock_delta', $delta, PDO::PARAM_INT); + $stmt->bindValue(':sku', (string) $sku); + $stmt->execute(); + } +} + +function sync_order_stock_reservation(array $oldItems, string $oldStatus, array $newItems, string $newStatus): void +{ + $previousReserved = $oldStatus === 'order' ? sale_item_quantities($oldItems) : []; + $nextReserved = $newStatus === 'order' ? sale_item_quantities($newItems) : []; + + $stockDeltaBySku = []; + foreach ($previousReserved as $sku => $qty) { + $stockDeltaBySku[$sku] = ($stockDeltaBySku[$sku] ?? 0) + $qty; + } + foreach ($nextReserved as $sku => $qty) { + $stockDeltaBySku[$sku] = ($stockDeltaBySku[$sku] ?? 0) - $qty; + } + + adjust_item_base_stock($stockDeltaBySku); } function base_sales_query_filters(array &$params, ?string $mode = null, ?string $branch = null): string @@ -1386,6 +1488,10 @@ function stock_snapshot(): array $catalog = catalog(); $sold = []; foreach (fetch_all_sales_for_scope() as $sale) { + if ((string) ($sale['status'] ?? 'completed') === 'order') { + continue; + } + foreach ($sale['items'] as $item) { $sku = (string) ($item['sku'] ?? ''); $sold[$sku] = ($sold[$sku] ?? 0) + (int) ($item['qty'] ?? 0); @@ -1420,7 +1526,7 @@ function module_cards(): array { return [ ['title_ar' => 'نقاط البيع', 'title_en' => 'POS Sale', 'path' => 'pos.php', 'desc_ar' => 'إتمام البيع السريع مع تحديث السجل.', 'desc_en' => 'Fast checkout with instant sales logging.'], - ['title_ar' => 'بيع عادي', 'title_en' => 'Normal Sale', 'path' => 'normal_sale.php', 'desc_ar' => 'فاتورة يدوية مع العميل والملاحظات.', 'desc_en' => 'Manual invoice flow with customer details and notes.'], + ['title_ar' => 'فاتورة', 'title_en' => 'Invoice', 'path' => 'normal_sale.php', 'desc_ar' => 'فاتورة يدوية مع العميل والملاحظات.', 'desc_en' => 'Manual invoice flow with customer details and notes.'], ['title_ar' => 'المبيعات', 'title_en' => 'Sales Ledger', 'path' => 'sales.php', 'desc_ar' => 'قائمة الفواتير مع التفاصيل والفرز.', 'desc_en' => 'Invoice list with filters and detail views.'], ['title_ar' => 'المخزون', 'title_en' => 'Stock', 'path' => 'stock.php', 'desc_ar' => 'قراءة فورية للمخزون الحالي والتنبيهات.', 'desc_en' => 'Live stock snapshot and low-stock indicators.'], ['title_ar' => 'المشتريات', 'title_en' => 'Purchases', 'path' => 'purchases.php', 'desc_ar' => 'واجهة مبدئية لاستلام الموردين بين الفروع.', 'desc_en' => 'Starter receiving board for suppliers and branches.'], diff --git a/includes/header.php b/includes/header.php index 1308962..f9070aa 100644 --- a/includes/header.php +++ b/includes/header.php @@ -96,17 +96,23 @@ $isPublic = !empty($forcePublic) || !isset($user) || !$user; - +
-
+
+ + + + + + @@ -162,9 +168,6 @@ $isPublic = !empty($forcePublic) || !isset($user) || !$user; - - - diff --git a/index.php b/index.php index e62abbc..9c398e5 100644 --- a/index.php +++ b/index.php @@ -90,7 +90,7 @@ require __DIR__ . '/includes/header.php';
-
+

@@ -212,7 +212,7 @@ require __DIR__ . '/includes/header.php'; - +