من يُصدر الفواتير في Odoo انطلاقًا من أوامر البيع وتذاكر Helpdesk وتسجيل الوقت يعرف النتيجة: لا يتلقى العميل في نهاية الشهر فاتورة واحدة بل خمس فواتير. كل منها ناتجة عن أمر أو تذكرة مختلفة، وكل منها صحيحة من الناحية المهنية – ومع ذلك يتساءل العميل عما إذا كان من الممكن جمعها في مستند واحد.
وفي Odoo 19 Community لا يوجد حل سليم لذلك. فوحدة OCA المعروفة account_invoice_merge لم تُصدر بعدُ لـ Odoo 19، وهي تعالج المشكلة بطريقة تشكّل خطرًا جسيمًا على مقدّمي الخدمات: إذ تُدمج البنود المتماثلة، وبذلك تضيع الصلة بأمر البيع والمهمة والوقت المسجَّل.
ولذلك طوّرنا لتركيب Odoo 19 Community الخاص بنا الوحدة Merge Draft Invoices. ولأن هذه المشكلة لا تخصنا وحدنا، فإننا نتيحها مجانًا على GitHub بموجب رخصة AGPL-3 الحرة.
إلى المشروع على GitHub:
لماذا وحدة خاصة بدلاً من حل OCA؟
تتولى OCA منذ سنوات طويلة صيانة account_invoice_merge في مستودع OCA/account-invoicing. وفي وقت كتابة هذا المقال لا يزال الترحيل إلى 19.0 هناك طلب دمج (Pull Request) مفتوحًا، وليس وحدة منشورة.
لكن الأهم أن الوحدتين تقومان بأمرين مختلفين:
| OCA account_invoice_merge | Merge Draft Invoices | |
|---|---|---|
| النتيجة | تُدمج في إحدى الفواتير المحددة | دائمًا فاتورة جديدة |
| البنود | تُدمج البنود المتماثلة وتُجمع الكميات | يُنقل كل بند بنسبة واحد إلى واحد |
| المصدر | invoice_origin / payment_reference تُضمّ معًا في الترويسة | حقول المصدر لكل بند تُنسخ عبر خطاف Odoo نفسه _copy_data_extend_business_fields |
| تسجيل الوقت | غير مأخوذ في الاعتبار | يُنقل 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) | يُنقل إلى الفاتورة الجديدة قبل أن تصبح المصادر غير صالحة |
| تذكرة Helpdesk | ترتبط بتسجيل الوقت، وبذلك تشير تلقائيًا إلى الفاتورة الجديدة |
| الدردشة (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
- التذاكر، تُحدَّد عبر تسجيل الوقت – وتُعرض بصيغة Ticket: HT00219 أو Tickets: …، بحد أقصى خمس تذاكر زائد «و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:
- نسخ مجلد الوحدة إلى مسار إضافات (Addons) مُعدّ.
- تحديث قائمة التطبيقات في Odoo.
- تثبيت الوحدة account_invoice_merge_draft.
تعتمد الوحدة على account فقط. وكل ما تعرفه عن sale أو purchase أو project أو hr_timesheet أو sale_timesheet أو وحدة Helpdesk يُحدَّد أثناء التشغيل – فهي تُثبَّت وتعمل أيضًا على قاعدة بيانات فوترة خالصة.
بعد ذلك: المحاسبة ← العملاء ← الفواتير (أو الموردون ← الفواتير)، حدّد مسودتين أو أكثر، قائمة الترس ← دمج مسودات الفواتير. الإجراء متاح لجميع المستخدمين ضمن المجموعة الفوترة / إصدار الفواتير.
من يحدّث من إصدار سابق للوحدة ينبغي أن يُجري تحديثًا لمرة واحدة بالأمر -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}
)
من لديه إحالة عكسية على بند الفاتورة لا يحتاج إلى هذه الوحدة أصلًا: يكفي خطاف Odoo نفسه account.move.line._copy_data_extend_business_fields، ويُنقل الحقل بالتساوي عند الدمج وعند الإلغاء وعند النسخ. ولوحدات Helpdesk الأخرى أو لصياغة أخرى للعناوين توجد _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.