mirror of
https://github.com/bringout/oca-ocb-mrp.git
synced 2026-04-23 15:32:00 +02:00
Initial commit: Mrp packages
This commit is contained in:
commit
50d736b3bd
739 changed files with 538193 additions and 0 deletions
7
odoo-bringout-oca-ocb-mrp/mrp/report/__init__.py
Normal file
7
odoo-bringout-oca-ocb-mrp/mrp/report/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import mrp_report_bom_structure
|
||||
from . import stock_forecasted
|
||||
from . import report_stock_reception
|
||||
from . import report_stock_rule
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,196 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="report_mrporder">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="o">
|
||||
<t t-call="web.internal_layout">
|
||||
<div class="page">
|
||||
<div class="oe_structure"/>
|
||||
<div class="row">
|
||||
<div class="col-7">
|
||||
<h2><span t-field="o.name"/></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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt32 mb32">
|
||||
<div class="col-3" t-if="o.origin">
|
||||
<strong>Source:</strong><br/>
|
||||
<span t-field="o.origin"/>
|
||||
</div>
|
||||
<div class="col-3" t-if="o.user_id">
|
||||
<strong>Responsible:</strong><br/>
|
||||
<span t-field="o.user_id"/>
|
||||
</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"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt32 mb32">
|
||||
<div class="col-3">
|
||||
<strong>Product:</strong><br/>
|
||||
<span t-field="o.product_id"/>
|
||||
</div>
|
||||
<div class="col-3" t-if="o.product_description_variants">
|
||||
<strong>Description:</strong><br/>
|
||||
<span t-field="o.product_description_variants"/>
|
||||
</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"/>
|
||||
</div>
|
||||
<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"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-if="o.workorder_ids" groups="mrp.group_mrp_routings">
|
||||
<h3>
|
||||
<span t-if="o.state == 'done'">Operations Done</span>
|
||||
<span t-elif="o.is_planned">Operations Planned</span>
|
||||
<span t-else="">Operations</span>
|
||||
</h3>
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<th><strong>Operation</strong></th>
|
||||
<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>
|
||||
</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-if="o.state != 'done'" t-field="line2.duration_expected"/>
|
||||
<span t-if="o.state == 'done'" t-field="line2.duration"/>
|
||||
</td>
|
||||
<td t-if="o.state in ('progress', 'to_close')">
|
||||
<span t-field="line2.duration"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<t t-call="mrp.report_mrp_production_components" t-if="o.move_raw_ids"/>
|
||||
|
||||
<div class="oe_structure"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="report_mrp_production_components">
|
||||
<h3>
|
||||
<span>
|
||||
Components
|
||||
</span>
|
||||
</h3>
|
||||
<table class="table table-sm">
|
||||
<t t-set="has_product_barcode" t-value="any(m.product_id.barcode for m in o.move_raw_ids)"/>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th t-if="o.state in ('progress', 'to_close','done')" t-attf-class="{{ 'text-end' if not has_product_barcode else '' }}">Consumed</th>
|
||||
<th t-attf-class="{{ 'text-end' if not has_product_barcode else '' }}">To Consume</th>
|
||||
<th t-if="has_product_barcode" width="15%" class="text-center">Barcode</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="o.move_raw_ids" t-as="raw_line">
|
||||
<td>
|
||||
<span t-field="raw_line.product_id"/>
|
||||
</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"/>
|
||||
</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"/>
|
||||
</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'}"/>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<template id="label_production_view_pdf">
|
||||
<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="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">
|
||||
<!-- 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 t-else="">
|
||||
<t t-set="move_line_qtys" t-value="move_line_qtys + [1]"/>
|
||||
</t>
|
||||
</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 t-foreach="range(num_pages)" t-as="page" class="o_label_sheet" t-att-style="'padding: 20mm 8mm'">
|
||||
<table class="my-0 table table-sm table-borderless">
|
||||
<t t-foreach="range(nRows)" t-as="row">
|
||||
<tr>
|
||||
<t t-foreach="range(nCols)" t-as="column">
|
||||
<t t-if="index_to_qtys and not current_quantity">
|
||||
<t t-set="index_to_qty" t-value="index_to_qtys.popitem()"/>
|
||||
<t t-set="move_line" t-value="move_lines[index_to_qty[0]]"/>
|
||||
<t t-set="current_quantity" t-value="index_to_qty[1]"/>
|
||||
</t>
|
||||
<t t-if="current_quantity">
|
||||
<t t-set="make_invisible" t-value="False"/>
|
||||
<t t-set="current_quantity" t-value="current_quantity - 1"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-set="make_invisible" t-value="True"/>
|
||||
</t>
|
||||
<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"/>
|
||||
<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"/>
|
||||
</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"/>
|
||||
</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"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="text-muted">No barcode available</span>
|
||||
</t>
|
||||
</div>
|
||||
</td>
|
||||
</t>
|
||||
</tr>
|
||||
</t>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
714
odoo-bringout-oca-ocb-mrp/mrp/report/mrp_report_bom_structure.py
Normal file
714
odoo-bringout-oca-ocb-mrp/mrp/report/mrp_report_bom_structure.py
Normal file
|
|
@ -0,0 +1,714 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from collections import defaultdict, OrderedDict
|
||||
from datetime import date, timedelta
|
||||
import json
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.tools import float_compare, float_round, format_date, float_is_zero
|
||||
|
||||
class ReportBomStructure(models.AbstractModel):
|
||||
_name = 'report.mrp.report_bom_structure'
|
||||
_description = 'BOM Overview Report'
|
||||
|
||||
@api.model
|
||||
def get_html(self, bom_id=False, searchQty=1, searchVariant=False):
|
||||
res = self._get_report_data(bom_id=bom_id, searchQty=searchQty, searchVariant=searchVariant)
|
||||
res['has_attachments'] = self._has_attachments(res['lines'])
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def get_warehouses(self):
|
||||
return self.env['stock.warehouse'].search_read([('company_id', 'in', self.env.companies.ids)], fields=['id', 'name'])
|
||||
|
||||
@api.model
|
||||
def _compute_current_production_capacity(self, bom_data):
|
||||
# Get the maximum amount producible product of the selected bom given each component's stock levels.
|
||||
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):
|
||||
continue
|
||||
components_qty_to_produce[comp['product_id']] += comp['base_bom_line_qty']
|
||||
components_qty_available[comp['product_id']] = comp['quantity_available']
|
||||
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)
|
||||
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')))
|
||||
for product_variant_id in candidates.ids:
|
||||
docs.append(self._get_pdf_doc(bom_id, data, quantity, product_variant_id))
|
||||
if not candidates:
|
||||
docs.append(self._get_pdf_doc(bom_id, data, quantity))
|
||||
return {
|
||||
'doc_ids': docids,
|
||||
'doc_model': 'mrp.bom',
|
||||
'docs': docs,
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _get_pdf_doc(self, bom_id, data, quantity, product_variant_id=None):
|
||||
if data and data.get('unfolded_ids'):
|
||||
doc = self._get_pdf_line(bom_id, product_id=product_variant_id, qty=quantity, unfolded_ids=set(json.loads(data.get('unfolded_ids'))))
|
||||
else:
|
||||
doc = self._get_pdf_line(bom_id, product_id=product_variant_id, qty=quantity, unfolded=True)
|
||||
doc['show_availabilities'] = False if data and data.get('availabilities') == 'false' else True
|
||||
doc['show_costs'] = False if data and data.get('costs') == 'false' else True
|
||||
doc['show_operations'] = False if data and data.get('operations') == 'false' else True
|
||||
doc['show_lead_times'] = False if data and data.get('lead_times') == 'false' else True
|
||||
return doc
|
||||
|
||||
@api.model
|
||||
def _get_report_data(self, bom_id, searchQty=0, searchVariant=False):
|
||||
lines = {}
|
||||
bom = self.env['mrp.bom'].browse(bom_id)
|
||||
bom_quantity = searchQty or bom.product_qty or 1
|
||||
bom_product_variants = {}
|
||||
bom_uom_name = ''
|
||||
|
||||
if searchVariant:
|
||||
product = self.env['product.product'].browse(int(searchVariant))
|
||||
else:
|
||||
product = bom.product_id or bom.product_tmpl_id.product_variant_id or bom.product_tmpl_id.with_context(active_test=False).product_variant_ids[:1]
|
||||
|
||||
if bom:
|
||||
bom_uom_name = bom.product_uom_id.name
|
||||
|
||||
# Get variants used for search
|
||||
if not bom.product_id:
|
||||
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'))
|
||||
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'),
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _get_components_closest_forecasted(self, lines, line_quantities, parent_bom, product_info, 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.
|
||||
|
||||
E.g. {'product_1_id': {'line_1_id': date_1, line_2_id: date_2}, 'product_2': {line_3_id: date_3}, ...}.
|
||||
|
||||
Note that
|
||||
- if a product is unavailable + not forecasted for a specific bom line => its date will be `date.max`
|
||||
- if a product's type is not `product` or is already in stock for a specific bom line => its date will be `date.min`.
|
||||
"""
|
||||
if ignore_stock:
|
||||
return {}
|
||||
# Use defaultdict(OrderedDict) in case there are lines with the same component.
|
||||
closest_forecasted = defaultdict(OrderedDict)
|
||||
remaining_products = []
|
||||
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)
|
||||
stock_loc = quantities_info['stock_loc']
|
||||
product_info[product.id]['consumptions'][stock_loc] += line_quantities.get(line.id, 0.0)
|
||||
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):
|
||||
# Use date.min as a sentinel value for _get_stock_availability
|
||||
closest_forecasted[product.id][line.id] = date.min
|
||||
elif stock_loc != 'in_stock':
|
||||
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())
|
||||
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 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
|
||||
)
|
||||
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']])
|
||||
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)
|
||||
# Find the first available quantity for the given product and update closest_forecasted
|
||||
for min_date, product_qty in available_quantities[product_id]:
|
||||
if product_qty >= product_quantities_info[product_id][line_id]:
|
||||
closest_forecasted[product_id][line_id] = min_date
|
||||
break
|
||||
if not closest_forecasted[product_id][line_id]:
|
||||
closest_forecasted[product_id][line_id] = date.max
|
||||
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):
|
||||
""" Gets recursively the BoM and all its subassemblies and computes availibility estimations for each component and their disponibility in stock.
|
||||
Accepts specific keys in context that will affect the data computed :
|
||||
- 'minimized': Will cut all data not required to compute availability estimations.
|
||||
- 'from_date': Gives a single value for 'today' across the functions, as well as using this date in products quantity computes.
|
||||
"""
|
||||
is_minimized = self.env.context.get('minimized', False)
|
||||
if not product:
|
||||
product = bom.product_id or bom.product_tmpl_id.product_variant_id
|
||||
if line_qty is False:
|
||||
line_qty = bom.product_qty
|
||||
|
||||
if not product_info:
|
||||
product_info = {}
|
||||
|
||||
company = bom.company_id or self.env.company
|
||||
current_quantity = line_qty
|
||||
if bom_line:
|
||||
current_quantity = bom_line.product_uom_id._compute_quantity(line_qty, bom.product_uom_id) or 0
|
||||
|
||||
prod_cost = 0
|
||||
attachment_ids = []
|
||||
if not is_minimized:
|
||||
if product:
|
||||
prod_cost = product.uom_id._compute_price(product.with_company(company).standard_price, bom.product_uom_id) * current_quantity
|
||||
attachment_ids = self.env['mrp.document'].search(['|', '&', ('res_model', '=', 'product.product'),
|
||||
('res_id', '=', product.id), '&', ('res_model', '=', 'product.template'),
|
||||
('res_id', '=', product.product_tmpl_id.id)]).ids
|
||||
else:
|
||||
# Use the product template instead of the variant
|
||||
prod_cost = bom.product_tmpl_id.uom_id._compute_price(bom.product_tmpl_id.with_company(company).standard_price, bom.product_uom_id) * current_quantity
|
||||
attachment_ids = self.env['mrp.document'].search([('res_model', '=', 'product.template'), ('res_id', '=', bom.product_tmpl_id.id)]).ids
|
||||
|
||||
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)
|
||||
route_info = product_info[key].get(bom_key, {})
|
||||
quantities_info = {}
|
||||
if not ignore_stock:
|
||||
# Useless to compute quantities_info if it's not going to be used later on
|
||||
quantities_info = self._get_quantities_info(product, bom.product_uom_id, parent_bom, product_info)
|
||||
|
||||
bom_report_line = {
|
||||
'index': index,
|
||||
'bom': bom,
|
||||
'bom_id': bom and bom.id or False,
|
||||
'bom_code': bom and bom.code or False,
|
||||
'type': 'bom',
|
||||
'quantity': current_quantity,
|
||||
'quantity_available': quantities_info.get('free_qty') or 0,
|
||||
'quantity_on_hand': quantities_info.get('on_hand_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,
|
||||
'uom_name': bom.product_uom_id.name if bom else product.uom_id.name,
|
||||
'route_type': route_info.get('route_type', ''),
|
||||
'route_name': route_info.get('route_name', ''),
|
||||
'route_detail': route_info.get('route_detail', ''),
|
||||
'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,
|
||||
'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,
|
||||
'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 = {}
|
||||
for line in bom.bom_line_ids:
|
||||
if product and line._skip_bom_line(product):
|
||||
continue
|
||||
line_quantity = (current_quantity / (bom.product_qty or 1.0)) * line.product_qty
|
||||
line_quantities[line.id] = line_quantity
|
||||
if not line.child_bom_id:
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
bom_report_line['bom_cost'] += component['bom_cost']
|
||||
bom_report_line['components'] = components
|
||||
bom_report_line['producible_qty'] = self._compute_current_production_capacity(bom_report_line)
|
||||
|
||||
if not is_minimized:
|
||||
byproducts, byproduct_cost_portion = self._get_byproducts_lines(product, bom, current_quantity, level + 1, bom_report_line['bom_cost'], index)
|
||||
bom_report_line['byproducts'] = byproducts
|
||||
bom_report_line['cost_share'] = float_round(1 - byproduct_cost_portion, precision_rounding=0.0001)
|
||||
bom_report_line['byproducts_cost'] = sum(byproduct['bom_cost'] for byproduct in byproducts)
|
||||
bom_report_line['byproducts_total'] = sum(byproduct['quantity'] for byproduct in byproducts)
|
||||
bom_report_line['bom_cost'] *= bom_report_line['cost_share']
|
||||
|
||||
availabilities = self._get_availabilities(product, current_quantity, product_info, bom_key, quantities_info, level, ignore_stock, components)
|
||||
bom_report_line.update(availabilities)
|
||||
|
||||
if level == 0:
|
||||
# Gives a unique key for the first line that indicates if product is ready for production right now.
|
||||
bom_report_line['components_available'] = all([c['stock_avail_state'] == 'available' for c in components])
|
||||
return bom_report_line
|
||||
|
||||
@api.model
|
||||
def _get_component_data(self, parent_bom, warehouse, bom_line, line_quantity, level, index, product_info, ignore_stock=False):
|
||||
company = parent_bom.company_id or self.env.company
|
||||
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)
|
||||
|
||||
key = bom_line.product_id.id
|
||||
bom_key = parent_bom.id
|
||||
route_info = product_info[key].get(bom_key, {})
|
||||
|
||||
quantities_info = {}
|
||||
if not ignore_stock:
|
||||
# Useless to compute quantities_info if it's not going to be used later on
|
||||
quantities_info = self._get_quantities_info(bom_line.product_id, bom_line.product_uom_id, parent_bom, product_info)
|
||||
availabilities = self._get_availabilities(bom_line.product_id, line_quantity, product_info, bom_key, quantities_info, level, ignore_stock, bom_line=bom_line)
|
||||
|
||||
attachment_ids = []
|
||||
if not self.env.context.get('minimized', False):
|
||||
attachment_ids = self.env['mrp.document'].search(['|', '&', ('res_model', '=', 'product.product'), ('res_id', '=', bom_line.product_id.id),
|
||||
'&', ('res_model', '=', 'product.template'), ('res_id', '=', bom_line.product_id.product_tmpl_id.id)]).ids
|
||||
|
||||
return {
|
||||
'type': 'component',
|
||||
'index': index,
|
||||
'bom_id': False,
|
||||
'product': bom_line.product_id,
|
||||
'product_id': bom_line.product_id.id,
|
||||
'link_id': bom_line.product_id.id if bom_line.product_id.product_variant_count > 1 else bom_line.product_id.product_tmpl_id.id,
|
||||
'link_model': 'product.product' if bom_line.product_id.product_variant_count > 1 else 'product.template',
|
||||
'name': bom_line.product_id.display_name,
|
||||
'code': '',
|
||||
'currency': company.currency_id,
|
||||
'currency_id': company.currency_id.id,
|
||||
'quantity': line_quantity,
|
||||
'quantity_available': quantities_info.get('free_qty', 0),
|
||||
'quantity_on_hand': quantities_info.get('on_hand_qty', 0),
|
||||
'base_bom_line_qty': bom_line.product_qty,
|
||||
'uom': bom_line.product_uom_id,
|
||||
'uom_name': bom_line.product_uom_id.name,
|
||||
'prod_cost': rounded_price,
|
||||
'bom_cost': rounded_price,
|
||||
'route_type': route_info.get('route_type', ''),
|
||||
'route_name': route_info.get('route_name', ''),
|
||||
'route_detail': route_info.get('route_detail', ''),
|
||||
'route_alert': route_info.get('route_alert', False),
|
||||
'lead_time': route_info.get('lead_time', False),
|
||||
'stock_avail_state': availabilities['stock_avail_state'],
|
||||
'resupply_avail_delay': availabilities['resupply_avail_delay'],
|
||||
'availability_display': availabilities['availability_display'],
|
||||
'availability_state': availabilities['availability_state'],
|
||||
'availability_delay': availabilities['availability_delay'],
|
||||
'parent_id': parent_bom.id,
|
||||
'level': level or 0,
|
||||
'attachment_ids': attachment_ids,
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _get_quantities_info(self, product, bom_uom, parent_bom, product_info):
|
||||
return {
|
||||
'free_qty': product.uom_id._compute_quantity(product.free_qty, bom_uom) if product.detailed_type == 'product' else False,
|
||||
'on_hand_qty': product.uom_id._compute_quantity(product.qty_available, bom_uom) if product.detailed_type == 'product' else False,
|
||||
'stock_loc': 'in_stock',
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _update_product_info(self, product, bom_key, product_info, warehouse, quantity, bom, parent_bom):
|
||||
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)
|
||||
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)
|
||||
|
||||
@api.model
|
||||
def _get_byproducts_lines(self, product, bom, bom_quantity, level, total, index):
|
||||
byproducts = []
|
||||
byproduct_cost_portion = 0
|
||||
company = bom.company_id or self.env.company
|
||||
byproduct_index = 0
|
||||
for byproduct in bom.byproduct_ids:
|
||||
if byproduct._skip_byproduct_line(product):
|
||||
continue
|
||||
line_quantity = (bom_quantity / (bom.product_qty or 1.0)) * byproduct.product_qty
|
||||
cost_share = byproduct.cost_share / 100 if byproduct.product_qty > 0 else 0
|
||||
byproduct_cost_portion += cost_share
|
||||
price = byproduct.product_id.uom_id._compute_price(byproduct.product_id.with_company(company).standard_price, byproduct.product_uom_id) * line_quantity
|
||||
byproducts.append({
|
||||
'id': byproduct.id,
|
||||
'index': f"{index}{byproduct_index}",
|
||||
'type': 'byproduct',
|
||||
'link_id': byproduct.product_id.id if byproduct.product_id.product_variant_count > 1 else byproduct.product_id.product_tmpl_id.id,
|
||||
'link_model': 'product.product' if byproduct.product_id.product_variant_count > 1 else 'product.template',
|
||||
'currency_id': company.currency_id.id,
|
||||
'name': byproduct.product_id.display_name,
|
||||
'quantity': line_quantity,
|
||||
'uom_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),
|
||||
'cost_share': cost_share,
|
||||
})
|
||||
byproduct_index += 1
|
||||
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):
|
||||
operations = []
|
||||
total = 0.0
|
||||
qty = bom.product_uom_id._compute_quantity(qty, bom.product_tmpl_id.uom_id)
|
||||
company = bom.company_id or self.env.company
|
||||
operation_index = 0
|
||||
for operation in bom.operation_ids:
|
||||
if 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)
|
||||
operations.append({
|
||||
'type': 'operation',
|
||||
'index': f"{index}{operation_index}",
|
||||
'level': level or 0,
|
||||
'operation': operation,
|
||||
'link_id': operation.id,
|
||||
'link_model': 'mrp.routing.workcenter',
|
||||
'name': operation.name + ' - ' + operation.workcenter_id.name,
|
||||
'uom_name': _("Minutes"),
|
||||
'quantity': duration_expected,
|
||||
'bom_cost': self.env.company.currency_id.round(total),
|
||||
'currency_id': company.currency_id.id,
|
||||
'model': 'mrp.routing.workcenter',
|
||||
})
|
||||
operation_index += 1
|
||||
return operations
|
||||
|
||||
@api.model
|
||||
def _get_pdf_line(self, bom_id, product_id=False, qty=1, unfolded_ids=None, unfolded=False):
|
||||
if unfolded_ids is None:
|
||||
unfolded_ids = set()
|
||||
|
||||
bom = self.env['mrp.bom'].browse(bom_id)
|
||||
if product_id:
|
||||
product = self.env['product.product'].browse(int(product_id))
|
||||
else:
|
||||
product = bom.product_id or bom.product_tmpl_id.product_variant_id 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'))
|
||||
else:
|
||||
warehouse = self.env['stock.warehouse'].browse(self.get_warehouses()[0]['id'])
|
||||
|
||||
level = 1
|
||||
data = self._get_bom_data(bom, warehouse, product=product, line_qty=qty, level=0)
|
||||
pdf_lines = self._get_bom_array_lines(data, level, unfolded_ids, unfolded, True)
|
||||
|
||||
data['lines'] = pdf_lines
|
||||
return data
|
||||
|
||||
@api.model
|
||||
def _get_bom_array_lines(self, data, level, unfolded_ids, unfolded, parent_unfolded=True):
|
||||
bom_lines = data['components']
|
||||
lines = []
|
||||
for bom_line in bom_lines:
|
||||
line_unfolded = ('bom_' + str(bom_line['index'])) in unfolded_ids
|
||||
line_visible = level == 1 or unfolded or parent_unfolded
|
||||
lines.append({
|
||||
'bom_id': bom_line['bom_id'],
|
||||
'name': bom_line['name'],
|
||||
'type': bom_line['type'],
|
||||
'quantity': bom_line['quantity'],
|
||||
'quantity_available': bom_line['quantity_available'],
|
||||
'quantity_on_hand': bom_line['quantity_on_hand'],
|
||||
'producible_qty': bom_line.get('producible_qty', False),
|
||||
'uom': bom_line['uom_name'],
|
||||
'prod_cost': bom_line['prod_cost'],
|
||||
'bom_cost': bom_line['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'],
|
||||
'level': bom_line['level'],
|
||||
'code': bom_line['code'],
|
||||
'availability_state': bom_line['availability_state'],
|
||||
'availability_display': bom_line['availability_display'],
|
||||
'visible': line_visible,
|
||||
})
|
||||
if bom_line.get('components'):
|
||||
lines += self._get_bom_array_lines(bom_line, level + 1, unfolded_ids, unfolded, line_visible and line_unfolded)
|
||||
|
||||
if data['operations']:
|
||||
lines.append({
|
||||
'name': _('Operations'),
|
||||
'type': 'operation',
|
||||
'quantity': data['operations_time'],
|
||||
'uom': _('minutes'),
|
||||
'bom_cost': data['operations_cost'],
|
||||
'level': level,
|
||||
'visible': parent_unfolded,
|
||||
})
|
||||
operations_unfolded = unfolded or (parent_unfolded and ('operations_' + str(data['index'])) in unfolded_ids)
|
||||
for operation in data['operations']:
|
||||
lines.append({
|
||||
'name': operation['name'],
|
||||
'type': 'operation',
|
||||
'quantity': operation['quantity'],
|
||||
'uom': _('minutes'),
|
||||
'bom_cost': operation['bom_cost'],
|
||||
'level': level + 1,
|
||||
'visible': operations_unfolded,
|
||||
})
|
||||
if data['byproducts']:
|
||||
lines.append({
|
||||
'name': _('Byproducts'),
|
||||
'type': 'byproduct',
|
||||
'uom': False,
|
||||
'quantity': data['byproducts_total'],
|
||||
'bom_cost': data['byproducts_cost'],
|
||||
'level': level,
|
||||
'visible': parent_unfolded,
|
||||
})
|
||||
byproducts_unfolded = unfolded or (parent_unfolded and ('byproducts_' + str(data['index'])) in unfolded_ids)
|
||||
for byproduct in data['byproducts']:
|
||||
lines.append({
|
||||
'name': byproduct['name'],
|
||||
'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,
|
||||
})
|
||||
return lines
|
||||
|
||||
@api.model
|
||||
def _get_resupply_route_info(self, warehouse, product, quantity, bom=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 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)
|
||||
|
||||
@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):
|
||||
return False
|
||||
|
||||
@api.model
|
||||
def _find_special_rules(self, product, product_info, parent_bom=False, parent_product_id=False):
|
||||
return False
|
||||
|
||||
@api.model
|
||||
def _format_route_info(self, rules, rules_delay, warehouse, product, bom, quantity):
|
||||
manufacture_rules = [rule for rule in rules if rule.action == 'manufacture' and bom]
|
||||
if manufacture_rules:
|
||||
# Need to get rules from Production location to get delays before production
|
||||
wh_manufacture_rules = product._get_rules_from_location(product.property_stock_production, route_ids=warehouse.route_ids)
|
||||
wh_manufacture_rules -= rules
|
||||
rules_delay += sum(rule.delay for rule in wh_manufacture_rules)
|
||||
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,
|
||||
}
|
||||
return {}
|
||||
|
||||
@api.model
|
||||
def _get_availabilities(self, product, quantity, product_info, bom_key, quantities_info, level, ignore_stock=False, components=False, bom_line=None):
|
||||
# Get availabilities according to stock (today & forecasted).
|
||||
stock_state, stock_delay = ('unavailable', False)
|
||||
if not ignore_stock:
|
||||
stock_state, stock_delay = self._get_stock_availability(product, quantity, product_info, quantities_info, bom_line=bom_line)
|
||||
|
||||
# Get availabilities from applied resupply rules
|
||||
components = components or []
|
||||
route_info = product_info[product.id].get(bom_key)
|
||||
resupply_state, resupply_delay = ('unavailable', False)
|
||||
if product and product.detailed_type != 'product':
|
||||
resupply_state, resupply_delay = ('available', 0)
|
||||
elif route_info:
|
||||
resupply_state, resupply_delay = self._get_resupply_availability(route_info, components)
|
||||
|
||||
base = {
|
||||
'resupply_avail_delay': resupply_delay,
|
||||
'stock_avail_state': stock_state,
|
||||
}
|
||||
if level != 0 and stock_state != 'unavailable':
|
||||
return {**base, **{
|
||||
'availability_display': self._format_date_display(stock_state, stock_delay),
|
||||
'availability_state': stock_state,
|
||||
'availability_delay': stock_delay,
|
||||
}}
|
||||
return {**base, **{
|
||||
'availability_display': self._format_date_display(resupply_state, resupply_delay),
|
||||
'availability_state': resupply_state,
|
||||
'availability_delay': resupply_delay,
|
||||
}}
|
||||
|
||||
@api.model
|
||||
def _get_stock_availability(self, product, quantity, product_info, quantities_info, bom_line=None):
|
||||
closest_forecasted = None
|
||||
if bom_line:
|
||||
closest_forecasted = self.env.context.get('components_closest_forecasted', {}).get(product.id, {}).get(bom_line.id)
|
||||
if closest_forecasted == date.min:
|
||||
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':
|
||||
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:
|
||||
return ('available', 0)
|
||||
|
||||
# No need to check forecast if the product isn't located in our stock
|
||||
if stock_loc == 'in_stock':
|
||||
domain = [('state', '=', 'forecast'), ('date', '>=', date_today), ('product_id', '=', product.id), ('product_qty', '>=', product_info[product.id]['consumptions'][stock_loc])]
|
||||
if self.env.context.get('warehouse'):
|
||||
domain.append(('warehouse_id', '=', self.env.context.get('warehouse')))
|
||||
|
||||
# Seek the closest date in the forecast report where consummed quantity >= forecasted quantity
|
||||
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']
|
||||
if closest_forecasted:
|
||||
days_to_forecast = (closest_forecasted - date_today).days
|
||||
return ('expected', days_to_forecast)
|
||||
return ('unavailable', False)
|
||||
|
||||
@api.model
|
||||
def _get_resupply_availability(self, route_info, components):
|
||||
if route_info.get('route_type') == 'manufacture':
|
||||
max_component_delay = self._get_max_component_delay(components)
|
||||
if max_component_delay is False:
|
||||
return ('unavailable', False)
|
||||
produce_delay = route_info.get('manufacture_delay', 0) + max_component_delay
|
||||
return ('estimated', produce_delay)
|
||||
return ('unavailable', False)
|
||||
|
||||
@api.model
|
||||
def _get_max_component_delay(self, components):
|
||||
max_component_delay = 0
|
||||
for component in components:
|
||||
line_delay = component.get('availability_delay', False)
|
||||
if line_delay is False:
|
||||
# This component isn't available right now and cannot be resupplied, so the manufactured product can't be resupplied either.
|
||||
return False
|
||||
max_component_delay = max(max_component_delay, line_delay)
|
||||
return max_component_delay
|
||||
|
||||
@api.model
|
||||
def _format_date_display(self, state, delay):
|
||||
date_today = self.env.context.get('from_date', fields.date.today())
|
||||
if state == 'available':
|
||||
return _('Available')
|
||||
if state == 'unavailable':
|
||||
return _('Not Available')
|
||||
if state == 'expected':
|
||||
return _('Expected %s', format_date(self.env, date_today + timedelta(days=delay)))
|
||||
if state == 'estimated':
|
||||
return _('Estimated %s', format_date(self.env, date_today + timedelta(days=delay)))
|
||||
return ''
|
||||
|
||||
@api.model
|
||||
def _has_attachments(self, data):
|
||||
return data['attachment_ids'] or any(self._has_attachments(component) for component in data.get('components', []))
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<template id="report_mrp_bom">
|
||||
<div class="o_mrp_bom_report_page container py-3 py-lg-5 px-0 bg-view">
|
||||
<div t-if="data.get('lines')">
|
||||
<div class="px-3 mb-5">
|
||||
<h1>BoM Overview</h1>
|
||||
<h3 t-esc="data['name']"/>
|
||||
<hr t-if="data['bom_code']"/>
|
||||
<h6 t-if="data['bom_code']">Reference: <t t-esc="data['bom_code']"/></h6>
|
||||
</div>
|
||||
<t t-set="currency" t-value="data['currency']"/>
|
||||
<table class="o_mrp_bom_expandable table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th 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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td name="td_mrp_bom" t-esc="data['name']"/>
|
||||
<td class="text-end" t-esc="data['quantity']" t-options='{"widget": "float", "decimal_precision": "Product Unit of Measure"}'/>
|
||||
<td class="text-start" groups="uom.group_uom" t-esc="data['uom_name']"/>
|
||||
<td t-if="data['show_availabilities']" class="text-end" t-esc="data['producible_qty']" t-options="{'widget': 'float', 'precision': 0}"/>
|
||||
<td t-if="data['show_availabilities']" class="text-end">
|
||||
<t t-esc="data['quantity_available']" t-options='{"widget": "float", "decimal_precision": "Product Unit of Measure"}'/> /
|
||||
<t t-esc="data['quantity_on_hand']" t-options='{"widget": "float", "decimal_precision": "Product Unit of Measure"}'/>
|
||||
</td>
|
||||
<td t-if="data['show_availabilities']" class="text-center">
|
||||
<t t-if="data.get('components_available', None) != None">
|
||||
<span t-attf-class="{{'text-success' if data['components_available'] and data['availability_state'] != 'unavailable' else 'text-danger' }}" t-esc="data['availability_display']"/>
|
||||
</t>
|
||||
</td>
|
||||
<td t-if="data['show_lead_times']" class="text-end">
|
||||
<span t-if="data['lead_time'] is not False">
|
||||
<t t-esc="data['lead_time']" t-options="{'widget': 'float', 'precision': 0}"/>
|
||||
Days
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<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}'/>
|
||||
</tr>
|
||||
<t t-call="mrp.report_mrp_bom_pdf_line"/>
|
||||
<tr t-if="data['show_costs']">
|
||||
<td name="td_mrp_bom_f" class="text-end">
|
||||
<span t-if="data['byproducts']" t-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}'/>
|
||||
</tr>
|
||||
<t t-if="data['show_costs'] and 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}'/>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div t-else="" class="d-flex align-items-center justify-content-center h-50">
|
||||
<h4 class="text-muted">No data available.</h4>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="report_mrp_bom_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'])">
|
||||
<td name="td_mrp_code">
|
||||
<span t-attf-style="margin-left: {{ str(l['level'] * 20) }}px"/>
|
||||
<span t-esc="l['name']"/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<t t-if="l['type'] == 'operation'" t-esc="l['quantity']" t-options='{"widget": "float_time"}'/>
|
||||
<t t-else="" t-esc="l['quantity']" t-options='{"widget": "float", "decimal_precision": "Product Unit of Measure"}'/>
|
||||
</td>
|
||||
<td class="text-start" groups="uom.group_uom">
|
||||
<t t-esc="l['uom']"/>
|
||||
</td>
|
||||
<td t-if="data['show_availabilities']" class="text-end">
|
||||
<t t-if="l.get('producible_qty', False) is not False" t-esc="l['producible_qty']" t-options="{'widget': 'float', 'precision': 0}"/>
|
||||
</td>
|
||||
<td t-if="data['show_availabilities']" class="text-end">
|
||||
<t t-if="l.get('quantity_available', False) is not False">
|
||||
<t t-esc="l['quantity_available']" t-options='{"widget": "float", "decimal_precision": "Product Unit of Measure"}'/> /
|
||||
<t t-esc="l['quantity_on_hand']" t-options='{"widget": "float", "decimal_precision": "Product Unit of Measure"}'/>
|
||||
</t>
|
||||
</td>
|
||||
<td t-if="data['show_availabilities']" class="text-center">
|
||||
<t t-if="l.get('availability_state', None) != None">
|
||||
<span t-attf-class="{{'text-success' if l['availability_state'] == 'available' else ''}}{{'text-warning' if l['availability_state'] == 'expected' else ''}}{{'text-danger' if l['availability_state'] == 'unavailable' else ''}}" t-esc="l['availability_display']"/>
|
||||
</t>
|
||||
</td>
|
||||
<td t-if="data['show_lead_times']" class="text-end">
|
||||
<span t-if="l.get('lead_time', False) is not False">
|
||||
<t t-esc="l['lead_time']" t-options="{'widget': 'float', 'precision': 0}"/>
|
||||
Days
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span t-if="l.get('route_name')" t-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>
|
||||
</tr>
|
||||
</t>
|
||||
</template>
|
||||
<template id="report_bom_structure">
|
||||
<t t-set="data_report_landscape" t-value="True"/>
|
||||
<t t-call="web.basic_layout">
|
||||
<t t-foreach="docs" t-as="data">
|
||||
<div class="page">
|
||||
<t t-call="mrp.report_mrp_bom"/>
|
||||
</div>
|
||||
<p style="page-break-before:always;"> </p>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="action_report_production_order" model="ir.actions.report">
|
||||
<field name="name">Production Order</field>
|
||||
<field name="model">mrp.production</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">mrp.report_mrporder</field>
|
||||
<field name="report_file">mrp.report.mrp_production_templates</field>
|
||||
<field name="print_report_name">'Production Order - %s' % object.name</field>
|
||||
<field name="binding_model_id" ref="model_mrp_production"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
<record id="action_report_bom_structure" model="ir.actions.report">
|
||||
<field name="name">BoM Overview</field>
|
||||
<field name="model">mrp.bom</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">mrp.report_bom_structure</field>
|
||||
<field name="report_file">mrp.report_bom_structure</field>
|
||||
<field name="print_report_name">'Bom Overview - %s' % object.display_name</field>
|
||||
<field name="binding_model_id" ref="model_mrp_bom"/>
|
||||
<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>
|
||||
<field name="report_type">qweb-text</field>
|
||||
<field name="report_name">mrp.label_production_view</field>
|
||||
<field name="report_file">mrp.label_production_view</field>
|
||||
<field name="binding_model_id" ref="model_mrp_production"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
<record id="action_report_finished_product" model="ir.actions.report">
|
||||
<field name="name">Finished Product Label (PDF)</field>
|
||||
<field name="model">mrp.production</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">mrp.label_production_view_pdf</field>
|
||||
<field name="report_file">mrp.label_production_view_pdf</field>
|
||||
<field name="paperformat_id" ref="product.paperformat_label_sheet"/>
|
||||
<field name="print_report_name">'Finished products - %s' % object.name</field>
|
||||
<field name="binding_model_id" ref="model_mrp_production"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
<record id="action_report_workorder" model="ir.actions.report">
|
||||
<field name="name">Work Order</field>
|
||||
<field name="model">mrp.workorder</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">mrp.report_mrp_workorder</field>
|
||||
<field name="report_file">mrp.report.mrp_workorder</field>
|
||||
<field name="print_report_name">'Work Order - %s' % object.name</field>
|
||||
<field name="binding_model_id" ref="model_mrp_workorder"/>
|
||||
<field name="binding_type">report</field>
|
||||
<field name="multi">True</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="report_mrp_workorder">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="o">
|
||||
<t t-call="web.internal_layout">
|
||||
<div class="page">
|
||||
<div class="oe_structure"/>
|
||||
<div class="row">
|
||||
<div class="col-7">
|
||||
<h2><span t-field="o.name"/></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>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row mt32 mb32">
|
||||
<div class="col-3">
|
||||
<strong>Responsible:</strong><br/>
|
||||
<span t-field="o.production_id.user_id"/>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<strong>Manufacturing Order:</strong><br/>
|
||||
<span t-field="o.production_id.name"/>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row mt32 mb32">
|
||||
<div class="col-3">
|
||||
<strong>Finished Product:</strong><br/>
|
||||
<span t-field="o.product_id"/>
|
||||
</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"/>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<t t-call="mrp.report_mrp_production_components" t-if="o.move_raw_ids"/>
|
||||
|
||||
<div class="oe_structure"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="label_production_view">
|
||||
<t t-set="uom_categ_unit" t-value="env.ref('uom.product_uom_categ_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 t-else="">
|
||||
<t t-set="qty" t-value="1"/>
|
||||
</t>
|
||||
<t t-foreach="range(qty)" t-as="item">
|
||||
<t t-translation="off">
|
||||
^XA
|
||||
^FO100,50
|
||||
^A0N,44,33^FD<t t-esc="move_line.product_id.display_name"/>^FS
|
||||
<t t-if="move_line.product_id.tracking != 'none' and move_line.lot_id">
|
||||
^FO100,100
|
||||
^A0N,44,33^FDLN/SN: <t t-esc="move_line.lot_id.name"/>^FS
|
||||
^FO100,150^BY3
|
||||
^BCN,100,Y,N,N
|
||||
^FD<t t-esc="move_line.lot_id.name"/>^FS
|
||||
</t>
|
||||
<t t-elif="move_line.product_id.tracking == 'none' and move_line.product_id.barcode">
|
||||
^FO100,100^BY3
|
||||
^BCN,100,Y,N,N
|
||||
^FD<t t-esc="move_line.product_id.barcode"/>^FS
|
||||
</t>
|
||||
<t t-else="">
|
||||
^FO100,100
|
||||
^A0N,44,33^FDNo barcode available^FS
|
||||
</t>
|
||||
^XZ
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
||||
89
odoo-bringout-oca-ocb-mrp/mrp/report/report_deliveryslip.xml
Normal file
89
odoo-bringout-oca-ocb-mrp/mrp/report/report_deliveryslip.xml
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="stock_report_delivery_document_inherit_mrp" inherit_id="stock.report_delivery_document">
|
||||
<!-- needs to be set before so elif directly follows if later on -->
|
||||
<xpath expr="//t[@name='has_packages']" position="before">
|
||||
<!-- get only the top level kits' (i.e. no subkit) move lines for easier mapping later on + we ignore subkit groupings-->
|
||||
<!-- note that move.name uses top level kit's product.template.display_name value instead of product.template.name -->
|
||||
<t t-set="has_kits" t-value="o.move_line_ids.filtered(lambda l: l.move_id.bom_line_id and l.move_id.bom_line_id.bom_id.type == 'phantom')"/>
|
||||
</xpath>
|
||||
<xpath expr="//t[@name='no_package_section']" position="before">
|
||||
<t t-set="has_kits" t-value="o.move_line_ids.filtered(lambda l: l.move_id.bom_line_id and l.move_id.bom_line_id.bom_id.type == 'phantom')"/>
|
||||
<t t-if="has_kits">
|
||||
<!-- print the products not in a package or kit first -->
|
||||
<t t-set="move_lines" t-value="move_lines.filtered(lambda m: not m.move_id.bom_line_id)"/>
|
||||
</t>
|
||||
</xpath>
|
||||
<xpath expr="//t[@name='no_package_move_lines']" position="inside">
|
||||
<t t-call="mrp.stock_report_delivery_kit_sections"/>
|
||||
</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-call="mrp.stock_report_delivery_kit_sections"/>
|
||||
<t t-call="mrp.stock_report_delivery_no_kit_section"/>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<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')"/>
|
||||
<!-- 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">
|
||||
<!-- Separate product.product from template for variants-->
|
||||
<t t-if="bom.product_id">
|
||||
<t t-set="kit_product" t-value="bom.product_id"/>
|
||||
</t>
|
||||
<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'">
|
||||
<td colspan="99">
|
||||
<span t-esc="kit_product.display_name"/>
|
||||
</td>
|
||||
</tr>
|
||||
<t t-set="kit_move_lines" t-value="all_kits_move_lines.filtered(lambda l: l.move_id.bom_line_id.bom_id == bom)"/>
|
||||
<t t-if="has_serial_number">
|
||||
<tr t-foreach="kit_move_lines" t-as="move_line">
|
||||
<t t-set="description" t-value="move_line.move_id.description_picking"/>
|
||||
<t t-if="description == kit_product.display_name">
|
||||
<t t-set="description" t-value=""/>
|
||||
</t>
|
||||
<t t-call="stock.stock_report_delivery_has_serial_move_line"/>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-set="aggregated_lines" t-value="kit_move_lines._get_aggregated_product_quantities(kit_name=kit_product.display_name)"/>
|
||||
<t t-if="aggregated_lines">
|
||||
<t t-call="stock.stock_report_delivery_aggregated_move_lines"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- 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-if="no_kit_move_lines">
|
||||
<tr t-att-class="'bg-200 fw-bold o_line_section'">
|
||||
<td colspan="99">
|
||||
<span>Products not associated with a kit</span>
|
||||
</td>
|
||||
</tr>
|
||||
<t t-if="has_serial_number">
|
||||
<tr t-foreach="no_kit_move_lines" t-as="move_line">
|
||||
<t t-call="stock.stock_report_delivery_has_serial_move_line"/>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-set="aggregated_lines" t-value="no_kit_move_lines._get_aggregated_product_quantities()"/>
|
||||
<t t-if="aggregated_lines">
|
||||
<t t-call="stock.stock_report_delivery_aggregated_move_lines"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="mrp_report_product_product_replenishment" inherit_id="stock.report_product_product_replenishment">
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
from odoo.tools import format_date
|
||||
|
||||
|
||||
class ReceptionReport(models.AbstractModel):
|
||||
_inherit = 'report.stock.report_reception'
|
||||
|
||||
def _get_docs(self, docids):
|
||||
if self.env.context.get('default_production_ids'):
|
||||
return self.env['mrp.production'].search([('id', 'in', self.env.context.get('default_production_ids')), ('state', '!=', 'cancel')])
|
||||
return super()._get_docs(docids)
|
||||
|
||||
def _get_doc_model(self):
|
||||
if self.env.context.get('default_production_ids'):
|
||||
return 'mrp.production'
|
||||
return super()._get_doc_model()
|
||||
|
||||
def _get_doc_types(self):
|
||||
return super()._get_doc_types() + " or manufacturing orders"
|
||||
|
||||
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 super()._get_moves(docs)
|
||||
|
||||
def _get_extra_domain(self, docs):
|
||||
if self.env.context.get('default_production_ids'):
|
||||
return [('raw_material_production_id', 'not in', docs.ids)]
|
||||
return super()._get_extra_domain(docs)
|
||||
|
||||
def _get_formatted_scheduled_date(self, source):
|
||||
if source._name == 'mrp.production':
|
||||
return format_date(self.env, source.date_planned_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):
|
||||
if in_move.production_id:
|
||||
in_move.production_id.move_dest_ids -= out_move
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="mrp_reception_action" model="ir.actions.client">
|
||||
<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>
|
||||
17
odoo-bringout-oca-ocb-mrp/mrp/report/report_stock_rule.py
Normal file
17
odoo-bringout-oca-ocb-mrp/mrp/report/report_stock_rule.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class ReportStockRule(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)
|
||||
if rule.action == 'manufacture':
|
||||
res['source'] = product_id.property_stock_production
|
||||
return res
|
||||
32
odoo-bringout-oca-ocb-mrp/mrp/report/report_stock_rule.xml
Normal file
32
odoo-bringout-oca-ocb-mrp/mrp/report/report_stock_rule.xml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<template id="mrp_report_stock_rule" inherit_id="stock.report_stock_rule">
|
||||
<xpath expr="//div[hasclass('o_report_stock_rule_rule')]/t" position="before">
|
||||
<t t-if="rule[0].action == 'manufacture'">
|
||||
<t t-if="rule[1] == 'origin'">
|
||||
<t t-call="stock.report_stock_rule_left_arrow"/>
|
||||
</t>
|
||||
</t>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('o_report_stock_rule_rule')]/t[last()]" position="after">
|
||||
<t t-if="rule[0].action == 'manufacture'">
|
||||
<t t-if="rule[1] == 'destination'">
|
||||
<t t-call="stock.report_stock_rule_right_arrow"/>
|
||||
</t>
|
||||
</t>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('o_report_stock_rule_rule_name')]/span" position="before">
|
||||
<t t-if="rule[0].action == 'manufacture'">
|
||||
<i class="fa fa-wrench fa-fw" t-attf-style="color: #{color};"/>
|
||||
</t>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('o_report_stock_rule_legend')]" position="inside">
|
||||
<div class="o_report_stock_rule_legend_line">
|
||||
<div class="o_report_stock_rule_legend_label">Manufacture</div>
|
||||
<div class="o_report_stock_rule_legend_symbol">
|
||||
<div class="fa fa-wrench fa-fw" t-attf-style="color: #{color};"/>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
47
odoo-bringout-oca-ocb-mrp/mrp/report/stock_forecasted.py
Normal file
47
odoo-bringout-oca-ocb-mrp/mrp/report/stock_forecasted.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class ReplenishmentReport(models.AbstractModel):
|
||||
_inherit = 'report.stock.report_product_product_replenishment'
|
||||
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
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)
|
||||
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 + [
|
||||
('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']
|
||||
|
||||
return res
|
||||
Loading…
Add table
Add a link
Reference in a new issue