Merge PR #951 into 14.0

Signed-off-by LoisRForgeFlow
This commit is contained in:
OCA-git-bot
2023-11-13 10:13:57 +00:00
25 changed files with 1406 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
from . import models
from . import report

View File

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

View File

@@ -0,0 +1,4 @@
from . import mrp_production
from . import mrp_bom
from . import product
from . import stock_move

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
You just have to set a 'Cost Share' value for each By-Product in the BOM form.

View File

@@ -0,0 +1,3 @@
* `ForgeFlow <https://www.forgeflow.com/>`__:
* Maria de Luna <maria.de.luna@forgeflow.com>

View File

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

View File

@@ -0,0 +1,4 @@
14.0.1.0.0 (2023-01-23)
~~~~~~~~~~~~~~~~~~~~~~~
* Start of the history.

View File

@@ -0,0 +1 @@
from . import mrp_report_bom_structure

View File

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

View File

@@ -0,0 +1,204 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<template id="report_mrp_bom" inherit_id="mrp.report_mrp_bom">
<xpath
expr="//div[@class='container o_mrp_bom_report_page']/t"
position="attributes"
>
<attribute
name="t-if"
>data.get('components') or data.get('lines') or data.get('operations')</attribute>
</xpath>
<xpath
expr="//tbody/tr/td[@class='o_mrp_bom_cost text-right']/span/t"
position="attributes"
>
<attribute name="t-esc">data['bom_cost']</attribute>
</xpath>
<xpath expr="//tfoot" position="attributes">
<attribute name="t-if">False</attribute>
</xpath>
<xpath expr="//tfoot" position="after">
<tfoot>
<tr
t-if="data['report_structure'] != 'bom_structure'"
class="o_mrp_prod_cost"
>
<td />
<td name="td_mrp_bom_f" class="text-right">
<span><t
t-if="data['byproducts']"
t-esc="data['bom_prod_name']"
/></span>
</td>
<td class="text-right"><span><strong>Unit Cost</strong></span></td>
<td groups="uom.group_uom"><span><t
t-esc="data['bom'].product_uom_id.name"
/></span></td>
<td class="text-right">
<span><t
t-esc="data['price']/data['bom_qty']"
t-options='{"widget": "monetary", "display_currency": currency}'
/></span>
</td>
<td class="text-right">
<span><t
t-esc="data['cost_share'] * data['total'] / data['bom_qty']"
t-options='{"widget": "monetary", "display_currency": currency}'
/></span>
</td>
</tr>
<t
t-if="data['report_structure'] != 'bom_structure'"
t-foreach="data['byproducts']"
t-as="byproduct"
>
<tr class="o_mrp_bom_cost">
<td />
<td name="td_mrp_bom_byproducts_f" class="text-right">
<span><t t-esc="byproduct['product_name']" /></span>
</td>
<td class="text-right"><span><strong
>Unit Cost</strong></span></td>
<td groups="uom.group_uom"><span><t
t-esc="byproduct['product_uom']"
/></span></td>
<td class="text-right">
<span><t
t-esc="byproduct['product_cost'] / byproduct['product_qty']"
t-options='{"widget": "monetary", "display_currency": currency}'
/></span>
</td>
<td class="text-right">
<span><t
t-esc="byproduct['cost_share'] * data['total'] / byproduct['product_qty']"
t-options='{"widget": "monetary", "display_currency": currency}'
/></span>
</td>
</tr>
</t>
</tfoot>
</xpath>
</template>
<template
id="report_mrp_bom_line_byproduct_cost"
inherit_id="mrp.report_mrp_bom_line"
>
<xpath expr="//td[@class='o_mrp_prod_cost']" position="attributes">
<attribute name="class" />
</xpath>
<xpath expr="//t[last()]" posiiton="after">
<t t-if="data['byproducts']">
<t
t-set="space_td"
t-value="'margin-left: '+ str(data['level'] * 20) + 'px;'"
/>
<tr
class="o_mrp_bom_report_line o_mrp_bom_cost"
t-att-data-id="'byproduct-' + str(data['bom'].id)"
t-att-data-bom-id="data['bom'].id"
t-att-parent_id="data['bom'].id"
t-att-data-qty="data['bom_qty']"
t-att-data-level="data['level']"
t-att-data-total="data['total']"
>
<td name="td_byproducts">
<span t-att-style="space_td" />
<span
class="o_mrp_bom_unfoldable fa fa-fw fa-caret-right"
t-att-data-function="'get_byproducts'"
role="img"
aria-label="Unfold"
title="Unfold"
/>
By-Products
</td>
<td />
<td class="text-right">
<span t-esc="data['byproducts_total']" />
</td>
<td groups="uom.group_uom" />
<td />
<td class="text-right">
<span
t-esc="data['byproducts_cost']"
t-options='{"widget": "monetary", "display_currency": currency}'
/>
</td>
</tr>
</t>
</xpath>
</template>
<template
id="report_mrp_operation_line_byproduct"
inherit_id="mrp.report_mrp_operation_line"
>
<xpath expr="//td[@class='o_mrp_prod_cost']" position="attributes">
<attribute name="class" />
</xpath>
</template>
<template id="report_mrp_byproduct_line">
<t t-set="currency" t-value="data['currency']" />
<t t-foreach="data['byproducts']" t-as="byproduct">
<t
t-set="space_td"
t-value="'margin-left: '+ str(byproduct['level'] * 20) + 'px;'"
/>
<tr
class="o_mrp_bom_report_line o_mrp_bom_cost"
t-att-parent_id="'byproduct-' + str(data['bom_id'])"
>
<td name="td_byproduct_line">
<span t-att-style="space_td" />
<a
href="#"
t-att-data-res-id="byproduct['product_id'].id"
t-att-data-model="byproduct['product_id']._name"
class="o_mrp_bom_action"
><t t-esc="byproduct['product_name']" /></a>
</td>
<td />
<td class="text-right">
<span t-esc="byproduct['product_qty']" />
</td>
<td groups="uom.group_uom"><span
t-esc="byproduct['product_uom']"
/></td>
<td class="text-right">
<span
t-esc="byproduct['product_cost']"
t-options='{"widget": "monetary", "display_currency": currency}'
/>
</td>
<td class="text-right">
<span
t-esc="byproduct['bom_cost']"
t-options='{"widget": "monetary", "display_currency": currency}'
/>
</td>
<td />
</tr>
</t>
</template>
<template
id="report_mrp_bom_pdf_line_byproduct_cost"
inherit_id="mrp.report_mrp_bom_pdf_line"
>
<xpath expr="//t[@t-as='l']/tr" position="attributes">
<attribute
name="t-if"
>data['report_structure'] != 'bom_structure' or l['type'] not in ['operation', 'byproduct']</attribute>
</xpath>
<xpath
expr="///t[@t-as='l']/tr/td[@class='text-right']/span/t[2]"
position="attributes"
>
<attribute name="t-if">l['type'] in ['bom', 'byproduct']</attribute>
</xpath>
</template>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -0,0 +1,525 @@
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.15.1: http://docutils.sourceforge.net/" />
<title>Production Grouped By Product</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="production-grouped-by-product">
<h1 class="title">Production Grouped By Product</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/manufacture/tree/14.0/mrp_production_grouped_by_product"><img alt="OCA/manufacture" src="https://img.shields.io/badge/github-OCA%2Fmanufacture-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/manufacture-14-0/manufacture-14-0-mrp_production_grouped_by_product"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/129/14.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>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.</p>
<p>With this module, each time an MTO manufacturing order is required to be
created, it first checks that theres no other existing order not yet started
for the same product and bill of materials inside the specied time frame , and
if theres one, then the quantity of that order is increased instead of
creating a new one.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#configuration" id="id8">Configuration</a></li>
<li><a class="reference internal" href="#known-issues-roadmap" id="id9">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#changelog" id="id10">Changelog</a><ul>
<li><a class="reference internal" href="#id1" id="id11">14.0.1.0.0 (2021-11-16)</a></li>
<li><a class="reference internal" href="#id2" id="id12">13.0.1.0.0 (2020-01-09)</a></li>
<li><a class="reference internal" href="#id3" id="id13">12.0.1.0.0 (2019-04-17)</a></li>
<li><a class="reference internal" href="#id4" id="id14">11.0.2.0.1 (2018-07-02)</a></li>
<li><a class="reference internal" href="#id5" id="id15">11.0.2.0.0 (2018-06-04)</a></li>
<li><a class="reference internal" href="#id6" id="id16">11.0.1.0.1 (2018-05-11)</a></li>
<li><a class="reference internal" href="#id7" id="id17">11.0.1.0.0 (2018-05-11)</a></li>
</ul>
</li>
<li><a class="reference internal" href="#bug-tracker" id="id18">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id19">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id20">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id21">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id22">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#id8">Configuration</a></h1>
<p>To configure the time frame for grouping manufacturing order:</p>
<ol class="arabic">
<li><p class="first">Go to <em>Inventory &gt; Configuration &gt; Warehouse Management &gt; Operation Types</em></p>
</li>
<li><p class="first">Locate the manufacturing type you are using (default one is called
“Manufacturing”).</p>
</li>
<li><p class="first">Open it and change these 2 values:</p>
<ul class="simple">
<li>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.</li>
<li>MO grouping interval (days): The number of days for grouping together on
the same manufacturing order.</li>
</ul>
<p>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.</p>
</li>
</ol>
</div>
<div class="section" id="known-issues-roadmap">
<h1><a class="toc-backref" href="#id9">Known issues / Roadmap</a></h1>
<ul class="simple">
<li>Add a check in the product form for excluding it from being grouped.</li>
</ul>
</div>
<div class="section" id="changelog">
<h1><a class="toc-backref" href="#id10">Changelog</a></h1>
<div class="section" id="id1">
<h2><a class="toc-backref" href="#id11">14.0.1.0.0 (2021-11-16)</a></h2>
<ul class="simple">
<li>[MIG] Migration to v14.</li>
</ul>
</div>
<div class="section" id="id2">
<h2><a class="toc-backref" href="#id12">13.0.1.0.0 (2020-01-09)</a></h2>
<ul class="simple">
<li>[MIG] Migration to v13.</li>
</ul>
</div>
<div class="section" id="id3">
<h2><a class="toc-backref" href="#id13">12.0.1.0.0 (2019-04-17)</a></h2>
<ul class="simple">
<li>[MIG] Migration to v12:</li>
</ul>
</div>
<div class="section" id="id4">
<h2><a class="toc-backref" href="#id14">11.0.2.0.1 (2018-07-02)</a></h2>
<ul class="simple">
<li>[FIX] fix test in mrp_production_grouped_by_product</li>
</ul>
</div>
<div class="section" id="id5">
<h2><a class="toc-backref" href="#id15">11.0.2.0.0 (2018-06-04)</a></h2>
<ul class="simple">
<li>[IMP] mrp_production_grouped_by_product: Time frames</li>
</ul>
</div>
<div class="section" id="id6">
<h2><a class="toc-backref" href="#id16">11.0.1.0.1 (2018-05-11)</a></h2>
<ul class="simple">
<li>[IMP] mrp_production_grouped_by_company: Context evaluation on mrp.production + tests</li>
</ul>
</div>
<div class="section" id="id7">
<h2><a class="toc-backref" href="#id17">11.0.1.0.0 (2018-05-11)</a></h2>
<ul class="simple">
<li>Start of the history.</li>
</ul>
</div>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id18">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/manufacture/issues">GitHub Issues</a>.
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
<a class="reference external" href="https://github.com/OCA/manufacture/issues/new?body=module:%20mrp_production_grouped_by_product%0Aversion:%2014.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#id19">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id20">Authors</a></h2>
<ul class="simple">
<li>Tecnativa</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id21">Contributors</a></h2>
<ul class="simple">
<li><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a><ul>
<li>David Vidal</li>
<li>Pedro M. Baeza</li>
</ul>
</li>
<li><a class="reference external" href="https://ecosoft.co.th/">Ecosoft</a>:<ul>
<li>Pimolnat Suntian &lt;<a class="reference external" href="mailto:pimolnats&#64;ecosoft.co.th">pimolnats&#64;ecosoft.co.th</a>&gt;</li>
</ul>
</li>
<li><a class="reference external" href="https://www.forgeflow.com/">ForgeFlow</a>:<ul>
<li>Lois Rilo &lt;<a class="reference external" href="mailto:lois.rilo&#64;forgeflow.com">lois.rilo&#64;forgeflow.com</a>&gt;</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id22">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>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.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/manufacture/tree/14.0/mrp_production_grouped_by_product">OCA/manufacture</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View File

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

