diff --git a/sale_price_compliance/README.rst b/sale_price_compliance/README.rst new file mode 100644 index 00000000000..a03f1f6bc80 --- /dev/null +++ b/sale_price_compliance/README.rst @@ -0,0 +1,282 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +===================== +Sale Price Compliance +===================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:f260e276fb4e11d03fc4654e03277b5e5d55090c447b6619ead20cc260e5687c + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsale--workflow-lightgray.png?logo=github + :target: https://github.com/OCA/sale-workflow/tree/16.0/sale_price_compliance + :alt: OCA/sale-workflow +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/sale-workflow-16-0/sale-workflow-16-0-sale_price_compliance + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/sale-workflow&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module extends the sales pricing functionality to display a color +code based on the price at which products are being sold. + +Managing Price Compliance Thresholds can be done by adding *Manage Price +Compliance* group to any user. Compliance Tiers can be filtered on sale +reports. + +You can use up to 3 different price compliance tiers for products, +categories or for all company. + +You can customize the texts and icons of the tiers via the System +Parameters. + +Labels of the tier fields will change according to +``sale_price_compliance.price_compliance_selection_tiers_text`` System +Parameter in views. + +This functionality only applies to Sales. + +A sale with a line with Non Compliant price (doesn't fit in any defined +tiers) can't be confirmed. Only Sale Administrators can validate this +sales and a message will be posted. + +Price Compliance thresholds are selected in this order: Product > +Product Category > Company + +Each Tier represents the maximun discount applied to the Price of the +Product to achieve the target tier. + +**Color Compliance Tiers** + +- Tier 1 🟩: High-yield (Fully compliant) +- Tier 2 🟨: Medium-yield (Moderately compliant) +- Tier 3 🟧: Low-yield (Low compliant) +- Non Compliant 🟥: Non Compliant price (blocked) +- Pricelist 🟦: Pricelist has been used and it's price is between + pricelist and last compliant tier. + +**Information Display on Price Compliant Tiers** Includes a popup to +display detailed information about the price ranges within each price +compliance tiers. + +Also displays Product Base UoM and Product List Price in Sale Currency +in the Top Right corner of the Popup. + +|Price Compliance Widget| + +|Price Compliance Sale Line Form| + +|Price Compliance Sale Line List| + +|Price Compliance Sale Line Kanban| + +.. |Price Compliance Widget| image:: https://raw.githubusercontent.com/OCA/sale-workflow/16.0/sale_price_compliance/static/img/price_compliance_widget.png +.. |Price Compliance Sale Line Form| image:: https://raw.githubusercontent.com/OCA/sale-workflow/16.0/sale_price_compliance/static/img/price_compliance_sale_line_form.png +.. |Price Compliance Sale Line List| image:: https://raw.githubusercontent.com/OCA/sale-workflow/16.0/sale_price_compliance/static/img/price_compliance_sale_line_list.png +.. |Price Compliance Sale Line Kanban| image:: https://raw.githubusercontent.com/OCA/sale-workflow/16.0/sale_price_compliance/static/img/price_compliance_sale_line_kanban.png + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Use Cases / Context +=================== + +This module was developed because sometimes commercial users are +incentivized to sell at prices that are more beneficial to the company. + +It will be useful if you want to visually show your commercial users +whether the price they are selling at is in line with the company's +pricing policy. + +Usage +===== + +To use this module, you need to: + +**Configure Parameter Text: Change the default price compliance tiers +texts** + +1. Go to System Parameters. +2. Create a new parameter with key + ``sale_price_compliance.price_compliance_selection_tiers_text``. +3. Write a *dictionary* with the values for tiers that you want to + change it's default text. +4. Save the parameter. +5. Create a new parameter with key + ``sale_price_compliance.price_compliance_selection_tiers_icon``. +6. Write a *dictionary* with the values for tiers that you want to + change it's default icon. +7. Save the parameter. + +Example: + +Param *sale_price_compliance.price_compliance_selection_tiers_text*: + +{'t1': 'Gold', 't2': 'Silver', 't3': 'Bronze', 'pricelist': 'Draw'} + +Param *sale_price_compliance.price_compliance_selection_tiers_icon*: + +{'t1': '🥇', 't2': '🥈', 't3': '🥉', 'non_compliant': '⛔️', 'pricelist': +'🤝'} + +Resulting texts and icons will be: + +- Gold: 🥇 +- Silver: 🥈 +- Bronze: 🥉 +- Non Compliant: ⛔️ +- Draw: 🤝 + +Available keys to be used in the dictionary are: + +- t1: Tier 1 +- t2: Tier 2 +- t3: Tier 3 +- non_compliant: Non Compliant +- pricelist: Pricelist + +**Configure: Product Price Compliance Thresholds configuration** + +1. Go to Sales > Products > Products > Select one > Sales tab +2. Enable Use Price Compliance Thresholds under Sale Price Compliance + Thresholds +3. Fill from 1 to 3 tiers that you want to use in this product. + +Example: Tier 1: 10%, Tier 2: 20%, Tier 3: 30%. (Use all tiers) + +**Configure: Product Category Price Compliance Thresholds +configuration** + +1. Go to Sales > Configuration > Products > Product Categories > Select + one +2. Enable Use Price Compliance Thresholds under Sale Price Compliance + Thresholds +3. Fill from 1 to 3 tiers that you want to use in this category. + +Example: Tier 1: 15%, Tier 2: 25%, Tier 3: 0%. (Don't use tier 3) + +**Configure: Company Price Compliance Thresholds configuration** + +1. Go to Sales > Configuration > Settings +2. Enable Use Price Compliance Thresholds under Pricing section +3. Fill from 1 to 3 tiers that you want to use for the company. + +Example: Tier 1: 30%, Tier 2: 0%, Tier 3: 0%. (Don't use tier 2 and 3) + +**Sale: As a Salesman user** + +1. Create a new sale and fill contact field +2. Add a new line with a product that has been configured to use Price + Compliance +3. Click on the 🟩, 🟨, 🟧, 🟥 or 🟦 icon at the start of the line. +4. You will see a popup with useful information about the Tier ranges. +5. Play with it and then set a Non Compliant price (change price or + discount to achieve this). +6. Try to confirm the Sale and see the error. + +**Sale: As a Sales Administrator** + +1. Confirm the previous Sale. +2. See the message on the chatter. + +**Reporting: Sales report** + +1. Go to Sales > Reporting > Sales +2. Select bar chart or Pivot view +3. Group by Price Compliance Level + +**Reporting: Salesperson report** + +1. Go to Sales > Reporting > Salesperson +2. Select Bar chart + stacked or Pivot view +3. Group by Price Compliance Level + +**Reporting: Product report** + +1. Go to Sales > Reporting > Product +2. Select Pie chart + stacked or Pivot view +3. Group by Price Compliance Level + +**Reporting: Customers report** + +1. Go to Sales > Reporting > Customers +2. Select Bar chart + stacked or Pivot view +3. Group by Price Compliance Level + +Known issues / Roadmap +====================== + +- Widget is not shown properly if the order is not saved and refreshed. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Moduon + +Contributors +------------ + +- Eduardo de Miguel (`Moduon `__) + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-Shide| image:: https://github.com/Shide.png?size=40px + :target: https://github.com/Shide + :alt: Shide +.. |maintainer-rafaelbn| image:: https://github.com/rafaelbn.png?size=40px + :target: https://github.com/rafaelbn + :alt: rafaelbn + +Current `maintainers `__: + +|maintainer-Shide| |maintainer-rafaelbn| + +This module is part of the `OCA/sale-workflow `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/sale_price_compliance/__init__.py b/sale_price_compliance/__init__.py new file mode 100644 index 00000000000..616f71634de --- /dev/null +++ b/sale_price_compliance/__init__.py @@ -0,0 +1,3 @@ +from . import models +from . import report +from .hooks import pre_init_hook diff --git a/sale_price_compliance/__manifest__.py b/sale_price_compliance/__manifest__.py new file mode 100644 index 00000000000..e36aa08387c --- /dev/null +++ b/sale_price_compliance/__manifest__.py @@ -0,0 +1,35 @@ +# Copyright 2025 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) + +{ + "name": "Sale Price Compliance", + "summary": "Visual price compliance based on product, category and company thresholds", + "version": "16.0.1.0.0", + "development_status": "Alpha", + "category": "Sales/Sales", + "website": "https://github.com/OCA/sale-workflow", + "author": "Moduon, Odoo Community Association (OCA)", + "maintainers": ["Shide", "rafaelbn"], + "license": "LGPL-3", + "application": False, + "installable": True, + "depends": [ + "sale", + ], + "data": [ + "security/sale_price_compliance.xml", + "views/res_config_settings_view.xml", + "views/product_category_view.xml", + "views/product_template_view.xml", + "views/product_product_view.xml", + "views/sale_order_line_view.xml", + "views/sale_order_view.xml", + "report/sale_report_view.xml", + ], + "pre_init_hook": "pre_init_hook", + "assets": { + "web.assets_backend": [ + "sale_price_compliance/static/src/**/*", + ], + }, +} diff --git a/sale_price_compliance/hooks.py b/sale_price_compliance/hooks.py new file mode 100644 index 00000000000..50204722c47 --- /dev/null +++ b/sale_price_compliance/hooks.py @@ -0,0 +1,28 @@ +# Copyright 2026 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) + +import logging + +from odoo.tools import sql + +logger = logging.getLogger(__name__) + + +def pre_init_hook(cr): + """Create table columns for computed fields to not get them computed by Odoo.""" + if not sql.column_exists(cr, "sale_order_line", "price_compliance_tier"): + sql.create_column( + cr, + "sale_order_line", + "price_compliance_tier", + "VARCHAR", + comment="Price Compliance Tier", + ) + if not sql.column_exists(cr, "sale_order_line", "price_compliance_data"): + sql.create_column( + cr, + "sale_order_line", + "price_compliance_data", + "JSONB", + comment="Price Compliance Data", + ) diff --git a/sale_price_compliance/i18n/es.po b/sale_price_compliance/i18n/es.po new file mode 100644 index 00000000000..6c28a9d7d16 --- /dev/null +++ b/sale_price_compliance/i18n/es.po @@ -0,0 +1,325 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_price_compliance +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-02-19 11:09+0000\n" +"PO-Revision-Date: 2026-02-19 12:10+0100\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.6\n" + +#. module: sale_price_compliance +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.res_config_settings_view_form +msgid "" +"" +msgstr "" +"" + +#. module: sale_price_compliance +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.res_config_settings_view_form +msgid "(High-yield: Fully compliant)" +msgstr "(Alta Rentabilidad: Totalmente conforme)" + +#. module: sale_price_compliance +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.res_config_settings_view_form +msgid "(Low-yield: Low compliant)" +msgstr "(Baja Rentabilidad: Cumplimiento mínimo)" + +#. module: sale_price_compliance +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.res_config_settings_view_form +msgid "(Medium-yield: Moderately compliant)" +msgstr "(Rentabilidad Media: Moderadamente conforme)" + +#. module: sale_price_compliance +#: model:ir.model,name:sale_price_compliance.model_res_company +msgid "Companies" +msgstr "Compañías" + +#. module: sale_price_compliance +#: model:ir.model,name:sale_price_compliance.model_res_config_settings +msgid "Config Settings" +msgstr "Ajustes de configuración" + +#. module: sale_price_compliance +#. odoo-javascript +#: code:addons/sale_price_compliance/static/src/widgets/price_compliance_level_widget.xml:0 +#, python-format +msgid "Discount" +msgstr "Descuento" + +#. module: sale_price_compliance +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.res_config_settings_view_form +msgid "" +"Enable Default Price Compliance Thresholds for the Company.\n" +" Prices under last filled Tier are considered non-compliant and will be blocked." +msgstr "" +"Habilitar umbrales de cumplimiento de precios por defecto para la compañía.\n" +" Los precios por debajo del último Nivel completado se consideran no conformes y serán bloqueados." + +#. module: sale_price_compliance +#: model:ir.model.fields,help:sale_price_compliance.field_sale_order_line__price_compliance_data +msgid "Holds additional data related to price compliance calculations." +msgstr "Contiene datos adicionales relacionados con los cálculos de cumplimiento de precios." + +#. module: sale_price_compliance +#: model:ir.model.fields,help:sale_price_compliance.field_product_price_compliance_threshold_tier_mixin__price_compliance_tier +#: model:ir.model.fields,help:sale_price_compliance.field_sale_order_line__price_compliance_tier +#: model:ir.model.fields,help:sale_price_compliance.field_sale_report__price_compliance_tier +msgid "Indicates the Tier of Price Compliance based on the unit price and applied discount compared to defined thresholds." +msgstr "" +"Indica el Nivel de cumplimiento de precios basado en el precio unitario y el descuento aplicado en comparación con los " +"umbrales definidos." + +#. module: sale_price_compliance +#: model:res.groups,name:sale_price_compliance.price_compliance_threshold_manager +msgid "Manage Price Compliance" +msgstr "Gestionar el cumplimiento de precios" + +#. module: sale_price_compliance +#. odoo-python +#: code:addons/sale_price_compliance/models/price_compliance_threshold_tier_mixin.py:0 +#, python-format +msgid "Non Compliant" +msgstr "No Conforme" + +#. module: sale_price_compliance +#. odoo-python +#: code:addons/sale_price_compliance/models/sale_order.py:0 +#, python-format +msgid "Order confirmed with Non Compliant prices by %s." +msgstr "Pedido confirmado con precios No Conformes por %s." + +#. module: sale_price_compliance +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.sale_report_view_tree +msgid "PCL" +msgstr "NCP" + +#. module: sale_price_compliance +#: model:ir.model.fields,field_description:sale_price_compliance.field_sale_order_line__price_compliance_data +msgid "Price Compliance Data" +msgstr "Datos de cumplimiento de precios" + +#. module: sale_price_compliance +#. odoo-javascript +#: code:addons/sale_price_compliance/static/src/widgets/price_compliance_level_widget.xml:0 +#, python-format +msgid "Price Compliance Thresholds" +msgstr "Umbrales de cumplimiento de precios" + +#. module: sale_price_compliance +#: model:ir.model.constraint,message:sale_price_compliance.constraint_product_category_check_price_compliance_le_1 +#: model:ir.model.constraint,message:sale_price_compliance.constraint_product_price_compliance_threshold_config_mixin_check_price_compliance_le_1 +#: model:ir.model.constraint,message:sale_price_compliance.constraint_product_product_check_price_compliance_le_1 +#: model:ir.model.constraint,message:sale_price_compliance.constraint_product_template_check_price_compliance_le_1 +#: model:ir.model.constraint,message:sale_price_compliance.constraint_res_company_check_price_compliance_le_1 +msgid "Price Compliance Thresholds Percentage should be lower or equal to 100%." +msgstr "El porcentaje de los umbrales de cumplimiento de precios no puede ser superior al 100%." + +#. module: sale_price_compliance +#: model:ir.model.constraint,message:sale_price_compliance.constraint_product_category_check_price_compliance_positive +#: model:ir.model.constraint,message:sale_price_compliance.constraint_product_price_compliance_threshold_config_mixin_check_price_compliance_positive +#: model:ir.model.constraint,message:sale_price_compliance.constraint_product_product_check_price_compliance_positive +#: model:ir.model.constraint,message:sale_price_compliance.constraint_product_template_check_price_compliance_positive +#: model:ir.model.constraint,message:sale_price_compliance.constraint_res_company_check_price_compliance_positive +msgid "Price Compliance Thresholds Percentage should be positive." +msgstr "El porcentaje de los umbrales de cumplimiento de precios debe ser positivo." + +#. module: sale_price_compliance +#: model:ir.model.constraint,message:sale_price_compliance.constraint_product_category_check_price_compliance_no_gaps +#: model:ir.model.constraint,message:sale_price_compliance.constraint_product_price_compliance_threshold_config_mixin_check_price_compliance_no_gaps +#: model:ir.model.constraint,message:sale_price_compliance.constraint_product_product_check_price_compliance_no_gaps +#: model:ir.model.constraint,message:sale_price_compliance.constraint_product_template_check_price_compliance_no_gaps +#: model:ir.model.constraint,message:sale_price_compliance.constraint_res_company_check_price_compliance_no_gaps +msgid "Price Compliance Thresholds should not have gaps." +msgstr "Los umbrales de cumplimiento de precios no deben tener saltos." + +#. module: sale_price_compliance +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_price_compliance_threshold_tier_mixin__price_compliance_tier +#: model:ir.model.fields,field_description:sale_price_compliance.field_sale_order_line__price_compliance_tier +#: model:ir.model.fields,field_description:sale_price_compliance.field_sale_report__price_compliance_tier +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.view_order_form +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.view_order_product_search +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.view_sales_order_line_filter +msgid "Price Compliance Tier" +msgstr "Nivel de cumplimiento de precios" + +#. module: sale_price_compliance +#. odoo-python +#: code:addons/sale_price_compliance/models/price_compliance_threshold_tier_mixin.py:0 +#, python-format +msgid "Pricelist" +msgstr "Tarifa" + +#. module: sale_price_compliance +#: model:ir.model,name:sale_price_compliance.model_product_template +msgid "Product" +msgstr "Producto" + +#. module: sale_price_compliance +#: model:ir.model,name:sale_price_compliance.model_product_category +msgid "Product Category" +msgstr "Categoría de producto" + +#. module: sale_price_compliance +#: model:ir.model,name:sale_price_compliance.model_product_price_compliance_threshold_config_mixin +msgid "Product Price Compliance Threshold Config Mixin" +msgstr "Mixin de configuración del umbral de cumplimiento de precios de producto" + +#. module: sale_price_compliance +#: model:ir.model,name:sale_price_compliance.model_product_price_compliance_threshold_tier_mixin +msgid "Product Price Compliance Threshold Tier Mixin" +msgstr "Mixin de niveles del umbral de cumplimiento de precios de producto" + +#. module: sale_price_compliance +#: model:ir.model,name:sale_price_compliance.model_product_product +msgid "Product Variant" +msgstr "Variante de producto" + +#. module: sale_price_compliance +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.product_category_form_view +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.product_template_form_view +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.product_variant_easy_edit_view +msgid "Sale Price Compliance Thresholds" +msgstr "Umbrales de cumplimiento de precios de venta" + +#. module: sale_price_compliance +#: model:ir.model,name:sale_price_compliance.model_sale_report +msgid "Sales Analysis Report" +msgstr "Informe de análisis de ventas" + +#. module: sale_price_compliance +#: model:ir.model,name:sale_price_compliance.model_sale_order +msgid "Sales Order" +msgstr "Órdenes de Venta" + +#. module: sale_price_compliance +#: model:ir.model,name:sale_price_compliance.model_sale_order_line +msgid "Sales Order Line" +msgstr "Línea de pedido de ventas" + +#. module: sale_price_compliance +#. odoo-python +#: code:addons/sale_price_compliance/models/sale_order.py:0 +#, python-format +msgid "" +"The order contains lines with non-compliant prices.\n" +"Please review the prices before confirming the order or contact your sales manager for further assistance." +msgstr "" +"El pedido contiene líneas con precios no conformes.\n" +"Por favor, revise los precios antes de confirmar el pedido o contacte con su responsable de ventas para obtener ayuda." + +#. module: sale_price_compliance +#: model:res.groups,comment:sale_price_compliance.price_compliance_threshold_manager +msgid "The user will be able to manage Price Compliance Thresholds." +msgstr "El usuario será capaz de gestionar los umbrales de cumplimiento de precios." + +#. module: sale_price_compliance +#: model:ir.model.fields,help:sale_price_compliance.field_res_config_settings__price_compliance_threshold_t1 +msgid "Threshold for Tier 1 Price Compliance (e.g., High-yield). Prices below this tier are considered fully compliant." +msgstr "" +"Umbral para el Tramo 1 de cumplimiento de precios (ej. Alta Rentabilidad). Los precios por debajo de este nivel se " +"consideran totalmente conformes." + +#. module: sale_price_compliance +#: model:ir.model.fields,help:sale_price_compliance.field_res_config_settings__price_compliance_threshold_t2 +msgid "" +"Threshold for Tier 2 Price Compliance (e.g., Medium-yield). Prices between Tier 1 and Tier 2 are considered moderately " +"compliant." +msgstr "" +"Umbral para el Tramo 2 de cumplimiento de precios (ej. Rentabilidad Media). Los precios entre el Nivel 1 y el Nivel 2 se " +"consideran moderadamente conformes." + +#. module: sale_price_compliance +#: model:ir.model.fields,help:sale_price_compliance.field_res_config_settings__price_compliance_threshold_t3 +msgid "Threshold for Tier 3 Price Compliance (e.g., Low-yield). Prices between Tier 2 and Tier 3 are considered low compliant." +msgstr "" +"Umbral para el Tramo 3 de cumplimiento de precios (ej. Baja Rentabilidad). Los precios entre el Nivel 2 y el Nivel 3 se " +"consideran de cumplimiento mínimo." + +#. module: sale_price_compliance +#. odoo-python +#: code:addons/sale_price_compliance/models/price_compliance_threshold_tier_mixin.py:0 +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_category__price_compliance_threshold_t1 +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_price_compliance_threshold_config_mixin__price_compliance_threshold_t1 +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_product__price_compliance_threshold_t1 +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_template__price_compliance_threshold_t1 +#: model:ir.model.fields,field_description:sale_price_compliance.field_res_company__price_compliance_threshold_t1 +#: model:ir.model.fields,field_description:sale_price_compliance.field_res_config_settings__price_compliance_threshold_t1 +#, python-format +msgid "Tier 1" +msgstr "Tramo 1" + +#. module: sale_price_compliance +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.res_config_settings_view_form +msgid "Tier 1 🟩" +msgstr "Tramo 1 🟩" + +#. module: sale_price_compliance +#. odoo-python +#: code:addons/sale_price_compliance/models/price_compliance_threshold_tier_mixin.py:0 +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_category__price_compliance_threshold_t2 +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_price_compliance_threshold_config_mixin__price_compliance_threshold_t2 +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_product__price_compliance_threshold_t2 +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_template__price_compliance_threshold_t2 +#: model:ir.model.fields,field_description:sale_price_compliance.field_res_company__price_compliance_threshold_t2 +#: model:ir.model.fields,field_description:sale_price_compliance.field_res_config_settings__price_compliance_threshold_t2 +#, python-format +msgid "Tier 2" +msgstr "Tramo 2" + +#. module: sale_price_compliance +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.res_config_settings_view_form +msgid "Tier 2 🟨" +msgstr "Tramo 2 🟨" + +#. module: sale_price_compliance +#. odoo-python +#: code:addons/sale_price_compliance/models/price_compliance_threshold_tier_mixin.py:0 +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_category__price_compliance_threshold_t3 +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_price_compliance_threshold_config_mixin__price_compliance_threshold_t3 +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_product__price_compliance_threshold_t3 +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_template__price_compliance_threshold_t3 +#: model:ir.model.fields,field_description:sale_price_compliance.field_res_company__price_compliance_threshold_t3 +#: model:ir.model.fields,field_description:sale_price_compliance.field_res_config_settings__price_compliance_threshold_t3 +#, python-format +msgid "Tier 3" +msgstr "Tramo 3" + +#. module: sale_price_compliance +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.res_config_settings_view_form +msgid "Tier 3 🟧" +msgstr "Tramo 3 🟧" + +#. module: sale_price_compliance +#. odoo-javascript +#: code:addons/sale_price_compliance/static/src/widgets/price_compliance_level_widget.xml:0 +#, python-format +msgid "Unit Price" +msgstr "Precio unitario" + +#. module: sale_price_compliance +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_category__use_price_compliance_threshold +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_price_compliance_threshold_config_mixin__use_price_compliance_threshold +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_product__use_price_compliance_threshold +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_template__use_price_compliance_threshold +#: model:ir.model.fields,field_description:sale_price_compliance.field_res_company__use_price_compliance_threshold +#: model:ir.model.fields,field_description:sale_price_compliance.field_res_config_settings__use_price_compliance_threshold +msgid "Use Price Compliance Thresholds" +msgstr "Usar umbrales de cumplimiento de precios" + +#. module: sale_price_compliance +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.product_category_search_view +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.product_template_search_view +msgid "Uses Price Compliant Threshold" +msgstr "Usa umbral de cumplimiento de precios" diff --git a/sale_price_compliance/i18n/sale_price_compliance.pot b/sale_price_compliance/i18n/sale_price_compliance.pot new file mode 100644 index 00000000000..36b1207f669 --- /dev/null +++ b/sale_price_compliance/i18n/sale_price_compliance.pot @@ -0,0 +1,322 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_price_compliance +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-02-19 11:09+0000\n" +"PO-Revision-Date: 2026-02-19 11:09+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: sale_price_compliance +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.res_config_settings_view_form +msgid "" +"" +msgstr "" + +#. module: sale_price_compliance +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.res_config_settings_view_form +msgid "" +"(High-yield: Fully " +"compliant)" +msgstr "" + +#. module: sale_price_compliance +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.res_config_settings_view_form +msgid "" +"(Low-yield: Low compliant)" +msgstr "" + +#. module: sale_price_compliance +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.res_config_settings_view_form +msgid "" +"(Medium-yield: Moderately " +"compliant)" +msgstr "" + +#. module: sale_price_compliance +#: model:ir.model,name:sale_price_compliance.model_res_company +msgid "Companies" +msgstr "" + +#. module: sale_price_compliance +#: model:ir.model,name:sale_price_compliance.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: sale_price_compliance +#. odoo-javascript +#: code:addons/sale_price_compliance/static/src/widgets/price_compliance_level_widget.xml:0 +#, python-format +msgid "Discount" +msgstr "" + +#. module: sale_price_compliance +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.res_config_settings_view_form +msgid "" +"Enable Default Price Compliance Thresholds for the Company.\n" +" Prices under last filled Tier are considered non-compliant and will be blocked." +msgstr "" + +#. module: sale_price_compliance +#: model:ir.model.fields,help:sale_price_compliance.field_sale_order_line__price_compliance_data +msgid "Holds additional data related to price compliance calculations." +msgstr "" + +#. module: sale_price_compliance +#: model:ir.model.fields,help:sale_price_compliance.field_product_price_compliance_threshold_tier_mixin__price_compliance_tier +#: model:ir.model.fields,help:sale_price_compliance.field_sale_order_line__price_compliance_tier +#: model:ir.model.fields,help:sale_price_compliance.field_sale_report__price_compliance_tier +msgid "" +"Indicates the Tier of Price Compliance based on the unit price and applied " +"discount compared to defined thresholds." +msgstr "" + +#. module: sale_price_compliance +#: model:res.groups,name:sale_price_compliance.price_compliance_threshold_manager +msgid "Manage Price Compliance" +msgstr "" + +#. module: sale_price_compliance +#. odoo-python +#: code:addons/sale_price_compliance/models/price_compliance_threshold_tier_mixin.py:0 +#, python-format +msgid "Non Compliant" +msgstr "" + +#. module: sale_price_compliance +#. odoo-python +#: code:addons/sale_price_compliance/models/sale_order.py:0 +#, python-format +msgid "Order confirmed with Non Compliant prices by %s." +msgstr "" + +#. module: sale_price_compliance +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.sale_report_view_tree +msgid "PCL" +msgstr "" + +#. module: sale_price_compliance +#: model:ir.model.fields,field_description:sale_price_compliance.field_sale_order_line__price_compliance_data +msgid "Price Compliance Data" +msgstr "" + +#. module: sale_price_compliance +#. odoo-javascript +#: code:addons/sale_price_compliance/static/src/widgets/price_compliance_level_widget.xml:0 +#, python-format +msgid "Price Compliance Thresholds" +msgstr "" + +#. module: sale_price_compliance +#: model:ir.model.constraint,message:sale_price_compliance.constraint_product_category_check_price_compliance_le_1 +#: model:ir.model.constraint,message:sale_price_compliance.constraint_product_price_compliance_threshold_config_mixin_check_price_compliance_le_1 +#: model:ir.model.constraint,message:sale_price_compliance.constraint_product_product_check_price_compliance_le_1 +#: model:ir.model.constraint,message:sale_price_compliance.constraint_product_template_check_price_compliance_le_1 +#: model:ir.model.constraint,message:sale_price_compliance.constraint_res_company_check_price_compliance_le_1 +msgid "" +"Price Compliance Thresholds Percentage should be lower or equal to 100%." +msgstr "" + +#. module: sale_price_compliance +#: model:ir.model.constraint,message:sale_price_compliance.constraint_product_category_check_price_compliance_positive +#: model:ir.model.constraint,message:sale_price_compliance.constraint_product_price_compliance_threshold_config_mixin_check_price_compliance_positive +#: model:ir.model.constraint,message:sale_price_compliance.constraint_product_product_check_price_compliance_positive +#: model:ir.model.constraint,message:sale_price_compliance.constraint_product_template_check_price_compliance_positive +#: model:ir.model.constraint,message:sale_price_compliance.constraint_res_company_check_price_compliance_positive +msgid "Price Compliance Thresholds Percentage should be positive." +msgstr "" + +#. module: sale_price_compliance +#: model:ir.model.constraint,message:sale_price_compliance.constraint_product_category_check_price_compliance_no_gaps +#: model:ir.model.constraint,message:sale_price_compliance.constraint_product_price_compliance_threshold_config_mixin_check_price_compliance_no_gaps +#: model:ir.model.constraint,message:sale_price_compliance.constraint_product_product_check_price_compliance_no_gaps +#: model:ir.model.constraint,message:sale_price_compliance.constraint_product_template_check_price_compliance_no_gaps +#: model:ir.model.constraint,message:sale_price_compliance.constraint_res_company_check_price_compliance_no_gaps +msgid "Price Compliance Thresholds should not have gaps." +msgstr "" + +#. module: sale_price_compliance +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_price_compliance_threshold_tier_mixin__price_compliance_tier +#: model:ir.model.fields,field_description:sale_price_compliance.field_sale_order_line__price_compliance_tier +#: model:ir.model.fields,field_description:sale_price_compliance.field_sale_report__price_compliance_tier +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.view_order_form +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.view_order_product_search +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.view_sales_order_line_filter +msgid "Price Compliance Tier" +msgstr "" + +#. module: sale_price_compliance +#. odoo-python +#: code:addons/sale_price_compliance/models/price_compliance_threshold_tier_mixin.py:0 +#, python-format +msgid "Pricelist" +msgstr "" + +#. module: sale_price_compliance +#: model:ir.model,name:sale_price_compliance.model_product_template +msgid "Product" +msgstr "" + +#. module: sale_price_compliance +#: model:ir.model,name:sale_price_compliance.model_product_category +msgid "Product Category" +msgstr "" + +#. module: sale_price_compliance +#: model:ir.model,name:sale_price_compliance.model_product_price_compliance_threshold_config_mixin +msgid "Product Price Compliance Threshold Config Mixin" +msgstr "" + +#. module: sale_price_compliance +#: model:ir.model,name:sale_price_compliance.model_product_price_compliance_threshold_tier_mixin +msgid "Product Price Compliance Threshold Tier Mixin" +msgstr "" + +#. module: sale_price_compliance +#: model:ir.model,name:sale_price_compliance.model_product_product +msgid "Product Variant" +msgstr "" + +#. module: sale_price_compliance +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.product_category_form_view +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.product_template_form_view +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.product_variant_easy_edit_view +msgid "Sale Price Compliance Thresholds" +msgstr "" + +#. module: sale_price_compliance +#: model:ir.model,name:sale_price_compliance.model_sale_report +msgid "Sales Analysis Report" +msgstr "" + +#. module: sale_price_compliance +#: model:ir.model,name:sale_price_compliance.model_sale_order +msgid "Sales Order" +msgstr "" + +#. module: sale_price_compliance +#: model:ir.model,name:sale_price_compliance.model_sale_order_line +msgid "Sales Order Line" +msgstr "" + +#. module: sale_price_compliance +#. odoo-python +#: code:addons/sale_price_compliance/models/sale_order.py:0 +#, python-format +msgid "" +"The order contains lines with non-compliant prices.\n" +"Please review the prices before confirming the order or contact your sales manager for further assistance." +msgstr "" + +#. module: sale_price_compliance +#: model:res.groups,comment:sale_price_compliance.price_compliance_threshold_manager +msgid "The user will be able to manage Price Compliance Thresholds." +msgstr "" + +#. module: sale_price_compliance +#: model:ir.model.fields,help:sale_price_compliance.field_res_config_settings__price_compliance_threshold_t1 +msgid "" +"Threshold for Tier 1 Price Compliance (e.g., High-yield). Prices below this " +"tier are considered fully compliant." +msgstr "" + +#. module: sale_price_compliance +#: model:ir.model.fields,help:sale_price_compliance.field_res_config_settings__price_compliance_threshold_t2 +msgid "" +"Threshold for Tier 2 Price Compliance (e.g., Medium-yield). Prices between " +"Tier 1 and Tier 2 are considered moderately compliant." +msgstr "" + +#. module: sale_price_compliance +#: model:ir.model.fields,help:sale_price_compliance.field_res_config_settings__price_compliance_threshold_t3 +msgid "" +"Threshold for Tier 3 Price Compliance (e.g., Low-yield). Prices between Tier" +" 2 and Tier 3 are considered low compliant." +msgstr "" + +#. module: sale_price_compliance +#. odoo-python +#: code:addons/sale_price_compliance/models/price_compliance_threshold_tier_mixin.py:0 +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_category__price_compliance_threshold_t1 +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_price_compliance_threshold_config_mixin__price_compliance_threshold_t1 +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_product__price_compliance_threshold_t1 +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_template__price_compliance_threshold_t1 +#: model:ir.model.fields,field_description:sale_price_compliance.field_res_company__price_compliance_threshold_t1 +#: model:ir.model.fields,field_description:sale_price_compliance.field_res_config_settings__price_compliance_threshold_t1 +#, python-format +msgid "Tier 1" +msgstr "" + +#. module: sale_price_compliance +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.res_config_settings_view_form +msgid "Tier 1 🟩" +msgstr "" + +#. module: sale_price_compliance +#. odoo-python +#: code:addons/sale_price_compliance/models/price_compliance_threshold_tier_mixin.py:0 +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_category__price_compliance_threshold_t2 +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_price_compliance_threshold_config_mixin__price_compliance_threshold_t2 +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_product__price_compliance_threshold_t2 +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_template__price_compliance_threshold_t2 +#: model:ir.model.fields,field_description:sale_price_compliance.field_res_company__price_compliance_threshold_t2 +#: model:ir.model.fields,field_description:sale_price_compliance.field_res_config_settings__price_compliance_threshold_t2 +#, python-format +msgid "Tier 2" +msgstr "" + +#. module: sale_price_compliance +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.res_config_settings_view_form +msgid "Tier 2 🟨" +msgstr "" + +#. module: sale_price_compliance +#. odoo-python +#: code:addons/sale_price_compliance/models/price_compliance_threshold_tier_mixin.py:0 +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_category__price_compliance_threshold_t3 +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_price_compliance_threshold_config_mixin__price_compliance_threshold_t3 +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_product__price_compliance_threshold_t3 +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_template__price_compliance_threshold_t3 +#: model:ir.model.fields,field_description:sale_price_compliance.field_res_company__price_compliance_threshold_t3 +#: model:ir.model.fields,field_description:sale_price_compliance.field_res_config_settings__price_compliance_threshold_t3 +#, python-format +msgid "Tier 3" +msgstr "" + +#. module: sale_price_compliance +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.res_config_settings_view_form +msgid "Tier 3 🟧" +msgstr "" + +#. module: sale_price_compliance +#. odoo-javascript +#: code:addons/sale_price_compliance/static/src/widgets/price_compliance_level_widget.xml:0 +#, python-format +msgid "Unit Price" +msgstr "" + +#. module: sale_price_compliance +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_category__use_price_compliance_threshold +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_price_compliance_threshold_config_mixin__use_price_compliance_threshold +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_product__use_price_compliance_threshold +#: model:ir.model.fields,field_description:sale_price_compliance.field_product_template__use_price_compliance_threshold +#: model:ir.model.fields,field_description:sale_price_compliance.field_res_company__use_price_compliance_threshold +#: model:ir.model.fields,field_description:sale_price_compliance.field_res_config_settings__use_price_compliance_threshold +msgid "Use Price Compliance Thresholds" +msgstr "" + +#. module: sale_price_compliance +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.product_category_search_view +#: model_terms:ir.ui.view,arch_db:sale_price_compliance.product_template_search_view +msgid "Uses Price Compliant Threshold" +msgstr "" diff --git a/sale_price_compliance/models/__init__.py b/sale_price_compliance/models/__init__.py new file mode 100644 index 00000000000..521fef89045 --- /dev/null +++ b/sale_price_compliance/models/__init__.py @@ -0,0 +1,9 @@ +from . import price_compliance_threshold_config_mixin +from . import price_compliance_threshold_tier_mixin +from . import res_company +from . import res_config_settings +from . import product_category +from . import product_product +from . import product_template +from . import sale_order_line +from . import sale_order diff --git a/sale_price_compliance/models/price_compliance_threshold_config_mixin.py b/sale_price_compliance/models/price_compliance_threshold_config_mixin.py new file mode 100644 index 00000000000..555f742ad04 --- /dev/null +++ b/sale_price_compliance/models/price_compliance_threshold_config_mixin.py @@ -0,0 +1,123 @@ +# Copyright 2026 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) + +from lxml import etree + +from odoo import api, fields, models + + +class PriceComplianceThresholdConfigMixin(models.AbstractModel): + _name = "product.price.compliance.threshold.config.mixin" + _description = "Product Price Compliance Threshold Config Mixin" + + use_price_compliance_threshold = fields.Boolean( + string="Use Price Compliance Thresholds", + ) + price_compliance_threshold_t1 = fields.Float( + string="Tier 1", + default=0.0, + digits="Discount", + ) + price_compliance_threshold_t2 = fields.Float( + string="Tier 2", + default=0.0, + digits="Discount", + ) + price_compliance_threshold_t3 = fields.Float( + string="Tier 3", + default=0.0, + digits="Discount", + ) + + _sql_constraints = [ + ( + "check_price_compliance_positive", + """CHECK ( + use_price_compliance_threshold IS NOT TRUE + OR ( + price_compliance_threshold_t1 >= 0.0 AND + price_compliance_threshold_t2 >= 0.0 AND + price_compliance_threshold_t3 >= 0.0 + ) + )""", + "Price Compliance Thresholds Percentage should be positive.", + ), + ( + "check_price_compliance_le_1", + """CHECK ( + use_price_compliance_threshold IS NOT TRUE + OR ( + price_compliance_threshold_t1 <= 1.0 AND + price_compliance_threshold_t2 <= 1.0 AND + price_compliance_threshold_t3 <= 1.0 + ) + )""", + "Price Compliance Thresholds Percentage should be lower or equal to 100%.", + ), + ( + "check_price_compliance_no_gaps", + """CHECK ( + use_price_compliance_threshold IS NOT TRUE + OR ( + ( + price_compliance_threshold_t1 > 0.0 OR + price_compliance_threshold_t2 = 0.0 + ) + AND + ( + price_compliance_threshold_t2 > 0.0 OR + price_compliance_threshold_t3 = 0.0 + ) + ) + )""", + "Price Compliance Thresholds should not have gaps.", + ), + ] + + def _get_price_compliance_thresholds(self): + """Returns price compliance thresholds""" + self.ensure_one() + if not self.use_price_compliance_threshold: + return [] + # Check thresholds in order and stop at the first missing one + used_threshold_tiers = [] + for threshold in [ + self.price_compliance_threshold_t1, + self.price_compliance_threshold_t2, + self.price_compliance_threshold_t3, + ]: + if not threshold: + break + used_threshold_tiers.append(threshold) + return used_threshold_tiers + + @api.model + def get_view(self, view_id=None, view_type="form", **options): + """Replaces price_compliance_threshold_t[1,2,3] labels based on tier text + definitions from sale.order.line, which can be customized via system + parameters.""" + result = super().get_view(view_id=view_id, view_type=view_type, **options) + # Check if parameter has been created + if ( + not self.env["ir.config_parameter"] + .sudo() + .get_param( + "sale_price_compliance.price_compliance_selection_tiers_text", + default=False, + ) + ): + return result + # Get tier text selection from sale.order.line (only t1, t2, t3) + tiers_selection = self.env[ + "product.price.compliance.threshold.tier.mixin" + ]._get_price_compliance_selection_tiers_text()[:3] + # Update field labels in XML + doc = etree.XML(result["arch"]) + for tier_key, tier_text in tiers_selection: + field_name = f"price_compliance_threshold_{tier_key}" + for node in doc.xpath(f"//field[@name='{field_name}']"): + node.set("string", tier_text) + if "fields" in result and field_name in result["fields"]: + result["fields"][field_name]["string"] = tier_text + result["arch"] = etree.tostring(doc, encoding="unicode") + return result diff --git a/sale_price_compliance/models/price_compliance_threshold_tier_mixin.py b/sale_price_compliance/models/price_compliance_threshold_tier_mixin.py new file mode 100644 index 00000000000..3be7956d63c --- /dev/null +++ b/sale_price_compliance/models/price_compliance_threshold_tier_mixin.py @@ -0,0 +1,90 @@ +# Copyright 2026 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) + +import logging + +from odoo import _, api, fields, models +from odoo.tools.safe_eval import safe_eval + +_logger = logging.getLogger(__name__) + + +class PriceComplianceThresholdTierMixin(models.AbstractModel): + _name = "product.price.compliance.threshold.tier.mixin" + _description = "Product Price Compliance Threshold Tier Mixin" + + price_compliance_tier = fields.Selection( + selection="_get_price_compliance_selection_tiers", + readonly=True, + help="Indicates the Tier of Price Compliance based on the unit price and " + "applied discount compared to defined thresholds.", + ) + + @api.model + def _get_price_compliance_selection_tiers(self): + """Price compliance selection tiers with icons. + + Override this method to provide custom icons or define any + other logic (visual impairments for example) + """ + return self._get_price_compliance_selection_tiers_icon_color() + + @api.model + def _get_tier_selections(self, param, default_selection): + """Get tier selections using config parameters""" + icp_st = self.env["ir.config_parameter"].sudo().get_param(param) + try: + st_dict = safe_eval(icp_st) + except Exception: + _logger.warning("Wrong parameter value for %s", param) + return default_selection + if st_dict and isinstance(st_dict, dict): + return [ + ("t1", st_dict.get("t1", default_selection[0][1])), + ("t2", st_dict.get("t2", default_selection[1][1])), + ("t3", st_dict.get("t3", default_selection[2][1])), + ( + "non_compliant", + st_dict.get("non_compliant", default_selection[3][1]), + ), + ("pricelist", st_dict.get("pricelist", default_selection[4][1])), + ] + return default_selection + + @api.model + def _get_price_compliance_selection_tiers_icon_color(self): + """Default Price Compliance tiers with icon colors""" + return self._get_tier_selections( + "sale_price_compliance.price_compliance_selection_tiers_icon", + self._get_price_compliance_selection_tiers_icon_color_default(), + ) + + @api.model + def _get_price_compliance_selection_tiers_text(self): + """Price Compliance selection tiers on text""" + return self._get_tier_selections( + "sale_price_compliance.price_compliance_selection_tiers_text", + self._get_price_compliance_selection_tiers_text_default(), + ) + + @api.model + def _get_price_compliance_selection_tiers_icon_color_default(self): + """Default Price Compliance tiers with icon colors (default)""" + return [ + ("t1", "🟩"), + ("t2", "🟨"), + ("t3", "🟧"), + ("non_compliant", "🟥"), + ("pricelist", "🟦"), + ] + + @api.model + def _get_price_compliance_selection_tiers_text_default(self): + """Price Compliance selection tiers on text (default)""" + return [ + ("t1", _("Tier 1")), + ("t2", _("Tier 2")), + ("t3", _("Tier 3")), + ("non_compliant", _("Non Compliant")), + ("pricelist", _("Pricelist")), + ] diff --git a/sale_price_compliance/models/product_category.py b/sale_price_compliance/models/product_category.py new file mode 100644 index 00000000000..92f2165eae7 --- /dev/null +++ b/sale_price_compliance/models/product_category.py @@ -0,0 +1,15 @@ +# Copyright 2026 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) + +from odoo import models + + +class ProductCategory(models.Model): + _name = "product.category" + _inherit = ["product.category", "product.price.compliance.threshold.config.mixin"] + + def _get_price_compliance_thresholds(self): + res = super()._get_price_compliance_thresholds() + if not res and self.parent_id: + res = self.parent_id._get_price_compliance_thresholds() + return res or self.env.company._get_price_compliance_thresholds() diff --git a/sale_price_compliance/models/product_product.py b/sale_price_compliance/models/product_product.py new file mode 100644 index 00000000000..ecec5bd261a --- /dev/null +++ b/sale_price_compliance/models/product_product.py @@ -0,0 +1,15 @@ +# Copyright 2026 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) + +from odoo import models + + +class ProductProduct(models.Model): + _name = "product.product" + _inherit = ["product.product", "product.price.compliance.threshold.config.mixin"] + + def _get_price_compliance_thresholds(self): + return ( + super()._get_price_compliance_thresholds() + or self.product_tmpl_id._get_price_compliance_thresholds() + ) diff --git a/sale_price_compliance/models/product_template.py b/sale_price_compliance/models/product_template.py new file mode 100644 index 00000000000..4fb0d99e16c --- /dev/null +++ b/sale_price_compliance/models/product_template.py @@ -0,0 +1,15 @@ +# Copyright 2026 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) + +from odoo import models + + +class ProductTemplate(models.Model): + _name = "product.template" + _inherit = ["product.template", "product.price.compliance.threshold.config.mixin"] + + def _get_price_compliance_thresholds(self): + return ( + super()._get_price_compliance_thresholds() + or self.categ_id._get_price_compliance_thresholds() + ) diff --git a/sale_price_compliance/models/res_company.py b/sale_price_compliance/models/res_company.py new file mode 100644 index 00000000000..eda0c523159 --- /dev/null +++ b/sale_price_compliance/models/res_company.py @@ -0,0 +1,9 @@ +# Copyright 2026 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) +# +from odoo import models + + +class ResCompany(models.Model): + _name = "res.company" + _inherit = ["res.company", "product.price.compliance.threshold.config.mixin"] diff --git a/sale_price_compliance/models/res_config_settings.py b/sale_price_compliance/models/res_config_settings.py new file mode 100644 index 00000000000..c6d5c52e68f --- /dev/null +++ b/sale_price_compliance/models/res_config_settings.py @@ -0,0 +1,31 @@ +# Copyright 2026 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) +# +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + use_price_compliance_threshold = fields.Boolean( + related="company_id.use_price_compliance_threshold", + readonly=False, + ) + price_compliance_threshold_t1 = fields.Float( + related="company_id.price_compliance_threshold_t1", + readonly=False, + help="Threshold for Tier 1 Price Compliance (e.g., High-yield). " + "Prices below this tier are considered fully compliant.", + ) + price_compliance_threshold_t2 = fields.Float( + related="company_id.price_compliance_threshold_t2", + readonly=False, + help="Threshold for Tier 2 Price Compliance (e.g., Medium-yield). " + "Prices between Tier 1 and Tier 2 are considered moderately compliant.", + ) + price_compliance_threshold_t3 = fields.Float( + related="company_id.price_compliance_threshold_t3", + readonly=False, + help="Threshold for Tier 3 Price Compliance (e.g., Low-yield). " + "Prices between Tier 2 and Tier 3 are considered low compliant.", + ) diff --git a/sale_price_compliance/models/sale_order.py b/sale_price_compliance/models/sale_order.py new file mode 100644 index 00000000000..5be8bdd19d4 --- /dev/null +++ b/sale_price_compliance/models/sale_order.py @@ -0,0 +1,40 @@ +# Copyright 2026 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) + +from odoo import _, exceptions, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + def _action_confirm(self): + res = super()._action_confirm() + self._check_compliant_pricing() + return res + + def _check_compliant_pricing(self): + """Check if lines are in compliant state""" + is_sale_manager = self.env.user.has_group("sales_team.group_sale_manager") + for record in self: + # Check all lines are in compliant state + non_compliant_lines = record.order_line.filtered_domain( + [("price_compliance_tier", "=", "non_compliant")] + ) + if not non_compliant_lines: + continue + # If user is a Sales manager, skip this check + if is_sale_manager: + record.message_post( + body=_( + "Order confirmed with Non Compliant prices by %s.", + self.env.user.name, + ) + ) + continue + raise exceptions.UserError( + _( + "The order contains lines with non-compliant prices.\n" + "Please review the prices before confirming the order or " + "contact your sales manager for further assistance." + ) + ) diff --git a/sale_price_compliance/models/sale_order_line.py b/sale_price_compliance/models/sale_order_line.py new file mode 100644 index 00000000000..41767427aa7 --- /dev/null +++ b/sale_price_compliance/models/sale_order_line.py @@ -0,0 +1,206 @@ +# Copyright 2026 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) + +import logging + +from odoo import api, fields, models +from odoo.tools import float_compare, float_round + +_logger = logging.getLogger(__name__) + + +class SaleOrderLine(models.Model): + _name = "sale.order.line" + _inherit = ["sale.order.line", "product.price.compliance.threshold.tier.mixin"] + + price_compliance_tier = fields.Selection( + compute="_compute_price_compliance_tier", + store=True, + ) + price_compliance_data = fields.Json( + compute="_compute_price_compliance_tier", + store=True, + readonly=True, + help="Holds additional data related to price compliance calculations.", + ) + + def _get_price_compliance_thresholds(self): + """Price compliance thresholds for this sale line""" + self.ensure_one() + return self.product_id._get_price_compliance_thresholds() + + def _get_price_compliance_data(self, precision_digits): + """Gets a full dictionary with all calculated information about + price compliance for this product""" + self.ensure_one() + + def _precision_rounder(val): + return round( + float_round(val, precision_digits=precision_digits), + # Post round to ensure only display X decimals + precision_digits, + ) + + # Base price in sale order currency + base_price = self.product_id.list_price + if self.product_id.currency_id != self.currency_id: + base_price = self.product_id.currency_id._convert( + base_price, + self.currency_id, + self.company_id, + self._get_order_date(), + ) + thresholds = self._get_price_compliance_thresholds() + selection_tiers_map = dict(self._get_price_compliance_selection_tiers()) + selection_tiers_text_map = dict( + self._get_price_compliance_selection_tiers_text() + ) + common_data_values = { + "currency_symbol": self.currency_id.symbol, + "product_base_uom": self.product_id.uom_id.name, + "precision_digits": precision_digits, + "extra_description": "", + } + # Prepare data per tier + price_compliance_data = [] + for idx_tier, current_threshold_disc in enumerate(thresholds, 1): + current_threshold_price = _precision_rounder( + base_price * (1.0 - current_threshold_disc), + ) + if idx_tier == 1: + price_compliance_data.append( + { + "tier": f"t{idx_tier}", + "discount": (0.0, current_threshold_disc), + "price": (current_threshold_price, base_price), + "display": ( + selection_tiers_map[f"t{idx_tier}"], + selection_tiers_text_map[f"t{idx_tier}"], + ), + **common_data_values, + } + ) + continue + # -2 because idx_tier is 1-based + prev_threshold_disc = thresholds[idx_tier - 2] + prev_threshold_price = _precision_rounder( + base_price * (1.0 - prev_threshold_disc), + ) + price_compliance_data.append( + { + "tier": f"t{idx_tier}", + "discount": (prev_threshold_disc, current_threshold_disc), + "price": (current_threshold_price, prev_threshold_price), + "display": ( + selection_tiers_map[f"t{idx_tier}"], + selection_tiers_text_map[f"t{idx_tier}"], + ), + **common_data_values, + } + ) + if not thresholds: + # If no thresholds are defined, return empty data + return price_compliance_data + + # Add non_compliant tier + last_threshold_disc = thresholds[-1] + last_threshold_price = _precision_rounder( + base_price * (1.0 - last_threshold_disc), + ) + price_compliance_data.append( + { + "tier": "non_compliant", + "discount": (last_threshold_disc, 1.0), + "price": (0.0, last_threshold_price), + "display": ( + selection_tiers_map["non_compliant"], + selection_tiers_text_map["non_compliant"], + ), + **common_data_values, + } + ) + + # Add Pricelist pricelist item data if any + if self.pricelist_item_id: + pricelist_price = self._get_pricelist_price() + pricelist_description = self.pricelist_item_id.price + price_compliance_data.append( + { + "tier": "pricelist", + "discount": (0.0, 0.0), + "price": (pricelist_price, pricelist_price), + "display": ( + selection_tiers_map["pricelist"], + selection_tiers_text_map["pricelist"], + ), + **common_data_values, + **{"extra_description": pricelist_description}, + } + ) + return price_compliance_data + + @api.depends( + "price_unit", "product_uom", "product_id", "discount", "pricelist_item_id" + ) + def _compute_price_compliance_tier(self): + """Set price compliance tier""" + self.price_compliance_tier = False + self.price_compliance_data = None + precision_digits = self.env["decimal.precision"].precision_get("Product Price") + for line in self: + # 1. Line section/note, no product assigned + if line.display_type or not line.product_id: + continue + # 2. Prepare Widget Display + price_compliance_data = line._get_price_compliance_data(precision_digits) + line.price_compliance_data = price_compliance_data + if not price_compliance_data: + # Nothing to check. No thresholds defined for this product + continue + # 3. Get the UoM factor to convert line price to product base UoM + uom_factor = line.product_uom._compute_quantity( + qty=1.0, + to_unit=line.product_id.uom_id, + round=False, + ) + # 4. Convert line price_unit to product base UoM and apply discount + discount_line_unit_price_on_base_uom = float_round( + (line.price_unit / uom_factor) * (1 - line.discount / 100.0), + precision_digits=precision_digits, + ) + # 5. Apply standard compliance logic + # Check negative prices + if ( + float_compare( + discount_line_unit_price_on_base_uom, + 0.0, + precision_digits=precision_digits, + ) + < 0 + ): + line.price_compliance_tier = "non_compliant" + continue + # Default to t1 in case price is higher than all thresholds + compliant_tier = "t1" + for compliance_data in price_compliance_data: + min_price, max_price = compliance_data["price"] + if min_price <= discount_line_unit_price_on_base_uom <= max_price: + compliant_tier = compliance_data["tier"] + break + + # 6. Only check pricelist price equality if non compliant + if compliant_tier == "non_compliant" and line.pricelist_item_id: + # Consider pricelist compliance if the price after discount is + # betweeen pricelist price and last compliant tier price. + if ( + float_compare( + discount_line_unit_price_on_base_uom, + line._get_pricelist_price(), + precision_digits=precision_digits, + ) + >= 0 + ): + # If the final price after discount is equal to the + # pricelist price, we consider pricelist compliance. + compliant_tier = "pricelist" + line.price_compliance_tier = compliant_tier diff --git a/sale_price_compliance/pyproject.toml b/sale_price_compliance/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/sale_price_compliance/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/sale_price_compliance/readme/CONTEXT.md b/sale_price_compliance/readme/CONTEXT.md new file mode 100644 index 00000000000..516cc2675e9 --- /dev/null +++ b/sale_price_compliance/readme/CONTEXT.md @@ -0,0 +1,5 @@ +This module was developed because sometimes commercial users are incentivized +to sell at prices that are more beneficial to the company. + +It will be useful if you want to visually show your commercial users +whether the price they are selling at is in line with the company's pricing policy. diff --git a/sale_price_compliance/readme/CONTRIBUTORS.md b/sale_price_compliance/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..290347f4260 --- /dev/null +++ b/sale_price_compliance/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Eduardo de Miguel ([Moduon](https://www.moduon.team/)) diff --git a/sale_price_compliance/readme/DESCRIPTION.md b/sale_price_compliance/readme/DESCRIPTION.md new file mode 100644 index 00000000000..7e6b5fd229f --- /dev/null +++ b/sale_price_compliance/readme/DESCRIPTION.md @@ -0,0 +1,43 @@ +This module extends the sales pricing functionality to display a color code +based on the price at which products are being sold. + +Managing Price Compliance Thresholds can be done by adding *Manage Price Compliance* + group to any user. +Compliance Tiers can be filtered on sale reports. + +You can use up to 3 different price compliance tiers for products, +categories or for all company. + +You can customize the texts and icons of the tiers via the System Parameters. + +Labels of the tier fields will change according to `sale_price_compliance.price_compliance_selection_tiers_text` System Parameter in views. + +This functionality only applies to Sales. + +A sale with a line with Non Compliant price (doesn't fit in any defined tiers) can't be confirmed. +Only Sale Administrators can validate this sales and a message will be posted. + +Price Compliance thresholds are selected in this order: Product > Product Category > Company + +Each Tier represents the maximun discount applied to the Price of the Product to achieve the target tier. + +**Color Compliance Tiers** +- Tier 1 🟩: High-yield (Fully compliant) +- Tier 2 🟨: Medium-yield (Moderately compliant) +- Tier 3 🟧: Low-yield (Low compliant) +- Non Compliant 🟥: Non Compliant price (blocked) +- Pricelist 🟦: Pricelist has been used and it's price is between pricelist and last compliant tier. + +**Information Display on Price Compliant Tiers** +Includes a popup to display detailed information about the price ranges +within each price compliance tiers. + +Also displays Product Base UoM and Product List Price in Sale Currency in the Top Right corner of the Popup. + +![Price Compliance Widget](../static/img/price_compliance_widget.png) + +![Price Compliance Sale Line Form](../static/img/price_compliance_sale_line_form.png) + +![Price Compliance Sale Line List](../static/img/price_compliance_sale_line_list.png) + +![Price Compliance Sale Line Kanban](../static/img/price_compliance_sale_line_kanban.png) diff --git a/sale_price_compliance/readme/ROADMAP.md b/sale_price_compliance/readme/ROADMAP.md new file mode 100644 index 00000000000..7f6123196f1 --- /dev/null +++ b/sale_price_compliance/readme/ROADMAP.md @@ -0,0 +1 @@ +- Widget is not shown properly if the order is not saved and refreshed. diff --git a/sale_price_compliance/readme/USAGE.md b/sale_price_compliance/readme/USAGE.md new file mode 100644 index 00000000000..e6a98f18f1d --- /dev/null +++ b/sale_price_compliance/readme/USAGE.md @@ -0,0 +1,94 @@ +To use this module, you need to: + +**Configure Parameter Text: Change the default price compliance tiers texts** +1. Go to System Parameters. +1. Create a new parameter with key `sale_price_compliance.price_compliance_selection_tiers_text`. +1. Write a *dictionary* with the values for tiers that you want to change it's default text. +1. Save the parameter. +1. Create a new parameter with key `sale_price_compliance.price_compliance_selection_tiers_icon`. +1. Write a *dictionary* with the values for tiers that you want to change it's default icon. +1. Save the parameter. + +Example: + +Param *sale_price_compliance.price_compliance_selection_tiers_text*: + +{'t1': 'Gold', 't2': 'Silver', 't3': 'Bronze', 'pricelist': 'Draw'} + +Param *sale_price_compliance.price_compliance_selection_tiers_icon*: + +{'t1': '🥇', 't2': '🥈', 't3': '🥉', 'non_compliant': '⛔️', 'pricelist': '🤝'} + +Resulting texts and icons will be: + +- Gold: 🥇 +- Silver: 🥈 +- Bronze: 🥉 +- Non Compliant: ⛔️ +- Draw: 🤝 + +Available keys to be used in the dictionary are: + +- t1: Tier 1 +- t2: Tier 2 +- t3: Tier 3 +- non_compliant: Non Compliant +- pricelist: Pricelist + +**Configure: Product Price Compliance Thresholds configuration** +1. Go to Sales > Products > Products > Select one > Sales tab +1. Enable Use Price Compliance Thresholds under Sale Price Compliance Thresholds +1. Fill from 1 to 3 tiers that you want to use in this product. + +Example: +Tier 1: 10%, Tier 2: 20%, Tier 3: 30%. (Use all tiers) + +**Configure: Product Category Price Compliance Thresholds configuration** +1. Go to Sales > Configuration > Products > Product Categories > Select one +1. Enable Use Price Compliance Thresholds under Sale Price Compliance Thresholds +1. Fill from 1 to 3 tiers that you want to use in this category. + +Example: +Tier 1: 15%, Tier 2: 25%, Tier 3: 0%. (Don't use tier 3) + +**Configure: Company Price Compliance Thresholds configuration** +1. Go to Sales > Configuration > Settings +1. Enable Use Price Compliance Thresholds under Pricing section +1. Fill from 1 to 3 tiers that you want to use for the company. + +Example: +Tier 1: 30%, Tier 2: 0%, Tier 3: 0%. (Don't use tier 2 and 3) + + +**Sale: As a Salesman user** +1. Create a new sale and fill contact field +1. Add a new line with a product that has been configured to use Price Compliance +1. Click on the 🟩, 🟨, 🟧, 🟥 or 🟦 icon at the start of the line. +1. You will see a popup with useful information about the Tier ranges. +1. Play with it and then set a Non Compliant price (change price or discount to achieve this). +1. Try to confirm the Sale and see the error. + +**Sale: As a Sales Administrator** +1. Confirm the previous Sale. +1. See the message on the chatter. + + +**Reporting: Sales report** +1. Go to Sales > Reporting > Sales +1. Select bar chart or Pivot view +1. Group by Price Compliance Level + +**Reporting: Salesperson report** +1. Go to Sales > Reporting > Salesperson +1. Select Bar chart + stacked or Pivot view +1. Group by Price Compliance Level + +**Reporting: Product report** +1. Go to Sales > Reporting > Product +1. Select Pie chart + stacked or Pivot view +1. Group by Price Compliance Level + +**Reporting: Customers report** +1. Go to Sales > Reporting > Customers +1. Select Bar chart + stacked or Pivot view +1. Group by Price Compliance Level diff --git a/sale_price_compliance/report/__init__.py b/sale_price_compliance/report/__init__.py new file mode 100644 index 00000000000..cd23411b84d --- /dev/null +++ b/sale_price_compliance/report/__init__.py @@ -0,0 +1 @@ +from . import sale_report diff --git a/sale_price_compliance/report/sale_report.py b/sale_price_compliance/report/sale_report.py new file mode 100644 index 00000000000..8cf9da898e4 --- /dev/null +++ b/sale_price_compliance/report/sale_report.py @@ -0,0 +1,29 @@ +# Copyright 2026 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) + +from odoo import api, fields, models + + +class SaleReport(models.Model): + _inherit = "sale.report" + + @api.model + def _get_price_compliance_tiers(self): + return self.env["sale.order.line"]._get_price_compliance_selection_tiers_text() + + price_compliance_tier = fields.Selection( + selection="_get_price_compliance_tiers", + help="Indicates the Tier of Price Compliance based on the unit price and " + "applied discount compared to defined thresholds.", + ) + + def _select_additional_fields(self): + res = super()._select_additional_fields() + res["price_compliance_tier"] = "l.price_compliance_tier" + return res + + def _group_by_sale(self): + res = super()._group_by_sale() + res += """, + l.price_compliance_tier""" + return res diff --git a/sale_price_compliance/report/sale_report_view.xml b/sale_price_compliance/report/sale_report_view.xml new file mode 100644 index 00000000000..c1cec366604 --- /dev/null +++ b/sale_price_compliance/report/sale_report_view.xml @@ -0,0 +1,31 @@ + + + + + sale.report.view.pcl.list + sale.report + + + + + + + + + + sale.report.pcl.search + sale.report + + + + + + + + diff --git a/sale_price_compliance/security/sale_price_compliance.xml b/sale_price_compliance/security/sale_price_compliance.xml new file mode 100644 index 00000000000..38b4133ce77 --- /dev/null +++ b/sale_price_compliance/security/sale_price_compliance.xml @@ -0,0 +1,12 @@ + + + + + Manage Price Compliance + + The user will be able to manage Price Compliance Thresholds. + + diff --git a/sale_price_compliance/static/description/icon.png b/sale_price_compliance/static/description/icon.png new file mode 100644 index 00000000000..3a0328b516c Binary files /dev/null and b/sale_price_compliance/static/description/icon.png differ diff --git a/sale_price_compliance/static/description/index.html b/sale_price_compliance/static/description/index.html new file mode 100644 index 00000000000..a068e151e1a --- /dev/null +++ b/sale_price_compliance/static/description/index.html @@ -0,0 +1,597 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Sale Price Compliance

