Moon ERP · تقرير تحليل محاسبي/مخزني

الصرف الجزئي من فاتورة البيع: ماذا يحدث للقيود والمخزون؟ Partial Goods-Issue from a Sales Invoice — Accounting & Inventory Impact

السيناريو: تعمل فاتورة بيع بـ100 وحدة → النظام يولّد إذن صرف (GDN) → مسؤول المخزن يصرف 50 وحدة فقط. هذا التقرير يوثّق — من الكود الفعلي — ما الذي يحدث الآن ، وما الذي يُفترض أن يحدث محاسبيًا ومخزنيًا، والحلول.

📅 2026-06-22 الوضع: تحليل للوضع الحالي المصدر: تتبّع كود Modules/Sales · Inventory · Accounting الخلاصة: تضارب صامت مؤكَّد في الدفاتر

! الخلاصة في سطرين (TL;DR)

عند ترحيل الفاتورة، النظام يسجّل قيد تكلفة المبيعات (COGS) فورًا بالكمية الكاملة (100): Dr COGS / Cr Inventory. لكن الخصم الفعلي من المخزون يتأجّل لاعتماد إذن الصرف، حيث يصرف المخزن 50 فقط. إذن الصرف نفسه لا يُنشئ أي قيد محاسبي.

⛔ النتيجة: الإيراد والتكلفة مسجّلان لـ100، والمخزون الفعلي نقص 50 فقط → حساب المخزون في دفتر الأستاذ لا يساوي الرصيد الفعلي في المخازن، وربح الفترة مُبالَغ فيه. لا يوجد ربط كمية بين الفاتورة وإذن الصرف، ولا قيد تسوية، ولا تتبّع لـ«الكمية المسلَّمة».

2 أولًا: السلوك يعتمد على إعدادين

قبل ما نحكم، لازم نفرّق بين 3 أوضاع تتحكّم فيها إعدادات البيع. السيناريو اللي بتسأل عنه هو الوضع B.

الوضع الإعدادات متى يُخصم المخزون؟ متى يُسجّل COGS؟ الاتساق
A — المباشر
الافتراضي
auto_create_stock_issue_on_invoice = false
stock_deduction_point = 'invoice'
عند ترحيل الفاتورة مباشرةً (داخل نفس الـ transaction) عند الفاتورة — كمية كاملة متّسق ذرّيًا
لا يوجد صرف جزئي ممكن
B — إذن صرف يدوي
السيناريو محل السؤال
auto_create_stock_issue_on_invoice = true
auto_approve... = false
عند اعتماد إذن الصرف (قد يكون 50 فقط) عند الفاتورة — كمية كاملة (100) تضارب ممكن
B2 — اعتماد تلقائي auto_create... = true
auto_approve_stock_issue_on_invoice = true
عند الفاتورة (يُعتمد الإذن آليًا بالكامل) عند الفاتورة — كمية كاملة متّسق لكن لا يسمح بصرف جزئي
C — التأجيل للإذن stock_deduction_point = 'issue' الفاتورة لا تخصم ولا تنشئ إذن آليًا لا يُسجّل إطلاقًا! عيب كامن منفصل
ملاحظة على الوضع C: لو حدّ ظبط stock_deduction_point = 'issue' متوقّعًا إن COGS يتسجّل وقت الصرف — فالنتيجة إن قيد التكلفة لا يُسجّل أبدًا (لأن إذن الصرف لا يولّد قيودًا)، فيظهر ربح مُبالغ فيه 100%. ده عيب كامن منفصل عن سؤالك لكنه من نفس العائلة (انظر القسم 6).

3 ما الذي يحدث فعلًا الآن (الوضع B)

مثال رقمي: 100 وحدة، تكلفة الوحدة 10، سعر البيع 15، ضريبة 15%.

1

ترحيل الفاتورة — PostSalesInvoice::execute()

يُنشأ قيدان فورًا، الاثنان بالكمية الكاملة (100):

# 1) قيد الإيراد (revenue JE) — source_type='sales_invoice' Dr Accounts Receivable 1725 Cr Sales Revenue 1500 Cr VAT Payable 225 # 2) قيد التكلفة (COGS JE) — source_type='sales_invoice_cogs' Dr COGS 1000 (100 × 10) Cr Inventory 1000 (100 × 10)

⚠️ المخزون لم يُخصم فعليًا بعد — الـ guard if (! $autoCreateGdn) يتخطّى الخصم لأن الإذن سيتولّاه. لكن قيد Cr Inventory 1000 اتسجّل خلاص.

2

توليد إذن الصرف — handleAutoStockIssue()

يُنشأ InventoryIssue بحالة Draft، بالكمية الكاملة (100)، ومربوط بالفاتورة عبر reference_type='sale', reference_id=invoice.id. لا قيود محاسبية هنا.

3

مسؤول المخزن يصرف 50 فقط — ApproveIssue::execute()

