diff --git a/mrp_production_byproduct_cost_share/__init__.py b/mrp_production_byproduct_cost_share/__init__.py new file mode 100644 index 000000000..bf588bc8b --- /dev/null +++ b/mrp_production_byproduct_cost_share/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import report diff --git a/mrp_production_byproduct_cost_share/__manifest__.py b/mrp_production_byproduct_cost_share/__manifest__.py new file mode 100644 index 000000000..d59c4d0b5 --- /dev/null +++ b/mrp_production_byproduct_cost_share/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0). + +{ + "name": "Production By-Product Cost Share", + "version": "14.0.1.0.0", + "category": "MRP", + "author": "ForgeFlow, Odoo Community Association (OCA), Odoo S.A.", + "website": "https://github.com/OCA/manufacture", + "license": "LGPL-3", + "depends": ["mrp", "mrp_account"], + "data": [ + "views/mrp_production_views.xml", + "views/mrp_bom_views.xml", + "views/mrp_template.xml", + "report/mrp_report_bom_structure.xml", + ], + "installable": True, +} diff --git a/mrp_production_byproduct_cost_share/models/__init__.py b/mrp_production_byproduct_cost_share/models/__init__.py new file mode 100644 index 000000000..b35d45a6f --- /dev/null +++ b/mrp_production_byproduct_cost_share/models/__init__.py @@ -0,0 +1,4 @@ +from . import mrp_production +from . import mrp_bom +from . import product +from . import stock_move diff --git a/mrp_production_byproduct_cost_share/models/mrp_bom.py b/mrp_production_byproduct_cost_share/models/mrp_bom.py new file mode 100644 index 000000000..0bd0bc6e7 --- /dev/null +++ b/mrp_production_byproduct_cost_share/models/mrp_bom.py @@ -0,0 +1,46 @@ +# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0). + +from odoo import _, fields, models +from odoo.exceptions import ValidationError + + +class MrpBom(models.Model): + _inherit = "mrp.bom" + + def _check_bom_lines(self): + res = super()._check_bom_lines() + for bom in self: + for byproduct in bom.byproduct_ids: + if bom.product_id: + same_product = bom.product_id == byproduct.product_id + else: + same_product = ( + bom.product_tmpl_id == byproduct.product_id.product_tmpl_id + ) + if same_product: + raise ValidationError( + _("By-product %s should not be the same as BoM product.") + % bom.display_name + ) + if byproduct.cost_share < 0: + raise ValidationError( + _("By-products cost shares must be positive.") + ) + if sum(bom.byproduct_ids.mapped("cost_share")) > 100: + raise ValidationError( + _("The total cost share for a BoM's by-products cannot exceed 100.") + ) + return res + + +class MrpByProduct(models.Model): + _inherit = "mrp.bom.byproduct" + + cost_share = fields.Float( + "Cost Share (%)", + digits=(5, 2), + help="The percentage of the final production cost for this by-product line" + " (divided between the quantity produced)." + "The total of all by-products' cost share must be less than or equal to 100.", + ) diff --git a/mrp_production_byproduct_cost_share/models/mrp_production.py b/mrp_production_byproduct_cost_share/models/mrp_production.py new file mode 100644 index 000000000..6095568fb --- /dev/null +++ b/mrp_production_byproduct_cost_share/models/mrp_production.py @@ -0,0 +1,86 @@ +# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0). + +from odoo import _, api, models +from odoo.exceptions import ValidationError +from odoo.tools import float_round + + +class MrpProduction(models.Model): + _inherit = "mrp.production" + + @api.constrains("move_byproduct_ids") + def _check_byproducts(self): + for order in self: + if any(move.cost_share < 0 for move in order.move_byproduct_ids): + raise ValidationError(_("By-products cost shares must be positive.")) + if sum(order.move_byproduct_ids.mapped("cost_share")) > 100: + raise ValidationError( + _( + "The total cost share for a manufacturing order's by-products" + " cannot exceed 100." + ) + ) + + def _get_move_finished_values( + self, + product_id, + product_uom_qty, + product_uom, + operation_id=False, + byproduct_id=False, + ): + res = super()._get_move_finished_values( + product_id, product_uom_qty, product_uom, operation_id, byproduct_id + ) + res["cost_share"] = ( + 0 + if not byproduct_id + else self.env["mrp.bom.byproduct"].browse(byproduct_id).cost_share + ) + return res + + def _cal_price(self, consumed_moves): + """Set a price unit on the finished move according to `consumed_moves` and + taking into account cost_share from by-products. + """ + super(MrpProduction, self)._cal_price(consumed_moves) + finished_move = self.move_finished_ids.filtered( + lambda x: x.product_id == self.product_id + and x.state not in ("done", "cancel") + and x.quantity_done > 0 + ) + if finished_move: + finished_move.ensure_one() + qty_done = finished_move.product_uom._compute_quantity( + finished_move.quantity_done, finished_move.product_id.uom_id + ) + # already calculated, but we want to change it according to cost_share + # from by-products + total_cost = finished_move.price_unit * qty_done + byproduct_moves = self.move_byproduct_ids.filtered( + lambda m: m.state not in ("done", "cancel") and m.quantity_done > 0 + ) + byproduct_cost_share = 0 + for byproduct in byproduct_moves: + if byproduct.cost_share == 0: + continue + byproduct_cost_share += byproduct.cost_share + if byproduct.product_id.cost_method in ("fifo", "average"): + byproduct.price_unit = ( + total_cost + * byproduct.cost_share + / 100 + / byproduct.product_uom._compute_quantity( + byproduct.quantity_done, byproduct.product_id.uom_id + ) + ) + if finished_move.product_id.cost_method in ("fifo", "average"): + finished_move.price_unit = ( + total_cost + * float_round( + 1 - byproduct_cost_share / 100, precision_rounding=0.0001 + ) + / qty_done + ) + return True diff --git a/mrp_production_byproduct_cost_share/models/product.py b/mrp_production_byproduct_cost_share/models/product.py new file mode 100644 index 000000000..fa95617c2 --- /dev/null +++ b/mrp_production_byproduct_cost_share/models/product.py @@ -0,0 +1,77 @@ +# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0). + +from odoo import models +from odoo.tools import float_round + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + def _compute_bom_count(self): + for product in self: + super()._compute_bom_count() + product.bom_count += self.env["mrp.bom"].search_count( + [("byproduct_ids.product_id.product_tmpl_id", "=", product.id)] + ) + + +class ProductProduct(models.Model): + _inherit = "product.product" + + def _compute_bom_count(self): + for product in self: + super()._compute_bom_count() + product.bom_count += self.env["mrp.bom"].search_count( + [("byproduct_ids.product_id", "=", product.id)] + ) + + def _set_price_from_bom(self, boms_to_recompute=False): + super()._set_price_from_bom(boms_to_recompute=False) + byproduct_bom = self.env["mrp.bom"].search( + [("byproduct_ids.product_id", "=", self.id)], + order="sequence, product_id, id", + limit=1, + ) + if byproduct_bom: + price = self._compute_bom_price( + byproduct_bom, boms_to_recompute=boms_to_recompute, byproduct_bom=True + ) + if price: + self.standard_price = price + + def _compute_bom_price(self, bom, boms_to_recompute=False, byproduct_bom=False): + price = super()._compute_bom_price(bom, boms_to_recompute) + if byproduct_bom: + byproduct_lines = bom.byproduct_ids.filtered( + lambda b: b.product_id == self and b.cost_share != 0 + ) + product_uom_qty = 0 + for line in byproduct_lines: + product_uom_qty += line.product_uom_id._compute_quantity( + line.product_qty, self.uom_id, round=False + ) + byproduct_cost_share = sum(byproduct_lines.mapped("cost_share")) + if byproduct_cost_share and product_uom_qty: + return price * byproduct_cost_share / 100 + else: + byproduct_cost_share = sum(bom.byproduct_ids.mapped("cost_share")) + if byproduct_cost_share: + price *= float_round( + 1 - byproduct_cost_share / 100, precision_rounding=0.0001 + ) + return price + + def action_view_bom(self): + action = super().action_view_bom() + template_ids = self.mapped("product_tmpl_id").ids + action["domain"] = [ + "|", + "|", + ("byproduct_ids.product_id", "in", self.ids), + ("product_id", "in", self.ids), + "&", + ("product_id", "=", False), + ("product_tmpl_id", "in", template_ids), + ] + return action diff --git a/mrp_production_byproduct_cost_share/models/stock_move.py b/mrp_production_byproduct_cost_share/models/stock_move.py new file mode 100644 index 000000000..8a3be8bf8 --- /dev/null +++ b/mrp_production_byproduct_cost_share/models/stock_move.py @@ -0,0 +1,20 @@ +# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0). + +from odoo import api, fields, models + + +class StockMove(models.Model): + _inherit = "stock.move" + + cost_share = fields.Float( + "Cost Share (%)", + digits=(5, 2), + # decimal = 2 is important for rounding calculations!! + help="The percentage of the final production cost for this by-product. The" + " total of all by-products' cost share must be smaller or equal to 100.", + ) + + @api.model + def _prepare_merge_moves_distinct_fields(self): + return super()._prepare_merge_moves_distinct_fields() + ["cost_share"] diff --git a/mrp_production_byproduct_cost_share/readme/CONFIGURE.rst b/mrp_production_byproduct_cost_share/readme/CONFIGURE.rst new file mode 100644 index 000000000..c0cf7aada --- /dev/null +++ b/mrp_production_byproduct_cost_share/readme/CONFIGURE.rst @@ -0,0 +1 @@ +You just have to set a 'Cost Share' value for each By-Product in the BOM form. diff --git a/mrp_production_byproduct_cost_share/readme/CONTRIBUTORS.rst b/mrp_production_byproduct_cost_share/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..90e9afc94 --- /dev/null +++ b/mrp_production_byproduct_cost_share/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `ForgeFlow `__: + + * Maria de Luna diff --git a/mrp_production_byproduct_cost_share/readme/DESCRIPTION.rst b/mrp_production_byproduct_cost_share/readme/DESCRIPTION.rst new file mode 100644 index 000000000..d6946d32f --- /dev/null +++ b/mrp_production_byproduct_cost_share/readme/DESCRIPTION.rst @@ -0,0 +1,5 @@ +This module adds the field 'Cost Share' in by-products and its logic as it is in odoo +v15 (added here https://github.com/odoo/odoo/commit/fd52760266ede3b9353936da8dc23e795f5a5031). + +This field is the percentage of the final production cost for this by-product, so it +will affect the final product cost. diff --git a/mrp_production_byproduct_cost_share/readme/HISTORY.rst b/mrp_production_byproduct_cost_share/readme/HISTORY.rst new file mode 100644 index 000000000..42019405f --- /dev/null +++ b/mrp_production_byproduct_cost_share/readme/HISTORY.rst @@ -0,0 +1,4 @@ +14.0.1.0.0 (2023-01-23) +~~~~~~~~~~~~~~~~~~~~~~~ + +* Start of the history. diff --git a/mrp_production_byproduct_cost_share/readme/ROADMAP.rst b/mrp_production_byproduct_cost_share/readme/ROADMAP.rst new file mode 100644 index 000000000..e69de29bb diff --git a/mrp_production_byproduct_cost_share/report/__init__.py b/mrp_production_byproduct_cost_share/report/__init__.py new file mode 100644 index 000000000..d5f0e0470 --- /dev/null +++ b/mrp_production_byproduct_cost_share/report/__init__.py @@ -0,0 +1 @@ +from . import mrp_report_bom_structure diff --git a/mrp_production_byproduct_cost_share/report/mrp_report_bom_structure.py b/mrp_production_byproduct_cost_share/report/mrp_report_bom_structure.py new file mode 100644 index 000000000..1c5117769 --- /dev/null +++ b/mrp_production_byproduct_cost_share/report/mrp_report_bom_structure.py @@ -0,0 +1,225 @@ +# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0). + +from odoo import _, api, models +from odoo.tools import float_round + + +class ReportBomStructure(models.AbstractModel): + _inherit = "report.mrp.report_bom_structure" + + @api.model + def get_byproducts(self, bom_id=False, qty=0, level=0, total=0): + bom = self.env["mrp.bom"].browse(bom_id) + lines, dummy = self._get_byproducts_lines(bom, qty, level, total) + values = { + "bom_id": bom_id, + "currency": self.env.company.currency_id, + "byproducts": lines, + } + return self.env.ref( + "mrp_production_byproduct_cost_share.report_mrp_byproduct_line" + )._render({"data": values}) + + def _get_bom( + self, bom_id=False, product_id=False, line_qty=False, line_id=False, level=False + ): + res = super()._get_bom(bom_id, product_id, line_qty, line_id, level) + byproducts, byproduct_cost_portion = self._get_byproducts_lines( + res["bom"], res["bom_qty"], res["level"], res["total"] + ) + res["byproducts"] = byproducts + res["cost_share"] = float_round( + 1 - byproduct_cost_portion, precision_rounding=0.0001 + ) + res["bom_cost"] = res["total"] * res["cost_share"] + res["byproducts_cost"] = sum(byproduct["bom_cost"] for byproduct in byproducts) + res["byproducts_total"] = sum( + byproduct["product_qty"] for byproduct in byproducts + ) + return res + + def _get_bom_lines(self, bom, bom_quantity, product, line_id, level): + components, total = super()._get_bom_lines( + bom, bom_quantity, product, line_id, level + ) + for line in bom.bom_line_ids: + line_quantity = (bom_quantity / (bom.product_qty or 1.0)) * line.product_qty + if line._skip_bom_line(product): + continue + if line.child_bom_id: + factor = ( + line.product_uom_id._compute_quantity( + line_quantity, line.child_bom_id.product_uom_id + ) + / line.child_bom_id.product_qty + ) + sub_total = self._get_price(line.child_bom_id, factor, line.product_id) + byproduct_cost_share = sum( + line.child_bom_id.byproduct_ids.mapped("cost_share") + ) + if byproduct_cost_share: + sub_total_byproducts = float_round( + sub_total * byproduct_cost_share / 100, + precision_rounding=0.0001, + ) + total -= sub_total_byproducts + return components, total + + def _get_byproducts_lines(self, bom, bom_quantity, level, total): + byproducts = [] + byproduct_cost_portion = 0 + company = bom.company_id or self.env.company + for byproduct in bom.byproduct_ids: + line_quantity = ( + bom_quantity / (bom.product_qty or 1.0) + ) * byproduct.product_qty + cost_share = byproduct.cost_share / 100 + byproduct_cost_portion += cost_share + price = ( + byproduct.product_id.uom_id._compute_price( + byproduct.product_id.with_company(company).standard_price, + byproduct.product_uom_id, + ) + * line_quantity + ) + byproducts.append( + { + "product_id": byproduct.product_id, + "product_name": byproduct.product_id.display_name, + "product_qty": line_quantity, + "product_uom": byproduct.product_uom_id.name, + "product_cost": company.currency_id.round(price), + "parent_id": bom.id, + "level": level or 0, + "bom_cost": company.currency_id.round(total * cost_share), + "cost_share": cost_share, + } + ) + return byproducts, byproduct_cost_portion + + def _get_price(self, bom, factor, product): + price = super()._get_price(bom, factor, product) + for line in bom.bom_line_ids: + if line._skip_bom_line(product): + continue + if line.child_bom_id: + qty = ( + line.product_uom_id._compute_quantity( + line.product_qty * factor, line.child_bom_id.product_uom_id + ) + / line.child_bom_id.product_qty + ) + sub_price = self._get_price(line.child_bom_id, qty, line.product_id) + byproduct_cost_share = sum( + line.child_bom_id.byproduct_ids.mapped("cost_share") + ) + if byproduct_cost_share: + sub_price_byproducts = float_round( + sub_price * byproduct_cost_share / 100, + precision_rounding=0.0001, + ) + price -= sub_price_byproducts + return price + + # pylint: disable=W0102 + # flake8: noqa:B006 + def _get_pdf_line( + self, bom_id, product_id=False, qty=1, child_bom_ids=[], unfolded=False + ): + data = super()._get_pdf_line(bom_id, product_id, qty, child_bom_ids, unfolded) + + def get_sub_lines(bom, product_id, line_qty, line_id, level): + # method overriden + data = self._get_bom( + bom_id=bom.id, + product_id=product_id, + line_qty=line_qty, + line_id=line_id, + level=level, + ) + bom_lines = data["components"] + lines = [] + for bom_line in bom_lines: + lines.append( + { + "name": bom_line["prod_name"], + "type": "bom", + "quantity": bom_line["prod_qty"], + "uom": bom_line["prod_uom"], + "prod_cost": bom_line["prod_cost"], + "bom_cost": bom_line["total"], + "level": bom_line["level"], + "code": bom_line["code"], + "child_bom": bom_line["child_bom"], + "prod_id": bom_line["prod_id"], + } + ) + if bom_line["child_bom"] and ( + unfolded or bom_line["child_bom"] in child_bom_ids + ): + line = self.env["mrp.bom.line"].browse(bom_line["line_id"]) + lines += get_sub_lines( + line.child_bom_id, + line.product_id.id, + bom_line["prod_qty"], + line, + level + 1, + ) + if data["operations"]: + lines.append( + { + "name": _("Operations"), + "type": "operation", + "quantity": data["operations_time"], + "uom": _("minutes"), + "bom_cost": data["operations_cost"], + "level": level, + } + ) + for operation in data["operations"]: + if unfolded or "operation-" + str(bom.id) in child_bom_ids: + lines.append( + { + "name": operation["name"], + "type": "operation", + "quantity": operation["duration_expected"], + "uom": _("minutes"), + "bom_cost": operation["total"], + "level": level + 1, + } + ) + # start of changes + if data["byproducts"]: + lines.append( + { + "name": _("Byproducts"), + "type": "byproduct", + "uom": False, + "quantity": data["byproducts_total"], + "bom_cost": data["byproducts_cost"], + "level": level, + } + ) + for byproduct in data["byproducts"]: + if unfolded or "byproduct-" + str(bom.id) in child_bom_ids: + lines.append( + { + "name": byproduct["product_name"], + "type": "byproduct", + "quantity": byproduct["product_qty"], + "uom": byproduct["product_uom"], + "prod_cost": byproduct["product_cost"], + "bom_cost": byproduct["bom_cost"], + "level": level + 1, + } + ) + return lines + + bom = self.env["mrp.bom"].browse(bom_id) + product_id = ( + product_id or bom.product_id.id or bom.product_tmpl_id.product_variant_id.id + ) + pdf_lines = get_sub_lines(bom, product_id, qty, False, 1) + data["lines"] = pdf_lines + return data diff --git a/mrp_production_byproduct_cost_share/report/mrp_report_bom_structure.xml b/mrp_production_byproduct_cost_share/report/mrp_report_bom_structure.xml new file mode 100644 index 000000000..32a05073e --- /dev/null +++ b/mrp_production_byproduct_cost_share/report/mrp_report_bom_structure.xml @@ -0,0 +1,204 @@ + + + + + + + + + + + + + diff --git a/mrp_production_byproduct_cost_share/static/description/icon.png b/mrp_production_byproduct_cost_share/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/mrp_production_byproduct_cost_share/static/description/icon.png differ diff --git a/mrp_production_byproduct_cost_share/static/description/index.html b/mrp_production_byproduct_cost_share/static/description/index.html new file mode 100644 index 000000000..881a34736 --- /dev/null +++ b/mrp_production_byproduct_cost_share/static/description/index.html @@ -0,0 +1,525 @@ + + + + + + +Production Grouped By Product + + + +
+

