تحليل جاهزية · المرحلة ١ — بلا كود · implement-research

هل تصلح شاشة نقاط البيع كواجهة صيدلية؟ — وإيه الناقص بالظبط

تدقيق عرضي في كل الأنظمة (POS · Inventory · Core · Sales · Clinic · LIS · NPHIES · EInvoicing · WebStore) عبر تلات مسارات متوازية: الدوا كمخزون · الدوا كرعاية · الشاشة نفسها. الإجابة المختصرة: أيوه — الشاشة تصلح كأساس ومش محتاجة شاشة تانية، لكن اللي تحت الشاشة مش جاهز: دفتر التشغيلات دلوقتي دفتر جانبي مش نظام مخزون، والصلاحية لا ترفض أي حاجة، والصرف مش مستند أصلًا.

الحكم: أساس صالح + وضع صيدلية العوائق الهيكلية: ٣ مالي: [FIN] ١ أغسطس ٢٠٢٦ · moonui2 · hazemdev2 الحالة: تحليل — مستنّي موافقتك

٠ تصحيح قاعدة المعرفة — قبل أي حاجة

وثيقتين في الـKB بيقولوا حاجة اتبنت فعلًا

plans/purchases-inventory-flexible-flow.html وtopics/purchases-controlled-flow.md (قسم DEFERRED/BACKLOG — Phase 4) بيوصفوا كـ«مؤجَّل» حاجات شحنت خلاص: inventory_lot_balances · inventory_issue_lot_allocations · تخصيص FEFO · بُعد الملكية · سياسة اللوط المنتهي · الاختيار اليدوي للّوط · الحفاظ على اللوط في التحويلات — كلها موجودة ومعاها اختبارات.

وبالتحديد السطر purchases-controlled-flow.md:14 («الصلاحية على الأرصدة = رؤية، مش FEFO») بقى غلط.

لكن التصحيح بيقطع في الاتجاهين: المرحلة ٤ بنت دفتر لوطات، ما بنتش نظام مخزون قائم على اللوط. والفرق ده هو كل الإجابة اللي تحت.

topics/pos-overhaul.md اتراجع كمان ومافيهوش انحراف — كل اللي فيه مطابق للكود.

١ المشكلة

طلبك: «بص على نقاط البيع وحلّل — ينفع تكون واجهة نقاط بيع صيدلية وإيه اللي ناقصه، بعد ما تعمل بحث وتدقيق في كل الأنظمة بشكل عام».

الطلب ده بيتفكّ لتلات أسئلة مختلفة تمامًا، وكل واحد له إجابة مختلفة:

السؤالالإجابةليه
هل الشاشة نفسها تصلح؟ أيوه — من غير شاشة تانية حلقة الكاشير (باركود أولًا · مفاتيح F · لوحة الباقي · طابور أوفلاين بمفتاح idempotency · جرد أعمى عند الإقفال) صح خلاص، وفيها طبقة حوكمة سعر بتفرض سعر ثابت غير قابل للتعديل — دي بالظبط قاعدة السعر الجبري بإسم تاني.
هل اللي تحت الشاشة يصلح؟ لأ — لسه الصيدلية مخزونها هو التشغيلة. هنا التشغيلة «تقرير بيطلع صح غالبًا»: ٤ من ٦ منتجات في قاعدة بيانات التطوير خارجة عن التطابق فعلًا.
هل نقدر نصرف روشتة؟ لأ — مفيش حدث صرف أصلًا الروشتة سجل إكلينيكي بلا كمية وبلا ربط بأي مستند بيع أو مخزون. وPrescriptionStatus::Dispensed متعلَّم عليه في الكود: «محجوز؛ غير قابل للوصول في v1».

٢ الوضع الحالي — اللي موجود فعلًا

٢-١ الشاشة: تخطيط واحد بالظبط

الغلاف بيعرض تخطيط واحد لا غير — pos-layout.component.html:38 فيه @if (posLayout() === 'cashier') وبس. مجلدات cart-panel/ وproduct-grid/ كود ميت (مش متسمّيين في أي قالب). فالشاشة = pos-header + invoice-table (يمين) + عمود تحكم ثابت (شمال) + pos-footer.

عرض لوحة الفاتورة @1366
959px
عرض عمود التحكم
383px
مساحة حرة في عمود التحكم
0px
لوحة الأرقام أقصر من طبيعتها بـ
64px

الحساب: 668px ارتفاع متاح، والكتل الثابتة (كلها flex-shrink:0 بقرار صريح في pos-layout.component.scss:116-136) بتاخد ≈340px، فيفضل ≈328px للوحة الأرقام اللي ارتفاعها الطبيعي 392px — يعني هي أصلًا بتـ scroll جوه نفسها. النتيجة العملية: أي إضافة في عمود التحكم بتتاخد من منطقة مضغوطة أصلًا.

٢-٢ الدوا كمخزون — الطبقة اللي اتبنت

القدرةالحالةالدليل
inventory_lot_balances (منتج·متغيّر·مخزن·مالك·تشغيلة·صلاحية → متبقّي/تكلفة) موجود وموصول Modules/Inventory/database/migrations/2026_07_11_400000_…:43-86
كتابة اللوط عند اعتماد الاستلامموجود وموصول Inventory/app/Services/LotBalanceService.php:27-57
البيعة بتخصم من دفتر اللوطات (منتجات batch فقط)موجود وموصول Sales/app/Actions/PostSalesInvoice.php:330 → StockService.php:245-259
التخصيص FEFO بالصلاحية تصاعديًا (والـnull آخرًا)، قابل للتحويل لـFIFOموجود وموصول Inventory/app/Services/LotAllocationService.php:154-158, :236-241
عكس اللوط عند إلغاء فاتورةموجود وموصول Sales/app/Actions/CancelSalesInvoice.php:175
تقرير «التشغيلات القاربة على الانتهاء» + تبويب في الواجهةموجود وموصول Inventory/app/Http/Controllers/InventoryReportController.php:1122+
وحدات البيع المتعددة: عامل تحويل ١٥,٦ + باركود لكل وحدة + سعر بيع لكل وحدةموجود وموصول Core/…/create_product_units_table.php:11-25
تحويل الكمية للوحدة الأساسية قبل خصم المخزون (صحيح تمامًا)موجود وموصول Core/app/Services/UnitConversionService.php:42-107
أرضية السعر بتتدرّج مع الوحدة (علبة ١٠ تتقارن بـ١٠× الأرضية)موجود وموصول Sales/…/Concerns/ValidatesMinimumSalePrice.php:143-150
باركود لكل وحدة → منتج + وحدة + سعر (matched_unit)موجود وموصول POS/app/Http/Controllers/POSProductController.php:131-151

