diff --git a/product_cost_rollup_to_bom/README.rst b/product_cost_rollup_to_bom/README.rst new file mode 100644 index 000000000..3b9886280 --- /dev/null +++ b/product_cost_rollup_to_bom/README.rst @@ -0,0 +1,101 @@ +======================= +Product BOM Cost Rollup +======================= + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fmanufacture-lightgray.png?logo=github + :target: https://github.com/OCA/manufacture/tree/14.0/product_cost_rollup_to_bom + :alt: OCA/manufacture +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/manufacture-14-0/manufacture-14-0-product_cost_rollup_to_bom + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/129/14.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +With Standard Cost Method, products that have bill of materials defined show rollup cost from the BOM. This module allows the cost to be rolled up based on BOM's that have components change standard price. The module can be used from product template/product variant forms using "Compute BOM Cost Rollup" or from the Scheduled Job across all BOM's that need an update. The module sends an email on modified standard cost products to an email configured on the Manufacturing App. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +Go to Manufacturing/ Configuration/ Settings/ BoM Cost Rollup Email and add email that you want to send BoM cost rollup email to. + +Or + +Go to Settings/ Users & Companies/ Companies and add email that you want to send BoM cost rollup email to. + +Turn On Debugger mode Go to Settings/ Technical/ Automation/ Scheduled Actions Change the interval you want scheduler to run. + +Usage +===== + +* Set the BOM Cost Rollup Notification Email +* To update a single product: Click on the Compute BOM Cost Rollup button +* To run on BOMs that have changed, run the scheduler manually or in cron mode. + +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 +~~~~~~~ + +* Open Source Integrators + +Contributors +~~~~~~~~~~~~ + +* `Open Source Integrators `: + + * Balaji Kannan + * Mayank Gosai + * Daniel Reis + * Chandresh Thakkar + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/manufacture `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/product_cost_rollup_to_bom/__init__.py b/product_cost_rollup_to_bom/__init__.py new file mode 100644 index 000000000..798e371ed --- /dev/null +++ b/product_cost_rollup_to_bom/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2021, Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import models diff --git a/product_cost_rollup_to_bom/__manifest__.py b/product_cost_rollup_to_bom/__manifest__.py new file mode 100644 index 000000000..534e8dd7e --- /dev/null +++ b/product_cost_rollup_to_bom/__manifest__.py @@ -0,0 +1,27 @@ +# Copyright (C) 2021, Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": "Product BOM Cost Rollup", + "version": "14.0.1.0.0", + "author": "Open Source Integrators, Odoo Community Association (OCA)", + "summary": """Update BOM costs by rolling up. Adds scheduled job for + unattended rollups.""", + "license": "AGPL-3", + "category": "Product", + "maintainer": "dreispt", + "development_status": "Alpha", + "website": "https://github.com/OCA/manufacture", + "depends": [ + "mrp_account", + "stock_account", + ], + "data": [ + "views/product_views.xml", + "views/mrp_bom.xml", + "views/res_config_settings.xml", + "data/cost_rollup_scheduler.xml", + "data/email_template.xml", + ], + "installable": True, +} diff --git a/product_cost_rollup_to_bom/data/cost_rollup_scheduler.xml b/product_cost_rollup_to_bom/data/cost_rollup_scheduler.xml new file mode 100644 index 000000000..80ab1e60e --- /dev/null +++ b/product_cost_rollup_to_bom/data/cost_rollup_scheduler.xml @@ -0,0 +1,23 @@ + + + + + + + + BoM Cost Rollup: run scheduler + + code + +model.compute_bom_cost_rollup() + + + + 30 + days + -1 + + + + + diff --git a/product_cost_rollup_to_bom/data/email_template.xml b/product_cost_rollup_to_bom/data/email_template.xml new file mode 100644 index 000000000..16776ba79 --- /dev/null +++ b/product_cost_rollup_to_bom/data/email_template.xml @@ -0,0 +1,28 @@ + + + + Event Scheduler Notification for event: BoM Cost Rollup + + + ${ctx["email_from"]} + ${ctx["email_to"]} + Event Scheduler Notification for event: BoM Cost Rollup + + - Date: ${datetime.datetime.now().strftime('%m/%d/%Y, %H:%M:%S')}
+ - Total Product's updated: ${ctx["product_list_len"]}
+ % set line_dict = ctx.get('product_list',False) + % for key, value in line_dict.items() + Product ${key} Standard Cost: ${'%8.2f' % value}
+ % endfor + ]]> +
+
+
+
diff --git a/product_cost_rollup_to_bom/models/__init__.py b/product_cost_rollup_to_bom/models/__init__.py new file mode 100644 index 000000000..fc0c338ef --- /dev/null +++ b/product_cost_rollup_to_bom/models/__init__.py @@ -0,0 +1,6 @@ +# Copyright (C) 2021, Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import product +from . import mrp_bom +from . import res_config_settings diff --git a/product_cost_rollup_to_bom/models/mrp_bom.py b/product_cost_rollup_to_bom/models/mrp_bom.py new file mode 100644 index 000000000..5b7f26a9c --- /dev/null +++ b/product_cost_rollup_to_bom/models/mrp_bom.py @@ -0,0 +1,102 @@ +# Copyright (C) 2021, Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import logging +from datetime import datetime + +from odoo import api, fields, models + +_logger = logging.getLogger(__name__) + + +class MrpBom(models.Model): + _inherit = "mrp.bom" + + std_cost_update_date = fields.Datetime( + string="Standard Cost Update Date", + copy=False, + help="Last time the standard cost was performed on this BOM.", + ) + + def get_product_variants(self, product): + # Returns all the Variants for the requested product + return product.with_prefetch().product_variant_ids + + def _update_bom(self, bomdate): + self.ensure_one() + for line in self.bom_line_ids: + if line.child_bom_id: + result = line.child_bom_id._update_bom(self.std_cost_update_date) + if result: + return True + elif ( + not line.product_id.std_cost_update_date + or not self.std_cost_update_date + or line.product_id.std_cost_update_date > self.std_cost_update_date + or line.product_id.write_date > self.std_cost_update_date + or line.product_id.product_tmpl_id.write_date + > self.std_cost_update_date + or (bomdate and line.product_id.write_date > bomdate) + ): + return True + return False + + @api.model + def compute_bom_cost_rollup(self): + + _logger.info("BOM Cost Rollup Process Started") + + # Get BoM's whose product is using costing method as standard + current_time = datetime.now() + + bom_ids = self.sudo().search( + [("product_tmpl_id.categ_id.property_cost_method", "=", "standard")] + ) + for bom in bom_ids: + # Check if cost method is standard + if ( + bom.product_tmpl_id.categ_id.property_cost_method + and bom.product_tmpl_id.categ_id.property_cost_method == "standard" + ): + # Get all product variants for BoM product template + product_variants = self.get_product_variants(bom.product_tmpl_id) + # update only if necessary + if bom._update_bom(bom.std_cost_update_date): + product_variants.action_bom_cost() + + _logger.info("BOM Cost Rollup Process Completed") + + product_list = {} + # FIXME: code smell - variable name reused + bom_ids = self.sudo().search([("std_cost_update_date", ">=", current_time)]) + if bom_ids: + _logger.info("BOM Cost Rollup Email Process Started") + for bom in bom_ids: + product_variants = self.get_product_variants(bom.product_tmpl_id) + for variant in product_variants: + product_list[variant.default_code] = variant.standard_price + + # TODO: use an email template, no settings config will be needed then + # Log if no user email to notify + if not self.env.user.company_id.bom_cost_email: + _logger.error( + "Exception while executing \ + BoM Cost Rollup: \ + Please configure email to notify from Company." + ) + template_id = self.env.ref( + "product_cost_rollup_to_bom.bom_cost_rollup_email_template" + ) + template_id.with_context( + { + "product_list_len": len(product_list), + "email_to": self.env.user.company_id.bom_cost_email, + "email_from": self.env.user.partner_id.email, + "product_list": product_list, + } + ).send_mail(self.id, force_send=True) + + _logger.info("BOM Cost Rollup Email Process Completed") + else: + _logger.info("No changes to BOM Cost Rollup. No Email.") + return True diff --git a/product_cost_rollup_to_bom/models/product.py b/product_cost_rollup_to_bom/models/product.py new file mode 100644 index 000000000..d123e9456 --- /dev/null +++ b/product_cost_rollup_to_bom/models/product.py @@ -0,0 +1,135 @@ +# Copyright (C) 2021, Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import logging +from datetime import datetime + +from odoo import _, fields, models +from odoo.exceptions import UserError +from odoo.tools import float_is_zero + +_logger = logging.getLogger(__name__) + + +class ProductProduct(models.Model): + _inherit = "product.product" + + std_cost_update_date = fields.Datetime( + string="Standard Cost Update Date", + copy=False, + help="Last time the standard cost was performed on this product.", + ) + + def action_bom_cost(self): + real_time_products = self.filtered( + lambda p: p.valuation == "real_time" and p.valuation == "fifo" + ) + if real_time_products: + raise UserError( + _( + "The costing method on some products %s is FIFO." + " The cost will be computed during manufacturing process." + " Use Standard Costing to update BOM cost manually." + ) + % (real_time_products.mapped("display_name")) + ) + # else: + boms_to_recompute = self.env["mrp.bom"].search( + [ + "|", + ("product_id", "in", self.ids), + "&", + ("product_id", "=", False), + ("product_tmpl_id", "in", self.mapped("product_tmpl_id").ids), + ] + ) + for product in self: + new_price = product._set_price_from_bom(boms_to_recompute) or 0.0 + # FIXME: precision rounding should be taken from configs + if product.cost_method == "standard" and not float_is_zero( + new_price - product.standard_price, precision_rounding=2 + ): + product._change_standard_price(new_price) + product.std_cost_update_date = datetime.now() + if product.product_tmpl_id.product_variant_count == 1: + _logger.info( + "Product : %s Standard Price: %s ", + product.default_code, + str(product.product_tmpl_id.standard_price), + ) + else: + _logger.info( + "Product : %s Standard Price: %s ", + product.default_code, + str(product.standard_price), + ) + + def _set_price_from_bom(self, boms_to_recompute=False): + self.ensure_one() + bom = self.env["mrp.bom"]._bom_find(product=self) + if bom: + self.standard_price = self.with_context(cost_all=True)._compute_bom_price( + bom, boms_to_recompute=boms_to_recompute + ) + bom.std_cost_update_date = datetime.now() + + def _compute_bom_price(self, bom, boms_to_recompute=False): + self.ensure_one() + if not boms_to_recompute: + boms_to_recompute = [] + total = 0 + for opt in bom.operation_ids: + duration_expected = ( + opt.workcenter_id.time_start + + opt.workcenter_id.time_stop + + opt.time_cycle + ) + total += (duration_expected / 60) * opt.workcenter_id.costs_hour + + for line in bom.bom_line_ids: + if line._skip_bom_line(self): + continue + + # Compute recursive if line has `child_line_ids` and the product + # has not been computed recently + if ( + line.child_bom_id + and ( + line.child_bom_id in boms_to_recompute + or self.env.context.get("cost_all", True) + ) + and ( + not bom.std_cost_update_date + or not line.product_id.std_cost_update_date + or line.child_bom_id._update_bom(bom.std_cost_update_date) + ) + ): + child_total = line.product_id._compute_bom_price( + line.child_bom_id, boms_to_recompute=boms_to_recompute + ) + total += ( + line.product_id.uom_id._compute_price( + child_total, line.product_uom_id + ) + * line.product_qty + ) + if not float_is_zero( + child_total - line.product_id.standard_price, + precision_rounding=2, + ): + line.product_id._change_standard_price(child_total) + line.product_id.std_cost_update_date = datetime.now() + _logger.info( + "Product : %s Standard Price: %s ", + line.product_id.default_code, + str(line.product_id.standard_price), + ) + else: + ctotal = ( + line.product_id.uom_id._compute_price( + line.product_id.standard_price, line.product_uom_id + ) + * line.product_qty + ) + total += ctotal + return bom.product_uom_id._compute_price(total / bom.product_qty, self.uom_id) diff --git a/product_cost_rollup_to_bom/models/res_config_settings.py b/product_cost_rollup_to_bom/models/res_config_settings.py new file mode 100644 index 000000000..dc848a503 --- /dev/null +++ b/product_cost_rollup_to_bom/models/res_config_settings.py @@ -0,0 +1,39 @@ +# Copyright (C) 2021, Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + bom_cost_email = fields.Char( + string="BoM cost rollup email", + related="company_id.bom_cost_email", + readonly=False, + help="BoM Cost rollup Email notification will be sent to this email address", + ) + + def get_values(self): + res = super(ResConfigSettings, self).get_values() + res.update( + bom_cost_email=self.env["ir.config_parameter"] + .sudo() + .get_param("product_cost_rollup_to_bom.bom_cost_email") + ) + return res + + def set_values(self): + super(ResConfigSettings, self).set_values() + self.env["ir.config_parameter"].sudo().set_param( + "product_cost_rollup_to_bom.bom_cost_email", self.bom_cost_email + ) + + +class ResCompany(models.Model): + _inherit = "res.company" + + bom_cost_email = fields.Char( + string="BoM cost rollup email", + help="BoM Cost rollup Email notification will be sent to this email address", + ) diff --git a/product_cost_rollup_to_bom/readme/CONFIGURE.rst b/product_cost_rollup_to_bom/readme/CONFIGURE.rst new file mode 100644 index 000000000..b09db59f5 --- /dev/null +++ b/product_cost_rollup_to_bom/readme/CONFIGURE.rst @@ -0,0 +1,7 @@ +Go to Manufacturing/ Configuration/ Settings/ BoM Cost Rollup Email and add email that you want to send BoM cost rollup email to. + +Or + +Go to Settings/ Users & Companies/ Companies and add email that you want to send BoM cost rollup email to. + +Turn On Debugger mode Go to Settings/ Technical/ Automation/ Scheduled Actions Change the interval you want scheduler to run. diff --git a/product_cost_rollup_to_bom/readme/CONTRIBUTORS.rst b/product_cost_rollup_to_bom/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..2d74f6c71 --- /dev/null +++ b/product_cost_rollup_to_bom/readme/CONTRIBUTORS.rst @@ -0,0 +1,6 @@ +* `Open Source Integrators `: + + * Balaji Kannan + * Mayank Gosai + * Daniel Reis + * Chandresh Thakkar diff --git a/product_cost_rollup_to_bom/readme/DESCRIPTION.rst b/product_cost_rollup_to_bom/readme/DESCRIPTION.rst new file mode 100644 index 000000000..127dd14d4 --- /dev/null +++ b/product_cost_rollup_to_bom/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +With Standard Cost Method, products that have bill of materials defined show rollup cost from the BOM. This module allows the cost to be rolled up based on BOM's that have components change standard price. The module can be used from product template/product variant forms using "Compute BOM Cost Rollup" or from the Scheduled Job across all BOM's that need an update. The module sends an email on modified standard cost products to an email configured on the Manufacturing App. diff --git a/product_cost_rollup_to_bom/readme/USAGE.rst b/product_cost_rollup_to_bom/readme/USAGE.rst new file mode 100644 index 000000000..5c7bfcee8 --- /dev/null +++ b/product_cost_rollup_to_bom/readme/USAGE.rst @@ -0,0 +1,3 @@ +* Set the BOM Cost Rollup Notification Email +* To update a single product: Click on the Compute BOM Cost Rollup button +* To run on BOMs that have changed, run the scheduler manually or in cron mode. diff --git a/product_cost_rollup_to_bom/static/description/icon.png b/product_cost_rollup_to_bom/static/description/icon.png new file mode 100644 index 000000000..84791119f Binary files /dev/null and b/product_cost_rollup_to_bom/static/description/icon.png differ diff --git a/product_cost_rollup_to_bom/views/mrp_bom.xml b/product_cost_rollup_to_bom/views/mrp_bom.xml new file mode 100644 index 000000000..42d4f3427 --- /dev/null +++ b/product_cost_rollup_to_bom/views/mrp_bom.xml @@ -0,0 +1,14 @@ + + + + + view.std.price.mrp.bom + mrp.bom + + +
+ +
+
+
+
diff --git a/product_cost_rollup_to_bom/views/product_views.xml b/product_cost_rollup_to_bom/views/product_views.xml new file mode 100644 index 000000000..a634f2963 --- /dev/null +++ b/product_cost_rollup_to_bom/views/product_views.xml @@ -0,0 +1,19 @@ + + + + + view.std.price.product.product + product.product + + + + + + + + + diff --git a/product_cost_rollup_to_bom/views/res_config_settings.xml b/product_cost_rollup_to_bom/views/res_config_settings.xml new file mode 100644 index 000000000..2cb90d378 --- /dev/null +++ b/product_cost_rollup_to_bom/views/res_config_settings.xml @@ -0,0 +1,45 @@ + + + + + + + view.email.res.settings + res.config.settings + + + +
+
+
+
+ +
+ BoM Cost rollup Email notification will be sent to this email address
+
+
+
+
+
+
+ + + + view.email.company + res.company + + + + + + + + +
diff --git a/setup/product_cost_rollup_to_bom/odoo/addons/product_cost_rollup_to_bom b/setup/product_cost_rollup_to_bom/odoo/addons/product_cost_rollup_to_bom new file mode 120000 index 000000000..3989e2123 --- /dev/null +++ b/setup/product_cost_rollup_to_bom/odoo/addons/product_cost_rollup_to_bom @@ -0,0 +1 @@ +../../../../product_cost_rollup_to_bom \ No newline at end of file diff --git a/setup/product_cost_rollup_to_bom/setup.py b/setup/product_cost_rollup_to_bom/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/product_cost_rollup_to_bom/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)