diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 8e8aad16ac7..71e5cd9e4f6 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -593,7 +593,7 @@ class AccountMove(models.Model): if move.move_type == 'entry': repartition_field = is_refund and 'refund_repartition_line_ids' or 'invoice_repartition_line_ids' - repartition_tags = base_line.tax_ids.mapped(repartition_field).filtered(lambda x: x.repartition_type == 'base').tag_ids + repartition_tags = base_line.tax_ids.flatten_taxes_hierarchy().mapped(repartition_field).filtered(lambda x: x.repartition_type == 'base').tag_ids return balance_taxes_res @@ -647,7 +647,7 @@ class AccountMove(models.Model): 'grouping_dict': False, }) taxes_map_entry['amount'] += tax_vals['amount'] - taxes_map_entry['tax_base_amount'] += self._get_base_amount_to_display(tax_vals['base'], tax_repartition_line) + taxes_map_entry['tax_base_amount'] += self._get_base_amount_to_display(tax_vals['base'], tax_repartition_line, tax_vals['group']) taxes_map_entry['grouping_dict'] = grouping_dict line.tax_exigible = tax_exigible @@ -713,12 +713,14 @@ class AccountMove(models.Model): taxes_map_entry['tax_line'].update(taxes_map_entry['tax_line']._get_fields_onchange_balance(force_computation=True)) @api.model - def _get_base_amount_to_display(self, base_amount, tax_rep_ln): + def _get_base_amount_to_display(self, base_amount, tax_rep_ln, parent_tax_group=None): """ The base amount returned for taxes by compute_all has is the balance of the base line. For inbound operations, positive sign is on credit, so we need to invert the sign of this amount before displaying it. """ - if tax_rep_ln.invoice_tax_id.type_tax_use == 'sale' or tax_rep_ln.refund_tax_id.type_tax_use == 'purchase': + source_tax = parent_tax_group or tax_rep_ln.invoice_tax_id or tax_rep_ln.refund_tax_id + if (tax_rep_ln.invoice_tax_id and source_tax.type_tax_use == 'sale') \ + or (tax_rep_ln.refund_tax_id and source_tax.type_tax_use == 'purchase'): return -base_amount return base_amount diff --git a/addons/account/models/account_tax.py b/addons/account/models/account_tax.py index 759928f083f..afa0e44a540 100644 --- a/addons/account/models/account_tax.py +++ b/addons/account/models/account_tax.py @@ -299,17 +299,27 @@ class AccountTax(models.Model): return rslt - def flatten_taxes_hierarchy(self): + def flatten_taxes_hierarchy(self, create_map=False): # Flattens the taxes contained in this recordset, returning all the # children at the bottom of the hierarchy, in a recordset, ordered by sequence. # Eg. considering letters as taxes and alphabetic order as sequence : # [G, B([A, D, F]), E, C] will be computed as [A, D, F, C, E, G] + # If create_map is True, an additional value is returned, a dictionary + # mapping each child tax to its parent group all_taxes = self.env['account.tax'] + groups_map = {} for tax in self.sorted(key=lambda r: r.sequence): if tax.amount_type == 'group': - all_taxes += tax.children_tax_ids.flatten_taxes_hierarchy() + flattened_children = tax.children_tax_ids.flatten_taxes_hierarchy() + all_taxes += flattened_children + for flat_child in flattened_children: + groups_map[flat_child] = tax else: all_taxes += tax + + if create_map: + return all_taxes, groups_map + return all_taxes def get_tax_tags(self, is_refund, repartition_type): @@ -345,7 +355,7 @@ class AccountTax(models.Model): company = self[0].company_id # 1) Flatten the taxes. - taxes = self.flatten_taxes_hierarchy() + taxes, groups_map = self.flatten_taxes_hierarchy(create_map=True) # 2) Avoid mixing taxes having price_include=False && include_base_amount=True # with taxes having price_include=True. This use case is not supported as the @@ -560,6 +570,7 @@ class AccountTax(models.Model): 'price_include': price_include, 'tax_exigibility': tax.tax_exigibility, 'tax_repartition_line_id': repartition_line.id, + 'group': groups_map.get(tax), 'tag_ids': (repartition_line.tag_ids + subsequent_tags).ids, 'tax_ids': subsequent_taxes.ids, }) diff --git a/addons/account/tests/test_invoice_taxes.py b/addons/account/tests/test_invoice_taxes.py index c47eace11f2..0336f4db676 100644 --- a/addons/account/tests/test_invoice_taxes.py +++ b/addons/account/tests/test_invoice_taxes.py @@ -491,6 +491,133 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon): {'balance': 1100.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0, 'tax_repartition_line_id': False, 'tax_tag_invert': False}, ]) + def test_misc_entry_tax_group_signs(self): + """ Tests sign inversion of the tags on misc operations made with tax + groups. + """ + def _create_group_of_taxes(tax_type): + # We use asymmetric tags between the child taxes to avoid shadowing errors + child1_sale_tax = self.env['account.tax'].create({ + 'sequence': 1, + 'name': 'child1_%s' % tax_type, + 'type_tax_use': 'none', + 'amount_type': 'percent', + 'amount': 5, + 'invoice_repartition_line_ids': [ + (0, 0, { + 'repartition_type': 'base', + 'factor_percent': 100.0, + 'tag_ids': [(6, 0, self.base_tag_pos.ids)], + }), + (0, 0, { + 'repartition_type': 'tax', + 'factor_percent': 100.0, + 'tag_ids': [(6, 0, self.tax_tag_pos.ids)], + }), + ], + 'refund_repartition_line_ids': [ + (0, 0, { + 'repartition_type': 'base', + 'factor_percent': 100.0, + }), + (0, 0, { + 'repartition_type': 'tax', + 'factor_percent': 100.0, + }), + ], + }) + child2_sale_tax = self.env['account.tax'].create({ + 'sequence': 2, + 'name': 'child2_%s' % tax_type, + 'type_tax_use': 'none', + 'amount_type': 'percent', + 'amount': 10, + 'invoice_repartition_line_ids': [ + (0, 0, { + 'repartition_type': 'base', + 'factor_percent': 100.0, + }), + (0, 0, { + 'repartition_type': 'tax', + 'factor_percent': 100.0, + }), + ], + 'refund_repartition_line_ids': [ + (0, 0, { + 'repartition_type': 'base', + 'factor_percent': 100.0, + 'tag_ids': [(6, 0, self.base_tag_neg.ids)], + }), + (0, 0, { + 'repartition_type': 'tax', + 'factor_percent': 100.0, + 'tag_ids': [(6, 0, self.tax_tag_neg.ids)], + }), + ], + }) + return self.env['account.tax'].create({ + 'name': 'group_%s' % tax_type, + 'type_tax_use': tax_type, + 'amount_type': 'group', + 'amount': 10, + 'children_tax_ids':[(6,0,[child1_sale_tax.id, child2_sale_tax.id])] + }) + + def _create_misc_operation(tax, tax_field): + with Form(self.env['account.move'], view='account.view_move_form') as move_form: + for line_field in ('debit', 'credit'): + line_amount = tax_field == line_field and 1000 or 1150 + with move_form.line_ids.new() as line_form: + line_form.name = '%s_line' % line_field + line_form.account_id = self.company_data['default_account_revenue'] + line_form.debit = line_field == 'debit' and line_amount or 0 + line_form.credit = line_field == 'credit' and line_amount or 0 + + if tax_field == line_field: + line_form.tax_ids.clear() + line_form.tax_ids.add(tax) + + return move_form.save() + + sale_group = _create_group_of_taxes('sale') + purchase_group = _create_group_of_taxes('purchase') + + # Sale tax on debit: use refund repartition + debit_sale_move = _create_misc_operation(sale_group, 'debit') + self.assertRecordValues(debit_sale_move.line_ids.sorted('balance'), [ + {'balance': -1150.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0}, + {'balance': 50.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 1000}, + {'balance': 100.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_neg.ids, 'tax_base_amount': 1000}, + {'balance': 1000.0, 'tax_ids': sale_group.ids, 'tax_tag_ids': self.base_tag_neg.ids, 'tax_base_amount': 0}, + ]) + + # Sale tax on credit: use invoice repartition + credit_sale_move = _create_misc_operation(sale_group, 'credit') + self.assertRecordValues(credit_sale_move.line_ids.sorted('balance'), [ + {'balance': -1000.0, 'tax_ids': sale_group.ids, 'tax_tag_ids': self.base_tag_pos.ids, 'tax_base_amount': 0}, + {'balance': -100.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 1000}, + {'balance': -50.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_pos.ids, 'tax_base_amount': 1000}, + {'balance': 1150.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0}, + ]) + + # Purchase tax on debit: use invoice repartition + debit_purchase_move = _create_misc_operation(purchase_group, 'debit') + self.assertRecordValues(debit_purchase_move.line_ids.sorted('balance'), [ + {'balance': -1150.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0}, + {'balance': 50.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_pos.ids, 'tax_base_amount': 1000}, + {'balance': 100.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 1000}, + {'balance': 1000.0, 'tax_ids': purchase_group.ids, 'tax_tag_ids': self.base_tag_pos.ids, 'tax_base_amount': 0}, + ]) + + # Purchase tax on credit: use refund repartition + credit_purchase_move = _create_misc_operation(purchase_group, 'credit') + self.assertRecordValues(credit_purchase_move.line_ids.sorted('balance'), [ + {'balance': -1000.0, 'tax_ids': purchase_group.ids, 'tax_tag_ids': self.base_tag_neg.ids, 'tax_base_amount': 0}, + {'balance': -100.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_neg.ids, 'tax_base_amount': 1000}, + {'balance': -50.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 1000}, + {'balance': 1150.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0}, + ]) + def test_tax_calculation_foreign_currency_large_quantity(self): ''' Test: Foreign currency with rate of 1.1726 and tax of 21%