فحص الدورة الكاملة (PR → PO → GRN → فاتورة → دفع + مرتجع + تكلفة وصول)، هل صحيحة، الترحيل للمخازن بكل الاحتمالات، وإجابة كل أسئلتك بأدلة من الكود.
الفاتورة بتحمل purchase_order_id + كل سطر purchase_order_item_id، والأصناف منسوخة من الأمر. فاتورة واحدة = أمر واحد.
مفيش sales_order_id على أمر الشراء خالص. الربط الوحيد غير مباشر عبر MRP وهو مكسور عند التحويل.
أمر الشراء التزام فقط — صفر تأثير على المخزون. اللي بيورّد = إذن الاستلام (GRN) أو الفاتورة (في الوضع المباشر).
purchase_request_id، الأصناف منسوخة) — بس بدون تتبّع على مستوى السطر.طلب شراء ──convertFromRequest──► أمر شراء ──┬─ createFromOrder ──► فاتورة شراء ──► دفع
(PR) (PO) │ (Bill)
└─ GRN.store(po_id) ──► إذن استلام ──► (مخزون+)
(GRN)
مرتجع شراء ◄── مربوط بالفاتورة (bill_id) تكلفة وصول ◄── مربوطة بالـ GRN (purchase_grn_id)
| المستند | آلة الحالة (Status) | مين بينشئ/يرحّل |
|---|---|---|
| طلب شراء (PR) | Draft → PendingApproval → Approved → Converted (+Rejected/Cancelled) | CreatePurchaseRequest (مدخل MRP كمان) |
| أمر شراء (PO) | Draft → Approved → Confirmed → Sent → PartiallyReceived → FullyReceived → PartiallyBilled → FullyBilled → Closed | الاستلام/الفوترة تلقائية (recalculateReceiveStatus/recalculateBillStatus) |
| إذن استلام (GRN) | Draft → PendingQuality → QualityApproved/Rejected → Approved | PurchaseGrnController::approve (المنطق inline، مفيش Action) |
| فاتورة (Bill) | Draft → Approved → Posted → PartiallyPaid → Paid | PostPurchaseBill (قيد + مخزون مباشر + كمية مفوترة) |
| دفع | Draft → Posted | PostPurchasePayment |
| مرتجع | Draft → Approved → Posted | PostPurchaseReturn (عكس AP + خصم مخزون) |
| تكلفة وصول | Draft → Posted | PostLandedCostVoucher (إعادة تقييم، مش كمية) |
| التحويل | الطريقة | الأصناف |
|---|---|---|
| PR → PO | convertFromRequest (PurchaseOrderController:433) | تُنسخ؛ header purchase_request_id؛ مفيش رابط سطر-بسطر |
| PO → فاتورة | createFromOrder (PurchaseBillController:392) | تُنسخ (الكمية المتبقية بس)؛ header purchase_order_id + سطر purchase_order_item_id ✅ |
| PO → GRN | GRN.store (يدوي، الـ FE يبعت السطور) | رابط اختياري purchase_order_item_id؛ مفيش "اسحب أصناف الأمر" تلقائي |
| GRN → فاتورة | مش موجود | مفيش createFromGrn — الفاتورة من الأمر أو يدوي بس |
purchase_order_id (header) + كل سطر purchase_order_item_id (PurchaseBill:35 · PurchaseBillItem:23).createFromOrder بيسحب أصناف الأمر وينسخها (يأخذ الكمية المتبقية بس، يتخطّى المفوتر بالكامل).sales_order_id ولا customer_id على purchase_orders/purchase_order_items خالص (متأكَّد من الـ migrations). يعني النظام مش قادر يقول "الشراء ده لأوردر العميل الفلاني". مفيش drop-ship ولا procure-to-order مباشر.الطريق غير المباشر (MRP) موجود بس مكسور عند التحويل:
collectSalesDemand → planned order → CreatePurchaseRequest (PR) → PO.Draft بس التحويل لـ PO بيطلب Approved → بوابتان موافقة يدوية، مش تلقائي.purchase_order_id على mfg_peggings. فمتقدرش تتبع SO→PR→PO.كل نقاط دورة أمر الشراء (approve / confirm / send / close / convert) بتغيّر الحالة + التدقيق فقط. مفيش InventoryReceipt بيتعمل وStockService مبيتنادهش أبداً من PurchaseOrderController. عمود received_quantity بيتكتب بس من PurchaseGrnController:525 (اعتماد الـ GRN) — الأمر بيقراه بس.
ApproveReceipt → StockService::increaseStock (يبني cost layer + movement). أمر الشراء مجرد التزام بالشراء.grn_mode (direct / grn / grn_quality).| المستند | direct (افتراضي) | grn | grn_quality | الآلية |
|---|---|---|---|---|
| طلب شراء (PR) | لا | لا | لا | مستند تخطيط |
| أمر شراء (PO) | لا | لا | لا | التزام فقط |
| إذن استلام (GRN) | محظور (403) | نعم ↑ | نعم ↑ | ينشئ إذن + ApproveReceipt |
| الفاتورة (Bill) | نعم ↑ | لا | لا | handleDirectModeStock (في direct بس) |
| المرتجع (Return) | نعم ↓ | نعم ↓ | نعم ↓ | decreaseStock (مستقل عن الوضع) |
| تكلفة وصول | إعادة تقييم | إعادة تقييم | إعادة تقييم | يعيد تقييم الطبقات (مش كمية) |
direct، والـ GRN في grn/grn_quality (والـ GRN محظور في direct). ده تصميم صحيح، مفيش ازدواج في المسار السليم.track_inventory=true و له warehouse_id. الخدمات → مصروف (GL) بدون مخزون. (تحذير: صنف مخزني بدون warehouse → بيتسجّل في GL مخزون بدون كمية فعلية — مفيش تحقّق يمنع ده).الفاتورة دايماً بتعمل DR مخزون للأصناف المخزنية في GL — في كل الأوضاع. لكن إذن الاستلام مبيعملش قيد محاسبي (بيحرّك الكمية والقيمة في الـ subledger بس).
| اللحظة (في وضع grn) | الكمية الفعلية | قيمة المخزون في GL |
|---|---|---|
| عند اعتماد الـ GRN | ↑ تزيد | مفيش قيد |
| عند ترحيل الفاتورة | ثابتة (مفيش حركة) | ↑ تتسجّل |
| # | المشكلة | المكان | الخطورة |
|---|---|---|---|
| 1 | المرتجع مبيقللش received_quantity ولا يعيد حساب حالة الاستلام → أمر الشراء يفضل "مستلَم" غلط | PostPurchaseReturn | HIGH |
| 2 | إلغاء GRN معتمد مبيعكسش المخزون ولا يقلّل received_quantity (بس بيقلب الحالة) → مخزون منفوخ | PurchaseGrnController:440 | HIGH |
| 3 | فجوة GRNI (القسم ٥) — قيمة GL تتأخّر، مفيش حساب وسيط ولا PPV | PostPurchaseBill:97 | HIGH |
| 4 | خطر ازدواج المخزون لو grn_mode اتقرى مختلف بين الاستلام والفاتورة (default=direct) — مفيش فحص "إذن موجود بالفعل" | PostPurchaseBill:187,221 | MED |
| 5 | صنف مخزني بدون warehouse → DR مخزون في GL بدون كمية فعلية (مفيش تحقّق) | PostPurchaseBill:89 vs 195 | MED |
| 6 | فاتورة بسطور من أوامر شراء مختلفة → الأوامر التانية حالة فوترتها متتحدّثش (over-bill) | PostPurchaseBill:265 | MED |
| 7 | converted_at بيتكتب بس مش في $fillable → بيُهمل بصمت (mass-assignment) | PurchaseOrderController:489 | LOW |
| 8 | المرتجع بيخصم بتكلفة سطر المرتجع مش طبقة FIFO الأصلية → احتمال فرق تقييم | PostPurchaseReturn:191 | LOW |
| الأولوية | التوصية |
|---|---|
| ١ | المرتجع + إلغاء GRN يقللوا received_quantity ويعيدوا حساب حالة الاستلام (ويعكسوا المخزون في إلغاء GRN) |
| ٢ | ضيف حساب GRNI (بضاعة مستلمة ولم تُفوتر) + حساب فرق السعر (PPV) لفصل قيمة الاستلام عن الفاتورة |
| ٣ | فحص "إذن استلام موجود بالفعل لهذا المصدر" قبل إنشاء إذن في الفاتورة (يمنع الازدواج)؛ وألزم warehouse_id للأصناف المخزنية |
| ٤ | لو محتاج تربط طلب بيع بأمر شراء: ضيف sales_order_id على أمر الشراء (+ propagate من MRP)، وأضف purchase_order_id على الـ pegging عشان تتبع SO→PR→PO |
| ٥ | تطابق ثلاثي حقيقي (المفوتر ≤ المستلَم)؛ وإصلاح converted_at fillable |