[IMP] {purchase_,}mrp: Add MO Overview
Adds in a new report that allows the user to monitor the entire production of a product, including the resupply of the components (i.e. subassemblies, purchases, ...) in a single view. Task-3059467 Part-of: odoo/odoo#113394
This commit is contained in:
committed by
William Henrotin
parent
c823f477aa
commit
1ef03b92a0
@@ -41,6 +41,7 @@
|
||||
'report/report_deliveryslip.xml',
|
||||
'report/mrp_report_views_main.xml',
|
||||
'report/mrp_report_bom_structure.xml',
|
||||
'report/mrp_report_mo_overview.xml',
|
||||
'report/mrp_production_templates.xml',
|
||||
'report/report_stock_reception.xml',
|
||||
'report/report_stock_rule.xml',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,468 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import copy
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.tools import float_compare, format_date, float_is_zero
|
||||
from datetime import timedelta
|
||||
from math import log10
|
||||
|
||||
class ReportMoOverview(models.AbstractModel):
|
||||
_name = 'report.mrp.report_mo_overview'
|
||||
_description = 'MO Overview Report'
|
||||
|
||||
@api.model
|
||||
def get_report_values(self, production_id):
|
||||
""" Endpoint for HTML display. """
|
||||
return {
|
||||
'data': self._get_report_data(production_id),
|
||||
'context': self._get_display_context(),
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _get_report_values(self, docids, data=None):
|
||||
""" Endpoint for PDF display. """
|
||||
if not data:
|
||||
data = {}
|
||||
docs = []
|
||||
for prod_id in docids:
|
||||
doc = self._get_report_data(prod_id)
|
||||
doc['show_replenishments'] = data.get('replenishments') == '1'
|
||||
doc['show_availabilities'] = data.get('availabilities') == '1'
|
||||
doc['show_receipts'] = data.get('receipts') == '1'
|
||||
doc['show_mo_costs'] = data.get('moCosts') == '1'
|
||||
doc['show_product_costs'] = data.get('productCosts') == '1'
|
||||
doc['show_uom'] = self.env.user.user_has_groups('uom.group_uom')
|
||||
doc['data_mo_unit_cost'] = doc['summary'].get('mo_cost', 0) / doc['summary'].get('quantity', 1)
|
||||
doc['data_product_unit_cost'] = doc['summary'].get('product_cost', 0) / doc['summary'].get('quantity', 1)
|
||||
doc['unfolded_ids'] = set(json.loads(data.get('unfoldedIds', '[]')))
|
||||
|
||||
docs.append(doc)
|
||||
return {
|
||||
'doc_ids': docids,
|
||||
'doc_model': 'mrp.production',
|
||||
'docs': docs,
|
||||
}
|
||||
|
||||
def _get_display_context(self):
|
||||
return {
|
||||
'show_uom': self.env.user.user_has_groups('uom.group_uom'),
|
||||
}
|
||||
|
||||
def _get_report_data(self, production_id):
|
||||
production = self.env['mrp.production'].browse(production_id)
|
||||
components = self._get_components_data(production, level=1, current_index='')
|
||||
operations = self._get_operations_data(production, level=1, current_index='')
|
||||
summary = self._get_mo_summary(production, components, operations)
|
||||
return {
|
||||
'id': production.id,
|
||||
'summary': summary,
|
||||
'components': components,
|
||||
'operations': operations,
|
||||
'extras': self._get_report_extra_lines(summary),
|
||||
}
|
||||
|
||||
def _get_report_extra_lines(self, summary):
|
||||
currency = summary.get('currency', self.env.company)
|
||||
unit_mo_cost = currency.round(summary.get('mo_cost', 0) / summary.get('quantity', 1))
|
||||
unit_product_cost = currency.round(summary.get('product_cost', 0) / summary.get('quantity', 1))
|
||||
return {
|
||||
'unit_mo_cost': unit_mo_cost,
|
||||
'unit_mo_cost_decorator': self._get_comparison_decorator(unit_product_cost, unit_mo_cost, currency.rounding),
|
||||
'unit_product_cost': unit_product_cost,
|
||||
}
|
||||
|
||||
def _get_mo_summary(self, production, components, operations):
|
||||
product = production.product_id
|
||||
company = production.company_id or self.env.company
|
||||
mo_cost = sum(component.get('summary', {}).get('mo_cost', 0.0) for component in components)
|
||||
return {
|
||||
'level': 0,
|
||||
'model': production._name,
|
||||
'id': production.id,
|
||||
'name': production.product_id.display_name,
|
||||
'product_model': production.product_id._name,
|
||||
'product_id': production.product_id.id,
|
||||
'state': production.state,
|
||||
'formatted_state': self._format_state(production),
|
||||
'quantity': production.product_uom_qty,
|
||||
'uom_name': production.product_uom_id.display_name,
|
||||
'uom_precision': self._get_uom_precision(production.product_uom_id.rounding),
|
||||
'quantity_free': product.uom_id._compute_quantity(product.free_qty, production.product_uom_id) if product.type == 'product' else False,
|
||||
'quantity_on_hand': product.uom_id._compute_quantity(product.qty_available, production.product_uom_id) if product.type == 'product' else False,
|
||||
'quantity_reserved': 0.0,
|
||||
'receipt': self._check_planned_start(production.date_planned_start, self._get_replenishment_receipt(production, components)),
|
||||
'mo_cost': company.currency_id.round(mo_cost + operations.get('summary', {}).get('mo_cost', 0.0)),
|
||||
'product_cost': company.currency_id.round(product.standard_price * production.product_uom_qty),
|
||||
'currency_id': company.currency_id.id,
|
||||
'currency': company.currency_id,
|
||||
}
|
||||
|
||||
def _format_state(self, record):
|
||||
return dict(record._fields['state']._description_selection(self.env)).get(record.state)
|
||||
|
||||
def _get_uom_precision(self, uom_rounding):
|
||||
return max(0, int(-(log10(uom_rounding))))
|
||||
|
||||
def _get_comparison_decorator(self, expected, current, rounding):
|
||||
compare = float_compare(current, expected, precision_rounding=rounding)
|
||||
if float_is_zero(current, precision_rounding=rounding) or compare == 0:
|
||||
return False
|
||||
elif compare > 0:
|
||||
return 'danger'
|
||||
else:
|
||||
return 'success'
|
||||
|
||||
def _get_operations_data(self, production, level=0, current_index=False):
|
||||
currency = (production.company_id or self.env.company).currency_id
|
||||
operation_uom = _("Minutes")
|
||||
operations = []
|
||||
total_expected_time = 0.0
|
||||
total_current_time = 0.0
|
||||
total_expected_cost = 0.0
|
||||
total_current_cost = 0.0
|
||||
for index, workorder in enumerate(production.workorder_ids):
|
||||
wo_duration = workorder.duration + workorder.get_working_duration() # Add duration of ongoing work sessions.
|
||||
expected_cost = (workorder.duration_expected / 60.0) * workorder.workcenter_id.costs_hour
|
||||
current_cost = (wo_duration / 60.0) * workorder.workcenter_id.costs_hour
|
||||
operations.append({
|
||||
'level': level,
|
||||
'index': f"{current_index}W{index}",
|
||||
'model': workorder._name,
|
||||
'id': workorder.id,
|
||||
'name': workorder.name,
|
||||
'state': workorder.state,
|
||||
'formatted_state': self._format_state(workorder),
|
||||
'quantity': workorder.duration_expected if float_is_zero(wo_duration, precision_digits=2) else wo_duration,
|
||||
'quantity_decorator': self._get_comparison_decorator(workorder.duration_expected, wo_duration, 0.01),
|
||||
'uom_name': operation_uom,
|
||||
'production_id': production.id,
|
||||
'mo_cost': expected_cost if float_is_zero(current_cost, precision_rounding=currency.rounding) else current_cost,
|
||||
'mo_cost_decorator': self._get_comparison_decorator(expected_cost, current_cost, currency.rounding),
|
||||
'currency_id': currency.id,
|
||||
'currency': currency,
|
||||
})
|
||||
total_expected_time += workorder.duration_expected
|
||||
total_current_time += workorder.duration_expected if float_is_zero(wo_duration, precision_digits=2) else wo_duration
|
||||
total_expected_cost += expected_cost
|
||||
total_current_cost += expected_cost if float_is_zero(current_cost, precision_rounding=currency.rounding) else current_cost
|
||||
|
||||
return {
|
||||
'summary': {
|
||||
'index': f"{current_index}W",
|
||||
'quantity': total_current_time,
|
||||
'quantity_decorator': self._get_comparison_decorator(total_expected_time, total_current_time, 0.01),
|
||||
'mo_cost': total_current_cost,
|
||||
'mo_cost_decorator': self._get_comparison_decorator(total_expected_cost, total_current_cost, currency.rounding),
|
||||
'uom_name': operation_uom,
|
||||
'currency_id': currency.id,
|
||||
'currency': currency,
|
||||
},
|
||||
'details': operations,
|
||||
}
|
||||
|
||||
def _get_components_data(self, production, replenish_data=False, level=0, current_index=False):
|
||||
if not replenish_data:
|
||||
replenish_data = {
|
||||
'products': {},
|
||||
'warehouses': {},
|
||||
'qty_already_reserved': defaultdict(float),
|
||||
'qty_reserved': {},
|
||||
}
|
||||
components = []
|
||||
company = production.company_id or self.env.company
|
||||
replenish_data = self._get_replenishments_from_forecast(production, replenish_data)
|
||||
for count, move_raw in enumerate(production.move_raw_ids):
|
||||
component_index = f"{current_index}{count}"
|
||||
replenishments = self._get_replenishment_lines(production, move_raw, replenish_data, level, component_index)
|
||||
# If not enough replenishment -> To Order / Might get "non-available" in summary since all component won't be there in time
|
||||
components.append({
|
||||
'summary': self._format_component_move(production, move_raw, replenishments, company, replenish_data, level, component_index),
|
||||
'replenishments': replenishments
|
||||
})
|
||||
|
||||
return components
|
||||
|
||||
def _format_component_move(self, production, move_raw, replenishments, company, replenish_data, level, index):
|
||||
product = move_raw.product_id
|
||||
mo_cost = sum(rep.get('summary', {}).get('mo_cost', 0.0) for rep in replenishments)
|
||||
product_cost = product.standard_price * move_raw.product_qty
|
||||
component = {
|
||||
'level': level,
|
||||
'index': index,
|
||||
'id': product.id,
|
||||
'model': product._name,
|
||||
'name': product.display_name,
|
||||
'product_model': product._name,
|
||||
'product_id': product.id,
|
||||
'quantity': move_raw.product_uom_qty,
|
||||
'uom_name': move_raw.product_uom.display_name,
|
||||
'uom_precision': self._get_uom_precision(move_raw.product_uom.rounding),
|
||||
'quantity_free': product.uom_id._compute_quantity(product.free_qty, move_raw.product_uom) if product.type == 'product' else False,
|
||||
'quantity_on_hand': product.uom_id._compute_quantity(product.qty_available, move_raw.product_uom) if product.type == 'product' else False,
|
||||
'quantity_reserved': self._get_reserved_qty(move_raw, production.warehouse_id, replenish_data),
|
||||
'receipt': self._check_planned_start(production.date_planned_start, self._get_component_receipt(product, move_raw, production.warehouse_id, replenishments, replenish_data)),
|
||||
'mo_cost': company.currency_id.round(mo_cost if replenishments else product_cost),
|
||||
'product_cost': company.currency_id.round(product_cost),
|
||||
'currency_id': company.currency_id.id,
|
||||
'currency': company.currency_id,
|
||||
}
|
||||
if product.type != 'product':
|
||||
return component
|
||||
if any(rep.get('summary', {}).get('model') == 'to_order' for rep in replenishments):
|
||||
# Means that there's an extra "To Order" line summing up what's left to order.
|
||||
component['formatted_state'] = _("To Order")
|
||||
component['state'] = 'to_order'
|
||||
|
||||
return component
|
||||
|
||||
def _check_planned_start(self, mo_planned_start, receipt):
|
||||
if receipt.get('date', False) and receipt['date'] > mo_planned_start:
|
||||
receipt['decorator'] = 'danger'
|
||||
return receipt
|
||||
|
||||
def _get_component_receipt(self, product, move, warehouse, replenishments, replenish_data):
|
||||
def get(replenishment, key, check_in_receipt=False):
|
||||
fetch = replenishment.get('summary', {})
|
||||
if check_in_receipt:
|
||||
fetch = fetch.get('receipt', {})
|
||||
return fetch.get(key, False)
|
||||
|
||||
if any(get(rep, 'type', True) == 'unavailable' for rep in replenishments):
|
||||
return self._format_receipt_date('unavailable')
|
||||
if product.type != 'product':
|
||||
return self._format_receipt_date('available')
|
||||
|
||||
has_to_order_line = any(rep.get('summary', {}).get('model') == 'to_order' for rep in replenishments)
|
||||
reserved_quantity = self._get_reserved_qty(move, warehouse, replenish_data)
|
||||
missing_quantity = move.product_uom_qty - reserved_quantity
|
||||
free_qty = product.uom_id._compute_quantity(product.free_qty, move.product_uom)
|
||||
if not has_to_order_line and float_compare(missing_quantity, free_qty, precision_rounding=move.product_uom.rounding) <= 0:
|
||||
return self._format_receipt_date('available')
|
||||
|
||||
max_date = max(map(lambda rep: get(rep, 'date', True), replenishments))
|
||||
if has_to_order_line or any(get(rep, 'type', True) == 'estimated' for rep in replenishments):
|
||||
return self._format_receipt_date('estimated', max_date)
|
||||
else:
|
||||
return self._format_receipt_date('expected', max_date)
|
||||
|
||||
def _get_replenishment_lines(self, production, move_raw, replenish_data, level, current_index):
|
||||
product = move_raw.product_id
|
||||
currency = (production.company_id or self.env.company).currency_id
|
||||
forecast = replenish_data['products'][product.id].get('forecast', [])
|
||||
current_lines = filter(lambda line: line.get('document_in', False) and line.get('document_out', False) and line['document_out'].get('id', False) == production.id, forecast)
|
||||
total_ordered = 0
|
||||
replenishments = []
|
||||
for count, forecast_line in enumerate(current_lines):
|
||||
doc_in = self.env[forecast_line['document_in']['_name']].browse(forecast_line['document_in']['id'])
|
||||
replenishment_index = f"{current_index}{count}"
|
||||
replenishment = {}
|
||||
uom_id = self.env['uom.uom'].browse(forecast_line['uom_id']['id'])
|
||||
replenishment['summary'] = {
|
||||
'level': level + 1,
|
||||
'index': replenishment_index,
|
||||
'id': doc_in.id,
|
||||
'model': doc_in._name,
|
||||
'name': doc_in.display_name,
|
||||
'product_model': product._name,
|
||||
'product_id': product.id,
|
||||
'state': doc_in.state,
|
||||
'formatted_state': self._format_state(doc_in),
|
||||
'quantity': forecast_line['quantity'],
|
||||
'uom_name': forecast_line['uom_id']['display_name'],
|
||||
'uom_precision': self._get_uom_precision(forecast_line['uom_id']['rounding']),
|
||||
'mo_cost': self._get_replenishment_cost(product, forecast_line['quantity'], uom_id, currency, forecast_line['move_in']),
|
||||
'product_cost': currency.round(uom_id._compute_quantity(forecast_line['quantity'], product.uom_id) * product.standard_price),
|
||||
'currency_id': currency.id,
|
||||
'currency': currency,
|
||||
}
|
||||
if doc_in._name == 'mrp.production':
|
||||
replenishment['components'] = self._get_components_data(doc_in, replenish_data, level + 2, replenishment_index)
|
||||
replenishment['operations'] = self._get_operations_data(doc_in, level + 2, replenishment_index)
|
||||
replenishment['summary']['mo_cost'] = currency.round(sum(component.get('summary', {}).get('mo_cost', 0.0) for component in replenishment['components']) + replenishment['operations'].get('summary', {}).get('mo_cost', 0.0))
|
||||
replenishment['summary']['receipt'] = self._check_planned_start(production.date_planned_start, self._get_replenishment_receipt(doc_in, replenishment.get('components', [])))
|
||||
replenishments.append(replenishment)
|
||||
total_ordered += replenishment['summary']['quantity']
|
||||
|
||||
reserved_quantity = self._get_reserved_qty(move_raw, production.warehouse_id, replenish_data)
|
||||
missing_quantity = move_raw.product_uom_qty - (reserved_quantity + total_ordered)
|
||||
# Avoid creating a "to_order" line to compensate for missing stock (i.e. negative free_qty).
|
||||
free_qty = max(0, product.uom_id._compute_quantity(product.free_qty, move_raw.product_uom))
|
||||
if product.type == 'product' and float_compare(missing_quantity, free_qty, precision_rounding=move_raw.product_uom.rounding) > 0:
|
||||
# Need to order more products to fulfill the need
|
||||
resupply_rules = replenish_data['products'][product.id].get('resupply_rules', [])
|
||||
rules_delay = sum(rule.delay for rule in resupply_rules)
|
||||
resupply_data = self._get_resupply_data(resupply_rules, rules_delay, missing_quantity, move_raw.product_uom, product, production.warehouse_id)
|
||||
if resupply_data:
|
||||
receipt = self._check_planned_start(production.date_planned_start, self._format_receipt_date('estimated', fields.datetime.today() + timedelta(days=resupply_data['delay'])))
|
||||
else:
|
||||
receipt = self._format_receipt_date('unavailable')
|
||||
|
||||
to_order_line = {'summary': {
|
||||
'level': level + 1,
|
||||
'index': f"{current_index}{len(replenishments) + 1}",
|
||||
'name': _("To Order"),
|
||||
'model': "to_order",
|
||||
'product_model': product._name,
|
||||
'product_id': product.id,
|
||||
'quantity': missing_quantity,
|
||||
'uom_name': move_raw.product_uom.display_name,
|
||||
'uom_precision': self._get_uom_precision(move_raw.product_uom.rounding),
|
||||
'receipt': receipt,
|
||||
'mo_cost': currency.round(resupply_data['cost'] if resupply_data else product.standard_price * move_raw.product_uom._compute_quantity(missing_quantity, product.uom_id)),
|
||||
'product_cost': currency.round(product.standard_price * move_raw.product_uom._compute_quantity(missing_quantity, product.uom_id)),
|
||||
'currency_id': currency.id,
|
||||
'currency': currency,
|
||||
}}
|
||||
if resupply_data:
|
||||
to_order_line['mo_cost'] = currency.round(resupply_data['cost'] * move_raw.product_uom._compute_quantity(missing_quantity, product.uom_id))
|
||||
to_order_line['receipt'] = self._check_planned_start(production.date_planned_start, self._format_receipt_date('estimated', fields.datetime.today() + timedelta(days=resupply_data['delay']))),
|
||||
else:
|
||||
to_order_line['mo_cost'] = currency.round(product.standard_price * move_raw.product_uom._compute_quantity(missing_quantity, product.uom_id))
|
||||
to_order_line['receipt'] = self._format_receipt_date('unavailable')
|
||||
replenishments.append(to_order_line)
|
||||
|
||||
return replenishments
|
||||
|
||||
def _get_replenishment_cost(self, product, quantity, uom_id, currency, move_in=False):
|
||||
return currency.round(product.standard_price * uom_id._compute_quantity(quantity, product.uom_id))
|
||||
|
||||
def _get_replenishment_receipt(self, doc_in, components):
|
||||
if doc_in._name == 'mrp.production':
|
||||
max_date_start = doc_in.date_planned_start
|
||||
all_available = True
|
||||
some_unavailable = False
|
||||
some_estimated = False
|
||||
for component in components:
|
||||
if component['summary']['receipt']['date']:
|
||||
max_date_start = max(max_date_start, component['summary']['receipt']['date'])
|
||||
all_available = all_available and component['summary']['receipt']['type'] == 'available'
|
||||
some_unavailable = some_unavailable or component['summary']['receipt']['type'] == 'unavailable'
|
||||
some_estimated = some_estimated or component['summary']['receipt']['type'] == 'estimated'
|
||||
|
||||
if some_unavailable:
|
||||
return self._format_receipt_date('unavailable')
|
||||
if all_available:
|
||||
return self._format_receipt_date('expected', doc_in.date_planned_finished)
|
||||
|
||||
new_date = max_date_start + timedelta(days=doc_in.product_id.produce_delay)
|
||||
receipt_state = 'estimated' if some_estimated else 'expected'
|
||||
return self._format_receipt_date(receipt_state, new_date)
|
||||
return self._format_receipt_date('unavailable')
|
||||
|
||||
def _format_receipt_date(self, state, date=False):
|
||||
if state == 'available':
|
||||
return {'display': _("Available"), 'type': 'available', 'decorator': 'success', 'date': False}
|
||||
elif state == 'estimated':
|
||||
return {'display': _("Estimated %s", format_date(self.env, date)), 'type': 'estimated', 'decorator': False, 'date': date}
|
||||
elif state == 'expected':
|
||||
return {'display': _("Expected %s", format_date(self.env, date)), 'type': 'expected', 'decorator': 'warning', 'date': date}
|
||||
else:
|
||||
return {'display': _("Not Available"), 'type': 'unavailable', 'decorator': 'danger', 'date': False}
|
||||
|
||||
def _get_replenishments_from_forecast(self, production, replenish_data):
|
||||
products = production.move_raw_ids.product_id
|
||||
unknown_products = products.filtered(lambda product: product.id not in replenish_data)
|
||||
if unknown_products:
|
||||
warehouse = production.warehouse_id
|
||||
wh_location_ids = self._get_warehouse_locations(warehouse, replenish_data)
|
||||
forecast_lines = self.env['stock.forecasted_product_product']._get_report_lines(False, unknown_products.ids, wh_location_ids, warehouse.lot_stock_id, read=False)
|
||||
for product in unknown_products:
|
||||
extra_docs = self._get_extra_replenishments(product)
|
||||
# Sorting the extra documents so that the ones flagged with an explicit production_id are on top of the list.
|
||||
extra_docs.sort(key=lambda ex: ex.get('production_id', False), reverse=True)
|
||||
product_forecast_lines = list(filter(lambda line: line.get('product', {}).get('id') == product.id, forecast_lines))
|
||||
replenish_data['products'][product.id] = {
|
||||
'forecast': self._add_extra_in_forecast(product_forecast_lines, extra_docs, product.uom_id.rounding),
|
||||
'resupply_rules': product._get_rules_from_location(production.warehouse_id.lot_stock_id),
|
||||
}
|
||||
|
||||
return replenish_data
|
||||
|
||||
def _add_extra_in_forecast(self, forecast_lines, extras, product_rounding):
|
||||
if not extras:
|
||||
return forecast_lines
|
||||
|
||||
lines_with_extras = []
|
||||
for forecast_line in forecast_lines:
|
||||
if forecast_line.get('document_in', False) or forecast_line.get('replenishment_filled'):
|
||||
lines_with_extras.append(forecast_line)
|
||||
continue
|
||||
line_qty = forecast_line['quantity']
|
||||
if forecast_line.get('document_out', False) and forecast_line['document_out']['_name'] == 'mrp.production':
|
||||
production_id = forecast_line['document_out']['id']
|
||||
else:
|
||||
production_id = False
|
||||
index_to_remove = []
|
||||
for index, extra in enumerate(extras):
|
||||
if float_is_zero(extra['quantity'], precision_rounding=product_rounding):
|
||||
index_to_remove.append(index)
|
||||
continue
|
||||
if production_id and extra.get('production_id', False) and extra['production_id'] != production_id:
|
||||
continue
|
||||
taken_from_extra = min(line_qty, extra['quantity'])
|
||||
line_qty -= taken_from_extra
|
||||
# Create copy of the current forecast line to add a possible replenishment.
|
||||
# Needs to be a copy since it might take multiple replenishment to fulfill a single "out" line.
|
||||
new_extra_line = copy.copy(forecast_line)
|
||||
new_extra_line['quantity'] = taken_from_extra
|
||||
new_extra_line['document_in'] = {
|
||||
'_name': extra['_name'],
|
||||
'id': extra['id'],
|
||||
}
|
||||
lines_with_extras.append(new_extra_line)
|
||||
extra['quantity'] -= taken_from_extra
|
||||
if float_compare(extra['quantity'], 0, precision_rounding=product_rounding) <= 0:
|
||||
index_to_remove.append(index)
|
||||
if float_is_zero(line_qty, precision_rounding=product_rounding):
|
||||
break
|
||||
|
||||
for index in reversed(index_to_remove):
|
||||
del extras[index]
|
||||
|
||||
return lines_with_extras
|
||||
|
||||
def _get_extra_replenishments(self, product):
|
||||
return []
|
||||
|
||||
def _get_resupply_data(self, rules, rules_delay, quantity, uom_id, product, warehouse):
|
||||
manufacture_rules = [rule for rule in rules if rule.action == 'manufacture']
|
||||
if manufacture_rules:
|
||||
# Need to get rules from Production location to get delays before production
|
||||
wh_manufacture_rules = product._get_rules_from_location(product.property_stock_production, route_ids=warehouse.route_ids)
|
||||
wh_manufacture_rules -= rules
|
||||
rules_delay += sum(rule.delay for rule in wh_manufacture_rules)
|
||||
return {
|
||||
'delay': product.produce_delay + rules_delay,
|
||||
'cost': product.standard_price * uom_id._compute_quantity(quantity, product.uom_id),
|
||||
}
|
||||
return False
|
||||
|
||||
def _get_warehouse_locations(self, warehouse, replenish_data):
|
||||
if not replenish_data['warehouses'].get(warehouse.id):
|
||||
replenish_data['warehouses'][warehouse.id] = [loc['id'] for loc in self.env['stock.location'].search_read(
|
||||
[('id', 'child_of', warehouse.view_location_id.id)],
|
||||
['id']
|
||||
)]
|
||||
return replenish_data['warehouses'][warehouse.id]
|
||||
|
||||
def _get_reserved_qty(self, move_raw, warehouse, replenish_data):
|
||||
if not replenish_data['qty_reserved'].get(move_raw):
|
||||
total_reserved = 0
|
||||
wh_location_ids = self._get_warehouse_locations(warehouse, replenish_data)
|
||||
linked_moves = self.env['stock.move'].browse(move_raw._rollup_move_origs()).filtered(lambda m: m.location_id.id in wh_location_ids or m.location_dest_id.id not in wh_location_ids)
|
||||
for move in linked_moves:
|
||||
if move.state not in ('partially_available', 'assigned'):
|
||||
continue
|
||||
# count reserved stock.
|
||||
reserved = move.product_uom._compute_quantity(move.reserved_availability, move.product_id.uom_id)
|
||||
# check if the move reserved qty was counted before (happens if multiple outs share pick/pack)
|
||||
reserved = min(reserved - replenish_data['qty_already_reserved'][move], move_raw.product_qty)
|
||||
total_reserved += reserved
|
||||
replenish_data['qty_already_reserved'][move] += reserved
|
||||
if float_compare(total_reserved, move_raw.product_qty, precision_rounding=move.product_id.uom_id.rounding) >= 0:
|
||||
break
|
||||
replenish_data['qty_reserved'][move_raw] = total_reserved
|
||||
|
||||
return replenish_data['qty_reserved'][move_raw]
|
||||
@@ -0,0 +1,140 @@
|
||||
<?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">
|
||||
<table class="table">
|
||||
<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 t-if="data['show_mo_costs']" class="text-end">MO Cost</th>
|
||||
<th t-if="data['show_product_costs']" class="text-end">Product 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>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td class="text-start">Unit Cost</td>
|
||||
<td t-if="data['show_replenishments']"/>
|
||||
<td/>
|
||||
<td t-if="data['show_uom']"/>
|
||||
<td t-if="data['show_availabilities']"/>
|
||||
<td t-if="data['show_availabilities']"/>
|
||||
<td t-if="data['show_receipts']"/>
|
||||
<td t-if="data['show_mo_costs']" t-attf-class="text-end {{ 'text-' + data['extras']['unit_mo_cost_decorator'] if data.get('extras', {}).get('unit_mo_cost_decorator') else '' }}" t-out="data['extras']['unit_mo_cost']" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
|
||||
<td t-if="data['show_product_costs']" class="text-end" t-out="data['extras']['unit_product_cost']" t-options="{'widget': 'monetary', 'display_currency': currency}"/>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</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 {{ 'text-' + line['quantity_decorator'] if line.get('quantity_decorator') else '' }}">
|
||||
<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', 'decimal_precision': 'Product Unit of Measure'}"/>
|
||||
</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']" t-attf-class="text-end {{ 'text-' + line['receipt']['decorator'] if line.get('receipt', {}).get('decorator') else '' }}">
|
||||
<span t-if="line.get('receipt')" t-attf-class="{{ 'text-' + line['receipt']['decorator'] }}" t-out="line['receipt']['display']"/>
|
||||
</td>
|
||||
<td t-if="data['show_mo_costs']" t-attf-class="text-end {{ 'text-' + line['mo_cost_decorator'] if line.get('mo_cost_decorator') else '' }}">
|
||||
<t t-out="line['mo_cost']" t-options="{'widget': 'monetary', 'display_currency': line['currency']}"/>
|
||||
</td>
|
||||
<td t-if="data['show_product_costs']" class="text-end">
|
||||
<t t-if="line.get('product_cost', False) is not False" t-out="line['product_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>
|
||||
</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>
|
||||
</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 {{ 'text-' + summary['quantity_decorator'] if summary.get('quantity_decorator') else '' }}">
|
||||
<t 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_mo_costs']" t-attf-class="text-end {{ 'text-' + summary['mo_cost_decorator'] if summary.get('mo_cost_decorator') else '' }}">
|
||||
<t t-out="summary['mo_cost']" t-options="{'widget': 'monetary', 'display_currency': summary['currency']}"/>
|
||||
</td>
|
||||
<td t-if="data['show_product_costs']" class="text-end"/>
|
||||
</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>
|
||||
</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>
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component, EventBus, onWillStart, useSubEnv, useState } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useBus, useService } from "@web/core/utils/hooks";
|
||||
import { Layout } from "@web/search/layout";
|
||||
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
|
||||
import { MoOverviewLine } from "../mo_overview_line/mrp_mo_overview_line";
|
||||
import { MoOverviewDisplayFilter } from "../mo_overview_display_filter/mrp_mo_overview_display_filter";
|
||||
import { MoOverviewComponentsBlock } from "../mo_overview_components_block/mrp_mo_overview_components_block";
|
||||
import { formatMonetary } from "@web/views/fields/formatters";
|
||||
|
||||
export class MoOverview extends Component {
|
||||
static template = "mrp.MoOverview";
|
||||
|
||||
setup() {
|
||||
this.actionService = useService("action");
|
||||
this.ormService = useService("orm");
|
||||
this.unfoldedIds = new Set();
|
||||
this.context = {};
|
||||
|
||||
this.state = useState({
|
||||
data: {},
|
||||
showOptions: this.getDefaultConfig(),
|
||||
});
|
||||
|
||||
useSubEnv({ overviewBus: new EventBus() });
|
||||
|
||||
onWillStart(async () => {
|
||||
await this.getManufacturingData();
|
||||
});
|
||||
useBus(this.env.overviewBus, "update-folded", (ev) => this.onChangeFolded(ev.detail));
|
||||
useBus(this.env.overviewBus, "reload", () => this.getManufacturingData());
|
||||
}
|
||||
|
||||
async getManufacturingData() {
|
||||
const reportValues = await this.ormService.call(
|
||||
"report.mrp.report_mo_overview",
|
||||
"get_report_values",
|
||||
[this.activeId],
|
||||
);
|
||||
this.state.data = reportValues.data;
|
||||
this.state.showOptions.uom = reportValues.context.show_uom;
|
||||
this.context = reportValues.context;
|
||||
if (reportValues.data?.operations?.summary?.index) {
|
||||
// Main MO's operations are always unfolded by default.
|
||||
this.unfoldedIds.add(reportValues.data.operations.summary.index);
|
||||
}
|
||||
}
|
||||
|
||||
//---- Handlers ----
|
||||
|
||||
onChangeDisplay(displayInfo) {
|
||||
this.state.showOptions[displayInfo] = !this.state.showOptions[displayInfo];
|
||||
}
|
||||
|
||||
onChangeFolded(foldInfo) {
|
||||
const { indexes, isFolded } = foldInfo;
|
||||
const operation = isFolded ? "delete" : "add";
|
||||
indexes.forEach(index => this.unfoldedIds[operation](index));
|
||||
}
|
||||
|
||||
async onPrint() {
|
||||
const reportName = `mrp.report_mo_overview?docids=${this.activeId}`
|
||||
+ `&replenishments=${+this.state.showOptions.replenishments}`
|
||||
+ `&availabilities=${+this.state.showOptions.availabilities}`
|
||||
+ `&receipts=${+this.state.showOptions.receipts}`
|
||||
+ `&moCosts=${+this.state.showOptions.moCosts}`
|
||||
+ `&productCosts=${+this.state.showOptions.productCosts}`
|
||||
+ `&unfoldedIds=${JSON.stringify(Array.from(this.unfoldedIds))}`;
|
||||
|
||||
return this.actionService.doAction({
|
||||
type: "ir.actions.report",
|
||||
report_type: "qweb-pdf",
|
||||
report_name: reportName,
|
||||
report_file: "mrp.report_mo_overview",
|
||||
});
|
||||
}
|
||||
|
||||
onUnfold() {
|
||||
this.env.overviewBus.trigger("unfold-all")
|
||||
}
|
||||
|
||||
//---- Helpers ----
|
||||
|
||||
getDefaultConfig() {
|
||||
return {
|
||||
uom: false,
|
||||
replenishments: true,
|
||||
availabilities: true,
|
||||
receipts: true,
|
||||
moCosts: true,
|
||||
productCosts: true,
|
||||
};
|
||||
}
|
||||
|
||||
getColorClass(decorator) {
|
||||
return decorator ? `text-${decorator}` : "";
|
||||
}
|
||||
|
||||
formatCost(cost) {
|
||||
return formatMonetary(cost, { currencyId: this.state.data.summary.currency_id });
|
||||
}
|
||||
|
||||
//---- Getters ----
|
||||
|
||||
get activeId() {
|
||||
return this.props.action.context.active_id;
|
||||
}
|
||||
|
||||
get showUom() {
|
||||
return this.state.showOptions.uom;
|
||||
}
|
||||
|
||||
get showReplenishments() {
|
||||
return this.state.showOptions.replenishments;
|
||||
}
|
||||
|
||||
get showAvailabilities() {
|
||||
return this.state.showOptions.availabilities;
|
||||
}
|
||||
|
||||
get showReceipts() {
|
||||
return this.state.showOptions.receipts;
|
||||
}
|
||||
|
||||
get showMoCosts() {
|
||||
return this.state.showOptions.moCosts;
|
||||
}
|
||||
|
||||
get showProductCosts() {
|
||||
return this.state.showOptions.productCosts;
|
||||
}
|
||||
}
|
||||
|
||||
MoOverview.components = {
|
||||
Layout,
|
||||
MoOverviewLine,
|
||||
MoOverviewDisplayFilter,
|
||||
MoOverviewComponentsBlock,
|
||||
};
|
||||
MoOverview.props = {
|
||||
...standardActionServiceProps,
|
||||
globalState: { type: Object, optional: true }, // Appears when coming back from the breadcrumb
|
||||
};
|
||||
|
||||
registry.category("actions").add("mrp_mo_overview", MoOverview);
|
||||
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<div t-name="mrp.MoOverview" class="o_action" owl="1">
|
||||
<Layout display="{ controlPanel: { 'top-right': false } }">
|
||||
<t t-set-slot="control-panel-bottom-right">
|
||||
<MoOverviewDisplayFilter showOptions="state.showOptions" changeDisplay.bind="onChangeDisplay"/>
|
||||
</t>
|
||||
|
||||
<t t-set-slot="control-panel-bottom-left">
|
||||
<button class="btn btn-primary" t-on-click="onPrint">Print</button>
|
||||
<button class="btn btn-primary" t-on-click="onUnfold">Unfold</button>
|
||||
</t>
|
||||
|
||||
<div class="py-3 py-lg-5 overflow-auto border-bottom bg-view">
|
||||
<div class="container-fluid">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-start"/>
|
||||
<th class="text-center" t-if="showReplenishments">Status</th>
|
||||
<th t-attf-class="{{ showUom ? 'text-center' : 'text-end' }}" t-attf-colspan="{{ showUom ? 2 : 1 }}">Quantity</th>
|
||||
<th class="text-end" t-if="showAvailabilities">Free to use / On Hand</th>
|
||||
<th class="text-end" t-if="showAvailabilities">Reserved</th>
|
||||
<th class="text-end" t-if="showReceipts">Receipt</th>
|
||||
<th class="text-end" t-if="showMoCosts">MO Cost</th>
|
||||
<th class="text-end" t-if="showProductCosts">Product Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<MoOverviewLine data="state.data.summary" showOptions="state.showOptions"/>
|
||||
<MoOverviewComponentsBlock
|
||||
components="state.data.components"
|
||||
operations="state.data.operations"
|
||||
showOptions="state.showOptions"/>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td class="text-start">Unit Cost</td>
|
||||
<td t-if="showReplenishments"/>
|
||||
<td/>
|
||||
<td t-if="showUom"/>
|
||||
<td t-if="showAvailabilities"/>
|
||||
<td t-if="showAvailabilities"/>
|
||||
<td t-if="showReceipts"/>
|
||||
<td class="text-end" t-att-class="getColorClass(state.data.extras.unit_mo_cost_decorator)" t-if="showMoCosts" t-out="formatCost(state.data.extras.unit_mo_cost)"/>
|
||||
<td class="text-end" t-if="showProductCosts" t-out="formatCost(state.data.extras.unit_product_cost)"/>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component, useState } from "@odoo/owl";
|
||||
import { useBus } from "@web/core/utils/hooks";
|
||||
import { MoOverviewLine } from "../mo_overview_line/mrp_mo_overview_line";
|
||||
import { MoOverviewOperationsBlock } from "../mo_overview_operations_block/mrp_mo_overview_operations_block";
|
||||
|
||||
export class MoOverviewComponentsBlock extends Component {
|
||||
static template = "mrp.MoOverviewComponentsBlock";
|
||||
|
||||
setup() {
|
||||
this.state = useState({
|
||||
fold: this.getIndexStates(),
|
||||
unfoldAll: this.props.unfoldAll || false,
|
||||
});
|
||||
|
||||
if (this.props.unfoldAll) {
|
||||
this.env.overviewBus.trigger("update-folded", { indexes: Object.keys(this.state.fold), isFolded: false });
|
||||
}
|
||||
|
||||
useBus(this.env.overviewBus, "unfold-all", () => this.unfoldAll());
|
||||
}
|
||||
|
||||
//---- Handlers ----
|
||||
|
||||
onToggleFolded(foldIndex) {
|
||||
this.state.unfoldAll = false;
|
||||
const newState = !this.state.fold[foldIndex];
|
||||
if (newState) {
|
||||
// If a line is folded, its children lines must be folded as well
|
||||
Object.keys(this.state.fold).filter(key => key.startsWith(foldIndex)).forEach(index => {
|
||||
this.state.fold[index] = newState;
|
||||
});
|
||||
}
|
||||
this.state.fold[foldIndex] = newState;
|
||||
this.env.overviewBus.trigger("update-folded", { indexes: [foldIndex], isFolded: newState });
|
||||
}
|
||||
|
||||
unfoldAll() {
|
||||
this.state.unfoldAll = true;
|
||||
const foldIndexes = Object.keys(this.state.fold);
|
||||
foldIndexes.forEach(index => this.state.fold[index] = false);
|
||||
this.env.overviewBus.trigger("update-folded", { indexes: foldIndexes, isFolded: false });
|
||||
}
|
||||
|
||||
//---- Helpers ----
|
||||
|
||||
getIndexStates() {
|
||||
const indexStates = {};
|
||||
(this.props?.components ?? []).forEach(component => {
|
||||
indexStates[component?.summary.index] = !this.props.unfoldAll;
|
||||
(component?.replenishments ?? []).forEach(replenishment => {
|
||||
indexStates[replenishment?.summary.index] = !this.props.unfoldAll;
|
||||
});
|
||||
});
|
||||
return indexStates;
|
||||
}
|
||||
|
||||
hasReplenishments(component) {
|
||||
return component?.replenishments?.length > 0;
|
||||
}
|
||||
|
||||
hasReplenishmentsBlock(component) {
|
||||
return this.hasReplenishments(component) && !this.state.fold[component?.summary.index];
|
||||
}
|
||||
|
||||
hasComponents(replenishment) {
|
||||
return replenishment?.components?.length > 0 || replenishment?.operations?.details?.length > 0;
|
||||
}
|
||||
|
||||
hasComponentsBlock(replenishment) {
|
||||
return this.hasComponents(replenishment) && !this.state.fold[replenishment?.summary.index];
|
||||
}
|
||||
}
|
||||
|
||||
MoOverviewComponentsBlock.components = {
|
||||
MoOverviewLine,
|
||||
MoOverviewOperationsBlock,
|
||||
MoOverviewComponentsBlock,
|
||||
};
|
||||
MoOverviewComponentsBlock.props = {
|
||||
unfoldAll: { type: Boolean, optional: true },
|
||||
components: { type: Array, optional: true },
|
||||
operations: {
|
||||
type: Object,
|
||||
shape: {
|
||||
summary: Object,
|
||||
details: Array,
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
showOptions: {
|
||||
type: Object,
|
||||
shape: {
|
||||
uom: Boolean,
|
||||
replenishments: Boolean,
|
||||
availabilities: Boolean,
|
||||
receipts: Boolean,
|
||||
moCosts: Boolean,
|
||||
productCosts: Boolean,
|
||||
},
|
||||
},
|
||||
};
|
||||
MoOverviewComponentsBlock.defaultProps = {
|
||||
unfoldAll: false,
|
||||
};
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mrp.MoOverviewComponentsBlock" owl="1">
|
||||
<t t-foreach="props.components" t-as="component" t-key="component.summary.index">
|
||||
<MoOverviewLine
|
||||
data="component.summary"
|
||||
hasFoldButton="hasReplenishments(component)"
|
||||
isFolded="state.fold[component.summary.index]"
|
||||
showOptions="props.showOptions"
|
||||
toggleFolded.bind="onToggleFolded"/>
|
||||
<!-- Display Replenishment block -->
|
||||
<t t-if="hasReplenishmentsBlock(component)" t-foreach="component.replenishments" t-as="replenishment" t-key="replenishment.summary.index">
|
||||
<MoOverviewLine
|
||||
data="replenishment.summary"
|
||||
hasFoldButton="hasComponents(replenishment)"
|
||||
isFolded="state.fold[replenishment.summary.index]"
|
||||
showOptions="props.showOptions"
|
||||
toggleFolded.bind="onToggleFolded"/>
|
||||
<t t-if="hasComponentsBlock(replenishment)">
|
||||
<MoOverviewComponentsBlock
|
||||
unfoldAll="state.unfoldAll"
|
||||
components="replenishment.components"
|
||||
operations="replenishment.operations"
|
||||
showOptions="props.showOptions"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
<MoOverviewOperationsBlock
|
||||
unfoldAll="state.unfoldAll"
|
||||
summary="props.operations.summary"
|
||||
operations="props.operations.details"
|
||||
showOptions="props.showOptions"/>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/** @odoo-module **/
|
||||
import { BomOverviewDisplayFilter } from "../bom_overview_display_filter/mrp_bom_overview_display_filter";
|
||||
|
||||
export class MoOverviewDisplayFilter extends BomOverviewDisplayFilter {
|
||||
setup() {
|
||||
this.displayOptions = {
|
||||
replenishments: this.env._t("Replenishments"),
|
||||
availabilities: this.env._t("Availabilities"),
|
||||
receipts: this.env._t("Receipts"),
|
||||
moCosts: this.env._t("MO Costs"),
|
||||
productCosts: this.env._t("Product Costs"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
MoOverviewDisplayFilter.props = {
|
||||
showOptions: {
|
||||
type: Object,
|
||||
shape: {
|
||||
uom: Boolean,
|
||||
replenishments: Boolean,
|
||||
availabilities: Boolean,
|
||||
receipts: Boolean,
|
||||
moCosts: Boolean,
|
||||
productCosts: Boolean,
|
||||
}
|
||||
},
|
||||
changeDisplay: Function,
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
const PRODUCTION_DECORATORS = {
|
||||
draft: "muted",
|
||||
confirmed: "info",
|
||||
progress: "warning",
|
||||
done: "success",
|
||||
to_close: "success",
|
||||
cancel: "danger",
|
||||
};
|
||||
|
||||
const PURCHASE_DECORATORS = {
|
||||
draft: "info",
|
||||
sent: "info",
|
||||
['to approve']: "info",
|
||||
purchase: "info",
|
||||
done: "info",
|
||||
cancel: "muted",
|
||||
};
|
||||
|
||||
const OPERATION_DECORATORS = {
|
||||
pending: "info",
|
||||
waiting: "info",
|
||||
ready: "info",
|
||||
progress: "warning",
|
||||
done: "success",
|
||||
cancel: "muted",
|
||||
};
|
||||
|
||||
const PRODUCT_DECORATORS = {
|
||||
to_order: "danger",
|
||||
};
|
||||
|
||||
export function getStateDecorator(model, state) {
|
||||
let decorators = null;
|
||||
switch (model) {
|
||||
case "mrp.production":
|
||||
decorators = PRODUCTION_DECORATORS;
|
||||
break;
|
||||
case "mrp.workorder":
|
||||
decorators = OPERATION_DECORATORS;
|
||||
break;
|
||||
case "purchase.order":
|
||||
decorators = PURCHASE_DECORATORS;
|
||||
break;
|
||||
case "product.product":
|
||||
decorators = PRODUCT_DECORATORS;
|
||||
break;
|
||||
}
|
||||
return decorators ? `text-bg-${decorators[state]}` : "";
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { formatFloat, formatFloatTime, formatMonetary } from "@web/views/fields/formatters";
|
||||
import { getStateDecorator } from "./mo_overview_colors";
|
||||
|
||||
export class MoOverviewLine extends Component {
|
||||
static template = "mrp.MoOverviewLine";
|
||||
|
||||
setup() {
|
||||
this.actionService = useService("action");
|
||||
this.ormService = useService("orm");
|
||||
this.formatFloat = (val) => formatFloat(val, { digits: [false, this.data.uom_precision || undefined] });
|
||||
this.formatFloatTime = formatFloatTime;
|
||||
this.formatMonetary = (val) => formatMonetary(val, { currencyId: this.data.currency_id });
|
||||
}
|
||||
|
||||
//---- Handlers ----
|
||||
|
||||
async openForm() {
|
||||
const model = this.data.level === 0 ? this.data.product_model : this.data.model;
|
||||
const id = this.data.level === 0 ? this.data.product_id : this.data.id;
|
||||
return this.actionService.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
res_model: model,
|
||||
res_id: id,
|
||||
views: [[false, "form"]],
|
||||
target: "current",
|
||||
context: {
|
||||
active_id: id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async openForecast() {
|
||||
const action = await this.ormService.call(
|
||||
this.data.product_model,
|
||||
this.forecastAction,
|
||||
[[this.data.product_id]],
|
||||
);
|
||||
action.context = {
|
||||
active_model: this.data.product_model,
|
||||
active_id: this.data.product_id,
|
||||
};
|
||||
return this.actionService.doAction(action);
|
||||
}
|
||||
|
||||
async openReplenish() {
|
||||
return this.actionService.doAction("stock.action_product_replenish", {
|
||||
additionalContext: { default_product_id: this.data.product_id, default_quantity: this.data.quantity },
|
||||
onClose: (closeInfo) => {
|
||||
if (closeInfo?.done) {
|
||||
// Trigger the reload only if a replenishment was done.
|
||||
this.env.overviewBus.trigger("reload");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async openWorkorder() {
|
||||
return this.actionService.doAction({
|
||||
name: this.data.name,
|
||||
type: "ir.actions.act_window",
|
||||
res_model: "mrp.workorder",
|
||||
views: [[false, "list"]],
|
||||
context: {
|
||||
search_default_ready: true,
|
||||
search_default_waiting: true,
|
||||
search_default_progress: true,
|
||||
search_default_pending: true,
|
||||
search_default_name: this.data.name,
|
||||
search_default_production_id: this.data.production_id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
//---- Helpers ----
|
||||
|
||||
getColorClass(decorator) {
|
||||
return decorator ? `text-${decorator}` : "";
|
||||
}
|
||||
|
||||
hasQuantity(keyName) {
|
||||
return this.data.hasOwnProperty(keyName) && this.data[keyName] !== false;
|
||||
}
|
||||
|
||||
//---- Getters ----
|
||||
|
||||
get data() {
|
||||
return this.props.data;
|
||||
}
|
||||
|
||||
get stateDecorator() {
|
||||
return getStateDecorator(this.data.model, this.data.state);
|
||||
}
|
||||
|
||||
get formattedQuantity() {
|
||||
if (this.data.model === "mrp.workorder") {
|
||||
return this.formatFloatTime(this.data.quantity);
|
||||
}
|
||||
return this.formatFloat(this.data.quantity);
|
||||
}
|
||||
|
||||
get hasFoldButton() {
|
||||
return false;
|
||||
}
|
||||
|
||||
get marginMultiplicator() {
|
||||
return this.data.level - (this.props.hasFoldButton ? 1 : 0);
|
||||
}
|
||||
|
||||
get foldButtonTitle() {
|
||||
return this.props.isFolded ? this.env._t("Unfold") : this.env._t("Fold");
|
||||
}
|
||||
|
||||
get forecastAction() {
|
||||
switch (this.data.product_model) {
|
||||
case "product.product":
|
||||
return "action_product_forecast_report";
|
||||
case "product.template":
|
||||
return "action_product_tmpl_forecast_report";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MoOverviewLine.props = {
|
||||
data: {
|
||||
type: Object,
|
||||
shape: {
|
||||
level: Number,
|
||||
index: { type: String, optional: true },
|
||||
id: { type: Number, optional: true },
|
||||
model: { type: String, optional: true },
|
||||
name: String,
|
||||
product_model: { type: String, optional: true },
|
||||
product_id: { type: Number, optional: true },
|
||||
state: { type: String, optional: true },
|
||||
formatted_state: { type: String, optional: true },
|
||||
quantity: Number,
|
||||
quantity_decorator: { type: [String, Boolean], optional: true },
|
||||
uom_name: { type: String, optional: true },
|
||||
uom_precision: { type: Number, optional: true },
|
||||
quantity_free: { type: [Number, Boolean], optional: true },
|
||||
quantity_on_hand: { type: [Number, Boolean], optional: true },
|
||||
quantity_reserved: { type: Number, optional: true },
|
||||
receipt: {
|
||||
type: Object,
|
||||
shape: {
|
||||
display: String,
|
||||
type: String,
|
||||
decorator: [String, Boolean],
|
||||
date: [String, Boolean],
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
mo_cost: Number,
|
||||
mo_cost_decorator: { type: [String, Boolean], optional: true },
|
||||
product_cost: { type: Number, optional: true },
|
||||
currency_id: Number,
|
||||
currency: { type: String, optional: true },
|
||||
production_id: { type: Number, optional: true },
|
||||
},
|
||||
},
|
||||
showOptions: {
|
||||
type: Object,
|
||||
shape: {
|
||||
uom: Boolean,
|
||||
replenishments: Boolean,
|
||||
availabilities: Boolean,
|
||||
receipts: Boolean,
|
||||
moCosts: Boolean,
|
||||
productCosts: Boolean,
|
||||
}
|
||||
},
|
||||
hasFoldButton: { type: Boolean, optional: true },
|
||||
isFolded: { type: Boolean, optional: true },
|
||||
toggleFolded: { type: Function, optional: true },
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
.o_mrp_overview_badge {
|
||||
font-size: $badge-font-size * 1.1;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<tr t-name="mrp.MoOverviewLine" owl="1">
|
||||
<td class="text-start">
|
||||
<div t-attf-style="margin-left: {{ marginMultiplicator * 20 }}px" class="d-inline-block">
|
||||
<button t-if="props.hasFoldButton" t-on-click="() => this.props.toggleFolded(data.index)" class="btn btn-light p-0" t-attf-aria-label="{{ foldButtonTitle }}" t-attf-title="{{ foldButtonTitle }}" style="margin-right: 1px">
|
||||
<i t-attf-class="fa fa-fw fa-caret-{{ props.isFolded ? 'right' : 'down' }}" role="img"/>
|
||||
</button>
|
||||
<a t-if="data.id and data.model === 'mrp.workorder'" href="#" t-on-click.prevent="openWorkorder" t-out="data.name"/>
|
||||
<a t-elif="data.id and data.model" href="#" t-on-click.prevent="openForm" t-out="data.name"/>
|
||||
<t t-else="" t-out="data.name"/>
|
||||
<button t-if="data.model === 'to_order'" class="ms-2 py-0 btn btn-secondary" t-on-click="openReplenish">Replenish</button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center" t-if="props.showOptions.replenishments">
|
||||
<div class="o_field_badge">
|
||||
<span t-attf-class="badge rounded-pill o_mrp_overview_badge {{ stateDecorator }}" t-out="data.formatted_state"/>
|
||||
</div>
|
||||
</td>
|
||||
<td t-attf-class="text-end {{ getColorClass(data.quantity_decorator) }}" t-out="formattedQuantity"/>
|
||||
<td t-if="props.showOptions.uom" class="text-start" t-out="data.uom_name"/>
|
||||
<td class="text-end" t-if="props.showOptions.availabilities">
|
||||
<t t-if="hasQuantity('quantity_on_hand')">
|
||||
<t t-out="formatFloat(data.quantity_free)"/> /
|
||||
<t t-out="formatFloat(data.quantity_on_hand)"/>
|
||||
</t>
|
||||
</td>
|
||||
<td class="text-end" t-if="props.showOptions.availabilities">
|
||||
<t t-if="hasQuantity('quantity_reserved')" t-out="formatFloat(data.quantity_reserved)"/>
|
||||
</td>
|
||||
<td class="text-end" t-if="props.showOptions.receipts">
|
||||
<t t-if="data.receipt">
|
||||
<span t-attf-class="{{ getColorClass(data.receipt.decorator) }}" t-out="data.receipt.display"/>
|
||||
<a href="#" role="button" t-on-click.prevent="openForecast" title="Forecast Report" t-attf-class="fa fa-fw fa-area-chart ms-1 {{getColorClass(data.receipt.decorator)}}"/>
|
||||
</t>
|
||||
</td>
|
||||
<td t-attf-class="text-end {{ getColorClass(data.mo_cost_decorator) }}" t-if="props.showOptions.moCosts" t-out="formatMonetary(data.mo_cost)"/>
|
||||
<td class="text-end" t-if="props.showOptions.productCosts">
|
||||
<t t-if="hasQuantity('product_cost')" t-out="formatMonetary(data.product_cost)"/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</templates>
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component, useState } from "@odoo/owl";
|
||||
import { useBus } from "@web/core/utils/hooks";
|
||||
import { formatFloatTime, formatMonetary } from "@web/views/fields/formatters";
|
||||
import { MoOverviewLine } from "../mo_overview_line/mrp_mo_overview_line";
|
||||
|
||||
export class MoOverviewOperationsBlock extends Component {
|
||||
static template = "mrp.MoOverviewOperationsBlock";
|
||||
|
||||
setup() {
|
||||
this.formatFloatTime = formatFloatTime;
|
||||
this.state = useState({
|
||||
// Unfold the main MO's operations by default
|
||||
isFolded: this.level > 0 && !this.props.unfoldAll,
|
||||
});
|
||||
if (this.props.unfoldAll) {
|
||||
this.env.overviewBus.trigger("update-folded", { indexes: [this.index], isFolded: false });
|
||||
}
|
||||
|
||||
useBus(this.env.overviewBus, "unfold-all", () => this.unfold());
|
||||
}
|
||||
|
||||
//---- Handlers ----
|
||||
|
||||
toggleFolded() {
|
||||
this.state.isFolded = !this.state.isFolded;
|
||||
this.env.overviewBus.trigger("update-folded", { indexes: [this.index], isFolded: this.state.isFolded });
|
||||
}
|
||||
|
||||
unfold() {
|
||||
this.state.isFolded = false;
|
||||
this.env.overviewBus.trigger("update-folded", { indexes: [this.index], isFolded: false });
|
||||
}
|
||||
|
||||
//---- Helpers ----
|
||||
|
||||
formatMonetary(val) {
|
||||
return formatMonetary(val, { currencyId: this.props.summary.currency_id });
|
||||
}
|
||||
|
||||
getColorClass(decorator) {
|
||||
return decorator ? `text-${decorator}` : "";
|
||||
}
|
||||
|
||||
//---- Getters ----
|
||||
|
||||
get hasOperations() {
|
||||
return this.props?.operations?.length > 0;
|
||||
}
|
||||
|
||||
get level() {
|
||||
return this.hasOperations ? this.props.operations[0].level - 1 : 0;
|
||||
}
|
||||
|
||||
get index() {
|
||||
return this.props.summary.index;
|
||||
}
|
||||
}
|
||||
|
||||
MoOverviewOperationsBlock.components = {
|
||||
MoOverviewLine,
|
||||
};
|
||||
MoOverviewOperationsBlock.props = {
|
||||
unfoldAll: { type: Boolean, optional: true },
|
||||
operations: Array,
|
||||
summary: {
|
||||
type: Object,
|
||||
shape: {
|
||||
index: String,
|
||||
quantity: { type: Number, optional: true },
|
||||
quantity_decorator: { type: [String, Boolean], optional: true },
|
||||
mo_cost: { type: Number, optional: true },
|
||||
mo_cost_decorator: { type: [String, Boolean], optional: true },
|
||||
uom_name: { type: String, optional: true },
|
||||
currency_id: { type: Number, optional: true },
|
||||
currency: { type: String, optional: true },
|
||||
},
|
||||
},
|
||||
showOptions: {
|
||||
type: Object,
|
||||
shape: {
|
||||
uom: Boolean,
|
||||
replenishments: Boolean,
|
||||
availabilities: Boolean,
|
||||
receipts: Boolean,
|
||||
moCosts: Boolean,
|
||||
productCosts: Boolean,
|
||||
},
|
||||
},
|
||||
};
|
||||
MoOverviewOperationsBlock.defaultProps = {
|
||||
unfoldAll: false,
|
||||
};
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mrp.MoOverviewOperationsBlock" owl="1">
|
||||
<t t-if="hasOperations">
|
||||
<tr>
|
||||
<td class="text-start">
|
||||
<span t-attf-style="margin-left: {{ level * 20 }}px"/>
|
||||
<button t-on-click="toggleFolded" class="btn btn-light ps-0 py-0" t-attf-aria-label="{{ state.isFolded ? 'Unfold' : 'Fold' }}" t-attf-title="{{ state.isFolded ? 'Unfold' : 'Fold' }}">
|
||||
<i t-attf-class="fa fa-fw fa-caret-{{ state.isFolded ? 'right' : 'down' }}" role="img"/>
|
||||
Operations
|
||||
</button>
|
||||
</td>
|
||||
<td class="text-center" t-if="props.showOptions.replenishments"/>
|
||||
<td t-attf-class="text-end {{ getColorClass(props.summary.quantity_decorator) }}" t-out="formatFloatTime(props.summary.quantity)"/>
|
||||
<td class="text-start" t-if="props.showOptions.uom" t-out="props.summary.uom_name"/>
|
||||
<td class="text-end" t-if="props.showOptions.availabilities"/>
|
||||
<td class="text-end" t-if="props.showOptions.availabilities"/>
|
||||
<td class="text-end" t-if="props.showOptions.receipts"/>
|
||||
<td t-attf-class="text-end {{ getColorClass(props.summary.mo_cost_decorator) }}" t-if="props.showOptions.moCosts" t-out="formatMonetary(props.summary.mo_cost)"/>
|
||||
<td class="text-end" t-if="props.showOptions.productCosts"/>
|
||||
</tr>
|
||||
<t t-if="!state.isFolded" t-foreach="props.operations" t-as="operation" t-key="operation.index">
|
||||
<MoOverviewLine data="operation" showOptions="props.showOptions"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -2,6 +2,12 @@
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Manufacturing Order -->
|
||||
<record id="action_report_mo_overview" model="ir.actions.client">
|
||||
<field name="name">MO Overview</field>
|
||||
<field name="tag">mrp_mo_overview</field>
|
||||
<field name="context" eval="{'model': 'report.mrp.report_mo_overview'}" />
|
||||
</record>
|
||||
|
||||
<record id="mrp_production_tree_view" model="ir.ui.view">
|
||||
<field name="name">mrp.production.tree</field>
|
||||
<field name="model">mrp.production</field>
|
||||
@@ -181,6 +187,7 @@
|
||||
</button>
|
||||
<button name="%(stock.action_stock_report)d" icon="fa-arrow-up" class="oe_stat_button" string="Traceability" type="action" states="done" groups="stock.group_production_lot"/>
|
||||
<button name="%(action_mrp_production_moves)d" type="action" string="Product Moves" class="oe_stat_button" icon="fa-exchange" attrs="{'invisible': [('state', 'not in', ('progress', 'done'))]}"/>
|
||||
<button name="%(action_report_mo_overview)d" type="action" class="oe_stat_button" icon="fa-bars" string="Overview"/>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<label class="o_form_label" for="name" string="MO Reference"/>
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import mrp_report_bom_structure
|
||||
from . import mrp_report_mo_overview
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
from odoo.tools import float_compare
|
||||
|
||||
class ReportMoOverview(models.AbstractModel):
|
||||
_inherit = 'report.mrp.report_mo_overview'
|
||||
|
||||
def _get_extra_replenishments(self, product):
|
||||
res = super()._get_extra_replenishments(product)
|
||||
domain = [('state', 'in', ['draft', 'sent', 'to approve']), ('product_id', '=', product.id)]
|
||||
warehouse_id = self.env.context.get('warehouse', False)
|
||||
if warehouse_id:
|
||||
domain += [('order_id.picking_type_id.warehouse_id', '=', warehouse_id)]
|
||||
po_lines = self.env['purchase.order.line'].search(domain, order='date_planned, id')
|
||||
|
||||
for po_line in po_lines:
|
||||
line_qty = po_line.product_uom_qty
|
||||
if po_line.move_dest_ids.raw_material_production_id:
|
||||
for move in po_line.move_dest_ids:
|
||||
if float_compare(line_qty, move.product_uom_qty, precision_rounding=po_line.product_uom.rounding) >= 0:
|
||||
prod_qty = move.product_uom_qty
|
||||
else:
|
||||
prod_qty = line_qty
|
||||
res.append(self._format_extra_replenishment(po_line, prod_qty, move.raw_material_production_id.id))
|
||||
line_qty -= prod_qty
|
||||
else:
|
||||
res.append(self._format_extra_replenishment(po_line, po_line.product_uom_qty))
|
||||
|
||||
return res
|
||||
|
||||
def _format_extra_replenishment(self, po_line, quantity, production_id=False):
|
||||
return {
|
||||
'_name': 'purchase.order',
|
||||
'id': po_line.order_id.id,
|
||||
'cost': po_line.price_total,
|
||||
'quantity': quantity,
|
||||
'production_id': production_id
|
||||
}
|
||||
|
||||
def _get_replenishment_receipt(self, doc_in, components):
|
||||
res = super()._get_replenishment_receipt(doc_in, components)
|
||||
if doc_in._name == 'purchase.order':
|
||||
receipt_state = 'expected' if doc_in.state == 'purchase' else 'estimated'
|
||||
return self._format_receipt_date(receipt_state, doc_in.date_planned)
|
||||
return res
|
||||
|
||||
def _get_resupply_data(self, rules, rules_delay, quantity, product, warehouse):
|
||||
res = super()._get_resupply_data(rules, rules_delay, quantity, product, warehouse)
|
||||
if any(rule for rule in rules if rule.action == 'buy' and product.seller_ids):
|
||||
supplier = product._select_seller(quantity=quantity, uom_id=product.uom_id)
|
||||
return {
|
||||
'delay': supplier.delay + rules_delay,
|
||||
'cost': supplier.price * quantity,
|
||||
}
|
||||
return res
|
||||
|
||||
def _get_replenishment_cost(self, product, quantity, currency, move_in=False):
|
||||
if move_in and move_in.purchase_line_id:
|
||||
return currency.round(move_in.purchase_line_id.price_unit * quantity)
|
||||
return super()._get_replenishment_cost(product, quantity, currency, move_in)
|
||||
Reference in New Issue
Block a user