diff --git a/mrp_bom_hierarchy/__init__.py b/mrp_bom_hierarchy/__init__.py index a85801536..9186ee3ad 100644 --- a/mrp_bom_hierarchy/__init__.py +++ b/mrp_bom_hierarchy/__init__.py @@ -1,20 +1 @@ -############################################################################## -# -# Copyright (C) 2015 Eficent () -# -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## from . import model diff --git a/mrp_bom_hierarchy/__manifest__.py b/mrp_bom_hierarchy/__manifest__.py index 07c2e141d..c24525cea 100644 --- a/mrp_bom_hierarchy/__manifest__.py +++ b/mrp_bom_hierarchy/__manifest__.py @@ -1,35 +1,18 @@ -############################################################################## -# -# Copyright (C) 2015 Eficent () -# -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## +# Copyright 2015-22 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + { - "name": "MRP BOM Hierarchy", - "version": "1.0", - "author": "Eficent, Odoo Community Association (OCA)", + "name": "MRP BoM Hierarchy", + "summary": "Make it easy to navigate through BoM hierarchy.", + "version": "14.0.1.0.0", + "author": "ForgeFlow, Odoo Community Association (OCA)", "category": "Manufacturing", "depends": ["mrp"], "website": "https://github.com/OCA/manufacture", "license": "AGPL-3", - "demo": [], "data": [ "view/mrp.xml", ], - "test": [], "installable": True, "auto_install": False, } diff --git a/mrp_bom_hierarchy/model/__init__.py b/mrp_bom_hierarchy/model/__init__.py index 4bc3d01cd..a352efe9b 100644 --- a/mrp_bom_hierarchy/model/__init__.py +++ b/mrp_bom_hierarchy/model/__init__.py @@ -1,20 +1 @@ -############################################################################## -# -# Copyright (C) 2014 Eficent () -# -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## -from . import mrp +from . import mrp_bom diff --git a/mrp_bom_hierarchy/model/mrp.py b/mrp_bom_hierarchy/model/mrp.py deleted file mode 100644 index 2b28aa247..000000000 --- a/mrp_bom_hierarchy/model/mrp.py +++ /dev/null @@ -1,280 +0,0 @@ -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -from odoo import fields, models - - -class Bom(models.Model): - _inherit = "mrp.bom" - - def get_child_boms(self, cr, uid, ids, context=None): - result = {} - if not ids: - return result - for curr_id in ids: - result[curr_id] = True - # Now add the children - cr.execute( - """ - WITH RECURSIVE children AS ( - SELECT bom_id, id - FROM mrp_bom - WHERE bom_id IN %s - UNION ALL - SELECT a.bom_id, a.id - FROM mrp_bom a - JOIN children b ON(a.bom_id = b.id) - ) - SELECT * FROM children order by bom_id - """, - (tuple(ids),), - ) - res = cr.fetchall() - for _x, y in res: - result[y] = True - return result - - def _get_boms_from_product(self, cr, uid, ids, context=None): - result = {} - bom_obj = self.pool.get("mrp.bom") - for p in ids: - product_bom_ids = bom_obj.search(cr, uid, [("product_id", "=", p)]) - bom_ids = bom_obj.get_child_boms(cr, uid, product_bom_ids, context=context) - for bom_id in bom_ids: - result[bom_id] = True - return result - - def _is_bom(self, cr, uid, ids, name, arg, context=None): - result = {} - if context is None: - context = {} - for bom in self.browse(cr, uid, ids, context=context): - result[bom.id] = False - if bom.bom_lines: - result[bom.id] = True - return result - - def _bom_hierarchy_indent_calc(self, cr, uid, ids, prop, unknow_none, unknow_dict): - if not ids: - return [] - res = [] - for bom in self.browse(cr, uid, ids, context=None): - data = [] - b = bom - while b: - if b.name and b.bom_id: - data.insert(0, ">") - else: - data.insert(0, "") - - b = b.bom_id - data = "".join(data) - res.append((bom.id, data)) - return dict(res) - - def _complete_bom_hierarchy_code_calc( - self, cr, uid, ids, prop, unknow_none, unknow_dict - ): - if not ids: - return [] - res = [] - for bom in self.browse(cr, uid, ids, context=None): - data = [] - b = bom - while b: - if b.code: - data.insert(0, b.code) - elif b.position: - data.insert(0, b.position) - elif b.product_id.default_code: - data.insert(0, b.product_id.default_code) - else: - data.insert(0, "") - - b = b.bom_id - data = " / ".join(data) - data = "[" + data + "] " - - res.append((bom.id, data)) - return dict(res) - - def _complete_bom_hierarchy_name_calc( - self, cr, uid, ids, prop, unknow_none, unknow_dict - ): - if not ids: - return [] - res = [] - for bom in self.browse(cr, uid, ids, context=None): - data = [] - b = bom - while b: - if b.name: - data.insert(0, b.name) - elif b.product_id.name: - data.insert(0, b.product_id.name) - else: - data.insert(0, "") - - b = b.bom_id - - data = " / ".join(data) - res.append((bom.id, data)) - return dict(res) - - def _is_parent(self, cr, uid, ids, prop, unknow_none, unknow_dict): - res = {} - for bom in self.browse(cr, uid, ids, context=None): - if not bom.bom_id: - res[bom.id] = True - else: - res[bom.id] = False - return res - - def _product_has_own_bom(self, cr, uid, ids, prop, unknow_none, unknow_dict): - res = {} - for bom in self.browse(cr, uid, ids, context=None): - bom_ids = self.pool.get("mrp.bom").search( - cr, - uid, - [("product_id", "=", bom.product_id.id), ("bom_id", "=", False)], - context=None, - ) - if bom_ids: - res[bom.id] = True - else: - res[bom.id] = False - return res - - _columns = { - "is_parent": fields.function( - _is_parent, - string="Is parent BOM", - type="boolean", - readonly=True, - store=True, - ), - "has_child": fields.function( - _is_bom, string="Has components", type="boolean", readonly=True - ), - "product_has_own_bom": fields.function( - _product_has_own_bom, - string="Product has own BOM", - type="boolean", - readonly=True, - ), - "bom_hierarchy_indent": fields.function( - _bom_hierarchy_indent_calc, - method=True, - type="char", - string="Level", - size=32, - readonly=True, - ), - "complete_bom_hierarchy_code": fields.function( - _complete_bom_hierarchy_code_calc, - method=True, - type="char", - string="Complete Reference", - size=250, - help="Describes the full path of this " - "component within the BOM hierarchy using the BOM reference.", - store={ - "mrp.bom": (get_child_boms, ["name", "code", "position", "bom_id"], 20), - "product.product": (_get_boms_from_product, ["default_code"], 20), - }, - ), - "complete_bom_hierarchy_name": fields.function( - _complete_bom_hierarchy_name_calc, - method=True, - type="char", - string="Complete Name", - size=250, - help="Describes the full path of this " - "component within the BOM hierarchy using the BOM name.", - store={ - "mrp.bom": (get_child_boms, ["name", "bom_id"], 20), - "product.product": (_get_boms_from_product, ["name"], 20), - }, - ), - } - - _order = "complete_bom_hierarchy_code" - - def action_openChildTreeView(self, cr, uid, ids, context=None): - """ - :return dict: dictionary value for created view - """ - if context is None: - context = {} - bom = self.browse(cr, uid, ids[0], context) - child_bom_ids = self.pool.get("mrp.bom").search( - cr, uid, [("bom_id", "=", bom.id)] - ) - res = self.pool.get("ir.actions.act_window").for_xml_id( - cr, uid, "mrp_bom_hierarchy", "action_mrp_bom_hierarchy_tree2", context - ) - res["context"] = { - "default_bom_id": bom.id, - } - res["domain"] = "[('id', 'in', [" + ",".join(map(str, child_bom_ids)) + "])]" - res["nodestroy"] = False - return res - - def action_openParentTreeView(self, cr, uid, ids, context=None): - """ - :return dict: dictionary value for created view - """ - if context is None: - context = {} - bom = self.browse(cr, uid, ids[0], context) - res = self.pool.get("ir.actions.act_window").for_xml_id( - cr, uid, "mrp_bom_hierarchy", "action_mrp_bom_hierarchy_tree2", context - ) - if bom.bom_id: - for parent_bom_id in self.pool.get("mrp.bom").search( - cr, uid, [("id", "=", bom.bom_id.id)] - ): - res["domain"] = "[('id','='," + str(parent_bom_id) + ")]" - res["nodestroy"] = False - return res - - def action_openProductBOMTreeView(self, cr, uid, ids, context=None): - """ - :return dict: dictionary value for created view - """ - if context is None: - context = {} - bom = self.browse(cr, uid, ids[0], context) - product_bom_ids = self.pool.get("mrp.bom").search( - cr, - uid, - [("product_id", "=", bom.product_id.id), ("bom_id", "=", False)], - context=context, - ) - res = self.pool.get("ir.actions.act_window").for_xml_id( - cr, uid, "mrp", "mrp_bom_form_action2", context - ) - - res["context"] = { - "default_product_id": bom.product_id.id, - } - res["domain"] = "[('id', 'in', [" + ",".join(map(str, product_bom_ids)) + "])]" - res["nodestroy"] = False - return res diff --git a/mrp_bom_hierarchy/model/mrp_bom.py b/mrp_bom_hierarchy/model/mrp_bom.py new file mode 100644 index 000000000..8e90af23c --- /dev/null +++ b/mrp_bom_hierarchy/model/mrp_bom.py @@ -0,0 +1,263 @@ +# Copyright 2015-22 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). +import operator as py_operator + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class MrpBom(models.Model): + _inherit = "mrp.bom" + _order = "sequence, code, product_default_code, id" + + @api.depends("bom_line_ids.bom_id", "product_id", "product_tmpl_id") + def _compute_product_has_other_bom(self): + for bom in self: + if bom.product_id: + bom_ids = self.env["mrp.bom"].search( + [("product_id", "=", bom.product_id.id), ("id", "!=", bom.id)], + ) + else: + bom_ids = self.env["mrp.bom"].search( + [ + ("product_tmpl_id", "=", bom.product_tmpl_id.id), + ("id", "!=", bom.id), + ], + ) + if bom_ids: + bom.product_has_other_bom = True + else: + bom.product_has_other_bom = False + + @api.depends("bom_line_ids.bom_id", "product_id", "product_tmpl_id") + def _compute_parent_bom_ids(self): + for bom in self: + parent_bom_line_ids = self.env["mrp.bom.line"]._bom_line_find( + product_tmpl=bom.product_id.product_tmpl_id or bom.product_tmpl_id, + product=bom.product_id, + ) + if parent_bom_line_ids: + bom.parent_bom_ids = parent_bom_line_ids.bom_id + bom.has_parent = True + else: + bom.parent_bom_ids = False + bom.has_parent = False + + @api.depends("bom_line_ids.bom_id", "bom_line_ids.product_id") + def _compute_child_bom_ids(self): + for bom in self: + bom_line_ids = bom.bom_line_ids + bom.child_bom_ids = bom_line_ids.child_bom_id + bom.has_child = bool(bom.child_bom_ids) + + def _search_has_child(self, operator, value): + if operator not in ["=", "!="]: + raise UserError(_("This operator is not supported")) + if value == "True": + value = True + elif value == "False": + value = False + if not isinstance(value, bool): + raise UserError(_("Value should be True or False (not %s)") % value) + ops = {"=": py_operator.eq, "!=": py_operator.ne} + ids = [] + for bom in self.search([]): + if ops[operator](value, bom.has_child): + ids.append(bom.id) + return [("id", "in", ids)] + + def _search_has_parent(self, operator, value): + if operator not in ["=", "!="]: + raise UserError(_("This operator is not supported")) + if value == "True": + value = True + elif value == "False": + value = False + if not isinstance(value, bool): + raise UserError(_("Value should be True or False (not %s)") % value) + ops = {"=": py_operator.eq, "!=": py_operator.ne} + ids = [] + for bom in self.search([]): + if ops[operator](value, bom.has_parent): + ids.append(bom.id) + return [("id", "in", ids)] + + @api.depends( + "product_id", + "product_id.default_code", + "product_id.product_tmpl_id.default_code", + "product_tmpl_id.default_code", + ) + def _compute_internal_reference(self): + for bom in self: + bom.product_default_code = ( + bom.product_id.default_code + or bom.product_id.product_tmpl_id.default_code + or bom.product_tmpl_id.default_code + ) + + child_bom_ids = fields.One2many("mrp.bom", compute="_compute_child_bom_ids") + parent_bom_ids = fields.One2many("mrp.bom", compute="_compute_parent_bom_ids") + has_child = fields.Boolean( + string="Has components", + compute="_compute_child_bom_ids", + search="_search_has_child", + ) + has_parent = fields.Boolean( + string="Is component", + compute="_compute_parent_bom_ids", + search="_search_has_parent", + ) + product_has_other_bom = fields.Boolean( + string="Product has other BoMs", + compute="_compute_product_has_other_bom", + ) + product_default_code = fields.Char( + string="Internal Reference", + compute="_compute_internal_reference", + store="True", + ) + + def action_open_child_tree_view(self): + self.ensure_one() + res = self.env["ir.actions.actions"]._for_xml_id("mrp.mrp_bom_form_action") + res["context"] = {"default_bom_line_ids": self.bom_line_ids.ids} + if self.child_bom_ids: + res["domain"] = ( + "[('id', 'in', [" + ",".join(map(str, self.child_bom_ids.ids)) + "])]" + ) + return res + + def action_open_parent_tree_view(self): + self.ensure_one() + res = self.env["ir.actions.actions"]._for_xml_id("mrp.mrp_bom_form_action") + if self.parent_bom_ids: + res["domain"] = ( + "[('id', 'in', [" + ",".join(map(str, self.parent_bom_ids.ids)) + "])]" + ) + return res + + def action_open_product_other_bom_tree_view(self): + self.ensure_one() + if self.product_id: + product_bom_ids = self.env["mrp.bom"].search( + [("product_id", "=", self.product_id.id), ("id", "!=", self.id)], + ) + else: + product_bom_ids = self.env["mrp.bom"].search( + [ + ("product_tmpl_id", "=", self.product_tmpl_id.id), + ("id", "!=", self.id), + ], + ) + res = self.env["ir.actions.actions"]._for_xml_id("mrp.mrp_bom_form_action") + if self.product_id: + res["context"] = { + "default_product_id": self.product_id.id, + "default_product_tmpl_id": self.product_id.product_tmpl_id.id, + } + elif self.product_tmpl_id: + res["context"] = { + "default_product_tmpl_id": self.product_tmpl_id.id, + } + res["domain"] = ( + "[('id', 'in', [" + ",".join(map(str, product_bom_ids.ids)) + "])]" + ) + return res + + +class MrpBomLine(models.Model): + _inherit = "mrp.bom.line" + + has_bom = fields.Boolean( + string="Has sub BoM", + compute="_compute_child_bom_id", + ) + + @api.depends("product_id", "bom_id") + def _compute_child_bom_id(self): + super()._compute_child_bom_id() + for line in self: + line.has_bom = bool(line.child_bom_id) + + def action_open_product_bom_tree_view(self): + self.ensure_one() + res = self.env["ir.actions.actions"]._for_xml_id("mrp.mrp_bom_form_action") + res["domain"] = ( + "[('id', 'in', [" + ",".join(map(str, self.child_bom_id.ids)) + "])]" + ) + return res + + @api.model + def _bom_line_find_domain( + self, + product_tmpl=None, + product=None, + picking_type=None, + company_id=False, + bom_type=False, + ): + if product: + if not product_tmpl: + product_tmpl = product.product_tmpl_id + domain = [ + "|", + ("product_id", "=", product.id), + "&", + ("product_id", "=", False), + ("product_tmpl_id", "=", product_tmpl.id), + ] + elif product_tmpl: + domain = [("product_tmpl_id", "=", product_tmpl.id)] + else: + # neither product nor template, makes no sense to search + raise UserError( + _( + "You should provide either a product or " + "a product template to search a BoM Line" + ) + ) + if picking_type: + domain += [ + "|", + ("bom_id.picking_type_id", "=", picking_type.id), + ("bom_id.picking_type_id", "=", False), + ] + if company_id or self.env.context.get("company_id"): + domain = domain + [ + "|", + ("company_id", "=", False), + ("company_id", "=", company_id or self.env.context.get("company_id")), + ] + if bom_type: + domain += [("bom_id.type", "=", bom_type)] + # order to prioritize bom line with product_id over the one without + return domain + + @api.model + def _bom_line_find( + self, + product_tmpl=None, + product=None, + picking_type=None, + company_id=False, + bom_type=False, + ): + """Finds BoM lines for particular product, picking and company""" + if ( + product + and product.type == "service" + or product_tmpl + and product_tmpl.type == "service" + ): + return self.env["mrp.bom.line"] + domain = self._bom_line_find_domain( + product_tmpl=product_tmpl, + product=product, + picking_type=picking_type, + company_id=company_id, + bom_type=bom_type, + ) + if domain is False: + return self.env["mrp.bom.line"] + return self.search(domain, order="sequence, product_id") diff --git a/mrp_bom_hierarchy/readme/CONTRIBUTORS.rst b/mrp_bom_hierarchy/readme/CONTRIBUTORS.rst index c84e2e8e0..67d9a1a59 100644 --- a/mrp_bom_hierarchy/readme/CONTRIBUTORS.rst +++ b/mrp_bom_hierarchy/readme/CONTRIBUTORS.rst @@ -1 +1,2 @@ * Jordi Ballester Alomar +* Miquel Raïch Regué diff --git a/mrp_bom_hierarchy/readme/DESCRIPTION.rst b/mrp_bom_hierarchy/readme/DESCRIPTION.rst index 8e34ee0ef..966b69c16 100644 --- a/mrp_bom_hierarchy/readme/DESCRIPTION.rst +++ b/mrp_bom_hierarchy/readme/DESCRIPTION.rst @@ -1,12 +1,5 @@ -This module was written to extend the functionality of Bill of -Materials to support users to better maintain the BOM hierarchy. +This module extends the functionality of Bill of Materials +to support users to better maintain the BoM hierarchy. -This module replaces the existing BOM tree views with a new one, from -which the user can create a complete BOM hierarchy. - -The user can navigate from the tree view to child BOM's, or to the -product's BOM components with a single click. - -The user can now search using the field 'Complete Reference' (or Name) to -find all the BOM hierarchy associated to a particular BOM Reference (or -Name) at once. +The user can navigate from the tree view to child's BoM or parent's BoM, +or to the product's BoM components with a single click. diff --git a/mrp_bom_hierarchy/readme/USAGE.rst b/mrp_bom_hierarchy/readme/USAGE.rst index ef67c3843..2525293d9 100644 --- a/mrp_bom_hierarchy/readme/USAGE.rst +++ b/mrp_bom_hierarchy/readme/USAGE.rst @@ -1,2 +1,2 @@ To use this module, you need to go to 'Manufacturing | Products | Bill of -Materials Hierarchy'. +Materials'. diff --git a/mrp_bom_hierarchy/static/description/index.html b/mrp_bom_hierarchy/static/description/index.html index 82a9f1a8a..c21defba0 100644 --- a/mrp_bom_hierarchy/static/description/index.html +++ b/mrp_bom_hierarchy/static/description/index.html @@ -1,19 +1,19 @@
-

MRP BOM Hierarchy

+

MRP BoM Hierarchy

This module was written to extend the functionality of Bill of -Materials to support users to better maintain the BOM hierarchy. +Materials to support users to better maintain the BoM hierarchy. -This module replaces the existing BOM tree views with a new one, from -which the user can create a complete BOM hierarchy. +This module replaces the existing BoM tree views with a new one, from +which the user can create a complete BoM hierarchy. -The user can navigate from the tree view to child BOM's, or to the -product's BOM components with a single click. +The user can navigate from the tree view to child BoM's, or to the +product's BoM components with a single click. The user can now search using the field 'Complete Reference' (or Name) to -find all the BOM hierarchy associated to a particular BOM Reference (or +find all the BoM hierarchy associated to a particular BoM Reference (or Name) at once.

@@ -76,7 +76,7 @@ Materials Hierarchy'

Contributors

diff --git a/mrp_bom_hierarchy/view/mrp.xml b/mrp_bom_hierarchy/view/mrp.xml index 421766d9a..fff266ab5 100644 --- a/mrp_bom_hierarchy/view/mrp.xml +++ b/mrp_bom_hierarchy/view/mrp.xml @@ -1,6 +1,5 @@ - mrp.bom.form @@ -8,17 +7,17 @@ - - +