٢-٣ الدوا كرعاية — اللي موجود لكن معزول

القدرةالحالةالدليل / ليه معزول
كيان روشتة حقيقي (prescriptions + prescription_items: جرعة، تكرار، مدة، طريق إعطاء بـenum ١٠ حالات، طبيب، مريض، ترقيم تسلسلي) موجود · مش موصول مسار الكتابة الوحيد IssuePrescription::handle(Encounter,…)مربوط بالزيارة. المسار POST clinic/encounters/{encounter}/prescriptions. مفيش حاجة برّه Clinic تقدر تقراها أو تكتبها.
lab_patients.partner_idbusiness_partners — الجسر بالظبط اللي الـPOS محتاجه موجود · مش موصول LIS/…/create_lab_patients_table.php:27 · فحص القاعدة: 11,797 مريض، 41 بس عندهم partner_id
نموذج جهة دافعة ناضج بتقسيم حقيقي (نسبة تحمّل، رقم موافقة، فاتورة مريض + فاتورة تأمين مترابطتين) موجود · مش موصول LIS/app/Services/InsuranceInvoiceService.php:224-367 · Clinic/app/Models/Claim.php
NPHIES اتصال HTTP حقيقي (mTLS): أهلية، موافقة مسبقة، تقديم مطالبة موجود · مش موصول مدخل POST nphies/eligibility بيقبل patient_idLabPatient. الـPOS ما عندوش patient_id يبعته.
تاريخ الصرف الإكلينيكي مُجمَّع بالفعل مع أسماء الأدوية موجود · مش موصول Clinic/app/Services/VisitHistoryService.php:44-52 (بيحمّل prescriptions.items.drug)
رقم ترخيص مهنيموجود · مش موصول lab_doctors.license_number — جدول إحالة/عمولة أطباء، مش سجل اعتماد موظفين. users ما فيهاش ترخيص.
product_serials بـbatch_number/expiry_date/حالة موجود · مش موصول بتتكتب بس من ApproveReceipt/ApproveIssue. والقرار المقفول رقم ٣ في الـPOS إن البيعة ما بتعملش إذن صرف ⇒ السيريال ما بيتحركش أبدًا في بيعة POS.
الفوترة الإلكترونية موصولة بمسار بيعة POS — وZATCA بيصنّف بيعة الكاش صح كفاتورة مبسّطة 0200000، وفيه مولّد QR موجود · مشروط EInvoicing/…/ZatcaXmlBuilder.php:99 · ZatcaQrGenerator.php:23
تصحيح (2026-08-01): كنت كتبت هنا «الـQR متولّد ومتخزّن خلاص» — ودي مضلّلة. einvoicing.enabled مقفول افتراضيًا ومفيش شركة مفعّلاه هنا ⇒ ما بيتعملش مستند ⇒ مفيش QR. وكمان التوقيع نفسه stub (بيتحقن كتعليق XML مش UBLExtensions/XMLDSIG)، وأعمدة العنوان الوطني السعودي (رقم المبنى، الحي، القطعة، المنطقة) والاسم القانوني العربي مش موجودين/مش بيتطبعوا. ⇒ الإيصال السعودي المطابق متوقّف على الـschema مش على القالب، وWP24 مش شبه مجاني زي ما قدّرت.

٣ المطلوب — إيه اللي بيميّز صيدلية عن أي نقطة بيع

مش قايمة رغبات — دي القدرات اللي من غيرها الشاشة مش صيدلية:

  1. التشغيلة والصلاحية على كل سطر — الصيدلي بيبيع «علبة من التشغيلة B-2291 بتنتهي ٠٦/٢٠٢٦»، مش «علبة».
  2. منع بيع المنتهي — رفض حقيقي، مش تصفية.
  3. وحدة الصرف — علبة / شريط / قرص، والسعر يتبع.
  4. السعر الجبري — السعر المطبوع على العلبة هو السعر، لا يزيد ولا ينقص.
  5. المريض مش الزبون — ملف، عمر، تأمين، حساسية.
  6. الروشتة — صرف كامل أو جزئي، وباقي يتصرف بعدين.
  7. التأمين — تحمّل الجهة مقابل تحمّل المريض، وموافقة مسبقة.
  8. المخدِّرات والمقيَّدة — سجل، ترقيم، منع مرتجع.
  9. هوية الصيدلي على البيعة (مين صرف، بأي ترخيص).
  10. البديل الجنيسي — «نفس المادة الفعّالة، ٤ بدائل».
  11. إيصال قانوني — QR، رقم ضريبي، تشغيلة، صلاحية، صيدلي.
  12. مرتجع دوائي بقواعده — يرجع مخزون ولا يتحجر؟

٤ الفجوة (GAP) — جدول التصنيف الثلاثي

موجود وموصول = شغّال دلوقتي · موجود · مش موصول = مبني بس الـPOS ما يوصلوش (ده أرخص مكسب) · مش موجود = محتاج بناء.

