39528-vm/sales_orders.php
2026-04-09 09:46:40 +00:00

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(); ?>