|
|
|
|
@ -901,7 +901,7 @@ if (!function_exists('sales_purchases_load_logic')) {
|
|
|
|
|
$inv['total_in_words'] = numberToWordsOMR($inv['total_with_vat']);
|
|
|
|
|
|
|
|
|
|
if ($type === 'sale') {
|
|
|
|
|
$item_stmt = db()->prepare('SELECT ii.*, i.name_en, i.name_ar, i.vat_rate FROM invoice_items ii LEFT JOIN stock_items i ON ii.item_id = i.id WHERE ii.invoice_id = ?');
|
|
|
|
|
$item_stmt = db()->prepare('SELECT ii.*, i.name_en, i.name_ar, i.vat_rate, i.last_sale_price FROM invoice_items ii LEFT JOIN stock_items i ON ii.item_id = i.id WHERE ii.invoice_id = ?');
|
|
|
|
|
$item_stmt->execute([$inv['id']]);
|
|
|
|
|
$inv['items'] = $item_stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
|
} else {
|
|
|
|
|
@ -912,7 +912,7 @@ if (!function_exists('sales_purchases_load_logic')) {
|
|
|
|
|
}
|
|
|
|
|
unset($inv);
|
|
|
|
|
|
|
|
|
|
$items_list_raw = db()->query('SELECT i.id, i.name_en, i.name_ar, i.sale_price, i.purchase_price, i.stock_quantity, i.vat_rate, i.is_promotion, i.promotion_start, i.promotion_end, i.promotion_percent FROM stock_items i ORDER BY i.name_en ASC')->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
|
$items_list_raw = db()->query('SELECT i.id, i.name_en, i.name_ar, i.sale_price, i.last_sale_price, i.purchase_price, i.stock_quantity, i.vat_rate, i.is_promotion, i.promotion_start, i.promotion_end, i.promotion_percent FROM stock_items i ORDER BY i.name_en ASC')->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
|
foreach ($items_list_raw as &$item) {
|
|
|
|
|
$item['sale_price'] = getPromotionalPrice($item);
|
|
|
|
|
}
|
|
|
|
|
@ -1861,7 +1861,7 @@ if (isset($_GET['action']) || isset($_POST['action'])) {
|
|
|
|
|
|
|
|
|
|
// Render Card HTML
|
|
|
|
|
?>
|
|
|
|
|
<div class="product-card" data-id="<?= $p['id'] ?>" data-name-en="<?= htmlspecialchars($p['name_en']) ?>" data-name-ar="<?= htmlspecialchars($p['name_ar']) ?>" data-price="<?= $p['sale_price'] ?>" data-purchase-price="<?= (float)$p['purchase_price'] ?>" data-sku="<?= htmlspecialchars($p['sku']) ?>" data-stock-quantity="<?= format_quantity($p['stock_quantity']) ?>" data-vat-rate="<?= $p['vat_rate'] ?>">
|
|
|
|
|
<div class="product-card" data-id="<?= $p['id'] ?>" data-name-en="<?= htmlspecialchars($p['name_en']) ?>" data-name-ar="<?= htmlspecialchars($p['name_ar']) ?>" data-price="<?= $p['sale_price'] ?>" data-last-sale-price="<?= number_format((float)($p['last_sale_price'] ?? 0), 3, '.', '') ?>" data-purchase-price="<?= (float)$p['purchase_price'] ?>" data-sku="<?= htmlspecialchars($p['sku']) ?>" data-stock-quantity="<?= format_quantity($p['stock_quantity']) ?>" data-vat-rate="<?= $p['vat_rate'] ?>">
|
|
|
|
|
<?php if ($p['image_path']): ?>
|
|
|
|
|
<img src="<?= htmlspecialchars($p['image_path']) ?>" alt="<?= htmlspecialchars($p['name_en']) ?>">
|
|
|
|
|
<?php else: ?>
|
|
|
|
|
@ -1909,6 +1909,7 @@ if (isset($_GET['action']) || isset($_POST['action'])) {
|
|
|
|
|
$p['original_price'] = (float)$p['sale_price'];
|
|
|
|
|
$p['sale_price'] = getPromotionalPrice($p);
|
|
|
|
|
$p['price'] = (float)$p['sale_price'];
|
|
|
|
|
$p['lastSalePrice'] = (float)($p['last_sale_price'] ?? 0);
|
|
|
|
|
$p['nameEn'] = $p['name_en'];
|
|
|
|
|
$p['nameAr'] = $p['name_ar'];
|
|
|
|
|
$p['vatRate'] = $p['vat_rate'];
|
|
|
|
|
@ -2008,7 +2009,12 @@ if (isset($_GET['action']) || isset($_POST['action'])) {
|
|
|
|
|
$items = [];
|
|
|
|
|
}
|
|
|
|
|
foreach ($items as $itemIndex => $item) {
|
|
|
|
|
$itemId = (int)($item['id'] ?? 0);
|
|
|
|
|
$itemPrice = max(0, (float)($item['price'] ?? 0));
|
|
|
|
|
$items[$itemIndex]['id'] = $itemId;
|
|
|
|
|
$items[$itemIndex]['qty'] = normalize_quantity($item['qty'] ?? 0);
|
|
|
|
|
$items[$itemIndex]['price'] = $itemPrice;
|
|
|
|
|
assertEditedSalePriceWithinLastSalePrice($itemId, $itemPrice);
|
|
|
|
|
}
|
|
|
|
|
$total_amount = (float)($_POST['total_amount'] ?? 0);
|
|
|
|
|
$tax_amount = (float)($_POST['tax_amount'] ?? 0);
|
|
|
|
|
@ -2180,7 +2186,7 @@ if (isset($_GET['action']) || isset($_POST['action'])) {
|
|
|
|
|
$invoice['party_name'] = $partyName !== '' ? $partyName : '---';
|
|
|
|
|
$invoice['paid_amount'] = (float)($invoice['paid_amount'] ?? 0);
|
|
|
|
|
|
|
|
|
|
$stmtItems = db()->prepare("SELECT li.*, i.name_en, i.name_ar, i.sku, i.vat_rate, i.stock_quantity, i.purchase_price FROM $itemTable li LEFT JOIN stock_items i ON li.item_id = i.id WHERE li.$fkColumn = ?");
|
|
|
|
|
$stmtItems = db()->prepare("SELECT li.*, i.name_en, i.name_ar, i.sku, i.vat_rate, i.stock_quantity, i.purchase_price, i.last_sale_price FROM $itemTable li LEFT JOIN stock_items i ON li.item_id = i.id WHERE li.$fkColumn = ?");
|
|
|
|
|
$stmtItems->execute([$invoice_id]);
|
|
|
|
|
$invoice['items'] = $stmtItems->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
|
|
|
|
|
|
@ -2199,7 +2205,7 @@ if (isset($_GET['action']) || isset($_POST['action'])) {
|
|
|
|
|
LEFT JOIN stock_items i ON pi.item_id = i.id
|
|
|
|
|
WHERE pi.purchase_id = ?");
|
|
|
|
|
} else {
|
|
|
|
|
$stmt = db()->prepare("SELECT ii.*, i.name_en, i.name_ar, i.sku, i.vat_rate, i.stock_quantity
|
|
|
|
|
$stmt = db()->prepare("SELECT ii.*, i.name_en, i.name_ar, i.sku, i.vat_rate, i.stock_quantity, i.last_sale_price
|
|
|
|
|
FROM invoice_items ii
|
|
|
|
|
LEFT JOIN stock_items i ON ii.item_id = i.id
|
|
|
|
|
WHERE ii.invoice_id = ?");
|
|
|
|
|
@ -2956,6 +2962,73 @@ function getPromotionalPrice($item) {
|
|
|
|
|
return $price;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateItemLastSalePrice($itemId, $price): void {
|
|
|
|
|
// Compatibility no-op: last_sale_price is admin-managed only.
|
|
|
|
|
// It represents the minimum allowed sale price and must not be changed by POS/invoice sales.
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function assertEditedSalePriceWithinLastSalePrice($itemId, $price, $lockedAllowedPrice = null): void {
|
|
|
|
|
// last_sale_price is an admin-managed minimum sale price floor.
|
|
|
|
|
// When it is left at 0, cashier/manual price edits are locked to the item sale price.
|
|
|
|
|
$itemId = (int)$itemId;
|
|
|
|
|
$price = (float)$price;
|
|
|
|
|
if ($itemId <= 0 || !db_column_exists('stock_items', 'last_sale_price')) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$stmt = db()->prepare("SELECT name_en, name_ar, sale_price, last_sale_price, is_promotion, promotion_start, promotion_end, promotion_percent FROM stock_items WHERE id = ? LIMIT 1");
|
|
|
|
|
$stmt->execute([$itemId]);
|
|
|
|
|
$item = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
|
if (!$item) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$itemName = trim((string)($item['name_en'] ?? ''));
|
|
|
|
|
if ($itemName === '') {
|
|
|
|
|
$itemName = trim((string)($item['name_ar'] ?? ''));
|
|
|
|
|
}
|
|
|
|
|
if ($itemName === '') {
|
|
|
|
|
$itemName = 'item #' . $itemId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
global $lang;
|
|
|
|
|
$isArabic = (($lang ?? 'en') === 'ar');
|
|
|
|
|
$lastSalePrice = (float)($item['last_sale_price'] ?? 0);
|
|
|
|
|
|
|
|
|
|
if ($lastSalePrice <= 0) {
|
|
|
|
|
$allowedLockedPrices = [
|
|
|
|
|
(float)($item['sale_price'] ?? 0),
|
|
|
|
|
(float)getPromotionalPrice($item),
|
|
|
|
|
];
|
|
|
|
|
if ($lockedAllowedPrice !== null) {
|
|
|
|
|
$allowedLockedPrices[] = (float)$lockedAllowedPrice;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ($allowedLockedPrices as $allowedPrice) {
|
|
|
|
|
if (abs($price - (float)$allowedPrice) <= 0.0005) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($isArabic) {
|
|
|
|
|
throw new Exception('لا يمكن تغيير سعر الصنف ' . $itemName . ' لأن آخر سعر بيع / الحد الأدنى مضبوط على 0.000. يرجى تحديد حد أدنى للسعر أولاً.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new Exception('Price for ' . $itemName . ' cannot be changed because Last Sale Price / minimum sale price is 0.000. Set a minimum price limit first.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($price >= ($lastSalePrice - 0.0005)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($isArabic) {
|
|
|
|
|
throw new Exception('لا يمكن بيع الصنف ' . $itemName . ' بسعر أقل من آخر سعر بيع / الحد الأدنى (' . number_format($lastSalePrice, 3) . ').');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new Exception('Price for ' . $itemName . ' cannot be below Last Sale Price / minimum sale price (OMR ' . number_format($lastSalePrice, 3) . ').');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Inventory & Core Handlers ---
|
|
|
|
|
if (isset($_POST['add_item'])) {
|
|
|
|
|
$name_en = $_POST['name_en'] ?? '';
|
|
|
|
|
@ -2968,6 +3041,10 @@ function getPromotionalPrice($item) {
|
|
|
|
|
redirectWithMessage($sku_error, 'index.php?page=items');
|
|
|
|
|
}
|
|
|
|
|
$sale_price = (float)($_POST['sale_price'] ?? 0);
|
|
|
|
|
$last_sale_price = (float)($_POST['last_sale_price'] ?? 0);
|
|
|
|
|
if ($sale_price > 0 && $last_sale_price > ($sale_price + 0.0005)) {
|
|
|
|
|
redirectWithMessage('Last Sale Price (Minimum) cannot be higher than Sale Price.', 'index.php?page=items');
|
|
|
|
|
}
|
|
|
|
|
$purchase_price = (float)($_POST['purchase_price'] ?? 0);
|
|
|
|
|
$stock_quantity = normalize_quantity($_POST['stock_quantity'] ?? 0);
|
|
|
|
|
$min_stock_level = normalize_quantity($_POST['min_stock_level'] ?? 0);
|
|
|
|
|
@ -2986,8 +3063,29 @@ function getPromotionalPrice($item) {
|
|
|
|
|
if (move_uploaded_file($_FILES['image']['tmp_name'], $filename)) $image_path = $filename;
|
|
|
|
|
}
|
|
|
|
|
$current_oid = current_outlet_id();
|
|
|
|
|
$stmt = db()->prepare("INSERT INTO stock_items (outlet_id, name_en, name_ar, category_id, unit_id, supplier_id, sku, sale_price, purchase_price, stock_quantity, min_stock_level, image_path, vat_rate, expiry_date, is_promotion, promotion_start, promotion_end, promotion_percent) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
|
|
|
$stmt->execute([$current_oid, $name_en, $name_ar, $category_id, $unit_id, $supplier_id, $sku, $sale_price, $purchase_price, $stock_quantity, $min_stock_level, $image_path, $vat_rate, $expiry_date, $is_promotion, $promotion_start, $promotion_end, $promotion_percent]);
|
|
|
|
|
[$stockItemInsertSql, $stockItemInsertValues] = db_insert_sql_for_existing_columns('stock_items', [
|
|
|
|
|
'outlet_id' => $current_oid,
|
|
|
|
|
'name_en' => $name_en,
|
|
|
|
|
'name_ar' => $name_ar,
|
|
|
|
|
'category_id' => $category_id,
|
|
|
|
|
'unit_id' => $unit_id,
|
|
|
|
|
'supplier_id' => $supplier_id,
|
|
|
|
|
'sku' => $sku,
|
|
|
|
|
'sale_price' => $sale_price,
|
|
|
|
|
'last_sale_price' => $last_sale_price,
|
|
|
|
|
'purchase_price' => $purchase_price,
|
|
|
|
|
'stock_quantity' => $stock_quantity,
|
|
|
|
|
'min_stock_level' => $min_stock_level,
|
|
|
|
|
'image_path' => $image_path,
|
|
|
|
|
'vat_rate' => $vat_rate,
|
|
|
|
|
'expiry_date' => $expiry_date,
|
|
|
|
|
'is_promotion' => $is_promotion,
|
|
|
|
|
'promotion_start' => $promotion_start,
|
|
|
|
|
'promotion_end' => $promotion_end,
|
|
|
|
|
'promotion_percent' => $promotion_percent,
|
|
|
|
|
]);
|
|
|
|
|
$stmt = db()->prepare($stockItemInsertSql);
|
|
|
|
|
$stmt->execute($stockItemInsertValues);
|
|
|
|
|
$new_item_id = db()->lastInsertId();
|
|
|
|
|
redirectWithMessage("Item added successfully!");
|
|
|
|
|
}
|
|
|
|
|
@ -3004,6 +3102,10 @@ function getPromotionalPrice($item) {
|
|
|
|
|
redirectWithMessage($sku_error, 'index.php?page=items');
|
|
|
|
|
}
|
|
|
|
|
$sale_price = (float)($_POST['sale_price'] ?? 0);
|
|
|
|
|
$last_sale_price = (float)($_POST['last_sale_price'] ?? 0);
|
|
|
|
|
if ($sale_price > 0 && $last_sale_price > ($sale_price + 0.0005)) {
|
|
|
|
|
redirectWithMessage('Last Sale Price (Minimum) cannot be higher than Sale Price.', 'index.php?page=items');
|
|
|
|
|
}
|
|
|
|
|
$purchase_price = (float)($_POST['purchase_price'] ?? 0);
|
|
|
|
|
$stock_quantity = normalize_quantity($_POST['stock_quantity'] ?? 0);
|
|
|
|
|
$min_stock_level = normalize_quantity($_POST['min_stock_level'] ?? 0);
|
|
|
|
|
@ -3014,8 +3116,32 @@ function getPromotionalPrice($item) {
|
|
|
|
|
$promotion_end = !empty($_POST['promotion_end']) ? $_POST['promotion_end'] : null;
|
|
|
|
|
$promotion_percent = (float)($_POST['promotion_percent'] ?? 0);
|
|
|
|
|
// Update stock_items
|
|
|
|
|
$stmt = db()->prepare("UPDATE stock_items SET name_en = ?, name_ar = ?, category_id = ?, unit_id = ?, supplier_id = ?, sku = ?, sale_price = ?, purchase_price = ?, stock_quantity = ?, min_stock_level = ?, vat_rate = ?, expiry_date = ?, is_promotion = ?, promotion_start = ?, promotion_end = ?, promotion_percent = ? WHERE id = ?");
|
|
|
|
|
$stmt->execute([$name_en, $name_ar, $category_id, $unit_id, $supplier_id, $sku, $sale_price, $purchase_price, $stock_quantity, $min_stock_level, $vat_rate, $expiry_date, $is_promotion, $promotion_start, $promotion_end, $promotion_percent, $id]);
|
|
|
|
|
$stockItemUpdateValues = [
|
|
|
|
|
'name_en' => $name_en,
|
|
|
|
|
'name_ar' => $name_ar,
|
|
|
|
|
'category_id' => $category_id,
|
|
|
|
|
'unit_id' => $unit_id,
|
|
|
|
|
'supplier_id' => $supplier_id,
|
|
|
|
|
'sku' => $sku,
|
|
|
|
|
'sale_price' => $sale_price,
|
|
|
|
|
'purchase_price' => $purchase_price,
|
|
|
|
|
'stock_quantity' => $stock_quantity,
|
|
|
|
|
'min_stock_level' => $min_stock_level,
|
|
|
|
|
'vat_rate' => $vat_rate,
|
|
|
|
|
'expiry_date' => $expiry_date,
|
|
|
|
|
'is_promotion' => $is_promotion,
|
|
|
|
|
'promotion_start' => $promotion_start,
|
|
|
|
|
'promotion_end' => $promotion_end,
|
|
|
|
|
'promotion_percent' => $promotion_percent,
|
|
|
|
|
];
|
|
|
|
|
if (db_column_exists('stock_items', 'last_sale_price')) {
|
|
|
|
|
$stockItemUpdateValues['last_sale_price'] = $last_sale_price;
|
|
|
|
|
}
|
|
|
|
|
$stockItemUpdateSet = implode(', ', array_map(static fn(string $column): string => "`{$column}` = ?", array_keys($stockItemUpdateValues)));
|
|
|
|
|
$stockItemUpdateParams = array_values($stockItemUpdateValues);
|
|
|
|
|
$stockItemUpdateParams[] = $id;
|
|
|
|
|
$stmt = db()->prepare("UPDATE stock_items SET {$stockItemUpdateSet} WHERE id = ?");
|
|
|
|
|
$stmt->execute($stockItemUpdateParams);
|
|
|
|
|
if (isset($_FILES['image']) && $_FILES['image']['error'] === 0) {
|
|
|
|
|
$ext = pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION);
|
|
|
|
|
$filename = 'uploads/item_' . $id . '_' . time() . '.' . $ext;
|
|
|
|
|
@ -7432,6 +7558,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
|
|
|
|
<th data-en="Category" data-ar="الفئة">Category</th>
|
|
|
|
|
<th data-en="Supplier" data-ar="المورد">Supplier</th>
|
|
|
|
|
<th data-en="Sale Price" data-ar="سعر البيع">Sale Price</th>
|
|
|
|
|
<th data-en="Last Sale Price (Minimum)" data-ar="آخر سعر بيع (حد أدنى)">Last Sale Price (Minimum)</th>
|
|
|
|
|
<th data-en="Stock Level" data-ar="المخزون">Stock Level</th>
|
|
|
|
|
<th data-en="Expiry" data-ar="تاريخ الانتهاء">Expiry</th>
|
|
|
|
|
<th data-en="VAT" data-ar="الضريبة">VAT</th>
|
|
|
|
|
@ -7472,6 +7599,9 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
|
|
|
|
<div class="small text-muted" data-en="Incl. VAT: <?= number_format((float)$itemBarcodePrice, 3) ?>" data-ar="شامل الضريبة: <?= number_format((float)$itemBarcodePrice, 3) ?>">Incl. VAT: <?= number_format((float)$itemBarcodePrice, 3) ?></div>
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
<td class="text-end">
|
|
|
|
|
<strong><?= number_format((float)($item['last_sale_price'] ?? 0), 3) ?></strong>
|
|
|
|
|
</td>
|
|
|
|
|
<td>
|
|
|
|
|
<div class="text-end">
|
|
|
|
|
<strong><?= format_quantity($item['stock_quantity']) ?></strong>
|
|
|
|
|
@ -7526,6 +7656,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
|
|
|
|
<tr><th class="text-muted" data-en="Category" data-ar="الفئة">Category</th><td><?= htmlspecialchars($item['cat_en'] ?? '---') ?></td></tr>
|
|
|
|
|
<tr><th class="text-muted" data-en="Supplier" data-ar="المورد">Supplier</th><td><?= htmlspecialchars($item['supplier_name'] ?? '---') ?></td></tr>
|
|
|
|
|
<tr><th class="text-muted">Sale Price</th><td>OMR <?= number_format((float)$item['sale_price'], 3) ?></td></tr>
|
|
|
|
|
<tr><th class="text-muted">Last Sale Price (Minimum)</th><td>OMR <?= number_format((float)($item['last_sale_price'] ?? 0), 3) ?></td></tr>
|
|
|
|
|
<tr><th class="text-muted">Stock Level</th><td><?= format_quantity($item['stock_quantity']) ?></td></tr>
|
|
|
|
|
<tr><th class="text-muted">VAT Rate</th><td><?= number_format((float)$item['vat_rate'], 2) ?>%</td></tr>
|
|
|
|
|
</table>
|
|
|
|
|
@ -7559,8 +7690,9 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
|
|
|
|
<div class="col-md-4"><label class="form-label" data-en="Category" data-ar="الفئة">Category</label><select name="category_id" class="form-select"><option value="" data-en="---" data-ar="---">---</option><?php foreach ($data['categories'] ?? [] as $c): ?><option value="<?= $c['id'] ?>" <?= $c['id'] == $item['category_id'] ? 'selected' : '' ?> data-en="<?= htmlspecialchars(localized_option_label($c, 'en')) ?>" data-ar="<?= htmlspecialchars(localized_option_label($c, 'ar')) ?>"><?= htmlspecialchars(localized_option_label($c)) ?></option><?php endforeach; ?></select></div>
|
|
|
|
|
<div class="col-md-4"><label class="form-label" data-en="Unit" data-ar="الوحدة">Unit</label><select name="unit_id" class="form-select"><option value="" data-en="---" data-ar="---">---</option><?php foreach ($data['units'] ?? [] as $u): ?><option value="<?= $u['id'] ?>" <?= $u['id'] == $item['unit_id'] ? 'selected' : '' ?> data-en="<?= htmlspecialchars(localized_option_label($u, 'en')) ?>" data-ar="<?= htmlspecialchars(localized_option_label($u, 'ar')) ?>"><?= htmlspecialchars(localized_option_label($u)) ?></option><?php endforeach; ?></select></div>
|
|
|
|
|
<div class="col-md-4"><label class="form-label" data-en="Supplier" data-ar="المورد">Supplier</label><select name="supplier_id" class="form-select"><option value="">---</option><?php foreach ($data['suppliers'] ?? [] as $s): ?><option value="<?= $s['id'] ?>" <?= $s['id'] == $item['supplier_id'] ? 'selected' : '' ?>><?= htmlspecialchars($s['name']) ?></option><?php endforeach; ?></select></div>
|
|
|
|
|
<div class="col-md-4"><label class="form-label" data-en="Sale Price" data-ar="سعر البيع">Sale Price</label><input type="number" step="0.001" name="sale_price" class="form-control" value="<?= (float)$item['sale_price'] ?>"></div>
|
|
|
|
|
<div class="col-md-4"><label class="form-label" data-en="Purchase Price" data-ar="سعر الشراء">Purchase Price</label><input type="number" step="0.001" name="purchase_price" class="form-control" value="<?= (float)$item['purchase_price'] ?>"></div>
|
|
|
|
|
<div class="col-md-4"><label class="form-label" data-en="Sale Price" data-ar="سعر البيع">Sale Price</label><input type="number" step="0.001" min="0" name="sale_price" class="form-control" value="<?= (float)$item['sale_price'] ?>"></div>
|
|
|
|
|
<div class="col-md-4"><label class="form-label" data-en="Last Sale Price (Minimum)" data-ar="آخر سعر بيع (حد أدنى)">Last Sale Price (Minimum)</label><input type="number" step="0.001" min="0" name="last_sale_price" class="form-control" value="<?= number_format((float)($item['last_sale_price'] ?? 0), 3, '.', '') ?>"><div class="form-text" data-en="Admin-only minimum selling price; POS and sales cannot sell below this value." data-ar="حد أدنى للبيع يحدده المدير؛ لا يمكن البيع في نقاط البيع أو المبيعات بأقل منه.">Admin-only minimum selling price; POS and sales cannot sell below this value.</div></div>
|
|
|
|
|
<div class="col-md-4"><label class="form-label" data-en="Purchase Price" data-ar="سعر الشراء">Purchase Price</label><input type="number" step="0.001" min="0" name="purchase_price" class="form-control" value="<?= (float)$item['purchase_price'] ?>"></div>
|
|
|
|
|
<div class="col-md-4"><label class="form-label" data-en="Stock Qty" data-ar="كمية المخزون">Stock Qty</label><input type="number" step="<?= quantity_step() ?>" name="stock_quantity" class="form-control" value="<?= format_quantity($item['stock_quantity']) ?>"></div>
|
|
|
|
|
<div class="col-md-4"><label class="form-label" data-en="Min Stock Level" data-ar="الحد الأدنى للمخزون">Min Stock Level</label><input type="number" step="<?= quantity_step() ?>" name="min_stock_level" class="form-control" value="<?= format_quantity($item['min_stock_level']) ?>"></div>
|
|
|
|
|
<div class="col-md-4"><label class="form-label" data-en="VAT Rate (%)" data-ar="ضريبة القيمة المضافة (%)">VAT Rate (%)</label><input type="number" step="0.01" name="vat_rate" class="form-control" value="<?= number_format((float)$item['vat_rate'], 2, '.', '') ?>"></div>
|
|
|
|
|
@ -7838,7 +7970,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
|
|
|
|
</div>
|
|
|
|
|
<div class="product-grid" id="productGrid">
|
|
|
|
|
<?php foreach ($products as $p): ?>
|
|
|
|
|
<div class="product-card" data-id="<?= $p['id'] ?>" data-name-en="<?= htmlspecialchars($p['name_en']) ?>" data-name-ar="<?= htmlspecialchars($p['name_ar']) ?>" data-price="<?= $p['sale_price'] ?>" data-purchase-price="<?= (float)$p['purchase_price'] ?>" data-sku="<?= htmlspecialchars($p['sku']) ?>" data-stock-quantity="<?= format_quantity($p['stock_quantity']) ?>" data-vat-rate="<?= $p['vat_rate'] ?>">
|
|
|
|
|
<div class="product-card" data-id="<?= $p['id'] ?>" data-name-en="<?= htmlspecialchars($p['name_en']) ?>" data-name-ar="<?= htmlspecialchars($p['name_ar']) ?>" data-price="<?= $p['sale_price'] ?>" data-last-sale-price="<?= number_format((float)($p['last_sale_price'] ?? 0), 3, '.', '') ?>" data-purchase-price="<?= (float)$p['purchase_price'] ?>" data-sku="<?= htmlspecialchars($p['sku']) ?>" data-stock-quantity="<?= format_quantity($p['stock_quantity']) ?>" data-vat-rate="<?= $p['vat_rate'] ?>">
|
|
|
|
|
<?php if ($p['image_path']): ?>
|
|
|
|
|
<img src="<?= htmlspecialchars($p['image_path']) ?>" alt="<?= htmlspecialchars($p['name_en']) ?>">
|
|
|
|
|
<?php else: ?>
|
|
|
|
|
@ -8156,17 +8288,28 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
|
|
|
|
const purchasePrice = (product.purchasePrice !== undefined && product.purchasePrice !== null)
|
|
|
|
|
? (parseFloat(product.purchasePrice) || 0)
|
|
|
|
|
: (parseFloat(product.purchase_price) || 0);
|
|
|
|
|
const normalizedProduct = {...product, price: unitPrice, purchasePrice};
|
|
|
|
|
const lastSalePrice = (product.lastSalePrice !== undefined && product.lastSalePrice !== null)
|
|
|
|
|
? (parseFloat(product.lastSalePrice) || 0)
|
|
|
|
|
: (parseFloat(product.last_sale_price) || 0);
|
|
|
|
|
const normalizedProduct = {...product, price: unitPrice, originalPrice: unitPrice, purchasePrice, lastSalePrice, priceEdited: false};
|
|
|
|
|
|
|
|
|
|
const existing = this.items.find(item => item.id === product.id);
|
|
|
|
|
const existing = this.items.find(item => Number(item.id) === Number(product.id));
|
|
|
|
|
if (existing) {
|
|
|
|
|
if (!allowZeroStock && (existing.qty + addQty) > currentStock) {
|
|
|
|
|
Swal.fire(posT('Error', 'خطأ'), posT('Insufficient stock!', 'المخزون غير كافٍ!'), 'error');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
existing.qty = normalizeQuantity(existing.qty + addQty);
|
|
|
|
|
existing.price = unitPrice;
|
|
|
|
|
if (!Number.isFinite(parseFloat(existing.price))) {
|
|
|
|
|
existing.price = unitPrice;
|
|
|
|
|
}
|
|
|
|
|
existing.purchasePrice = purchasePrice;
|
|
|
|
|
if (!Number.isFinite(parseFloat(existing.lastSalePrice))) {
|
|
|
|
|
existing.lastSalePrice = lastSalePrice;
|
|
|
|
|
}
|
|
|
|
|
if (!Number.isFinite(parseFloat(existing.originalPrice))) {
|
|
|
|
|
existing.originalPrice = unitPrice;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (!allowZeroStock && currentStock < addQty) {
|
|
|
|
|
Swal.fire(posT('Error', 'خطأ'), posT('Insufficient stock!', 'المخزون غير كافٍ!'), 'error');
|
|
|
|
|
@ -8194,7 +8337,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
|
|
|
|
},
|
|
|
|
|
updateQty(id, delta) {
|
|
|
|
|
if (!this.items) return;
|
|
|
|
|
const item = this.items.find(i => i.id === id);
|
|
|
|
|
const item = this.items.find(i => Number(i.id) === Number(id));
|
|
|
|
|
if (item) {
|
|
|
|
|
const allowZeroStock = (typeof companySettings !== 'undefined' && String(companySettings.allow_zero_stock_sell) === '1');
|
|
|
|
|
const currentStock = normalizeQuantity(item.stock_quantity);
|
|
|
|
|
@ -8209,6 +8352,82 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
|
|
|
|
else this.render();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
updatePrice(id, value) {
|
|
|
|
|
if (!this.items) return;
|
|
|
|
|
const item = this.items.find(i => Number(i.id) === Number(id));
|
|
|
|
|
if (!item) return;
|
|
|
|
|
|
|
|
|
|
const nextPrice = parseFloat(value);
|
|
|
|
|
const previousPrice = parseFloat(item.price) || 0;
|
|
|
|
|
if (!Number.isFinite(nextPrice) || nextPrice < 0) {
|
|
|
|
|
Swal.fire(posT('Invalid price', 'سعر غير صالح'), posT('Please enter a valid price.', 'يرجى إدخال سعر صالح.'), 'error');
|
|
|
|
|
this.render();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const lastSalePrice = parseFloat(item.lastSalePrice ?? item.last_sale_price ?? 0) || 0;
|
|
|
|
|
const originalPrice = parseFloat(item.originalPrice ?? item.original_price ?? previousPrice) || 0;
|
|
|
|
|
|
|
|
|
|
if (lastSalePrice <= 0 && Math.abs(nextPrice - originalPrice) > 0.0005) {
|
|
|
|
|
Swal.fire(
|
|
|
|
|
posT('Price locked', 'السعر مقفل'),
|
|
|
|
|
posIsArabic
|
|
|
|
|
? 'لا يمكن تغيير السعر لأن آخر سعر بيع / الحد الأدنى مضبوط على 0.000. يرجى تحديد حد أدنى للسعر أولاً.'
|
|
|
|
|
: 'Price changes are disabled because Last Sale Price / minimum sale price is 0.000. Set a minimum price limit first.',
|
|
|
|
|
'error'
|
|
|
|
|
);
|
|
|
|
|
item.price = originalPrice;
|
|
|
|
|
item.priceEdited = false;
|
|
|
|
|
this.render();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (lastSalePrice > 0 && nextPrice < (lastSalePrice - 0.0005)) {
|
|
|
|
|
Swal.fire(
|
|
|
|
|
posT('Price rejected', 'تم رفض السعر'),
|
|
|
|
|
posIsArabic
|
|
|
|
|
? `لا يمكن أن يكون السعر أقل من آخر سعر بيع / الحد الأدنى (${lastSalePrice.toFixed(3)}).`
|
|
|
|
|
: `Price cannot be below Last Sale Price / minimum sale price (OMR ${lastSalePrice.toFixed(3)}).`,
|
|
|
|
|
'error'
|
|
|
|
|
);
|
|
|
|
|
item.price = previousPrice;
|
|
|
|
|
this.render();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
item.price = nextPrice;
|
|
|
|
|
item.priceEdited = Math.abs(nextPrice - originalPrice) > 0.0005;
|
|
|
|
|
this.render();
|
|
|
|
|
},
|
|
|
|
|
validatePriceEdits() {
|
|
|
|
|
if (!Array.isArray(this.items)) return true;
|
|
|
|
|
for (const item of this.items) {
|
|
|
|
|
const lastSalePrice = parseFloat(item.lastSalePrice ?? item.last_sale_price ?? 0) || 0;
|
|
|
|
|
const price = parseFloat(item.price) || 0;
|
|
|
|
|
const originalPrice = parseFloat(item.originalPrice ?? item.original_price ?? price) || 0;
|
|
|
|
|
if (lastSalePrice <= 0 && Math.abs(price - originalPrice) > 0.0005) {
|
|
|
|
|
Swal.fire(
|
|
|
|
|
posT('Price locked', 'السعر مقفل'),
|
|
|
|
|
posIsArabic
|
|
|
|
|
? 'لا يمكن تغيير السعر لأن آخر سعر بيع / الحد الأدنى مضبوط على 0.000. يرجى تحديد حد أدنى للسعر أولاً.'
|
|
|
|
|
: 'Price changes are disabled because Last Sale Price / minimum sale price is 0.000. Set a minimum price limit first.',
|
|
|
|
|
'error'
|
|
|
|
|
);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (lastSalePrice > 0 && price < (lastSalePrice - 0.0005)) {
|
|
|
|
|
Swal.fire(
|
|
|
|
|
posT('Price rejected', 'تم رفض السعر'),
|
|
|
|
|
posIsArabic
|
|
|
|
|
? `لا يمكن أن يكون السعر أقل من آخر سعر بيع / الحد الأدنى (${lastSalePrice.toFixed(3)}).`
|
|
|
|
|
: `Price cannot be below Last Sale Price / minimum sale price (OMR ${lastSalePrice.toFixed(3)}).`,
|
|
|
|
|
'error'
|
|
|
|
|
);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
},
|
|
|
|
|
clear() {
|
|
|
|
|
this.items = [];
|
|
|
|
|
this.discount = null;
|
|
|
|
|
@ -8466,6 +8685,14 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
|
|
|
|
const price = parseFloat(item.price) || 0;
|
|
|
|
|
const qty = normalizeQuantity(item.qty);
|
|
|
|
|
const itemTotal = price * qty;
|
|
|
|
|
const lastSalePrice = parseFloat(item.lastSalePrice ?? item.last_sale_price ?? 0) || 0;
|
|
|
|
|
const priceLocked = lastSalePrice <= 0;
|
|
|
|
|
const priceHelp = priceLocked
|
|
|
|
|
? `<div class="form-text smaller text-muted mt-1">${posT('Price locked until minimum is set', 'السعر مقفل حتى يتم تحديد الحد الأدنى')}</div>`
|
|
|
|
|
: `<div class="form-text smaller text-muted mt-1">${posT('Minimum price', 'الحد الأدنى')}: ${lastSalePrice.toFixed(3)}</div>`;
|
|
|
|
|
const priceLockAttr = priceLocked ? ' readonly' : '';
|
|
|
|
|
const priceClass = priceLocked ? ' bg-light' : '';
|
|
|
|
|
const priceMinAttr = priceLocked ? '0' : lastSalePrice.toFixed(3);
|
|
|
|
|
subtotal += itemTotal;
|
|
|
|
|
const vatRate = (item.vatRate !== undefined && item.vatRate !== null) ? item.vatRate : 0;
|
|
|
|
|
const itemVat = itemTotal * (vatRate / (100 + vatRate));
|
|
|
|
|
@ -8480,7 +8707,11 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
|
|
|
|
<div class="cart-item">
|
|
|
|
|
<div class="flex-grow-1">
|
|
|
|
|
<div class="small">${nameHtml}</div>
|
|
|
|
|
<div class="text-muted smaller"><?= __('currency') ?> ${price.toFixed(3)} <span class="badge bg-light text-dark smaller">${posT('VAT', 'الضريبة')} ${parseFloat(vatRate || 0).toFixed(2)}%</span></div>
|
|
|
|
|
<div class="mt-1" style="max-width: 220px;">
|
|
|
|
|
<input type="number" step="0.001" min="${priceMinAttr}" class="form-control form-control-sm text-end pos-cart-price-input${priceClass}" value="${price.toFixed(3)}" onchange="cart.updatePrice(${item.id}, this.value)" aria-label="${posT('Unit price', 'سعر الوحدة')}"${priceLockAttr}>
|
|
|
|
|
${priceHelp}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="text-muted smaller mt-1"><span class="badge bg-light text-dark smaller">${posT('VAT', 'الضريبة')} ${parseFloat(vatRate || 0).toFixed(2)}%</span></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="qty-controls mx-3">
|
|
|
|
|
<button class="qty-btn" onclick="cart.updateQty(${item.id}, -1)">-</button>
|
|
|
|
|
@ -8534,6 +8765,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
|
|
|
|
},
|
|
|
|
|
async checkout() {
|
|
|
|
|
if (this.items.length === 0) return;
|
|
|
|
|
if (!this.validatePriceEdits()) return;
|
|
|
|
|
|
|
|
|
|
const customerSelect = document.getElementById('posCustomer');
|
|
|
|
|
const customerName = customerSelect.options[customerSelect.selectedIndex].text;
|
|
|
|
|
@ -8676,6 +8908,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
|
|
|
|
Swal.fire(<?= json_encode($lang === 'ar' ? 'خطأ' : 'Error', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>, <?= json_encode($lang === 'ar' ? 'السلة فارغة' : 'Cart is empty', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>, 'error');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!this.validatePriceEdits()) return;
|
|
|
|
|
|
|
|
|
|
// If there's an amount in the input and payments are not enough, add it
|
|
|
|
|
const remainingBefore = this.getRemaining();
|
|
|
|
|
@ -8949,6 +9182,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
|
|
|
|
nameEn: card.dataset.nameEn,
|
|
|
|
|
nameAr: card.dataset.nameAr,
|
|
|
|
|
price: parseFloat(card.dataset.price),
|
|
|
|
|
lastSalePrice: parseFloat(card.dataset.lastSalePrice) || 0,
|
|
|
|
|
purchasePrice: parseFloat(card.dataset.purchasePrice) || 0,
|
|
|
|
|
sku: card.dataset.sku,
|
|
|
|
|
stock_quantity: parseFloat(card.dataset.stockQuantity),
|
|
|
|
|
@ -12036,8 +12270,9 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
|
|
|
|
<div class="col-md-4"><label class="form-label" data-en="Category" data-ar="الفئة">Category</label><select name="category_id" class="form-select"><option value="" data-en="---" data-ar="---">---</option><?php foreach ($data['categories'] ?? [] as $c): ?><option value="<?= $c['id'] ?>" data-en="<?= htmlspecialchars(localized_option_label($c, 'en')) ?>" data-ar="<?= htmlspecialchars(localized_option_label($c, 'ar')) ?>"><?= htmlspecialchars(localized_option_label($c)) ?></option><?php endforeach; ?></select></div>
|
|
|
|
|
<div class="col-md-4"><label class="form-label" data-en="Unit" data-ar="الوحدة">Unit</label><select name="unit_id" class="form-select"><option value="" data-en="---" data-ar="---">---</option><?php foreach ($data['units'] ?? [] as $u): ?><option value="<?= $u['id'] ?>" data-en="<?= htmlspecialchars(localized_option_label($u, 'en')) ?>" data-ar="<?= htmlspecialchars(localized_option_label($u, 'ar')) ?>"><?= htmlspecialchars(localized_option_label($u)) ?></option><?php endforeach; ?></select></div>
|
|
|
|
|
<div class="col-md-4"><label class="form-label" data-en="Supplier" data-ar="المورد">Supplier</label><select name="supplier_id" class="form-select"><option value="">---</option><?php foreach ($data['suppliers'] ?? [] as $s): ?><option value="<?= $s['id'] ?>"><?= htmlspecialchars($s['name']) ?></option><?php endforeach; ?></select></div>
|
|
|
|
|
<div class="col-md-4"><label class="form-label" data-en="Sale Price" data-ar="سعر البيع">Sale Price</label><input type="number" step="0.001" name="sale_price" class="form-control" value="0.000"></div>
|
|
|
|
|
<div class="col-md-4"><label class="form-label" data-en="Purchase Price" data-ar="سعر الشراء">Purchase Price</label><input type="number" step="0.001" name="purchase_price" class="form-control" value="0.000"></div>
|
|
|
|
|
<div class="col-md-4"><label class="form-label" data-en="Sale Price" data-ar="سعر البيع">Sale Price</label><input type="number" step="0.001" min="0" name="sale_price" class="form-control" value="0.000"></div>
|
|
|
|
|
<div class="col-md-4"><label class="form-label" data-en="Last Sale Price (Minimum)" data-ar="آخر سعر بيع (حد أدنى)">Last Sale Price (Minimum)</label><input type="number" step="0.001" min="0" name="last_sale_price" class="form-control" value="0.000"><div class="form-text" data-en="Admin-only minimum selling price; POS and sales cannot sell below this value." data-ar="حد أدنى للبيع يحدده المدير؛ لا يمكن البيع في نقاط البيع أو المبيعات بأقل منه.">Admin-only minimum selling price; POS and sales cannot sell below this value.</div></div>
|
|
|
|
|
<div class="col-md-4"><label class="form-label" data-en="Purchase Price" data-ar="سعر الشراء">Purchase Price</label><input type="number" step="0.001" min="0" name="purchase_price" class="form-control" value="0.000"></div>
|
|
|
|
|
<div class="col-md-4"><label class="form-label" data-en="Initial Stock" data-ar="المخزون الحالي">Initial Stock</label><input type="number" step="<?= quantity_step() ?>" name="stock_quantity" class="form-control" value="0.00"></div>
|
|
|
|
|
<div class="col-md-4"><label class="form-label" data-en="Min Stock Level" data-ar="الحد الأدنى للمخزون">Min Stock Level</label><input type="number" step="<?= quantity_step() ?>" name="min_stock_level" class="form-control" value="0.00"></div>
|
|
|
|
|
<div class="col-md-4"><label class="form-label" data-en="VAT Rate (%)" data-ar="ضريبة القيمة المضافة (%)">VAT Rate (%)</label><input type="number" step="0.01" name="vat_rate" class="form-control" value="0"></div>
|
|
|
|
|
|