19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:31:47 +01:00
parent accf5918df
commit 6e65e8c877
688 changed files with 225434 additions and 199401 deletions

View file

@ -5,3 +5,4 @@ from . import mrp_report_bom_structure
from . import stock_forecasted
from . import report_stock_reception
from . import report_stock_rule
from . import mrp_report_mo_overview

View file

@ -8,47 +8,47 @@
<div class="oe_structure"/>
<div class="row">
<div class="col-7">
<h2><span t-field="o.name"/></h2>
<h2><span t-field="o.name">MRP-001</span></h2>
</div>
<div class="col-5">
<span class="text-end">
<div t-field="o.name" t-options="{'widget': 'barcode', 'width': 600, 'height': 100, 'img_style': 'width:350px;height:60px'}"/>
<span t-field="o.name" t-options="{'widget': 'barcode', 'width': 600, 'height': 100, 'img_style': 'width:350px;height:60px'}">Package barcode</span>
</span>
</div>
</div>
<div class="row mt32 mb32">
<div class="col-3" t-if="o.origin">
<strong>Source:</strong><br/>
<span t-field="o.origin"/>
<span t-field="o.origin">Vendor ABC</span>
</div>
<div class="col-3" t-if="o.user_id">
<strong>Responsible:</strong><br/>
<span t-field="o.user_id"/>
<span t-field="o.user_id">John Doe</span>
</div>
<div class="col-3" t-if="o.state not in ('done', 'cancel') and o.date_deadline">
<strong>Deadline:</strong><br/>
<span t-field="o.date_deadline"/>
<span t-field="o.date_deadline">2023-09-15</span>
</div>
</div>
<div class="row mt32 mb32">
<div class="col-3">
<strong>Product:</strong><br/>
<span t-field="o.product_id"/>
<span t-field="o.product_id">Laptop Model X</span>
</div>
<div class="col-3" t-if="o.product_description_variants">
<strong>Description:</strong><br/>
<span t-field="o.product_description_variants"/>
<span t-field="o.product_description_variants">Laptop with 16GB RAM</span>
</div>
<div class="col-3">
<strong>Quantity to Produce:</strong><br/>
<span t-field="o.product_qty"/>
<span t-field="o.product_uom_id.name" groups="uom.group_uom"/>
<span t-field="o.product_qty">100</span>
<span t-field="o.product_uom_id.name" groups="uom.group_uom">Units</span>
</div>
<div class="col-3" t-if="o.qty_producing">
<div class="col-3" t-if="o.qty_producing">
<strong>Quantity Producing:</strong><br/>
<span t-field="o.qty_producing"/>
<span t-field="o.product_uom_id.name" groups="uom.group_uom"/>
<span t-field="o.qty_producing">50</span>
<span t-field="o.product_uom_id.name" groups="uom.group_uom">Units</span>
</div>
</div>
@ -64,23 +64,29 @@
<th><strong>WorkCenter</strong></th>
<th><strong>Duration (minutes)</strong></th>
<th t-if="o.state in ('progress', 'to_close')"><strong>Actual Duration (minutes)</strong></th>
<th style="width:30%"><strong>Barcode</strong></th>
</tr>
<tr t-foreach="o.workorder_ids" t-as="line2">
<td><span t-field="line2.name"/></td>
<td><span t-field="line2.workcenter_id.name"/></td>
<td><span t-field="line2.name">Assembling</span></td>
<td><span t-field="line2.workcenter_id.name">Center A</span></td>
<td>
<span t-if="o.state != 'done'" t-field="line2.duration_expected"/>
<span t-if="o.state == 'done'" t-field="line2.duration"/>
<span t-if="o.state != 'done'" t-field="line2.duration_expected">60</span>
<span t-if="o.state == 'done'" t-field="line2.duration">58</span>
</td>
<td t-if="o.state in ('progress', 'to_close')">
<span t-field="line2.duration"/>
<span t-field="line2.duration">58</span>
</td>
<td>
<span t-field="line2.barcode" t-options="{'widget': 'barcode', 'width': 600, 'height': 100, 'img_style': 'width:100%;height:35px'}">987654321098</span>
</td>
</tr>
</table>
</div>
<t t-call="mrp.report_mrp_production_components" t-if="o.move_raw_ids"/>
<div t-if="o.move_raw_ids">
<t t-call="mrp.report_mrp_production_components"/>
</div>
<div class="oe_structure"/>
</div>
</t>
@ -95,6 +101,7 @@
</span>
</h3>
<table class="table table-sm">
<div class="oe_structure"></div>
<t t-set="has_product_barcode" t-value="any(m.product_id.barcode for m in o.move_raw_ids)"/>
<thead>
<tr>
@ -107,20 +114,20 @@
<tbody>
<tr t-foreach="o.move_raw_ids" t-as="raw_line">
<td>
<span t-field="raw_line.product_id"/>
<span t-field="raw_line.product_id">8 GB RAM</span>
</td>
<td t-attf-class="{{ 'text-end' if not has_product_barcode else '' }}" t-if="o.state in ('progress', 'to_close','done')">
<div>
<span t-field="raw_line.quantity_done"/>
<span t-field="raw_line.quantity">2</span>
</div>
</td>
<td t-attf-class="{{ 'text-end' if not has_product_barcode else '' }}">
<span t-field="raw_line.product_uom_qty"/>
<span t-field="raw_line.product_uom" groups="uom.group_uom"/>
<span t-field="raw_line.product_uom_qty">25</span>
<span t-field="raw_line.product_uom" groups="uom.group_uom">Pieces</span>
</td>
<td t-if="has_product_barcode" width="15%" class="text-center">
<t t-if="raw_line.product_id.barcode">
<div t-field="raw_line.product_id.barcode" t-options="{'widget': 'barcode', 'width': 600, 'height': 100, 'img_style': 'width:100%;height:35px'}"/>
<span t-field="raw_line.product_id.barcode" t-options="{'widget': 'barcode', 'width': 600, 'height': 100, 'img_style': 'width:100%;height:35px'}">12345678901</span>
</t>
</td>
</tr>
@ -132,13 +139,14 @@
<t t-call="web.basic_layout">
<t t-set='nRows' t-value='12'/>
<t t-set='nCols' t-value='4'/>
<t t-set="uom_categ_unit" t-value="env.ref('uom.product_uom_categ_unit')"/>
<t t-set="move_lines" t-value="docs.move_finished_ids.move_line_ids.filtered(lambda ml: ml.move_id.production_id.state == 'done' and ml.state == 'done' and ml.qty_done)"/>
<t t-set="uom_unit" t-value="env.ref('uom.product_uom_unit')"/>
<div class="oe_structure"></div>
<t t-set="move_lines" t-value="docs.move_finished_ids.move_line_ids.filtered(lambda ml: ml.move_id.production_id.state == 'done' and ml.state == 'done' and ml.quantity)"/>
<t t-set="move_line_qtys" t-value="[]"/>
<t t-foreach="move_lines" t-as="move_line">
<t t-if="move_line.product_uom_id.category_id == uom_categ_unit">
<t t-if="move_line.product_id.uom_id._has_common_reference(uom_unit)">
<!-- print 1 label per 1 uom when uom category = units -->
<t t-set="move_line_qtys" t-value="move_line_qtys + [int(move_line.qty_done)]"/>
<t t-set="move_line_qtys" t-value="move_line_qtys + [int(move_line.quantity)]"/>
</t>
<t t-else="">
<t t-set="move_line_qtys" t-value="move_line_qtys + [1]"/>
@ -146,7 +154,9 @@
</t>
<t t-set="index_to_qtys" t-value="{i: qty for i, qty in zip(range(0, len(move_line_qtys)), move_line_qtys)}"/>
<t t-set="num_pages" t-value="sum(move_line_qtys)// (nRows * nCols) + 1"/>
<div class="oe_structure"></div>
<div t-foreach="range(num_pages)" t-as="page" class="o_label_sheet" t-att-style="'padding: 20mm 8mm'">
<div class="oe_structure"></div>
<table class="my-0 table table-sm table-borderless">
<t t-foreach="range(nRows)" t-as="row">
<tr>
@ -166,20 +176,20 @@
<td t-att-style="make_invisible and 'visibility:hidden'">
<div t-if="move_line" t-translation="off" t-att-style="'position:relative; width:43mm; height:19mm; border: 1px solid %s;' % (move_line.env.user.company_id.primary_color or 'black')">
<div class="o_label_name o_label_4x12 fw-bold">
<div t-out="move_line.product_id.display_name"/>
<span t-out="move_line.product_id.display_name">Product</span>
<div><span>Quantity: </span>
<span t-if="move_line.product_uom_id.category_id == uom_categ_unit">1.0</span>
<span t-else="" t-out="move_line.qty_done"/>
<span t-field="move_line.product_uom_id" groups="uom.group_uom"/>
<span t-if="move_line.product_id.uom_id._has_common_reference(uom_unit)">1.0</span>
<span t-else="" t-out="move_line.quantity">5</span>
<span t-field="move_line.product_uom_id" groups="uom.group_uom">UOM id</span>
</div>
</div>
<t t-if="move_line.product_id.tracking != 'none' and (move_line.lot_name or move_line.lot_id)">
<div t-field="move_line.lot_name or move_line.lot_id.name" t-options="{'widget': 'barcode', 'img_style': 'width:100%;height:35%'}"/>
<div class="o_label_4x12 text-center" t-out="move_line.lot_name or move_line.lot_id.name"/>
<div class="o_label_4x12 text-center"><span t-out="move_line.lot_name or move_line.lot_id.name">Demo Barcode</span></div>
</t>
<t t-elif="move_line.product_id.tracking == 'none' and move_line.product_id.barcode">
<div t-field="move_line.product_id.barcode" t-options="{'widget': 'barcode', 'img_style': 'width:100%;height:35%'}"/>
<div class="o_label_4x12 text-center" t-out="move_line.product_id.barcode"/>
<div class="o_label_4x12 text-center"><span t-out="move_line.product_id.barcode">12345678901</span></div>
</t>
<t t-else="">
<span class="text-muted">No barcode available</span>
@ -190,6 +200,7 @@
</tr>
</t>
</table>
<div class="oe_structure"></div>
</div>
</t>
</template>

View file

