أمر الشراء يفضل مفتوح للاستلام

طلبك: أمر الشراء ما يتقفلش قبل ما يتعمل له إذن استلام — حتى لو اتعملت له فاتورة واتدفعت، لأن ساعات بتدفعوا الأول وتستلموا بعدين. الوثيقة دي بتقول: السبب كان إيه بالظبط، غيّرت إيه، وإزاي اتأكدت إن الإصلاح شغال.

[FIN] يمسّ دورة الشراء اتنفّذ ✓ ٥ اختبارات انحدار moonui2 · hazemdev2

اللي طلبته

«أمر الشراء يفضل مفتوح بدل ما يتعملش له إذن، حتى لو اتعمل له فاتورة شراء واتدفعت يبقى لسه ما اتسلمش — ممكن يتعمل له إذن استلام في أي وقت، لأن ساعات بندفع وبعدين بنستلم.»

وده امتداد طبيعي لخيار «الفوترة قبل الاستلام» اللي فتحناه قبل كده: فتحنا الفوترة، بس الأمر كان بيتقفل بعدها فما يبقاش ينفع تستلم عليه.

السبب الجذري — حقل واحد شايل بُعدين

أمر الشراء عنده تلات حقول بيوصفوا حالته:

الحقلبيقول إيهكان بيتحسب صح؟
receive_statusوصل قد إيه — pending/partial/complete آه، دايمًا صح
bill_statusاتفوتر قد إيه — نفس القيم آه، دايمًا صح
statusحالة واحدة عامة بتخلط الاتنين: confirmed · partially_received · fully_received · partially_billed · fully_billed لأ — هنا المشكلة
العيب رقم ١ — الفوترة كانت بتمسح الاستلام. في PurchaseOrder::recalculateBillStatus() كان مكتوب:
if ($allBilled) {
    $this->update(['status' => PurchaseOrderStatus::FullyBilled]);
}
من غير أي شرط. فأمر اتفوتر بالكامل وما وصلش منه ولا قطعة كان بياخد حالة fully_billed — والنظام بيقراها كأن الأمر خلص.
العيب رقم ٢ — الواجهة كانت بتخفي الزرار. في orders.component.html:136:
@if (o.status === 'confirmed' || o.status === 'partially_received')
fully_billed مش في القائمة، فزرار «إنشاء إذن استلام» كان بيختفي خالص — مش بيبقى مقفول، بيختفي. وده بالظبط اللي إنت شايفه.
العيب رقم ٣ — حتى لو استلمت، الحالة كانت هتفضل عالقة. recalculateReceiveStatus() بتحرّك الحالة لـ«مستلم جزئيًا» بس لو الحالة الحالية واحدة من confirmed / sent / fully_received — وحالات الفوترة مش فيهم. فحتى لو دخلت إذن استلام بطريقة تانية، الحالة كانت هتفضل متجمّدة على حالة الفوترة.

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

اللي غيّرته — تلات تعديلات

١. الفوترة مابقتش تدهس الاستلام خلفية

Modules/Purchases/app/Models/PurchaseOrder.php — recalculateBillStatus()

$stillAwaitingGoods = $items->contains(
    fn ($item) => (float) $item->received_quantity < (float) $item->quantity
);

if ($stillAwaitingGoods) {
    return;   // الأمر لسه عليه بضاعة لم تصل — حالة الاستلام تفضل زي ما هي
}

يعني الأمر اللي لسه مستني بضاعة بيحتفظ بحالة الاستلام بتاعته، والفوترة بتتسجّل في مكانها الصح: bill_status.

٢. الاستلام بقى يقدر يتحرّك وهو في حالة فوترة خلفية

نفس الملف — recalculateReceiveStatus(): ضفت حالتين للقائمة اللي بتسمح بالانتقال لـ«مستلم جزئيًا»:

PurchaseOrderStatus::PartiallyBilled,
PurchaseOrderStatus::FullyBilled,

وكمان لما الاتنين يخلصوا (وصلت كلها + متفوترة كلها) الحالة بتروح fully_billed بدل ما تدهس الفوترة بـfully_received — عشان الحقيقة الأحدث هي اللي تبان.

٣. الزرار بقى بيبان طول ما فيه بضاعة لم تصل واجهة

orders.component.html — الشرط بقى مبني على الحقل اللي بيقول الحقيقة فعلًا:

@if (
  o.receive_status !== 'complete' &&
  (o.status === 'confirmed' ||
   o.status === 'partially_received' ||
   o.status === 'partially_billed' ||
   o.status === 'fully_billed')
)

حالات الفوترة مضافة عشان الأوامر العالقة عندك دلوقتي يرجع لها الزرار، وreceive_status !== 'complete' عشان الزرار يختفي لما البضاعة توصل بالكامل.

إزاي اختبرته

كتبت ملف اختبار انحدار جديد: Modules/Purchases/tests/Feature/OrderStaysReceivableAfterBillingTest.php — خمس حالات:

#الحالةبيثبت إيه
١أمر اتفوتر بالكامل وما وصلش منه حاجة bill_status = complete لكن الحالة تفضل confirmed وreceive_status = pending — ده العيب المبلَّغ
٢وصل ١٠ من ٣٠ على أمر متفوتر الحالة تتحرك لـpartially_received فعلًا
٣فوترة الأول ثم استلام كامل ينتهي على fully_billed — الفوترة ما اتفقدتش
٤الطريق العادي: استلام ثم فوترة ما اتغيّرش ولا حاجة — fully_received ثم fully_billed
٥فاتورة جزئية على أمر ما وصلش يفضل مفتوح للاستلام برضه
والأهم — أثبتّ إن الاختبارات دي بتفشل من غير الإصلاح. اختبار بينجح دايمًا مش بيثبت حاجة. فعطّلت الحارس الجديد بس (سطر واحد)، وشغّلت الاختبارات تاني:
مع الإصلاح:     5 passed
بعد تعطيل الحارس: 2 failed, 3 passed
الحالتان ١ و٥ — اللي بيثبتوا الحارس بالظبط — فشلوا. (٢ و٣ و٤ فضلوا خُضر لأنهم بيختبروا النص التاني من الإصلاح، وهو كان لسه مكانه.) وبعدين رجّعت الملف لحالته المصلَّحة وتأكدت إن الحارس موجود والتعطيل اتشال.

وبعدها شغّلت سويت المشتريات كاملة عشان أتأكد إن مفيش حاجة تانية اتكسرت، وبنيت الواجهة (ng build) للتأكد من التعديل.

اللي ما اتغيّرش — عن قصد

الأوامر العالقة عندك حاليًا

الأوامر اللي اتفوترت قبل الإصلاح واقفة دلوقتي على fully_billed / partially_billed وهي ما وصلتش. تعديل الواجهة عامل حسابهم: الزرار هيرجع يبان لهم فورًا، وأول ما تعمل إذن استلام الحالة هتتصحح لوحدها (بفضل التعديل رقم ٢).

يعني مش محتاج أي إصلاح بيانات. الأوامر القديمة بتتعافى بمجرد أول استلام. لو حبيت أعدّ لك كام أمر في الحالة دي على prod.elbaset، قولّي — بس ده استعلام على بيئة الإنتاج، مش هعمله من غير كلمة منك.

الشغل على hazemdev2 ومنشور على /app بتاع moonui2 — ما اتعملش دمج لـmain، مستني اختبارك.