269 lines
14 KiB
PHP
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(); }
|
|
}
|