diff --git a/addons/sale/__manifest__.py b/addons/sale/__manifest__.py index de75dfc1812..992e81c7b63 100644 --- a/addons/sale/__manifest__.py +++ b/addons/sale/__manifest__.py @@ -63,7 +63,9 @@ This module contains all the common features of Sales Management and eCommerce. 'sale/static/src/scss/product_configurator.scss', 'sale/static/src/js/sale.js', 'sale/static/src/js/tours/sale.js', + 'sale/static/src/js/sale_order_line_mixin.js', 'sale/static/src/js/product_configurator_widget.js', + 'sale/static/src/js/sale_order_controller.js', 'sale/static/src/js/sale_order_view.js', 'sale/static/src/js/product_discount_widget.js', ], diff --git a/addons/sale/data/sale_demo.xml b/addons/sale/data/sale_demo.xml index 19ab8581ae4..3f5d78eac42 100644 --- a/addons/sale/data/sale_demo.xml +++ b/addons/sale/data/sale_demo.xml @@ -236,7 +236,6 @@ - Test/001 diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index 4c88e874a6b..54ed61650f2 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -841,10 +841,11 @@ class SaleOrder(models.Model): lines_to_recompute.discount = 0.0 lines_to_recompute._compute_discount() self.show_update_pricelist = False - self.message_post(body=_( - "Product prices have been recomputed according to pricelist %s.", - self.pricelist_id._get_html_link(), - )) + if self.partner_id and self.id: + self.message_post(body=_( + "Product prices have been recomputed according to pricelist %s.", + self.pricelist_id._get_html_link(), + )) # INVOICING # @@ -916,9 +917,9 @@ class SaleOrder(models.Model): def _get_invoice_grouping_keys(self): return ['company_id', 'partner_id', 'currency_id'] - @api.model - def _nothing_to_invoice_error(self): - return UserError(_( + def _nothing_to_invoice_error_message(self): + self.ensure_one() + return _( "There is nothing to invoice!\n\n" "Reason(s) of this behavior could be:\n" "- You should deliver your products before invoicing them: Click on the \"truck\" icon " @@ -927,7 +928,7 @@ class SaleOrder(models.Model): "\"Sales\" tab and modify invoicing policy from \"delivered quantities\" to \"ordered " "quantities\". For Services, you should modify the Service Invoicing Policy to " "'Prepaid'." - )) + ) def _get_invoiceable_lines(self, final=False): """Return the invoiceable lines for order `self`.""" @@ -1007,7 +1008,7 @@ class SaleOrder(models.Model): invoice_vals_list.append(invoice_vals) if not invoice_vals_list: - raise self._nothing_to_invoice_error() + raise UserError(order._nothing_to_invoice_error_message()) # 2) Manage 'grouped' parameter: group by (partner_id, currency_id). if not grouped: diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 67d98c6aa89..ec23f36dc43 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -731,7 +731,7 @@ class SaleOrderLine(models.Model): calculated from the ordered quantity. Otherwise, the quantity delivered is used. """ for line in self: - if line.state in ['sale', 'done']: + if line.state in ['sale', 'done'] and not line.display_type: if line.product_id.invoice_policy == 'order': line.qty_to_invoice = line.product_uom_qty - line.qty_invoiced else: @@ -904,7 +904,6 @@ class SaleOrderLine(models.Model): } #=== CRUD METHODS ===# - def _add_precomputed_values(self, vals_list): """ In the specific case where the discount is provided in the create values without being rounded, we have to 'manually' round it otherwise it won't be, diff --git a/addons/sale/static/src/js/product_discount_widget.js b/addons/sale/static/src/js/product_discount_widget.js index ede118000c0..14933d654f2 100644 --- a/addons/sale/static/src/js/product_discount_widget.js +++ b/addons/sale/static/src/js/product_discount_widget.js @@ -3,6 +3,7 @@ odoo.define('sale.product_discount', function (require) { const BasicFields = require('web.basic_fields'); const FieldsRegistry = require('web.field_registry'); + const UpdateAllLinesMixin = require('sale.UpdateAllLinesMixin'); /** * The sale.product_discount widget is a simple widget extending FieldFloat @@ -11,22 +12,10 @@ odoo.define('sale.product_discount', function (require) { * !!! WARNING !!! * * This widget is only designed for sale_order_line creation/updates. - * !!! It should only be used on a discount field !!! */ - const ProductDiscountWidget = BasicFields.FieldFloat.extend({ - - /** - * Override changes at a discount. - * - * @override - * @param {OdooEvent} ev - * - */ - async reset(record, ev) { - if (ev && ev.data.changes && ev.data.changes.discount >= 0) { - this.trigger_up('open_discount_wizard'); - } - this._super(...arguments); + const ProductDiscountWidget = BasicFields.FieldFloat.extend(UpdateAllLinesMixin, { + _getUpdateAllLinesAction: function () { + return 'open_update_all_wizard'; }, }); diff --git a/addons/sale/static/src/js/sale_order_controller.js b/addons/sale/static/src/js/sale_order_controller.js new file mode 100644 index 00000000000..81d7105a955 --- /dev/null +++ b/addons/sale/static/src/js/sale_order_controller.js @@ -0,0 +1,88 @@ +odoo.define('sale.SaleOrderFormController', function (require) { + "use strict"; + + const FormController = require('web.FormController'); + const Dialog = require('web.Dialog'); + const core = require('web.core'); + const _t = core._t; + + const SaleOrderFormController = FormController.extend({ + custom_events: _.extend({}, FormController.prototype.custom_events, { + open_update_all_wizard: '_onOpenUpdateAllWizard', + }), + + // ------------------------------------------------------------------------- + // Handlers + // ------------------------------------------------------------------------- + + /** + * Handler called if user changes a value in the sale order line. + * The wizard will open only if + * (1) Sale order line is 3 or more + * (2) First sale order line is changed + * (3) value is the same in all other sale order line + */ + _onOpenUpdateAllWizard(ev) { + const orderLines = this._DialogReady(ev, 'normal') + const confirmCallback = () => { + orderLines.slice(1).forEach((line) => { + this.trigger_up('field_changed', { + dataPointID: this.renderer.state.id, + changes: {order_line: {operation: "UPDATE", id: line.id, data: {[ev.data.fieldName]: ev.data.value}}}, + }); + }); + }; + if (orderLines) { + Dialog.confirm(this, _t("Do you want to apply this value to all order lines?"), { + buttons: [{ + text: _t('YES'), + classes: 'btn-primary', + close: true, + click: () => confirmCallback(), + }, { + text: _t("NO"), + close: true, + }], + }); + } + }, + + _isEqualValue (type, fieldName, orderLines) { + let isEqualValue; + let secondValue = orderLines[1].data[fieldName]; + if (type === 'normal') { + if (secondValue instanceof moment) { + isEqualValue = orderLines.slice(1).every(line => secondValue.isSame(line.data[fieldName])); + } else { + isEqualValue = orderLines.slice(1).every(line => line.data[fieldName] === secondValue); + } + } else if (type === 'one2many') { + // only works for display_name because we want to be able to apply different id with similar properties + if (secondValue.data && secondValue.data.display_name) { + secondValue = secondValue.data.display_name; + isEqualValue = orderLines.slice(1).every(line => line.data[fieldName].data && line.data[fieldName].data.display_name === secondValue); + } else { + isEqualValue = orderLines.slice(1).every(line => line.data[fieldName] === secondValue); + } + } + return isEqualValue; + }, + + _DialogReady (ev, type) { + const recordData = ev.target.recordData; + const fieldName = ev.data.fieldName; + const orderLines = this.renderer.state.data.order_line.data.filter(line => !line.data.display_type); + if (orderLines.length < 3) { + return false; + } + if (recordData.id === orderLines[0].data.id && this._isEqualValue(type, fieldName, orderLines)) { + return orderLines; + } + return false; + }, + + }); + + return SaleOrderFormController; + +}); diff --git a/addons/sale/static/src/js/sale_order_line_mixin.js b/addons/sale/static/src/js/sale_order_line_mixin.js new file mode 100644 index 00000000000..ff5d7376cc2 --- /dev/null +++ b/addons/sale/static/src/js/sale_order_line_mixin.js @@ -0,0 +1,18 @@ +odoo.define('sale.UpdateAllLinesMixin', function (require) { + 'use strict'; + + const UpdateAllLinesMixin = { + _onFieldChanged: function (ev) { + if (ev && ev.data.changes) { + const fieldName = Object.keys(ev.data.changes)[0]; + const value = ev.data.changes[fieldName]; + this.trigger_up(this._getUpdateAllLinesAction(), {fieldName: fieldName, value: value}); + } + this._super.apply(this, arguments); + }, + _getUpdateAllLinesAction: function () { + return ''; + }, + }; + return UpdateAllLinesMixin; +}); diff --git a/addons/sale/static/src/js/sale_order_view.js b/addons/sale/static/src/js/sale_order_view.js index ef74cee9737..6583670769d 100644 --- a/addons/sale/static/src/js/sale_order_view.js +++ b/addons/sale/static/src/js/sale_order_view.js @@ -1,47 +1,9 @@ odoo.define('sale.SaleOrderView', function (require) { "use strict"; - const FormController = require('web.FormController'); + const SaleOrderFormController = require('sale.SaleOrderFormController'); const FormView = require('web.FormView'); const viewRegistry = require('web.view_registry'); - const Dialog = require('web.Dialog'); - const core = require('web.core'); - const _t = core._t; - - const SaleOrderFormController = FormController.extend({ - custom_events: _.extend({}, FormController.prototype.custom_events, { - open_discount_wizard: '_onOpenDiscountWizard', - }), - - // ------------------------------------------------------------------------- - // Handlers - // ------------------------------------------------------------------------- - - /** - * Handler called if user changes the discount field in the sale order line. - * The wizard will open only if - * (1) Sale order line is 3 or more - * (2) First sale order line is changed to discount - * (3) Discount is the same in all sale order line - */ - _onOpenDiscountWizard(ev) { - const orderLines = this.renderer.state.data.order_line.data.filter(line => !line.data.display_type); - const recordData = ev.target.recordData; - const isEqualDiscount = orderLines.slice(1).every(line => line.data.discount === recordData.discount); - if (orderLines.length >= 3 && recordData.sequence === orderLines[0].data.sequence && isEqualDiscount) { - Dialog.confirm(this, _t("Do you want to apply this discount to all order lines?"), { - confirm_callback: () => { - orderLines.slice(1).forEach((line) => { - this.trigger_up('field_changed', { - dataPointID: this.renderer.state.id, - changes: {order_line: {operation: "UPDATE", id: line.id, data: {discount: orderLines[0].data.discount}}}, - }); - }); - }, - }); - } - }, - }); const SaleOrderView = FormView.extend({ config: _.extend({}, FormView.prototype.config, { diff --git a/addons/sale/views/sale_order_views.xml b/addons/sale/views/sale_order_views.xml index 7e3a0830133..a1d9dbafa36 100644 --- a/addons/sale/views/sale_order_views.xml +++ b/addons/sale/views/sale_order_views.xml @@ -195,10 +195,10 @@ string="Void Transaction" data-hotkey="shift+v" confirm="Are you sure you want to void the authorized transaction? This action can't be undone." attrs="{'invisible': [('authorized_transaction_ids', '=', [])]}"/> -