diff --git a/purchase_stock_price_variance/README.rst b/purchase_stock_price_variance/README.rst new file mode 100644 index 00000000..38c17c29 --- /dev/null +++ b/purchase_stock_price_variance/README.rst @@ -0,0 +1,132 @@ +============================= +Purchase Stock Price Variance +============================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:72cb1add4c5ee6b19905a1be828b07390a01f239384becae1d2de35e95adf800 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstock--logistics--workflow-lightgray.png?logo=github + :target: https://github.com/OCA/stock-logistics-workflow/tree/16.0/purchase_stock_price_variance + :alt: OCA/stock-logistics-workflow +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/stock-logistics-workflow-16-0/stock-logistics-workflow-16-0-purchase_stock_price_variance + :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/stock-logistics-workflow&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module checks the variance between the purchase price and the +product's standard price at the time of receipt picking validation. If +the variance exceeds a given threshold, the notification can be left in +the chatter. Additionally, an error can be triggered before receiving +the stock if needed. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +1. Navigate to *Inventory > Configuration > Settings*. +2. Find and enable the 'Enable Price Variance Error' option to activate + this feature. An error will occur if the price difference exceeds the + threshold when receiving the product. +3. Set the following global values to apply if no specific value is set + at the product level. If the threshold value is set to 0, this + threshold will not be checked. + + - **Price Variance Threshold Percent**: Default percentage variance + for all products. + - **Price Variance Threshold Amount**: Default maximum variance for + all products. + +4. Go to *Inventory > Configuration > Product Categories*. +5. Enable **Bypass Price Variance Check** to skip the error check for + the products under this category. +6. Go to *Inventory > Products* and open the product. +7. Click on the Inventory tab and configure the following fields. If the + threshold value is set to 0, the threshold will refer to the global + value. + + - **Bypass Price Variance Check**: Enable this to skip the error + check for this specific product. + - **Price Variance Threshold Percent**: Set the allowable percentage + variance. + - **Price Variance Threshold Amount**: Define the maximum allowable + price variance. + +8. Assign the internal user to the "Manage Price Variance Check" group. + This will make the "Bypass Price Variance Check" checkbox updatable + in the receipt form. + +Usage +===== + +This module logs a notification in the chatter when the variance between +the purchase price and the standard price exceeds a given threshold +during the receipt process. + +You can configure the price variance threshold at the product level. + +If needed, an error message can also be triggered. The 'Bypass Price +Variance Check' option can be set both at the product level and in each +Stock Picking to skip the error + +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 +------- + +* Quartile + +Contributors +------------ + +- `Quartile `__: + + - Aung Ko Ko Lin + - Yoshi Tashiro + - Toshikimi Shigenobu + +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. + +This module is part of the `OCA/stock-logistics-workflow `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/purchase_stock_price_variance/__init__.py b/purchase_stock_price_variance/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/purchase_stock_price_variance/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/purchase_stock_price_variance/__manifest__.py b/purchase_stock_price_variance/__manifest__.py new file mode 100644 index 00000000..9969baf8 --- /dev/null +++ b/purchase_stock_price_variance/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright 2025 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Purchase Stock Price Variance", + "version": "16.0.1.0.0", + "category": "Stock", + "author": "Quartile, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/stock-logistics-workflow", + "depends": ["purchase_stock"], + "license": "AGPL-3", + "data": [ + "security/purchase_stock_price_variance_security.xml", + "views/product_category_views.xml", + "views/product_template_views.xml", + "views/res_config_setting_views.xml", + "views/stock_picking_views.xml", + ], + "installable": True, +} diff --git a/purchase_stock_price_variance/i18n/ja.po b/purchase_stock_price_variance/i18n/ja.po new file mode 100644 index 00000000..bfd0b5b8 --- /dev/null +++ b/purchase_stock_price_variance/i18n/ja.po @@ -0,0 +1,208 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * purchase_stock_price_variance +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-03-11 06:28+0000\n" +"PO-Revision-Date: 2025-03-11 06:28+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: purchase_stock_price_variance +#: model:ir.model.fields,field_description:purchase_stock_price_variance.field_product_category__bypass_price_variance_check +#: model:ir.model.fields,field_description:purchase_stock_price_variance.field_product_product__bypass_price_variance_check +#: model:ir.model.fields,field_description:purchase_stock_price_variance.field_product_template__bypass_price_variance_check +#: model:ir.model.fields,field_description:purchase_stock_price_variance.field_stock_picking__bypass_price_variance_check +msgid "Bypass Price Variance Check" +msgstr "価格差チェックをバイパス" + +#. module: purchase_stock_price_variance +#: model:ir.model,name:purchase_stock_price_variance.model_res_company +msgid "Companies" +msgstr "会社" + +#. module: purchase_stock_price_variance +#: model:ir.model,name:purchase_stock_price_variance.model_res_config_settings +msgid "Config Settings" +msgstr "コンフィグ設定" + +#. module: purchase_stock_price_variance +#: model:ir.model.fields,field_description:purchase_stock_price_variance.field_res_company__enable_price_variance_error +#: model:ir.model.fields,field_description:purchase_stock_price_variance.field_res_config_settings__enable_price_variance_error +msgid "Enable Price Variance Error" +msgstr "価格差エラーを有効化" + +#. module: purchase_stock_price_variance +#: model_terms:ir.ui.view,arch_db:purchase_stock_price_variance.view_stock_config_settings +msgid "" +"Enable the feature that triggers an error when receiving a product if\n" +" the price difference exceeds the threshold." +msgstr "入荷時に価格差がしきい値を超えた場合にエラーを発生させる機能を有効化します。" + +#. module: purchase_stock_price_variance +#: model:ir.model.fields,help:purchase_stock_price_variance.field_stock_picking__bypass_price_variance_check +msgid "" +"If enabled, no error is raised for price variance between the product's " +"standard price and purchase receipt unit price." +msgstr "有効にすると、商品の標準価格と購買単価の間の価格差によるエラーは発生しません。" + +#. module: purchase_stock_price_variance +#: model:ir.model.fields,help:purchase_stock_price_variance.field_product_category__bypass_price_variance_check +msgid "" +"If enabled, the products under this category will not raise an error for " +"price variance between the product's standard price and the purchase receipt" +" unit price." +msgstr "有効にすると、このカテゴリ内の製品は、標準価格と購買単価の価格差によるエラーを発生させません。" + +#. module: purchase_stock_price_variance +#: model:ir.model.fields,help:purchase_stock_price_variance.field_product_product__bypass_price_variance_check +#: model:ir.model.fields,help:purchase_stock_price_variance.field_product_template__bypass_price_variance_check +msgid "" +"If enabled, this product will not raise an error for price variance between " +"the product's standard price and the purchase receipt unit price." +msgstr "有効にすると、この製品は、標準価格と購買単価の価格差によるエラーを発生させません。" + +#. module: purchase_stock_price_variance +#: model:res.groups,name:purchase_stock_price_variance.group_manage_price_variance_check +msgid "Manage Price Variance Check" +msgstr "価格差チェックの管理" + +#. module: purchase_stock_price_variance +#: model_terms:ir.ui.view,arch_db:purchase_stock_price_variance.view_stock_config_settings +msgid "" +"Maximum allowable variance (in monetary amount, based on company\n" +" currency) between the product's standard price and the purchase receipt\n" +" unit price.\n" +" Keeping it at zero means that this threshold will not be checked." +msgstr "" +"製品の標準価格と購買単価の間で許容される最大の価格差(会社の通貨単位)。\n" +" 0に設定すると、このしきい値はチェックされません。" + +#. module: purchase_stock_price_variance +#: model:ir.model.fields,help:purchase_stock_price_variance.field_product_product__price_variance_threshold_amount +#: model:ir.model.fields,help:purchase_stock_price_variance.field_product_template__price_variance_threshold_amount +msgid "" +"Maximum allowable variance (in monetary amount, based on company currency) " +"between the product's standard price and the purchase receipt unit price. " +"Setting this to zero means the threshold will refer to the global setting." +msgstr "製品の標準価格と購買単価の間で許容される最大の価格差(会社の通貨単位)。0に設定すると、このグローバル設定が利用されます。" + +#. module: purchase_stock_price_variance +#: model:ir.model.fields,help:purchase_stock_price_variance.field_res_company__price_variance_threshold_amount +#: model:ir.model.fields,help:purchase_stock_price_variance.field_res_config_settings__price_variance_threshold_amount +msgid "" +"Maximum allowable variance (in monetary amount, based on company currency) " +"between the product's standard price and the purchase receipt unit price. " +"Setting this to zero means this threshold will not be checked." +msgstr "製品の標準価格と購買単価の間で許容される最大の価格差(会社の通貨単位)。0に設定すると、このしきい値はチェックされません。" + +#. module: purchase_stock_price_variance +#: model_terms:ir.ui.view,arch_db:purchase_stock_price_variance.view_stock_config_settings +msgid "" +"Maximum variance (in percent) allowable between the product's standard\n" +" price and purchase receipt unit price.\n" +" Keeping it at zero means that this threshold will not be checked." +msgstr "製品の標準価格に対する購買単価の許容される最大差異(パーセント)。0に設定すると、このしきい値はチェックされません。" + +#. module: purchase_stock_price_variance +#: model:ir.model.fields,help:purchase_stock_price_variance.field_product_product__price_variance_threshold_percent +#: model:ir.model.fields,help:purchase_stock_price_variance.field_product_template__price_variance_threshold_percent +msgid "" +"Maximum variance (in percent) allowable between the product's standard price" +" and purchase receipt unit price. Setting this to zero means the threshold " +"will refer to the global setting." +msgstr "製品の標準価格に対する購買単価の許容される最大差異(パーセント)。0 に設定すると、このグローバル設定が利用されます。" + +#. module: purchase_stock_price_variance +#: model:ir.model.fields,help:purchase_stock_price_variance.field_res_company__price_variance_threshold_percent +#: model:ir.model.fields,help:purchase_stock_price_variance.field_res_config_settings__price_variance_threshold_percent +msgid "" +"Maximum variance (in percent) allowable between the product's standard price" +" and purchase receipt unit price.Setting this to zero means this threshold " +"will not be checked." +msgstr "製品の標準価格に対する購買単価の許容される最大差異(パーセント)。0に設定すると、このしきい値はチェックされません。" + +#. module: purchase_stock_price_variance +#: model_terms:ir.ui.view,arch_db:purchase_stock_price_variance.view_template_property_form +msgid "Price Discrepancy Settings" +msgstr "価格差異設定" + +#. module: purchase_stock_price_variance +#: model:ir.model.fields,field_description:purchase_stock_price_variance.field_product_product__price_variance_threshold_amount +#: model:ir.model.fields,field_description:purchase_stock_price_variance.field_product_template__price_variance_threshold_amount +#: model:ir.model.fields,field_description:purchase_stock_price_variance.field_res_company__price_variance_threshold_amount +#: model:ir.model.fields,field_description:purchase_stock_price_variance.field_res_config_settings__price_variance_threshold_amount +msgid "Price Variance Threshold Amount" +msgstr "価格差しきい値(金額)" + +#. module: purchase_stock_price_variance +#: model:ir.model.fields,field_description:purchase_stock_price_variance.field_product_product__price_variance_threshold_percent +#: model:ir.model.fields,field_description:purchase_stock_price_variance.field_product_template__price_variance_threshold_percent +#: model:ir.model.fields,field_description:purchase_stock_price_variance.field_res_company__price_variance_threshold_percent +#: model:ir.model.fields,field_description:purchase_stock_price_variance.field_res_config_settings__price_variance_threshold_percent +msgid "Price Variance Threshold Percent" +msgstr "価格差しきい値(パーセント)" + +#. module: purchase_stock_price_variance +#. odoo-python +#: code:addons/purchase_stock_price_variance/models/stock_picking.py:0 +#, python-format +msgid "" +"Price variance exceeding a threshold detected for the following products:" +msgstr "以下の製品でしきい値を超える価格差が検出されました:" + +#. module: purchase_stock_price_variance +#. odoo-python +#: code:addons/purchase_stock_price_variance/models/stock_picking.py:0 +#, python-format +msgid "" +"Price variance exceeding a threshold detected for the following products:\n" +"\n" +msgstr "" +"以下の製品でしきい値を超える価格差が検出されました:\n" +"\n" + +#. module: purchase_stock_price_variance +#: model:ir.model,name:purchase_stock_price_variance.model_product_template +msgid "Product" +msgstr "プロダクト" + +#. module: purchase_stock_price_variance +#: model:ir.model,name:purchase_stock_price_variance.model_product_category +msgid "Product Category" +msgstr "プロダクトカテゴリ" + +#. module: purchase_stock_price_variance +#. odoo-python +#: code:addons/purchase_stock_price_variance/models/product_template.py:0 +#: code:addons/purchase_stock_price_variance/models/product_template.py:0 +#: code:addons/purchase_stock_price_variance/models/res_company.py:0 +#: code:addons/purchase_stock_price_variance/models/res_company.py:0 +#, python-format +msgid "The threshold values cannot be negative." +msgstr "しきい値は負の値にはできません。" + +#. module: purchase_stock_price_variance +#: model:ir.model,name:purchase_stock_price_variance.model_stock_picking +msgid "Transfer" +msgstr "運送" + +#. module: purchase_stock_price_variance +#. odoo-python +#: code:addons/purchase_stock_price_variance/models/product_category.py:0 +#: code:addons/purchase_stock_price_variance/models/product_template.py:0 +#: code:addons/purchase_stock_price_variance/models/stock_picking.py:0 +#: code:addons/purchase_stock_price_variance/models/stock_picking.py:0 +#, python-format +msgid "" +"You do not have permission to modify the 'Bypass Price Variance Check' " +"field. Please contact an administrator or a user with the appropriate " +"permissions." +msgstr "「価格差異チェックをバイパス」のフィールドを変更する権限がありません。管理者または適切な権限を持つユーザーにお問い合わせください。" diff --git a/purchase_stock_price_variance/models/__init__.py b/purchase_stock_price_variance/models/__init__.py new file mode 100644 index 00000000..60c83992 --- /dev/null +++ b/purchase_stock_price_variance/models/__init__.py @@ -0,0 +1,5 @@ +from . import product_category +from . import product_template +from . import res_company +from . import res_config_settings +from . import stock_picking diff --git a/purchase_stock_price_variance/models/product_category.py b/purchase_stock_price_variance/models/product_category.py new file mode 100644 index 00000000..c365f25d --- /dev/null +++ b/purchase_stock_price_variance/models/product_category.py @@ -0,0 +1,31 @@ +# Copyright 2025 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, fields, models +from odoo.exceptions import UserError + + +class ProductCategory(models.Model): + _inherit = "product.category" + + bypass_price_variance_check = fields.Boolean( + copy=False, + tracking=True, + help="If enabled, the products under this category will not raise an error for price" + " variance between the product's standard price and the purchase receipt unit price.", + ) + + def write(self, vals): + if "bypass_price_variance_check" in vals: + if not self.env.user.has_group( + "purchase_stock_price_variance.group_manage_price_variance_check" + ): + raise UserError( + _( + "You do not have permission to modify the " + "'Bypass Price Variance Check' field. " + "Please contact an administrator or a user " + "with the appropriate permissions." + ) + ) + return super().write(vals) diff --git a/purchase_stock_price_variance/models/product_template.py b/purchase_stock_price_variance/models/product_template.py new file mode 100644 index 00000000..a7fa53d4 --- /dev/null +++ b/purchase_stock_price_variance/models/product_template.py @@ -0,0 +1,52 @@ +# Copyright 2025 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import UserError, ValidationError + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + price_variance_threshold_percent = fields.Float( + help="Maximum variance (in percent) allowable between the product's standard price" + " and purchase receipt unit price. " + "Setting this to zero means the threshold will refer to the global setting." + ) + price_variance_threshold_amount = fields.Monetary( + help="Maximum allowable variance (in monetary amount, based on company currency)" + " between the product's standard price and the purchase receipt unit price. " + "Setting this to zero means the threshold will refer to the global setting." + ) + bypass_price_variance_check = fields.Boolean( + copy=False, + tracking=True, + help="If enabled, this product will not raise an error for price variance between " + "the product's standard price and the purchase receipt unit price.", + ) + + @api.constrains( + "price_variance_threshold_percent", "price_variance_threshold_amount" + ) + def _check_price_variance_threshold(self): + for rec in self: + if ( + rec.price_variance_threshold_percent < 0 + or rec.price_variance_threshold_amount < 0 + ): + raise ValidationError(_("The threshold values cannot be negative.")) + + def write(self, vals): + if "bypass_price_variance_check" in vals: + if not self.env.user.has_group( + "purchase_stock_price_variance.group_manage_price_variance_check" + ): + raise UserError( + _( + "You do not have permission to modify the " + "'Bypass Price Variance Check' field. " + "Please contact an administrator or a user " + "with the appropriate permissions." + ) + ) + return super().write(vals) diff --git a/purchase_stock_price_variance/models/res_company.py b/purchase_stock_price_variance/models/res_company.py new file mode 100644 index 00000000..f8557230 --- /dev/null +++ b/purchase_stock_price_variance/models/res_company.py @@ -0,0 +1,32 @@ +# Copyright 2025 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class ResCompany(models.Model): + _inherit = "res.company" + + enable_price_variance_error = fields.Boolean() + price_variance_threshold_percent = fields.Float( + help="Maximum variance (in percent) allowable between the product's standard price" + " and purchase receipt unit price." + "Setting this to zero means this threshold will not be checked." + ) + price_variance_threshold_amount = fields.Monetary( + help="Maximum allowable variance (in monetary amount, based on company currency)" + " between the product's standard price and the purchase receipt unit price." + "Setting this to zero means this threshold will not be checked." + ) + + @api.constrains( + "price_variance_threshold_percent", "price_variance_threshold_amount" + ) + def _check_price_variance_threshold(self): + for rec in self: + if ( + rec.price_variance_threshold_percent < 0 + or rec.price_variance_threshold_amount < 0 + ): + raise ValidationError(_("The threshold values cannot be negative.")) diff --git a/purchase_stock_price_variance/models/res_config_settings.py b/purchase_stock_price_variance/models/res_config_settings.py new file mode 100644 index 00000000..68e8a778 --- /dev/null +++ b/purchase_stock_price_variance/models/res_config_settings.py @@ -0,0 +1,27 @@ +# Copyright 2025 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + enable_price_variance_error = fields.Boolean( + related="company_id.enable_price_variance_error", + readonly=False, + ) + price_variance_threshold_percent = fields.Float( + related="company_id.price_variance_threshold_percent", + readonly=False, + help="Maximum variance (in percent) allowable between the product's standard price" + " and purchase receipt unit price. " + "Setting this to zero means this threshold will not be checked.", + ) + price_variance_threshold_amount = fields.Monetary( + related="company_id.price_variance_threshold_amount", + readonly=False, + help="Maximum allowable variance (in monetary amount, based on company currency)" + " between the product's standard price and the purchase receipt unit price. " + "Setting this to zero means this threshold will not be checked.", + ) diff --git a/purchase_stock_price_variance/models/stock_picking.py b/purchase_stock_price_variance/models/stock_picking.py new file mode 100644 index 00000000..a6012567 --- /dev/null +++ b/purchase_stock_price_variance/models/stock_picking.py @@ -0,0 +1,83 @@ +# Copyright 2025 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, fields, models +from odoo.exceptions import UserError + + +class Stockpick(models.Model): + _inherit = "stock.picking" + + bypass_price_variance_check = fields.Boolean( + copy=False, + tracking=True, + help="If enabled, no error is raised for price variance between " + "the product's standard price and purchase receipt unit price.", + ) + + def write(self, vals): + if "bypass_price_variance_check" in vals: + if not self.env.user.has_group( + "purchase_stock_price_variance.group_manage_price_variance_check" + ): + raise UserError( + _( + "You do not have permission to modify the " + "'Bypass Price Variance Check' field. " + "Please contact an administrator or a user " + "with the appropriate permissions." + ) + ) + return super().write(vals) + + def _action_done(self): + company = self.env.company + global_threshold_percent = company.price_variance_threshold_percent + global_threshold_amount = company.price_variance_threshold_amount + for pick in self: + error_messages = [] + messages = [] + for move in pick.move_ids: + if not (move._is_in() or move._is_dropshipped()): + continue + product = move.product_id + threshold_percent = ( + product.price_variance_threshold_percent or global_threshold_percent + ) + threshold_amount = ( + product.price_variance_threshold_amount or global_threshold_amount + ) + if not threshold_percent and not threshold_amount: + continue + received_price = move._get_price_unit() + standard_price = product.standard_price + amount_difference = abs(received_price - standard_price) + percentage_difference = ( + (amount_difference / standard_price) * 100 if standard_price else 0 + ) + if ( + threshold_percent and percentage_difference > threshold_percent + ) or (threshold_amount and amount_difference > threshold_amount): + message = ( + f"{product.name}: Received Price = {received_price}, " + f"Product Price = {standard_price}." + ) + if ( + not product.categ_id.bypass_price_variance_check + and not product.bypass_price_variance_check + and not pick.bypass_price_variance_check + ): + error_messages.append(message) + messages.append(message) + + def get_message(products, delimiter): + message = _( + "Price variance exceeding a threshold detected for the following products:" + ) + return message + delimiter + delimiter.join(products) + + if pick.company_id.enable_price_variance_error and error_messages: + raise UserError(get_message(error_messages, "\n")) + if messages: + pick.message_post(body=get_message(messages, "
")) + return super()._action_done() diff --git a/purchase_stock_price_variance/readme/CONFIGURE.md b/purchase_stock_price_variance/readme/CONFIGURE.md new file mode 100644 index 00000000..49c979c5 --- /dev/null +++ b/purchase_stock_price_variance/readme/CONFIGURE.md @@ -0,0 +1,17 @@ +1. Navigate to *Inventory > Configuration > Settings*. +2. Find and enable the 'Enable Price Variance Error' option to activate this feature. + An error will occur if the price difference exceeds the threshold when receiving the product. +3. Set the following global values to apply if no specific value is set at the product level. + If the threshold value is set to 0, this threshold will not be checked. + * **Price Variance Threshold Percent**: Default percentage variance for all products. + * **Price Variance Threshold Amount**: Default maximum variance for all products. +4. Go to *Inventory > Configuration > Product Categories*. +5. Enable **Bypass Price Variance Check** to skip the error check for the products under this category. +6. Go to *Inventory > Products* and open the product. +7. Click on the Inventory tab and configure the following fields. + If the threshold value is set to 0, the threshold will refer to the global value. + * **Bypass Price Variance Check**: Enable this to skip the error check for this specific product. + * **Price Variance Threshold Percent**: Set the allowable percentage variance. + * **Price Variance Threshold Amount**: Define the maximum allowable price variance. +8. Assign the internal user to the "Manage Price Variance Check" group. + This will make the "Bypass Price Variance Check" checkbox updatable in the receipt form. diff --git a/purchase_stock_price_variance/readme/CONTRIBUTORS.md b/purchase_stock_price_variance/readme/CONTRIBUTORS.md new file mode 100644 index 00000000..9fc009d1 --- /dev/null +++ b/purchase_stock_price_variance/readme/CONTRIBUTORS.md @@ -0,0 +1,4 @@ +- [Quartile](https://www.quartile.co): + - Aung Ko Ko Lin + - Yoshi Tashiro + - Toshikimi Shigenobu diff --git a/purchase_stock_price_variance/readme/DESCRIPTION.md b/purchase_stock_price_variance/readme/DESCRIPTION.md new file mode 100644 index 00000000..7c8f4d78 --- /dev/null +++ b/purchase_stock_price_variance/readme/DESCRIPTION.md @@ -0,0 +1,2 @@ +This module checks the variance between the purchase price and the product's standard price at the time of receipt picking validation. +If the variance exceeds a given threshold, the notification can be left in the chatter. Additionally, an error can be triggered before receiving the stock if needed. \ No newline at end of file diff --git a/purchase_stock_price_variance/readme/USAGE.md b/purchase_stock_price_variance/readme/USAGE.md new file mode 100644 index 00000000..e5fbf5b2 --- /dev/null +++ b/purchase_stock_price_variance/readme/USAGE.md @@ -0,0 +1,9 @@ +This module logs a notification in the chatter when the variance between +the purchase price and the standard price exceeds a given threshold during +the receipt process. + +You can configure the price variance threshold at the product level. + +If needed, an error message can also be triggered. +The 'Bypass Price Variance Check' option can be set both at the product +level and in each Stock Picking to skip the error diff --git a/purchase_stock_price_variance/security/purchase_stock_price_variance_security.xml b/purchase_stock_price_variance/security/purchase_stock_price_variance_security.xml new file mode 100644 index 00000000..c424d39f --- /dev/null +++ b/purchase_stock_price_variance/security/purchase_stock_price_variance_security.xml @@ -0,0 +1,11 @@ + + + + Manage Price Variance Check + + + + diff --git a/purchase_stock_price_variance/static/description/index.html b/purchase_stock_price_variance/static/description/index.html new file mode 100644 index 00000000..db22ebe6 --- /dev/null +++ b/purchase_stock_price_variance/static/description/index.html @@ -0,0 +1,477 @@ + + + + + +Purchase Stock Price Variance + + + +
+

