تخطي للذهاب إلى المحتوى

Odoo 19 Community: مجموعات المنتجات كأقسام في عروض الأسعار – وحدتنا مفتوحة المصدر Sale Product Set Layout

2 نوفمبر 2026 | Odoo 19 Community | تطوير خاص
2 نوفمبر 2026 بواسطة
Odoo 19 Community: مجموعات المنتجات كأقسام في عروض الأسعار – وحدتنا مفتوحة المصدر Sale Product Set Layout

وحدة Odoo 19 Sale Product Set Layout: مجموعات المنتجات كقسم في عرض السعر مع وضع الطباعة والسطور المخفية والسعر الثابت

من يبيع حزمًا في Odoo، مثل جهاز قياس مع ملحقاته وتركيبه، أو تجهيز محطة عمل بالأجهزة وإعدادها، أو حزمة صيانة تضم عدة خدمات، يحتاج إلى أمرين معًا في عرض السعر: أن تُدرج مكوّنات الحزمة في أمر البيع، لتعرف فرق المستودع والفنيين والفوترة ما سيُسلَّم. لكن النسخة المطبوعة للعميل لا يلزم أن تعرض دائمًا كل بند وسعره على حدة.

تتيح وحدة OCA المسماة sale_product_set إدراج مجموعات المنتجات (قوائم مكوّنات مخصّصة للبيع) في عرض السعر بنقرة واحدة. لكنها لا تتحكّم في شكل الطباعة. ولهذا طوّرنا وحدة Sale Product Set Layout (sale_product_set_layout). وقد كُتبت لـ Odoo 19، وهي متاحة مجانًا على GitHub كبرمجية حرة بموجب ترخيص AGPL-3:

github.com/Alex0176/odoo-sale-product-set-layout

المشكلة: مجموعات منتجات دون تحكّم في شكل الطباعة

مجموعة المنتجات في Odoo هي قائمة معدّة مسبقًا تضم منتجات وكمياتها. فبدلًا من البحث عن عشرة أصناف كلٍّ على حدة، تختار إضافة مجموعة في عرض السعر، فتُدرج جميع البنود دفعة واحدة. وهذا يوفّر الوقت ويمنع نسيان أي بند. والوحدة sale_product_set من المستودع OCA/sale-workflow تؤدي هذه المهمة تحديدًا؛ أما المجموعات نفسها فتوفرها الوحدة product_set في المستودع OCA/product-attribute.

لكن تبقى عمليًا ثلاث مشكلات:

  • غياب تجميع واضح. تُدرج بنود المجموعة كبنود منفصلة في عرض السعر. وبعد إدراجها، لا يعود واضحًا أيّ البنود ينتمي إلى المجموعة نفسها، ولا يظهر إجمالي المجموعة في أي موضع.
  • غياب التحكّم في النسخة المطبوعة. قد ترغب في إظهار كل بند وسعره للعميل، أو سعر الحزمة فقط، أو الاكتفاء بسطر واحد. وهذا خيار ينبغي تحديده لكل مجموعة أو عميل، بدلًا من تنفيذه يدويًا من جديد في كل عرض سعر.
  • تسويات الأسعار لا تظهر بصورة مناسبة. إذا أُريد للحزمة سعر ثابت يختلف عن مجموع الأسعار المدرجة في قائمة الأسعار، فلا بد من إضافة بند تسوية. ويجب أن يدخل هذا البند في الحساب، لكن لا يلزم بالضرورة أن يظهر في النسخة المطبوعة للعميل.

يوفّر Odoo 19 الأدوات اللازمة لحل ذلك بصورة سليمة: أقسام وأقسام فرعية ضمن بنود عرض السعر، مع خياري إخفاء الأسعار وإخفاء المكوّنات (الحقلان collapse_prices وcollapse_composition). وتربط الوحدة مجموعة المنتجات بهذه الخيارات القياسية، بدلًا من إنشاء تنسيق طباعة خاص بها.

