[IMP] stock/mrp: add a new alert system in the form view
Add two widgets in the `mrp.production` / `stock.picking` form view. The widget is visible only if one of the stock move linked to the `mrp.production` / `stock.picking` has the flag `delay_alert_date` set. Task #1970595
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
import datetime
|
||||
from collections import defaultdict
|
||||
from itertools import groupby
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
from odoo.tools import date_utils, float_round, float_is_zero
|
||||
from odoo.tools import date_utils, float_round, float_is_zero, format_datetime
|
||||
|
||||
|
||||
class MrpProduction(models.Model):
|
||||
@@ -205,6 +206,8 @@ class MrpProduction(models.Model):
|
||||
'Propagate cancel and split',
|
||||
help='If checked, when the previous move of the move (which was generated by a next procurement) is cancelled or split, the move generated by this move will too')
|
||||
delay_alert = fields.Boolean(string='Delay alert')
|
||||
delay_alert_date = fields.Datetime('Delay Alert Date', compute='_compute_delay_alert_date', search='_search_delay_alert_date')
|
||||
json_popover = fields.Char('JSON data for the popover widget', compute='_compute_json_popover')
|
||||
propagate_date = fields.Boolean(string="Propagate Rescheduling",
|
||||
help='The rescheduling is propagated to the next move.')
|
||||
propagate_date_minimum_delta = fields.Integer(string='Reschedule if Higher Than',
|
||||
@@ -232,6 +235,26 @@ class MrpProduction(models.Model):
|
||||
def _set_date_planned_finished(self):
|
||||
self.move_finished_ids.write({'date_expected': self.date_planned_finished})
|
||||
|
||||
@api.depends('move_raw_ids.delay_alert_date')
|
||||
def _compute_delay_alert_date(self):
|
||||
delay_alert_date_data = self.env['stock.move'].read_group([('id', 'in', self.move_raw_ids.ids), ('delay_alert_date', '!=', False)], ['delay_alert_date:max'], 'raw_material_production_id')
|
||||
delay_alert_date_data = {data['raw_material_production_id'][0]: data['delay_alert_date'] for data in delay_alert_date_data}
|
||||
for production in self:
|
||||
production.delay_alert_date = delay_alert_date_data.get(production.id, False)
|
||||
|
||||
def _compute_json_popover(self):
|
||||
for production in self:
|
||||
production.json_popover = json.dumps({
|
||||
'popoverTemplate': 'stock.PopoverStockRescheduling',
|
||||
'delay_alert_date': format_datetime(self.env, production.delay_alert_date, dt_format=False) if production.delay_alert_date else False,
|
||||
'late_elements': [{
|
||||
'id': late_document.id,
|
||||
'name': late_document.display_name,
|
||||
'model': late_document._name,
|
||||
} for late_document in production.move_raw_ids.filtered(lambda m: m.delay_alert_date).move_orig_ids._delay_alert_get_documents()
|
||||
]
|
||||
})
|
||||
|
||||
@api.depends('move_raw_ids.state', 'move_finished_ids.state')
|
||||
def _compute_confirm_cancel(self):
|
||||
""" If the manufacturing order contains some done move (via an intermediate
|
||||
@@ -403,6 +426,11 @@ class MrpProduction(models.Model):
|
||||
('qty_positive', 'check (product_qty > 0)', 'The quantity to produce must be positive!'),
|
||||
]
|
||||
|
||||
@api.model
|
||||
def _search_delay_alert_date(self, operator, value):
|
||||
late_stock_moves = self.env['stock.move'].search([('delay_alert_date', operator, value)])
|
||||
return ['|', ('move_raw_ids', 'in', late_stock_moves.ids), ('move_finished_ids', 'in', late_stock_moves.ids)]
|
||||
|
||||
@api.onchange('company_id')
|
||||
def onchange_company_id(self):
|
||||
if self.company_id:
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
<field name="state" optional="show"/>
|
||||
<field name="company_id" readonly="1" groups="base.group_multi_company" optional="show"/>
|
||||
<field name="activity_exception_decoration" widget="activity_exception"/>
|
||||
<field name="delay_alert_date" invisible="1"/>
|
||||
<field string=" " name="json_popover" widget="stock_rescheduling_popover" attrs="{'invisible': [('delay_alert_date', '=', False)]}"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
@@ -157,6 +159,8 @@
|
||||
('routing_id', '!=', False),
|
||||
('state', 'in', ['draft', 'confirmed'])
|
||||
]}"/>
|
||||
<field name="delay_alert_date" invisible="1"/>
|
||||
<field string=" " name="json_popover" widget="stock_rescheduling_popover" attrs="{'invisible': [('delay_alert_date', '=', False)]}"/>
|
||||
</div>
|
||||
<field name="user_id"/>
|
||||
<field name="origin"/>
|
||||
@@ -234,6 +238,8 @@
|
||||
<div class="oe_kanban_bottom_left text-muted">
|
||||
<span><t t-esc="record.name.value"/> <t t-esc="record.date_deadline.value and record.date_deadline.value.split(' ')[0] or False"/></span>
|
||||
<field name="activity_ids" widget="kanban_activity"/>
|
||||
<field name="delay_alert_date" invisible="1"/>
|
||||
<field string=" " name="json_popover" widget="stock_rescheduling_popover" attrs="{'invisible': [('delay_alert_date', '=', False)]}"/>
|
||||
</div>
|
||||
<div class="oe_kanban_bottom_right">
|
||||
<span t-attf-class="badge #{['draft', 'cancel'].indexOf(record.state.raw_value) > -1 ? 'badge-secondary' : ['none'].indexOf(record.state.raw_value) > -1 ? 'badge-danger' : ['confirmed'].indexOf(record.state.raw_value) > -1 ? 'badge-warning' : ['done'].indexOf(record.state.raw_value) > -1 ? 'badge-success' : 'badge-primary'}"><t t-esc="record.state.value"/></span>
|
||||
@@ -317,6 +323,7 @@
|
||||
<separator/>
|
||||
<filter string="Late" name="late" help="Production started late"
|
||||
domain="['&', ('date_deadline', '<', current_date), ('state', '=', 'confirmed')]"/>
|
||||
<filter string="Planning Issues" name="delay_alert_date" domain="[('delay_alert_date', '!=', False)]"/>
|
||||
<separator/>
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
|
||||
@@ -36,7 +36,7 @@ Preferences
|
||||
'wizard/sale_order_cancel_views.xml',
|
||||
],
|
||||
'demo': ['data/sale_order_demo.xml'],
|
||||
'qweb': ['static/src/xml/qty_at_date.xml'],
|
||||
'qweb': ['static/src/xml/sale_stock.xml'],
|
||||
'installable': True,
|
||||
'auto_install': True,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from collections import defaultdict
|
||||
|
||||
@@ -40,6 +41,8 @@ class SaleOrder(models.Model):
|
||||
"the order lines in case of Service products. In case of shipping, the shipping policy of "
|
||||
"the order will be taken into account to either use the minimum or maximum lead time of "
|
||||
"the order lines.")
|
||||
json_popover = fields.Char('JSON data for the popover widget', compute='_compute_json_popover')
|
||||
show_json_popover = fields.Boolean('Has late picking', compute='_compute_json_popover')
|
||||
|
||||
@api.depends('picking_ids.date_done')
|
||||
def _compute_effective_date(self):
|
||||
@@ -88,6 +91,20 @@ class SaleOrder(models.Model):
|
||||
order._log_decrease_ordered_quantity(documents)
|
||||
return res
|
||||
|
||||
def _compute_json_popover(self):
|
||||
for order in self:
|
||||
late_stock_picking = order.picking_ids.filtered(lambda p: p.delay_alert_date)
|
||||
order.json_popover = json.dumps({
|
||||
'popoverTemplate': 'sale_stock.DelayAlertWidget',
|
||||
'late_elements': [{
|
||||
'id': late_move.id,
|
||||
'name': late_move.display_name,
|
||||
'model': 'stock.picking',
|
||||
} for late_move in late_stock_picking
|
||||
]
|
||||
})
|
||||
order.show_json_popover = bool(late_stock_picking)
|
||||
|
||||
def _action_confirm(self):
|
||||
self.order_line._action_launch_stock_rule()
|
||||
return super(SaleOrder, self)._action_confirm()
|
||||
|
||||
+8
@@ -42,4 +42,12 @@
|
||||
View Forecast
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div t-name="sale_stock.DelayAlertWidget">
|
||||
<p>The deliveries
|
||||
<t t-foreach="late_elements" t-as="late_element">
|
||||
<a t-esc="late_element.name" href="#" t-att-element-id="late_element.id" t-att-element-model="model"/>,
|
||||
</t> will be late.
|
||||
</p>
|
||||
</div>
|
||||
</templates>
|
||||
@@ -25,6 +25,10 @@
|
||||
<field name="incoterm" widget="selection" groups="sale_stock.group_display_incoterm"/>
|
||||
<field name="picking_policy" required="True"/>
|
||||
</xpath>
|
||||
<xpath expr="//page[@name='other_information']//field[@name='expected_date']" position="after">
|
||||
<field name="show_json_popover" invisible="1"/>
|
||||
<field string=" " name="json_popover" widget="stock_rescheduling_popover" attrs="{'invisible': [('show_json_popover', '=', False)]}"/>
|
||||
</xpath>
|
||||
<xpath expr="//group[@name='sale_shipping']" position="inside">
|
||||
<field name="effective_date" attrs="{'invisible': [('effective_date', '=', False)]}"/>
|
||||
</xpath>
|
||||
|
||||
@@ -6,10 +6,11 @@ from datetime import date
|
||||
from itertools import groupby
|
||||
from operator import itemgetter
|
||||
import time
|
||||
import json
|
||||
|
||||
from odoo import api, fields, models, SUPERUSER_ID, _
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
|
||||
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, format_datetime
|
||||
from odoo.tools.float_utils import float_compare, float_is_zero, float_round
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.addons.stock.models.stock_move import PROCUREMENT_PRIORITIES
|
||||
@@ -294,6 +295,8 @@ class Picking(models.Model):
|
||||
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
|
||||
help="Creation Date, usually the time of the order")
|
||||
date_done = fields.Datetime('Date of Transfer', copy=False, readonly=True, help="Date at which the transfer has been processed or cancelled.")
|
||||
delay_alert_date = fields.Datetime('Delay Alert Date', compute='_compute_delay_alert_date', search='_search_delay_alert_date')
|
||||
json_popover = fields.Char('JSON data for the popover widget', compute='_compute_json_popover')
|
||||
location_id = fields.Many2one(
|
||||
'stock.location', "Source Location",
|
||||
default=lambda self: self.env['stock.picking.type'].browse(self._context.get('default_picking_type_id')).default_location_src_id,
|
||||
@@ -377,6 +380,13 @@ class Picking(models.Model):
|
||||
for picking in self:
|
||||
picking.has_tracking = any(m.has_tracking != 'none' for m in picking.move_lines)
|
||||
|
||||
@api.depends('move_lines.delay_alert_date')
|
||||
def _compute_delay_alert_date(self):
|
||||
delay_alert_date_data = self.env['stock.move'].read_group([('id', 'in', self.move_lines.ids), ('delay_alert_date', '!=', False)], ['delay_alert_date:max'], 'picking_id')
|
||||
delay_alert_date_data = {data['picking_id'][0]: data['delay_alert_date'] for data in delay_alert_date_data}
|
||||
for picking in self:
|
||||
picking.delay_alert_date = delay_alert_date_data.get(picking.id, False)
|
||||
|
||||
@api.depends('picking_type_id.show_operations')
|
||||
def _compute_show_operations(self):
|
||||
for picking in self:
|
||||
@@ -403,6 +413,19 @@ class Picking(models.Model):
|
||||
else:
|
||||
picking.show_lots_text = False
|
||||
|
||||
def _compute_json_popover(self):
|
||||
for picking in self:
|
||||
picking.json_popover = json.dumps({
|
||||
'popoverTemplate': 'stock.PopoverStockRescheduling',
|
||||
'delay_alert_date': format_datetime(self.env, picking.delay_alert_date, dt_format=False) if picking.delay_alert_date else False,
|
||||
'late_elements': [{
|
||||
'id': late_move.id,
|
||||
'name': late_move.display_name,
|
||||
'model': late_move._name,
|
||||
} for late_move in picking.move_lines.filtered(lambda m: m.delay_alert_date).move_orig_ids._delay_alert_get_documents()
|
||||
]
|
||||
})
|
||||
|
||||
@api.depends('move_type', 'move_lines.state', 'move_lines.picking_id')
|
||||
def _compute_state(self):
|
||||
''' State of a picking depends on the state of its related stock.move
|
||||
@@ -510,6 +533,11 @@ class Picking(models.Model):
|
||||
else:
|
||||
picking.show_validate = True
|
||||
|
||||
@api.model
|
||||
def _search_delay_alert_date(self, operator, value):
|
||||
late_stock_moves = self.env['stock.move'].search([('delay_alert_date', operator, value)])
|
||||
return [('move_lines', 'in', late_stock_moves.ids)]
|
||||
|
||||
@api.onchange('picking_type_id', 'partner_id')
|
||||
def onchange_picking_type(self):
|
||||
if self.picking_type_id:
|
||||
@@ -1265,3 +1293,4 @@ class Picking(models.Model):
|
||||
body=message,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
odoo.define('stock.PopoverStockPicking', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
|
||||
var PopoverWidgetField = require('stock.popover_widget');
|
||||
var registry = require('web.field_registry');
|
||||
var _t = core._t;
|
||||
|
||||
var PopoverStockPicking = PopoverWidgetField.extend({
|
||||
title: _t('Planning Issue'),
|
||||
color: 'text-warning',
|
||||
trigger: 'focus',
|
||||
|
||||
_render: function () {
|
||||
this._super();
|
||||
if (this.$popover) {
|
||||
var self = this;
|
||||
this.$popover.find('a').on('click', function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
self.do_action({
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: ev.currentTarget.getAttribute('element-model'),
|
||||
res_id: parseInt(ev.currentTarget.getAttribute('element-id'), 10),
|
||||
views: [[false, 'form']],
|
||||
target: 'current'
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
registry.add('stock_rescheduling_popover', PopoverStockPicking);
|
||||
|
||||
return PopoverStockPicking;
|
||||
});
|
||||
@@ -9,4 +9,11 @@
|
||||
<t t-esc="msg"/>
|
||||
</div>
|
||||
|
||||
<div t-name="stock.PopoverStockRescheduling">
|
||||
<p>Preceding operations
|
||||
<t t-foreach="late_elements" t-as="late_element">
|
||||
<a t-esc="late_element.name" href="#" t-att-element-id="late_element.id" t-att-element-model="late_element.model"/>,
|
||||
</t>
|
||||
planned on <t t-esc="delay_alert_date"/>.</p>
|
||||
</div>
|
||||
</templates>
|
||||
|
||||
@@ -171,6 +171,8 @@
|
||||
<div class="oe_kanban_bottom_left">
|
||||
<t t-esc="record.partner_id.value"/>
|
||||
<field name="activity_ids" widget="kanban_activity"/>
|
||||
<field name="delay_alert_date" invisible="1"/>
|
||||
<field name="json_popover" string=" " widget="stock_rescheduling_popover" attrs="{'invisible': [('delay_alert_date', '=', False)]}"/>
|
||||
</div>
|
||||
<div class="oe_kanban_bottom_right">
|
||||
<t t-esc="record.date.value and record.date.value.split(' ')[0] or False"/>
|
||||
@@ -206,6 +208,8 @@
|
||||
<field name="picking_type_id" optional="hide"/>
|
||||
<field name="company_id" groups="base.group_multi_company" optional="show"/>
|
||||
<field name="activity_exception_decoration" widget="activity_exception"/>
|
||||
<field name="delay_alert_date" invisible="1"/>
|
||||
<field name="json_popover" string=" " widget="stock_rescheduling_popover" attrs="{'invisible': [('delay_alert_date', '=', False)]}"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
@@ -294,7 +298,12 @@
|
||||
<field name="backorder_id" attrs="{'invisible': [('backorder_id','=',False)]}"/>
|
||||
</group>
|
||||
<group>
|
||||
<label for="scheduled_date"/>
|
||||
<div class="o_row">
|
||||
<field name="scheduled_date" attrs="{'required': [('id', '!=', False)]}"/>
|
||||
<field name="delay_alert_date" invisible="1"/>
|
||||
<field name="json_popover" widget="stock_rescheduling_popover" attrs="{'invisible': [('delay_alert_date', '=', False)]}"/>
|
||||
</div>
|
||||
<field name="date_done" string="Effective Date" attrs="{'invisible': [('state', '!=', 'done')]}"/>
|
||||
<field name="origin" placeholder="e.g. PO0032"/>
|
||||
<field name="owner_id" groups="stock.group_tracking_owner"
|
||||
@@ -433,6 +442,7 @@
|
||||
<filter name="cancel" string="Cancelled" domain="[('state', '=', 'cancel')]" help="Cancelled Moves"/>
|
||||
<separator/>
|
||||
<filter name="late" string="Late" domain="[('scheduled_date','<', time.strftime('%%Y-%%m-%%d %%H:%%M:%%S')), ('state', 'in', ('assigned', 'waiting', 'confirmed'))]" help="Transfers that are late on scheduled time"/>
|
||||
<filter string="Planning Issues" name="delay_alert_date" domain="[('delay_alert_date', '!=', False)]"/>
|
||||
<separator/>
|
||||
<filter name="backorder" string="Backorders" domain="[('backorder_id', '!=', False), ('state', 'in', ('assigned', 'waiting', 'confirmed'))]" help="Remaining parts of picking partially processed"/>
|
||||
<separator/>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<script type="text/javascript" src="/stock/static/src/js/stock_traceability_report_widgets.js"></script>
|
||||
<script type="text/javascript" src="/stock/static/src/js/stock_location_hierarchy.js"></script>
|
||||
<script type="text/javascript" src="/stock/static/src/js/popover_widget.js"></script>
|
||||
<script type="text/javascript" src="/stock/static/src/js/stock_rescheduling_popover.js"></script>
|
||||
<link rel="stylesheet" type="text/scss" href="/stock/static/src/scss/stock_traceability_report.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/stock/static/src/scss/stock_location_hierarchy.scss"/>
|
||||
</xpath>
|
||||
|
||||
Reference in New Issue
Block a user