Purchase Stock Price Variance

+ + +

Beta License: AGPL-3 OCA/stock-logistics-workflow Translate me on Weblate Try me on Runboat

+

This module checks the variance between the purchase price and the +product’s standard price at the time of receipt picking validation. If +the variance exceeds a given threshold, the notification can be left in +the chatter. Additionally, an error can be triggered before receiving +the stock if needed.

+

Table of contents

+ +
+

Configuration

+
    +
  1. Navigate to Inventory > Configuration > Settings.
  2. +
  3. Find and enable the ‘Enable Price Variance Error’ option to activate +this feature. An error will occur if the price difference exceeds the +threshold when receiving the product.
  4. +
  5. Set the following global values to apply if no specific value is set +at the product level. If the threshold value is set to 0, this +threshold will not be checked.
      +
    • Price Variance Threshold Percent: Default percentage variance +for all products.
    • +
    • Price Variance Threshold Amount: Default maximum variance for +all products.
    • +
    +
  6. +
  7. Go to Inventory > Configuration > Product Categories.
  8. +
  9. Enable Bypass Price Variance Check to skip the error check for +the products under this category.
  10. +
  11. Go to Inventory > Products and open the product.
  12. +
  13. Click on the Inventory tab and configure the following fields. If the +threshold value is set to 0, the threshold will refer to the global +value.
      +
    • Bypass Price Variance Check: Enable this to skip the error +check for this specific product.
    • +
    • Price Variance Threshold Percent: Set the allowable percentage +variance.
    • +
    • Price Variance Threshold Amount: Define the maximum allowable +price variance.
    • +
    +
  14. +
  15. Assign the internal user to the “Manage Price Variance Check” group. +This will make the “Bypass Price Variance Check” checkbox updatable +in the receipt form.
  16. +
