mirror of
https://github.com/bringout/oca-ocb-mrp.git
synced 2026-04-26 16:32:04 +02:00
19.0 vanilla
This commit is contained in:
parent
accf5918df
commit
6e65e8c877
688 changed files with 225434 additions and 199401 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue