توحيد العملة في Moon ERP

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

[FIN] يمسّ مستندات مالية ١٠ موديولات تصحيح مباشر — مش توصيات moonui2 · فرع hazemdev2 ١٩ أغسطس ٢٠٢٦

الخلاصة في سطرين

البرنامج مكانش عنده إجابة واحدة لسؤال «الشركة دي بتتعامل بأي عملة؟». كان عنده تلاتة: المحاسبة والمبيعات والمشتريات بيفترضوا KWD، والمعمل (LIS) بيفترض SAR، والمتجر الإلكتروني بيفترض EGP — كل واحد مكتوب في الكود نفسه. يعني نفس الشركة تقدر تطبع فاتورة بعملة وإيصال بعملة تانية.

والأسوأ إن أغلب صفوف الفلوس مكانش مكتوب عليها عملة أصلًا: ٥٥٤ صف من ٦٤١ في جداول المدفوعات والسندات والتحويلات فاضيين تمامًا من العملة، لأن كل قواعد التحقق كاتبة nullable — فأي مسار إنشاء مايذكرش الحقل بيكتب فراغ في صمت.

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

الرقم اللي بيحكي القصة

ده مش انطباع — ده إحصاء فعلي على قاعدة moonui2_dev_be لكل جدول بيحمل فلوس:

٥٥٤
صف فلوس من غير عملة
٦٤١
إجمالي صفوف الفلوس
٨٦٪
نسبة الخواء
٣
عملات في شركة واحدة
الجدولالعمودصفوفمن غير عملةالحالة
journal_entry_linescurrency_id523523 (100%)بالتصميم — شرح تحت
sales_paymentscurrency_id2121 (100%)اتقفل
receipt_voucherscurrency_id33 (100%)اتقفل
purchase_paymentscurrency_id22 (100%)اتقفل
payment_voucherscurrency_id11 (100%)اتقفل
account_transferscurrency_id11 (100%)اتقفل
bank_accountscurrency_id32اتقفل
purchase_billscurrency_id41اتقفل
sales_invoicescurrency_code320مليانة — بس بتلات عملات

لاحظ آخر سطر: sales_invoices مش فاضية — دي المشكلة التانية، وهي أخطر من الفراغ: بيانات مكتوبة وغلط.

السبب الجذري: تلات قيم افتراضية في برنامج واحد

لما الكود مايلاقيش عملة، بيرجع لقيمة مكتوبة فيه. المشكلة إن كل موديول اتكتب في وقت مختلف وبتفكير مختلف، فطلعت تلات إجابات:

الموديولالافتراضي القديمالملف
المحاسبة (سندات صرف/قبض)'KWD'PaymentVoucherController.php:255 · ReceiptVoucherController.php:255
المبيعات (الطباعة)'KWD'SalesPrintController.php:138
المشتريات (الطباعة)'KWD'PurchasePrintController.php:138
المعمل LIS'SAR'PatientPortalController.php:642 · LabInfoController.php:411 · ExternalLabPortalRequestController.php:1390
المتجر WebStore'EGP'MetaConversionsService.php:209 · StoreAnalyticsOutboxService.php:131
يعني إيه عمليًا؟ شركة سعودية تطبع سند صرف بالدينار الكويتي، وفي نفس اليوم تطبع إيصال معمل بالريال، ويتبعت لـMeta أوردر متجر بالجنيه المصري. تلاتتهم من نفس قاعدة البيانات، ونفس الشركة، ونفس اليوم.

وفي حالة واحدة كانت أوحش: StorefrontSeoMetadata.php:210 مكانش عنده افتراضي خالص — كان بيكتب strtoupper((string) $company->currency) على طول. لو عمود الشركة فاضي، بيطلع لجوجل "priceCurrency": "" في بيانات المنتج المهيكلة، وده بيلغي العرض من نتائج التسوّق.

واقعة «EG» — كود عملة مش موجود في الدنيا

شركة «بلاك سيركل» (رقم 4) عملتها المعرَّفة SAR، والتعريفين متطابقين (العمود companies.currency وcurrencies.is_base — مفيش تعارض هنا). ومع ذلك فواتيرها الـ٣٢ اتقسمت كده:

