diff --git a/addons/sale_stock/__manifest__.py b/addons/sale_stock/__manifest__.py index 704e3e9d1b2..b57c890e633 100644 --- a/addons/sale_stock/__manifest__.py +++ b/addons/sale_stock/__manifest__.py @@ -46,8 +46,7 @@ Preferences 'auto_install': True, 'assets': { 'web.assets_backend': [ - 'sale_stock/static/src/js/**/*', - 'sale_stock/static/src/xml/**/*', + 'sale_stock/static/src/**/*', ], }, 'license': 'LGPL-3', diff --git a/addons/sale_stock/static/src/js/qty_at_date_widget.js b/addons/sale_stock/static/src/js/qty_at_date_widget.js deleted file mode 100644 index 3199ee344cb..00000000000 --- a/addons/sale_stock/static/src/js/qty_at_date_widget.js +++ /dev/null @@ -1,146 +0,0 @@ -odoo.define('sale_stock.QtyAtDateWidget', function (require) { -"use strict"; - -var core = require('web.core'); -var QWeb = core.qweb; - -var Widget = require('web.Widget'); -var widget_registry = require('web.widget_registry'); -var utils = require('web.utils'); - -var _t = core._t; -var time = require('web.time'); - -var QtyAtDateWidget = Widget.extend({ - template: 'sale_stock.qtyAtDate', - events: _.extend({}, Widget.prototype.events, { - 'click .fa-area-chart': '_onClickButton', - }), - - /** - * @override - * @param {Widget|null} parent - * @param {Object} params - */ - init: function (parent, params) { - this.data = params.data; - this.fields = params.fields; - this._updateData(); - this._super(parent); - }, - - start: function () { - var self = this; - return this._super.apply(this, arguments).then(function () { - self._setPopOver(); - }); - }, - - _updateData: function() { - // add some data to simplify the template - if (this.data.scheduled_date) { - var qty_to_deliver = utils.round_decimals(this.data.qty_to_deliver, this.fields.qty_to_deliver.digits[1]); - if (this.data.state === 'sale') { - this.data.will_be_fulfilled = utils.round_decimals(this.data.free_qty_today, this.fields.free_qty_today.digits[1]) >= qty_to_deliver - } else { - this.data.will_be_fulfilled = utils.round_decimals(this.data.virtual_available_at_date, this.fields.virtual_available_at_date.digits[1]) >= qty_to_deliver - } - this.data.will_be_late = this.data.forecast_expected_date && this.data.forecast_expected_date > this.data.scheduled_date; - if (['draft', 'sent'].includes(this.data.state)){ - // Moves aren't created yet, then the forecasted is only based on virtual_available of quant - this.data.forecasted_issue = !this.data.will_be_fulfilled && !this.data.is_mto; - } else { - // Moves are created, using the forecasted data of related moves - this.data.forecasted_issue = !this.data.will_be_fulfilled || this.data.will_be_late; - } - } - }, - - updateState: function (state) { - this.$el.popover('dispose'); - var candidate = state.data[this.getParent().currentRow]; - if (candidate) { - this.data = candidate.data; - this._updateData(); - this.renderElement(); - this._setPopOver(); - } - }, - /** - * Redirect to the product graph view. - * - * @private - * @param {MouseEvent} event - * @returns {Promise} action loaded - */ - async _openForecast(ev) { - ev.stopPropagation(); - // TODO: in case of kit product, the forecast view should show the kit's components (get_component) - // The forecast_report doesn't not allow for now multiple products - var action = await this._rpc({ - model: 'product.product', - method: 'action_product_forecast_report', - args: [[this.data.product_id.data.id]] - }); - action.context = { - active_model: 'product.product', - active_id: this.data.product_id.data.id, - warehouse: this.data.warehouse_id && this.data.warehouse_id.res_id, - move_to_match_ids: this.data.move_ids.res_ids, - sale_line_to_match_id: this.data.id, - }; - return this.do_action(action); - }, - - _getContent() { - if (!this.data.scheduled_date) { - return; - } - this.data.delivery_date = this.data.scheduled_date.clone().add(this.getSession().getTZOffset(this.data.scheduled_date), 'minutes').format(time.getLangDateFormat()); - if (this.data.forecast_expected_date) { - this.data.forecast_expected_date_str = this.data.forecast_expected_date.clone().add(this.getSession().getTZOffset(this.data.forecast_expected_date), 'minutes').format(time.getLangDateFormat()); - } - const $content = $(QWeb.render('sale_stock.QtyDetailPopOver', { - data: this.data, - })); - $content.on('click', '.action_open_forecast', this._openForecast.bind(this)); - return $content; - }, - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - /** - * Set a bootstrap popover on the current QtyAtDate widget that display available - * quantity. - */ - _setPopOver() { - const $content = this._getContent(); - if (!$content) { - return; - } - const options = { - content: $content, - html: true, - placement: 'left', - title: _t('Availability'), - trigger: 'focus', - delay: {'show': 0, 'hide': 100 }, - }; - this.$el.popover(options); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - _onClickButton: function () { - // We add the property special click on the widget link. - // This hack allows us to trigger the popover (see _setPopOver) without - // triggering the _onRowClicked that opens the order line form view. - this.$el.find('.fa-area-chart').prop('special_click', true); - }, -}); - -widget_registry.add('qty_at_date_widget', QtyAtDateWidget); - -return QtyAtDateWidget; -}); diff --git a/addons/sale_stock/static/src/widgets/qty_at_date_widget.js b/addons/sale_stock/static/src/widgets/qty_at_date_widget.js new file mode 100644 index 00000000000..3e541f9db83 --- /dev/null +++ b/addons/sale_stock/static/src/widgets/qty_at_date_widget.js @@ -0,0 +1,92 @@ +/** @odoo-module **/ + +import { formatDateTime } from "@web/core/l10n/dates"; +import { localization } from "@web/core/l10n/localization"; +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; +import { usePopover } from "@web/core/popover/popover_hook"; + +const { Component, EventBus, onWillRender } = owl; + +export class QtyAtDatePopover extends Component { + setup() { + this.actionService = useService("action"); + } + + openForecast() { + this.actionService.doAction("stock.stock_replenishment_product_product_action", { + additionalContext: { + active_model: 'product.product', + active_id: this.props.record.data.product_id[0], + warehouse: this.props.record.data.warehouse_id && this.props.record.data.warehouse_id[0], + move_to_match_ids: this.props.record.data.move_ids.records.map(record => record.data.id), + sale_line_to_match_id: this.props.record.data.id, + }, + }); + } +} + +QtyAtDatePopover.template = "sale_stock.QtyDetailPopOver"; + +export class QtyAtDateWidget extends Component { + setup() { + this.bus = new EventBus(); + this.popover = usePopover(); + this.closePopover = null; + this.calcData = {}; + onWillRender(() => { + this.initCalcData(); + }) + } + + initCalcData() { + // calculate data not in record + const { data } = this.props.record; + if (data.scheduled_date) { + // TODO: might need some round_decimals to avoid errors + if (data.state === 'sale') { + this.calcData.will_be_fulfilled = data.free_qty_today >= data.qty_to_deliver; + } else { + this.calcData.will_be_fulfilled = data.virtual_available_at_date >= data.qty_to_deliver; + } + this.calcData.will_be_late = data.forecast_expected_date && data.forecast_expected_date > data.scheduled_date; + if (['draft', 'sent'].includes(data.state)) { + // Moves aren't created yet, then the forecasted is only based on virtual_available of quant + this.calcData.forecasted_issue = !this.calcData.will_be_fulfilled && !data.is_mto; + } else { + // Moves are created, using the forecasted data of related moves + this.calcData.forecasted_issue = !this.calcData.will_be_fulfilled || this.calcData.will_be_late; + } + } + } + + updateCalcData() { + // popup specific data + const { data } = this.props.record; + if (!data.scheduled_date) { + return; + } + this.calcData.delivery_date = formatDateTime(data.scheduled_date, { format: localization.dateFormat }); + if (data.forecast_expected_date) { + this.calcData.forecast_expected_date_str = formatDateTime(data.forecast_expected_date, { format: localization.dateFormat }); + } + } + + showPopup(ev) { + this.updateCalcData(); + this.closePopover = this.popover.add( + ev.currentTarget, + this.constructor.components.Popover, + {bus: this.bus, record: this.props.record, calcData: this.calcData}, + { + position: 'top', + } + ); + this.bus.addEventListener('close-popover', this.closePopover); + } +} + +QtyAtDateWidget.components = { Popover: QtyAtDatePopover }; +QtyAtDateWidget.template = "sale_stock.qtyAtDate"; + +registry.category("view_widgets").add("qty_at_date_widget", QtyAtDateWidget); diff --git a/addons/sale_stock/static/src/widgets/qty_at_date_widget.xml b/addons/sale_stock/static/src/widgets/qty_at_date_widget.xml new file mode 100644 index 00000000000..69abc29b801 --- /dev/null +++ b/addons/sale_stock/static/src/widgets/qty_at_date_widget.xml @@ -0,0 +1,71 @@ + + diff --git a/addons/sale_stock/static/src/xml/delay_alert.xml b/addons/sale_stock/static/src/xml/delay_alert.xml new file mode 100644 index 00000000000..ac3e6c9349a --- /dev/null +++ b/addons/sale_stock/static/src/xml/delay_alert.xml @@ -0,0 +1,9 @@ + +
+

The delivery + + , + will be late. +

+
+
diff --git a/addons/sale_stock/static/src/xml/sale_stock.xml b/addons/sale_stock/static/src/xml/sale_stock.xml deleted file mode 100644 index ba28b1b12f8..00000000000 --- a/addons/sale_stock/static/src/xml/sale_stock.xml +++ /dev/null @@ -1,74 +0,0 @@ - -
-
- -
-
- -
- - - - - - - - - - - - - - - - - - -

This product is replenished on demand.

- - - - - - - - - - - - -
Forecasted Stock
On
-
Available
All planned operations included
-
Expected Delivery
- Reserved
-
- -
- - Remaining demand available at - - - No enough future availaibility - - - No future availaibility - - - Available in stock - -
- -
- -
-

The delivery - - , - will be late. -

-
-