product_cost_rollup_to_bom: update the standard cost of a product

This commit is contained in:
Chandresh Thakkar OSI
2021-05-17 20:13:09 +05:30
committed by Héctor Vi Or
parent 9df9a33095
commit 47576de8b2
19 changed files with 567 additions and 0 deletions

View File

@@ -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 <https://odoo-community.org/page/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 <https://github.com/OCA/manufacture/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 <https://github.com/OCA/manufacture/issues/new?body=module:%20product_cost_rollup_to_bom%0Aversion:%2014.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Open Source Integrators
Contributors
~~~~~~~~~~~~
* `Open Source Integrators <https://opensourceintegrators.com>`:
* Balaji Kannan <bkannan@opensourceintegrators.com>
* Mayank Gosai <mgosai@opensourceintegrators.com>
* Daniel Reis <dreis@opensourceintegrators.com>
* Chandresh Thakkar <cthakkr@opensourceintegrators.com>
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 <https://github.com/OCA/manufacture/tree/14.0/product_cost_rollup_to_bom>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@@ -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

View File

@@ -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,
}

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<data noupdate="1">
<!-- Scheduler for BoM Cost Rollup -->
<record forcecreate="True" id="ir_cron_scheduler_cost_rollup" model="ir.cron">
<field name="name">BoM Cost Rollup: run scheduler</field>
<field name="model_id" ref="model_mrp_bom" />
<field name="state">code</field>
<field name="code">
model.compute_bom_cost_rollup()
</field>
<field eval="False" name="active" />
<field name="user_id" ref="base.user_root" />
<field name="interval_number">30</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field eval="False" name="doall" />
</record>
</data>
</odoo>

View File

@@ -0,0 +1,28 @@
<odoo>
<data>
<record id="bom_cost_rollup_email_template" model="mail.template">
<field
name="name"
>Event Scheduler Notification for event: BoM Cost Rollup</field>
<field name="model_id" ref="mrp.model_mrp_bom" />
<field name="auto_delete" eval="True" />
<field name="email_from">${ctx["email_from"]}</field>
<field name="email_to">${ctx["email_to"]}</field>
<field
name="subject"
>Event Scheduler Notification for event: BoM Cost Rollup</field>
<field
name="body_html"
><![CDATA[
Event Scheduler for BoM Cost Rollup was completed: <br/>
- Date: ${datetime.datetime.now().strftime('%m/%d/%Y, %H:%M:%S')}<br/>
- Total Product's updated: ${ctx["product_list_len"]}<br/>
% set line_dict = ctx.get('product_list',False)
% for key, value in line_dict.items()
Product ${key} Standard Cost: ${'%8.2f' % value} <br/>
% endfor
]]>
</field>
</record>
</data>
</odoo>

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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",
)

View File

@@ -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.

View File

@@ -0,0 +1,6 @@
* `Open Source Integrators <https://opensourceintegrators.com>`:
* Balaji Kannan <bkannan@opensourceintegrators.com>
* Mayank Gosai <mgosai@opensourceintegrators.com>
* Daniel Reis <dreis@opensourceintegrators.com>
* Chandresh Thakkar <cthakkr@opensourceintegrators.com>

View File

@@ -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.

View File

@@ -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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" ?>
<odoo>
<!-- BoM form view -->
<record id="view_std_price_mrp_bom" model="ir.ui.view">
<field name="name">view.std.price.mrp.bom</field>
<field name="model">mrp.bom</field>
<field name="inherit_id" ref="mrp.mrp_bom_form_view" />
<field name="arch" type="xml">
<div class="o_row" position="after">
<field name="std_cost_update_date" readonly="1" />
</div>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" ?>
<odoo>
<!-- product product form view -->
<record id="view_std_price_product_product" model="ir.ui.view">
<field name="name">view.std.price.product.product</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="product.product_normal_form_view" />
<field name="arch" type="xml">
<field name="company_id" position="before">
<field name="std_cost_update_date" readonly="1" />
</field>
<button name="button_bom_cost" position="attributes">
<attribute
name="attrs"
>{'invisible': ['|', ('bom_count', '=', 0), '&amp;', ('valuation', '=', 'real_time'), ('cost_method', 'in', ('fifo','standard'))]}</attribute>
</button>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" ?>
<odoo>
<!-- Copyright 2021 Open Source Integrators
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<!-- Res Config Settings form view -->
<record id="view_email_res_settings" model="ir.ui.view">
<field name="name">view.email.res.settings</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="mrp.res_config_settings_view_form" />
<field name="arch" type="xml">
<xpath expr="//div[@id='mrp_byproduct']" position="after">
<div
class="col-lg-6 col-12 o_setting_box"
id="bom_rollup"
title="BoM Cost rollup Email notification will be sent to this email address."
>
<div class="o_setting_left_pane">
<label for="bom_cost_email" string="Email" />
</div>
<div class="o_setting_right_pane">
<field name="bom_cost_email" widget="email" />
<div class="text-muted">
BoM Cost rollup Email notification will be sent to this email address<br
/>
</div>
</div>
</div>
</xpath>
</field>
</record>
<!-- Company form view -->
<record id="view_email_company" model="ir.ui.view">
<field name="name">view.email.company</field>
<field name="model">res.company</field>
<field name="inherit_id" ref="base.view_company_form" />
<field name="arch" type="xml">
<xpath expr="//div[hasclass('o_address_format')]" position="after">
<field name="bom_cost_email" />
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1 @@
../../../../product_cost_rollup_to_bom

View File

@@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)