From 079a13a349c942ab69ca704263bc5cb52c391d92 Mon Sep 17 00:00:00 2001 From: Joren Van Onder Date: Wed, 31 Jan 2018 15:22:39 +0100 Subject: [PATCH] [IMP] analytic: in-depth refactoring * analytic accounts can be shared among companies * amount on analytic lines always refer to amount in company currency of which it belongs to * add analytic groups (hierarchical way to classify analytic accounts) * analytic tags: - only present on analytic lines, not anymore on analytic accounts - can be used to distribue/split an analytic lines among several analytic accounts - enabled by a dedicated res.group - added on several objects (vouchers, expenses, analytic defaults...) as well as in bank statement reconciliation widget (manual counterpart and account.reconcile.model) * bunch of fixes made related to analytic Was PR #22124. Was task 32386. Original work by tbe. --- addons/account/models/account.py | 7 +- .../account/models/account_analytic_line.py | 2 +- addons/account/models/account_invoice.py | 10 +- addons/account/models/account_move.py | 50 ++++++++-- addons/account/models/res_config_settings.py | 1 + .../js/reconciliation/reconciliation_model.js | 36 ++++++- .../reconciliation/reconciliation_renderer.js | 16 +++- .../static/src/xml/account_reconciliation.xml | 6 +- .../static/tests/reconciliation_tests.js | 20 ++-- .../tests/test_templates_consistency.py | 2 +- .../account/views/account_analytic_view.xml | 34 +++---- addons/account/views/account_invoice_view.xml | 8 +- addons/account/views/account_menuitem.xml | 2 +- addons/account/views/account_view.xml | 15 +-- .../views/res_config_settings_views.xml | 20 +++- .../models/account_analytic_default.py | 17 +++- .../views/account_analytic_default_view.xml | 6 +- .../account_voucher/models/account_voucher.py | 2 + .../views/account_voucher_views.xml | 4 + .../analytic/data/analytic_account_demo.xml | 4 - addons/analytic/models/analytic_account.py | 81 +++++++++++++--- .../analytic/security/analytic_security.xml | 19 ++++ addons/analytic/security/ir.model.access.csv | 2 + .../analytic/views/analytic_account_views.xml | 96 ++++++++++++++----- addons/hr_expense/models/hr_expense.py | 2 + addons/hr_expense/views/hr_expense_views.xml | 3 + addons/purchase/models/res_config_settings.py | 2 - .../purchase/security/purchase_security.xml | 6 -- addons/purchase/views/purchase_views.xml | 8 +- .../views/res_config_settings_views.xml | 21 ---- .../views/purchase_requisition_views.xml | 6 +- addons/sale/views/sale_views.xml | 6 +- 32 files changed, 374 insertions(+), 140 deletions(-) diff --git a/addons/account/models/account.py b/addons/account/models/account.py index f04cc40538a..cabee127276 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -864,7 +864,10 @@ class AccountTax(models.Model): def get_grouping_key(self, invoice_tax_val): """ Returns a string that will be used to group account.invoice.tax sharing the same properties""" self.ensure_one() - return str(invoice_tax_val['tax_id']) + '-' + str(invoice_tax_val['account_id']) + '-' + str(invoice_tax_val['account_analytic_id']) + return str(invoice_tax_val['tax_id']) + '-' + \ + str(invoice_tax_val['account_id']) + '-' + \ + str(invoice_tax_val['account_analytic_id']) + '-' + \ + str(invoice_tax_val.get('analytic_tag_ids', [])) def _compute_amount(self, base_amount, price_unit, quantity=1.0, product=None, partner=None): """ Returns the amount of a single tax. base_amount is the actual amount on which the tax is applied, which is @@ -1048,6 +1051,7 @@ class AccountReconcileModel(models.Model): amount = fields.Float(digits=0, required=True, default=100.0, help="Fixed amount will count as a debit if it is negative, as a credit if it is positive.") tax_id = fields.Many2one('account.tax', string='Tax', ondelete='restrict') analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account', ondelete='set null') + analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags') second_account_id = fields.Many2one('account.account', string='Second Account', ondelete='cascade', domain=[('deprecated', '=', False)]) second_journal_id = fields.Many2one('account.journal', string='Second Journal', ondelete='cascade', help="This field is ignored in a bank statement reconciliation.") @@ -1059,6 +1063,7 @@ class AccountReconcileModel(models.Model): second_amount = fields.Float(string='Second Amount', digits=0, required=True, default=100.0, help="Fixed amount will count as a debit if it is negative, as a credit if it is positive.") second_tax_id = fields.Many2one('account.tax', string='Second Tax', ondelete='restrict', domain=[('type_tax_use', '=', 'purchase')]) second_analytic_account_id = fields.Many2one('account.analytic.account', string='Second Analytic Account', ondelete='set null') + second_analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Second Analytic Tags') @api.onchange('name') def onchange_name(self): diff --git a/addons/account/models/account_analytic_line.py b/addons/account/models/account_analytic_line.py index 38d9b5e57bd..feca837b5b7 100644 --- a/addons/account/models/account_analytic_line.py +++ b/addons/account/models/account_analytic_line.py @@ -13,7 +13,7 @@ class AccountAnalyticLine(models.Model): product_id = fields.Many2one('product.product', string='Product') general_account_id = fields.Many2one('account.account', string='Financial Account', ondelete='restrict', readonly=True, related='move_id.account_id', store=True, domain=[('deprecated', '=', False)]) - move_id = fields.Many2one('account.move.line', string='Move Line', ondelete='cascade', index=True) + move_id = fields.Many2one('account.move.line', string='Journal Item', ondelete='cascade', index=True) code = fields.Char(size=8) ref = fields.Char(string='Ref.') diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 2f82816d67d..f756ec42100 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -940,6 +940,7 @@ class AccountInvoice(models.Model): 'sequence': tax['sequence'], 'account_analytic_id': tax['analytic'] and line.account_analytic_id.id or False, 'account_id': self.type in ('out_invoice', 'in_invoice') and (tax['account_id'] or line.account_id.id) or (tax['refund_account_id'] or line.account_id.id), + 'analytic_tag_ids': tax['analytic'] and line.analytic_tag_ids.ids or False, } # If the taxes generate moves on the same financial account as the invoice line, @@ -948,7 +949,6 @@ class AccountInvoice(models.Model): # to ensure the tax move is allocated to the proper analytic account. if not vals.get('account_analytic_id') and line.account_analytic_id and vals['account_id'] == line.account_id.id: vals['account_analytic_id'] = line.account_analytic_id.id - return vals @api.multi @@ -1056,9 +1056,9 @@ class AccountInvoice(models.Model): 'product_id': line.product_id.id, 'uom_id': line.uom_id.id, 'account_analytic_id': line.account_analytic_id.id, + 'analytic_tag_ids': analytic_tag_ids, 'tax_ids': tax_ids, 'invoice_id': self.id, - 'analytic_tag_ids': analytic_tag_ids } res.append(move_line_dict) return res @@ -1075,6 +1075,8 @@ class AccountInvoice(models.Model): if tax.amount_type == "group": for child_tax in tax.children_tax_ids: done_taxes.append(child_tax.id) + + analytic_tag_ids = [(4, analytic_tag.id, None) for analytic_tag in tax_line.analytic_tag_ids] res.append({ 'invoice_tax_line_id': tax_line.id, 'tax_line_id': tax_line.tax_id.id, @@ -1085,6 +1087,7 @@ class AccountInvoice(models.Model): 'price': tax_line.amount_total, 'account_id': tax_line.account_id.id, 'account_analytic_id': tax_line.account_analytic_id.id, + 'analytic_tag_ids': analytic_tag_ids, 'invoice_id': self.id, 'tax_ids': [(6, 0, list(done_taxes))] if tax_line.tax_id.include_base_amount else [] }) @@ -1261,7 +1264,6 @@ class AccountInvoice(models.Model): 'debit': line['price'] > 0 and line['price'], 'credit': line['price'] < 0 and -line['price'], 'account_id': line['account_id'], - 'analytic_line_ids': line.get('analytic_line_ids', []), 'amount_currency': line['price'] > 0 and abs(line.get('amount_currency', False)) or -abs(line.get('amount_currency', False)), 'currency_id': line.get('currency_id', False), 'quantity': line.get('quantity', 1.00), @@ -1733,6 +1735,7 @@ class AccountInvoiceTax(models.Model): 'tax_id': tax.tax_id.id, 'account_id': tax.account_id.id, 'account_analytic_id': tax.account_analytic_id.id, + 'analytic_tag_ids': tax.analytic_tag_ids.ids or False, }) if tax.invoice_id and key in tax_grouped[tax.invoice_id.id]: tax.base = tax_grouped[tax.invoice_id.id][key]['base'] @@ -1744,6 +1747,7 @@ class AccountInvoiceTax(models.Model): tax_id = fields.Many2one('account.tax', string='Tax', ondelete='restrict') account_id = fields.Many2one('account.account', string='Tax Account', required=True, domain=[('deprecated', '=', False)]) account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic account') + analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags') amount = fields.Monetary('Tax Amount') amount_rounding = fields.Monetary('Amount Delta') amount_total = fields.Monetary(string="Amount Total", compute='_compute_amount_total') diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 07052fe5740..8d0c8d27bcd 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -173,6 +173,8 @@ class AccountMove(models.Model): for move in self: if not move.journal_id.update_posted: raise UserError(_('You cannot modify a posted entry of this journal.\nFirst you should set the journal to allow cancelling entries.')) + # We remove all the analytics entries for this journal + move.mapped('line_ids.analytic_line_ids').unlink() if self.ids: self.check_access_rights('write') self.check_access_rule('write') @@ -469,7 +471,7 @@ class AccountMoveLine(models.Model): tax_ids = fields.Many2many('account.tax', string='Taxes') tax_line_id = fields.Many2one('account.tax', string='Originator tax', ondelete='restrict') analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account') - analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic tags') + analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags') company_id = fields.Many2one('res.company', related='account_id.company_id', string='Company', store=True) counterpart = fields.Char("Counterpart", compute='_get_counterpart', help="Compute the counter part accounts of this journal item for this journal entry. This can be needed in reports.") @@ -1452,13 +1454,19 @@ class AccountMoveLine(models.Model): currency_id = invoice_currency.id return debit, credit, amount_currency, currency_id + def _get_analytic_tag_ids(self): + self.ensure_one() + return self.analytic_tag_ids.filtered(lambda r: not r.active_analytic_distribution).ids + @api.multi def create_analytic_lines(self): - """ Create analytic items upon validation of an account.move.line having an analytic account. This - method first remove any existing analytic item related to the line before creating any new one. + """ Create analytic items upon validation of an account.move.line having an analytic account or an analytic distribution. """ - self.mapped('analytic_line_ids').unlink() for obj_line in self: + for tag in obj_line.analytic_tag_ids.filtered('active_analytic_distribution'): + for distribution in tag.analytic_distribution_ids: + vals_line = obj_line._prepare_analytic_distribution_line(distribution) + self.env['account.analytic.line'].create(vals_line) if obj_line.analytic_account_id: vals_line = obj_line._prepare_analytic_line()[0] self.env['account.analytic.line'].create(vals_line) @@ -1473,16 +1481,40 @@ class AccountMoveLine(models.Model): 'name': self.name, 'date': self.date, 'account_id': self.analytic_account_id.id, - 'tag_ids': [(6, 0, self.analytic_tag_ids.ids)], + 'tag_ids': [(6, 0, self._get_analytic_tag_ids())], 'unit_amount': self.quantity, 'product_id': self.product_id and self.product_id.id or False, 'product_uom_id': self.product_uom_id and self.product_uom_id.id or False, - 'amount': self.company_currency_id.with_context(date=self.date or fields.Date.context_today(self)).compute(amount, self.analytic_account_id.currency_id) if self.analytic_account_id.currency_id else amount, + 'amount': amount, 'general_account_id': self.account_id.id, 'ref': self.ref, 'move_id': self.id, 'user_id': self.invoice_id.user_id.id or self._uid, 'partner_id': self.partner_id.id, + 'company_id': self.env.user.company_id.id, + } + + def _prepare_analytic_distribution_line(self, distribution): + """ Prepare the values used to create() an account.analytic.line upon validation of an account.move.line having + analytic tags with analytic distribution. + """ + self.ensure_one() + amount = -self.balance * distribution.percentage / 100.0 + return { + 'name': self.name, + 'date': self.date, + 'account_id': distribution.account_id.id, + 'partner_id': self.partner_id.id, + 'tag_ids': [(6, 0, [distribution.tag_id.id] + self._get_analytic_tag_ids())], + 'unit_amount': self.quantity, + 'product_id': self.product_id and self.product_id.id or False, + 'product_uom_id': self.product_uom_id and self.product_uom_id.id or False, + 'amount': amount, + 'general_account_id': self.account_id.id, + 'ref': self.ref, + 'move_id': self.id, + 'user_id': self.invoice_id.user_id.id or self._uid, + 'company_id': self.env.user.company_id.id } @api.model @@ -1528,7 +1560,7 @@ class AccountMoveLine(models.Model): domain += [('account_id', 'in', context['account_ids'].ids)] if context.get('analytic_tag_ids'): - domain += ['|', ('analytic_account_id.tag_ids', 'in', context['analytic_tag_ids'].ids), ('analytic_tag_ids', 'in', context['analytic_tag_ids'].ids)] + domain += [('analytic_tag_ids', 'in', context['analytic_tag_ids'].ids)] if context.get('analytic_account_ids'): domain += [('analytic_account_id', 'in', context['analytic_account_ids'].ids)] @@ -1689,6 +1721,8 @@ class AccountPartialReconcile(models.Model): 'debit': abs(rounded_amt) if rounded_amt < 0 else 0.0, 'credit': rounded_amt if rounded_amt > 0 else 0.0, 'account_id': line.account_id.id, + 'analytic_account_id': line.analytic_account_id.id, + 'analytic_tag_ids': line.analytic_tag_ids.ids, 'tax_exigible': True, 'amount_currency': self.amount_currency and line.currency_id.round(-line.amount_currency * amount / line.balance) or 0.0, 'currency_id': line.currency_id.id, @@ -1701,6 +1735,8 @@ class AccountPartialReconcile(models.Model): 'debit': rounded_amt if rounded_amt > 0 else 0.0, 'credit': abs(rounded_amt) if rounded_amt < 0 else 0.0, 'account_id': line.tax_line_id.cash_basis_account.id, + 'analytic_account_id': line.analytic_account_id.id, + 'analytic_tag_ids': line.analytic_tag_ids.ids, 'tax_line_id': line.tax_line_id.id, 'tax_exigible': True, 'amount_currency': self.amount_currency and line.currency_id.round(line.amount_currency * amount / line.balance) or 0.0, diff --git a/addons/account/models/res_config_settings.py b/addons/account/models/res_config_settings.py index d7a37b62a76..9b63d09aabe 100644 --- a/addons/account/models/res_config_settings.py +++ b/addons/account/models/res_config_settings.py @@ -28,6 +28,7 @@ class ResConfigSettings(models.TransientModel): module_account_accountant = fields.Boolean(string='Accounting') group_analytic_accounting = fields.Boolean(string='Analytic Accounting', implied_group='analytic.group_analytic_accounting') + group_analytic_tags = fields.Boolean(string='Analytic Tags', implied_group='analytic.group_analytic_tags') group_warning_account = fields.Boolean(string="Warnings in Invoices", implied_group='account.group_warning_account') group_cash_rounding = fields.Boolean(string="Cash Rounding", implied_group='account.group_cash_rounding') module_account_asset = fields.Boolean(string='Assets Management') diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index 249e8b20862..60cba94fffc 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -81,6 +81,8 @@ var _t = core._t; * id: integer * display_name: string * } + * analytic_tag_ids: { + * } * label: string * amount: number, * [journal_id]: { @@ -92,7 +94,7 @@ var _t = core._t; */ var StatementModel = BasicModel.extend({ avoidCreate: false, - quickCreateFields: ['account_id', 'amount', 'analytic_account_id', 'label', 'tax_id'], + quickCreateFields: ['account_id', 'amount', 'analytic_account_id', 'label', 'tax_id', 'analytic_tag_ids'], /** * @override @@ -449,7 +451,7 @@ var StatementModel = BasicModel.extend({ quickCreateProposition: function (handle, reconcileModelId) { var line = this.getLine(handle); var reconcileModel = _.find(this.reconcileModels, function (r) {return r.id === reconcileModelId;}); - var fields = ['account_id', 'amount', 'amount_type', 'analytic_account_id', 'journal_id', 'label', 'tax_id']; + var fields = ['account_id', 'amount', 'amount_type', 'analytic_account_id', 'journal_id', 'label', 'tax_id', 'analytic_tag_ids']; this._blurProposition(handle); var focus = this._formatQuickCreate(line, _.pick(reconcileModel, fields)); @@ -555,6 +557,7 @@ var StatementModel = BasicModel.extend({ * @returns {Deferred} */ updateProposition: function (handle, values) { + var self = this; var line = this.getLine(handle); var prop = _.last(_.filter(line.reconciliation_proposition, '__focus')); if (!prop) { @@ -562,7 +565,23 @@ var StatementModel = BasicModel.extend({ line.reconciliation_proposition.push(prop); } _.each(values, function (value, fieldName) { - prop[fieldName] = values[fieldName]; + if (fieldName === 'analytic_tag_ids') { + switch (value.operation) { + case "ADD_M2M": + if (!_.findWhere(prop.analytic_tag_ids, {id: value.ids.id})) { + prop.analytic_tag_ids.push(value.ids); + } + break; + case "FORGET": + var id = self.localData[value.ids[0]].ref; + prop.analytic_tag_ids = _.filter(prop.analytic_tag_ids, function (val) { + return val.id !== id; + }); + break; + } + } else { + prop[fieldName] = values[fieldName]; + } }); if ('account_id' in values) { prop.account_code = prop.account_id ? this.accounts[prop.account_id.id] : ''; @@ -857,6 +876,13 @@ var StatementModel = BasicModel.extend({ _formatNameGet: function (value) { return value ? (value.id ? value : {'id': value[0], 'display_name': value[1]}) : false; }, + _formatMany2ManyTags: function (value) { + var res = []; + for (var i=0, len=value.length; iTax - + Analytic Acc. + + Analytic Tags. + + diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js index 93e4a2f365b..1c413c422a2 100644 --- a/addons/account/static/tests/reconciliation_tests.js +++ b/addons/account/static/tests/reconciliation_tests.js @@ -105,6 +105,10 @@ var db = { {id: 9, display_name: "Delta PC - Delta PC"}, ] }, + 'account.analytic.tag': { + fields: {}, + records: [] + }, 'account.bank.statement': { fields: {}, reconciliation_widget_preprocess: function () { @@ -572,8 +576,8 @@ QUnit.module('account', { assert.strictEqual(widget.$('.o_input_dropdown input').val(), "Agrolait", "the partner many2one should display agrolait"); assert.strictEqual(clientAction.widgets[2].$('.o_input_dropdown input').val(), "Camptocamp", "the partner many2one should display Camptocamp"); widget.$('.accounting_view tfoot td:first').trigger('click'); - assert.strictEqual(widget.$('.create .o_input').length, 6, - "create panel should display 5 fields (account_id, tax_id, analytic_account_id, label, amount)"); + assert.strictEqual(widget.$('.create input.o_input').length, 7, + "create panel should contain 7 fields (account_id, tax_id, journal_id, analytic_account_id, analytic_tag_ids, label, amount)"); assert.strictEqual(widget.$('.create .create_account_id .o_required_modifier, .create .create_label .o_required_modifier, .create .create_amount .o_required_modifier').length, 3, "account_id, label and amount should be required fields"); assert.strictEqual(widget.$('.create .create_label input').val(), 'SAJ/2014/002 and SAJ/2014/003', @@ -643,13 +647,15 @@ QUnit.module('account', { "counterpart_aml_id": 109, "credit": 650, "debit": 0, - "name": "INV/2017/0002" + "name": "INV/2017/0002", + "analytic_tag_ids": [[6, null, []]] }, { "counterpart_aml_id": 112, "credit": 525, "debit": 0, - "name": "INV/2017/0003" + "name": "INV/2017/0003", + "analytic_tag_ids": [[6, null, []]] }], payment_aml_ids: [], new_aml_dicts: []}]], "Should call process_reconciliations with ids"); @@ -691,7 +697,8 @@ QUnit.module('account', { account_id: 287, credit: 1175, debit: 0, - name: 'SAJ/2014/002 and SAJ/2014/003' + name: 'SAJ/2014/002 and SAJ/2014/003', + analytic_tag_ids: [[6, null, []]] }]}]], "Should call process_reconciliations with ids"); }); @@ -736,7 +743,8 @@ QUnit.module('account', { counterpart_aml_id: 109, credit: 650, debit: 0, - name: 'INV/2017/0002' + name: 'INV/2017/0002', + analytic_tag_ids: [[6, null, []]] }], payment_aml_ids: [], new_aml_dicts: [{ diff --git a/addons/account/tests/test_templates_consistency.py b/addons/account/tests/test_templates_consistency.py index 74f86415e18..dcd631855b5 100644 --- a/addons/account/tests/test_templates_consistency.py +++ b/addons/account/tests/test_templates_consistency.py @@ -59,4 +59,4 @@ class AccountingTestTemplConsistency(HttpCase): '''Test fields consistency for ('account.reconcile.model', 'account.reconcile.model.template') ''' self.check_fields_consistency('account.reconcile.model.template', 'account.reconcile.model', exceptions=['chart_template_id']) - self.check_fields_consistency('account.reconcile.model', 'account.reconcile.model.template', exceptions=['company_id', 'journal_id', 'analytic_account_id', 'second_journal_id', 'second_analytic_account_id']) + self.check_fields_consistency('account.reconcile.model', 'account.reconcile.model.template', exceptions=['company_id', 'journal_id', 'analytic_account_id', 'second_journal_id', 'second_analytic_account_id', 'analytic_tag_ids', 'second_analytic_tag_ids']) diff --git a/addons/account/views/account_analytic_view.xml b/addons/account/views/account_analytic_view.xml index ab08b3c15bf..7874cd2a094 100644 --- a/addons/account/views/account_analytic_view.xml +++ b/addons/account/views/account_analytic_view.xml @@ -8,15 +8,20 @@ - - + + + + + + + @@ -73,29 +78,18 @@ parent="menu_analytic_accounting" groups="analytic.group_analytic_accounting" sequence="0"/> - - - Analytic Entries - account.analytic.line - pivot - - form - - - - - + + diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml index a2b4a4de431..d699ff237b9 100644 --- a/addons/account/views/account_invoice_view.xml +++ b/addons/account/views/account_invoice_view.xml @@ -75,7 +75,7 @@ - + @@ -272,7 +272,7 @@ - + @@ -295,6 +295,7 @@ + @@ -428,7 +429,7 @@ - + @@ -502,6 +503,7 @@ + diff --git a/addons/account/views/account_menuitem.xml b/addons/account/views/account_menuitem.xml index e4a9bc1f687..463c7f41823 100644 --- a/addons/account/views/account_menuitem.xml +++ b/addons/account/views/account_menuitem.xml @@ -39,7 +39,7 @@ - + diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml index 0fbf7970d78..0ce1fd32918 100644 --- a/addons/account/views/account_view.xml +++ b/addons/account/views/account_view.xml @@ -840,7 +840,8 @@ - + + @@ -862,7 +863,8 @@ - + + @@ -1185,9 +1187,9 @@ - - - + + + @@ -1227,6 +1229,7 @@ + @@ -1473,7 +1476,7 @@ domain="['|', ('parent_id', '=', False), ('is_company', '=', True)]"/> - + diff --git a/addons/account/views/res_config_settings_views.xml b/addons/account/views/res_config_settings_views.xml index 605d7e70ec9..8e77002ba10 100644 --- a/addons/account/views/res_config_settings_views.xml +++ b/addons/account/views/res_config_settings_views.xml @@ -359,8 +359,24 @@ -
-
+ + + +
+
+ +
+
+
diff --git a/addons/account_analytic_default/models/account_analytic_default.py b/addons/account_analytic_default/models/account_analytic_default.py index 9ef2add29bc..ac9c6e57be3 100644 --- a/addons/account_analytic_default/models/account_analytic_default.py +++ b/addons/account_analytic_default/models/account_analytic_default.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError class AccountAnalyticDefault(models.Model): @@ -12,6 +13,7 @@ class AccountAnalyticDefault(models.Model): sequence = fields.Integer(string='Sequence', help="Gives the sequence order when displaying a list of analytic distribution") analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account') + analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags') product_id = fields.Many2one('product.product', string='Product', ondelete='cascade', help="Select a product which will use analytic account specified in analytic default (e.g. create new customer invoice or Sales order if we select this product, it will automatically take this as an analytic account)") partner_id = fields.Many2one('res.partner', string='Partner', ondelete='cascade', help="Select a partner which will use analytic account specified in analytic default (e.g. create new customer invoice or Sales order if we select this partner, it will automatically take this as an analytic account)") user_id = fields.Many2one('res.users', string='User', ondelete='cascade', help="Select a user which will use analytic account specified in analytic default.") @@ -19,6 +21,11 @@ class AccountAnalyticDefault(models.Model): date_start = fields.Date(string='Start Date', help="Default start date for this Analytic Account.") date_stop = fields.Date(string='End Date', help="Default end date for this Analytic Account.") + @api.constrains('analytic_id', 'analytic_tag_ids') + def _check_account_or_tags(self): + if any(not default.analytic_id and not default.analytic_tag_ids for default in self): + raise ValidationError(_('An analytic default requires at least an analytic account or an analytic tag.')) + @api.model def account_get(self, product_id=None, partner_id=None, user_id=None, date=None, company_id=None): domain = [] @@ -62,13 +69,17 @@ class AccountInvoiceLine(models.Model): rec = self.env['account.analytic.default'].account_get(self.product_id.id, self.invoice_id.partner_id.id, self.env.uid, fields.Date.today(), company_id=self.company_id.id) self.account_analytic_id = rec.analytic_id.id + self.analytic_tag_ids = rec.analytic_tag_ids.ids return res def _set_additional_fields(self, invoice): - if not self.account_analytic_id: + if not self.account_analytic_id or not self.analytic_tag_ids: rec = self.env['account.analytic.default'].account_get( self.product_id.id, self.invoice_id.partner_id.id, self.env.uid, fields.Date.today(), company_id=self.company_id.id) if rec: - self.account_analytic_id = rec.analytic_id.id + if self.account_analytic_id: + self.account_analytic_id = rec.analytic_id.id + if self.analytic_tag_ids: + self.analytic_tag_ids = rec.analytic_tag_ids.ids super(AccountInvoiceLine, self)._set_additional_fields(invoice) diff --git a/addons/account_analytic_default/views/account_analytic_default_view.xml b/addons/account_analytic_default/views/account_analytic_default_view.xml index ce58ef8d9b4..60723e1c2ac 100644 --- a/addons/account_analytic_default/views/account_analytic_default_view.xml +++ b/addons/account_analytic_default/views/account_analytic_default_view.xml @@ -5,8 +5,9 @@ account.analytic.default - + + @@ -23,7 +24,8 @@
- + + diff --git a/addons/account_voucher/models/account_voucher.py b/addons/account_voucher/models/account_voucher.py index aac79a719f4..43437216da8 100644 --- a/addons/account_voucher/models/account_voucher.py +++ b/addons/account_voucher/models/account_voucher.py @@ -280,6 +280,7 @@ class AccountVoucher(models.Model): 'move_id': move_id, 'partner_id': self.partner_id.id, 'analytic_account_id': line.account_analytic_id and line.account_analytic_id.id or False, + 'analytic_tag_ids': [(6, 0, line.analytic_tag_ids.ids)], 'quantity': 1, 'credit': abs(amount) if self.voucher_type == 'sale' else 0.0, 'debit': abs(amount) if self.voucher_type == 'purchase' else 0.0, @@ -366,6 +367,7 @@ class AccountVoucherLine(models.Model): quantity = fields.Float(digits=dp.get_precision('Product Unit of Measure'), required=True, default=1) account_analytic_id = fields.Many2one('account.analytic.account', 'Analytic Account') + analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags') company_id = fields.Many2one('res.company', related='voucher_id.company_id', string='Company', store=True, readonly=True) tax_ids = fields.Many2many('account.tax', string='Tax', help="Only for tax excluded from price") currency_id = fields.Many2one('res.currency', related='voucher_id.currency_id') diff --git a/addons/account_voucher/views/account_voucher_views.xml b/addons/account_voucher/views/account_voucher_views.xml index 86f2248aab1..1e1e0099590 100644 --- a/addons/account_voucher/views/account_voucher_views.xml +++ b/addons/account_voucher/views/account_voucher_views.xml @@ -86,6 +86,7 @@ + @@ -104,6 +105,7 @@ + @@ -252,6 +254,7 @@ + @@ -368,6 +371,7 @@ + diff --git a/addons/analytic/data/analytic_account_demo.xml b/addons/analytic/data/analytic_account_demo.xml index e1c2244f1a8..919a720e9f2 100644 --- a/addons/analytic/data/analytic_account_demo.xml +++ b/addons/analytic/data/analytic_account_demo.xml @@ -10,22 +10,18 @@ Our Super Product - Seagate P2 - Millennium Industries - CampToCamp - Agrolait diff --git a/addons/analytic/models/analytic_account.py b/addons/analytic/models/analytic_account.py index 47fee210a3f..d6ee2b69275 100644 --- a/addons/analytic/models/analytic_account.py +++ b/addons/analytic/models/analytic_account.py @@ -1,16 +1,57 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models +from odoo import api, fields, models, _ +from odoo.osv import expression +from odoo.exceptions import ValidationError +class AccountAnalyticDistribution(models.Model): + _name = 'account.analytic.distribution' + _rec_name = 'account_id' + + account_id = fields.Many2one('account.analytic.account', string='Analytic Account', required=True) + percentage = fields.Float(string='Percentage', required=True, default=100.0) + name = fields.Char(string='Name', related='account_id.name') + tag_id = fields.Many2one('account.analytic.tag', string="Parent tag", required=True) + + _sql_constraints = [ + ('check_percentage', 'CHECK(percentage >= 0 AND percentage <= 100)', + 'The percentage of an analytic distribution should be between 0 and 100.') + ] + class AccountAnalyticTag(models.Model): _name = 'account.analytic.tag' _description = 'Analytic Tags' name = fields.Char(string='Analytic Tag', index=True, required=True) color = fields.Integer('Color Index') active = fields.Boolean(default=True, help="Set active to false to hide the Analytic Tag without removing it.") + active_analytic_distribution = fields.Boolean('Analytic Distribution') + analytic_distribution_ids = fields.One2many('account.analytic.distribution', 'tag_id', string="Analytic Accounts") + company_id = fields.Many2one('res.company', string='Company') +class AccountAnalyticGroup(models.Model): + _name = 'account.analytic.group' + _description = 'Analytic Categories' + _parent_store = True + _rec_name = 'complete_name' + + name = fields.Char(required=True) + description = fields.Text(string='Description') + parent_id = fields.Many2one('account.analytic.group', string="Parent", ondelete='cascade') + parent_left = fields.Integer('Left Parent', index=True) + parent_right = fields.Integer('Right Parent', index=True) + children_ids = fields.One2many('account.analytic.group', 'parent_id', string="Childrens") + complete_name = fields.Char('Complete Name', compute='_compute_complete_name', store=True) + company_id = fields.Many2one('res.company', string='Company') + + @api.depends('name', 'parent_id.complete_name') + def _compute_complete_name(self): + for group in self: + if group.parent_id: + group.complete_name = '%s / %s' % (group.parent_id.complete_name, group.name) + else: + group.complete_name = group.name class AccountAnalyticAccount(models.Model): _name = 'account.analytic.account' @@ -20,22 +61,31 @@ class AccountAnalyticAccount(models.Model): @api.multi def _compute_debit_credit_balance(self): + res_currency_obj = self.env['res.currency'] analytic_line_obj = self.env['account.analytic.line'] domain = [('account_id', 'in', self.mapped('id'))] if self._context.get('from_date', False): domain.append(('date', '>=', self._context['from_date'])) if self._context.get('to_date', False): domain.append(('date', '<=', self._context['to_date'])) + if self._context.get('tag_ids'): + tag_domain = expression.OR([[('tag_ids', 'in', [tag])] for tag in self._context['tag_ids']]) + domain = expression.AND([domain, tag_domain]) + if self._context.get('company_ids'): + domain.append(('company_id', 'in', self._context['company_ids'])) - account_amounts = analytic_line_obj.search_read(domain, ['account_id', 'amount']) + user_currency = self.env.user.company_id.currency_id + account_amounts = analytic_line_obj.search_read(domain, ['account_id', 'amount', 'currency_id']) account_ids = set([line['account_id'][0] for line in account_amounts]) data_debit = {account_id: 0.0 for account_id in account_ids} data_credit = {account_id: 0.0 for account_id in account_ids} for account_amount in account_amounts: - if account_amount['amount'] < 0.0: - data_debit[account_amount['account_id'][0]] += account_amount['amount'] + currency_id = account_amount['currency_id'][0] + amount = res_currency_obj.browse(currency_id).compute(account_amount['amount'], user_currency) + if amount < 0.0: + data_debit[account_amount['account_id'][0]] += amount else: - data_credit[account_amount['account_id'][0]] += account_amount['amount'] + data_credit[account_amount['account_id'][0]] += amount for account in self: account.debit = abs(data_debit.get(account.id, 0.0)) @@ -46,10 +96,11 @@ class AccountAnalyticAccount(models.Model): code = fields.Char(string='Reference', index=True, track_visibility='onchange') active = fields.Boolean('Active', help="If the active field is set to False, it will allow you to hide the account without removing it.", default=True) - tag_ids = fields.Many2many('account.analytic.tag', 'account_analytic_account_tag_rel', 'account_id', 'tag_id', string='Tags', copy=True) + group_id = fields.Many2one('account.analytic.group', string='Group') + line_ids = fields.One2many('account.analytic.line', 'account_id', string="Analytic Lines") - company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.user.company_id) + company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.user.company_id) # use auto_join to speed up name_search call partner_id = fields.Many2one('res.partner', string='Customer', auto_join=True, track_visibility='onchange') @@ -66,9 +117,9 @@ class AccountAnalyticAccount(models.Model): for analytic in self: name = analytic.name if analytic.code: - name = '['+analytic.code+'] '+name + name = '[' + analytic.code + '] ' + name if analytic.partner_id: - name = name +' - '+analytic.partner_id.commercial_partner_id.name + name = name + ' - ' + analytic.partner_id.commercial_partner_id.name res.append((analytic.id, name)) return res @@ -101,8 +152,14 @@ class AccountAnalyticLine(models.Model): account_id = fields.Many2one('account.analytic.account', 'Analytic Account', required=True, ondelete='restrict', index=True) partner_id = fields.Many2one('res.partner', string='Partner') user_id = fields.Many2one('res.users', string='User', default=_default_user) - tag_ids = fields.Many2many('account.analytic.tag', 'account_analytic_line_tag_rel', 'line_id', 'tag_id', string='Tags', copy=True) + company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, default=lambda self: self.env.user.company_id) + currency_id = fields.Many2one(related="company_id.currency_id", string="Currency", readonly=True, store=True) + group_id = fields.Many2one('account.analytic.group', related='account_id.group_id', store=True, readonly=True) - company_id = fields.Many2one(related='account_id.company_id', string='Company', store=True, readonly=True) - currency_id = fields.Many2one(related="company_id.currency_id", string="Currency", readonly=True) + @api.multi + @api.constrains('company_id', 'account_id') + def _check_company_id(self): + for line in self: + if line.account_id.company_id and line.company_id.id != line.account_id.company_id.id: + raise ValidationError(_('The selected account belongs to another company that the one you\'re trying to create an analytic item for')) diff --git a/addons/analytic/security/analytic_security.xml b/addons/analytic/security/analytic_security.xml index 12e2ed75cbb..5e66d317f5c 100644 --- a/addons/analytic/security/analytic_security.xml +++ b/addons/analytic/security/analytic_security.xml @@ -15,6 +15,20 @@ ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + + + Analytic line multi company rule + + + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + + + + Analytic line multi company rule + + + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + @@ -23,5 +37,10 @@ + + Analytic Accounting Tags + + + diff --git a/addons/analytic/security/ir.model.access.csv b/addons/analytic/security/ir.model.access.csv index 32ee04d9f9d..0958e76401d 100644 --- a/addons/analytic/security/ir.model.access.csv +++ b/addons/analytic/security/ir.model.access.csv @@ -2,3 +2,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_account_analytic_account,access_account_analytic_account,model_account_analytic_account,group_analytic_accounting,1,1,1,1 access_account_analytic_line,access_account_analytic_line,model_account_analytic_line,group_analytic_accounting,1,1,1,1 access_account_analytic_tag,access_account_analytic_tag,model_account_analytic_tag,group_analytic_accounting,1,1,1,1 +access_account_analytic_group,access_account_analytic_group,model_account_analytic_group,group_analytic_accounting,1,1,1,1 +access_account_analytic_distribution,access_account_analytic_distribution,model_account_analytic_distribution,group_analytic_accounting,1,1,1,1 diff --git a/addons/analytic/views/analytic_account_views.xml b/addons/analytic/views/analytic_account_views.xml index 4a8df66015d..f5afe619774 100644 --- a/addons/analytic/views/analytic_account_views.xml +++ b/addons/analytic/views/analytic_account_views.xml @@ -4,7 +4,7 @@ account.analytic.tag.tree account.analytic.tag - + @@ -14,7 +14,7 @@ account.analytic.tag.form account.analytic.tag - +
+ + + + + + + +
- Analytic Accounts Tags + Analytic Tags account.analytic.tag form tree,form @@ -41,6 +50,51 @@
+ + account.analytic.group.form + account.analytic.group + +
+ + + + + + + + + + + +
+
+ + + account.analytic.group.tree + account.analytic.group + + + + + + + + + Analytic Account Groups + account.analytic.group + form + tree,form + +