المطلوبالحالة الموجود فعلًااللي لازم يتغيّر
١. التشغيلة والصلاحية على السطر مش موجود عند العدّاد الدفتر موجود وFEFO شغّالة على السيرفر. لكن POSProductResource ما بيرجّعش أي lot/batch/expiry — وtracking_type نفسه متشال منه (موجود في ProductResource العام). إضافة الحقول للـResource + عرضها في خانة .product-meta الموجودة أصلًا في سطر السلة.
٢. منع بيع المنتهي مش موجود حرج inventory.expired_issue_policy افتراضيًا block — لكن قراءته الوحيدة في الريبو هي جملة SELECT واحدة في LotAllocationService.php:249. block «تصفية» مش «رفض»: بيستبعد اللوط المنتهي من FEFO، والكمية غير المغطّاة بترجع shortfall، وdecreaseStock بيرميها. المخزون اتخصم خلاص. ⇒ الدوا المنتهي بيتباع عادي.
٣. وحدة الصرف (علبة/شريط/قرص) موجود · مش موصول كل الحسابات صح: تحويل، تسعير، أرضية سعر متدرّجة، دمج أسطر منفصل لكل وحدة. مفيش أي مُبدِّل وحدة في أي قالب POS (بحث في كل الـHTML = صفر نتيجة). وindex()/search() بيحمّلوا ['category','baseUnit'] بس ⇒ منتج جاي من البحث ما معهوش قايمة وحدات أصلًا. + عيب: سطر السلة بيعرض base_unit دايمًا حتى لو الوحدة كرتونة.
٤. السعر الجبري نصف موجود طبقة حوكمة السعر شغّالة وبتقول للكاشير ليه السعر مقفول — دي أقوى ملاءمة صيدلية في الشاشة كلها. الموجود أرضية، والمطلوب مساواة. مافيش max ولا علم «سعر مقفول»، وPriceType ما فيهوش حالة سعر حكومي. + allow_price_override مش مُطبَّق على السيرفر إطلاقًا (صفر قراءة في أي controller).
٥. المريض بدل الزبون موجود · مش موصول أرخص مكسب الجسر lab_patients.partner_id موجود، وLabPatientWriteService بينشئ الشريك تلقائيًا. ومنتقي الزبون في الـPOS فيه بحث بعيد + إنشاء فوري. مصدر بحث تاني في المنتقي بيرجّع partner_id. العقد ما بيتغيّرش — الـPOS يفضل يبعت customer_id. + تعبئة رجعية لـ11,756 مريض.
٦. الروشتة والصرف مش موجود أكبر عائق الروشتة كسجل إكلينيكي كاملة ومختبَرة. مفيش كمية على سطر الروشتة ⇒ مفيش حاجة تتصرف. مفيش dispensed_quantity، مفيش تكرار صرف، مفيش صلاحية للروشتة، ومفيش مفتاح أجنبي في أي اتجاه بينها وبين أي مستند بيع.
٧. التأمين على العدّاد مش موجود على مسار Sales/POS نموذج كامل في LIS/Clinic، وNPHIES حقيقي. sales_invoices مافيهاش أي عمود تأمين/دافع/موافقة، وPOSPaymentMethod مافيهوش حالة تأمين. بحث «insurance/payer» في Modules/Sales + Modules/POS = صفر.
٨. المخدِّرات والمقيَّدة مش موجود — ولا حاجة مفيش علم is_controlled/requires_prescription، مفيش تصنيف جدول، مفيش سجل، مفيش تقرير رقابي. بحث في الريبو كله = صفر.
٩. هوية الصيدلي مش موجود البيعة بتسجّل created_by وapproved_by. مفيش «صرف بمعرفة» منفصل، مفيش ترخيص على users، ومفيش صلاحية pharmacy.*. وملحوظة أمنية: مدخل بيعة الـPOS محروس بـsales.invoices.createأي حد يقدر يعمل فاتورة مكتبية يقدر يرحّل بيعة على الماكينة.
١٠. البديل الجنيسي نصف موجود منتقي المتغيّرات موجود بالكامل (قائمة خيارات بسعر ومخزون + مفتاح Escape) — بس ميت لأن POSProductResource ما بيرجّعش has_variants أبدًا. يتحوّل لمنتقي بدائل: «نفس المادة الفعّالة — ٤ بدائل». محتاج عمود اسم علمي ما هوش موجود على products (موجود على clinic_drugs.active_ingredient — جدول تاني).
١١. الإيصال القانوني موجود · مش مطبوع الـQR بتاع ZATCA متولّد ومتخزّن على السيرفر خلاص. بحث في pos-receipt.service.ts عن qr|vat|tax_number|zatca = صفر. ناقص كمان: التشغيلة، الصلاحية، الوحدة المصروفة (normaliseLines بيرمي unit_id تمامًا)، الاسم العلمي، الطبيب، الصيدلي، الرقم الضريبي.
١٢. المرتجع الدوائي مش موجود مصدر انحراف المرتجع بيرجّع المخزون فعلًا. POSRefundController.php:222 بيثبّت 'affects_inventory' => trueمفيش خيار حجر أو إتلاف. والأهم: PostSalesReturn بيرجّع المخزون بلا أي لوط ⇒ الوحدات ترجع بلا تشغيلة وبلا صلاحية وتختفي من FEFO للأبد.

٥ العوائق الهيكلية الثلاثة

العائق ١ — دفتر اللوطات دفتر جانبي، مش نظام المخزون [FIN] حرج

في جملة واحدة: التوافر بيتقرّر بلا لوط، والصلاحية ما تقدرش ترفض حاجة، وخصم البيعة بينجح دايمًا وخصم اللوط اختياري، ومسار المرتجع ما بيرجّعش لوط أبدًا.

أربع تسريبات مستقلة بتخلّيه ينحرف:

  1. decreaseStock بيخصم stock_balances أولًا وبلا شرط (StockService.php:212)، وبعدين يخصّص لوطات ويرمي الـshortfall (:245-259). بلا لوج، بلا علم، بلا استثناء.
  2. expired_issue_policy = block يستبعد بدل ما يرفض.
  3. PostSalesReturn بيرجّع بلا لوط، والـPOS بيخلّي الإرجاع بلا شرط.
  4. التسويات والجرد وإلغاء الاستلام بيخصموا بدون allocate_lots.

وده مش نظري — قِسناه على moonui2_dev_be (قراءة فقط):

المنتجΣ stock_balancesΣ lot_balances المتبقّيالفرق
17159365.000154.000−211
1716130.00030.000مطابق
171627.7405002.740+4995
17163687.000687.000مطابق
17164200.00010.000−190

٤ من ٦ منتجات batch خارجة عن التطابق — في الاتجاهين — والـmigration نفسها بتعلن هذا التطابق كـ«ثابت» في رأسها. وكمان: ٩ لوطات منتهية لسه رصيدها أكبر من صفر ⇒ غير قابلة للتخصيص للأبد تحت سياسة block، بينما وحداتها قابلة للبيع بالكامل من خلال stock_balances.

