وثيقة للاعتماد قبل التنفيذ. بتوصّف الدورة زي ما الكود شغّال دلوقتي، والفلو المرن اللي إنت عايزه (فوترة من أمر الشراء أو من إذن الاستلام، استلام على دفعات، فاتورة شراء مباشرة بدون أمر، والحسابات — 3-way match + GR/IR — هي اللي تقرر الصح مش الأقفال الصلبة)، ثم الفجوات بينهم، وقرار أفضل نقطة لالتقاط الباتش/السيريال (استلام؟ جودة؟ إذن إضافة؟)، ومراجعة شاشة أرصدة المخزون وهل بتدعم الباتشات/الصلاحيات، ووحدة توريد ذكي جديدة (RFQ): بوابة تسعير للمورد بلينك يونيك + مصفوفة مقارنة ملوّنة تولّد أوامر الشراء + طلب شراء واحد يطلّع كذا أمر. كل قسم بيقفل بتوصية محتاجة موافقتك.
الفلو المُحكَم اتبنى كـحوكمة بالأقفال الصلبة: كل مسار غير المسار الرسمي بيتمنع بـ 422/403. أثناء تجربتك اتضح إن سير عملك الفعلي أوسع — عايز مرونة: تستلم على دفعات، تفوتر من أمر الشراء أو من إذن الاستلام، وتعمل فاتورة شراء مباشرة من غير أمر شراء — والحسابات هي اللي تحرس الصح (3-way match يمنع الفوترة الزيادة، وحساب GR/IR يوازن ما بين الاستلام والفوترة أيًّا كان ترتيبهم).
grn_quality — GRN مبدئي بيتعمل، والاستلام لا يدخل مخزون فورًا.الوضع المُحكَم بيتجاهل الإعدادات الفردية ويفرض هذا الطُقم الثابت — عشان ما يتضعّفش بإعداد شارد:
| الحدث | القيد | الغرض |
|---|---|---|
| اعتماد إذن الاستلام | DR Inventory / CR GR-IR | إثبات دخول المخزون بتكلفة الاستلام (استحقاق «بضاعة مستلمة لم تُفوتَر»). |
| ترحيل الفاتورة (من GRN) | DR GR-IR (+PPV) / CR AP | يقفل الاستحقاق ويحوّل فرق السعر لحساب انحراف أسعار الشراء. |
طلب شراء ──┬──► أمر شراء (لاحقًا: طلب واحد → كذا أمر)
└──► أمر شراء
│
أمر الشراء ────┼──► إذن استلام ١ (دفعة) ─► جودة ─► إذن إضافة ─► أمين المخازن يعتمد ─► مخزون
├──► إذن استلام ٢ (دفعة) ─► … (كذا GRN بحسب الكميات)
│
└──► فاتورة: من أمر الشراء (تجميعي لكل المستلَم)
أو من كل إذن استلام (بالدفعة)
→ الاتنين متاحين، والحسابات تقرر
فاتورة شراء مباشرة (بدون أمر شراء) ─► إذن إضافة مسودة ─► أمين المخازن يعتمد ─► مخزون
| البند | الحالة |
|---|---|
| وحدة RFQ / تسعير موردين / مقارنة | مفيش خالص (greenfield) — الأقرب: SalesQuotation (نمط دورة حياة للاستعارة) وSupplierPriceList (نموذج بيانات). |
| مقارنة أسعار الموردين | فيه endpoint compare() بيرتّب منتج واحد ويعلّم الأرخص — بس الواجهة مبتستهلكوش، ومفيش مصفوفة منتج×مورد. لازم تتبني. |
| طلب شراء → كذا أمر | مقفول 1→1: canConvert() = الحالة «معتمد» فقط، وبعد التحويل الحالة تبقى «محوَّل» + converted_to_order_id عمود مفرد → التحويل التاني يترفض 422. |
| بوابة خارجية بلينك بدون دخول | نمط جاهز للاستعارة: بوابة المريض (portal_link_token 64 حرف → linkAccess → توكن جلسة مُجزّأ → middleware بيقصر الرؤية على سجل واحد + تسجيل محاولات IDOR). (مش بوابة المعمل الخارجي — دي username/password.) |
| إرسال إيميل | ضعيف: الـmailer الافتراضي log، صفر Mailable، والإشعارات كلها database بس → محتاج Mailable جديد + SMTP حقيقي. |
| واتساب | جاهز للاستعارة: روابط wa.me من الواجهة + قالب رسالة مخزّن (نفس نمط LIS). مفيش API سيرفر (الإرسال التلقائي هيحتاج WhatsApp Business API — مش موجود). |
نموذج البيانات — 4 جداول بس، الـRFQ مستند مستقل بيشير للطلب (مش الطلب نفسه):
| الجدول | الوظيفة |
|---|---|
purchase_rfq | المناقصة: مرجع الطلب، رقم، عملة، أساس الضريبة، موعد نهائي، حالة (مسودة → مُرسل → مفتوح → مقفول → مُرسى/ملغي). |
purchase_rfq_items | نسخة (snapshot) من بنود الطلب (منتج، كمية، وحدة، ملاحظة) — منسوخة مش مربوطة حيّة، فتعديل الطلب بعدين ما يغيّرش مناقصة شغّالة. |
purchase_rfq_suppliers | الدعوة + رأس العرض في صف واحد (عرض واحد لكل مورد لكل RFQ): المورد، الـtoken، القناة، توقيتات الإرسال/الفتح/التقديم، الحالة، ملاحظات المورد. |
purchase_rfq_quote_items | الأسعار: دعوة × بند → سعر الوحدة، مهلة التوريد، حد أدنى للكمية، علامة «مسعّر». |
الترسية بأعمدة مش جدول خامس: awarded + purchase_order_id على سطور العرض الفايزة، وrfq_id على أمر الشراء → أثر كامل: طلب → RFQ → عرض → أمر.
أمان البوابة (نسخ نمط بوابة المريض — عطاء مغلق):
noindex، جدول سجل وصول، مسار خارج الـauth زي بوابة LIS، HTTPS.المصفوفة — السعر هو اللون، وكل حاجة تانية badge:
converted_to_order_id المفرد، ونعتمد على purchase_orders.purchase_request_id الموجود (hasMany) + تتبّع «الكمية المطلوبة مقابل المأمورة» على مستوى بند الطلب + حالة partially_converted. كده الطلب الواحد يطلّع كذا أمر — من المصفوفة ومن المسار اليدوي. (بونس رخيص: عند الترسية نحدّث سعر المورد في SupplierPriceList تلقائيًا.)طلب → كذا أمر (تتبّع كمية بالسطر + حالة تحويل جزئي). المسار اليدوي والمصفوفة الاتنين بيحتاجوها؛ تتشحن وتتجرّب لوحدها.
مستند RFQ + الدعوات + البوابة (إرسال إيميل + wa.me/نسخ لينك) + المصفوفة بالألوان + توليد أوامر Draft + إدخال المشتري اليدوي. ما ننزلش البوابة من غير المصفوفة — من غير المقارنة والترسية تبقى مجرد فورم بيعمل شغل إدخال زيادة.
تذكيرات الموعد، رفض بسبب، إيميلات ترسية/اعتذار (بلا كشف الفايز أو السعر)، مرفقات العرض، طباعة/تصدير المصفوفة (PDF عربي)، تحديث SupplierPriceList لو ما اتعملش في ب، WhatsApp API لو الحجم برّره.
| البند | الحالي | المطلوب | التغيير المقترح |
|---|---|---|---|
| فوترة من أمر الشراء | ممنوعة (oneBillPerGrn) — وحاليًا بعد إصلاح سريع بتُوجَّه لـGRN واحد فقط | فاتورة تجميعية للمستلَم عبر كل إذون الاستلام (فاتورة واحدة في الآخر) | شيل قفل oneBillPerGrn على createFromOrder + حارس الترحيل؛ الفوترة بالمستلَم-غير-المفوتَر؛ الـ3-way match يسقّف |
| فوترة من إذن الاستلام (بالدفعة) | شغّالة (createFromGrn) | تفضل متاحة بالتوازي مع فوترة الأمر | بدون تغيير — بس تتعايش مع مسار الأمر |
| فاتورة شراء مباشرة (بدون أمر) | ممنوعة (require_po_for_bill) | مسموحة → إذن إضافة مسودة → اعتماد → مخزون | مسار «فاتورة شراء مباشر» مميّز بعلامة؛ إثبات GR/IR معكوس؛ إرخاء require_po للفواتير بدون أمر |
| كذا إذن استلام لأمر واحد | تسلسلي (single_active_grn_cycle يمنع فتح دورتين معًا) | استلام على دفعات حسب الكميات | تسلسلي يكفي غالبًا؛ قرار: نرخّي للسماح بدورتين نشطتين معًا؟ |
| طلب شراء → كذا أمر | مقفول 1→1 — canConvert()=«معتمد»، وبعد التحويل الحالة «محوَّل» + converted_to_order_id مفرد → التاني 422 |
أي عدد من الأوامر من طلب واحد (يدوي + من مصفوفة RFQ) | شيل العمود المفرد → hasMany عبر purchase_request_id + تتبّع كمية بالسطر + حالة partially_converted (القسم ٣ب) |
| توريد ذكي (RFQ + مصفوفة مقارنة) | مفيش خالص — بس compare() لمنتج واحد (غير مستهلَك) + نمط بوابة المريض للاستعارة |
بوابة تسعير للمورد بلينك يونيك + مصفوفة ملوّنة تولّد أوامر شراء | وحدة جديدة (4 جداول) — التفاصيل والمراحل في القسم ٣ب |
| التقاط الباتش/السيريال + الصلاحية | — القسم ٥ (تحت التحليل) — | ||
| عرض الأرصدة بالباتش/الصلاحية | — القسم ٦ (تحت التحليل) — | ||
oneBillPerGrn وrequire_po_for_bill لا يفتح باب الفوترة الزيادة — حارس الـ3-way match عند الترحيل بيسقّف الفاتورة عند الكمية المستلَمة تراكميًا (اتأكدنا: alreadyBilled + billQty ≤ received × tolerance). فالأقفال اللي هنشيلها كانت «تقييد مسار»، مش «حماية محاسبية».tracking_type = none / batch / serial (يتحدد من فورم المنتج).| النقطة | الباتش | الصلاحية | السيريال (لكل وحدة) | الواقع |
|---|---|---|---|---|
| أ) إنشاء إذن الاستلام (GRN) | ✗ | ✗ | ✗ | الباك إند فيه الأعمدة، لكن الواجهة مفيهاش فورم بنود (بنود GRN بتيجي من أمر الشراء) — مفيش إدخال هنا خالص. |
| ب) فحص الجودة | ✓ | ✓ (للسطر) | ✗ | بيلتقط باتش + صلاحية للسطر ويحفظهم على بنود الـGRN. مفيش سيريالات. |
| ج) إذن الإضافة (stock-receipts) | ✓ | ✓ | ✓ + صلاحية/سيريال | الشاشة الوحيدة فيها UI لإدخال السيريالات (ديالوج بيجمّع صلاحية لكل سيريال). بس مقفولة عن مسار الشراء في المُحكَم، والالتقاط الفعلي (persist) للسيريالات بيحصل هنا عند اعتماد أمين المخازن. |
serial_numbers = null. للمنتج المتتبَّع بسيريال، اعتماد أمين المخازن بيرمي serial_count_mismatch (0 ≠ الكمية).receipt_locked_by_grn) → أمين المخازن مش قادر يدخل السيريالات كمان. طريق مسدود.rowSerialsExpiryMap)، لكن الـpayload بيبعت serial_numbers[] + صلاحية سطر واحدة بس. الباك إند بيحط صلاحية السطر على كل السيريالات.| النقطة | الإدخال البشري اليوم | الحفظ الفعلي (product_serials / حركة) |
|---|---|---|
| إنشاء GRN | لا شيء (بنود من الأمر) | لا |
| فحص الجودة | باتش + صلاحية (بيتحفظ على بند الـGRN) | لا (مرحلي على الـGRN بس) |
| اعتماد أمين المخازن | لا شيء جديد — الإذن مقفول للتعديل | نعم — نقطة الحفظ: باتش/صلاحية → حركة؛ سيريال → product_serials. (بس السيريال null فبيتبلوك) |
| إذن إضافة مستقل | باتش + سيريال + صلاحية/سيريال (الشاشة الوحيدة) | نعم عند الاعتماد — بس مقيّدة في المُحكَم + بتفقد صلاحية/سيريال |
product_serials بيحصل هناك أصلًا (بنلتقط في نفس لحظة الحفظ)؛ أمين المخازن هو آخر مين بيلمس كل وحدة قبل الرفّ (الجودة بتفحص عيّنة، متعرفش سيريال كل كرتونة)؛ والأهم: نقطة (ج) هي الوحيدة اللي بتخدم المسارين — أمر شراء→GRN، و الفاتورة المباشرة (اللي متفقين عليها). نقطة واحدة = تدريب واحد + كود واحد + مصدر حقيقة واحد.receipt_locked_by_grn كليًا — الكميات والأصناف تفضل مقفولة (مرآة الـGRN = جوهر المطابقة)، ويتفتح قسم الباتش/السيريال بس. بصريًا: الكميات رمادية بأيقونة قفل، وقسم الدفعات مفتوح.serial بس، السيريالات تتداخل تحت كل دفعة وترث تواريخها (مع إمكانية تعديل فردي). ده بيخلّي فقدان الصلاحية-لكل-سيريال مستحيل بالتصميم، وبيخدم الأصناف المتتبَّعة بالتشغيلة (الأشيع) من غير ما يفرض سيريالات.الأعمدة: كود المنتج · الاسم · المتغيّر (variant) · المخزن · الكمية · المتاح · متوسط التكلفة · القيمة الإجمالية + زرار كارت الصنف. وكروت إحصائية: عدد المنتجات، القيمة الإجمالية، تحت الحد، رصيد صفر.
جدول الأرصدة inventory_stock_balances إجمالي بحت: صف واحد لكل (منتج + متغيّر + مخزن)، بعمود كمية واحد. مفيهوش أعمدة باتش/صلاحية/سيريال أصلًا (مؤكَّد من الميجريشن والموديل والـResource). فالصلاحية مش موجودة على مستوى الرصيد — هي متسجّلة في أماكن تانية موازية:
| البيان | الرصيد (stock_balances) | الحركات (movements) | السيريالات (product_serials) | بنود الإذون |
|---|---|---|---|---|
| رقم الباتش | ✗ | ✓ نَسَب | ✓ | ✓ |
| تاريخ الصلاحية (نهاية) | ✗ | ✓ نَسَب | ✓ | ✓ |
| السيريال (لكل وحدة) | ✗ | ✗ | ✓ صف/وحدة | ✓ JSON |
| تاريخ الإنتاج/البداية | ✗ | ✗ | ✗ | ✗ |
| رصيد لكل دفعة (كمية) | ✗ إجمالي | ✗ | عبر حالة السيريال فقط | ✗ |
product_serials)، ومؤشّر لون للأصناف اللي ليها وحدات قربت تنتهي. يدّيك رؤية الصلاحية من غير تغيير النموذج.production_date على product_serials + بنود الإذون (تغيير صغير في الميجريشن + الالتقاط).production_date جنب expiry_date في product_serials (+ يتنسخ للحركات زي الصلاحية). حدّان: (١) مفيش أي منطق يعتمد عليه — معلوماتي بحت؛ (٢) تحقّق بسيط: الإنتاج < الانتهاء. مش هنبني «جدول دفعات رئيسي» دلوقتي (جزء من P4 المؤجّل).product_serials للأصناف بسيريال، تقريبي من حركات الوارد للأصناف بتشغيلة (ويتوسم «تقريبي»). (٢) شارات لونية: أحمر=منتهي، برتقالي=قريب (عتبة أيام قابلة للضبط). (٣) تنقيب: نقرة الصف تفتح تقرير الصلاحية الموجود أصلًا مفلتَر. تنويه: دي رؤية مش إنفاذ — الصرف مش هيختار الأقرب-انتهاءً تلقائيًا (FEFO محتاج أرصدة بالدفعة = مؤجّل).الفوترة المرنة (من الأمر تجميعي + من الـGRN بالدفعة) + الفاتورة المباشرة بدون أمر (بإثبات GR/IR معكوس) + إصلاح بلوكر (ب) فقدان صلاحية السيريالات.
ملاحظة: بلوكر (ب) مش إصلاح واجهة بس — شكل الـpayload ومسار الحفظ بيقبلوا صلاحية واحدة؛ صغير بس FE+BE. لو الفقد مش مؤلم دلوقتي، ممكن تبتلعه المرحلة ٢.
الالتقاط الموحّد عند إذن الإضافة: الفتح الجزئي (بيصلح بلوكر (أ) الطريق المسدود جذريًا) + ديالوج الدفعات-أولًا + تعبئة مسبقة من الجودة + إضافة production_date.
لو أصناف السيريال محجوبة فعليًا دلوقتي → نقدّم الفتح الجزئي وحده كإصلاح عاجل قبل باقي المرحلة.
عمود «أقرب صلاحية» + الشارات + التنقيب على شاشة الأرصدة. بعد المرحلة ٢ حتمًا — رؤية بلا التقاط موثوق = بيانات مضلِّلة.
أرصدة لكل دفعة (per-lot balances) + صرف FEFO. مذكورة في الكود كـ«عمل مستقبلي». مبتبدأش إلا بعد إثبات جودة البيانات من المرحلتين ٢-٣.
tracking_type لـ«بلا» هربًا. العلاج: إدخال جماعي (توليد نطاق، لصق قائمة، مسح باركود) في ديالوج الأمين. الإلزام بلا تسهيل = التفاف.oneBillPerGrn → فوترة من أمر الشراء (تجميعي لكل المستلَم) و من الـGRN (بالدفعة) — الاتنين متاحين، والـ3-way match يمنع الزيادة. موصى به.single_active_grn_cycle علشان تفتح كذا دورة استلام معًا؟ (توصيتي: تسلسلي دلوقتي، نرخّي بس لو احتجت فعلًا)production_date كعمود معلوماتي (إنتاج < انتهاء)؟ موصى به.wa.me + قالب (جاهز). نمشي بـwa.me أولًا والإيميل يتفعّل لما نضبط SMTP؟ (توصيتي: أيوة)