From 7e3403068fc3fbc40182b3cfeb80e97a9300e8ff Mon Sep 17 00:00:00 2001 From: gawa-odoo Date: Thu, 28 Apr 2022 07:29:42 +0000 Subject: [PATCH] [REF] *: Analytic Apocalypse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The goal of this commit is to get rid of the analytic tags as they were confusing, serving tag purposes as well as distribution on analytic accounts. Everywhere analytic tags were used as a distribution have been replaced with a new widget that will dispatch distribution on analytic accounts. If there was an analytic account field next to the tags, it has been included in the distribution. Analytic tags that were used simply as information tags have been removed. To fill the new widget, there are now 2 kind of rules that will help fill and prefill it. The first are applicability: previous groups have been removed, and have by replaced by plans. Each account is required to have a plan. These plans define when they are available in the widget: a default applicability per plan and applicability lines that can specify rules following the context of the widget. The second one are distribution models, that will replace previous default rules but follow the same principles. The accounts (and so the plans) that will be given by the distribution model can override the applicability rules from before. closes odoo/odoo#98914 Related: odoo/upgrade#3885 Related: odoo/enterprise#30743 Signed-off-by: William André (wan) Co-authored-by: Habib (ayh) --- addons/account/__manifest__.py | 6 +- addons/account/models/__init__.py | 4 +- .../models/account_analytic_account.py | 64 ++ .../models/account_analytic_default.py | 68 -- .../account_analytic_distribution_model.py | 37 ++ .../account/models/account_analytic_line.py | 148 ++--- .../account/models/account_analytic_plan.py | 35 + addons/account/models/account_move.py | 19 +- addons/account/models/account_move_line.py | 192 ++---- .../models/account_move_line_tax_details.py | 4 +- .../account/models/account_reconcile_model.py | 13 +- addons/account/models/account_tax.py | 16 +- addons/account/models/res_config_settings.py | 1 - .../account/report/account_invoice_report.py | 4 +- .../report/account_invoice_report_view.xml | 11 - addons/account/security/account_security.xml | 8 - addons/account/security/ir.model.access.csv | 6 +- addons/account/tests/test_account_analytic.py | 189 +++++- .../tests/test_account_move_out_invoice.py | 88 +-- .../tests/test_account_move_reconcile.py | 5 +- .../tests/test_templates_consistency.py | 2 +- .../views/account_analytic_account_views.xml | 24 + .../views/account_analytic_default_view.xml | 119 ---- ...ount_analytic_distribution_model_views.xml | 38 ++ ...ew.xml => account_analytic_line_views.xml} | 30 +- .../views/account_analytic_plan_views.xml | 18 + addons/account/views/account_menuitem.xml | 8 +- addons/account/views/account_move_views.xml | 68 +- .../views/account_reconcile_model_views.xml | 20 +- .../views/res_config_settings_views.xml | 13 - addons/analytic/__manifest__.py | 14 +- .../analytic/data/analytic_account_demo.xml | 47 +- addons/analytic/data/analytic_data.xml | 9 + addons/analytic/data/analytic_demo.xml | 10 - addons/analytic/models/__init__.py | 4 + addons/analytic/models/analytic_account.py | 230 +++---- .../models/analytic_distribution_model.py | 79 +++ addons/analytic/models/analytic_line.py | 92 +++ addons/analytic/models/analytic_mixin.py | 49 ++ addons/analytic/models/analytic_plan.py | 222 +++++++ .../analytic/security/analytic_security.xml | 17 +- addons/analytic/security/ir.model.access.csv | 6 +- .../analytic_distribution.js | 603 ++++++++++++++++++ .../analytic_distribution.scss | 71 +++ .../analytic_distribution.xml | 97 +++ .../components/autocomplete/autocomplete.js | 14 + .../components/autocomplete/autocomplete.xml | 10 + .../tests/analytic_distribution_tests.js | 164 +++++ addons/analytic/tests/__init__.py | 3 + .../analytic/tests/test_analytic_account.py | 121 ++++ .../analytic/views/analytic_account_views.xml | 292 +-------- .../analytic_distribution_model_views.xml | 45 ++ addons/analytic/views/analytic_line_views.xml | 182 ++++++ addons/analytic/views/analytic_plan_views.xml | 81 +++ addons/hr_expense/data/hr_expense_demo.xml | 29 +- addons/hr_expense/models/hr_expense.py | 33 +- .../hr_expense/report/hr_expense_report.xml | 6 +- addons/hr_expense/tests/common.py | 3 + addons/hr_expense/tests/test_expenses.py | 22 +- addons/hr_expense/views/hr_expense_views.xml | 25 +- .../hr_timesheet/data/hr_timesheet_demo.xml | 41 -- addons/hr_timesheet/models/hr_timesheet.py | 13 +- addons/hr_timesheet/tests/test_timesheet.py | 48 +- .../hr_timesheet/views/hr_timesheet_views.xml | 3 - addons/hr_timesheet/views/project_views.xml | 1 - .../tests/test_analytic_account.py | 15 +- addons/project/data/project_demo.xml | 46 +- addons/project/models/__init__.py | 2 +- addons/project/models/analytic_account_tag.py | 10 - addons/project/models/company.py | 19 + addons/project/models/project.py | 11 +- addons/project/models/res_config_settings.py | 8 + .../tests/test_project_profitability.py | 5 + addons/project/views/project_views.xml | 2 - .../views/res_config_settings_views.xml | 4 + .../data/project_hr_expense_demo.xml | 4 +- addons/project_hr_expense/models/project.py | 17 +- .../tests/test_project_profitability.py | 2 +- .../data/project_purchase_demo.xml | 4 +- addons/project_purchase/models/project.py | 16 +- .../models/purchase_order_line.py | 10 +- .../models/project_project.py | 2 +- .../tests/test_project_profitability.py | 2 +- addons/purchase/models/purchase.py | 46 +- addons/purchase/report/purchase_report.py | 4 - .../purchase/tests/test_purchase_invoice.py | 48 +- addons/purchase/views/purchase_views.xml | 24 +- .../models/purchase_requisition.py | 6 +- .../views/purchase_requisition_views.xml | 16 +- .../purchase_stock/views/purchase_views.xml | 2 +- addons/sale/models/account_move_line.py | 45 +- addons/sale/models/sale_order.py | 20 +- addons/sale/models/sale_order_line.py | 40 +- addons/sale/security/ir.model.access.csv | 1 - addons/sale/tests/test_reinvoice.py | 24 +- addons/sale/tests/test_sale_flow.py | 6 + addons/sale/tests/test_sale_order.py | 28 +- addons/sale/tests/test_sale_to_invoice.py | 26 +- addons/sale/views/sale_order_views.xml | 22 +- .../sale/wizard/sale_make_invoice_advance.py | 16 +- addons/sale_expense/models/hr_expense.py | 18 +- addons/sale_expense/tests/test_reinvoice.py | 10 +- .../sale_expense/tests/test_sale_expense.py | 4 +- .../sale_expense/views/hr_expense_views.xml | 2 +- addons/sale_project/models/account_move.py | 6 +- addons/sale_project/models/project.py | 7 +- addons/sale_project/models/sale_order_line.py | 19 +- addons/sale_project/tests/common.py | 5 + .../sale_project/tests/test_sale_project.py | 5 + addons/sale_stock/views/sale_order_views.xml | 4 +- .../sale_timesheet/data/sale_service_demo.xml | 1 + .../static/tests/tours/sale_timesheet_tour.js | 6 +- addons/sale_timesheet/tests/common.py | 5 + addons/sale_timesheet/tests/test_reinvoice.py | 24 +- .../sale_timesheet/tests/test_sale_service.py | 4 +- addons/stock_account/models/account_move.py | 3 +- addons/stock_account/models/stock_move.py | 10 +- .../tests/legacy/core/py_utils_tests.js | 6 +- 118 files changed, 3047 insertions(+), 1589 deletions(-) create mode 100644 addons/account/models/account_analytic_account.py delete mode 100644 addons/account/models/account_analytic_default.py create mode 100644 addons/account/models/account_analytic_distribution_model.py create mode 100644 addons/account/models/account_analytic_plan.py create mode 100644 addons/account/views/account_analytic_account_views.xml delete mode 100644 addons/account/views/account_analytic_default_view.xml create mode 100644 addons/account/views/account_analytic_distribution_model_views.xml rename addons/account/views/{account_analytic_view.xml => account_analytic_line_views.xml} (75%) create mode 100644 addons/account/views/account_analytic_plan_views.xml create mode 100644 addons/analytic/data/analytic_data.xml delete mode 100644 addons/analytic/data/analytic_demo.xml create mode 100644 addons/analytic/models/analytic_distribution_model.py create mode 100644 addons/analytic/models/analytic_line.py create mode 100644 addons/analytic/models/analytic_mixin.py create mode 100644 addons/analytic/models/analytic_plan.py create mode 100644 addons/analytic/static/src/components/analytic_distribution/analytic_distribution.js create mode 100644 addons/analytic/static/src/components/analytic_distribution/analytic_distribution.scss create mode 100644 addons/analytic/static/src/components/analytic_distribution/analytic_distribution.xml create mode 100644 addons/analytic/static/src/components/autocomplete/autocomplete.js create mode 100644 addons/analytic/static/src/components/autocomplete/autocomplete.xml create mode 100644 addons/analytic/static/tests/analytic_distribution_tests.js create mode 100644 addons/analytic/tests/__init__.py create mode 100644 addons/analytic/tests/test_analytic_account.py create mode 100644 addons/analytic/views/analytic_distribution_model_views.xml create mode 100644 addons/analytic/views/analytic_line_views.xml create mode 100644 addons/analytic/views/analytic_plan_views.xml delete mode 100644 addons/project/models/analytic_account_tag.py create mode 100644 addons/project/models/company.py diff --git a/addons/account/__manifest__.py b/addons/account/__manifest__.py index 5518bce7bba..da47decc260 100644 --- a/addons/account/__manifest__.py +++ b/addons/account/__manifest__.py @@ -48,7 +48,10 @@ You could use this simplified accounting in case you work with an (external) acc 'wizard/account_validate_move_view.xml', 'views/res_company_views.xml', 'views/product_view.xml', - 'views/account_analytic_view.xml', + 'views/account_analytic_plan_views.xml', + 'views/account_analytic_account_views.xml', + 'views/account_analytic_distribution_model_views.xml', + 'views/account_analytic_line_views.xml', 'views/report_invoice.xml', 'report/account_invoice_report_view.xml', 'views/account_cash_rounding_view.xml', @@ -67,7 +70,6 @@ You could use this simplified accounting in case you work with an (external) acc 'report/account_hash_integrity_templates.xml', 'views/res_currency.xml', 'views/account_menuitem.xml', - 'views/account_analytic_default_view.xml', 'wizard/account_tour_upload_bill.xml', 'wizard/accrued_orders.xml', 'views/bill_preview_template.xml', diff --git a/addons/account/models/__init__.py b/addons/account/models/__init__.py index 989c64792a7..4bd291d3e70 100644 --- a/addons/account/models/__init__.py +++ b/addons/account/models/__init__.py @@ -12,13 +12,15 @@ from . import account_payment_term from . import account_move from . import account_move_line from . import account_move_line_tax_details -from . import account_analytic_default from . import account_partial_reconcile from . import account_full_reconcile from . import account_payment from . import account_payment_method from . import account_bank_statement from . import chart_template +from . import account_analytic_account +from . import account_analytic_distribution_model +from . import account_analytic_plan from . import account_analytic_line from . import account_journal_dashboard from . import product diff --git a/addons/account/models/account_analytic_account.py b/addons/account/models/account_analytic_account.py new file mode 100644 index 00000000000..9334690a8d3 --- /dev/null +++ b/addons/account/models/account_analytic_account.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- + +from odoo import api, fields, models + + +class AccountAnalyticAccount(models.Model): + _inherit = 'account.analytic.account' + + invoice_count = fields.Integer( + "Invoice Count", + compute='_compute_invoice_count', + ) + vendor_bill_count = fields.Integer( + "Vendor Bill Count", + compute='_compute_vendor_bill_count', + ) + + @api.depends('line_ids') + def _compute_invoice_count(self): + sale_types = self.env['account.move'].get_sale_types(include_receipts=True) + domain = [ + ('move_line_id.move_id.move_type', 'in', sale_types), + ('account_id', 'in', self.ids) + ] + groups = self.env['account.analytic.line']._read_group(domain, ['move_line_id.move_id:count_distinct'], ['account_id']) + moves_count_mapping = dict((g['account_id'][0], g['account_id_count']) for g in groups) + for account in self: + account.invoice_count = moves_count_mapping.get(account.id, 0) + + @api.depends('line_ids') + def _compute_vendor_bill_count(self): + purchase_types = self.env['account.move'].get_purchase_types(include_receipts=True) + domain = [ + ('move_line_id.move_id.move_type', 'in', purchase_types), + ('account_id', 'in', self.ids) + ] + groups = self.env['account.analytic.line']._read_group(domain, ['move_line_id.move_id:count_distinct'], ['account_id']) + moves_count_mapping = dict((g['account_id'][0], g['account_id_count']) for g in groups) + for account in self: + account.vendor_bill_count = moves_count_mapping.get(account.id, 0) + + def action_view_invoice(self): + self.ensure_one() + result = { + "type": "ir.actions.act_window", + "res_model": "account.move", + "domain": [('line_ids.analytic_distribution_stored_char', '=ilike', f'%"{self.id}":%'), ('move_type', 'in', self.env['account.move'].get_sale_types())], + "context": {"create": False}, + "name": "Customer Invoices", + 'view_mode': 'tree,form', + } + return result + + def action_view_vendor_bill(self): + self.ensure_one() + result = { + "type": "ir.actions.act_window", + "res_model": "account.move", + "domain": [('line_ids.analytic_distribution_stored_char', '=ilike', f'%"{self.id}":%'), ('move_type', 'in', self.env['account.move'].get_purchase_types())], + "context": {"create": False}, + "name": "Vendor Bills", + 'view_mode': 'tree,form', + } + return result diff --git a/addons/account/models/account_analytic_default.py b/addons/account/models/account_analytic_default.py deleted file mode 100644 index 8055bc9843e..00000000000 --- a/addons/account/models/account_analytic_default.py +++ /dev/null @@ -1,68 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import api, fields, models, _ -from odoo.exceptions import ValidationError - - -class AccountAnalyticDefault(models.Model): - _name = "account.analytic.default" - _description = "Analytic Distribution" - _rec_name = "analytic_id" - _order = "sequence" - - 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)") - account_id = fields.Many2one('account.account', string='Account', ondelete='cascade', help="Select an accounting account which will use analytic account specified in analytic default (e.g. create new customer invoice or Sales order if we select this account, 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.") - company_id = fields.Many2one('res.company', string='Company', ondelete='cascade', help="Select a company which will use analytic account specified in analytic default (e.g. create new customer invoice or Sales order if we select this company, it will automatically take this as an analytic account)") - 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 any(tag.analytic_distribution_ids for tag in default.analytic_tag_ids) - for default in self - ): - raise ValidationError(_('An analytic default requires an analytic account or an analytic tag used for analytic distribution.')) - - @api.model - def account_get(self, product_id=None, partner_id=None, account_id=None, user_id=None, date=None, company_id=None): - domain = [] - if product_id: - domain += ['|', ('product_id', '=', product_id)] - domain += [('product_id', '=', False)] - if partner_id: - domain += ['|', ('partner_id', '=', partner_id)] - domain += [('partner_id', '=', False)] - if account_id: - domain += ['|', ('account_id', '=', account_id)] - domain += [('account_id', '=', False)] - if company_id: - domain += ['|', ('company_id', '=', company_id)] - domain += [('company_id', '=', False)] - if user_id: - domain += ['|', ('user_id', '=', user_id)] - domain += [('user_id', '=', False)] - if date: - domain += ['|', ('date_start', '<=', date), ('date_start', '=', False)] - domain += ['|', ('date_stop', '>=', date), ('date_stop', '=', False)] - best_index = -1 - res = self.env['account.analytic.default'] - for rec in self.search(domain): - index = 0 - if rec.product_id: index += 1 - if rec.partner_id: index += 1 - if rec.account_id: index += 1 - if rec.company_id: index += 1 - if rec.user_id: index += 1 - if rec.date_start: index += 1 - if rec.date_stop: index += 1 - if index > best_index: - res = rec - best_index = index - return res diff --git a/addons/account/models/account_analytic_distribution_model.py b/addons/account/models/account_analytic_distribution_model.py new file mode 100644 index 00000000000..c398fa16120 --- /dev/null +++ b/addons/account/models/account_analytic_distribution_model.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- + +from odoo import fields, models + + +class AccountAnalyticDistributionModel(models.Model): + _inherit = 'account.analytic.distribution.model' + + account_prefix = fields.Char( + string='Financial Accounts Prefix', + help="Prefix that defines which accounts from the financial accounting this model should apply on.", + ) + product_id = fields.Many2one( + 'product.product', + string='Product', + ondelete='cascade', + help="Select a product for which the analytic distribution will be used (e.g. create new customer invoice or Sales order if we select this product, it will automatically take this as an analytic account)", + ) + product_categ_id = fields.Many2one( + 'product.category', + string='Product Category', + ondelete='cascade', + help="Select a product category 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)", + ) + + def _create_domain(self, fname, value): + if not fname == 'account_prefix': + return super()._create_domain(fname, value) + + def _check_score(self, key, value): + if key == 'account_prefix': + if value.startswith(self.account_prefix): + return 1 + else: + return -1 + else: + return super()._check_score(key, value) diff --git a/addons/account/models/account_analytic_line.py b/addons/account/models/account_analytic_line.py index 503949b3917..97c7d86e71b 100644 --- a/addons/account/models/account_analytic_line.py +++ b/addons/account/models/account_analytic_line.py @@ -1,129 +1,61 @@ # -*- coding: utf-8 -*- from odoo import api, fields, models, _ -from odoo.exceptions import UserError - - -class AccountAnalyticAccount(models.Model): - _inherit = 'account.analytic.account' - - invoice_count = fields.Integer("Invoice Count", compute='_compute_invoice_count') - vendor_bill_count = fields.Integer("Vendor Bill Count", compute='_compute_vendor_bill_count') - - @api.constrains('company_id') - def _check_company_consistency(self): - analytic_accounts = self.filtered('company_id') - - if not analytic_accounts: - return - - self.flush_recordset(['company_id']) - self.env['account.move.line'].flush_model(['analytic_account_id', 'company_id']) - self._cr.execute(''' - SELECT line.id - FROM account_move_line line - JOIN account_analytic_account account ON account.id = line.analytic_account_id - WHERE line.analytic_account_id IN %s - AND line.company_id != account.company_id - ''', [tuple(analytic_accounts.ids)]) - - if self._cr.fetchone(): - raise UserError(_("You can't set a different company on your analytic account since there are some journal items linked to it.")) - - @api.depends('line_ids') - def _compute_invoice_count(self): - sale_types = self.env['account.move'].get_sale_types() - domain = [ - ('move_id.state', '=', 'posted'), - ('move_id.move_type', 'in', sale_types), - ('analytic_account_id', 'in', self.ids) - ] - groups = self.env['account.move.line']._read_group(domain, ['move_id:count_distinct'], ['analytic_account_id']) - moves_count_mapping = dict((g['analytic_account_id'][0], g['move_id']) for g in groups) - for account in self: - account.invoice_count = moves_count_mapping.get(account.id, 0) - - @api.depends('line_ids') - def _compute_vendor_bill_count(self): - purchase_types = self.env['account.move'].get_purchase_types() - domain = [ - ('move_id.state', '=', 'posted'), - ('move_id.move_type', 'in', purchase_types), - ('analytic_account_id', 'in', self.ids) - ] - groups = self.env['account.move.line']._read_group(domain, ['move_id:count_distinct'], ['analytic_account_id']) - moves_count_mapping = dict((g['analytic_account_id'][0], g['move_id']) for g in groups) - for account in self: - account.vendor_bill_count = moves_count_mapping.get(account.id, 0) - - def action_view_invoice(self): - self.ensure_one() - result = { - "type": "ir.actions.act_window", - "res_model": "account.move", - "domain": [('id', 'in', self.line_ids.move_id.move_id.ids), ('move_type', 'in', self.env['account.move'].get_sale_types())], - "context": {"create": False}, - "name": "Customer Invoices", - 'view_mode': 'tree,form', - } - return result - - def action_view_vendor_bill(self): - self.ensure_one() - result = { - "type": "ir.actions.act_window", - "res_model": "account.move", - "domain": [('id', 'in', self.line_ids.move_id.move_id.ids), ('move_type', 'in', self.env['account.move'].get_purchase_types())], - "context": {"create": False}, - "name": "Vendor Bills", - 'view_mode': 'tree,form', - } - return result - -class AccountAnalyticTag(models.Model): - _inherit = 'account.analytic.tag' - - @api.constrains('company_id') - def _check_company_consistency(self): - analytic_tags = self.filtered('company_id') - - if not analytic_tags: - return - - self.flush_recordset(['company_id']) - self.env['account.move.line'].flush_model(['company_id']) - self._cr.execute(''' - SELECT line.id - FROM account_analytic_tag_account_move_line_rel tag_rel - JOIN account_analytic_tag tag ON tag.id = tag_rel.account_analytic_tag_id - JOIN account_move_line line ON line.id = tag_rel.account_move_line_id - WHERE tag_rel.account_analytic_tag_id IN %s - AND line.company_id != tag.company_id - ''', [tuple(analytic_tags.ids)]) - - if self._cr.fetchone(): - raise UserError(_("You can't set a different company on your analytic tags since there are some journal items linked to it.")) class AccountAnalyticLine(models.Model): _inherit = 'account.analytic.line' _description = 'Analytic Line' - product_id = fields.Many2one('product.product', string='Product', check_company=True) - general_account_id = fields.Many2one('account.account', string='Financial Account', ondelete='restrict', readonly=True, - related='move_id.account_id', store=True, domain="[('deprecated', '=', False), ('company_id', '=', company_id)]", - compute_sudo=True) - move_id = fields.Many2one('account.move.line', string='Journal Item', ondelete='cascade', index=True, check_company=True) + product_id = fields.Many2one( + 'product.product', + string='Product', + check_company=True, + ) + general_account_id = fields.Many2one( + 'account.account', + string='Financial Account', + ondelete='restrict', + domain="[('deprecated', '=', False), ('company_id', '=', company_id)]", + readonly=True, + related='move_line_id.account_id', + store=True, + compute_sudo=True, + ) + journal_id = fields.Many2one( + 'account.journal', + string='Financial Journal', + check_company=True, + readonly=True, + related='move_line_id.journal_id', + store=True, + ) + partner_id = fields.Many2one( + readonly=False, + compute="_compute_partner_id", + store=True, + ) + move_line_id = fields.Many2one( + 'account.move.line', + string='Journal Item', + ondelete='cascade', + index=True, + check_company=True, + ) code = fields.Char(size=8) ref = fields.Char(string='Ref.') category = fields.Selection(selection_add=[('invoice', 'Customer Invoice'), ('vendor_bill', 'Vendor Bill')]) + @api.depends('move_line_id') + def _compute_partner_id(self): + for line in self: + line.partner_id = line.move_line_id.partner_id or line.partner_id + @api.onchange('product_id', 'product_uom_id', 'unit_amount', 'currency_id') def on_change_unit_amount(self): if not self.product_id: return {} - result = 0.0 prod_accounts = self.product_id.product_tmpl_id.with_company(self.company_id)._get_product_accounts() unit = self.product_uom_id account = prod_accounts['expense'] diff --git a/addons/account/models/account_analytic_plan.py b/addons/account/models/account_analytic_plan.py new file mode 100644 index 00000000000..005cd0bd300 --- /dev/null +++ b/addons/account/models/account_analytic_plan.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- + +from odoo import fields, models + + +class AccountAnalyticApplicability(models.Model): + _inherit = 'account.analytic.applicability' + _description = "Analytic Plan's Applicabilities" + + account_prefix = fields.Char( + string='Financial Accounts Prefix', + help="Prefix that defines which accounts from the financial accounting this applicability should apply on.", + ) + product_categ_id = fields.Many2one( + 'product.category', + string='Product Category' + ) + + def _get_score(self, **kwargs): + score = super(AccountAnalyticApplicability, self)._get_score(**kwargs) + if score == -1: + return -1 + product = self.env['product.product'].browse(kwargs.get('product', None)) + account = self.env['account.account'].browse(kwargs.get('account', None)) + if account and self.account_prefix: + if account.code.startswith(self.account_prefix): + score += 1 + else: + return -1 + if product and self.product_categ_id: + if product.categ_id == self.product_categ_id: + score += 1 + else: + return -1 + return score diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 28d4872a227..e8b678bce98 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -1203,12 +1203,6 @@ class AccountMove(models.Model): taxes = None if grouping_dict.get('tax_ids'): taxes = self.env['account.tax'].browse(grouping_dict['tax_ids'][0][2]) - analytic_account = None - if grouping_dict.get('analytic_account_id'): - analytic_account = self.env['account.analytic.account'].browse(grouping_dict['analytic_account_id']) - analytic_tags = None - if grouping_dict.get('analytic_tag_ids'): - analytic_tags = self.env['account.analytic.tag'].browse(grouping_dict['analytic_tag_ids'][0][2]) kwargs['base_lines'].append(self.env['account.tax']._convert_to_tax_base_line_dict( None, @@ -1218,8 +1212,7 @@ class AccountMove(models.Model): price_unit=values['price_subtotal'], quantity=1.0, account=self.env['account.account'].browse(grouping_dict['account_id']), - analytic_account=analytic_account, - analytic_tags=analytic_tags, + analytic_distribution=values.get('analytic_distribution'), price_subtotal=values['price_subtotal'], is_refund=move.move_type in ('out_refund', 'in_refund'), handle_price_include=False, @@ -1520,7 +1513,7 @@ class AccountMove(models.Model): def _inverse_partner_id(self): for invoice in self: if invoice.is_invoice(True): - for line in invoice.line_ids: + for line in invoice.line_ids + invoice.invoice_line_ids: if line.partner_id != invoice.commercial_partner_id: line.partner_id = invoice.commercial_partner_id line._inverse_partner_id() @@ -2789,8 +2782,7 @@ class AccountMove(models.Model): 'partner_id': base_line['partner'].id, 'currency_id': base_line['currency'].id, 'account_id': cash_discount_account.id, - 'analytic_account_id': base_line['analytic_account'].id, - 'analytic_tag_ids': [Command.set(base_line['analytic_tags'].ids)], + 'analytic_distribution': base_line['analytic_distribution'], } base_detail = resulting_delta_base_details.setdefault(frozendict(grouping_dict), { 'balance': 0.0, @@ -2841,8 +2833,7 @@ class AccountMove(models.Model): 'account_id': tax_detail['account_id'], 'partner_id': tax_detail['partner_id'], 'currency_id': tax_detail['currency_id'], - 'analytic_account_id': tax_detail['analytic_account_id'], - 'analytic_tag_ids': tax_detail['analytic_tag_ids'], + 'analytic_distribution': tax_detail['analytic_distribution'], 'tax_repartition_line_id': tax_rep.id, 'tax_ids': tax_detail['tax_ids'], 'tax_tag_ids': tax_detail['tax_tag_ids'], @@ -3245,7 +3236,7 @@ class AccountMove(models.Model): move.date = move._get_accounting_date(move.invoice_date or move.date, affects_tax_report) # Create the analytic lines in batch is faster as it leads to less cache invalidation. - to_post.mapped('line_ids').create_analytic_lines() + to_post.mapped('line_ids')._create_analytic_lines() to_post.write({ 'state': 'posted', 'posted_before': True, diff --git a/addons/account/models/account_move_line.py b/addons/account/models/account_move_line.py index eb3b61cca1a..30c0712f071 100644 --- a/addons/account/models/account_move_line.py +++ b/addons/account/models/account_move_line.py @@ -6,7 +6,7 @@ from functools import lru_cache from odoo import api, fields, models, Command, _ from odoo.exceptions import ValidationError, UserError -from odoo.tools import frozendict, formatLang, format_date +from odoo.tools import frozendict, formatLang, format_date, float_is_zero from odoo.tools.sql import create_index from odoo.addons.web.controllers.utils import clean_action @@ -15,6 +15,7 @@ INTEGRITY_HASH_LINE_FIELDS = ('debit', 'credit', 'account_id', 'partner_id') class AccountMoveLine(models.Model): _name = "account.move.line" + _inherit = 'analytic.mixin' _description = "Journal Item" _order = "date desc, move_name desc, sequence, id" _check_company_auto = True @@ -346,24 +347,9 @@ class AccountMoveLine(models.Model): # === Analytic fields === # analytic_line_ids = fields.One2many( - comodel_name='account.analytic.line', inverse_name='move_id', + comodel_name='account.analytic.line', inverse_name='move_line_id', string='Analytic lines', ) - analytic_account_id = fields.Many2one( - comodel_name='account.analytic.account', - string='Analytic Account', - compute="_compute_analytic_account_id", store=True, readonly=False, - index='btree_not_null', - check_company=True, - copy=True, - ) - analytic_tag_ids = fields.Many2many( - comodel_name='account.analytic.tag', - string='Analytic Tags', - compute="_compute_analytic_tag_ids", store=True, readonly=False, - check_company=True, - copy=True, - ) # === Early Pay fields === # discount_date = fields.Date( @@ -893,7 +879,7 @@ class AccountMoveLine(models.Model): return tax_ids - @api.depends('tax_ids', 'currency_id', 'partner_id', 'account_id', 'group_tax_id', 'analytic_tag_ids', 'analytic_account_id') + @api.depends('tax_ids', 'currency_id', 'partner_id', 'account_id', 'group_tax_id', 'analytic_distribution') def _compute_tax_key(self): for line in self: if line.tax_repartition_line_id: @@ -902,8 +888,7 @@ class AccountMoveLine(models.Model): 'group_tax_id': line.group_tax_id.id, 'account_id': line.account_id.id, 'currency_id': line.currency_id.id, - 'analytic_tag_ids': [(6, 0, line.analytic_tag_ids.ids or [])], - 'analytic_account_id': line.analytic_account_id.id, + 'analytic_distribution': line.analytic_distribution, 'tax_ids': [(6, 0, line.tax_ids.ids)], 'tax_tag_ids': [(6, 0, line.tax_tag_ids.ids)], 'partner_id': line.partner_id.id, @@ -912,7 +897,7 @@ class AccountMoveLine(models.Model): else: line.tax_key = frozendict({'id': line.id}) - @api.depends('tax_ids', 'currency_id', 'partner_id', 'analytic_tag_ids', 'analytic_account_id', 'balance', 'partner_id', 'move_id.partner_id', 'price_unit') + @api.depends('tax_ids', 'currency_id', 'partner_id', 'analytic_distribution', 'balance', 'partner_id', 'move_id.partner_id', 'price_unit') def _compute_all_tax(self): for line in self: sign = line.move_id.direction_sign @@ -955,8 +940,7 @@ class AccountMoveLine(models.Model): 'group_tax_id': tax['group'] and tax['group'].id or False, 'account_id': tax['account_id'] or line.account_id.id, 'currency_id': line.currency_id.id, - 'analytic_tag_ids': [(6, 0, tax['analytic'] and line.analytic_tag_ids.ids or [])], - 'analytic_account_id': tax['analytic'] and line.analytic_account_id.id, + 'analytic_distribution': tax['analytic'] and line.analytic_distribution, 'tax_ids': [(6, 0, tax['tax_ids'])], 'tax_tag_ids': [(6, 0, tax['tag_ids'])], 'partner_id': line.move_id.partner_id.id or line.partner_id.id, @@ -981,8 +965,7 @@ class AccountMoveLine(models.Model): if line.display_type == 'epd' and line.company_id.early_pay_discount_computation == 'mixed': line.epd_key = frozendict({ 'account_id': line.account_id.id, - 'analytic_account_id': line.analytic_account_id.id, - 'analytic_tag_ids': [Command.set(line.analytic_tag_ids.ids)], + 'analytic_distribution': line.analytic_distribution, 'tax_ids': [Command.set(line.tax_ids.ids)], 'tax_tag_ids': [Command.set(line.tax_tag_ids.ids)], 'move_id': line.move_id.id, @@ -990,7 +973,7 @@ class AccountMoveLine(models.Model): else: line.epd_key = False - @api.depends('move_id.needed_terms', 'account_id', 'analytic_account_id', 'analytic_tag_ids', 'tax_ids', 'tax_tag_ids', 'company_id') + @api.depends('move_id.needed_terms', 'account_id', 'analytic_distribution', 'tax_ids', 'tax_tag_ids', 'company_id') def _compute_epd_needed(self): for line in self: line.epd_dirty = True @@ -1013,8 +996,7 @@ class AccountMoveLine(models.Model): frozendict({ 'move_id': line.move_id.id, 'account_id': line.account_id.id, - 'analytic_account_id': line.analytic_account_id.id, - 'analytic_tag_ids': [Command.set(line.analytic_tag_ids.ids)], + 'analytic_distribution': line.analytic_distribution, 'tax_ids': [Command.set(line.tax_ids.ids)], 'tax_tag_ids': [Command.set(line.tax_tag_ids.ids)], 'display_type': 'epd', @@ -1076,35 +1058,19 @@ class AccountMoveLine(models.Model): else: line.term_key = False - @api.depends('product_id', 'account_id', 'partner_id', 'date') - def _compute_analytic_account_id(self): - for record in self: - if record.display_type == 'product' or not record.move_id.is_invoice(include_receipts=True): - rec = self.env['account.analytic.default'].account_get( - product_id=record.product_id.id, - partner_id=record.partner_id.commercial_partner_id.id or record.move_id.partner_id.commercial_partner_id.id, - account_id=record.account_id.id, - user_id=record.env.uid, - date=record.date, - company_id=record.move_id.company_id.id - ) - if rec: - record.analytic_account_id = rec.analytic_id - - @api.depends('product_id', 'account_id', 'partner_id', 'date') - def _compute_analytic_tag_ids(self): - for record in self: - if record.display_type == 'product' or not record.move_id.is_invoice(include_receipts=True): - rec = self.env['account.analytic.default'].account_get( - product_id=record.product_id.id, - partner_id=record.partner_id.commercial_partner_id.id or record.move_id.partner_id.commercial_partner_id.id, - account_id=record.account_id.id, - user_id=record.env.uid, - date=record.date, - company_id=record.move_id.company_id.id - ) - if rec: - record.analytic_tag_ids = rec.analytic_tag_ids + @api.depends('account_id', 'partner_id', 'product_id') + def _compute_analytic_distribution_stored_char(self): + for line in self: + distribution = self.env['account.analytic.distribution.model']._get_distributionjson({ + "product_id": line.product_id.id, + "product_categ_id": line.product_id.categ_id.id, + "partner_id": line.partner_id.id, + "partner_category_id": line.partner_id.category_id.ids, + "account_prefix": line.account_id.code, + "company_id": line.company_id.id, + }) + line.analytic_distribution_stored_char = distribution or line.analytic_distribution_stored_char + line._compute_analytic_distribution() # ------------------------------------------------------------------------- # INVERSE METHODS @@ -1134,6 +1100,16 @@ class AccountMoveLine(models.Model): for line in self: line.balance = line.debit - line.credit + @api.onchange('analytic_distribution') + def _inverse_analytic_distribution(self): + """ Unlink and recreate analytic_lines when modifying the distribution.""" + super()._inverse_analytic_distribution() + lines_to_modify = self.env['account.move.line'].browse([ + line.id for line in self if line.parent_state == "posted" + ]) + lines_to_modify.analytic_line_ids.unlink() + lines_to_modify._create_analytic_lines() + # ------------------------------------------------------------------------- # ONCHANGE METHODS # ------------------------------------------------------------------------- @@ -2334,88 +2310,60 @@ class AccountMoveLine(models.Model): # ANALYTIC # ------------------------------------------------------------------------- - def _prepare_analytic_line(self): - """ Prepare the values used to create() an account.analytic.line upon validation of an account.move.line having - an analytic account. This method is intended to be extended in other modules. - :return list of values to create analytic.line - :rtype list + def _create_analytic_lines(self): + """ Create analytic items upon validation of an account.move.line having an analytic distribution. """ - result = [] - for move_line in self: - amount = (move_line.credit or 0.0) - (move_line.debit or 0.0) - default_name = move_line.name or (move_line.ref or '/' + ' -- ' + (move_line.partner_id and move_line.partner_id.name or '/')) - category = 'other' - if move_line.move_id.is_sale_document(): - category = 'invoice' - elif move_line.move_id.is_purchase_document(): - category = 'vendor_bill' - result.append({ - 'name': default_name, - 'date': move_line.date, - 'account_id': move_line.analytic_account_id.id, - 'group_id': move_line.analytic_account_id.group_id.id, - 'tag_ids': [(6, 0, move_line._get_analytic_tag_ids())], - 'unit_amount': move_line.quantity, - 'product_id': move_line.product_id and move_line.product_id.id or False, - 'product_uom_id': move_line.product_uom_id and move_line.product_uom_id.id or False, - 'amount': amount, - 'general_account_id': move_line.account_id.id, - 'ref': move_line.ref, - 'move_id': move_line.id, - 'user_id': move_line.move_id.invoice_user_id.id or self._uid, - 'partner_id': move_line.partner_id.id, - 'company_id': move_line.analytic_account_id.company_id.id or move_line.move_id.company_id.id, - 'category': category, - }) - return result + analytic_line_vals = [] + for line in self: + analytic_line_vals.extend(line._prepare_analytic_lines()) - def _prepare_analytic_distribution_line(self, distribution): + self.env['account.analytic.line'].create(analytic_line_vals) + + def _prepare_analytic_lines(self): + self.ensure_one() + analytic_line_vals = [] + self._compute_analytic_distribution() + if self.analytic_distribution: + # distribution_on_each_plan corresponds to the proportion that is distributed to each plan to be able to + # give the real amount when we achieve a 100% distribution + distribution_on_each_plan = {} + + for account_id, distribution in self.analytic_distribution.items(): + line_values = self._prepare_analytic_distribution_line(float(distribution), account_id, distribution_on_each_plan) + if not float_is_zero(line_values.get("amount"), precision_digits=self.env.company.currency_id.decimal_places): + analytic_line_vals.append(line_values) + return analytic_line_vals + + def _prepare_analytic_distribution_line(self, distribution, account_id, distribution_on_each_plan): """ 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 + account_id = int(account_id) + account = self.env['account.analytic.account'].browse(account_id) + distribution_plan = distribution_on_each_plan.get(account.root_plan_id, 0) + distribution + if self.env.company.currency_id.compare_amounts(distribution_plan, 100) == 0: + amount = -self.balance * (100 - distribution_on_each_plan.get(account.root_plan_id, 0)) / 100.0 + else: + amount = -self.balance * distribution / 100.0 + distribution_on_each_plan[account.root_plan_id] = distribution_plan default_name = self.name or (self.ref or '/' + ' -- ' + (self.partner_id and self.partner_id.name or '/')) return { 'name': default_name, 'date': self.date, - 'account_id': distribution.account_id.id, - 'group_id': distribution.account_id.group_id.id, + 'account_id': account_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, + 'move_line_id': self.id, 'user_id': self.move_id.invoice_user_id.id or self._uid, - 'company_id': distribution.account_id.company_id.id or self.company_id.id or self.env.company.id, + 'company_id': account.company_id.id or self.company_id.id or self.env.company.id, } - def _get_analytic_tag_ids(self): - self.ensure_one() - return self.analytic_tag_ids.filtered(lambda r: not r.active_analytic_distribution).ids - - def create_analytic_lines(self): - """ Create analytic items upon validation of an account.move.line having an analytic account or an analytic distribution. - """ - lines_to_create_analytic_entries = self.env['account.move.line'] - analytic_line_vals = [] - for obj_line in self: - for tag in obj_line.analytic_tag_ids.filtered('active_analytic_distribution'): - for distribution in tag.analytic_distribution_ids: - analytic_line_vals.append(obj_line._prepare_analytic_distribution_line(distribution)) - if obj_line.analytic_account_id: - lines_to_create_analytic_entries |= obj_line - - # create analytic entries in batch - if lines_to_create_analytic_entries: - analytic_line_vals += lines_to_create_analytic_entries._prepare_analytic_line() - - self.env['account.analytic.line'].create(analytic_line_vals) - # ------------------------------------------------------------------------- # MISC # ------------------------------------------------------------------------- @@ -2485,8 +2433,7 @@ class AccountMoveLine(models.Model): quantity=self.quantity if is_invoice else 1.0, discount=self.discount if is_invoice else 0.0, account=self.account_id, - analytic_account=self.analytic_account_id, - analytic_tags=self.analytic_tag_ids, + analytic_distribution=self.analytic_distribution, price_subtotal=sign * self.amount_currency, is_refund=self.is_refund, rate=(abs(self.amount_currency) / abs(self.balance)) if self.balance else 1.0 @@ -2509,8 +2456,7 @@ class AccountMoveLine(models.Model): tax_repartition_line=self.tax_repartition_line_id, group_tax=self.group_tax_id, account=self.account_id, - analytic_account=self.analytic_account_id, - analytic_tags=self.analytic_tag_ids, + analytic_distribution=self.analytic_distribution, tax_amount=sign * self.amount_currency, ) diff --git a/addons/account/models/account_move_line_tax_details.py b/addons/account/models/account_move_line_tax_details.py index 69b98dce89a..9b0254eeb01 100644 --- a/addons/account/models/account_move_line_tax_details.py +++ b/addons/account/models/account_move_line_tax_details.py @@ -182,8 +182,8 @@ class AccountMoveLine(models.Model): ) AND ( NOT tax.analytic - OR (base_line.analytic_account_id IS NULL AND account_move_line.analytic_account_id IS NULL) - OR base_line.analytic_account_id = account_move_line.analytic_account_id + OR (base_line.analytic_distribution_stored_char IS NULL AND account_move_line.analytic_distribution_stored_char IS NULL) + OR base_line.analytic_distribution_stored_char = account_move_line.analytic_distribution_stored_char ) LEFT JOIN affecting_base_tax_ids tax_line_tax_ids ON tax_line_tax_ids.id = account_move_line.id JOIN affecting_base_tax_ids base_line_tax_ids ON base_line_tax_ids.id = base_line.id diff --git a/addons/account/models/account_reconcile_model.py b/addons/account/models/account_reconcile_model.py index c45ee5f022a..f711d3233af 100644 --- a/addons/account/models/account_reconcile_model.py +++ b/addons/account/models/account_reconcile_model.py @@ -37,6 +37,7 @@ class AccountReconcileModelPartnerMapping(models.Model): class AccountReconcileModelLine(models.Model): _name = 'account.reconcile.model.line' + _inherit = 'analytic.mixin' _description = 'Rules for the reconciliation model' _order = 'sequence, id' _check_company_auto = True @@ -72,9 +73,6 @@ class AccountReconcileModelLine(models.Model): * Fixed: The fixed value of the writeoff. The amount will count as a debit if it is negative, as a credit if it is positive. * From Label: There is no need for regex delimiter, only the regex is needed. For instance if you want to extract the amount from\nR:9672938 10/07 AX 9415126318 T:5L:NA BRT: 3358,07 C:\nYou could enter\nBRT: ([\d,]+)""") tax_ids = fields.Many2many('account.tax', string='Taxes', ondelete='restrict', check_company=True) - analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account', ondelete='set null', check_company=True) - analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags', check_company=True, - relation='account_reconcile_model_analytic_tag_rel') @api.onchange('tax_ids') def _onchange_tax_ids(self): @@ -138,8 +136,7 @@ class AccountReconcileModelLine(models.Model): 'name': self.label, 'account_id': self.account_id.id, 'partner_id': partner.id, - 'analytic_account_id': self.analytic_account_id.id, - 'analytic_tag_ids': [Command.set(self.analytic_tag_ids.ids)], + 'analytic_distribution': self.analytic_distribution, 'tax_ids': [Command.set(taxes.ids)], 'reconcile_model_id': self.model_id.id, } @@ -484,8 +481,7 @@ class AccountReconcileModel(models.Model): 'balance': balance, 'debit': balance > 0 and balance or 0, 'credit': balance < 0 and -balance or 0, - 'analytic_account_id': tax.analytic and base_line_dict['analytic_account_id'], - 'analytic_tag_ids': tax.analytic and base_line_dict['analytic_tag_ids'], + 'analytic_distribution': tax.analytic and base_line_dict['analytic_distribution'], 'tax_repartition_line_id': tax_res['tax_repartition_line_id'], 'tax_ids': [(6, 0, tax_res['tax_ids'])], 'tax_tag_ids': [(6, 0, tax_res['tag_ids'])], @@ -534,8 +530,7 @@ class AccountReconcileModel(models.Model): 'credit': balance < 0 and -balance or 0, 'account_id': line.account_id.id, 'currency_id': currency.id, - 'analytic_account_id': line.analytic_account_id.id, - 'analytic_tag_ids': [(6, 0, line.analytic_tag_ids.ids)], + 'analytic_distribution': line.analytic_distribution, 'reconcile_model_id': self.id, 'journal_id': line.journal_id.id, 'tax_ids': [], diff --git a/addons/account/models/account_tax.py b/addons/account/models/account_tax.py index 9c92eeaede7..89c395d9fee 100644 --- a/addons/account/models/account_tax.py +++ b/addons/account/models/account_tax.py @@ -719,7 +719,7 @@ class AccountTax(models.Model): def _convert_to_tax_base_line_dict( self, base_line, partner=None, currency=None, product=None, taxes=None, price_unit=None, quantity=None, - discount=None, account=None, analytic_account=None, analytic_tags=None, price_subtotal=None, + discount=None, account=None, analytic_distribution=None, price_subtotal=None, is_refund=False, rate=None, handle_price_include=None, extra_context=None, @@ -734,8 +734,7 @@ class AccountTax(models.Model): 'quantity': quantity or 0.0, 'discount': discount or 0.0, 'account': account or self.env['account.account'], - 'analytic_account': analytic_account or self.env['account.analytic.account'], - 'analytic_tags': analytic_tags or self.env['account.analytic.tag'], + 'analytic_distribution': analytic_distribution, 'price_subtotal': price_subtotal or 0.0, 'is_refund': is_refund, 'rate': rate or 1.0, @@ -747,7 +746,7 @@ class AccountTax(models.Model): def _convert_to_tax_line_dict( self, tax_line, partner=None, currency=None, taxes=None, tax_tags=None, tax_repartition_line=None, - group_tax=None, account=None, analytic_account=None, analytic_tags=None, tax_amount=None, + group_tax=None, account=None, analytic_distribution=None, tax_amount=None, ): return { 'record': tax_line, @@ -758,8 +757,7 @@ class AccountTax(models.Model): 'tax_repartition_line': tax_repartition_line or self.env['account.tax.repartition.line'], 'group_tax': group_tax or self.env['account.tax'], 'account': account or self.env['account.account'], - 'analytic_account': analytic_account or self.env['account.analytic.account'], - 'analytic_tags': analytic_tags or self.env['account.analytic.tag'], + 'analytic_distribution': analytic_distribution, 'tax_amount': tax_amount or 0.0, } @@ -783,8 +781,7 @@ class AccountTax(models.Model): 'tax_ids': [Command.set(tax_vals['tax_ids'])], 'tax_tag_ids': [Command.set(tax_vals['tag_ids'])], 'tax_id': tax_vals['group'].id if tax_vals['group'] else tax_vals['id'], - 'analytic_tag_ids': [Command.set(line_vals['analytic_tags'].ids if tax_vals['analytic'] else [])], - 'analytic_account_id': line_vals['analytic_account'].id if tax_vals['analytic'] else False, + 'analytic_distribution': line_vals['analytic_distribution'] if tax_vals['analytic'] else {}, } @api.model @@ -805,8 +802,7 @@ class AccountTax(models.Model): 'tax_ids': [Command.set(line_vals['taxes'].ids)], 'tax_tag_ids': [Command.set(line_vals['tax_tags'].ids)], 'tax_id': (line_vals['group_tax'] or tax).id, - 'analytic_tag_ids': [Command.set(line_vals['analytic_tags'].ids if tax.analytic else [])], - 'analytic_account_id': line_vals['analytic_account'].id if tax.analytic else False, + 'analytic_distribution': line_vals['analytic_distribution'] if tax.analytic else {}, } @api.model diff --git a/addons/account/models/res_config_settings.py b/addons/account/models/res_config_settings.py index c82fdbb4f0d..8163ca337fb 100644 --- a/addons/account/models/res_config_settings.py +++ b/addons/account/models/res_config_settings.py @@ -70,7 +70,6 @@ class ResConfigSettings(models.TransientModel): domain="[('reconcile', '=', True), ('account_type', '=', 'asset_current')]", help="Intermediary account used when moving from a liquidity account to another.") module_account_accountant = fields.Boolean(string='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') # group_show_line_subtotals_tax_excluded and group_show_line_subtotals_tax_included are opposite, diff --git a/addons/account/report/account_invoice_report.py b/addons/account/report/account_invoice_report.py index a462e9a1c02..06b4565dac0 100644 --- a/addons/account/report/account_invoice_report.py +++ b/addons/account/report/account_invoice_report.py @@ -47,7 +47,6 @@ class AccountInvoiceReport(models.Model): product_categ_id = fields.Many2one('product.category', string='Product Category', readonly=True) invoice_date_due = fields.Date(string='Due Date', readonly=True) account_id = fields.Many2one('account.account', string='Revenue/Expense Account', readonly=True, domain=[('deprecated', '=', False)]) - analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account', groups="analytic.group_analytic_accounting") price_subtotal = fields.Float(string='Untaxed Total', readonly=True) price_total = fields.Float(string='Total', readonly=True) price_average = fields.Float(string='Average Price', readonly=True, group_operator="avg") @@ -59,7 +58,7 @@ class AccountInvoiceReport(models.Model): ], 'account.move.line': [ 'quantity', 'price_subtotal', 'price_total', 'amount_residual', 'balance', 'amount_currency', - 'move_id', 'product_id', 'product_uom_id', 'account_id', 'analytic_account_id', + 'move_id', 'product_id', 'product_uom_id', 'account_id', 'journal_id', 'company_id', 'currency_id', 'partner_id', ], 'product.product': ['product_tmpl_id'], @@ -81,7 +80,6 @@ class AccountInvoiceReport(models.Model): line.move_id, line.product_id, line.account_id, - line.analytic_account_id, line.journal_id, line.company_id, line.company_currency_id, diff --git a/addons/account/report/account_invoice_report_view.xml b/addons/account/report/account_invoice_report_view.xml index 51f1724a991..a4031f26ae3 100644 --- a/addons/account/report/account_invoice_report_view.xml +++ b/addons/account/report/account_invoice_report_view.xml @@ -128,17 +128,6 @@ - - account.invoice.report.search_analytic_accounting - account.invoice.report - - - - - - - - Invoices Analysis account.invoice.report diff --git a/addons/account/security/account_security.xml b/addons/account/security/account_security.xml index f92f74dfc70..b5219c7e98f 100644 --- a/addons/account/security/account_security.xml +++ b/addons/account/security/account_security.xml @@ -291,14 +291,6 @@ - - - Analytic Default multi company rule - - - ['|',('company_id','=',False),('company_id', 'in', company_ids)] - - Report External Value multi-company diff --git a/addons/account/security/ir.model.access.csv b/addons/account/security/ir.model.access.csv index d59e76a0e61..a64c8a3f55f 100644 --- a/addons/account/security/ir.model.access.csv +++ b/addons/account/security/ir.model.access.csv @@ -58,6 +58,9 @@ access_account_analytic_account,account.analytic.account,analytic.model_account_ access_account_analytic_line_readonly,account.analytic.line invoice,model_account_analytic_line,account.group_account_readonly,1,0,0,0 access_account_analytic_line_invoice,account.analytic.line invoice,model_account_analytic_line,account.group_account_invoice,1,1,1,1 access_account_analytic_accountant,account.analytic.account accountant,analytic.model_account_analytic_account,account.group_account_user,1,1,1,1 +access_account_analytic_plan_accountant,account.analytic.plan accountant,analytic.model_account_analytic_plan,account.group_account_user,1,1,1,1 +access_account_analytic_distribution_readonly,account.analytic.distribution invoice,analytic.model_account_analytic_distribution_model,account.group_account_readonly,1,0,0,0 +access_account_analytic_distribution_invoice,account.analytic.distribution invoice,analytic.model_account_analytic_distribution_model,account.group_account_invoice,1,1,1,1 access_account_journal_readonly,account.journal,model_account_journal,account.group_account_readonly,1,0,0,0 access_account_journal_manager,account.journal,model_account_journal,account.group_account_manager,1,1,1,1 @@ -130,9 +133,6 @@ access_account_financial_year_op,access.account.financial.year.op,model_account_ access_account_setup_bank_manual_config,access.account.setup.bank.manual.config,model_account_setup_bank_manual_config,account.group_account_manager,1,1,1,0 access_account_invoice_send,access.account.invoice.send,model_account_invoice_send,account.group_account_invoice,1,1,1,0 -access_account_analytic_default,account.analytic.default,model_account_analytic_default,account.group_account_readonly,1,0,0,0 -access_account_analytic_default_analytic,account.analytic.default analytic,model_account_analytic_default,analytic.group_analytic_accounting,1,0,0,0 -access_account_analytic_default_invoice,account.analytic.default invoice,model_account_analytic_default,account.group_account_invoice,1,1,1,1 access_account_tour_upload_bill,account.tour.upload.bill,model_account_tour_upload_bill,account.group_account_manager,1,1,1,0 access_account_tour_upload_bill_email_confirm,account.tour.upload.bill.email.confirm,model_account_tour_upload_bill_email_confirm,account.group_account_manager,1,1,1,0 diff --git a/addons/account/tests/test_account_analytic.py b/addons/account/tests/test_account_analytic.py index 48ab4258ffb..744989190f1 100644 --- a/addons/account/tests/test_account_analytic.py +++ b/addons/account/tests/test_account_analytic.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- from odoo.addons.account.tests.common import AccountTestInvoicingCommon -from odoo.tests import tagged +from odoo.tests import tagged, Form from odoo.exceptions import UserError +from odoo import Command @tagged('post_install', '-at_install') @@ -11,49 +12,171 @@ class TestAccountAnalyticAccount(AccountTestInvoicingCommon): def setUpClass(cls, chart_template_ref=None): super().setUpClass(chart_template_ref=chart_template_ref) - cls.env.user.write({ - 'groups_id': [ - (4, cls.env.ref('analytic.group_analytic_accounting').id), - (4, cls.env.ref('analytic.group_analytic_tags').id), - ], - }) + cls.env.user.groups_id += cls.env.ref('analytic.group_analytic_accounting') # By default, tests are run with the current user set on the first company. cls.env.user.company_id = cls.company_data['company'] - cls.test_analytic_account = cls.env['account.analytic.account'].create({'name': 'test_analytic_account'}) - cls.test_analytic_tag = cls.env['account.analytic.tag'].create({'name': 'test_analytic_tag'}) + cls.default_plan = cls.env['account.analytic.plan'].create({'name': 'Default', 'company_id': False}) + cls.analytic_account_a = cls.env['account.analytic.account'].create({ + 'name': 'analytic_account_a', + 'plan_id': cls.default_plan.id, + }) + cls.analytic_account_b = cls.env['account.analytic.account'].create({ + 'name': 'analytic_account_b', + 'plan_id': cls.default_plan.id, + }) def test_changing_analytic_company(self): - ''' Ensure you can't change the company of an account.analytic.account if there are some journal entries ''' - - self.env['account.move'].create({ - 'move_type': 'entry', - 'date': '2019-01-01', - 'line_ids': [ - (0, 0, { - 'name': 'line_debit', - 'account_id': self.company_data['default_account_revenue'].id, - 'analytic_account_id': self.test_analytic_account.id, - 'analytic_tag_ids': [(6, 0, self.test_analytic_tag.ids)], - }), - (0, 0, { - 'name': 'line_credit', - 'account_id': self.company_data['default_account_revenue'].id, - }), - ], + """ Ensure you can't change the company of an account.analytic.account if there are analytic lines linked to + the account + """ + self.env['account.analytic.line'].create({ + 'name': 'company specific account', + 'account_id': self.analytic_account_a.id, + 'amount': 100, }) # Set a different company on the analytic account. with self.assertRaises(UserError), self.cr.savepoint(): - self.test_analytic_account.company_id = self.company_data_2['company'] + self.analytic_account_a.company_id = self.company_data_2['company'] # Making the analytic account not company dependent is allowed. - self.test_analytic_account.company_id = False + self.analytic_account_a.company_id = False - # Set a different company on the analytic tag. - with self.assertRaises(UserError), self.cr.savepoint(): - self.test_analytic_tag.company_id = self.company_data_2['company'] + def test_analytic_lines(self): + ''' Ensures analytic lines are created when posted and are recreated when editing the account.move''' + def get_analytic_lines(): + return self.env['account.analytic.line'].search([ + ('move_line_id', 'in', out_invoice.line_ids.ids) + ]).sorted('amount') - # Making the analytic tag not company dependent is allowed. - self.test_analytic_tag.company_id = False + out_invoice = self.env['account.move'].create([{ + 'move_type': 'out_invoice', + 'partner_id': self.partner_a.id, + 'date': '2017-01-01', + 'invoice_date': '2017-01-01', + 'invoice_line_ids': [Command.create({ + 'product_id': self.product_a.id, + 'price_unit': 200.0, + 'analytic_distribution': { + self.analytic_account_a.id: 100, + self.analytic_account_b.id: 50, + }, + })] + }]) + + out_invoice.action_post() + + # Analytic lines are created when posting the invoice + self.assertRecordValues(get_analytic_lines(), [{ + 'amount': 100, + 'account_id': self.analytic_account_b.id, + 'partner_id': self.partner_a.id, + 'product_id': self.product_a.id, + }, { + 'amount': 200, + 'account_id': self.analytic_account_a.id, + 'partner_id': self.partner_a.id, + 'product_id': self.product_a.id, + }]) + + # Analytic lines are updated when a posted invoice's distribution changes + out_invoice.invoice_line_ids.analytic_distribution = { + self.analytic_account_a.id: 100, + self.analytic_account_b.id: 25, + } + self.assertRecordValues(get_analytic_lines(), [{ + 'amount': 50, + 'account_id': self.analytic_account_b.id, + }, { + 'amount': 200, + 'account_id': self.analytic_account_a.id, + }]) + + # Analytic lines are deleted when resetting to draft + out_invoice.button_draft() + self.assertFalse(get_analytic_lines()) + + def test_plan_score(self): + """Test that the plans are applied correctly based on the score""" + def create_invoice(partner, product): + return self.env['account.move'].create([{ + 'move_type': 'out_invoice', + 'partner_id': partner.id, + 'date': '2017-01-01', + 'invoice_date': '2017-01-01', + 'invoice_line_ids': [Command.create({ + 'product_id': product.id, + })] + }]) + + self.env['account.analytic.distribution.model'].create([{ + 'product_id': self.product_a.id, + 'analytic_distribution': {self.analytic_account_a.id: 100} + }, { + 'partner_id': self.partner_a.id, + 'product_id': self.product_a.id, + 'analytic_distribution': {self.analytic_account_b.id: 100} + }]) + + # Partner and product match, score 2 + invoice = create_invoice(self.partner_a, self.product_a) + self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {self.analytic_account_b.id: 100}) + + # Match the partner but not the product, score 0 + invoice = create_invoice(self.partner_a, self.product_b) + self.assertEqual(invoice.invoice_line_ids.analytic_distribution, False) + + # Product match, score 1 + invoice = create_invoice(self.partner_b, self.product_a) + self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {self.analytic_account_a.id: 100}) + + # No rule match with the product, score 0 + invoice = create_invoice(self.partner_b, self.product_b) + self.assertEqual(invoice.invoice_line_ids.analytic_distribution, False) + + def test_plan_application(self): + """Test that the distribution is recomputed if and only if it is needed when changing the partner.""" + self.env['account.analytic.distribution.model'].create([{ + 'partner_id': self.partner_a.id, + 'analytic_distribution': {self.analytic_account_a.id: 100} + }, { + 'partner_id': self.partner_b.id, + 'analytic_distribution': {self.analytic_account_b.id: 100} + }]) + + invoice = self.env['account.move'].create([{ + 'move_type': 'out_invoice', + 'date': '2017-01-01', + 'invoice_date': '2017-01-01', + 'invoice_line_ids': [Command.create({ + 'product_id': self.product_a.id, + })] + }]) + # No model is found, don't put anything + self.assertEqual(invoice.invoice_line_ids.analytic_distribution, False) + + # A model is found, set the new values + invoice.partner_id = self.partner_a + self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {self.analytic_account_a.id: 100}) + + # A model is found, set the new values + invoice.partner_id = self.partner_b + self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {self.analytic_account_b.id: 100}) + + # No model is found, don't change previously set values + invoice.partner_id = invoice.company_id.partner_id + self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {self.analytic_account_b.id: 100}) + + # No model is found, don't change previously set values + invoice.partner_id = False + self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {self.analytic_account_b.id: 100}) + + # It manual value is not erased in form view when saving + with Form(invoice) as invoice_form: + invoice_form.partner_id = self.partner_a + with invoice_form.invoice_line_ids.edit(0) as line_form: + self.assertEqual(line_form.analytic_distribution, {self.analytic_account_a.id: 100}) + line_form.analytic_distribution = {self.analytic_account_b.id: 100} + self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {self.analytic_account_b.id: 100}) diff --git a/addons/account/tests/test_account_move_out_invoice.py b/addons/account/tests/test_account_move_out_invoice.py index 61fb3995996..f4fd3df5d8d 100644 --- a/addons/account/tests/test_account_move_out_invoice.py +++ b/addons/account/tests/test_account_move_out_invoice.py @@ -1164,57 +1164,49 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon): def test_out_invoice_line_onchange_analytic(self): self.env.user.groups_id += self.env.ref('analytic.group_analytic_accounting') - self.env.user.groups_id += self.env.ref('analytic.group_analytic_tags') - - analytic_tag = self.env['account.analytic.tag'].create({ - 'name': 'test_analytic_tag', - }) + analytic_plan = self.env['account.analytic.plan'].create({'name': 'Plan Test', 'company_id': False}) analytic_account = self.env['account.analytic.account'].create({ 'name': 'test_analytic_account', 'partner_id': self.invoice.partner_id.id, + 'plan_id': analytic_plan.id, 'code': 'TEST' }) + analytic_distribution = {analytic_account.id: 100} + move_form = Form(self.invoice) with move_form.invoice_line_ids.edit(0) as line_form: - line_form.analytic_account_id = analytic_account - line_form.analytic_tag_ids.add(analytic_tag) + line_form.analytic_distribution = analytic_distribution move_form.save() # The tax is not flagged as an analytic one. It should change nothing on the taxes. self.assertInvoiceValues(self.invoice, [ { **self.product_line_vals_1, - 'analytic_account_id': analytic_account.id, - 'analytic_tag_ids': analytic_tag.ids, + 'analytic_distribution': analytic_distribution, }, { **self.product_line_vals_2, - 'analytic_account_id': False, - 'analytic_tag_ids': [], + 'analytic_distribution': False, }, { **self.tax_line_vals_1, - 'analytic_account_id': False, - 'analytic_tag_ids': [], + 'analytic_distribution': False, }, { **self.tax_line_vals_2, - 'analytic_account_id': False, - 'analytic_tag_ids': [], + 'analytic_distribution': False, }, { **self.term_line_vals_1, - 'analytic_account_id': False, - 'analytic_tag_ids': [], + 'analytic_distribution': False, }, ], self.move_vals) move_form = Form(self.invoice) with move_form.invoice_line_ids.edit(0) as line_form: - line_form.analytic_account_id = self.env['account.analytic.account'] - line_form.analytic_tag_ids.clear() + line_form.analytic_distribution = {} move_form.save() # Enable the analytic @@ -1222,8 +1214,7 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon): move_form = Form(self.invoice) with move_form.invoice_line_ids.edit(0) as line_form: - line_form.analytic_account_id = analytic_account - line_form.analytic_tag_ids.add(analytic_tag) + line_form.analytic_distribution = analytic_distribution move_form.save() # The tax is flagged as an analytic one. @@ -1231,103 +1222,94 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon): self.assertInvoiceValues(self.invoice, [ { **self.product_line_vals_1, - 'analytic_account_id': analytic_account.id, - 'analytic_tag_ids': analytic_tag.ids, + 'analytic_distribution': analytic_distribution, }, { **self.product_line_vals_2, - 'analytic_account_id': False, - 'analytic_tag_ids': [], + 'analytic_distribution': False, }, { **self.tax_line_vals_1, 'amount_currency': -150.0, 'credit': 150.0, - 'analytic_account_id': analytic_account.id, - 'analytic_tag_ids': analytic_tag.ids, + 'analytic_distribution': analytic_distribution, }, { **self.tax_line_vals_1, 'amount_currency': -30.0, 'credit': 30.0, - 'analytic_account_id': False, - 'analytic_tag_ids': [], + 'analytic_distribution': False, }, { **self.tax_line_vals_2, - 'analytic_account_id': False, - 'analytic_tag_ids': [], + 'analytic_distribution': False, }, { **self.term_line_vals_1, - 'analytic_account_id': False, - 'analytic_tag_ids': [], + 'analytic_distribution': False, }, ], self.move_vals) move_form = Form(self.invoice) with move_form.invoice_line_ids.edit(0) as line_form: - line_form.analytic_account_id = self.env['account.analytic.account'] - line_form.analytic_tag_ids.clear() + line_form.analytic_distribution = {} with move_form.invoice_line_ids.edit(1) as line_form: - line_form.analytic_account_id = self.env['account.analytic.account'] - line_form.analytic_tag_ids.clear() + line_form.analytic_distribution = {} move_form.save() # The tax line has been removed. self.assertInvoiceValues(self.invoice, [ { **self.product_line_vals_1, - 'analytic_account_id': False, - 'analytic_tag_ids': [], + 'analytic_distribution': False, }, { **self.product_line_vals_2, - 'analytic_account_id': False, - 'analytic_tag_ids': [], + 'analytic_distribution': False, }, { **self.tax_line_vals_1, - 'analytic_account_id': False, - 'analytic_tag_ids': [], + 'analytic_distribution': False, }, { **self.tax_line_vals_2, - 'analytic_account_id': False, - 'analytic_tag_ids': [], + 'analytic_distribution': False, }, { **self.term_line_vals_1, - 'analytic_account_id': False, - 'analytic_tag_ids': [], + 'analytic_distribution': False, }, ], self.move_vals) def test_out_invoice_line_onchange_analytic_2(self): self.env.user.groups_id += self.env.ref('analytic.group_analytic_accounting') + analytic_plan = self.env['account.analytic.plan'].create({'name': 'Plan Test', 'company_id': False}) analytic_account = self.env['account.analytic.account'].create({ 'name': 'test_analytic_account1', + 'plan_id': analytic_plan.id, 'code': 'TEST1' }) + analytic_distribution = {analytic_account.id: 100} + self.invoice.write({'invoice_line_ids': [(1, self.invoice.invoice_line_ids.ids[0], { - 'analytic_account_id': analytic_account.id, + 'analytic_distribution': analytic_distribution, })]}) self.assertRecordValues(self.invoice.invoice_line_ids, [ - {'analytic_account_id': analytic_account.id}, - {'analytic_account_id': False}, + {'analytic_distribution': analytic_distribution}, + {'analytic_distribution': False}, ]) # We can remove the analytic account, it is not recomputed by an invalidation self.invoice.write({'invoice_line_ids': [(1, self.invoice.invoice_line_ids.ids[0], { - 'analytic_account_id': False, + 'analytic_distribution': False, })]}) self.assertRecordValues(self.invoice.invoice_line_ids, [ - {'analytic_account_id': False}, - {'analytic_account_id': False}, + {'analytic_distribution': False}, + {'analytic_distribution': False}, ]) def test_out_invoice_line_onchange_cash_rounding_1(self): diff --git a/addons/account/tests/test_account_move_reconcile.py b/addons/account/tests/test_account_move_reconcile.py index 34b6a6ac08f..e121943c10f 100644 --- a/addons/account/tests/test_account_move_reconcile.py +++ b/addons/account/tests/test_account_move_reconcile.py @@ -3573,7 +3573,8 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): move_form.invoice_date = fields.Date.from_string('2019-01-01') move_form.partner_id = self.partner_a self.cash_basis_tax_a_third_amount.analytic = True - test_analytic_account = self.env['account.analytic.account'].create({'name': 'test_analytic_account'}) + analytic_plan = self.env['account.analytic.plan'].create({'name': 'Plan Test', 'company_id': False}) + test_analytic_account = self.env['account.analytic.account'].create({'name': 'test_analytic_account', 'plan_id': analytic_plan.id}) tax = self.cash_basis_tax_a_third_amount @@ -3583,7 +3584,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): line_form.product_id = self.product_a line_form.tax_ids.clear() line_form.tax_ids.add(tax) - line_form.analytic_account_id = test_analytic_account + line_form.analytic_distribution = {test_analytic_account.id: 100} line_form.price_unit = 100 # line with analytic account, will generate other 2 lines in CABA move diff --git a/addons/account/tests/test_templates_consistency.py b/addons/account/tests/test_templates_consistency.py index ec052543495..f37cc317891 100644 --- a/addons/account/tests/test_templates_consistency.py +++ b/addons/account/tests/test_templates_consistency.py @@ -78,7 +78,7 @@ class AccountingTestTemplConsistency(TransactionCase): ) # lines self.check_fields_consistency('account.reconcile.model.line.template', 'account.reconcile.model.line', exceptions=['chart_template_id']) - self.check_fields_consistency('account.reconcile.model.line', 'account.reconcile.model.line.template', exceptions=['company_id', 'journal_id', 'analytic_account_id', 'analytic_tag_ids', 'amount']) + self.check_fields_consistency('account.reconcile.model.line', 'account.reconcile.model.line.template', exceptions=['company_id', 'journal_id', 'analytic_distribution_stored_char', 'amount']) def test_account_group_fields(self): '''Test fields consistency for ('account.group', 'account.group.template') diff --git a/addons/account/views/account_analytic_account_views.xml b/addons/account/views/account_analytic_account_views.xml new file mode 100644 index 00000000000..598f82526e1 --- /dev/null +++ b/addons/account/views/account_analytic_account_views.xml @@ -0,0 +1,24 @@ + + + + + + account.analytic.account.form.inherit + account.analytic.account + + + +
+ + +
+
+
+
+
\ No newline at end of file diff --git a/addons/account/views/account_analytic_default_view.xml b/addons/account/views/account_analytic_default_view.xml deleted file mode 100644 index 42ae72a86d5..00000000000 --- a/addons/account/views/account_analytic_default_view.xml +++ /dev/null @@ -1,119 +0,0 @@ - - - - account.analytic.default.tree - account.analytic.default - - - - - - - - - - - - - - - - - - account.analytic.default.form - account.analytic.default - -
- - - - - - - - - - - - - - - -
-
-
- - - account.analytic.default.kanban - account.analytic.default - - - - - - - - - - -
-
- -
-
From to
-
Product
-
Customer
-
- -
-
-
-
-
-
-
- - - account.analytic.default.search - account.analytic.default - - - - - - - - - - - - - - - - - - - - - Analytic Defaults Rules - account.analytic.default - tree,kanban,form - - {"search_default_current":1} - - - - - - - Analytic Rules - account.analytic.default - {'search_default_user_id': [active_id], 'default_user_id': active_id} - - form - - -
diff --git a/addons/account/views/account_analytic_distribution_model_views.xml b/addons/account/views/account_analytic_distribution_model_views.xml new file mode 100644 index 00000000000..c71faa853ec --- /dev/null +++ b/addons/account/views/account_analytic_distribution_model_views.xml @@ -0,0 +1,38 @@ + + + + + account.analytic.distribution.model.inherit.tree + account.analytic.distribution.model + + + + + + + + + + + + + + + + account.analytic.distribution.model.inherit.form + account.analytic.distribution.model + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addons/account/views/account_analytic_view.xml b/addons/account/views/account_analytic_line_views.xml similarity index 75% rename from addons/account/views/account_analytic_view.xml rename to addons/account/views/account_analytic_line_views.xml index 34307b3fcb4..03bb2d0c865 100644 --- a/addons/account/views/account_analytic_view.xml +++ b/addons/account/views/account_analytic_line_views.xml @@ -7,7 +7,7 @@ - + @@ -18,7 +18,7 @@ - + @@ -33,7 +33,7 @@ - + @@ -69,25 +69,15 @@
- - account.analytic.account.form.inherit - account.analytic.account - - + + account.analytic.line.select.inherit + account.analytic.line + -
- - -
+ + +
- - diff --git a/addons/account/views/account_analytic_plan_views.xml b/addons/account/views/account_analytic_plan_views.xml new file mode 100644 index 00000000000..e785dde426d --- /dev/null +++ b/addons/account/views/account_analytic_plan_views.xml @@ -0,0 +1,18 @@ + + + + + account.analytic.plan.inherit.form + account.analytic.plan + + + + + + + + + + + + \ No newline at end of file diff --git a/addons/account/views/account_menuitem.xml b/addons/account/views/account_menuitem.xml index a0084039712..2be20736116 100644 --- a/addons/account/views/account_menuitem.xml +++ b/addons/account/views/account_menuitem.xml @@ -35,6 +35,7 @@ + @@ -63,11 +64,10 @@ - + + - - - + diff --git a/addons/account/views/account_move_views.xml b/addons/account/views/account_move_views.xml index 57bc310bda5..69119b2bbf3 100644 --- a/addons/account/views/account_move_views.xml +++ b/addons/account/views/account_move_views.xml @@ -69,12 +69,10 @@ - - + + - @@ -163,18 +161,17 @@ account.move.line - + - - + + - + - @@ -184,14 +181,19 @@ - + - - - - + + + + + + - @@ -274,7 +276,7 @@ - + hide @@ -345,7 +347,6 @@ - @@ -884,15 +885,12 @@ options="{'no_create': True}" domain="[('deprecated', '=', False), ('account_type', 'not in', ('asset_receivable', 'liability_payable')), ('company_id', '=', parent.company_id), ('is_off_balance', '=', False)]" attrs="{'required': [('display_type', 'not in', ('line_note', 'line_section'))]}"/> - + - + options="{'product_field': 'product_id', 'account_field': 'account_id'}" + business_domain_compute="parent.move_type in ['out_invoice', 'out_refund', 'out_receipt'] and 'sale' or parent.move_type in ['in_invoice', 'in_refund', 'in_receipt'] and 'purchase' or 'general'"/> @@ -994,10 +992,10 @@ - - + +
@@ -174,7 +179,12 @@ - + + + diff --git a/addons/purchase_stock/views/purchase_views.xml b/addons/purchase_stock/views/purchase_views.xml index 63573286768..63c8508cfee 100644 --- a/addons/purchase_stock/views/purchase_views.xml +++ b/addons/purchase_stock/views/purchase_views.xml @@ -52,7 +52,7 @@ - + diff --git a/addons/sale/models/account_move_line.py b/addons/sale/models/account_move_line.py index 6cb4f6d4e35..5e989dac515 100644 --- a/addons/sale/models/account_move_line.py +++ b/addons/sale/models/account_move_line.py @@ -21,26 +21,26 @@ class AccountMoveLine(models.Model): super(AccountMoveLine, self)._copy_data_extend_business_fields(values) values['sale_line_ids'] = [(6, None, self.sale_line_ids.ids)] - def _prepare_analytic_line(self): - """ Note: This method is called only on the move.line that having an analytic account, and + def _prepare_analytic_lines(self): + """ Note: This method is called only on the move.line that having an analytic distribution, and so that should create analytic entries. """ - values_list = super(AccountMoveLine, self)._prepare_analytic_line() + values_list = super(AccountMoveLine, self)._prepare_analytic_lines() # filter the move lines that can be reinvoiced: a cost (negative amount) analytic line without SO line but with a product can be reinvoiced move_to_reinvoice = self.env['account.move.line'] - for index, move_line in enumerate(self): - values = values_list[index] - if 'so_line' not in values: - if move_line._sale_can_be_reinvoice(): - move_to_reinvoice |= move_line + if len(values_list) > 0: + for index, move_line in enumerate(self): + values = values_list[index] + if 'so_line' not in values: + if move_line._sale_can_be_reinvoice(): + move_to_reinvoice |= move_line # insert the sale line in the create values of the analytic entries if move_to_reinvoice: map_sale_line_per_move = move_to_reinvoice._sale_create_reinvoice_sale_line() - for values in values_list: - sale_line = map_sale_line_per_move.get(values.get('move_id')) + sale_line = map_sale_line_per_move.get(values.get('move_line_id')) if sale_line: values['so_line'] = sale_line.id @@ -133,23 +133,20 @@ class AccountMoveLine(models.Model): """ Get the mapping of move.line with the sale.order record on which its analytic entries should be reinvoiced :return a dict where key is the move line id, and value is sale.order record (or None). """ - analytic_accounts = self.mapped('analytic_account_id') - - # link the analytic account with its open SO by creating a map: {AA.id: sale.order}, if we find some analytic accounts mapping = {} - if analytic_accounts: # first, search for the open sales order - sale_orders = self.env['sale.order'].search([('analytic_account_id', 'in', analytic_accounts.ids), ('state', '=', 'sale')], order='create_date DESC') - for sale_order in sale_orders: - mapping[sale_order.analytic_account_id.id] = sale_order - - analytic_accounts_without_open_order = analytic_accounts.filtered(lambda account: not mapping.get(account.id)) - if analytic_accounts_without_open_order: # then, fill the blank with not open sales orders - sale_orders = self.env['sale.order'].search([('analytic_account_id', 'in', analytic_accounts_without_open_order.ids)], order='create_date DESC') - for sale_order in sale_orders: - mapping[sale_order.analytic_account_id.id] = sale_order + for move_line in self: + if move_line.analytic_distribution: + distribution_json = move_line.analytic_distribution + sale_order = self.env['sale.order'].search([('analytic_account_id', 'in', list(int(account_id) for account_id in distribution_json.keys())), + ('state', '=', 'sale')], order='create_date ASC', limit=1) + if sale_order: + mapping[move_line.id] = sale_order + else: + sale_order = self.env['sale.order'].search([('analytic_account_id', 'in', list(int(account_id) for account_id in distribution_json.keys()))], order='create_date ASC', limit=1) + mapping[move_line.id] = sale_order # map of AAL index with the SO on which it needs to be reinvoiced. Maybe be None if no SO found - return {move_line.id: mapping.get(move_line.analytic_account_id.id) for move_line in self} + return mapping def _sale_prepare_sale_line_values(self, order, price): """ Generate the sale.line creation value from the current move line """ diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index fb688744065..49a7b803315 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -250,7 +250,6 @@ class SaleOrder(models.Model): analytic_account_id = fields.Many2one( comodel_name='account.analytic.account', string="Analytic Account", - compute='_compute_analytic_account_id', store=True, readonly=False, copy=False, check_company=True, # Unrequired company states=READONLY_FIELD_STATES, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") @@ -554,18 +553,6 @@ class SaleOrder(models.Model): else: record.tax_country_id = record.company_id.account_fiscal_country_id - @api.depends('partner_id', 'date_order') - def _compute_analytic_account_id(self): - for order in self: - if not order.analytic_account_id: - default_analytic_account = order.env['account.analytic.default'].sudo().account_get( - partner_id=order.partner_id.id, - user_id=order.env.uid, - date=order.date_order, - company_id=order.company_id.id, - ) - order.analytic_account_id = default_analytic_account.analytic_id - @api.depends('company_id', 'partner_id', 'amount_total') def _compute_partner_credit_warning(self): for order in self: @@ -1391,10 +1378,17 @@ class SaleOrder(models.Model): name = self.name if prefix: name = prefix + ": " + self.name + plan = self.env['account.analytic.plan'].search(['|', ('company_id', '=', self.company_id.id), ('company_id', '=', False)], limit=1) + if not plan: + plan = self.env['account.analytic.plan'].create({ + 'name': 'Default', + 'company_id': self.company_id.id + }) return { 'name': name, 'code': self.client_order_ref, 'company_id': self.company_id.id, + 'plan_id': plan.id, 'partner_id': self.partner_id.id } diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 38190cc1bab..9136d2c7e70 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -13,6 +13,7 @@ from odoo.tools import float_is_zero, float_compare, float_round class SaleOrderLine(models.Model): _name = 'sale.order.line' + _inherit = 'analytic.mixin' _description = "Sales Order Line" _rec_names_search = ['name', 'order_id.name'] _order = 'order_id, sequence, id' @@ -217,11 +218,6 @@ class SaleOrderLine(models.Model): digits='Product Unit of Measure', store=True) - analytic_tag_ids = fields.Many2many( - comodel_name='account.analytic.tag', - string="Analytic Tags", - compute='_compute_analytic_tag_ids', store=True, readonly=False, - domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") analytic_line_ids = fields.One2many( comodel_name='account.analytic.line', inverse_name='so_line', string="Analytic lines") @@ -879,18 +875,19 @@ class SaleOrderLine(models.Model): line.untaxed_amount_to_invoice = amount_to_invoice - @api.depends('product_id', 'order_id.date_order', 'order_id.partner_id') - def _compute_analytic_tag_ids(self): + @api.depends('order_id.partner_id', 'product_id') + def _compute_analytic_distribution_stored_char(self): for line in self: if not line.display_type and line.state == 'draft': - default_analytic_account = line.env['account.analytic.default'].sudo().account_get( - product_id=line.product_id.id, - partner_id=line.order_id.partner_id.id, - user_id=self.env.uid, - date=line.order_id.date_order, - company_id=line.company_id.id, - ) - line.analytic_tag_ids = default_analytic_account.analytic_tag_ids + distribution = line.env['account.analytic.distribution.model']._get_distributionjson({ + "product_id": line.product_id.id, + "product_categ_id": line.product_id.categ_id.id, + "partner_id": line.order_id.partner_id.id, + "partner_category_id": line.order_id.partner_id.category_id.ids, + "company_id": line.company_id.id, + }) + line.analytic_distribution_stored_char = distribution or line.analytic_distribution_stored_char + line._compute_analytic_distribution() @api.depends('product_id', 'state', 'qty_invoiced', 'qty_delivered') def _compute_product_updatable(self): @@ -1000,7 +997,7 @@ class SaleOrderLine(models.Model): def _get_protected_fields(self): return [ 'product_id', 'name', 'price_unit', 'product_uom', 'product_uom_qty', - 'tax_id', 'analytic_tag_ids' + 'tax_id', 'analytic_distribution_stored_char' ] def _update_line_quantity(self, values): @@ -1077,12 +1074,17 @@ class SaleOrderLine(models.Model): 'discount': self.discount, 'price_unit': self.price_unit, 'tax_ids': [Command.set(self.tax_id.ids)], - 'analytic_tag_ids': [Command.set(self.analytic_tag_ids.ids)], + 'analytic_distribution': self.analytic_distribution, 'sale_line_ids': [Command.link(self.id)], 'is_downpayment': self.is_downpayment, } - if self.order_id.analytic_account_id: - res['analytic_account_id'] = self.order_id.analytic_account_id.id + analytic_account_id = self.order_id.analytic_account_id.id + if analytic_account_id: + res['analytic_distribution'] = res['analytic_distribution'] or {} + if self.analytic_distribution: + res['analytic_distribution'][analytic_account_id] = self.analytic_distribution.get(analytic_account_id, 0) + 100 + else: + res['analytic_distribution'][analytic_account_id] = 100 if optional_values: res.update(optional_values) if self.display_type: diff --git a/addons/sale/security/ir.model.access.csv b/addons/sale/security/ir.model.access.csv index 83041148a3b..ec6bbc89646 100644 --- a/addons/sale/security/ir.model.access.csv +++ b/addons/sale/security/ir.model.access.csv @@ -1,7 +1,6 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_account_account_salesman,account_account salesman,account.model_account_account,sales_team.group_sale_salesman,1,0,0,0 access_account_analytic_account_salesman,account_analytic_account salesman,analytic.model_account_analytic_account,sales_team.group_sale_salesman,1,1,1,0 -access_account_analytic_tag_sale_salesman,account.analytic.tag.sale.salesman,analytic.model_account_analytic_tag,sales_team.group_sale_salesman,1,0,0,0 access_account_account_tag_sale_salesman,account.account.tag.sale.salesman,account.model_account_account_tag,sales_team.group_sale_salesman,1,0,0,0 access_account_invoice_send_salesman,access.account.invoice.send.salesman,account.model_account_invoice_send,sales_team.group_sale_salesman,1,1,1,0 access_sale_account_journal,account.journal sale order.user,account.model_account_journal,sales_team.group_sale_salesman,1,0,0,0 diff --git a/addons/sale/tests/test_reinvoice.py b/addons/sale/tests/test_reinvoice.py index 6b893d46680..9669a7aa3ba 100644 --- a/addons/sale/tests/test_reinvoice.py +++ b/addons/sale/tests/test_reinvoice.py @@ -12,10 +12,16 @@ class TestReInvoice(TestSaleCommon): def setUpClass(cls, chart_template_ref=None): super().setUpClass(chart_template_ref=chart_template_ref) + cls.analytic_plan = cls.env['account.analytic.plan'].create({ + 'name': 'Plan', + 'company_id': cls.partner_a.company_id.id, + }) + cls.analytic_account = cls.env['account.analytic.account'].create({ 'name': 'Test AA', 'code': 'TESTSALE_REINVOICE', 'company_id': cls.partner_a.company_id.id, + 'plan_id': cls.analytic_plan.id, 'partner_id': cls.partner_a.id }) @@ -60,11 +66,11 @@ class TestReInvoice(TestSaleCommon): with move_form.invoice_line_ids.new() as line_form: line_form.product_id = self.company_data['product_order_cost'] line_form.quantity = 3.0 - line_form.analytic_account_id = self.analytic_account + line_form.analytic_distribution = {self.analytic_account.id: 100} with move_form.invoice_line_ids.new() as line_form: line_form.product_id = self.company_data['product_delivery_cost'] line_form.quantity = 3.0 - line_form.analytic_account_id = self.analytic_account + line_form.analytic_distribution = {self.analytic_account.id: 100} invoice_a = move_form.save() invoice_a.action_post() @@ -88,11 +94,11 @@ class TestReInvoice(TestSaleCommon): with move_form.invoice_line_ids.new() as line_form: line_form.product_id = self.company_data['product_order_cost'] line_form.quantity = 2.0 - line_form.analytic_account_id = self.analytic_account + line_form.analytic_distribution = {self.analytic_account.id: 100} with move_form.invoice_line_ids.new() as line_form: line_form.product_id = self.company_data['product_delivery_cost'] line_form.quantity = 2.0 - line_form.analytic_account_id = self.analytic_account + line_form.analytic_distribution = {self.analytic_account.id: 100} invoice_b = move_form.save() invoice_b.action_post() @@ -171,11 +177,11 @@ class TestReInvoice(TestSaleCommon): with move_form.invoice_line_ids.new() as line_form: line_form.product_id = self.company_data['product_delivery_sales_price'] line_form.quantity = 3.0 - line_form.analytic_account_id = self.analytic_account + line_form.analytic_distribution = {self.analytic_account.id: 100} with move_form.invoice_line_ids.new() as line_form: line_form.product_id = self.company_data['product_order_sales_price'] line_form.quantity = 3.0 - line_form.analytic_account_id = self.analytic_account + line_form.analytic_distribution = {self.analytic_account.id: 100} invoice_a = move_form.save() invoice_a.action_post() @@ -199,11 +205,11 @@ class TestReInvoice(TestSaleCommon): with move_form.invoice_line_ids.new() as line_form: line_form.product_id = self.company_data['product_delivery_sales_price'] line_form.quantity = 2.0 - line_form.analytic_account_id = self.analytic_account + line_form.analytic_distribution = {self.analytic_account.id: 100} with move_form.invoice_line_ids.new() as line_form: line_form.product_id = self.company_data['product_order_sales_price'] line_form.quantity = 2.0 - line_form.analytic_account_id = self.analytic_account + line_form.analytic_distribution = {self.analytic_account.id: 100} invoice_b = move_form.save() invoice_b.action_post() @@ -237,7 +243,7 @@ class TestReInvoice(TestSaleCommon): with move_form.invoice_line_ids.new() as line_form: line_form.product_id = self.company_data['product_delivery_no'] line_form.quantity = 3.0 - line_form.analytic_account_id = self.analytic_account + line_form.analytic_distribution = {self.analytic_account.id: 100} invoice_a = move_form.save() invoice_a.action_post() diff --git a/addons/sale/tests/test_sale_flow.py b/addons/sale/tests/test_sale_flow.py index 3095b5345da..f320d53b60f 100644 --- a/addons/sale/tests/test_sale_flow.py +++ b/addons/sale/tests/test_sale_flow.py @@ -31,9 +31,15 @@ class TestSaleFlow(TestSaleCommonBase): 'company_id': False, }) + cls.analytic_plan = cls.env['account.analytic.plan'].create({ + 'name': 'Plan', + 'company_id': cls.company.id, + }) + cls.analytic_account = cls.env['account.analytic.account'].create({ 'name': 'Test analytic_account', 'code': 'analytic_account', + 'plan_id': cls.analytic_plan.id, 'company_id': cls.company.id, 'partner_id': cls.partner_a.id }) diff --git a/addons/sale/tests/test_sale_order.py b/addons/sale/tests/test_sale_order.py index dbc7351a857..3de988b19cd 100644 --- a/addons/sale/tests/test_sale_order.py +++ b/addons/sale/tests/test_sale_order.py @@ -374,27 +374,23 @@ class TestSalesTeam(SaleCommon): sale_order.user_id = self.user_not_in_team self.assertEqual(sale_order.team_id.id, self.sale_team_2.id, 'Should not reset the team to default') - def test_sale_order_analytic_tag_change(self): + def test_sale_order_analytic_distribution_change(self): self.env.user.groups_id += self.env.ref('analytic.group_analytic_accounting') - self.env.user.groups_id += self.env.ref('analytic.group_analytic_tags') - analytic_account_super = self.env['account.analytic.account'].create({'name': 'Super Account'}) - analytic_account_great = self.env['account.analytic.account'].create({'name': 'Great Account'}) - analytic_tag_super = self.env['account.analytic.tag'].create({'name': 'Super Tag'}) - analytic_tag_great = self.env['account.analytic.tag'].create({'name': 'Great Tag'}) + analytic_plan = self.env['account.analytic.plan'].create({'name': 'Plan Test', 'company_id': False}) + analytic_account_super = self.env['account.analytic.account'].create({'name': 'Super Account', 'plan_id': analytic_plan.id}) + analytic_account_great = self.env['account.analytic.account'].create({'name': 'Great Account', 'plan_id': analytic_plan.id}) super_product = self.env['product.product'].create({'name': 'Super Product'}) great_product = self.env['product.product'].create({'name': 'Great Product'}) product_no_account = self.env['product.product'].create({'name': 'Product No Account'}) - self.env['account.analytic.default'].create([ + self.env['account.analytic.distribution.model'].create([ { - 'analytic_id': analytic_account_super.id, + 'analytic_distribution': {analytic_account_super.id: 100}, 'product_id': super_product.id, - 'analytic_tag_ids': [analytic_tag_super.id], }, { - 'analytic_id': analytic_account_great.id, + 'analytic_distribution': {analytic_account_great.id: 100}, 'product_id': great_product.id, - 'analytic_tag_ids': [analytic_tag_great.id], }, ]) sale_order = self.env['sale.order'].create({ @@ -406,11 +402,9 @@ class TestSalesTeam(SaleCommon): 'order_id': sale_order.id, }) - self.assertEqual(sol.analytic_tag_ids.id, analytic_tag_super.id, "The analytic tag should be set to 'Super Tag'") + self.assertEqual(sol.analytic_distribution, {analytic_account_super.id: 100}, "The analytic distribution should be set to Super Account") sol.write({'product_id': great_product.id}) - self.assertEqual(sol.analytic_tag_ids.id, analytic_tag_great.id, "The analytic tag should be set to 'Great Tag'") - sol.write({'product_id': product_no_account.id}) - self.assertFalse(sol.analytic_tag_ids.id, "The analytic account should not be set") + self.assertEqual(sol.analytic_distribution, {analytic_account_great.id: 100}, "The analytic distribution should be set to Great Account") so_no_analytic_account = self.env['sale.order'].create({ 'partner_id': self.env.ref('base.res_partner_1').id, @@ -419,7 +413,7 @@ class TestSalesTeam(SaleCommon): 'name': super_product.name, 'product_id': super_product.id, 'order_id': so_no_analytic_account.id, - 'analytic_tag_ids': False, + 'analytic_distribution': False, }) so_no_analytic_account.action_confirm() - self.assertFalse(sol_no_analytic_account.analytic_tag_ids.id, "The compute should not overwrite what the user has set.") + self.assertFalse(sol_no_analytic_account.analytic_distribution, "The compute should not overwrite what the user has set.") diff --git a/addons/sale/tests/test_sale_to_invoice.py b/addons/sale/tests/test_sale_to_invoice.py index be7c6d2d29b..2b65c54f52a 100644 --- a/addons/sale/tests/test_sale_to_invoice.py +++ b/addons/sale/tests/test_sale_to_invoice.py @@ -414,14 +414,15 @@ class TestSaleToInvoice(TestSaleCommon): downpayment.create_invoices() self.assertEqual(so_for_downpayment.invoice_ids[0].company_id.id, so_company_id, "The company of the downpayment invoice should be the same as the one from the SO") - def test_invoice_analytic_account_default(self): + def test_invoice_analytic_distribution_model(self): """ Tests whether, when an analytic account rule is set and the so has no analytic account, - the default analytic acount is correctly computed in the invoice. + the default analytic account is correctly computed in the invoice. """ - analytic_account_default = self.env['account.analytic.account'].create({'name': 'default'}) + analytic_plan_default = self.env['account.analytic.plan'].create({'name': 'default'}) + analytic_account_default = self.env['account.analytic.account'].create({'name': 'default', 'plan_id': analytic_plan_default.id}) - self.env['account.analytic.default'].create({ - 'analytic_id': analytic_account_default.id, + self.env['account.analytic.distribution.model'].create({ + 'analytic_distribution': {analytic_account_default.id: 100}, 'product_id': self.product_a.id, }) @@ -446,7 +447,7 @@ class TestSaleToInvoice(TestSaleCommon): down_payment.create_invoices() aml = self.env['account.move.line'].search([('move_id', 'in', so.invoice_ids.ids)])[0] - self.assertRecordValues(aml, [{'analytic_account_id': analytic_account_default.id}]) + self.assertRecordValues(aml, [{'analytic_distribution': {analytic_account_default.id: 100}}]) def test_invoice_analytic_account_so_not_default(self): """ Tests whether, when an analytic account rule is set and the so has an analytic account, @@ -454,11 +455,12 @@ class TestSaleToInvoice(TestSaleCommon): """ # Required for `analytic_account_id` to be visible in the view self.env.user.groups_id += self.env.ref('analytic.group_analytic_accounting') - analytic_account_default = self.env['account.analytic.account'].create({'name': 'default'}) - analytic_account_so = self.env['account.analytic.account'].create({'name': 'so'}) + analytic_plan_default = self.env['account.analytic.plan'].create({'name': 'default'}) + analytic_account_default = self.env['account.analytic.account'].create({'name': 'default', 'plan_id': analytic_plan_default.id}) + analytic_account_so = self.env['account.analytic.account'].create({'name': 'so', 'plan_id': analytic_plan_default.id}) - self.env['account.analytic.default'].create({ - 'analytic_id': analytic_account_default.id, + self.env['account.analytic.distribution.model'].create({ + 'analytic_distribution': {analytic_account_default.id: 100}, 'product_id': self.product_a.id, }) @@ -484,7 +486,7 @@ class TestSaleToInvoice(TestSaleCommon): down_payment.create_invoices() aml = self.env['account.move.line'].search([('move_id', 'in', so.invoice_ids.ids)])[0] - self.assertRecordValues(aml, [{'analytic_account_id': analytic_account_so.id}]) + self.assertRecordValues(aml, [{'analytic_distribution': {analytic_account_default.id: 100, analytic_account_so.id: 100}}]) def test_invoice_after_product_return_price_not_default(self): so = self.env['sale.order'].create({ @@ -598,7 +600,7 @@ class TestSaleToInvoice(TestSaleCommon): 'product_uom_id': serv_cost.uom_id.id, 'quantity': 2, 'price_unit': serv_cost.standard_price, - 'analytic_account_id': so.analytic_account_id.id, + 'analytic_distribution': {so.analytic_account_id.id: 100}, }), ], }) diff --git a/addons/sale/views/sale_order_views.xml b/addons/sale/views/sale_order_views.xml index 1d3603f31f5..d74a626e4a5 100644 --- a/addons/sale/views/sale_order_views.xml +++ b/addons/sale/views/sale_order_views.xml @@ -362,7 +362,12 @@
days
- + + +
diff --git a/addons/sale_timesheet/static/tests/tours/sale_timesheet_tour.js b/addons/sale_timesheet/static/tests/tours/sale_timesheet_tour.js index 981d8a10c9a..4fe1bbf68c0 100644 --- a/addons/sale_timesheet/static/tests/tours/sale_timesheet_tour.js +++ b/addons/sale_timesheet/static/tests/tours/sale_timesheet_tour.js @@ -160,7 +160,7 @@ tour.register('sale_timesheet_tour', { content: 'Select a Sales Order Item as Default Sales Order Item for each task in this project.', run: 'text S', }, { - trigger: 'ul.ui-autocomplete > li:first-child > a:not(:has(i.fa))', + trigger: '[name="sale_line_id"] ul.ui-autocomplete > li:first-child > a:not(:has(i.fa))', content: 'Select the Sales Order Item in the autocomplete dropdown.', }, { trigger: 'div[name="sale_line_employee_ids"] td.o_field_x2many_list_row_add > a[role="button"]', @@ -170,7 +170,7 @@ tour.register('sale_timesheet_tour', { content: 'Select an employee to link a Sales Order Item on his timesheets into this project.', run: 'click', }, { - trigger: 'ul.ui-autocomplete > li:first-child > a:not(:has(i.fa))', + trigger: '[name="employee_id"] ul.ui-autocomplete > li:first-child > a:not(:has(i.fa))', content: 'Select the first employee in the autocomplete dropdown', }, { trigger: 'div[name="sale_line_employee_ids"] div[name="sale_line_id"] input', @@ -178,7 +178,7 @@ tour.register('sale_timesheet_tour', { position: 'bottom', run: 'text S', }, { - trigger: 'ul.ui-autocomplete > li:first-child > a:not(:has(i.fa))', + trigger: '[name=sale_line_id] ul.ui-autocomplete > li:first-child > a:not(:has(i.fa))', content: 'Select the first Sales Order Item in the autocomplete dropdown.', }, { trigger: 'h1 > div[name="name"] > input', diff --git a/addons/sale_timesheet/tests/common.py b/addons/sale_timesheet/tests/common.py index 3bee18fbce5..5d30672d5de 100644 --- a/addons/sale_timesheet/tests/common.py +++ b/addons/sale_timesheet/tests/common.py @@ -55,9 +55,14 @@ class TestCommonSaleTimesheet(TestSaleProjectCommon): # Account and project cls.analytic_account_sale.name = 'Project for selling timesheet - AA' + cls.analytic_plan = cls.env['account.analytic.plan'].create({ + 'name': 'Plan Test', + 'company_id': cls.company_data_2['company'].id, + }) cls.analytic_account_sale_company_B = cls.env['account.analytic.account'].create({ 'name': 'Project for selling timesheet Company B - AA', 'code': 'AA-2030', + 'plan_id': cls.analytic_plan.id, 'company_id': cls.company_data_2['company'].id, }) diff --git a/addons/sale_timesheet/tests/test_reinvoice.py b/addons/sale_timesheet/tests/test_reinvoice.py index 9c78639d6d3..471b84b9b4f 100644 --- a/addons/sale_timesheet/tests/test_reinvoice.py +++ b/addons/sale_timesheet/tests/test_reinvoice.py @@ -28,10 +28,16 @@ class TestReInvoice(TestCommonSaleTimesheet): cls.company_data['product_order_no'].write(service_values) # create AA, SO and invoices + cls.analytic_plan = cls.env['account.analytic.plan'].create({ + 'name': 'Plan', + 'company_id': cls.company_data['company'].id, + }) + cls.analytic_account = cls.env['account.analytic.account'].create({ 'name': 'Test AA', 'code': 'TESTSALE_TIMESHEET_REINVOICE', 'company_id': cls.company_data['company'].id, + 'plan_id': cls.analytic_plan.id, 'partner_id': cls.partner_a.id }) @@ -87,11 +93,11 @@ class TestReInvoice(TestCommonSaleTimesheet): with move_form.invoice_line_ids.new() as line_form: line_form.product_id = self.company_data['product_order_cost'] line_form.quantity = 3.0 - line_form.analytic_account_id = self.analytic_account + line_form.analytic_distribution = {self.analytic_account.id: 100} with move_form.invoice_line_ids.new() as line_form: line_form.product_id = self.company_data['product_delivery_cost'] line_form.quantity = 3.0 - line_form.analytic_account_id = self.analytic_account + line_form.analytic_distribution = {self.analytic_account.id: 100} invoice_a = move_form.save() invoice_a.action_post() @@ -122,11 +128,11 @@ class TestReInvoice(TestCommonSaleTimesheet): with move_form.invoice_line_ids.new() as line_form: line_form.product_id = self.company_data['product_order_cost'] line_form.quantity = 2.0 - line_form.analytic_account_id = self.analytic_account + line_form.analytic_distribution = {self.analytic_account.id: 100} with move_form.invoice_line_ids.new() as line_form: line_form.product_id = self.company_data['product_delivery_cost'] line_form.quantity = 2.0 - line_form.analytic_account_id = self.analytic_account + line_form.analytic_distribution = {self.analytic_account.id: 100} invoice_b = move_form.save() invoice_b.action_post() @@ -179,11 +185,11 @@ class TestReInvoice(TestCommonSaleTimesheet): with move_form.invoice_line_ids.new() as line_form: line_form.product_id = self.company_data['product_delivery_sales_price'] line_form.quantity = 3.0 - line_form.analytic_account_id = self.analytic_account + line_form.analytic_distribution = {self.analytic_account.id: 100} with move_form.invoice_line_ids.new() as line_form: line_form.product_id = self.company_data['product_order_sales_price'] line_form.quantity = 3.0 - line_form.analytic_account_id = self.analytic_account + line_form.analytic_distribution = {self.analytic_account.id: 100} invoice_a = move_form.save() invoice_a.action_post() @@ -214,11 +220,11 @@ class TestReInvoice(TestCommonSaleTimesheet): with move_form.invoice_line_ids.new() as line_form: line_form.product_id = self.company_data['product_delivery_sales_price'] line_form.quantity = 2.0 - line_form.analytic_account_id = self.analytic_account + line_form.analytic_distribution = {self.analytic_account.id: 100} with move_form.invoice_line_ids.new() as line_form: line_form.product_id = self.company_data['product_order_sales_price'] line_form.quantity = 2.0 - line_form.analytic_account_id = self.analytic_account + line_form.analytic_distribution = {self.analytic_account.id: 100} invoice_b = move_form.save() invoice_b.action_post() @@ -252,7 +258,7 @@ class TestReInvoice(TestCommonSaleTimesheet): with move_form.invoice_line_ids.new() as line_form: line_form.product_id = self.company_data['product_order_no'] line_form.quantity = 3.0 - line_form.analytic_account_id = self.analytic_account + line_form.analytic_distribution = {self.analytic_account.id: 100} invoice_a = move_form.save() invoice_a.action_post() diff --git a/addons/sale_timesheet/tests/test_sale_service.py b/addons/sale_timesheet/tests/test_sale_service.py index 962f7fefcc6..afd91b59d76 100644 --- a/addons/sale_timesheet/tests/test_sale_service.py +++ b/addons/sale_timesheet/tests/test_sale_service.py @@ -685,8 +685,8 @@ class TestSaleService(TestCommonSaleTimesheet): # Check that the resulting invoice line and the project have the same analytic account invoice_line = self.sale_order.invoice_ids.line_ids.filtered(lambda line: line.product_id == product_add) - self.assertEqual(invoice_line.analytic_account_id, self.project_global.analytic_account_id, - "SOL's analytic account should be the same as the project's") + self.assertEqual(invoice_line.analytic_distribution, {self.project_global.analytic_account_id.id: 100}, + "SOL's analytic distribution should contain the project analytic account") def test_sale_timesheet_invoice(self): """ Test timesheet is correctly linked to an invoice when its SOL is invoiced diff --git a/addons/stock_account/models/account_move.py b/addons/stock_account/models/account_move.py index 4d8d332c646..65e8c3ad7ec 100644 --- a/addons/stock_account/models/account_move.py +++ b/addons/stock_account/models/account_move.py @@ -177,8 +177,7 @@ class AccountMove(models.Model): 'price_unit': -price_unit, 'amount_currency': amount_currency, 'account_id': credit_expense_account.id, - 'analytic_account_id': line.analytic_account_id.id, - 'analytic_tag_ids': [(6, 0, line.analytic_tag_ids.ids)], + 'analytic_distribution': line.analytic_distribution, 'display_type': 'cogs', 'tax_ids': [], }) diff --git a/addons/stock_account/models/stock_move.py b/addons/stock_account/models/stock_move.py index ac9ead92817..f08fbcbd0d2 100644 --- a/addons/stock_account/models/stock_move.py +++ b/addons/stock_account/models/stock_move.py @@ -393,7 +393,7 @@ class StockMove(models.Model): return res - def _prepare_analytic_line(self): + def _prepare_analytic_lines(self): self.ensure_one() if not self._get_analytic_account(): return False @@ -411,9 +411,9 @@ class StockMove(models.Model): accounts_data = self.product_id.product_tmpl_id.get_product_accounts() account_valuation = accounts_data.get('stock_valuation', False) analytic_line_vals = self.stock_valuation_layer_ids.account_move_id.line_ids.filtered( - lambda l: l.account_id == account_valuation)._prepare_analytic_line() - amount = - sum(vals['amount'] for vals in analytic_line_vals) - unit_amount = - sum(vals['unit_amount'] for vals in analytic_line_vals) + lambda l: l.account_id == account_valuation)._prepare_analytic_lines() + amount = - sum(sum(vals['amount'] for vals in lists) for lists in analytic_line_vals) + unit_amount = - sum(sum(vals['unit_amount'] for vals in lists) for lists in analytic_line_vals) elif sum(self.stock_valuation_layer_ids.mapped('quantity')): amount = sum(self.stock_valuation_layer_ids.mapped('value')) unit_amount = - sum(self.stock_valuation_layer_ids.mapped('quantity')) @@ -520,7 +520,7 @@ class StockMove(models.Model): analytic_lines_vals = [] moves_to_link = [] for move in self: - analytic_line_vals = move._prepare_analytic_line() + analytic_line_vals = move._prepare_analytic_lines() if not analytic_line_vals: continue moves_to_link.append(move.id) diff --git a/addons/web/static/tests/legacy/core/py_utils_tests.js b/addons/web/static/tests/legacy/core/py_utils_tests.js index e3cec9ede9c..a82fea85f69 100644 --- a/addons/web/static/tests/legacy/core/py_utils_tests.js +++ b/addons/web/static/tests/legacy/core/py_utils_tests.js @@ -941,7 +941,7 @@ QUnit.module('core', function () { "price_unit": 100, "account_id": 853, "discount": 0, - "account_analytic_id": false, + "analytic_distribution": false, "company_id": false, "note": false, "invoice_line_tax_ids": [[6, false, [1]]], @@ -998,7 +998,7 @@ QUnit.module('core', function () { [0, false, { account_id: 55, amount_currency: 0, - analytic_account_id: false, + analytic_distribution: false, credit: 0, currency_id: false, date_maturity: false, @@ -1025,7 +1025,7 @@ QUnit.module('core', function () { line_id: [[0, false, { account_id: 55, amount_currency: 0, - analytic_account_id: false, + analytic_distribution: false, credit: 0, currency_id: false, date_maturity: false,