ما الذي تفعله الوحدة

  1. إدراج المجموعة كقسم. ينشئ معالج إضافة مجموعة سطر قسم يحمل اسم المجموعة، مثل 2 × Messkit Standard، ويضع بنود المجموعة تحته. وتتحوّل الأقسام الموجودة داخل المجموعة إلى أقسام فرعية.
  2. وضع طباعة لكل مجموعة. تُحدَّد طريقة طباعة المجموعة مرة واحدة في إعداداتها. وعند إدراجها، يمكن تغيير الوضع في المعالج لهذه الحالة وحدها.
  3. إخفاء البنود. يمكن وضع علامة عدم الطباعة على بنود محددة (وتُسمّى مخفي في المجموعة). وتدخل هذه البنود في الإجمالي، لكنها لا تُطبع ولا تظهر في بوابة العميل. وتنتقل العلامة من عرض السعر إلى الفاتورة.
  4. سعر ثابت لكل بند في المجموعة. يمكن تحديد سعر وحدة ثابت لبند في المجموعة ليحل محل السعر الوارد في قائمة الأسعار، مثلًا لإجراء تسوية سعر مخفية.
دون الوحدة (باستخدام sale_product_set فقط)باستخدام sale_product_set_layout
بنود المجموعة تظهر كبنود منفصلةسطر قسم يحمل اسم المجموعة والكمية ضمن الاسم، وتحته البنود
تبقى الأقسام داخل المجموعة أقسامًا رئيسية، فتفكّك بنيتهاتتحوّل الأقسام داخل المجموعة إلى أقسام فرعية (line_subsection)
شكل الطباعة ثابت دائمًاوضع طباعة لكل مجموعة، قابل للتغيير عند الإدراج
تسوية السعر ظاهرة في عرض السعر والفاتورةيمكن إخفاء البند مع بقائه ضمن حساب الإجمالي
السعر مأخوذ من قائمة الأسعار فقطسعر ثابت اختياري لكل بند في المجموعة
يقتصر التنسيق على عرض السعريسري التنسيق والإخفاء على عرض السعر وأمر البيع والفاتورة وبوابة العميل

أوضاع الطباعة الثلاثة بالتفصيل

وضع الطباعة هو حقل اختيار باسم print_mode في مجموعة المنتجات. وعند الإدراج، تُحوَّل قيمته إلى إعدادات الخيارين القياسيين في سطر القسم. ولتنفيذ ذلك، تحتوي الوحدة على جدول ربط واحد سهل القراءة:

# Druckmodus -> (collapse_prices, collapse_composition) der Abschnittszeile
PRINT_MODE_FLAGS = {
    "detailed":   (False, False),
    "total_only": (True,  False),
    "summary":    (False, True),
}

ثلاثة أشكال لطباعة مجموعة المنتجات نفسها: كل بند مع سعره، أو السعر الإجمالي فقط، أو سطر واحد فقط

الوضع (print_mode)التسمية الظاهرة في المجموعةالخيار القياسي في Odooشكل الطباعة
detailedكل بند مع سعرهلا شيءالقسم وجميع البنود مع مبالغها
total_onlyالبنود دون أسعار، مع إجمالي المجموعة فقطcollapse_prices (إخفاء الأسعار)القسم مع الإجمالي، والبنود بأسمائها وكمياتها فقط
summaryسطر واحد فقط بإجمالي المجموعةcollapse_composition (إخفاء المكوّنات)سطر القسم فقط مع الإجمالي

لأن الوحدة تستخدم الخيارات القياسية في Odoo 19 دون إنشاء تنسيق طباعة خاص بها، فإنها تعمل مع عرض السعر وتأكيد الطلب والفاتورة وبوابة العميل دون تخصيص قوالب التقارير. وينطبق السلوك نفسه على قوالب التقارير المخصّصة التي تراعي خيارات الأقسام.

البنود المخفية والسعر الثابت

من الحالات الشائعة أن يكون سعر الحزمة أقل من مجموع أسعار عناصرها. وبدلًا من تعديل أسعار العناصر بصورة مصطنعة، يُضاف بند تسوية بسعر ثابت (مثلًا −50,00 € لكل مجموعة) ثم يُخفى. وتظل طريقة الحساب واضحة في الواجهة الخلفية، بينما يظهر سعر الحزمة في النسخة المطبوعة.