View File

@@ -0,0 +1 @@
from . import test_mrp_production_byproduct_cost_share

View File

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

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2023 ForgFlow S.L
License LGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="mrp_bom_form_view" model="ir.ui.view">
<field name="name">mrp.bom.form</field>
<field name="model">mrp.bom</field>
<field name="inherit_id" ref="mrp.mrp_bom_form_view" />
<field name="arch" type="xml">
<xpath
expr="//field[@name='byproduct_ids']/tree//field[@name='product_uom_id']"
position="after"
>
<field name="cost_share" optional="show" />
</xpath>
</field>
</record>
<record id="mrp.template_open_bom" model="ir.actions.act_window">
<field name="context">{'default_product_tmpl_id': active_id}</field>
<field
name="domain"
>['|', ('product_tmpl_id', '=', active_id), ('byproduct_ids.product_id.product_tmpl_id', '=', active_id)]</field>
</record>
<record id="mrp.product_open_bom" model="ir.actions.act_window">
<field name="context">{'default_product_id': active_id}</field>
</record>
</odoo>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2023 ForgFlow S.L
License LGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="mrp_production_form_view" model="ir.ui.view">
<field name="name">mrp.production.form</field>
<field name="model">mrp.production</field>
<field name="inherit_id" ref="mrp.mrp_production_form_view" />
<field name="arch" type="xml">
<xpath
expr="//field[@name='move_byproduct_ids']/tree//field[@name='product_uom']"
position="after"
>
<field name="cost_share" optional="show" />
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<template id="assets_backend" name="mrp assets" inherit_id="mrp.assets_backend">
<xpath expr="." position="inside">
<link
rel="stylesheet"
type="text/scss"
href="/mrp/static/src/scss/mrp_workorder_kanban.scss"
/>
<script
type="text/javascript"
src="/mrp_production_byproduct_cost_share/static/src/js/mrp_bom_report.js"
/>
</xpath>
</template>
</odoo>

View File

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

View File

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