السيناريو: تعمل فاتورة بيع بـ100 وحدة → النظام يولّد إذن صرف (GDN) → مسؤول المخزن يصرف 50 وحدة فقط. هذا التقرير يوثّق — من الكود الفعلي — ما الذي يحدث الآن ، وما الذي يُفترض أن يحدث محاسبيًا ومخزنيًا، والحلول.
عند ترحيل الفاتورة، النظام يسجّل قيد تكلفة المبيعات (COGS) فورًا بالكمية الكاملة (100):
Dr COGS / Cr Inventory. لكن الخصم الفعلي من المخزون يتأجّل لاعتماد إذن الصرف،
حيث يصرف المخزن 50 فقط. إذن الصرف نفسه لا يُنشئ أي قيد محاسبي.
⛔ النتيجة: الإيراد والتكلفة مسجّلان لـ100، والمخزون الفعلي نقص 50 فقط → حساب المخزون في دفتر الأستاذ لا يساوي الرصيد الفعلي في المخازن، وربح الفترة مُبالَغ فيه. لا يوجد ربط كمية بين الفاتورة وإذن الصرف، ولا قيد تسوية، ولا تتبّع لـ«الكمية المسلَّمة».
قبل ما نحكم، لازم نفرّق بين 3 أوضاع تتحكّم فيها إعدادات البيع. السيناريو اللي بتسأل عنه هو الوضع B.
| الوضع | الإعدادات | متى يُخصم المخزون؟ | متى يُسجّل COGS؟ | الاتساق |
|---|---|---|---|---|
| A — المباشر الافتراضي |
auto_create_stock_issue_on_invoice = falsestock_deduction_point = 'invoice' |
عند ترحيل الفاتورة مباشرةً (داخل نفس الـ transaction) | عند الفاتورة — كمية كاملة | متّسق ذرّيًا لا يوجد صرف جزئي ممكن |
| B — إذن صرف يدوي السيناريو محل السؤال |
auto_create_stock_issue_on_invoice = trueauto_approve... = false |
عند اعتماد إذن الصرف (قد يكون 50 فقط) | عند الفاتورة — كمية كاملة (100) | تضارب ممكن |
| B2 — اعتماد تلقائي | auto_create... = trueauto_approve_stock_issue_on_invoice = true |
عند الفاتورة (يُعتمد الإذن آليًا بالكامل) | عند الفاتورة — كمية كاملة | متّسق لكن لا يسمح بصرف جزئي |
| C — التأجيل للإذن | stock_deduction_point = 'issue' |
الفاتورة لا تخصم ولا تنشئ إذن آليًا | لا يُسجّل إطلاقًا! | عيب كامن منفصل |
stock_deduction_point = 'issue' متوقّعًا إن COGS يتسجّل وقت
الصرف — فالنتيجة إن قيد التكلفة لا يُسجّل أبدًا (لأن إذن الصرف لا يولّد قيودًا)، فيظهر ربح مُبالغ فيه 100%.
ده عيب كامن منفصل عن سؤالك لكنه من نفس العائلة (انظر القسم 6).
مثال رقمي: 100 وحدة، تكلفة الوحدة 10، سعر البيع 15، ضريبة 15%.
يُنشأ قيدان فورًا، الاثنان بالكمية الكاملة (100):
⚠️ المخزون لم يُخصم فعليًا بعد — الـ guard if (! $autoCreateGdn) يتخطّى الخصم لأن الإذن سيتولّاه.
لكن قيد Cr Inventory 1000 اتسجّل خلاص.
يُنشأ InventoryIssue بحالة Draft، بالكمية الكاملة (100)،
ومربوط بالفاتورة عبر reference_type='sale', reference_id=invoice.id. لا قيود محاسبية هنا.
يعدّل كمية الإذن من 100 إلى 50 (مسموح طالما Draft) ثم يعتمده. يحدث:
StockService::decreaseStock.InventoryMovement (out 50) وتحديث StockBalance.total_value −500.insufficient_stock) ما لم يسمح المخزن بالرصيد السالب — فيُجبَر المستخدم على تخفيض الكمية.
أي أن «الصرف الجزئي» ليس حالة نادرة، بل المسار الإجباري كلما قصُر المخزون.
| المؤشّر | دفتر الأستاذ (GL) | الواقع الفعلي | الفجوة |
|---|---|---|---|
| قيمة المخزون المنصرف | 1000 | 500 | 500 فرق |
| الوحدات الخارجة فعليًا | 100 | 50 | 50 وحدة على الرف لكن GL صرفها |
| تكلفة المبيعات (COGS) | 1000 | يجب أن تكون 500 | مصروف زائد 500 |
| مجمل الربح للصفقة | 500 | يجب أن يعكس التسليم الجزئي | مُبالَغ فيه |
| ربط الكمية فاتورة↔إذن | لا يوجد — العميل فُوتر على 100 واستلم 50، ولا يوجد تتبّع «كمية مسلَّمة» ولا أمر متبقٍّ (back-order) | ||
حساب مراقبة المخزون (control account) في GL لم يعد يطابق مجموع أرصدة المخزون الفعلية. أي جرد أو تسوية شهرية سيُظهر فرق 500 غير مفسَّر، و50 وحدة موجودة ماديًا لكنها «مصروفة» محاسبيًا.
قيد COGS/المخزون يُنشأ وقت ترحيل الفاتورة بالكمية المفوترة، بينما الخروج الفعلي للبضاعة يحدث لاحقًا في إذن الصرف وبكمية مختلفة. التكلفة تُعترف قبل أن تغادر البضاعة المخزن.
PostSalesInvoice.php:54–58, 202, 273–290اعتماد الإذن يحرّك المخزون فقط (InventoryMovement) ولا يُنشئ أي
JournalEntry. فعند اختلاف الكمية، لا يوجد قيد يصحّح الفرق.
سطر الفاتورة وسطر الإذن فيهما عمود quantity واحد فقط — لا تمييز بين المطلوب والمنصرف فعليًا،
ولا حالة «جزئي»، ولا أمر متبقٍّ.
AccountingIntegrityService يفحص توازن القيود فقط، ولا يقارن رصيد حساب المخزون في GL
بمجموع أرصدة المخزون الفعلية — فالفجوة تمرّ بصمت.
المبدأ المحاسبي: تكلفة المبيعات تُعترف عند خروج البضاعة فعليًا، وبالكمية الفعلية — مبدأ المطابقة (Matching). الكمية غير المسلَّمة تبقى أصلًا (Inventory) لا مصروفًا.
المخزون GL ينقص 500 = الخروج الفعلي 500. متطابق.
إمّا إذن صرف ثانٍ لاحقًا بـ50 (يسجّل COGS 500 إضافية عند تسليمها)، أو
إشعار خصم/تعديل للفاتورة إن لن تُسلَّم. delivered_qty على سطر الفاتورة يجعل الحالة مرئية.
رصيد حساب المخزون في GL = مجموع أرصدة المخزون الفعلية،
وCOGS = تكلفة ما سُلّم فقط، والمتبقّي مرئي كأمر مفتوح. لا فجوة، لا مفاجآت في الجرد.
auto_create_stock_issue_on_invoice = true): لا تسجّل COGS عند الفاتورة — اكتفِ بقيد الإيراد.ApproveIssue::execute() يُنشئ قيد Dr COGS / Cr Inventory بالكمية المعتمَدة فعلًا (50)، عبر خدمة قيود مشتركة + نفس منطق حلّ الحسابات (cogs/inventory account) الموجود في resolveFallbackInventoryAccount.مزايا: صحيح محاسبيًا في كل الحالات ويفتح الباب للتسليم الجزئي الحقيقي. تكلفة: نقل مسؤولية القيد للمخزون + ربط حسابات COGS/Inventory على مستوى المخزن، واختبارات Pest للحالتين.
عند اعتماد إذن صرف بكمية أقل من المفوتر، سجّل قيدًا عكسيًا للفرق فقط:
الرابط موجود بالفعل (invoice.cogs_journal_entry_id + reference_id على الإذن).
مزايا: تعديل أصغر. عيوب: الدفاتر تظل خاطئة حتى يُعتمد الإذن، والإيراد يظل كاملًا مقابل تسليم جزئي.
auto_approve_stock_issue_on_invoice = true) — يصبح الوضع B2 المتّسق، لكن دون صرف جزئي.AccountingIntegrityService يقارن رصيد حساب المخزون في GL بمجموع StockBalance.total_value ويُبلّغ عن أي فرق.الغرض: إيقاف النزيف فورًا وكشف أي فجوة قائمة، ريثما يُنفّذ الحل ① أو ②.
إضافة delivered_quantity على سطر الفاتورة + كيان تسليمات يربط الفاتورة بأذون الصرف،
مع حالات (تم/جزئي/متبقٍّ) واعتراف بالإيراد عند التسليم اختياريًا. يجعل التسليم الجزئي والأوامر المفتوحة
مواطنًا من الدرجة الأولى. مناسب كـ«مشروع» لاحق مبني على الحل ①.
| الموضوع | الملف والسطر |
|---|---|
| تنسيق الترحيل (إيراد ثم COGS ثم إذن) | Modules/Sales/app/Actions/PostSalesInvoice.php:32–94 |
| قيد الإيراد (Dr AR / Cr Revenue / Cr VAT) | PostSalesInvoice.php:96–177 |
COGS بالكمية الكاملة + الـ guard if(!$autoCreateGdn) | PostSalesInvoice.php:179–303 (202, 227, 273–290) |
| توليد إذن الصرف Draft بالكمية الكاملة | PostSalesInvoice.php:336–401 |
| حلّ حساب المخزون (sales → purchases → inventory.stock_account) | PostSalesInvoice.php:305–334 |
| اعتماد الإذن: خصم المخزون بلا قيد محاسبي | Modules/Inventory/app/Actions/ApproveIssue.php:22–169 |
| فحص المخزون الكافي / السماح بالسالب | ApproveIssue.php:90–129 · warehouse.allow_negative_stock |
| الخصم والتقييم (FIFO / weighted_avg) + حركة المخزون | Modules/Inventory/app/Services/StockService.php:114–275 |
| تعديل الإذن مسموح في حالة Draft فقط | Modules/Inventory/app/Http/Controllers/InventoryIssueController.php:154–188 |
| عمود quantity واحد (لا مطلوب/منصرف) | migrations/2026_02_22_200006_create_inventory_issue_items_table.php |
| محرّك القيود (polymorphic + idempotent + auto-post) | Modules/Accounting/app/Actions/CreateJournalEntry.php:27–196 |
| فحص النزاهة (لا يقارن GL بالمخزون الفعلي) | Modules/Accounting/app/Services/AccountingIntegrityService.php:24–150 |
sales.stock_deduction_point
sales.auto_create_stock_issue_on_invoice
sales.auto_approve_stock_issue_on_invoice
inventory.auto_approve_issues
inventory.valuation_method
warehouse.allow_negative_stock