diff options
| author | Simon Lejeune <sle@openerp.com> | 2020-07-15 12:31:51 +0000 |
|---|---|---|
| committer | svs-odoo <svs@odoo.com> | 2020-07-31 12:36:54 +0000 |
| commit | f44da631f49e83533f11a3303b88d00b2864255d (patch) | |
| tree | be46b85c116b20e04972eacb7b56e9e54793484b | |
| parent | ea48c5e75617ba675df0f2332cf1ef7a6c698fe8 (diff) | |
[ADD] stock: forecast widget
| -rw-r--r-- | addons/stock/__manifest__.py | 1 | ||||
| -rw-r--r-- | addons/stock/models/stock_move.py | 67 | ||||
| -rw-r--r-- | addons/stock/report/report_stock_forecasted.py | 6 | ||||
| -rw-r--r-- | addons/stock/static/src/js/forecast_widget.js | 53 | ||||
| -rw-r--r-- | addons/stock/static/src/scss/forecast_widget.scss | 8 | ||||
| -rw-r--r-- | addons/stock/static/src/xml/forecast_widget.xml | 11 | ||||
| -rw-r--r-- | addons/stock/views/stock_picking_views.xml | 6 | ||||
| -rw-r--r-- | addons/stock/views/stock_template.xml | 2 |
8 files changed, 149 insertions, 5 deletions
diff --git a/addons/stock/__manifest__.py b/addons/stock/__manifest__.py index 79fa1c3773a..abc367e2942 100644 --- a/addons/stock/__manifest__.py +++ b/addons/stock/__manifest__.py @@ -83,6 +83,7 @@ 'static/src/xml/inventory_report.xml', 'static/src/xml/inventory_lines.xml', 'static/src/xml/popover_widget.xml', + 'static/src/xml/forecast_widget.xml', 'static/src/xml/report_stock_forecasted.xml', 'static/src/xml/stock_orderpoint.xml', 'static/src/xml/stock_traceability_report_backend.xml', diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index cd9b3d6ebb7..782683e6462 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import json from collections import defaultdict from datetime import datetime from itertools import groupby @@ -12,8 +13,8 @@ from dateutil import relativedelta from odoo import SUPERUSER_ID, _, api, fields, models from odoo.exceptions import UserError -from odoo.osv import expression -from odoo.tools.float_utils import float_compare, float_is_zero, float_round +from odoo.tools.float_utils import float_compare, float_is_zero, float_repr, float_round +from odoo.tools.misc import format_date PROCUREMENT_PRIORITIES = [('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')] @@ -179,6 +180,7 @@ class StockMove(models.Model): next_serial = fields.Char('First SN') next_serial_count = fields.Integer('Number of SN') orderpoint_id = fields.Many2one('stock.warehouse.orderpoint', 'Original Reordering Rule', check_company=True) + json_forecast = fields.Char('JSON data for the forecast widget', compute='_compute_json_forecast') @api.onchange('product_id', 'picking_type_id') def onchange_product(self): @@ -388,6 +390,63 @@ class StockMove(models.Model): total_availability = self.env['stock.quant']._get_available_quantity(move.product_id, move.location_id) if move.product_id else 0.0 move.availability = min(move.product_qty, total_availability) + @api.depends('product_id', 'picking_type_id', 'picking_id', 'reserved_availability') + def _compute_json_forecast(self): + self.json_forecast = False + if not any(self._ids): + # onchange + return + # compute + precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') + consu_moves = self.filtered(lambda move: move.product_id.type == 'consu') + for move in consu_moves: + reserved_availability = float_repr(move.reserved_availability, precision) + move.json_forecast = json.dumps({ + 'reservedAvailability': reserved_availability, + 'hideReportButton': True, + }) + outgoing_unreserved_moves_per_warehouse = defaultdict(lambda: self.env['stock.move']) + for move in (self - consu_moves): + picking_type = move.picking_type_id or move.picking_id.picking_type_id + is_unreserved = float_is_zero(move.reserved_availability, precision_rounding=move.product_uom.rounding) + if picking_type.code in self._consuming_picking_types(): + if is_unreserved: + outgoing_unreserved_moves_per_warehouse[picking_type.warehouse_id] |= move + else: + reserved_availability = float_repr(move.reserved_availability, precision) + move.json_forecast = json.dumps({'reservedAvailability': reserved_availability}) + if not outgoing_unreserved_moves_per_warehouse: + return + + for warehouse, moves in outgoing_unreserved_moves_per_warehouse.items(): + product_variant_ids = moves.product_id.ids + wh_location_ids = [loc['id'] for loc in self.env['stock.location'].search_read( + [('id', 'child_of', warehouse.view_location_id.id)], + ['id'], + )] + forecast_lines = self.env['report.stock.report_product_product_replenishment']\ + ._get_report_lines(None, product_variant_ids, wh_location_ids) + for move in moves: + found = [l for l in forecast_lines if l["move_out"] == move and l["replenishment_filled"] is True] + if found: + move_ins = list(filter(lambda report_line: report_line['move_in'], found)) + # If the move is linked to multiple ingoing moves, take only the last one to have the worst expected date. + if len(move_ins): + found = [move_ins[-1]] + # The move is replenished but there's no expected date -> take from stock + if found[0]["receipt_date_short"] is False: + reserved_availability = float_repr(move.reserved_availability, precision) + move.json_forecast = json.dumps({'reservedAvailability': reserved_availability}) + else: + move.json_forecast = json.dumps({ + 'sortingDate': format_date(self.env, found[0]['move_in'].date_expected, date_format='yyyy-MM-dd'), + 'expectedDate': found[0]["receipt_date_short"], + 'isLate': found[0]["is_late"], + 'replenishmentFilled': found[0]["replenishment_filled"] + }) + else: + move.json_forecast = json.dumps({'expectedDate': None}) + @api.constrains('product_uom') def _check_uom(self): moves_error = self.filtered(lambda move: move.product_id.uom_id.category_id != move.product_uom.category_id) @@ -1535,6 +1594,10 @@ class StockMove(models.Model): else: move.state = 'confirmed' + @api.model + def _consuming_picking_types(self): + return ['outgoing'] + def _get_source_document(self): """ Return the move's document, used by `report.stock.report_product_product_replenishment` and must be overrided to add more document type in the report. diff --git a/addons/stock/report/report_stock_forecasted.py b/addons/stock/report/report_stock_forecasted.py index a670872e6cb..581dd9204be 100644 --- a/addons/stock/report/report_stock_forecasted.py +++ b/addons/stock/report/report_stock_forecasted.py @@ -4,7 +4,7 @@ from collections import defaultdict from odoo import api, models -from odoo.tools import float_is_zero, format_datetime +from odoo.tools import float_is_zero, format_datetime, format_date class ReplenishmentReport(models.AbstractModel): @@ -124,8 +124,12 @@ class ReplenishmentReport(models.AbstractModel): 'uom_id': product.uom_id, 'receipt_date': format_datetime(self.env, move_in.date_expected, timezone, 'medium') if move_in else False, 'delivery_date': format_datetime(self.env, move_out.date_expected, timezone, 'medium') if move_out else False, + 'receipt_date_short': format_date(self.env, move_in.date_expected) if move_in else False, + 'delivery_date_short': format_date(self.env, move_out.date_expected) if move_out else False, 'is_late': is_late, 'quantity': quantity, + 'move_out': move_out, + 'move_in': move_in, } def _get_report_lines(self, product_template_ids, product_variant_ids, wh_location_ids): diff --git a/addons/stock/static/src/js/forecast_widget.js b/addons/stock/static/src/js/forecast_widget.js new file mode 100644 index 00000000000..ae6e39f1d41 --- /dev/null +++ b/addons/stock/static/src/js/forecast_widget.js @@ -0,0 +1,53 @@ +odoo.define('stock.forecast_widget', function (require) { +'use strict'; + +const AbstractField = require('web.AbstractField'); +const fieldRegistry = require('web.field_registry'); +const core = require('web.core'); +const QWeb = core.qweb; + +const ForecastWidgetField = AbstractField.extend({ + supportedFieldTypes: ['char'], + + _render: function () { + const forecastData = JSON.parse(this.value); + if (!forecastData) { + this.$el.html(''); + } else { + this.$el.html(QWeb.render('stock.forecastWidget', forecastData)); + this.$el.on('click', this._onOpenReport.bind(this)); + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Opens the Forecast Report for the `stock.move` product. + * + * @param {MouseEvent} ev + */ + _onOpenReport: function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + const productId = this.recordData.product_id.res_id; + const resModel = 'product.product'; + this._rpc({ + model: resModel, + method: 'action_product_forecast_report', + args: [productId], + }).then(action => { + action.context = { + active_model: resModel, + active_id: productId, + }; + this.do_action(action); + }); + }, +}); + +fieldRegistry.add('forecast_widget', ForecastWidgetField); + +return ForecastWidgetField; +}); diff --git a/addons/stock/static/src/scss/forecast_widget.scss b/addons/stock/static/src/scss/forecast_widget.scss new file mode 100644 index 00000000000..d9167a69e11 --- /dev/null +++ b/addons/stock/static/src/scss/forecast_widget.scss @@ -0,0 +1,8 @@ +.o_forecast_widget_cell { + text-align: right; + padding-right: 24px!important; + + button { + position: absolute; + } +} diff --git a/addons/stock/static/src/xml/forecast_widget.xml b/addons/stock/static/src/xml/forecast_widget.xml new file mode 100644 index 00000000000..2e53943803c --- /dev/null +++ b/addons/stock/static/src/xml/forecast_widget.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<templates id="template" xml:space="preserve"> + <t t-name="stock.forecastWidget"> + <span t-if="reservedAvailability" t-esc="reservedAvailability"/> + <span t-elif="!expectedDate" style="color: red;">Not Available</span> + <span t-else="" t-att-style="'color: ' + (isLate ? 'red;' : 'orange;')">Exp <t t-esc="expectedDate"/></span> + <button t-if="!hideReportButton" class="o_forecast_report_button btn btn-link o_icon_button ml-2" title="Forecasted Report"> + <i class="fa fa-fw fa-area-chart"/> + </button> + </t> +</templates> diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml index b3fc85b1afe..574f6ac0f79 100644 --- a/addons/stock/views/stock_picking_views.xml +++ b/addons/stock/views/stock_picking_views.xml @@ -367,7 +367,8 @@ <field name="is_initial_demand_editable" invisible="1"/> <field name="is_quantity_done_editable" invisible="1"/> <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)]}"/> - <field name="reserved_availability" string="Reserved" attrs="{'column_invisible': (['|','|', ('parent.state','=', 'done'), ('parent.picking_type_code', '=', 'incoming'), ('parent.immediate_transfer', '=', True)])}"/> + <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="json_forecast" string="Reserved" attrs="{'column_invisible': ['|', ('parent.picking_type_code', '!=', 'outgoing'), ('parent.state','=', 'done')]}" widget="forecast_widget"/> <field name="quantity_done" string="Done" attrs="{'readonly': [('is_quantity_done_editable', '=', False)]}"/> <field name="product_uom" attrs="{'readonly': [('state', '!=', 'draft'), ('additional', '=', False)]}" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="uom.group_uom"/> <button name="action_show_details" string="Register lots, packs, location" type="object" icon="fa-list" width="0.1" @@ -391,7 +392,8 @@ <field name="is_initial_demand_editable" invisible="1"/> <field name="is_quantity_done_editable" invisible="1"/> <field name="product_uom_qty" attrs="{'invisible': [('parent.immediate_transfer', '=', True)], 'readonly': [('is_initial_demand_editable', '=', False)]}"/> - <field name="reserved_availability" string="Reserved" attrs="{'invisible': (['|','|', ('parent.state','=', 'done'), ('parent.picking_type_code', '=', 'incoming'), ('parent.immediate_transfer', '=', True)])}"/> + <field name="reserved_availability" string="Reserved" attrs="{'invisible': (['|','|', ('parent.state','=', 'done'), ('parent.picking_type_code', 'in', ['outgoing', 'incoming']), ('parent.immediate_transfer', '=', True)])}"/> + <field name="json_forecast" string="Reserved" attrs="{'column_invisible': ['|', ('parent.picking_type_code', '!=', 'outgoing'), ('parent.state','=', 'done')]}" widget="forecast_widget"/> <field name="quantity_done" string="Done" attrs="{'readonly': [('is_quantity_done_editable', '=', False)]}"/> <field name="product_uom" attrs="{'readonly': [('state', '!=', 'draft'), ('additional', '=', False)]}" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="uom.group_uom"/> <field name="description_picking" string="Description"/> diff --git a/addons/stock/views/stock_template.xml b/addons/stock/views/stock_template.xml index 03d0f8aaeea..2d6515717cb 100644 --- a/addons/stock/views/stock_template.xml +++ b/addons/stock/views/stock_template.xml @@ -16,8 +16,10 @@ <script type="text/javascript" src="/stock/static/src/js/stock_traceability_report_backend.js"></script> <script type="text/javascript" src="/stock/static/src/js/stock_traceability_report_widgets.js"></script> <script type="text/javascript" src="/stock/static/src/js/popover_widget.js"></script> + <script type="text/javascript" src="/stock/static/src/js/forecast_widget.js"></script> <script type="text/javascript" src="/stock/static/src/js/basic_model.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/forecast_widget.scss"/> <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_empty_screen.scss"/> </xpath> |
