[IMP] mrp: MO/WO UI Improvements
Improve the UI for the MO and the WO with some new features, better readability and usability - The forecast for the MO in draft makes the preparation of a MO easier. The behavior of `forecast_availability` depend of the state of the move - The sum of expected duration and real duration in the MO, make the MO visually clearer - The field Scheduled Date is rounding up to the next hour for less manipulation - The automatic reference in a BOM if there is already a BOM for this product allows making less manipulation. I'm using this variable `number_of_bom_of_this_product` with the result of the query inside so I didn't need to do the query twice and the query doesn't take the current bom. - A rounded value is better for the OEE in WO summary - Have a tag for a workcenter can be a good idea to add more information to it. I'm using a random value for the color of the tag, we are doing it frequently to have a random color on multiple think. - Knowing the final product of a WO is clearer - To notify the user when creating a product with a reference that are already existing is a good idea. - Change some color in the MO view for better readability - And more minor tweak (like to align some part in the view) --- Links --- The PR: https://github.com/odoo/odoo/pull/74540 The PR on enterprise: https://github.com/odoo/enterprise/pull/20006 Task related: 2442895 closes odoo/odoo#74540 Related: odoo/enterprise#20006 Signed-off-by: Rémy Voet <ryv-odoo@users.noreply.github.com>
This commit is contained in:
@@ -141,6 +141,15 @@ class MrpBom(models.Model):
|
||||
self.operation_ids.bom_product_template_attribute_value_ids = False
|
||||
self.byproduct_ids.bom_product_template_attribute_value_ids = False
|
||||
|
||||
domain = [('product_tmpl_id', '=', self.product_tmpl_id.id)]
|
||||
if self.id.origin:
|
||||
domain.append(('id', '!=', self.id.origin))
|
||||
number_of_bom_of_this_product = self.env['mrp.bom'].search_count(domain)
|
||||
if number_of_bom_of_this_product: # add a reference to the bom if there is already a bom for this product
|
||||
self.code = _("%s (new) %s", self.product_tmpl_id.name, number_of_bom_of_this_product)
|
||||
else:
|
||||
self.code = False
|
||||
|
||||
def copy(self, default=None):
|
||||
res = super().copy(default)
|
||||
for bom_line in res.bom_line_ids:
|
||||
@@ -446,6 +455,7 @@ class MrpByProduct(models.Model):
|
||||
_description = 'Byproduct'
|
||||
_rec_name = "product_id"
|
||||
_check_company_auto = True
|
||||
_order = 'sequence, id'
|
||||
|
||||
product_id = fields.Many2one('product.product', 'By-product', required=True, check_company=True)
|
||||
company_id = fields.Many2one(related='bom_id.company_id', store=True, index=True, readonly=True)
|
||||
@@ -465,6 +475,7 @@ class MrpByProduct(models.Model):
|
||||
'product.template.attribute.value', string="Apply on Variants", ondelete='restrict',
|
||||
domain="[('id', 'in', possible_bom_product_template_attribute_value_ids)]",
|
||||
help="BOM Product Variants needed to apply this line.")
|
||||
sequence = fields.Integer("Sequence")
|
||||
|
||||
@api.onchange('product_id')
|
||||
def _onchange_product_id(self):
|
||||
|
||||
@@ -65,7 +65,7 @@ class MrpProduction(models.Model):
|
||||
def _get_default_date_planned_start(self):
|
||||
if self.env.context.get('default_date_deadline'):
|
||||
return fields.Datetime.to_datetime(self.env.context.get('default_date_deadline'))
|
||||
return datetime.datetime.now()
|
||||
return fields.Datetime.now().replace(minute=0, second=0, microsecond=0) + datetime.timedelta(hours=1)
|
||||
|
||||
@api.model
|
||||
def _get_default_is_locked(self):
|
||||
@@ -143,6 +143,10 @@ class MrpProduction(models.Model):
|
||||
help="Informative date allowing to define when the manufacturing order should be processed at the latest to fulfill delivery on time.")
|
||||
date_start = fields.Datetime('Start Date', copy=False, readonly=True, help="Date of the WO")
|
||||
date_finished = fields.Datetime('End Date', copy=False, readonly=True, help="Date when the MO has been close")
|
||||
|
||||
production_duration_expected = fields.Float("Expected Duration", help="Total expected duration (in minutes)", compute='_compute_production_duration_expected')
|
||||
production_real_duration = fields.Float("Real Duration", help="Total real duration (in minutes)", compute='_compute_production_real_duration')
|
||||
|
||||
bom_id = fields.Many2one(
|
||||
'mrp.bom', 'Bill of Material',
|
||||
readonly=True, states={'draft': [('readonly', False)]},
|
||||
@@ -308,6 +312,16 @@ class MrpProduction(models.Model):
|
||||
for production in self:
|
||||
production.move_finished_ids.date_deadline = production.date_deadline
|
||||
|
||||
@api.depends('workorder_ids.duration_expected')
|
||||
def _compute_production_duration_expected(self):
|
||||
for production in self:
|
||||
production.production_duration_expected = sum(production.workorder_ids.mapped('duration_expected'))
|
||||
|
||||
@api.depends('workorder_ids.duration')
|
||||
def _compute_production_real_duration(self):
|
||||
for production in self:
|
||||
production.production_real_duration = sum(production.workorder_ids.mapped('duration'))
|
||||
|
||||
@api.depends("workorder_ids.date_planned_start", "workorder_ids.date_planned_finished")
|
||||
def _compute_is_planned(self):
|
||||
for production in self:
|
||||
|
||||
@@ -6,6 +6,7 @@ from datetime import timedelta
|
||||
from functools import partial
|
||||
import datetime
|
||||
from pytz import timezone
|
||||
from random import randint
|
||||
|
||||
from odoo import api, exceptions, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
@@ -71,6 +72,7 @@ class MrpWorkcenter(models.Model):
|
||||
string="Alternative Workcenters", check_company=True,
|
||||
help="Alternative workcenters that can be substituted to this one in order to dispatch production"
|
||||
)
|
||||
tag_ids = fields.Many2many('mrp.workcenter.tag')
|
||||
|
||||
@api.constrains('alternative_workcenter_ids')
|
||||
def _check_alternative_workcenter(self):
|
||||
@@ -264,6 +266,23 @@ class MrpWorkcenter(models.Model):
|
||||
return False, 'Not available slot 700 days after the planned start'
|
||||
|
||||
|
||||
class WorkcenterTag(models.Model):
|
||||
_name = 'mrp.workcenter.tag'
|
||||
_description = 'Add tag for the workcenter'
|
||||
_order = 'name'
|
||||
|
||||
def _get_default_color(self):
|
||||
return randint(1, 11)
|
||||
|
||||
name = fields.Char("Tag Name", required=True)
|
||||
color = fields.Integer("Color Index", default=_get_default_color)
|
||||
|
||||
_sql_constraints = [
|
||||
('tag_name_unique', 'unique(name)',
|
||||
'The tag name must be unique.'),
|
||||
]
|
||||
|
||||
|
||||
class MrpWorkcenterProductivityLossType(models.Model):
|
||||
_name = "mrp.workcenter.productivity.loss.type"
|
||||
_description = 'MRP Workorder productivity losses'
|
||||
|
||||
@@ -63,3 +63,5 @@ access_mrp_consumption_warning,access.mrp.consumption.warning,model_mrp_consumpt
|
||||
access_mrp_consumption_warning_line,access.mrp.consumption.warning.line,model_mrp_consumption_warning_line,mrp.group_mrp_user,1,1,1,0
|
||||
access_mrp_immediate_production,access.mrp.immediate.production,model_mrp_immediate_production,mrp.group_mrp_user,1,1,1,0
|
||||
access_mrp_immediate_production_line,access.mrp.immediate.production.line,model_mrp_immediate_production_line,mrp.group_mrp_user,1,1,1,0
|
||||
access_mrp_workcenter_tag_group_user,access.mrp.workcenter.tag,model_mrp_workcenter_tag,mrp.group_mrp_user,1,0,0,0
|
||||
access_mrp_workcenter_tag_manager,access.mrp.workcenter.tag,model_mrp_workcenter_tag,mrp.group_mrp_manager,1,1,1,1
|
||||
|
||||
|
@@ -116,6 +116,7 @@
|
||||
<tree string="By-products" editable="top">
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="product_uom_category_id" invisible="1"/>
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="product_id" context="{'default_type': 'product'}"/>
|
||||
<field name="product_qty"/>
|
||||
<field name="product_uom_id" groups="uom.group_uom"/>
|
||||
|
||||
@@ -17,13 +17,15 @@
|
||||
<field name="date_planned_start" readonly="1" optional="show" widget="remaining_days"/>
|
||||
<field name="date_deadline" widget="remaining_days" attrs="{'invisible': [('state', 'in', ['done', 'cancel'])]}" optional="hide"/>
|
||||
<field name="product_id" readonly="1" optional="show"/>
|
||||
<field name="product_uom_id" string="Unit of Measure" options="{'no_open':True,'no_create':True}" groups="uom.group_uom" optional="show"/>
|
||||
<field name="production_duration_expected" attrs="{'invisible': [('production_duration_expected', '=', 0)]}" widget="float_time" sum="Total expected duration"/>
|
||||
<field name="production_real_duration" attrs="{'invisible': [('production_real_duration', '=', 0)]}" widget="float_time" sum="Total real duration"/>
|
||||
<field name="lot_producing_id" optional="hide"/>
|
||||
<field name="bom_id" readonly="1" optional="hide"/>
|
||||
<field name="origin" optional="show"/>
|
||||
<field name="user_id" optional="hide" widget="many2one_avatar_user"/>
|
||||
<field name="reservation_state" optional="show"/>
|
||||
<field name="reservation_state" optional="show" decoration-danger="reservation_state == 'confirmed'" decoration-success="reservation_state == 'assigned'"/>
|
||||
<field name="product_qty" sum="Total Qty" string="Quantity" readonly="1" optional="show"/>
|
||||
<field name="product_uom_id" string="UoM" options="{'no_open':True,'no_create':True}" groups="uom.group_uom" optional="show"/>
|
||||
<field name="company_id" readonly="1" groups="base.group_multi_company" optional="show"/>
|
||||
<field name="state" optional="show" widget='badge' decoration-success="state == 'done'" decoration-info="state not in ('done', 'cancel')"/>
|
||||
<field name="activity_exception_decoration" widget="activity_exception"/>
|
||||
@@ -154,7 +156,7 @@
|
||||
<field name="use_create_components_lots" invisible="1"/>
|
||||
<field name="show_lot_ids" invisible="1"/>
|
||||
<field name="product_tracking" invisible="1"/>
|
||||
<field name="product_id" context="{'default_type': 'product'}" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
|
||||
<field name="product_id" context="{'default_type': 'product'}" attrs="{'readonly': [('state', '!=', 'draft')]}" default_focus="1"/>
|
||||
<field name="product_tmpl_id" invisible="1"/>
|
||||
<field name="forecasted_issue" invisible="1"/>
|
||||
<field name="product_description_variants" attrs="{'invisible': [('product_description_variants', 'in', (False, ''))], 'readonly': [('state', '!=', 'draft')]}"/>
|
||||
@@ -276,17 +278,20 @@
|
||||
<field name="state" invisible="1" force_save="1"/>
|
||||
<field name="should_consume_qty" invisible="1"/>
|
||||
<field name="product_uom_qty" widget="mrp_should_consume" string="To Consume" attrs="{'readonly': ['&', ('parent.state', '!=', 'draft'), '|', '&', ('parent.state', 'not in', ('confirmed', 'progress', 'to_close')), ('parent.is_planned', '!=', True), ('parent.is_locked', '=', True)]}" width="1"/>
|
||||
<field name="product_uom" attrs="{'readonly': [('state', '!=', 'draft')]}" options="{'no_open': True, 'no_create': True}" groups="uom.group_uom"/>
|
||||
<field name="product_type" invisible="1"/>
|
||||
<field name="product_qty" invisible="1" readonly="1"/>
|
||||
<field name="reserved_availability" invisible="1"/>
|
||||
<field name="forecast_expected_date" invisible="1"/>
|
||||
<field name="forecast_availability" string="Reserved" attrs="{'column_invisible': [('parent.state', 'in', ['draft', 'done'])]}" widget="forecast_widget"/>
|
||||
<!-- Button are used in state draft to doesn't have the name of the column "Reserved"-->
|
||||
<button type="object" name="action_product_forecast_report" icon="fa-area-chart" attrs="{'column_invisible': [('parent.state', '!=', 'draft')], 'invisible': [('forecast_availability', '<', 0)]}"/>
|
||||
<button type="object" name="action_product_forecast_report" icon="fa-area-chart text-danger" attrs="{'column_invisible': [('parent.state', '!=', 'draft')], 'invisible': [('forecast_availability', '>=', 0)]}"/>
|
||||
<field name="forecast_availability" string="Reserved" attrs="{'column_invisible': [('parent.state', 'in', ('draft', 'done'))]}" widget="forecast_widget"/>
|
||||
<field name="is_quantity_done_editable" invisible="1"/>
|
||||
<field name="quantity_done" string="Consumed"
|
||||
decoration-success="not is_done and (quantity_done - should_consume_qty == 0)"
|
||||
decoration-warning="not is_done and (quantity_done - should_consume_qty > 0.0001)"
|
||||
attrs="{'column_invisible': [('parent.state', '=', 'draft')], 'readonly': [('has_tracking', '!=','none')]}"/>
|
||||
<field name="product_uom" attrs="{'readonly': [('state', '!=', 'draft')]}" options="{'no_open': True, 'no_create': True}" groups="uom.group_uom"/>
|
||||
<field name="show_details_visible" invisible="1"/>
|
||||
<field name="lot_ids" widget="many2many_tags"
|
||||
groups="stock.group_production_lot" optional="hide"
|
||||
|
||||
@@ -36,15 +36,16 @@
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="code"/>
|
||||
<field name="alternative_workcenter_ids" widget="many2many_tags" optional="hide"/>
|
||||
<field name="tag_ids" widget="many2many_tags" options="{'no_create': True, 'color_field': 'color'}" optional="show"/>
|
||||
<field name="alternative_workcenter_ids" widget="many2many_tags" optional="show"/>
|
||||
<field name="productive_time" optional="hide"/>
|
||||
<field name="costs_hour"/>
|
||||
<field name="capacity"/>
|
||||
<field name="time_efficiency" optional="hide"/>
|
||||
<field name="oee_target" optional="hide"/>
|
||||
<field name="time_efficiency" optional="show"/>
|
||||
<field name="oee_target" optional="show"/>
|
||||
<field name="time_start" optional="hide"/>
|
||||
<field name="time_stop" optional="hide"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<field name="company_id" groups="base.group_multi_company" optional="hide"/>
|
||||
<field name="active" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
@@ -223,7 +224,7 @@
|
||||
<div class="col-6 text-right">
|
||||
<span t-att-class="record.oee_target.raw_value and (record.oee.raw_value < record.oee_target.raw_value) and 'text-danger' or (record.oee.raw_value > record.oee_target.raw_value) and 'text-success' or 'text-warning'">
|
||||
<strong>
|
||||
<field name="oee"/>%
|
||||
<field name="oee" widget="integer"/>%
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
@@ -278,7 +279,7 @@
|
||||
<div role="menuitem" aria-haspopup="true" class="col-8">
|
||||
<ul role="menu" class="oe_kanban_colorpicker" data-field="color"/>
|
||||
</div>
|
||||
<div role="menuitem" class="col-4 text-right">
|
||||
<div role="menuitem" class="text-right">
|
||||
<a type="edit">Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -333,6 +334,7 @@
|
||||
<group>
|
||||
<field name="active" invisible="1"/>
|
||||
<field name="name" string="Work Center Name" required="True"/>
|
||||
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
|
||||
<field
|
||||
name="alternative_workcenter_ids"
|
||||
widget="many2many_tags"
|
||||
|
||||
@@ -76,13 +76,14 @@
|
||||
<field name="operation_id" invisible="1" domain="['|', ('bom_id', '=', production_bom_id), ('bom_id', '=', False)]" context="{'default_workcenter_id': workcenter_id, 'default_company_id': company_id}"/>
|
||||
<field name="name" string="Operation"/>
|
||||
<field name="workcenter_id"/>
|
||||
<field name="product_id" optional="show"/>
|
||||
<field name="date_planned_start" optional="show"/>
|
||||
<field name="date_planned_finished" optional="hide"/>
|
||||
<field name="date_start" optional="hide" readonly="1"/>
|
||||
<field name="date_finished" optional="hide" readonly="1"/>
|
||||
<field name="duration_expected" widget="float_time"/>
|
||||
<field name="duration_expected" widget="float_time" sum="expected duration"/>
|
||||
<field name="duration" widget="mrp_time_counter"
|
||||
attrs="{'invisible': [('production_state','=', 'draft')], 'readonly': [('is_user_working', '=', True)]}"/>
|
||||
attrs="{'invisible': [('production_state','=', 'draft')], 'readonly': [('is_user_working', '=', True)]}" sum="real duration"/>
|
||||
<field name="state" widget="badge" decoration-warning="state == 'waiting'" decoration-success="state == 'done'" decoration-info="state not in ('waiting', 'done', 'cancel')"
|
||||
attrs="{'invisible': [('production_state', '=', 'draft')], 'column_invisible': [('parent.state', '=', 'draft')]}"/>
|
||||
<button name="button_start" type="object" string="Start" class="btn-success"
|
||||
@@ -369,19 +370,19 @@
|
||||
<div class="oe_kanban_global_click">
|
||||
<div class="o_kanban_card_header o_kanban_record_top">
|
||||
<div class="o_kanban_record_headings o_kanban_card_header_title">
|
||||
<strong class="o_primary ml8">
|
||||
<strong class="o_primary">
|
||||
<span><t t-esc="record.production_id.value"/></span> - <span><t t-esc="record.name.value"/></span>
|
||||
</strong>
|
||||
</div>
|
||||
<div class="o_kanban_manage_button_section">
|
||||
<h2>
|
||||
<h2 class="ml8">
|
||||
<span t-attf-class="badge #{['pending', 'waiting'].indexOf(record.state.raw_value) > -1 ? 'badge-warning' :['progress'].indexOf(record.state.raw_value) > -1 ? 'badge-secondary' : ['ready'].indexOf(record.state.raw_value) > -1 ? 'badge-primary' : ['done'].indexOf(record.state.raw_value) > -1 ? 'badge-success' : ['cancel'].indexOf(record.state.raw_value) > -1 ? 'badge-muted' : 'badge-danger'}">
|
||||
<t t-esc="record.state.value"/>
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container o_kanban_record_bottom">
|
||||
<div class="o_kanban_record_bottom">
|
||||
<h5 class="oe_kanban_bottom_left">
|
||||
<span><t t-esc="record.product_id.value"/>, </span> <span><t t-esc="record.qty_production.value"/> <t t-esc="record.product_uom_id.value"/></span>
|
||||
</h5>
|
||||
|
||||
@@ -38,8 +38,8 @@
|
||||
colspan="1" type="object" class="btn-primary"/>
|
||||
<button name="action_confirm" string="Confirm" attrs="{'invisible': [('consumption', '=', 'strict')]}" data-hotkey="q"
|
||||
colspan="1" type="object" class="btn-primary"/>
|
||||
<button name="action_cancel" string="Review Consumption" data-hotkey="w"
|
||||
colspan="1" type="object" class="btn-primary"/>
|
||||
<button name="action_cancel" string="Discard" data-hotkey="w"
|
||||
colspan="1" type="object" class="btn-secondary"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
|
||||
@@ -337,6 +337,21 @@ class ProductProduct(models.Model):
|
||||
if self.uom_id and self.uom_po_id and self.uom_id.category_id != self.uom_po_id.category_id:
|
||||
self.uom_po_id = self.uom_id
|
||||
|
||||
@api.onchange('default_code')
|
||||
def _onchange_default_code(self):
|
||||
if not self.default_code:
|
||||
return
|
||||
|
||||
domain = [('default_code', '=', self.default_code)]
|
||||
if self.id.origin:
|
||||
domain.append(('id', '!=', self.id.origin))
|
||||
|
||||
if self.env['product.product'].search(domain, limit=1):
|
||||
return {'warning': {
|
||||
'title': _("Note:"),
|
||||
'message': _("The Internal Reference '%s' already exists.", self.default_code),
|
||||
}}
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
products = super(ProductProduct, self.with_context(create_product_product=True)).create(vals_list)
|
||||
|
||||
@@ -347,6 +347,21 @@ class ProductTemplate(models.Model):
|
||||
for template in self:
|
||||
template.product_variant_count = len(template.product_variant_ids)
|
||||
|
||||
@api.onchange('default_code')
|
||||
def _onchange_default_code(self):
|
||||
if not self.default_code:
|
||||
return
|
||||
|
||||
domain = [('default_code', '=', self.default_code)]
|
||||
if self.id.origin:
|
||||
domain.append(('id', '!=', self.id.origin))
|
||||
|
||||
if self.env['product.template'].search(domain, limit=1):
|
||||
return {'warning': {
|
||||
'title': _("Note:"),
|
||||
'message': _("The Internal Reference '%s' already exists.", self.default_code),
|
||||
}}
|
||||
|
||||
@api.depends('product_variant_ids', 'product_variant_ids.default_code')
|
||||
def _compute_default_code(self):
|
||||
unique_variants = self.filtered(lambda template: len(template.product_variant_ids) == 1)
|
||||
|
||||
@@ -64,7 +64,7 @@ class StockMove(models.Model):
|
||||
"be moved. Lowering this quantity does not generate a "
|
||||
"backorder. Changing this quantity on assigned moves affects "
|
||||
"the product reservation, and should be done with care.")
|
||||
product_uom = fields.Many2one('uom.uom', 'Unit of Measure', required=True, domain="[('category_id', '=', product_uom_category_id)]")
|
||||
product_uom = fields.Many2one('uom.uom', "UoM", required=True, domain="[('category_id', '=', product_uom_category_id)]")
|
||||
product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id')
|
||||
# TDE FIXME: make it stored, otherwise group will not work
|
||||
product_tmpl_id = fields.Many2one(
|
||||
@@ -418,14 +418,19 @@ class StockMove(models.Model):
|
||||
|
||||
outgoing_unreserved_moves_per_warehouse = defaultdict(lambda: self.env['stock.move'])
|
||||
for move in product_moves:
|
||||
is_unreserved = move.state in ('waiting', 'confirmed', 'partially_available')
|
||||
if move.picking_type_id.code in self._consuming_picking_types() and is_unreserved:
|
||||
outgoing_unreserved_moves_per_warehouse[warehouse_by_location[move.location_id]] |= move
|
||||
elif move.picking_type_id.code in self._consuming_picking_types():
|
||||
move.forecast_availability = move.product_uom._compute_quantity(
|
||||
move.reserved_availability, move.product_id.uom_id, rounding_method='HALF-UP')
|
||||
if move.picking_type_id.code in self._consuming_picking_types():
|
||||
if move.state == 'assigned':
|
||||
move.forecast_availability = move.product_uom._compute_quantity(
|
||||
move.reserved_availability, move.product_id.uom_id, rounding_method='HALF-UP')
|
||||
elif move.state == 'draft':
|
||||
# for move _consuming_picking_types and in draft -> the forecast_availability > 0 if in stock
|
||||
warehouse = move.location_id.warehouse_id
|
||||
next_date = max(move.date, fields.Datetime.now())
|
||||
move.forecast_availability = move.product_id.with_context(warehouse=warehouse.id, to_date=next_date).virtual_available - move.product_qty
|
||||
elif move.state in ('waiting', 'confirmed', 'partially_available'):
|
||||
outgoing_unreserved_moves_per_warehouse[warehouse_by_location[move.location_id]] |= move
|
||||
elif move.picking_type_id.code == 'incoming':
|
||||
move._get_forecast_availability_incoming()
|
||||
move.forecast_availability = move._get_forecast_availability_incoming()
|
||||
|
||||
for warehouse, moves in outgoing_unreserved_moves_per_warehouse.items():
|
||||
if not warehouse: # No prediction possible if no warehouse.
|
||||
@@ -909,9 +914,10 @@ class StockMove(models.Model):
|
||||
def _get_forecast_availability_incoming(self):
|
||||
self.ensure_one()
|
||||
warehouse = self.location_dest_id.warehouse_id
|
||||
self.forecast_availability = self.product_id.with_context(warehouse=warehouse.id, to_date=self.date).virtual_available
|
||||
forecast_availability = self.product_id.with_context(warehouse=warehouse.id, to_date=self.date).virtual_available
|
||||
if self.state == 'draft':
|
||||
self.forecast_availability += self.product_uom_qty
|
||||
forecast_availability += self.product_uom_qty
|
||||
return forecast_availability
|
||||
|
||||
@api.onchange('product_id', 'picking_type_id')
|
||||
def _onchange_product_id(self):
|
||||
|
||||
@@ -370,7 +370,7 @@
|
||||
<field name="product_packaging_id" groups="product.group_stock_packaging"/>
|
||||
<field name="product_uom_qty" string="Demand" attrs="{'column_invisible': [('parent.immediate_transfer', '=', True)], 'readonly': ['|', ('is_initial_demand_editable', '=', False), '&', '&', ('show_operations', '=', True), ('is_locked', '=', True), ('is_initial_demand_editable', '=', False)]}"/>
|
||||
<button type="object" name="action_product_forecast_report" icon="fa-area-chart" attrs="{'invisible': ['|', ('forecast_availability', '<', 0), '|', ('parent.immediate_transfer', '=', True), ('parent.picking_type_code', '=', 'outgoing')]}"/>
|
||||
<button type="object" name="action_product_forecast_report" icon="fa-area-chart" attrs="{'invisible': ['|', ('forecast_availability', '>=', 0), '|', ('parent.immediate_transfer', '=', True), ('parent.picking_type_code', '=', 'outgoing')]}" class="text-danger"/>
|
||||
<button type="object" name="action_product_forecast_report" icon="fa-area-chart text-danger" attrs="{'invisible': ['|', ('forecast_availability', '>=', 0), '|', ('parent.immediate_transfer', '=', True), ('parent.picking_type_code', '=', 'outgoing')]}"/>
|
||||
<field name="reserved_availability" string="Reserved" attrs="{'column_invisible': (['|','|', ('parent.state','=', 'done'), ('parent.picking_type_code', 'in', ['incoming', 'outgoing']), ('parent.immediate_transfer', '=', True)])}"/>
|
||||
<field name="product_qty" invisible="1" readonly="1"/>
|
||||
<field name="forecast_expected_date" invisible="1" />
|
||||
|
||||
Reference in New Issue
Block a user