Production Grouped By Product

+ + +

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

+

When you have several sales orders with make to order (MTO) products that +require to be manufactured, you end up with one manufacturing order for each of +these sales orders, which is very bad for the management.

+

With this module, each time an MTO manufacturing order is required to be +created, it first checks that there’s no other existing order not yet started +for the same product and bill of materials inside the specied time frame , and +if there’s one, then the quantity of that order is increased instead of +creating a new one.

+

Table of contents

+ +
+

Configuration

+

To configure the time frame for grouping manufacturing order:

+
    +
  1. Go to Inventory > Configuration > Warehouse Management > Operation Types

    +
  2. +
  3. Locate the manufacturing type you are using (default one is called +“Manufacturing”).

    +
  4. +
  5. Open it and change these 2 values:

    +
      +
    • MO grouping max. hour (UTC): The maximum hour (between 0 and 23) for +considering new manufacturing orders inside the same interval period, and +thus being grouped on the same MO. IMPORTANT: The hour should be expressed +in UTC.
    • +
    • MO grouping interval (days): The number of days for grouping together on +the same manufacturing order.
    • +
    +

    Example: If you leave the default values 19 and 1, all the planned orders +between 19:00:01 of the previous day and 20:00:00 of the target date will +be grouped together.

    +
  6. +