يعدّل كمية الإذن من 100 إلى 50 (مسموح طالما Draft) ثم يعتمده. يحدث:

  • خصم المخزون فعليًا 50 وحدة فقط — StockService::decreaseStock.
  • إنشاء حركة مخزنية InventoryMovement (out 50) وتحديث StockBalance.total_value −500.
  • لا يُنشأ أي قيد محاسبي — إذن الصرف لا يلمس دفتر الأستاذ إطلاقًا.
لو المخزون المتاح أقل من 100 أصلًا، الاعتماد بالكمية الكاملة يُرفض (insufficient_stock) ما لم يسمح المخزن بالرصيد السالب — فيُجبَر المستخدم على تخفيض الكمية. أي أن «الصرف الجزئي» ليس حالة نادرة، بل المسار الإجباري كلما قصُر المخزون.

الحالة النهائية للدفاتر بعد الخطوات الثلاث

Inventory (asset) — GL
DrCr 1000
 الرصيد: −1000
Stock sub-ledger (فعلي)
out 500 
القيمة: −500 
COGS — P&L
Dr 1000 
مصروف: 1000 

⛔ التضارب الناتج

المؤشّردفتر الأستاذ (GL)الواقع الفعليالفجوة
قيمة المخزون المنصرف1000500500 فرق
الوحدات الخارجة فعليًا1005050 وحدة على الرف لكن GL صرفها
تكلفة المبيعات (COGS)1000يجب أن تكون 500مصروف زائد 500
مجمل الربح للصفقة500يجب أن يعكس التسليم الجزئيمُبالَغ فيه
ربط الكمية فاتورة↔إذنلا يوجد — العميل فُوتر على 100 واستلم 50، ولا يوجد تتبّع «كمية مسلَّمة» ولا أمر متبقٍّ (back-order)

حساب مراقبة المخزون (control account) في GL لم يعد يطابق مجموع أرصدة المخزون الفعلية. أي جرد أو تسوية شهرية سيُظهر فرق 500 غير مفسَّر، و50 وحدة موجودة ماديًا لكنها «مصروفة» محاسبيًا.

4 السبب الجذري (Root Cause)

سبب #1

القيد متعلّق بالفاتورة لا بالتسليم

قيد COGS/المخزون يُنشأ وقت ترحيل الفاتورة بالكمية المفوترة، بينما الخروج الفعلي للبضاعة يحدث لاحقًا في إذن الصرف وبكمية مختلفة. التكلفة تُعترف قبل أن تغادر البضاعة المخزن.

PostSalesInvoice.php:54–58, 202, 273–290
سبب #2

إذن الصرف لا يولّد قيودًا

اعتماد الإذن يحرّك المخزون فقط (InventoryMovement) ولا يُنشئ أي JournalEntry. فعند اختلاف الكمية، لا يوجد قيد يصحّح الفرق.

ApproveIssue.php:22–169 — لا استدعاء لـ CreateJournalEntry
سبب #3

لا يوجد عمود «كمية مسلَّمة»

سطر الفاتورة وسطر الإذن فيهما عمود quantity واحد فقط — لا تمييز بين المطلوب والمنصرف فعليًا، ولا حالة «جزئي»، ولا أمر متبقٍّ.

inventory_issue_items / sales_invoice_items — single quantity column
سبب #4

لا يوجد فحص اتساق (tie-out)

AccountingIntegrityService يفحص توازن القيود فقط، ولا يقارن رصيد حساب المخزون في GL بمجموع أرصدة المخزون الفعلية — فالفجوة تمرّ بصمت.

AccountingIntegrityService.php:24–150
سياق مهم: الفريق واعٍ بهذا النوع من الفجوات — يوجد تعليق وإصلاح سابق في handleAutoStockIssue() (PostSalesInvoice.php:353–357) لحالة مشابهة (فاتورة من أمر بلا مخزن في الرأس كانت تسجّل COGS بدون خروج بضاعة). الصرف الجزئي هو المتغيّر المتبقّي من نفس العائلة ولم يُعالَج بعد.

5 ما الذي يُفترض أن يحدث (الدورة الصحيحة)

المبدأ المحاسبي: تكلفة المبيعات تُعترف عند خروج البضاعة فعليًا، وبالكمية الفعلية — مبدأ المطابقة (Matching). الكمية غير المسلَّمة تبقى أصلًا (Inventory) لا مصروفًا.

الفاتورة: إيراد فقط (أو إيراد مؤجَّل حسب سياسة الاعتراف)

Dr Accounts Receivable 1725 Cr Sales Revenue 1500 Cr VAT Payable 225

اعتماد إذن الصرف (50) → هنا يُسجّل COGS بالكمية الفعلية

Dr COGS 500 (50 × 10) Cr Inventory 500 (50 × 10)

المخزون GL ينقص 500 = الخروج الفعلي 500. متطابق.

الـ50 المتبقية = تسليم لاحق (back-order) أو تسوية فاتورة