+ +

Alpha License: LGPL-3 OCA/sale-workflow Translate me on Weblate Try me on Runboat

+

This module extends the sales pricing functionality to display a color +code based on the price at which products are being sold.

+

Managing Price Compliance Thresholds can be done by adding Manage Price +Compliance group to any user. Compliance Tiers can be filtered on sale +reports.

+

You can use up to 3 different price compliance tiers for products, +categories or for all company.

+

You can customize the texts and icons of the tiers via the System +Parameters.

+

Labels of the tier fields will change according to +sale_price_compliance.price_compliance_selection_tiers_text System +Parameter in views.

+

This functionality only applies to Sales.

+

A sale with a line with Non Compliant price (doesn’t fit in any defined +tiers) can’t be confirmed. Only Sale Administrators can validate this +sales and a message will be posted.

+

Price Compliance thresholds are selected in this order: Product > +Product Category > Company

+

Each Tier represents the maximun discount applied to the Price of the +Product to achieve the target tier.

+

Color Compliance Tiers

+
    +
  • Tier 1 🟩: High-yield (Fully compliant)
  • +
  • Tier 2 🟨: Medium-yield (Moderately compliant)
  • +
  • Tier 3 🟧: Low-yield (Low compliant)
  • +
  • Non Compliant 🟥: Non Compliant price (blocked)
  • +
  • Pricelist 🟦: Pricelist has been used and it’s price is between +pricelist and last compliant tier.
  • +