الكودعدد الفواتيرالإجماليالتقييم
SAR1812,352.500سليمة
KWD133,038.900غالبًا مبالغ ريال متسمّية غلط
EG195.000كود غير صالح

وفي جدول العملات نفسه لقيت صفّين:

id=7  co=4  code=EG    active=0   جنيه
id=8  co=4  code=EGP   active=0   جنيه مصري

يعني حد كتب «EG» بالغلط، اكتشف الغلط، عمل «EGP» صح، وعطّل الاتنين — لكن الفاتورة INV-2026-00006 (بتاريخ 2026-04-28، بمبلغ 95.000، وحالتها مدفوعة) فضلت شايلة الكود الغلط للأبد.

ليه دخل أصلًا؟

لقيت الباب مفتوح على مصراعيه في StoreCurrencyRequest.php:20:

'code' => ['required', 'string', 'max:10'],

أي نص لحد ١٠ حروف كان يعدّي ككود عملة. مفيش أي تحقق من إنه كود ISO-4217 حقيقي.

اتصلّح: بقى regex:/^[A-Z]{3}$/ مع تحويل تلقائي للحروف الكبيرة قبل التحقق، فمابقاش ممكن لا «EG» ولا «sar» و«SAR» يتسجّلوا كعملتين مختلفتين. (شاشة التعديل أصلًا مابتسمحش بتغيير الكود، فالباب كان واحد وقفلناه.)

اللي اتصلّح فعلًا — ١٩ ملف

كل تعديل تحت ده منفّذ ومتحقّق نحويًا، مش اقتراح:

١. توحيد القراءة (١٠ مواقع)

كل الافتراضيات المكتوبة في الكود اتشالت، وبقى كله بيسأل نفس المصدر: app(CompanyCurrency::class)->code($companyId).

#الملفكانبقى
1Accounting/PaymentVoucherController?? 'KWD'عملة المستند ← عملة الشركة
2Accounting/ReceiptVoucherController?? 'KWD'عملة المستند ← عملة الشركة
3Sales/SalesPrintController?? 'KWD'currency_code بتاع المستند ← عملة الشركة
4Purchases/PurchasePrintController?? 'KWD'currency_code بتاع المستند ← عملة الشركة
5LIS/PatientPortalController?: 'SAR'إعداد المعمل ← عملة الشركة
6LIS/LabInfoController?: 'SAR'إعداد المعمل ← عملة الشركة
7LIS/ExternalLabPortalRequestController?: 'SAR'إعداد المعمل ← عملة الشركة
8WebStore/MetaConversionsService?: 'EGP'عملة التاجر ← عملة الشركة
9WebStore/StoreAnalyticsOutboxService?: 'EGP'عملة التاجر ← عملة الشركة
10WebStore/StorefrontSeoMetadataمفيش افتراضي — ممكن تطلع فاضيةعملة الشركة مضمونة

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

٢. قفل مسار الكتابة (٧ نماذج + طبقة جديدة)

تفاصيلها في القسم الجاي.

٣. قفل بيانات العملات الرئيسية (ملف واحد)

StoreCurrencyRequest — شرحه فوق في واقعة EG.

٤. الواجهة (Angular) — وفيها كان مستخبّي رابع افتراضي

مسحت moon-erp/src كلها. الواجهة عندها مصدر واحد بالفعل (CompanyCurrencyService) وأغلب الشاشات بتستخدمه — بس فيه تلات نتايج حقيقية:

الموضعالمشكلةاللي عملته
company-currency.service.ts:46 رابع افتراضي: || 'EGP' — الواجهة بتخمّن جنيه بينما الخلفية بتخمّن دينار. عميل بيعرض عملة وسيرفر بيطبع عملة تانية. اتوحّد على KWD مطابق لـCompanyCurrency::FALLBACK
quotations.component.ts (٣ مواضع) عيب حقيقي: أي عرض سعر جديد بيتختم 'SAR' مهما كانت عملة الشركة — يعني شركة كويتية عروضها كلها بالريال السعودي. بقت عملة الشركة عبر CompanyCurrencyService
setup-wizard.component.ts · currencies.component.html سليمة دي قائمة اختيار عملات + نص إرشادي في خانة، مش افتراضات