إمّا إذن صرف ثانٍ لاحقًا بـ50 (يسجّل COGS 500 إضافية عند تسليمها)، أو إشعار خصم/تعديل للفاتورة إن لن تُسلَّم. delivered_qty على سطر الفاتورة يجعل الحالة مرئية.

المحصّلة الصحيحة: في كل لحظة: رصيد حساب المخزون في GL = مجموع أرصدة المخزون الفعلية، وCOGS = تكلفة ما سُلّم فقط، والمتبقّي مرئي كأمر مفتوح. لا فجوة، لا مفاجآت في الجرد.

6 الحلول (مرتّبة بالأفضلية)

جهد: متوسط

① نقل تسجيل COGS إلى إذن الصرف، بالكمية الفعلية المُوصى به

  • في وضع الإذن المُفعّل (auto_create_stock_issue_on_invoice = true): لا تسجّل COGS عند الفاتورة — اكتفِ بقيد الإيراد.
  • اجعل ApproveIssue::execute() يُنشئ قيد Dr COGS / Cr Inventory بالكمية المعتمَدة فعلًا (50)، عبر خدمة قيود مشتركة + نفس منطق حلّ الحسابات (cogs/inventory account) الموجود في resolveFallbackInventoryAccount.
  • إذن الصرف الثاني لاحقًا (للـ50 المتبقية) يسجّل تلقائيًا COGS الخاص به. تطابق دائم.

مزايا: صحيح محاسبيًا في كل الحالات ويفتح الباب للتسليم الجزئي الحقيقي. تكلفة: نقل مسؤولية القيد للمخزون + ربط حسابات COGS/Inventory على مستوى المخزن، واختبارات Pest للحالتين.

جهد: منخفض

② قيد تسوية عند الاعتماد الجزئي (يحافظ على البنية الحالية)

عند اعتماد إذن صرف بكمية أقل من المفوتر، سجّل قيدًا عكسيًا للفرق فقط:

# الفرق = 50 وحدة لم تُصرف Dr Inventory 500 (إعادة الـ50 لأصل المخزون) Cr COGS 500 (عكس المصروف الزائد)

الرابط موجود بالفعل (invoice.cogs_journal_entry_id + reference_id على الإذن). مزايا: تعديل أصغر. عيوب: الدفاتر تظل خاطئة حتى يُعتمد الإذن، والإيراد يظل كاملًا مقابل تسليم جزئي.

جهد: منخفض · فوري

③ حاجز وقائي يمنع التضارب الصامت (حل مؤقت سريع)

  • إجبار الاعتماد التلقائي بالكامل (auto_approve_stock_issue_on_invoice = true) — يصبح الوضع B2 المتّسق، لكن دون صرف جزئي.
  • أو منع تعديل كمية الإذن المُولَّد آليًا لأقل من الكمية المفوترة + تحذير عند نقص المخزون.
  • إضافة فحص tie-out في AccountingIntegrityService يقارن رصيد حساب المخزون في GL بمجموع StockBalance.total_value ويُبلّغ عن أي فرق.

الغرض: إيقاف النزيف فورًا وكشف أي فجوة قائمة، ريثما يُنفّذ الحل ① أو ②.

جهد: عالٍ

④ نموذج تسليم كامل (Delivery / Fulfillment) — الحل الجذري طويل المدى

إضافة delivered_quantity على سطر الفاتورة + كيان تسليمات يربط الفاتورة بأذون الصرف، مع حالات (تم/جزئي/متبقٍّ) واعتراف بالإيراد عند التسليم اختياريًا. يجعل التسليم الجزئي والأوامر المفتوحة مواطنًا من الدرجة الأولى. مناسب كـ«مشروع» لاحق مبني على الحل ①.

7 التوصية

  1. فورًا: فعّل الحل ③ (حاجز وقائي + فحص tie-out) لإيقاف التضارب الصامت وكشف أي فروق قائمة في بياناتك الحالية.
  2. قصير المدى: نفّذ الحل ① (نقل COGS لإذن الصرف بالكمية الفعلية) — هو التصحيح المحاسبي الحقيقي ويصلح كل الأوضاع.
  3. لاحقًا: الحل ④ لو احتجت تسليمات جزئية وأوامر متبقّية كميزة كاملة.
قرار يخصّك قبل التنفيذ: هل سياستك أن الإيراد يُعترف كاملًا عند الفاتورة (والتسليم الجزئي مجرد لوجستيات)، أم أن الإيراد يجب أن يتبع التسليم؟ الإجابة تحدّد إن كنا سنلمس قيد الإيراد أم نكتفي بتصحيح COGS/المخزون. الحلول ①–③ أعلاه تصحّح طرف المخزون/COGS دون المساس بالإيراد — وهو الأنسب لمعظم سياسات البيع.

§ ملحق: مراجع الكود (file:line)

الموضوعالملف والسطر
تنسيق الترحيل (إيراد ثم 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