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