الباقي (دالتا طباعة السندات وخدمات LIS) مسجّل بالتفصيل في «الباقي المفتوح» تحت — مش متسكّت عليه.

الطبقة المشتركة الجديدة — FillsCompanyCurrency

الملف: Modules/Core/app/Concerns/FillsCompanyCurrency.php

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

static::creating(function ($model) {
    // لو الحقل فاضي فقط — عمرها ما بتلمس اختيار المستخدم
    if (in_array('currency_id', $fillable) && $model->currency_id === null) {
        $model->currency_id = $currency->id($companyId);
    }
    if (in_array('currency_code', $fillable) && blank($model->currency_code)) {
        $model->currency_code = $currency->code($companyId);
    }
});

مطبّقة على السبعة اللي الإحصاء أثبت خواءهم — ولا واحد زيادة عن الدليل: SalesPayment · PurchasePayment · PaymentVoucher · ReceiptVoucher · AccountTransfer · PurchaseBill · BankAccount.

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

دفتر الأستاذ — ليه قررت أسيبه زي ما هو

journal_entry_lines هو أكبر رقم في الإحصاء: ٥٢٣ من ٥٢٣ من غير عملة. الحاجة السهلة كانت إني أملاه زي الباقي. مافعلتش، وده السبب — وهو سبب بالدليل مش برأي.

روحت شفت مين بيقرأ العمود ده أصلًا. الجهة الوحيدة اللي بتاخد قرار بناءً عليه هي CurrencyRevaluationService.php:51:

->whereNotNull('currency_id')
->where('currency_id', '!=', $baseCurrencyId)

الاستعلام بيستبعد الفاضي وعملة الأساس مع بعض. يعني في المنطق ده NULL معناها بالظبط «بعملة الشركة» — وهي معلومة محمولة مش إهمال.

النتيجة: لو ملّيت الـ٥٢٣ سطر بعملة الشركة، هيفضلوا مستبعدين من إعادة التقييم (لأن الشرط التاني بيستبعد عملة الأساس أصلًا) — يعني صفر تغيير في السلوك المحاسبي، مقابل مخاطرة إني ألمس الأستاذ العام من غير فايدة. سِبته.

السطور اللي فعلًا بعملة أجنبية بتاخد عملتها من المستند المصدر (ApprovePaymentVoucher وApproveReceiptVoucher بينقلوا $voucher->currency_id) — والسندات دي بقت مضمونة العملة دلوقتي بعد التعديل. يعني الأستاذ بيتحسّن من المنبع، وده المكان الصح.

حاجات شكلها غلط وهي سليمة — سِبتها عمدًا

المراجعة اللي بتبلّغ عن كل حاجة كأنها عيب بتفقد قيمتها. ده اللي فحصته وقررت إنه صح:

الموضعالعملة المكتوبةليه سليمة
ZatcaXmlBuilder.php (٧ مواضع)SARالفوترة الإلكترونية السعودية — الريال إلزامي بالمواصفة، مش افتراض
FhirServiceItemBuilder.php (٢)SARNPHIES السعودي — نفس السبب
EtaDocumentBuilder.phpEGPمصلحة الضرائب المصرية — الجنيه إلزامي بالمواصفة
NumberToWordsService.phpKWD/EGP/SARدي جداول تفريغ لفظي (دينار/جنيه/ريال + الكسور) — لازم تكون لكل عملة على حدة
٦ ملفات ترحيل بـdefault('KWD')KWDقيمة عمود افتراضية في الـschema؛ الكود بقى بيحدد العملة صراحةً قبل الكتابة فمابقاش ليها أثر

الملاحظة الوحيدة على NumberToWordsService إن الـparameter الافتراضي بتاعها 'KWD' — بس كل نداءاتها بتمرّر العملة صراحةً، والنداءات دي بقت سليمة بعد التعديل.