+
+
+

Usage

+

This module logs a notification in the chatter when the variance between +the purchase price and the standard price exceeds a given threshold +during the receipt process.

+

You can configure the price variance threshold at the product level.

+

If needed, an error message can also be triggered. The ‘Bypass Price +Variance Check’ option can be set both at the product level and in each +Stock Picking to skip the error

+
+
+

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

+
    +
  • Quartile
  • +
+
+
+

Contributors

+
    +
  • Quartile:
      +
    • Aung Ko Ko Lin
    • +
    • Yoshi Tashiro
    • +
    • Toshikimi Shigenobu
    • +
    +
  • +
+
+
+

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.

+

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

+

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

+
+
+
+ + diff --git a/purchase_stock_price_variance/tests/__init__.py b/purchase_stock_price_variance/tests/__init__.py new file mode 100644 index 00000000..7ec77b31 --- /dev/null +++ b/purchase_stock_price_variance/tests/__init__.py @@ -0,0 +1 @@ +from . import test_purchase_stock_price_variance diff --git a/purchase_stock_price_variance/tests/test_purchase_stock_price_variance.py b/purchase_stock_price_variance/tests/test_purchase_stock_price_variance.py new file mode 100644 index 00000000..06347ec6 --- /dev/null +++ b/purchase_stock_price_variance/tests/test_purchase_stock_price_variance.py @@ -0,0 +1,128 @@ +# Copyright 2025 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import Command +from odoo.exceptions import UserError +from odoo.tests.common import TransactionCase + + +class TestPurchaseStockPriceVariance(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.company = cls.env.user.company_id + cls.product_category = cls.env["product.category"].create( + {"name": "Test Category"} + ) + cls.product = cls.env["product.template"].create( + { + "name": "Test Product", + "standard_price": 100, + "categ_id": cls.product_category.id, + } + ) + cls.supplier = cls.env["res.partner"].create({"name": "Test Supplier"}) + cls.group_manage_price_variance_check = cls.env.ref( + "purchase_stock_price_variance.group_manage_price_variance_check" + ) + cls.company.enable_price_variance_error = True + + def create_purchase_order(self, price_unit=100): + return self.env["purchase.order"].create( + { + "partner_id": self.supplier.id, + "order_line": [ + Command.create( + { + "product_id": self.product.product_variant_id.id, + "product_qty": 1, + "price_unit": price_unit, + } + ) + ], + } + ) + + def validate_picking(self, picking, expect_error=False): + picking.move_ids.write({"quantity_done": 1}) + if expect_error: + with self.assertRaises(UserError): + picking.button_validate() + else: + picking.button_validate() + self.assertEqual(picking.state, "done") + + def check_chatter_message(self, picking, should_contain): + messages = picking.message_ids.mapped("body") + found = any( + "Price variance exceeding a threshold detected" in message + for message in messages + ) + self.assertEqual(found, should_contain) + + def test_01_normal_workflow_no_error(self): + po = self.create_purchase_order() + po.button_confirm() + picking = po.picking_ids + self.validate_picking(picking) + self.check_chatter_message(picking, False) + + def test_02_price_variance_check_category(self): + with self.assertRaises(UserError): + self.product_category.bypass_price_variance_check = True + self.product.price_variance_threshold_percent = 5 + po = self.create_purchase_order(price_unit=110) + po.button_confirm() + picking = po.picking_ids + self.validate_picking(picking, expect_error=True) + self.env.user.groups_id |= self.group_manage_price_variance_check + self.product_category.bypass_price_variance_check = True + self.validate_picking(picking) + self.check_chatter_message(picking, True) + + def test_03_price_variance_check_product(self): + with self.assertRaises(UserError): + self.product.bypass_price_variance_check = True + self.product.price_variance_threshold_percent = 5 + po = self.create_purchase_order(price_unit=110) + po.button_confirm() + picking = po.picking_ids + self.validate_picking(picking, expect_error=True) + self.env.user.groups_id |= self.group_manage_price_variance_check + self.product.bypass_price_variance_check = True + self.validate_picking(picking) + self.check_chatter_message(picking, True) + + def test_04_price_variance_check_threshold_amount(self): + self.product.price_variance_threshold_amount = 5 + po = self.create_purchase_order(price_unit=110) + po.button_confirm() + picking = po.picking_ids + self.validate_picking(picking, expect_error=True) + self.env.user.groups_id |= self.group_manage_price_variance_check + self.product.bypass_price_variance_check = True + self.validate_picking(picking) + self.check_chatter_message(picking, True) + + def test_05_price_variance_check_global_threshold_amount(self): + self.company.price_variance_threshold_amount = 5 + po = self.create_purchase_order(price_unit=110) + po.button_confirm() + picking = po.picking_ids + self.validate_picking(picking, expect_error=True) + + def test_06_price_variance_check_global_threshold_percent(self): + self.company.price_variance_threshold_percent = 5 + po = self.create_purchase_order(price_unit=110) + po.button_confirm() + picking = po.picking_ids + self.validate_picking(picking, expect_error=True) + + def test_07_global_price_variance_check_disable(self): + self.company.enable_price_variance_error = False + self.company.price_variance_threshold_percent = 5 + po = self.create_purchase_order(price_unit=110) + po.button_confirm() + picking = po.picking_ids + self.validate_picking(picking) + self.check_chatter_message(picking, True) diff --git a/purchase_stock_price_variance/views/product_category_views.xml b/purchase_stock_price_variance/views/product_category_views.xml new file mode 100644 index 00000000..b831a0b7 --- /dev/null +++ b/purchase_stock_price_variance/views/product_category_views.xml @@ -0,0 +1,12 @@ + + + + product.category + + + + + + + + diff --git a/purchase_stock_price_variance/views/product_template_views.xml b/purchase_stock_price_variance/views/product_template_views.xml new file mode 100644 index 00000000..e482253c --- /dev/null +++ b/purchase_stock_price_variance/views/product_template_views.xml @@ -0,0 +1,20 @@ + + + + product.template.stock.property.form.inherit + product.template + + + + + + + + + + + + diff --git a/purchase_stock_price_variance/views/res_config_setting_views.xml b/purchase_stock_price_variance/views/res_config_setting_views.xml new file mode 100644 index 00000000..69f42487 --- /dev/null +++ b/purchase_stock_price_variance/views/res_config_setting_views.xml @@ -0,0 +1,60 @@ + + + + res.config.settings + res.config.settings + + + +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
diff --git a/purchase_stock_price_variance/views/stock_picking_views.xml b/purchase_stock_price_variance/views/stock_picking_views.xml new file mode 100644 index 00000000..2746e8bc --- /dev/null +++ b/purchase_stock_price_variance/views/stock_picking_views.xml @@ -0,0 +1,16 @@ + + + + stock.picking.form + stock.picking + + + + + + + + diff --git a/setup/purchase_stock_price_variance/odoo/addons/purchase_stock_price_variance b/setup/purchase_stock_price_variance/odoo/addons/purchase_stock_price_variance new file mode 120000 index 00000000..c19226ab --- /dev/null +++ b/setup/purchase_stock_price_variance/odoo/addons/purchase_stock_price_variance @@ -0,0 +1 @@ +../../../../purchase_stock_price_variance \ No newline at end of file diff --git a/setup/purchase_stock_price_variance/setup.py b/setup/purchase_stock_price_variance/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/purchase_stock_price_variance/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)