From 5419fe445dbaab24bdda1e7a5d25e4fac3fb2f1c Mon Sep 17 00:00:00 2001 From: oco-odoo Date: Tue, 31 Aug 2021 15:54:04 +0000 Subject: [PATCH] [IMP] account: allow instantiating taxes from foreign charts of accounts In multivat setup, we need to create taxes in foreign countries to map them with domestic taxes. Before this commit, this was only possible manually, even though all the necessary information lies in Odoo's account.tax.template objects created by l10n modules. We now display a banner on top of foreign vat fiscal positions with a button allowing instantiating those templates directly from them when it's appropriate. closes odoo/odoo#75803 Task: 2453325 Related: odoo/enterprise#20553 Signed-off-by: Quentin De Paoli (qdp) --- addons/account/models/account_tax.py | 2 +- addons/account/models/chart_template.py | 189 ++++++++++++++---- addons/account/models/partner.py | 24 +++ addons/account/views/account_tax_views.xml | 1 + addons/account/views/partner_view.xml | 22 ++ .../models/account_chart_template.py | 2 +- 6 files changed, 200 insertions(+), 40 deletions(-) diff --git a/addons/account/models/account_tax.py b/addons/account/models/account_tax.py index 7fe82dbe3bb..d9c3bb66470 100644 --- a/addons/account/models/account_tax.py +++ b/addons/account/models/account_tax.py @@ -110,7 +110,7 @@ class AccountTax(models.Model): tax_exigibility = fields.Selection( [('on_invoice', 'Based on Invoice'), ('on_payment', 'Based on Payment'), - ], string='Tax Due', default='on_invoice', + ], string='Tax Exigibility', default='on_invoice', help="Based on Invoice: the tax is due as soon as the invoice is validated.\n" "Based on Payment: the tax is due as soon as the payment of the invoice is received.") cash_basis_transition_account_id = fields.Many2one(string="Cash Basis Transition Account", diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py index cbb3c320dbe..62df0a789a6 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -472,9 +472,9 @@ class AccountChartTemplate(models.Model): # Get the default accounts default_account = False if journal['type'] == 'sale': - default_account = acc_template_ref.get(self.property_account_income_categ_id.id) + default_account = acc_template_ref.get(self.property_account_income_categ_id).id elif journal['type'] == 'purchase': - default_account = acc_template_ref.get(self.property_account_expense_categ_id.id) + default_account = acc_template_ref.get(self.property_account_expense_categ_id).id return default_account @@ -525,7 +525,7 @@ class AccountChartTemplate(models.Model): ] for field, model in todo_list: account = self[field] - value = acc_template_ref[account.id] if account else False + value = acc_template_ref[account].id if account else False if value: PropertyObj._set_default(field, model, value, company=company) @@ -536,7 +536,7 @@ class AccountChartTemplate(models.Model): ] for stock_property in stock_properties: account = getattr(self, stock_property) - value = account and acc_template_ref[account.id] or False + value = account and acc_template_ref[account].id or False if value: company.write({stock_property: value}) return True @@ -604,18 +604,13 @@ class AccountChartTemplate(models.Model): self.generate_account_groups(company) # writing account values after creation of accounts - for key, value in generated_tax_res['account_dict']['account.tax'].items(): + for tax, value in generated_tax_res['account_dict']['account.tax'].items(): if value['cash_basis_transition_account_id']: - AccountTaxObj.browse(key).write({ - 'cash_basis_transition_account_id': account_ref.get(value['cash_basis_transition_account_id'], False), - }) + tax.cash_basis_transition_account_id = account_ref.get(value['cash_basis_transition_account_id']) - AccountTaxRepartitionLineObj = self.env['account.tax.repartition.line'] - for key, value in generated_tax_res['account_dict']['account.tax.repartition.line'].items(): + for repartition_line, value in generated_tax_res['account_dict']['account.tax.repartition.line'].items(): if value['account_id']: - AccountTaxRepartitionLineObj.browse(key).write({ - 'account_id': account_ref.get(value['account_id']), - }) + repartition_line.account_id = account_ref.get(value['account_id']) # Set the company accounts self._load_company_accounts(account_ref, company) @@ -638,15 +633,15 @@ class AccountChartTemplate(models.Model): def _load_company_accounts(self, account_ref, company): # Set the default accounts on the company accounts = { - 'default_cash_difference_income_account_id': self.default_cash_difference_income_account_id.id, - 'default_cash_difference_expense_account_id': self.default_cash_difference_expense_account_id.id, - 'account_journal_suspense_account_id': self.account_journal_suspense_account_id.id, - 'account_journal_payment_debit_account_id': self.account_journal_payment_debit_account_id.id, - 'account_journal_payment_credit_account_id': self.account_journal_payment_credit_account_id.id, - 'account_cash_basis_base_account_id': self.property_cash_basis_base_account_id.id, - 'account_default_pos_receivable_account_id': self.default_pos_receivable_account_id.id, - 'income_currency_exchange_account_id': self.income_currency_exchange_account_id.id, - 'expense_currency_exchange_account_id': self.expense_currency_exchange_account_id.id, + 'default_cash_difference_income_account_id': self.default_cash_difference_income_account_id, + 'default_cash_difference_expense_account_id': self.default_cash_difference_expense_account_id, + 'account_journal_suspense_account_id': self.account_journal_suspense_account_id, + 'account_journal_payment_debit_account_id': self.account_journal_payment_debit_account_id, + 'account_journal_payment_credit_account_id': self.account_journal_payment_credit_account_id, + 'account_cash_basis_base_account_id': self.property_cash_basis_base_account_id, + 'account_default_pos_receivable_account_id': self.default_pos_receivable_account_id, + 'income_currency_exchange_account_id': self.income_currency_exchange_account_id, + 'expense_currency_exchange_account_id': self.expense_currency_exchange_account_id, } values = {} @@ -708,7 +703,7 @@ class AccountChartTemplate(models.Model): self.ensure_one() tax_ids = [] for tax in account_template.tax_ids: - tax_ids.append(tax_template_ref[tax.id]) + tax_ids.append(tax_template_ref[tax].id) val = { 'name': account_template.name, 'currency_id': account_template.currency_id and account_template.currency_id.id or False, @@ -745,7 +740,7 @@ class AccountChartTemplate(models.Model): template_vals.append((account_template, vals)) accounts = self._create_records_with_xmlid('account.account', template_vals, company) for template, account in zip(acc_template, accounts): - acc_template_ref[template.id] = account.id + acc_template_ref[template] = account return acc_template_ref def generate_account_groups(self, company): @@ -798,12 +793,12 @@ class AccountChartTemplate(models.Model): 'match_partner_ids': [(6, None, account_reconcile_model.match_partner_ids.ids)], 'match_partner_category_ids': [(6, None, account_reconcile_model.match_partner_category_ids.ids)], 'line_ids': [(0, 0, { - 'account_id': acc_template_ref[line.account_id.id], + 'account_id': acc_template_ref[line.account_id].id, 'label': line.label, 'amount_type': line.amount_type, 'force_tax_included': line.force_tax_included, 'amount_string': line.amount_string, - 'tax_ids': [[4, tax_template_ref[tax.id], 0] for tax in line.tax_ids], + 'tax_ids': [[4, tax_template_ref[tax].id, 0] for tax in line.tax_ids], }) for line in account_reconcile_model_lines], } @@ -879,14 +874,14 @@ class AccountChartTemplate(models.Model): for position, fp in zip(positions, fps): for tax in position.tax_ids: tax_template_vals.append((tax, { - 'tax_src_id': tax_template_ref[tax.tax_src_id.id], - 'tax_dest_id': tax.tax_dest_id and tax_template_ref[tax.tax_dest_id.id] or False, + 'tax_src_id': tax_template_ref[tax.tax_src_id].id, + 'tax_dest_id': tax.tax_dest_id and tax_template_ref[tax.tax_dest_id].id or False, 'position_id': fp.id, })) for acc in position.account_ids: account_template_vals.append((acc, { - 'account_src_id': acc_template_ref[acc.account_src_id.id], - 'account_dest_id': acc_template_ref[acc.account_dest_id.id], + 'account_src_id': acc_template_ref[acc.account_src_id].id, + 'account_dest_id': acc_template_ref[acc.account_dest_id].id, 'position_id': fp.id, })) self._create_records_with_xmlid('account.fiscal.position.tax', tax_template_vals, company) @@ -950,14 +945,132 @@ class AccountTaxTemplate(models.Model): res.append((record.id, name)) return res + @api.model + def _try_instantiating_foreign_taxes(self, country, company): + """ This function is called in multivat setup, when a company needs to submit a + tax report in a foreign country. + + It searches for tax templates in the provided countries and instantiates the + ones it find in the provided company. + + Tax accounts are not kept from the templates (this wouldn't make sense, + as they don't belong to the same CoA as the one installed on the company). + Instead, we search existing tax accounts for approximately equivalent accounts + and use their prefix to create new accounts. Doing this gives a roughly correct suggestion + that then needs to be reviewed by the user to ensure its consistency. + It is intended as a shortcut to avoid hours of encoding, not as an out-of-the-box, always + correct solution. + """ + def create_foreign_tax_account(existing_account, additional_label): + new_code = self.env['account.account']._search_new_account_code(existing_account.company_id, len(existing_account.code), existing_account.code[:-2]) + return self.env['account.account'].create({ + 'name': f"{existing_account.name} - {additional_label}", + 'code': new_code, + 'user_type_id': existing_account.user_type_id.id, + 'company_id': existing_account.company_id.id, + }) + + def get_existing_tax_account(foreign_tax_rep_line, force_tax=None): + company = foreign_tax_rep_line.company_id + sign_comparator = '<' if foreign_tax_rep_line.factor_percent < 0 else '>' + + search_domain = [ + ('account_id', '!=', False), + ('factor_percent', sign_comparator, 0), + ('company_id', '=', company.id), + '|', + '&', ('invoice_tax_id.type_tax_use', '=', tax_rep_line.invoice_tax_id.type_tax_use), + ('invoice_tax_id.country_id', '=', company.account_fiscal_country_id.id), + '&', ('refund_tax_id.type_tax_use', '=', tax_rep_line.refund_tax_id.type_tax_use), + ('refund_tax_id.country_id', '=', company.account_fiscal_country_id.id), + ] + + if force_tax: + search_domain += [ + '|', ('invoice_tax_id.id', 'in', force_tax.ids), + ('refund_tax_id.id', 'in', force_tax.ids), + ] + + return self.env['account.tax.repartition.line'].search(search_domain, limit=1).account_id + + + taxes_in_country = self.env['account.tax'].search([ + ('country_id', '=', country.id), + ('company_id', '=', company.id) + ]) + + if taxes_in_country: + return + + templates_to_instantiate = self.env['account.tax.template'].with_context(active_test=False).search([('chart_template_id.country_id', '=', country.id)]) + default_company_taxes = company.account_sale_tax_id + company.account_purchase_tax_id + rep_lines_accounts = templates_to_instantiate._generate_tax(company)['account_dict'] + + new_accounts_map = {} + + # Handle tax repartition line accounts + tax_rep_lines_accounts_dict = rep_lines_accounts['account.tax.repartition.line'] + for tax_rep_line, account_dict in tax_rep_lines_accounts_dict.items(): + account_template = account_dict['account_id'] + rep_account = new_accounts_map.get(account_template) + + if not rep_account: + + existing_account = get_existing_tax_account(tax_rep_line, force_tax=default_company_taxes) + + if not existing_account: + # If the default taxes were not enough to provide the account + # we need, search on all other taxes. + existing_account = get_existing_tax_account(tax_rep_line) + + if existing_account: + rep_account = create_foreign_tax_account(existing_account, _("Foreign tax account (%s)", country.code)) + new_accounts_map[account_template] = rep_account + + tax_rep_line.account_id = rep_account + + # Handle cash basis taxes transtion account + caba_transition_dict = rep_lines_accounts['account.tax'] + for tax, account_dict in caba_transition_dict.items(): + transition_account_template = account_dict['cash_basis_transition_account_id'] + + if transition_account_template: + transition_account = new_accounts_map.get(transition_account_template) + + if not transition_account: + rep_lines = tax.invoice_repartition_line_ids + tax.refund_repartition_line_ids + tax_accounts = rep_lines.account_id + + if tax_accounts: + transition_account = create_foreign_tax_account(tax_accounts[0], _("Cash basis transition account")) + + tax.cash_basis_transition_account_id = transition_account + + # Setup tax closing accounts on foreign tax groups ; we don't want to use the domestic accounts + groups = self.env['account.tax.group'].search([('country_id', '=', country.id)]) + group_property_fields = [ + 'property_tax_payable_account_id', + 'property_tax_receivable_account_id', + 'property_advance_tax_payment_account_id' + ] + + property_company = self.env['ir.property'].with_company(company) + groups_company = groups.with_company(company) + for property_field in group_property_fields: + default_acc = property_company._get(property_field, 'account.tax.group') + if default_acc: + groups_company.write({ + property_field: create_foreign_tax_account(default_acc, _("Foreign account (%s)", country.code)) + }) + def _get_tax_vals(self, company, tax_template_to_tax): """ This method generates a dictionary of all the values for the tax that will be created. """ # Compute children tax ids children_ids = [] for child_tax in self.children_tax_ids: - if tax_template_to_tax.get(child_tax.id): - children_ids.append(tax_template_to_tax[child_tax.id]) + if tax_template_to_tax.get(child_tax): + children_ids.append(tax_template_to_tax[child_tax].id) self.ensure_one() val = { 'name': self.name, @@ -1011,7 +1124,7 @@ class AccountTaxTemplate(models.Model): # create taxes in batch tax_template_vals = [] for template in templates: - if all(child.id in tax_template_to_tax for child in template.children_tax_ids): + if all(child in tax_template_to_tax for child in template.children_tax_ids): vals = template._get_tax_vals(company, tax_template_to_tax) if self.chart_template_id.country_id: @@ -1030,10 +1143,10 @@ class AccountTaxTemplate(models.Model): # fill in tax_template_to_tax and todo_dict for tax, (template, vals) in zip(taxes, tax_template_vals): - tax_template_to_tax[template.id] = tax.id + tax_template_to_tax[template] = tax # Since the accounts have not been created yet, we have to wait before filling these fields - todo_dict['account.tax'][tax.id] = { - 'cash_basis_transition_account_id': template.cash_basis_transition_account_id.id, + todo_dict['account.tax'][tax] = { + 'cash_basis_transition_account_id': template.cash_basis_transition_account_id, } # We also have to delay the assignation of accounts to repartition lines @@ -1047,8 +1160,8 @@ class AccountTaxTemplate(models.Model): # We assume template and tax repartition lines are in the same order template_account = all_template_rep_lines[i].account_id if template_account: - todo_dict['account.tax.repartition.line'][all_tax_rep_lines[i].id] = { - 'account_id': template_account.id, + todo_dict['account.tax.repartition.line'][all_tax_rep_lines[i]] = { + 'account_id': template_account, } if any(template.tax_exigibility == 'on_payment' for template in self): diff --git a/addons/account/models/partner.py b/addons/account/models/partner.py index 75a9b3acdb1..d31cec93adf 100644 --- a/addons/account/models/partner.py +++ b/addons/account/models/partner.py @@ -42,11 +42,31 @@ class AccountFiscalPosition(models.Model): # To be used in hiding the 'Federal States' field('attrs' in view side) when selected 'Country' has 0 states. states_count = fields.Integer(compute='_compute_states_count') foreign_vat = fields.Char(string="Foreign Tax ID", help="The tax ID of your company in the region mapped by this fiscal position.") + foreign_vat_header_mode = fields.Selection( + selection=[('templates_found', "Templates Found"), ('no_template', "No Template")], + compute='_compute_foreign_vat_header_mode', + help="Technical field used to display a banner on top of foreign vat fiscal positions, " + "in order to ease the instantiation of foreign taxes when possible." + ) def _compute_states_count(self): for position in self: position.states_count = len(position.country_id.state_ids) + @api.depends('foreign_vat', 'country_id') + def _compute_foreign_vat_header_mode(self): + for record in self: + if not record.foreign_vat or not record.country_id: + record.foreign_vat_header_mode = None + continue + + if self.env['account.tax'].search([('country_id', '=', record.country_id.id)], limit=1): + record.foreign_vat_header_mode = None + elif self.env['account.tax.template'].search([('chart_template_id.country_id', '=', record.country_id.id)], limit=1): + record.foreign_vat_header_mode = 'templates_found' + else: + record.foreign_vat_header_mode = 'no_template' + @api.constrains('zip_from', 'zip_to') def _check_zip(self): for position in self: @@ -204,6 +224,10 @@ class AccountFiscalPosition(models.Model): return fp or self.env['account.fiscal.position'] + def action_create_foreign_taxes(self): + self.ensure_one() + self.env['account.tax.template']._try_instantiating_foreign_taxes(self.country_id, self.company_id) + class AccountFiscalPositionTax(models.Model): _name = 'account.fiscal.position.tax' diff --git a/addons/account/views/account_tax_views.xml b/addons/account/views/account_tax_views.xml index f8eaf05e937..9cb9320f24d 100644 --- a/addons/account/views/account_tax_views.xml +++ b/addons/account/views/account_tax_views.xml @@ -13,6 +13,7 @@ + diff --git a/addons/account/views/partner_view.xml b/addons/account/views/partner_view.xml index a2651907aff..b3aee9eedf3 100644 --- a/addons/account/views/partner_view.xml +++ b/addons/account/views/partner_view.xml @@ -6,6 +6,27 @@ account.fiscal.position
+ +
@@ -14,6 +35,7 @@ + diff --git a/addons/stock_account/models/account_chart_template.py b/addons/stock_account/models/account_chart_template.py index 8f6105d09bb..d5c8984955b 100644 --- a/addons/stock_account/models/account_chart_template.py +++ b/addons/stock_account/models/account_chart_template.py @@ -29,7 +29,7 @@ class AccountChartTemplate(models.Model): ] for field in todo_list: account = self[field] - value = acc_template_ref[account.id] if account else False + value = acc_template_ref[account].id if account else False PropertyObj._set_default(field, "product.category", value, company) return res