[IMP] mrp{,_account}: Integrate Cost Analysis data into Overview

The point of the MO Overview is to be a single place where you can
quickly get all info for a single Manufacturing Order.
Since we have all data the cost_analysis is already fetching, we add
those data in the report when the production is done.

Also use the Stock Valuation Layers when available for component costs
to better reflect the real cost of production.

Task-3251506

Part-of: odoo/odoo#114536
This commit is contained in:
clesgow
2023-10-20 14:59:09 +00:00
parent 9b822496cb
commit ebef2c3e4f
13 changed files with 318 additions and 16 deletions
+120 -7
View File
@@ -49,6 +49,9 @@ class ReportMoOverview(models.AbstractModel):
doc['show_receipts'] = data.get('receipts') == '1'
if doc['show_receipts']:
footer_colspan += 1
doc['show_unit_costs'] = data.get('unitCosts') == '1'
if doc['show_unit_costs']:
footer_colspan += 1
doc['show_mo_costs'] = data.get('moCosts') == '1'
doc['show_real_costs'] = data.get('realCosts') == '1'
doc['show_uom'] = self.env.user.user_has_groups('uom.group_uom')
@@ -76,6 +79,7 @@ class ReportMoOverview(models.AbstractModel):
initial_mo_cost, initial_real_cost = self._compute_cost_sums(components, operations)
remaining_cost_share, byproducts = self._get_byproducts_data(production, initial_mo_cost, initial_real_cost, level=1, current_index='')
summary = self._get_mo_summary(production, components, initial_mo_cost, initial_real_cost, remaining_cost_share)
extra_lines = self._get_report_extra_lines(summary, components, operations, production.state == 'done')
return {
'id': production.id,
'name': production.display_name,
@@ -83,18 +87,77 @@ class ReportMoOverview(models.AbstractModel):
'components': components,
'operations': operations,
'byproducts': byproducts,
'extras': self._get_report_extra_lines(summary),
'extras': extra_lines,
'cost_breakdown': self._get_cost_breakdown_data(production, extra_lines, remaining_cost_share),
}
def _get_report_extra_lines(self, summary):
currency = summary.get('currency', self.env.company)
def _get_report_extra_lines(self, summary, components, operations, production_done=False):
currency = summary.get('currency', self.env.company.currency_id)
unit_mo_cost = currency.round(summary.get('mo_cost', 0) / summary.get('quantity', 1))
unit_real_cost = currency.round(summary.get('real_cost', 0) / summary.get('quantity', 1))
return {
extras = {
'unit_mo_cost': unit_mo_cost,
'unit_mo_cost_decorator': self._get_comparison_decorator(unit_real_cost, unit_mo_cost, currency.rounding),
'unit_real_cost': unit_real_cost,
}
if production_done:
production_qty = summary.get('quantity', 1.0)
extras['total_mo_cost_components'] = sum(compo.get('summary', {}).get('mo_cost', 0.0) for compo in components)
extras['total_real_cost_components'] = sum(compo.get('summary', {}).get('real_cost', 0.0) for compo in components)
extras['total_mo_cost_components_decorator'] = self._get_comparison_decorator(extras['total_real_cost_components'], extras['total_mo_cost_components'], currency.rounding)
extras['unit_mo_cost_components'] = extras['total_mo_cost_components'] / production_qty
extras['unit_real_cost_components'] = extras['total_real_cost_components'] / production_qty
extras['total_mo_cost_operations'] = operations.get('summary', {}).get('mo_cost', 0.0)
extras['total_real_cost_operations'] = operations.get('summary', {}).get('real_cost', 0.0)
extras['total_mo_cost_operations_decorator'] = self._get_comparison_decorator(extras['total_real_cost_operations'], extras['total_mo_cost_operations'], currency.rounding)
extras['unit_mo_cost_operations'] = extras['total_mo_cost_operations'] / production_qty
extras['unit_real_cost_operations'] = extras['total_real_cost_operations'] / production_qty
extras['total_mo_cost'] = extras['total_mo_cost_components'] + extras['total_mo_cost_operations']
extras['total_real_cost'] = extras['total_real_cost_components'] + extras['total_real_cost_operations']
extras['total_mo_cost_decorator'] = self._get_comparison_decorator(extras['total_real_cost'], extras['total_mo_cost'], currency.rounding)
return extras
def _get_cost_breakdown_data(self, production, extras, remaining_cost_share):
if production.state != 'done' or not production.move_byproduct_ids:
return []
# Prepare byproducts data
quantities_by_product = defaultdict(float)
total_cost_by_product = defaultdict(float)
component_cost_by_product = defaultdict(float)
operation_cost_by_product = defaultdict(float)
for bp_move in production.move_byproduct_ids:
# Byproducts without cost share are irrelevant in a cost breakdrown.
if bp_move.state == 'cancel' or float_is_zero(bp_move.cost_share, precision_digits=2):
continue
# As UoMs can vary, we use the default UoM of each product
quantities_by_product[bp_move.product_id] += bp_move.product_qty
cost_share = bp_move.cost_share / 100
total_cost_by_product[bp_move.product_id] += extras['total_real_cost'] * cost_share
component_cost_by_product[bp_move.product_id] += extras['total_real_cost_components'] * cost_share
operation_cost_by_product[bp_move.product_id] += extras['total_real_cost_operations'] * cost_share
# Add finished product to its own default UoM (not the production UoM)
breakdown_lines = [self._format_cost_breakdown_lines(0, production.product_id.display_name, production.product_id.uom_id.display_name,
(extras['total_real_cost_components'] * remaining_cost_share) / production.product_uom_qty,
(extras['total_real_cost_operations'] * remaining_cost_share) / production.product_uom_qty,
(extras['total_real_cost'] * remaining_cost_share) / production.product_uom_qty)]
for index, product in enumerate(quantities_by_product.keys()):
breakdown_lines.append(self._format_cost_breakdown_lines(index + 1, product.display_name, product.uom_id.display_name,
component_cost_by_product[product] / quantities_by_product[product],
operation_cost_by_product[product] / quantities_by_product[product],
total_cost_by_product[product] / quantities_by_product[product]))
return breakdown_lines
def _format_cost_breakdown_lines(self, index, product_name, uom_name, component_cost, operation_cost, total_cost):
return {
'index': f"BR{index}",
'name': product_name,
'unit_avg_cost_component': component_cost,
'unit_avg_cost_operation': operation_cost,
'unit_avg_total_cost': total_cost,
'uom_name': uom_name,
}
def _get_mo_summary(self, production, components, current_mo_cost, current_real_cost, remaining_cost_share):
currency = (production.company_id or self.env.company).currency_id
@@ -117,6 +180,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)),
'unit_cost': self._get_unit_cost(production.move_finished_ids.filtered(lambda m: m.product_id == production.product_id)),
'mo_cost': currency.round(mo_cost),
'mo_cost_decorator': self._get_comparison_decorator(real_cost, mo_cost, currency.rounding),
'real_cost': currency.round(real_cost),
@@ -124,6 +188,9 @@ class ReportMoOverview(models.AbstractModel):
'currency': currency,
}
def _get_unit_cost(self, move):
return move.product_id.uom_id._compute_price(move.product_id.standard_price, move.product_uom)
def _format_state(self, record):
if record._name != 'mrp.production' or record.state not in ('draft', 'confirmed') or not record.move_raw_ids:
return dict(record._fields['state']._description_selection(self.env)).get(record.state)
@@ -165,6 +232,8 @@ class ReportMoOverview(models.AbstractModel):
return 'success'
def _get_operations_data(self, production, level=0, current_index=False):
if production.state == "done":
return self._get_finished_operation_data(production, level, current_index)
currency = (production.company_id or self.env.company).currency_id
operation_uom = _("Minutes")
operations = []
@@ -202,6 +271,7 @@ class ReportMoOverview(models.AbstractModel):
'quantity_decorator': self._get_comparison_decorator(workorder.duration_expected, wo_duration, 0.01),
'uom_name': operation_uom,
'production_id': production.id,
'unit_cost': expected_cost / workorder.duration_expected,
'mo_cost': mo_cost,
'real_cost': real_cost,
'currency_id': currency.id,
@@ -226,6 +296,46 @@ class ReportMoOverview(models.AbstractModel):
'details': operations,
}
def _get_finished_operation_data(self, production, level=0, current_index=False):
currency = (production.company_id or self.env.company).currency_id
done_operation_uom = _("Hours")
operations = []
total_duration = total_duration_expected = total_cost = 0
for index, workorder in enumerate(production.workorder_ids):
hourly_cost = workorder.costs_hour or workorder.workcenter_id.costs_hour
duration = workorder.get_duration() / 60
total_duration += duration
total_duration_expected += workorder.duration_expected
operation_cost = duration * hourly_cost
total_cost += operation_cost
operations.append({
'level': level,
'index': f"{current_index}W{index}",
'name': f"{workorder.workcenter_id.display_name}: {workorder.display_name}",
'quantity': duration,
'uom_name': done_operation_uom,
'uom_precision': 4,
'unit_cost': hourly_cost,
'mo_cost': currency.round(operation_cost),
'real_cost': currency.round(operation_cost),
'currency_id': currency.id,
'currency': currency,
})
return {
'summary': {
'index': f"{current_index}W",
'done': True,
'quantity': total_duration,
'quantity_decorator': self._get_comparison_decorator(total_duration_expected, total_duration, 0.01),
'mo_cost': total_cost,
'real_cost': total_cost,
'uom_name': done_operation_uom,
'currency_id': currency.id,
'currency': currency,
},
'details': operations,
}
def _get_byproducts_data(self, production, current_mo_cost, current_real_cost, level=0, current_index=False):
currency = (production.company_id or self.env.company).currency_id
byproducts = []
@@ -249,6 +359,7 @@ class ReportMoOverview(models.AbstractModel):
'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),
'unit_cost': self._get_unit_cost(move_bp),
'mo_cost': currency.round(mo_cost),
'mo_cost_decorator': self._get_comparison_decorator(real_cost, mo_cost, currency.rounding),
'real_cost': currency.round(real_cost),
@@ -328,6 +439,7 @@ class ReportMoOverview(models.AbstractModel):
'quantity_on_hand': product.uom_id._compute_quantity(product.qty_available, move_raw.product_uom) if product.type == 'product' else False,
'quantity_reserved': self._get_reserved_qty(move_raw, production.warehouse_id, replenish_data),
'receipt': self._check_planned_start(production.date_start, self._get_component_receipt(product, move_raw, production.warehouse_id, replenishments, replenish_data)),
'unit_cost': self._get_unit_cost(move_raw),
'mo_cost': currency.round(replenish_mo_cost + missing_quantity_cost),
'real_cost': currency.round(self._get_component_real_cost(move_raw, move_raw.product_uom_qty)),
'currency_id': currency.id,
@@ -346,8 +458,7 @@ class ReportMoOverview(models.AbstractModel):
def _get_component_real_cost(self, move_raw, quantity):
if float_is_zero(quantity, precision_rounding=move_raw.product_uom.rounding):
return 0
product = move_raw.product_id
return product.standard_price * move_raw.product_uom._compute_quantity(quantity, product.uom_id)
return self._get_unit_cost(move_raw) * quantity
def _check_planned_start(self, mo_planned_start, receipt):
if mo_planned_start and receipt.get('date', False) and receipt['date'] > mo_planned_start:
@@ -413,8 +524,9 @@ class ReportMoOverview(models.AbstractModel):
'quantity': line_quantity,
'uom_name': move_raw.product_uom.display_name,
'uom_precision': self._get_uom_precision(forecast_line['uom_id']['rounding']),
'unit_cost': self._get_unit_cost(move_raw),
'mo_cost': forecast_line.get('cost', self._get_replenishment_mo_cost(product, line_quantity, move_raw.product_uom, currency, forecast_line.get('move_in'))),
'real_cost': currency.round(move_raw.product_uom._compute_quantity(line_quantity, product.uom_id) * product.standard_price),
'real_cost': currency.round(self._get_component_real_cost(move_raw, line_quantity)),
'currency_id': currency.id,
'currency': currency,
}
@@ -474,6 +586,7 @@ class ReportMoOverview(models.AbstractModel):
else:
to_order_line['summary']['mo_cost'] = currency.round(product.standard_price * move_raw.product_uom._compute_quantity(missing_quantity, product.uom_id))
to_order_line['summary']['receipt'] = self._format_receipt_date('unavailable')
to_order_line['summary']['unit_cost'] = currency.round(to_order_line['summary']['mo_cost'] / missing_quantity)
to_order_line['summary']['mo_cost_decorator'] = self._get_comparison_decorator(to_order_line['summary']['real_cost'], to_order_line['summary']['mo_cost'], currency.rounding)
replenishments.append(to_order_line)
+85 -3
View File
@@ -25,6 +25,7 @@
<th t-if="data['show_availabilities']" class="text-end">Free to use / On Hand</th>
<th t-if="data['show_availabilities']" class="text-end">Reserved</th>
<th t-if="data['show_receipts']" class="text-end">Receipt</th>
<th class="text-end" t-if="data['show_unit_costs']">Unit Cost</th>
<th t-if="data['show_mo_costs']" class="text-end">MO Cost</th>
<th t-if="data['show_real_costs']" class="text-end">Real Cost</th>
</tr>
@@ -46,7 +47,83 @@
<td t-if="data['show_real_costs']" class="text-end" t-out="data['extras']['unit_real_cost']" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
</tr>
</t>
<t t-if="data['summary'].get('state') == 'done' and data.get('extras')">
<tr>
<td class="text-end" t-att-colspan="data['footer_colspan']">Total Cost of Components</td>
<td t-attf-class="text-end {{ data['get_color'](data['extras'].get('total_mo_cost_components_decorator')) }}" t-if="data['show_mo_costs']">
<t t-out="data['extras'].get('total_mo_cost_components')" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
</td>
<td class="text-end" t-if="data['show_real_costs']">
<t t-out="data['extras'].get('total_real_cost_components')" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
</td>
</tr>
<tr t-if="data['summary'].get('quantity') != 1">
<td class="text-end" t-att-colspan="data['footer_colspan']">
Cost of Components per unit
<t t-if="data['show_uom']">(in <t t-out="data['summary'].get('uom_name')"/>)</t>
</td>
<td t-attf-class="text-end {{ data['get_color'](data['extras'].get('unit_mo_cost_components_decorator')) }}" t-if="data['show_mo_costs']">
<t t-out="data['extras'].get('unit_mo_cost_components')" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
</td>
<td class="text-end" t-if="data['show_real_costs']">
<t t-out="data['extras'].get('unit_real_cost_components')" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
</td>
</tr>
<tr t-if="len(data.get('operations', {}).get('details', []))">
<td class="text-end" t-att-colspan="data['footer_colspan']">Total Cost of Operations</td>
<td t-attf-class="text-end {{ data['get_color'](data['extras'].get('total_mo_cost_operations_decorator')) }}" t-if="data['show_mo_costs']">
<t t-out="data['extras'].get('total_mo_cost_operations')" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
</td>
<td class="text-end" t-if="data['show_real_costs']">
<t t-out="data['extras'].get('total_real_cost_operations')" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
</td>
</tr>
<tr t-if="len(data.get('operations', {}).get('details', [])) and data['summary'].get('quantity') != 1">
<td class="text-end" t-att-colspan="data['footer_colspan']">
Cost of Operations per unit
<t t-if="data['show_uom']">(in <t t-out="data['summary'].get('uom_name')"/>)</t>
</td>
<td t-attf-class="text-end {{ data['get_color'](data['extras'].get('unit_mo_cost_operations_decorator')) }}" t-if="data['show_mo_costs']">
<t t-out="data['extras'].get('unit_mo_cost_operations')" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
</td>
<td class="text-end" t-if="data['show_real_costs']">
<t t-out="data['extras'].get('unit_real_cost_operations')" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
</td>
</tr>
<tr t-if="len(data.get('cost_breakdown', [])) and len(data.get('operations', {}).get('details', []))">
<td class="text-end" t-att-colspan="data['footer_colspan']">Total Cost of Production</td>
<td t-attf-class="text-end {{ data['get_color'](data['extras'].get('total_mo_cost_decorator')) }}" t-if="data['show_mo_costs']">
<t t-out="data['extras'].get('total_mo_cost')" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
</td>
<td class="text-end" t-if="data['show_real_costs']">
<t t-out="data['extras'].get('total_real_cost')" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
</td>
</tr>
</t>
</table>
<t t-if="len(data.get('cost_breakdown', []))">
<h3>Cost Breakdown of Products</h3>
<table name="breakdown" class="table table-borderless">
<thead>
<tr>
<th class="text-start">Product</th>
<th class="text-end">Avg Cost of Components per Unit</th>
<th t-if="len(data.get('operations', {}).get('details', []))" class="text-end">Avg Cost of Operations per Unit</th>
<th class="text-end">Avg Total Cost per Unit</th>
<th t-if="data['show_uom']" class="text-end">Unit of Measure</th>
</tr>
</thead>
<tbody>
<tr t-foreach="data['cost_breakdown']" t-as="line">
<td class="text-start" t-out="line['name']"/>
<td class="text-end" t-out="line['unit_avg_cost_component']" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
<td t-if="len(data.get('operations', {}).get('details', []))" class="text-end" t-out="line['unit_avg_cost_operation']" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
<td class="text-end" t-out="line['unit_avg_total_cost']" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
<td t-if="data['show_uom']" class="text-end" t-out="line['uom_name']"/>
</tr>
</tbody>
</table>
</t>
</div>
</template>
@@ -73,6 +150,9 @@
<td t-if="data['show_receipts']" class="text-end">
<span t-if="line.get('receipt')" t-att-class="data['get_color'](line['receipt'].get('decorator'))" t-out="line['receipt']['display']"/>
</td>
<td class="text-end" t-if="data['show_unit_costs']">
<t t-out="line['unit_cost']" t-options="{'widget': 'monetary', 'display_currency': line['currency']}"/>
</td>
<td t-if="data['show_mo_costs']" t-attf-class="text-end {{ data['get_color'](line.get('mo_cost_decorator')) }}">
<t t-out="line['mo_cost']" t-options="{'widget': 'monetary', 'display_currency': line['currency']}"/>
</td>
@@ -119,13 +199,14 @@
</td>
<td t-if="data['show_replenishments']" class="text-center"/>
<td t-attf-class="text-end {{ data['get_color'](summary.get('quantity_decorator')) }}">
<t t-out="summary['quantity']" t-options="{'widget': 'float_time'}"/>
</td>
<t t-if="summary.get('done')" t-out="summary['quantity']" t-options="{'widget': 'float', 'precision': operations[0].get('uom_precision', 0)}"/>
<t t-else="" t-out="summary['quantity']" t-options="{'widget': 'float_time'}"/>
</td>
<td t-if="data['show_uom']" class="text-start" t-out="summary['uom_name']"/>
<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_unit_costs']"/>
<td t-if="data['show_mo_costs']" t-attf-class="text-end {{ data['get_color'](summary.get('mo_cost_decorator')) }}">
<t t-out="summary['mo_cost']" t-options="{'widget': 'monetary', 'display_currency': summary['currency']}"/>
</td>
@@ -154,6 +235,7 @@
<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_unit_costs']"/>
<td t-if="data['show_mo_costs']" t-attf-class="text-end {{ data['get_color'](summary.get('mo_cost_decorator')) }}">
<t t-out="summary['mo_cost']" t-options="{'widget': 'monetary', 'display_currency': summary['currency']}"/>
</td>
@@ -21,7 +21,6 @@ export class MoOverview extends Component {
this.state = useState({
data: {},
isDone: false,
showOptions: this.getDefaultConfig(),
});
@@ -41,12 +40,12 @@ export class MoOverview extends Component {
[this.activeId],
);
this.state.data = reportValues.data;
this.state.isDone = ['done', 'cancel'].some(doneState => reportValues.data?.summary?.state === doneState);
if (this.state.isDone) {
if (this.isProductionDone) {
// Hide Availabilities / Receipts / Status columns when the MO is done.
this.state.showOptions.availabilities = false;
this.state.showOptions.receipts = false;
this.state.showOptions.replenishments = false;
this.state.showOptions.unitCosts = true;
}
this.state.showOptions.uom = reportValues.context.show_uom;
this.context = reportValues.context;
@@ -92,6 +91,7 @@ export class MoOverview extends Component {
replenishments: true,
availabilities: true,
receipts: true,
unitCosts: false,
moCosts: true,
realCosts: true,
};
@@ -127,6 +127,10 @@ export class MoOverview extends Component {
return this.state.showOptions.receipts;
}
get showUnitCosts() {
return this.state.showOptions.unitCosts;
}
get showMoCosts() {
return this.state.showOptions.moCosts;
}
@@ -139,12 +143,21 @@ export class MoOverview extends Component {
return this.state.data?.summary?.state === "done";
}
get hasOperations() {
return this.state.data?.operations?.details?.length > 0;
}
get hasBreakdown() {
return this.state.data?.cost_breakdown?.length > 0;
}
get totalColspan() {
let colspan = 2; // Name & Quantity
if (this.showReplenishments) colspan++;
if (this.showAvailabilities) colspan += 2; // Free to use / On Hand & Reserved
if (this.showUom) colspan++;
if (this.showReceipts) colspan++;
if (this.showUnitCosts) colspan++;
return colspan;
}
@@ -153,6 +166,7 @@ export class MoOverview extends Component {
+ `&replenishments=${+this.state.showOptions.replenishments}`
+ `&availabilities=${+this.state.showOptions.availabilities}`
+ `&receipts=${+this.state.showOptions.receipts}`
+ `&unitCosts=${+this.state.showOptions.unitCosts}`
+ `&moCosts=${+this.state.showOptions.moCosts}`
+ `&realCosts=${+this.state.showOptions.realCosts}`
+ `&unfoldedIds=${JSON.stringify(Array.from(this.unfoldedIds))}`;
@@ -4,7 +4,7 @@
<div t-name="mrp.MoOverview" class="o_action">
<Layout display="{ controlPanel: {} }">
<t t-set-slot="layout-actions">
<MoOverviewDisplayFilter showOptions="state.showOptions" limited="state.isDone" changeDisplay.bind="onChangeDisplay"/>
<MoOverviewDisplayFilter showOptions="state.showOptions" limited="isProductionDone" changeDisplay.bind="onChangeDisplay"/>
</t>
<t t-set-slot="control-panel-create-button">
@@ -24,6 +24,7 @@
<th class="text-end" t-if="showAvailabilities">Free to use / On Hand</th>
<th class="text-end" t-if="showAvailabilities">Reserved</th>
<th class="text-end" t-if="showReceipts">Receipt</th>
<th class="text-end" t-if="showUnitCosts">Unit Cost</th>
<th class="text-end" t-if="showMoCosts" title="Cost based on related replenishments. Otherwise cost from product form">MO Cost</th>
<th class="text-end" t-if="showRealCosts" title="Cost from product form">Real Cost</th>
</tr>
@@ -42,8 +43,65 @@
<td t-attf-class="text-end {{ getColorClass(state.data.extras.unit_mo_cost_decorator) }}" t-if="showMoCosts" t-out="formatCost(state.data.extras.unit_mo_cost)"/>
<td class="text-end" t-if="showRealCosts" t-out="formatCost(state.data.extras.unit_real_cost)"/>
</tr>
<t t-if="isProductionDone">
<tr>
<td class="text-end" t-att-colspan="totalColspan">Total Cost of Components</td>
<td t-attf-class="text-end {{ getColorClass(state.data.extras.total_mo_cost_components_decorator) }}" t-if="showMoCosts" t-out="formatCost(state.data.extras.total_mo_cost_components)"/>
<td class="text-end" t-if="showRealCosts" t-out="formatCost(state.data.extras.total_real_cost_components)"/>
</tr>
<tr t-if="state.data.summary.quantity != 1">
<td class="text-end" t-att-colspan="totalColspan">
Cost of Components per unit
<t t-if="showUom">(in <t t-out='state.data.summary.uom_name'/>)</t>
</td>
<td t-attf-class="text-end {{ getColorClass(state.data.extras.total_mo_cost_components_decorator) }}" t-if="showMoCosts" t-out="formatCost(state.data.extras.unit_mo_cost_components)"/>
<td class="text-end" t-if="showRealCosts" t-out="formatCost(state.data.extras.unit_real_cost_components)"/>
</tr>
<tr t-if="hasOperations">
<td class="text-end" t-att-colspan="totalColspan">Total Cost of Operations</td>
<td t-attf-class="text-end {{ getColorClass(state.data.extras.total_mo_cost_operations_decorator) }}" t-if="showMoCosts" t-out="formatCost(state.data.extras.total_mo_cost_operations)"/>
<td class="text-end" t-if="showRealCosts" t-out="formatCost(state.data.extras.total_real_cost_operations)"/>
</tr>
<tr t-if="hasOperations and state.data.summary.quantity != 1">
<td class="text-end" t-att-colspan="totalColspan">
Cost of Operations per unit
<t t-if="showUom">(in <t t-out='state.data.summary.uom_name'/>)</t>
</td>
<td t-attf-class="text-end {{ getColorClass(state.data.extras.total_mo_cost_operations_decorator) }}" t-if="showMoCosts" t-out="formatCost(state.data.extras.unit_mo_cost_operations)"/>
<td class="text-end" t-if="showRealCosts" t-out="formatCost(state.data.extras.unit_real_cost_operations)"/>
</tr>
<tr t-if="hasBreakdown and hasOperations">
<td class="text-end" t-att-colspan="totalColspan">Total Cost of Production</td>
<td t-attf-class="text-end {{ getColorClass(state.data.extras.total_mo_cost_decorator) }}" t-if="showMoCosts" t-out="formatCost(state.data.extras.total_mo_cost)"/>
<td class="text-end" t-if="showRealCosts" t-out="formatCost(state.data.extras.total_real_cost)"/>
</tr>
</t>
</tfoot>
</table>
<t t-if="hasBreakdown">
<hr/>
<h2 class="pt-3">Cost Breakdown of Products</h2>
<table name="breakdown" class="table">
<thead>
<tr>
<th class="text-start">Product</th>
<th class="text-end">Avg Cost of Components per Unit</th>
<th t-if="hasOperations" class="text-end">Avg Cost of Operations per Unit</th>
<th class="text-end">Avg Total Cost per Unit</th>
<th t-if="showUom" class="text-end">Unit of Measure</th>
</tr>
</thead>
<tbody>
<tr t-foreach="state.data.cost_breakdown" t-as="line" t-key="line.index">
<td class="text-start" t-out="line.name"/>
<td class="text-end" t-out="formatCost(line.unit_avg_cost_component)"/>
<td t-if="hasOperations" class="text-end" t-out="formatCost(line.unit_avg_cost_operation)"/>
<td class="text-end" t-out="formatCost(line.unit_avg_total_cost)"/>
<td t-if="showUom" class="text-end" t-out="line.uom_name"/>
</tr>
</tbody>
</table>
</t>
</div>
</Layout>
</div>
@@ -17,6 +17,7 @@
<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 class="text-end" t-if="props.showOptions.unitCosts"/>
<td class="text-end" t-if="props.showOptions.moCosts" t-out="formatMonetary(props.summary.mo_cost)"/>
<td t-attf-class="text-end {{ getColorClass(props.summary.real_cost_decorator) }}" t-if="props.showOptions.realCosts" t-out="formatMonetary(props.summary.real_cost)"/>
</tr>
@@ -10,6 +10,7 @@ export const SHOW_OPTIONS = {
replenishments: Boolean,
availabilities: Boolean,
receipts: Boolean,
unitCosts: Boolean,
moCosts: Boolean,
realCosts: Boolean,
},
@@ -26,6 +27,7 @@ export class MoOverviewDisplayFilter extends BomOverviewDisplayFilter {
}
this.displayOptions = {
...(this.displayOptions || {}),
unitCosts: _t("Unit Costs"),
moCosts: _t("MO Costs"),
realCosts: _t("Real Costs"),
};
@@ -158,6 +158,7 @@ MoOverviewLine.props = {
},
optional: true,
},
unit_cost: { type: Number, optional: true },
mo_cost: Number,
mo_cost_decorator: { type: [String, Boolean], optional: true },
real_cost: Number,
@@ -35,6 +35,7 @@
<a href="#" role="button" t-on-click.prevent="openForecast" title="Forecast Report" t-attf-class="fa fa-fw fa-area-chart ms-1 {{getColorClass(data.receipt.decorator)}}"/>
</t>
</td>
<td class="text-end" t-if="props.showOptions.unitCosts" t-out="formatMonetary(data.unit_cost)"/>
<td t-attf-class="text-end {{ getColorClass(data.mo_cost_decorator) }}" t-if="props.showOptions.moCosts" t-out="formatMonetary(data.mo_cost)"/>
<td class="text-end" t-if="props.showOptions.realCosts" t-out="formatMonetary(data.real_cost)"/>
</tr>
@@ -2,7 +2,7 @@
import { Component, useState } from "@odoo/owl";
import { useBus } from "@web/core/utils/hooks";
import { formatFloatTime, formatMonetary } from "@web/views/fields/formatters";
import { formatFloat, formatFloatTime, formatMonetary } from "@web/views/fields/formatters";
import { MoOverviewLine } from "../mo_overview_line/mrp_mo_overview_line";
import { SHOW_OPTIONS } from "../mo_overview_display_filter/mrp_mo_overview_display_filter";
@@ -57,6 +57,13 @@ export class MoOverviewOperationsBlock extends Component {
get index() {
return this.props.summary.index;
}
get totalQuantity() {
// Float for Hours when displaying done productions, FloatTime for Minutes otherwise.
return this.props.summary?.done ?
formatFloat(this.props.summary.quantity, { digits: [false, this.props.operations[0].uom_precision || undefined] }) :
formatFloatTime(this.props.summary.quantity)
}
}
MoOverviewOperationsBlock.components = {
@@ -77,6 +84,7 @@ MoOverviewOperationsBlock.props = {
uom_name: { type: String, optional: true },
currency_id: { type: Number, optional: true },
currency: { type: String, optional: true },
done: { type: Boolean, optional: true },
},
},
showOptions: SHOW_OPTIONS,
@@ -12,11 +12,12 @@
</button>
</td>
<td class="text-center" t-if="props.showOptions.replenishments"/>
<td t-attf-class="text-end {{ getColorClass(props.summary.quantity_decorator) }}" t-out="formatFloatTime(props.summary.quantity)"/>
<td t-attf-class="text-end {{ getColorClass(props.summary.quantity_decorator) }}" t-out="totalQuantity"/>
<td class="text-start" t-if="props.showOptions.uom" t-out="props.summary.uom_name"/>
<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 class="text-end" t-if="props.showOptions.unitCosts"/>
<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.realCosts" t-out="formatMonetary(props.summary.real_cost)"/>
</tr>
+1
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
from . import report
def _configure_journals(env):
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import mrp_report_mo_overview
@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from odoo.tools import float_is_zero
class ReportMoOverview(models.AbstractModel):
_inherit = 'report.mrp.report_mo_overview'
def _get_unit_cost(self, move):
valuation_layers = move.sudo().stock_valuation_layer_ids
layers_quantity = sum(valuation_layers.mapped('quantity'))
if valuation_layers and not float_is_zero(layers_quantity, precision_rounding=valuation_layers.uom_id.rounding):
unit_price = sum(valuation_layers.mapped('value')) / layers_quantity
return move.product_id.uom_id._compute_price(unit_price, move.product_uom)
return super()._get_unit_cost(move)