From 3751379f1e9a4c215fb6eb898b4ccc67659b9ace Mon Sep 17 00:00:00 2001 From: stephanchrst Date: Tue, 10 May 2022 21:51:50 +0700 Subject: initial commit 2 --- addons/stock/static/description/icon.png | Bin 0 -> 8047 bytes addons/stock/static/description/icon.svg | 1 + addons/stock/static/img/barcode_scanner.png | Bin 0 -> 38231 bytes addons/stock/static/img/cable_management.png | Bin 0 -> 20462 bytes addons/stock/static/img/replenishment.svg | 1 + addons/stock/static/img/res_partner_address_41.jpg | Bin 0 -> 4344 bytes addons/stock/static/src/img/barcode.gif | Bin 0 -> 30013 bytes addons/stock/static/src/js/basic_model.js | 16 ++ addons/stock/static/src/js/forecast_widget.js | 76 +++++ .../src/js/inventory_report_list_controller.js | 66 +++++ .../static/src/js/inventory_report_list_view.js | 19 ++ .../src/js/inventory_singleton_list_controller.js | 68 +++++ .../static/src/js/inventory_singleton_list_view.js | 18 ++ .../src/js/inventory_validate_button_controller.js | 89 ++++++ .../src/js/inventory_validate_button_view.js | 16 ++ addons/stock/static/src/js/popover_widget.js | 84 ++++++ .../stock/static/src/js/report_stock_forecasted.js | 268 ++++++++++++++++++ .../src/js/stock_orderpoint_list_controller.js | 74 +++++ .../static/src/js/stock_orderpoint_list_model.js | 46 ++++ .../static/src/js/stock_orderpoint_list_view.js | 21 ++ .../static/src/js/stock_rescheduling_popover.js | 39 +++ .../src/js/stock_traceability_report_backend.js | 106 +++++++ .../src/js/stock_traceability_report_widgets.js | 131 +++++++++ addons/stock/static/src/scss/forecast_widget.scss | 8 + .../static/src/scss/report_stock_forecasted.scss | 11 + .../stock/static/src/scss/report_stock_rule.scss | 122 +++++++++ .../stock/static/src/scss/stock_empty_screen.scss | 16 ++ .../static/src/scss/stock_traceability_report.scss | 83 ++++++ addons/stock/static/src/xml/forecast_widget.xml | 12 + addons/stock/static/src/xml/inventory_lines.xml | 8 + addons/stock/static/src/xml/inventory_report.xml | 8 + addons/stock/static/src/xml/popover_widget.xml | 19 ++ .../static/src/xml/report_stock_forecasted.xml | 27 ++ addons/stock/static/src/xml/stock_orderpoint.xml | 50 ++++ .../src/xml/stock_traceability_report_backend.xml | 23 ++ .../src/xml/stock_traceability_report_line.xml | 57 ++++ addons/stock/static/tests/popover_widget_tests.js | 53 ++++ addons/stock/static/tests/singleton_list_tests.js | 305 +++++++++++++++++++++ .../stock_traceability_report_backend_tests.js | 151 ++++++++++ .../stock/static/tests/tours/stock_report_tests.js | 23 ++ 40 files changed, 2115 insertions(+) create mode 100644 addons/stock/static/description/icon.png create mode 100644 addons/stock/static/description/icon.svg create mode 100644 addons/stock/static/img/barcode_scanner.png create mode 100644 addons/stock/static/img/cable_management.png create mode 100644 addons/stock/static/img/replenishment.svg create mode 100644 addons/stock/static/img/res_partner_address_41.jpg create mode 100644 addons/stock/static/src/img/barcode.gif create mode 100644 addons/stock/static/src/js/basic_model.js create mode 100644 addons/stock/static/src/js/forecast_widget.js create mode 100644 addons/stock/static/src/js/inventory_report_list_controller.js create mode 100644 addons/stock/static/src/js/inventory_report_list_view.js create mode 100644 addons/stock/static/src/js/inventory_singleton_list_controller.js create mode 100644 addons/stock/static/src/js/inventory_singleton_list_view.js create mode 100644 addons/stock/static/src/js/inventory_validate_button_controller.js create mode 100644 addons/stock/static/src/js/inventory_validate_button_view.js create mode 100644 addons/stock/static/src/js/popover_widget.js create mode 100644 addons/stock/static/src/js/report_stock_forecasted.js create mode 100644 addons/stock/static/src/js/stock_orderpoint_list_controller.js create mode 100644 addons/stock/static/src/js/stock_orderpoint_list_model.js create mode 100644 addons/stock/static/src/js/stock_orderpoint_list_view.js create mode 100644 addons/stock/static/src/js/stock_rescheduling_popover.js create mode 100644 addons/stock/static/src/js/stock_traceability_report_backend.js create mode 100644 addons/stock/static/src/js/stock_traceability_report_widgets.js create mode 100644 addons/stock/static/src/scss/forecast_widget.scss create mode 100644 addons/stock/static/src/scss/report_stock_forecasted.scss create mode 100644 addons/stock/static/src/scss/report_stock_rule.scss create mode 100644 addons/stock/static/src/scss/stock_empty_screen.scss create mode 100644 addons/stock/static/src/scss/stock_traceability_report.scss create mode 100644 addons/stock/static/src/xml/forecast_widget.xml create mode 100644 addons/stock/static/src/xml/inventory_lines.xml create mode 100644 addons/stock/static/src/xml/inventory_report.xml create mode 100644 addons/stock/static/src/xml/popover_widget.xml create mode 100644 addons/stock/static/src/xml/report_stock_forecasted.xml create mode 100644 addons/stock/static/src/xml/stock_orderpoint.xml create mode 100644 addons/stock/static/src/xml/stock_traceability_report_backend.xml create mode 100644 addons/stock/static/src/xml/stock_traceability_report_line.xml create mode 100644 addons/stock/static/tests/popover_widget_tests.js create mode 100644 addons/stock/static/tests/singleton_list_tests.js create mode 100644 addons/stock/static/tests/stock_traceability_report_backend_tests.js create mode 100644 addons/stock/static/tests/tours/stock_report_tests.js (limited to 'addons/stock/static') diff --git a/addons/stock/static/description/icon.png b/addons/stock/static/description/icon.png new file mode 100644 index 00000000..692e7624 Binary files /dev/null and b/addons/stock/static/description/icon.png differ diff --git a/addons/stock/static/description/icon.svg b/addons/stock/static/description/icon.svg new file mode 100644 index 00000000..834a8e1f --- /dev/null +++ b/addons/stock/static/description/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/stock/static/img/barcode_scanner.png b/addons/stock/static/img/barcode_scanner.png new file mode 100644 index 00000000..e8f7a7fa Binary files /dev/null and b/addons/stock/static/img/barcode_scanner.png differ diff --git a/addons/stock/static/img/cable_management.png b/addons/stock/static/img/cable_management.png new file mode 100644 index 00000000..bddc79b8 Binary files /dev/null and b/addons/stock/static/img/cable_management.png differ diff --git a/addons/stock/static/img/replenishment.svg b/addons/stock/static/img/replenishment.svg new file mode 100644 index 00000000..5e56d8e5 --- /dev/null +++ b/addons/stock/static/img/replenishment.svg @@ -0,0 +1 @@ +MaximumMinimumSafety StockLead timeTodayTime \ No newline at end of file diff --git a/addons/stock/static/img/res_partner_address_41.jpg b/addons/stock/static/img/res_partner_address_41.jpg new file mode 100644 index 00000000..1de43e92 Binary files /dev/null and b/addons/stock/static/img/res_partner_address_41.jpg differ 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 Binary files /dev/null and b/addons/stock/static/src/img/barcode.gif 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."+ + "
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': '', + * 'icon': '' (optionnal), + * 'color': '' (optionnal), + * 'title': '' (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 <= 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' & typeof c != 'number'"><span t-att-style="c[1]"> + <t t-esc="c[0]"/> + </span></t> + </t> + </td> + </t> + </tr> + </t> + +</templates> diff --git a/addons/stock/static/tests/popover_widget_tests.js b/addons/stock/static/tests/popover_widget_tests.js new file mode 100644 index 00000000..dff13d60 --- /dev/null +++ b/addons/stock/static/tests/popover_widget_tests.js @@ -0,0 +1,53 @@ +odoo.define('stock.popover_widget_tests', function (require) { +"use strict"; + +var testUtils = require('web.test_utils'); +var FormView = require('web.FormView'); +var createView = testUtils.createView; + +QUnit.module('widgets', {}, function () { +QUnit.module('ModelFieldSelector', { + beforeEach: function () { + this.data = { + partner: { + fields: { + json_data: {string: " ", type: "char"}, + }, + records: [ + {id:1, json_data:'{"color": "text-danger", "msg": "var that = self // why not?", "title": "JS Master"}'} + ] + } + }; + }, +}, function () { + QUnit.test("Test creation/usage popover widget form", async function (assert) { + assert.expect(6); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<field name="json_data" widget="popover_widget"/>' + + '</form>', + res_id: 1 + }); + + var $popover = $('div.popover'); + assert.strictEqual($popover.length, 0, "Shouldn't have a popover container in DOM"); + + var $popoverButton = form.$('a.fa.fa-info-circle.text-danger'); + assert.strictEqual($popoverButton.length, 1, "Should have a popover icon/button in red"); + assert.strictEqual($popoverButton.prop('special_click'), true, "Special click properpy should be activated"); + await testUtils.dom.triggerEvents($popoverButton, ['focus']); + $popover = $('div.popover'); + assert.strictEqual($popover.length, 1, "Should have a popover container in DOM"); + assert.strictEqual($popover.html().includes("var that = self // why not?"), true, "The message should be in DOM"); + assert.strictEqual($popover.html().includes("JS Master"), true, "The title should be in DOM"); + + form.destroy(); + }); +}); +}); + +}); diff --git a/addons/stock/static/tests/singleton_list_tests.js b/addons/stock/static/tests/singleton_list_tests.js new file mode 100644 index 00000000..aad90a29 --- /dev/null +++ b/addons/stock/static/tests/singleton_list_tests.js @@ -0,0 +1,305 @@ +odoo.define('web.singleton_list_tests', function (require) { +"use strict"; + +var SingletonListView = require('stock.SingletonListView'); +var testUtils = require('web.test_utils'); + +var createView = testUtils.createView; + + +QUnit.module('Views', { + beforeEach: function () { + this.data = { + person: { + fields: { + name: {string: "Name", type: "char"}, + age: {string: "Age", type: "integer"}, + job: {string: "Profession", type: "char"}, + }, + records: [ + {id: 1, name: 'Daniel Fortesque', age: 32, job: 'Soldier'}, + {id: 2, name: 'Samuel Oak', age: 64, job: 'Professor'}, + {id: 3, name: 'Leto II Atreides', age: 128, job: 'Emperor'}, + ] + }, + }; + this.mockRPC = function (route, args) { + if (route === '/web/dataset/call_kw/person/create') { + var name = args.args[0].name; + var age = args.args[0].age; + var job = args.args[0].job; + for (var d of this.data.person.records) { + if (d.name === name) { + d.age = age; + d.job = job; + return Promise.resolve(d.id); + } + } + } + return this._super.apply(this, arguments); + }; + } +}, function () { + + QUnit.module('SingletonListView'); + + QUnit.test('Create new record correctly', async function (assert) { + assert.expect(2); + + var list = await createView({ + View: SingletonListView, + model: 'person', + data: this.data, + arch: '<tree editable="top" js_class="singleton_list">'+ + '<field name="name"/>'+ + '<field name="age"/>'+ + '</tree>', + mockRPC: this.mockRPC, + }); + // Checks we have initially 3 records + assert.containsN(list, '.o_data_row', 3, "should have 3 records"); + + // Creates a new line... + await testUtils.dom.click($('.o_list_button_add')); + // ... and fills fields with new values + var $input = $('.o_selected_row input[name=name]'); + await testUtils.fields.editInput($input, 'Bilou'); + await testUtils.fields.triggerKeydown($input, 'tab'); + + $input = $('.o_selected_row input[name=age]'); + await testUtils.fields.editInput($input, '24'); + await testUtils.fields.triggerKeydown($input, 'enter'); + await testUtils.dom.click($('.o_list_button_save')); + + // Checks new record is in the list + assert.containsN(list, '.o_data_row', 4, "should now have 4 records"); + list.destroy(); + }); + + QUnit.test('Don\'t duplicate record', async function (assert) { + assert.expect(3); + + var list = await createView({ + View: SingletonListView, + model: 'person', + data: this.data, + arch: '<tree editable="top" js_class="singleton_list">'+ + '<field name="name"/>'+ + '<field name="age"/>'+ + '</tree>', + mockRPC: this.mockRPC, + }); + // Checks we have initially 3 records + assert.containsN(list, '.o_data_row', 3, "should have 3 records"); + + // Creates a new line... + await testUtils.dom.click($('.o_list_button_add')); + // ... and fills fields with already existing value + var $input = $('.o_selected_row input[name=name]'); + var name = 'Samuel Oak'; + await testUtils.fields.editInput($input, name); + await testUtils.fields.triggerKeydown($input, 'tab'); + + $input = $('.o_selected_row input[name=age]'); + var age = '72'; + await testUtils.fields.editInput($input, age); + await testUtils.fields.triggerKeydown($input, 'enter'); + + // Checks we have still only 3 records... + assert.containsN(list, '.o_data_row', 3, "should still have 3 records"); + // ... and verify modification was occured. + var nameField = list.$('td[title="' + name + '"]'); + var ageField = nameField.parent().find('.o_list_number'); + assert.strictEqual(ageField.text(), age, "The age field must be updated"); + list.destroy(); + }); + + QUnit.test('Don\'t raise error when trying to create duplicate line', async function (assert) { + assert.expect(3); + /* In some condition, a list editable with the `singletonlist` js_class + can try to select a record at a line who isn't the same place anymore. + In this case, the list can try to find the id of an undefined record. + This test just insures we don't raise a traceback in this case. + */ + var list = await createView({ + View: SingletonListView, + model: 'person', + data: { + person: { + fields: { + name: {string: "Name", type: "char"}, + age: {string: "Age", type: "integer"}, + }, + records: [ + {id: 1, name: 'Bobby B. Bop', age: 18}, + ] + } + }, + arch: '<tree editable="top" js_class="singleton_list">'+ + '<field name="name"/>'+ + '<field name="age"/>'+ + '</tree>', + mockRPC: this.mockRPC, + }); + // Checks we have initially 1 record + assert.containsN(list, '.o_data_row', 1, "should have 1 records"); + + // Creates a new line... + await testUtils.dom.click($('.o_list_button_add')); + // ... and fills fields with already existing value + var $input = $('.o_selected_row input[name=name]'); + var name = 'Bobby B. Bop'; + await testUtils.fields.editInput($input, name); + await testUtils.fields.triggerKeydown($input, 'tab'); + + $input = $('.o_selected_row input[name=age]'); + var age = '22'; + await testUtils.fields.editInput($input, age); + // This operation causes list'll try to select undefined record. + await testUtils.fields.triggerKeydown($input, 'enter'); + + // Checks we have still only 1 record... + assert.containsN(list, '.o_data_row', 1, "should now have 1 records"); + // ... and verify modification was occured. + var nameField = list.$('td[title="' + name + '"]'); + var ageField = nameField.parent().find('.o_list_number'); + assert.strictEqual(ageField.text(), age, "The age field must be updated"); + list.destroy(); + }); + + QUnit.test('Refresh the list only when needed', async function (assert) { + assert.expect(3); + + var refresh_count = 0; + var list = await createView({ + View: SingletonListView, + model: 'person', + data: this.data, + arch: '<tree editable="top" js_class="singleton_list">'+ + '<field name="name"/>'+ + '<field name="age"/>'+ + '</tree>', + mockRPC: this.mockRPC, + }); + list.realReload = list.reload; + list.reload = function () { + refresh_count++; + return this.realReload(); + }; + // Modify Record + await testUtils.dom.click(list.$('.o_data_row:nth-child(2) > .o_list_number')); + var $input = $('.o_selected_row input[name=age]'); + await testUtils.fields.editInput($input, '70'); + await testUtils.fields.triggerKeydown($input, 'enter'); + await testUtils.dom.click($('.o_list_button_save')); + assert.strictEqual(refresh_count, 0, "don't refresh when edit existing line"); + + // Add existing record + await testUtils.dom.click($('.o_list_button_add')); + $input = $('.o_selected_row input[name=name]'); + await testUtils.fields.editInput($input, 'Leto II Atreides'); + await testUtils.fields.triggerKeydown($input, 'tab'); + $input = $('.o_selected_row input[name=age]'); + await testUtils.fields.editInput($input, '800'); + await testUtils.dom.click($('.o_list_button_save')); + assert.strictEqual(refresh_count, 1, "refresh after tried to create an existing record"); + + // Add new record + await testUtils.dom.click($('.o_list_button_add')); + $input = $('.o_selected_row input[name=name]'); + await testUtils.fields.editInput($input, 'Valentin Cognito'); + await testUtils.fields.triggerKeydown($input, 'tab'); + $input = $('.o_selected_row input[name=age]'); + await testUtils.fields.editInput($input, '37'); + await testUtils.fields.triggerKeydown($input, 'enter'); + await testUtils.dom.click($('.o_list_button_save')); + assert.strictEqual(refresh_count, 1, "don't refresh when create entirely new record"); + + list.destroy(); + }); + + QUnit.test('Work in grouped list', async function (assert) { + assert.expect(6); + + var refresh_count = 0; + var list = await createView({ + View: SingletonListView, + model: 'person', + data: this.data, + arch: '<tree editable="top" js_class="singleton_list">'+ + '<field name="name"/>'+ + '<field name="age"/>'+ + '<field name="job"/>'+ + '</tree>', + mockRPC: this.mockRPC, + groupBy: ['job'], + }); + list.realReload = list.reload; + list.reload = function () { + refresh_count++; + return this.realReload(); + }; + // Opens 'Professor' group + await testUtils.dom.click(list.$('.o_group_header:nth-child(2)')); + + // Creates a new record... + await testUtils.dom.click(list.$('.o_add_record_row a')); + var $input = $('.o_selected_row input[name=name]'); + await testUtils.fields.editInput($input, 'Del Tutorial'); + await testUtils.fields.triggerKeydown($input, 'tab'); + $input = $('.o_selected_row input[name=age]'); + await testUtils.fields.editInput($input, '32'); + await testUtils.fields.triggerKeydown($input, 'tab'); + await testUtils.dom.click($('.o_list_button_save')); + // ... then checks the list didn't refresh + assert.strictEqual(refresh_count, 0, + "don't refresh when creating new record"); + + // Creates an existing record in same group... + await testUtils.dom.click(list.$('.o_add_record_row a')); + var $input = $('.o_selected_row input[name=name]'); + await testUtils.fields.editInput($input, 'Samuel Oak'); + await testUtils.dom.click($('.o_list_button_save')); + // ... then checks the list has been refreshed + assert.strictEqual(refresh_count, 1, + "refresh when try to create an existing record"); + + // Creates an existing but not displayed record... + await testUtils.dom.click(list.$('.o_add_record_row a')); + var $input = $('.o_selected_row input[name=name]'); + await testUtils.fields.editInput($input, 'Daniel Fortesque'); + await testUtils.fields.triggerKeydown($input, 'tab'); + $input = $('.o_selected_row input[name=age]'); + await testUtils.fields.editInput($input, '55'); + await testUtils.fields.triggerKeydown($input, 'tab'); + $input = $('.o_selected_row input[name=job]'); + await testUtils.fields.editInput($input, 'Soldier'); + await testUtils.dom.click($('.o_list_button_save')); + // .. then checks the list didn't refresh + assert.strictEqual(refresh_count, 1, + "don't refresh when creating an existing record but this record " + + "isn't present in the view"); + + // Opens 'Soldier' group + await testUtils.dom.click(list.$('.o_group_header:nth-child(1)').first()); + // Checks the record has been correctly updated + var ageCell = $('tr.o_data_row td.o_list_number').first(); + assert.strictEqual(ageCell.text(), "55", + "age of the record must be updated"); + // Edits the freshly created record... + await testUtils.dom.click(list.$('tr.o_data_row td.o_list_number').eq(1)); + $input = $('.o_selected_row input[name=age]'); + await testUtils.fields.editInput($input, '66'); + await testUtils.dom.click($('.o_list_button_save')); + // ... then checks the list and data have been refreshed + assert.strictEqual(refresh_count, 2, + "refresh when try to create an existing record present in the view"); + ageCell = $('tr.o_data_row td.o_list_number').first(); + assert.strictEqual(ageCell.text(), "66", + "age of the record must be updated"); + + list.destroy(); + }); +}); + +}); diff --git a/addons/stock/static/tests/stock_traceability_report_backend_tests.js b/addons/stock/static/tests/stock_traceability_report_backend_tests.js new file mode 100644 index 00000000..5ab69403 --- /dev/null +++ b/addons/stock/static/tests/stock_traceability_report_backend_tests.js @@ -0,0 +1,151 @@ +odoo.define('stock.stock_traceability_report_backend_tests', function (require) { + "use strict"; + + const ControlPanel = require('web.ControlPanel'); + const dom = require('web.dom'); + const StockReportGeneric = require('stock.stock_report_generic'); + const testUtils = require('web.test_utils'); + + const { createActionManager, dom: domUtils } = testUtils; + + /** + * Helper function to instantiate a stock report action. + * @param {Object} params + * @param {Object} params.action + * @param {boolean} [params.debug] + * @returns {Promise<StockReportGeneric>} + */ + async function createStockReportAction(params) { + const parent = await testUtils.createParent(params); + const report = new StockReportGeneric(parent, params.action); + const target = testUtils.prepareTarget(params.debug); + + const _destroy = report.destroy; + report.destroy = function () { + report.destroy = _destroy; + parent.destroy(); + }; + const fragment = document.createDocumentFragment(); + await report.appendTo(fragment); + dom.prepend(target, fragment, { + callbacks: [{ widget: report }], + in_DOM: true, + }); + // Wait for the ReportWidget to be appended + await testUtils.nextTick(); + + return report; + } + + QUnit.module('Stock', {}, function () { + QUnit.module('Traceability report'); + + QUnit.test("Rendering with no lines", async function (assert) { + assert.expect(1); + + const template = ` + <div class="container-fluid o_stock_reports_page o_stock_reports_no_print"> + <div class="o_stock_reports_table table-responsive"> + <span class="text-center"> + <h1>No operation made on this lot.</h1> + </span> + </div> + </div>`; + const report = await createStockReportAction({ + action: { + context: {}, + params: {}, + }, + data: { + 'stock.traceability.report': { + fields: {}, + get_html: () => ({ html: template }), + }, + }, + }); + + // HTML content is nested in a div inside of the content + assert.strictEqual(report.el.querySelector('.o_content > div').innerHTML, template, + "Displayed template should match"); + + report.destroy(); + }); + + QUnit.test("mounted is called once when returning on 'Stock report' from breadcrumb", async assert => { + // This test can be removed as soon as we don't mix legacy and owl layers anymore. + assert.expect(7); + + let mountCount = 0; + + ControlPanel.patch('test.ControlPanel', T => { + class ControlPanelPatchTest extends T { + mounted() { + mountCount = mountCount + 1; + this.__uniqueId = mountCount; + assert.step(`mounted ${this.__uniqueId}`); + super.mounted(...arguments); + } + willUnmount() { + assert.step(`willUnmount ${this.__uniqueId}`); + super.mounted(...arguments); + } + } + return ControlPanelPatchTest; + }); + + const actionManager = await createActionManager({ + actions: [ + { + id: 42, + name: "Stock report", + tag: 'stock_report_generic', + type: 'ir.actions.client', + context: {}, + params: {}, + }, + ], + archs: { + 'partner,false,form': '<form><field name="display_name"/></form>', + 'partner,false,search': '<search></search>', + }, + data: { + partner: { + fields: { + display_name: { string: "Displayed name", type: "char" }, + }, + records: [ + {id: 1, display_name: "Genda Swami"}, + ], + }, + }, + mockRPC: function (route) { + if (route === '/web/dataset/call_kw/stock.traceability.report/get_html') { + return Promise.resolve({ + html: '<a class="o_stock_reports_web_action" href="#" data-active-id="1" data-res-model="partner">Go to form view</a>', + }); + } + return this._super.apply(this, arguments); + }, + intercepts: { + do_action: ev => actionManager.doAction(ev.data.action, ev.data.options), + }, + }); + + await actionManager.doAction(42); + await domUtils.click(actionManager.$('.o_stock_reports_web_action')); + await domUtils.click(actionManager.$('.breadcrumb-item:first')); + actionManager.destroy(); + + assert.verifySteps([ + 'mounted 1', + 'willUnmount 1', + 'mounted 2', + 'willUnmount 2', + 'mounted 3', + 'willUnmount 3', + ]); + + ControlPanel.unpatch('test.ControlPanel'); + }); + }); +}); diff --git a/addons/stock/static/tests/tours/stock_report_tests.js b/addons/stock/static/tests/tours/stock_report_tests.js new file mode 100644 index 00000000..c757390f --- /dev/null +++ b/addons/stock/static/tests/tours/stock_report_tests.js @@ -0,0 +1,23 @@ +odoo.define('stock.reports.setup.tour', function (require) { + "use strict"; + + const tour = require('web_tour.tour'); + + tour.register('test_stock_route_diagram_report', { + test: true, + }, [ + { + trigger: '.o_kanban_record', + extra_trigger:'.breadcrumb', + }, + { + trigger: '.nav-item > a:contains("Inventory")', + }, + { + trigger: '.btn[id="stock.view_diagram_button"]', + }, + { + trigger: 'iframe .o_report_stock_rule', + }, + ]); +}); -- cgit v1.2.3