diff --git a/ddmrp/tests/test_ddmrp.py b/ddmrp/tests/test_ddmrp.py index d734e0f41..13ab5ff66 100644 --- a/ddmrp/tests/test_ddmrp.py +++ b/ddmrp/tests/test_ddmrp.py @@ -6,7 +6,7 @@ from odoo.exceptions import ValidationError -from odoo.addons.ddmrp.tests.common import TestDdmrpCommon +from .common import TestDdmrpCommon class TestDdmrp(TestDdmrpCommon): diff --git a/ddmrp_product_replace/README.rst b/ddmrp_product_replace/README.rst new file mode 100644 index 000000000..a4c967e3e --- /dev/null +++ b/ddmrp_product_replace/README.rst @@ -0,0 +1,112 @@ +===================== +DDMRP Product Replace +===================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-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%2Fddmrp-lightgray.png?logo=github + :target: https://github.com/OCA/ddmrp/tree/14.0/ddmrp_product_replace + :alt: OCA/ddmrp +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/ddmrp-14-0/ddmrp-14-0-ddmrp_product_replace + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/255/14.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Provides a tool for product replacement. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Go to *Inventory > Configuration > DDMRP > Product Replacement Tool*. + +Then you can fill the wizard options to complete the replacement. There are two +modes of operation: *Create a new buffer for the replacing product* and +*Replace product in existing buffers*. + +Known issues / Roadmap +====================== + +* Option to create new buffer and make old ones inactive. +* Consider Demand estimates in the replacement. + +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 smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ForgeFlow + +Contributors +~~~~~~~~~~~~ + +* Lois Rilo +* Jordi Ballester +* Akim Juillerat +* `Trobz `_: + * Khoi Vo + +Other credits +~~~~~~~~~~~~~ + +The initial development of this module has been financially supported by: + +* Aleph Objects, Inc. + +The migration of this module from 13.0 to 14.0 was financially supported by Camptocamp + +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-JordiBForgeFlow| image:: https://github.com/JordiBForgeFlow.png?size=40px + :target: https://github.com/JordiBForgeFlow + :alt: JordiBForgeFlow +.. |maintainer-LoisRForgeFlow| image:: https://github.com/LoisRForgeFlow.png?size=40px + :target: https://github.com/LoisRForgeFlow + :alt: LoisRForgeFlow + +Current `maintainers `__: + +|maintainer-JordiBForgeFlow| |maintainer-LoisRForgeFlow| + +This module is part of the `OCA/ddmrp `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/ddmrp_product_replace/__init__.py b/ddmrp_product_replace/__init__.py new file mode 100644 index 000000000..aee8895e7 --- /dev/null +++ b/ddmrp_product_replace/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizards diff --git a/ddmrp_product_replace/__manifest__.py b/ddmrp_product_replace/__manifest__.py new file mode 100644 index 000000000..eda5d99f0 --- /dev/null +++ b/ddmrp_product_replace/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2017-21 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +{ + "name": "DDMRP Product Replace", + "summary": "Provides a assisting tool for product replacement.", + "version": "14.0.1.0.0", + "development_status": "Beta", + "author": "ForgeFlow, Odoo Community Association (OCA)", + "maintainers": ["JordiBForgeFlow", "LoisRForgeFlow"], + "website": "https://github.com/OCA/ddmrp", + "category": "Warehouse Management", + "depends": ["ddmrp"], + "data": [ + "security/ir.model.access.csv", + "wizards/ddmrp_product_replace_view.xml", + "wizards/make_procurement_buffer_view.xml", + "views/stock_buffer_view.xml", + ], + "license": "LGPL-3", + "installable": True, +} diff --git a/ddmrp_product_replace/i18n/ddmrp_product_replace.pot b/ddmrp_product_replace/i18n/ddmrp_product_replace.pot new file mode 100644 index 000000000..97c270b00 --- /dev/null +++ b/ddmrp_product_replace/i18n/ddmrp_product_replace.pot @@ -0,0 +1,354 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * ddmrp_product_replace +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 13.0\n" +"Report-Msgid-Bugs-To: \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: ddmrp_product_replace +#: model_terms:ir.ui.view,arch_db:ddmrp_product_replace.view_ddmrp_adjustment_sheet_wizard_form +msgid "" +"\n" +" When there are more than one product replaced, only the buffer(s) of the primary product in\n" +" this list will be used to create the buffer(s) and/or product(s) for replacement." +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_ddmrp_product_replace__buffer_ids +msgid "Affected Buffers" +msgstr "" + +#. module: ddmrp_product_replace +#: code:addons/ddmrp_product_replace/models/stock_buffer.py:0 +#, python-format +msgid "Buffered product must be considered as demand." +msgstr "" + +#. module: ddmrp_product_replace +#: code:addons/ddmrp_product_replace/models/stock_buffer.py:0 +#, python-format +msgid "Buffers Replaced" +msgstr "" + +#. module: ddmrp_product_replace +#: model_terms:ir.ui.view,arch_db:ddmrp_product_replace.view_ddmrp_adjustment_sheet_wizard_form +msgid "Cancel" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_ddmrp_product_replace__consider_past_demand +msgid "Consider Old Product Demand" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,help:ddmrp_product_replace.field_ddmrp_product_replace__consider_past_demand +msgid "Consider Old product moves as demand for new product" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_stock_buffer__demand_product_ids +msgid "Considered As Demand" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_ddmrp_product_replace__copy_packaging +msgid "Copy Packaging" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_ddmrp_product_replace__copy_putaway +msgid "Copy Put Away Strategy" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_ddmrp_product_replace__copy_route +msgid "Copy Routes" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields.selection,name:ddmrp_product_replace.selection__ddmrp_product_replace__use_existing__new +msgid "Create New Product" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields.selection,name:ddmrp_product_replace.selection__ddmrp_product_replace__mode__new_buffer +msgid "Create a new buffer for the replacing product." +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_ddmrp_product_replace__create_uid +msgid "Created by" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_ddmrp_product_replace__create_date +msgid "Created on" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model,name:ddmrp_product_replace.model_ddmrp_product_replace +msgid "DDMRP Product Replace" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_ddmrp_product_replace__display_name +msgid "Display Name" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_make_procurement_buffer__has_replaced_buffers +msgid "Has Replaced Buffers" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_ddmrp_product_replace__id +msgid "ID" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,help:ddmrp_product_replace.field_stock_buffer__use_replacement_for_buffer_status +msgid "" +"If you tick this option, the buffer will consider the incoming and on-hand " +"of all products it replaces and this will impact its NFP." +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_stock_buffer__use_replacement_for_buffer_status +msgid "Include Incoming & On-Hands of replaced products" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_ddmrp_product_replace__is_already_replaced +msgid "Is Already Replaced" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_ddmrp_product_replace____last_update +msgid "Last Modified on" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_ddmrp_product_replace__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_ddmrp_product_replace__write_date +msgid "Last Updated on" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model,name:ddmrp_product_replace.model_make_procurement_buffer +msgid "Make Procurements from Stock Buffers" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_ddmrp_product_replace__mode +msgid "Mode" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_ddmrp_product_replace__multi_product +msgid "Multi Product" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_ddmrp_product_replace__new_product_default_code +msgid "New Product Internal Ref." +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_ddmrp_product_replace__new_product_name +msgid "New Product Name" +msgstr "" + +#. module: ddmrp_product_replace +#: code:addons/ddmrp_product_replace/wizards/ddmrp_product_replace.py:0 +#, python-format +msgid "New Stock Buffers" +msgstr "" + +#. module: ddmrp_product_replace +#: code:addons/ddmrp_product_replace/wizards/ddmrp_product_replace.py:0 +#, python-format +msgid "No affected buffers found." +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_ddmrp_product_replace__primary_old_product_id +msgid "Primary Replaced Product" +msgstr "" + +#. module: ddmrp_product_replace +#: model_terms:ir.ui.view,arch_db:ddmrp_product_replace.stock_buffer_view_form +msgid "Product Replacement" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.actions.act_window,name:ddmrp_product_replace.action_ddmrp_product_replace_wizard +#: model:ir.ui.menu,name:ddmrp_product_replace.menu_ddmrp_product_replace +#: model_terms:ir.ui.view,arch_db:ddmrp_product_replace.view_ddmrp_adjustment_sheet_wizard_form +msgid "Product Replacement Tool" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,help:ddmrp_product_replace.field_ddmrp_product_replace__new_product_id +msgid "Product that is going to replace the other one." +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,help:ddmrp_product_replace.field_ddmrp_product_replace__old_product_ids +msgid "Product to be replaced." +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields.selection,name:ddmrp_product_replace.selection__ddmrp_product_replace__mode__use_existing +msgid "Replace product in existing buffers" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_make_procurement_buffer__replaced_by_alert_text +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_stock_buffer__replaced_by_alert_text +msgid "Replaced By Alert Text" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_ddmrp_product_replace__old_product_ids +msgid "Replaced Products" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_stock_buffer__replaced_by_id +msgid "Replaced by" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_stock_buffer__replacement_for_count +msgid "Replacement For Count" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_stock_buffer__is_replacement_product +#: model_terms:ir.ui.view,arch_db:ddmrp_product_replace.stock_buffer_search +msgid "Replacement Product" +msgstr "" + +#. module: ddmrp_product_replace +#: model_terms:ir.ui.view,arch_db:ddmrp_product_replace.stock_buffer_view_form +msgid "Replacement for" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_stock_buffer__replacement_for_ids +msgid "Replaces" +msgstr "" + +#. module: ddmrp_product_replace +#: code:addons/ddmrp_product_replace/wizards/ddmrp_product_replace.py:0 +#, python-format +msgid "Replacing Product" +msgstr "" + +#. module: ddmrp_product_replace +#: model_terms:ir.ui.view,arch_db:ddmrp_product_replace.view_ddmrp_adjustment_sheet_wizard_form +msgid "Select Replaced Products" +msgstr "" + +#. module: ddmrp_product_replace +#: model_terms:ir.ui.view,arch_db:ddmrp_product_replace.view_ddmrp_adjustment_sheet_wizard_form +msgid "Select Replacing Product" +msgstr "" + +#. module: ddmrp_product_replace +#: code:addons/ddmrp_product_replace/wizards/ddmrp_product_replace.py:0 +#, python-format +msgid "" +"Some of the affected buffers have a different product than the replaced " +"ones." +msgstr "" + +#. module: ddmrp_product_replace +#: code:addons/ddmrp_product_replace/wizards/ddmrp_product_replace.py:0 +#, python-format +msgid "Some of the buffers have already been replaced." +msgstr "" + +#. module: ddmrp_product_replace +#: code:addons/ddmrp_product_replace/wizards/make_procurement_buffer.py:0 +#, python-format +msgid "Some products are being replaced:" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model,name:ddmrp_product_replace.model_stock_buffer +msgid "Stock Buffer" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model,name:ddmrp_product_replace.model_stock_move +msgid "Stock Move" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_ddmrp_product_replace__new_product_id +msgid "Substitute Product" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,help:ddmrp_product_replace.field_stock_buffer__replaced_by_id +msgid "" +"The product in this buffer is replaced by the product of selected buffer. When you replace another buffer:\n" +" - Past Demand of the replacement buffer will include the past demand of this product\n" +" - Several buffers can be replaced in chained and coexist at the same time: A replaces B that replaces C, then A aggregates both B and C where B only aggregates C" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,help:ddmrp_product_replace.field_stock_buffer__is_replacement_product +msgid "The product of this buffer is replacing another product" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,help:ddmrp_product_replace.field_stock_buffer__demand_product_ids +msgid "" +"This field is used for a correct product replacement within a DDMRP buffer." +msgstr "" + +#. module: ddmrp_product_replace +#: code:addons/ddmrp_product_replace/models/stock_buffer.py:0 +#, python-format +msgid "This product is replaced by %s." +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields.selection,name:ddmrp_product_replace.selection__ddmrp_product_replace__use_existing__existing +msgid "Use Existing Product" +msgstr "" + +#. module: ddmrp_product_replace +#: model:ir.model.fields,field_description:ddmrp_product_replace.field_ddmrp_product_replace__use_existing +msgid "Use Existing/New Product" +msgstr "" + +#. module: ddmrp_product_replace +#: model_terms:ir.ui.view,arch_db:ddmrp_product_replace.view_ddmrp_adjustment_sheet_wizard_form +msgid "Validate" +msgstr "" + +#. module: ddmrp_product_replace +#: code:addons/ddmrp_product_replace/models/stock_buffer.py:0 +#, python-format +msgid "You cannot create recursive \"Replaced by\" chains." +msgstr "" + +#. module: ddmrp_product_replace +#: model_terms:ir.ui.view,arch_db:ddmrp_product_replace.view_ddmrp_adjustment_sheet_wizard_form +msgid "or" +msgstr "" diff --git a/ddmrp_product_replace/models/__init__.py b/ddmrp_product_replace/models/__init__.py new file mode 100644 index 000000000..911aa93ea --- /dev/null +++ b/ddmrp_product_replace/models/__init__.py @@ -0,0 +1,2 @@ +from . import stock_buffer +from . import stock_move diff --git a/ddmrp_product_replace/models/stock_buffer.py b/ddmrp_product_replace/models/stock_buffer.py new file mode 100644 index 000000000..1784b2a78 --- /dev/null +++ b/ddmrp_product_replace/models/stock_buffer.py @@ -0,0 +1,178 @@ +# Copyright 2019-21 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class StockBuffer(models.Model): + _inherit = "stock.buffer" + + replaced_by_id = fields.Many2one( + comodel_name="stock.buffer", + string="Replaced by", + help="The product in this buffer is replaced by the product of selected " + "buffer. When you replace another buffer:\n - Past Demand of the " + "replacement buffer will include the past demand of this product\n" + " - Several buffers can be replaced in chained and coexist at the " + "same time: A replaces B that replaces C, then A aggregates both B" + " and C where B only aggregates C", + tracking=True, + ) + replaced_by_alert_text = fields.Char( + compute="_compute_replaced_by_alert_text", + ) + replacement_for_ids = fields.One2many( + string="Replaces", + comodel_name="stock.buffer", + inverse_name="replaced_by_id", + ) + replacement_for_count = fields.Integer( + compute="_compute_replacement_for_count", + ) + is_replacement_product = fields.Boolean( + string="Replacement Product", + compute="_compute_is_replacement_product", + store=True, + help="The product of this buffer is replacing another product", + ) + demand_product_ids = fields.Many2many( + comodel_name="product.product", + string="Considered As Demand", + help="This field is used for a correct product replacement within a " + "DDMRP buffer.", + ) + use_replacement_for_buffer_status = fields.Boolean( + string="Include Incoming & On-Hands of replaced products", + compute="_compute_use_replacement_for_buffer_status", + store=True, + readonly=False, + copy=False, + help="If you tick this option, the buffer will consider the incoming " + "and on-hand of all products it replaces and this will impact its " + "NFP.", + ) + + @api.constrains("replaced_by_id") + def _check_replaced_by_id(self): + if not self._check_recursion(parent="replaced_by_id"): + raise ValidationError( + _('You cannot create recursive "Replaced by" chains.') + ) + + @api.constrains("demand_product_ids") + def _check_demand_product_ids(self): + for rec in self: + if rec.demand_product_ids and rec.product_id not in rec.demand_product_ids: + raise ValidationError( + _("Buffered product must be considered as demand.") + ) + + def _compute_replaced_by_alert_text(self): + for rec in self: + if rec.replaced_by_id: + rec.replaced_by_alert_text = ( + _("This product is replaced by %s.") + % rec.replaced_by_id.product_id.display_name + ) + else: + rec.replaced_by_alert_text = "" + + def _compute_replacement_for_count(self): + for rec in self: + rec.replacement_for_count = len(rec.replacement_for_ids) + + @api.depends("replaced_by_id", "replacement_for_ids", "replacement_for_ids.active") + def _compute_is_replacement_product(self): + for rec in self: + rec.is_replacement_product = ( + rec.replacement_for_ids and not rec.replaced_by_id + ) + + @api.depends("item_type") + def _compute_use_replacement_for_buffer_status(self): + for rec in self: + rec.use_replacement_for_buffer_status = rec.item_type in [ + "manufactured", + "purchased", + ] + + @api.depends("item_type") + def _compute_procure_recommended_qty(self): + replaced = self.filtered( + lambda r: r.replaced_by_id and r.item_type in ["manufactured", "purchased"] + ) + res = super(StockBuffer, self - replaced)._compute_procure_recommended_qty() + for rec in replaced: + rec.procure_recommended_qty = 0.0 + return res + + def _past_moves_domain(self, date_from, date_to, locations): + if not self.demand_product_ids: + return super()._past_moves_domain(date_from, date_to, locations) + domain = super()._past_moves_domain(date_from, date_to, locations) + index_replace = False + for n, clause in enumerate(domain): + if isinstance(clause, tuple) and clause[0] == "product_id": + index_replace = n + if isinstance(index_replace, int): + domain[index_replace] = ("product_id", "in", self.demand_product_ids.ids) + return domain + + @api.model + def _recursive_replacement_for_ids(self, buffers): + """Returns the list of buffers being replaced recursively.""" + res = self.env["stock.buffer"] + for rec in buffers: + if rec.replacement_for_ids: + res += self._recursive_replacement_for_ids(rec.replacement_for_ids) + res += rec + return res + + def _compute_product_available_qty(self): + res = super()._compute_product_available_qty() + for rec in self: + if not (rec.use_replacement_for_buffer_status and rec.replacement_for_ids): + continue + for buffer in rec.replacement_for_ids: + replacements_qty = buffer.product_uom._compute_quantity( + buffer.product_location_qty_available_not_res, + rec.product_uom, + round=False, + ) + rec.product_location_qty_available_not_res += replacements_qty + return res + + def _search_stock_moves_incoming_domain(self, outside_dlt=False): + domain = super()._search_stock_moves_incoming_domain(outside_dlt=outside_dlt) + if not (self.use_replacement_for_buffer_status and self.replacement_for_ids): + return domain + index_replace = False + for n, clause in enumerate(domain): + if isinstance(clause, tuple) and clause[0] == "product_id": + index_replace = n + if isinstance(index_replace, int): + domain[index_replace] = ( + "product_id", + "in", + (self + self._recursive_replacement_for_ids(self.replacement_for_ids)) + .mapped("product_id") + .ids, + ) + return domain + + def _search_stock_moves_qualified_demand_domain(self): + domain = super()._search_stock_moves_qualified_demand_domain() + if not self.demand_product_ids: + return domain + domain = [x for x in domain if x[0] != "product_id"] + [ + ("product_id", "in", self.demand_product_ids.ids) + ] + return domain + + def action_view_buffers_replaced(self): + action = self.env.ref("ddmrp.action_stock_buffer") + result = action.read()[0] + result["name"] = _("Buffers Replaced") + result["domain"] = [("id", "in", self.replacement_for_ids.ids)] + return result diff --git a/ddmrp_product_replace/models/stock_move.py b/ddmrp_product_replace/models/stock_move.py new file mode 100644 index 000000000..fdb0e3ef0 --- /dev/null +++ b/ddmrp_product_replace/models/stock_move.py @@ -0,0 +1,22 @@ +# Copyright 2021 ForgeFlow S.L. (http://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo import models + + +class StockMove(models.Model): + _inherit = "stock.move" + + def _find_buffers_to_update_nfp(self): + out_buffers, in_buffers = super()._find_buffers_to_update_nfp() + new_out_buffers = out_buffers + while new_out_buffers: + new_out_buffers = new_out_buffers.mapped("replaced_by_id") + out_buffers |= new_out_buffers + + new_in_buffers = in_buffers + while new_in_buffers: + new_in_buffers = new_in_buffers.mapped("replaced_by_id") + in_buffers |= new_in_buffers + + return out_buffers, in_buffers diff --git a/ddmrp_product_replace/readme/CONTRIBUTORS.rst b/ddmrp_product_replace/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..81608a1f3 --- /dev/null +++ b/ddmrp_product_replace/readme/CONTRIBUTORS.rst @@ -0,0 +1,5 @@ +* Lois Rilo +* Jordi Ballester +* Akim Juillerat +* `Trobz `_: + * Khoi Vo diff --git a/ddmrp_product_replace/readme/CREDITS.rst b/ddmrp_product_replace/readme/CREDITS.rst new file mode 100644 index 000000000..7ac68d53d --- /dev/null +++ b/ddmrp_product_replace/readme/CREDITS.rst @@ -0,0 +1,5 @@ +The initial development of this module has been financially supported by: + +* Aleph Objects, Inc. + +The migration of this module from 13.0 to 14.0 was financially supported by Camptocamp diff --git a/ddmrp_product_replace/readme/DESCRIPTION.rst b/ddmrp_product_replace/readme/DESCRIPTION.rst new file mode 100644 index 000000000..136e83fbc --- /dev/null +++ b/ddmrp_product_replace/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Provides a tool for product replacement. diff --git a/ddmrp_product_replace/readme/ROADMAP.rst b/ddmrp_product_replace/readme/ROADMAP.rst new file mode 100644 index 000000000..63104b15b --- /dev/null +++ b/ddmrp_product_replace/readme/ROADMAP.rst @@ -0,0 +1,2 @@ +* Option to create new buffer and make old ones inactive. +* Consider Demand estimates in the replacement. diff --git a/ddmrp_product_replace/readme/USAGE.rst b/ddmrp_product_replace/readme/USAGE.rst new file mode 100644 index 000000000..fb9ccdd43 --- /dev/null +++ b/ddmrp_product_replace/readme/USAGE.rst @@ -0,0 +1,5 @@ +Go to *Inventory > Configuration > DDMRP > Product Replacement Tool*. + +Then you can fill the wizard options to complete the replacement. There are two +modes of operation: *Create a new buffer for the replacing product* and +*Replace product in existing buffers*. diff --git a/ddmrp_product_replace/security/ir.model.access.csv b/ddmrp_product_replace/security/ir.model.access.csv new file mode 100644 index 000000000..d7badb2ec --- /dev/null +++ b/ddmrp_product_replace/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_ddmrp_product_replace,access_ddmrp_product_replace,model_ddmrp_product_replace,stock.group_stock_user,1,1,1,0 diff --git a/ddmrp_product_replace/static/description/icon.png b/ddmrp_product_replace/static/description/icon.png new file mode 100644 index 000000000..f95fc7269 Binary files /dev/null and b/ddmrp_product_replace/static/description/icon.png differ diff --git a/ddmrp_product_replace/static/description/index.html b/ddmrp_product_replace/static/description/index.html new file mode 100644 index 000000000..d33d592eb --- /dev/null +++ b/ddmrp_product_replace/static/description/index.html @@ -0,0 +1,456 @@ + + + + + + +DDMRP Product Replace + + + +
+