+
+
+

Known issues / Roadmap

+
    +
  • Add a check in the product form for excluding it from being grouped.
  • +
+
+
+

Changelog

+
+

14.0.1.0.0 (2021-11-16)

+
    +
  • [MIG] Migration to v14.
  • +
+
+
+

13.0.1.0.0 (2020-01-09)

+
    +
  • [MIG] Migration to v13.
  • +
+
+
+

12.0.1.0.0 (2019-04-17)

+
    +
  • [MIG] Migration to v12:
  • +
+
+
+

11.0.2.0.1 (2018-07-02)

+
    +
  • [FIX] fix test in mrp_production_grouped_by_product
  • +
+
+
+

11.0.2.0.0 (2018-06-04)

+
    +
  • [IMP] mrp_production_grouped_by_product: Time frames
  • +
+
+
+

11.0.1.0.1 (2018-05-11)

+
    +
  • [IMP] mrp_production_grouped_by_company: Context evaluation on mrp.production + tests
  • +
+
+
+

11.0.1.0.0 (2018-05-11)

+
    +
  • Start of the history.
  • +
+
+
+
+

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

+
    +
  • Tecnativa
  • +
+
+
+

Contributors

+ +
+
+

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/manufacture project on GitHub.

+

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

+
+
+
+ + diff --git a/mrp_production_byproduct_cost_share/static/src/js/mrp_bom_report.js b/mrp_production_byproduct_cost_share/static/src/js/mrp_bom_report.js new file mode 100644 index 000000000..f6f8f02d3 --- /dev/null +++ b/mrp_production_byproduct_cost_share/static/src/js/mrp_bom_report.js @@ -0,0 +1,23 @@ +odoo.define("mrp_production_byproduct_cost_share.mrp_bom_report", function (require) { + "use strict"; + + var MrpBomReport = require("mrp.mrp_bom_report"); + + MrpBomReport.include({ + get_byproducts: function (event) { + var self = this; + var $parent = $(event.currentTarget).closest("tr"); + var activeID = $parent.data("bom-id"); + var qty = $parent.data("qty"); + var level = $parent.data("level") || 0; + var total = $parent.data("total") || 0; + return this._rpc({ + model: "report.mrp.report_bom_structure", + method: "get_byproducts", + args: [activeID, parseFloat(qty), level + 1, parseFloat(total)], + }).then(function (result) { + self.render_html(event, $parent, result); + }); + }, + }); +}); diff --git a/mrp_production_byproduct_cost_share/tests/__init__.py b/mrp_production_byproduct_cost_share/tests/__init__.py new file mode 100644 index 000000000..b12d6f85d --- /dev/null +++ b/mrp_production_byproduct_cost_share/tests/__init__.py @@ -0,0 +1 @@ +from . import test_mrp_production_byproduct_cost_share diff --git a/mrp_production_byproduct_cost_share/tests/test_mrp_production_byproduct_cost_share.py b/mrp_production_byproduct_cost_share/tests/test_mrp_production_byproduct_cost_share.py new file mode 100644 index 000000000..79c68b84b --- /dev/null +++ b/mrp_production_byproduct_cost_share/tests/test_mrp_production_byproduct_cost_share.py @@ -0,0 +1,91 @@ +# Copyright 2023 ForgeFlow S.L. (http://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0). + +from odoo.tests import common + + +class TestProductionByProductCostShare(common.TransactionCase): + def setUp(self): + super().setUp() + self.MrpBom = self.env["mrp.bom"] + self.warehouse = self.env.ref("stock.warehouse0") + route_manufacture = self.warehouse.manufacture_pull_id.route_id.id + route_mto = self.warehouse.mto_pull_id.route_id.id + self.uom_unit_id = self.env.ref("uom.product_uom_unit").id + + def create_product( + name, + standard_price, + route_ids, + ): + return self.env["product.product"].create( + { + "name": name, + "type": "product", + "route_ids": route_ids, + "standard_price": standard_price, + } + ) + + # Products. + self.product_a = create_product( + "Product A", 0, [(6, 0, [route_manufacture, route_mto])] + ) + self.product_b = create_product( + "Product B", 0, [(6, 0, [route_manufacture, route_mto])] + ) + self.product_c_id = create_product("Product C", 100, []).id + self.bom_byproduct = self.MrpBom.create( + { + "product_tmpl_id": self.product_a.product_tmpl_id.id, + "product_qty": 1.0, + "type": "normal", + "product_uom_id": self.uom_unit_id, + "bom_line_ids": [ + ( + 0, + 0, + { + "product_id": self.product_c_id, + "product_uom_id": self.uom_unit_id, + "product_qty": 1, + }, + ) + ], + "byproduct_ids": [ + ( + 0, + 0, + { + "product_id": self.product_b.id, + "product_uom_id": self.uom_unit_id, + "product_qty": 1, + "cost_share": 15, + }, + ) + ], + } + ) + + def test_01_compute_byproduct_price(self): + """Test BoM cost when byproducts with cost share""" + self.assertEqual( + self.product_a.standard_price, 0, "Initial price of the Product should be 0" + ) + self.assertEqual( + self.product_b.standard_price, + 0, + "Initial price of the By-Product should be 0", + ) + self.product_a.button_bom_cost() + self.assertEqual( + self.product_a.standard_price, + 85, + "After computing price from BoM price should be 85", + ) + self.product_b.button_bom_cost() + self.assertEqual( + self.product_b.standard_price, + 15, + "After computing price from BoM price should be 15", + ) diff --git a/mrp_production_byproduct_cost_share/views/mrp_bom_views.xml b/mrp_production_byproduct_cost_share/views/mrp_bom_views.xml new file mode 100644 index 000000000..1585b49f3 --- /dev/null +++ b/mrp_production_byproduct_cost_share/views/mrp_bom_views.xml @@ -0,0 +1,28 @@ + + + + + mrp.bom.form + mrp.bom + + + + + + + + + {'default_product_tmpl_id': active_id} + ['|', ('product_tmpl_id', '=', active_id), ('byproduct_ids.product_id.product_tmpl_id', '=', active_id)] + + + + {'default_product_id': active_id} + + diff --git a/mrp_production_byproduct_cost_share/views/mrp_production_views.xml b/mrp_production_byproduct_cost_share/views/mrp_production_views.xml new file mode 100644 index 000000000..d2c132228 --- /dev/null +++ b/mrp_production_byproduct_cost_share/views/mrp_production_views.xml @@ -0,0 +1,18 @@ + + + + + mrp.production.form + mrp.production + + + + + + + + diff --git a/mrp_production_byproduct_cost_share/views/mrp_template.xml b/mrp_production_byproduct_cost_share/views/mrp_template.xml new file mode 100644 index 000000000..80c8504b0 --- /dev/null +++ b/mrp_production_byproduct_cost_share/views/mrp_template.xml @@ -0,0 +1,16 @@ + + +