+

Information Display on Price Compliant Tiers Includes a popup to +display detailed information about the price ranges within each price +compliance tiers.

+

Also displays Product Base UoM and Product List Price in Sale Currency +in the Top Right corner of the Popup.

+

Price Compliance Widget

+

Price Compliance Sale Line Form

+

Price Compliance Sale Line List

+

Price Compliance Sale Line Kanban

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Use Cases / Context

+

This module was developed because sometimes commercial users are +incentivized to sell at prices that are more beneficial to the company.

+

It will be useful if you want to visually show your commercial users +whether the price they are selling at is in line with the company’s +pricing policy.

+
+
+

Usage

+

To use this module, you need to:

+

Configure Parameter Text: Change the default price compliance tiers +texts

+
    +
  1. Go to System Parameters.
  2. +
  3. Create a new parameter with key +sale_price_compliance.price_compliance_selection_tiers_text.
  4. +
  5. Write a dictionary with the values for tiers that you want to +change it’s default text.
  6. +
  7. Save the parameter.
  8. +
  9. Create a new parameter with key +sale_price_compliance.price_compliance_selection_tiers_icon.
  10. +
  11. Write a dictionary with the values for tiers that you want to +change it’s default icon.
  12. +
  13. Save the parameter.
  14. +
+

Example:

+

Param sale_price_compliance.price_compliance_selection_tiers_text:

+

{‘t1’: ‘Gold’, ‘t2’: ‘Silver’, ‘t3’: ‘Bronze’, ‘pricelist’: ‘Draw’}

+

Param sale_price_compliance.price_compliance_selection_tiers_icon:

+

{‘t1’: ‘🥇’, ‘t2’: ‘🥈’, ‘t3’: ‘🥉’, ‘non_compliant’: ‘⛔️’, ‘pricelist’: +‘🤝’}

+

Resulting texts and icons will be:

+
    +
  • Gold: 🥇
  • +
  • Silver: 🥈
  • +
  • Bronze: 🥉
  • +
  • Non Compliant: ⛔️
  • +
  • Draw: 🤝
  • +
+

Available keys to be used in the dictionary are:

+
    +
  • t1: Tier 1
  • +
  • t2: Tier 2
  • +
  • t3: Tier 3
  • +
  • non_compliant: Non Compliant
  • +
  • pricelist: Pricelist
  • +
+

Configure: Product Price Compliance Thresholds configuration

+
    +
  1. Go to Sales > Products > Products > Select one > Sales tab
  2. +
  3. Enable Use Price Compliance Thresholds under Sale Price Compliance +Thresholds
  4. +
  5. Fill from 1 to 3 tiers that you want to use in this product.
  6. +
