From 762adedece1c6c2b97b282348659c1b39b988d24 Mon Sep 17 00:00:00 2001 From: Ravi Gadhia Date: Tue, 10 Jul 2018 13:52:44 +0200 Subject: [PATCH] [IMP] various: improve section and add note on sale order and invoice The goal is to be able to more easily create section and notes on sale order, invoice, and quote. ================================ Problems of the existing feature ================================ The problematic part in the existing feature was that the same section had to be defined again and again for every line, instead of being defined only once where it needed to be. There was also no easy way to move them. Those concerns are solved here. Moreover, due to the complexity of the feature, it was hidden by default, requiring a setting and multiple overrides. The new feature is simple enough that it will always be present. ================== New implementation ================== Sections are lines solely used for display purpose, there is no business logic attached to them. They will show as a nice header at the top of the section, and a subtotal at the bottom of the section. We also take the opportunity to add notes, which are just like sections except they are displayed differently and don't include a subtotal. ========= Reasoning ========= We decided to implement it this way, as opposed to creating a proper section model, because it creates less changes in the code. Creating a intermediary section model between the sale order/invoice/quote and their lines would have required to rewrite the entirety of the code associated to them. It would also have required to create a new JS widget to handle on the same view a double-level One2many (or a One2many + group by). The current solution is much easier because it just requires adding 2 buttons, 1 simple field on the corresponding models which even has a NULL default value, and making some business checks (don't apply business to section/note). We tried our best to impact as less as possible the existing models. Notably we didn't want to add any extra field if this was avoidable. This also makes the migration easier. No matter the implementation choice the views/reports had to be updated to display the sections and notes correctly, which has been done. =========== Limitations =========== The current solution has several limitations: * since the sections don't have business logic, it's not possible to make any reporting based on them * any business related to sections has to be computed on the fly by browsing in order all of the lines, notably the subtotal has to be computed on the fly at every display - when sections are moved (drag&drop) on the back-end, the following lines that are part of the section are not moving with the section =========== Technically =========== The commit will: * update the models to add the display_type field (along with related sql constraints to guarantee integrity) * add new buttons at the bottom of the lists to allow creating section and note, along with the existing add a line/product button * add some business checks to not apply business to section/note (product_id and account_id became non-required fields) !!! The most likely cause of potential bugs introduced in this commit would be to have forgotten to add those checks on some places where they would be needed. This is tedious to check but fortunately easy to fix. * update the views to display sections and notes correctly, also made various improvements/fixes to those views (notably to apply B2B/B2C setting more consistently) !!! This is the second likely cause of potential bugs: if the updated views are inherited in a way that is not compatible with the changes. Or if we forgot to apply the changes to some other views. Again this is tedious to check but fortunately easy to fix. Task #53996 PR #25209 --- addons/account/models/account_invoice.py | 38 ++- .../account/report/account_invoice_report.py | 8 +- .../src/js/section_and_note_fields_backend.js | 194 ++++++++++++ .../src/scss/section_and_note_backend.scss | 17 ++ addons/account/views/account.xml | 4 + addons/account/views/account_invoice_view.xml | 122 ++++++-- addons/account/views/report_invoice.xml | 166 ++++++----- addons/l10n_in/views/report_invoice.xml | 129 +++----- .../views/report_invoice_view.xml | 114 -------- .../l10n_in_sale/views/report_sale_order.xml | 4 +- .../report_intrastat_invoice_template.xml | 107 +++++-- addons/sale/__manifest__.py | 2 - addons/sale/data/sale_data.xml | 14 - addons/sale/models/__init__.py | 1 - addons/sale/models/account_invoice.py | 28 +- addons/sale/models/res_config_settings.py | 1 - addons/sale/models/res_users.py | 5 - addons/sale/models/sale.py | 89 +++--- addons/sale/models/sale_layout.py | 14 - .../sale/report/invoice_report_templates.xml | 147 ---------- addons/sale/report/sale_report.py | 1 + addons/sale/report/sale_report_templates.xml | 164 ++++++----- addons/sale/security/ir.model.access.csv | 5 - addons/sale/security/sale_security.xml | 4 - addons/sale/views/account_invoice_views.xml | 16 - .../sale/views/res_config_settings_views.xml | 16 - addons/sale/views/res_users_views.xml | 4 - .../sale/views/sale_layout_category_view.xml | 62 ---- addons/sale/views/sale_portal_templates.xml | 98 +++++-- addons/sale/views/sale_views.xml | 222 ++++++++++---- .../views/sale_management_views.xml | 3 - addons/web/static/src/scss/layout_boxed.scss | 4 + addons/web/static/src/scss/layout_clean.scss | 4 - addons/web/static/src/scss/report.scss | 18 ++ addons/website_quote/controllers/main.py | 1 - addons/website_quote/models/sale_order.py | 52 ++-- addons/website_quote/models/sale_quote.py | 29 +- .../static/src/css/website_quotation.css | 7 - .../website_quote/views/sale_order_views.xml | 1 - .../website_quote/views/sale_quote_views.xml | 45 ++- .../views/website_quote_templates.xml | 275 ++++++++++-------- addons/website_sale/views/templates.xml | 8 +- .../views/website_sale_digital.xml | 2 +- 43 files changed, 1195 insertions(+), 1050 deletions(-) create mode 100644 addons/account/static/src/js/section_and_note_fields_backend.js create mode 100644 addons/account/static/src/scss/section_and_note_backend.scss delete mode 100644 addons/l10n_in_sale/views/report_invoice_view.xml delete mode 100644 addons/sale/models/sale_layout.py delete mode 100644 addons/sale/views/account_invoice_views.xml delete mode 100644 addons/sale/views/sale_layout_category_view.xml diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 05035015de2..9b8afa4791c 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -1007,6 +1007,8 @@ class AccountInvoice(models.Model): def get_taxes_values(self): tax_grouped = {} for line in self.invoice_line_ids: + if not line.account_id: + continue price_unit = line.price_unit * (1 - (line.discount or 0.0) / 100.0) taxes = line.invoice_line_tax_ids.compute_all(price_unit, self.currency_id, line.quantity, line.product_id, self.partner_id)['taxes'] for tax in taxes: @@ -1089,6 +1091,8 @@ class AccountInvoice(models.Model): def invoice_line_move_line_get(self): res = [] for line in self.invoice_line_ids: + if not line.account_id: + continue if line.quantity==0: continue tax_ids = [] @@ -1193,8 +1197,8 @@ class AccountInvoice(models.Model): for inv in self: if not inv.journal_id.sequence_id: raise UserError(_('Please define sequence on the journal related to this invoice.')) - if not inv.invoice_line_ids: - raise UserError(_('Please create some invoice lines.')) + if not inv.invoice_line_ids.filtered(lambda line: line.account_id): + raise UserError(_('Please add at least one invoice line.')) if inv.move_id: continue @@ -1575,8 +1579,7 @@ class AccountInvoiceLine(models.Model): product_id = fields.Many2one('product.product', string='Product', ondelete='restrict', index=True) product_image = fields.Binary('Product Image', related="product_id.image", store=False) - account_id = fields.Many2one('account.account', string='Account', - required=True, domain=[('deprecated', '=', False)], + account_id = fields.Many2one('account.account', string='Account', domain=[('deprecated', '=', False)], default=_default_account, help="The income or expense account related to the selected product.") price_unit = fields.Float(string='Unit Price', required=True, digits=dp.get_precision('Product Price')) @@ -1605,6 +1608,10 @@ class AccountInvoiceLine(models.Model): company_currency_id = fields.Many2one('res.currency', related='invoice_id.company_currency_id', readonly=True, related_sudo=False) is_rounding_line = fields.Boolean(string='Rounding Line', help='Is a rounding line in case of cash rounding.') + display_type = fields.Selection([ + ('line_section', "Section"), + ('line_note', "Note")], default=False, help="Technical field for UX purpose.") + @api.model def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): res = super(AccountInvoiceLine, self).fields_view_get( @@ -1778,6 +1785,29 @@ class AccountInvoiceLine(models.Model): } return data + @api.model + def create(self, vals): + if vals.get('display_type', self.default_get(['display_type'])['display_type']): + vals.update(price_unit=0, account_id=False, quantity=0) + + return super(AccountInvoiceLine, self).create(vals) + + @api.multi + def write(self, values): + if 'display_type' in values and self.filtered(lambda line: line.display_type != values.get('display_type')): + raise UserError("You cannot change the type of an invoice line. Instead you should delete the current line and create a new line of the proper type.") + return super(AccountInvoiceLine, self).write(values) + + _sql_constraints = [ + ('accountable_required_fields', + "CHECK(display_type IS NOT NULL OR account_id IS NOT NULL)", + "Missing required account on accountable invoice line."), + + ('non_accountable_fields_null', + "CHECK(display_type IS NULL OR (price_unit = 0 AND account_id IS NULL and quantity = 0))", + "Forbidden unit price, account and quantity on non-accountable invoice line"), + ] + class AccountInvoiceTax(models.Model): _name = "account.invoice.tax" diff --git a/addons/account/report/account_invoice_report.py b/addons/account/report/account_invoice_report.py index 0282010032e..0ec63187a9a 100644 --- a/addons/account/report/account_invoice_report.py +++ b/addons/account/report/account_invoice_report.py @@ -27,6 +27,7 @@ class AccountInvoiceReport(models.Model): record.user_currency_price_average = base_currency_id._convert(record.price_average, user_currency_id, company, date) record.user_currency_residual = base_currency_id._convert(record.residual, user_currency_id, company, date) + number = fields.Char('Invoice Number', readonly=True) date = fields.Date(readonly=True) product_id = fields.Many2one('product.product', string='Product', readonly=True) product_qty = fields.Float(string='Product Quantity', readonly=True) @@ -73,7 +74,7 @@ class AccountInvoiceReport(models.Model): 'account.invoice': [ 'account_id', 'amount_total_company_signed', 'commercial_partner_id', 'company_id', 'currency_id', 'date_due', 'date_invoice', 'fiscal_position_id', - 'journal_id', 'partner_bank_id', 'partner_id', 'payment_term_id', + 'journal_id', 'number', 'partner_bank_id', 'partner_id', 'payment_term_id', 'residual', 'state', 'type', 'user_id', ], 'account.invoice.line': [ @@ -89,7 +90,7 @@ class AccountInvoiceReport(models.Model): def _select(self): select_str = """ - SELECT sub.id, sub.date, sub.product_id, sub.partner_id, sub.country_id, sub.account_analytic_id, + SELECT sub.id, sub.number, sub.date, sub.product_id, sub.partner_id, sub.country_id, sub.account_analytic_id, sub.payment_term_id, sub.uom_name, sub.currency_id, sub.journal_id, sub.fiscal_position_id, sub.user_id, sub.company_id, sub.nbr, sub.type, sub.state, sub.categ_id, sub.date_due, sub.account_id, sub.account_line_id, sub.partner_bank_id, @@ -102,6 +103,7 @@ class AccountInvoiceReport(models.Model): select_str = """ SELECT ail.id AS id, ai.date_invoice AS date, + ai.number as number, ail.product_id, ai.partner_id, ai.payment_term_id, ail.account_analytic_id, u2.name AS uom_name, ai.currency_id, ai.journal_id, ai.fiscal_position_id, ai.user_id, ai.company_id, @@ -161,7 +163,7 @@ class AccountInvoiceReport(models.Model): WITH currency_rate AS (%s) %s FROM ( - %s %s %s + %s %s WHERE ail.account_id IS NOT NULL %s ) AS sub LEFT JOIN currency_rate cr ON (cr.currency_id = sub.currency_id AND diff --git a/addons/account/static/src/js/section_and_note_fields_backend.js b/addons/account/static/src/js/section_and_note_fields_backend.js new file mode 100644 index 00000000000..0c3039ce477 --- /dev/null +++ b/addons/account/static/src/js/section_and_note_fields_backend.js @@ -0,0 +1,194 @@ + +odoo.define('account.section_and_note_backend', function (require) { +// The goal of this file is to contain JS hacks related to allowing +// section and note on sale order and invoice. + +"use strict"; +var ListRenderer = require('web.ListRenderer'); +var fieldRegistry = require('web.field_registry'); +var FieldOne2Many = require('web.relational_fields').FieldOne2Many; +var InputField = require('web.basic_fields').InputField; +var TranslatableFieldMixin = require('web.basic_fields').TranslatableFieldMixin; +var dom = require('web.dom'); + +var SectionAndNoteListRenderer = ListRenderer.extend({ + /** + * We want section and note to take the whole line (except handle and trash) + * to look better and to hide the unnecessary fields. + * + * @override + */ + _renderBodyCell: function (record, node, index, options) { + var $cell = this._super.apply(this, arguments); + + var isSection = record.data.display_type === 'line_section'; + var isNote = record.data.display_type === 'line_note'; + + if (isSection || isNote) { + if (node.attrs.widget === "handle") { + return $cell; + } else if (node.attrs.name === "name") { + var nbrColumns = this._getNumberOfCols(); + if (this.handleField) { + nbrColumns--; + } + if (this.addTrashIcon) { + nbrColumns--; + } + $cell.attr('colspan', nbrColumns); + } else { + return $cell.addClass('o_hidden'); + } + } + + return $cell; + }, + /** + * We add the o_is_{display_type} class to allow custom behaviour both in JS and CSS. + * + * @override + */ + _renderRow: function (record, index) { + var $row = this._super.apply(this, arguments); + + if (record.data.display_type) { + $row.addClass('o_is_' + record.data.display_type); + } + + return $row; + }, + /** + * We want to add .o_section_and_note_list_view on the table to have stronger CSS. + * + * @override + * @private + */ + _renderView: function () { + var def = this._super(); + this.$el.find('> table').addClass('o_section_and_note_list_view'); + return def; + }, +}); + +// We create a custom widget because this is the cleanest way to do it: +// to be sure this custom code will only impact selected fields having the widget +// and not applied to any other existing ListRenderer. +var SectionAndNoteFieldOne2Many = FieldOne2Many.extend({ + /** + * We want to use our custom renderer for the list. + * + * @override + */ + _getRenderer: function () { + if (this.view.arch.tag === 'tree') { + return SectionAndNoteListRenderer; + } + return this._super.apply(this, arguments); + }, +}); + +// This is a merge between a FieldText and a FieldChar. +// Indeed, we want a FieldChar for section, and a FieldText for the rest (product and note). +var SectionAndNoteFieldText = InputField.extend(TranslatableFieldMixin, { + /** + * @constructor + */ + init: function (parent, name, record, options) { + this.isSection = record.data.display_type === 'line_section'; + + if (!this.isSection) { + this.className = 'o_field_text'; + this.supportedFieldTypes = ['text']; + this.tagName = 'span'; + } + if (this.isSection) { + this.className = 'o_field_char'; + this.tagName = 'span'; + this.supportedFieldTypes = ['char']; + } + + this._super.apply(this, arguments); + + if (!this.isSection) { + if (this.mode === 'edit') { + this.tagName = 'textarea'; + } + } + }, + /** + * As it it done in the start function, the autoresize is done only once. + * + * @override + */ + start: function () { + if (!this.isSection) { + if (this.mode === 'edit') { + dom.autoresize(this.$el, {parent: this}); + + this.$el = this.$el.add(this._renderTranslateButton()); + } + } + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Stops the enter navigation in a text area. + * + * @private + * @param {OdooEvent} ev + */ + _onKeydown: function (ev) { + if (!this.isSection) { + if (ev.which === $.ui.keyCode.ENTER) { + return; + } + } + this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Add translation button + * + * @override + * @private + */ + _renderEdit: function () { + var def = this._super.apply(this, arguments); + if (this.isSection) { + if (this.field.size && this.field.size > 0) { + this.$el.attr('maxlength', this.field.size); + } + this.$el = this.$el.add(this._renderTranslateButton()); + } + return def; + }, + /** + * Trim the value input by the user. + * + * @override + * @private + * @param {any} value + * @param {Object} [options] + */ + _setValue: function (value, options) { + if (this.isSection) { + if (this.field.trim) { + value = value.trim(); + } + } + return this._super(value, options); + }, +}); + +fieldRegistry.add('section_and_note_one2many', SectionAndNoteFieldOne2Many); +fieldRegistry.add('section_and_note_text', SectionAndNoteFieldText); + +}); diff --git a/addons/account/static/src/scss/section_and_note_backend.scss b/addons/account/static/src/scss/section_and_note_backend.scss new file mode 100644 index 00000000000..2c86b3e241f --- /dev/null +++ b/addons/account/static/src/scss/section_and_note_backend.scss @@ -0,0 +1,17 @@ + +// The goal of this file is to contain CSS hacks related to allowing +// section and note on sale order and invoice. + +table.o_section_and_note_list_view tr.o_data_row.o_is_line_note, +div.oe_kanban_card.o_is_line_note { + color: #999; +} +table.o_section_and_note_list_view tr.o_data_row.o_is_line_section, +div.oe_kanban_card.o_is_line_section { + font-weight: bold; + background-color: #EEE; +} +table.o_section_and_note_list_view tr.o_data_row.o_is_line_section { + border-top: 1px solid #BBB; + border-bottom: 1px solid #BBB; +} diff --git a/addons/account/views/account.xml b/addons/account/views/account.xml index e7218152387..ca67c3a11d3 100644 --- a/addons/account/views/account.xml +++ b/addons/account/views/account.xml @@ -14,6 +14,8 @@ + + @@ -21,6 +23,8 @@ + + diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml index 8327d889601..16c42777562 100644 --- a/addons/account/views/account_invoice_view.xml +++ b/addons/account/views/account_invoice_view.xml @@ -47,18 +47,35 @@ - + + + account.invoice.line.form account.invoice.line -
- + + + + + @@ -70,13 +87,6 @@ - - @@ -86,7 +96,9 @@ -