من يبيع في Odoo حزماً متكاملة، مثل جهاز قياس مع ملحقاته وتركيبه، أو محطة عمل مع الأجهزة والإعداد، أو حزمة صيانة من عدة خدمات، يحتاج في عرض السعر إلى أمرين في آن واحد: يجب أن يظهر التركيب في الطلب كي يعرف المخزن والفريق التقني والفوترة ما الذي سيُسلَّم. أما المطبوع الموجّه للعميل فلا ينبغي في كل حالة أن يعرض كل بند بمفرده مع سعره المنفرد.
تتيح وحدة OCA المسماة sale_product_set إدراج مجموعات المنتجات (قوائم مواد للبيع) في عرض السعر بنقرة واحدة. لكنها لا تتحكم في شكل الطباعة. ولهذا طورنا الوحدة Sale Product Set Layout (sale_product_set_layout). وهي مكتوبة لـ Odoo 19 وتتوفر كبرمجية حرة بموجب رخصة AGPL-3 مجاناً على GitHub:
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). وتربط الوحدة مجموعة المنتجات بهذين الخيارين القياسيين بدلاً من ابتكار تنسيق طباعة خاص.
ما الذي تفعله الوحدة
- إدراج المجموعة كقسم. ينشئ المعالج إضافة مجموعة سطر قسم باسم المجموعة، مثل 2 × مجموعة قياس قياسية، ويضع سطور المجموعة تحته. وتتحول الأقسام داخل المجموعة إلى أقسام فرعية.
- وضع الطباعة لكل مجموعة. يُحدَّد في المجموعة مرة واحدة كيف تُطبع. وعند الإدراج يمكن تغيير الوضع في المعالج لهذه الحالة وحدها.
- إخفاء السطور. يمكن تعليم سطور محددة بـ عدم الطباعة (في المجموعة: مخفي). وتُحتسب في المجموع لكنها لا تُطبع ولا تظهر في بوابة العملاء. وتنتقل هذه العلامة من عرض السعر إلى الفاتورة.
- سعر ثابت لكل سطر في المجموعة. يمكن أن يحمل سطر المجموعة سعر وحدة ثابتاً يحل محل السعر من قائمة الأسعار، مثلاً لتصحيح سعر مخفي.
بدون الوحدة (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 € لكل مجموعة) ويخفيه. في الواجهة الخلفية تكون الفاتورة قابلة للتتبع، وعلى المطبوع يظهر سعر الحزمة.
مهم للفهم: العلامة hide_in_report لا تغيّر شيئاً في الحساب. فالكمية والسعر والخصم والضريبة والمجاميع وقيود اليومية تنشأ دون تغيير عبر Odoo القياسي. السطر يغيب فقط عن المطبوع والبوابة.
- في مجموعة المنتجات: سطر المجموعة مع مخفي واختيارياً سعر ثابت مع سعر الوحدة.
- في سطور عرض السعر والفاتورة: العمود عدم الطباعة (
hide_in_report)، وهو متاح أيضاً للسطور العادية بدون مجموعة. - الانتقال: عند إنشاء الفاتورة من الطلب تنتقل العلامة إلى سطر الفاتورة. وعند نسخ عرض سعر تبقى العلامة محفوظة.
التنفيذ التقني
الوحدة صغيرة عن قصد: فهي توسّع النماذج الموجودة وتستبدل فقط المواضع التي يقرر فيها Odoo أي السطور تُطبع وكيف تُدرج المجموعة. لا توجد قوالب تقارير جديدة ولا نماذج جديدة ولا تدخلات في SQL.
النماذج والحقول الموسَّعة
| النموذج | الإضافة |
|---|---|
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 × مجموعة قياس قياسية، مع الفاصلة كفاصل عشري)، حتى يُعرف في المطبوع أن هناك مجموعتين وليس مجموعة واحدة بكمية مضاعفة.
- السعر الثابت في سطور المنتجات فقط. تضبط
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. وهذه هي طريقتنا:
- استنساخ مستودعي OCA استنساخاً سطحياً (
git clone --depth 1 --branch 19.0). - في مجلد مستقل نربط بروابط رمزية الوحدتين المطلوبتين فقط ونضيف هذا المجلد إلى
addons_path، وليس المستودعين بأكملهما. - التثبيت على قاعدة بيانات اختبار والتحقق من الوحدات التي تحصل على حالة مثبّتة بذلك.
- ثم فقط التثبيت في النسخة الإنتاجية، مع
--workers=0كي لا يتنافس الاستدعاء مع الخدمة العاملة على منفذ HTTP.
odoo-bin -c /etc/odoo/odoo.conf -d <datenbank> -i sale_product_set_layout --stop-after-init
تصبح الحقول والواجهات الجديدة في قاعدة البيانات بعد التثبيت. وإذا كانت الخدمة تعمل في وضع تعدد العمليات، فإن العمال يعيدون تحميل السجل المعدَّل عبر إشارة قاعدة البيانات. أما التغييرات في ملفات Python الخاصة بالوحدة فلا تصبح سارية إلا بعد إعادة تشغيل الخدمة.
طريقة الاستخدام خطوة بخطوة
- إنشاء مجموعة: المبيعات ← المنتجات ← مجموعات المنتجات. سمّوا المجموعة، واختاروا إدراج كقسم ووضع الطباعة، وأدخلوا المنتجات مع الكميات. وضعوا مخفي وسعر ثابت على سطور التصحيح.
- في عرض السعر: انقروا على إضافة مجموعة واختاروا المجموعة والكمية. ويمكن تغيير اسم القسم ووضع الطباعة قبل الإدراج.
- إخفاء سطور محددة: فعّلوا العمود عدم الطباعة في عرض السعر أو في الفاتورة.
- التحقق: افتحوا المعاينة أو ملف 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 قد تظهر وحدات auto_install إذا وُضعت مستودعات كاملة في addons_path. لذلك اربطوا فقط الوحدتين المطلوبتين (انظروا قسم التثبيت).
هل تعمل الوحدة مع Odoo Enterprise؟
تعتمد الوحدة على وحدات Community فقط (sale_management وaccount) ووحدتي OCA. ونحن نستخدمها على Odoo 19 Community. ولم نختبرها على Enterprise.
تحت أي رخصة تتوفر الوحدة؟
تحت AGPL-3. يحق لكم استخدامها وتعديلها ونشرها، ما دامت التعديلات تبقى تحت الرخصة نفسها. والأخطاء ومقترحات التحسين مرحَّب بها كمسألة (Issue) في مستودع GitHub .
الدعم من Biricon IT Services
نرعى تثبيتات Odoo للشركات الصغيرة والمتوسطة: من التطبيق الأولي مروراً بالتخصيصات والترحيلات وصولاً إلى التشغيل المستمر. وإذا أردتم تكييف مجموعات المنتجات أو قوالب عروض الأسعار أو التقارير في Odoo 19 مع سير عملكم، أو أردتم فحص وحدة مثل هذه وإدخالها بأمان إلى نسختكم الإنتاجية، فيسعدنا دعمكم. وتجدون تطويرات ذاتية أخرى في مدونتنا، مثلاً دمج مسودات الفواتير و مطابقة المدفوعات المصرفية مع الفواتير تلقائياً.
الحالة بتاريخ: 2 نوفمبر 2026. إصدار الوحدة 19.0.1.0.0. بيانات الأمثلة في الصور مختارة اعتباطياً، والمبالغ صافية.