[IMP] mrp: Add byproducts to MO Overview

Adds the `By-Products` section in the MO Overview as done in the BoM
Overview. This allows to correctly factor in the cost share of the
production's byproducts.

Part of task-3251506

Part-of: odoo/odoo#114536
This commit is contained in:
clesgow
2023-10-20 14:59:09 +00:00
parent 7e5505da06
commit d0f0621c86
8 changed files with 172 additions and 6 deletions
+57 -5
View File
@@ -54,12 +54,16 @@ class ReportMoOverview(models.AbstractModel):
production = self.env['mrp.production'].browse(production_id)
components = self._get_components_data(production, level=1, current_index='')
operations = self._get_operations_data(production, level=1, current_index='')
summary = self._get_mo_summary(production, components, operations)
initial_mo_cost = sum(component.get('summary', {}).get('mo_cost', 0.0) for component in components)\
+ operations.get('summary', {}).get('mo_cost', 0.0)
byproducts_cost_portion, byproducts = self._get_byproducts_data(production, initial_mo_cost, level=1, current_index='')
summary = self._get_mo_summary(production, components, initial_mo_cost, byproducts_cost_portion)
return {
'id': production.id,
'summary': summary,
'components': components,
'operations': operations,
'byproducts': byproducts,
'extras': self._get_report_extra_lines(summary),
}
@@ -73,10 +77,10 @@ class ReportMoOverview(models.AbstractModel):
'unit_product_cost': unit_product_cost,
}
def _get_mo_summary(self, production, components, operations):
def _get_mo_summary(self, production, components, current_mo_cost, byproducts_cost_portion):
product = production.product_id
company = production.company_id or self.env.company
mo_cost = sum(component.get('summary', {}).get('mo_cost', 0.0) for component in components)
byproducts_cost_share = float_round(1 - byproducts_cost_portion, precision_rounding=0.0001)
return {
'level': 0,
'model': production._name,
@@ -93,7 +97,7 @@ class ReportMoOverview(models.AbstractModel):
'quantity_on_hand': product.uom_id._compute_quantity(product.qty_available, production.product_uom_id) if product.type == 'product' else False,
'quantity_reserved': 0.0,
'receipt': self._check_planned_start(production.date_deadline, self._get_replenishment_receipt(production, components)),
'mo_cost': company.currency_id.round(mo_cost + operations.get('summary', {}).get('mo_cost', 0.0)),
'mo_cost': company.currency_id.round(current_mo_cost * byproducts_cost_share),
'product_cost': company.currency_id.round(product.standard_price * production.product_uom_qty),
'currency_id': company.currency_id.id,
'currency': company.currency_id,
@@ -187,6 +191,48 @@ class ReportMoOverview(models.AbstractModel):
'details': operations,
}
def _get_byproducts_data(self, production, current_mo_cost, level=0, current_index=False):
currency = (production.company_id or self.env.company).currency_id
byproducts = []
byproducts_cost_portion = 0
total_mo_cost = 0
total_product_cost = 0
for index, move_bp in enumerate(production.move_byproduct_ids):
product = move_bp.product_id
cost_share = move_bp.cost_share / 100
byproducts_cost_portion += cost_share
mo_cost = current_mo_cost * cost_share
product_cost = product.standard_price * move_bp.product_uom._compute_quantity(move_bp.product_uom_qty, product.uom_id)
total_mo_cost += mo_cost
total_product_cost += product_cost
byproducts.append({
'level': level,
'index': f"{current_index}B{index}",
'model': product._name,
'id': product.id,
'name': product.display_name,
'quantity': move_bp.product_uom_qty,
'uom_name': move_bp.product_uom.display_name,
'uom_precision': self._get_uom_precision(move_bp.product_uom.rounding),
'mo_cost': currency.round(mo_cost),
'mo_cost_decorator': self._get_comparison_decorator(product_cost, mo_cost, currency.rounding),
'product_cost': currency.round(product_cost),
'currency_id': currency.id,
'currency': currency,
})
return byproducts_cost_portion, {
'summary': {
'index': f"{current_index}B",
'mo_cost': currency.round(total_mo_cost),
'mo_cost_decorator': self._get_comparison_decorator(total_product_cost, total_mo_cost, currency.rounding),
'product_cost': currency.round(total_product_cost),
'currency_id': currency.id,
'currency': currency,
},
'details': byproducts,
}
def _get_components_data(self, production, replenish_data=False, level=0, current_index=False):
if not replenish_data:
replenish_data = {
@@ -315,7 +361,13 @@ class ReportMoOverview(models.AbstractModel):
if doc_in._name == 'mrp.production':
replenishment['components'] = self._get_components_data(doc_in, replenish_data, level + 2, replenishment_index)
replenishment['operations'] = self._get_operations_data(doc_in, level + 2, replenishment_index)
replenishment['summary']['mo_cost'] = currency.round(sum(component.get('summary', {}).get('mo_cost', 0.0) for component in replenishment['components']) + replenishment['operations'].get('summary', {}).get('mo_cost', 0.0))
initial_mo_cost = sum(component.get('summary', {}).get('mo_cost', 0.0) for component in replenishment['components'])\
+ replenishment['operations'].get('summary', {}).get('mo_cost', 0.0)
byproducts_cost_portion, byproducts = self._get_byproducts_data(doc_in, initial_mo_cost, level + 2, replenishment_index)
byproducts_cost_share = float_round(1 - byproducts_cost_portion, precision_rounding=0.0001)
replenishment['byproducts'] = byproducts
replenishment['summary']['mo_cost'] = initial_mo_cost * byproducts_cost_share
replenishment['summary']['receipt'] = self._check_planned_start(production.date_start, self._get_replenishment_receipt(doc_in, replenishment.get('components', [])))
replenishments.append(replenishment)
forecast_line['already_used'] = True
@@ -35,6 +35,7 @@
<t t-call="mrp.mo_overview_components">
<t t-set="components" t-value="data['components']"/>
<t t-set="operations" t-value="data['operations']"/>
<t t-set="byproducts" t-value="data['byproducts']"/>
</t>
</tbody>
<tfoot>
@@ -99,6 +100,7 @@
<t t-call="mrp.mo_overview_components">
<t t-set="components" t-value="replenishment.get('components', [])"/>
<t t-set="operations" t-value="replenishment.get('operations', {})"/>
<t t-set="byproducts" t-value="replenishment.get('byproducts', {})"/>
</t>
</t>
</t>
@@ -107,6 +109,10 @@
<t t-set="summary" t-value="operations.get('summary', {})"/>
<t t-set="operations" t-value="operations.get('details', [])"/>
</t>
<t t-call="mrp.mo_overview_byproducts">
<t t-set="summary" t-value="byproducts.get('summary', {})"/>
<t t-set="byproducts" t-value="byproducts.get('details', [])"/>
</t>
</template>
<template id="mo_overview_operations">
@@ -137,4 +143,32 @@
</t>
</t>
</template>
<template id="mo_overview_byproducts">
<t t-if="byproducts">
<tr>
<td class="text-start">
<span t-attf-style="margin-left: {{ (byproducts[0]['level'] - 1 if len(byproducts) else 0) * 20 }}px"/>
By-Products
</td>
<td t-if="data['show_replenishments']" class="text-center"/>
<td class="text-end"/>
<td t-if="data['show_uom']" class="text-start"/>
<td t-if="data['show_availabilities']" class="text-end"/>
<td t-if="data['show_availabilities']" class="text-end"/>
<td t-if="data['show_receipts']" class="text-end"/>
<td t-if="data['show_mo_costs']" t-attf-class="text-end {{ 'text-' + summary['mo_cost_decorator'] if summary.get('mo_cost_decorator') else '' }}">
<t t-out="summary['mo_cost']" t-options="{'widget': 'monetary', 'display_currency': summary['currency']}"/>
</td>
<td t-if="data['show_product_costs']" class="text-end">
<t t-out="summary['product_cost']" t-options="{'widget': 'monetary', 'display_currency': summary['currency']}"/>
</td>
</tr>
</t>
<t t-if="summary['index'] in data['unfolded_ids']" t-foreach="byproducts" t-as="byproduct">
<t t-call="mrp.mo_overview_line">
<t t-set="line" t-value="byproduct"/>
</t>
</t>
</template>
</odoo>
@@ -50,10 +50,13 @@ export class MoOverview extends Component {
}
this.state.showOptions.uom = reportValues.context.show_uom;
this.context = reportValues.context;
// Main MO's operations & byproducts are always unfolded by default.
if (reportValues.data?.operations?.summary?.index) {
// Main MO's operations are always unfolded by default.
this.unfoldedIds.add(reportValues.data.operations.summary.index);
}
if (reportValues.data?.byproducts?.summary?.index) {
this.unfoldedIds.add(reportValues.data.byproducts.summary.index);
}
}
//---- Handlers ----
@@ -34,6 +34,7 @@
<MoOverviewComponentsBlock
components="state.data.components"
operations="state.data.operations"
byproducts="state.data.byproducts"
showOptions="state.showOptions"/>
</tbody>
<tfoot>
@@ -0,0 +1,31 @@
/** @odoo-module **/
import { MoOverviewOperationsBlock } from "../mo_overview_operations_block/mrp_mo_overview_operations_block";
import { MoOverviewLine } from "../mo_overview_line/mrp_mo_overview_line";
export class MoOverviewByproductsBlock extends MoOverviewOperationsBlock {
static template = "mrp.MoOverviewByproductsBlock";
//---- Getters ----
get hasByproducts() {
return this.props?.byproducts?.length > 0;
}
get level() {
return this.hasByproducts ? this.props.byproducts[0].level - 1 : 0;
}
}
MoOverviewByproductsBlock.components = {
MoOverviewLine,
};
MoOverviewByproductsBlock.props = {
// Keep all props except "operations"
...(({ operations, ...props}) => props)(MoOverviewOperationsBlock.props),
byproducts: Array,
};
MoOverviewByproductsBlock.props.summary.shape = {
...MoOverviewByproductsBlock.props.summary.shape,
product_cost: { type: Number, optional: true },
};
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mrp.MoOverviewByproductsBlock" owl="1">
<t t-if="hasByproducts">
<tr>
<td class="text-start">
<span t-attf-style="margin-left: {{ level * 20 }}px"/>
<button t-on-click="toggleFolded" class="btn btn-light ps-0 py-0" t-attf-aria-label="{{ state.isFolded ? 'Unfold' : 'Fold' }}" t-attf-title="{{ state.isFolded ? 'Unfold' : 'Fold' }}">
<i t-attf-class="fa fa-fw fa-caret-{{ state.isFolded ? 'right' : 'down' }}" role="img"/>
By-Products
</button>
</td>
<td class="text-center" t-if="props.showOptions.replenishments"/>
<td class="text-end"/>
<td class="text-start" t-if="props.showOptions.uom"/>
<td class="text-end" t-if="props.showOptions.availabilities"/>
<td class="text-end" t-if="props.showOptions.availabilities"/>
<td class="text-end" t-if="props.showOptions.receipts"/>
<td t-attf-class="text-end {{ getColorClass(props.summary.mo_cost_decorator) }}" t-if="props.showOptions.moCosts" t-out="formatMonetary(props.summary.mo_cost)"/>
<td class="text-end" t-if="props.showOptions.productCosts" t-out="formatMonetary(props.summary.product_cost)"/>
</tr>
<t t-if="!state.isFolded" t-foreach="props.byproducts" t-as="byproduct" t-key="byproduct.index">
<MoOverviewLine data="byproduct" showOptions="props.showOptions"/>
</t>
</t>
</t>
</templates>
@@ -4,6 +4,7 @@ import { Component, onWillUpdateProps, useState } from "@odoo/owl";
import { useBus } from "@web/core/utils/hooks";
import { MoOverviewLine } from "../mo_overview_line/mrp_mo_overview_line";
import { MoOverviewOperationsBlock } from "../mo_overview_operations_block/mrp_mo_overview_operations_block";
import { MoOverviewByproductsBlock } from "../mo_overview_byproducts_block/mrp_mo_overview_byproducts_block";
export class MoOverviewComponentsBlock extends Component {
static template = "mrp.MoOverviewComponentsBlock";
@@ -81,6 +82,7 @@ export class MoOverviewComponentsBlock extends Component {
MoOverviewComponentsBlock.components = {
MoOverviewLine,
MoOverviewOperationsBlock,
MoOverviewByproductsBlock,
MoOverviewComponentsBlock,
};
MoOverviewComponentsBlock.props = {
@@ -94,6 +96,14 @@ MoOverviewComponentsBlock.props = {
},
optional: true,
},
byproducts: {
type: Object,
shape: {
summary: Object,
details: Array,
},
optional: true,
},
showOptions: {
type: Object,
shape: {
@@ -22,6 +22,7 @@
unfoldAll="state.unfoldAll"
components="replenishment.components"
operations="replenishment.operations"
byproducts="replenishment.byproducts"
showOptions="props.showOptions"/>
</t>
</t>
@@ -31,6 +32,11 @@
summary="props.operations.summary"
operations="props.operations.details"
showOptions="props.showOptions"/>
<MoOverviewByproductsBlock
unfoldAll="state.unfoldAll"
summary="props.byproducts.summary"
byproducts="props.byproducts.details"
showOptions="props.showOptions"/>
</t>
</templates>