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 @@ -