+ Click to add a new analytic account group. +

+

+ This allows to classify your analytic accounts. +

+
+
+ + account.analytic.line.tree account.analytic.line @@ -56,7 +110,7 @@
- + {'search_default_group_date': 1, 'default_account_id': active_id} [('account_id','=', active_id)] @@ -111,7 +165,7 @@ - + @@ -131,7 +185,7 @@ - + @@ -149,7 +203,6 @@ - @@ -179,7 +232,6 @@ - @@ -226,22 +278,20 @@ 1
- - - - - - - - - - - - - - + + + + + + + - + + + + + +
diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index 39d062820a9..a7dead17eba 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -44,6 +44,7 @@ class HrExpense(models.Model): currency_id = fields.Many2one('res.currency', string='Currency', readonly=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]}, default=lambda self: self.env.user.company_id.currency_id) company_currency_id = fields.Many2one('res.currency', string="Report Company Currency", related='sheet_id.currency_id', store=True) analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account', states={'post': [('readonly', True)], 'done': [('readonly', True)]}, oldname='analytic_account') + analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags', states={'post': [('readonly', True)], 'done': [('readonly', True)]}) account_id = fields.Many2one('account.account', string='Account', states={'post': [('readonly', True)], 'done': [('readonly', True)]}, default=_default_account_id, help="An expense account is expected") description = fields.Text('Notes...', readonly=True, states={'draft': [('readonly', False)], 'reported': [('readonly', False)], 'refused': [('readonly', False)]}) payment_mode = fields.Selection([ @@ -273,6 +274,7 @@ class HrExpense(models.Model): 'product_id': expense.product_id.id, 'product_uom_id': expense.product_uom_id.id, 'analytic_account_id': expense.analytic_account_id.id, + 'analytic_tag_ids': [(6, 0, expense.analytic_tag_ids.ids)], 'expense_id': expense.id, 'partner_id': partner_id, 'tax_ids': [(6, 0, expense.tax_ids.ids)], diff --git a/addons/hr_expense/views/hr_expense_views.xml b/addons/hr_expense/views/hr_expense_views.xml index b15c6aaf637..e75e4467e72 100644 --- a/addons/hr_expense/views/hr_expense_views.xml +++ b/addons/hr_expense/views/hr_expense_views.xml @@ -26,6 +26,7 @@ + @@ -65,6 +66,7 @@ + @@ -443,6 +445,7 @@ +