وطبقة تانية: تقرير «التشغيلات القاربة على الانتهاء» لا يعكس بيعة POS أبدًا — فرعا الـbatch المملوك للشركة بيقروا inventory_receipt_item_batches.quantity اللي بتتكتب مرة واحدة عند الاستلام وما بتتخصمش من أي مسار صرف. فتشغيلة اتباعت للصفر بتفضل ظاهرة بكميتها الكاملة كـ«قاربة على الانتهاء» للأبد.

العائق ٢ — الصرف مش مستند حرج

الروشتة سجل إكلينيكي، ومفيش حدث صرف من أي نوع تتحوّل ليه. مش حقل ناقص — النص التاني كله (التنفيذ) غايب: سطر الروشتة ملوش كمية، مفيش dispensed_quantity، مفيش عدّاد تكرار، مفيش نافذة صلاحية، ومفيش مفتاح أجنبي في أي اتجاه بين prescriptions وأي مستند بيع أو مخزون.

والتفصيلة الحاملة: ResolveDrugProduct.php:59 بيحوّل دواء الفورميلاري لمنتج Core بـ'track_inventory' => false (والتعليق: «الفورميلاري مفهوم إكلينيكي، مش مخزون»). يعني المنتجات اللي الطبيب بيكتبها هي بالتحديد المنتجات اللي بيعة POS ما تقدرش تخصم مخزونها. الوصف والبيع دلوقتي بيتحلّوا لمجموعتين منفصلتين من سجل الأصناف.

وكل فجوة تانية في مسار الرعاية معلّقة على الحدث الغايب ده: سجل المخدِّرات هو سجل صرفيات، والتحقق من التفاعلات بيحصل عند الصرف، و«المريض ده أخد إيه قبل كده» استعلام على الصرفيات.

العائق ٣ — سجل الأصناف تجاري بحت، بلا أي دلالة دوائية عالي

products فيها ٣٩ عمود. لا اسم علمي، لا مادة فعّالة، لا تركيز، لا شكل صيدلاني، لا تصنيف ATC، لا Rx/OTC، لا مخدِّر، لا حرارة تخزين، لا بلد منشأ.

٦ الملفات المتأثرة والاعتماديات

باك-إند

  • Inventory/app/Services/StockService.php جوهري
  • Inventory/app/Services/LotAllocationService.php
  • Inventory/app/Services/ReceiptLotService.php
  • Inventory/app/Actions/ApproveAdjustment.php · CancelReceipt.php
  • Inventory/…/InventoryReportController.php:1147-1211
  • Sales/app/Actions/PostSalesReturn.php [FIN]
  • Sales/…/Concerns/ValidatesMinimumSalePrice.php [FIN]
  • POS/…/Controllers/POSSaleController.php (assertStockAvailable)
  • POS/…/Controllers/POSProductController.php (eager-load)
  • POS/…/Resources/POSProductResource.php
  • POS/…/Requests/StorePOSSaleRequest.php (RejectsUnknownKeys)
  • POS/…/Requests/StorePOSRefundRequest.php
  • POS/app/Models/POSTerminal.php (إعدادات وضع الصيدلية)
  • Core/…/create_products_table (+ migration أعمدة دوائية)
  • Clinic/app/Actions/ResolveDrugProduct.php حرج

