38471-vm/pages/sales_purchases_save_logic.php
2026-05-12 10:32:52 +00:00

269 lines
14 KiB
PHP

<?php
// Shared Sales/Purchases create/update handlers extracted from index.php
// to reduce regression risk while preserving the existing behavior.
// Invoices
if (isset($_POST['add_invoice'])) {
$db = db();
try {
$db->beginTransaction();
$type = $_POST['type'] ?? 'sale';
$table = ($type === 'purchase') ? 'purchases' : 'invoices';
$item_table = ($type === 'purchase') ? 'purchase_items' : 'invoice_items';
$cust_supplier_col = ($type === 'purchase') ? 'supplier_id' : 'customer_id';
$fk_col = ($type === 'purchase') ? 'purchase_id' : 'invoice_id';
$rawCustomerId = $_POST['customer_id'] ?? '';
$cust_id = ($type === 'sale' && ($rawCustomerId === '' || $rawCustomerId === null)) ? null : (int)$rawCustomerId;
$inv_date = $_POST['invoice_date'] ?: date('Y-m-d');
$due_date = $_POST['due_date'] ?: null;
$status = $_POST['status'] ?? 'pending';
$pay_type = $_POST['payment_type'] ?? 'cash';
$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;
$profitLines = [];
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
$qty = normalize_quantity($qtys[$i] ?? 0);
$price = (float)$prices[$i];
if ($type === 'sale') {
assertEditedSalePriceWithinLastSalePrice($item_id, $price);
}
$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;
$profitLines[] = [
'item_id' => (int)$item_id,
'qty' => $qty,
'unit_price' => $price,
];
}
$gross_total = $total_subtotal + $total_vat;
$manualDiscountEnabled = getSettingValue('manual_discount_enabled', '0') === '1';
$hasDiscountColumn = ($type === 'sale') && db_column_exists($table, 'discount_amount');
$discount_amount = 0.0;
if ($type === 'sale' && $hasDiscountColumn && $manualDiscountEnabled) {
$discount_amount = max(0, (float)($_POST['discount_amount'] ?? 0));
$discountMetrics = calculateManualDiscountProfitMetrics($profitLines, false);
$maxManualDiscount = min(max(0, $gross_total), max(0, (float)($discountMetrics['max_discount'] ?? 0)));
if ($discount_amount > ($maxManualDiscount + 0.0005)) {
throw new Exception(manualDiscountLimitMessage($discountMetrics, $discount_amount));
}
if ($discount_amount > $gross_total) {
$discount_amount = $gross_total;
}
}
$total_with_vat = max(0, $gross_total - $discount_amount);
$paid = max(0, (float)($_POST['paid_amount'] ?? 0));
if ($paid > $total_with_vat) {
$paid = $total_with_vat;
}
if ($status === 'paid') $paid = $total_with_vat;
if ($hasDiscountColumn) {
$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, discount_amount) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$cust_id, $inv_date, $due_date, $status, $pay_type, $total_subtotal, $total_vat, $total_with_vat, $paid, $discount_amount]);
} else {
$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();
if (db_column_exists($table, 'outlet_id')) {
$db->prepare("UPDATE $table SET outlet_id = ? WHERE id = ?")->execute([current_outlet_id(), $inv_id]);
}
$items_for_journal = [];
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
$qty = normalize_quantity($qtys[$i] ?? 0);
$price = (float)$prices[$i];
if ($type === 'sale') {
assertEditedSalePriceWithinLastSalePrice($item_id, $price);
}
$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 ($fk_col, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$inv_id, $item_id, $qty, $price, $vatAmount, $subtotal]);
// Update stock
$change = ($type === 'sale') ? -$qty : $qty;
update_stock($item_id, $change);
$items_for_journal[] = ['id' => $item_id, 'qty' => $qty];
}
// Accounting
if ($type === 'sale') {
recordSaleJournal($inv_id, $total_with_vat, $inv_date, $items_for_journal, $total_vat);
} else {
// For purchases, you might have recordPurchaseJournal, but let's check if it exists
if (function_exists('recordPurchaseJournal')) {
recordPurchaseJournal($inv_id, $total_with_vat, $inv_date, $items_for_journal, $total_vat);
}
}
$db->commit();
$wablasNotice = '';
if ($type === 'sale' && function_exists('wablasQueueInvoiceNotification')) {
$wablasQueue = wablasQueueInvoiceNotification((int)$inv_id);
$wablasNotice = (string)($wablasQueue['notice'] ?? '');
}
$_SESSION['trigger_invoice_modal'] = true;
$_SESSION['show_invoice_id'] = (int)$inv_id;
$_SESSION['show_invoice_page'] = ($type === 'purchase') ? 'purchases' : 'sales';
$msg = ($type === 'purchase' ? "Purchase" : "Invoice") . " #$inv_id created!" . $wablasNotice;
redirectWithMessage($msg, page_url($type === 'purchase' ? 'purchases' : 'sales'));
} catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
}
if (isset($_POST['edit_invoice'])) {
$db = db();
try {
$db->beginTransaction();
$id = (int)$_POST['invoice_id'];
$type = ($page === 'purchases') ? 'purchase' : 'sale';
$table = ($type === 'purchase') ? 'purchases' : 'invoices';
$item_table = ($type === 'purchase') ? 'purchase_items' : 'invoice_items';
$cust_supplier_col = ($type === 'purchase') ? 'supplier_id' : 'customer_id';
$fk_col = ($type === 'purchase') ? 'purchase_id' : 'invoice_id';
$rawCustomerId = $_POST['customer_id'] ?? '';
$cust_id = ($type === 'sale' && ($rawCustomerId === '' || $rawCustomerId === null)) ? null : (int)$rawCustomerId;
$date = $_POST['invoice_date'] ?: date('Y-m-d');
$due_date = $_POST['due_date'] ?: null;
$status = $_POST['status'] ?? 'pending';
$pay_type = $_POST['payment_type'] ?? 'cash';
$items = $_POST['item_ids'] ?? [];
$qtys = $_POST['quantities'] ?? [];
$prices = $_POST['prices'] ?? [];
$total_subtotal = 0;
$total_vat = 0;
$profitLines = [];
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
$qty = normalize_quantity($qtys[$i] ?? 0);
$price = (float)$prices[$i];
if ($type === 'sale') {
$existingLockedPrice = null;
$existingPriceStmt = $db->prepare("SELECT unit_price FROM $item_table WHERE $fk_col = ? AND item_id = ? LIMIT 1");
$existingPriceStmt->execute([$id, $item_id]);
$existingStoredPrice = $existingPriceStmt->fetchColumn();
if ($existingStoredPrice !== false) {
$existingLockedPrice = (float)$existingStoredPrice;
}
assertEditedSalePriceWithinLastSalePrice($item_id, $price, $existingLockedPrice);
}
$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;
$profitLines[] = [
'item_id' => (int)$item_id,
'qty' => $qty,
'unit_price' => $price,
];
}
$gross_total = $total_subtotal + $total_vat;
$manualDiscountEnabled = getSettingValue('manual_discount_enabled', '0') === '1';
$hasDiscountColumn = ($type === 'sale') && db_column_exists($table, 'discount_amount');
$discount_amount = 0.0;
if ($hasDiscountColumn) {
if ($manualDiscountEnabled && array_key_exists('discount_amount', $_POST)) {
$discount_amount = max(0, (float)($_POST['discount_amount'] ?? 0));
$discountMetrics = calculateManualDiscountProfitMetrics($profitLines, false);
$maxManualDiscount = min(max(0, $gross_total), max(0, (float)($discountMetrics['max_discount'] ?? 0)));
if ($discount_amount > ($maxManualDiscount + 0.0005)) {
throw new Exception(manualDiscountLimitMessage($discountMetrics, $discount_amount));
}
} else {
$existingDiscountStmt = $db->prepare("SELECT discount_amount FROM $table WHERE id = ? LIMIT 1");
$existingDiscountStmt->execute([$id]);
$discount_amount = max(0, (float)$existingDiscountStmt->fetchColumn());
}
if ($discount_amount > $gross_total) {
$discount_amount = $gross_total;
}
}
$total_with_vat = max(0, $gross_total - $discount_amount);
$paid = max(0, (float)($_POST['paid_amount'] ?? 0));
if ($paid > $total_with_vat) {
$paid = $total_with_vat;
}
if ($status === 'paid') $paid = $total_with_vat;
if ($hasDiscountColumn) {
$db->prepare("UPDATE $table SET $cust_supplier_col = ?, invoice_date = ?, due_date = ?, status = ?, payment_type = ?, total_amount = ?, vat_amount = ?, total_with_vat = ?, paid_amount = ?, discount_amount = ? WHERE id = ?")
->execute([$cust_id, $date, $due_date, $status, $pay_type, $total_subtotal, $total_vat, $total_with_vat, $paid, $discount_amount, $id]);
} else {
$db->prepare("UPDATE $table SET $cust_supplier_col = ?, invoice_date = ?, due_date = ?, status = ?, payment_type = ?, total_amount = ?, vat_amount = ?, total_with_vat = ?, paid_amount = ? WHERE id = ?")
->execute([$cust_id, $date, $due_date, $status, $pay_type, $total_subtotal, $total_vat, $total_with_vat, $paid, $id]);
}
if (db_column_exists($table, 'outlet_id')) {
$db->prepare("UPDATE $table SET outlet_id = COALESCE(outlet_id, ?) WHERE id = ?")->execute([current_outlet_id(), $id]);
}
// Revert stock for old items
$stmtOld = $db->prepare("SELECT item_id, quantity FROM $item_table WHERE $fk_col = ?");
$stmtOld->execute([$id]);
$oldItems = $stmtOld->fetchAll();
foreach ($oldItems as $old) {
$change = ($type === 'sale') ? normalize_quantity($old['quantity'] ?? 0) : -normalize_quantity($old['quantity'] ?? 0);
update_stock($old['item_id'], $change);
}
// Delete old items
$db->prepare("DELETE FROM $item_table WHERE $fk_col = ?")->execute([$id]);
// Insert new items and update stock
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
$qty = normalize_quantity($qtys[$i] ?? 0);
$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 ($fk_col, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$id, $item_id, $qty, $price, $vatAmount, $subtotal]);
$change = ($type === 'sale') ? -$qty : $qty;
update_stock($item_id, $change);
}
$db->commit();
$msg = ($type === 'purchase' ? "Purchase" : "Invoice") . " updated successfully!";
redirectWithMessage($msg, page_url($type === 'purchase' ? 'purchases' : 'sales'));
} catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
}