[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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
+1
@@ -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>
|
||||
|
||||
+2
@@ -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>
|
||||
|
||||
+9
-1
@@ -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,
|
||||
|
||||
+2
-1
@@ -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,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):
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user