تسوية السعر ظاهرة ومميّزة في الواجهة الخلفية، ومخفية في عرض السعر المطبوع وملف PDF والفاتورة والبوابة، مع بقاء الإجمالي مطابقًا

للتوضيح: العلامة hide_in_report لا تغيّر شيئًا في الحساب. إذ يظل Odoo القياسي يتولى حساب الكمية والسعر والخصم والضريبة والإجماليات وإنشاء القيود المحاسبية كالمعتاد. ويقتصر الأمر على إخفاء البند من النسخة المطبوعة والبوابة.

  • في مجموعة المنتجات: بند في المجموعة مع خيار مخفي، واختياريًا سعر ثابت مع تحديد سعر الوحدة.
  • في بنود عروض الأسعار والفواتير: عمود عدم الطباعة (hide_in_report)، وهو متاح أيضًا للبنود العادية التي لا تنتمي إلى مجموعة.
  • انتقال العلامة: عند إنشاء الفاتورة من أمر البيع، تنتقل العلامة إلى بند الفاتورة. كما تُحفظ عند نسخ عرض السعر.

التنفيذ التقني

صُمّمت الوحدة عمدًا لتكون صغيرة: فهي توسّع النماذج الموجودة، ولا تتجاوز السلوك الافتراضي إلا في المواضع التي يحدّد فيها Odoo البنود التي ستُطبع وكيفية إدراج المجموعة. ولا تضيف قوالب تقارير جديدة أو نماذج جديدة، ولا تُجري تعديلات مباشرة باستخدام SQL.

تدفّق البيانات: مجموعة المنتجات، معالج إضافة مجموعة، بنود عرض السعر، بنود الفاتورة، ملف PDF وبوابة العميل

النماذج والحقول الموسّعة

النموذجإضافة
product.setالحقلان insert_as_section (منطقي، مفعّل افتراضيًا) وprint_mode (اختيار، إلزامي، قيمته الافتراضية detailed)
product.set.lineالحقول hide_in_report وfixed_price وprice_unit؛ تمرّرها prepare_sale_order_line_values() إلى سطر عرض السعر
sale.product.set.wizardالحقول insert_as_section وsection_name وprint_mode؛ تنشئ _prepare_order_lines() سطر القسم
sale.order / sale.order.lineالحقل hide_in_report (مع copy=True)؛ تتجاوز _get_order_lines_to_report() الأسطر المخفية؛ وتمرّر _prepare_invoice_line() علامة الإخفاء
account.move / account.move.lineالحقل hide_in_report؛ تستبعد _get_move_lines_to_report() و_get_detailed_lines() و_get_subsection_lines() الأسطر المخفية؛ وتضيف _get_aml_vals() علامة الإخفاء إلى بيانات التقرير

الإخفاء في عرض السعر

يحدّد Odoo 19 أسطر عرض السعر المراد طباعتها في _get_order_lines_to_report(). وتضيف الوحدة مرشّحًا في هذا الموضع. ولا حاجة إلى تعديل قالب التقرير، إذ يستدعي كلٌّ من قالب تقرير عرض السعر (sale/report/ir_actions_report_templates.xml) وقالب البوابة (sale/views/sale_portal_templates.xml) هذه الطريقة:

class SaleOrder(models.Model):
    _inherit = "sale.order"

    def _get_order_lines_to_report(self):
        return super()._get_order_lines_to_report().filtered(
            lambda line: not line.hide_in_report
        )

تمرير علامة الإخفاء إلى الفاتورة

عند إنشاء سطر الفاتورة من سطر عرض السعر (_prepare_invoice_line())، تُنسخ علامة الإخفاء. وفي جانب الفواتير، توجد في Odoo 19 مسارات متعددة لوصول الأسطر إلى التقرير: القائمة الرئيسية، وأسطر القسم، وأسطر القسم الفرعي. لذلك تطبّق الوحدة التصفية في مواضع متعددة (_get_move_lines_to_report() على مستوى المستند، و_get_detailed_lines() و_get_subsection_lines() على مستوى السطر)، وإلا فقد يظهر سطر مخفي مجددًا عبر أحد المسارات الأخرى:

class AccountMoveLine(models.Model):
    _inherit = "account.move.line"

    hide_in_report = fields.Boolean(string="Hidden in report")

    def _get_aml_vals(self, hide_taxes):
        res = super()._get_aml_vals(hide_taxes)
        res["hide_in_report"] = self.hide_in_report
        return res

    def _get_detailed_lines(self, children_lines, direct_children_lines, subsection_lines):
        res = super()._get_detailed_lines(children_lines, direct_children_lines, subsection_lines)
        return [vals for vals in res if not vals.get("hide_in_report")]

    def _get_subsection_lines(self, subsection_line, children_lines):
        res = super()._get_subsection_lines(subsection_line, children_lines)
        return [vals for vals in res if not vals.get("hide_in_report")]

المعالج: قسم بدلًا من أسطر متفرقة

ينشئ المعالج sale.product.set.wizard من sale_product_set أسطر عرض السعر في _prepare_order_lines(). وتعيد الوحدة تعريف هذه الطريقة. إذا كان خيار إدراج كقسم معطّلًا، فإنها تستدعي الطريقة الأصلية ببساطة، فيبقى السلوك كما هو في وحدة OCA. وفي الحالة الأخرى، يُنشأ سطر القسم أولًا مع مفتاحَي التبديل القياسيين، ثم تتبعه أسطر المجموعة. وتتحول الأقسام داخل المجموعة إلى أقسام فرعية، حتى لا تُخلّ بالبنية الرئيسية:

def _prepare_order_lines(self):
    if not self.insert_as_section:
        return super()._prepare_order_lines()
    sequence = self._get_max_sequence() + 1
    collapse_prices, collapse_composition = PRINT_MODE_FLAGS[self.print_mode or "detailed"]
    order_lines = [Command.create({
        "display_type": "line_section",
        "name": self.section_name or self.product_set_id.name,
        "sequence": sequence,
        "collapse_prices": collapse_prices,
        "collapse_composition": collapse_composition,
    })]
    for set_line in self._get_lines():
        sequence += 1
        values = set_line.prepare_sale_order_line_values(self.order_id, self.quantity, sequence)
        if values.get("display_type") == "line_section":
            values["display_type"] = "line_subsection"
        order_lines.append(Command.create(values))
    return order_lines

قرارات وراء الكواليس

  • طرق حساب منفصلة في المعالج. لكلٍّ من اسم القسم ووضع الطباعة وخيار إدراج كقسم طريقة compute مستقلة (store=True وreadonly=False). ولو اشتركت الحقول الثلاثة في الطريقة نفسها، لأدّى تمرير قيمة صراحةً لأحدها إلى إعادة تعيين الحقول الأخرى. ويظهر ذلك خصوصًا عند إنشاء المعالج بقيم محددة من خلال الشيفرة أو الاختبارات.
  • اسم القسم مع الكمية. عندما تختلف الكمية عن 1، تظهر في الاسم (2 × Messkit Standard، مع استخدام الفاصلة فاصلًا عشريًا)، حتى يتضح في النسخة المطبوعة أنها مجموعتان وليست مجموعة واحدة بكميات مضاعفة.
  • السعر الثابت لأسطر المنتجات فقط. لا تضبط prepare_sale_order_line_values() قيمتَي hide_in_report وprice_unit إلا إذا لم يكن سطر المجموعة سطر قسم أو ملاحظة.
  • Odoo القياسي بدلاً من التطوير الخاص. يستخدم وضع الطباعة والإخفاء الخطافات والحقول الموجودة في Odoo 19. وهذا يجعل التحديثات قابلة للإدارة: إذا غيّر Odoo قالب التقرير تبقى الوحدة صالحة ما دامت الدوال موجودة.

التثبيت دون آثار جانبية غير مرغوبة

المتطلبات: Odoo 19.0، وsale_product_set (OCA/sale-workflow، الفرع 19.0)، وproduct_set (OCA/product-attribute، الفرع 19.0). وتعتمد الوحدة نفسها على sale_management وsale_product_set وaccount.

