هر کسی که در 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 × Messkit Standard، و ردیفهای مجموعه را زیر آن قرار میدهد. بخشهای داخل مجموعه به زیربخش تبدیل میشوند.
- حالت چاپ برای هر مجموعه. در مجموعه یک بار تعیین میشود که چگونه چاپ شود. هنگام درج، حالت را میتوان در دستیار برای همان یک مورد تغییر داد.
- پنهانکردن ردیفها. ردیفهای منفرد را میتوان با چاپ نشود (در مجموعه: پنهان) علامت زد. این ردیفها در جمع محاسبه میشوند، اما نه چاپ میشوند و نه در پورتال مشتری نمایش داده میشوند. این نشانگر از پیشنهاد قیمت به فاکتور منتقل میشود.
- قیمت ثابت برای هر ردیف مجموعه. یک ردیف مجموعه میتواند قیمت واحد ثابتی داشته باشد که جایگزین قیمت فهرست قیمت میشود، مثلاً برای یک اصلاح قیمت پنهان.
بدون ماژول (فقط 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 (Boolean، پیشفرض روشن) و 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 چنین است. روش ما:
- دو مخزن OCA را بهصورت سطحی کلون کنید (
git clone --depth 1 --branch 19.0). - در یک پوشهٔ جداگانه فقط دو ماژول موردنیاز را با symlink پیوند دهید و همان پوشه را به
addons_pathاضافه کنید، نه کل مخزنها را. - روی یک پایگاهدادهٔ آزمایشی نصب کنید و بررسی کنید چه ماژولهایی در این فرایند وضعیت نصبشده میگیرند.
- تازه بعد از آن در محیط عملیاتی نصب کنید، با
--workers=0، تا این فراخوانی با سرویس در حال اجرا بر سر پورت HTTP رقابت نکند.
odoo-bin -c /etc/odoo/odoo.conf -d <datenbank> -i sale_product_set_layout --stop-after-init
فیلدها و نماهای جدید پس از نصب در پایگاه داده قرار دارند. اگر سرویس در حالت چندفرایندی اجرا شود، workerها رجیستری تغییریافته را از طریق سیگنال پایگاه داده دوباره بارگذاری میکنند. تغییرات در فایلهای 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 اگر کل مخزنها در addons_path قرار داده شوند، ممکن است ماژولهای auto_install وارد شوند. بنابراین فقط دو ماژول موردنیاز را پیوند دهید (بخش نصب را ببینید).
آیا ماژول با Odoo Enterprise کار میکند؟
ماژول فقط به ماژولهای Community (sale_management، account) و دو ماژول OCA وابسته است. ما آن را روی Odoo 19 Community به کار میبریم. روی Enterprise آن را آزمایش نکردهایم.
ماژول تحت چه مجوزی است؟
تحت AGPL-3. میتوانید از آن استفاده کنید، آن را سازگار کنید و منتشر کنید، به شرط آنکه تغییرات تحت همان مجوز باقی بمانند. خطاها و پیشنهادهای بهبود را بهصورت Issue در مخزن GitHub با خوشحالی میپذیریم.
پشتیبانی از سوی Biricon IT Services
ما نصبهای Odoo را برای کسبوکارهای کوچک و متوسط پشتیبانی میکنیم: از راهاندازی اولیه و سفارشیسازیها و مهاجرتها تا بهرهبرداری جاری. اگر میخواهید مجموعههای محصول، قالبهای پیشنهاد قیمت یا گزارشها را در Odoo 19 با روندهای کاری خود منطبق کنید، یا اگر میخواهید ماژولی مانند این را بررسی کنید و با اطمینان به نمونهٔ عملیاتی خود بیاورید، با کمال میل در کنار شما هستیم. توسعههای داخلی دیگر ما را در وبلاگمان مییابید، برای مثال ادغام فاکتورهای پیشنویس و تخصیص خودکار پرداختهای بانکی به فاکتورها.
تاریخ بروزرسانی: ۲ نوامبر ۲۰۲۶. نسخهٔ ماژول 19.0.1.0.0. دادهٔ نمونه در تصاویر آزادانه انتخاب شدهاند، مبالغ بهصورت خالص.