+

Example: Tier 1: 10%, Tier 2: 20%, Tier 3: 30%. (Use all tiers)

+

Configure: Product Category Price Compliance Thresholds +configuration

+
    +
  1. Go to Sales > Configuration > Products > Product Categories > Select +one
  2. +
  3. Enable Use Price Compliance Thresholds under Sale Price Compliance +Thresholds
  4. +
  5. Fill from 1 to 3 tiers that you want to use in this category.
  6. +
+

Example: Tier 1: 15%, Tier 2: 25%, Tier 3: 0%. (Don’t use tier 3)

+

Configure: Company Price Compliance Thresholds configuration

+
    +
  1. Go to Sales > Configuration > Settings
  2. +
  3. Enable Use Price Compliance Thresholds under Pricing section
  4. +
  5. Fill from 1 to 3 tiers that you want to use for the company.
  6. +
+

Example: Tier 1: 30%, Tier 2: 0%, Tier 3: 0%. (Don’t use tier 2 and 3)

+

Sale: As a Salesman user

+
    +
  1. Create a new sale and fill contact field
  2. +
  3. Add a new line with a product that has been configured to use Price +Compliance
  4. +
  5. Click on the 🟩, 🟨, 🟧, 🟥 or 🟦 icon at the start of the line.
  6. +
  7. You will see a popup with useful information about the Tier ranges.
  8. +
  9. Play with it and then set a Non Compliant price (change price or +discount to achieve this).
  10. +
  11. Try to confirm the Sale and see the error.
  12. +
