summaryrefslogtreecommitdiff
path: root/addons/stock/static/src
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/stock/static/src
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/stock/static/src')
-rw-r--r--addons/stock/static/src/img/barcode.gifbin0 -> 30013 bytes
-rw-r--r--addons/stock/static/src/js/basic_model.js16
-rw-r--r--addons/stock/static/src/js/forecast_widget.js76
-rw-r--r--addons/stock/static/src/js/inventory_report_list_controller.js66
-rw-r--r--addons/stock/static/src/js/inventory_report_list_view.js19
-rw-r--r--addons/stock/static/src/js/inventory_singleton_list_controller.js68
-rw-r--r--addons/stock/static/src/js/inventory_singleton_list_view.js18
-rw-r--r--addons/stock/static/src/js/inventory_validate_button_controller.js89
-rw-r--r--addons/stock/static/src/js/inventory_validate_button_view.js16
-rw-r--r--addons/stock/static/src/js/popover_widget.js84
-rw-r--r--addons/stock/static/src/js/report_stock_forecasted.js268
-rw-r--r--addons/stock/static/src/js/stock_orderpoint_list_controller.js74
-rw-r--r--addons/stock/static/src/js/stock_orderpoint_list_model.js46
-rw-r--r--addons/stock/static/src/js/stock_orderpoint_list_view.js21
-rw-r--r--addons/stock/static/src/js/stock_rescheduling_popover.js39
-rw-r--r--addons/stock/static/src/js/stock_traceability_report_backend.js106
-rw-r--r--addons/stock/static/src/js/stock_traceability_report_widgets.js131
-rw-r--r--addons/stock/static/src/scss/forecast_widget.scss8
-rw-r--r--addons/stock/static/src/scss/report_stock_forecasted.scss11
-rw-r--r--addons/stock/static/src/scss/report_stock_rule.scss122
-rw-r--r--addons/stock/static/src/scss/stock_empty_screen.scss16
-rw-r--r--addons/stock/static/src/scss/stock_traceability_report.scss83
-rw-r--r--addons/stock/static/src/xml/forecast_widget.xml12
-rw-r--r--addons/stock/static/src/xml/inventory_lines.xml8
-rw-r--r--addons/stock/static/src/xml/inventory_report.xml8
-rw-r--r--addons/stock/static/src/xml/popover_widget.xml19
-rw-r--r--addons/stock/static/src/xml/report_stock_forecasted.xml27
-rw-r--r--addons/stock/static/src/xml/stock_orderpoint.xml50
-rw-r--r--addons/stock/static/src/xml/stock_traceability_report_backend.xml23
-rw-r--r--addons/stock/static/src/xml/stock_traceability_report_line.xml57
30 files changed, 1581 insertions, 0 deletions
diff --git a/addons/stock/static/src/img/barcode.gif b/addons/stock/static/src/img/barcode.gif
new file mode 100644
index 00000000..ef828b3b
--- /dev/null
+++ b/addons/stock/static/src/img/barcode.gif
Binary files differ
diff --git a/addons/stock/static/src/js/basic_model.js b/addons/stock/static/src/js/basic_model.js
new file mode 100644
index 00000000..6a5653c6
--- /dev/null
+++ b/addons/stock/static/src/js/basic_model.js
@@ -0,0 +1,16 @@
+odoo.define('stock.BasicModel', function (require) {
+"use strict";
+
+var BasicModel = require('web.BasicModel');
+var localStorage = require('web.local_storage');
+
+BasicModel.include({
+
+ _invalidateCache: function (dataPoint) {
+ this._super.apply(this, arguments);
+ if (dataPoint.model === 'stock.warehouse' && !localStorage.getItem('running_tour')) {
+ this.do_action('reload_context');
+ }
+ }
+});
+});
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 00000000..b1c4dc31
--- /dev/null
+++ b/addons/stock/static/src/js/forecast_widget.js
@@ -0,0 +1,76 @@
+odoo.define('stock.forecast_widget', function (require) {
+'use strict';
+
+const AbstractField = require('web.AbstractField');
+const fieldRegistry = require('web.field_registry');
+const field_utils = require('web.field_utils');
+const utils = require('web.utils');
+const core = require('web.core');
+const QWeb = core.qweb;
+
+const ForecastWidgetField = AbstractField.extend({
+ supportedFieldTypes: ['float'],
+
+ _render: function () {
+ var data = Object.assign({}, this.record.data, {
+ forecast_availability_str: field_utils.format.float(
+ this.record.data.forecast_availability,
+ this.record.fields.forecast_availability,
+ this.nodeOptions
+ ),
+ reserved_availability_str: field_utils.format.float(
+ this.record.data.reserved_availability,
+ this.record.fields.reserved_availability,
+ this.nodeOptions
+ ),
+ forecast_expected_date_str: field_utils.format.date(
+ this.record.data.forecast_expected_date,
+ this.record.fields.forecast_expected_date
+ ),
+ });
+ if (data.forecast_expected_date && data.date_deadline) {
+ data.forecast_is_late = data.forecast_expected_date > data.date_deadline;
+ }
+ data.will_be_fulfilled = utils.round_decimals(data.forecast_availability, this.record.fields.forecast_availability.digits[1]) >= utils.round_decimals(data.product_qty, this.record.fields.forecast_availability.digits[1]);
+
+ this.$el.html(QWeb.render('stock.forecastWidget', data));
+ this.$('.o_forecast_report_button').on('click', this._onOpenReport.bind(this));
+ },
+
+ isSet: function () {
+ return true;
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Opens the Forecast Report for the `stock.move` product.
+ *
+ * @param {MouseEvent} ev
+ */
+ _onOpenReport: function (ev) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ if (!this.recordData.id) {
+ return;
+ }
+ this._rpc({
+ model: 'stock.move',
+ method: 'action_product_forecast_report',
+ args: [this.recordData.id],
+ }).then(action => {
+ action.context = Object.assign(action.context || {}, {
+ active_model: 'product.product',
+ active_id: this.recordData.product_id.res_id,
+ });
+ this.do_action(action);
+ });
+ },
+});
+
+fieldRegistry.add('forecast_widget', ForecastWidgetField);
+
+return ForecastWidgetField;
+});
diff --git a/addons/stock/static/src/js/inventory_report_list_controller.js b/addons/stock/static/src/js/inventory_report_list_controller.js
new file mode 100644
index 00000000..eb6a3ed6
--- /dev/null
+++ b/addons/stock/static/src/js/inventory_report_list_controller.js
@@ -0,0 +1,66 @@
+odoo.define('stock.InventoryReportListController', function (require) {
+"use strict";
+
+var core = require('web.core');
+var ListController = require('web.ListController');
+
+var qweb = core.qweb;
+
+
+var InventoryReportListController = ListController.extend({
+
+ // -------------------------------------------------------------------------
+ // Public
+ // -------------------------------------------------------------------------
+
+ init: function (parent, model, renderer, params) {
+ this.context = renderer.state.getContext();
+ return this._super.apply(this, arguments);
+ },
+
+ /**
+ * @override
+ */
+ renderButtons: function ($node) {
+ this._super.apply(this, arguments);
+ if (this.context.no_at_date) {
+ return;
+ }
+ var $buttonToDate = $(qweb.render('InventoryReport.Buttons'));
+ $buttonToDate.on('click', this._onOpenWizard.bind(this));
+ this.$buttons.prepend($buttonToDate);
+ },
+
+ // -------------------------------------------------------------------------
+ // Handlers
+ // -------------------------------------------------------------------------
+
+ /**
+ * Handler called when the user clicked on the 'Inventory at Date' button.
+ * Opens wizard to display, at choice, the products inventory or a computed
+ * inventory at a given date.
+ */
+ _onOpenWizard: function () {
+ var state = this.model.get(this.handle, {raw: true});
+ var stateContext = state.getContext();
+ var context = {
+ active_model: this.modelName,
+ };
+ if (stateContext.default_product_id) {
+ context.product_id = stateContext.default_product_id;
+ } else if (stateContext.product_tmpl_id) {
+ context.product_tmpl_id = stateContext.product_tmpl_id;
+ }
+ this.do_action({
+ res_model: 'stock.quantity.history',
+ views: [[false, 'form']],
+ target: 'new',
+ type: 'ir.actions.act_window',
+ context: context,
+ });
+ },
+});
+
+return InventoryReportListController;
+
+});
diff --git a/addons/stock/static/src/js/inventory_report_list_view.js b/addons/stock/static/src/js/inventory_report_list_view.js
new file mode 100644
index 00000000..494dd838
--- /dev/null
+++ b/addons/stock/static/src/js/inventory_report_list_view.js
@@ -0,0 +1,19 @@
+odoo.define('stock.InventoryReportListView', function (require) {
+"use strict";
+
+var ListView = require('web.ListView');
+var InventoryReportListController = require('stock.InventoryReportListController');
+var viewRegistry = require('web.view_registry');
+
+
+var InventoryReportListView = ListView.extend({
+ config: _.extend({}, ListView.prototype.config, {
+ Controller: InventoryReportListController,
+ }),
+});
+
+viewRegistry.add('inventory_report_list', InventoryReportListView);
+
+return InventoryReportListView;
+
+});
diff --git a/addons/stock/static/src/js/inventory_singleton_list_controller.js b/addons/stock/static/src/js/inventory_singleton_list_controller.js
new file mode 100644
index 00000000..9e5bd4db
--- /dev/null
+++ b/addons/stock/static/src/js/inventory_singleton_list_controller.js
@@ -0,0 +1,68 @@
+odoo.define('stock.SingletonListController', function (require) {
+"use strict";
+
+var core = require('web.core');
+var InventoryReportListController = require('stock.InventoryReportListController');
+
+var _t = core._t;
+
+/**
+ * The purpose of this override is to avoid to have two or more similar records
+ * in the list view.
+ *
+ * It's used in quant list view, a list editable where when you create a new
+ * line about a quant who already exists, we want to update the existing one
+ * instead of create a new one, and then we don't want to have two similar line
+ * in the list view, so we refresh it.
+ */
+
+var SingletonListController = InventoryReportListController.extend({
+ /**
+ * @override
+ * @return {Promise} rejected when update the list because we don't want
+ * anymore to select a cell who maybe doesn't exist anymore.
+ */
+ _confirmSave: function (id) {
+ var newRecord = this.model.localData[id];
+ var model = newRecord.model;
+ var res_id = newRecord.res_id;
+
+ var findSimilarRecords = function (record) {
+ if ((record.groupedBy && record.groupedBy.length > 0) || record.data.length) {
+ var recordsToReturn = [];
+ for (var i in record.data) {
+ var foundRecords = findSimilarRecords(record.data[i]);
+ recordsToReturn = recordsToReturn.concat(foundRecords || []);
+ }
+ return recordsToReturn;
+ } else {
+ if (record.res_id === res_id && record.model === model) {
+ if (record.count === 0){
+ return [record];
+ }
+ else if (record.ref && record.ref.indexOf('virtual') !== -1) {
+ return [record];
+ }
+ }
+ }
+ };
+
+ var handle = this.model.get(this.handle);
+ var similarRecords = findSimilarRecords(handle);
+
+ if (similarRecords.length > 1) {
+ var notification = _t("You tried to create a record who already exists."+
+ "<br/>This last one has been modified instead.");
+ this.do_notify(_t("This record already exists."), notification);
+ this.reload();
+ return Promise.reject();
+ }
+ else {
+ return this._super.apply(this, arguments);
+ }
+ },
+});
+
+return SingletonListController;
+
+});
diff --git a/addons/stock/static/src/js/inventory_singleton_list_view.js b/addons/stock/static/src/js/inventory_singleton_list_view.js
new file mode 100644
index 00000000..53faf5b8
--- /dev/null
+++ b/addons/stock/static/src/js/inventory_singleton_list_view.js
@@ -0,0 +1,18 @@
+odoo.define('stock.SingletonListView', function (require) {
+'use strict';
+
+var InventoryReportListView = require('stock.InventoryReportListView');
+var SingletonListController = require('stock.SingletonListController');
+var viewRegistry = require('web.view_registry');
+
+var SingletonListView = InventoryReportListView.extend({
+ config: _.extend({}, InventoryReportListView.prototype.config, {
+ Controller: SingletonListController,
+ }),
+});
+
+viewRegistry.add('singleton_list', SingletonListView);
+
+return SingletonListView;
+
+});
diff --git a/addons/stock/static/src/js/inventory_validate_button_controller.js b/addons/stock/static/src/js/inventory_validate_button_controller.js
new file mode 100644
index 00000000..cd554bd1
--- /dev/null
+++ b/addons/stock/static/src/js/inventory_validate_button_controller.js
@@ -0,0 +1,89 @@
+odoo.define('stock.InventoryValidationController', function (require) {
+"use strict";
+
+var core = require('web.core');
+var ListController = require('web.ListController');
+
+var _t = core._t;
+var qweb = core.qweb;
+
+var InventoryValidationController = ListController.extend({
+ events: _.extend({
+ 'click .o_button_validate_inventory': '_onValidateInventory'
+ }, ListController.prototype.events),
+ /**
+ * @override
+ */
+ init: function (parent, model, renderer, params) {
+ var context = renderer.state.getContext();
+ this.inventory_id = context.active_id;
+ return this._super.apply(this, arguments);
+ },
+
+ // -------------------------------------------------------------------------
+ // Public
+ // -------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ renderButtons: function () {
+ this._super.apply(this, arguments);
+ var $validationButton = $(qweb.render('InventoryLines.Buttons'));
+ this.$buttons.prepend($validationButton);
+ },
+
+ // -------------------------------------------------------------------------
+ // Handlers
+ // -------------------------------------------------------------------------
+
+ /**
+ * Handler called when user click on validation button in inventory lines
+ * view. Makes an rpc to try to validate the inventory, then will go back on
+ * the inventory view form if it was validated.
+ * This method could also open a wizard in case something was missing.
+ *
+ * @private
+ */
+ _onValidateInventory: function () {
+ var self = this;
+ var prom = Promise.resolve();
+ var recordID = this.renderer.getEditableRecordID();
+ if (recordID) {
+ // If user's editing a record, we wait to save it before to try to
+ // validate the inventory.
+ prom = this.saveRecord(recordID);
+ }
+
+ prom.then(function () {
+ self._rpc({
+ model: 'stock.inventory',
+ method: 'action_validate',
+ args: [self.inventory_id]
+ }).then(function (res) {
+ var exitCallback = function (infos) {
+ // In case we discarded a wizard, we do nothing to stay on
+ // the same view...
+ if (infos && infos.special) {
+ return;
+ }
+ // ... but in any other cases, we go back on the inventory form.
+ self.do_notify(
+ false,
+ _t("The inventory has been validated"));
+ self.trigger_up('history_back');
+ };
+
+ if (_.isObject(res)) {
+ self.do_action(res, { on_close: exitCallback });
+ } else {
+ return exitCallback();
+ }
+ });
+ });
+ },
+});
+
+return InventoryValidationController;
+
+});
diff --git a/addons/stock/static/src/js/inventory_validate_button_view.js b/addons/stock/static/src/js/inventory_validate_button_view.js
new file mode 100644
index 00000000..ed5a5f42
--- /dev/null
+++ b/addons/stock/static/src/js/inventory_validate_button_view.js
@@ -0,0 +1,16 @@
+odoo.define('stock.InventoryValidationView', function (require) {
+"use strict";
+
+var InventoryValidationController = require('stock.InventoryValidationController');
+var ListView = require('web.ListView');
+var viewRegistry = require('web.view_registry');
+
+var InventoryValidationView = ListView.extend({
+ config: _.extend({}, ListView.prototype.config, {
+ Controller: InventoryValidationController
+ })
+});
+
+viewRegistry.add('inventory_validate_button', InventoryValidationView);
+
+});
diff --git a/addons/stock/static/src/js/popover_widget.js b/addons/stock/static/src/js/popover_widget.js
new file mode 100644
index 00000000..567dd494
--- /dev/null
+++ b/addons/stock/static/src/js/popover_widget.js
@@ -0,0 +1,84 @@
+odoo.define('stock.popover_widget', function (require) {
+'use strict';
+
+var AbstractField = require('web.AbstractField');
+var core = require('web.core');
+var QWeb = core.qweb;
+var Context = require('web.Context');
+var data_manager = require('web.data_manager');
+var fieldRegistry = require('web.field_registry');
+
+/**
+ * Widget Popover for JSON field (char), by default render a simple html message
+ * {
+ * 'msg': '<CONTENT OF THE POPOVER>',
+ * 'icon': '<FONT AWESOME CLASS>' (optionnal),
+ * 'color': '<COLOR CLASS OF ICON>' (optionnal),
+ * 'title': '<TITLE OF POPOVER>' (optionnal),
+ * 'popoverTemplate': '<TEMPLATE OF THE TEMPLATE>' (optionnal)
+ * }
+ */
+var PopoverWidgetField = AbstractField.extend({
+ supportedFieldTypes: ['char'],
+ buttonTemplape: 'stock.popoverButton',
+ popoverTemplate: 'stock.popoverContent',
+ trigger: 'focus',
+ placement: 'top',
+ html: true,
+ color: 'text-primary',
+ icon: 'fa-info-circle',
+
+ _render: function () {
+ var value = JSON.parse(this.value);
+ if (!value) {
+ this.$el.html('');
+ return;
+ }
+ this.$el.css('max-width', '17px');
+ this.$el.html(QWeb.render(this.buttonTemplape, _.defaults(value, {color: this.color, icon: this.icon})));
+ this.$el.find('a').prop('special_click', true);
+ this.$popover = $(QWeb.render(value.popoverTemplate || this.popoverTemplate, value));
+ this.$popover.on('click', '.action_open_forecast', this._openForecast.bind(this));
+ this.$el.find('a').popover({
+ content: this.$popover,
+ html: this.html,
+ placement: this.placement,
+ title: value.title || this.title.toString(),
+ trigger: this.trigger,
+ delay: {'show': 0, 'hide': 100},
+ });
+ },
+
+ /**
+ * Redirect to the product forecasted report.
+ *
+ * @private
+ * @param {MouseEvent} event
+ * @returns {Promise} action loaded
+ */
+ async _openForecast(ev) {
+ ev.stopPropagation();
+ const reportContext = {
+ active_model: 'product.product',
+ active_id: this.recordData.product_id.data.id,
+ };
+ const action = await this._rpc({
+ model: reportContext.active_model,
+ method: 'action_product_forecast_report',
+ args: [[reportContext.active_id]],
+ });
+ action.context = new Context(action.context, reportContext);
+ return this.do_action(action);
+ },
+
+ destroy: function () {
+ this.$el.find('a').popover('dispose');
+ this._super.apply(this, arguments);
+ },
+
+});
+
+fieldRegistry.add('popover_widget', PopoverWidgetField);
+
+return PopoverWidgetField;
+});
diff --git a/addons/stock/static/src/js/report_stock_forecasted.js b/addons/stock/static/src/js/report_stock_forecasted.js
new file mode 100644
index 00000000..27121e8e
--- /dev/null
+++ b/addons/stock/static/src/js/report_stock_forecasted.js
@@ -0,0 +1,268 @@
+odoo.define('stock.ReplenishReport', function (require) {
+"use strict";
+
+const clientAction = require('report.client_action');
+const core = require('web.core');
+const dom = require('web.dom');
+const GraphView = require('web.GraphView');
+
+const qweb = core.qweb;
+const _t = core._t;
+
+
+const ReplenishReport = clientAction.extend({
+ /**
+ * @override
+ */
+ init: function (parent, action, options) {
+ this._super.apply(this, arguments);
+ this.context = action.context;
+ this.productId = this.context.active_id;
+ this.resModel = this.context.active_model || this.context.params.active_model || 'product.template';
+ const isTemplate = this.resModel === 'product.template';
+ this.actionMethod = `action_product_${isTemplate ? 'tmpl_' : ''}forecast_report`;
+ const reportName = `report_product_${isTemplate ? 'template' : 'product'}_replenishment`;
+ this.report_url = `/report/html/stock.${reportName}/${this.productId}`;
+ if (this.context.warehouse) {
+ this.active_warehouse = {id: this.context.warehouse};
+ }
+ this.report_url += `?context=${JSON.stringify(this.context)}&force_context_lang=1`;
+ this._title = action.name;
+ },
+
+ /**
+ * @override
+ */
+ start: function () {
+ return Promise.all([
+ this._super(...arguments),
+ this._renderWarehouseFilters(),
+ ]).then(() => {
+ this._renderButtons();
+ });
+ },
+
+ /**
+ * @override
+ */
+ on_attach_callback: function () {
+ this._super();
+ this._createGraphView();
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Instanciates a chart graph and moves it into the report (which is in the iframe).
+ */
+ _createGraphView: async function () {
+ let viewController;
+ const appendGraph = () => {
+ promController.then(() => {
+ this.iframe.removeEventListener('load', appendGraph);
+ const $reportGraphDiv = $(this.iframe).contents().find('.o_report_graph');
+ dom.append(this.$el, viewController.$el, {
+ in_DOM: true,
+ callbacks: [{widget: viewController}],
+ });
+ const renderer = viewController.renderer;
+ // Remove the graph control panel.
+ $('.o_control_panel:last').remove();
+ const $graphPanel = $('.o_graph_controller');
+ $graphPanel.appendTo($reportGraphDiv);
+
+ if (!renderer.state.dataPoints.length) {
+ // Changes the "No Data" helper message.
+ const graphHelper = renderer.$('.o_view_nocontent');
+ const newMessage = qweb.render('View.NoContentHelper', {
+ description: _t("Try to add some incoming or outgoing transfers."),
+ });
+ graphHelper.replaceWith(newMessage);
+ } else {
+ this.chart = renderer.chart;
+ // Lame hack to fix the size of the graph.
+ setTimeout(() => {
+ this.chart.canvas.height = 300;
+ this.chart.canvas.style.height = "300px";
+ this.chart.resize();
+ }, 1);
+ }
+ });
+ };
+ // Wait the iframe fo append the graph chart and move it into the iframe.
+ this.iframe.addEventListener('load', appendGraph);
+
+ const model = 'report.stock.quantity';
+ const promController = this._rpc({
+ model: model,
+ method: 'fields_view_get',
+ kwargs: {
+ view_type: 'graph',
+ }
+ }).then(viewInfo => {
+ const params = {
+ modelName: model,
+ domain: this._getReportDomain(),
+ hasActionMenus: false,
+ };
+ const graphView = new GraphView(viewInfo, params);
+ return graphView.getController(this);
+ }).then(res => {
+ viewController = res;
+
+ // Hack to put the res_model on the url. This way, the report always know on with res_model it refers.
+ if (location.href.indexOf('active_model') === -1) {
+ const url = window.location.href + `&active_model=${this.resModel}`;
+ window.history.pushState({}, "", url);
+ }
+ const fragment = document.createDocumentFragment();
+ return viewController.appendTo(fragment);
+ });
+ },
+
+ /**
+ * Return the action to open this report.
+ *
+ * @returns {Promise}
+ */
+ _getForecastedReportAction: function () {
+ return this._rpc({
+ model: this.resModel,
+ method: this.actionMethod,
+ args: [this.productId],
+ context: this.context,
+ });
+ },
+
+ /**
+ * Returns a domain to filter on the product variant or product template
+ * depending of the active model.
+ *
+ * @returns {Array}
+ */
+ _getReportDomain: function () {
+ const domain = [
+ ['state', '=', 'forecast'],
+ ['warehouse_id', '=', this.active_warehouse.id],
+ ];
+ if (this.resModel === 'product.template') {
+ domain.push(['product_tmpl_id', '=', this.productId]);
+ } else if (this.resModel === 'product.product') {
+ domain.push(['product_id', '=', this.productId]);
+ }
+ return domain;
+ },
+
+ /**
+ * TODO
+ *
+ * @param {Object} additionnalContext
+ */
+ _reloadReport: function (additionnalContext) {
+ return this._getForecastedReportAction().then((action) => {
+ action.context = Object.assign({
+ active_id: this.productId,
+ active_model: this.resModel,
+ }, this.context, additionnalContext);
+ return this.do_action(action, {replace_last_action: true});
+ });
+ },
+
+ /**
+ * Renders the 'Replenish' button and replaces the default 'Print' button by this new one.
+ */
+ _renderButtons: function () {
+ const $newButtons = $(qweb.render('replenish_report_buttons', {}));
+ this.$buttons.find('.o_report_print').replaceWith($newButtons);
+ this.$buttons.on('click', '.o_report_replenish_buy', this._onClickReplenish.bind(this));
+ this.controlPanelProps.cp_content = {
+ $buttons: this.$buttons,
+ };
+ },
+
+ /**
+ * TODO
+ * @returns {Promise}
+ */
+ _renderWarehouseFilters: function () {
+ return this._rpc({
+ model: 'report.stock.report_product_product_replenishment',
+ method: 'get_filter_state',
+ }).then((res) => {
+ const warehouses = res.warehouses;
+ const active_warehouse = (this.active_warehouse && this.active_warehouse.id) || res.active_warehouse;
+ if (active_warehouse) {
+ this.active_warehouse = _.findWhere(warehouses, {id: active_warehouse});
+ } else {
+ this.active_warehouse = warehouses[0];
+ }
+ const $filters = $(qweb.render('warehouseFilter', {
+ active_warehouse: this.active_warehouse,
+ warehouses: warehouses,
+ displayWarehouseFilter: (warehouses.length > 1),
+ }));
+ // Bind handlers.
+ $filters.on('click', '.warehouse_filter', this._onClickFilter.bind(this));
+ this.$('.o_search_options').append($filters);
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Opens the product replenish wizard. Could re-open the report if pending
+ * forecasted quantities need to be updated.
+ *
+ * @returns {Promise}
+ */
+ _onClickReplenish: function () {
+ const context = Object.assign({}, this.context);
+ if (this.resModel === 'product.product') {
+ context.default_product_id = this.productId;
+ } else if (this.resModel === 'product.template') {
+ context.default_product_tmpl_id = this.productId;
+ }
+ context.default_warehouse_id = this.active_warehouse.id;
+
+ const on_close = function (res) {
+ if (res && res.special) {
+ // Do nothing when the wizard is discarded.
+ return;
+ }
+ // Otherwise, opens again the report.
+ return this._reloadReport();
+ };
+
+ const action = {
+ res_model: 'product.replenish',
+ name: _t('Product Replenish'),
+ type: 'ir.actions.act_window',
+ views: [[false, 'form']],
+ target: 'new',
+ context: context,
+ };
+
+ return this.do_action(action, {
+ on_close: on_close.bind(this),
+ });
+ },
+
+ /**
+ * Re-opens the report with data for the specified warehouse.
+ *
+ * @returns {Promise}
+ */
+ _onClickFilter: function (ev) {
+ const data = ev.target.dataset;
+ const warehouse_id = Number(data.warehouseId);
+ return this._reloadReport({warehouse: warehouse_id});
+ }
+});
+
+core.action_registry.add('replenish_report', ReplenishReport);
+
+}); \ No newline at end of file
diff --git a/addons/stock/static/src/js/stock_orderpoint_list_controller.js b/addons/stock/static/src/js/stock_orderpoint_list_controller.js
new file mode 100644
index 00000000..4ad07508
--- /dev/null
+++ b/addons/stock/static/src/js/stock_orderpoint_list_controller.js
@@ -0,0 +1,74 @@
+odoo.define('stock.StockOrderpointListController', function (require) {
+"use strict";
+
+var core = require('web.core');
+var ListController = require('web.ListController');
+
+var qweb = core.qweb;
+
+
+var StockOrderpointListController = ListController.extend({
+
+ // -------------------------------------------------------------------------
+ // Public
+ // -------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ renderButtons: function () {
+ this._super.apply(this, arguments);
+ this.$buttons.find('.o_button_import').addClass('d-none');
+ this.$buttons.find('.o_list_export_xlsx').addClass('d-none');
+ this.$buttons.find('.o_list_button_add').removeClass('btn-primary').addClass('btn-secondary');
+ var $buttons = $(qweb.render('StockOrderpoint.Buttons'));
+ var $buttonOrder = $buttons.find('.o_button_order');
+ var $buttonSnooze = $buttons.find('.o_button_snooze');
+ $buttonOrder.on('click', this._onReplenish.bind(this));
+ $buttonSnooze.on('click', this._onSnooze.bind(this));
+ $buttons.prependTo(this.$buttons);
+ },
+
+ // -------------------------------------------------------------------------
+ // Handlers
+ // -------------------------------------------------------------------------
+
+ _onButtonClicked: function (ev) {
+ if (ev.data.attrs.class.split(' ').includes('o_replenish_buttons')) {
+ ev.stopPropagation();
+ var self = this;
+ this._callButtonAction(ev.data.attrs, ev.data.record).then(function () {
+ self.reload();
+ });
+ } else {
+ this._super.apply(this, arguments);
+ }
+ },
+
+ _onReplenish: function () {
+ var records = this.getSelectedRecords();
+ this.model.replenish(records);
+ },
+
+ _onSelectionChanged: function (ev) {
+ this._super(ev);
+ var $buttonOrder = this.$el.find('.o_button_order');
+ var $buttonSnooze = this.$el.find('.o_button_snooze');
+ if (this.getSelectedIds().length === 0){
+ $buttonOrder.addClass('d-none');
+ $buttonSnooze.addClass('d-none');
+ } else {
+ $buttonOrder.removeClass('d-none');
+ $buttonSnooze.removeClass('d-none');
+ }
+ },
+
+ _onSnooze: function () {
+ var records = this.getSelectedRecords();
+ this.model.snooze(records);
+ },
+});
+
+return StockOrderpointListController;
+
+});
diff --git a/addons/stock/static/src/js/stock_orderpoint_list_model.js b/addons/stock/static/src/js/stock_orderpoint_list_model.js
new file mode 100644
index 00000000..c9f0acb9
--- /dev/null
+++ b/addons/stock/static/src/js/stock_orderpoint_list_model.js
@@ -0,0 +1,46 @@
+odoo.define('stock.StockOrderpointListModel', function (require) {
+"use strict";
+
+var core = require('web.core');
+var ListModel = require('web.ListModel');
+
+var qweb = core.qweb;
+
+
+var StockOrderpointListModel = ListModel.extend({
+
+ // -------------------------------------------------------------------------
+ // Public
+ // -------------------------------------------------------------------------
+ /**
+ */
+ replenish: function (records) {
+ var self = this;
+ var model = records[0].model;
+ var recordResIds = _.pluck(records, 'res_id');
+ var context = records[0].getContext();
+ return this._rpc({
+ model: model,
+ method: 'action_replenish',
+ args: [recordResIds],
+ context: context,
+ }).then(function () {
+ return self.do_action('stock.action_replenishment');
+ });
+ },
+
+ snooze: function (records) {
+ var recordResIds = _.pluck(records, 'res_id');
+ var self = this;
+ return this.do_action('stock.action_orderpoint_snooze', {
+ additional_context: {
+ default_orderpoint_ids: recordResIds
+ },
+ on_close: () => self.do_action('stock.action_replenishment')
+ });
+ },
+});
+
+return StockOrderpointListModel;
+
+});
diff --git a/addons/stock/static/src/js/stock_orderpoint_list_view.js b/addons/stock/static/src/js/stock_orderpoint_list_view.js
new file mode 100644
index 00000000..d893ba9e
--- /dev/null
+++ b/addons/stock/static/src/js/stock_orderpoint_list_view.js
@@ -0,0 +1,21 @@
+odoo.define('stock.StockOrderpointListView', function (require) {
+"use strict";
+
+var ListView = require('web.ListView');
+var StockOrderpointListController = require('stock.StockOrderpointListController');
+var StockOrderpointListModel = require('stock.StockOrderpointListModel');
+var viewRegistry = require('web.view_registry');
+
+
+var StockOrderpointListView = ListView.extend({
+ config: _.extend({}, ListView.prototype.config, {
+ Controller: StockOrderpointListController,
+ Model: StockOrderpointListModel,
+ }),
+});
+
+viewRegistry.add('stock_orderpoint_list', StockOrderpointListView);
+
+return StockOrderpointListView;
+
+});
diff --git a/addons/stock/static/src/js/stock_rescheduling_popover.js b/addons/stock/static/src/js/stock_rescheduling_popover.js
new file mode 100644
index 00000000..c3959642
--- /dev/null
+++ b/addons/stock/static/src/js/stock_rescheduling_popover.js
@@ -0,0 +1,39 @@
+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 _lt = core._lt;
+
+var PopoverStockPicking = PopoverWidgetField.extend({
+ title: _lt('Planning Issue'),
+ trigger: 'focus',
+ color: 'text-danger',
+ icon: 'fa-exclamation-triangle',
+
+ _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;
+});
diff --git a/addons/stock/static/src/js/stock_traceability_report_backend.js b/addons/stock/static/src/js/stock_traceability_report_backend.js
new file mode 100644
index 00000000..6a504cd2
--- /dev/null
+++ b/addons/stock/static/src/js/stock_traceability_report_backend.js
@@ -0,0 +1,106 @@
+odoo.define('stock.stock_report_generic', function (require) {
+'use strict';
+
+var AbstractAction = require('web.AbstractAction');
+var core = require('web.core');
+var session = require('web.session');
+var ReportWidget = require('stock.ReportWidget');
+var framework = require('web.framework');
+
+var QWeb = core.qweb;
+
+var stock_report_generic = AbstractAction.extend({
+ hasControlPanel: true,
+
+ // Stores all the parameters of the action.
+ init: function(parent, action) {
+ this._super.apply(this, arguments);
+ this.actionManager = parent;
+ this.given_context = Object.assign({}, session.user_context);
+ this.controller_url = action.context.url;
+ if (action.context.context) {
+ this.given_context = action.context.context;
+ }
+ this.given_context.active_id = action.context.active_id || action.params.active_id;
+ this.given_context.model = action.context.active_model || false;
+ this.given_context.ttype = action.context.ttype || false;
+ this.given_context.auto_unfold = action.context.auto_unfold || false;
+ this.given_context.lot_name = action.context.lot_name || false;
+ },
+ willStart: function() {
+ return Promise.all([this._super.apply(this, arguments), this.get_html()]);
+ },
+ set_html: function() {
+ var self = this;
+ var def = Promise.resolve();
+ if (!this.report_widget) {
+ this.report_widget = new ReportWidget(this, this.given_context);
+ def = this.report_widget.appendTo(this.$('.o_content'));
+ }
+ return def.then(function () {
+ self.report_widget.$el.html(self.html);
+ self.report_widget.$el.find('.o_report_heading').html('<h1>Traceability Report</h1>');
+ if (self.given_context.auto_unfold) {
+ _.each(self.$el.find('.fa-caret-right'), function (line) {
+ self.report_widget.autounfold(line, self.given_context.lot_name);
+ });
+ }
+ });
+ },
+ start: async function() {
+ this.controlPanelProps.cp_content = { $buttons: this.$buttons };
+ await this._super(...arguments);
+ this.set_html();
+ },
+ // Fetches the html and is previous report.context if any, else create it
+ get_html: async function() {
+ const { html } = await this._rpc({
+ args: [this.given_context],
+ method: 'get_html',
+ model: 'stock.traceability.report',
+ });
+ this.html = html;
+ this.renderButtons();
+ },
+ // Updates the control panel and render the elements that have yet to be rendered
+ update_cp: function() {
+ if (!this.$buttons) {
+ this.renderButtons();
+ }
+ this.controlPanelProps.cp_content = { $buttons: this.$buttons };
+ return this.updateControlPanel();
+ },
+ renderButtons: function() {
+ var self = this;
+ this.$buttons = $(QWeb.render("stockReports.buttons", {}));
+ // pdf output
+ this.$buttons.bind('click', function () {
+ var $element = $(self.$el[0]).find('.o_stock_reports_table tbody tr');
+ var dict = [];
+
+ $element.each(function( index ) {
+ var $el = $($element[index]);
+ dict.push({
+ 'id': $el.data('id'),
+ 'model_id': $el.data('model_id'),
+ 'model_name': $el.data('model'),
+ 'unfoldable': $el.data('unfold'),
+ 'level': $el.find('td:first').data('level') || 1
+ });
+ });
+ framework.blockUI();
+ var url_data = self.controller_url.replace('active_id', self.given_context.active_id);
+ session.get_file({
+ url: url_data.replace('output_format', 'pdf'),
+ data: {data: JSON.stringify(dict)},
+ complete: framework.unblockUI,
+ error: (error) => self.call('crash_manager', 'rpc_error', error),
+ });
+ });
+ return this.$buttons;
+ },
+});
+
+core.action_registry.add("stock_report_generic", stock_report_generic);
+return stock_report_generic;
+});
diff --git a/addons/stock/static/src/js/stock_traceability_report_widgets.js b/addons/stock/static/src/js/stock_traceability_report_widgets.js
new file mode 100644
index 00000000..97468a48
--- /dev/null
+++ b/addons/stock/static/src/js/stock_traceability_report_widgets.js
@@ -0,0 +1,131 @@
+odoo.define('stock.ReportWidget', function (require) {
+'use strict';
+
+var core = require('web.core');
+var Widget = require('web.Widget');
+
+var QWeb = core.qweb;
+
+var _t = core._t;
+
+var ReportWidget = Widget.extend({
+ events: {
+ 'click span.o_stock_reports_foldable': 'fold',
+ 'click span.o_stock_reports_unfoldable': 'unfold',
+ 'click .o_stock_reports_web_action': 'boundLink',
+ 'click .o_stock_reports_stream': 'updownStream',
+ 'click .o_stock_report_lot_action': 'actionOpenLot'
+ },
+ init: function(parent) {
+ this._super.apply(this, arguments);
+ },
+ start: function() {
+ QWeb.add_template("/stock/static/src/xml/stock_traceability_report_line.xml");
+ return this._super.apply(this, arguments);
+ },
+ boundLink: function(e) {
+ e.preventDefault();
+ return this.do_action({
+ type: 'ir.actions.act_window',
+ res_model: $(e.target).data('res-model'),
+ res_id: $(e.target).data('active-id'),
+ views: [[false, 'form']],
+ target: 'current'
+ });
+ },
+ actionOpenLot: function(e) {
+ e.preventDefault();
+ var $el = $(e.target).parents('tr');
+ this.do_action({
+ type: 'ir.actions.client',
+ tag: 'stock_report_generic',
+ name: $el.data('lot_name') !== undefined && $el.data('lot_name').toString(),
+ context: {
+ active_id : $el.data('lot_id'),
+ active_model : 'stock.production.lot',
+ url: '/stock/output_format/stock/active_id'
+ },
+ });
+ },
+ updownStream: function(e) {
+ var $el = $(e.target).parents('tr');
+ this.do_action({
+ type: "ir.actions.client",
+ tag: 'stock_report_generic',
+ name: _t("Traceability Report"),
+ context: {
+ active_id : $el.data('model_id'),
+ active_model : $el.data('model'),
+ auto_unfold: true,
+ lot_name: $el.data('lot_name') !== undefined && $el.data('lot_name').toString(),
+ url: '/stock/output_format/stock/active_id'
+ },
+ });
+ },
+ removeLine: function(element) {
+ var self = this;
+ var el, $el;
+ var rec_id = element.data('id');
+ var $stockEl = element.nextAll('tr[data-parent_id=' + rec_id + ']')
+ for (el in $stockEl) {
+ $el = $($stockEl[el]).find(".o_stock_reports_domain_line_0, .o_stock_reports_domain_line_1");
+ if ($el.length === 0) {
+ break;
+ }
+ else {
+ var $nextEls = $($el[0]).parents("tr");
+ self.removeLine($nextEls);
+ $nextEls.remove();
+ }
+ $el.remove();
+ }
+ return true;
+ },
+ fold: function(e) {
+ this.removeLine($(e.target).parents('tr'));
+ var active_id = $(e.target).parents('tr').find('td.o_stock_reports_foldable').data('id');
+ $(e.target).parents('tr').find('td.o_stock_reports_foldable').attr('class', 'o_stock_reports_unfoldable ' + active_id); // Change the class, rendering, and remove line from model
+ $(e.target).parents('tr').find('span.o_stock_reports_foldable').replaceWith(QWeb.render("unfoldable", {lineId: active_id}));
+ $(e.target).parents('tr').toggleClass('o_stock_reports_unfolded');
+ },
+ autounfold: function(target, lot_name) {
+ var self = this;
+ var $CurretElement;
+ $CurretElement = $(target).parents('tr').find('td.o_stock_reports_unfoldable');
+ var active_id = $CurretElement.data('id');
+ var active_model_name = $CurretElement.data('model');
+ var active_model_id = $CurretElement.data('model_id');
+ var row_level = $CurretElement.data('level');
+ var $cursor = $(target).parents('tr');
+ this._rpc({
+ model: 'stock.traceability.report',
+ method: 'get_lines',
+ args: [parseInt(active_id, 10)],
+ kwargs: {
+ 'model_id': active_model_id,
+ 'model_name': active_model_name,
+ 'level': parseInt(row_level) + 30 || 1
+ },
+ })
+ .then(function (lines) {// After loading the line
+ _.each(lines, function (line) { // Render each line
+ $cursor.after(QWeb.render("report_mrp_line", {l: line}));
+ $cursor = $cursor.next();
+ if ($cursor && line.unfoldable && line.lot_name == lot_name) {
+ self.autounfold($cursor.find(".fa-caret-right"), lot_name);
+ }
+ });
+ });
+ $CurretElement.attr('class', 'o_stock_reports_foldable ' + active_id); // Change the class, and rendering of the unfolded line
+ $(target).parents('tr').find('span.o_stock_reports_unfoldable').replaceWith(QWeb.render("foldable", {lineId: active_id}));
+ $(target).parents('tr').toggleClass('o_stock_reports_unfolded');
+ },
+ unfold: function(e) {
+ this.autounfold($(e.target));
+ },
+
+});
+
+return ReportWidget;
+
+});
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 00000000..d9167a69
--- /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/scss/report_stock_forecasted.scss b/addons/stock/static/src/scss/report_stock_forecasted.scss
new file mode 100644
index 00000000..edb6aabd
--- /dev/null
+++ b/addons/stock/static/src/scss/report_stock_forecasted.scss
@@ -0,0 +1,11 @@
+.o_report_replenishment_page {
+ .o_report_replenishment {
+ .o_grid_warning {
+ background-color: #f4cccc;
+ }
+ }
+
+ .o_forecasted_row {
+ background-color: #dee2e6;
+ }
+}
diff --git a/addons/stock/static/src/scss/report_stock_rule.scss b/addons/stock/static/src/scss/report_stock_rule.scss
new file mode 100644
index 00000000..2e554762
--- /dev/null
+++ b/addons/stock/static/src/scss/report_stock_rule.scss
@@ -0,0 +1,122 @@
+.o_report_stock_rule{
+ .o_report_stock_rule_rule {
+ display: flex;
+ flex-flow: row nowrap;
+ }
+ .o_report_stock_rule_legend {
+ display: flex;
+ flex-flow: row wrap;
+ max-width: 1000px;
+ }
+
+ .o_report_stock_rule_legend_line {
+ flex: 0 1 auto;
+ display: flex;
+ flex-flow: row nowrap;
+ width: 29%;
+ margin-right: 20px;
+ margin-left: 20px;
+ margin-top: 15px;
+ min-width: 200px;
+ >.o_report_stock_rule_legend_label {
+ flex: 1 1 auto;
+ width: 30%;
+ min-width: 100px;
+ }
+ >.o_report_stock_rule_legend_symbol {
+ flex: 1 1 auto;
+ width: 70%;
+ }
+ }
+
+
+ .o_report_stock_rule_putaway {
+ >p {
+ text-align: center;
+ color: black;
+ font-weight: normal;
+ font-size: 12px
+ }
+ }
+
+ .o_report_stock_rule_line {
+ flex: 1 1 auto;
+ height: 20px;
+ >line {
+ stroke: black;
+ stroke-width: 1;
+ }
+ }
+
+ .o_report_stock_rule_arrow {
+ flex: 0 0 auto;
+ height: 20px;
+ width: 20px;
+ >svg {
+ >line {
+ stroke: black;
+ stroke-width: 1;
+ }
+ >polygon {
+ fill: black;
+ fill-opacity: 0.5;
+ stroke: black;
+ stroke-width: 1;
+ }
+ }
+ }
+
+ .o_report_stock_rule_vertical_bar {
+ flex: 0 0 auto;
+ height: 20px;
+ width: 2px;
+ >svg {
+ >line {
+ stroke: black;
+ stroke-width: 2;
+ }
+ }
+ }
+
+ .o_report_stock_rule_rule_name {
+ text-align: center;
+ }
+
+ .o_report_stock_rule_symbol_cell {
+ border: none !important;
+ >div {
+ max-width: 200px;
+ height: 20px;
+ }
+ }
+
+ .o_report_stock_rule_rule_main {
+ height: 100%;
+ padding-top: 2px;
+ }
+ .o_report_stock_rule_location_header {
+ text-align: center;
+ >a {
+ display: block;
+ &:hover {
+ text-decoration: none;
+ cursor: pointer;
+ background-color: #efefef;
+ }
+ >div {
+ color: black;
+ }
+ }
+ }
+ .o_report_stock_rule_rule_cell {
+ padding:0 !important;
+ >a {
+ display: block;
+ &:hover {
+ text-decoration: none;
+ cursor: pointer;
+ background-color: #efefef;
+ }
+ }
+ }
+}
diff --git a/addons/stock/static/src/scss/stock_empty_screen.scss b/addons/stock/static/src/scss/stock_empty_screen.scss
new file mode 100644
index 00000000..44187f77
--- /dev/null
+++ b/addons/stock/static/src/scss/stock_empty_screen.scss
@@ -0,0 +1,16 @@
+.o_view_nocontent {
+ &_barcode_scanner:before {
+ @extend %o-nocontent-init-image;
+ @include size(250px, 250px);
+ background: transparent url(/stock/static/img/barcode_scanner.png) no-repeat center;
+ background-size: 250px 250px;
+ }
+
+ &_replenishment:before {
+ @extend %o-nocontent-init-image;
+ width: 100%;
+ height: 300px;
+ max-width: 500px;
+ background: transparent url(/stock/static/img/replenishment.svg) no-repeat center;
+ }
+}
diff --git a/addons/stock/static/src/scss/stock_traceability_report.scss b/addons/stock/static/src/scss/stock_traceability_report.scss
new file mode 100644
index 00000000..b58f4846
--- /dev/null
+++ b/addons/stock/static/src/scss/stock_traceability_report.scss
@@ -0,0 +1,83 @@
+@mixin o-stock-reports-lines($border-width: 5px, $font-weight: inherit, $border-top-style: initial, $border-bottom-style: initial) {
+ border-width: $border-width;
+ border-left-style: hidden;
+ border-right-style: hidden;
+ font-weight: $font-weight;
+ border-top-style: $border-top-style;
+ border-bottom-style: $border-bottom-style;
+}
+.o_stock_reports_body_print {
+ background-color: white;
+ color: black;
+ .o_stock_reports_level0 {
+ @include o-stock-reports-lines($border-width: 1px, $font-weight: bold, $border-top-style: solid, $border-bottom-style: groove);
+ }
+}
+
+.o_main_content {
+ .o_stock_reports_page {
+ position: absolute;
+ }
+}
+.o_stock_reports_page {
+ background-color: $o-view-background-color;
+ &.o_stock_reports_no_print {
+ margin: $o-horizontal-padding auto;
+ @include o-webclient-padding($top: $o-sheet-vpadding, $bottom: $o-sheet-vpadding);
+ .o_stock_reports_level0 {
+ @include o-stock-reports-lines($border-width: 1px, $font-weight: normal, $border-top-style: solid, $border-bottom-style: groove);
+ }
+ .o_stock_reports_table {
+ white-space: nowrap;
+ margin-top: 30px;
+ }
+ .o_report_line_header {
+ text-align: left;
+ padding-left: 10px;
+ }
+ .o_report_header {
+ border-top-style: solid;
+ border-top-style: groove;
+ border-bottom-style: groove;
+ border-width: 2px;
+ }
+ }
+ .o_stock_reports_unfolded {
+ display: inline-block;
+ }
+ .o_stock_reports_nofoldable {
+ margin-left: 17px;
+ }
+ a.o_stock_report_lot_action {
+ cursor: pointer;
+ }
+ .o_stock_reports_unfolded td + td {
+ visibility: hidden;
+ }
+ div.o_stock_reports_web_action,
+ span.o_stock_reports_web_action, i.fa,
+ span.o_stock_reports_unfoldable, span.o_stock_reports_foldable, a.o_stock_reports_web_action {
+ cursor: pointer;
+ }
+ .o_stock_reports_caret_icon {
+ margin-left: -3px;
+ }
+ th {
+ border-bottom: thin groove;
+ }
+ .o_stock_reports_level1 {
+ @include o-stock-reports-lines($border-width: 2px, $border-top-style: hidden, $border-bottom-style: solid);
+ }
+ .o_stock_reports_level2 {
+ @include o-stock-reports-lines($border-width: 1px, $border-top-style: solid, $border-bottom-style: solid);
+ > td > span:last-child {
+ margin-left: 25px;
+ }
+ }
+ .o_stock_reports_default_style {
+ @include o-stock-reports-lines($border-width: 0px, $border-top-style: solid, $border-bottom-style: solid);
+ > td > span:last-child {
+ margin-left: 50px;
+ }
+ }
+}
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 00000000..463e14d6
--- /dev/null
+++ b/addons/stock/static/src/xml/forecast_widget.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<templates id="template" xml:space="preserve">
+ <t t-name="stock.forecastWidget">
+ <span t-if="['draft', 'partially_available', 'assigned', 'cancel', 'done'].includes(state)" t-esc="reserved_availability_str"/>
+ <span t-elif="!forecast_expected_date_str and will_be_fulfilled" class="text-success">Available</span>
+ <span t-elif="forecast_expected_date_str and will_be_fulfilled" t-att-class="forecast_is_late ? 'text-danger' : 'text-warning'">Exp <t t-esc="forecast_expected_date_str"/></span>
+ <span t-else="" class="text-danger">Not Available</span>
+ <button t-if="product_type == 'product'" t-att="id ? {} : {'disabled': ''}" class="o_forecast_report_button btn btn-link o_icon_button ml-2" title="Forecasted Report">
+ <i t-attf-class="fa fa-fw fa-area-chart {{ state != 'draft' and (!will_be_fulfilled or forecast_is_late) ? 'text-danger' : '' }}"/>
+ </button>
+ </t>
+</templates>
diff --git a/addons/stock/static/src/xml/inventory_lines.xml b/addons/stock/static/src/xml/inventory_lines.xml
new file mode 100644
index 00000000..37fa0f5b
--- /dev/null
+++ b/addons/stock/static/src/xml/inventory_lines.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<templates id="template" xml:space="preserve">
+ <t t-name="InventoryLines.Buttons">
+ <button type="button" class='btn btn-primary o_button_validate_inventory'>
+ Validate Inventory
+ </button>
+ </t>
+</templates>
diff --git a/addons/stock/static/src/xml/inventory_report.xml b/addons/stock/static/src/xml/inventory_report.xml
new file mode 100644
index 00000000..f33d7297
--- /dev/null
+++ b/addons/stock/static/src/xml/inventory_report.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<templates id="template" xml:space="preserve">
+
+<button t-name="InventoryReport.Buttons" class="btn btn-primary" type="button">
+ Inventory at Date
+</button>
+
+</templates>
diff --git a/addons/stock/static/src/xml/popover_widget.xml b/addons/stock/static/src/xml/popover_widget.xml
new file mode 100644
index 00000000..d5c50356
--- /dev/null
+++ b/addons/stock/static/src/xml/popover_widget.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<templates id="template" xml:space="preserve">
+
+ <t t-name="stock.popoverButton">
+ <a tabindex="0" t-attf-class="p-1 fa #{ icon || 'fa-info-circle'} #{ color || 'text-primary'}"/>
+ </t>
+
+ <div t-name="stock.popoverContent">
+ <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>
diff --git a/addons/stock/static/src/xml/report_stock_forecasted.xml b/addons/stock/static/src/xml/report_stock_forecasted.xml
new file mode 100644
index 00000000..7b6240b3
--- /dev/null
+++ b/addons/stock/static/src/xml/report_stock_forecasted.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<templates id="template" xml:space="preserve">
+
+<button t-name="replenish_report_buttons"
+ class="btn btn-primary o_report_replenish_buy"
+ type="button" title="Replenish">
+ Replenish
+</button>
+
+<t t-name="warehouseFilter">
+ <div id="warehouse_filter" class="btn-group o_dropdown o_stock_report_warehouse_filter"
+ t-if="displayWarehouseFilter">
+ <button type="button" class="o_dropdown_toggler_btn btn btn-secondary dropdown-toggle"
+ data-toggle="dropdown">
+ <span class="fa fa-home"/> Warehouse: <t t-esc="active_warehouse['name']"/>
+ </button>
+ <div class="dropdown-menu o_dropdown_menu o_filter_menu" role="menu">
+ <t t-foreach="warehouses" t-as="wh">
+ <a role="menuitem" class="dropdown-item warehouse_filter"
+ data-filter="warehouses" t-att-data-warehouse-id="wh['id']"
+ t-esc="wh['name']"/>
+ </t>
+ </div>
+ </div>
+</t>
+
+</templates>
diff --git a/addons/stock/static/src/xml/stock_orderpoint.xml b/addons/stock/static/src/xml/stock_orderpoint.xml
new file mode 100644
index 00000000..a9155181
--- /dev/null
+++ b/addons/stock/static/src/xml/stock_orderpoint.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<templates id="template" xml:space="preserve">
+ <div t-name="stock.leadDaysPopOver">
+ <p>
+ The forecasted stock on the <t t-esc="lead_days_date"/>
+ is <t t-if="qty_to_order &lt;= 0"><t t-esc="qty_forecast"/> <t t-esc="product_uom_name"/></t><t t-else="">
+ below the minimum inventory of <t t-esc="product_min_qty"/> <t t-esc="product_uom_name"/>
+ : <t t-esc="qty_to_order"/> <t t-esc="product_uom_name"/> should be replenished to reach the maximum of
+ <t t-esc="product_max_qty"/> <t t-esc="product_uom_name"/>.</t>
+ </p>
+ <table t-if="lead_days_description" class="table table-borderless">
+ <tbody>
+ <tr>
+ <td>
+ Today
+ </td>
+ <td class="text-right">
+ <t t-esc="today"/>
+ </td>
+ </tr>
+ <t t-raw="lead_days_description"/>
+ <tr class="table-info">
+ <td>
+ Forecasted Date
+ </td>
+ <td class="text-right text-nowrap">
+ = <t t-esc="lead_days_date"/>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <button class="text-left btn btn-link action_open_forecast"
+ type="button">
+ <i class="fa fa-fw o_button_icon fa-arrow-right"></i>
+ View Forecast
+ </button>
+ </div>
+
+ <t t-name="StockOrderpoint.Buttons">
+ <span>
+ <button type="button" class="btn d-none btn-primary o_button_order">
+ Order
+ </button>
+ <button type="button" class="btn d-none btn-primary o_button_snooze">
+ Snooze
+ </button>
+ </span>
+ </t>
+
+</templates>
diff --git a/addons/stock/static/src/xml/stock_traceability_report_backend.xml b/addons/stock/static/src/xml/stock_traceability_report_backend.xml
new file mode 100644
index 00000000..e2aa016b
--- /dev/null
+++ b/addons/stock/static/src/xml/stock_traceability_report_backend.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<templates>
+
+ <t t-name="stockReports.buttons">
+ <button type="button" class='btn btn-primary o_stock-widget-pdf'>PRINT</button>
+ </t>
+
+ <div role="dialog" t-name='stockReports.errorModal' class="modal" id="editable_error" tabindex="-1" data-backdrop="static" style="z-index:9999;">
+ <div class="modal-dialog modal-sm">
+ <div class="modal-content">
+ <header class="modal-header">
+ <h3 class="modal-title">Error</h3>
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close">×</button>
+ </header>
+ <main class="modal-body">
+ <p id='insert_error' class='text-center'></p>
+ </main>
+ </div>
+ </div>
+ </div>
+
+</templates>
diff --git a/addons/stock/static/src/xml/stock_traceability_report_line.xml b/addons/stock/static/src/xml/stock_traceability_report_line.xml
new file mode 100644
index 00000000..11504054
--- /dev/null
+++ b/addons/stock/static/src/xml/stock_traceability_report_line.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<templates>
+
+ <t t-name="foldable">
+ <span t-att-class="'o_stock_reports_foldable ' + lineId + ' o_stock_reports_caret_icon'"><i class="fa fa-fw fa-caret-down" role="img" aria-label="Fold" title="Fold"></i></span>
+ </t>
+
+ <t t-name="unfoldable">
+ <span t-att-class="'o_stock_reports_unfoldable ' + lineId + ' o_stock_reports_caret_icon'"><i class="fa fa-fw fa-caret-right" role="img" aria-label="Unfold" title="Unfold"></i></span>
+ </t>
+
+ <t t-name="report_mrp_line">
+ <t t-set="trclass" t-value="'o_stock_reports_default_style'"/>
+ <t t-if="l.model == 'stock.move.line'"><t t-set="trclass" t-value="'o_stock_reports_level0'"/></t>
+ <t t-set="space_td" t-value="'margin-left: '+ l.level + 'px;'"/>
+ <t t-set="domainClass" t-value="'o_stock_reports_domain_line_0'"/>
+ <t t-if="l.unfoldable == false">
+ <t t-set="spanclass" t-value="'o_stock_reports_nofoldable'" />
+ <t t-set="domainClass" t-value="'o_stock_reports_domain_line_1'"/>
+ </t>
+
+ <tr t-att-data-unfold="l.unfoldable" t-att-data-parent_id="l.parent_id" t-att-data-id="l.id" t-att-data-model_id="l.model_id" t-att-data-model="l.model" t-att-class="trclass" t-att-data-lot_name="l.lot_name" t-att-data-lot_id="l.lot_id">
+ <t t-if="l.unfoldable == true"><t t-set="tdclass" t-value="'o_stock_reports_unfoldable'" /></t>
+ <t t-set="column" t-value="0" />
+ <t t-foreach="l.columns" t-as="c">
+ <t t-set="column" t-value="column + 1" />
+ <td style="white-space: nowrap;" t-att-data-id="l.id" t-att-data-model="l.model" t-att-data-model_id="l.model_id" t-att-class="tdclass" t-att-data-level="l.level" t-att-data-lot_name="l.lot_name">
+ <t t-if="column == 1">
+ <span t-att-style="space_td" t-att-class="domainClass"></span>
+ <t t-if="l.unfoldable">
+ <span class="o_stock_reports_unfoldable o_stock_reports_caret_icon"><i class="fa fa-fw fa-caret-right" role="img" aria-label="Unfold" title="Unfold"></i></span>
+ </t>
+ </t>
+ <t t-if="l.reference == c">
+ <span t-if="c" t-att-class="spanclass">
+ <a t-att-data-active-id="l.res_id" t-att-data-res-model="l.res_model" class="o_stock_reports_web_action" href="#"><t t-esc="c"/></a>
+ </span>
+ </t><t t-elif="l.lot_name == c and l.lot_name != false">
+ <span>
+ <a class="o_stock_report_lot_action" href="#"><t t-esc="c"/></a>
+ </span>
+ </t>
+ <t t-if="l.reference != c and l.lot_name != c">
+ <t t-if="typeof c == 'string' || typeof c == 'number'">
+ <t t-esc="c"/>
+ </t>
+ <t t-if="typeof c != 'string' &amp; typeof c != 'number'"><span t-att-style="c[1]">
+ <t t-esc="c[0]"/>
+ </span></t>
+ </t>
+ </td>
+ </t>
+ </tr>
+ </t>
+
+</templates>