diff --git a/mrp_bom_current_stock/README.rst b/mrp_bom_current_stock/README.rst index ac566048..3c3742cd 100644 --- a/mrp_bom_current_stock/README.rst +++ b/mrp_bom_current_stock/README.rst @@ -7,7 +7,7 @@ MRP BoM Current Stock !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:a758367d719d80a03cc4e0f8bdebf4a565bcd256d60a4ca7f86e507545656c39 + !! source digest: sha256:0d4a33388dea8f7e38fda18342bed56a3f53bde3e2dc6098bf6e8e5a2445143b !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png @@ -70,6 +70,7 @@ Contributors * Lois Rilo * Héctor Villarreal +* Bernat Puig Maintainers ~~~~~~~~~~~ diff --git a/mrp_bom_current_stock/__manifest__.py b/mrp_bom_current_stock/__manifest__.py index e83b1b5c..c1b5b6dc 100644 --- a/mrp_bom_current_stock/__manifest__.py +++ b/mrp_bom_current_stock/__manifest__.py @@ -11,7 +11,7 @@ "website": "https://github.com/OCA/manufacture-reporting", "author": "ForgeFlow, Odoo Community Association (OCA)", "license": "AGPL-3", - "depends": ["mrp_bom_location", "report_xlsx"], + "depends": ["mrp", "report_xlsx", "stock_helper"], "data": [ "security/ir.model.access.csv", "reports/report_mrpcurrentstock.xml", diff --git a/mrp_bom_current_stock/i18n/mrp_bom_current_stock.pot b/mrp_bom_current_stock/i18n/mrp_bom_current_stock.pot index 34f1d3e8..9419b095 100644 --- a/mrp_bom_current_stock/i18n/mrp_bom_current_stock.pot +++ b/mrp_bom_current_stock/i18n/mrp_bom_current_stock.pot @@ -87,7 +87,7 @@ msgid "Explode" msgstr "" #. module: mrp_bom_current_stock -#: model:ir.model.fields,field_description:mrp_bom_current_stock.field_mrp_bom_current_stock_line__explosion_id +#: model:ir.model.fields,field_description:mrp_bom_current_stock.field_mrp_bom_current_stock_line__wizard_id msgid "Explosion" msgstr "" diff --git a/mrp_bom_current_stock/readme/CONTRIBUTORS.rst b/mrp_bom_current_stock/readme/CONTRIBUTORS.rst index e527f867..44e5928b 100644 --- a/mrp_bom_current_stock/readme/CONTRIBUTORS.rst +++ b/mrp_bom_current_stock/readme/CONTRIBUTORS.rst @@ -1,2 +1,3 @@ * Lois Rilo * Héctor Villarreal +* Bernat Puig diff --git a/mrp_bom_current_stock/reports/report_mrpcurrentstock.xml b/mrp_bom_current_stock/reports/report_mrpcurrentstock.xml index d87f1b3f..850c7e18 100644 --- a/mrp_bom_current_stock/reports/report_mrpcurrentstock.xml +++ b/mrp_bom_current_stock/reports/report_mrpcurrentstock.xml @@ -18,8 +18,9 @@ Level Product Quantity - Location Qty Available (Location) + Potential Qty + Location @@ -58,15 +59,22 @@ /> - + + - + + + + diff --git a/mrp_bom_current_stock/reports/report_mrpcurrentstock_xlsx.py b/mrp_bom_current_stock/reports/report_mrpcurrentstock_xlsx.py index 20390cc4..b5920f10 100644 --- a/mrp_bom_current_stock/reports/report_mrpcurrentstock_xlsx.py +++ b/mrp_bom_current_stock/reports/report_mrpcurrentstock_xlsx.py @@ -18,14 +18,15 @@ class ReportMrpBomCurrentStockXlsx(models.AbstractModel): def _print_bom_children(ch, sheet, row): i = row sheet.write(i, 0, ch.bom_level or "") - sheet.write(i, 1, ch.bom_line.bom_id.code or "") + sheet.write(i, 1, ch.bom_id.code or "") sheet.write(i, 2, ch.product_id.product_tmpl_id.display_name or "") sheet.write(i, 3, ch.product_qty or "") sheet.write(i, 4, ch.qty_available_in_source_loc or 0.0) - sheet.write(i, 5, ch.product_uom_id.name or "") - sheet.write(i, 6, ch.location_id.name or "") - sheet.write(i, 7, ch.bom_id.code or "") - sheet.write(i, 8, ch.bom_id.product_tmpl_id.display_name or "") + sheet.write(i, 5, ch.potential_qty or 0.0) + sheet.write(i, 6, ch.product_uom_id.name or "") + sheet.write(i, 7, ch.location_id.name or "") + sheet.write(i, 8, ch.parent_bom_id.code or "") + sheet.write(i, 9, ch.parent_bom_id.product_tmpl_id.display_name or "") i += 1 return i @@ -40,10 +41,10 @@ def generate_xlsx_report(self, workbook, data, objects): sheet.set_column(0, 0, 5) sheet.set_column(1, 2, 40) sheet.set_column(3, 3, 10) - sheet.set_column(4, 4, 20) - sheet.set_column(5, 5, 7) - sheet.set_column(6, 6, 20) - sheet.set_column(7, 8, 40) + sheet.set_column(4, 5, 20) + sheet.set_column(6, 6, 7) + sheet.set_column(7, 7, 20) + sheet.set_column(8, 9, 40) title_style = workbook.add_format( {"bold": True, "bg_color": "#FFFFCC", "bottom": 1} @@ -54,6 +55,7 @@ def generate_xlsx_report(self, workbook, data, objects): _("Product Reference"), _("Quantity"), _("Qty Available (Location)"), + _("Potential Qty"), _("UoM"), _("Location"), _("Parent BoM Ref"), @@ -69,10 +71,11 @@ def generate_xlsx_report(self, workbook, data, objects): sheet.write(i, 0, "0", bold) sheet.write(i, 1, o.bom_id.code or "", bold) sheet.write(i, 2, o.product_tmpl_id.name or "", bold) - sheet.write(i, 3, o.product_qty or "", bold) - sheet.write(i, 5, o.product_uom_id.name or "", bold) - sheet.write(i, 6, o.location_id.name or "", bold) + sheet.write(i, 4, o.qty_available_in_source_loc or 0.0, bold) + sheet.write(i, 5, o.potential_qty or 0.0, bold) + sheet.write(i, 6, o.product_uom_id.name or "", bold) + sheet.write(i, 7, o.location_id.name or "", bold) i += 1 for ch in o.line_ids: i = self._print_bom_children(ch, sheet, i) diff --git a/mrp_bom_current_stock/static/description/index.html b/mrp_bom_current_stock/static/description/index.html index fa9807ad..ed85b39d 100644 --- a/mrp_bom_current_stock/static/description/index.html +++ b/mrp_bom_current_stock/static/description/index.html @@ -367,7 +367,7 @@

MRP BoM Current Stock

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:a758367d719d80a03cc4e0f8bdebf4a565bcd256d60a4ca7f86e507545656c39 +!! source digest: sha256:0d4a33388dea8f7e38fda18342bed56a3f53bde3e2dc6098bf6e8e5a2445143b !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Beta License: AGPL-3 OCA/manufacture-reporting Translate me on Weblate Try me on Runboat

This modules extend the Manufacturing App adding a report that explodes the @@ -417,6 +417,7 @@

Contributors

diff --git a/mrp_bom_current_stock/tests/test_mrp_bom_current_stock.py b/mrp_bom_current_stock/tests/test_mrp_bom_current_stock.py index 66608a13..d0f60005 100644 --- a/mrp_bom_current_stock/tests/test_mrp_bom_current_stock.py +++ b/mrp_bom_current_stock/tests/test_mrp_bom_current_stock.py @@ -181,7 +181,7 @@ def test_wizard(self): self.wizard.do_explode() sol = (1, 1, 200, 0.01, 1, 2.4) lines = self.wizard.line_ids - self.assertEquals(self.wizard.location_id, self.stock_loc) + self.assertEqual(self.wizard.location_id, self.stock_loc) for i, line in enumerate(lines): self.assertEqual(line.product_qty, sol[i]) self._product_change_qty(line.product_id, line.product_qty) diff --git a/mrp_bom_current_stock/wizard/bom_route_current_stock.py b/mrp_bom_current_stock/wizard/bom_route_current_stock.py index 46df10c5..f7a0f4e7 100644 --- a/mrp_bom_current_stock/wizard/bom_route_current_stock.py +++ b/mrp_bom_current_stock/wizard/bom_route_current_stock.py @@ -2,6 +2,8 @@ # Copyright 2017-20 ForgeFlow S.L. (https://www.forgeflow.com) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import math + from odoo import api, fields, models @@ -9,9 +11,6 @@ class BomRouteCurrentStock(models.TransientModel): _name = "mrp.bom.current.stock" _description = "MRP Bom Route Current Stock" - bom_id = fields.Many2one( - comodel_name="mrp.bom", string="Starting Bill of Materials", required=True - ) product_id = fields.Many2one( comodel_name="product.product", string="Product Variant", @@ -23,6 +22,9 @@ class BomRouteCurrentStock(models.TransientModel): string="Product Template", related="product_id.product_tmpl_id", ) + bom_id = fields.Many2one( + comodel_name="mrp.bom", string="Starting Bill of Materials", required=True + ) product_qty = fields.Float( related="bom_id.product_qty", digits="Product Unit of Measure" ) @@ -30,59 +32,139 @@ class BomRouteCurrentStock(models.TransientModel): comodel_name="uom.uom", related="bom_id.product_uom_id" ) location_id = fields.Many2one( - comodel_name="stock.location", string="Starting location" + comodel_name="stock.location", string="Source location" + ) + exclude_location_ids = fields.Many2many( + comodel_name="stock.location", + string="Exclude locations", + domain="[('id', 'child_of', location_id), ('id', '!=', location_id)]", + help="Select only the parent location you want to exclude.", ) line_ids = fields.One2many( - comodel_name="mrp.bom.current.stock.line", inverse_name="explosion_id" + comodel_name="mrp.bom.current.stock.line", + inverse_name="wizard_id", + ) + explosion_type = fields.Selection( + [("one", "One Level"), ("all", "All Levels")], default="all" + ) + desired_qty = fields.Integer(string="Desired Quantity", default=1) + qty_available_in_source_loc = fields.Float( + string="Qty Available in Source", + compute="_compute_qty_available_in_source_loc", + store=True, + ) + potential_qty = fields.Float() + potential_qty_rounded = fields.Integer( + string="Potential Quantity", + help="Potential quantity that could be manufactured given all current " + "on-hand stock at the specified locations.", + compute="_compute_potential_qty_rounded", + store="True", + ) + availability = fields.Selection( + [("available", "Available"), ("not_available", "Not Available")], + compute="_compute_availability", + store=True, + help="The field shows the availability that you have taking into " + "account the potential quantity that could end up being manufactured.", ) @api.onchange("product_id") def _onchange_product_id(self): if self.product_id: - self.bom_id = self.env["mrp.bom"]._bom_find(product_tmpl=self.product_id) + self.bom_id = self.env["mrp.bom"]._bom_find(product=self.product_id) + + def _get_exclude_locations_qty(self, product_id, location_id): + qty = 0 + for exclude_location_id in self.exclude_location_ids: + if exclude_location_id.is_sublocation_of(location_id): + qty += product_id.with_context( + location=exclude_location_id.id + ).qty_available + return qty + + def _get_outgoing_reservation_qty(self, product_id, location_id): + # Copy of stock.buffer.`_get_outgoing_reservation_qty` method. + domain = [ + ("product_id", "=", product_id.id), + ("state", "in", ("partially_available", "assigned")), + ] + lines = self.env["stock.move.line"].search(domain) + lines = lines.filtered( + lambda line: line.location_id.is_sublocation_of(location_id) + and not line.location_id.is_sublocation_of(self.exclude_location_ids) + and ( + not line.location_dest_id.is_sublocation_of(location_id) + or line.location_dest_id.is_sublocation_of(self.exclude_location_ids) + ) + ) + return sum(lines.mapped("product_qty")) - @api.onchange("bom_id") - def _onchange_bom_id(self): - if self.bom_id.location_id: - self.location_id = self.bom_id.location_id + @api.depends("product_id", "location_id") + def _compute_qty_available_in_source_loc(self): + for rec in self: + available_qty = 0 + if rec.bom_id.type != "phantom": + available_qty = rec.product_id.with_context( + location=rec.location_id.id + ).qty_available + available_qty -= rec._get_outgoing_reservation_qty( + rec.product_id, + rec.location_id, + ) + available_qty -= rec._get_exclude_locations_qty( + rec.product_id, + rec.location_id, + ) + available_qty = rec.product_id.product_tmpl_id.uom_id._compute_quantity( + available_qty, rec.product_uom_id + ) + rec.qty_available_in_source_loc = available_qty + + @api.depends("potential_qty") + def _compute_availability(self): + for rec in self: + rec.availability = ( + "available" if rec.potential_qty >= rec.desired_qty else "not_available" + ) + + @api.depends("potential_qty") + def _compute_potential_qty_rounded(self): + for rec in self: + rec.potential_qty_rounded = math.floor(rec.potential_qty) @api.model def _prepare_line(self, bom_line, level, factor): return { - "product_id": bom_line.product_id.id, - "bom_line": bom_line.id, + "bom_line_id": bom_line.id, "bom_level": level, "product_qty": bom_line.product_qty * factor, - "product_uom_id": bom_line.product_uom_id.id, - "location_id": ( - bom_line.location_id.id if bom_line.location_id else self.location_id.id - ), - "explosion_id": self.id, + "location_id": self.location_id.id, + "wizard_id": self.id, } - def do_explode(self): - self.ensure_one() + def _create_lines(self, bom, level, factor): line_obj = self.env["mrp.bom.current.stock.line"] - - def _create_lines(bom, level=0, factor=1): - level += 1 - for line in bom.bom_line_ids: - vals = self._prepare_line(line, level, factor) - line_obj.create(vals) - location = line.location_id - line_boms = line.product_id.bom_ids - boms = ( - line_boms.filtered(lambda bom: bom.location_id == location) - or line_boms + level += 1 + for bom_line in bom.bom_line_ids: + line_vals = self._prepare_line(bom_line, level, factor) + line = line_obj.create(line_vals) + if line.bom_id: + line_qty = line.product_uom_id._compute_quantity( + line.product_qty, line.bom_id.product_uom_id + ) + new_factor = ( + factor * line_qty / (line.bom_id.product_qty * self.desired_qty) ) - if boms: - line_qty = line.product_uom_id._compute_quantity( - line.product_qty, boms[0].product_uom_id - ) - new_factor = factor * line_qty / boms[0].product_qty - _create_lines(boms[0], level, new_factor) - - _create_lines(self.bom_id) + if self.explosion_type == "all" or ( + self.explosion_type == "one" and line.bom_id.type == "phantom" + ): + self._create_lines(line.bom_id, level, new_factor) + + def do_explode(self): + self.ensure_one() + self._create_lines(self.bom_id, 0, self.desired_qty) + self.potential_qty = self.compute_potential_qty(self, self.bom_id, 0) return { "type": "ir.actions.act_window", "name": "Open lines", @@ -95,47 +177,152 @@ def _create_lines(bom, level=0, factor=1): "res_id": self.id, } + def compute_potential_qty(self, rec, bom, level): + lines = self.line_ids.filtered( + lambda x: x.parent_bom_id.id == bom.id and x.bom_level == level + 1 + ) + potential_quantities = [] + for line in lines: + potential_quantity = 0 + if line.bom_id and ( + self.explosion_type != "one" or line.bom_id.type == "phantom" + ): + potential_quantity = self.compute_potential_qty( + line, line.bom_id, level + 1 + ) + elif line.product_qty != 0: + potential_quantity = line.qty_available_in_source_loc / ( + line.product_qty / self.desired_qty + ) + potential_quantities.append(potential_quantity) + qty = ( + min(potential_quantities) + rec.qty_available_in_source_loc + if potential_quantities + else rec.qty_available_in_source_loc + ) + return qty + + def action_go_back(self): + action = self.env["ir.actions.actions"]._for_xml_id( + "mrp_bom_current_stock.mrp_bom_current_stock_action" + ) + action["context"] = { + "default_bom_id": self.bom_id.id, + "default_product_id": self.product_id.id, + "default_location_id": self.location_id.id, + "default_exclude_location_ids": self.exclude_location_ids.ids, + "default_desired_qty": self.desired_qty, + "default_explosion_type": self.explosion_type, + } + return action + class BomRouteCurrentStockLine(models.TransientModel): _name = "mrp.bom.current.stock.line" _description = "MRP Bom Route Current Stock Line" + _order = "bom_level, parent_bom_id" - explosion_id = fields.Many2one(comodel_name="mrp.bom.current.stock", readonly=True) - product_id = fields.Many2one( - comodel_name="product.product", string="Product Variant", readonly=True - ) + wizard_id = fields.Many2one(comodel_name="mrp.bom.current.stock", readonly=True) bom_level = fields.Integer(string="BoM Level", readonly=True) - product_qty = fields.Float( - string="Product Quantity", readonly=True, digits="Product Unit of Measure" + product_id = fields.Many2one( + comodel_name="product.product", + string="Product Variant", + related="bom_line_id.product_id", + readonly=True, ) - product_uom_id = fields.Many2one( - comodel_name="uom.uom", string="Product Unit of Measure", readonly=True + bom_id = fields.Many2one( + comodel_name="mrp.bom", + string="BoM", + compute="_compute_bom_id", + store=True, + readonly=True, ) - location_id = fields.Many2one( - comodel_name="stock.location", string="Source location" + bom_type = fields.Selection( + related="bom_id.type", + readonly=True, ) - bom_line = fields.Many2one( + bom_line_id = fields.Many2one( comodel_name="mrp.bom.line", string="BoM line", readonly=True ) + explosion_type = fields.Selection(related="wizard_id.explosion_type", store=True) + product_qty = fields.Float( + string="Product Quantity", + readonly=True, + digits="Product Unit of Measure", + default=1, + ) qty_available_in_source_loc = fields.Float( string="Qty Available in Source", compute="_compute_qty_available_in_source_loc", - readonly=True, + digits="Product Unit of Measure", ) - bom_id = fields.Many2one( + potential_qty = fields.Float( + string="Potential Quantity", + help="Potential quantity that could be manufactured given all current " + "on-hand stock at the specified locations.", + digits="Product Unit of Measure", + compute="_compute_potential_qty", + store=True, + ) + product_uom_id = fields.Many2one( + comodel_name="uom.uom", + string="Product Unit of Measure", + related="bom_line_id.product_uom_id", + ) + availability = fields.Selection( + [("available", "Available"), ("not_available", "Not Available")], + compute="_compute_availability", + help="The field shows the availability that you have taking into " + "account the available quantity that could end up being manufactured.", + ) + parent_bom_id = fields.Many2one( comodel_name="mrp.bom", string="Parent BoM", - related="bom_line.bom_id", - readonly=True, + related="bom_line_id.bom_id", + ) + location_id = fields.Many2one( + comodel_name="stock.location", string="Source location" ) @api.onchange("location_id") def _compute_qty_available_in_source_loc(self): - for record in self: - product_available = record.product_id.with_context( - location=record.location_id.id - )._product_available()[record.product_id.id]["qty_available"] - res = record.product_id.product_tmpl_id.uom_id._compute_quantity( - product_available, record.product_uom_id + for rec in self: + available_qty = 0 + if rec.bom_id.type != "phantom": + available_qty = rec.product_id.with_context( + location=rec.location_id.id + ).qty_available + available_qty -= rec.wizard_id._get_outgoing_reservation_qty( + rec.product_id, + rec.location_id, + ) + available_qty -= rec.wizard_id._get_exclude_locations_qty( + rec.product_id, + rec.location_id, + ) + available_qty = rec.product_id.product_tmpl_id.uom_id._compute_quantity( + available_qty, rec.product_uom_id + ) + rec.qty_available_in_source_loc = available_qty + + @api.depends("bom_line_id") + def _compute_bom_id(self): + for rec in self: + boms = rec.bom_line_id.product_id.bom_ids + rec.bom_id = boms[0] if boms else None + + @api.depends("product_qty", "qty_available_in_source_loc") + def _compute_availability(self): + for rec in self: + rec.availability = ( + "available" if rec.potential_qty >= rec.product_qty else "not_available" + ) + + @api.depends("qty_available_in_source_loc") + def _compute_potential_qty(self): + for rec in self: + rec.potential_qty = ( + rec.wizard_id.compute_potential_qty(rec, rec.bom_id, rec.bom_level) + if rec.bom_id + else rec.qty_available_in_source_loc ) - record.qty_available_in_source_loc = res diff --git a/mrp_bom_current_stock/wizard/bom_route_current_stock_view.xml b/mrp_bom_current_stock/wizard/bom_route_current_stock_view.xml index c7a81e2d..4348a744 100644 --- a/mrp_bom_current_stock/wizard/bom_route_current_stock_view.xml +++ b/mrp_bom_current_stock/wizard/bom_route_current_stock_view.xml @@ -7,19 +7,45 @@ mrp.bom.current.stock
+

+ It is necessary to differentiate between the two types of explosion. +

+

+ - One Level: Allows you to evaluate what the potential quantity is in the event + that only the selected product in particular should be manufactured. In other + words, it emulates a manufacturing order. +

+

+ - All Levels: Allows you to evaluate what is the total potential amount that could + be manufactured if all the existing material in the specified locations were used. +

+

+ As a final comment, it is necessary to indicate that in the event that a component + is repeated in different BoMs, it is possible that the potential quantity is not + correctly computed since it is not taken into account that if it is being used by + a BoM, it cannot be used by another. +

- + + +
+ BoM Current Stock Explosion mrp.bom.current.stock form new + +