summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimon Lejeune <sle@openerp.com>2020-07-15 12:31:51 +0000
committersvs-odoo <svs@odoo.com>2020-07-31 12:36:54 +0000
commitf44da631f49e83533f11a3303b88d00b2864255d (patch)
treebe46b85c116b20e04972eacb7b56e9e54793484b
parentea48c5e75617ba675df0f2332cf1ef7a6c698fe8 (diff)
[ADD] stock: forecast widget
-rw-r--r--addons/stock/__manifest__.py1
-rw-r--r--addons/stock/models/stock_move.py67
-rw-r--r--addons/stock/report/report_stock_forecasted.py6
-rw-r--r--addons/stock/static/src/js/forecast_widget.js53
-rw-r--r--addons/stock/static/src/scss/forecast_widget.scss8
-rw-r--r--addons/stock/static/src/xml/forecast_widget.xml11
-rw-r--r--addons/stock/views/stock_picking_views.xml6
-rw-r--r--addons/stock/views/stock_template.xml2
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), '&amp;', '&amp;', ('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>