[REF] mrp: change BoM Structure&Cost into Overview
Changes "Structure & Cost" report to "Overview", as the goal for this report is improved. The point is to have on a single report all components and sub-assemblies that make it up, in parallel with the route and lead times associated to each. This way it becomes easier to identify issues, such as: - Missing routes. - Longer lead times on a specific sub-assembly. - Unavailable component in the foreseable future. - ... Also did the conversion to OWL, changing drastically the structure of the report. Task-2628323 Part-of: odoo/odoo#93194
This commit is contained in:
@@ -58,26 +58,14 @@
|
||||
'uninstall_hook': 'uninstall_hook',
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'mrp/static/src/scss/mrp_workorder_kanban.scss',
|
||||
'mrp/static/src/scss/mrp_fields.scss',
|
||||
'mrp/static/src/scss/mrp_gantt.scss',
|
||||
'mrp/static/src/scss/mrp_document_kanban_view.scss',
|
||||
'mrp/static/src/js/mrp.js',
|
||||
'mrp/static/src/js/mrp_bom_report.js',
|
||||
'mrp/static/src/js/mrp_workorder_popover.js',
|
||||
'mrp/static/src/js/mrp_documents_controller_mixin.js',
|
||||
'mrp/static/src/js/mrp_documents_document_viewer.js',
|
||||
'mrp/static/src/js/mrp_documents_kanban_controller.js',
|
||||
'mrp/static/src/js/mrp_documents_kanban_record.js',
|
||||
'mrp/static/src/js/mrp_documents_kanban_renderer.js',
|
||||
'mrp/static/src/js/mrp_document_kanban_view.js',
|
||||
'mrp/static/src/js/mrp_should_consume.js',
|
||||
'mrp/static/src/**/*.js',
|
||||
'mrp/static/src/**/*.scss',
|
||||
],
|
||||
'web.qunit_suite_tests': [
|
||||
'mrp/static/tests/**/*',
|
||||
],
|
||||
'web.assets_qweb': [
|
||||
'mrp/static/src/xml/*.xml',
|
||||
'mrp/static/src/**/*.xml',
|
||||
],
|
||||
},
|
||||
'license': 'LGPL-3',
|
||||
|
||||
@@ -2,12 +2,51 @@
|
||||
|
||||
import json
|
||||
|
||||
from odoo import api, models, _
|
||||
from odoo.tools import float_round
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.tools import float_compare, float_round, format_date
|
||||
from datetime import timedelta
|
||||
|
||||
class ReportBomStructure(models.AbstractModel):
|
||||
_name = 'report.mrp.report_bom_structure'
|
||||
_description = 'BOM Structure Report'
|
||||
_description = 'BOM Overview Report'
|
||||
|
||||
@api.model
|
||||
def get_html(self, bom_id=False, searchQty=1, searchVariant=False):
|
||||
res = self._get_report_data(bom_id=bom_id, searchQty=searchQty, searchVariant=searchVariant)
|
||||
res['has_attachments'] = self._has_attachments(res['lines'])
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def get_warehouses(self):
|
||||
return self.env['stock.warehouse'].search_read([('company_id', '=', self.env.company.id)], fields=['id', 'name'])
|
||||
|
||||
@api.model
|
||||
def _compute_current_production_capacity(self, bom_data):
|
||||
# Get the maximum amount producible product of the selected bom given each component's stock levels.
|
||||
stockable_components = filter(lambda c: c['product'].detailed_type == 'product', bom_data.get('components', []))
|
||||
producibles = [float_round(comp['quantity_available'] / comp['base_bom_line_qty'], precision_digits=0, rounding_method='DOWN') for comp in stockable_components]
|
||||
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):
|
||||
@@ -15,24 +54,14 @@ class ReportBomStructure(models.AbstractModel):
|
||||
for bom_id in docids:
|
||||
bom = self.env['mrp.bom'].browse(bom_id)
|
||||
variant = data.get('variant')
|
||||
candidates = variant and self.env['product.product'].browse(variant) or bom.product_id or bom.product_tmpl_id.product_variant_ids
|
||||
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')))
|
||||
for product_variant_id in candidates.ids:
|
||||
if data and data.get('unfolded_ids'):
|
||||
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['report_type'] = 'pdf'
|
||||
doc['report_structure'] = data and data.get('report_type') or 'all'
|
||||
docs.append(doc)
|
||||
docs.append(self._get_pdf_doc(bom_id, data, quantity, product_variant_id))
|
||||
if not candidates:
|
||||
if data and data.get('unfolded_ids'):
|
||||
doc = self._get_pdf_line(bom_id, qty=quantity, unfolded_ids=set(json.loads(data.get('unfolded_ids'))))
|
||||
else:
|
||||
doc = self._get_pdf_line(bom_id, qty=quantity, unfolded=True)
|
||||
doc['report_type'] = 'pdf'
|
||||
doc['report_structure'] = data and data.get('report_type') or 'all'
|
||||
docs.append(doc)
|
||||
docs.append(self._get_pdf_doc(bom_id, data, quantity))
|
||||
return {
|
||||
'doc_ids': docids,
|
||||
'doc_model': 'mrp.bom',
|
||||
@@ -40,43 +69,16 @@ class ReportBomStructure(models.AbstractModel):
|
||||
}
|
||||
|
||||
@api.model
|
||||
def get_html(self, bom_id=False, searchQty=1, searchVariant=False):
|
||||
res = self._get_report_data(bom_id=bom_id, searchQty=searchQty, searchVariant=searchVariant)
|
||||
res['lines']['report_type'] = 'html'
|
||||
res['lines']['report_structure'] = 'all'
|
||||
res['lines']['has_attachments'] = res['lines']['attachments'] or any(component['attachments'] for component in res['lines']['components'])
|
||||
res['lines'] = self.env['ir.qweb']._render('mrp.report_mrp_bom', {'data': res['lines']})
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def get_bom(self, bom_id=False, product_id=False, line_qty=False, line_id=False, level=False):
|
||||
lines = self._get_bom(bom_id=bom_id, product_id=product_id, line_qty=line_qty, line_id=line_id, level=level)
|
||||
return self.env['ir.qweb']._render('mrp.report_mrp_bom_line', {'data': lines})
|
||||
|
||||
@api.model
|
||||
def get_operations(self, product_id=False, bom_id=False, qty=0, level=0):
|
||||
bom = self.env['mrp.bom'].browse(bom_id)
|
||||
product = self.env['product.product'].browse(product_id)
|
||||
lines = self._get_operation_line(product, bom, float_round(qty / bom.product_qty, precision_rounding=1, rounding_method='UP'), level)
|
||||
values = {
|
||||
'bom_id': bom_id,
|
||||
'currency': self.env.company.currency_id,
|
||||
'operations': lines,
|
||||
'extra_column_count': self._get_extra_column_count()
|
||||
}
|
||||
return self.env['ir.qweb']._render('mrp.report_mrp_operation_line', {'data': values})
|
||||
|
||||
@api.model
|
||||
def get_byproducts(self, bom_id=False, qty=0, level=0, total=0):
|
||||
bom = self.env['mrp.bom'].browse(bom_id)
|
||||
lines, dummy = self._get_byproducts_lines(bom, qty, level, total)
|
||||
values = {
|
||||
'bom_id': bom_id,
|
||||
'currency': self.env.company.currency_id,
|
||||
'byproducts': lines,
|
||||
'extra_column_count': self._get_extra_column_count(),
|
||||
}
|
||||
return self.env['ir.qweb']._render('mrp.report_mrp_byproduct_line', {'data': values})
|
||||
def _get_pdf_doc(self, bom_id, data, quantity, product_variant_id=None):
|
||||
if data and data.get('unfolded_ids'):
|
||||
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
|
||||
return doc
|
||||
|
||||
@api.model
|
||||
def _get_report_data(self, bom_id, searchQty=0, searchVariant=False):
|
||||
@@ -86,6 +88,11 @@ class ReportBomStructure(models.AbstractModel):
|
||||
bom_product_variants = {}
|
||||
bom_uom_name = ''
|
||||
|
||||
if searchVariant:
|
||||
product = self.env['product.product'].browse(int(searchVariant))
|
||||
else:
|
||||
product = bom.product_id or bom.product_tmpl_id.product_variant_id
|
||||
|
||||
if bom:
|
||||
bom_uom_name = bom.product_uom_id.name
|
||||
|
||||
@@ -94,7 +101,14 @@ class ReportBomStructure(models.AbstractModel):
|
||||
for variant in bom.product_tmpl_id.product_variant_ids:
|
||||
bom_product_variants[variant.id] = variant.display_name
|
||||
|
||||
lines = self._get_bom(bom_id, product_id=searchVariant, line_qty=bom_quantity, level=1)
|
||||
if self.env.context.get('warehouse'):
|
||||
warehouse = self.env['stock.warehouse'].browse(self.env.context.get('warehouse'))
|
||||
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,
|
||||
@@ -102,121 +116,232 @@ class ReportBomStructure(models.AbstractModel):
|
||||
'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'),
|
||||
'extra_column_count': self._get_extra_column_count()
|
||||
}
|
||||
|
||||
def _get_bom(self, bom_id=False, product_id=False, line_qty=False, line_id=False, level=False):
|
||||
bom = self.env['mrp.bom'].browse(bom_id)
|
||||
company = bom.company_id or self.env.company
|
||||
bom_quantity = line_qty
|
||||
if line_id:
|
||||
current_line = self.env['mrp.bom.line'].browse(int(line_id))
|
||||
bom_quantity = current_line.product_uom_id._compute_quantity(line_qty, bom.product_uom_id) or 0
|
||||
# Display bom components for current selected product variant
|
||||
if product_id:
|
||||
product = self.env['product.product'].browse(int(product_id))
|
||||
else:
|
||||
@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):
|
||||
""" 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.
|
||||
- 'from_date': Gives a single value for 'today' across the functions, as well as using this date in products quantity computes.
|
||||
"""
|
||||
is_minimized = self.env.context.get('minimized', False)
|
||||
if not product:
|
||||
product = bom.product_id or bom.product_tmpl_id.product_variant_id
|
||||
if product:
|
||||
price = product.uom_id._compute_price(product.with_company(company).standard_price, bom.product_uom_id) * bom_quantity
|
||||
attachments = self.env['mrp.document'].search(['|', '&', ('res_model', '=', 'product.product'),
|
||||
('res_id', '=', product.id), '&', ('res_model', '=', 'product.template'), ('res_id', '=', product.product_tmpl_id.id)])
|
||||
else:
|
||||
# Use the product template instead of the variant
|
||||
price = bom.product_tmpl_id.uom_id._compute_price(bom.product_tmpl_id.with_company(company).standard_price, bom.product_uom_id) * bom_quantity
|
||||
attachments = self.env['mrp.document'].search([('res_model', '=', 'product.template'), ('res_id', '=', bom.product_tmpl_id.id)])
|
||||
operations = self._get_operation_line(product, bom, float_round(bom_quantity, precision_rounding=1, rounding_method='UP'), 0)
|
||||
lines = {
|
||||
'bom': bom,
|
||||
'bom_qty': bom_quantity,
|
||||
'bom_prod_name': product.display_name,
|
||||
'currency': company.currency_id,
|
||||
'product': product,
|
||||
'code': bom and bom.display_name or '',
|
||||
'price': price,
|
||||
'total': sum([op['total'] for op in operations]),
|
||||
'level': level or 0,
|
||||
'operations': operations,
|
||||
'operations_cost': sum([op['total'] for op in operations]),
|
||||
'attachments': attachments,
|
||||
'operations_time': sum([op['duration_expected'] for op in operations])
|
||||
}
|
||||
components, total = self._get_bom_lines(bom, bom_quantity, product, line_id, level)
|
||||
lines['total'] += total
|
||||
lines['components'] = components
|
||||
byproducts, byproduct_cost_portion = self._get_byproducts_lines(bom, bom_quantity, level, lines['total'])
|
||||
lines['byproducts'] = byproducts
|
||||
lines['cost_share'] = float_round(1 - byproduct_cost_portion, precision_rounding=0.0001)
|
||||
lines['bom_cost'] = lines['total'] * lines['cost_share']
|
||||
lines['byproducts_cost'] = sum(byproduct['bom_cost'] for byproduct in byproducts)
|
||||
lines['byproducts_total'] = sum(byproduct['product_qty'] for byproduct in byproducts)
|
||||
lines['extra_column_count'] = self._get_extra_column_count()
|
||||
return lines
|
||||
if not line_qty:
|
||||
line_qty = bom.product_qty
|
||||
|
||||
if not product_info:
|
||||
product_info = {}
|
||||
key = product.id
|
||||
if key not in product_info:
|
||||
product_info[key] = {'consumptions': {'in_stock': 0}}
|
||||
|
||||
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 = []
|
||||
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'),
|
||||
('res_id', '=', product.id), '&', ('res_model', '=', 'product.template'),
|
||||
('res_id', '=', product.product_tmpl_id.id)]).ids
|
||||
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
|
||||
|
||||
bom_key = bom.id
|
||||
if not product_info[key].get(bom_key):
|
||||
product_info[key][bom_key] = self._get_resupply_route_info(warehouse, product, current_quantity, bom)
|
||||
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)
|
||||
|
||||
bom_report_line = {
|
||||
'index': index,
|
||||
'bom': bom,
|
||||
'bom_id': bom and bom.id or False,
|
||||
'bom_code': bom and bom.code or False,
|
||||
'type': 'bom',
|
||||
'quantity': current_quantity,
|
||||
'quantity_available': quantities_info.get('free_qty', 0),
|
||||
'quantity_on_hand': quantities_info.get('on_hand_qty', 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,
|
||||
'uom': bom.product_uom_id if bom else product.uom_id,
|
||||
'uom_name': bom.product_uom_id.name if bom else product.uom_id.name,
|
||||
'route_type': route_info.get('route_type', ''),
|
||||
'route_name': route_info.get('route_name', ''),
|
||||
'route_detail': route_info.get('route_detail', ''),
|
||||
'lead_time': route_info.get('lead_time', False),
|
||||
'currency': company.currency_id,
|
||||
'currency_id': company.currency_id.id,
|
||||
'product': product,
|
||||
'product_id': product.id,
|
||||
'link_id': product.id if product.product_variant_count > 1 else product.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,
|
||||
'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']
|
||||
|
||||
def _get_bom_lines(self, bom, bom_quantity, product, line_id, level):
|
||||
components = []
|
||||
total = 0
|
||||
for line in bom.bom_line_ids:
|
||||
line_quantity = (bom_quantity / (bom.product_qty or 1.0)) * line.product_qty
|
||||
for component_index, line in enumerate(bom.bom_line_ids):
|
||||
new_index = f"{index}{component_index}"
|
||||
if line._skip_bom_line(product):
|
||||
continue
|
||||
company = bom.company_id or self.env.company
|
||||
price = line.product_id.uom_id._compute_price(line.product_id.with_company(company).standard_price, line.product_uom_id) * line_quantity
|
||||
line_quantity = (current_quantity / (bom.product_qty or 1.0)) * line.product_qty
|
||||
if line.child_bom_id:
|
||||
factor = line.product_uom_id._compute_quantity(line_quantity, line.child_bom_id.product_uom_id)
|
||||
sub_total = self._get_price(line.child_bom_id, factor, line.product_id)
|
||||
byproduct_cost_share = sum(line.child_bom_id.byproduct_ids.mapped('cost_share'))
|
||||
if byproduct_cost_share:
|
||||
sub_total *= float_round(1 - byproduct_cost_share / 100, precision_rounding=0.0001)
|
||||
component = self._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)
|
||||
else:
|
||||
sub_total = price
|
||||
sub_total = self.env.company.currency_id.round(sub_total)
|
||||
components.append({
|
||||
'prod_id': line.product_id.id,
|
||||
'prod_name': line.product_id.display_name,
|
||||
'code': line.child_bom_id and line.child_bom_id.display_name or '',
|
||||
'prod_qty': line_quantity,
|
||||
'prod_uom': line.product_uom_id.name,
|
||||
'prod_cost': company.currency_id.round(price),
|
||||
'parent_id': bom.id,
|
||||
'line_id': line.id,
|
||||
'level': level or 0,
|
||||
'total': sub_total,
|
||||
'child_bom': line.child_bom_id.id,
|
||||
'phantom_bom': line.child_bom_id and line.child_bom_id.type == 'phantom' or False,
|
||||
'attachments': self.env['mrp.document'].search(['|', '&',
|
||||
('res_model', '=', 'product.product'), ('res_id', '=', line.product_id.id), '&', ('res_model', '=', 'product.template'), ('res_id', '=', line.product_id.product_tmpl_id.id)]),
|
||||
component = self._get_component_data(bom, warehouse, line, line_quantity, level + 1, new_index, product_info, ignore_stock)
|
||||
components.append(component)
|
||||
bom_report_line['bom_cost'] += component['bom_cost']
|
||||
bom_report_line['components'] = components
|
||||
bom_report_line['producible_qty'] = self._compute_current_production_capacity(bom_report_line)
|
||||
|
||||
})
|
||||
total += sub_total
|
||||
return components, total
|
||||
if not is_minimized:
|
||||
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)
|
||||
bom_report_line['byproducts_cost'] = sum(byproduct['bom_cost'] for byproduct in byproducts)
|
||||
bom_report_line['byproducts_total'] = sum(byproduct['quantity'] for byproduct in byproducts)
|
||||
bom_report_line['bom_cost'] *= bom_report_line['cost_share']
|
||||
|
||||
def _get_byproducts_lines(self, bom, bom_quantity, level, total):
|
||||
availabilities = self._get_availabilities(product, current_quantity, product_info, bom_key, quantities_info, level, ignore_stock, components)
|
||||
bom_report_line.update(availabilities)
|
||||
|
||||
if level == 0:
|
||||
# Gives a unique key for the first line that indicates if product is ready for production right now.
|
||||
bom_report_line['components_available'] = all([c['stock_avail_state'] == 'available' for c in components])
|
||||
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):
|
||||
company = parent_bom.company_id or self.env.company
|
||||
key = bom_line.product_id.id
|
||||
if key not in product_info:
|
||||
product_info[key] = {'consumptions': {'in_stock': 0}}
|
||||
|
||||
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)
|
||||
|
||||
bom_key = 'no_bom'
|
||||
if not product_info[key].get(bom_key):
|
||||
product_info[key][bom_key] = self._get_resupply_route_info(warehouse, bom_line.product_id, line_quantity)
|
||||
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(bom_line.product_id, bom_line.product_uom_id, parent_bom, product_info)
|
||||
availabilities = self._get_availabilities(bom_line.product_id, line_quantity, product_info, bom_key, quantities_info, level, ignore_stock)
|
||||
|
||||
attachment_ids = []
|
||||
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
|
||||
|
||||
return {
|
||||
'type': 'component',
|
||||
'index': index,
|
||||
'bom_id': False,
|
||||
'product': bom_line.product_id,
|
||||
'product_id': bom_line.product_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,
|
||||
'quantity': line_quantity,
|
||||
'quantity_available': quantities_info.get('free_qty', 0),
|
||||
'quantity_on_hand': quantities_info.get('on_hand_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', ''),
|
||||
'lead_time': route_info.get('lead_time', False),
|
||||
'stock_avail_state': availabilities['stock_avail_state'],
|
||||
'resupply_avail_delay': availabilities['resupply_avail_delay'],
|
||||
'availability_display': availabilities['availability_display'],
|
||||
'availability_state': availabilities['availability_state'],
|
||||
'availability_delay': availabilities['availability_delay'],
|
||||
'parent_id': parent_bom.id,
|
||||
'level': level or 0,
|
||||
'attachment_ids': attachment_ids,
|
||||
}
|
||||
|
||||
@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,
|
||||
'stock_loc': 'in_stock',
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _get_byproducts_lines(self, product, bom, bom_quantity, level, total, index):
|
||||
byproducts = []
|
||||
byproduct_cost_portion = 0
|
||||
company = bom.company_id or self.env.company
|
||||
byproduct_index = 0
|
||||
for byproduct in bom.byproduct_ids:
|
||||
if byproduct._skip_byproduct_line(product):
|
||||
continue
|
||||
line_quantity = (bom_quantity / (bom.product_qty or 1.0)) * byproduct.product_qty
|
||||
cost_share = byproduct.cost_share / 100
|
||||
byproduct_cost_portion += cost_share
|
||||
price = byproduct.product_id.uom_id._compute_price(byproduct.product_id.with_company(company).standard_price, byproduct.product_uom_id) * line_quantity
|
||||
byproducts.append({
|
||||
'product_id': byproduct.product_id,
|
||||
'product_name': byproduct.product_id.display_name,
|
||||
'product_qty': line_quantity,
|
||||
'product_uom': byproduct.product_uom_id.name,
|
||||
'product_cost': company.currency_id.round(price),
|
||||
'id': byproduct.id,
|
||||
'index': f"{index}{byproduct_index}",
|
||||
'type': 'byproduct',
|
||||
'link_id': byproduct.product_id.id if byproduct.product_id.product_variant_count > 1 else byproduct.product_id.product_tmpl_id.id,
|
||||
'link_model': 'product.product' if byproduct.product_id.product_variant_count > 1 else 'product.template',
|
||||
'currency_id': company.currency_id.id,
|
||||
'name': byproduct.product_id.display_name,
|
||||
'quantity': line_quantity,
|
||||
'uom': 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),
|
||||
'cost_share': cost_share,
|
||||
})
|
||||
byproduct_index += 1
|
||||
return byproducts, byproduct_cost_portion
|
||||
|
||||
def _get_operation_line(self, product, bom, qty, level):
|
||||
@api.model
|
||||
def _get_operation_line(self, product, bom, qty, level, index):
|
||||
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
|
||||
operation_index = 0
|
||||
for operation in bom.operation_ids:
|
||||
if operation._skip_operation_line(product):
|
||||
continue
|
||||
@@ -225,64 +350,75 @@ class ReportBomStructure(models.AbstractModel):
|
||||
duration_expected = (operation_cycle * operation.time_cycle * 100.0 / operation.workcenter_id.time_efficiency) + (operation.workcenter_id.time_stop + operation.workcenter_id.time_start)
|
||||
total = ((duration_expected / 60.0) * operation.workcenter_id.costs_hour)
|
||||
operations.append({
|
||||
'type': 'operation',
|
||||
'index': f"{index}{operation_index}",
|
||||
'level': level or 0,
|
||||
'operation': operation,
|
||||
'link_id': operation.id,
|
||||
'link_model': 'mrp.routing.workcenter',
|
||||
'name': operation.name + ' - ' + operation.workcenter_id.name,
|
||||
'duration_expected': duration_expected,
|
||||
'total': self.env.company.currency_id.round(total),
|
||||
'uom': _("Minutes"),
|
||||
'quantity': duration_expected,
|
||||
'bom_cost': self.env.company.currency_id.round(total),
|
||||
'currency_id': company.currency_id.id,
|
||||
'model': 'mrp.routing.workcenter',
|
||||
})
|
||||
operation_index += 1
|
||||
return operations
|
||||
|
||||
def _get_price(self, bom, factor, product):
|
||||
price = 0
|
||||
if bom.operation_ids:
|
||||
# routing are defined on a BoM and don't have a concept of quantity.
|
||||
# It means that the operation time are defined for the quantity on
|
||||
# the BoM (the user produces a batch of products). E.g the user
|
||||
# product a batch of 10 units with a 5 minutes operation, the time
|
||||
# will be the 5 for a quantity between 1-10, then doubled for
|
||||
# 11-20,...
|
||||
operation_cycle = float_round(factor, precision_rounding=1, rounding_method='UP')
|
||||
operations = self._get_operation_line(product, bom, operation_cycle, 0)
|
||||
price += sum([op['total'] for op in operations])
|
||||
@api.model
|
||||
def _get_pdf_line(self, bom_id, product_id=False, qty=1, unfolded_ids=None, unfolded=False):
|
||||
if unfolded_ids is None:
|
||||
unfolded_ids = set()
|
||||
|
||||
for line in bom.bom_line_ids:
|
||||
if line._skip_bom_line(product):
|
||||
continue
|
||||
if line.child_bom_id:
|
||||
qty = line.product_uom_id._compute_quantity(line.product_qty * (factor / bom.product_qty), line.child_bom_id.product_uom_id) / line.child_bom_id.product_qty
|
||||
sub_price = self._get_price(line.child_bom_id, qty, line.product_id)
|
||||
byproduct_cost_share = sum(line.child_bom_id.byproduct_ids.mapped('cost_share'))
|
||||
if byproduct_cost_share:
|
||||
sub_price *= float_round(1 - byproduct_cost_share / 100, precision_rounding=0.0001)
|
||||
price += sub_price
|
||||
else:
|
||||
prod_qty = line.product_qty * factor / bom.product_qty
|
||||
company = bom.company_id or self.env.company
|
||||
not_rounded_price = line.product_id.uom_id._compute_price(line.product_id.with_company(company).standard_price, line.product_uom_id) * prod_qty
|
||||
price += company.currency_id.round(not_rounded_price)
|
||||
return price
|
||||
bom = self.env['mrp.bom'].browse(bom_id)
|
||||
if product_id:
|
||||
product = self.env['product.product'].browse(int(product_id))
|
||||
else:
|
||||
product = bom.product_id or bom.product_tmpl_id.product_variant_id
|
||||
|
||||
def _get_sub_lines(self, bom, product_id, line_qty, line_id, level, unfolded_ids, unfolded):
|
||||
data = self._get_bom(bom_id=bom.id, product_id=product_id, line_qty=line_qty, line_id=line_id, level=level)
|
||||
if self.env.context.get('warehouse'):
|
||||
warehouse = self.env['stock.warehouse'].browse(self.env.context.get('warehouse'))
|
||||
else:
|
||||
warehouse = self.env['stock.warehouse'].browse(self.get_warehouses()[0]['id'])
|
||||
|
||||
level = 1
|
||||
data = self._get_bom_data(bom, warehouse, product=product, line_qty=qty, level=0)
|
||||
pdf_lines = self._get_bom_array_lines(data, level, unfolded_ids, unfolded, True)
|
||||
|
||||
data['lines'] = pdf_lines
|
||||
return data
|
||||
|
||||
@api.model
|
||||
def _get_bom_array_lines(self, data, level, unfolded_ids, unfolded, parent_unfolded=True):
|
||||
bom_lines = data['components']
|
||||
lines = []
|
||||
for bom_line in bom_lines:
|
||||
line_unfolded = ('bom_' + str(bom_line['index'])) in unfolded_ids
|
||||
line_visible = level == 1 or unfolded or parent_unfolded
|
||||
lines.append({
|
||||
'name': bom_line['prod_name'],
|
||||
'type': 'bom',
|
||||
'quantity': bom_line['prod_qty'],
|
||||
'uom': bom_line['prod_uom'],
|
||||
'bom_id': bom_line['bom_id'],
|
||||
'name': bom_line['name'],
|
||||
'type': bom_line['type'],
|
||||
'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['total'],
|
||||
'bom_cost': bom_line['bom_cost'],
|
||||
'route_name': bom_line['route_name'],
|
||||
'route_detail': bom_line['route_detail'],
|
||||
'lead_time': bom_line['lead_time'],
|
||||
'level': bom_line['level'],
|
||||
'code': bom_line['code'],
|
||||
'child_bom': bom_line['child_bom'],
|
||||
'prod_id': bom_line['prod_id']
|
||||
'availability_state': bom_line['availability_state'],
|
||||
'availability_display': bom_line['availability_display'],
|
||||
'visible': line_visible,
|
||||
})
|
||||
if bom_line['child_bom'] and (unfolded or bom_line['child_bom'] in unfolded_ids):
|
||||
line = self.env['mrp.bom.line'].browse(bom_line['line_id'])
|
||||
lines += (self._get_sub_lines(line.child_bom_id, line.product_id.id, bom_line['prod_qty'], line, level + 1, unfolded_ids, unfolded))
|
||||
if bom_line.get('components'):
|
||||
lines += self._get_bom_array_lines(bom_line, level + 1, unfolded_ids, unfolded, line_visible and line_unfolded)
|
||||
|
||||
if data['operations']:
|
||||
lines.append({
|
||||
'name': _('Operations'),
|
||||
@@ -291,17 +427,19 @@ class ReportBomStructure(models.AbstractModel):
|
||||
'uom': _('minutes'),
|
||||
'bom_cost': data['operations_cost'],
|
||||
'level': level,
|
||||
'visible': parent_unfolded,
|
||||
})
|
||||
operations_unfolded = unfolded or (parent_unfolded and ('operations_' + str(data['index'])) in unfolded_ids)
|
||||
for operation in data['operations']:
|
||||
if unfolded or 'operation-' + str(bom.id) in unfolded_ids:
|
||||
lines.append({
|
||||
'name': operation['name'],
|
||||
'type': 'operation',
|
||||
'quantity': operation['duration_expected'],
|
||||
'uom': _('minutes'),
|
||||
'bom_cost': operation['total'],
|
||||
'level': level + 1,
|
||||
})
|
||||
lines.append({
|
||||
'name': operation['name'],
|
||||
'type': 'operation',
|
||||
'quantity': operation['quantity'],
|
||||
'uom': _('minutes'),
|
||||
'bom_cost': operation['bom_cost'],
|
||||
'level': level + 1,
|
||||
'visible': operations_unfolded,
|
||||
})
|
||||
if data['byproducts']:
|
||||
lines.append({
|
||||
'name': _('Byproducts'),
|
||||
@@ -310,32 +448,138 @@ class ReportBomStructure(models.AbstractModel):
|
||||
'quantity': data['byproducts_total'],
|
||||
'bom_cost': data['byproducts_cost'],
|
||||
'level': level,
|
||||
'visible': parent_unfolded,
|
||||
})
|
||||
byproducts_unfolded = unfolded or (parent_unfolded and ('byproducts_' + str(data['index'])) in unfolded_ids)
|
||||
for byproduct in data['byproducts']:
|
||||
if unfolded or 'byproduct-' + str(bom.id) in unfolded_ids:
|
||||
lines.append({
|
||||
'name': byproduct['product_name'],
|
||||
'type': 'byproduct',
|
||||
'quantity': byproduct['product_qty'],
|
||||
'uom': byproduct['product_uom'],
|
||||
'prod_cost': byproduct['product_cost'],
|
||||
'bom_cost': byproduct['bom_cost'],
|
||||
'level': level + 1,
|
||||
})
|
||||
lines.append({
|
||||
'name': byproduct['name'],
|
||||
'type': 'byproduct',
|
||||
'quantity': byproduct['quantity'],
|
||||
'uom': byproduct['uom'],
|
||||
'prod_cost': byproduct['prod_cost'],
|
||||
'bom_cost': byproduct['bom_cost'],
|
||||
'level': level + 1,
|
||||
'visible': byproducts_unfolded,
|
||||
})
|
||||
return lines
|
||||
|
||||
def _get_pdf_line(self, bom_id, product_id=False, qty=1, unfolded_ids=None, unfolded=False):
|
||||
if unfolded_ids is None:
|
||||
unfolded_ids = set()
|
||||
@api.model
|
||||
def _get_resupply_route_info(self, warehouse, product, quantity, bom=False):
|
||||
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)
|
||||
|
||||
bom = self.env['mrp.bom'].browse(bom_id)
|
||||
product_id = product_id or bom.product_id.id or bom.product_tmpl_id.product_variant_id.id
|
||||
data = self._get_bom(bom_id=bom_id, product_id=product_id, line_qty=qty, )
|
||||
pdf_lines = self._get_sub_lines(bom, product_id, qty, False, 1, unfolded_ids, unfolded)
|
||||
data['components'] = []
|
||||
data['lines'] = pdf_lines
|
||||
data['extra_column_count'] = self._get_extra_column_count()
|
||||
return data
|
||||
@api.model
|
||||
def _format_route_info(self, rules, rules_delay, warehouse, product, bom, quantity):
|
||||
manufacture_rules = [rule for rule in rules if rule.action == 'manufacture' and bom]
|
||||
if manufacture_rules:
|
||||
# Need to get rules from Production location to get delays before production
|
||||
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)
|
||||
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,
|
||||
'manufacture_delay': product.produce_delay + rules_delay,
|
||||
}
|
||||
return {}
|
||||
|
||||
def _get_extra_column_count(self):
|
||||
return 0
|
||||
@api.model
|
||||
def _get_availabilities(self, product, quantity, product_info, bom_key, quantities_info, level, ignore_stock=False, components=False):
|
||||
# Get availabilities according to stock (today & forecasted).
|
||||
stock_state, stock_delay = ('unavailable', False)
|
||||
if not ignore_stock:
|
||||
stock_state, stock_delay = self._get_stock_availability(product, quantity, product_info, quantities_info)
|
||||
|
||||
# Get availabilities from applied resupply rules
|
||||
components = components or []
|
||||
route_info = product_info[product.id].get(bom_key)
|
||||
resupply_state, resupply_delay = ('unavailable', False)
|
||||
if product.detailed_type != 'product':
|
||||
resupply_state, resupply_delay = ('available', 0)
|
||||
elif route_info:
|
||||
resupply_state, resupply_delay = self._get_resupply_availability(route_info, components)
|
||||
|
||||
base = {
|
||||
'resupply_avail_delay': resupply_delay,
|
||||
'stock_avail_state': stock_state,
|
||||
}
|
||||
if level != 0 and stock_state != 'unavailable':
|
||||
return {**base, **{
|
||||
'availability_display': self._format_date_display(stock_state, stock_delay),
|
||||
'availability_state': stock_state,
|
||||
'availability_delay': stock_delay,
|
||||
}}
|
||||
return {**base, **{
|
||||
'availability_display': self._format_date_display(resupply_state, resupply_delay),
|
||||
'availability_state': resupply_state,
|
||||
'availability_delay': resupply_delay,
|
||||
}}
|
||||
|
||||
@api.model
|
||||
def _get_stock_availability(self, product, quantity, product_info, quantities_info):
|
||||
date_today = self.env.context.get('from_date', fields.date.today())
|
||||
if product.detailed_type != 'product':
|
||||
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 float_compare(product_info[product.id]['consumptions'][stock_loc], quantities_info['free_qty'], precision_rounding=product.uom_id.rounding) <= 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')))
|
||||
|
||||
# Seek the closest date in the forecast report where consummed quantity >= forecasted quantity
|
||||
closest_forecasted = self.env['report.stock.quantity']._read_group(domain, ['min_date:min(date)', 'product_id'], ['product_id'])
|
||||
if closest_forecasted:
|
||||
days_to_forecast = (closest_forecasted[0]['min_date'] - date_today).days
|
||||
return ('expected', days_to_forecast)
|
||||
return ('unavailable', False)
|
||||
|
||||
@api.model
|
||||
def _get_resupply_availability(self, route_info, components):
|
||||
if route_info.get('route_type') == 'manufacture':
|
||||
max_component_delay = self._get_max_component_delay(components)
|
||||
if max_component_delay is False:
|
||||
return ('unavailable', False)
|
||||
produce_delay = route_info.get('manufacture_delay', 0) + max_component_delay
|
||||
return ('estimated', produce_delay)
|
||||
return ('unavailable', False)
|
||||
|
||||
@api.model
|
||||
def _get_max_component_delay(self, components):
|
||||
max_component_delay = 0
|
||||
for component in components:
|
||||
line_delay = component.get('availability_delay', False)
|
||||
if line_delay is False:
|
||||
# This component isn't available right now and cannot be resupplied, so the manufactured product can't be resupplied either.
|
||||
return False
|
||||
max_component_delay = max(max_component_delay, line_delay)
|
||||
return max_component_delay
|
||||
|
||||
@api.model
|
||||
def _format_date_display(self, state, delay):
|
||||
date_today = self.env.context.get('from_date', fields.date.today())
|
||||
if state == 'available':
|
||||
return _('Available')
|
||||
if state == 'unavailable':
|
||||
return _('Not Available')
|
||||
if state == 'expected':
|
||||
return _('Expected %s', format_date(self.env, date_today + timedelta(days=delay)))
|
||||
if state == 'estimated':
|
||||
return _('Estimated %s', format_date(self.env, date_today + timedelta(days=delay)))
|
||||
return ''
|
||||
|
||||
@api.model
|
||||
def _has_attachments(self, data):
|
||||
return data['attachment_ids'] or any(self._has_attachments(component) for component in data.get('components', []))
|
||||
|
||||
@@ -1,278 +1,144 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<template id="report_mrp_bom">
|
||||
<div class="o_mrp_bom_report_page container py-3 py-lg-5 px-0 overflow-auto border-start border-end border-bottom bg-white">
|
||||
<div t-if="data.get('components') or data.get('lines') or data.get('operations')">
|
||||
<div class="o_mrp_bom_report_page container py-3 py-lg-5 px-0 bg-white">
|
||||
<div t-if="data.get('lines')">
|
||||
<div class="px-3 mb-5">
|
||||
<h1>BoM Structure
|
||||
<span t-if="data['report_structure'] != 'bom_structure'" class="o_mrp_prod_cost">& Cost</span>
|
||||
</h1>
|
||||
<h3>
|
||||
<a href="#" t-if="data['report_type'] == 'html'" t-att-data-res-id="data['product'].id" t-att-data-model="data['product']._name" class="o_mrp_bom_action" t-out="data['bom_prod_name']"/>
|
||||
<t t-else="" t-esc="data['bom_prod_name']"/>
|
||||
</h3>
|
||||
<hr t-if="data['bom'].code"/>
|
||||
<h6 t-if="data['bom'].code">Reference: <t t-esc="data['bom'].code"/></h6>
|
||||
<h1>BoM Overview</h1>
|
||||
<h3 t-esc="data['name']"/>
|
||||
<hr t-if="data['bom_code']"/>
|
||||
<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">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th name="th_mrp_bom_h">BoM</th>
|
||||
<th name="th_mrp_bom_h">Product</th>
|
||||
<th class="text-end">Quantity</th>
|
||||
<th class="text-start" groups="uom.group_uom">Unit of Measure</th>
|
||||
<th t-if="data['report_structure'] != 'bom_structure'" class="o_mrp_prod_cost text-end" title="This is the cost defined on the product.">Product Cost</th>
|
||||
<th t-if="data['report_structure'] != 'bom_structure'" class="o_mrp_bom_cost text-end" title="This is the cost based on the BoM of the product. It is computed by summing the costs of the components and operations needed to build the product.">BoM Cost</th>
|
||||
<th t-if="data['report_type'] == 'html' and data['has_attachments']" class="o_mrp_has_attachments" title="Files attached to the product">Attachments</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>
|
||||
</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-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>
|
||||
<td t-if="data['show_availabilities']" 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">
|
||||
<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>
|
||||
<a href="#" t-if="data['report_type'] == 'html'" t-att-data-res-id="data['product'].id" t-att-data-model="'product.product'" class="o_mrp_bom_action"><t t-esc="data['bom_prod_name']"/></a><t t-else="" t-esc="data['bom_prod_name']"/>
|
||||
</td>
|
||||
<td name="td_mrp_bom">
|
||||
<a href="#" t-if="data['report_type'] == 'html'" t-att-data-res-id="data['bom'].id" t-att-data-model="'mrp.bom'" class="o_mrp_bom_action" t-out="data['code']"/>
|
||||
<t t-else="" t-out="data['code']"/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span t-out="data['bom_qty']" t-options='{"widget": "float", "decimal_precision": "Product Unit of Measure"}'/>
|
||||
</td>
|
||||
<td groups="uom.group_uom" t-put="data['bom'].product_uom_id.name"/>
|
||||
<td t-if="data['report_structure'] != 'bom_structure'" class="o_mrp_prod_cost text-end">
|
||||
<span t-out="data['price']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</td>
|
||||
<td t-if="data['report_structure'] != 'bom_structure'" class="o_mrp_bom_cost text-end">
|
||||
<span t-out="data['bom_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</td>
|
||||
<td t-if="data['report_type'] == 'html' and data['attachments']">
|
||||
<a t-if="data['attachments']" href="#" role="button" t-att-data-res-id="data['attachments'].ids" t-att-data-model="'mrp.document'" class="o_mrp_show_attachment_action fa fa-fw o_button_icon fa-files-o"/>
|
||||
<span t-if="data['route_name']"><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}'/>
|
||||
</tr>
|
||||
<t t-if="data['report_type'] == 'html'" t-call="mrp.report_mrp_bom_line"/>
|
||||
<t t-if="data['report_type'] == 'pdf'" t-call="mrp.report_mrp_bom_pdf_line"/>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr t-if="data['report_structure'] != 'bom_structure'" class="o_mrp_prod_cost">
|
||||
<td/>
|
||||
<td t-foreach="range(data.get('extra_column_count', 0))" t-as="index"/>
|
||||
<t t-call="mrp.report_mrp_bom_pdf_line"/>
|
||||
<tr t-if="data['show_costs']">
|
||||
<td name="td_mrp_bom_f" class="text-end">
|
||||
<span t-if="data['byproducts']" t-out="data['bom_prod_name']"/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<strong>Unit Cost</strong>
|
||||
</td>
|
||||
<td groups="uom.group_uom">
|
||||
<span t-out="data['bom'].product_uom_id.name"/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span t-out="data['price']/data['bom_qty']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span t-out="data['cost_share'] * data['total'] / data['bom_qty']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
<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['cost_share'] * 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}'/>
|
||||
</tr>
|
||||
<t t-if="data['report_structure'] != 'bom_structure'" t-foreach="data['byproducts']" t-as="byproduct">
|
||||
<tr class="o_mrp_bom_cost">
|
||||
<t t-if="data['show_costs'] and data['byproducts']" t-foreach="data['byproducts']" t-as="byproduct">
|
||||
<tr>
|
||||
<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']"/>
|
||||
<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 t-foreach="range(data.get('extra_column_count', 0))" t-as="index"/>
|
||||
<td name="td_mrp_bom_byproducts_f" class="text-end">
|
||||
<span t-out="byproduct['product_name']"/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<strong>Unit Cost</strong>
|
||||
</td>
|
||||
<td groups="uom.group_uom">
|
||||
<span t-out="byproduct['product_uom']"/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span t-out="byproduct['product_cost'] / byproduct['product_qty']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span t-out="byproduct['cost_share'] * data['total'] / byproduct['product_qty']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</td>
|
||||
<td class="text-end" t-esc="byproduct['cost_share'] * data['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}'/>
|
||||
</tr>
|
||||
</t>
|
||||
</tfoot>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div t-else="" class="d-flex align-items-center justify-content-center h-50">
|
||||
<h4 class="text-muted">No data available.</h4>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="report_mrp_bom_line">
|
||||
<t t-set="currency" t-value="data['currency']"/>
|
||||
<t t-foreach="data['components']" t-as="l">
|
||||
<t t-set="space_td" t-value="'margin-left: '+ str(l['level'] * 20) + 'px;'"/>
|
||||
<tr class="o_mrp_bom_report_line" t-att-data-id="l['child_bom']" t-att-parent_id="l['parent_id']" t-att-data-line="l['line_id']" t-att-data-product_id="l['prod_id']" t-att-data-qty="l['prod_qty']" t-att-data-level="l['level']">
|
||||
<td>
|
||||
<div t-att-style="space_td">
|
||||
<button t-if="l['child_bom']" t-att-data-function="'get_bom'" class="o_mrp_bom_unfoldable btn btn-light p-0" aria-label="Unfold" title="Unfold">
|
||||
<i class="fa fa-fw fa-caret-right" role="img"/>
|
||||
</button>
|
||||
<div t-att-class="None if l['child_bom'] else 'o_mrp_bom_no_fold'" style="display:inline-block;">
|
||||
<a href="#" t-att-data-res-id="l['prod_id']" t-att-data-model="'product.product'" class="o_mrp_bom_action" t-out="l['prod_name']"/>
|
||||
</div>
|
||||
<i t-if="l['phantom_bom']" class="fa fa-dropbox" title="This is a BoM of type Kit!" role="img" aria-label="This is a BoM of type Kit!"/>
|
||||
</div>
|
||||
</td>
|
||||
<td name="td_mrp_bom">
|
||||
<a href="#" t-att-data-res-id="l['child_bom']" t-att-data-model="'mrp.bom'" class="o_mrp_bom_action" t-out="l['code']"/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span t-out="l['prod_qty']" t-options='{"widget": "float", "decimal_precision": "Product Unit of Measure"}'/>
|
||||
</td>
|
||||
<td groups="uom.group_uom" t-out="l['prod_uom']"/>
|
||||
<td class="o_mrp_prod_cost text-end">
|
||||
<span t-out="l['prod_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</td>
|
||||
<td class="o_mrp_bom_cost text-end">
|
||||
<span t-out="l['total']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</td>
|
||||
<td t-if="l['attachments']">
|
||||
<a href="#" role="button" t-att-data-res-id="l['attachments'].ids" t-att-data-model="'mrp.document'" class="o_mrp_show_attachment_action">
|
||||
<i class="fa fa-fw fa-files-o"/>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-if="data['operations']" name="operations">
|
||||
<t t-set="space_td" t-value="'margin-left: '+ str(data['level'] * 20) + 'px;'"/>
|
||||
<tr class="o_mrp_bom_report_line o_mrp_bom_cost" t-att-data-product_id="data['product'].id" t-att-data-id="'operation-' + str(data['bom'].id)" t-att-data-bom-id="data['bom'].id" t-att-parent_id="data['bom'].id" t-att-data-qty="data['bom_qty']" t-att-data-level="data['level']">
|
||||
<td name="td_opr">
|
||||
<span t-att-style="space_td"/>
|
||||
<button class="o_mrp_bom_unfoldable btn btn-light ps-0 py-0" t-att-data-function="'get_operations'" aria-label="Unfold" title="Unfold">
|
||||
<i class="fa fa-fw fa-caret-right" role="img"/>
|
||||
Operations
|
||||
</button>
|
||||
</td>
|
||||
<td t-foreach="range(data.get('extra_column_count', 0))" t-as="index"/>
|
||||
<td/>
|
||||
<td class="text-end">
|
||||
<span t-out="data['operations_time']" t-options='{"widget": "float_time"}'/>
|
||||
</td>
|
||||
<td groups="uom.group_uom"><span>Minutes</span></td>
|
||||
<td/>
|
||||
<td class="o_mrp_bom_cost text-end">
|
||||
<span t-out="data['operations_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</td>
|
||||
<td t-if="l['attachments']"/>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-if="data['byproducts']">
|
||||
<t t-set="space_td" t-value="'margin-left: '+ str(data['level'] * 20) + 'px;'"/>
|
||||
<tr class="o_mrp_bom_report_line o_mrp_bom_cost" t-att-data-id="'byproduct-' + str(data['bom'].id)" t-att-data-bom-id="data['bom'].id" t-att-parent_id="data['bom'].id" t-att-data-qty="data['bom_qty']" t-att-data-level="data['level']" t-att-data-total="data['total']">
|
||||
<td name="td_byproducts">
|
||||
<span t-att-style="space_td"/>
|
||||
<button class="o_mrp_bom_unfoldable btn btn-light ps-0 py-0" t-att-data-function="'get_byproducts'" aria-label="Unfold" title="Unfold">
|
||||
<i class="fa fa-fw fa-caret-right" role="img"/>
|
||||
By-Products
|
||||
</button>
|
||||
</td>
|
||||
<td t-foreach="range(data.get('extra_column_count', 0))" t-as="index"/>
|
||||
<td/>
|
||||
<td class="text-end" t-out="data['byproducts_total']"/>
|
||||
<td groups="uom.group_uom"/>
|
||||
<td/>
|
||||
<td class="text-end">
|
||||
<span t-out="data['byproducts_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="report_mrp_operation_line">
|
||||
<t t-set="currency" t-value="data['currency']"/>
|
||||
<t t-foreach="data['operations']" t-as="op">
|
||||
<t t-set="space_td" t-value="'margin-left: '+ str(op['level'] * 20) + 'px;'"/>
|
||||
<tr class="o_mrp_bom_report_line o_mrp_bom_cost" t-att-parent_id="'operation-' + str(data['bom_id'])">
|
||||
<td name="td_opr_line">
|
||||
<span t-att-style="space_td"/>
|
||||
<a href="#" t-att-data-res-id="op['operation'].id" t-att-data-model="'mrp.routing.workcenter'" class="o_mrp_bom_action" t-out="op['name']"/>
|
||||
</td>
|
||||
<td t-foreach="range(data.get('extra_column_count', 0))" t-as="index"/>
|
||||
<td/>
|
||||
<td class="text-end">
|
||||
<span t-out="op['duration_expected']" t-options='{"widget": "float_time"}'/>
|
||||
</td>
|
||||
<td groups="uom.group_uom"><span>Minutes</span></td>
|
||||
<td/>
|
||||
<td class="o_mrp_bom_cost text-end">
|
||||
<span t-out="op['total']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="report_mrp_byproduct_line">
|
||||
<t t-set="currency" t-value="data['currency']"/>
|
||||
<t t-foreach="data['byproducts']" t-as="byproduct">
|
||||
<t t-set="space_td" t-value="'margin-left: '+ str(byproduct['level'] * 20) + 'px;'"/>
|
||||
<tr class="o_mrp_bom_report_line o_mrp_bom_cost" t-att-parent_id="'byproduct-' + str(data['bom_id'])">
|
||||
<td name="td_byproduct_line">
|
||||
<span t-att-style="space_td"/>
|
||||
<a href="#" t-att-data-res-id="byproduct['product_id'].id" t-att-data-model="byproduct['product_id']._name" class="o_mrp_bom_action"><t t-esc="byproduct['product_name']"/></a>
|
||||
</td>
|
||||
<td t-foreach="range(data.get('extra_column_count', 0))" t-as="index"/>
|
||||
<td/>
|
||||
<td class="text-end">
|
||||
<span t-out="byproduct['product_qty']"/>
|
||||
</td>
|
||||
<td groups="uom.group_uom"><span t-out="byproduct['product_uom']"/></td>
|
||||
<td class="text-end">
|
||||
<span t-out="byproduct['product_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span t-out="byproduct['bom_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="report_mrp_bom_pdf">
|
||||
<t t-call="web.html_container">
|
||||
<t t-call="mrp.report_mrp_bom"/>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="report_mrp_bom_pdf_line">
|
||||
<t t-set="currency" t-value="data['currency']"/>
|
||||
<t t-foreach="data['lines']" t-as="l">
|
||||
<t t-set="space_td" t-value="'margin-left: '+ str(l['level'] * 20) + 'px;'"/>
|
||||
<tr t-if="data['report_structure'] != 'bom_structure' or l['type'] not in ['operation', 'byproduct']">
|
||||
<td>
|
||||
<div t-att-style="space_td">
|
||||
<div><t t-esc="l['name']"/></div>
|
||||
</div>
|
||||
</td>
|
||||
<td name="td_mrp_code">
|
||||
<div t-if="l.get('code')" t-esc="l['code']" />
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span>
|
||||
<t t-if="l['type'] == 'operation'" t-esc="l['quantity']" t-options='{"widget": "float_time"}'/>
|
||||
<t t-if="l['type'] in ['bom', 'byproduct']" t-esc="l['quantity']" t-options='{"widget": "float", "decimal_precision": "Product Unit of Measure"}'/>
|
||||
</span>
|
||||
</td>
|
||||
<td groups="uom.group_uom"><span><t t-esc="l['uom']"/></span></td>
|
||||
<td t-if="data['report_structure'] != 'bom_structure'" class="o_mrp_prod_cost text-end">
|
||||
<span t-if="'prod_cost' in l" t-esc="l['prod_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</td>
|
||||
<td t-if="data['report_structure'] != 'bom_structure'" class="o_mrp_bom_cost text-end">
|
||||
<span t-esc="l['bom_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<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'])">
|
||||
<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"}'/>
|
||||
</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"}'/>
|
||||
</t>
|
||||
</td>
|
||||
<td t-if="data['show_availabilities']" 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">
|
||||
<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>
|
||||
<span t-if="l.get('route_name')"><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>
|
||||
</tr>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="report_bom_structure">
|
||||
<t t-set="data_report_landscape" t-value="True"/>
|
||||
<t t-call="web.basic_layout">
|
||||
<t t-call-assets="mrp.assets_common" t-js="False"/>
|
||||
<t t-foreach="docs" t-as="data">
|
||||
<div class="page">
|
||||
<t t-call="mrp.report_mrp_bom"/>
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
<record id="action_report_bom_structure" model="ir.actions.report">
|
||||
<field name="name">BoM Structure</field>
|
||||
<field name="name">BoM Overview</field>
|
||||
<field name="model">mrp.bom</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">mrp.report_bom_structure</field>
|
||||
<field name="report_file">mrp.report_bom_structure</field>
|
||||
<field name="print_report_name">'Bom Structure - %s' % object.display_name</field>
|
||||
<field name="print_report_name">'Bom Overview - %s' % object.display_name</field>
|
||||
<field name="binding_model_id" ref="model_mrp_bom"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { BomOverviewControlPanel } from "../bom_overview_control_panel/mrp_bom_overview_control_panel";
|
||||
import { BomOverviewTable } from "../bom_overview_table/mrp_bom_overview_table";
|
||||
|
||||
const { Component, EventBus, onMounted, onWillStart, onWillUnmount, useState } = owl;
|
||||
|
||||
export class BomOverviewComponent extends Component {
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
this.actionService = useService("action");
|
||||
this.bus = new EventBus();
|
||||
|
||||
this.variants = [];
|
||||
this.warehouses = [];
|
||||
this.showVariants = false;
|
||||
this.uomName = "";
|
||||
this.extraColumnCount = 0;
|
||||
this.unfoldedIds = new Set();
|
||||
|
||||
this.state = useState({
|
||||
showOptions: {
|
||||
uom: false,
|
||||
availabilities: true,
|
||||
costs: true,
|
||||
operations: true,
|
||||
leadTimes: true,
|
||||
attachments: false,
|
||||
},
|
||||
currentWarehouse: null,
|
||||
currentVariantId: null,
|
||||
bomData: {},
|
||||
bomQuantity: null,
|
||||
});
|
||||
|
||||
onWillStart(async () => {
|
||||
await this.getWarehouses();
|
||||
await this.initBomData();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
this.bus.addEventListener("change-fold", (ev) => this._onChangeFolded(ev.detail));
|
||||
this.bus.addEventListener("change-display", (ev) => this._onChangeDisplay(ev.detail));
|
||||
this.bus.addEventListener("change-quantity", (ev) => this._onChangeBomQuantity(ev.detail));
|
||||
this.bus.addEventListener("change-variant", (ev) => this._onChangeVariant(ev.detail));
|
||||
this.bus.addEventListener("change-warehouse", (ev) => this._onChangeWarehouse(ev.detail));
|
||||
this.bus.addEventListener("print", (ev) => this._onClickPrint(ev.detail));
|
||||
});
|
||||
|
||||
onWillUnmount(() => {
|
||||
this.bus.removeEventListener("change-fold", (ev) => this._onChangeFolded(ev.detail));
|
||||
this.bus.removeEventListener("change-display", (ev) => this._onChangeDisplay(ev.detail));
|
||||
this.bus.removeEventListener("change-quantity", (ev) => this._onChangeBomQuantity(ev.detail));
|
||||
this.bus.removeEventListener("change-variant", (ev) => this._onChangeVariant(ev.detail));
|
||||
this.bus.removeEventListener("change-warehouse", (ev) => this._onChangeWarehouse(ev.detail));
|
||||
this.bus.removeEventListener("print", (ev) => this._onClickPrint(ev.detail));
|
||||
});
|
||||
}
|
||||
|
||||
//---- Data ----
|
||||
|
||||
async initBomData() {
|
||||
const bomData = await this.getBomData();
|
||||
this.state.bomQuantity = bomData["bom_qty"];
|
||||
this.state.showOptions.uom = bomData["is_uom_applied"];
|
||||
this.uomName = bomData["bom_uom_name"];
|
||||
this.variants = bomData["variants"];
|
||||
this.showVariants = bomData["is_variant_applied"];
|
||||
if (this.showVariants) {
|
||||
this.state.currentVariantId = Object.keys(this.variants)[0];
|
||||
}
|
||||
}
|
||||
|
||||
async getBomData() {
|
||||
const args = [
|
||||
this.activeId,
|
||||
this.state.bomQuantity,
|
||||
this.state.currentVariantId,
|
||||
];
|
||||
const context = this.state.currentWarehouse ? { warehouse: this.state.currentWarehouse.id } : {};
|
||||
const bomData = await this.orm.call(
|
||||
"report.mrp.report_bom_structure",
|
||||
"get_html",
|
||||
args,
|
||||
{ context }
|
||||
);
|
||||
this.state.bomData = bomData["lines"];
|
||||
this.state.showOptions.attachments = bomData["has_attachments"];
|
||||
return bomData;
|
||||
}
|
||||
|
||||
async getWarehouses() {
|
||||
const warehouses = await this.orm.call(
|
||||
"report.mrp.report_bom_structure",
|
||||
"get_warehouses",
|
||||
);
|
||||
this.warehouses = warehouses;
|
||||
this.state.currentWarehouse = warehouses[0];
|
||||
}
|
||||
|
||||
//---- Handlers ----
|
||||
|
||||
_onChangeFolded(foldInfo) {
|
||||
const { ids, isFolded } = foldInfo;
|
||||
const operation = isFolded ? "delete" : "add";
|
||||
ids.forEach(id => this.unfoldedIds[operation](id));
|
||||
}
|
||||
|
||||
_onChangeDisplay(displayInfo) {
|
||||
const { type, value } = displayInfo;
|
||||
this.state.showOptions[type] = value;
|
||||
}
|
||||
|
||||
async _onChangeBomQuantity(newQuantity) {
|
||||
if (this.state.bomQuantity != newQuantity) {
|
||||
this.state.bomQuantity = newQuantity;
|
||||
await this.getBomData();
|
||||
}
|
||||
}
|
||||
|
||||
async _onChangeVariant(variantId) {
|
||||
if (this.state.currentVariantId != variantId) {
|
||||
this.state.currentVariantId = variantId;
|
||||
await this.getBomData();
|
||||
}
|
||||
}
|
||||
|
||||
async _onChangeWarehouse(warehouseId) {
|
||||
if (this.state.currentWarehouse.id != warehouseId) {
|
||||
this.state.currentWarehouse = this.warehouses.find(wh => wh.id == warehouseId);
|
||||
await this.getBomData();
|
||||
}
|
||||
}
|
||||
|
||||
async _onClickPrint(printAll) {
|
||||
return this.actionService.doAction({
|
||||
type: "ir.actions.report",
|
||||
report_type: "qweb-pdf",
|
||||
report_name: this.getReportName(printAll),
|
||||
report_file: "mrp.report_bom_structure",
|
||||
});
|
||||
}
|
||||
|
||||
//---- Getters ----
|
||||
|
||||
get activeId() {
|
||||
return this.props.action.context.active_id;
|
||||
}
|
||||
|
||||
// ---- Helpers ----
|
||||
|
||||
getReportName(printAll) {
|
||||
let reportName = "mrp.report_bom_structure?docids=" + this.activeId +
|
||||
"&availabilities=" + this.state.showOptions.availabilities +
|
||||
"&costs=" + this.state.showOptions.costs +
|
||||
"&operations=" + this.state.showOptions.operations +
|
||||
"&lead_times=" + this.state.showOptions.leadTimes +
|
||||
"&quantity=" + (this.state.bomQuantity || 1) +
|
||||
"&unfolded_ids=" + JSON.stringify(Array.from(this.unfoldedIds)) +
|
||||
"&warehouse_id=" + (this.state.currentWarehouse ? this.state.currentWarehouse.id : false);
|
||||
if (printAll) {
|
||||
reportName += "&all_variants=1";
|
||||
} else if (this.showVariants && this.state.currentVariantId) {
|
||||
reportName += "&variant=" + this.state.currentVariantId;
|
||||
}
|
||||
return reportName;
|
||||
}
|
||||
}
|
||||
|
||||
BomOverviewComponent.template = "mrp.BomOverviewComponent";
|
||||
BomOverviewComponent.components = {
|
||||
BomOverviewControlPanel,
|
||||
BomOverviewTable,
|
||||
};
|
||||
|
||||
registry.category("actions").add("mrp_bom_report", BomOverviewComponent);
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<div t-name="mrp.BomOverviewComponent" class="o_action" owl="1">
|
||||
<BomOverviewControlPanel
|
||||
bus="bus"
|
||||
bomQuantity="state.bomQuantity"
|
||||
uomName="uomName"
|
||||
variants="variants"
|
||||
showOptions="state.showOptions"
|
||||
showVariants="showVariants"
|
||||
currentWarehouse="state.currentWarehouse"
|
||||
warehouses="warehouses"/>
|
||||
|
||||
<BomOverviewTable
|
||||
bus="bus"
|
||||
uomName="uomName"
|
||||
showOptions="state.showOptions"
|
||||
currentWarehouseId="state.currentWarehouse.id"
|
||||
data="state.bomData"/>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { BomOverviewLine } from "../bom_overview_line/mrp_bom_overview_line";
|
||||
import { BomOverviewExtraBlock } from "../bom_overview_extra_block/mrp_bom_overview_extra_block";
|
||||
|
||||
const { Component, onMounted, onWillUnmount, onWillUpdateProps, useState } = owl;
|
||||
|
||||
export class BomOverviewComponentsBlock extends Component {
|
||||
setup() {
|
||||
const childFoldstate = this.childIds.reduce((prev, curr) => ({ ...prev, [curr]: !this.props.unfoldAll}), {});
|
||||
this.state = useState({
|
||||
...childFoldstate,
|
||||
unfoldAll: this.props.unfoldAll || false,
|
||||
});
|
||||
if (this.props.unfoldAll) {
|
||||
this.props.bus.trigger("change-fold", { ids: this.childIds, isFolded: false });
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (this.hasComponents) {
|
||||
this.props.bus.addEventListener(`toggle-fold-${this.identifier}`, (ev) => this._onToggleFolded(ev.detail));
|
||||
this.props.bus.addEventListener("unfold-all", () => this._unfoldAll());
|
||||
}
|
||||
});
|
||||
|
||||
onWillUpdateProps(newProps => {
|
||||
if (this.data.product_id != newProps.data.product_id) {
|
||||
this.childIds.forEach(id => delete this.state[id]);
|
||||
const newChildIds = this.getHasComponents(newProps.data) ? newProps.data.components.map(c => this.getIdentifier(c)) : [];
|
||||
newChildIds.forEach(id => this.state[id] = true);
|
||||
this.state.unfoldAll = false;
|
||||
}
|
||||
});
|
||||
|
||||
onWillUnmount(() => {
|
||||
if (this.hasComponents) {
|
||||
this.props.bus.trigger("change-fold", { ids: this.childIds, isFolded: true });
|
||||
this.props.bus.removeEventListener(`toggle-fold-${this.identifier}`, (ev) => this._onToggleFolded(ev.detail));
|
||||
this.props.bus.removeEventListener("unfold-all", () => this._unfoldAll());
|
||||
}
|
||||
});
|
||||
}
|
||||
//---- Handlers ----
|
||||
|
||||
_onToggleFolded(foldId) {
|
||||
const newState = !this.state[foldId];
|
||||
this.state[foldId] = newState;
|
||||
this.state.unfoldAll = false;
|
||||
this.props.bus.trigger("change-fold", { ids: [foldId], isFolded: newState });
|
||||
}
|
||||
|
||||
_unfoldAll() {
|
||||
const allChildIds = this.childIds;
|
||||
this.state.unfoldAll = true;
|
||||
allChildIds.forEach(id => this.state[id] = false);
|
||||
this.props.bus.trigger("change-fold", { ids: allChildIds, isFolded: false });
|
||||
}
|
||||
|
||||
//---- Getters ----
|
||||
|
||||
get data() {
|
||||
return this.props.data;
|
||||
}
|
||||
|
||||
get hasComponents() {
|
||||
return this.getHasComponents(this.data);
|
||||
}
|
||||
|
||||
get childIds() {
|
||||
return this.hasComponents ? this.data.components.map(c => this.getIdentifier(c)) : [];
|
||||
}
|
||||
|
||||
get identifier() {
|
||||
return this.getIdentifier(this.data);
|
||||
}
|
||||
|
||||
get operationsId() {
|
||||
return this.getIdentifier(this.data, "operations");
|
||||
}
|
||||
|
||||
get byproductsId() {
|
||||
return this.getIdentifier(this.data, "byproducts");
|
||||
}
|
||||
|
||||
get showOperations() {
|
||||
return this.props.showOptions.operations;
|
||||
}
|
||||
|
||||
//---- Utils ----
|
||||
|
||||
getHasComponents(data) {
|
||||
return data.components && data.components.length > 0;
|
||||
}
|
||||
|
||||
getIdentifier(data, type=null) {
|
||||
return `${type ? type : data.type}_${data.index}`;
|
||||
}
|
||||
}
|
||||
|
||||
BomOverviewComponentsBlock.template = "mrp.BomOverviewComponentsBlock";
|
||||
BomOverviewComponentsBlock.components = {
|
||||
BomOverviewLine,
|
||||
BomOverviewComponentsBlock,
|
||||
BomOverviewExtraBlock,
|
||||
};
|
||||
BomOverviewComponentsBlock.props = {
|
||||
bus: Object,
|
||||
unfoldAll: { type: Boolean, optional: true },
|
||||
showOptions: Object,
|
||||
currentWarehouseId: { type: Number, optional: true },
|
||||
data: Object,
|
||||
};
|
||||
BomOverviewComponentsBlock.defaultProps = {
|
||||
unfoldAll: false,
|
||||
};
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mrp.BomOverviewComponentsBlock" owl="1">
|
||||
<t name="components" t-if="hasComponents">
|
||||
<t t-foreach="data.components" t-as="line" t-key="line.index">
|
||||
<BomOverviewLine
|
||||
bus="props.bus"
|
||||
isFolded="state[getIdentifier(line)]"
|
||||
parentId="identifier"
|
||||
showOptions="props.showOptions"
|
||||
data="line"/>
|
||||
|
||||
<t t-if="!state[getIdentifier(line)] && hasComponents">
|
||||
<BomOverviewComponentsBlock
|
||||
bus="props.bus"
|
||||
unfoldAll="state.unfoldAll"
|
||||
showOptions="props.showOptions"
|
||||
currentWarehouseId="props.currentWarehouseId"
|
||||
data="line"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
<t name="operations" t-if="showOperations && !!data.operations && data.operations.length > 0">
|
||||
<BomOverviewExtraBlock
|
||||
bus="props.bus"
|
||||
unfoldAll="state.unfoldAll"
|
||||
type="'operations'"
|
||||
showOptions="props.showOptions"
|
||||
data="data"/>
|
||||
</t>
|
||||
<t name="byproducts" t-if="!!data.byproducts && data.byproducts.length > 0">
|
||||
<BomOverviewExtraBlock
|
||||
bus="props.bus"
|
||||
unfoldAll="state.unfoldAll"
|
||||
type="'byproducts'"
|
||||
showOptions="props.showOptions"
|
||||
data="data"/>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { ControlPanel } from "@web/search/control_panel/control_panel";
|
||||
import { BomOverviewDisplayFilter } from "../bom_overview_display_filter/mrp_bom_overview_display_filter";
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class BomOverviewControlPanel extends Component {
|
||||
setup() {
|
||||
this.controlPanelDisplay = {};
|
||||
// Cannot use 'control-panel-bottom-right' slot without this, as viewSwitcherEntries doesn't exist in this.env.config here.
|
||||
this.env.config.viewSwitcherEntries = [];
|
||||
}
|
||||
|
||||
//---- Handlers ----
|
||||
|
||||
updateQuantity(ev) {
|
||||
const newVal = isNaN(ev.target.value) ? 1 : parseInt(ev.target.value);
|
||||
this.props.bus.trigger("change-quantity", newVal);
|
||||
}
|
||||
|
||||
onKeyPress(ev) {
|
||||
if (ev.keyCode === 13 || ev.which === 13) {
|
||||
ev.preventDefault();
|
||||
this.updateQuantity(ev);
|
||||
}
|
||||
}
|
||||
|
||||
changeVariant(ev) {
|
||||
this.props.bus.trigger("change-variant", ev.target.value);
|
||||
}
|
||||
|
||||
changeWarehouse(warehouseId) {
|
||||
this.props.bus.trigger("change-warehouse", warehouseId);
|
||||
}
|
||||
|
||||
clickPrint(printAll=false) {
|
||||
this.props.bus.trigger("print", printAll);
|
||||
}
|
||||
|
||||
clickUnfold() {
|
||||
this.props.bus.trigger("unfold-all");
|
||||
}
|
||||
}
|
||||
|
||||
BomOverviewControlPanel.template = "mrp.BomOverviewControlPanel";
|
||||
BomOverviewControlPanel.components = {
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
ControlPanel,
|
||||
BomOverviewDisplayFilter,
|
||||
};
|
||||
BomOverviewControlPanel.props = {
|
||||
bus: Object,
|
||||
bomQuantity: Number,
|
||||
showOptions: Object,
|
||||
showVariants: { type: Boolean, optional: true },
|
||||
variants: { type: Object, optional: true },
|
||||
showUom: { type: Boolean, optional: true },
|
||||
uomName: { type: String, optional: true },
|
||||
currentWarehouse: Object,
|
||||
warehouses: { type: Array, optional: true },
|
||||
};
|
||||
BomOverviewControlPanel.defaultProps = {
|
||||
variants: {},
|
||||
warehouses: [],
|
||||
};
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mrp.BomOverviewControlPanel" owl="1">
|
||||
<ControlPanel display="controlPanelDisplay">
|
||||
<t t-set-slot="control-panel-bottom-left-buttons">
|
||||
<div class="o_cp_buttons">
|
||||
<div class="o_list_buttons o_mrp_bom_report_buttons">
|
||||
<button t-on-click="() => this.clickPrint()" type="button" class="btn btn-primary">Print</button>
|
||||
<t t-if="props.showVariants">
|
||||
<button t-on-click="() => this.clickPrint(true)" type="button" class="btn btn-primary ms-1">Print All Variants</button>
|
||||
</t>
|
||||
<button t-on-click="clickUnfold" type="button" class="btn btn-primary ms-1">Unfold</button>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-set-slot="control-panel-top-right">
|
||||
<form class="row gx-0">
|
||||
<div class="col-lg-8 row">
|
||||
<div class="col-lg-11 row gx-1 mb-2">
|
||||
<label for="bom_quantity" class="col-xl-2 col-lg-3 col-sm-4">Quantity:</label>
|
||||
<div t-attf-class="{{ props.showOptions.uom ? 'col-xl-7 col-lg-6 col-sm-5' : 'col-xl-10 col-lg-9 col-sm-8' }}">
|
||||
<input id="bom_quantity" type="number" step="any" t-on-change="ev => this.updateQuantity(ev)" t-on-keypress="ev => this.onKeyPress(ev)" t-att-value="props.bomQuantity" min="1" class="o_input"/>
|
||||
</div>
|
||||
<div t-if="props.showOptions.uom" class="col-xl-3 col-lg-3 col-sm-3">
|
||||
<span t-esc="props.uomName"/>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="props.showVariants" class="col-lg-11 row gx-1 mb-1">
|
||||
<label class="col-xl-2 col-lg-3 col-sm-4">Variant:</label>
|
||||
<div class="col-xl-10 col-lg-9 col-sm-8">
|
||||
<select class="o_input" t-on-change="changeVariant">
|
||||
<option t-foreach="props.variants" t-as="variant" t-att-value="variant" t-key="variant">
|
||||
<t t-esc="props.variants[variant]"/>
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</t>
|
||||
|
||||
<t t-set-slot="control-panel-bottom-right">
|
||||
<div class="o_cp_bottom_right_sp">
|
||||
<t t-if="props.warehouses.length > 1">
|
||||
<Dropdown class="'btn-group'" togglerClass="'btn btn-secondary'">
|
||||
<t t-set-slot="toggler">
|
||||
<span class="fa fa-home"/> Warehouse: <span t-esc="props.currentWarehouse.name"/>
|
||||
</t>
|
||||
<t t-foreach="props.warehouses" t-as="wh" t-key="wh.id">
|
||||
<DropdownItem onSelected="() => this.changeWarehouse(wh.id)" t-esc="wh.name"/>
|
||||
</t>
|
||||
</Dropdown>
|
||||
</t>
|
||||
<BomOverviewDisplayFilter
|
||||
bus="props.bus"
|
||||
showOptions="props.showOptions"/>
|
||||
</div>
|
||||
</t>
|
||||
</ControlPanel>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/** @odoo-module **/
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class BomOverviewDisplayFilter extends Component {
|
||||
setup() {
|
||||
this.displayOptions = {
|
||||
availabilities: this.env._t('Availabilities'),
|
||||
leadTimes: this.env._t('Lead Times'),
|
||||
costs: this.env._t('Costs'),
|
||||
operations: this.env._t('Operations'),
|
||||
};
|
||||
}
|
||||
//---- Handlers ----
|
||||
|
||||
onClickDisplay(optionKey) {
|
||||
this.props.bus.trigger("change-display", { type: optionKey, value: !this.props.showOptions[optionKey] });
|
||||
}
|
||||
|
||||
//---- Getters ----
|
||||
|
||||
get displayableOptions() {
|
||||
return Object.keys(this.displayOptions);
|
||||
}
|
||||
|
||||
get currentDisplayedNames() {
|
||||
return this.displayableOptions.filter(key => this.props.showOptions[key]).map(key => this.displayOptions[key]).join(", ");
|
||||
}
|
||||
}
|
||||
|
||||
BomOverviewDisplayFilter.template = "mrp.BomOverviewDisplayFilter";
|
||||
BomOverviewDisplayFilter.components = {
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
}
|
||||
BomOverviewDisplayFilter.props = {
|
||||
bus: Object,
|
||||
showOptions: {
|
||||
type: Object,
|
||||
shape: {
|
||||
availabilities: Boolean,
|
||||
costs: Boolean,
|
||||
operations: Boolean,
|
||||
leadTimes: Boolean,
|
||||
uom: Boolean,
|
||||
attachments: Boolean,
|
||||
},
|
||||
},
|
||||
};
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mrp.BomOverviewDisplayFilter" owl="1">
|
||||
<Dropdown class="'btn-group'" togglerClass="'btn btn-secondary'">
|
||||
<t t-set-slot="toggler">
|
||||
<span class="fa fa-filter"/>
|
||||
Display: <span t-esc="currentDisplayedNames"/>
|
||||
</t>
|
||||
<t t-foreach="displayableOptions" t-as="optionKey" t-key="optionKey">
|
||||
<DropdownItem parentClosingMode="'none'" class="{ o_menu_item: true, selected: this.props.showOptions[optionKey] }" onSelected="() => this.onClickDisplay(optionKey)" t-esc="displayOptions[optionKey]"/>
|
||||
</t>
|
||||
</Dropdown>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { BomOverviewLine } from "../bom_overview_line/mrp_bom_overview_line";
|
||||
import { BomOverviewSpecialLine } from "../bom_overview_special_line/mrp_bom_overview_special_line";
|
||||
|
||||
const { Component, onMounted, onWillUnmount, onWillUpdateProps, useState } = owl;
|
||||
|
||||
export class BomOverviewExtraBlock extends Component {
|
||||
setup() {
|
||||
this.state = useState({
|
||||
isFolded: !this.props.unfoldAll,
|
||||
});
|
||||
if (this.props.unfoldAll) {
|
||||
this.props.bus.trigger("change-fold", { ids: [this.identifier], isFolded: false });
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
this.props.bus.addEventListener(`toggle-fold-${this.identifier}`, () => this._onToggleFolded());
|
||||
this.props.bus.addEventListener("unfold-all", () => this._unfold());
|
||||
});
|
||||
|
||||
onWillUpdateProps(newProps => {
|
||||
if (this.props.data.product_id != newProps.data.product_id) {
|
||||
this.state.isFolded = true;
|
||||
}
|
||||
});
|
||||
|
||||
onWillUnmount(() => {
|
||||
this.props.bus.trigger("change-fold", { ids: [this.identifier], isFolded: true });
|
||||
this.props.bus.removeEventListener(`toggle-fold-${this.identifier}`, () => this._onToggleFolded());
|
||||
this.props.bus.removeEventListener("unfold-all", () => this._unfold());
|
||||
});
|
||||
}
|
||||
|
||||
//---- Handlers ----
|
||||
|
||||
_onToggleFolded() {
|
||||
const newState = !this.state.isFolded;
|
||||
this.state.isFolded = newState;
|
||||
this.props.bus.trigger("change-fold", { ids: [this.identifier], isFolded: newState });
|
||||
}
|
||||
|
||||
_unfold() {
|
||||
this.state.isFolded = false;
|
||||
this.props.bus.trigger("change-fold", { ids: [this.identifier], isFolded: false });
|
||||
}
|
||||
|
||||
//---- Getters ----
|
||||
|
||||
get identifier() {
|
||||
return `${this.props.type}_${this.props.data.index}`;
|
||||
}
|
||||
}
|
||||
|
||||
BomOverviewExtraBlock.template = "mrp.BomOverviewExtraBlock";
|
||||
BomOverviewExtraBlock.components = {
|
||||
BomOverviewLine,
|
||||
BomOverviewSpecialLine,
|
||||
};
|
||||
BomOverviewExtraBlock.props = {
|
||||
bus: Object,
|
||||
unfoldAll: { type: Boolean, optional: true },
|
||||
type: {
|
||||
type: String,
|
||||
validate: t => ["operations", "byproducts"].includes(t),
|
||||
},
|
||||
showOptions: Object,
|
||||
data: Object,
|
||||
};
|
||||
BomOverviewExtraBlock.defaultProps = {
|
||||
showAvailabilities: false,
|
||||
showCosts: false,
|
||||
extraColumnCount: 0,
|
||||
};
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mrp.BomOverviewExtraBlock" owl="1">
|
||||
<BomOverviewSpecialLine
|
||||
bus="props.bus"
|
||||
type="props.type"
|
||||
isFolded="state.isFolded"
|
||||
parentId="identifier"
|
||||
showOptions="props.showOptions"
|
||||
data="props.data"/>
|
||||
|
||||
<t t-if="!state.isFolded" t-foreach="props.type == 'operations' ? props.data.operations : props.data.byproducts" t-as="extra_data" t-key="extra_data.index">
|
||||
<BomOverviewLine
|
||||
bus="props.bus"
|
||||
showOptions="props.showOptions"
|
||||
data="extra_data"/>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,174 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { formatFloat, formatFloatTime, formatMonetary } from "@web/views/fields/formatters";
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class BomOverviewLine extends Component {
|
||||
setup() {
|
||||
this.actionService = useService("action");
|
||||
this.ormService = useService("orm");
|
||||
this.formatFloat = formatFloat;
|
||||
this.formatFloatTime = formatFloatTime;
|
||||
this.formatMonetary = (val) => formatMonetary(val, { currencyId: this.data.currency_id });
|
||||
}
|
||||
|
||||
//---- Handlers ----
|
||||
|
||||
async goToRoute(routeType) {
|
||||
if (routeType == "manufacture") {
|
||||
return this.goToAction(this.data.bom_id, "mrp.bom");
|
||||
}
|
||||
}
|
||||
|
||||
async goToAction(id, model) {
|
||||
return this.actionService.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
res_model: model,
|
||||
res_id: id,
|
||||
views: [[false, "form"]],
|
||||
target: "current",
|
||||
context: {
|
||||
active_id: id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async goToForecast() {
|
||||
const action = await this.ormService.call(
|
||||
this.data.link_model,
|
||||
this.forecastAction,
|
||||
[[this.data.link_id]],
|
||||
);
|
||||
action.context = {
|
||||
active_model: this.data.link_model,
|
||||
active_id: this.data.link_id,
|
||||
};
|
||||
if (this.props.currentWarehouseId) {
|
||||
action.context["warehouse"] = this.props.currentWarehouseId;
|
||||
}
|
||||
return this.actionService.doAction(action);
|
||||
}
|
||||
|
||||
async goToAttachment() {
|
||||
return this.actionService.doAction({
|
||||
name: this.env._t("Attachments"),
|
||||
type: "ir.actions.act_window",
|
||||
res_model: "mrp.document",
|
||||
domain: [["id", "in", this.data.attachment_ids]],
|
||||
views: [[false, "kanban"], [false, "list"], [false, "form"]],
|
||||
view_mode: "kanban,list,form",
|
||||
target: "current",
|
||||
});
|
||||
}
|
||||
|
||||
onToggleFolded() {
|
||||
if (this.props.parentId) {
|
||||
this.props.bus.trigger(`toggle-fold-${this.props.parentId}`, this.identifier);
|
||||
}
|
||||
}
|
||||
|
||||
//---- Getters ----
|
||||
|
||||
get data() {
|
||||
return this.props.data;
|
||||
}
|
||||
|
||||
get identifier() {
|
||||
return `${this.data.type}_${this.data.index}`;
|
||||
}
|
||||
|
||||
get hasComponents() {
|
||||
return this.data.components && this.data.components.length > 0;
|
||||
}
|
||||
|
||||
get hasQuantity() {
|
||||
return this.data.hasOwnProperty('quantity_available') && this.data.quantity_available !== false;
|
||||
}
|
||||
|
||||
get hasLeadTime() {
|
||||
return this.data.hasOwnProperty('lead_time') && this.data.lead_time !== false;
|
||||
}
|
||||
|
||||
get hasFoldButton() {
|
||||
return this.data.level > 0 && this.hasComponents;
|
||||
}
|
||||
|
||||
get marginMultiplicator() {
|
||||
return this.data.level - (this.hasFoldButton ? 1 : 0);
|
||||
}
|
||||
|
||||
get showAvailabilities() {
|
||||
return this.props.showOptions.availabilities;
|
||||
}
|
||||
|
||||
get showCosts() {
|
||||
return this.props.showOptions.costs;
|
||||
}
|
||||
|
||||
get showLeadTimes() {
|
||||
return this.props.showOptions.leadTimes;
|
||||
}
|
||||
|
||||
get showUom() {
|
||||
return this.props.showOptions.uom;
|
||||
}
|
||||
|
||||
get showAttachments() {
|
||||
return this.props.showOptions.attachments;
|
||||
}
|
||||
|
||||
get availabilityColorClass() {
|
||||
// For first line, another rule applies : green if doable now, red otherwise.
|
||||
if (this.data.hasOwnProperty('components_available')) {
|
||||
if (this.data.components_available && this.data.availability_state != 'unavailable') {
|
||||
return "text-success";
|
||||
} else {
|
||||
return "text-danger";
|
||||
}
|
||||
}
|
||||
switch (this.data.availability_state) {
|
||||
case "available":
|
||||
return "text-success";
|
||||
case "expected":
|
||||
return "text-warning";
|
||||
case "unavailable":
|
||||
return "text-danger";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
get forecastAction() {
|
||||
switch (this.data.link_model) {
|
||||
case "product.product":
|
||||
return "action_product_forecast_report";
|
||||
case "product.template":
|
||||
return "action_product_tmpl_forecast_report";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BomOverviewLine.template = "mrp.BomOverviewLine";
|
||||
BomOverviewLine.props = {
|
||||
bus: Object,
|
||||
isFolded: { type: Boolean, optional: true },
|
||||
parentId: { type: String, optional: true },
|
||||
showOptions: {
|
||||
type: Object,
|
||||
shape: {
|
||||
availabilities: Boolean,
|
||||
costs: Boolean,
|
||||
operations: Boolean,
|
||||
leadTimes: Boolean,
|
||||
uom: Boolean,
|
||||
attachments: Boolean,
|
||||
},
|
||||
},
|
||||
currentWarehouseId: { type: Number, optional: true },
|
||||
data: Object,
|
||||
};
|
||||
BomOverviewLine.defaultProps = {
|
||||
isFolded: true,
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<tr t-name="mrp.BomOverviewLine" owl="1">
|
||||
<td name="td_mrp_bom">
|
||||
<div t-attf-style="margin-left: {{ marginMultiplicator * 20 }}px">
|
||||
<t t-if="data.level > 0 && hasComponents">
|
||||
<button t-on-click="onToggleFolded" class="o_mrp_bom_unfoldable btn btn-light p-0" t-attf-aria-label="{{ props.isFolded ? 'Unfold' : 'Fold' }}" t-attf-title="{{ props.isFolded ? 'Unfold' : 'Fold' }}" style="margin-right: 1px">
|
||||
<i t-attf-class="fa fa-fw fa-caret-{{ props.isFolded ? 'right' : 'down' }}" role="img"/>
|
||||
</button>
|
||||
</t>
|
||||
<div t-attf-class="d-inline-block">
|
||||
<a href="#" t-on-click.prevent="() => this.goToAction(data.link_id, data.link_model)" t-esc="data.name"/>
|
||||
</div>
|
||||
<t t-if="data.phantom_bom">
|
||||
<div class="fa fa-dropbox" title="This is a BoM of type Kit!" role="img" aria-label="This is a BoM of type Kit!"/>
|
||||
</t>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<t t-if="data.type == 'operation'" t-esc="formatFloatTime(data.quantity)"/>
|
||||
<t t-else="" t-esc="formatFloat(data.quantity)"/>
|
||||
</td>
|
||||
<td t-if="showUom" class="text-start" t-esc="data.uom_name"/>
|
||||
<td t-if="showAvailabilities" class="text-end">
|
||||
<t t-if="data.hasOwnProperty('producible_qty')" t-esc="data.producible_qty"/>
|
||||
</td>
|
||||
<td t-if="showAvailabilities" class="text-end">
|
||||
<t t-if="hasQuantity">
|
||||
<t t-esc="formatFloat(data.quantity_available)"/> /
|
||||
<t t-esc="formatFloat(data.quantity_on_hand)"/>
|
||||
</t>
|
||||
</td>
|
||||
<td t-if="showAvailabilities" class="text-center">
|
||||
<t t-if="data.hasOwnProperty('availability_state')">
|
||||
<span t-attf-class="{{availabilityColorClass}}" t-esc="data.availability_display"/>
|
||||
<a href="#" role="button" t-on-click.prevent="goToForecast" title="Forecast Report" t-attf-class="fa fa-fw fa-area-chart o_mrp_bom_forecast ms-1 {{availabilityColorClass}}"/>
|
||||
</t>
|
||||
</td>
|
||||
<td t-if="showLeadTimes" class="text-end">
|
||||
<span t-if="hasLeadTime"><t t-esc="data.lead_time"/> Days</span>
|
||||
</td>
|
||||
<td>
|
||||
<div t-if="data.route_name">
|
||||
<span><t t-esc="data.route_name"/>: </span>
|
||||
<a href="#" t-on-click.prevent="() => this.goToRoute(data.route_type)" t-esc="data.route_detail"/>
|
||||
</div>
|
||||
</td>
|
||||
<td t-if="showCosts" t-attf-class="text-end {{ data.type == 'component' ? 'opacity-50' : '' }}" t-esc="formatMonetary(data.bom_cost)"/>
|
||||
<td t-if="showCosts" class="text-end">
|
||||
<span t-if="data.hasOwnProperty('prod_cost')" t-esc="formatMonetary(data.prod_cost)"/>
|
||||
</td>
|
||||
<td t-if="showAttachments" class="text-center">
|
||||
<span t-if="!!data.attachment_ids && data.attachment_ids.length > 0">
|
||||
<a href="#" role="button" t-on-click.prevent="goToAttachment" class="fa fa-fw o_button_icon fa-files-o"/>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</templates>
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { formatFloat, formatFloatTime, formatMonetary } from "@web/views/fields/formatters";
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class BomOverviewSpecialLine extends Component {
|
||||
setup() {
|
||||
this.formatFloat = formatFloat;
|
||||
this.formatFloatTime = formatFloatTime;
|
||||
this.formatMonetary = (val) => formatMonetary(val, { currencyId: this.data.currency_id });
|
||||
}
|
||||
|
||||
//---- Handlers ----
|
||||
|
||||
onToggleFolded() {
|
||||
if (this.props.parentId) {
|
||||
this.props.bus.trigger(`toggle-fold-${this.props.parentId}`);
|
||||
}
|
||||
}
|
||||
|
||||
//---- Getters ----
|
||||
|
||||
get data() {
|
||||
return this.props.data;
|
||||
}
|
||||
|
||||
get hasFoldButton() {
|
||||
return ["operations", "byproducts"].includes(this.props.type);
|
||||
}
|
||||
|
||||
get showAvailabilities() {
|
||||
return this.props.showOptions.availabilities;
|
||||
}
|
||||
|
||||
get showCosts() {
|
||||
return this.props.showOptions.costs;
|
||||
}
|
||||
|
||||
get showLeadTimes() {
|
||||
return this.props.showOptions.leadTimes;
|
||||
}
|
||||
|
||||
get showUom() {
|
||||
return this.props.showOptions.uom;
|
||||
}
|
||||
|
||||
get showAttachments() {
|
||||
return this.props.showOptions.attachments;
|
||||
}
|
||||
}
|
||||
|
||||
BomOverviewSpecialLine.template = "mrp.BomOverviewSpecialLine";
|
||||
BomOverviewSpecialLine.props = {
|
||||
bus: Object,
|
||||
type: String,
|
||||
parentId: { type: String, optional: true },
|
||||
isFolded: { type: Boolean, optional: true },
|
||||
showOptions: {
|
||||
type: Object,
|
||||
shape: {
|
||||
availabilities: Boolean,
|
||||
costs: Boolean,
|
||||
operations: Boolean,
|
||||
leadTimes: Boolean,
|
||||
uom: Boolean,
|
||||
attachments: Boolean,
|
||||
},
|
||||
},
|
||||
data: Object,
|
||||
toggleFolded: { type: Function, optional: true },
|
||||
};
|
||||
BomOverviewSpecialLine.defaultProps = {
|
||||
isFolded: true,
|
||||
toggleFolded: () => {},
|
||||
};
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<tr t-name="mrp.BomOverviewSpecialLine" owl="1">
|
||||
<td name="td_mrp_bom">
|
||||
<span t-attf-style="margin-left: {{ data.level * 20 }}px"/>
|
||||
<button t-if="hasFoldButton" t-on-click="onToggleFolded" t-attf-class="o_mrp_bom_{{ props.isFolded ? 'unfoldable' : 'foldable' }} btn btn-light ps-0 py-0" t-attf-aria-label="{{ props.isFolded ? 'Unfold' : 'Fold' }}" t-attf-title="{{ props.isFolded ? 'Unfold' : 'Fold' }}">
|
||||
<i t-attf-class="fa fa-fw fa-caret-{{ props.isFolded ? 'right' : 'down' }}" role="img"/>
|
||||
<t t-if="props.type == 'operations'">Operations</t>
|
||||
<t t-elif="props.type == 'byproducts'">By-Products</t>
|
||||
</button>
|
||||
</td>
|
||||
<td name="quantity" class="text-end">
|
||||
<span t-if="props.type == 'operations'" t-esc="formatFloatTime(data.operations_time)"/>
|
||||
<span t-elif="props.type == 'byproducts'" t-esc="formatFloat(data.byproducts_total)"/>
|
||||
</td>
|
||||
<td name="uom" t-if="showUom" class="text-start">
|
||||
<span t-if="props.type == 'operations'">Minutes</span>
|
||||
</td>
|
||||
<td t-if="showAvailabilities"/>
|
||||
<td t-if="showAvailabilities"/>
|
||||
<td t-if="showAvailabilities"/>
|
||||
<td t-if="showLeadTimes"/>
|
||||
<td/>
|
||||
<td name="bom_cost" t-if="showCosts" class="text-end">
|
||||
<span t-if="props.type == 'operations'" t-esc="formatMonetary(data.operations_cost)"/>
|
||||
<span t-elif="props.type == 'byproducts'" t-esc="formatMonetary(data.byproducts_cost)"/>
|
||||
</td>
|
||||
<td name="prod_cost" t-if="showCosts" class="text-end"/>
|
||||
<td t-if="showAttachments"/>
|
||||
</tr>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,84 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { formatMonetary, formatFloat } from "@web/views/fields/formatters";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { BomOverviewLine } from "../bom_overview_line/mrp_bom_overview_line";
|
||||
import { BomOverviewComponentsBlock } from "../bom_overview_components_block/mrp_bom_overview_components_block";
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class BomOverviewTable extends Component {
|
||||
setup() {
|
||||
this.actionService = useService("action");
|
||||
this.formatFloat = formatFloat;
|
||||
this.formatMonetary = (val) => formatMonetary(val, { currencyId: this.data.currency_id });
|
||||
}
|
||||
|
||||
//---- Handlers ----
|
||||
|
||||
async goToProduct() {
|
||||
return this.actionService.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
res_model: this.data.link_model,
|
||||
res_id: this.data.link_id,
|
||||
views: [[false, "form"]],
|
||||
target: "current",
|
||||
context: {
|
||||
active_id: this.data.link_id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
//---- Getters ----
|
||||
|
||||
get data() {
|
||||
return this.props.data;
|
||||
}
|
||||
|
||||
get showAvailabilities() {
|
||||
return this.props.showOptions.availabilities;
|
||||
}
|
||||
|
||||
get showCosts() {
|
||||
return this.props.showOptions.costs;
|
||||
}
|
||||
|
||||
get showOperations() {
|
||||
return this.props.showOptions.operations;
|
||||
}
|
||||
|
||||
get showLeadTimes() {
|
||||
return this.props.showOptions.leadTimes;
|
||||
}
|
||||
|
||||
get showUom() {
|
||||
return this.props.showOptions.uom;
|
||||
}
|
||||
|
||||
get showAttachments() {
|
||||
return this.props.showOptions.attachments;
|
||||
}
|
||||
}
|
||||
|
||||
BomOverviewTable.template = "mrp.BomOverviewTable";
|
||||
BomOverviewTable.components = {
|
||||
BomOverviewLine,
|
||||
BomOverviewComponentsBlock,
|
||||
};
|
||||
BomOverviewTable.props = {
|
||||
bus: Object,
|
||||
showOptions: {
|
||||
type: Object,
|
||||
shape: {
|
||||
availabilities: Boolean,
|
||||
costs: Boolean,
|
||||
operations: Boolean,
|
||||
leadTimes: Boolean,
|
||||
uom: Boolean,
|
||||
attachments: Boolean,
|
||||
},
|
||||
},
|
||||
uomName: { type: String, optional: true },
|
||||
currentWarehouseId: { type: Number, optional: true },
|
||||
data: Object,
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<div t-name="mrp.BomOverviewTable" class="o_content" owl="1">
|
||||
<div class="o_mrp_bom_report_page py-3 py-lg-5 px-0 overflow-auto border-bottom bg-white">
|
||||
<div t-if="!!data.components || !!data.lines || !!data.operations" class="container-fluid">
|
||||
<div class="d-flex mb-5">
|
||||
<div class="me-auto">
|
||||
<h2><a href="#" t-on-click.prevent="goToProduct" t-esc="data.name"/></h2>
|
||||
<hr t-if="data.bom_code"/>
|
||||
<h6 t-if="data.bom_code">Reference: <t t-esc="data.bom_code"/></h6>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<h3><t t-esc="formatFloat(data.quantity_available)"/> <t t-if="showUom" t-esc="props.uomName"/></h3>
|
||||
<span>Free to Use</span>
|
||||
</div>
|
||||
<div t-if="data.hasOwnProperty('earliest_capacity')" class="ps-5 text-center">
|
||||
<h3><t t-esc="formatFloat(data.earliest_capacity)"/> <t t-if="showUom" t-esc="props.uomName"/></h3>
|
||||
<span t-esc="data.earliest_date"/>
|
||||
</div>
|
||||
<div t-if="data.hasOwnProperty('leftover_capacity')" class="ps-5 text-center">
|
||||
<h3><t t-esc="formatFloat(data.leftover_capacity)"/> <t t-if="showUom" t-esc="props.uomName"/></h3>
|
||||
<span t-esc="data.leftover_date"/>
|
||||
</div>
|
||||
</div>
|
||||
<table class="o_mrp_bom_expandable table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th name="th_mrp_bom_h">Product</th>
|
||||
<th t-attf-class="{{ showUom ? 'text-center' : 'text-end' }}" t-attf-colspan="{{ showUom ? 2 : 1 }}">Quantity</th>
|
||||
<th t-if="showAvailabilities" class="text-end">Ready to Produce</th>
|
||||
<th t-if="showAvailabilities" class="text-end" title="Availabilities on products.">Free to Use / On Hand</th>
|
||||
<th t-if="showAvailabilities" class="text-center" title="Reception time estimation.">Availability</th>
|
||||
<th t-if="showLeadTimes" class="text-end" title="Resupply lead time.">Lead Time</th>
|
||||
<th>Route</th>
|
||||
<th t-if="showCosts" class="text-end" title="This is the cost based on the BoM of the product. It is computed by summing the costs of the components and operations needed to build the product.">BoM Cost</th>
|
||||
<th t-if="showCosts" class="text-end" title="This is the cost defined on the product.">Product Cost</th>
|
||||
<th t-if="showAttachments" class="text-center" title="Files attached to the product.">Attachments</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<BomOverviewLine
|
||||
bus="props.bus"
|
||||
showOptions="props.showOptions"
|
||||
currentWarehouseId="props.currentWarehouseId"
|
||||
data="data"/>
|
||||
|
||||
<BomOverviewComponentsBlock
|
||||
bus="props.bus"
|
||||
showOptions="props.showOptions"
|
||||
currentWarehouseId="props.currentWarehouseId"
|
||||
data="data"/>
|
||||
</tbody>
|
||||
<tfoot t-if="showCosts">
|
||||
<tr>
|
||||
<td name="td_mrp_bom_f" class="text-end">
|
||||
<span t-if="!!data.byproducts && data.byproducts.length > 0" t-esc="data.name"/>
|
||||
</td>
|
||||
<td class="text-end"><strong>Unit Cost</strong></td>
|
||||
<td t-if="showUom"/>
|
||||
<td t-if="showAvailabilities"/>
|
||||
<td t-if="showAvailabilities"/>
|
||||
<td t-if="showAvailabilities"/>
|
||||
<td t-if="showLeadTimes"/>
|
||||
<td/>
|
||||
<td class="text-end" t-esc="formatMonetary(data.cost_share * data.bom_cost / data.quantity)"/>
|
||||
<td class="text-end" t-esc="formatMonetary(data.prod_cost / data.quantity)"/>
|
||||
<td t-if="showAttachments"/>
|
||||
</tr>
|
||||
<t t-if="data.byproducts && data.byproducts.length > 0" t-foreach="data.byproducts" t-as="byproduct" t-key="byproduct.id">
|
||||
<tr>
|
||||
<td name="td_mrp_bom_b" class="text-end" t-esc="byproduct.name"/>
|
||||
<td class="text-end"><strong>Unit Cost</strong></td>
|
||||
<td t-if="showUom"/>
|
||||
<td t-if="showAvailabilities"/>
|
||||
<td t-if="showAvailabilities"/>
|
||||
<td t-if="showAvailabilities"/>
|
||||
<td t-if="showLeadTimes"/>
|
||||
<td/>
|
||||
<td class="text-end" t-esc="formatMonetary(byproduct.cost_share * data.bom_cost / byproduct.quantity)"/>
|
||||
<td class="text-end" t-esc="formatMonetary(byproduct.prod_cost / byproduct.quantity)"/>
|
||||
<td t-if="showAttachments"/>
|
||||
</tr>
|
||||
</t>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div t-else="" class="d-flex align-items-center justify-content-center h-50">
|
||||
<h4 class="text-muted">No data available.</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -1,282 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import core from 'web.core';
|
||||
import stock_report_generic from 'stock.stock_report_generic';
|
||||
|
||||
var QWeb = core.qweb;
|
||||
var _t = core._t;
|
||||
|
||||
var MrpBomReport = stock_report_generic.extend({
|
||||
events: {
|
||||
'click .o_mrp_bom_unfoldable': '_onClickUnfold',
|
||||
'click .o_mrp_bom_foldable': '_onClickFold',
|
||||
'click .o_mrp_bom_action': '_onClickAction',
|
||||
'click .o_mrp_show_attachment_action': '_onClickShowAttachment',
|
||||
},
|
||||
get_html: function() {
|
||||
var self = this;
|
||||
var args = [
|
||||
this.given_context.active_id,
|
||||
this.given_context.searchQty || false,
|
||||
this.given_context.searchVariant,
|
||||
];
|
||||
return this._rpc({
|
||||
model: 'report.mrp.report_bom_structure',
|
||||
method: 'get_html',
|
||||
args: args,
|
||||
context: this.given_context,
|
||||
})
|
||||
.then(function (result) {
|
||||
self.data = result;
|
||||
if (! self.given_context.searchVariant) {
|
||||
self.given_context.searchVariant = result.is_variant_applied && Object.keys(result.variants)[0];
|
||||
}
|
||||
});
|
||||
},
|
||||
set_html: function() {
|
||||
var self = this;
|
||||
return this._super().then(function () {
|
||||
self.$('.o_content').html(self.data.lines);
|
||||
self.renderSearch();
|
||||
self.update_cp();
|
||||
});
|
||||
},
|
||||
render_html: function(event, $el, result){
|
||||
if (result.indexOf('mrp.document') > 0) {
|
||||
if (this.$('.o_mrp_has_attachments').length === 0) {
|
||||
var column = $('<th/>', {
|
||||
class: 'o_mrp_has_attachments',
|
||||
title: 'Files attached to the product Attachments',
|
||||
text: 'Attachments',
|
||||
});
|
||||
this.$('table thead th:last-child').after(column);
|
||||
}
|
||||
}
|
||||
$el.after(result);
|
||||
$(event.currentTarget).toggleClass('o_mrp_bom_foldable o_mrp_bom_unfoldable').find('i').toggleClass('fa-caret-right fa-caret-down');
|
||||
this._reload_report_type();
|
||||
},
|
||||
get_bom: function(event) {
|
||||
var self = this;
|
||||
var $parent = $(event.currentTarget).closest('tr');
|
||||
var activeID = $parent.data('id');
|
||||
var productID = $parent.data('product_id');
|
||||
var lineID = $parent.data('line');
|
||||
var qty = $parent.data('qty');
|
||||
var level = $parent.data('level') || 0;
|
||||
return this._rpc({
|
||||
model: 'report.mrp.report_bom_structure',
|
||||
method: 'get_bom',
|
||||
args: [
|
||||
activeID,
|
||||
productID,
|
||||
parseFloat(qty),
|
||||
lineID,
|
||||
level + 1,
|
||||
]
|
||||
})
|
||||
.then(function (result) {
|
||||
self.render_html(event, $parent, result);
|
||||
});
|
||||
},
|
||||
get_operations: function(event) {
|
||||
var self = this;
|
||||
var $parent = $(event.currentTarget).closest('tr');
|
||||
var activeID = $parent.data('bom-id');
|
||||
var qty = $parent.data('qty');
|
||||
var productId = $parent.data('product_id');
|
||||
var level = $parent.data('level') || 0;
|
||||
return this._rpc({
|
||||
model: 'report.mrp.report_bom_structure',
|
||||
method: 'get_operations',
|
||||
args: [
|
||||
productId,
|
||||
activeID,
|
||||
parseFloat(qty),
|
||||
level + 1
|
||||
]
|
||||
})
|
||||
.then(function (result) {
|
||||
self.render_html(event, $parent, result);
|
||||
});
|
||||
},
|
||||
get_byproducts: function(event) {
|
||||
var self = this;
|
||||
var $parent = $(event.currentTarget).closest('tr');
|
||||
var activeID = $parent.data('bom-id');
|
||||
var qty = $parent.data('qty');
|
||||
var level = $parent.data('level') || 0;
|
||||
var total = $parent.data('total') || 0;
|
||||
return this._rpc({
|
||||
model: 'report.mrp.report_bom_structure',
|
||||
method: 'get_byproducts',
|
||||
args: [
|
||||
activeID,
|
||||
parseFloat(qty),
|
||||
level + 1,
|
||||
parseFloat(total)
|
||||
]
|
||||
})
|
||||
.then(function (result) {
|
||||
self.render_html(event, $parent, result);
|
||||
});
|
||||
},
|
||||
update_cp: function () {
|
||||
var status = {
|
||||
cp_content: {
|
||||
$buttons: this.$buttonsPanel,
|
||||
$searchview: this.$searchView
|
||||
},
|
||||
};
|
||||
return this.updateControlPanel(status);
|
||||
},
|
||||
getControlPanelProps: function() {
|
||||
this.renderSearch();
|
||||
return {
|
||||
$buttons: this.$buttonsPanel,
|
||||
$searchview: this.$searchView
|
||||
}
|
||||
},
|
||||
renderSearch: function () {
|
||||
this.$buttonsPanel = $(QWeb.render('mrp.button', {'is_variant_applied': this.data.is_variant_applied}));
|
||||
this.$buttonsPanel.find('.o_mrp_bom_print').on('click', this._onClickPrint.bind(this));
|
||||
this.$buttonsPanel.find('.o_mrp_bom_print_all_variants').on('click', this._onClickPrint.bind(this));
|
||||
this.$buttonsPanel.find('.o_mrp_bom_unfold_all').on('click', this._onClickUnfoldAll.bind(this));
|
||||
this.$searchView = $(QWeb.render('mrp.report_bom_search', _.omit(this.data, 'lines')));
|
||||
this.$searchView.find('.o_mrp_bom_report_qty').on('change', this._onChangeQty.bind(this)).change();
|
||||
this.$searchView.find('.o_mrp_bom_report_qty').on('keydown', this._onKeyDownQty.bind(this));
|
||||
this.$searchView.find('.o_mrp_bom_report_variants').on('change', this._onChangeVariants.bind(this)).change();
|
||||
this.$searchView.find('.o_mrp_bom_report_type').on('change', this._onChangeType.bind(this));
|
||||
},
|
||||
_onClickPrint: function (ev) {
|
||||
var reportname = 'mrp.report_bom_structure?docids=' + this.given_context.active_id +
|
||||
'&report_type=' + this.given_context.report_type +
|
||||
'&quantity=' + (this.given_context.searchQty || 1) +
|
||||
'&unfolded_ids=' + JSON.stringify(this._unfoldedIDs());
|
||||
if ($(ev.currentTarget).hasClass('o_mrp_bom_print_all_variants')) {
|
||||
reportname += '&all_variants=' + 1;
|
||||
} else if (this.given_context.searchVariant) {
|
||||
reportname += '&variant=' + this.given_context.searchVariant;
|
||||
}
|
||||
var action = {
|
||||
'type': 'ir.actions.report',
|
||||
'report_type': 'qweb-pdf',
|
||||
'report_name': reportname,
|
||||
'report_file': 'mrp.report_bom_structure',
|
||||
};
|
||||
return this.do_action(action);
|
||||
},
|
||||
_onChangeQty: function (ev) {
|
||||
var qty = $(ev.currentTarget).val().trim();
|
||||
if (qty) {
|
||||
this.given_context.searchQty = parseFloat(qty);
|
||||
this._reload();
|
||||
}
|
||||
},
|
||||
_onKeyDownQty: function (ev) {
|
||||
if(ev.which == 13) {
|
||||
ev.preventDefault();
|
||||
this._onChangeQty(ev);
|
||||
}
|
||||
},
|
||||
_onChangeType: function (ev) {
|
||||
var report_type = $("option:selected", $(ev.currentTarget)).data('type');
|
||||
this.given_context.report_type = report_type;
|
||||
this._reload_report_type();
|
||||
},
|
||||
_onChangeVariants: function (ev) {
|
||||
this.given_context.searchVariant = $(ev.currentTarget).val();
|
||||
this._reload();
|
||||
},
|
||||
_onClickUnfold: function (ev) {
|
||||
var redirect_function = $(ev.currentTarget).data('function');
|
||||
this[redirect_function](ev);
|
||||
},
|
||||
_onClickFold: function (ev) {
|
||||
this._removeLines($(ev.currentTarget).closest('tr'));
|
||||
$(ev.currentTarget).toggleClass('o_mrp_bom_foldable o_mrp_bom_unfoldable').find('i').toggleClass('fa-caret-right fa-caret-down');
|
||||
},
|
||||
_onClickUnfoldAll: function (ev) {
|
||||
this._unfold();
|
||||
},
|
||||
_onClickAction: function (ev) {
|
||||
ev.preventDefault();
|
||||
return this.do_action({
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: $(ev.currentTarget).data('model'),
|
||||
res_id: $(ev.currentTarget).data('res-id'),
|
||||
context: {
|
||||
'active_id': $(ev.currentTarget).data('res-id')
|
||||
},
|
||||
views: [[false, 'form']],
|
||||
target: 'current'
|
||||
});
|
||||
},
|
||||
_onClickShowAttachment: function (ev) {
|
||||
ev.preventDefault();
|
||||
var ids = $(ev.currentTarget).data('res-id');
|
||||
return this.do_action({
|
||||
name: _t('Attachments'),
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: $(ev.currentTarget).data('model'),
|
||||
domain: [['id', 'in', ids]],
|
||||
views: [[false, 'kanban'], [false, 'list'], [false, 'form']],
|
||||
view_mode: 'kanban,list,form',
|
||||
target: 'current',
|
||||
});
|
||||
},
|
||||
_reload: function () {
|
||||
const unfolded_ids = this._unfoldedIDs();
|
||||
var self = this;
|
||||
return this.get_html().then(function () {
|
||||
self.$('.o_content').html(self.data.lines);
|
||||
self._reload_report_type();
|
||||
if (unfolded_ids.length) {
|
||||
self.$('.o_content').css("display", "none");
|
||||
self._unfold(unfolded_ids);
|
||||
}
|
||||
});
|
||||
},
|
||||
_reload_report_type: function () {
|
||||
this.$('.o_mrp_bom_cost.o_hidden, .o_mrp_prod_cost.o_hidden').toggleClass('o_hidden');
|
||||
if (this.given_context.report_type === 'bom_structure') {
|
||||
this.$('.o_mrp_bom_cost, .o_mrp_prod_cost').toggleClass('o_hidden');
|
||||
}
|
||||
},
|
||||
_removeLines: function ($el) {
|
||||
var self = this;
|
||||
var activeID = $el.data('id');
|
||||
_.each(this.$('tr[parent_id='+ activeID +']'), function (parent) {
|
||||
var $parent = self.$(parent);
|
||||
var $el = self.$('tr[parent_id='+ $parent.data('id') +']');
|
||||
if ($el.length) {
|
||||
self._removeLines($parent);
|
||||
}
|
||||
$parent.remove();
|
||||
});
|
||||
},
|
||||
_unfoldedIDs: function() {
|
||||
return _.map(this.$('.o_mrp_bom_foldable').closest('tr'), el => $(el).data('id'));
|
||||
},
|
||||
_unfold: function (ids) {
|
||||
const promises = [];
|
||||
for (const element of this.el.querySelectorAll('.o_mrp_bom_unfoldable')) {
|
||||
const id = $(element).closest('tr').data('id');
|
||||
if (ids && !ids.includes(id)) {
|
||||
continue;
|
||||
}
|
||||
const methodName = element.dataset.function;
|
||||
promises.push(this[methodName]({currentTarget: element}));
|
||||
}
|
||||
Promise.all(promises).then(values => {
|
||||
if (values.length) {
|
||||
this._unfold(ids);
|
||||
} else if (ids) {
|
||||
this.$('.o_content').css("display", "inherit");
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
core.action_registry.add('mrp_bom_report', MrpBomReport);
|
||||
export default MrpBomReport;
|
||||
@@ -1,45 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-name="mrp.button">
|
||||
<div class="o_list_buttons o_mrp_bom_report_buttons">
|
||||
<button type="button" class="btn btn-primary o_mrp_bom_print">Print</button>
|
||||
<t t-if="is_variant_applied">
|
||||
<button type="button" class="btn btn-primary o_mrp_bom_print_all_variants">Print All Variants</button>
|
||||
</t>
|
||||
<button type="button" class="btn btn-primary o_mrp_bom_unfold_all">Unfold</button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<form class="container-fluid w-lg-75 w-xl-auto pt-lg-3 me-0" t-name="mrp.report_bom_search">
|
||||
<div class="row gap-3">
|
||||
<div class="col-12 col-md">
|
||||
<div class="row">
|
||||
<label class="col-3 col-xl flex-grow-0">Quantity:</label>
|
||||
<input type="number" step="any" t-att-value="bom_qty" min="1" class="o_input o_mrp_bom_report_qty col"/>
|
||||
<div t-if="is_uom_applied" class="col" t-out="bom_uom_name"/>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="is_variant_applied" class="col-12 col-md">
|
||||
<div class="row">
|
||||
<label class="col-3 col-xl flex-grow-0">Variant:</label>
|
||||
<select class="o_input o_mrp_bom_report_variants col">
|
||||
<option t-foreach="variants" t-as="variant" t-att-value="variant">
|
||||
<t t-esc="variants[variant]"/>
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div t-attf-class="col-12 col-xl">
|
||||
<div class="row">
|
||||
<label class="col-3 col-xl flex-grow-0">Report:</label>
|
||||
<select class="o_input o_mrp_bom_report_type col">
|
||||
<option t-att-data-type="'all'">BoM Structure & Cost</option>
|
||||
<option t-att-data-type="'bom_structure'">BoM Structure</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div t-name="mrp.workorderPopover">
|
||||
<t t-foreach="infos" t-as="info">
|
||||
|
||||
@@ -538,15 +538,15 @@ class TestBoM(TestMrpCommon):
|
||||
|
||||
for component_line in report_values['lines']['components']:
|
||||
# standard price * bom line quantity * current quantity / bom finished product quantity
|
||||
if component_line['prod_id'] == butter.id:
|
||||
if component_line['product'].id == butter.id:
|
||||
# 5 kg of butter at 7.01$ for 11kg of crumble -> 35.05$
|
||||
self.assertEqual(float_compare(component_line['total'], (7.01 * 5), precision_digits=2), 0)
|
||||
if component_line['prod_id'] == biscuit.id:
|
||||
self.assertEqual(float_compare(component_line['bom_cost'], (7.01 * 5), precision_digits=2), 0)
|
||||
if component_line['product'].id == biscuit.id:
|
||||
# 6 kg of biscuits at 1.50$ for 11kg of crumble -> 9$
|
||||
self.assertEqual(float_compare(component_line['total'], (1.5 * 6), precision_digits=2), 0)
|
||||
self.assertEqual(float_compare(component_line['bom_cost'], (1.5 * 6), precision_digits=2), 0)
|
||||
# total price = 35.05 + 9 + operation_cost(23.84) = 67.89
|
||||
self.assertEqual(float_compare(report_values['lines']['total'], 67.89, precision_digits=2), 0, 'Product Bom Price is not correct')
|
||||
self.assertEqual(float_compare(report_values['lines']['total'] / 11.0, 6.17, precision_digits=2), 0, 'Product Unit Bom Price is not correct')
|
||||
self.assertEqual(float_compare(report_values['lines']['bom_cost'], 67.89, precision_digits=2), 0, 'Product Bom Price is not correct')
|
||||
self.assertEqual(float_compare(report_values['lines']['bom_cost'] / 11.0, 6.17, precision_digits=2), 0, 'Product Unit Bom Price is not correct')
|
||||
|
||||
# TEST BOM STRUCTURE VALUE BY UNIT
|
||||
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=1, searchVariant=False)
|
||||
@@ -558,14 +558,14 @@ class TestBoM(TestMrpCommon):
|
||||
|
||||
for component_line in report_values['lines']['components']:
|
||||
# standard price * bom line quantity * current quantity / bom finished product quantity
|
||||
if component_line['prod_id'] == butter.id:
|
||||
if component_line['product'].id == butter.id:
|
||||
# 5 kg of butter at 7.01$ for 11kg of crumble -> / 11 for price per unit (3.19)
|
||||
self.assertEqual(float_compare(component_line['total'], (7.01 * 5) * (1 / 11), precision_digits=2), 0)
|
||||
if component_line['prod_id'] == biscuit.id:
|
||||
self.assertEqual(float_compare(component_line['bom_cost'], (7.01 * 5) * (1 / 11), precision_digits=2), 0)
|
||||
if component_line['product'].id == biscuit.id:
|
||||
# 6 kg of biscuits at 1.50$ for 11kg of crumble -> / 11 for price per unit (0.82)
|
||||
self.assertEqual(float_compare(component_line['total'], (1.5 * 6) * (1 / 11), precision_digits=2), 0)
|
||||
self.assertEqual(float_compare(component_line['bom_cost'], (1.5 * 6) * (1 / 11), precision_digits=2), 0)
|
||||
# total price = 3.19 + 0.82 + operation_cost(0.83 + 0.83 + 0.5 = 2.16) = 6,17
|
||||
self.assertEqual(float_compare(report_values['lines']['total'], 6.17, precision_digits=2), 0, 'Product Unit Bom Price is not correct')
|
||||
self.assertEqual(float_compare(report_values['lines']['bom_cost'], 6.17, precision_digits=2), 0, 'Product Unit Bom Price is not correct')
|
||||
|
||||
# TEST OPERATION COST WHEN PRODUCED QTY > BOM QUANTITY
|
||||
report_values_12 = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=12, searchVariant=False)
|
||||
@@ -628,15 +628,15 @@ class TestBoM(TestMrpCommon):
|
||||
|
||||
for component_line in report_values['lines']['components']:
|
||||
# standard price * bom line quantity * current quantity / bom finished product quantity
|
||||
if component_line['prod_id'] == cream.id:
|
||||
if component_line['product'].id == cream.id:
|
||||
# 3 liter of cream at 5.17$ for 60 unit of cheese cake -> 15.51$
|
||||
self.assertEqual(float_compare(component_line['total'], (3 * 5.17), precision_digits=2), 0)
|
||||
if component_line['prod_id'] == crumble.id:
|
||||
self.assertEqual(float_compare(component_line['bom_cost'], (3 * 5.17), precision_digits=2), 0)
|
||||
if component_line['product'].id == crumble.id:
|
||||
# 5.4 kg of crumble at the cost of a batch.
|
||||
crumble_cost = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=5.4, searchVariant=False)['lines']['total']
|
||||
self.assertEqual(float_compare(component_line['total'], crumble_cost, precision_digits=2), 0)
|
||||
crumble_cost = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=5.4, searchVariant=False)['lines']['bom_cost']
|
||||
self.assertEqual(float_compare(component_line['bom_cost'], crumble_cost, precision_digits=2), 0)
|
||||
# total price = Cream (15.51€) + crumble_cost (34.63 €) + operation_cost(208,33) = 258.47€
|
||||
self.assertEqual(float_compare(report_values['lines']['total'], 258.47, precision_digits=2), 0, 'Product Bom Price is not correct')
|
||||
self.assertEqual(float_compare(report_values['lines']['bom_cost'], 258.47, precision_digits=2), 0, 'Product Bom Price is not correct')
|
||||
|
||||
def test_bom_report_dozens(self):
|
||||
""" Simulate a drawer bom with dozens as bom units
|
||||
@@ -863,39 +863,36 @@ class TestBoM(TestMrpCommon):
|
||||
|
||||
# 10l of blue paint
|
||||
blue_paint = self.paint._get_variant_for_combination(self.paint_color_blue)
|
||||
self.assertEqual(blue_paint.id, report_values['lines']['components'][0]['prod_id'])
|
||||
self.assertEqual(report_values['lines']['components'][0]['prod_qty'], 10)
|
||||
self.assertEqual(blue_paint.id, report_values['lines']['components'][0]['product'].id)
|
||||
self.assertEqual(report_values['lines']['components'][0]['quantity'], 10)
|
||||
# 1 blue dashboard with GPS
|
||||
blue_dashboard_gps = self.dashboard._get_variant_for_combination(self.dashboard_color_blue + self.dashboard_gps_yes)
|
||||
self.assertEqual(blue_dashboard_gps.id, report_values['lines']['components'][1]['prod_id'])
|
||||
self.assertEqual(report_values['lines']['components'][1]['prod_qty'], 1)
|
||||
component = report_values['lines']['components'][1]
|
||||
report_values_dashboad = self.env['report.mrp.report_bom_structure']._get_bom(
|
||||
component['child_bom'], component['prod_id'], component['prod_qty'],
|
||||
component['line_id'], component['level'] + 1)
|
||||
self.assertEqual(blue_dashboard_gps.id, report_values['lines']['components'][1]['product'].id)
|
||||
self.assertEqual(report_values['lines']['components'][1]['quantity'], 1)
|
||||
report_values_dashboad = report_values['lines']['components'][1]
|
||||
|
||||
self.assertEqual(len(report_values_dashboad['components']), 2)
|
||||
self.assertEqual(blue_paint.id, report_values_dashboad['components'][0]['prod_id'])
|
||||
self.assertEqual(self.gps.id, report_values_dashboad['components'][1]['prod_id'])
|
||||
self.assertEqual(blue_paint.id, report_values_dashboad['components'][0]['product'].id)
|
||||
self.assertEqual(self.gps.id, report_values_dashboad['components'][1]['product'].id)
|
||||
|
||||
# 0.5l of paint at price of 20$/litre -> 10$
|
||||
self.assertEqual(report_values_dashboad['components'][0]['total'], 10)
|
||||
self.assertEqual(report_values_dashboad['components'][0]['bom_cost'], 10)
|
||||
# GPS 700$
|
||||
self.assertEqual(report_values_dashboad['components'][1]['total'], 700)
|
||||
self.assertEqual(report_values_dashboad['components'][1]['bom_cost'], 700)
|
||||
|
||||
# Dashboard blue with GPS should have a BoM cost of 710$
|
||||
self.assertEqual(report_values['lines']['components'][1]['total'], 710)
|
||||
self.assertEqual(report_values['lines']['components'][1]['bom_cost'], 710)
|
||||
# 10l of paint at price of 20$/litre -> 200$
|
||||
self.assertEqual(report_values['lines']['components'][0]['total'], 200)
|
||||
self.assertEqual(report_values['lines']['components'][0]['bom_cost'], 200)
|
||||
|
||||
# Total cost of blue car with GPS: 10 + 700 + 200 = 910
|
||||
self.assertEqual(report_values['lines']['total'], 910)
|
||||
self.assertEqual(report_values['lines']['bom_cost'], 910)
|
||||
|
||||
red_car_without_gps = self.car._get_variant_for_combination(self.car_color_red + self.car_gps_no)
|
||||
|
||||
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_car.id, searchQty=1, searchVariant=red_car_without_gps.id)
|
||||
# Same math than before but without GPS
|
||||
self.assertEqual(report_values['lines']['total'], 210)
|
||||
self.assertEqual(report_values['lines']['bom_cost'], 210)
|
||||
|
||||
def test_22_bom_report_recursive_bom(self):
|
||||
""" Test report with recursive BoM and different quantities.
|
||||
@@ -981,7 +978,7 @@ class TestBoM(TestMrpCommon):
|
||||
|
||||
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_finished.id, searchQty=80)
|
||||
|
||||
self.assertAlmostEqual(report_values['lines']['total'], 0.58)
|
||||
self.assertAlmostEqual(report_values['lines']['bom_cost'], 2.92)
|
||||
|
||||
def test_validate_no_bom_line_with_same_product(self):
|
||||
"""
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<data>
|
||||
<!-- Bill of Materials -->
|
||||
<record id="action_report_mrp_bom" model="ir.actions.client">
|
||||
<field name="name">BoM Structure & Cost</field>
|
||||
<field name="name">BoM Overview</field>
|
||||
<field name="tag">mrp_bom_report</field>
|
||||
<field name="context" eval="{'model': 'report.mrp.report_bom_structure'}" />
|
||||
</record>
|
||||
@@ -45,7 +45,7 @@
|
||||
</div>
|
||||
</button>
|
||||
<button name="%(action_report_mrp_bom)d" type="action"
|
||||
class="oe_stat_button" icon="fa-bars" string="Structure & Cost"/>
|
||||
class="oe_stat_button" icon="fa-bars" string="Overview"/>
|
||||
</div>
|
||||
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
|
||||
<group>
|
||||
|
||||
Reference in New Issue
Block a user