كل من يُصدر الفواتير في Odoo استنادًا إلى أوامر البيع وتذاكر الدعم وسجلات الوقت يعرف النتيجة: يتلقى العميل في نهاية الشهر خمس فواتير بدلًا من فاتورة واحدة. تأتي كل منها من أمر بيع أو تذكرة مختلفة، وكلها صحيحة من حيث المحتوى، ومع ذلك يسأل العميل: ألا يمكن جمعها في مستند واحد؟
لا يوفر Odoo 19 Community حلًا متكاملًا لهذه المشكلة تحديدًا. فالوحدة المعروفة account_invoice_merge من OCA لم تُصدر بعد لـ Odoo 19، كما أنها تعالج المشكلة بطريقة تنطوي على خطر حقيقي لمقدمي الخدمات: إذ تجمع البنود المتطابقة، ما يؤدي إلى فقدان ارتباطها بأمر البيع والمهمة والوقت المسجل.
لذلك طوّرنا وحدة Merge Draft Invoices لاستخدامها في تثبيت Odoo 19 Community الخاص بنا. ولأن هذه المشكلة لا تخصنا وحدنا، أتحناها مجانًا على GitHub بموجب الرخصة الحرة AGPL-3.
المشروع على GitHub:
لماذا طوّرنا وحدة خاصة بدلًا من استخدام حل OCA؟
تتولى OCA منذ سنوات طويلة صيانة account_invoice_merge في المستودع OCA/account-invoicing. وحتى وقت كتابة هذا المقال، لا تزال الترقية إلى الإصدار 19.0 طلب سحب مفتوحًا هناك، ولم تصبح وحدة منشورة بعد.
لكن الأهم أن الوحدتين تؤديان وظيفتين مختلفتين:
| OCA account_invoice_merge | Merge Draft Invoices | |
|---|---|---|
| النتيجة | تُدمج في إحدى الفواتير المحددة | فاتورة جديدة دائمًا |
| البنود | تُجمع البنود المتطابقة وتُضاف كمياتها | يُنقل كل بند كما هو دون دمج |
| المصدر | تُجمع قيم invoice_origin / payment_reference في رأس الفاتورة | تُنسخ حقول المصدر لكل بند عبر نقطة التوسعة _copy_data_extend_business_fields الخاصة بـ Odoo |
| تسجيل الوقت | لا يؤخذ في الاعتبار | يُعاد ربط timesheet_invoice_id بالفاتورة الجديدة |
| وضوح المعلومات للعميل | لا شيء | عنوان فوق بنود كل مصدر يتضمن أمر البيع والتذكرة |
| الفواتير المصدر | تُلغى | إلغاؤها (الخيار الافتراضي) أو حذفها أو الاحتفاظ بها، مع توضيح تبعات كل خيار |
إن دمج البنود المتماثلة هو الفرق الحاسم. فالبند المدموج لا يمكن أن يحمل سوى مصدر واحد. وبمجرد أن يبدو سطران من أمرين مختلفين متماثلين، تضيع الصلة من الفاتورة عائدةً إلى أمر البيع والمهمة وقيد الوقت. وفي منشأة خدمات تجري فيها الفوترة عبر التذكرة ← تسجيل الوقت ← الفاتورة، فهذا يعني: إما أن تُحتسب الساعات نفسها مرتين أو تختفي.
لذلك لا تجمع وحدتنا أي بنود على الإطلاق. وتحتوي الفاتورة المدمجة على عدد من البنود يساوي تمامًا مجموع بنود الفواتير المصدر.
لم يُستخدم أي جزء من شيفرة وحدة OCA. وكلتا الوحدتين متاحتان بموجب AGPL-3.
ما الذي تفعله الوحدة
حدد مسودتَي فاتورة أو أكثر في عرض قائمة الفواتير، ثم اختر دمج مسودات الفواتير من قائمة الإجراءات. سيعرض معالجٌ الفواتير المحددة ونتائج التحقق وخيارات التعامل مع الفواتير المصدر. وبعد التأكيد، تنتقل مباشرة إلى الفاتورة الجديدة.
قبل الدمج، تتحقق الوحدة من جميع النقاط التالية دفعة واحدة، حتى لا يظهر خطأ جديد بعد كل تصحيح:
- تحديد فاتورتين على الأقل
- أن تكون جميعها في حالة مسودة
- ألا يكون قد سبق ترحيل أي منها محاسبيًا (فالمستند الذي أُعيد إلى حالة المسودة يحتفظ بسجل ترحيله ويُرفض)
- ألا يكون قد سبق دمج أي منها
- أن تكون جميعها فواتير عملاء أو فواتير موردين أو إشعارات دائنة أو إيصالات، وليست قيودًا محاسبية مجردة
- تطابق الشريك ونوع المستند والشركة والعملة ودفتر اليومية
لا تمنع الاختلافات في شروط الدفع أو الوضع الضريبي أو عنوان التسليم أو الحساب البنكي أو مندوب المبيعات عملية الدمج. يعرض المعالج تحذيرًا ويعتمد القيمة الواردة في أقدم فاتورة محددة.
ما الذي يُحتفظ به
تتمثل الفكرة الأساسية للوحدة في الحفاظ على كل ارتباط بين بند الفاتورة ومصدره ضمن الفاتورة الجديدة أيضًا:
| الارتباط | طريقة المعالجة |
|---|---|
| بنود أوامر البيع (sale_line_ids) | تُنسخ لكل بند عبر _copy_data_extend_business_fields الخاصة بـ Odoo |
| بنود أوامر الشراء (purchase_line_id) | بالطريقة نفسها |
| توزيع مراكز التكلفة (analytic_distribution) | يُنسخ لكل بند |
| حقول المصدر الأخرى لكل بند | تلقائيًا، إذا كانت الوحدة التي توفرها تطبّق نقطة التوسعة الموثقة في Odoo |
| المصدر والمرجع في رأس الفاتورة | يُدمجان مع إزالة التكرارات، ويمكن تعديلهما في المعالج |
| أمر البيع: invoice_ids وinvoice_status وqty_invoiced | تُحتسب من البنود، ولذلك يتعرف أمر البيع على الفاتورة الجديدة تلقائيًا |
| تسجيل الوقت (timesheet_invoice_id) | يُعاد ربطه بالفاتورة الجديدة قبل إبطال الفواتير المصدر |
| تذكرة الدعم | ترتبط بسجل الوقت، ولذلك تشير تلقائيًا إلى الفاتورة الجديدة |
| الدردشة (Chatter) | رسالة على الفاتورة الجديدة تتضمن جميع الفواتير المصدر، ورسالة على كل فاتورة مصدر تتضمن رابطًا إلى الفاتورة الجديدة |
| merged_into_move_id / merged_from_move_ids | ارتباط محفوظ في كلا الاتجاهين |
لا تُنسخ بنود الضرائب وشروط الدفع والتقريب وخصم السداد المبكر، بل يعيد Odoo احتسابها استنادًا إلى المحتوى. ولذلك تحتوي الفاتورة المدمجة على بند ضريبي واحد لكل نسبة ضريبية، وليس بندًا لكل فاتورة مصدر. وتُنشأ كمسودة دون رقم؛ فعملية الدمج نفسها لا تستهلك رقم مستند.
العناوين: يعرف العميل مصدر كل بند
هذه هي الميزة التي تُحدث فرقًا في العمل اليومي. تضيف الوحدة سطر قسم فوق بنود كل فاتورة مصدر، وهو العنصر نفسه الذي يستخدمه Odoo لقسم «الدفعات المقدمة»:
BE10181 <- عنوان
22,500 x ساعة عمل فني 3.577,50
المجموع الفرعي 3.577,50
BE10182 · التذكرة: HT00219 <- عنوان
3,000 x ساعة عمل فني 477,00
المجموع الفرعي 477,00
المبلغ الصافي 4.054,50
يُطبع العنوان على المستند الذي يتلقاه العميل، ويضيف تقرير الفواتير القياسي في Odoo مجموعًا فرعيًا لكل قسم تلقائيًا. وبذلك يعرف العميل أيضًا تكلفة كل جزء.
تُركّب تسمية العنوان من المعلومات المتوفرة فعليًا في الفاتورة المصدر:
- أوامر البيع، مباشرة من ارتباطات البنود، ما يضمن دقة الأسماء؛ وإذا لم توجد ارتباطات بأوامر بيع، يُستخدم النص الحر من invoice_origin
- التذاكر، ويُتعرف عليها عبر سجلات الوقت، وتُعرض بصيغة «التذكرة: HT00219» أو «التذاكر: …»، بحد أقصى خمس تذاكر تليها عبارة «وN أخرى»
- إذا لم يتوفر أي منهما: يُستخدم رقم الفاتورة المصدر، ثم مرجعها
- إذا لم تتوفر أي معلومات: يُستخدم العنوان المحايد «بنود إضافية»، حتى لا تندرج بنود أي مصدر ضمن القسم السابق دون توضيح
تُحذف المعلومات غير المتوفرة ولا تُختلق. وتُطبع التذكرة برقمها الخاص، سواء كان رقم helpdesk_mgmt أو ticket_ref في Enterprise أو اسم العرض الخاص بالنموذج، ولا يُستخدم معرّف قاعدة البيانات مطلقًا.
تُكتب العناوين بلغة العميل، كما يفعل Odoo عند إنشاء الفواتير من أوامر البيع. أما Chatter فيبقى بلغة المستخدم.
لا يتضمن العنوان منتجًا أو كمية أو سعرًا أو ضريبة أو حسابًا أو توزيعًا لمراكز التكلفة. ولذلك لا يمكنه التأثير في الإجماليات أو الوعاء الضريبي أو محاسبة التكاليف. وعند تعطيل العناوين في المعالج، تحصل على الفاتورة نفسها تمامًا التي كانت ستُنشأ دون هذه الميزة، ويقارن اختبارٌ النتيجتين حتى آخر سنت.
ماذا يحدث للفواتير المصدر
يوفر المعالج ثلاثة خيارات:
- الإلغاء (الخيار الافتراضي والموصى به). لا يترتب على إلغاء المسودة أثر محاسبي: فلا رقم مستند ولا ترحيل. وتبقى الفواتير في قاعدة البيانات كمستندات ملغاة، مع Chatter ورابط إلى الفاتورة الجديدة. ويتجاهل qty_invoiced في أمر البيع المستندات الملغاة، فلا يُحتسب أي شيء مرتين. ويتيح هذا الخيار تتبّع ما حدث بوضوح حتى بعد ستة أشهر.
- الحذف. لا يمكن التراجع عنه. يُحذف Chatter والرابط المرجعي مع السجل، ولا تبقى سوى الرسالة الموجودة على الفاتورة الجديدة. وهو مناسب للمسودات المؤقتة التي لم تعد هناك حاجة إلى إظهارها.
- الإبقاء كمسودة. تبقى الفواتير المصدر مفتوحة. وعندئذ تحتسب أوامر البيع المرتبطة بها الفاتورة المصدر والفاتورة الجديدة ضمن المفوتر، فيبدو العمل وكأنه فُوتر مرتين إلى أن يعالج أحدهم المسودات يدويًا. وهذا الخيار مخصص للحالات النادرة التي تستدعي المقارنة قبل اتخاذ القرار. ويؤكد اختبارٌ هذا الاحتساب المزدوج تحديدًا حتى لا يفاجأ به أحد.
الحدود: ما تستبعده الوحدة عمدًا
يرجى القراءة قبل الاستخدام في بيئة الإنتاج:
- المسودات فقط. لا تُدمج الفواتير المرحّلة محاسبيًا. فمعالجتها تتطلب إشعارات دائنة، وليست مجرد عملية نسخ.
- تُرفض المسودات التي سبق ترحيلها ثم أُعيدت إلى حالة المسودة. فهي تحمل رقمًا وسجل ترحيل، وإخفاء ذلك داخل مستند آخر ليس أمرًا ينبغي أن ينفذه إجراء جماعي دون تنبيه.
- لا يمكن دمج فواتير تختلف في الشريك أو العملة أو الشركة أو دفتر اليومية. فلا توجد قيمة واحدة مناسبة لهذه الحالات، ولذلك ترفض الوحدة الدمج بدلًا من التخمين.
- لا يُنقل payment_reference. فمراجع الدفع المهيكلة تحدد مستندًا بعينه، وجمع مرجعين لا ينتج مرجعًا يطابق أي شيء لدى البنك. وتحصل الفاتورة الجديدة على مرجعها الخاص عند ترحيلها.
- تُنسخ بنود الدفعات المقدمة مثل أي بند آخر. إذا كانت الفواتير المصدر تتضمن بنود دفعات مقدمة من sale، فيجب مراجعة النتيجة قبل الترحيل.
- يُعاد ربط timesheet_invoice_id وحده تلقائيًا. أما أي نموذج آخر يحتوي على حقل Many2one يشير إلى account.move، مثل دورات الفوترة المخصصة أو ارتباطات الاشتراكات أو عمليات إرسال EDI، فيبقى مرتبطًا بالفاتورة المصدر. ولهذا الغرض تتوفر نقطة التوسعة _merge_draft_relink.
- لا دمج للبنود المتماثلة – عن قصد. ومن يريد فاتورة قصيرة مرتّبة فهذا ليس مكانه، وسيدفع ثمن ذلك من روابط المصدر.
- تُنشأ العناوين مرة واحدة عند الدمج. وبعد ذلك تصبح أسطر أقسام عادية يمكن تعديلها وحذفها، ولا تُحدّث إذا تغير اسم أمر البيع أو التذكرة لاحقًا.
- يذكر العنوان تذاكر الفاتورة المصدر بأكملها، وليس تذاكر كل بند على حدة.
التثبيت في Odoo 19 Community
تُثبّت الوحدة بالطريقة المعتادة لتثبيت وحدات Odoo الإضافية:
- انسخ مجلد الوحدة إلى أحد مسارات الإضافات المضبوطة.
- حدّث قائمة التطبيقات في Odoo.
- ثبّت الوحدة account_invoice_merge_draft.
تعتمد الوحدة على account فقط. وتكتشف وقت التشغيل كل ما تحتاجه عن sale وpurchase وproject وhr_timesheet وsale_timesheet أو أي وحدة دعم، ولذلك يمكن تثبيتها وتشغيلها أيضًا على قاعدة بيانات مخصصة للفوترة وحدها.
بعد ذلك: المحاسبة → العملاء → الفواتير (أو الموردون → الفواتير)، ثم حدد مسودتَي فاتورة أو أكثر، واختر من قائمة الترس → دمج مسودات الفواتير. يتوفر هذا الإجراء لجميع المستخدمين المنتمين إلى مجموعة الفوترة / إصدار الفواتير.
عند التحديث من إصدار سابق للوحدة، ينبغي تنفيذ التحديث مرة واحدة باستخدام -u account_invoice_merge_draft --i18n-overwrite لضمان ظهور تسميات الحقول المعدّلة في الترجمة الألمانية. ولا ينطبق ذلك على التثبيت الجديد.
للمطورين: واجهة API ونقاط التوسعة
يمكن أيضًا استدعاء عملية الدمج من الشيفرة الخاصة بك:
python
merged = draft_moves.merge_draft_moves(
extra_vals={"invoice_date": date(2026, 3, 31), "ref": "Q1"},
source_handling="cancel", # "delete" أو "keep"
add_source_sections=True, # عناوين الأقسام؛ القيمة الافتراضية True
)
تُطلق merge_draft_moves استثناء UserError يتضمن جميع أسباب الرفض إذا تعذّر دمج العناصر المحددة. وتُرجع _merge_draft_get_errors() الأسباب نفسها في قائمة دون إطلاق استثناء.
إذا كانت لديك مراجع عكسية مخصصة تشير إلى account.move، فأعِد تعريف _merge_draft_relink:
python
class AccountMove(models.Model):
_inherit = "account.move"
def _merge_draft_relink(self, new_move):
super()._merge_draft_relink(new_move)
if "my_move_id" not in self.env["my.model"]._fields:
return
self.env["my.model"].search([("my_move_id", "in", self.ids)]).write(
{"my_move_id": new_move.id}
)
إذا كان المرجع العكسي موجودًا على بند الفاتورة، فلن تحتاج إلى هذه الوحدة أصلًا: تكفي نقطة التوسعة account.move.line._copy_data_extend_business_fields الخاصة بـ Odoo، ويُنقل الحقل عند الدمج والإلغاء والتكرار على حد سواء. وللتعامل مع وحدات دعم أخرى أو تغيير صياغة العناوين، تتوفر _merge_draft_get_source_tickets() و_merge_draft_section_name().
اختبارات تسبق التشغيل على بيانات الإنتاج
يغطي 48 اختبارًا الحالات المختلطة واختلاف الشركاء والعملات وأنواع المستندات، والبنود المرتبطة وغير المرتبطة بأوامر بيع أو شراء، والضرائب والأقسام والملاحظات وتوزيعات مراكز التكلفة، وإعادة ربط سجلات الوقت، والخيارات الثلاثة للفواتير المصدر، والمعالج، والعناوين. وتتحقق الاختبارات من تفعيل العناوين افتراضيًا، ومن أن تعطيلها يعيد إنتاج النتيجة السابقة حتى آخر سنت، ومن خلو العنوان من المبالغ والحسابات، وبقاء بنود المصدر الواحد معًا، وظهور التذكرة برقمها دون استخدام معرّف قاعدة البيانات مطلقًا.
تستند بيانات الاختبار إلى دليل الحسابات الموجود للشركة النشطة بدلًا من AccountTestInvoicingCommon، لأن هذه الأداة المساعدة كثيرًا ما تفشل في البدء على قواعد بيانات الإنتاج التي خضعت للترحيل، وهي تحديدًا القواعد التي يُفترض أن تعمل عليها الوحدة.
odoo-bin -d <db> -u account_invoice_merge_draft --test-enable \
--test-tags /account_invoice_merge_draft --stop-after-init
كما هو الحال مع أي إضافة في مجال المحاسبة، نوصي بالتحقق من التثبيت أولًا في بيئة اختبار باستخدام نسخة من قاعدة بياناتك.
مجاني ومفتوح المصدر لمجتمع Odoo
نطوّر في Biricon IT Services GmbH حلولًا تلبي أولًا احتياجات محددة في عملنا. وعندما ينتج عن ذلك وحدة قابلة للاستخدام على نطاق أوسع، نرغب في إتاحتها للجميع بدلًا من حصرها في تثبيت واحد.
لذلك تتوفر Merge Draft Invoices مجانًا كمشروع مفتوح المصدر. ويمكن الاطلاع على الشيفرة المصدرية وفحصها وتعديلها ومواصلة تطويرها ضمن شروط رخصة AGPL-3.
كان هدفنا إنشاء فاتورة يظل فيها كل بند مرتبطًا بأمر البيع والتذكرة وقيد الوقت الخاص به حتى بعد مرور عام، لا مجرد فاتورة بأقل عدد ممكن من البنود.
المشروع والشيفرة المصدرية والمساهمة
تتوفر الشيفرة المصدرية الكاملة وإرشادات التثبيت والوثائق التقنية على GitHub:
فتح Odoo 19 Merge Draft Invoices على GitHub
نرحب بتجارب الاستخدام العملية وتقارير الأخطاء واقتراحات التحسين والمساهمات من مجتمع Odoo.
مجاني. قابل للتتبع. مفتوح المصدر. مخصص لـ Odoo 19 Community.