فخ auto_install في وحدات OCA. تحتوي مستودعات OCA كثيرة على وحدات تستخدم auto_install. وبمجرد توافر اعتمادياتها، يثبّتها Odoo تلقائيًا عند عملية التثبيت أو التحديث التالية دون طلب تأكيد. لذا فإن وضع مستودع OCA كامل في addons_path لبيئة الإنتاج قد يؤدي إلى تثبيت وحدات لم يطلبها أحد. وفي OCA/product-attribute، ينطبق ذلك مثلًا على purchase_product_template_tags. ونتّبع الخطوات التالية:

  1. استنساخ مستودعَي OCA بنسخة محدودة العمق (git clone --depth 1 --branch 19.0).
  2. إنشاء روابط رمزية في مجلد مستقل للوحدتين المطلوبتين فقط، وإضافة هذا المجلد إلى addons_path، بدلًا من المستودعات كاملة.
  3. التثبيت على قاعدة بيانات اختبار، والتحقق من الوحدات التي تحصل خلال ذلك على حالة مثبّت.
  4. بعد ذلك فقط، إجراء التثبيت في بيئة الإنتاج باستخدام --workers=0، حتى لا تتنافس عملية التشغيل مع الخدمة القائمة على منفذ HTTP.
odoo-bin -c /etc/odoo/odoo.conf -d <datenbank> -i sale_product_set_layout --stop-after-init

تصبح الحقول وواجهات العرض الجديدة موجودة في قاعدة البيانات بعد التثبيت. وإذا كانت الخدمة تعمل بعمليات متعددة، تعيد العمليات العاملة تحميل سجل النماذج المعدّل عبر إشارة قاعدة البيانات. أما التغييرات على ملفات Python في الوحدة، فلا تسري إلا بعد إعادة تشغيل الخدمة.

الاستخدام خطوة بخطوة

  1. إنشاء مجموعة: المبيعات → المنتجات → مجموعات المنتجات. أدخل اسم المجموعة، واضبط إدراج كقسم ووضع الطباعة، وأضف المنتجات وكمياتها. وفعّل مخفي وسعر ثابت لأسطر التصحيح.
  2. في عرض السعر: انقر على إضافة مجموعة، واختر المجموعة والكمية. ويمكن تغيير اسم القسم ووضع الطباعة قبل الإدراج.
  3. إخفاء أسطر محددة: فعّل عمود عدم الطباعة في عرض السعر أو الفاتورة.
  4. التحقق: افتح المعاينة أو ملف PDF. ويرى العملاء العرض نفسه في البوابة.

الاختبارات

تتضمن الوحدة ستة اختبارات آلية نجريها على قاعدة بيانات اختبار قبل نقل أي تغيير إلى بيئة الإنتاج:

  • الإدراج كقسم، بما يشمل وضع الطباعة والقسم الفرعي
  • غياب السطر المخفي عن الأسطر المراد طباعتها
  • وضع سطر واحد (summary)
  • الإدراج دون قسم، وفق السلوك الأصلي
  • انتقال علامة hide_in_report إلى الفاتورة، وغياب السطر عن _get_move_lines_to_report()، مع استمرار احتسابه في إجمالي القسم
  • عدم ظهور السطر المخفي ضمن أسطر القسم في الفاتورة عند استخدام وضع السعر الإجمالي فقط، مع بقاء إجمالي القسم كاملًا
odoo-bin -d <testdatenbank> -i sale_product_set_layout \
         --test-enable --test-tags=/sale_product_set_layout --stop-after-init

الترجمة الألمانية موجودة في i18n/de.po.

حدود الوحدة وتنويه قانوني

  • تنسيق العرض فقط. لا تغيّر الوحدة طريقة حساب Odoo للأسعار أو الخصومات أو الضرائب. إنما تغيّر فقط ما يُطبع وما يُعرض في البوابة.
  • الأسطر المخفية تظل ظاهرة في الواجهة الخلفية. تؤثر علامة الإخفاء في النسخة المطبوعة والبوابة، ولا تؤثر في العرض الذي يراه الموظفون على الشاشة.
  • التحقق من بيانات الفاتورة. تُحتسب الأسطر المخفية ضمن الإجمالي، لكنها لا تظهر في المستند. وينبغي أن تتحقق مع مستشارك الضريبي قبل استخدام هذه الميزة في الفواتير من توافق ذلك مع متطلبات الفواتير في بلدك، مثل اشتراط وصف السلع المسلّمة أو الخدمات المقدّمة. أما في عروض الأسعار، فالإخفاء أقل إشكالًا. ولا يُعدّ هذا المقال استشارة قانونية.
  • مُختبَرة على Odoo 19. طوّرنا الوحدة واختبرناها على Odoo 19.0 Community. وهي تستخدم خيارات الأقسام المتاحة بهذه الصيغة في Odoo 19. ولم نختبرها على إصدارات أخرى.