فرونت-إند

  • features/pos/models/pos.model.ts
  • features/pos/components/invoice-table/* (سطر السلة + شريط التعديل + منتقي الزبون)
  • features/pos/components/pos-header/* (نتائج البحث)
  • features/pos/components/variant-picker/* (يُعاد توظيفه)
  • features/pos/services/pos-cart.service.ts (setUnit)
  • features/pos/services/pos-policy.service.ts (سعر مقفول)
  • features/pos/services/pos-receipt.service.ts (normaliseLines + QR)
  • features/pos/services/pos-offline.service.ts (باركود الوحدات)
  • features/pos/pos-layout.component.* (وضع الصيدلية)
  • features/pos/pos-settings/* (مفاتيح الوضع)
  • assets/i18n/{ar,en}.json ⚠ تُحرَّر فقط — لا git checkout

الاعتماديات الترتيبية (لازم تتحترم)

دفتر اللوطات يبقى صادق  ←  الصلاحية تبقى رفض  ←  العدّاد يشوف اللوط
                                                       ↓
                              المريض على البيعة  ←  الأهلية/التأمين  ←  الفاتورة المقسومة
                                                       ↓
                              هوية الدواء (اسم علمي)  ←  البديل الجنيسي
                                                       ↓
                              الصرف كمستند  ←  المخدِّرات + السيريال + تاريخ الصرف

كل سهم اعتماد حقيقي: عرض تشغيلة على شاشة بينما الدفتر منحرف = عرض رقم غلط بثقة. وسجل مخدِّرات بلا حدث صرف = سجل فاضي.

٧ الحالات الحدّية

الحالةالمعالجة المقترحة
الكمية موزّعة على أكتر من تشغيلة (١٠ علب: ٦ من B-1، ٤ من B-2) سطر واحد في السلة، وتخصيص متعدد تحته. الإيصال يطبع التشغيلتين. FEFO بتعمل ده على السيرفر خلاص — الناقص العرض.
الكمية أكبر من كل اللوطات غير المنتهية اليوم: يبيع بصمت ويترك عجز. المقترح: 422 باسم الصنف والمتاح — بانر خطأ البيعة الموجود بيعرض الجملة دي حرفيًا خلاص.
منتج batch لكن مستلم بلا تشغيلة ممكن اليوم (ReceiptLotService.php:45 بيعدّي على السطر). لازم يبقى ممنوع عند الاستلام، مش عند البيع — وإلا نكتشفه على العدّاد قدام العميل.
مرتجع لدوا خرج من الصيدلية قرار المالك (بند ٤ تحت). تقنيًا: مفتاح restock على سطر المرتجع + إعادة اللوط الأصلي أو حجر.
مرتجع لتشغيلة انتهت بعد البيع ترجع للحجر إجباريًا مهما كان الإعداد.
أوفلاين + صلاحية الكاش المحلي بيحمل أقرب صلاحية وقت المزامنة. ما يتقفلش الباب: التحذير يفضل، والرفض النهائي على السيرفر عند المزامنة — نفس منطق idempotency الموجود.
أوفلاين + مخدِّر ما يتصرفش أوفلاين إطلاقًا. سجل مخدِّرات بيتزامن بعدين مش سجل.
صرف جزئي لروشتة «الأوردر المعلَّق» الموجود هو الشكل الطبيعي: المريض ياخد ٢ من ٤ دلوقتي والباقي محجوز على نفس الروشتة. HeldOrder.notes بيدور ذهابًا وإيابًا خلاص.
تأمين + كوبون + خصم يدوي معًا تحمّل الجهة يُحسب بعد الخصومات. ومتروك معروف: خصم السطر لسه يقدر ينزل تحت أرضية السعر — ولازم يتقفل على التلات طلبات معًا (invoices/orders/POS).
الوحدة كرتونة والأرضية على القرص متعالَج صح خلاص — الأرضية بتتدرّج بعامل التحويل.
باركود مكرر بين منتجين حل غير محدَّد اليوم. مفتاح فريد لكل شركة + التحقق من صيغة GTIN (الدالة موجودة في WebStore).
مريض بلا partner_id يتعمل عند الاختيار (LabPatientWriteService بيعمل كده خلاص) + تعبئة رجعية مرة واحدة.

٨ معاينة الواجهة المتوقَّعة

القاعدة اللي التصميم كله ماشي عليها: عمود التحكم (شمال) ما ياخدش أي حاجة جديدة غير الفلوس. كل اللي وصفي بيروح للوحة اليمين — عندها ≈872px عرض غير مستغل موزّعة على ستة أعمدة مرنة، وجسم بيـscroll.

٨-١ قبل — الشاشة الحالية (١٣٦٦×٧٦٨ · RTL)

قبل الوضع الحالي المنشور على /app
خروجع/EN● متصل
🔍 بحث▭ امسح الباركود …فرع القاهرة — ك١
الإجمالي
185.50 ج
💵 نقدي · F10
💳 شبكة F11
👛 مقسّم F1
ⓘ جاهز للدفع
لوحة الأرقام
(أقصر ٦٤px من طبيعتها — بتـscroll)
استرجاع
خصم
إيصال
إغلاق
👤 زبون نقديتعليق · إلغاء الكل · مضغوط
#اسم الصنفالكميةالسعرالخصمالضريبةالإجمالي
1أوجمنتين 1جم
قرص6221…
248.0096.00🗑
2بانادول إكسترا
قرص5000…
58.9044.50🗑
٢ أصناف · ٧ قطعالفرعي 178.50 · الضريبة 7.00 · الخصم −0.00

اللي الصيدلي مش شايفه: مفيش تشغيلة · مفيش صلاحية · مفيش رصيد أصلًا — بيعرف إن الصنف خلص من خطأ ٤٢٢ لحظة الدفع. والوحدة المعروضة هي الأساسية دايمًا حتى لو مسح كرتونة.

٨-٢ بعد — نفس الشاشة في «وضع الصيدلية»

بعد نفس المكوّنات · المخطّط الأخضر = جديد · بدون شاشة تانية
خروجع/EN● متصل
🔍 بحث بالاسم أو المادة الفعّالة▭ امسح الباركود …فرع القاهرة — ك١
الإجمالي
185.50 ج
🛡 تتحمّل الجهة 92.75
يدفع المريض
92.75 ج
💵 نقدي · F10
💳 شبكة F11
👛 مقسّم F1
ⓘ جاهز للدفع
لوحة الأرقام — شريط مطوي (يحرّر ≈290px)
استرجاع
خصم
سجل الصرف
إغلاق
👤 المريض: منى ع. · ٣٤س · #ب-4471 وصفة ▾
🛡 مصر للتأمين · عضوية 88-2210 · تحمّل ٥٠٪آخر صرف #911
#اسم الصنفالكميةالوحدةالسعرالخصمالضريبةالإجمالي
1 أوجمنتين 1جم ⟨أموكسيسيلين⟩
⚠ ينتهي 06/2026ت B-2291متاح 12
2علبة ▾48.00 🔒96.00🗑
2 بانادول إكسترا ⟨باراسيتامول⟩
11/2027ت B-7734متاح 40
5شريط ▾8.90 🔒44.50🗑
شريط التعديل: الكميةالسعر 🔒خصم٪الضريبة ▾ الوحدة ▾التشغيلة ▾🗑 حذف
٢ أصناف · ٧ قطعالفرعي 178.50 · الضريبة 7.00 · الخصم −0.00
🛡 تتحمّل الجهة 92.75→ يدفع المريض 92.75

ليه ده رخيص — كل إضافة نازلة في خانة موجودة أصلًا

الإضافةبتنزل فينالتكلفة
التشغيلة + الصلاحية.product-info > .product-metaموجودة ومنسّقة وبتحمل دلوقتي الوحدة والباركودصفر عرض · +12px للصف
شارة انتهاء / قرب انتهاءنفس مفردات الشارات .badge-* الموجودةCSS فقط
عمود الوحدةعمود جديد ~72px من باقي عمود الاسم (العمود الوحيد غير المقيَّد)عمود واحد
مُبدِّل الوحدةحقل سادس في شريط التعديل (خمسة حقول على ~940px)فيه مساحة
شريط المريضصف رأس اللوحة (44px → 64px، سطرين)تعليق/إلغاء/مضغوط تروح لـ«⋯»
تقسيم التأمينشريط المديونية — نفس الشكل ونفس المكان، أرقام مختلفةمكوّن موجود
منتقي البديل الجنيسيمنتقي المتغيّرات الميت — قائمة بسعر ومخزون ومفتاح Escape جاهزينإعادة توظيف
«يدفع المريض»سطر رابع في .pay-totalالإضافة الوحيدة المقبولة في عمود التحكم لأنها بتغيّر المبلغ المستلَم14px من لوحة الأرقام
السعر الجبريقفل حوكمة السعر الموجودallow_price_override=false + أرضية = السعر المطبوعإعداد، مش كود UI

٨-٣ سطر السلة — تكبير

التغيير كله في خليّة الاسم + عمود واحد جديد
#الصنفالكميةالوحدةالسعرالإجمالي
1
أوجمنتين 1جم ⟨أموكسيسيلين⟩ ← عمود BE جديد
🔴 ينتهي 06/2026ت B-2291متاح 12
↑ في .product-meta الموجودة — بدل الوحدة+الباركود
─ 2 +علبة ▾
72px
48.00 🔒96.00🗑

٨-٤ الإيصال — قبل / بعد

قبل الحالي
صيدلية النور
القاهرة — 0100…
فاتورة: POS-000911
التاريخ: 2026-08-01 14:22
الكاشير: أحمد
العميل: زبون نقدي
أوجمنتين 1جم
  2 × 48.00 = 96.00
بانادول إكسترا
  5 × 8.90 = 44.50
الفرعي 178.50
الضريبة 7.00
الإجمالي 185.50
نقدي 200.00
الباقي 14.50
شكرًا لزيارتكم

مفيش QR · مفيش رقم ضريبي · مفيش تشغيلة · مفيش صلاحية · مفيش وحدة («2» يعني إيه؟) · مفيش صيدلي

بعد الأخضر = مضاف
صيدلية النور
القاهرة — 0100…
الرقم الضريبي: 310…003 · ترخيص: 4471
فاتورة: POS-000911 (مبسّطة)
التاريخ: 2026-08-01 14:22
الكاشير: أحمد
الصيدلي: د. منى ع. — ترخيص 88221
العميل: منى ع. #ب-4471
أوجمنتين 1جم
  2 علبة × 48.00 = 96.00
ت B-2291 · ينتهي 06/2026
بانادول إكسترا
  5 شريط × 8.90 = 44.50
ت B-7734 · ينتهي 11/2027
الفرعي 178.50
الضريبة 7.00
الإجمالي 185.50
تتحمّل الجهة 92.75 · يدفع المريض 92.75
نقدي 100.00
الباقي 7.25
QR — موجود على السيرفر خلاص

٩ خطة التنفيذ — حزم عمل مرتّبة

الترتيب مش اقتراح — هو اعتماد. عرض تشغيلة على الشاشة قبل ما الدفتر يبقى صادق = عرض رقم غلط بثقة، وده أسوأ من عدم العرض. المرحلة أ شرط لكل اللي بعدها.

المرحلة أ — دفتر اللوطات يبقى صادق [FIN] شرط مسبق

WPالنطاقالمستودعMigrationالاختبار اللي يثبته
WP1التشغيلة + الصلاحية إجباريتين عند اعتماد الاستلام لمنتج tracking_type=batch (دلوقتي ReceiptLotService.php:45 بيعدّي على السطر)BEلااستلام batch بلا تشغيلة ⇒ 422
WP2الـshortfall يبقى قاتل مش صامت لمنتجات batch: التخصيص قبل خصم الرصيد، أو استثناء قبل StockService.php:212BEلابيع أكتر من اللوطات ⇒ 422 والمخزون ما يتحركش
WP3إعادة اللوط في المرتجعات: PostSalesReturn يعكس تخصيص الفاتورة أو يقبل لوط صريح + مفتاح restock على سطر مرتجع الـPOSBEنعممرتجع ⇒ يرجع لنفس اللوط؛ حجر ⇒ ما يرجعش للرصيد المتاح
WP4توصيل allocate_lots في ApproveAdjustment وCancelReceiptBEلاتسوية سالبة ⇒ اللوط يتخصم
WP5تقرير + أمر تسوية Σ لوطات مقابل stock_balances — لتصفية الانحراف القائم (٤ من ٦ منتجات)BEلاالتقرير يكشف الخمس حالات المقيسة

المرحلة ب — الصلاحية تبقى رفض حرج

WPالنطاقالمستودعMigrationالاختبار
WP6توافر واعٍ بالصلاحية في assertStockAvailable(): المتاح = Σ المتبقّي غير المنتهي، مش stock_balances.quantity + 422 على السيرفر لما block ما تتحققشBEلالوط منتهي فقط ⇒ البيعة ترفض بالاسم والمتاح
WP7تطبيق inventory.issues.expired-override على السيرفر (دلوقتي نص مزروع بمعنى واجهة فقط) + فرع صلاحية في pickManual()BEلابلا الصلاحية ⇒ 403؛ بيها ⇒ يمر مع تعليم expired_override
WP8توجيه فرعَي الـbatch في تقرير «القاربة على الانتهاء» لـlot_balances.remaining_quantity (الفرع ٤ بيعمل ده للأمانات خلاص — قلب فلتر)BEلاتشغيلة اتباعت للصفر ⇒ ما تظهرش

المرحلة ج — العدّاد يشوف

WPالنطاقالمستودعMigrationالاختبار
WP9POSProductResource: tracking_type + أقرب صلاحية + المتاح لكل لوط. + إعداد pharmacy_mode على الماكينة (وقايمة إعداداتها الموجودة)BEنعمعقد الحمولة في POSFrontendPayloadContractTest
WP10عرض التشغيلة والصلاحية في .product-meta + شارة انتهاء/قرب انتهاء + المتاح عند المسح (بدل ما يعرفه من ٤٢٢)FEng build + فحص بصري RTL
WP11اختيار لوط اختياري في الـPOS: إعلان items.*.lot_allocations (StockService بيمرّر manual خلاص)BE+FEلااختيار يدوي ⇒ يتخصم من نفس اللوط

المرحلة د — وحدة الصرف أرخص مكسب تشغيلي

WPالنطاقالمستودعMigrationالاختبار
WP12eager-load('productUnits') في index() وsearch()ده كل شغل الباك-إندBEلاالبحث يرجّع قايمة وحدات
WP13مُبدِّل وحدة في شريط التعديل + setUnit(index, unitId) بإعادة استخدام resolveUnitPrice + إصلاح عرض الوحدة الخطأ على السطرFEتبديل علبة/شريط ⇒ السعر والأرضية يتبعوا
WP14الكاش الأوفلاين يطابق باركود الوحدات (دلوقتي p.barcode بس)FEمسح كرتونة أوفلاين ⇒ يتعرف

المرحلة هـ — المريض والتأمين [FIN]

WPالنطاقالمستودعMigrationالاختبار
WP15منتقي الزبون يبحث المرضى ويرجّع partner_idالعقد ما يتغيّرش (الـPOS يفضل يبعت customer_id)BE+FEلااختيار مريض ⇒ بيعة صحيحة على شريكه
WP16تعبئة رجعية لـpartner_id (١١٬٧٥٦ مريض) — migration أمامية idempotentBEنعمتشغيل مرتين ⇒ نفس النتيجة
WP17شريط المريض في رأس اللوحة + لوحة «آخر صرف» من visit-history الموجودFEng build
WP18مفهوم الجهة الدافعة على مسار Sales/POS: تقسيم تحمّل + رقم موافقة + شريط التأمين (شكل شريط المديونية)BE+FEنعمتقسيم = الإجمالي؛ الترحيل صح على الحسابين
WP19زرار أهلية NPHIES من العدّاد (المدخل موجود، محتاج patient_id بس)FEاستدعاء يرجّع نسبة التحمّل

المرحلة و — هوية الدواء والسعر الجبري [FIN]

WPالنطاقالمستودعMigrationالاختبار
WP20أعمدة دوائية على products: اسم علمي/مادة فعّالة · تركيز · شكل · Rx/OTC · مخدِّر · حرارة تخزين · ATC + كشف manufacturer_id وshelf_life_days في CoreBEنعمCRUD + Resource
WP21البحث بالاسم العلمي (ProductService::search()) + تحويل منتقي المتغيّرات الميت لمنتقي بدائلBE+FEلابحث «باراسيتامول» ⇒ كل البدائل
WP22سعر مقفول: علم price_is_fixed + فحص مساواة جنب الأرضية، يغطي خصم السطر والرأس معًا (بيقفل كمان المتروك المعروف)BEنعمأي انحراف ⇒ 422 على التلات طلبات
WP23تطبيق allow_price_override على السيرفر + مفتاح فريد للباركود لكل شركة + تحقق GTINBEنعمPOST مباشر بسعر معدَّل ⇒ 422
WP24إيصال قانوني: QR بتاع ZATCA + الرقم الضريبي + الوحدة (normaliseLines بيرميها) + التشغيلة + الصلاحية + الصيدليBE+FEلامعاينة الإيصال

المرحلة ز — الصرف كمستند أكبر بند

WPالنطاقالمستودعMigrationالاختبار
WP25إصلاح ResolveDrugProduct: يحافظ على المادة الفعّالة والشكل والتصنيف، ويطابق على code/sku مش name، وtrack_inventory يبقى قرار مش ثابتBEلادواء فورميلاري ⇒ منتج قابل للتخزين
WP26كمية على سطر الروشتة + dispensed_quantity + صلاحية الروشتة + تكرار الصرفBEنعمصرف جزئي ⇒ الباقي صحيح
WP27حدث الصرف: ربط بين الروشتة والفاتورة، وتفعيل PrescriptionStatus::DispensedBEنعمصرف كامل ⇒ الحالة تتغيّر؛ جزئي ⇒ لأ
WP28الصرف الجزئي على الأوردر المعلَّق (F4) بمرجع الروشتةFEاسترجاع ⇒ الباقي بس

المرحلة ح — المخدِّرات والتتبّع تنظيمي

WPالنطاقالمستودعMigrationالاختبار
WP29سجل المخدِّرات: قيد متسلسل لكل صرف + الطبيب + المريض + منع الصرف أوفلاين + منع المرتجعBE+FEنعممخدِّر أوفلاين ⇒ يترفض؛ التسلسل بلا فجوات
WP30هوية الصيدلي: ترخيص على المستخدم + «صرف بمعرفة» على البيعة + صلاحية pos.sales.create منفصلة بدل sales.invoices.createBEنعمصلاحية مكتبية وحدها ⇒ ما تقدرش ترحّل على الماكينة
WP31سيريال عند البيع (اليوم ما بيتحركش أبدًا في بيعة POS) + قراءة GS1 DataMatrixBE+FEلابيع سيريال ⇒ حالته تتغيّر لمباع
إجمالي حزم العمل
31
شرط مسبق (أ+ب)
8
أرخص مكسب (ج+د+هـ١)
7
مُعلَّمة [FIN]
14

١٠ قرارات محتاجة قرارك

١. وضع صيدلية على نفس الشاشة، ولا شاشة منفصلة؟

توصيتي: علم pharmacy_mode على الماكينة — مش فرع. حلقة العدّاد صح خلاص وكلّفت مراجعة ١٢ حزمة (RejectsUnknownKeys، سباق local_id، سقف مرتجع الدرج). شاشة تانية معناها إعادة كسب الصح ده من الأول بلا مقابل. وقايمة إعدادات الماكينة الحالية (allow_discount، require_customer…) هي بالظبط المكان الطبيعي للمفاتيح الجديدة.

٢. أي بلد؟ (سعودية / مصر / الاتنين)

ده بيحدّد تلات حاجات ما ينفعش نخمّنها: السعر الجبري (مساواة ولا سقف)، التتبّع الحكومي (RSD السعودي مقابل T&T المصري)، ومحتوى الإيصال القانوني. لو السعودية أولًا: مسار ZATCA متتبَّع بالكامل والـQR جاهز على السيرفر — WP24 يبقى شبه مجاني. لو مصر، محتاج نقرا متطلبات EtaDocumentBuilder الأول.

٣. صيدلية تجزئة مستقلة، ولا مربوطة بالعيادة؟

لو تجزئة مستقلة: المرحلة ز (الصرف كمستند — ٤ حزم) تتأجّل بالكامل، والصيدلية تشتغل ببيع عادي بتشغيلة وصلاحية. لو مربوطة بالعيادة: المرحلة ز شرط، وWP25 (ResolveDrugProduct) لازم يسبق كل حاجة لأن الأدوية الموصوفة حاليًا غير قابلة للتخزين أصلًا. توصيتي: ابدأ تجزئة مستقلة — بتوصل ٨٠٪ من القيمة بـ٢٠٪ من الشغل، والصرف يتبني بعدين على أساس صادق.

٤. مرتجع الدوا: يرجع للمخزون ولا يتحجر؟ [FIN]

اليوم بيرجع دايمًا وبلا شرط، وبلا تشغيلة — ودي أكبر مصادر الانحراف المقيس. توصيتي: إعداد على الشركة، الافتراضي «حجر» — أغلب اللوائح بتمنع إعادة بيع دوا خرج من الصيدلية. الحجر برضه بيحتاج WP3 عشان نعرف اللوط اللي رجع.

٥. الانحراف القائم: نصلّحه ولا نمنع الجديد بس؟ [FIN]

٤ من ٦ منتجات batch في التطوير خارجة عن التطابق، و٩ لوطات منتهية برصيد موجب. توصيتي: WP5 يطلع تقرير أولًا (بلا كتابة) — نشوف الحجم على بيانات العميل الحقيقية، وبعدين تقرّر التسوية. التسوية العمياء على أرقام ما شفناهاش أخطر من الانحراف نفسه.

٦. المخدِّرات في نطاق المرحلة الأولى؟

مفيش منها ولا حاجة النهارده — ولا علم ولا سجل ولا تقرير. توصيتي: برّه المرحلة الأولى، بس علم is_controlled يتضاف في WP20 مع باقي الأعمدة عشان ما نعملش migration تاني على products بعدين. السجل نفسه (WP29) يستنى قرار البلد.

٧. الثغرة الأمنية دي تتقفل دلوقتي ولا مع الباقة؟

مدخل بيعة الـPOS محروس بـsales.invoices.createأي حد يقدر يعمل فاتورة مكتبية يقدر يرحّل بيعة على الماكينة، وallow_price_override مش مطبَّق على السيرفر إطلاقًا. دول مش خاصين بالصيدلية — مفتوحين دلوقتي على نقاط البيع العادية. توصيتي: نقلهم لباتش مستقل صغير قبل أي شغل صيدلية.

١١ اللي ما قدرناش نحسمه — بصراحة

البندالحالة
سبب كل صف انحراف على حدةقِسنا الفروق (17162 بـ+4995، 17164 بـ−190) وحدّدنا أربع آليات تقدر تنتجها، بس ما تتبعناش تاريخ الحركات لكل منتج عشان ننسب كل فرق لآليته. تتبّع inventory_movements بالقراءة يحسمه.
هل مسار POS→لوط اشتغل على بيانات حقيقية؟موصول في الكود ومغطّى باختبارات، بس في التطوير: ١١ فاتورة POS وصفر تخصيص لوط. يعني مش مُجرَّب، مش مثبَت خطأه.
هل NPHIES شغّال عند أي عميل؟config.php:12-13 بيوجّه الـsandbox والإنتاج لنفس العنوان — يقرا كقيمة نائبة مستنية env (والموديول ما بيقراش أي env أصلًا؛ كل الاعتماد في جدول nphies_config). وتأكّد لاحقًا (2026-08-01): ما اتصلش ولا مرة على البيئة دي — ١٢ معاملة كلهم timeout، is_active=0، والشهادات مش مرفوعة. تصحيح: nphies:poll مجدول فعلًا كل ٥ دقايق في NPHIESServiceProvider.php:55-61 — العبارة الأصلية هنا كانت غلط.
هل نسبة ٤١/١١٬٧٩٧ تعكس الإنتاج؟دي قاعدة التطوير بس. تكلفة التعبئة الرجعية (WP16) متوقّفة على الرقم الحقيقي.
أعمدة tracking_type وvariant_typeاتحسم (2026-08-01): ليهم migrations فعلًا — في database/migrations/ الجذر مش تحت Modules/*، ومحروسين بـhasTable/hasColumn. ودي كمان النمط المعتمد لإضافة عمود على جدول تملكه وحدة تانية.
المتطلبات التنظيمية نفسهاقلنا إيه اللي الإيصال بيرميه مقارنة بمواصفة ZATCA وبالممارسة المعتادة — مش مقارنة بقايمة قانونية من وزارة صحة. ده مصدر تنظيمي، مش الريبو.

الخلاصة في فقرة

الشاشة تصلح، ومش محتاجة شاشة تانية. الغالي في نقطة البيع — الحلقة — مبني وصح: باركود أولًا مع رجوع التركيز، بانر مسح غير معروف، مفاتيح F، لوحة باقي بتعيش لحد المسحة اللي بعدها، طابور أوفلاين بإعادة تشغيل آمنة، جرد أعمى عند الإقفال، وطبقة حوكمة سعر بتفرض سعرًا ثابتًا غير قابل للتعديل بأرضية — وهي بالظبط قاعدة السعر المطبوع بإسم تاني. والإضافات كلها نازلة في خانات موجودة: التشغيلة والصلاحية في سطر الميتا اللي بيعرض دلوقتي الوحدة والباركود، وتقسيم التأمين في شريط المديونية، والبديل في منتقي المتغيّرات الميت، والروشتة في الأوردر المعلَّق.

الخطر مش في الشاشة — الخطر تحتها. اختيار التشغيلة عند البيع مش موجود في Modules/POS إطلاقًا (تعليق واحد، صفر كود، مقابل inventory_lot_balances مبني بالكامل)، والصلاحية لا ترفض أي حاجة، والمرتجع ما بيرجّعش لوط — و٤ من ٦ منتجات batch في بيئتك خارجة عن التطابق دلوقتي. والصرف مش مستند، والأدوية اللي الطبيب بيكتبها غير قابلة للتخزين بالتصميم.

عشان كده الترتيب هو الخطة: صحّح الدفتر (أ) → خلّي الصلاحية ترفض (ب) → ورّي العدّاد (ج) → وحدة الصرف (د). دي ١٤ حزمة بتديك صيدلية تجزئة تشتغل صح. المريض والتأمين والروشتة والمخدِّرات بيتبنوا بعدين — على أساس صادق.

تحليل المرحلة ١ — مفيش كود ولا migration اتكتب · تلات مسارات بحث متوازية · ١ أغسطس ٢٠٢٦ · moonui2 · hazemdev2
مستنّي موافقتك على النطاق والترتيب قبل أي تنفيذ.