+

Sale: As a Sales Administrator

+
    +
  1. Confirm the previous Sale.
  2. +
  3. See the message on the chatter.
  4. +
+

Reporting: Sales report

+
    +
  1. Go to Sales > Reporting > Sales
  2. +
  3. Select bar chart or Pivot view
  4. +
  5. Group by Price Compliance Level
  6. +
+

Reporting: Salesperson report

+
    +
  1. Go to Sales > Reporting > Salesperson
  2. +
  3. Select Bar chart + stacked or Pivot view
  4. +
  5. Group by Price Compliance Level
  6. +
+

Reporting: Product report

+
    +
  1. Go to Sales > Reporting > Product
  2. +
  3. Select Pie chart + stacked or Pivot view
  4. +
  5. Group by Price Compliance Level
  6. +
+

Reporting: Customers report

+
    +
  1. Go to Sales > Reporting > Customers
  2. +
  3. Select Bar chart + stacked or Pivot view
  4. +
  5. Group by Price Compliance Level
  6. +
+
+
+

Known issues / Roadmap

+
    +
  • Widget is not shown properly if the order is not saved and refreshed.
  • +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Moduon
  • +
+
+
+

Contributors

+
    +
  • Eduardo de Miguel (Moduon)
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainers:

+

Shide rafaelbn