DDMRP Product Replace

+ + +

Beta License: LGPL-3 OCA/ddmrp Translate me on Weblate Try me on Runbot

+

Provides a tool for product replacement.

+

Table of contents

+ +
+

Usage

+

Go to Inventory > Configuration > DDMRP > Product Replacement Tool.

+

Then you can fill the wizard options to complete the replacement. There are two +modes of operation: Create a new buffer for the replacing product and +Replace product in existing buffers.

+
+
+

Known issues / Roadmap

+
    +
  • Option to create new buffer and make old ones inactive.
  • +
  • Consider Demand estimates in the replacement.
  • +
+
+
+

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 smashing it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • ForgeFlow
  • +
+
+
+

Contributors

+ +
+
+

Other credits

+

The initial development of this module has been financially supported by:

+
    +
  • Aleph Objects, Inc.
  • +
+

The migration of this module from 13.0 to 14.0 was financially supported by Camptocamp

+
+
+

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:

+

JordiBForgeFlow LoisRForgeFlow

+

This module is part of the OCA/ddmrp project on GitHub.

+

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

+
+
+
+ + diff --git a/ddmrp_product_replace/tests/__init__.py b/ddmrp_product_replace/tests/__init__.py new file mode 100644 index 000000000..74c5eae89 --- /dev/null +++ b/ddmrp_product_replace/tests/__init__.py @@ -0,0 +1 @@ +from . import test_product_replace diff --git a/ddmrp_product_replace/tests/test_product_replace.py b/ddmrp_product_replace/tests/test_product_replace.py new file mode 100644 index 000000000..6cf490a01 --- /dev/null +++ b/ddmrp_product_replace/tests/test_product_replace.py @@ -0,0 +1,142 @@ +# Copyright 2018 Camptocamp SA +# Copyright 2018-21 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from datetime import datetime, timedelta + +from odoo.addons.ddmrp.tests.common import TestDdmrpCommon + + +class TestDDMRPProductReplace(TestDdmrpCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.buffer = cls.env.ref("ddmrp.stock_buffer_rm01") + cls.old_product = cls.env.ref("ddmrp.product_product_rm01") + cls.put_away_rule_obj = cls.env["stock.putaway.rule"] + cls.old_product.write( + { + "route_ids": [ + (6, 0, [cls.env.ref("mrp.route_warehouse0_manufacture").id]) + ], + } + ) + cls.old_product_putaway = cls.put_away_rule_obj.create( + { + "product_id": cls.old_product.id, + "location_in_id": cls.env.ref("stock.stock_location_stock").id, + "location_out_id": cls.env.ref("stock.stock_location_components").id, + } + ) + # Change adu method: + method = cls.env.ref("ddmrp.adu_calculation_method_past_120") + cls.buffer.adu_calculation_method = method + + def test_01_product_replace_use_existing(self): + date_move = datetime.today() - timedelta(days=30) + picking = self.create_picking_out(self.old_product, date_move, 60) + self._do_picking(picking, date_move) + self.buffer._calc_adu() + adu_previous = 60 / 120 + self.assertEqual(self.buffer.adu, adu_previous) + self.assertEqual(self.buffer.product_id, self.old_product) + self.assertEqual(len(self.buffer.demand_product_ids), 0) + + wiz = self.env["ddmrp.product.replace"].create( + { + "mode": "use_existing", + "old_product_ids": [(6, 0, self.buffer.product_id.ids)], + "use_existing": "new", + "new_product_name": "RM-01 Replacement", + "new_product_default_code": "ABCDE012345", + "copy_route": True, + "copy_putaway": True, + } + ) + self.assertEqual(wiz.buffer_ids, self.buffer) + new_product_id = wiz.button_validate().get("res_id") + new_product = self.env["product.product"].browse(new_product_id) + + self.assertEqual(new_product.name, "RM-01 Replacement") + self.assertEqual(new_product.default_code, "ABCDE012345") + self.assertEqual(new_product.route_ids, self.old_product.route_ids) + + new_product_putaway = self.put_away_rule_obj.search( + [("product_id", "=", new_product.id)] + ) + new_putaway_tuple = ( + new_product_putaway.location_in_id.id, + new_product_putaway.location_out_id.id, + ) + for putaway in self.old_product_putaway: + putaway_tuple = (putaway.location_in_id.id, putaway.location_out_id.id) + self.assertEqual(putaway_tuple, new_putaway_tuple) + + self.assertEqual(self.buffer.product_id, new_product) + self.assertIn(self.old_product, self.buffer.demand_product_ids) + self.buffer._calc_adu() + self.assertEqual(self.buffer.adu, adu_previous) + + def test_02_product_replace_new_buffer(self): + # Complete one delivery + date_move = datetime.today() - timedelta(days=30) + picking = self.create_picking_out(self.old_product, date_move, 60) + self._do_picking(picking, date_move) + # and confirm an incoming + self.create_picking_in(self.old_product, datetime.today(), 30) + self.buffer.cron_actions() + self.assertEqual(self.buffer.product_id, self.old_product) + self.assertEqual(len(self.buffer.demand_product_ids), 0) + old_onhand = self.buffer.product_location_qty_available_not_res + self.assertEqual(old_onhand, -60.0) + old_incoming = self.buffer.incoming_dlt_qty + self.assertEqual(old_incoming, 30.0) + wiz = self.env["ddmrp.product.replace"].create( + { + "mode": "new_buffer", + "old_product_ids": [(6, 0, self.buffer.product_id.ids)], + "use_existing": "new", + "new_product_name": "RM-01 Replacement 2", + "new_product_default_code": "ABC000222", + "copy_route": True, + "copy_putaway": False, + } + ) + self.assertEqual(wiz.buffer_ids, self.buffer) + self.assertFalse(wiz.is_already_replaced) + res = wiz.button_validate() + new_buffer_ids = res.get("domain")[0][2] + model = res.get("res_model") + self.assertEqual(model, "stock.buffer") + new_buffer = self.bufferModel.browse(new_buffer_ids) + self.assertEqual(len(new_buffer), 1) + self.assertNotEqual(self.buffer.id, new_buffer.id) + # Check new product + new_product = new_buffer.product_id + self.assertEqual(new_product.name, "RM-01 Replacement 2") + self.assertEqual(new_product.default_code, "ABC000222") + self.assertEqual(new_product.route_ids, self.old_product.route_ids) + self.assertNotEqual(self.buffer.product_id, new_product) + # Check replacing fields in buffers: + self.assertEqual(self.buffer.replaced_by_id, new_buffer) + self.assertTrue(new_buffer.is_replacement_product) + self.assertTrue(new_buffer.use_replacement_for_buffer_status) + # Check buffer values: + self.buffer.cron_actions() + self.assertEqual(old_onhand, new_buffer.product_location_qty_available_not_res) + self.assertEqual(old_incoming, new_buffer.incoming_dlt_qty) + new_buffer.invalidate_cache() + new_buffer.use_replacement_for_buffer_status = False + new_buffer.cron_actions() + self.assertNotEqual( + old_onhand, new_buffer.product_location_qty_available_not_res + ) + self.assertNotEqual(old_incoming, new_buffer.incoming_dlt_qty) + # Demand: + self.assertIn(self.old_product, new_buffer.demand_product_ids) + self.assertEqual(new_buffer.qualified_demand, 0) + self.create_picking_out(self.old_product, datetime.today(), 11) + self.buffer.cron_actions() + self.assertEqual(self.buffer.qualified_demand, 11) + new_buffer.cron_actions() + self.assertEqual(new_buffer.qualified_demand, 11) diff --git a/ddmrp_product_replace/views/stock_buffer_view.xml b/ddmrp_product_replace/views/stock_buffer_view.xml new file mode 100644 index 000000000..ea85ea0d3 --- /dev/null +++ b/ddmrp_product_replace/views/stock_buffer_view.xml @@ -0,0 +1,87 @@ + + + + + stock.buffer.form - ddmrp_product_replace + stock.buffer + + + + + +
+ +
+ + + + + + + + + + + + + +
+ + stock.buffer.tree - ddmrp_product_replace + stock.buffer + + + + + + + + + + stock.buffer.search - ddmrp_product_replace + stock.buffer + + + + + + + + +
diff --git a/ddmrp_product_replace/wizards/__init__.py b/ddmrp_product_replace/wizards/__init__.py new file mode 100644 index 000000000..67932ed67 --- /dev/null +++ b/ddmrp_product_replace/wizards/__init__.py @@ -0,0 +1,2 @@ +from . import ddmrp_product_replace +from . import make_procurement_buffer diff --git a/ddmrp_product_replace/wizards/ddmrp_product_replace.py b/ddmrp_product_replace/wizards/ddmrp_product_replace.py new file mode 100644 index 000000000..ab0f06b8d --- /dev/null +++ b/ddmrp_product_replace/wizards/ddmrp_product_replace.py @@ -0,0 +1,224 @@ +# Copyright 2017-21 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class DdmrpProductReplace(models.TransientModel): + _name = "ddmrp.product.replace" + _description = "DDMRP Product Replace" + + old_product_ids = fields.Many2many( + comodel_name="product.product", + string="Replaced Products", + help="Product to be replaced.", + required=True, + ondelete="cascade", + ) + multi_product = fields.Boolean(compute="_compute_multi_product") + primary_old_product_id = fields.Many2one( + string="Primary Replaced Product", + comodel_name="product.product", + compute="_compute_primary_old_product_id", + store=True, + readonly=False, + domain="[('id', 'in', old_product_ids)]", + ) + buffer_ids = fields.Many2many( + comodel_name="stock.buffer", + string="Affected Buffers", + readonly=False, + compute="_compute_buffer_ids", + store=True, + ) + is_already_replaced = fields.Boolean( + compute="_compute_is_already_replaced", + ) + new_product_id = fields.Many2one( + comodel_name="product.product", + string="Substitute Product", + help="Product that is going to replace the other one.", + ) + mode = fields.Selection( + selection=[ + ("new_buffer", "Create a new buffer for the replacing product."), + ("use_existing", "Replace product in existing buffers"), + ], + default="new_buffer", + required=True, + ) + use_existing = fields.Selection( + string="Use Existing/New Product", + required=True, + selection=[("existing", "Use Existing Product"), ("new", "Create New Product")], + ) + new_product_name = fields.Char(string="New Product Name") + new_product_default_code = fields.Char(string="New Product Internal Ref.") + copy_route = fields.Boolean(string="Copy Routes") + copy_putaway = fields.Boolean(string="Copy Put Away Strategy") + copy_packaging = fields.Boolean(string="Copy Packaging") + consider_past_demand = fields.Boolean( + string="Consider Old Product Demand", + help="Consider Old product moves as demand for new product", + default=True, + ) + + @api.depends("old_product_ids") + def _compute_multi_product(self): + for rec in self: + rec.multi_product = len(rec.old_product_ids) > 1 + + @api.depends("old_product_ids") + def _compute_primary_old_product_id(self): + for rec in self: + product = fields.first(rec.old_product_ids) + if isinstance(product.id, models.NewId): + # NewId instances are not handled correctly in v13, this is a + # small workaround. In future versions it might not be needed. + product_id = product.id.origin + product = self.env["product.product"].browse(product_id) + rec.primary_old_product_id = product + + @api.depends("old_product_ids") + def _compute_buffer_ids(self): + for rec in self: + rec.buffer_ids = self.env["stock.buffer"].search( + [("product_id", "in", rec.old_product_ids.ids)] + ) + + @api.depends("buffer_ids") + def _compute_is_already_replaced(self): + for rec in self: + rec.is_already_replaced = any(b.replaced_by_id for b in rec.buffer_ids) + + @api.constrains("buffer_ids") + def _check_buffer_ids(self): + for rec in self: + if rec.old_product_ids and any( + b.product_id not in rec.old_product_ids for b in rec.buffer_ids + ): + raise ValidationError( + _( + "Some of the affected buffers have a different product than " + "the replaced ones." + ) + ) + + def _do_replacement_use_existing(self): + vals = { + "product_id": self.new_product_id.id, + } + if self.consider_past_demand: + vals["demand_product_ids"] = [ + (6, 0, (self.old_product_ids + self.new_product_id).ids) + ] + self.buffer_ids.write(vals) + return { + "name": _("Replacing Product"), + "res_id": self.new_product_id.id, + "view_type": "form", + "view_mode": "form", + "res_model": "product.product", + "type": "ir.actions.act_window", + } + + def _do_replacement_new_buffer(self): + primary_old = self.primary_old_product_id + new_buffers = self.env["stock.buffer"] + for replaced in self.buffer_ids.filtered(lambda b: b.product_id == primary_old): + default = dict( + product_id=self.new_product_id.id, + auto_procure=False, + demand_product_ids=False, + ) + replacing = replaced.copy(default=default) + replaced.write({"replaced_by_id": replacing.id}) + new_buffers |= replacing + for replaced in self.buffer_ids.filtered(lambda b: b.product_id != primary_old): + # Do not create buffers for non-primary products. + # Instead assign one of the already created. + replacing = fields.first( + new_buffers.filtered(lambda b: b.location_id == replaced.location_id) + ) + if not replacing: + replacing = new_buffers[0] + replaced.write({"replaced_by_id": replacing.id}) + if self.consider_past_demand: + for buffer in new_buffers: + recursive_buffers = buffer._recursive_replacement_for_ids( + buffer.replacement_for_ids + ) + buffer.write( + { + "demand_product_ids": [ + ( + 6, + 0, + ( + recursive_buffers.mapped("product_id") + + buffer.product_id + ).ids, + ) + ], + } + ) + new_buffers.cron_actions() + return { + "name": _("New Stock Buffers"), + "domain": [("id", "in", new_buffers.ids)], + "view_mode": "tree,form", + "res_model": "stock.buffer", + "type": "ir.actions.act_window", + } + + def button_validate(self): + self.ensure_one() + if self.is_already_replaced: + raise ValidationError(_("Some of the buffers have already been replaced.")) + if not self.buffer_ids: + raise ValidationError(_("No affected buffers found.")) + # Only the first product is used as a template to create new products/buffers. + primary_old = self.primary_old_product_id + if self.use_existing == "new": + default = dict( + name=self.new_product_name, + default_code=self.new_product_default_code, + ) + if not self.copy_route: + default["route_ids"] = None + self.new_product_id = primary_old.copy(default=default) + elif self.use_existing == "existing": + if self.copy_route: + self.new_product_id.write( + {"route_ids": [(6, 0, primary_old.route_ids.ids)]} + ) + # Check if copy putaway strategies is True + if self.copy_putaway: + # Check if there exist putaway strategies for the from product + putaway_ids = self.env["stock.putaway.rule"].search( + [("product_id", "=", primary_old.id)] + ) + if putaway_ids: + # Copy putaway strategies + default_putaway = dict( + product_id=self.new_product_id.id, + ) + for pa in putaway_ids: + pa.copy(default=default_putaway) + if self.copy_packaging: + packs = self.env["product.packaging"].search( + [("product_id", "=", primary_old.id)] + ) + if packs: + default_packs = dict( + product_id=self.new_product_id.id, + ) + for pack in packs: + pack.copy(default=default_packs) + + if self.mode == "use_existing": + res = self._do_replacement_use_existing() + elif self.mode == "new_buffer": + res = self._do_replacement_new_buffer() + return res diff --git a/ddmrp_product_replace/wizards/ddmrp_product_replace_view.xml b/ddmrp_product_replace/wizards/ddmrp_product_replace_view.xml new file mode 100644 index 000000000..a6285eeed --- /dev/null +++ b/ddmrp_product_replace/wizards/ddmrp_product_replace_view.xml @@ -0,0 +1,91 @@ + + + + + ddmrp.product.replace.form + ddmrp.product.replace + +
+ + + + + + +
+

