376 lines
22 KiB
PHP
376 lines
22 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
require_once __DIR__ . '/app.php';
|
|
require_permission('orders');
|
|
|
|
function sales_unit_price(array $customer, array $product): float {
|
|
$customerPayload = $customer['payload_data'];
|
|
$productPayload = $product['payload_data'];
|
|
$sku = (string)($productPayload['sku'] ?? $product['code']);
|
|
$overrides = $customerPayload['price_overrides'] ?? [];
|
|
if (isset($overrides[$sku])) {
|
|
return (float)$overrides[$sku];
|
|
}
|
|
return (float)($productPayload['sale_price'] ?? 0);
|
|
}
|
|
|
|
function validate_sales_input(?array $customer, ?array $product, string $branch, float $qty, array &$errors): void {
|
|
if (!$customer) {
|
|
$errors[] = 'اختر عميلًا صحيحًا.';
|
|
}
|
|
if (!$product) {
|
|
$errors[] = 'اختر صنفًا صحيحًا.';
|
|
}
|
|
if ($qty <= 0) {
|
|
$errors[] = 'أدخل كمية أكبر من صفر.';
|
|
}
|
|
if ($customer) {
|
|
$customerPayload = $customer['payload_data'];
|
|
$branches = array_values($customerPayload['branches'] ?? []);
|
|
if ($branches && $branch === '') {
|
|
$errors[] = 'اختر فرع العميل.';
|
|
}
|
|
$allowedSkus = array_values($customerPayload['allowed_skus'] ?? []);
|
|
if ($product && $allowedSkus) {
|
|
$sku = (string)(($product['payload_data']['sku'] ?? null) ?: $product['code']);
|
|
if (!in_array($sku, $allowedSkus, true)) {
|
|
$errors[] = 'هذا الصنف غير مسموح لهذا العميل.';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function build_sales_payload(array $customer, array $product, string $branch, float $qty, string $notes, string $documentType, ?array $source = null): array {
|
|
$productPayload = $product['payload_data'];
|
|
$unitPrice = sales_unit_price($customer, $product);
|
|
$subtotal = $qty * $unitPrice;
|
|
$vat = $subtotal * 0.15;
|
|
$grand = $subtotal + $vat;
|
|
|
|
return [
|
|
'customer_id' => (int)$customer['id'],
|
|
'customer_name' => $customer['title'],
|
|
'branch' => $branch,
|
|
'product_id' => (int)$product['id'],
|
|
'product_name' => $product['title'],
|
|
'sku' => $productPayload['sku'] ?? $product['code'],
|
|
'unit' => $productPayload['unit'] ?? 'وحدة',
|
|
'qty' => $qty,
|
|
'unit_price' => $unitPrice,
|
|
'subtotal' => $subtotal,
|
|
'vat' => $vat,
|
|
'grand_total' => $grand,
|
|
'notes' => $notes,
|
|
'document_type' => $documentType,
|
|
'document_label' => sales_document_label($documentType),
|
|
'created_date' => date('Y-m-d H:i'),
|
|
'created_by' => current_user()['username'] ?? 'system',
|
|
'source_document_id' => (int)($source['id'] ?? 0),
|
|
'source_document_code' => $source['code'] ?? null,
|
|
'source_document_type' => $source['record_type'] ?? null,
|
|
'source_document_label' => $source ? sales_document_label((string)$source['record_type']) : null,
|
|
'inventory_effect' => $documentType === 'sales_order' ? 'deducted' : 'none',
|
|
];
|
|
}
|
|
|
|
$errors = [];
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
verify_csrf();
|
|
$action = (string)($_POST['form_action'] ?? 'create_document');
|
|
|
|
if ($action === 'create_document') {
|
|
$documentType = (string)($_POST['document_type'] ?? 'sales_order');
|
|
$customerId = (int)($_POST['customer_id'] ?? 0);
|
|
$branch = trim((string)($_POST['branch'] ?? ''));
|
|
$productId = (int)($_POST['product_id'] ?? 0);
|
|
$qty = (float)($_POST['qty'] ?? 0);
|
|
$notes = trim((string)($_POST['notes'] ?? ''));
|
|
|
|
if (!in_array($documentType, ['sales_quote', 'sales_order'], true)) {
|
|
$errors[] = 'نوع المستند غير صحيح.';
|
|
}
|
|
|
|
$customer = fetch_record('customer', $customerId);
|
|
$product = fetch_record('product', $productId);
|
|
validate_sales_input($customer, $product, $branch, $qty, $errors);
|
|
|
|
if (!$errors && $customer && $product) {
|
|
db()->beginTransaction();
|
|
try {
|
|
$payload = build_sales_payload($customer, $product, $branch, $qty, $notes, $documentType);
|
|
$status = $documentType === 'sales_quote' ? 'draft' : 'confirmed';
|
|
$documentId = create_record(
|
|
$documentType,
|
|
sales_document_label($documentType) . ' ' . $customer['title'],
|
|
next_code(sales_document_prefix($documentType), $documentType),
|
|
$payload,
|
|
$status
|
|
);
|
|
|
|
if ($documentType === 'sales_order') {
|
|
$document = fetch_sales_document_by_id($documentId);
|
|
if ($document) {
|
|
$stock = adjust_product_stock($product, -$qty, 'sales_confirm', $document['code'], 'sales_order', $documentId, 'بيع للعميل ' . $customer['title']);
|
|
$payload = $document['payload_data'];
|
|
$payload['stock_after'] = $stock['after'];
|
|
$payload['inventory_effect'] = 'deducted';
|
|
update_record_payload($documentId, $payload, 'confirmed');
|
|
}
|
|
}
|
|
|
|
db()->commit();
|
|
set_flash('success', $documentType === 'sales_quote' ? 'تم إنشاء عرض السعر بنجاح.' : 'تم إنشاء أمر البيع وتحديث المخزون بنجاح.');
|
|
redirect('sales_orders.php?id=' . $documentId);
|
|
} catch (Throwable $e) {
|
|
db()->rollBack();
|
|
$errors[] = $e instanceof RuntimeException ? 'المخزون غير كافٍ لتأكيد أمر البيع.' : 'تعذر حفظ المستند، حاول مرة أخرى.';
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($action === 'convert_document') {
|
|
$sourceId = (int)($_POST['source_id'] ?? 0);
|
|
$targetType = (string)($_POST['target_type'] ?? '');
|
|
$source = fetch_sales_document_by_id($sourceId);
|
|
|
|
if (!$source) {
|
|
$errors[] = 'المستند المصدر غير موجود.';
|
|
} elseif (!in_array($targetType, sales_document_conversion_targets((string)$source['record_type']), true)) {
|
|
$errors[] = 'التحويل المطلوب غير مسموح لهذه المرحلة.';
|
|
} elseif (sales_document_child_exists($sourceId, $targetType)) {
|
|
$errors[] = 'تم إنشاء هذا المستند مسبقًا من نفس المصدر.';
|
|
}
|
|
|
|
if (!$errors && $source) {
|
|
$payload = $source['payload_data'];
|
|
$customer = fetch_record('customer', (int)($payload['customer_id'] ?? 0));
|
|
$product = fetch_record('product', (int)($payload['product_id'] ?? 0));
|
|
$branch = (string)($payload['branch'] ?? '');
|
|
$qty = (float)($payload['qty'] ?? 0);
|
|
$notes = trim((string)($payload['notes'] ?? ''));
|
|
|
|
validate_sales_input($customer, $product, $branch, $qty, $errors);
|
|
|
|
if (!$errors && $customer && $product) {
|
|
db()->beginTransaction();
|
|
try {
|
|
$newPayload = build_sales_payload($customer, $product, $branch, $qty, $notes, $targetType, $source);
|
|
if ($targetType === 'delivery_note') {
|
|
$newPayload['delivered_at'] = date('Y-m-d H:i');
|
|
}
|
|
if ($targetType === 'sales_invoice') {
|
|
$newPayload['invoiced_at'] = date('Y-m-d H:i');
|
|
$newPayload['payment_status'] = 'unpaid';
|
|
}
|
|
|
|
$newId = create_record(
|
|
$targetType,
|
|
sales_document_label($targetType) . ' ' . $customer['title'],
|
|
next_code(sales_document_prefix($targetType), $targetType),
|
|
$newPayload,
|
|
'confirmed'
|
|
);
|
|
|
|
if ($targetType === 'sales_order') {
|
|
$document = fetch_sales_document_by_id($newId);
|
|
if ($document) {
|
|
$stock = adjust_product_stock($product, -$qty, 'sales_convert', $document['code'], 'sales_order', $newId, 'تحويل من عرض سعر إلى أمر بيع للعميل ' . $customer['title']);
|
|
$newPayload = $document['payload_data'];
|
|
$newPayload['stock_after'] = $stock['after'];
|
|
$newPayload['inventory_effect'] = 'deducted';
|
|
update_record_payload($newId, $newPayload, 'confirmed');
|
|
}
|
|
}
|
|
|
|
db()->commit();
|
|
set_flash('success', 'تم إنشاء ' . sales_document_label($targetType) . ' بنجاح.');
|
|
redirect('sales_orders.php?id=' . $newId);
|
|
} catch (Throwable $e) {
|
|
db()->rollBack();
|
|
$errors[] = $e instanceof RuntimeException ? 'المخزون غير كافٍ لإتمام التحويل.' : 'تعذر تنفيذ التحويل، حاول مرة أخرى.';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$customers = customer_dataset();
|
|
$products = product_dataset();
|
|
$documents = fetch_sales_documents();
|
|
$detail = isset($_GET['id']) ? fetch_sales_document_by_id((int)$_GET['id']) : null;
|
|
$detailChildren = $detail ? sales_document_children((int)$detail['id']) : [];
|
|
$summaryCounts = [];
|
|
foreach (sales_document_record_types() as $type) {
|
|
$summaryCounts[$type] = record_count($type);
|
|
}
|
|
|
|
render_header('المبيعات والوثائق', 'إدارة عروض الأسعار وأوامر البيع وأوامر التسليم والفواتير وربطها في مسار واحد قابل للطباعة.', 'orders');
|
|
?>
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-sm-6 col-xl-3"><div class="stat-card"><div class="stat-label">عروض الأسعار</div><div class="stat-value"><?= e((string)$summaryCounts['sales_quote']) ?></div><div class="stat-note">مرحلة التسعير قبل خصم المخزون</div></div></div>
|
|
<div class="col-sm-6 col-xl-3"><div class="stat-card"><div class="stat-label">أوامر البيع</div><div class="stat-value"><?= e((string)$summaryCounts['sales_order']) ?></div><div class="stat-note">تؤكد البيع وتخصم المخزون</div></div></div>
|
|
<div class="col-sm-6 col-xl-3"><div class="stat-card"><div class="stat-label">أوامر التسليم</div><div class="stat-value"><?= e((string)$summaryCounts['delivery_note']) ?></div><div class="stat-note">جاهزة للطباعة والتسليم</div></div></div>
|
|
<div class="col-sm-6 col-xl-3"><div class="stat-card"><div class="stat-label">الفواتير</div><div class="stat-value"><?= e((string)$summaryCounts['sales_invoice']) ?></div><div class="stat-note">مرتبطة بالعميل وبالمحاسبة لاحقًا</div></div></div>
|
|
</div>
|
|
|
|
<div class="row g-4">
|
|
<div class="col-xl-5">
|
|
<div class="panel-card">
|
|
<div class="section-header compact"><div><h1 class="section-title mb-1">إنشاء مستند مبيعات</h1><p class="section-copy">ابدأ بعرض سعر أو أنشئ أمر بيع مباشر. عند تأكيد أمر البيع يتم خصم المخزون تلقائيًا.</p></div></div>
|
|
<?php if ($errors): ?>
|
|
<div class="alert alert-danger py-2"><?php foreach ($errors as $error): ?><div><?= e($error) ?></div><?php endforeach; ?></div>
|
|
<?php endif; ?>
|
|
<form method="post" class="vstack gap-3">
|
|
<input type="hidden" name="csrf_token" value="<?= e(csrf_token()) ?>">
|
|
<input type="hidden" name="form_action" value="create_document">
|
|
<div>
|
|
<label class="form-label">نوع المستند</label>
|
|
<select class="form-select" name="document_type">
|
|
<option value="sales_quote">عرض سعر</option>
|
|
<option value="sales_order" selected>أمر بيع مباشر</option>
|
|
</select>
|
|
<div class="form-text">عرض السعر لا يغير المخزون، بينما أمر البيع يخصم الكمية مباشرة.</div>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">العميل</label>
|
|
<select class="form-select" name="customer_id" id="customerSelect" required>
|
|
<option value="">اختر العميل</option>
|
|
<?php foreach ($customers as $customer): ?>
|
|
<option value="<?= (int)$customer['id'] ?>"><?= e($customer['name']) ?> — <?= e($customer['code']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">الفرع</label>
|
|
<select class="form-select" name="branch" id="branchSelect"><option value="">اختر الفرع بعد العميل</option></select>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">الصنف</label>
|
|
<select class="form-select" name="product_id" id="productSelect" required>
|
|
<option value="">اختر الصنف</option>
|
|
<?php foreach ($products as $product): ?>
|
|
<option value="<?= (int)$product['id'] ?>"><?= e($product['name']) ?> — <?= e($product['sku']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
<div class="form-text" id="allowedHint">اختر العميل أولًا لمعرفة قيود الأصناف.</div>
|
|
</div>
|
|
<div class="row g-3">
|
|
<div class="col-md-6"><label class="form-label">الكمية</label><input type="number" min="1" step="1" class="form-control" name="qty" id="qtyInput" value="1" required></div>
|
|
<div class="col-md-6"><label class="form-label">السعر / الوحدة</label><input type="text" class="form-control" id="unitPricePreview" value="0.00 ر.س" readonly></div>
|
|
</div>
|
|
<div class="sales-summary-box">
|
|
<div class="summary-row"><span>المخزون المتاح</span><strong id="stockPreview">—</strong></div>
|
|
<div class="summary-row"><span>الإجمالي قبل الضريبة</span><strong id="subtotalPreview">0.00 ر.س</strong></div>
|
|
<div class="summary-row"><span>ضريبة القيمة المضافة 15%</span><strong id="vatPreview">0.00 ر.س</strong></div>
|
|
<div class="summary-row grand"><span>الإجمالي النهائي</span><strong id="grandPreview">0.00 ر.س</strong></div>
|
|
</div>
|
|
<div><label class="form-label">ملاحظات</label><textarea class="form-control" name="notes" rows="3"></textarea></div>
|
|
<button class="btn btn-dark" type="submit">إنشاء المستند</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-xl-7">
|
|
<div class="panel-card mb-4">
|
|
<div class="section-header"><div><h2 class="section-title">سجل وثائق المبيعات</h2><p class="section-copy">المسار الآن يدعم: عرض سعر → أمر بيع → أمر تسليم / فاتورة.</p></div></div>
|
|
<?php if ($documents): ?>
|
|
<div class="table-responsive">
|
|
<table class="table align-middle app-table">
|
|
<thead><tr><th>الرقم</th><th>النوع</th><th>العميل</th><th>الصنف</th><th>الإجمالي</th><th>المصدر</th><th>الطباعة</th></tr></thead>
|
|
<tbody>
|
|
<?php foreach ($documents as $document): $payload = $document['payload_data']; ?>
|
|
<tr>
|
|
<td><a class="table-link" href="sales_orders.php?id=<?= (int)$document['id'] ?>"><?= e($document['code']) ?></a><div class="small text-secondary"><?= e(order_status_label((string)$document['status'])) ?></div></td>
|
|
<td><?= e(sales_document_label((string)$document['record_type'])) ?></td>
|
|
<td><?= e($payload['customer_name'] ?? '') ?><div class="small text-secondary"><?= e($payload['branch'] ?? '') ?></div></td>
|
|
<td><?= e($payload['product_name'] ?? '') ?><div class="small text-secondary"><?= e((string)($payload['qty'] ?? 0)) ?> <?= e($payload['unit'] ?? '') ?></div></td>
|
|
<td><?= e(format_money((float)($payload['grand_total'] ?? 0))) ?></td>
|
|
<td><?= e($payload['source_document_code'] ?? '—') ?></td>
|
|
<td><a class="btn btn-sm btn-outline-secondary" href="print_order.php?id=<?= (int)$document['id'] ?>" target="_blank">طباعة</a></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="empty-inline">لا توجد وثائق مبيعات بعد.</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="panel-card">
|
|
<div class="section-header compact"><div><h2 class="section-title">تفاصيل المستند</h2></div></div>
|
|
<?php if ($detail): $payload = $detail['payload_data']; ?>
|
|
<div class="detail-grid mb-3">
|
|
<div class="detail-block"><span class="detail-label">رقم المستند</span><strong><?= e($detail['code']) ?></strong></div>
|
|
<div class="detail-block"><span class="detail-label">النوع</span><strong><?= e(sales_document_label((string)$detail['record_type'])) ?></strong></div>
|
|
<div class="detail-block"><span class="detail-label">الحالة</span><strong><?= e(order_status_label((string)$detail['status'])) ?></strong></div>
|
|
<div class="detail-block"><span class="detail-label">العميل / الفرع</span><strong><?= e(($payload['customer_name'] ?? '') . ' — ' . ($payload['branch'] ?? '')) ?></strong></div>
|
|
<div class="detail-block"><span class="detail-label">تاريخ الإنشاء</span><strong><?= e($payload['created_date'] ?? '') ?></strong></div>
|
|
<div class="detail-block"><span class="detail-label">المصدر</span><strong><?= e(($payload['source_document_code'] ?? '') ?: 'مباشر') ?></strong></div>
|
|
</div>
|
|
<div class="subtle-card mb-3">
|
|
<div class="d-flex justify-content-between border-bottom pb-2 mb-2"><strong><?= e($payload['product_name'] ?? '') ?></strong><span><?= e($payload['sku'] ?? '') ?></span></div>
|
|
<div class="summary-row"><span>الكمية</span><strong><?= e((string)($payload['qty'] ?? 0)) ?> <?= e($payload['unit'] ?? '') ?></strong></div>
|
|
<div class="summary-row"><span>سعر الوحدة</span><strong><?= e(format_money((float)($payload['unit_price'] ?? 0))) ?></strong></div>
|
|
<div class="summary-row"><span>الإجمالي قبل الضريبة</span><strong><?= e(format_money((float)($payload['subtotal'] ?? 0))) ?></strong></div>
|
|
<div class="summary-row"><span>الضريبة</span><strong><?= e(format_money((float)($payload['vat'] ?? 0))) ?></strong></div>
|
|
<div class="summary-row grand"><span>الإجمالي النهائي</span><strong><?= e(format_money((float)($payload['grand_total'] ?? 0))) ?></strong></div>
|
|
<?php if (isset($payload['stock_after'])): ?>
|
|
<div class="summary-row"><span>المخزون بعد العملية</span><strong><?= e((string)$payload['stock_after']) ?></strong></div>
|
|
<?php endif; ?>
|
|
<?php if (!empty($payload['payment_status'])): ?>
|
|
<div class="summary-row"><span>حالة الدفع</span><strong><?= e($payload['payment_status']) ?></strong></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="d-flex flex-wrap gap-2 mb-3">
|
|
<a href="print_order.php?id=<?= (int)$detail['id'] ?>" target="_blank" class="btn btn-dark">فتح صفحة الطباعة</a>
|
|
<a href="products.php?id=<?= (int)($payload['product_id'] ?? 0) ?>" class="btn btn-outline-secondary">عرض الصنف</a>
|
|
<?php foreach (sales_document_conversion_targets((string)$detail['record_type']) as $targetType): ?>
|
|
<?php if (!sales_document_child_exists((int)$detail['id'], $targetType)): ?>
|
|
<form method="post" class="d-inline-block m-0">
|
|
<input type="hidden" name="csrf_token" value="<?= e(csrf_token()) ?>">
|
|
<input type="hidden" name="form_action" value="convert_document">
|
|
<input type="hidden" name="source_id" value="<?= (int)$detail['id'] ?>">
|
|
<input type="hidden" name="target_type" value="<?= e($targetType) ?>">
|
|
<button class="btn btn-outline-secondary" type="submit">إنشاء <?= e(sales_document_label($targetType)) ?></button>
|
|
</form>
|
|
<?php endif; ?>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
|
|
<?php if ($detailChildren): ?>
|
|
<div class="subtle-card mb-3">
|
|
<div class="detail-label mb-2">الوثائق المرتبطة</div>
|
|
<div class="vstack gap-2">
|
|
<?php foreach ($detailChildren as $child): $childPayload = $child['payload_data']; ?>
|
|
<div class="list-row py-0 border-0">
|
|
<div>
|
|
<strong><?= e(sales_document_label((string)$child['record_type'])) ?></strong>
|
|
<div class="small text-secondary"><?= e($child['code']) ?> — <?= e($childPayload['created_date'] ?? '') ?></div>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<a class="btn btn-sm btn-outline-secondary" href="sales_orders.php?id=<?= (int)$child['id'] ?>">فتح</a>
|
|
<a class="btn btn-sm btn-outline-secondary" href="print_order.php?id=<?= (int)$child['id'] ?>" target="_blank">طباعة</a>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if (!empty($payload['notes'])): ?><div class="text-secondary small"><?= e($payload['notes']) ?></div><?php endif; ?>
|
|
<?php else: ?>
|
|
<div class="empty-inline">أنشئ مستندًا جديدًا أو اختر واحدًا من الجدول لعرض تفاصيله.</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
window.erpData = {
|
|
customers: <?= json_encode($customers, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>,
|
|
products: <?= json_encode($products, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>
|
|
};
|
|
</script>
|
|
<?php render_footer(); ?>
|