+

This module is part of the OCA/sale-workflow project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/sale_price_compliance/static/img/price_compliance_sale_line_form.png b/sale_price_compliance/static/img/price_compliance_sale_line_form.png new file mode 100644 index 00000000000..0b63706e6fd Binary files /dev/null and b/sale_price_compliance/static/img/price_compliance_sale_line_form.png differ diff --git a/sale_price_compliance/static/img/price_compliance_sale_line_kanban.png b/sale_price_compliance/static/img/price_compliance_sale_line_kanban.png new file mode 100644 index 00000000000..85872bc3997 Binary files /dev/null and b/sale_price_compliance/static/img/price_compliance_sale_line_kanban.png differ diff --git a/sale_price_compliance/static/img/price_compliance_sale_line_list.png b/sale_price_compliance/static/img/price_compliance_sale_line_list.png new file mode 100644 index 00000000000..8e77498249a Binary files /dev/null and b/sale_price_compliance/static/img/price_compliance_sale_line_list.png differ diff --git a/sale_price_compliance/static/img/price_compliance_widget.png b/sale_price_compliance/static/img/price_compliance_widget.png new file mode 100644 index 00000000000..5b0b8e5a72a Binary files /dev/null and b/sale_price_compliance/static/img/price_compliance_widget.png differ diff --git a/sale_price_compliance/static/src/widgets/price_compliance_level_widget.esm.js b/sale_price_compliance/static/src/widgets/price_compliance_level_widget.esm.js new file mode 100644 index 00000000000..29df46d57a4 --- /dev/null +++ b/sale_price_compliance/static/src/widgets/price_compliance_level_widget.esm.js @@ -0,0 +1,79 @@ +/** @odoo-module **/ + +import {registry} from "@web/core/registry"; +import {usePopover} from "@web/core/popover/popover_hook"; +import {useService} from "@web/core/utils/hooks"; + +const {Component, EventBus, onWillRender} = owl; + +export class PriceComplianceTierPopover extends Component { + setup() { + this.actionService = useService("action"); + } +} + +PriceComplianceTierPopover.template = + "sale_price_compliance.PriceComplianceTierPopover"; +PriceComplianceTierPopover.position = "right"; + +export class PriceComplianceTierWidget extends Component { + setup() { + this.bus = new EventBus(); + this.popover = usePopover(); + this.closePopover = null; + this.calcData = {}; + onWillRender(() => { + this.updateCalcData(); + }); + } + + updateCalcData() { + const {data} = this.props.record; + // Value to display on the widget + const foundElement = (data.price_compliance_data || []).find( + (element) => element.tier === data.price_compliance_tier + ); + this.calcData.price_compliance_tier_display = foundElement + ? foundElement.display[0] + : null; + this.calcData.currency_symbol = foundElement + ? foundElement.currency_symbol + : null; + this.calcData.product_base_uom = foundElement + ? foundElement.product_base_uom + : null; + // Get Tier 1 compliance data + const foundL1Element = (data.price_compliance_data || []).find( + (element) => element.tier === "t1" + ); + this.calcData.product_price_in_base_uom = foundL1Element + ? foundL1Element.price[1] + : null; + } + + showPopup(ev) { + this.updateCalcData(); + this.closePopover = this.popover.add( + ev.currentTarget, + this.constructor.components.Popover, + { + bus: this.bus, + record: this.props.record, + calcData: this.calcData, + }, + { + position: "top", + // Ensure popup full width on kanban + popoverClass: "mw-100", + } + ); + this.bus.addEventListener("close-popover", this.closePopover); + } +} + +PriceComplianceTierWidget.components = {Popover: PriceComplianceTierPopover}; +PriceComplianceTierWidget.template = "sale_price_compliance.PriceComplianceTier"; + +registry + .category("view_widgets") + .add("price_compliance_tier_widget", PriceComplianceTierWidget); diff --git a/sale_price_compliance/static/src/widgets/price_compliance_level_widget.xml b/sale_price_compliance/static/src/widgets/price_compliance_level_widget.xml new file mode 100644 index 00000000000..9cbdaa0937c --- /dev/null +++ b/sale_price_compliance/static/src/widgets/price_compliance_level_widget.xml @@ -0,0 +1,119 @@ + + + diff --git a/sale_price_compliance/tests/__init__.py b/sale_price_compliance/tests/__init__.py new file mode 100644 index 00000000000..3aa39994b40 --- /dev/null +++ b/sale_price_compliance/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_sale_price_compliance_constraints +from . import test_sale_price_compliance diff --git a/sale_price_compliance/tests/test_sale_price_compliance.py b/sale_price_compliance/tests/test_sale_price_compliance.py new file mode 100644 index 00000000000..4d07eef6115 --- /dev/null +++ b/sale_price_compliance/tests/test_sale_price_compliance.py @@ -0,0 +1,168 @@ +# Copyright 2026 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) + +from odoo.fields import Command +from odoo.tests.common import TransactionCase + + +class TestPriceCompliance(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner = cls.env["res.partner"].create({"name": "SPC Partner"}) + # Enable groups Pricelist and discount groups + cls.env.user.groups_id += cls.env.ref("product.group_product_pricelist") + cls.env.user.groups_id += cls.env.ref("product.group_discount_per_so_line") + # Company + cls.env.company.write( + { + "use_price_compliance_threshold": True, + "price_compliance_threshold_t1": 0.05, + "price_compliance_threshold_t2": 0.10, + "price_compliance_threshold_t3": 0.15, + } + ) + # Category + cls.product_category = cls.env["product.category"].create( + { + "name": "SPC Category", + "use_price_compliance_threshold": True, + "price_compliance_threshold_t1": 0.20, + "price_compliance_threshold_t2": 0.25, + "price_compliance_threshold_t3": 0.30, + } + ) + # Product: Product with thresholds directly + cls.product_pct = cls.env["product.product"].create( + { + "name": "PCT Product", + "detailed_type": "service", + "list_price": 10.0, + "use_price_compliance_threshold": True, + "price_compliance_threshold_t1": 0.35, + "price_compliance_threshold_t2": 0.40, + "price_compliance_threshold_t3": 0.45, + } + ) + # Product: Product with company thresholds + cls.product_company_pct = cls.env["product.product"].create( + { + "name": "PCT Company Product", + "detailed_type": "service", + "list_price": 10.0, + } + ) + # Product: Product with category thresholds + cls.product_categ_pct = cls.env["product.product"].create( + { + "name": "PCT Categ Product", + "detailed_type": "service", + "list_price": 10.0, + "categ_id": cls.product_category.id, + } + ) + # Product: Product with category thresholds and pricelist + cls.product_pricelist_pct = cls.env["product.product"].create( + { + "name": "PCT Pricelist Product", + "detailed_type": "service", + "list_price": 10.0, + "categ_id": cls.product_category.id, + } + ) + # Pricelist + cls.pricelist = ( + cls.env["product.pricelist"] + .with_company(cls.env.company) + .create( + { + "name": "PCT Pricelist", + "currency_id": cls.env.company.currency_id.id, + "item_ids": [ + Command.create( + { + "compute_price": "percentage", + "percent_price": 50.0, + "applied_on": "0_product_variant", + "product_id": cls.product_pricelist_pct.id, + } + ) + ], + } + ) + ) + + def _create_sale_order_line(self, product, pricelist=False): + sale = self.env["sale.order"].create( + { + "partner_id": self.partner.id, + "order_line": [Command.create({"product_id": product.id})], + } + ) + if pricelist: + sale.write({"pricelist_id": pricelist.id}) + sale._recompute_prices() + return sale.order_line[0] + + def _test_price_compliance_discount_tiers(self, sale_line, **tier_data): + """Test tiers all at once. + + Tier data example: + { + "t1": (0.0, 5.0), + "t2": (5.1, 10.0), + "t3": (10.1, 15.0), + "non_compliant": (15.1,), + "pricelist": (0.0,), + } + + :param sale_line: Sale Order Line record + :param tier_data: Data containing tiers to match and discounts. + """ + for tier_name, tier_values in tier_data.items(): + for tier_val in tier_values: + with self.subTest(tier=tier_name, discount=tier_val): + sale_line.write({"discount": tier_val}) + self.assertEqual(sale_line.price_compliance_tier, tier_name) + + def test_product_threshold(self): + """Test product thresholds""" + self._test_price_compliance_discount_tiers( + self._create_sale_order_line(self.product_pct), + t1=(0.0, 35.0), + t2=(35.1, 40.0), + t3=(40.1, 45.0), + non_compliant=(45.1,), + ) + + def test_product_company_threshold(self): + """Test product company thresholds""" + self._test_price_compliance_discount_tiers( + self._create_sale_order_line(self.product_company_pct), + t1=(0.0, 5.0), + t2=(5.1, 10.0), + t3=(10.1, 15.0), + non_compliant=(15.1,), + ) + + def test_product_category_threshold(self): + """Test product category thresholds""" + self._test_price_compliance_discount_tiers( + self._create_sale_order_line(self.product_categ_pct), + t1=(0.0, 20.0), + t2=(20.1, 25.0), + t3=(25.1, 30.0), + non_compliant=(30.1,), + ) + + def test_product_pricelist_threshold(self): + """Test product pricelist thresholds""" + # Because pricelist applies 50% discount directly inside the price, + # any discount should go out of Tier ranges and be Non Compliant + self._test_price_compliance_discount_tiers( + self._create_sale_order_line( + self.product_pricelist_pct, pricelist=self.pricelist + ), + pricelist=(0.0,), # Initial price + non_compliant=(1.0,), # Any extra discount + ) diff --git a/sale_price_compliance/tests/test_sale_price_compliance_constraints.py b/sale_price_compliance/tests/test_sale_price_compliance_constraints.py new file mode 100644 index 00000000000..18039b7f9fa --- /dev/null +++ b/sale_price_compliance/tests/test_sale_price_compliance_constraints.py @@ -0,0 +1,174 @@ +# Copyright 2026 Moduon Team S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) + +from psycopg2 import IntegrityError + +from odoo.tests.common import TransactionCase, tagged +from odoo.tools import mute_logger + + +@tagged("post_install", "-at_install") +class TestPriceComplianceConstraints(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + # Create a clean company for testing + cls.company = cls.env["res.company"].create( + { + "name": "Test Company SQL Constraints", + } + ) + + def test_valid_cases(self): + """Success Case: Positive values and no gaps.""" + # Case 1: All values are zero [0, 0, 0] + self.company.write( + { + "use_price_compliance_threshold": True, + "price_compliance_threshold_t1": 0.0, + "price_compliance_threshold_t2": 0.0, + "price_compliance_threshold_t3": 0.0, + } + ) + # Case 2: T1 has value, others are zero (No gaps) [10%, 0%, 0%] + self.company.write({"price_compliance_threshold_t1": 0.1}) + # Case 3: T1 and T2 have values (No gaps) [10%, 5%, 0%] + self.company.write({"price_compliance_threshold_t2": 0.05}) + # Case 4: T1 and T2 and T3 have values (No gaps) [10%, 5%, 1%] + self.company.write({"price_compliance_threshold_t3": 0.01}) + + @mute_logger("odoo.sql_db") + def test_constraint_positive(self): + """Expected Failure: Negative numbers are not allowed if use thresholds.""" + with self.assertRaises(IntegrityError): + self.company.write( + { + "use_price_compliance_threshold": True, + "price_compliance_threshold_t1": -0.05, # Negative value + "price_compliance_threshold_t2": 0.0, + "price_compliance_threshold_t3": 0.0, + } + ) + + @mute_logger("odoo.sql_db") + def test_constraint_le_1(self): + """Expected Failure: High numbers are not allowed if use thresholds.""" + with self.assertRaises(IntegrityError): + self.company.write( + { + "use_price_compliance_threshold": True, + "price_compliance_threshold_t1": 1.01, # > 1.0 value + "price_compliance_threshold_t2": 0.0, + "price_compliance_threshold_t3": 0.0, + } + ) + + @mute_logger("odoo.sql_db") + def test_constraint_gaps_start(self): + """Expected Failure: Gap at the beginning (T1=0, T2>0).""" + with self.assertRaises(IntegrityError): + self.company.write( + { + "use_price_compliance_threshold": True, + "price_compliance_threshold_t1": 0.0, # Gap here + "price_compliance_threshold_t2": 0.05, + "price_compliance_threshold_t3": 0.0, + } + ) + + @mute_logger("odoo.sql_db") + def test_constraint_gaps_middle(self): + """Expected Failure: Gap in the middle (T2=0, T3>0).""" + with self.assertRaises(IntegrityError): + self.company.write( + { + "use_price_compliance_threshold": True, + "price_compliance_threshold_t1": 0.1, + "price_compliance_threshold_t2": 0.0, # Gap here + "price_compliance_threshold_t3": 0.05, + } + ) + + def test_ignore_constraint_if_disabled(self): + """Success Case: If the check is disabled, allow anything.""" + # Even with negatives or gaps, if 'use_...' is False, it should save + self.company.write( + { + "use_price_compliance_threshold": False, + "price_compliance_threshold_t1": -0.1, # Negative + "price_compliance_threshold_t2": 0.0, + "price_compliance_threshold_t3": 0.05, # Gap + } + ) + + def test_default_texts_parameter(self): + """Check if default texts are used if parameter is not set.""" + # Set custom texts + selection_texts = self.env[ + "sale.order.line" + ]._get_price_compliance_selection_tiers_text() + default_texts = self.env[ + "sale.order.line" + ]._get_price_compliance_selection_tiers_text_default() + self.assertEqual(selection_texts[0][1], default_texts[0][1]) + self.assertEqual(selection_texts[1][1], default_texts[1][1]) + self.assertEqual(selection_texts[2][1], default_texts[2][1]) + self.assertEqual(selection_texts[3][1], default_texts[3][1]) + self.assertEqual(selection_texts[4][1], default_texts[4][1]) + + def test_custom_texts_parameter(self): + """Check if custom texts are used if parameter is set.""" + # Set custom texts + self.env["ir.config_parameter"].sudo().set_param( + "sale_price_compliance.price_compliance_selection_tiers_text", + "{'t1': 'T. Gold', 't2': 'T. Silver', 'pricelist': 'T. Agreeed Price'}", + ) + selection_texts = self.env[ + "sale.order.line" + ]._get_price_compliance_selection_tiers_text() + default_texts = self.env[ + "sale.order.line" + ]._get_price_compliance_selection_tiers_text_default() + self.assertEqual(selection_texts[0][1], "T. Gold") + self.assertEqual(selection_texts[1][1], "T. Silver") + self.assertEqual(selection_texts[2][1], default_texts[2][1]) + self.assertEqual(selection_texts[3][1], default_texts[3][1]) + self.assertEqual(selection_texts[4][1], "T. Agreeed Price") + + def test_custom_icons_parameter(self): + """Check if custom icons are used if parameter is set.""" + # Set custom icons + self.env["ir.config_parameter"].sudo().set_param( + "sale_price_compliance.price_compliance_selection_tiers_icon", + "{'t1': '🟢', 't2': '🟡', 't3': '🟠', 'pricelist': '🟣'}", + ) + selection_icons = self.env[ + "sale.order.line" + ]._get_price_compliance_selection_tiers_icon_color() + default_icons = self.env[ + "sale.order.line" + ]._get_price_compliance_selection_tiers_icon_color_default() + self.assertEqual(selection_icons[0][1], "🟢") + self.assertEqual(selection_icons[1][1], "🟡") + self.assertEqual(selection_icons[2][1], "🟠") + self.assertEqual(selection_icons[3][1], default_icons[3][1]) + self.assertEqual(selection_icons[4][1], "🟣") + + def test_custom_texts_wrong_parameter(self): + """Check if custom texts are used if parameter is not correctly set.""" + # Set custom texts + self.env["ir.config_parameter"].sudo().set_param( + "sale_price_compliance.price_compliance_selection_tiers_text", + "test wrong parameter", + ) + selection_texts = self.env[ + "sale.order.line" + ]._get_price_compliance_selection_tiers_text() + default_texts = self.env[ + "sale.order.line" + ]._get_price_compliance_selection_tiers_text_default() + self.assertEqual(selection_texts[0][1], default_texts[0][1]) + self.assertEqual(selection_texts[1][1], default_texts[1][1]) + self.assertEqual(selection_texts[2][1], default_texts[2][1]) + self.assertEqual(selection_texts[3][1], default_texts[3][1]) + self.assertEqual(selection_texts[4][1], default_texts[4][1]) diff --git a/sale_price_compliance/views/product_category_view.xml b/sale_price_compliance/views/product_category_view.xml new file mode 100644 index 00000000000..a1f0aa202c9 --- /dev/null +++ b/sale_price_compliance/views/product_category_view.xml @@ -0,0 +1,59 @@ + + + + + product.category.form + product.category + + + + + + + + + + + + + + + product.category.search + product.category + + + + + + + + + diff --git a/sale_price_compliance/views/product_product_view.xml b/sale_price_compliance/views/product_product_view.xml new file mode 100644 index 00000000000..25c6111d454 --- /dev/null +++ b/sale_price_compliance/views/product_product_view.xml @@ -0,0 +1,42 @@ + + + + + product.product.view.form.easy + product.product + + + + + + + + + + + + + diff --git a/sale_price_compliance/views/product_template_view.xml b/sale_price_compliance/views/product_template_view.xml new file mode 100644 index 00000000000..e080bf14d1d --- /dev/null +++ b/sale_price_compliance/views/product_template_view.xml @@ -0,0 +1,59 @@ + + + + + product.template.common.form + product.template + + + + + + + + + + + + + + + product.template.search + product.template + + + + + + + + + diff --git a/sale_price_compliance/views/res_config_settings_view.xml b/sale_price_compliance/views/res_config_settings_view.xml new file mode 100644 index 00000000000..0d2ee184598 --- /dev/null +++ b/sale_price_compliance/views/res_config_settings_view.xml @@ -0,0 +1,88 @@ + + + + + res.config.settings.view.form.inherit.price.compliance + res.config.settings + + + +
+
+ +
+
+
+
+
+
+
+
diff --git a/sale_price_compliance/views/sale_order_line_view.xml b/sale_price_compliance/views/sale_order_line_view.xml new file mode 100644 index 00000000000..22d16c80d7f --- /dev/null +++ b/sale_price_compliance/views/sale_order_line_view.xml @@ -0,0 +1,32 @@ + + + + + sale.order.line.tree + sale.order.line + + + + + + + + + + + sale.order.line.select + sale.order.line + + + + + + + + diff --git a/sale_price_compliance/views/sale_order_view.xml b/sale_price_compliance/views/sale_order_view.xml new file mode 100644 index 00000000000..8c7ddc9d62e --- /dev/null +++ b/sale_price_compliance/views/sale_order_view.xml @@ -0,0 +1,60 @@ + + + + + sale.order.form + sale.order + + + + + +
+ + +
+
+
+
+ + + + + + + + + + + + +
+ +
+
+
+
+
diff --git a/setup/sale_price_compliance/odoo/addons/sale_price_compliance b/setup/sale_price_compliance/odoo/addons/sale_price_compliance new file mode 120000 index 00000000000..ef4cdc32fbd --- /dev/null +++ b/setup/sale_price_compliance/odoo/addons/sale_price_compliance @@ -0,0 +1 @@ +../../../../sale_price_compliance \ No newline at end of file diff --git a/setup/sale_price_compliance/setup.py b/setup/sale_price_compliance/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/sale_price_compliance/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)