+ When there are more than one product replaced, only the buffer(s) of the primary product in + this list will be used to create the buffer(s) and/or product(s) for replacement. +

+
+ + + + + + + + + + + +
+ + + + + + + + + + +
+
+ +
+
+ + Product Replacement Tool + ddmrp.product.replace + + form + new + + +
diff --git a/ddmrp_product_replace/wizards/make_procurement_buffer.py b/ddmrp_product_replace/wizards/make_procurement_buffer.py new file mode 100644 index 000000000..24ae58bb2 --- /dev/null +++ b/ddmrp_product_replace/wizards/make_procurement_buffer.py @@ -0,0 +1,35 @@ +# Copyright 2021 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo import _, api, fields, models + + +class MakeProcurementBuffer(models.TransientModel): + _inherit = "make.procurement.buffer" + + has_replaced_buffers = fields.Boolean( + compute="_compute_replaced_by_alert_text", + ) + replaced_by_alert_text = fields.Text( + compute="_compute_replaced_by_alert_text", + ) + + @api.depends("item_ids") + def _compute_replaced_by_alert_text(self): + for rec in self: + if any(r.buffer_id.replaced_by_id for r in rec.item_ids): + rec.has_replaced_buffers = True + alert_text = _("Some products are being replaced:") + for item in rec.item_ids: + if item.buffer_id.replaced_by_id: + replacement = item.buffer_id.replaced_by_id + alert_text += "\n - {} ({}) replaced by {} ({})".format( + item.buffer_id.display_name, + item.buffer_id.product_id.display_name, + replacement.display_name, + replacement.product_id.display_name, + ) + rec.replaced_by_alert_text = alert_text + else: + rec.has_replaced_buffers = False + rec.replaced_by_alert_text = "" diff --git a/ddmrp_product_replace/wizards/make_procurement_buffer_view.xml b/ddmrp_product_replace/wizards/make_procurement_buffer_view.xml new file mode 100644 index 000000000..75ea5e764 --- /dev/null +++ b/ddmrp_product_replace/wizards/make_procurement_buffer_view.xml @@ -0,0 +1,30 @@ + + + + Request Procurement + make.procurement.buffer + + + + + + + + + + + diff --git a/setup/ddmrp_product_replace/odoo/addons/ddmrp_product_replace b/setup/ddmrp_product_replace/odoo/addons/ddmrp_product_replace new file mode 120000 index 000000000..f7c31c8d7 --- /dev/null +++ b/setup/ddmrp_product_replace/odoo/addons/ddmrp_product_replace @@ -0,0 +1 @@ +../../../../ddmrp_product_replace \ No newline at end of file diff --git a/setup/ddmrp_product_replace/setup.py b/setup/ddmrp_product_replace/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/ddmrp_product_replace/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)