الأسئلة الشائعة

لماذا أحتاج إلى هذه الوحدة مع أن Odoo 19 يدعم الأقسام بالفعل؟

خيارات الأقسام موجودة في Odoo نفسه. وما كان ينقص هو ربطها بمجموعة المنتجات: إذ ينبغي إدراج المجموعة تلقائيًا كقسم، مع وضع الطباعة المحفوظ فيها. ومن دون هذه الوحدة، يجب إنشاء سطر القسم وتهيئته يدويًا بعد كل إدراج.

ما الفرق بين «إخفاء الأسعار» و«إخفاء المكونات»؟

يُبقي «إخفاء الأسعار» (collapse_prices) الأسطر ظاهرة، لكنه يخفي مبالغها ويعرض إجمالي القسم فقط. أما «إخفاء المكونات» (collapse_composition) فيخفي الأسطر بالكامل، فلا يرى العميل سوى القسم وإجماليه.

هل يُحتسب السطر المخفي ضمن إجمالي الفاتورة؟

نعم. تبقى الأسطر المخفية مشمولة في الإجمالي والضريبة والقيد المحاسبي. ولا تؤثر علامة الإخفاء إلا في طريقة العرض على النسخة المطبوعة وفي بوابة العملاء.

هل يمكنني تغيير وضع الطباعة عند الإدراج؟

نعم. يُؤخذ الوضع الافتراضي من المجموعة، ويمكن تغييره في المعالج لعملية الإدراج الحالية. وتبقى المجموعة نفسها دون تغيير.

ماذا يحدث إذا عطّلت إدراج كقسم؟

عندئذ يعمل المعالج كما في وحدة OCA المسماة sale_product_set: تُدرَج أسطر المجموعة دون قسم.

هل تثبّت الوحدة وحدات إضافية على نحو غير متوقع؟

الوحدة نفسها لا تفعل ذلك. لكن عند إضافة مستودعات OCA كاملة إلى addons_path، قد تُثبَّت وحدات تستخدم auto_install. لذلك أنشئ روابط للوحدتين المطلوبتين فقط (انظر قسم التثبيت).

هل تعمل الوحدة مع Odoo Enterprise؟

تعتمد الوحدة فقط على وحدات Community (sale_management وaccount) ووحدتَي OCA. ونحن نستخدمها على Odoo 19 Community، ولم نختبرها على Enterprise.

بموجب أي ترخيص تتوفر الوحدة؟

بموجب AGPL-3. يمكنك استخدامها وتعديلها وتوزيعها، ما دامت التعديلات تخضع للترخيص نفسه. والإبلاغ عن الأخطاء واقتراح التحسينات عبر تذاكر في مستودع GitHub موضع ترحيب.

الدعم من Biricon IT Services

نرعى تثبيتات Odoo للشركات الصغيرة والمتوسطة: من التطبيق الأولي مروراً بالتخصيصات والترحيلات وصولاً إلى التشغيل المستمر. وإذا أردتم تكييف مجموعات المنتجات أو قوالب عروض الأسعار أو التقارير في Odoo 19 مع سير عملكم، أو أردتم فحص وحدة مثل هذه وإدخالها بأمان إلى نسختكم الإنتاجية، فيسعدنا دعمكم. وتجدون تطويرات ذاتية أخرى في مدونتنا، مثلاً دمج مسودات الفواتير و مطابقة المدفوعات المصرفية مع الفواتير تلقائيًا.

تواصل معنا الآن

آخر تحديث: 2 نوفمبر 2026. إصدار الوحدة 19.0.1.0.0. بيانات الأمثلة في الصور مختارة اعتباطًا، والمبالغ غير شاملة للضريبة.