قرارات محتاجة منك

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

تحديث ١٩ أغسطس — المالك وافق ونُفِّذ. القرارات ١ و٢ و٤ اتنفّذت بترحيل بيانات (2026_08_19_120000_repair_mislabelled_document_currencies.php) بعد نسخة احتياطية في knowledge-base/plans/currency-repair-backup-20260819-1151.sql. النتيجة المتحقَّق منها: ٣٢ فاتورة كلها SAR بإجمالي ١٥٬٤٨٦.٤٠٠ (= ١٢٬٣٥٢.٥ + ٣٬٠٣٨.٩ + ٩٥ — ولا مبلغ اتغيّر) · صف EG اتشال · حساب CIB البنكي اتحوّل لـEGP الصحيح · صفر تعارض متبقّي. القرار ٣ (الـ٥٥٤ صف) لسه مفتوح.

قرار ١ — الفاتورة INV-2026-00006 بكود «EG» اتنفّذ

فاتورة مدفوعة بمبلغ 95.000 وسعر صرف 1.000000. سعر الصرف = ١ على شركة عملتها SAR معناه إن المبلغ ده ريال واتسمّى غلط، مش جنيه.

قرار ٢ — الـ١٣ فاتورة بالدينار الكويتي على شركة سعودية اتنفّذ

إجماليها 3,038.900 وكلها سعر صرف ١. نفس المنطق: دي شبه مؤكد مبالغ بالريال واتختمت بالدينار من الافتراضي القديم 'KWD' اللي شِلته النهاردة.

قرار ٣ — الـ٥٥٤ صف الفاضيين الموجودين حاليًا لسه مفتوح

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

قرار ٤ — صفّ العملة «EG» في البيانات الرئيسية اتنفّذ

معطّل بالفعل (is_active=0) فمش هيظهر لحد. أرشّح حذفه بعد ما نحسم قرار ١، عشان مايفضلش لغم لو حد فعّله بالغلط. لكن لو الفاتورة هتفضل شايلة «EG»، لازم الصف يفضل موجود.

الباقي المفتوح — بأمانة

حاجات لقيتها ومقدرتش أقفلها في الشغل ده، مسجّلة هنا عشان ماتضيعش:

البندالحالةالملاحظة
حساب GR/IR (بضاعة مستلمة غير مفوترة) [FIN] مفتوح من قبل لسه 5403 «خسائر متنوعة» على الإنتاج — وده حساب مصروفات مش التزامات. توحيد العملة بيخلي النظام أنضف، مش أصحّ، طول ما القيود بتروح لحساب غلط. ده أهم بند مفتوح عندك.
قواعد التحقق nullable على currency_id اتحيّد أثرها سِبتها nullable عن قصد — الطبقة المشتركة بقت بتملا الفاضي، فالتشديد كان هيكسر مسارات شرعية من غير مكسب.
مستندات currency_code (عروض/أوامر/مرتجعات) مليانة حاليًا بتاخد عملتها من الكنترولرات اللي صلّحتها. ممكن تتضاف لها الطبقة المشتركة كتأمين إضافي لو حبيت.
دالتا طباعة السندات (واجهة) مسجّلة payment-voucher-print.config.ts:87 وreceipt-voucher-print.config.ts:87 لسه || 'SAR'. دول ملفات دوال من غير حقن تبعيات، والقيمة دي بتظهر للسندات القديمة بس (الجديدة بقت مضمونة العملة من الخلفية). إصلاحها يستلزم تمرير العملة من المكوّن — إعادة هيكلة أكبر من الفايدة دلوقتي.
خدمات LIS في الواجهة مسجّلة lis-lab-info.service.ts:628,631 وlis-tax-invoice.service.ts فيهم || 'SAR' — بس دي بتشتغل لو نداء الـAPI فشل فقط، والـAPI نفسه بقى بيرجّع عملة الشركة بعد تصحيح LabInfoController.

moonui2 · فرع hazemdev2 · مافيش نشر ولا دمج لـmain — الشغل واقف عند بيئة التطوير في انتظار مراجعتك.