@ -1,13 +1,15 @@
# -*- coding: utf-8 -*-
from collections import defaultdict, OrderedDict
from datetime import date, timedelta
from datetime import date, datetime, time, timedelta
import json
from odoo import api, fields, models, _
from odoo.tools import float_compare, float_round, format_date, float_is_zero
from odoo.tools import float_compare, float_round, format_date, float_is_zero, float_repr
from odoo.exceptions import UserError
class ReportBomStructure(models.AbstractModel):
class ReportMrpReport_Bom_Structure(models.AbstractModel):
_name = 'report.mrp.report_bom_structure'
_description = 'BOM Overview Report'
@ -19,7 +21,7 @@ class ReportBomStructure(models.AbstractModel):
@api.model
def get_warehouses(self):
return self.env['stock.warehouse'].search_read([('company_id', 'in', self.env.companies.ids)], fields=['id', 'name'])
return self.env['stock.warehouse'].search_read([('company_id', 'in', self.env.companies.ids)], fields=['id', 'name', 'manu_type_id'])
@api.model
def _compute_current_production_capacity(self, bom_data):
@ -27,44 +29,25 @@ class ReportBomStructure(models.AbstractModel):
components_qty_to_produce = defaultdict(lambda: 0)
components_qty_available = {}
for comp in bom_data.get('components', []):
if comp['product'].type != 'product' or float_is_zero(comp['base_bom_line_qty'], precision_rounding=comp['uom'].rounding):
if not comp['product'].is_storable or comp['uom'].is_zero(comp['base_bom_line_qty']):
continue
components_qty_to_produce[comp['product_id']] += comp['base_bom_line_qty']
components_qty_available[comp['product_id']] = comp['quantity_available']
components_qty_available[comp['product_id']] = comp['free_to_manufacture_qty']
producibles = [float_round(components_qty_available[p_id] / qty, precision_digits=0, rounding_method='DOWN') for p_id, qty in components_qty_to_produce.items()]
return min(producibles) * bom_data['bom']['product_qty'] if producibles else 0
@api.model
def _compute_production_capacities(self, bom_qty, bom_data):
date_today = self.env.context.get('from_date', fields.date.today())
same_delay = bom_data['lead_time'] == bom_data['availability_delay']
res = {}
if bom_data.get('producible_qty', 0):
# Check if something is producible today, at the earliest time possible considering product's lead time.
res['earliest_capacity'] = bom_data['producible_qty']
res['earliest_date'] = format_date(self.env, date_today + timedelta(days=bom_data['lead_time']))
if bom_data['availability_state'] != 'unavailable':
if same_delay:
# Means that stock will be resupplied at date_today, so the whole manufacture can start at date_today.
res['earliest_capacity'] = bom_qty
res['earliest_date'] = format_date(self.env, date_today + timedelta(days=bom_data['lead_time']))
else:
res['leftover_capacity'] = bom_qty - bom_data.get('producible_qty', 0)
res['leftover_date'] = format_date(self.env, date_today + timedelta(days=bom_data['availability_delay']))
return res
@api.model
def _get_report_values(self, docids, data=None):
docs = []
for bom_id in docids:
bom = self.env['mrp.bom'].browse(bom_id)
if not bom:
continue
variant = data.get('variant')
candidates = variant and self.env['product.product'].browse(int(variant)) or bom.product_id or bom.product_tmpl_id.product_variant_ids
quantity = float(data.get('quantity', bom.product_qty))
if data.get('warehouse_id'):
self = self.with_context(warehouse=int(data.get('warehouse_id')))
self = self.with_context(warehouse_id=int(data.get('warehouse_id'))) # noqa: PLW0642
for product_variant_id in candidates.ids:
docs.append(self._get_pdf_doc(bom_id, data, quantity, product_variant_id))
if not candidates:
@ -81,10 +64,7 @@ class ReportBomStructure(models.AbstractModel):
doc = self._get_pdf_line(bom_id, product_id=product_variant_id, qty=quantity, unfolded_ids=set(json.loads(data.get('unfolded_ids'))))
else:
doc = self._get_pdf_line(bom_id, product_id=product_variant_id, qty=quantity, unfolded=True)
doc['show_availabilities'] = False if data and data.get('availabilities') == 'false' else True
doc['show_costs'] = False if data and data.get('costs') == 'false' else True
doc['show_operations'] = False if data and data.get('operations') == 'false' else True
doc['show_lead_times'] = False if data and data.get('lead_times') == 'false' else True
doc['forecast_mode'] = data.get('mode', 'overview') == 'forecast'
return doc
@api.model
@ -108,26 +88,24 @@ class ReportBomStructure(models.AbstractModel):
for variant in bom.product_tmpl_id.product_variant_ids:
bom_product_variants[variant.id] = variant.display_name
if self.env.context.get('warehouse'):
warehouse = self.env['stock.warehouse'].browse(self.env.context.get('warehouse'))
if self.env.context.get('warehouse_id'):
warehouse = self.env['stock.warehouse'].browse(self.env.context.get('warehouse_id'))
else:
warehouse = self.env['stock.warehouse'].browse(self.get_warehouses()[0]['id'])
lines = self._get_bom_data(bom, warehouse, product=product, line_qty=bom_quantity, level=0)
production_capacities = self._compute_production_capacities(bom_quantity, lines)
lines.update(production_capacities)
return {
'lines': lines,
'variants': bom_product_variants,
'bom_uom_name': bom_uom_name,
'bom_qty': bom_quantity,
'is_variant_applied': self.env.user.user_has_groups('product.group_product_variant') and len(bom_product_variants) > 1,
'is_uom_applied': self.env.user.user_has_groups('uom.group_uom'),
'precision': self.env['decimal.precision'].precision_get('Product Unit of Measure'),
'is_variant_applied': self.env.user.has_group('product.group_product_variant') and len(bom_product_variants) > 1,
'is_uom_applied': self.env.user.has_group('uom.group_uom'),
'precision': self.env['decimal.precision'].precision_get('Product Unit'),
}
@api.model
def _get_components_closest_forecasted(self, lines, line_quantities, parent_bom, product_info, ignore_stock=False):
def _get_components_closest_forecasted(self, lines, line_quantities, parent_bom, product_info, parent_product, ignore_stock=False):
"""
Returns a dict mapping products to a dict of their corresponding BoM lines,
which are mapped to their closest date in the forecast report where consumed quantity >= forecasted quantity.
@ -146,34 +124,35 @@ class ReportBomStructure(models.AbstractModel):
product_quantities_info = defaultdict(OrderedDict)
for line in lines:
product = line.product_id
quantities_info = self._get_quantities_info(product, line.product_uom_id, parent_bom, product_info)
line_quantity = line_quantities.get(line.id, 0.0)
quantities_info = self._get_quantities_info(product, line.product_uom_id, product_info, parent_bom, parent_product)
stock_loc = quantities_info['stock_loc']
product_info[product.id]['consumptions'][stock_loc] += line_quantities.get(line.id, 0.0)
product_info[product.id]['consumptions'][stock_loc] += line_quantity
product_quantities_info[product.id][line.id] = product_info[product.id]['consumptions'][stock_loc]
if (product.detailed_type != 'product' or
float_compare(product_info[product.id]['consumptions'][stock_loc], quantities_info['free_qty'], precision_rounding=product.uom_id.rounding) <= 0):
if (not product.is_storable or
product.uom_id.compare(product_info[product.id]['consumptions'][stock_loc], quantities_info['free_qty']) <= 0):
# Use date.min as a sentinel value for _get_stock_availability
closest_forecasted[product.id][line.id] = date.min
elif stock_loc != 'in_stock':
elif stock_loc != 'in_stock' or quantities_info['forecasted_qty'] < line_quantity:
closest_forecasted[product.id][line.id] = date.max
else:
remaining_products.append(product.id)
closest_forecasted[product.id][line.id] = None
date_today = self.env.context.get('from_date', fields.date.today())
date_today = self.env.context.get('from_date', fields.Date.today())
domain = [('state', '=', 'forecast'), ('date', '>=', date_today), ('product_id', 'in', list(set(remaining_products)))]
if self.env.context.get('warehouse'):
domain.append(('warehouse_id', '=', self.env.context.get('warehouse')))
if self.env.context.get('warehouse_id'):
domain.append(('warehouse_id', '=', self.env.context.get('warehouse_id')))
if remaining_products:
res = self.env['report.stock.quantity']._read_group(
domain,
['min_date:min(date)', 'product_id', 'product_qty'],
['product_id', 'product_qty'],
orderby='product_id asc, min_date asc', lazy=False
groupby=['product_id', 'product_qty'],
aggregates=['date:min'],
order='product_id asc, date:min asc'
)
available_quantities = defaultdict(list)
for group in res:
product_id = group['product_id'][0]
available_quantities[product_id].append([group['min_date'], group['product_qty']])
product_id = group[0].id
available_quantities[product_id].append([group[2], group[1]])
for product_id in remaining_products:
# Find the first empty line_id for the given product_id.
line_id = next(filter(lambda k: not closest_forecasted[product_id][k], closest_forecasted[product_id].keys()), None)
@ -187,7 +166,7 @@ class ReportBomStructure(models.AbstractModel):
return closest_forecasted
@api.model
def _get_bom_data(self, bom, warehouse, product=False, line_qty=False, bom_line=False, level=0, parent_bom=False, index=0, product_info=False, ignore_stock=False):
def _get_bom_data(self, bom, warehouse, product=False, line_qty=False, bom_line=False, level=0, parent_bom=False, parent_product=False, index=0, product_info=False, ignore_stock=False, simulated_leaves_per_workcenter=False):
""" Gets recursively the BoM and all its subassemblies and computes availibility estimations for each component and their disponibility in stock.
Accepts specific keys in context that will affect the data computed :
- 'minimized': Will cut all data not required to compute availability estimations.
@ -198,37 +177,36 @@ class ReportBomStructure(models.AbstractModel):
product = bom.product_id or bom.product_tmpl_id.product_variant_id
if line_qty is False:
line_qty = bom.product_qty
if not product_info:
product_info = {}
if simulated_leaves_per_workcenter is False:
simulated_leaves_per_workcenter = defaultdict(list)
company = bom.company_id or self.env.company
current_quantity = line_qty
if bom_line:
current_quantity = bom_line.product_uom_id._compute_quantity(line_qty, bom.product_uom_id) or 0
prod_cost = 0
attachment_ids = []
has_attachments = False
if not is_minimized:
if product:
prod_cost = product.uom_id._compute_price(product.with_company(company).standard_price, bom.product_uom_id) * current_quantity
attachment_ids = self.env['mrp.document'].search(['|', '&', ('res_model', '=', 'product.product'),
has_attachments = self.env['product.document'].search_count(['&', '&', ('attached_on_mrp', '=', 'bom'), ('active', '=', 't'), '|', '&', ('res_model', '=', 'product.product'),
('res_id', '=', product.id), '&', ('res_model', '=', 'product.template'),
('res_id', '=', product.product_tmpl_id.id)]).ids
('res_id', '=', product.product_tmpl_id.id)], limit=1) > 0
else:
# Use the product template instead of the variant
prod_cost = bom.product_tmpl_id.uom_id._compute_price(bom.product_tmpl_id.with_company(company).standard_price, bom.product_uom_id) * current_quantity
attachment_ids = self.env['mrp.document'].search([('res_model', '=', 'product.template'), ('res_id', '=', bom.product_tmpl_id.id)]).ids
has_attachments = self.env['product.document'].search_count(['&', '&', ('attached_on_mrp', '=', 'bom'), ('active', '=', 't'),
'&', ('res_model', '=', 'product.template'), ('res_id', '=', bom.product_tmpl_id.id)], limit=1) > 0
key = product.id
bom_key = bom.id
qty_product_uom = bom.product_uom_id._compute_quantity(current_quantity, product.uom_id or bom.product_tmpl_id.uom_id)
self._update_product_info(product, bom_key, product_info, warehouse, qty_product_uom, bom=bom, parent_bom=parent_bom)
self._update_product_info(product, bom_key, product_info, warehouse, qty_product_uom, bom=bom, parent_bom=parent_bom, parent_product=parent_product)
route_info = product_info[key].get(bom_key, {})
quantities_info = {}
if not ignore_stock:
# Useless to compute quantities_info if it's not going to be used later on
quantities_info = self._get_quantities_info(product, bom.product_uom_id, parent_bom, product_info)
quantities_info = self._get_quantities_info(product, bom.product_uom_id, product_info, parent_bom, parent_product)
bom_report_line = {
'index': index,
@ -236,9 +214,12 @@ class ReportBomStructure(models.AbstractModel):
'bom_id': bom and bom.id or False,
'bom_code': bom and bom.code or False,
'type': 'bom',
'is_storable': product.is_storable,
'quantity': current_quantity,
'quantity_available': quantities_info.get('free_qty') or 0,
'quantity_on_hand': quantities_info.get('on_hand_qty') or 0,
'quantity_forecasted': quantities_info.get('forecasted_qty') or 0,
'free_to_manufacture_qty': quantities_info.get('free_to_manufacture_qty') or 0,
'base_bom_line_qty': bom_line.product_qty if bom_line else False, # bom_line isn't defined only for the top-level product
'name': product.display_name or bom.product_tmpl_id.display_name,
'uom': bom.product_uom_id if bom else product.uom_id,
@ -247,29 +228,21 @@ class ReportBomStructure(models.AbstractModel):
'route_name': route_info.get('route_name', ''),
'route_detail': route_info.get('route_detail', ''),
'route_alert': route_info.get('route_alert', False),
'lead_time': route_info.get('lead_time', False),
'currency': company.currency_id,
'currency_id': company.currency_id.id,
'product': product,
'product_id': product.id,
'product_template_id': product.product_tmpl_id.id,
'link_id': (product.id if product.product_variant_count > 1 else product.product_tmpl_id.id) or bom.product_tmpl_id.id,
'link_model': 'product.product' if product.product_variant_count > 1 else 'product.template',
'code': bom and bom.display_name or '',
'prod_cost': prod_cost,
'bom_cost': 0,
'level': level or 0,
'attachment_ids': attachment_ids,
'has_attachments': has_attachments,
'phantom_bom': bom.type == 'phantom',
'parent_id': parent_bom and parent_bom.id or False,
}
if not is_minimized:
operations = self._get_operation_line(product, bom, float_round(current_quantity, precision_rounding=1, rounding_method='UP'), level + 1, index)
bom_report_line['operations'] = operations
bom_report_line['operations_cost'] = sum([op['bom_cost'] for op in operations])
bom_report_line['operations_time'] = sum([op['quantity'] for op in operations])
bom_report_line['bom_cost'] += bom_report_line['operations_cost']
components = []
no_bom_lines = self.env['mrp.bom.line']
line_quantities = {}
@ -282,27 +255,67 @@ class ReportBomStructure(models.AbstractModel):
no_bom_lines |= line
# Update product_info for all the components before computing closest forecasted.
qty_product_uom = line.product_uom_id._compute_quantity(line_quantity, line.product_id.uom_id)
self.with_context(parent_product_id=product.id)._update_product_info(line.product_id, bom.id, product_info, warehouse, qty_product_uom, bom=False, parent_bom=bom)
components_closest_forecasted = self.with_context(parent_product_id=product.id)._get_components_closest_forecasted(no_bom_lines, line_quantities, bom, product_info, ignore_stock)
self._update_product_info(line.product_id, bom.id, product_info, warehouse, qty_product_uom, bom=False, parent_bom=bom, parent_product=product)
components_closest_forecasted = self._get_components_closest_forecasted(no_bom_lines, line_quantities, bom, product_info, product, ignore_stock)
for component_index, line in enumerate(bom.bom_line_ids):
new_index = f"{index}{component_index}"
if product and line._skip_bom_line(product):
continue
line_quantity = line_quantities.get(line.id, 0.0)
if line.child_bom_id:
component = self.with_context(parent_product_id=product.id)._get_bom_data(line.child_bom_id, warehouse, line.product_id, line_quantity, bom_line=line, level=level + 1, parent_bom=bom,
index=new_index, product_info=product_info, ignore_stock=ignore_stock)
component = self._get_bom_data(line.child_bom_id, warehouse, line.product_id, line_quantity, bom_line=line, level=level + 1, parent_bom=bom,
parent_product=product, index=new_index, product_info=product_info, ignore_stock=ignore_stock,
simulated_leaves_per_workcenter=simulated_leaves_per_workcenter)
else:
component = self.with_context(
parent_product_id=product.id,
components_closest_forecasted=components_closest_forecasted,
)._get_component_data(bom, warehouse, line, line_quantity, level + 1, new_index, product_info, ignore_stock)
components.append(component)
)._get_component_data(bom, product, warehouse, line, line_quantity, level + 1, new_index, product_info, ignore_stock)
for component_bom in components:
if component['product_id'] == component_bom['product_id'] and component['uom'].id == component_bom['uom'].id:
self._merge_components(component_bom, component)
break
else:
components.append(component)
bom_report_line['bom_cost'] += component['bom_cost']
for component in components:
if component['is_storable']:
if missing_qty := max(component['quantity'] - component['quantity_forecasted'], 0):
missing_qty = float_repr(missing_qty, self.env['decimal.precision'].precision_get('Product Unit'))
route_name = component['route_name'] or _('Order')
component['status'] = _("%(qty)s To %(route)s", qty=missing_qty, route=route_name)
bom_report_line['components'] = components
bom_report_line['producible_qty'] = self._compute_current_production_capacity(bom_report_line)
availabilities = self._get_availabilities(product, current_quantity, product_info, bom_key, quantities_info, level, ignore_stock, components, report_line=bom_report_line)
# in case of subcontracting, lead_time will be calculated with components availability delay
bom_report_line['lead_time'] = route_info.get('lead_time', False)
bom_report_line['manufacture_delay'] = route_info.get('manufacture_delay', False)
bom_report_line.update(availabilities)
if level == 0:
if bom_report_line['producible_qty'] > 0:
bom_report_line['status'] = _("%(qty)s Ready To Produce", qty=bom_report_line['producible_qty'])
else:
bom_report_line['status'] = _("No Ready To Produce")
elif missing_qty := max(bom_report_line['quantity'] - bom_report_line['quantity_available'], 0):
missing_qty = float_repr(missing_qty, self.env['decimal.precision'].precision_get('Product Unit'))
route_name = bom_report_line['route_name'] or _('Order')
bom_report_line['status'] = _("%(qty)s To %(route)s", qty=missing_qty, route=route_name)
if not is_minimized:
operations = self._get_operation_line(product, bom, float_round(current_quantity, precision_digits=self.env['decimal.precision'].precision_get('Product Unit'), rounding_method='UP'), level + 1, index, bom_report_line, simulated_leaves_per_workcenter)
bom_report_line['operations'] = operations
bom_report_line['operations_cost'] = sum(op['bom_cost'] for op in operations)
bom_report_line['operations_time'] = sum(op['quantity'] for op in operations)
bom_report_line['operations_delay'] = max((op['availability_delay'] for op in operations), default=0)
if 'simulated' in bom_report_line:
bom_report_line['availability_state'] = 'estimated'
max_component_delay = bom_report_line['max_component_delay']
bom_report_line['availability_delay'] = max_component_delay + max(bom.produce_delay, bom_report_line['operations_delay'])
bom_report_line['availability_display'] = self._format_date_display(bom_report_line['availability_state'], bom_report_line['availability_delay'])
bom_report_line['bom_cost'] += bom_report_line['operations_cost']
byproducts, byproduct_cost_portion = self._get_byproducts_lines(product, bom, current_quantity, level + 1, bom_report_line['bom_cost'], index)
bom_report_line['byproducts'] = byproducts
bom_report_line['cost_share'] = float_round(1 - byproduct_cost_portion, precision_rounding=0.0001)
@ -310,8 +323,7 @@ class ReportBomStructure(models.AbstractModel):
bom_report_line['byproducts_total'] = sum(byproduct['quantity'] for byproduct in byproducts)
bom_report_line['bom_cost'] *= bom_report_line['cost_share']
availabilities = self._get_availabilities(product, current_quantity, product_info, bom_key, quantities_info, level, ignore_stock, components)
bom_report_line.update(availabilities)
bom_report_line['foldable'] = len(bom.operation_ids) > 0 or (len(bom_report_line['components']) > 0 and level > 0) or any(component.get('foldable', False) for component in bom_report_line['components'])
if level == 0:
# Gives a unique key for the first line that indicates if product is ready for production right now.
@ -319,7 +331,7 @@ class ReportBomStructure(models.AbstractModel):
return bom_report_line
@api.model
def _get_component_data(self, parent_bom, warehouse, bom_line, line_quantity, level, index, product_info, ignore_stock=False):
def _get_component_data(self, parent_bom, parent_product, warehouse, bom_line, line_quantity, level, index, product_info, ignore_stock=False):
company = parent_bom.company_id or self.env.company
price = bom_line.product_id.uom_id._compute_price(bom_line.product_id.with_company(company).standard_price, bom_line.product_uom_id) * line_quantity
rounded_price = company.currency_id.round(price)
@ -331,13 +343,13 @@ class ReportBomStructure(models.AbstractModel):
quantities_info = {}
if not ignore_stock:
# Useless to compute quantities_info if it's not going to be used later on
quantities_info = self._get_quantities_info(bom_line.product_id, bom_line.product_uom_id, parent_bom, product_info)
quantities_info = self._get_quantities_info(bom_line.product_id, bom_line.product_uom_id, product_info, parent_bom, parent_product)
availabilities = self._get_availabilities(bom_line.product_id, line_quantity, product_info, bom_key, quantities_info, level, ignore_stock, bom_line=bom_line)
attachment_ids = []
has_attachments = False
if not self.env.context.get('minimized', False):
attachment_ids = self.env['mrp.document'].search(['|', '&', ('res_model', '=', 'product.product'), ('res_id', '=', bom_line.product_id.id),
'&', ('res_model', '=', 'product.template'), ('res_id', '=', bom_line.product_id.product_tmpl_id.id)]).ids
has_attachments = self.env['product.document'].search_count(['&', ('attached_on_mrp', '=', 'bom'), '|', '&', ('res_model', '=', 'product.product'), ('res_id', '=', bom_line.product_id.id),
'&', ('res_model', '=', 'product.template'), ('res_id', '=', bom_line.product_id.product_tmpl_id.id)]) > 0
return {
'type': 'component',
@ -345,25 +357,29 @@ class ReportBomStructure(models.AbstractModel):
'bom_id': False,
'product': bom_line.product_id,
'product_id': bom_line.product_id.id,
'product_template_id': bom_line.product_tmpl_id.id,
'link_id': bom_line.product_id.id if bom_line.product_id.product_variant_count > 1 else bom_line.product_id.product_tmpl_id.id,
'link_model': 'product.product' if bom_line.product_id.product_variant_count > 1 else 'product.template',
'name': bom_line.product_id.display_name,
'code': '',
'currency': company.currency_id,
'currency_id': company.currency_id.id,
'is_storable': bom_line.product_id.is_storable,
'quantity': line_quantity,
'quantity_available': quantities_info.get('free_qty', 0),
'quantity_on_hand': quantities_info.get('on_hand_qty', 0),
'quantity_forecasted': quantities_info.get('forecasted_qty', 0),
'free_to_manufacture_qty': quantities_info.get('free_to_manufacture_qty', 0),
'base_bom_line_qty': bom_line.product_qty,
'uom': bom_line.product_uom_id,
'uom_name': bom_line.product_uom_id.name,
'prod_cost': rounded_price,
'bom_cost': rounded_price,
'route_type': route_info.get('route_type', ''),
'route_name': route_info.get('route_name', ''),
'route_detail': route_info.get('route_detail', ''),
'route_alert': route_info.get('route_alert', False),
'lead_time': route_info.get('lead_time', False),
'manufacture_delay': route_info.get('manufacture_delay', False),
'stock_avail_state': availabilities['stock_avail_state'],
'resupply_avail_delay': availabilities['resupply_avail_delay'],
'availability_display': availabilities['availability_display'],
@ -371,31 +387,31 @@ class ReportBomStructure(models.AbstractModel):
'availability_delay': availabilities['availability_delay'],
'parent_id': parent_bom.id,
'level': level or 0,
'attachment_ids': attachment_ids,
'has_attachments': has_attachments,
}
@api.model
def _get_quantities_info(self, product, bom_uom, parent_bom, product_info):
return {
'free_qty': product.uom_id._compute_quantity(product.free_qty, bom_uom) if product.detailed_type == 'product' else False,
'on_hand_qty': product.uom_id._compute_quantity(product.qty_available, bom_uom) if product.detailed_type == 'product' else False,
def _get_quantities_info(self, product, bom_uom, product_info, parent_bom=False, parent_product=False):
quantities_info = {
'free_qty': max(product.uom_id._compute_quantity(product.free_qty, bom_uom), 0) if product.is_storable else 0,
'on_hand_qty': product.uom_id._compute_quantity(product.qty_available, bom_uom) if product.is_storable else 0,
'forecasted_qty': product.uom_id._compute_quantity(product.virtual_available, bom_uom) if product.is_storable else 0,
'stock_loc': 'in_stock',
}
quantities_info['free_to_manufacture_qty'] = quantities_info['free_qty']
return quantities_info
@api.model
def _update_product_info(self, product, bom_key, product_info, warehouse, quantity, bom, parent_bom):
def _update_product_info(self, product, bom_key, product_info, warehouse, quantity, bom, parent_bom, parent_product):
key = product.id
if key not in product_info:
product_info[key] = {'consumptions': {'in_stock': 0}}
if not product_info[key].get(bom_key):
product_info[key][bom_key] = self.with_context(
product_info=product_info, parent_bom=parent_bom
)._get_resupply_route_info(warehouse, product, quantity, bom)
product_info[key][bom_key] = self._get_resupply_route_info(warehouse, product, quantity, product_info, bom, parent_bom, parent_product)
elif product_info[key][bom_key].get('route_alert'):
# Need more quantity than a single line, might change with additional quantity
product_info[key][bom_key] = self.with_context(
product_info=product_info, parent_bom=parent_bom
)._get_resupply_route_info(warehouse, product, quantity + product_info[key][bom_key].get('qty_checked'), bom)
product_info[key][bom_key] = self._get_resupply_route_info(
warehouse, product, quantity + product_info[key][bom_key].get('qty_checked'), product_info, bom, parent_bom, parent_product)
@api.model
def _get_byproducts_lines(self, product, bom, bom_quantity, level, total, index):
@ -420,7 +436,6 @@ class ReportBomStructure(models.AbstractModel):
'name': byproduct.product_id.display_name,
'quantity': line_quantity,
'uom_name': byproduct.product_uom_id.name,
'prod_cost': company.currency_id.round(price),
'parent_id': bom.id,
'level': level or 0,
'bom_cost': company.currency_id.round(total * cost_share),
@ -430,24 +445,37 @@ class ReportBomStructure(models.AbstractModel):
return byproducts, byproduct_cost_portion
@api.model
def _get_operation_cost(self, duration, operation):
return (duration / 60.0) * operation.workcenter_id.costs_hour
@api.model
def _get_operation_line(self, product, bom, qty, level, index):
def _get_operation_line(self, product, bom, qty, level, index, bom_report_line, simulated_leaves_per_workcenter):
operations = []
total = 0.0
qty = bom.product_uom_id._compute_quantity(qty, bom.product_tmpl_id.uom_id)
company = bom.company_id or self.env.company
operations_planning = {}
if bom_report_line['availability_state'] in ['unavailable', 'estimated'] and bom.operation_ids:
qty_requested = bom.product_uom_id._compute_quantity(qty, bom.product_tmpl_id.uom_id)
qty_to_produce = bom.product_tmpl_id.uom_id._compute_quantity(max(0, qty_requested - (product.virtual_available if level > 1 else 0)), bom.product_uom_id)
if not (product or bom.product_tmpl_id).uom_id.is_zero(qty_to_produce):
max_component_delay = 0
for component in bom_report_line['components']:
line_delay = component.get('availability_delay', 0)
max_component_delay = max(max_component_delay, line_delay)
date_today = self.env.context.get('from_date', fields.Date.today()) + timedelta(days=max_component_delay)
operations_planning = self._simulate_bom_planning(bom, product, datetime.combine(date_today, time.min), qty_to_produce, simulated_leaves_per_workcenter=simulated_leaves_per_workcenter)
bom_report_line['simulated'] = True
bom_report_line['max_component_delay'] = max_component_delay
operation_index = 0
for operation in bom.operation_ids:
if not product or operation._skip_operation_line(product):
continue
capacity = operation.workcenter_id._get_capacity(product)
operation_cycle = float_round(qty / capacity, precision_rounding=1, rounding_method='UP')
duration_expected = (operation_cycle * operation.time_cycle * 100.0 / operation.workcenter_id.time_efficiency) + \
operation.workcenter_id._get_expected_duration(product)
total = self._get_operation_cost(duration_expected, operation)
op = operation.with_context(product=product, quantity=qty)
duration_expected = op.time_total
bom_cost = self.env.company.currency_id.round(op.cost)
if planning := operations_planning.get(operation, None):
availability_state = 'estimated'
availability_delay = (planning['date_finished'].date() - date_today).days
availability_display = _('Estimated %s', format_date(self.env, planning['date_finished'])) + (" [" + planning['workcenter'].name + "]" if planning['workcenter'] != operation.workcenter_id else "")
else:
availability_state = 'available'
availability_delay = 0
availability_display = ''
operations.append({
'type': 'operation',
'index': f"{index}{operation_index}",
@ -458,9 +486,12 @@ class ReportBomStructure(models.AbstractModel):
'name': operation.name + ' - ' + operation.workcenter_id.name,
'uom_name': _("Minutes"),
'quantity': duration_expected,
'bom_cost': self.env.company.currency_id.round(total),
'bom_cost': bom_cost,
'currency_id': company.currency_id.id,
'model': 'mrp.routing.workcenter',
'availability_state': availability_state,
'availability_delay': availability_delay,
'availability_display': availability_display,
})
operation_index += 1
return operations
@ -476,8 +507,8 @@ class ReportBomStructure(models.AbstractModel):
else:
product = bom.product_id or bom.product_tmpl_id.product_variant_id or bom.product_tmpl_id.with_context(active_test=False).product_variant_ids[:1]
if self.env.context.get('warehouse'):
warehouse = self.env['stock.warehouse'].browse(self.env.context.get('warehouse'))
if self.env.context.get('warehouse_id'):
warehouse = self.env['stock.warehouse'].browse(self.env.context.get('warehouse_id'))
else:
warehouse = self.env['stock.warehouse'].browse(self.get_warehouses()[0]['id'])
@ -499,22 +530,24 @@ class ReportBomStructure(models.AbstractModel):
'bom_id': bom_line['bom_id'],
'name': bom_line['name'],
'type': bom_line['type'],
'is_storable': bom_line['is_storable'],
'quantity': bom_line['quantity'],
'quantity_available': bom_line['quantity_available'],
'quantity_on_hand': bom_line['quantity_on_hand'],
'producible_qty': bom_line.get('producible_qty', False),
'uom': bom_line['uom_name'],
'prod_cost': bom_line['prod_cost'],
'bom_cost': bom_line['bom_cost'],
'route_name': bom_line['route_name'],
'route_detail': bom_line['route_detail'],
'route_alert': bom_line.get('route_alert', False),
'lead_time': bom_line['lead_time'],
'manufacture_delay': bom_line['manufacture_delay'],
'level': bom_line['level'],
'code': bom_line['code'],
'availability_state': bom_line['availability_state'],
'availability_display': bom_line['availability_display'],
'visible': line_visible,
'status': bom_line.get('status', ""),
})
if bom_line.get('components'):
lines += self._get_bom_array_lines(bom_line, level + 1, unfolded_ids, unfolded, line_visible and line_unfolded)
@ -538,6 +571,9 @@ class ReportBomStructure(models.AbstractModel):
'uom': _('minutes'),
'bom_cost': operation['bom_cost'],
'level': level + 1,
'availability_state': operation['availability_state'],
'availability_delay': operation['availability_delay'],
'availability_display': operation['availability_display'],
'visible': operations_unfolded,
})
if data['byproducts']:
@ -557,7 +593,6 @@ class ReportBomStructure(models.AbstractModel):
'type': 'byproduct',
'quantity': byproduct['quantity'],
'uom': byproduct['uom_name'],
'prod_cost': byproduct['prod_cost'],
'bom_cost': byproduct['bom_cost'],
'level': level + 1,
'visible': byproducts_unfolded,
@ -565,30 +600,27 @@ class ReportBomStructure(models.AbstractModel):
return lines
@api.model
def _get_resupply_route_info(self, warehouse, product, quantity, bom=False):
def _get_resupply_route_info(self, warehouse, product, quantity, product_info, bom=False, parent_bom=False, parent_product=False):
found_rules = []
if self._need_special_rules(self.env.context.get('product_info'), self.env.context.get('parent_bom'), self.env.context.get('parent_product_id')):
found_rules = self._find_special_rules(product, self.env.context.get('product_info'), self.env.context.get('parent_bom'), self.env.context.get('parent_product_id'))
if found_rules and not self._is_resupply_rules(found_rules, bom):
# We only want to show the effective resupply (i.e. a form of manufacture or buy)
found_rules = []
if self._need_special_rules(product_info, parent_bom, parent_product):
found_rules = self._find_special_rules(product, product_info, bom, parent_bom, parent_product)
if not found_rules:
found_rules = product._get_rules_from_location(warehouse.lot_stock_id)
if not found_rules:
return {}
rules_delay = sum(rule.delay for rule in found_rules)
return self._format_route_info(found_rules, rules_delay, warehouse, product, bom, quantity)
return self.with_context(parent_bom=parent_bom)._format_route_info(found_rules, rules_delay, warehouse, product, bom, quantity)
@api.model
def _is_resupply_rules(self, rules, bom):
return bom and any(rule.action == 'manufacture' for rule in rules)
@api.model
def _need_special_rules(self, product_info, parent_bom=False, parent_product_id=False):
def _need_special_rules(self, product_info, parent_bom=False, parent_product=False):
return False
@api.model
def _find_special_rules(self, product, product_info, parent_bom=False, parent_product_id=False):
def _find_special_rules(self, product, product_info, current_bom=False, parent_bom=False, parent_product=False):
return False
@api.model
@ -599,18 +631,18 @@ class ReportBomStructure(models.AbstractModel):
wh_manufacture_rules = product._get_rules_from_location(product.property_stock_production, route_ids=warehouse.route_ids)
wh_manufacture_rules -= rules
rules_delay += sum(rule.delay for rule in wh_manufacture_rules)
manufacturing_lead = bom.company_id.manufacturing_lead if bom and bom.company_id else 0
return {
'route_type': 'manufacture',
'route_name': manufacture_rules[0].route_id.display_name,
'route_detail': bom.display_name,
'lead_time': product.produce_delay + rules_delay + manufacturing_lead,
'manufacture_delay': product.produce_delay + rules_delay + manufacturing_lead,
'lead_time': bom.produce_delay + rules_delay + bom.days_to_prepare_mo,
'manufacture_delay': bom.produce_delay + rules_delay,
'bom': bom,
}
return {}
@api.model
def _get_availabilities(self, product, quantity, product_info, bom_key, quantities_info, level, ignore_stock=False, components=False, bom_line=None):
def _get_availabilities(self, product, quantity, product_info, bom_key, quantities_info, level, ignore_stock=False, components=False, bom_line=None, report_line=False):
# Get availabilities according to stock (today & forecasted).
stock_state, stock_delay = ('unavailable', False)
if not ignore_stock:
@ -620,11 +652,15 @@ class ReportBomStructure(models.AbstractModel):
components = components or []
route_info = product_info[product.id].get(bom_key)
resupply_state, resupply_delay = ('unavailable', False)
if product and product.detailed_type != 'product':
if product and not product.is_storable:
resupply_state, resupply_delay = ('available', 0)
elif route_info:
resupply_state, resupply_delay = self._get_resupply_availability(route_info, components)
if resupply_state == "unavailable" and route_info == {} and components and report_line and report_line['phantom_bom']:
val = self._get_last_availability(report_line)
return val
base = {
'resupply_avail_delay': resupply_delay,
'stock_avail_state': stock_state,
@ -650,26 +686,25 @@ class ReportBomStructure(models.AbstractModel):
return ('available', 0)
if closest_forecasted == date.max:
return ('unavailable', False)
date_today = self.env.context.get('from_date', fields.date.today())
if product and product.detailed_type != 'product':
date_today = self.env.context.get('from_date', fields.Date.today())
if product and not product.is_storable:
return ('available', 0)
stock_loc = quantities_info['stock_loc']
product_info[product.id]['consumptions'][stock_loc] += quantity
# Check if product is already in stock with enough quantity
if product and float_compare(product_info[product.id]['consumptions'][stock_loc], quantities_info['free_qty'], precision_rounding=product.uom_id.rounding) <= 0:
if product and product.uom_id.compare(product_info[product.id]['consumptions'][stock_loc], quantities_info['free_qty']) <= 0:
return ('available', 0)
# No need to check forecast if the product isn't located in our stock
if stock_loc == 'in_stock':
domain = [('state', '=', 'forecast'), ('date', '>=', date_today), ('product_id', '=', product.id), ('product_qty', '>=', product_info[product.id]['consumptions'][stock_loc])]
if self.env.context.get('warehouse'):
domain.append(('warehouse_id', '=', self.env.context.get('warehouse')))
if self.env.context.get('warehouse_id'):
domain.append(('warehouse_id', '=', self.env.context.get('warehouse_id')))
# Seek the closest date in the forecast report where consummed quantity >= forecasted quantity
if not closest_forecasted:
closest_forecasted = self.env['report.stock.quantity']._read_group(domain, ['min_date:min(date)', 'product_id'], ['product_id'])
closest_forecasted = closest_forecasted and closest_forecasted[0]['min_date']
[closest_forecasted] = self.env['report.stock.quantity']._read_group(domain, aggregates=['date:min'])[0]
if closest_forecasted:
days_to_forecast = (closest_forecasted - date_today).days
return ('expected', days_to_forecast)
@ -698,7 +733,7 @@ class ReportBomStructure(models.AbstractModel):
@api.model
def _format_date_display(self, state, delay):
date_today = self.env.context.get('from_date', fields.date.today())
date_today = self.env.context.get('from_date', fields.Date.today())
if state == 'available':
return _('Available')
if state == 'unavailable':
@ -711,4 +746,112 @@ class ReportBomStructure(models.AbstractModel):
@api.model
def _has_attachments(self, data):
return data['attachment_ids'] or any(self._has_attachments(component) for component in data.get('components', []))
return data['has_attachments'] or any(self._has_attachments(component) for component in data.get('components', []))
def _merge_components(self, component_1, component_2):
qty = component_2['quantity']
component_1["quantity"] = component_1["quantity"] + qty
component_1["base_bom_line_qty"] = component_1["quantity"] + qty
component_1["bom_cost"] = component_1["bom_cost"] + component_2["bom_cost"]
if component_2.get('availability_delay') is False or component_2.get('availability_delay') >= component_1.get('availability_delay'):
component_1.update(self._format_availability(component_2))
if not component_1.get("components"):
return
for index in range(len(component_1.get("components"))):
self._merge_components(component_1["components"][index], component_2["components"][index])
def _get_last_availability(self, report_line):
delay = 0
component_max_delay = False
for component in report_line["components"]:
if component["availability_delay"] is False:
component_max_delay = component
break
elif component["availability_delay"] >= delay:
component_max_delay = component
delay = component["availability_delay"]
return self._format_availability(component_max_delay)
def _format_availability(self, component):
return {
'resupply_avail_delay': component['resupply_avail_delay'],
'stock_avail_state': component['stock_avail_state'],
'availability_display': component['availability_display'],
'availability_state': component['availability_state'],
'availability_delay': component['availability_delay'],
}
def _simulate_bom_planning(self, bom, product, start_date, quantity, simulated_leaves_per_workcenter=False):
""" Simulate planning of all the operations depending on the workcenters work schedule.
(see '_plan_workorders' & '_link_workorders_and_moves')
"""
bom.ensure_one()
if not bom.operation_ids:
return {}
if not product:
product = bom.product_id or bom.product_tmpl_id.product_variant_id
planning_per_operation = {}
if simulated_leaves_per_workcenter is False:
simulated_leaves_per_workcenter = defaultdict(list)
if bom.allow_operation_dependencies:
final_operations = bom.operation_ids.filtered(lambda o: not o.needed_by_operation_ids)
for operation in final_operations:
if operation._skip_operation_line(product):
continue
self._simulate_operation_planning(operation, product, start_date, quantity, planning_per_operation, simulated_leaves_per_workcenter)
else:
for operation in bom.operation_ids:
if operation._skip_operation_line(product):
continue
self._simulate_operation_planning(operation, product, start_date, quantity, planning_per_operation, simulated_leaves_per_workcenter)
start_date = planning_per_operation[operation]['date_finished']
return planning_per_operation
def _simulate_operation_planning(self, operation, product, start_date, quantity, planning_per_operation=False, simulated_leaves_per_workcenter=False):
""" Simulate planning of an operation depending on its workcenter/alternatives work schedule.
(see '_plan_workorder')
"""
operation.ensure_one()
if planning_per_operation is False:
planning_per_operation = {}
if simulated_leaves_per_workcenter is False:
simulated_leaves_per_workcenter = defaultdict(list)
# Plan operation after its predecessors
date_start = max(start_date, datetime.now())
for op in operation.blocked_by_operation_ids:
if op._skip_operation_line(product):
continue
if op not in planning_per_operation:
self._simulate_operation_planning(op, product, start_date, quantity, planning_per_operation, simulated_leaves_per_workcenter)
date_start = max(date_start, planning_per_operation[op]['date_finished'])
# Consider workcenter and alternatives
workcenters = operation.workcenter_id | operation.workcenter_id.alternative_workcenter_ids
best_date_finished = datetime.max
best_date_start = best_workcenter = best_duration_expected = None
for workcenter in workcenters:
if not workcenter.resource_calendar_id:
raise UserError(_('There is no defined calendar on workcenter %s.', workcenter.name))
# Compute theoretical duration
duration_expected = operation.with_context(product=product, quantity=quantity, workcenter=workcenter).time_total
# Try to plan on workcenter
from_date, to_date = workcenter._get_first_available_slot(date_start, duration_expected, extra_leaves_slots=simulated_leaves_per_workcenter[workcenter])
# If the workcenter is unavailable, try planning on the next one
if not from_date:
continue
# Check if this workcenter is better than the previous ones
if to_date and to_date < best_date_finished:
best_date_start = from_date
best_date_finished = to_date
best_workcenter = workcenter
best_duration_expected = duration_expected
# If none of the workcenter are available, raise
if best_date_finished == datetime.max:
raise UserError(_('Impossible to plan. Please check the workcenter availabilities.'))
planning_per_operation[operation] = {
'date_start': best_date_start,
'date_finished': best_date_finished,
'workcenter': best_workcenter,
'duration_expected': best_duration_expected,
}
simulated_leaves_per_workcenter[best_workcenter].append((best_date_start, best_date_finished))
return planning_per_operation

View file

@ -10,79 +10,81 @@
<h6 t-if="data['bom_code']">Reference: <t t-esc="data['bom_code']"/></h6>
</div>
<t t-set="currency" t-value="data['currency']"/>
<table class="o_mrp_bom_expandable table">
<table class="o_mrp_bom_expandable table table-borderless">
<thead>
<tr>
<th name="th_mrp_bom_h">Product</th>
<th class="text-end">Quantity</th>
<th class="text-end" groups="uom.group_uom"/>
<th t-if="data['show_availabilities']" class="text-end">Ready to Produce</th>
<th t-if="data['show_availabilities']" class="text-end">Free to Use / On Hand</th>
<th t-if="data['show_availabilities']" class="text-center">Availability</th>
<th t-if="data['show_lead_times']" class="text-end">Lead Time</th>
<th>Route</th>
<th t-if="data['show_costs']" class="text-end">BoM Cost</th>
<th t-if="data['show_costs']" class="text-end">Product Cost</th>
<th t-if="data['forecast_mode']" class="text-end">Free to Use / On Hand</th>
<th t-if="data['forecast_mode']" class="text-center">Status</th>
<th t-if="data['forecast_mode']" class="text-center">Availability</th>
<th t-if="data['forecast_mode']" class="text-end">Lead Time</th>
<th t-if="data['forecast_mode']">Route</th>
<th t-else=""/>
<th class="text-end">BoM Cost</th>
</tr>
</thead>
<tbody>
<tr>
<td name="td_mrp_bom" t-esc="data['name']"/>
<td class="text-end" t-esc="data['quantity']" t-options='{"widget": "float", "decimal_precision": "Product Unit of Measure"}'/>
<td class="text-end" t-esc="data['quantity']" t-options='{"widget": "float", "decimal_precision": "Product Unit"}'/>
<td class="text-start" groups="uom.group_uom" t-esc="data['uom_name']"/>
<td t-if="data['show_availabilities']" class="text-end" t-esc="data['producible_qty']" t-options="{'widget': 'float', 'precision': 0}"/>
<td t-if="data['show_availabilities']" class="text-end">
<t t-esc="data['quantity_available']" t-options='{"widget": "float", "decimal_precision": "Product Unit of Measure"}'/> /
<t t-esc="data['quantity_on_hand']" t-options='{"widget": "float", "decimal_precision": "Product Unit of Measure"}'/>
<td t-if="data['forecast_mode']" class="text-end">
<t t-esc="data['quantity_available']" t-options='{"widget": "float", "decimal_precision": "Product Unit"}'/> /
<t t-esc="data['quantity_on_hand']" t-options='{"widget": "float", "decimal_precision": "Product Unit"}'/>
</td>
<td t-if="data['show_availabilities']" class="text-center">
<td t-if="data['forecast_mode']" class="text-center">
<t t-if="data['status']" t-esc="data['status']"/>
</td>
<td t-if="data['forecast_mode']" class="text-center">
<t t-if="data.get('components_available', None) != None">
<span t-attf-class="{{'text-success' if data['components_available'] and data['availability_state'] != 'unavailable' else 'text-danger' }}" t-esc="data['availability_display']"/>
</t>
</td>
<td t-if="data['show_lead_times']" class="text-end">
<td t-if="data['forecast_mode']" class="text-end">
<span t-if="data['lead_time'] is not False">
<t t-esc="data['lead_time']" t-options="{'widget': 'float', 'precision': 0}"/>
Days
</span>
</td>
<td>
<td t-if="data['forecast_mode']">
<span t-if="data['route_name']" t-attf-class="{{'text-danger' if data.get('route_alert') else ''}}"><t t-esc="data['route_name']"/>: </span>
<span t-esc="data['route_detail']"/>
</td>
<td t-if="data['show_costs']" class="text-end" t-esc="data['bom_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
<td t-if="data['show_costs']" class="text-end" t-esc="data['prod_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
<td t-else=""/>
<td class="text-end" t-esc="data['bom_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</tr>
<t t-call="mrp.report_mrp_bom_pdf_line"/>
<tr t-if="data['show_costs']">
</tbody>
<tfoot t-if="data['quantity'] &gt; 1">
<tr>
<td name="td_mrp_bom_f" class="text-end">
<span t-if="data['byproducts']" t-esc="data['name']"/>
</td>
<td class="text-end"><strong>Unit Cost</strong></td>
<td class="text-start" groups="uom.group_uom" t-esc="data['uom_name']"/>
<td t-if="data['show_availabilities']"/>
<td t-if="data['show_availabilities']"/>
<td t-if="data['show_availabilities']"/>
<td t-if="data['show_lead_times']"/>
<td/>
<td class="text-end" t-esc="data['bom_cost'] / data['quantity']" t-options='{"widget": "monetary", "display_currency": currency}'/>
<td class="text-end" t-esc="data['prod_cost']/data['quantity']" t-options='{"widget": "monetary", "display_currency": currency}'/>
<td class="text-start" groups="uom.group_uom" t-esc="data['uom_name']"/>
<td t-if="data['forecast_mode']"/>
<td t-if="data['forecast_mode']"/>
<td t-if="data['forecast_mode']"/>
<td t-if="data['forecast_mode']"/>
<td class="text-end"><strong>Unit Cost</strong></td>
<td class="text-end"><t t-esc="data['bom_cost'] / data['quantity']" t-options='{"widget": "monetary", "display_currency": currency}'/></td>
</tr>
<t t-if="data['show_costs'] and data['byproducts']" t-foreach="data['byproducts']" t-as="byproduct">
<t t-if="data['byproducts']" t-foreach="data['byproducts']" t-as="byproduct">
<tr t-if="byproduct['quantity'] &gt; 0">
<td name="td_mrp_bom_byproducts_f" class="text-end" t-esc="byproduct['name']"/>
<td class="text-end"><strong>Unit Cost</strong></td>
<td class="text-start" groups="uom.group_uom" t-esc="byproduct['uom_name']"/>
<td t-if="data['show_availabilities']"/>
<td t-if="data['show_availabilities']"/>
<td t-if="data['show_availabilities']"/>
<td t-if="data['show_lead_times']"/>
<td/>
<td class="text-end" t-esc="byproduct['bom_cost'] / byproduct['quantity']" t-options='{"widget": "monetary", "display_currency": currency}'/>
<td class="text-end" t-esc="byproduct['prod_cost'] / byproduct['quantity']" t-options='{"widget": "monetary", "display_currency": currency}'/>
<td class="text-start" groups="uom.group_uom" t-esc="byproduct['uom_name']"/>
<td t-if="data['forecast_mode']"/>
<td t-if="data['forecast_mode']"/>
<td t-if="data['forecast_mode']"/>
<td t-if="data['forecast_mode']"/>
<td class="text-end"><strong>Unit Cost</strong></td>
<td class="text-end"><t t-esc="byproduct['bom_cost'] / byproduct['quantity']" t-options='{"widget": "monetary", "display_currency": currency}'/></td>
</tr>
</t>
</tbody>
</tfoot>
</table>
</div>
<div t-else="" class="d-flex align-items-center justify-content-center h-50">
@ -94,45 +96,43 @@
<template id="report_mrp_bom_pdf_line">
<t t-set="currency" t-value="data['currency']"/>
<t t-foreach="data['lines']" t-as="l">
<tr t-if="l['visible'] and (l['type'] != 'operation' or data['show_operations'])">
<tr t-if="l['visible']">
<td name="td_mrp_code">
<span t-attf-style="margin-left: {{ str(l['level'] * 20) }}px"/>
<span t-esc="l['name']"/>
</td>
<td class="text-end">
<t t-if="l['type'] == 'operation'" t-esc="l['quantity']" t-options='{"widget": "float_time"}'/>
<t t-else="" t-esc="l['quantity']" t-options='{"widget": "float", "decimal_precision": "Product Unit of Measure"}'/>
<t t-else="" t-esc="l['quantity']" t-options='{"widget": "float", "decimal_precision": "Product Unit"}'/>
</td>
<td class="text-start" groups="uom.group_uom">
<t t-esc="l['uom']"/>
</td>
<td t-if="data['show_availabilities']" class="text-end">
<t t-if="l.get('producible_qty', False) is not False" t-esc="l['producible_qty']" t-options="{'widget': 'float', 'precision': 0}"/>
</td>
<td t-if="data['show_availabilities']" class="text-end">
<t t-if="l.get('quantity_available', False) is not False">
<t t-esc="l['quantity_available']" t-options='{"widget": "float", "decimal_precision": "Product Unit of Measure"}'/> /
<t t-esc="l['quantity_on_hand']" t-options='{"widget": "float", "decimal_precision": "Product Unit of Measure"}'/>
<td t-if="data['forecast_mode']" class="text-end">
<t t-if="l.get('is_storable', False)">
<t t-esc="l['quantity_available']" t-options='{"widget": "float", "decimal_precision": "Product Unit"}'/> /
<t t-esc="l['quantity_on_hand']" t-options='{"widget": "float", "decimal_precision": "Product Unit"}'/>
</t>
</td>
<td t-if="data['show_availabilities']" class="text-center">
<td t-if="data['forecast_mode']" class="text-center">
<t t-if="l.get('status', '') != ''" t-esc="l['status']"/>
</td>
<td t-if="data['forecast_mode']" class="text-center">
<t t-if="l.get('availability_state', None) != None">
<span t-attf-class="{{'text-success' if l['availability_state'] == 'available' else ''}}{{'text-warning' if l['availability_state'] == 'expected' else ''}}{{'text-danger' if l['availability_state'] == 'unavailable' else ''}}" t-esc="l['availability_display']"/>
</t>
</td>
<td t-if="data['show_lead_times']" class="text-end">
<td t-if="data['forecast_mode']" class="text-end">
<span t-if="l.get('lead_time', False) is not False">
<t t-esc="l['lead_time']" t-options="{'widget': 'float', 'precision': 0}"/>
Days
</span>
</td>
<td>
<td t-if="data['forecast_mode']">
<span t-if="l.get('route_name')" t-attf-class="{{'text-danger' if l.get('route_alert') else ''}}"><t t-esc="l['route_name']"/>: <t t-esc="l['route_detail']"/></span>
</td>
<td t-if="data['show_costs']" t-attf-class="text-end {{ 'text-muted' if l['type'] == 'component' else '' }}" t-esc="l['bom_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
<td t-if="data['show_costs']" class="text-end">
<span t-if="'prod_cost' in l" t-esc="l['prod_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</td>
<td t-else=""/>
<td t-attf-class="text-end {{ 'text-muted' if l['type'] == 'component' else '' }}" t-esc="l['bom_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</tr>
</t>
</template>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,279 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<template id="report_mo_overview">
<t t-set="data_report_landscape" t-value="True"/>
<t t-call="web.basic_layout">
<t t-foreach="docs" t-as="data">
<div class="page">
<t t-call="mrp.mo_overview_content"/>
</div>
<p style="page-break-before:always;"> </p>
</t>
</t>
</template>
<template id="mo_overview_content">
<t t-set="currency" t-value="data['summary']['currency']"/>
<div class="container bg-view">
<h2><t t-out="data['name']"/> - Overview</h2>
<table name="overview" class="table table-borderless">
<thead>
<tr>
<th/>
<th t-if="data['show_replenishments']" class="text-center">Status</th>
<th t-attf-class="{{ 'text-center' if data['show_uom'] else 'text-end' }}" t-attf-colspan="{{ 2 if data['show_uom'] else 1 }}">Quantity</th>
<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_bom_costs']" class="text-end">BoM Cost</th>
<th t-if="data['show_real_costs']" class="text-end">Real Cost</th>
</tr>
</thead>
<tbody>
<t t-call="mrp.mo_overview_line">
<t t-set="line" t-value="data['summary']"/>
</t>
<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>
<t t-if="data['show_mo_costs'] or data['show_real_costs']">
<tr name="unit_cost" class="border-top border-dark">
<td class="text-end" t-att-colspan="data['footer_colspan']">Unit Cost</td>
<td t-if="data['show_mo_costs']" t-attf-class="text-end" t-out="data['extras']['unit_mo_cost']" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
<td t-if="data['show_bom_costs']" class="text-end" t-out="data['extras']['unit_bom_cost']" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
<td t-if="data['show_real_costs']" t-attf-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" 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_bom_costs']">
<t t-out="data['extras'].get('total_bom_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_bom_costs']">
<t t-out="data['extras'].get('unit_bom_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_bom_costs']">
<t t-out="data['extras'].get('total_bom_cost_operations')" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
</td>
<td class="text-end" t-if="data['show_real_costs']"> <!-- AJOC TODO add decorator here? more below -->
<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_bom_costs']">
<t t-out="data['extras'].get('unit_bom_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" 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_bom_costs']">
<t t-out="data['extras'].get('total_bom_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>
<template id="mo_overview_line">
<tr>
<td class="text-start">
<div t-attf-style="margin-left: {{ line['level'] * 20 }}px" class="d-inline-block" t-out="line['name']"/>
</td>
<td t-if="data['show_replenishments']" class="text-center" t-out="line.get('formatted_state', '')"/>
<td t-attf-class="text-end {{ data['get_color'](line.get('quantity_decorator')) }}">
<t t-if="line.get('model') == 'mrp.workorder'" t-out="line['quantity']" t-options="{'widget': 'float_time'}"/>
<t t-else="" t-out="line['quantity']" t-options="{'widget': 'float', 'precision': line['uom_precision']}"/>
</td>
<td t-if="data['show_uom']" class="text-start" t-out="line['uom_name']"/>
<td t-if="data['show_availabilities']" class="text-end">
<t t-if="line.get('quantity_on_hand', False) is not False">
<t t-out="line['quantity_free']" t-options="{'widget': 'float', 'precision': line['uom_precision']}"/> /
<t t-out="line['quantity_on_hand']" t-options="{'widget': 'float', 'precision': line['uom_precision']}"/>
</t>
</td>
<td t-if="data['show_availabilities']" class="text-end">
<t t-if="line.get('quantity_reserved', False) is not False" t-out="line['quantity_reserved']" t-options="{'widget': 'float', 'precision': line['uom_precision']}"/>
</td>
<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>
<td t-if="data['show_bom_costs']" class="text-end">
<t t-out="line['bom_cost']" t-options="{'widget': 'monetary', 'display_currency': line['currency']}"/>
</td>
<td t-if="data['show_real_costs']" class="text-end">
<t t-out="line['real_cost']" t-options="{'widget': 'monetary', 'display_currency': line['currency']}"/>
</td>
</tr>
</template>
<template id="mo_overview_components">
<t t-foreach="components" t-as="component">
<t t-call="mrp.mo_overview_line">
<t t-set="line" t-value="component['summary']"/>
</t>
<t t-if="component['summary']['index'] in data['unfolded_ids'] and component.get('replenishments', [])" t-foreach="component['replenishments']" t-as="replenishment">
<t t-call="mrp.mo_overview_line">
<t t-set="line" t-value="replenishment['summary']"/>
</t>
<t t-if="replenishment['summary']['index'] in data['unfolded_ids'] and (replenishment.get('components', []) or replenishment.get('operations', {}).get('details', []))">
<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>
</t>
<t t-call="mrp.mo_overview_operations">
<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">
<t t-if="operations">
<tr>
<td class="text-start">
<span t-attf-style="margin-left: {{ (operations[0]['level'] - 1 if len(operations) else 0) * 20 }}px"/>
Operations
</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-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>
<td t-if="data['show_bom_costs']" class="text-end">
<t t-out="summary['bom_cost']" t-options="{'widget': 'monetary', 'display_currency': summary['currency']}"/>
</td>
<td t-if="data['show_real_costs']" class="text-end">
<t t-out="summary['real_cost']" t-options="{'widget': 'monetary', 'display_currency': summary['currency']}"/>
</td>
</tr>
</t>
<t t-if="summary['index'] in data['unfolded_ids']" t-foreach="operations" t-as="operation">
<t t-call="mrp.mo_overview_line">
<t t-set="line" t-value="operation"/>
</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_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>
<td t-if="data['show_bom_costs']" class="text-end">
<t t-out="summary['bom_cost']" t-options="{'widget': 'monetary', 'display_currency': summary['currency']}"/>
</td>
<td t-if="data['show_real_costs']" class="text-end">
<t t-out="summary['real_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>

View file

@ -21,6 +21,15 @@
<field name="binding_model_id" ref="model_mrp_bom"/>
<field name="binding_type">report</field>
</record>
<record id="action_report_mrp_mo_overview" model="ir.actions.report">
<field name="name">MO Overview</field>
<field name="model">mrp.production</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">mrp.report_mo_overview</field>
<field name="report_file">mrp.report_mo_overview</field>
<field name="print_report_name">'MO Overview - %s' % object.display_name</field>
<field name="binding_type">report</field>
</record>
<record id="label_manufacture_template" model="ir.actions.report">
<field name="name">Finished Product Label (ZPL)</field>
<field name="model">mrp.production</field>

View file

@ -8,11 +8,11 @@
<div class="oe_structure"/>
<div class="row">
<div class="col-7">
<h2><span t-field="o.name"/></h2>
<h2><span t-field="o.name">Laptop model X</span></h2>
</div>
<div class="col-5">
<span class="text-end">
<div t-field="o.name" t-options="{'widget': 'barcode', 'width': 600, 'height': 100, 'img_style': 'width:350px;height:60px'}"/>
<span t-field="o.production_id.name" t-options="{'widget': 'barcode', 'width': 600, 'height': 100, 'img_style': 'width:350px;height:60px'}">12345678901</span>
</span>
</div>
</div>
@ -20,23 +20,23 @@
<div class="row mt32 mb32">
<div class="col-3">
<strong>Responsible:</strong><br/>
<span t-field="o.production_id.user_id"/>
<span t-field="o.production_id.user_id">Marc Demo</span>
</div>
<div class="col-3">
<strong>Manufacturing Order:</strong><br/>
<span t-field="o.production_id.name"/>
<span t-field="o.production_id.name">RAM</span>
</div>
</div>
<br/>
<div class="row mt32 mb32">
<div class="col-3">
<strong>Finished Product:</strong><br/>
<span t-field="o.product_id"/>
<span t-field="o.product_id">Laptop</span>
</div>
<div class="col-3">
<strong>Quantity to Produce:</strong><br/>
<span t-field="o.qty_production"/>
<span t-field="o.product_uom_id.name" groups="uom.group_uom"/>
<span t-field="o.qty_production">2</span>
<span t-field="o.product_uom_id.name" groups="uom.group_uom">Unit</span>
</div>
</div>
<br/>

View file

@ -2,12 +2,12 @@
<odoo>
<data>
<template id="label_production_view">
<t t-set="uom_categ_unit" t-value="env.ref('uom.product_uom_categ_unit')"/>
<t t-set="uom_unit" t-value="env.ref('uom.product_uom_unit')"/>
<t t-foreach="docs" t-as="production">
<t t-foreach="production.move_finished_ids" t-as="move">
<t t-foreach="move.move_line_ids" t-as="move_line">
<t t-if="move_line.product_uom_id.category_id == uom_categ_unit">
<t t-set="qty" t-value="int(move_line.qty_done)"/>
<t t-if="move_line.product_id.uom_id._has_common_reference(uom_unit)">
<t t-set="qty" t-value="int(move_line.quantity)"/>
</t>
<t t-else="">
<t t-set="qty" t-value="1"/>

View file

@ -19,7 +19,7 @@
</xpath>
<xpath expr="//t[@name='has_packages']" position="after">
<!-- Additional use case: group by kits when no packages exist and then apply use case 1. (serial/lot numbers used/printed) -->
<t t-elif="has_kits and not has_packages">
<t t-elif="has_kits and not packages_count">
<t t-call="mrp.stock_report_delivery_kit_sections"/>
<t t-call="mrp.stock_report_delivery_no_kit_section"/>
</t>
@ -28,7 +28,7 @@
<template id="stock_report_delivery_kit_sections">
<!-- get all kits-related SML, including subkits -->
<t t-set="all_kits_move_lines" t-value="o.move_line_ids.filtered(lambda l: l.move_id.bom_line_id.bom_id.type == 'phantom')"/>
<t t-set="all_kits_move_lines" t-value="o.move_ids.move_line_ids.filtered(lambda l: l.move_id.bom_line_id.bom_id.type == 'phantom')"/>
<!-- do another map to get unique top level kits -->
<t t-set="boms" t-value="has_kits.mapped('move_id.bom_line_id.bom_id')"/>
<t t-foreach="boms" t-as="bom">
@ -39,7 +39,7 @@
<t t-else="">
<t t-set="kit_product" t-value="bom.product_tmpl_id"/>
</t>
<tr t-att-class="'bg-200 fw-bold o_line_section'">
<tr t-att-class="'fw-bold o_line_section'">
<td colspan="99">
<span t-esc="kit_product.display_name"/>
</td>
@ -66,9 +66,9 @@
<!-- No kit section is expected to only be called in no packages case -->
<template id="stock_report_delivery_no_kit_section">
<!-- Do another section for kit-less products if they exist -->
<t t-set="no_kit_move_lines" t-value="o.move_line_ids.filtered(lambda l: l.move_id.bom_line_id.bom_id.type != 'phantom')"/>
<t t-set="no_kit_move_lines" t-value="o.move_ids.move_line_ids.filtered(lambda l: l.move_id.bom_line_id.bom_id.type != 'phantom')"/>
<t t-if="no_kit_move_lines">
<tr t-att-class="'bg-200 fw-bold o_line_section'">
<tr t-att-class="'fw-bold o_line_section'">
<td colspan="99">
<span>Products not associated with a kit</span>
</td>

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="mrp_report_product_product_replenishment" inherit_id="stock.report_product_product_replenishment">
</template>
</odoo>

View file

@ -5,7 +5,7 @@ from odoo import models
from odoo.tools import format_date
class ReceptionReport(models.AbstractModel):
class ReportStockReport_Reception(models.AbstractModel):
_inherit = 'report.stock.report_reception'
def _get_docs(self, docids):
@ -23,7 +23,7 @@ class ReceptionReport(models.AbstractModel):
def _get_moves(self, docs):
if self.env.context.get('default_production_ids'):
return docs.move_finished_ids.filtered(lambda m: m.product_id.type == 'product' and m.state != 'cancel')
return docs.move_finished_ids.filtered(lambda m: m.product_id.is_storable and m.state != 'cancel')
return super()._get_moves(docs)
def _get_extra_domain(self, docs):
@ -33,15 +33,10 @@ class ReceptionReport(models.AbstractModel):
def _get_formatted_scheduled_date(self, source):
if source._name == 'mrp.production':
return format_date(self.env, source.date_planned_start)
return format_date(self.env, source.date_start)
return super()._get_formatted_scheduled_date(source)
def _action_assign(self, in_move, out_move):
if in_move.production_id:
in_move.production_id.move_dest_ids |= out_move
if not out_move.group_id and out_move._get_source_document() not in [False, out_move.picking_id]:
out_move.group_id = out_move._get_source_document()
def _action_unassign(self, in_move, out_move):
super()._action_unassign(in_move, out_move)
if in_move.production_id:
in_move.production_id.move_dest_ids -= out_move

View file

@ -4,6 +4,5 @@
<field name="name">MRP Reception Report</field>
<field name="tag">reception_report</field>
<field name="res_model">report.stock.report_reception</field>
<field name="context">{'default_production_ids': active_ids}</field>
</record>
</odoo>

View file

@ -4,14 +4,14 @@
from odoo import api, models
class ReportStockRule(models.AbstractModel):
class ReportStockReport_Stock_Rule(models.AbstractModel):
_inherit = 'report.stock.report_stock_rule'
@api.model
def _get_rule_loc(self, rule, product_id):
""" We override this method to handle manufacture rule which do not have a location_src_id.
"""
res = super(ReportStockRule, self)._get_rule_loc(rule, product_id)
res = super()._get_rule_loc(rule, product_id)
if rule.action == 'manufacture':
res['source'] = product_id.property_stock_production
return res

View file

@ -1,47 +1,53 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from odoo import api, models
class ReplenishmentReport(models.AbstractModel):
_inherit = 'report.stock.report_product_product_replenishment'
class StockForecasted_Product_Product(models.AbstractModel):
_inherit = 'stock.forecasted_product_product'
def _prepare_report_line(self, quantity, move_out=None, move_in=None, replenishment_filled=True, product=False, reserved_move=False, in_transit=False, read=True):
line = super()._prepare_report_line(quantity, move_out, move_in, replenishment_filled, product, reserved_move, in_transit, read)
def _serialize_docs(self, docs, product_template_ids=False, product_variant_ids=False):
res = super()._serialize_docs(docs, product_template_ids, product_variant_ids)
for i, line in enumerate(docs['lines']):
if not line['move_out'] or not line['move_out']['raw_material_production_id']:
continue
raw_material_production = line['move_out']['raw_material_production_id']
res['lines'][i]['move_out']['raw_material_production_id'] = raw_material_production.read(fields=['id', 'unreserve_visible', 'reserve_visible', 'priority'])[0]
return res
if not move_out or not move_out.raw_material_production_id or not read:
return line
def _move_draft_domain(self, product_template_ids, product_variant_ids, wh_location_ids):
in_domain, out_domain = super()._move_draft_domain(product_template_ids, product_variant_ids, wh_location_ids)
line['move_out']['raw_material_production_id'] = move_out.raw_material_production_id.read(fields=['id', 'unreserve_visible', 'reserve_visible', 'priority'])[0]
return line
def _move_draft_domain(self, product_template_ids, product_ids, wh_location_ids):
in_domain, out_domain = super()._move_draft_domain(product_template_ids, product_ids, wh_location_ids)
in_domain += [('production_id', '=', False)]
out_domain += [('raw_material_production_id', '=', False)]
return in_domain, out_domain
def _compute_draft_quantity_count(self, product_template_ids, product_variant_ids, wh_location_ids):
res = super()._compute_draft_quantity_count(product_template_ids, product_variant_ids, wh_location_ids)
res['draft_production_qty'] = {}
domain = self._product_domain(product_template_ids, product_variant_ids)
def _get_report_header(self, product_template_ids, product_ids, wh_location_ids):
res = super()._get_report_header(product_template_ids, product_ids, wh_location_ids)
domain = self._product_domain(product_template_ids, product_ids)
domain += [('state', '=', 'draft')]
# Pending incoming quantity.
mo_domain = domain + [('location_dest_id', 'in', wh_location_ids)]
grouped_mo = self.env['mrp.production'].read_group(mo_domain, ['product_qty:sum'], 'product_id')
res['draft_production_qty']['in'] = sum(mo['product_qty'] for mo in grouped_mo)
# Pending outgoing quantity.
move_domain = domain + [
in_domain = domain + [('location_dest_id', 'in', wh_location_ids)] # Pending incoming quantity.
out_domain = domain + [ # Pending outgoing quantity.
('raw_material_production_id', '!=', False),
('location_id', 'in', wh_location_ids),
]
grouped_moves = self.env['stock.move'].read_group(move_domain, ['product_qty:sum'], 'product_id')
res['draft_production_qty']['out'] = sum(move['product_qty'] for move in grouped_moves)
res['qty']['in'] += res['draft_production_qty']['in']
res['qty']['out'] += res['draft_production_qty']['out']
in_product_qty = {k.id: v for k, v in self.env['mrp.production']._read_group(in_domain, aggregates=['product_qty:sum'], groupby=['product_id'])}
out_product_qty = {k.id: v for k, v in self.env['stock.move']._read_group(out_domain, aggregates=['product_qty:sum'], groupby=['product_id'])}
self._add_product_quantities(res, product_template_ids, product_ids, 'draft_production_qty', in_product_qty, out_product_qty)
return res
def _get_reservation_data(self, move):
if move.production_id:
m2o = 'production_id'
elif move.raw_material_production_id:
m2o = 'raw_material_production_id'
else:
return super()._get_reservation_data(move)
return {
'_name': move[m2o]._name,
'name': move[m2o].name,
'id': move[m2o].id
}