mirror of
https://github.com/bringout/oca-ocb-mrp.git
synced 2026-04-26 09:31:58 +02:00
19.0 vanilla
This commit is contained in:
parent
accf5918df
commit
6e65e8c877
688 changed files with 225434 additions and 199401 deletions
|
|
@ -5,3 +5,4 @@ from . import mrp_report_bom_structure
|
|||
from . import stock_forecasted
|
||||
from . import report_stock_reception
|
||||
from . import report_stock_rule
|
||||
from . import mrp_report_mo_overview
|
||||
|
|
|
|||
|
|
@ -8,47 +8,47 @@
|
|||
<div class="oe_structure"/>
|
||||
<div class="row">
|
||||
<div class="col-7">
|
||||
<h2><span t-field="o.name"/></h2>
|
||||
<h2><span t-field="o.name">MRP-001</span></h2>
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<span class="text-end">
|
||||
<div t-field="o.name" t-options="{'widget': 'barcode', 'width': 600, 'height': 100, 'img_style': 'width:350px;height:60px'}"/>
|
||||
<span t-field="o.name" t-options="{'widget': 'barcode', 'width': 600, 'height': 100, 'img_style': 'width:350px;height:60px'}">Package barcode</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt32 mb32">
|
||||
<div class="col-3" t-if="o.origin">
|
||||
<strong>Source:</strong><br/>
|
||||
<span t-field="o.origin"/>
|
||||
<span t-field="o.origin">Vendor ABC</span>
|
||||
</div>
|
||||
<div class="col-3" t-if="o.user_id">
|
||||
<strong>Responsible:</strong><br/>
|
||||
<span t-field="o.user_id"/>
|
||||
<span t-field="o.user_id">John Doe</span>
|
||||
</div>
|
||||
<div class="col-3" t-if="o.state not in ('done', 'cancel') and o.date_deadline">
|
||||
<strong>Deadline:</strong><br/>
|
||||
<span t-field="o.date_deadline"/>
|
||||
<span t-field="o.date_deadline">2023-09-15</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt32 mb32">
|
||||
<div class="col-3">
|
||||
<strong>Product:</strong><br/>
|
||||
<span t-field="o.product_id"/>
|
||||
<span t-field="o.product_id">Laptop Model X</span>
|
||||
</div>
|
||||
<div class="col-3" t-if="o.product_description_variants">
|
||||
<strong>Description:</strong><br/>
|
||||
<span t-field="o.product_description_variants"/>
|
||||
<span t-field="o.product_description_variants">Laptop with 16GB RAM</span>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<strong>Quantity to Produce:</strong><br/>
|
||||
<span t-field="o.product_qty"/>
|
||||
<span t-field="o.product_uom_id.name" groups="uom.group_uom"/>
|
||||
<span t-field="o.product_qty">100</span>
|
||||
<span t-field="o.product_uom_id.name" groups="uom.group_uom">Units</span>
|
||||
</div>
|
||||
<div class="col-3" t-if="o.qty_producing">
|
||||
<div class="col-3" t-if="o.qty_producing">
|
||||
<strong>Quantity Producing:</strong><br/>
|
||||
<span t-field="o.qty_producing"/>
|
||||
<span t-field="o.product_uom_id.name" groups="uom.group_uom"/>
|
||||
<span t-field="o.qty_producing">50</span>
|
||||
<span t-field="o.product_uom_id.name" groups="uom.group_uom">Units</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -64,23 +64,29 @@
|
|||
<th><strong>WorkCenter</strong></th>
|
||||
<th><strong>Duration (minutes)</strong></th>
|
||||
<th t-if="o.state in ('progress', 'to_close')"><strong>Actual Duration (minutes)</strong></th>
|
||||
<th style="width:30%"><strong>Barcode</strong></th>
|
||||
</tr>
|
||||
<tr t-foreach="o.workorder_ids" t-as="line2">
|
||||
<td><span t-field="line2.name"/></td>
|
||||
<td><span t-field="line2.workcenter_id.name"/></td>
|
||||
<td><span t-field="line2.name">Assembling</span></td>
|
||||
<td><span t-field="line2.workcenter_id.name">Center A</span></td>
|
||||
<td>
|
||||
<span t-if="o.state != 'done'" t-field="line2.duration_expected"/>
|
||||
<span t-if="o.state == 'done'" t-field="line2.duration"/>
|
||||
<span t-if="o.state != 'done'" t-field="line2.duration_expected">60</span>
|
||||
<span t-if="o.state == 'done'" t-field="line2.duration">58</span>
|
||||
</td>
|
||||
<td t-if="o.state in ('progress', 'to_close')">
|
||||
<span t-field="line2.duration"/>
|
||||
<span t-field="line2.duration">58</span>
|
||||
</td>
|
||||
<td>
|
||||
<span t-field="line2.barcode" t-options="{'widget': 'barcode', 'width': 600, 'height': 100, 'img_style': 'width:100%;height:35px'}">987654321098</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<t t-call="mrp.report_mrp_production_components" t-if="o.move_raw_ids"/>
|
||||
<div t-if="o.move_raw_ids">
|
||||
<t t-call="mrp.report_mrp_production_components"/>
|
||||
|
||||
</div>
|
||||
<div class="oe_structure"/>
|
||||
</div>
|
||||
</t>
|
||||
|
|
@ -95,6 +101,7 @@
|
|||
</span>
|
||||
</h3>
|
||||
<table class="table table-sm">
|
||||
<div class="oe_structure"></div>
|
||||
<t t-set="has_product_barcode" t-value="any(m.product_id.barcode for m in o.move_raw_ids)"/>
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -107,20 +114,20 @@
|
|||
<tbody>
|
||||
<tr t-foreach="o.move_raw_ids" t-as="raw_line">
|
||||
<td>
|
||||
<span t-field="raw_line.product_id"/>
|
||||
<span t-field="raw_line.product_id">8 GB RAM</span>
|
||||
</td>
|
||||
<td t-attf-class="{{ 'text-end' if not has_product_barcode else '' }}" t-if="o.state in ('progress', 'to_close','done')">
|
||||
<div>
|
||||
<span t-field="raw_line.quantity_done"/>
|
||||
<span t-field="raw_line.quantity">2</span>
|
||||
</div>
|
||||
</td>
|
||||
<td t-attf-class="{{ 'text-end' if not has_product_barcode else '' }}">
|
||||
<span t-field="raw_line.product_uom_qty"/>
|
||||
<span t-field="raw_line.product_uom" groups="uom.group_uom"/>
|
||||
<span t-field="raw_line.product_uom_qty">25</span>
|
||||
<span t-field="raw_line.product_uom" groups="uom.group_uom">Pieces</span>
|
||||
</td>
|
||||
<td t-if="has_product_barcode" width="15%" class="text-center">
|
||||
<t t-if="raw_line.product_id.barcode">
|
||||
<div t-field="raw_line.product_id.barcode" t-options="{'widget': 'barcode', 'width': 600, 'height': 100, 'img_style': 'width:100%;height:35px'}"/>
|
||||
<span t-field="raw_line.product_id.barcode" t-options="{'widget': 'barcode', 'width': 600, 'height': 100, 'img_style': 'width:100%;height:35px'}">12345678901</span>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -132,13 +139,14 @@
|
|||
<t t-call="web.basic_layout">
|
||||
<t t-set='nRows' t-value='12'/>
|
||||
<t t-set='nCols' t-value='4'/>
|
||||
<t t-set="uom_categ_unit" t-value="env.ref('uom.product_uom_categ_unit')"/>
|
||||
<t t-set="move_lines" t-value="docs.move_finished_ids.move_line_ids.filtered(lambda ml: ml.move_id.production_id.state == 'done' and ml.state == 'done' and ml.qty_done)"/>
|
||||
<t t-set="uom_unit" t-value="env.ref('uom.product_uom_unit')"/>
|
||||
<div class="oe_structure"></div>
|
||||
<t t-set="move_lines" t-value="docs.move_finished_ids.move_line_ids.filtered(lambda ml: ml.move_id.production_id.state == 'done' and ml.state == 'done' and ml.quantity)"/>
|
||||
<t t-set="move_line_qtys" t-value="[]"/>
|
||||
<t t-foreach="move_lines" t-as="move_line">
|
||||
<t t-if="move_line.product_uom_id.category_id == uom_categ_unit">
|
||||
<t t-if="move_line.product_id.uom_id._has_common_reference(uom_unit)">
|
||||
<!-- print 1 label per 1 uom when uom category = units -->
|
||||
<t t-set="move_line_qtys" t-value="move_line_qtys + [int(move_line.qty_done)]"/>
|
||||
<t t-set="move_line_qtys" t-value="move_line_qtys + [int(move_line.quantity)]"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-set="move_line_qtys" t-value="move_line_qtys + [1]"/>
|
||||
|
|
@ -146,7 +154,9 @@
|
|||
</t>
|
||||
<t t-set="index_to_qtys" t-value="{i: qty for i, qty in zip(range(0, len(move_line_qtys)), move_line_qtys)}"/>
|
||||
<t t-set="num_pages" t-value="sum(move_line_qtys)// (nRows * nCols) + 1"/>
|
||||
<div class="oe_structure"></div>
|
||||
<div t-foreach="range(num_pages)" t-as="page" class="o_label_sheet" t-att-style="'padding: 20mm 8mm'">
|
||||
<div class="oe_structure"></div>
|
||||
<table class="my-0 table table-sm table-borderless">
|
||||
<t t-foreach="range(nRows)" t-as="row">
|
||||
<tr>
|
||||
|
|
@ -166,20 +176,20 @@
|
|||
<td t-att-style="make_invisible and 'visibility:hidden'">
|
||||
<div t-if="move_line" t-translation="off" t-att-style="'position:relative; width:43mm; height:19mm; border: 1px solid %s;' % (move_line.env.user.company_id.primary_color or 'black')">
|
||||
<div class="o_label_name o_label_4x12 fw-bold">
|
||||
<div t-out="move_line.product_id.display_name"/>
|
||||
<span t-out="move_line.product_id.display_name">Product</span>
|
||||
<div><span>Quantity: </span>
|
||||
<span t-if="move_line.product_uom_id.category_id == uom_categ_unit">1.0</span>
|
||||
<span t-else="" t-out="move_line.qty_done"/>
|
||||
<span t-field="move_line.product_uom_id" groups="uom.group_uom"/>
|
||||
<span t-if="move_line.product_id.uom_id._has_common_reference(uom_unit)">1.0</span>
|
||||
<span t-else="" t-out="move_line.quantity">5</span>
|
||||
<span t-field="move_line.product_uom_id" groups="uom.group_uom">UOM id</span>
|
||||
</div>
|
||||
</div>
|
||||
<t t-if="move_line.product_id.tracking != 'none' and (move_line.lot_name or move_line.lot_id)">
|
||||
<div t-field="move_line.lot_name or move_line.lot_id.name" t-options="{'widget': 'barcode', 'img_style': 'width:100%;height:35%'}"/>
|
||||
<div class="o_label_4x12 text-center" t-out="move_line.lot_name or move_line.lot_id.name"/>
|
||||
<div class="o_label_4x12 text-center"><span t-out="move_line.lot_name or move_line.lot_id.name">Demo Barcode</span></div>
|
||||
</t>
|
||||
<t t-elif="move_line.product_id.tracking == 'none' and move_line.product_id.barcode">
|
||||
<div t-field="move_line.product_id.barcode" t-options="{'widget': 'barcode', 'img_style': 'width:100%;height:35%'}"/>
|
||||
<div class="o_label_4x12 text-center" t-out="move_line.product_id.barcode"/>
|
||||
<div class="o_label_4x12 text-center"><span t-out="move_line.product_id.barcode">12345678901</span></div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="text-muted">No barcode available</span>
|
||||
|
|
@ -190,6 +200,7 @@
|
|||
</tr>
|
||||
</t>
|
||||
</table>
|
||||
<div class="oe_structure"></div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from collections import defaultdict, OrderedDict
|
||||
from datetime import date, timedelta
|
||||
from datetime import date, datetime, time, timedelta
|
||||
import json
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.tools import float_compare, float_round, format_date, float_is_zero
|
||||
from odoo.tools import float_compare, float_round, format_date, float_is_zero, float_repr
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
class ReportBomStructure(models.AbstractModel):
|
||||
|
||||
class ReportMrpReport_Bom_Structure(models.AbstractModel):
|
||||
_name = 'report.mrp.report_bom_structure'
|
||||
_description = 'BOM Overview Report'
|
||||
|
||||
|
|
@ -19,7 +21,7 @@ class ReportBomStructure(models.AbstractModel):
|
|||
|
||||
@api.model
|
||||
def get_warehouses(self):
|
||||
return self.env['stock.warehouse'].search_read([('company_id', 'in', self.env.companies.ids)], fields=['id', 'name'])
|
||||
return self.env['stock.warehouse'].search_read([('company_id', 'in', self.env.companies.ids)], fields=['id', 'name', 'manu_type_id'])
|
||||
|
||||
@api.model
|
||||
def _compute_current_production_capacity(self, bom_data):
|
||||
|
|
@ -27,44 +29,25 @@ class ReportBomStructure(models.AbstractModel):
|
|||
components_qty_to_produce = defaultdict(lambda: 0)
|
||||
components_qty_available = {}
|
||||
for comp in bom_data.get('components', []):
|
||||
if comp['product'].type != 'product' or float_is_zero(comp['base_bom_line_qty'], precision_rounding=comp['uom'].rounding):
|
||||
if not comp['product'].is_storable or comp['uom'].is_zero(comp['base_bom_line_qty']):
|
||||
continue
|
||||
components_qty_to_produce[comp['product_id']] += comp['base_bom_line_qty']
|
||||
components_qty_available[comp['product_id']] = comp['quantity_available']
|
||||
components_qty_available[comp['product_id']] = comp['free_to_manufacture_qty']
|
||||
producibles = [float_round(components_qty_available[p_id] / qty, precision_digits=0, rounding_method='DOWN') for p_id, qty in components_qty_to_produce.items()]
|
||||
return min(producibles) * bom_data['bom']['product_qty'] if producibles else 0
|
||||
|
||||
@api.model
|
||||
def _compute_production_capacities(self, bom_qty, bom_data):
|
||||
date_today = self.env.context.get('from_date', fields.date.today())
|
||||
same_delay = bom_data['lead_time'] == bom_data['availability_delay']
|
||||
res = {}
|
||||
if bom_data.get('producible_qty', 0):
|
||||
# Check if something is producible today, at the earliest time possible considering product's lead time.
|
||||
res['earliest_capacity'] = bom_data['producible_qty']
|
||||
res['earliest_date'] = format_date(self.env, date_today + timedelta(days=bom_data['lead_time']))
|
||||
|
||||
if bom_data['availability_state'] != 'unavailable':
|
||||
if same_delay:
|
||||
# Means that stock will be resupplied at date_today, so the whole manufacture can start at date_today.
|
||||
res['earliest_capacity'] = bom_qty
|
||||
res['earliest_date'] = format_date(self.env, date_today + timedelta(days=bom_data['lead_time']))
|
||||
else:
|
||||
res['leftover_capacity'] = bom_qty - bom_data.get('producible_qty', 0)
|
||||
res['leftover_date'] = format_date(self.env, date_today + timedelta(days=bom_data['availability_delay']))
|
||||
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _get_report_values(self, docids, data=None):
|
||||
docs = []
|
||||
for bom_id in docids:
|
||||
bom = self.env['mrp.bom'].browse(bom_id)
|
||||
if not bom:
|
||||
continue
|
||||
variant = data.get('variant')
|
||||
candidates = variant and self.env['product.product'].browse(int(variant)) or bom.product_id or bom.product_tmpl_id.product_variant_ids
|
||||
quantity = float(data.get('quantity', bom.product_qty))
|
||||
if data.get('warehouse_id'):
|
||||
self = self.with_context(warehouse=int(data.get('warehouse_id')))
|
||||
self = self.with_context(warehouse_id=int(data.get('warehouse_id'))) # noqa: PLW0642
|
||||
for product_variant_id in candidates.ids:
|
||||
docs.append(self._get_pdf_doc(bom_id, data, quantity, product_variant_id))
|
||||
if not candidates:
|
||||
|
|
@ -81,10 +64,7 @@ class ReportBomStructure(models.AbstractModel):
|
|||
doc = self._get_pdf_line(bom_id, product_id=product_variant_id, qty=quantity, unfolded_ids=set(json.loads(data.get('unfolded_ids'))))
|
||||
else:
|
||||
doc = self._get_pdf_line(bom_id, product_id=product_variant_id, qty=quantity, unfolded=True)
|
||||
doc['show_availabilities'] = False if data and data.get('availabilities') == 'false' else True
|
||||
doc['show_costs'] = False if data and data.get('costs') == 'false' else True
|
||||
doc['show_operations'] = False if data and data.get('operations') == 'false' else True
|
||||
doc['show_lead_times'] = False if data and data.get('lead_times') == 'false' else True
|
||||
doc['forecast_mode'] = data.get('mode', 'overview') == 'forecast'
|
||||
return doc
|
||||
|
||||
@api.model
|
||||
|
|
@ -108,26 +88,24 @@ class ReportBomStructure(models.AbstractModel):
|
|||
for variant in bom.product_tmpl_id.product_variant_ids:
|
||||
bom_product_variants[variant.id] = variant.display_name
|
||||
|
||||
if self.env.context.get('warehouse'):
|
||||
warehouse = self.env['stock.warehouse'].browse(self.env.context.get('warehouse'))
|
||||
if self.env.context.get('warehouse_id'):
|
||||
warehouse = self.env['stock.warehouse'].browse(self.env.context.get('warehouse_id'))
|
||||
else:
|
||||
warehouse = self.env['stock.warehouse'].browse(self.get_warehouses()[0]['id'])
|
||||
|
||||
lines = self._get_bom_data(bom, warehouse, product=product, line_qty=bom_quantity, level=0)
|
||||
production_capacities = self._compute_production_capacities(bom_quantity, lines)
|
||||
lines.update(production_capacities)
|
||||
return {
|
||||
'lines': lines,
|
||||
'variants': bom_product_variants,
|
||||
'bom_uom_name': bom_uom_name,
|
||||
'bom_qty': bom_quantity,
|
||||
'is_variant_applied': self.env.user.user_has_groups('product.group_product_variant') and len(bom_product_variants) > 1,
|
||||
'is_uom_applied': self.env.user.user_has_groups('uom.group_uom'),
|
||||
'precision': self.env['decimal.precision'].precision_get('Product Unit of Measure'),
|
||||
'is_variant_applied': self.env.user.has_group('product.group_product_variant') and len(bom_product_variants) > 1,
|
||||
'is_uom_applied': self.env.user.has_group('uom.group_uom'),
|
||||
'precision': self.env['decimal.precision'].precision_get('Product Unit'),
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _get_components_closest_forecasted(self, lines, line_quantities, parent_bom, product_info, ignore_stock=False):
|
||||
def _get_components_closest_forecasted(self, lines, line_quantities, parent_bom, product_info, parent_product, ignore_stock=False):
|
||||
"""
|
||||
Returns a dict mapping products to a dict of their corresponding BoM lines,
|
||||
which are mapped to their closest date in the forecast report where consumed quantity >= forecasted quantity.
|
||||
|
|
@ -146,34 +124,35 @@ class ReportBomStructure(models.AbstractModel):
|
|||
product_quantities_info = defaultdict(OrderedDict)
|
||||
for line in lines:
|
||||
product = line.product_id
|
||||
quantities_info = self._get_quantities_info(product, line.product_uom_id, parent_bom, product_info)
|
||||
line_quantity = line_quantities.get(line.id, 0.0)
|
||||
quantities_info = self._get_quantities_info(product, line.product_uom_id, product_info, parent_bom, parent_product)
|
||||
stock_loc = quantities_info['stock_loc']
|
||||
product_info[product.id]['consumptions'][stock_loc] += line_quantities.get(line.id, 0.0)
|
||||
product_info[product.id]['consumptions'][stock_loc] += line_quantity
|
||||
product_quantities_info[product.id][line.id] = product_info[product.id]['consumptions'][stock_loc]
|
||||
if (product.detailed_type != 'product' or
|
||||
float_compare(product_info[product.id]['consumptions'][stock_loc], quantities_info['free_qty'], precision_rounding=product.uom_id.rounding) <= 0):
|
||||
if (not product.is_storable or
|
||||
product.uom_id.compare(product_info[product.id]['consumptions'][stock_loc], quantities_info['free_qty']) <= 0):
|
||||
# Use date.min as a sentinel value for _get_stock_availability
|
||||
closest_forecasted[product.id][line.id] = date.min
|
||||
elif stock_loc != 'in_stock':
|
||||
elif stock_loc != 'in_stock' or quantities_info['forecasted_qty'] < line_quantity:
|
||||
closest_forecasted[product.id][line.id] = date.max
|
||||
else:
|
||||
remaining_products.append(product.id)
|
||||
closest_forecasted[product.id][line.id] = None
|
||||
date_today = self.env.context.get('from_date', fields.date.today())
|
||||
date_today = self.env.context.get('from_date', fields.Date.today())
|
||||
domain = [('state', '=', 'forecast'), ('date', '>=', date_today), ('product_id', 'in', list(set(remaining_products)))]
|
||||
if self.env.context.get('warehouse'):
|
||||
domain.append(('warehouse_id', '=', self.env.context.get('warehouse')))
|
||||
if self.env.context.get('warehouse_id'):
|
||||
domain.append(('warehouse_id', '=', self.env.context.get('warehouse_id')))
|
||||
if remaining_products:
|
||||
res = self.env['report.stock.quantity']._read_group(
|
||||
domain,
|
||||
['min_date:min(date)', 'product_id', 'product_qty'],
|
||||
['product_id', 'product_qty'],
|
||||
orderby='product_id asc, min_date asc', lazy=False
|
||||
groupby=['product_id', 'product_qty'],
|
||||
aggregates=['date:min'],
|
||||
order='product_id asc, date:min asc'
|
||||
)
|
||||
available_quantities = defaultdict(list)
|
||||
for group in res:
|
||||
product_id = group['product_id'][0]
|
||||
available_quantities[product_id].append([group['min_date'], group['product_qty']])
|
||||
product_id = group[0].id
|
||||
available_quantities[product_id].append([group[2], group[1]])
|
||||
for product_id in remaining_products:
|
||||
# Find the first empty line_id for the given product_id.
|
||||
line_id = next(filter(lambda k: not closest_forecasted[product_id][k], closest_forecasted[product_id].keys()), None)
|
||||
|
|
@ -187,7 +166,7 @@ class ReportBomStructure(models.AbstractModel):
|
|||
return closest_forecasted
|
||||
|
||||
@api.model
|
||||
def _get_bom_data(self, bom, warehouse, product=False, line_qty=False, bom_line=False, level=0, parent_bom=False, index=0, product_info=False, ignore_stock=False):
|
||||
def _get_bom_data(self, bom, warehouse, product=False, line_qty=False, bom_line=False, level=0, parent_bom=False, parent_product=False, index=0, product_info=False, ignore_stock=False, simulated_leaves_per_workcenter=False):
|
||||
""" Gets recursively the BoM and all its subassemblies and computes availibility estimations for each component and their disponibility in stock.
|
||||
Accepts specific keys in context that will affect the data computed :
|
||||
- 'minimized': Will cut all data not required to compute availability estimations.
|
||||
|
|
@ -198,37 +177,36 @@ class ReportBomStructure(models.AbstractModel):
|
|||
product = bom.product_id or bom.product_tmpl_id.product_variant_id
|
||||
if line_qty is False:
|
||||
line_qty = bom.product_qty
|
||||
|
||||
if not product_info:
|
||||
product_info = {}
|
||||
if simulated_leaves_per_workcenter is False:
|
||||
simulated_leaves_per_workcenter = defaultdict(list)
|
||||
|
||||
company = bom.company_id or self.env.company
|
||||
current_quantity = line_qty
|
||||
if bom_line:
|
||||
current_quantity = bom_line.product_uom_id._compute_quantity(line_qty, bom.product_uom_id) or 0
|
||||
|
||||
prod_cost = 0
|
||||
attachment_ids = []
|
||||
has_attachments = False
|
||||
if not is_minimized:
|
||||
if product:
|
||||
prod_cost = product.uom_id._compute_price(product.with_company(company).standard_price, bom.product_uom_id) * current_quantity
|
||||
attachment_ids = self.env['mrp.document'].search(['|', '&', ('res_model', '=', 'product.product'),
|
||||
has_attachments = self.env['product.document'].search_count(['&', '&', ('attached_on_mrp', '=', 'bom'), ('active', '=', 't'), '|', '&', ('res_model', '=', 'product.product'),
|
||||
('res_id', '=', product.id), '&', ('res_model', '=', 'product.template'),
|
||||
('res_id', '=', product.product_tmpl_id.id)]).ids
|
||||
('res_id', '=', product.product_tmpl_id.id)], limit=1) > 0
|
||||
else:
|
||||
# Use the product template instead of the variant
|
||||
prod_cost = bom.product_tmpl_id.uom_id._compute_price(bom.product_tmpl_id.with_company(company).standard_price, bom.product_uom_id) * current_quantity
|
||||
attachment_ids = self.env['mrp.document'].search([('res_model', '=', 'product.template'), ('res_id', '=', bom.product_tmpl_id.id)]).ids
|
||||
has_attachments = self.env['product.document'].search_count(['&', '&', ('attached_on_mrp', '=', 'bom'), ('active', '=', 't'),
|
||||
'&', ('res_model', '=', 'product.template'), ('res_id', '=', bom.product_tmpl_id.id)], limit=1) > 0
|
||||
|
||||
key = product.id
|
||||
bom_key = bom.id
|
||||
qty_product_uom = bom.product_uom_id._compute_quantity(current_quantity, product.uom_id or bom.product_tmpl_id.uom_id)
|
||||
self._update_product_info(product, bom_key, product_info, warehouse, qty_product_uom, bom=bom, parent_bom=parent_bom)
|
||||
self._update_product_info(product, bom_key, product_info, warehouse, qty_product_uom, bom=bom, parent_bom=parent_bom, parent_product=parent_product)
|
||||
route_info = product_info[key].get(bom_key, {})
|
||||
quantities_info = {}
|
||||
if not ignore_stock:
|
||||
# Useless to compute quantities_info if it's not going to be used later on
|
||||
quantities_info = self._get_quantities_info(product, bom.product_uom_id, parent_bom, product_info)
|
||||
quantities_info = self._get_quantities_info(product, bom.product_uom_id, product_info, parent_bom, parent_product)
|
||||
|
||||
bom_report_line = {
|
||||
'index': index,
|
||||
|
|
@ -236,9 +214,12 @@ class ReportBomStructure(models.AbstractModel):
|
|||
'bom_id': bom and bom.id or False,
|
||||
'bom_code': bom and bom.code or False,
|
||||
'type': 'bom',
|
||||
'is_storable': product.is_storable,
|
||||
'quantity': current_quantity,
|
||||
'quantity_available': quantities_info.get('free_qty') or 0,
|
||||
'quantity_on_hand': quantities_info.get('on_hand_qty') or 0,
|
||||
'quantity_forecasted': quantities_info.get('forecasted_qty') or 0,
|
||||
'free_to_manufacture_qty': quantities_info.get('free_to_manufacture_qty') or 0,
|
||||
'base_bom_line_qty': bom_line.product_qty if bom_line else False, # bom_line isn't defined only for the top-level product
|
||||
'name': product.display_name or bom.product_tmpl_id.display_name,
|
||||
'uom': bom.product_uom_id if bom else product.uom_id,
|
||||
|
|
@ -247,29 +228,21 @@ class ReportBomStructure(models.AbstractModel):
|
|||
'route_name': route_info.get('route_name', ''),
|
||||
'route_detail': route_info.get('route_detail', ''),
|
||||
'route_alert': route_info.get('route_alert', False),
|
||||
'lead_time': route_info.get('lead_time', False),
|
||||
'currency': company.currency_id,
|
||||
'currency_id': company.currency_id.id,
|
||||
'product': product,
|
||||
'product_id': product.id,
|
||||
'product_template_id': product.product_tmpl_id.id,
|
||||
'link_id': (product.id if product.product_variant_count > 1 else product.product_tmpl_id.id) or bom.product_tmpl_id.id,
|
||||
'link_model': 'product.product' if product.product_variant_count > 1 else 'product.template',
|
||||
'code': bom and bom.display_name or '',
|
||||
'prod_cost': prod_cost,
|
||||
'bom_cost': 0,
|
||||
'level': level or 0,
|
||||
'attachment_ids': attachment_ids,
|
||||
'has_attachments': has_attachments,
|
||||
'phantom_bom': bom.type == 'phantom',
|
||||
'parent_id': parent_bom and parent_bom.id or False,
|
||||
}
|
||||
|
||||
if not is_minimized:
|
||||
operations = self._get_operation_line(product, bom, float_round(current_quantity, precision_rounding=1, rounding_method='UP'), level + 1, index)
|
||||
bom_report_line['operations'] = operations
|
||||
bom_report_line['operations_cost'] = sum([op['bom_cost'] for op in operations])
|
||||
bom_report_line['operations_time'] = sum([op['quantity'] for op in operations])
|
||||
bom_report_line['bom_cost'] += bom_report_line['operations_cost']
|
||||
|
||||
components = []
|
||||
no_bom_lines = self.env['mrp.bom.line']
|
||||
line_quantities = {}
|
||||
|
|
@ -282,27 +255,67 @@ class ReportBomStructure(models.AbstractModel):
|
|||
no_bom_lines |= line
|
||||
# Update product_info for all the components before computing closest forecasted.
|
||||
qty_product_uom = line.product_uom_id._compute_quantity(line_quantity, line.product_id.uom_id)
|
||||
self.with_context(parent_product_id=product.id)._update_product_info(line.product_id, bom.id, product_info, warehouse, qty_product_uom, bom=False, parent_bom=bom)
|
||||
components_closest_forecasted = self.with_context(parent_product_id=product.id)._get_components_closest_forecasted(no_bom_lines, line_quantities, bom, product_info, ignore_stock)
|
||||
self._update_product_info(line.product_id, bom.id, product_info, warehouse, qty_product_uom, bom=False, parent_bom=bom, parent_product=product)
|
||||
components_closest_forecasted = self._get_components_closest_forecasted(no_bom_lines, line_quantities, bom, product_info, product, ignore_stock)
|
||||
for component_index, line in enumerate(bom.bom_line_ids):
|
||||
new_index = f"{index}{component_index}"
|
||||
if product and line._skip_bom_line(product):
|
||||
continue
|
||||
line_quantity = line_quantities.get(line.id, 0.0)
|
||||
if line.child_bom_id:
|
||||
component = self.with_context(parent_product_id=product.id)._get_bom_data(line.child_bom_id, warehouse, line.product_id, line_quantity, bom_line=line, level=level + 1, parent_bom=bom,
|
||||
index=new_index, product_info=product_info, ignore_stock=ignore_stock)
|
||||
component = self._get_bom_data(line.child_bom_id, warehouse, line.product_id, line_quantity, bom_line=line, level=level + 1, parent_bom=bom,
|
||||
parent_product=product, index=new_index, product_info=product_info, ignore_stock=ignore_stock,
|
||||
simulated_leaves_per_workcenter=simulated_leaves_per_workcenter)
|
||||
else:
|
||||
component = self.with_context(
|
||||
parent_product_id=product.id,
|
||||
components_closest_forecasted=components_closest_forecasted,
|
||||
)._get_component_data(bom, warehouse, line, line_quantity, level + 1, new_index, product_info, ignore_stock)
|
||||
components.append(component)
|
||||
)._get_component_data(bom, product, warehouse, line, line_quantity, level + 1, new_index, product_info, ignore_stock)
|
||||
for component_bom in components:
|
||||
if component['product_id'] == component_bom['product_id'] and component['uom'].id == component_bom['uom'].id:
|
||||
self._merge_components(component_bom, component)
|
||||
break
|
||||
else:
|
||||
components.append(component)
|
||||
bom_report_line['bom_cost'] += component['bom_cost']
|
||||
for component in components:
|
||||
if component['is_storable']:
|
||||
if missing_qty := max(component['quantity'] - component['quantity_forecasted'], 0):
|
||||
missing_qty = float_repr(missing_qty, self.env['decimal.precision'].precision_get('Product Unit'))
|
||||
route_name = component['route_name'] or _('Order')
|
||||
component['status'] = _("%(qty)s To %(route)s", qty=missing_qty, route=route_name)
|
||||
bom_report_line['components'] = components
|
||||
bom_report_line['producible_qty'] = self._compute_current_production_capacity(bom_report_line)
|
||||
|
||||
availabilities = self._get_availabilities(product, current_quantity, product_info, bom_key, quantities_info, level, ignore_stock, components, report_line=bom_report_line)
|
||||
# in case of subcontracting, lead_time will be calculated with components availability delay
|
||||
bom_report_line['lead_time'] = route_info.get('lead_time', False)
|
||||
bom_report_line['manufacture_delay'] = route_info.get('manufacture_delay', False)
|
||||
bom_report_line.update(availabilities)
|
||||
|
||||
if level == 0:
|
||||
if bom_report_line['producible_qty'] > 0:
|
||||
bom_report_line['status'] = _("%(qty)s Ready To Produce", qty=bom_report_line['producible_qty'])
|
||||
else:
|
||||
bom_report_line['status'] = _("No Ready To Produce")
|
||||
elif missing_qty := max(bom_report_line['quantity'] - bom_report_line['quantity_available'], 0):
|
||||
missing_qty = float_repr(missing_qty, self.env['decimal.precision'].precision_get('Product Unit'))
|
||||
route_name = bom_report_line['route_name'] or _('Order')
|
||||
bom_report_line['status'] = _("%(qty)s To %(route)s", qty=missing_qty, route=route_name)
|
||||
|
||||
if not is_minimized:
|
||||
|
||||
operations = self._get_operation_line(product, bom, float_round(current_quantity, precision_digits=self.env['decimal.precision'].precision_get('Product Unit'), rounding_method='UP'), level + 1, index, bom_report_line, simulated_leaves_per_workcenter)
|
||||
bom_report_line['operations'] = operations
|
||||
bom_report_line['operations_cost'] = sum(op['bom_cost'] for op in operations)
|
||||
bom_report_line['operations_time'] = sum(op['quantity'] for op in operations)
|
||||
bom_report_line['operations_delay'] = max((op['availability_delay'] for op in operations), default=0)
|
||||
if 'simulated' in bom_report_line:
|
||||
bom_report_line['availability_state'] = 'estimated'
|
||||
max_component_delay = bom_report_line['max_component_delay']
|
||||
bom_report_line['availability_delay'] = max_component_delay + max(bom.produce_delay, bom_report_line['operations_delay'])
|
||||
bom_report_line['availability_display'] = self._format_date_display(bom_report_line['availability_state'], bom_report_line['availability_delay'])
|
||||
bom_report_line['bom_cost'] += bom_report_line['operations_cost']
|
||||
|
||||
byproducts, byproduct_cost_portion = self._get_byproducts_lines(product, bom, current_quantity, level + 1, bom_report_line['bom_cost'], index)
|
||||
bom_report_line['byproducts'] = byproducts
|
||||
bom_report_line['cost_share'] = float_round(1 - byproduct_cost_portion, precision_rounding=0.0001)
|
||||
|
|
@ -310,8 +323,7 @@ class ReportBomStructure(models.AbstractModel):
|
|||
bom_report_line['byproducts_total'] = sum(byproduct['quantity'] for byproduct in byproducts)
|
||||
bom_report_line['bom_cost'] *= bom_report_line['cost_share']
|
||||
|
||||
availabilities = self._get_availabilities(product, current_quantity, product_info, bom_key, quantities_info, level, ignore_stock, components)
|
||||
bom_report_line.update(availabilities)
|
||||
bom_report_line['foldable'] = len(bom.operation_ids) > 0 or (len(bom_report_line['components']) > 0 and level > 0) or any(component.get('foldable', False) for component in bom_report_line['components'])
|
||||
|
||||
if level == 0:
|
||||
# Gives a unique key for the first line that indicates if product is ready for production right now.
|
||||
|
|
@ -319,7 +331,7 @@ class ReportBomStructure(models.AbstractModel):
|
|||
return bom_report_line
|
||||
|
||||
@api.model
|
||||
def _get_component_data(self, parent_bom, warehouse, bom_line, line_quantity, level, index, product_info, ignore_stock=False):
|
||||
def _get_component_data(self, parent_bom, parent_product, warehouse, bom_line, line_quantity, level, index, product_info, ignore_stock=False):
|
||||
company = parent_bom.company_id or self.env.company
|
||||
price = bom_line.product_id.uom_id._compute_price(bom_line.product_id.with_company(company).standard_price, bom_line.product_uom_id) * line_quantity
|
||||
rounded_price = company.currency_id.round(price)
|
||||
|
|
@ -331,13 +343,13 @@ class ReportBomStructure(models.AbstractModel):
|
|||
quantities_info = {}
|
||||
if not ignore_stock:
|
||||
# Useless to compute quantities_info if it's not going to be used later on
|
||||
quantities_info = self._get_quantities_info(bom_line.product_id, bom_line.product_uom_id, parent_bom, product_info)
|
||||
quantities_info = self._get_quantities_info(bom_line.product_id, bom_line.product_uom_id, product_info, parent_bom, parent_product)
|
||||
availabilities = self._get_availabilities(bom_line.product_id, line_quantity, product_info, bom_key, quantities_info, level, ignore_stock, bom_line=bom_line)
|
||||
|
||||
attachment_ids = []
|
||||
has_attachments = False
|
||||
if not self.env.context.get('minimized', False):
|
||||
attachment_ids = self.env['mrp.document'].search(['|', '&', ('res_model', '=', 'product.product'), ('res_id', '=', bom_line.product_id.id),
|
||||
'&', ('res_model', '=', 'product.template'), ('res_id', '=', bom_line.product_id.product_tmpl_id.id)]).ids
|
||||
has_attachments = self.env['product.document'].search_count(['&', ('attached_on_mrp', '=', 'bom'), '|', '&', ('res_model', '=', 'product.product'), ('res_id', '=', bom_line.product_id.id),
|
||||
'&', ('res_model', '=', 'product.template'), ('res_id', '=', bom_line.product_id.product_tmpl_id.id)]) > 0
|
||||
|
||||
return {
|
||||
'type': 'component',
|
||||
|
|
@ -345,25 +357,29 @@ class ReportBomStructure(models.AbstractModel):
|
|||
'bom_id': False,
|
||||
'product': bom_line.product_id,
|
||||
'product_id': bom_line.product_id.id,
|
||||
'product_template_id': bom_line.product_tmpl_id.id,
|
||||
'link_id': bom_line.product_id.id if bom_line.product_id.product_variant_count > 1 else bom_line.product_id.product_tmpl_id.id,
|
||||
'link_model': 'product.product' if bom_line.product_id.product_variant_count > 1 else 'product.template',
|
||||
'name': bom_line.product_id.display_name,
|
||||
'code': '',
|
||||
'currency': company.currency_id,
|
||||
'currency_id': company.currency_id.id,
|
||||
'is_storable': bom_line.product_id.is_storable,
|
||||
'quantity': line_quantity,
|
||||
'quantity_available': quantities_info.get('free_qty', 0),
|
||||
'quantity_on_hand': quantities_info.get('on_hand_qty', 0),
|
||||
'quantity_forecasted': quantities_info.get('forecasted_qty', 0),
|
||||
'free_to_manufacture_qty': quantities_info.get('free_to_manufacture_qty', 0),
|
||||
'base_bom_line_qty': bom_line.product_qty,
|
||||
'uom': bom_line.product_uom_id,
|
||||
'uom_name': bom_line.product_uom_id.name,
|
||||
'prod_cost': rounded_price,
|
||||
'bom_cost': rounded_price,
|
||||
'route_type': route_info.get('route_type', ''),
|
||||
'route_name': route_info.get('route_name', ''),
|
||||
'route_detail': route_info.get('route_detail', ''),
|
||||
'route_alert': route_info.get('route_alert', False),
|
||||
'lead_time': route_info.get('lead_time', False),
|
||||
'manufacture_delay': route_info.get('manufacture_delay', False),
|
||||
'stock_avail_state': availabilities['stock_avail_state'],
|
||||
'resupply_avail_delay': availabilities['resupply_avail_delay'],
|
||||
'availability_display': availabilities['availability_display'],
|
||||
|
|
@ -371,31 +387,31 @@ class ReportBomStructure(models.AbstractModel):
|
|||
'availability_delay': availabilities['availability_delay'],
|
||||
'parent_id': parent_bom.id,
|
||||
'level': level or 0,
|
||||
'attachment_ids': attachment_ids,
|
||||
'has_attachments': has_attachments,
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _get_quantities_info(self, product, bom_uom, parent_bom, product_info):
|
||||
return {
|
||||
'free_qty': product.uom_id._compute_quantity(product.free_qty, bom_uom) if product.detailed_type == 'product' else False,
|
||||
'on_hand_qty': product.uom_id._compute_quantity(product.qty_available, bom_uom) if product.detailed_type == 'product' else False,
|
||||
def _get_quantities_info(self, product, bom_uom, product_info, parent_bom=False, parent_product=False):
|
||||
quantities_info = {
|
||||
'free_qty': max(product.uom_id._compute_quantity(product.free_qty, bom_uom), 0) if product.is_storable else 0,
|
||||
'on_hand_qty': product.uom_id._compute_quantity(product.qty_available, bom_uom) if product.is_storable else 0,
|
||||
'forecasted_qty': product.uom_id._compute_quantity(product.virtual_available, bom_uom) if product.is_storable else 0,
|
||||
'stock_loc': 'in_stock',
|
||||
}
|
||||
quantities_info['free_to_manufacture_qty'] = quantities_info['free_qty']
|
||||
return quantities_info
|
||||
|
||||
@api.model
|
||||
def _update_product_info(self, product, bom_key, product_info, warehouse, quantity, bom, parent_bom):
|
||||
def _update_product_info(self, product, bom_key, product_info, warehouse, quantity, bom, parent_bom, parent_product):
|
||||
key = product.id
|
||||
if key not in product_info:
|
||||
product_info[key] = {'consumptions': {'in_stock': 0}}
|
||||
if not product_info[key].get(bom_key):
|
||||
product_info[key][bom_key] = self.with_context(
|
||||
product_info=product_info, parent_bom=parent_bom
|
||||
)._get_resupply_route_info(warehouse, product, quantity, bom)
|
||||
product_info[key][bom_key] = self._get_resupply_route_info(warehouse, product, quantity, product_info, bom, parent_bom, parent_product)
|
||||
elif product_info[key][bom_key].get('route_alert'):
|
||||
# Need more quantity than a single line, might change with additional quantity
|
||||
product_info[key][bom_key] = self.with_context(
|
||||
product_info=product_info, parent_bom=parent_bom
|
||||
)._get_resupply_route_info(warehouse, product, quantity + product_info[key][bom_key].get('qty_checked'), bom)
|
||||
product_info[key][bom_key] = self._get_resupply_route_info(
|
||||
warehouse, product, quantity + product_info[key][bom_key].get('qty_checked'), product_info, bom, parent_bom, parent_product)
|
||||
|
||||
@api.model
|
||||
def _get_byproducts_lines(self, product, bom, bom_quantity, level, total, index):
|
||||
|
|
@ -420,7 +436,6 @@ class ReportBomStructure(models.AbstractModel):
|
|||
'name': byproduct.product_id.display_name,
|
||||
'quantity': line_quantity,
|
||||
'uom_name': byproduct.product_uom_id.name,
|
||||
'prod_cost': company.currency_id.round(price),
|
||||
'parent_id': bom.id,
|
||||
'level': level or 0,
|
||||
'bom_cost': company.currency_id.round(total * cost_share),
|
||||
|
|
@ -430,24 +445,37 @@ class ReportBomStructure(models.AbstractModel):
|
|||
return byproducts, byproduct_cost_portion
|
||||
|
||||
@api.model
|
||||
def _get_operation_cost(self, duration, operation):
|
||||
return (duration / 60.0) * operation.workcenter_id.costs_hour
|
||||
|
||||
@api.model
|
||||
def _get_operation_line(self, product, bom, qty, level, index):
|
||||
def _get_operation_line(self, product, bom, qty, level, index, bom_report_line, simulated_leaves_per_workcenter):
|
||||
operations = []
|
||||
total = 0.0
|
||||
qty = bom.product_uom_id._compute_quantity(qty, bom.product_tmpl_id.uom_id)
|
||||
company = bom.company_id or self.env.company
|
||||
operations_planning = {}
|
||||
if bom_report_line['availability_state'] in ['unavailable', 'estimated'] and bom.operation_ids:
|
||||
qty_requested = bom.product_uom_id._compute_quantity(qty, bom.product_tmpl_id.uom_id)
|
||||
qty_to_produce = bom.product_tmpl_id.uom_id._compute_quantity(max(0, qty_requested - (product.virtual_available if level > 1 else 0)), bom.product_uom_id)
|
||||
if not (product or bom.product_tmpl_id).uom_id.is_zero(qty_to_produce):
|
||||
max_component_delay = 0
|
||||
for component in bom_report_line['components']:
|
||||
line_delay = component.get('availability_delay', 0)
|
||||
max_component_delay = max(max_component_delay, line_delay)
|
||||
date_today = self.env.context.get('from_date', fields.Date.today()) + timedelta(days=max_component_delay)
|
||||
operations_planning = self._simulate_bom_planning(bom, product, datetime.combine(date_today, time.min), qty_to_produce, simulated_leaves_per_workcenter=simulated_leaves_per_workcenter)
|
||||
bom_report_line['simulated'] = True
|
||||
bom_report_line['max_component_delay'] = max_component_delay
|
||||
operation_index = 0
|
||||
for operation in bom.operation_ids:
|
||||
if not product or operation._skip_operation_line(product):
|
||||
continue
|
||||
capacity = operation.workcenter_id._get_capacity(product)
|
||||
operation_cycle = float_round(qty / capacity, precision_rounding=1, rounding_method='UP')
|
||||
duration_expected = (operation_cycle * operation.time_cycle * 100.0 / operation.workcenter_id.time_efficiency) + \
|
||||
operation.workcenter_id._get_expected_duration(product)
|
||||
total = self._get_operation_cost(duration_expected, operation)
|
||||
op = operation.with_context(product=product, quantity=qty)
|
||||
duration_expected = op.time_total
|
||||
bom_cost = self.env.company.currency_id.round(op.cost)
|
||||
if planning := operations_planning.get(operation, None):
|
||||
availability_state = 'estimated'
|
||||
availability_delay = (planning['date_finished'].date() - date_today).days
|
||||
availability_display = _('Estimated %s', format_date(self.env, planning['date_finished'])) + (" [" + planning['workcenter'].name + "]" if planning['workcenter'] != operation.workcenter_id else "")
|
||||
else:
|
||||
availability_state = 'available'
|
||||
availability_delay = 0
|
||||
availability_display = ''
|
||||
operations.append({
|
||||
'type': 'operation',
|
||||
'index': f"{index}{operation_index}",
|
||||
|
|
@ -458,9 +486,12 @@ class ReportBomStructure(models.AbstractModel):
|
|||
'name': operation.name + ' - ' + operation.workcenter_id.name,
|
||||
'uom_name': _("Minutes"),
|
||||
'quantity': duration_expected,
|
||||
'bom_cost': self.env.company.currency_id.round(total),
|
||||
'bom_cost': bom_cost,
|
||||
'currency_id': company.currency_id.id,
|
||||
'model': 'mrp.routing.workcenter',
|
||||
'availability_state': availability_state,
|
||||
'availability_delay': availability_delay,
|
||||
'availability_display': availability_display,
|
||||
})
|
||||
operation_index += 1
|
||||
return operations
|
||||
|
|
@ -476,8 +507,8 @@ class ReportBomStructure(models.AbstractModel):
|
|||
else:
|
||||
product = bom.product_id or bom.product_tmpl_id.product_variant_id or bom.product_tmpl_id.with_context(active_test=False).product_variant_ids[:1]
|
||||
|
||||
if self.env.context.get('warehouse'):
|
||||
warehouse = self.env['stock.warehouse'].browse(self.env.context.get('warehouse'))
|
||||
if self.env.context.get('warehouse_id'):
|
||||
warehouse = self.env['stock.warehouse'].browse(self.env.context.get('warehouse_id'))
|
||||
else:
|
||||
warehouse = self.env['stock.warehouse'].browse(self.get_warehouses()[0]['id'])
|
||||
|
||||
|
|
@ -499,22 +530,24 @@ class ReportBomStructure(models.AbstractModel):
|
|||
'bom_id': bom_line['bom_id'],
|
||||
'name': bom_line['name'],
|
||||
'type': bom_line['type'],
|
||||
'is_storable': bom_line['is_storable'],
|
||||
'quantity': bom_line['quantity'],
|
||||
'quantity_available': bom_line['quantity_available'],
|
||||
'quantity_on_hand': bom_line['quantity_on_hand'],
|
||||
'producible_qty': bom_line.get('producible_qty', False),
|
||||
'uom': bom_line['uom_name'],
|
||||
'prod_cost': bom_line['prod_cost'],
|
||||
'bom_cost': bom_line['bom_cost'],
|
||||
'route_name': bom_line['route_name'],
|
||||
'route_detail': bom_line['route_detail'],
|
||||
'route_alert': bom_line.get('route_alert', False),
|
||||
'lead_time': bom_line['lead_time'],
|
||||
'manufacture_delay': bom_line['manufacture_delay'],
|
||||
'level': bom_line['level'],
|
||||
'code': bom_line['code'],
|
||||
'availability_state': bom_line['availability_state'],
|
||||
'availability_display': bom_line['availability_display'],
|
||||
'visible': line_visible,
|
||||
'status': bom_line.get('status', ""),
|
||||
})
|
||||
if bom_line.get('components'):
|
||||
lines += self._get_bom_array_lines(bom_line, level + 1, unfolded_ids, unfolded, line_visible and line_unfolded)
|
||||
|
|
@ -538,6 +571,9 @@ class ReportBomStructure(models.AbstractModel):
|
|||
'uom': _('minutes'),
|
||||
'bom_cost': operation['bom_cost'],
|
||||
'level': level + 1,
|
||||
'availability_state': operation['availability_state'],
|
||||
'availability_delay': operation['availability_delay'],
|
||||
'availability_display': operation['availability_display'],
|
||||
'visible': operations_unfolded,
|
||||
})
|
||||
if data['byproducts']:
|
||||
|
|
@ -557,7 +593,6 @@ class ReportBomStructure(models.AbstractModel):
|
|||
'type': 'byproduct',
|
||||
'quantity': byproduct['quantity'],
|
||||
'uom': byproduct['uom_name'],
|
||||
'prod_cost': byproduct['prod_cost'],
|
||||
'bom_cost': byproduct['bom_cost'],
|
||||
'level': level + 1,
|
||||
'visible': byproducts_unfolded,
|
||||
|
|
@ -565,30 +600,27 @@ class ReportBomStructure(models.AbstractModel):
|
|||
return lines
|
||||
|
||||
@api.model
|
||||
def _get_resupply_route_info(self, warehouse, product, quantity, bom=False):
|
||||
def _get_resupply_route_info(self, warehouse, product, quantity, product_info, bom=False, parent_bom=False, parent_product=False):
|
||||
found_rules = []
|
||||
if self._need_special_rules(self.env.context.get('product_info'), self.env.context.get('parent_bom'), self.env.context.get('parent_product_id')):
|
||||
found_rules = self._find_special_rules(product, self.env.context.get('product_info'), self.env.context.get('parent_bom'), self.env.context.get('parent_product_id'))
|
||||
if found_rules and not self._is_resupply_rules(found_rules, bom):
|
||||
# We only want to show the effective resupply (i.e. a form of manufacture or buy)
|
||||
found_rules = []
|
||||
if self._need_special_rules(product_info, parent_bom, parent_product):
|
||||
found_rules = self._find_special_rules(product, product_info, bom, parent_bom, parent_product)
|
||||
if not found_rules:
|
||||
found_rules = product._get_rules_from_location(warehouse.lot_stock_id)
|
||||
if not found_rules:
|
||||
return {}
|
||||
rules_delay = sum(rule.delay for rule in found_rules)
|
||||
return self._format_route_info(found_rules, rules_delay, warehouse, product, bom, quantity)
|
||||
return self.with_context(parent_bom=parent_bom)._format_route_info(found_rules, rules_delay, warehouse, product, bom, quantity)
|
||||
|
||||
@api.model
|
||||
def _is_resupply_rules(self, rules, bom):
|
||||
return bom and any(rule.action == 'manufacture' for rule in rules)
|
||||
|
||||
@api.model
|
||||
def _need_special_rules(self, product_info, parent_bom=False, parent_product_id=False):
|
||||
def _need_special_rules(self, product_info, parent_bom=False, parent_product=False):
|
||||
return False
|
||||
|
||||
@api.model
|
||||
def _find_special_rules(self, product, product_info, parent_bom=False, parent_product_id=False):
|
||||
def _find_special_rules(self, product, product_info, current_bom=False, parent_bom=False, parent_product=False):
|
||||
return False
|
||||
|
||||
@api.model
|
||||
|
|
@ -599,18 +631,18 @@ class ReportBomStructure(models.AbstractModel):
|
|||
wh_manufacture_rules = product._get_rules_from_location(product.property_stock_production, route_ids=warehouse.route_ids)
|
||||
wh_manufacture_rules -= rules
|
||||
rules_delay += sum(rule.delay for rule in wh_manufacture_rules)
|
||||
manufacturing_lead = bom.company_id.manufacturing_lead if bom and bom.company_id else 0
|
||||
return {
|
||||
'route_type': 'manufacture',
|
||||
'route_name': manufacture_rules[0].route_id.display_name,
|
||||
'route_detail': bom.display_name,
|
||||
'lead_time': product.produce_delay + rules_delay + manufacturing_lead,
|
||||
'manufacture_delay': product.produce_delay + rules_delay + manufacturing_lead,
|
||||
'lead_time': bom.produce_delay + rules_delay + bom.days_to_prepare_mo,
|
||||
'manufacture_delay': bom.produce_delay + rules_delay,
|
||||
'bom': bom,
|
||||
}
|
||||
return {}
|
||||
|
||||
@api.model
|
||||
def _get_availabilities(self, product, quantity, product_info, bom_key, quantities_info, level, ignore_stock=False, components=False, bom_line=None):
|
||||
def _get_availabilities(self, product, quantity, product_info, bom_key, quantities_info, level, ignore_stock=False, components=False, bom_line=None, report_line=False):
|
||||
# Get availabilities according to stock (today & forecasted).
|
||||
stock_state, stock_delay = ('unavailable', False)
|
||||
if not ignore_stock:
|
||||
|
|
@ -620,11 +652,15 @@ class ReportBomStructure(models.AbstractModel):
|
|||
components = components or []
|
||||
route_info = product_info[product.id].get(bom_key)
|
||||
resupply_state, resupply_delay = ('unavailable', False)
|
||||
if product and product.detailed_type != 'product':
|
||||
if product and not product.is_storable:
|
||||
resupply_state, resupply_delay = ('available', 0)
|
||||
elif route_info:
|
||||
resupply_state, resupply_delay = self._get_resupply_availability(route_info, components)
|
||||
|
||||
if resupply_state == "unavailable" and route_info == {} and components and report_line and report_line['phantom_bom']:
|
||||
val = self._get_last_availability(report_line)
|
||||
return val
|
||||
|
||||
base = {
|
||||
'resupply_avail_delay': resupply_delay,
|
||||
'stock_avail_state': stock_state,
|
||||
|
|
@ -650,26 +686,25 @@ class ReportBomStructure(models.AbstractModel):
|
|||
return ('available', 0)
|
||||
if closest_forecasted == date.max:
|
||||
return ('unavailable', False)
|
||||
date_today = self.env.context.get('from_date', fields.date.today())
|
||||
if product and product.detailed_type != 'product':
|
||||
date_today = self.env.context.get('from_date', fields.Date.today())
|
||||
if product and not product.is_storable:
|
||||
return ('available', 0)
|
||||
|
||||
stock_loc = quantities_info['stock_loc']
|
||||
product_info[product.id]['consumptions'][stock_loc] += quantity
|
||||
# Check if product is already in stock with enough quantity
|
||||
if product and float_compare(product_info[product.id]['consumptions'][stock_loc], quantities_info['free_qty'], precision_rounding=product.uom_id.rounding) <= 0:
|
||||
if product and product.uom_id.compare(product_info[product.id]['consumptions'][stock_loc], quantities_info['free_qty']) <= 0:
|
||||
return ('available', 0)
|
||||
|
||||
# No need to check forecast if the product isn't located in our stock
|
||||
if stock_loc == 'in_stock':
|
||||
domain = [('state', '=', 'forecast'), ('date', '>=', date_today), ('product_id', '=', product.id), ('product_qty', '>=', product_info[product.id]['consumptions'][stock_loc])]
|
||||
if self.env.context.get('warehouse'):
|
||||
domain.append(('warehouse_id', '=', self.env.context.get('warehouse')))
|
||||
if self.env.context.get('warehouse_id'):
|
||||
domain.append(('warehouse_id', '=', self.env.context.get('warehouse_id')))
|
||||
|
||||
# Seek the closest date in the forecast report where consummed quantity >= forecasted quantity
|
||||
if not closest_forecasted:
|
||||
closest_forecasted = self.env['report.stock.quantity']._read_group(domain, ['min_date:min(date)', 'product_id'], ['product_id'])
|
||||
closest_forecasted = closest_forecasted and closest_forecasted[0]['min_date']
|
||||
[closest_forecasted] = self.env['report.stock.quantity']._read_group(domain, aggregates=['date:min'])[0]
|
||||
if closest_forecasted:
|
||||
days_to_forecast = (closest_forecasted - date_today).days
|
||||
return ('expected', days_to_forecast)
|
||||
|
|
@ -698,7 +733,7 @@ class ReportBomStructure(models.AbstractModel):
|
|||
|
||||
@api.model
|
||||
def _format_date_display(self, state, delay):
|
||||
date_today = self.env.context.get('from_date', fields.date.today())
|
||||
date_today = self.env.context.get('from_date', fields.Date.today())
|
||||
if state == 'available':
|
||||
return _('Available')
|
||||
if state == 'unavailable':
|
||||
|
|
@ -711,4 +746,112 @@ class ReportBomStructure(models.AbstractModel):
|
|||
|
||||
@api.model
|
||||
def _has_attachments(self, data):
|
||||
return data['attachment_ids'] or any(self._has_attachments(component) for component in data.get('components', []))
|
||||
return data['has_attachments'] or any(self._has_attachments(component) for component in data.get('components', []))
|
||||
|
||||
def _merge_components(self, component_1, component_2):
|
||||
qty = component_2['quantity']
|
||||
component_1["quantity"] = component_1["quantity"] + qty
|
||||
component_1["base_bom_line_qty"] = component_1["quantity"] + qty
|
||||
component_1["bom_cost"] = component_1["bom_cost"] + component_2["bom_cost"]
|
||||
if component_2.get('availability_delay') is False or component_2.get('availability_delay') >= component_1.get('availability_delay'):
|
||||
component_1.update(self._format_availability(component_2))
|
||||
if not component_1.get("components"):
|
||||
return
|
||||
for index in range(len(component_1.get("components"))):
|
||||
self._merge_components(component_1["components"][index], component_2["components"][index])
|
||||
|
||||
def _get_last_availability(self, report_line):
|
||||
delay = 0
|
||||
component_max_delay = False
|
||||
for component in report_line["components"]:
|
||||
if component["availability_delay"] is False:
|
||||
component_max_delay = component
|
||||
break
|
||||
elif component["availability_delay"] >= delay:
|
||||
component_max_delay = component
|
||||
delay = component["availability_delay"]
|
||||
return self._format_availability(component_max_delay)
|
||||
|
||||
def _format_availability(self, component):
|
||||
return {
|
||||
'resupply_avail_delay': component['resupply_avail_delay'],
|
||||
'stock_avail_state': component['stock_avail_state'],
|
||||
'availability_display': component['availability_display'],
|
||||
'availability_state': component['availability_state'],
|
||||
'availability_delay': component['availability_delay'],
|
||||
}
|
||||
|
||||
def _simulate_bom_planning(self, bom, product, start_date, quantity, simulated_leaves_per_workcenter=False):
|
||||
""" Simulate planning of all the operations depending on the workcenters work schedule.
|
||||
(see '_plan_workorders' & '_link_workorders_and_moves')
|
||||
"""
|
||||
bom.ensure_one()
|
||||
if not bom.operation_ids:
|
||||
return {}
|
||||
if not product:
|
||||
product = bom.product_id or bom.product_tmpl_id.product_variant_id
|
||||
planning_per_operation = {}
|
||||
if simulated_leaves_per_workcenter is False:
|
||||
simulated_leaves_per_workcenter = defaultdict(list)
|
||||
if bom.allow_operation_dependencies:
|
||||
final_operations = bom.operation_ids.filtered(lambda o: not o.needed_by_operation_ids)
|
||||
for operation in final_operations:
|
||||
if operation._skip_operation_line(product):
|
||||
continue
|
||||
self._simulate_operation_planning(operation, product, start_date, quantity, planning_per_operation, simulated_leaves_per_workcenter)
|
||||
else:
|
||||
for operation in bom.operation_ids:
|
||||
if operation._skip_operation_line(product):
|
||||
continue
|
||||
self._simulate_operation_planning(operation, product, start_date, quantity, planning_per_operation, simulated_leaves_per_workcenter)
|
||||
start_date = planning_per_operation[operation]['date_finished']
|
||||
return planning_per_operation
|
||||
|
||||
def _simulate_operation_planning(self, operation, product, start_date, quantity, planning_per_operation=False, simulated_leaves_per_workcenter=False):
|
||||
""" Simulate planning of an operation depending on its workcenter/alternatives work schedule.
|
||||
(see '_plan_workorder')
|
||||
"""
|
||||
operation.ensure_one()
|
||||
if planning_per_operation is False:
|
||||
planning_per_operation = {}
|
||||
if simulated_leaves_per_workcenter is False:
|
||||
simulated_leaves_per_workcenter = defaultdict(list)
|
||||
# Plan operation after its predecessors
|
||||
date_start = max(start_date, datetime.now())
|
||||
for op in operation.blocked_by_operation_ids:
|
||||
if op._skip_operation_line(product):
|
||||
continue
|
||||
if op not in planning_per_operation:
|
||||
self._simulate_operation_planning(op, product, start_date, quantity, planning_per_operation, simulated_leaves_per_workcenter)
|
||||
date_start = max(date_start, planning_per_operation[op]['date_finished'])
|
||||
# Consider workcenter and alternatives
|
||||
workcenters = operation.workcenter_id | operation.workcenter_id.alternative_workcenter_ids
|
||||
best_date_finished = datetime.max
|
||||
best_date_start = best_workcenter = best_duration_expected = None
|
||||
for workcenter in workcenters:
|
||||
if not workcenter.resource_calendar_id:
|
||||
raise UserError(_('There is no defined calendar on workcenter %s.', workcenter.name))
|
||||
# Compute theoretical duration
|
||||
duration_expected = operation.with_context(product=product, quantity=quantity, workcenter=workcenter).time_total
|
||||
# Try to plan on workcenter
|
||||
from_date, to_date = workcenter._get_first_available_slot(date_start, duration_expected, extra_leaves_slots=simulated_leaves_per_workcenter[workcenter])
|
||||
# If the workcenter is unavailable, try planning on the next one
|
||||
if not from_date:
|
||||
continue
|
||||
# Check if this workcenter is better than the previous ones
|
||||
if to_date and to_date < best_date_finished:
|
||||
best_date_start = from_date
|
||||
best_date_finished = to_date
|
||||
best_workcenter = workcenter
|
||||
best_duration_expected = duration_expected
|
||||
# If none of the workcenter are available, raise
|
||||
if best_date_finished == datetime.max:
|
||||
raise UserError(_('Impossible to plan. Please check the workcenter availabilities.'))
|
||||
planning_per_operation[operation] = {
|
||||
'date_start': best_date_start,
|
||||
'date_finished': best_date_finished,
|
||||
'workcenter': best_workcenter,
|
||||
'duration_expected': best_duration_expected,
|
||||
}
|
||||
simulated_leaves_per_workcenter[best_workcenter].append((best_date_start, best_date_finished))
|
||||
return planning_per_operation
|
||||
|
|
|
|||
|
|
@ -10,79 +10,81 @@
|
|||
<h6 t-if="data['bom_code']">Reference: <t t-esc="data['bom_code']"/></h6>
|
||||
</div>
|
||||
<t t-set="currency" t-value="data['currency']"/>
|
||||
<table class="o_mrp_bom_expandable table">
|
||||
<table class="o_mrp_bom_expandable table table-borderless">
|
||||
<thead>
|
||||
<tr>
|
||||
<th name="th_mrp_bom_h">Product</th>
|
||||
<th class="text-end">Quantity</th>
|
||||
<th class="text-end" groups="uom.group_uom"/>
|
||||
<th t-if="data['show_availabilities']" class="text-end">Ready to Produce</th>
|
||||
<th t-if="data['show_availabilities']" class="text-end">Free to Use / On Hand</th>
|
||||
<th t-if="data['show_availabilities']" class="text-center">Availability</th>
|
||||
<th t-if="data['show_lead_times']" class="text-end">Lead Time</th>
|
||||
<th>Route</th>
|
||||
<th t-if="data['show_costs']" class="text-end">BoM Cost</th>
|
||||
<th t-if="data['show_costs']" class="text-end">Product Cost</th>
|
||||
<th t-if="data['forecast_mode']" class="text-end">Free to Use / On Hand</th>
|
||||
<th t-if="data['forecast_mode']" class="text-center">Status</th>
|
||||
<th t-if="data['forecast_mode']" class="text-center">Availability</th>
|
||||
<th t-if="data['forecast_mode']" class="text-end">Lead Time</th>
|
||||
<th t-if="data['forecast_mode']">Route</th>
|
||||
<th t-else=""/>
|
||||
<th class="text-end">BoM Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td name="td_mrp_bom" t-esc="data['name']"/>
|
||||
<td class="text-end" t-esc="data['quantity']" t-options='{"widget": "float", "decimal_precision": "Product Unit of Measure"}'/>
|
||||
<td class="text-end" t-esc="data['quantity']" t-options='{"widget": "float", "decimal_precision": "Product Unit"}'/>
|
||||
<td class="text-start" groups="uom.group_uom" t-esc="data['uom_name']"/>
|
||||
<td t-if="data['show_availabilities']" class="text-end" t-esc="data['producible_qty']" t-options="{'widget': 'float', 'precision': 0}"/>
|
||||
<td t-if="data['show_availabilities']" class="text-end">
|
||||
<t t-esc="data['quantity_available']" t-options='{"widget": "float", "decimal_precision": "Product Unit of Measure"}'/> /
|
||||
<t t-esc="data['quantity_on_hand']" t-options='{"widget": "float", "decimal_precision": "Product Unit of Measure"}'/>
|
||||
<td t-if="data['forecast_mode']" class="text-end">
|
||||
<t t-esc="data['quantity_available']" t-options='{"widget": "float", "decimal_precision": "Product Unit"}'/> /
|
||||
<t t-esc="data['quantity_on_hand']" t-options='{"widget": "float", "decimal_precision": "Product Unit"}'/>
|
||||
</td>
|
||||
<td t-if="data['show_availabilities']" class="text-center">
|
||||
<td t-if="data['forecast_mode']" class="text-center">
|
||||
<t t-if="data['status']" t-esc="data['status']"/>
|
||||
</td>
|
||||
<td t-if="data['forecast_mode']" class="text-center">
|
||||
<t t-if="data.get('components_available', None) != None">
|
||||
<span t-attf-class="{{'text-success' if data['components_available'] and data['availability_state'] != 'unavailable' else 'text-danger' }}" t-esc="data['availability_display']"/>
|
||||
</t>
|
||||
</td>
|
||||
<td t-if="data['show_lead_times']" class="text-end">
|
||||
<td t-if="data['forecast_mode']" class="text-end">
|
||||
<span t-if="data['lead_time'] is not False">
|
||||
<t t-esc="data['lead_time']" t-options="{'widget': 'float', 'precision': 0}"/>
|
||||
Days
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<td t-if="data['forecast_mode']">
|
||||
<span t-if="data['route_name']" t-attf-class="{{'text-danger' if data.get('route_alert') else ''}}"><t t-esc="data['route_name']"/>: </span>
|
||||
<span t-esc="data['route_detail']"/>
|
||||
</td>
|
||||
<td t-if="data['show_costs']" class="text-end" t-esc="data['bom_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
<td t-if="data['show_costs']" class="text-end" t-esc="data['prod_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
<td t-else=""/>
|
||||
<td class="text-end" t-esc="data['bom_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</tr>
|
||||
<t t-call="mrp.report_mrp_bom_pdf_line"/>
|
||||
<tr t-if="data['show_costs']">
|
||||
</tbody>
|
||||
<tfoot t-if="data['quantity'] > 1">
|
||||
<tr>
|
||||
<td name="td_mrp_bom_f" class="text-end">
|
||||
<span t-if="data['byproducts']" t-esc="data['name']"/>
|
||||
</td>
|
||||
<td class="text-end"><strong>Unit Cost</strong></td>
|
||||
<td class="text-start" groups="uom.group_uom" t-esc="data['uom_name']"/>
|
||||
<td t-if="data['show_availabilities']"/>
|
||||
<td t-if="data['show_availabilities']"/>
|
||||
<td t-if="data['show_availabilities']"/>
|
||||
<td t-if="data['show_lead_times']"/>
|
||||
<td/>
|
||||
<td class="text-end" t-esc="data['bom_cost'] / data['quantity']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
<td class="text-end" t-esc="data['prod_cost']/data['quantity']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
<td class="text-start" groups="uom.group_uom" t-esc="data['uom_name']"/>
|
||||
<td t-if="data['forecast_mode']"/>
|
||||
<td t-if="data['forecast_mode']"/>
|
||||
<td t-if="data['forecast_mode']"/>
|
||||
<td t-if="data['forecast_mode']"/>
|
||||
<td class="text-end"><strong>Unit Cost</strong></td>
|
||||
<td class="text-end"><t t-esc="data['bom_cost'] / data['quantity']" t-options='{"widget": "monetary", "display_currency": currency}'/></td>
|
||||
</tr>
|
||||
<t t-if="data['show_costs'] and data['byproducts']" t-foreach="data['byproducts']" t-as="byproduct">
|
||||
<t t-if="data['byproducts']" t-foreach="data['byproducts']" t-as="byproduct">
|
||||
<tr t-if="byproduct['quantity'] > 0">
|
||||
<td name="td_mrp_bom_byproducts_f" class="text-end" t-esc="byproduct['name']"/>
|
||||
<td class="text-end"><strong>Unit Cost</strong></td>
|
||||
<td class="text-start" groups="uom.group_uom" t-esc="byproduct['uom_name']"/>
|
||||
<td t-if="data['show_availabilities']"/>
|
||||
<td t-if="data['show_availabilities']"/>
|
||||
<td t-if="data['show_availabilities']"/>
|
||||
<td t-if="data['show_lead_times']"/>
|
||||
<td/>
|
||||
<td class="text-end" t-esc="byproduct['bom_cost'] / byproduct['quantity']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
<td class="text-end" t-esc="byproduct['prod_cost'] / byproduct['quantity']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
<td class="text-start" groups="uom.group_uom" t-esc="byproduct['uom_name']"/>
|
||||
<td t-if="data['forecast_mode']"/>
|
||||
<td t-if="data['forecast_mode']"/>
|
||||
<td t-if="data['forecast_mode']"/>
|
||||
<td t-if="data['forecast_mode']"/>
|
||||
<td class="text-end"><strong>Unit Cost</strong></td>
|
||||
<td class="text-end"><t t-esc="byproduct['bom_cost'] / byproduct['quantity']" t-options='{"widget": "monetary", "display_currency": currency}'/></td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<div t-else="" class="d-flex align-items-center justify-content-center h-50">
|
||||
|
|
@ -94,45 +96,43 @@
|
|||
<template id="report_mrp_bom_pdf_line">
|
||||
<t t-set="currency" t-value="data['currency']"/>
|
||||
<t t-foreach="data['lines']" t-as="l">
|
||||
<tr t-if="l['visible'] and (l['type'] != 'operation' or data['show_operations'])">
|
||||
<tr t-if="l['visible']">
|
||||
<td name="td_mrp_code">
|
||||
<span t-attf-style="margin-left: {{ str(l['level'] * 20) }}px"/>
|
||||
<span t-esc="l['name']"/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<t t-if="l['type'] == 'operation'" t-esc="l['quantity']" t-options='{"widget": "float_time"}'/>
|
||||
<t t-else="" t-esc="l['quantity']" t-options='{"widget": "float", "decimal_precision": "Product Unit of Measure"}'/>
|
||||
<t t-else="" t-esc="l['quantity']" t-options='{"widget": "float", "decimal_precision": "Product Unit"}'/>
|
||||
</td>
|
||||
<td class="text-start" groups="uom.group_uom">
|
||||
<t t-esc="l['uom']"/>
|
||||
</td>
|
||||
<td t-if="data['show_availabilities']" class="text-end">
|
||||
<t t-if="l.get('producible_qty', False) is not False" t-esc="l['producible_qty']" t-options="{'widget': 'float', 'precision': 0}"/>
|
||||
</td>
|
||||
<td t-if="data['show_availabilities']" class="text-end">
|
||||
<t t-if="l.get('quantity_available', False) is not False">
|
||||
<t t-esc="l['quantity_available']" t-options='{"widget": "float", "decimal_precision": "Product Unit of Measure"}'/> /
|
||||
<t t-esc="l['quantity_on_hand']" t-options='{"widget": "float", "decimal_precision": "Product Unit of Measure"}'/>
|
||||
<td t-if="data['forecast_mode']" class="text-end">
|
||||
<t t-if="l.get('is_storable', False)">
|
||||
<t t-esc="l['quantity_available']" t-options='{"widget": "float", "decimal_precision": "Product Unit"}'/> /
|
||||
<t t-esc="l['quantity_on_hand']" t-options='{"widget": "float", "decimal_precision": "Product Unit"}'/>
|
||||
</t>
|
||||
</td>
|
||||
<td t-if="data['show_availabilities']" class="text-center">
|
||||
<td t-if="data['forecast_mode']" class="text-center">
|
||||
<t t-if="l.get('status', '') != ''" t-esc="l['status']"/>
|
||||
</td>
|
||||
<td t-if="data['forecast_mode']" class="text-center">
|
||||
<t t-if="l.get('availability_state', None) != None">
|
||||
<span t-attf-class="{{'text-success' if l['availability_state'] == 'available' else ''}}{{'text-warning' if l['availability_state'] == 'expected' else ''}}{{'text-danger' if l['availability_state'] == 'unavailable' else ''}}" t-esc="l['availability_display']"/>
|
||||
</t>
|
||||
</td>
|
||||
<td t-if="data['show_lead_times']" class="text-end">
|
||||
<td t-if="data['forecast_mode']" class="text-end">
|
||||
<span t-if="l.get('lead_time', False) is not False">
|
||||
<t t-esc="l['lead_time']" t-options="{'widget': 'float', 'precision': 0}"/>
|
||||
Days
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<td t-if="data['forecast_mode']">
|
||||
<span t-if="l.get('route_name')" t-attf-class="{{'text-danger' if l.get('route_alert') else ''}}"><t t-esc="l['route_name']"/>: <t t-esc="l['route_detail']"/></span>
|
||||
</td>
|
||||
<td t-if="data['show_costs']" t-attf-class="text-end {{ 'text-muted' if l['type'] == 'component' else '' }}" t-esc="l['bom_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
<td t-if="data['show_costs']" class="text-end">
|
||||
<span t-if="'prod_cost' in l" t-esc="l['prod_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</td>
|
||||
<td t-else=""/>
|
||||
<td t-attf-class="text-end {{ 'text-muted' if l['type'] == 'component' else '' }}" t-esc="l['bom_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</tr>
|
||||
</t>
|
||||
</template>
|
||||
|
|
|
|||
1021
odoo-bringout-oca-ocb-mrp/mrp/report/mrp_report_mo_overview.py
Normal file
1021
odoo-bringout-oca-ocb-mrp/mrp/report/mrp_report_mo_overview.py
Normal file
File diff suppressed because it is too large
Load diff
279
odoo-bringout-oca-ocb-mrp/mrp/report/mrp_report_mo_overview.xml
Normal file
279
odoo-bringout-oca-ocb-mrp/mrp/report/mrp_report_mo_overview.xml
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<template id="report_mo_overview">
|
||||
<t t-set="data_report_landscape" t-value="True"/>
|
||||
<t t-call="web.basic_layout">
|
||||
<t t-foreach="docs" t-as="data">
|
||||
<div class="page">
|
||||
<t t-call="mrp.mo_overview_content"/>
|
||||
</div>
|
||||
<p style="page-break-before:always;"> </p>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="mo_overview_content">
|
||||
<t t-set="currency" t-value="data['summary']['currency']"/>
|
||||
<div class="container bg-view">
|
||||
<h2><t t-out="data['name']"/> - Overview</h2>
|
||||
<table name="overview" class="table table-borderless">
|
||||
<thead>
|
||||
<tr>
|
||||
<th/>
|
||||
<th t-if="data['show_replenishments']" class="text-center">Status</th>
|
||||
<th t-attf-class="{{ 'text-center' if data['show_uom'] else 'text-end' }}" t-attf-colspan="{{ 2 if data['show_uom'] else 1 }}">Quantity</th>
|
||||
<th t-if="data['show_availabilities']" class="text-end">Free to use / On Hand</th>
|
||||
<th t-if="data['show_availabilities']" class="text-end">Reserved</th>
|
||||
<th t-if="data['show_receipts']" class="text-end">Receipt</th>
|
||||
<th class="text-end" t-if="data['show_unit_costs']">Unit Cost</th>
|
||||
<th t-if="data['show_mo_costs']" class="text-end">MO Cost</th>
|
||||
<th t-if="data['show_bom_costs']" class="text-end">BoM Cost</th>
|
||||
<th t-if="data['show_real_costs']" class="text-end">Real Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-call="mrp.mo_overview_line">
|
||||
<t t-set="line" t-value="data['summary']"/>
|
||||
</t>
|
||||
<t t-call="mrp.mo_overview_components">
|
||||
<t t-set="components" t-value="data['components']"/>
|
||||
<t t-set="operations" t-value="data['operations']"/>
|
||||
<t t-set="byproducts" t-value="data['byproducts']"/>
|
||||
</t>
|
||||
</tbody>
|
||||
<t t-if="data['show_mo_costs'] or data['show_real_costs']">
|
||||
<tr name="unit_cost" class="border-top border-dark">
|
||||
<td class="text-end" t-att-colspan="data['footer_colspan']">Unit Cost</td>
|
||||
<td t-if="data['show_mo_costs']" t-attf-class="text-end" t-out="data['extras']['unit_mo_cost']" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
|
||||
<td t-if="data['show_bom_costs']" class="text-end" t-out="data['extras']['unit_bom_cost']" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
|
||||
<td t-if="data['show_real_costs']" t-attf-class="text-end" t-out="data['extras']['unit_real_cost']" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-if="data['summary'].get('state') == 'done' and data.get('extras')">
|
||||
<tr>
|
||||
<td class="text-end" t-att-colspan="data['footer_colspan']">Total Cost of Components</td>
|
||||
<td t-attf-class="text-end" t-if="data['show_mo_costs']">
|
||||
<t t-out="data['extras'].get('total_mo_cost_components')" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
|
||||
</td>
|
||||
<td class="text-end" t-if="data['show_bom_costs']">
|
||||
<t t-out="data['extras'].get('total_bom_cost_components')" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
|
||||
</td>
|
||||
<td class="text-end" t-if="data['show_real_costs']">
|
||||
<t t-out="data['extras'].get('total_real_cost_components')" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="data['summary'].get('quantity') != 1">
|
||||
<td class="text-end" t-att-colspan="data['footer_colspan']">
|
||||
Cost of Components per unit
|
||||
<t t-if="data['show_uom']">(in <t t-out="data['summary'].get('uom_name')"/>)</t>
|
||||
</td>
|
||||
<td t-attf-class="text-end {{ data['get_color'](data['extras'].get('unit_mo_cost_components_decorator')) }}" t-if="data['show_mo_costs']">
|
||||
<t t-out="data['extras'].get('unit_mo_cost_components')" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
|
||||
</td>
|
||||
<td class="text-end" t-if="data['show_bom_costs']">
|
||||
<t t-out="data['extras'].get('unit_bom_cost_components')" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
|
||||
</td>
|
||||
<td class="text-end" t-if="data['show_real_costs']">
|
||||
<t t-out="data['extras'].get('unit_real_cost_components')" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="len(data.get('operations', {}).get('details', []))">
|
||||
<td class="text-end" t-att-colspan="data['footer_colspan']">Total Cost of Operations</td>
|
||||
<td t-attf-class="text-end {{ data['get_color'](data['extras'].get('total_mo_cost_operations_decorator')) }}" t-if="data['show_mo_costs']">
|
||||
<t t-out="data['extras'].get('total_mo_cost_operations')" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
|
||||
</td>
|
||||
<td class="text-end" t-if="data['show_bom_costs']">
|
||||
<t t-out="data['extras'].get('total_bom_cost_operations')" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
|
||||
</td>
|
||||
<td class="text-end" t-if="data['show_real_costs']"> <!-- AJOC TODO add decorator here? more below -->
|
||||
<t t-out="data['extras'].get('total_real_cost_operations')" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="len(data.get('operations', {}).get('details', [])) and data['summary'].get('quantity') != 1">
|
||||
<td class="text-end" t-att-colspan="data['footer_colspan']">
|
||||
Cost of Operations per unit
|
||||
<t t-if="data['show_uom']">(in <t t-out="data['summary'].get('uom_name')"/>)</t>
|
||||
</td>
|
||||
<td t-attf-class="text-end {{ data['get_color'](data['extras'].get('unit_mo_cost_operations_decorator')) }}" t-if="data['show_mo_costs']">
|
||||
<t t-out="data['extras'].get('unit_mo_cost_operations')" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
|
||||
</td>
|
||||
<td class="text-end" t-if="data['show_bom_costs']">
|
||||
<t t-out="data['extras'].get('unit_bom_cost_operations')" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
|
||||
</td>
|
||||
<td class="text-end" t-if="data['show_real_costs']">
|
||||
<t t-out="data['extras'].get('unit_real_cost_operations')" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="len(data.get('cost_breakdown', [])) and len(data.get('operations', {}).get('details', []))">
|
||||
<td class="text-end" t-att-colspan="data['footer_colspan']">Total Cost of Production</td>
|
||||
<td t-attf-class="text-end" t-if="data['show_mo_costs']">
|
||||
<t t-out="data['extras'].get('total_mo_cost')" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
|
||||
</td>
|
||||
<td class="text-end" t-if="data['show_bom_costs']">
|
||||
<t t-out="data['extras'].get('total_bom_cost')" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
|
||||
</td>
|
||||
<td class="text-end" t-if="data['show_real_costs']">
|
||||
<t t-out="data['extras'].get('total_real_cost')" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</table>
|
||||
<t t-if="len(data.get('cost_breakdown', []))">
|
||||
<h3>Cost Breakdown of Products</h3>
|
||||
<table name="breakdown" class="table table-borderless">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-start">Product</th>
|
||||
<th class="text-end">Avg Cost of Components per Unit</th>
|
||||
<th t-if="len(data.get('operations', {}).get('details', []))" class="text-end">Avg Cost of Operations per Unit</th>
|
||||
<th class="text-end">Avg Total Cost per Unit</th>
|
||||
<th t-if="data['show_uom']" class="text-end">Unit of Measure</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="data['cost_breakdown']" t-as="line">
|
||||
<td class="text-start" t-out="line['name']"/>
|
||||
<td class="text-end" t-out="line['unit_avg_cost_component']" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
|
||||
<td t-if="len(data.get('operations', {}).get('details', []))" class="text-end" t-out="line['unit_avg_cost_operation']" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
|
||||
<td class="text-end" t-out="line['unit_avg_total_cost']" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
|
||||
<td t-if="data['show_uom']" class="text-end" t-out="line['uom_name']"/>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</t>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="mo_overview_line">
|
||||
<tr>
|
||||
<td class="text-start">
|
||||
<div t-attf-style="margin-left: {{ line['level'] * 20 }}px" class="d-inline-block" t-out="line['name']"/>
|
||||
</td>
|
||||
<td t-if="data['show_replenishments']" class="text-center" t-out="line.get('formatted_state', '')"/>
|
||||
<td t-attf-class="text-end {{ data['get_color'](line.get('quantity_decorator')) }}">
|
||||
<t t-if="line.get('model') == 'mrp.workorder'" t-out="line['quantity']" t-options="{'widget': 'float_time'}"/>
|
||||
<t t-else="" t-out="line['quantity']" t-options="{'widget': 'float', 'precision': line['uom_precision']}"/>
|
||||
</td>
|
||||
<td t-if="data['show_uom']" class="text-start" t-out="line['uom_name']"/>
|
||||
<td t-if="data['show_availabilities']" class="text-end">
|
||||
<t t-if="line.get('quantity_on_hand', False) is not False">
|
||||
<t t-out="line['quantity_free']" t-options="{'widget': 'float', 'precision': line['uom_precision']}"/> /
|
||||
<t t-out="line['quantity_on_hand']" t-options="{'widget': 'float', 'precision': line['uom_precision']}"/>
|
||||
</t>
|
||||
</td>
|
||||
<td t-if="data['show_availabilities']" class="text-end">
|
||||
<t t-if="line.get('quantity_reserved', False) is not False" t-out="line['quantity_reserved']" t-options="{'widget': 'float', 'precision': line['uom_precision']}"/>
|
||||
</td>
|
||||
<td t-if="data['show_receipts']" class="text-end">
|
||||
<span t-if="line.get('receipt')" t-att-class="data['get_color'](line['receipt'].get('decorator'))" t-out="line['receipt']['display']"/>
|
||||
</td>
|
||||
<td class="text-end" t-if="data['show_unit_costs']">
|
||||
<t t-out="line['unit_cost']" t-options="{'widget': 'monetary', 'display_currency': line['currency']}"/>
|
||||
</td>
|
||||
<td t-if="data['show_mo_costs']" t-attf-class="text-end {{ data['get_color'](line.get('mo_cost_decorator')) }}">
|
||||
<t t-out="line['mo_cost']" t-options="{'widget': 'monetary', 'display_currency': line['currency']}"/>
|
||||
</td>
|
||||
<td t-if="data['show_bom_costs']" class="text-end">
|
||||
<t t-out="line['bom_cost']" t-options="{'widget': 'monetary', 'display_currency': line['currency']}"/>
|
||||
</td>
|
||||
<td t-if="data['show_real_costs']" class="text-end">
|
||||
<t t-out="line['real_cost']" t-options="{'widget': 'monetary', 'display_currency': line['currency']}"/>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<template id="mo_overview_components">
|
||||
<t t-foreach="components" t-as="component">
|
||||
<t t-call="mrp.mo_overview_line">
|
||||
<t t-set="line" t-value="component['summary']"/>
|
||||
</t>
|
||||
<t t-if="component['summary']['index'] in data['unfolded_ids'] and component.get('replenishments', [])" t-foreach="component['replenishments']" t-as="replenishment">
|
||||
<t t-call="mrp.mo_overview_line">
|
||||
<t t-set="line" t-value="replenishment['summary']"/>
|
||||
</t>
|
||||
<t t-if="replenishment['summary']['index'] in data['unfolded_ids'] and (replenishment.get('components', []) or replenishment.get('operations', {}).get('details', []))">
|
||||
<t t-call="mrp.mo_overview_components">
|
||||
<t t-set="components" t-value="replenishment.get('components', [])"/>
|
||||
<t t-set="operations" t-value="replenishment.get('operations', {})"/>
|
||||
<t t-set="byproducts" t-value="replenishment.get('byproducts', {})"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
<t t-call="mrp.mo_overview_operations">
|
||||
<t t-set="summary" t-value="operations.get('summary', {})"/>
|
||||
<t t-set="operations" t-value="operations.get('details', [])"/>
|
||||
</t>
|
||||
<t t-call="mrp.mo_overview_byproducts">
|
||||
<t t-set="summary" t-value="byproducts.get('summary', {})"/>
|
||||
<t t-set="byproducts" t-value="byproducts.get('details', [])"/>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="mo_overview_operations">
|
||||
<t t-if="operations">
|
||||
<tr>
|
||||
<td class="text-start">
|
||||
<span t-attf-style="margin-left: {{ (operations[0]['level'] - 1 if len(operations) else 0) * 20 }}px"/>
|
||||
Operations
|
||||
</td>
|
||||
<td t-if="data['show_replenishments']" class="text-center"/>
|
||||
<td t-attf-class="text-end {{ data['get_color'](summary.get('quantity_decorator')) }}">
|
||||
<t t-if="summary.get('done')" t-out="summary['quantity']" t-options="{'widget': 'float', 'precision': operations[0].get('uom_precision', 0)}"/>
|
||||
<t t-else="" t-out="summary['quantity']" t-options="{'widget': 'float_time'}"/>
|
||||
</td>
|
||||
<td t-if="data['show_uom']" class="text-start" t-out="summary['uom_name']"/>
|
||||
<td t-if="data['show_availabilities']" class="text-end"/>
|
||||
<td t-if="data['show_availabilities']" class="text-end"/>
|
||||
<td t-if="data['show_receipts']" class="text-end"/>
|
||||
<td t-if="data['show_unit_costs']"/>
|
||||
<td t-if="data['show_mo_costs']" t-attf-class="text-end {{ data['get_color'](summary.get('mo_cost_decorator')) }}">
|
||||
<t t-out="summary['mo_cost']" t-options="{'widget': 'monetary', 'display_currency': summary['currency']}"/>
|
||||
</td>
|
||||
<td t-if="data['show_bom_costs']" class="text-end">
|
||||
<t t-out="summary['bom_cost']" t-options="{'widget': 'monetary', 'display_currency': summary['currency']}"/>
|
||||
</td>
|
||||
<td t-if="data['show_real_costs']" class="text-end">
|
||||
<t t-out="summary['real_cost']" t-options="{'widget': 'monetary', 'display_currency': summary['currency']}"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-if="summary['index'] in data['unfolded_ids']" t-foreach="operations" t-as="operation">
|
||||
<t t-call="mrp.mo_overview_line">
|
||||
<t t-set="line" t-value="operation"/>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="mo_overview_byproducts">
|
||||
<t t-if="byproducts">
|
||||
<tr>
|
||||
<td class="text-start">
|
||||
<span t-attf-style="margin-left: {{ (byproducts[0]['level'] - 1 if len(byproducts) else 0) * 20 }}px"/>
|
||||
By-Products
|
||||
</td>
|
||||
<td t-if="data['show_replenishments']" class="text-center"/>
|
||||
<td class="text-end"/>
|
||||
<td t-if="data['show_uom']" class="text-start"/>
|
||||
<td t-if="data['show_availabilities']" class="text-end"/>
|
||||
<td t-if="data['show_availabilities']" class="text-end"/>
|
||||
<td t-if="data['show_receipts']" class="text-end"/>
|
||||
<td t-if="data['show_unit_costs']"/>
|
||||
<td t-if="data['show_mo_costs']" t-attf-class="text-end {{ data['get_color'](summary.get('mo_cost_decorator')) }}">
|
||||
<t t-out="summary['mo_cost']" t-options="{'widget': 'monetary', 'display_currency': summary['currency']}"/>
|
||||
</td>
|
||||
<td t-if="data['show_bom_costs']" class="text-end">
|
||||
<t t-out="summary['bom_cost']" t-options="{'widget': 'monetary', 'display_currency': summary['currency']}"/>
|
||||
</td>
|
||||
<td t-if="data['show_real_costs']" class="text-end">
|
||||
<t t-out="summary['real_cost']" t-options="{'widget': 'monetary', 'display_currency': summary['currency']}"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-if="summary['index'] in data['unfolded_ids']" t-foreach="byproducts" t-as="byproduct">
|
||||
<t t-call="mrp.mo_overview_line">
|
||||
<t t-set="line" t-value="byproduct"/>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
@ -21,6 +21,15 @@
|
|||
<field name="binding_model_id" ref="model_mrp_bom"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
<record id="action_report_mrp_mo_overview" model="ir.actions.report">
|
||||
<field name="name">MO Overview</field>
|
||||
<field name="model">mrp.production</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">mrp.report_mo_overview</field>
|
||||
<field name="report_file">mrp.report_mo_overview</field>
|
||||
<field name="print_report_name">'MO Overview - %s' % object.display_name</field>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
<record id="label_manufacture_template" model="ir.actions.report">
|
||||
<field name="name">Finished Product Label (ZPL)</field>
|
||||
<field name="model">mrp.production</field>
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@
|
|||
<div class="oe_structure"/>
|
||||
<div class="row">
|
||||
<div class="col-7">
|
||||
<h2><span t-field="o.name"/></h2>
|
||||
<h2><span t-field="o.name">Laptop model X</span></h2>
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<span class="text-end">
|
||||
<div t-field="o.name" t-options="{'widget': 'barcode', 'width': 600, 'height': 100, 'img_style': 'width:350px;height:60px'}"/>
|
||||
<span t-field="o.production_id.name" t-options="{'widget': 'barcode', 'width': 600, 'height': 100, 'img_style': 'width:350px;height:60px'}">12345678901</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -20,23 +20,23 @@
|
|||
<div class="row mt32 mb32">
|
||||
<div class="col-3">
|
||||
<strong>Responsible:</strong><br/>
|
||||
<span t-field="o.production_id.user_id"/>
|
||||
<span t-field="o.production_id.user_id">Marc Demo</span>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<strong>Manufacturing Order:</strong><br/>
|
||||
<span t-field="o.production_id.name"/>
|
||||
<span t-field="o.production_id.name">RAM</span>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row mt32 mb32">
|
||||
<div class="col-3">
|
||||
<strong>Finished Product:</strong><br/>
|
||||
<span t-field="o.product_id"/>
|
||||
<span t-field="o.product_id">Laptop</span>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<strong>Quantity to Produce:</strong><br/>
|
||||
<span t-field="o.qty_production"/>
|
||||
<span t-field="o.product_uom_id.name" groups="uom.group_uom"/>
|
||||
<span t-field="o.qty_production">2</span>
|
||||
<span t-field="o.product_uom_id.name" groups="uom.group_uom">Unit</span>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
<odoo>
|
||||
<data>
|
||||
<template id="label_production_view">
|
||||
<t t-set="uom_categ_unit" t-value="env.ref('uom.product_uom_categ_unit')"/>
|
||||
<t t-set="uom_unit" t-value="env.ref('uom.product_uom_unit')"/>
|
||||
<t t-foreach="docs" t-as="production">
|
||||
<t t-foreach="production.move_finished_ids" t-as="move">
|
||||
<t t-foreach="move.move_line_ids" t-as="move_line">
|
||||
<t t-if="move_line.product_uom_id.category_id == uom_categ_unit">
|
||||
<t t-set="qty" t-value="int(move_line.qty_done)"/>
|
||||
<t t-if="move_line.product_id.uom_id._has_common_reference(uom_unit)">
|
||||
<t t-set="qty" t-value="int(move_line.quantity)"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-set="qty" t-value="1"/>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
</xpath>
|
||||
<xpath expr="//t[@name='has_packages']" position="after">
|
||||
<!-- Additional use case: group by kits when no packages exist and then apply use case 1. (serial/lot numbers used/printed) -->
|
||||
<t t-elif="has_kits and not has_packages">
|
||||
<t t-elif="has_kits and not packages_count">
|
||||
<t t-call="mrp.stock_report_delivery_kit_sections"/>
|
||||
<t t-call="mrp.stock_report_delivery_no_kit_section"/>
|
||||
</t>
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
|
||||
<template id="stock_report_delivery_kit_sections">
|
||||
<!-- get all kits-related SML, including subkits -->
|
||||
<t t-set="all_kits_move_lines" t-value="o.move_line_ids.filtered(lambda l: l.move_id.bom_line_id.bom_id.type == 'phantom')"/>
|
||||
<t t-set="all_kits_move_lines" t-value="o.move_ids.move_line_ids.filtered(lambda l: l.move_id.bom_line_id.bom_id.type == 'phantom')"/>
|
||||
<!-- do another map to get unique top level kits -->
|
||||
<t t-set="boms" t-value="has_kits.mapped('move_id.bom_line_id.bom_id')"/>
|
||||
<t t-foreach="boms" t-as="bom">
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
<t t-else="">
|
||||
<t t-set="kit_product" t-value="bom.product_tmpl_id"/>
|
||||
</t>
|
||||
<tr t-att-class="'bg-200 fw-bold o_line_section'">
|
||||
<tr t-att-class="'fw-bold o_line_section'">
|
||||
<td colspan="99">
|
||||
<span t-esc="kit_product.display_name"/>
|
||||
</td>
|
||||
|
|
@ -66,9 +66,9 @@
|
|||
<!-- No kit section is expected to only be called in no packages case -->
|
||||
<template id="stock_report_delivery_no_kit_section">
|
||||
<!-- Do another section for kit-less products if they exist -->
|
||||
<t t-set="no_kit_move_lines" t-value="o.move_line_ids.filtered(lambda l: l.move_id.bom_line_id.bom_id.type != 'phantom')"/>
|
||||
<t t-set="no_kit_move_lines" t-value="o.move_ids.move_line_ids.filtered(lambda l: l.move_id.bom_line_id.bom_id.type != 'phantom')"/>
|
||||
<t t-if="no_kit_move_lines">
|
||||
<tr t-att-class="'bg-200 fw-bold o_line_section'">
|
||||
<tr t-att-class="'fw-bold o_line_section'">
|
||||
<td colspan="99">
|
||||
<span>Products not associated with a kit</span>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="mrp_report_product_product_replenishment" inherit_id="stock.report_product_product_replenishment">
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
@ -5,7 +5,7 @@ from odoo import models
|
|||
from odoo.tools import format_date
|
||||
|
||||
|
||||
class ReceptionReport(models.AbstractModel):
|
||||
class ReportStockReport_Reception(models.AbstractModel):
|
||||
_inherit = 'report.stock.report_reception'
|
||||
|
||||
def _get_docs(self, docids):
|
||||
|
|
@ -23,7 +23,7 @@ class ReceptionReport(models.AbstractModel):
|
|||
|
||||
def _get_moves(self, docs):
|
||||
if self.env.context.get('default_production_ids'):
|
||||
return docs.move_finished_ids.filtered(lambda m: m.product_id.type == 'product' and m.state != 'cancel')
|
||||
return docs.move_finished_ids.filtered(lambda m: m.product_id.is_storable and m.state != 'cancel')
|
||||
return super()._get_moves(docs)
|
||||
|
||||
def _get_extra_domain(self, docs):
|
||||
|
|
@ -33,15 +33,10 @@ class ReceptionReport(models.AbstractModel):
|
|||
|
||||
def _get_formatted_scheduled_date(self, source):
|
||||
if source._name == 'mrp.production':
|
||||
return format_date(self.env, source.date_planned_start)
|
||||
return format_date(self.env, source.date_start)
|
||||
return super()._get_formatted_scheduled_date(source)
|
||||
|
||||
def _action_assign(self, in_move, out_move):
|
||||
if in_move.production_id:
|
||||
in_move.production_id.move_dest_ids |= out_move
|
||||
if not out_move.group_id and out_move._get_source_document() not in [False, out_move.picking_id]:
|
||||
out_move.group_id = out_move._get_source_document()
|
||||
|
||||
def _action_unassign(self, in_move, out_move):
|
||||
super()._action_unassign(in_move, out_move)
|
||||
if in_move.production_id:
|
||||
in_move.production_id.move_dest_ids -= out_move
|
||||
|
|
|
|||
|
|
@ -4,6 +4,5 @@
|
|||
<field name="name">MRP Reception Report</field>
|
||||
<field name="tag">reception_report</field>
|
||||
<field name="res_model">report.stock.report_reception</field>
|
||||
<field name="context">{'default_production_ids': active_ids}</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@
|
|||
from odoo import api, models
|
||||
|
||||
|
||||
class ReportStockRule(models.AbstractModel):
|
||||
class ReportStockReport_Stock_Rule(models.AbstractModel):
|
||||
_inherit = 'report.stock.report_stock_rule'
|
||||
|
||||
@api.model
|
||||
def _get_rule_loc(self, rule, product_id):
|
||||
""" We override this method to handle manufacture rule which do not have a location_src_id.
|
||||
"""
|
||||
res = super(ReportStockRule, self)._get_rule_loc(rule, product_id)
|
||||
res = super()._get_rule_loc(rule, product_id)
|
||||
if rule.action == 'manufacture':
|
||||
res['source'] = product_id.property_stock_production
|
||||
return res
|
||||
|
|
|
|||
|
|
@ -1,47 +1,53 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class ReplenishmentReport(models.AbstractModel):
|
||||
_inherit = 'report.stock.report_product_product_replenishment'
|
||||
class StockForecasted_Product_Product(models.AbstractModel):
|
||||
_inherit = 'stock.forecasted_product_product'
|
||||
|
||||
def _prepare_report_line(self, quantity, move_out=None, move_in=None, replenishment_filled=True, product=False, reserved_move=False, in_transit=False, read=True):
|
||||
line = super()._prepare_report_line(quantity, move_out, move_in, replenishment_filled, product, reserved_move, in_transit, read)
|
||||
|
||||
def _serialize_docs(self, docs, product_template_ids=False, product_variant_ids=False):
|
||||
res = super()._serialize_docs(docs, product_template_ids, product_variant_ids)
|
||||
for i, line in enumerate(docs['lines']):
|
||||
if not line['move_out'] or not line['move_out']['raw_material_production_id']:
|
||||
continue
|
||||
raw_material_production = line['move_out']['raw_material_production_id']
|
||||
res['lines'][i]['move_out']['raw_material_production_id'] = raw_material_production.read(fields=['id', 'unreserve_visible', 'reserve_visible', 'priority'])[0]
|
||||
return res
|
||||
if not move_out or not move_out.raw_material_production_id or not read:
|
||||
return line
|
||||
|
||||
def _move_draft_domain(self, product_template_ids, product_variant_ids, wh_location_ids):
|
||||
in_domain, out_domain = super()._move_draft_domain(product_template_ids, product_variant_ids, wh_location_ids)
|
||||
line['move_out']['raw_material_production_id'] = move_out.raw_material_production_id.read(fields=['id', 'unreserve_visible', 'reserve_visible', 'priority'])[0]
|
||||
return line
|
||||
|
||||
def _move_draft_domain(self, product_template_ids, product_ids, wh_location_ids):
|
||||
in_domain, out_domain = super()._move_draft_domain(product_template_ids, product_ids, wh_location_ids)
|
||||
in_domain += [('production_id', '=', False)]
|
||||
out_domain += [('raw_material_production_id', '=', False)]
|
||||
return in_domain, out_domain
|
||||
|
||||
def _compute_draft_quantity_count(self, product_template_ids, product_variant_ids, wh_location_ids):
|
||||
res = super()._compute_draft_quantity_count(product_template_ids, product_variant_ids, wh_location_ids)
|
||||
res['draft_production_qty'] = {}
|
||||
domain = self._product_domain(product_template_ids, product_variant_ids)
|
||||
def _get_report_header(self, product_template_ids, product_ids, wh_location_ids):
|
||||
res = super()._get_report_header(product_template_ids, product_ids, wh_location_ids)
|
||||
domain = self._product_domain(product_template_ids, product_ids)
|
||||
domain += [('state', '=', 'draft')]
|
||||
|
||||
# Pending incoming quantity.
|
||||
mo_domain = domain + [('location_dest_id', 'in', wh_location_ids)]
|
||||
grouped_mo = self.env['mrp.production'].read_group(mo_domain, ['product_qty:sum'], 'product_id')
|
||||
res['draft_production_qty']['in'] = sum(mo['product_qty'] for mo in grouped_mo)
|
||||
|
||||
# Pending outgoing quantity.
|
||||
move_domain = domain + [
|
||||
in_domain = domain + [('location_dest_id', 'in', wh_location_ids)] # Pending incoming quantity.
|
||||
out_domain = domain + [ # Pending outgoing quantity.
|
||||
('raw_material_production_id', '!=', False),
|
||||
('location_id', 'in', wh_location_ids),
|
||||
]
|
||||
grouped_moves = self.env['stock.move'].read_group(move_domain, ['product_qty:sum'], 'product_id')
|
||||
res['draft_production_qty']['out'] = sum(move['product_qty'] for move in grouped_moves)
|
||||
res['qty']['in'] += res['draft_production_qty']['in']
|
||||
res['qty']['out'] += res['draft_production_qty']['out']
|
||||
in_product_qty = {k.id: v for k, v in self.env['mrp.production']._read_group(in_domain, aggregates=['product_qty:sum'], groupby=['product_id'])}
|
||||
out_product_qty = {k.id: v for k, v in self.env['stock.move']._read_group(out_domain, aggregates=['product_qty:sum'], groupby=['product_id'])}
|
||||
|
||||
self._add_product_quantities(res, product_template_ids, product_ids, 'draft_production_qty', in_product_qty, out_product_qty)
|
||||
|
||||
return res
|
||||
|
||||
def _get_reservation_data(self, move):
|
||||
if move.production_id:
|
||||
m2o = 'production_id'
|
||||
elif move.raw_material_production_id:
|
||||
m2o = 'raw_material_production_id'
|
||||
else:
|
||||
return super()._get_reservation_data(move)
|
||||
return {
|
||||
'_name': move[m2o]._name,
|
||||
'name': move[m2o].name,
|
||||
'id': move[m2o].id
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue