summaryrefslogtreecommitdiff
path: root/addons/account/tests
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/account/tests
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/account/tests')
-rw-r--r--addons/account/tests/__init__.py37
-rw-r--r--addons/account/tests/common.py703
-rw-r--r--addons/account/tests/test_account_account.py137
-rw-r--r--addons/account/tests/test_account_all_l10n.py35
-rw-r--r--addons/account/tests/test_account_analytic.py59
-rw-r--r--addons/account/tests/test_account_bank_statement.py1594
-rw-r--r--addons/account/tests/test_account_incoming_supplier_invoice.py123
-rw-r--r--addons/account/tests/test_account_invoice_report.py118
-rw-r--r--addons/account/tests/test_account_journal.py84
-rw-r--r--addons/account/tests/test_account_journal_dashboard.py94
-rw-r--r--addons/account/tests/test_account_move_entry.py490
-rw-r--r--addons/account/tests/test_account_move_in_invoice.py1774
-rw-r--r--addons/account/tests/test_account_move_in_refund.py950
-rw-r--r--addons/account/tests/test_account_move_out_invoice.py2933
-rw-r--r--addons/account/tests/test_account_move_out_refund.py933
-rw-r--r--addons/account/tests/test_account_move_partner_count.py29
-rw-r--r--addons/account/tests/test_account_move_payments_widget.py184
-rw-r--r--addons/account/tests/test_account_move_reconcile.py2457
-rw-r--r--addons/account/tests/test_account_move_rounding.py31
-rw-r--r--addons/account/tests/test_account_onboarding.py42
-rw-r--r--addons/account/tests/test_account_payment.py709
-rw-r--r--addons/account/tests/test_account_payment_register.py800
-rw-r--r--addons/account/tests/test_account_tax.py30
-rw-r--r--addons/account/tests/test_fiscal_position.py168
-rw-r--r--addons/account/tests/test_invoice_tax_amount_by_group.py209
-rw-r--r--addons/account/tests/test_invoice_taxes.py669
-rw-r--r--addons/account/tests/test_payment_term.py98
-rw-r--r--addons/account/tests/test_portal_attachment.py272
-rw-r--r--addons/account/tests/test_reconciliation.py1208
-rw-r--r--addons/account/tests/test_reconciliation_matching_rules.py1061
-rw-r--r--addons/account/tests/test_sequence_mixin.py396
-rw-r--r--addons/account/tests/test_settings.py60
-rw-r--r--addons/account/tests/test_tax.py1075
-rw-r--r--addons/account/tests/test_tax_report.py285
-rw-r--r--addons/account/tests/test_templates_consistency.py73
-rw-r--r--addons/account/tests/test_tour.py15
-rw-r--r--addons/account/tests/test_transfer_wizard.py291
37 files changed, 20226 insertions, 0 deletions
diff --git a/addons/account/tests/__init__.py b/addons/account/tests/__init__.py
new file mode 100644
index 00000000..ffd45bb1
--- /dev/null
+++ b/addons/account/tests/__init__.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+
+from . import test_account_move_reconcile
+from . import test_account_move_payments_widget
+from . import test_account_move_out_invoice
+from . import test_account_move_out_refund
+from . import test_account_move_in_invoice
+from . import test_account_move_in_refund
+from . import test_account_move_entry
+from . import test_invoice_tax_amount_by_group
+from . import test_account_journal
+from . import test_account_account
+from . import test_account_tax
+from . import test_account_analytic
+from . import test_account_payment
+from . import test_account_bank_statement
+from . import test_account_move_partner_count
+from . import test_account_move_rounding
+from . import test_account_invoice_report
+from . import test_account_journal_dashboard
+from . import test_fiscal_position
+from . import test_reconciliation
+from . import test_sequence_mixin
+from . import test_settings
+from . import test_tax
+from . import test_invoice_taxes
+from . import test_templates_consistency
+from . import test_account_all_l10n
+from . import test_reconciliation_matching_rules
+from . import test_account_onboarding
+from . import test_portal_attachment
+from . import test_tax_report
+from . import test_transfer_wizard
+from . import test_account_incoming_supplier_invoice
+from . import test_payment_term
+from . import test_account_payment_register
+from . import test_tour
diff --git a/addons/account/tests/common.py b/addons/account/tests/common.py
new file mode 100644
index 00000000..42aba78e
--- /dev/null
+++ b/addons/account/tests/common.py
@@ -0,0 +1,703 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo import fields
+from odoo.tests.common import SavepointCase, HttpSavepointCase, tagged, Form
+
+import time
+import base64
+from lxml import etree
+
+@tagged('post_install', '-at_install')
+class AccountTestInvoicingCommon(SavepointCase):
+
+ @classmethod
+ def copy_account(cls, account):
+ suffix_nb = 1
+ while True:
+ new_code = '%s (%s)' % (account.code, suffix_nb)
+ if account.search_count([('company_id', '=', account.company_id.id), ('code', '=', new_code)]):
+ suffix_nb += 1
+ else:
+ return account.copy(default={'code': new_code})
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super(AccountTestInvoicingCommon, cls).setUpClass()
+
+ if chart_template_ref:
+ chart_template = cls.env.ref(chart_template_ref)
+ else:
+ chart_template = cls.env.ref('l10n_generic_coa.configurable_chart_template', raise_if_not_found=False)
+ if not chart_template:
+ cls.tearDownClass()
+ # skipTest raises exception
+ cls.skipTest(cls, "Accounting Tests skipped because the user's company has no chart of accounts.")
+
+ # Create user.
+ user = cls.env['res.users'].create({
+ 'name': 'Because I am accountman!',
+ 'login': 'accountman',
+ 'password': 'accountman',
+ 'groups_id': [(6, 0, cls.env.user.groups_id.ids), (4, cls.env.ref('account.group_account_user').id)],
+ })
+ user.partner_id.email = 'accountman@test.com'
+
+ # Shadow the current environment/cursor with one having the report user.
+ # This is mandatory to test access rights.
+ cls.env = cls.env(user=user)
+ cls.cr = cls.env.cr
+
+ cls.company_data_2 = cls.setup_company_data('company_2_data', chart_template=chart_template)
+ cls.company_data = cls.setup_company_data('company_1_data', chart_template=chart_template)
+
+ user.write({
+ 'company_ids': [(6, 0, (cls.company_data['company'] + cls.company_data_2['company']).ids)],
+ 'company_id': cls.company_data['company'].id,
+ })
+
+ cls.currency_data = cls.setup_multi_currency_data()
+
+ # ==== Taxes ====
+ cls.tax_sale_a = cls.company_data['default_tax_sale']
+ cls.tax_sale_b = cls.company_data['default_tax_sale'].copy()
+ cls.tax_purchase_a = cls.company_data['default_tax_purchase']
+ cls.tax_purchase_b = cls.company_data['default_tax_purchase'].copy()
+ cls.tax_armageddon = cls.setup_armageddon_tax('complex_tax', cls.company_data)
+
+ # ==== Products ====
+ cls.product_a = cls.env['product.product'].create({
+ 'name': 'product_a',
+ 'uom_id': cls.env.ref('uom.product_uom_unit').id,
+ 'lst_price': 1000.0,
+ 'standard_price': 800.0,
+ 'property_account_income_id': cls.company_data['default_account_revenue'].id,
+ 'property_account_expense_id': cls.company_data['default_account_expense'].id,
+ 'taxes_id': [(6, 0, cls.tax_sale_a.ids)],
+ 'supplier_taxes_id': [(6, 0, cls.tax_purchase_a.ids)],
+ })
+ cls.product_b = cls.env['product.product'].create({
+ 'name': 'product_b',
+ 'uom_id': cls.env.ref('uom.product_uom_dozen').id,
+ 'lst_price': 200.0,
+ 'standard_price': 160.0,
+ 'property_account_income_id': cls.copy_account(cls.company_data['default_account_revenue']).id,
+ 'property_account_expense_id': cls.copy_account(cls.company_data['default_account_expense']).id,
+ 'taxes_id': [(6, 0, (cls.tax_sale_a + cls.tax_sale_b).ids)],
+ 'supplier_taxes_id': [(6, 0, (cls.tax_purchase_a + cls.tax_purchase_b).ids)],
+ })
+
+ # ==== Fiscal positions ====
+ cls.fiscal_pos_a = cls.env['account.fiscal.position'].create({
+ 'name': 'fiscal_pos_a',
+ 'tax_ids': [
+ (0, None, {
+ 'tax_src_id': cls.tax_sale_a.id,
+ 'tax_dest_id': cls.tax_sale_b.id,
+ }),
+ (0, None, {
+ 'tax_src_id': cls.tax_purchase_a.id,
+ 'tax_dest_id': cls.tax_purchase_b.id,
+ }),
+ ],
+ 'account_ids': [
+ (0, None, {
+ 'account_src_id': cls.product_a.property_account_income_id.id,
+ 'account_dest_id': cls.product_b.property_account_income_id.id,
+ }),
+ (0, None, {
+ 'account_src_id': cls.product_a.property_account_expense_id.id,
+ 'account_dest_id': cls.product_b.property_account_expense_id.id,
+ }),
+ ],
+ })
+
+ # ==== Payment terms ====
+ cls.pay_terms_a = cls.env.ref('account.account_payment_term_immediate')
+ cls.pay_terms_b = cls.env['account.payment.term'].create({
+ 'name': '30% Advance End of Following Month',
+ 'note': 'Payment terms: 30% Advance End of Following Month',
+ 'line_ids': [
+ (0, 0, {
+ 'value': 'percent',
+ 'value_amount': 30.0,
+ 'sequence': 400,
+ 'days': 0,
+ 'option': 'day_after_invoice_date',
+ }),
+ (0, 0, {
+ 'value': 'balance',
+ 'value_amount': 0.0,
+ 'sequence': 500,
+ 'days': 31,
+ 'option': 'day_following_month',
+ }),
+ ],
+ })
+
+ # ==== Partners ====
+ cls.partner_a = cls.env['res.partner'].create({
+ 'name': 'partner_a',
+ 'property_payment_term_id': cls.pay_terms_a.id,
+ 'property_supplier_payment_term_id': cls.pay_terms_a.id,
+ 'property_account_receivable_id': cls.company_data['default_account_receivable'].id,
+ 'property_account_payable_id': cls.company_data['default_account_payable'].id,
+ 'company_id': False,
+ })
+ cls.partner_b = cls.env['res.partner'].create({
+ 'name': 'partner_b',
+ 'property_payment_term_id': cls.pay_terms_b.id,
+ 'property_supplier_payment_term_id': cls.pay_terms_b.id,
+ 'property_account_position_id': cls.fiscal_pos_a.id,
+ 'property_account_receivable_id': cls.company_data['default_account_receivable'].copy().id,
+ 'property_account_payable_id': cls.company_data['default_account_payable'].copy().id,
+ 'company_id': False,
+ })
+
+ # ==== Cash rounding ====
+ cls.cash_rounding_a = cls.env['account.cash.rounding'].create({
+ 'name': 'add_invoice_line',
+ 'rounding': 0.05,
+ 'strategy': 'add_invoice_line',
+ 'profit_account_id': cls.company_data['default_account_revenue'].copy().id,
+ 'loss_account_id': cls.company_data['default_account_expense'].copy().id,
+ 'rounding_method': 'UP',
+ })
+ cls.cash_rounding_b = cls.env['account.cash.rounding'].create({
+ 'name': 'biggest_tax',
+ 'rounding': 0.05,
+ 'strategy': 'biggest_tax',
+ 'rounding_method': 'DOWN',
+ })
+
+ @classmethod
+ def setup_company_data(cls, company_name, chart_template=None, **kwargs):
+ ''' Create a new company having the name passed as parameter.
+ A chart of accounts will be installed to this company: the same as the current company one.
+ The current user will get access to this company.
+
+ :param chart_template: The chart template to be used on this new company.
+ :param company_name: The name of the company.
+ :return: A dictionary will be returned containing all relevant accounting data for testing.
+ '''
+ def search_account(company, chart_template, field_name, domain):
+ template_code = chart_template[field_name].code
+ domain = [('company_id', '=', company.id)] + domain
+
+ account = None
+ if template_code:
+ account = cls.env['account.account'].search(domain + [('code', '=like', template_code + '%')], limit=1)
+
+ if not account:
+ account = cls.env['account.account'].search(domain, limit=1)
+ return account
+
+ chart_template = chart_template or cls.env.company.chart_template_id
+ company = cls.env['res.company'].create({
+ 'name': company_name,
+ **kwargs,
+ })
+ cls.env.user.company_ids |= company
+
+ chart_template.try_loading(company=company)
+
+ # The currency could be different after the installation of the chart template.
+ if kwargs.get('currency_id'):
+ company.write({'currency_id': kwargs['currency_id']})
+
+ return {
+ 'company': company,
+ 'currency': company.currency_id,
+ 'default_account_revenue': cls.env['account.account'].search([
+ ('company_id', '=', company.id),
+ ('user_type_id', '=', cls.env.ref('account.data_account_type_revenue').id)
+ ], limit=1),
+ 'default_account_expense': cls.env['account.account'].search([
+ ('company_id', '=', company.id),
+ ('user_type_id', '=', cls.env.ref('account.data_account_type_expenses').id)
+ ], limit=1),
+ 'default_account_receivable': search_account(company, chart_template, 'property_account_receivable_id', [
+ ('user_type_id.type', '=', 'receivable')
+ ]),
+ 'default_account_payable': cls.env['account.account'].search([
+ ('company_id', '=', company.id),
+ ('user_type_id.type', '=', 'payable')
+ ], limit=1),
+ 'default_account_assets': cls.env['account.account'].search([
+ ('company_id', '=', company.id),
+ ('user_type_id', '=', cls.env.ref('account.data_account_type_current_assets').id)
+ ], limit=1),
+ 'default_account_tax_sale': company.account_sale_tax_id.mapped('invoice_repartition_line_ids.account_id'),
+ 'default_account_tax_purchase': company.account_purchase_tax_id.mapped('invoice_repartition_line_ids.account_id'),
+ 'default_journal_misc': cls.env['account.journal'].search([
+ ('company_id', '=', company.id),
+ ('type', '=', 'general')
+ ], limit=1),
+ 'default_journal_sale': cls.env['account.journal'].search([
+ ('company_id', '=', company.id),
+ ('type', '=', 'sale')
+ ], limit=1),
+ 'default_journal_purchase': cls.env['account.journal'].search([
+ ('company_id', '=', company.id),
+ ('type', '=', 'purchase')
+ ], limit=1),
+ 'default_journal_bank': cls.env['account.journal'].search([
+ ('company_id', '=', company.id),
+ ('type', '=', 'bank')
+ ], limit=1),
+ 'default_journal_cash': cls.env['account.journal'].search([
+ ('company_id', '=', company.id),
+ ('type', '=', 'cash')
+ ], limit=1),
+ 'default_tax_sale': company.account_sale_tax_id,
+ 'default_tax_purchase': company.account_purchase_tax_id,
+ }
+
+ @classmethod
+ def setup_multi_currency_data(cls, default_values={}, rate2016=3.0, rate2017=2.0):
+ foreign_currency = cls.env['res.currency'].create({
+ 'name': 'Gold Coin',
+ 'symbol': '☺',
+ 'rounding': 0.001,
+ 'position': 'after',
+ 'currency_unit_label': 'Gold',
+ 'currency_subunit_label': 'Silver',
+ **default_values,
+ })
+ rate1 = cls.env['res.currency.rate'].create({
+ 'name': '2016-01-01',
+ 'rate': rate2016,
+ 'currency_id': foreign_currency.id,
+ 'company_id': cls.env.company.id,
+ })
+ rate2 = cls.env['res.currency.rate'].create({
+ 'name': '2017-01-01',
+ 'rate': rate2017,
+ 'currency_id': foreign_currency.id,
+ 'company_id': cls.env.company.id,
+ })
+ return {
+ 'currency': foreign_currency,
+ 'rates': rate1 + rate2,
+ }
+
+ @classmethod
+ def setup_armageddon_tax(cls, tax_name, company_data):
+ return cls.env['account.tax'].create({
+ 'name': '%s (group)' % tax_name,
+ 'amount_type': 'group',
+ 'amount': 0.0,
+ 'children_tax_ids': [
+ (0, 0, {
+ 'name': '%s (child 1)' % tax_name,
+ 'amount_type': 'percent',
+ 'amount': 20.0,
+ 'price_include': True,
+ 'include_base_amount': True,
+ 'tax_exigibility': 'on_invoice',
+ 'invoice_repartition_line_ids': [
+ (0, 0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ }),
+ (0, 0, {
+ 'factor_percent': 40,
+ 'repartition_type': 'tax',
+ 'account_id': company_data['default_account_tax_sale'].id,
+ }),
+ (0, 0, {
+ 'factor_percent': 60,
+ 'repartition_type': 'tax',
+ # /!\ No account set.
+ }),
+ ],
+ 'refund_repartition_line_ids': [
+ (0, 0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ }),
+ (0, 0, {
+ 'factor_percent': 40,
+ 'repartition_type': 'tax',
+ 'account_id': company_data['default_account_tax_sale'].id,
+ }),
+ (0, 0, {
+ 'factor_percent': 60,
+ 'repartition_type': 'tax',
+ # /!\ No account set.
+ }),
+ ],
+ }),
+ (0, 0, {
+ 'name': '%s (child 2)' % tax_name,
+ 'amount_type': 'percent',
+ 'amount': 10.0,
+ 'tax_exigibility': 'on_payment',
+ 'cash_basis_transition_account_id': company_data['default_account_tax_sale'].copy().id,
+ 'invoice_repartition_line_ids': [
+ (0, 0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ }),
+ (0, 0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'account_id': company_data['default_account_tax_sale'].id,
+ }),
+ ],
+ 'refund_repartition_line_ids': [
+ (0, 0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ }),
+
+ (0, 0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'account_id': company_data['default_account_tax_sale'].id,
+ }),
+ ],
+ }),
+ ],
+ })
+
+ @classmethod
+ def init_invoice(cls, move_type, partner=None, invoice_date=None, post=False, products=[], amounts=[], taxes=None):
+ move_form = Form(cls.env['account.move'].with_context(default_move_type=move_type, account_predictive_bills_disable_prediction=True))
+ move_form.invoice_date = invoice_date or fields.Date.from_string('2019-01-01')
+ move_form.date = move_form.invoice_date
+ move_form.partner_id = partner or cls.partner_a
+
+ for product in products:
+ with move_form.invoice_line_ids.new() as line_form:
+ line_form.product_id = product
+ if taxes:
+ line_form.tax_ids.clear()
+ line_form.tax_ids.add(taxes)
+
+ for amount in amounts:
+ with move_form.invoice_line_ids.new() as line_form:
+ line_form.name = "test line"
+ # We use account_predictive_bills_disable_prediction context key so that
+ # this doesn't trigger prediction in case enterprise (hence account_predictive_bills) is installed
+ line_form.price_unit = amount
+ if taxes:
+ line_form.tax_ids.clear()
+ line_form.tax_ids.add(taxes)
+
+ rslt = move_form.save()
+
+ if post:
+ rslt.action_post()
+
+ return rslt
+
+ def assertInvoiceValues(self, move, expected_lines_values, expected_move_values):
+ def sort_lines(lines):
+ return lines.sorted(lambda line: (line.exclude_from_invoice_tab, not bool(line.tax_line_id), line.name or '', line.balance))
+ self.assertRecordValues(sort_lines(move.line_ids.sorted()), expected_lines_values)
+ self.assertRecordValues(sort_lines(move.invoice_line_ids.sorted()), expected_lines_values[:len(move.invoice_line_ids)])
+ self.assertRecordValues(move, [expected_move_values])
+
+ ####################################################
+ # Xml Comparison
+ ####################################################
+
+ def _turn_node_as_dict_hierarchy(self, node):
+ ''' Turn the node as a python dictionary to be compared later with another one.
+ Allow to ignore the management of namespaces.
+ :param node: A node inside an xml tree.
+ :return: A python dictionary.
+ '''
+ tag_split = node.tag.split('}')
+ tag_wo_ns = tag_split[-1]
+ attrib_wo_ns = {k: v for k, v in node.attrib.items() if '}' not in k}
+ return {
+ 'tag': tag_wo_ns,
+ 'namespace': None if len(tag_split) < 2 else tag_split[0],
+ 'text': (node.text or '').strip(),
+ 'attrib': attrib_wo_ns,
+ 'children': [self._turn_node_as_dict_hierarchy(child_node) for child_node in node.getchildren()],
+ }
+
+ def assertXmlTreeEqual(self, xml_tree, expected_xml_tree):
+ ''' Compare two lxml.etree.
+ :param xml_tree: The current tree.
+ :param expected_xml_tree: The expected tree.
+ '''
+
+ def assertNodeDictEqual(node_dict, expected_node_dict):
+ ''' Compare nodes created by the `_turn_node_as_dict_hierarchy` method.
+ :param node_dict: The node to compare with.
+ :param expected_node_dict: The expected node.
+ '''
+ # Check tag.
+ self.assertEqual(node_dict['tag'], expected_node_dict['tag'])
+
+ # Check attributes.
+ node_dict_attrib = {k: '___ignore___' if expected_node_dict['attrib'].get(k) == '___ignore___' else v
+ for k, v in node_dict['attrib'].items()}
+ expected_node_dict_attrib = {k: v for k, v in expected_node_dict['attrib'].items() if v != '___remove___'}
+ self.assertDictEqual(
+ node_dict_attrib,
+ expected_node_dict_attrib,
+ "Element attributes are different for node %s" % node_dict['tag'],
+ )
+
+ # Check text.
+ if expected_node_dict['text'] != '___ignore___':
+ self.assertEqual(
+ node_dict['text'],
+ expected_node_dict['text'],
+ "Element text are different for node %s" % node_dict['tag'],
+ )
+
+ # Check children.
+ self.assertEqual(
+ [child['tag'] for child in node_dict['children']],
+ [child['tag'] for child in expected_node_dict['children']],
+ "Number of children elements for node %s is different." % node_dict['tag'],
+ )
+
+ for child_node_dict, expected_child_node_dict in zip(node_dict['children'], expected_node_dict['children']):
+ assertNodeDictEqual(child_node_dict, expected_child_node_dict)
+
+ assertNodeDictEqual(
+ self._turn_node_as_dict_hierarchy(xml_tree),
+ self._turn_node_as_dict_hierarchy(expected_xml_tree),
+ )
+
+ def with_applied_xpath(self, xml_tree, xpath):
+ ''' Applies the xpath to the xml_tree passed as parameter.
+ :param xml_tree: An instance of etree.
+ :param xpath: The xpath to apply as a string.
+ :return: The resulting etree after applying the xpaths.
+ '''
+ diff_xml_tree = etree.fromstring('<data>%s</data>' % xpath)
+ return self.env['ir.ui.view'].apply_inheritance_specs(xml_tree, diff_xml_tree)
+
+ def get_xml_tree_from_attachment(self, attachment):
+ ''' Extract an instance of etree from an ir.attachment.
+ :param attachment: An ir.attachment.
+ :return: An instance of etree.
+ '''
+ return etree.fromstring(base64.b64decode(attachment.with_context(bin_size=False).datas))
+
+ def get_xml_tree_from_string(self, xml_tree_str):
+ ''' Convert the string passed as parameter to an instance of etree.
+ :param xml_tree_str: A string representing an xml.
+ :return: An instance of etree.
+ '''
+ return etree.fromstring(xml_tree_str)
+
+
+@tagged('post_install', '-at_install')
+class AccountTestInvoicingHttpCommon(AccountTestInvoicingCommon, HttpSavepointCase):
+ pass
+
+
+class TestAccountReconciliationCommon(AccountTestInvoicingCommon):
+
+ """Tests for reconciliation (account.tax)
+
+ Test used to check that when doing a sale or purchase invoice in a different currency,
+ the result will be balanced.
+ """
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ cls.company = cls.company_data['company']
+ cls.company.currency_id = cls.env.ref('base.EUR')
+
+ cls.partner_agrolait = cls.env['res.partner'].create({
+ 'name': 'Deco Addict',
+ 'is_company': True,
+ 'country_id': cls.env.ref('base.us').id,
+ })
+ cls.partner_agrolait_id = cls.partner_agrolait.id
+ cls.currency_swiss_id = cls.env.ref("base.CHF").id
+ cls.currency_usd_id = cls.env.ref("base.USD").id
+ cls.currency_euro_id = cls.env.ref("base.EUR").id
+ cls.account_rcv = cls.company_data['default_account_receivable']
+ cls.account_rsa = cls.company_data['default_account_payable']
+ cls.product = cls.env['product.product'].create({
+ 'name': 'Product Product 4',
+ 'standard_price': 500.0,
+ 'list_price': 750.0,
+ 'type': 'consu',
+ 'categ_id': cls.env.ref('product.product_category_all').id,
+ })
+
+ cls.bank_journal_euro = cls.env['account.journal'].create({'name': 'Bank', 'type': 'bank', 'code': 'BNK67'})
+ cls.account_euro = cls.bank_journal_euro.default_account_id
+
+ cls.bank_journal_usd = cls.env['account.journal'].create({'name': 'Bank US', 'type': 'bank', 'code': 'BNK68', 'currency_id': cls.currency_usd_id})
+ cls.account_usd = cls.bank_journal_usd.default_account_id
+
+ cls.fx_journal = cls.company.currency_exchange_journal_id
+ cls.diff_income_account = cls.company.income_currency_exchange_account_id
+ cls.diff_expense_account = cls.company.expense_currency_exchange_account_id
+
+ cls.inbound_payment_method = cls.env['account.payment.method'].create({
+ 'name': 'inbound',
+ 'code': 'IN',
+ 'payment_type': 'inbound',
+ })
+
+ cls.expense_account = cls.company_data['default_account_expense']
+ # cash basis intermediary account
+ cls.tax_waiting_account = cls.env['account.account'].create({
+ 'name': 'TAX_WAIT',
+ 'code': 'TWAIT',
+ 'user_type_id': cls.env.ref('account.data_account_type_current_liabilities').id,
+ 'reconcile': True,
+ 'company_id': cls.company.id,
+ })
+ # cash basis final account
+ cls.tax_final_account = cls.env['account.account'].create({
+ 'name': 'TAX_TO_DEDUCT',
+ 'code': 'TDEDUCT',
+ 'user_type_id': cls.env.ref('account.data_account_type_current_assets').id,
+ 'company_id': cls.company.id,
+ })
+ cls.tax_base_amount_account = cls.env['account.account'].create({
+ 'name': 'TAX_BASE',
+ 'code': 'TBASE',
+ 'user_type_id': cls.env.ref('account.data_account_type_current_assets').id,
+ 'company_id': cls.company.id,
+ })
+ cls.company.account_cash_basis_base_account_id = cls.tax_base_amount_account.id
+
+
+ # Journals
+ cls.purchase_journal = cls.company_data['default_journal_purchase']
+ cls.cash_basis_journal = cls.env['account.journal'].create({
+ 'name': 'CABA',
+ 'code': 'CABA',
+ 'type': 'general',
+ })
+ cls.general_journal = cls.company_data['default_journal_misc']
+
+ # Tax Cash Basis
+ cls.tax_cash_basis = cls.env['account.tax'].create({
+ 'name': 'cash basis 20%',
+ 'type_tax_use': 'purchase',
+ 'company_id': cls.company.id,
+ 'amount': 20,
+ 'tax_exigibility': 'on_payment',
+ 'cash_basis_transition_account_id': cls.tax_waiting_account.id,
+ 'invoice_repartition_line_ids': [
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'account_id': cls.tax_final_account.id,
+ }),
+ ],
+ 'refund_repartition_line_ids': [
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'account_id': cls.tax_final_account.id,
+ }),
+ ],
+ })
+ cls.env['res.currency.rate'].create([
+ {
+ 'currency_id': cls.env.ref('base.EUR').id,
+ 'name': '2010-01-02',
+ 'rate': 1.0,
+ }, {
+ 'currency_id': cls.env.ref('base.USD').id,
+ 'name': '2010-01-02',
+ 'rate': 1.2834,
+ }, {
+ 'currency_id': cls.env.ref('base.USD').id,
+ 'name': time.strftime('%Y-06-05'),
+ 'rate': 1.5289,
+ }
+ ])
+
+ def _create_invoice(self, move_type='out_invoice', invoice_amount=50, currency_id=None, partner_id=None, date_invoice=None, payment_term_id=False, auto_validate=False):
+ date_invoice = date_invoice or time.strftime('%Y') + '-07-01'
+
+ invoice_vals = {
+ 'move_type': move_type,
+ 'partner_id': partner_id or self.partner_agrolait_id,
+ 'invoice_date': date_invoice,
+ 'date': date_invoice,
+ 'invoice_line_ids': [(0, 0, {
+ 'name': 'product that cost %s' % invoice_amount,
+ 'quantity': 1,
+ 'price_unit': invoice_amount,
+ 'tax_ids': [(6, 0, [])],
+ })]
+ }
+
+ if payment_term_id:
+ invoice_vals['invoice_payment_term_id'] = payment_term_id
+
+ if currency_id:
+ invoice_vals['currency_id'] = currency_id
+
+ invoice = self.env['account.move'].with_context(default_move_type=type).create(invoice_vals)
+ if auto_validate:
+ invoice.action_post()
+ return invoice
+
+ def create_invoice(self, move_type='out_invoice', invoice_amount=50, currency_id=None):
+ return self._create_invoice(move_type=move_type, invoice_amount=invoice_amount, currency_id=currency_id, auto_validate=True)
+
+ def create_invoice_partner(self, move_type='out_invoice', invoice_amount=50, currency_id=None, partner_id=False, payment_term_id=False):
+ return self._create_invoice(
+ move_type=move_type,
+ invoice_amount=invoice_amount,
+ currency_id=currency_id,
+ partner_id=partner_id,
+ payment_term_id=payment_term_id,
+ auto_validate=True
+ )
+
+ def make_payment(self, invoice_record, bank_journal, amount=0.0, amount_currency=0.0, currency_id=None, reconcile_param=[]):
+ bank_stmt = self.env['account.bank.statement'].create({
+ 'journal_id': bank_journal.id,
+ 'date': time.strftime('%Y') + '-07-15',
+ 'name': 'payment' + invoice_record.name,
+ 'line_ids': [(0, 0, {
+ 'payment_ref': 'payment',
+ 'partner_id': self.partner_agrolait_id,
+ 'amount': amount,
+ 'amount_currency': amount_currency,
+ 'foreign_currency_id': currency_id,
+ })],
+ })
+ bank_stmt.button_post()
+
+ bank_stmt.line_ids[0].reconcile(reconcile_param)
+ return bank_stmt
+
+ def make_customer_and_supplier_flows(self, invoice_currency_id, invoice_amount, bank_journal, amount, amount_currency, transaction_currency_id):
+ #we create an invoice in given invoice_currency
+ invoice_record = self.create_invoice(move_type='out_invoice', invoice_amount=invoice_amount, currency_id=invoice_currency_id)
+ #we encode a payment on it, on the given bank_journal with amount, amount_currency and transaction_currency given
+ line = invoice_record.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
+ bank_stmt = self.make_payment(invoice_record, bank_journal, amount=amount, amount_currency=amount_currency, currency_id=transaction_currency_id, reconcile_param=[{'id': line.id}])
+ customer_move_lines = bank_stmt.line_ids.line_ids
+
+ #we create a supplier bill in given invoice_currency
+ invoice_record = self.create_invoice(move_type='in_invoice', invoice_amount=invoice_amount, currency_id=invoice_currency_id)
+ #we encode a payment on it, on the given bank_journal with amount, amount_currency and transaction_currency given
+ line = invoice_record.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
+ bank_stmt = self.make_payment(invoice_record, bank_journal, amount=-amount, amount_currency=-amount_currency, currency_id=transaction_currency_id, reconcile_param=[{'id': line.id}])
+ supplier_move_lines = bank_stmt.line_ids.line_ids
+ return customer_move_lines, supplier_move_lines
diff --git a/addons/account/tests/test_account_account.py b/addons/account/tests/test_account_account.py
new file mode 100644
index 00000000..cded3517
--- /dev/null
+++ b/addons/account/tests/test_account_account.py
@@ -0,0 +1,137 @@
+# -*- coding: utf-8 -*-
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests import tagged
+from odoo.exceptions import UserError, ValidationError
+
+
+@tagged('post_install', '-at_install')
+class TestAccountAccount(AccountTestInvoicingCommon):
+
+ def test_changing_account_company(self):
+ ''' Ensure you can't change the company of an account.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,
+ }),
+ (0, 0, {
+ 'name': 'line_credit',
+ 'account_id': self.company_data['default_account_revenue'].id,
+ }),
+ ],
+ })
+
+ with self.assertRaises(UserError), self.cr.savepoint():
+ self.company_data['default_account_revenue'].company_id = self.company_data_2['company']
+
+ def test_toggle_reconcile(self):
+ ''' Test the feature when the user sets an account as reconcile/not reconcile with existing journal entries. '''
+ account = self.company_data['default_account_revenue']
+
+ move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2019-01-01',
+ 'line_ids': [
+ (0, 0, {
+ 'account_id': account.id,
+ 'currency_id': self.currency_data['currency'].id,
+ 'debit': 100.0,
+ 'credit': 0.0,
+ 'amount_currency': 200.0,
+ }),
+ (0, 0, {
+ 'account_id': account.id,
+ 'currency_id': self.currency_data['currency'].id,
+ 'debit': 0.0,
+ 'credit': 100.0,
+ 'amount_currency': -200.0,
+ }),
+ ],
+ })
+ move.action_post()
+
+ self.assertRecordValues(move.line_ids, [
+ {'reconciled': False, 'amount_residual': 0.0, 'amount_residual_currency': 0.0},
+ {'reconciled': False, 'amount_residual': 0.0, 'amount_residual_currency': 0.0},
+ ])
+
+ # Set the account as reconcile and fully reconcile something.
+ account.reconcile = True
+ self.env['account.move.line'].invalidate_cache()
+
+ self.assertRecordValues(move.line_ids, [
+ {'reconciled': False, 'amount_residual': 100.0, 'amount_residual_currency': 200.0},
+ {'reconciled': False, 'amount_residual': -100.0, 'amount_residual_currency': -200.0},
+ ])
+
+ move.line_ids.reconcile()
+ self.assertRecordValues(move.line_ids, [
+ {'reconciled': True, 'amount_residual': 0.0, 'amount_residual_currency': 0.0},
+ {'reconciled': True, 'amount_residual': 0.0, 'amount_residual_currency': 0.0},
+ ])
+
+ # Set back to a not reconcile account and check the journal items.
+ move.line_ids.remove_move_reconcile()
+ account.reconcile = False
+ self.env['account.move.line'].invalidate_cache()
+
+ self.assertRecordValues(move.line_ids, [
+ {'reconciled': False, 'amount_residual': 0.0, 'amount_residual_currency': 0.0},
+ {'reconciled': False, 'amount_residual': 0.0, 'amount_residual_currency': 0.0},
+ ])
+
+ def test_toggle_reconcile_with_partials(self):
+ ''' Test the feature when the user sets an account as reconcile/not reconcile with partial reconciliation. '''
+ account = self.company_data['default_account_revenue']
+
+ move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2019-01-01',
+ 'line_ids': [
+ (0, 0, {
+ 'account_id': account.id,
+ 'currency_id': self.currency_data['currency'].id,
+ 'debit': 100.0,
+ 'credit': 0.0,
+ 'amount_currency': 200.0,
+ }),
+ (0, 0, {
+ 'account_id': account.id,
+ 'currency_id': self.currency_data['currency'].id,
+ 'debit': 0.0,
+ 'credit': 50.0,
+ 'amount_currency': -100.0,
+ }),
+ (0, 0, {
+ 'account_id': self.company_data['default_account_expense'].id,
+ 'currency_id': self.currency_data['currency'].id,
+ 'debit': 0.0,
+ 'credit': 50.0,
+ 'amount_currency': -100.0,
+ }),
+ ],
+ })
+ move.action_post()
+
+ # Set the account as reconcile and partially reconcile something.
+ account.reconcile = True
+ self.env['account.move.line'].invalidate_cache()
+
+ move.line_ids.filtered(lambda line: line.account_id == account).reconcile()
+
+ # Try to set the account as a not-reconcile one.
+ with self.assertRaises(UserError), self.cr.savepoint():
+ account.reconcile = False
+
+ def test_toggle_reconcile_outstanding_account(self):
+ ''' Test the feature when the user sets an account as not reconcilable when a journal
+ is configured with this account as the payment credit or debit account.
+ Since such an account should be reconcilable by nature, a ValidationError is raised.'''
+ with self.assertRaises(ValidationError), self.cr.savepoint():
+ self.company_data['default_journal_bank'].payment_debit_account_id.reconcile = False
+ with self.assertRaises(ValidationError), self.cr.savepoint():
+ self.company_data['default_journal_bank'].payment_credit_account_id.reconcile = False
diff --git a/addons/account/tests/test_account_all_l10n.py b/addons/account/tests/test_account_all_l10n.py
new file mode 100644
index 00000000..fbf9887b
--- /dev/null
+++ b/addons/account/tests/test_account_all_l10n.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+import logging
+
+from odoo.tests import standalone
+
+
+_logger = logging.getLogger(__name__)
+
+
+@standalone('all_l10n')
+def test_all_l10n(env):
+ """ This test will install all the l10n_* modules.
+ As the module install is not yet fully transactional, the modules will
+ remain installed after the test.
+ """
+ l10n_mods = env['ir.module.module'].search([
+ ('name', 'like', 'l10n%'),
+ ('state', '=', 'uninstalled'),
+ ])
+ l10n_mods.button_immediate_install()
+ env.reset() # clear the set of environments
+ env = env() # get an environment that refers to the new registry
+
+ coas = env['account.chart.template'].search([])
+ for coa in coas:
+ cname = 'company_%s' % str(coa.id)
+ company = env['res.company'].create({'name': cname})
+ env.user.company_ids += company
+ env.user.company_id = company
+ _logger.info('Testing COA: %s (company: %s)' % (coa.name, cname))
+ try:
+ with env.cr.savepoint():
+ coa.try_loading()
+ except Exception:
+ _logger.error("Error when creating COA %s", coa.name, exc_info=True)
diff --git a/addons/account/tests/test_account_analytic.py b/addons/account/tests/test_account_analytic.py
new file mode 100644
index 00000000..48ab4258
--- /dev/null
+++ b/addons/account/tests/test_account_analytic.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests import tagged
+from odoo.exceptions import UserError
+
+
+@tagged('post_install', '-at_install')
+class TestAccountAnalyticAccount(AccountTestInvoicingCommon):
+
+ @classmethod
+ 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),
+ ],
+ })
+
+ # 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'})
+
+ 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,
+ }),
+ ],
+ })
+
+ # 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']
+
+ # Making the analytic account not company dependent is allowed.
+ self.test_analytic_account.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']
+
+ # Making the analytic tag not company dependent is allowed.
+ self.test_analytic_tag.company_id = False
diff --git a/addons/account/tests/test_account_bank_statement.py b/addons/account/tests/test_account_bank_statement.py
new file mode 100644
index 00000000..10ec0aba
--- /dev/null
+++ b/addons/account/tests/test_account_bank_statement.py
@@ -0,0 +1,1594 @@
+# -*- coding: utf-8 -*-
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests import tagged
+from odoo.tests.common import Form
+from odoo.exceptions import ValidationError, UserError
+from odoo import fields
+
+from unittest.mock import patch
+
+
+class TestAccountBankStatementCommon(AccountTestInvoicingCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ # We need a third currency as you could have a company's currency != journal's currency !=
+ cls.currency_data_2 = cls.setup_multi_currency_data(default_values={
+ 'name': 'Dark Chocolate Coin',
+ 'symbol': '🍫',
+ 'currency_unit_label': 'Dark Choco',
+ 'currency_subunit_label': 'Dark Cacao Powder',
+ }, rate2016=6.0, rate2017=4.0)
+ cls.currency_data_3 = cls.setup_multi_currency_data(default_values={
+ 'name': 'Black Chocolate Coin',
+ 'symbol': '🍫',
+ 'currency_unit_label': 'Black Choco',
+ 'currency_subunit_label': 'Black Cacao Powder',
+ }, rate2016=12.0, rate2017=8.0)
+
+ cls.bank_journal_1 = cls.company_data['default_journal_bank']
+ cls.bank_journal_2 = cls.bank_journal_1.copy()
+ cls.bank_journal_3 = cls.bank_journal_2.copy()
+ cls.currency_1 = cls.company_data['currency']
+ cls.currency_2 = cls.currency_data['currency']
+ cls.currency_3 = cls.currency_data_2['currency']
+ cls.currency_4 = cls.currency_data_3['currency']
+
+ def assertBankStatementLine(self, statement_line, expected_statement_line_vals, expected_move_line_vals):
+ self.assertRecordValues(statement_line, [expected_statement_line_vals])
+ self.assertRecordValues(statement_line.line_ids.sorted('balance'), expected_move_line_vals)
+
+
+@tagged('post_install', '-at_install')
+class TestAccountBankStatement(TestAccountBankStatementCommon):
+
+ # -------------------------------------------------------------------------
+ # TESTS about the statement model.
+ # -------------------------------------------------------------------------
+
+ def test_starting_ending_balance_chaining(self):
+ # Create first statement on 2019-01-02.
+ bnk1 = self.env['account.bank.statement'].create({
+ 'name': 'BNK1',
+ 'date': '2019-01-02',
+ 'journal_id': self.company_data['default_journal_bank'].id,
+ 'line_ids': [(0, 0, {'payment_ref': '/', 'amount': 100.0})],
+ })
+ self.assertRecordValues(bnk1, [{
+ 'balance_start': 0.0,
+ 'balance_end_real': 100.0,
+ 'balance_end': 100.0,
+ 'previous_statement_id': False,
+ }])
+
+ # Create a new statement after that one.
+ bnk2 = self.env['account.bank.statement'].create({
+ 'name': 'BNK2',
+ 'date': '2019-01-10',
+ 'journal_id': self.company_data['default_journal_bank'].id,
+ 'line_ids': [(0, 0, {'payment_ref': '/', 'amount': 50.0})],
+ })
+ self.assertRecordValues(bnk2, [{
+ 'balance_start': 100.0,
+ 'balance_end_real': 150.0,
+ 'balance_end': 150.0,
+ 'previous_statement_id': bnk1.id,
+ }])
+
+ # Create new statement with given ending balance.
+ bnk3 = self.env['account.bank.statement'].create({
+ 'name': 'BNK3',
+ 'date': '2019-01-15',
+ 'journal_id': self.company_data['default_journal_bank'].id,
+ 'line_ids': [(0, 0, {'payment_ref': '/', 'amount': 25.0})],
+ 'balance_end_real': 200.0,
+ })
+ self.assertRecordValues(bnk3, [{
+ 'balance_start': 150.0,
+ 'balance_end_real': 200.0,
+ 'balance_end': 175.0,
+ 'previous_statement_id': bnk2.id,
+ }])
+
+ # Create new statement with a date right after BNK1.
+ bnk4 = self.env['account.bank.statement'].create({
+ 'name': 'BNK4',
+ 'date': '2019-01-03',
+ 'journal_id': self.company_data['default_journal_bank'].id,
+ 'line_ids': [(0, 0, {'payment_ref': '/', 'amount': 100.0})],
+ })
+ self.assertRecordValues(bnk4, [{
+ 'balance_start': 100.0,
+ 'balance_end_real': 200.0,
+ 'balance_end': 200.0,
+ 'previous_statement_id': bnk1.id,
+ }])
+
+ # BNK2/BNK3 should have changed their previous statements.
+ self.assertRecordValues(bnk2, [{
+ 'balance_start': 200.0,
+ 'balance_end_real': 250.0,
+ 'balance_end': 250.0,
+ 'previous_statement_id': bnk4.id,
+ }])
+ self.assertRecordValues(bnk3, [{
+ 'balance_start': 250.0,
+ 'balance_end_real': 200.0,
+ 'balance_end': 275.0,
+ 'previous_statement_id': bnk2.id,
+ }])
+
+ # Correct the ending balance of BNK3.
+ bnk3.balance_end_real = 275
+
+ # Change date of BNK4 to be the last.
+ bnk4.date = '2019-01-20'
+ self.assertRecordValues(bnk1, [{
+ 'balance_start': 0.0,
+ 'balance_end_real': 100.0,
+ 'balance_end': 100.0,
+ 'previous_statement_id': False,
+ }])
+ self.assertRecordValues(bnk2, [{
+ 'balance_start': 100.0,
+ 'balance_end_real': 150.0,
+ 'balance_end': 150.0,
+ 'previous_statement_id': bnk1.id,
+ }])
+ self.assertRecordValues(bnk3, [{
+ 'balance_start': 150.0,
+ 'balance_end_real': 175.0,
+ 'balance_end': 175.0,
+ 'previous_statement_id': bnk2.id,
+ }])
+ self.assertRecordValues(bnk4, [{
+ 'balance_start': 175.0,
+ 'balance_end_real': 200.0,
+ 'balance_end': 275.0,
+ 'previous_statement_id': bnk3.id,
+ }])
+
+ # Correct the ending balance of BNK4.
+ bnk4.balance_end_real = 275
+
+ # Move BNK3 to first position.
+ bnk3.date = '2019-01-01'
+ self.assertRecordValues(bnk3, [{
+ 'balance_start': 0.0,
+ 'balance_end_real': 25.0,
+ 'balance_end': 25.0,
+ 'previous_statement_id': False,
+ }])
+ self.assertRecordValues(bnk1, [{
+ 'balance_start': 25.0,
+ 'balance_end_real': 125.0,
+ 'balance_end': 125.0,
+ 'previous_statement_id': bnk3.id,
+ }])
+ self.assertRecordValues(bnk2, [{
+ 'balance_start': 125.0,
+ 'balance_end_real': 175.0,
+ 'balance_end': 175.0,
+ 'previous_statement_id': bnk1.id,
+ }])
+ self.assertRecordValues(bnk4, [{
+ 'balance_start': 175.0,
+ 'balance_end_real': 275.0,
+ 'balance_end': 275.0,
+ 'previous_statement_id': bnk2.id,
+ }])
+
+ # Move BNK1 to the third position.
+ bnk1.date = '2019-01-11'
+ self.assertRecordValues(bnk3, [{
+ 'balance_start': 0.0,
+ 'balance_end_real': 25.0,
+ 'balance_end': 25.0,
+ 'previous_statement_id': False,
+ }])
+ self.assertRecordValues(bnk2, [{
+ 'balance_start': 25.0,
+ 'balance_end_real': 75.0,
+ 'balance_end': 75.0,
+ 'previous_statement_id': bnk3.id,
+ }])
+ self.assertRecordValues(bnk1, [{
+ 'balance_start': 75.0,
+ 'balance_end_real': 175.0,
+ 'balance_end': 175.0,
+ 'previous_statement_id': bnk2.id,
+ }])
+ self.assertRecordValues(bnk4, [{
+ 'balance_start': 175.0,
+ 'balance_end_real': 275.0,
+ 'balance_end': 275.0,
+ 'previous_statement_id': bnk1.id,
+ }])
+
+ # Delete BNK3 and BNK1.
+ (bnk3 + bnk1).unlink()
+ self.assertRecordValues(bnk2, [{
+ 'balance_start': 0.0,
+ 'balance_end_real': 50.0,
+ 'balance_end': 50.0,
+ 'previous_statement_id': False,
+ }])
+ self.assertRecordValues(bnk4, [{
+ 'balance_start': 50.0,
+ 'balance_end_real': 275.0,
+ 'balance_end': 150.0,
+ 'previous_statement_id': bnk2.id,
+ }])
+
+ def test_statements_different_journal(self):
+ # Create statements in bank journal.
+ bnk1_1 = self.env['account.bank.statement'].create({
+ 'name': 'BNK1_1',
+ 'date': '2019-01-01',
+ 'journal_id': self.company_data['default_journal_bank'].id,
+ 'line_ids': [(0, 0, {'payment_ref': '/', 'amount': 100.0})],
+ 'balance_end_real': 100.0,
+ })
+ bnk1_2 = self.env['account.bank.statement'].create({
+ 'name': 'BNK1_2',
+ 'date': '2019-01-10',
+ 'journal_id': self.company_data['default_journal_bank'].id,
+ 'line_ids': [(0, 0, {'payment_ref': '/', 'amount': 50.0})],
+ })
+
+ # Create statements in cash journal.
+ bnk2_1 = self.env['account.bank.statement'].create({
+ 'name': 'BNK2_1',
+ 'date': '2019-01-02',
+ 'journal_id': self.company_data['default_journal_cash'].id,
+ 'line_ids': [(0, 0, {'payment_ref': '/', 'amount': 20.0})],
+ 'balance_end_real': 20.0,
+ })
+ bnk2_2 = self.env['account.bank.statement'].create({
+ 'name': 'BNK2_2',
+ 'date': '2019-01-12',
+ 'journal_id': self.company_data['default_journal_cash'].id,
+ 'line_ids': [(0, 0, {'payment_ref': '/', 'amount': 10.0})],
+ })
+ self.assertRecordValues(bnk1_1, [{
+ 'balance_start': 0.0,
+ 'balance_end_real': 100.0,
+ 'balance_end': 100.0,
+ 'previous_statement_id': False,
+ }])
+ self.assertRecordValues(bnk1_2, [{
+ 'balance_start': 100.0,
+ 'balance_end_real': 150.0,
+ 'balance_end': 150.0,
+ 'previous_statement_id': bnk1_1.id,
+ }])
+ self.assertRecordValues(bnk2_1, [{
+ 'balance_start': 0.0,
+ 'balance_end_real': 20.0,
+ 'balance_end': 20.0,
+ 'previous_statement_id': False,
+ }])
+ self.assertRecordValues(bnk2_2, [{
+ 'balance_start': 20.0,
+ 'balance_end_real': 0.0,
+ 'balance_end': 30.0,
+ 'previous_statement_id': bnk2_1.id,
+ }])
+
+ def test_cash_statement_with_difference(self):
+ ''' A cash statement always creates an additional line to store the cash difference towards the ending balance.
+ '''
+ statement = self.env['account.bank.statement'].create({
+ 'name': 'test_statement',
+ 'date': '2019-01-01',
+ 'journal_id': self.company_data['default_journal_cash'].id,
+ 'balance_end_real': 100.0,
+ })
+
+ statement.button_post()
+
+ self.assertRecordValues(statement.line_ids, [{
+ 'amount': 100.0,
+ 'is_reconciled': True,
+ }])
+
+
+@tagged('post_install', '-at_install')
+class TestAccountBankStatementLine(TestAccountBankStatementCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ cls.statement = cls.env['account.bank.statement'].create({
+ 'name': 'test_statement',
+ 'date': '2019-01-01',
+ 'journal_id': cls.bank_journal_1.id,
+ 'line_ids': [
+ (0, 0, {
+ 'date': '2019-01-01',
+ 'payment_ref': 'line_1',
+ 'partner_id': cls.partner_a.id,
+ 'foreign_currency_id': cls.currency_2.id,
+ 'amount': 1250.0,
+ 'amount_currency': 2500.0,
+ }),
+ ],
+ })
+ cls.statement_line = cls.statement.line_ids
+
+ cls.expected_st_line = {
+ 'date': fields.Date.from_string('2019-01-01'),
+ 'journal_id': cls.statement.journal_id.id,
+ 'payment_ref': 'line_1',
+ 'partner_id': cls.partner_a.id,
+ 'currency_id': cls.currency_1.id,
+ 'foreign_currency_id': cls.currency_2.id,
+ 'amount': 1250.0,
+ 'amount_currency': 2500.0,
+ 'is_reconciled': False,
+ }
+
+ cls.expected_bank_line = {
+ 'name': cls.statement_line.payment_ref,
+ 'partner_id': cls.statement_line.partner_id.id,
+ 'currency_id': cls.currency_2.id,
+ 'account_id': cls.statement.journal_id.default_account_id.id,
+ 'debit': 1250.0,
+ 'credit': 0.0,
+ 'amount_currency': 2500.0,
+ }
+
+ cls.expected_counterpart_line = {
+ 'name': cls.statement_line.payment_ref,
+ 'partner_id': cls.statement_line.partner_id.id,
+ 'currency_id': cls.currency_2.id,
+ 'account_id': cls.statement.journal_id.suspense_account_id.id,
+ 'debit': 0.0,
+ 'credit': 1250.0,
+ 'amount_currency': -2500.0,
+ }
+
+ # -------------------------------------------------------------------------
+ # TESTS about the statement line model.
+ # -------------------------------------------------------------------------
+
+ def _test_statement_line_edition(
+ self,
+ journal,
+ amount, amount_currency,
+ journal_currency, foreign_currency,
+ expected_liquidity_values, expected_counterpart_values):
+ ''' Test the edition of a statement line from itself or from its linked journal entry.
+ :param journal: The account.journal record that will be set on the statement line.
+ :param amount: The amount in journal's currency.
+ :param amount_currency: The amount in the foreign currency.
+ :param journal_currency: The journal's currency as a res.currency record.
+ :param foreign_currency: The foreign currency as a res.currency record.
+ :param expected_liquidity_values: The expected account.move.line values for the liquidity line.
+ :param expected_counterpart_values: The expected account.move.line values for the counterpart line.
+ '''
+ if journal_currency:
+ journal.currency_id = journal_currency.id
+
+ statement = self.env['account.bank.statement'].create({
+ 'name': 'test_statement',
+ 'date': '2019-01-01',
+ 'journal_id': journal.id,
+ 'line_ids': [
+ (0, 0, {
+ 'date': '2019-01-01',
+ 'payment_ref': 'line_1',
+ 'partner_id': self.partner_a.id,
+ 'foreign_currency_id': foreign_currency and foreign_currency.id,
+ 'amount': amount,
+ 'amount_currency': amount_currency,
+ }),
+ ],
+ })
+ statement_line = statement.line_ids
+
+ # ==== Test the statement line amounts are correct ====
+ # If there is a bug in the compute/inverse methods, the amount/amount_currency could be
+ # incorrect directly after the creation of the statement line.
+
+ self.assertRecordValues(statement_line, [{
+ 'amount': amount,
+ 'amount_currency': amount_currency,
+ }])
+ self.assertRecordValues(statement_line.move_id, [{
+ 'partner_id': self.partner_a.id,
+ 'currency_id': (statement_line.foreign_currency_id or statement_line.currency_id).id,
+ }])
+
+ # ==== Test the edition of statement line amounts ====
+ # The statement line must remain consistent with its account.move.
+ # To test the compute/inverse methods are correctly managing all currency setup,
+ # we check the edition of amounts in both directions statement line <-> journal entry.
+
+ # Check initial state of the statement line.
+ liquidity_lines, suspense_lines, other_lines = statement_line._seek_for_lines()
+ self.assertRecordValues(liquidity_lines, [expected_liquidity_values])
+ self.assertRecordValues(suspense_lines, [expected_counterpart_values])
+
+ # Check the account.move is still correct after editing the account.bank.statement.line.
+ statement_line.write({
+ 'amount': statement_line.amount * 2,
+ 'amount_currency': statement_line.amount_currency * 2,
+ })
+ self.assertRecordValues(statement_line, [{
+ 'amount': amount * 2,
+ 'amount_currency': amount_currency * 2,
+ }])
+ self.assertRecordValues(liquidity_lines, [{
+ **expected_liquidity_values,
+ 'debit': expected_liquidity_values.get('debit', 0.0) * 2,
+ 'credit': expected_liquidity_values.get('credit', 0.0) * 2,
+ 'amount_currency': expected_liquidity_values.get('amount_currency', 0.0) * 2,
+ }])
+ self.assertRecordValues(suspense_lines, [{
+ 'debit': expected_counterpart_values.get('debit', 0.0) * 2,
+ 'credit': expected_counterpart_values.get('credit', 0.0) * 2,
+ 'amount_currency': expected_counterpart_values.get('amount_currency', 0.0) * 2,
+ }])
+
+ # Check the account.bank.statement.line is still correct after editing the account.move.
+ statement_line.move_id.write({'line_ids': [
+ (1, liquidity_lines.id, {
+ 'debit': expected_liquidity_values.get('debit', 0.0),
+ 'credit': expected_liquidity_values.get('credit', 0.0),
+ 'amount_currency': expected_liquidity_values.get('amount_currency', 0.0),
+ }),
+ (1, suspense_lines.id, {
+ 'debit': expected_counterpart_values.get('debit', 0.0),
+ 'credit': expected_counterpart_values.get('credit', 0.0),
+ 'amount_currency': expected_counterpart_values.get('amount_currency', 0.0),
+ }),
+ ]})
+ self.assertRecordValues(statement_line, [{
+ 'amount': amount,
+ 'amount_currency': amount_currency,
+ }])
+
+ def _test_edition_customer_and_supplier_flows(
+ self,
+ amount, amount_currency,
+ journal_currency, foreign_currency,
+ expected_liquidity_values, expected_counterpart_values):
+ ''' Test '_test_statement_line_edition' using the customer (positive amounts)
+ & the supplier flow (negative amounts).
+ :param amount: The amount in journal's currency.
+ :param amount_currency: The amount in the foreign currency.
+ :param journal_currency: The journal's currency as a res.currency record.
+ :param foreign_currency: The foreign currency as a res.currency record.
+ :param expected_liquidity_values: The expected account.move.line values for the liquidity line.
+ :param expected_counterpart_values: The expected account.move.line values for the counterpart line.
+ '''
+
+ # Check the full process with positive amount (customer process).
+ self._test_statement_line_edition(
+ self.bank_journal_2,
+ amount, amount_currency,
+ journal_currency, foreign_currency,
+ expected_liquidity_values,
+ expected_counterpart_values,
+ )
+
+ # Check the full process with negative amount (supplier process).
+ self._test_statement_line_edition(
+ self.bank_journal_3,
+ -amount, -amount_currency,
+ journal_currency, foreign_currency,
+ {
+ **expected_liquidity_values,
+ 'debit': expected_liquidity_values.get('credit', 0.0),
+ 'credit': expected_liquidity_values.get('debit', 0.0),
+ 'amount_currency': -expected_liquidity_values.get('amount_currency', 0.0),
+ },
+ {
+ **expected_counterpart_values,
+ 'debit': expected_counterpart_values.get('credit', 0.0),
+ 'credit': expected_counterpart_values.get('debit', 0.0),
+ 'amount_currency': -expected_counterpart_values.get('amount_currency', 0.0),
+ },
+ )
+
+ def test_edition_journal_curr_2_statement_curr_3(self):
+ self._test_edition_customer_and_supplier_flows(
+ 80.0, 120.0,
+ self.currency_2, self.currency_3,
+ {'debit': 40.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_2.id},
+ {'debit': 0.0, 'credit': 40.0, 'amount_currency': -120.0, 'currency_id': self.currency_3.id},
+ )
+
+ def test_edition_journal_curr_2_statement_curr_1(self):
+ self._test_edition_customer_and_supplier_flows(
+ 120.0, 80.0,
+ self.currency_2, self.currency_1,
+ {'debit': 80.0, 'credit': 0.0, 'amount_currency': 120.0, 'currency_id': self.currency_2.id},
+ {'debit': 0.0, 'credit': 80.0, 'amount_currency': -80.0, 'currency_id': self.currency_1.id},
+ )
+
+ def test_edition_journal_curr_1_statement_curr_2(self):
+ self._test_edition_customer_and_supplier_flows(
+ 80.0, 120.0,
+ self.currency_1, self.currency_2,
+ {'debit': 80.0, 'credit': 0.0, 'amount_currency': 120.0, 'currency_id': self.currency_2.id},
+ {'debit': 0.0, 'credit': 80.0, 'amount_currency': -120.0, 'currency_id': self.currency_2.id},
+ )
+
+ def test_edition_journal_curr_2_statement_false(self):
+ self._test_edition_customer_and_supplier_flows(
+ 80.0, 0.0,
+ self.currency_2, False,
+ {'debit': 40.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_2.id},
+ {'debit': 0.0, 'credit': 40.0, 'amount_currency': -80.0, 'currency_id': self.currency_2.id},
+ )
+
+ def test_edition_journal_curr_1_statement_false(self):
+ self._test_edition_customer_and_supplier_flows(
+ 80.0, 0.0,
+ self.currency_1, False,
+ {'debit': 80.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_1.id},
+ {'debit': 0.0, 'credit': 80.0, 'amount_currency': -80.0, 'currency_id': self.currency_1.id},
+ )
+
+ def test_zero_amount_journal_curr_1_statement_curr_2(self):
+ self.bank_journal_2.currency_id = self.currency_1
+
+ statement = self.env['account.bank.statement'].create({
+ 'name': 'test_statement',
+ 'date': '2019-01-01',
+ 'journal_id': self.bank_journal_2.id,
+ 'line_ids': [
+ (0, 0, {
+ 'date': '2019-01-01',
+ 'payment_ref': 'line_1',
+ 'partner_id': self.partner_a.id,
+ 'foreign_currency_id': self.currency_2.id,
+ 'amount': 0.0,
+ 'amount_currency': 10.0,
+ }),
+ ],
+ })
+
+ self.assertRecordValues(statement.line_ids.move_id.line_ids, [
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': 10.0, 'currency_id': self.currency_2.id},
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': -10.0, 'currency_id': self.currency_2.id},
+ ])
+
+ def test_zero_amount_currency_journal_curr_1_statement_curr_2(self):
+ self.bank_journal_2.currency_id = self.currency_1
+
+ statement = self.env['account.bank.statement'].create({
+ 'name': 'test_statement',
+ 'date': '2019-01-01',
+ 'journal_id': self.bank_journal_2.id,
+ 'line_ids': [
+ (0, 0, {
+ 'date': '2019-01-01',
+ 'payment_ref': 'line_1',
+ 'partner_id': self.partner_a.id,
+ 'foreign_currency_id': self.currency_2.id,
+ 'amount': 10.0,
+ 'amount_currency': 0.0,
+ }),
+ ],
+ })
+
+ self.assertRecordValues(statement.line_ids.move_id.line_ids, [
+ {'debit': 10.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': self.currency_2.id},
+ {'debit': 0.0, 'credit': 10.0, 'amount_currency': 0.0, 'currency_id': self.currency_2.id},
+ ])
+
+ def test_zero_amount_journal_curr_2_statement_curr_1(self):
+ self.bank_journal_2.currency_id = self.currency_2
+
+ statement = self.env['account.bank.statement'].create({
+ 'name': 'test_statement',
+ 'date': '2019-01-01',
+ 'journal_id': self.bank_journal_2.id,
+ 'line_ids': [
+ (0, 0, {
+ 'date': '2019-01-01',
+ 'payment_ref': 'line_1',
+ 'partner_id': self.partner_a.id,
+ 'foreign_currency_id': self.currency_1.id,
+ 'amount': 0.0,
+ 'amount_currency': 10.0,
+ }),
+ ],
+ })
+
+ self.assertRecordValues(statement.line_ids.move_id.line_ids, [
+ {'debit': 10.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': self.currency_2.id},
+ {'debit': 0.0, 'credit': 10.0, 'amount_currency': -10.0, 'currency_id': self.currency_1.id},
+ ])
+
+ def test_zero_amount_currency_journal_curr_2_statement_curr_1(self):
+ self.bank_journal_2.currency_id = self.currency_2
+
+ statement = self.env['account.bank.statement'].create({
+ 'name': 'test_statement',
+ 'date': '2019-01-01',
+ 'journal_id': self.bank_journal_2.id,
+ 'line_ids': [
+ (0, 0, {
+ 'date': '2019-01-01',
+ 'payment_ref': 'line_1',
+ 'partner_id': self.partner_a.id,
+ 'foreign_currency_id': self.currency_1.id,
+ 'amount': 10.0,
+ 'amount_currency': 0.0,
+ }),
+ ],
+ })
+
+ self.assertRecordValues(statement.line_ids.move_id.line_ids, [
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': 10.0, 'currency_id': self.currency_2.id},
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': self.currency_1.id},
+ ])
+
+ def test_zero_amount_journal_curr_2_statement_curr_3(self):
+ self.bank_journal_2.currency_id = self.currency_2
+
+ statement = self.env['account.bank.statement'].create({
+ 'name': 'test_statement',
+ 'date': '2019-01-01',
+ 'journal_id': self.bank_journal_2.id,
+ 'line_ids': [
+ (0, 0, {
+ 'date': '2019-01-01',
+ 'payment_ref': 'line_1',
+ 'partner_id': self.partner_a.id,
+ 'foreign_currency_id': self.currency_3.id,
+ 'amount': 0.0,
+ 'amount_currency': 10.0,
+ }),
+ ],
+ })
+
+ self.assertRecordValues(statement.line_ids.move_id.line_ids, [
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': self.currency_2.id},
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': -10.0, 'currency_id': self.currency_3.id},
+ ])
+
+ def test_zero_amount_currency_journal_curr_2_statement_curr_3(self):
+ self.bank_journal_2.currency_id = self.currency_2
+
+ statement = self.env['account.bank.statement'].create({
+ 'name': 'test_statement',
+ 'date': '2019-01-01',
+ 'journal_id': self.bank_journal_2.id,
+ 'line_ids': [
+ (0, 0, {
+ 'date': '2019-01-01',
+ 'payment_ref': 'line_1',
+ 'partner_id': self.partner_a.id,
+ 'foreign_currency_id': self.currency_3.id,
+ 'amount': 10.0,
+ 'amount_currency': 0.0,
+ }),
+ ],
+ })
+
+ self.assertRecordValues(statement.line_ids.move_id.line_ids, [
+ {'debit': 5.0, 'credit': 0.0, 'amount_currency': 10.0, 'currency_id': self.currency_2.id},
+ {'debit': 0.0, 'credit': 5.0, 'amount_currency': 0.0, 'currency_id': self.currency_3.id},
+ ])
+
+ def test_constraints(self):
+ def assertStatementLineConstraint(statement_vals, statement_line_vals):
+ with self.assertRaises(Exception), self.cr.savepoint():
+ self.env['account.bank.statement'].create({
+ **statement_vals,
+ 'line_ids': [(0, 0, statement_line_vals)],
+ })
+
+ statement_vals = {
+ 'name': 'test_statement',
+ 'date': '2019-01-01',
+ 'journal_id': self.bank_journal_2.id,
+ }
+ statement_line_vals = {
+ 'date': '2019-01-01',
+ 'payment_ref': 'line_1',
+ 'partner_id': self.partner_a.id,
+ 'foreign_currency_id': False,
+ 'amount': 10.0,
+ 'amount_currency': 0.0,
+ }
+
+ # ==== Test constraints at creation ====
+
+ # Foreign currency must not be the same as the journal one.
+ assertStatementLineConstraint(statement_vals, {
+ **statement_line_vals,
+ 'foreign_currency_id': self.currency_1.id,
+ })
+
+ # Can't have a stand alone amount in foreign currency without foreign currency set.
+ assertStatementLineConstraint(statement_vals, {
+ **statement_line_vals,
+ 'amount_currency': 10.0,
+ })
+
+ # ==== Test constraints at edition ====
+
+ statement = self.env['account.bank.statement'].create({
+ **statement_vals,
+ 'line_ids': [(0, 0, statement_line_vals)],
+ })
+ st_line = statement.line_ids
+
+ # You can't messed up the journal entry by adding another liquidity line.
+ addition_lines_to_create = [
+ {
+ 'debit': 1.0,
+ 'credit': 0,
+ 'account_id': self.bank_journal_2.default_account_id.id,
+ 'move_id': st_line.move_id.id,
+ },
+ {
+ 'debit': 0,
+ 'credit': 1.0,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'move_id': st_line.move_id.id,
+ },
+ ]
+ with self.assertRaises(UserError), self.cr.savepoint():
+ st_line.move_id.write({
+ 'line_ids': [(0, 0, vals) for vals in addition_lines_to_create]
+ })
+
+ with self.assertRaises(UserError), self.cr.savepoint():
+ st_line.line_ids.create(addition_lines_to_create)
+
+ # You can't set the journal entry in an unconsistent state.
+ with self.assertRaises(UserError), self.cr.savepoint():
+ st_line.move_id.action_post()
+
+ def test_statement_line_move_onchange_1(self):
+ ''' Test the consistency between the account.bank.statement.line and the generated account.move.lines
+ using the form view emulator.
+ '''
+
+ # Check the initial state of the statement line.
+ self.assertBankStatementLine(self.statement_line, self.expected_st_line, [self.expected_counterpart_line, self.expected_bank_line])
+
+ # Inverse the amount + change them.
+ with Form(self.statement) as statement_form:
+ with statement_form.line_ids.edit(0) as st_line_form:
+ st_line_form.amount = -2000.0
+ st_line_form.amount_currency = -4000.0
+ st_line_form.foreign_currency_id = self.currency_3
+
+ self.assertBankStatementLine(self.statement_line, {
+ **self.expected_st_line,
+ 'amount': -2000.0,
+ 'amount_currency': -4000.0,
+ 'foreign_currency_id': self.currency_3.id,
+ }, [
+ {
+ **self.expected_bank_line,
+ 'debit': 0.0,
+ 'credit': 2000.0,
+ 'amount_currency': -4000.0,
+ 'currency_id': self.currency_3.id,
+ },
+ {
+ **self.expected_counterpart_line,
+ 'debit': 2000.0,
+ 'credit': 0.0,
+ 'amount_currency': 4000.0,
+ 'currency_id': self.currency_3.id,
+ },
+ ])
+
+ # Check changing the label and the partner.
+ with Form(self.statement) as statement_form:
+ with statement_form.line_ids.edit(0) as st_line_form:
+ st_line_form.payment_ref = 'line_1 (bis)'
+ st_line_form.partner_id = self.partner_b
+
+ self.assertBankStatementLine(self.statement_line, {
+ **self.expected_st_line,
+ 'payment_ref': self.statement_line.payment_ref,
+ 'partner_id': self.statement_line.partner_id.id,
+ 'amount': -2000.0,
+ 'amount_currency': -4000.0,
+ 'foreign_currency_id': self.currency_3.id,
+ }, [
+ {
+ **self.expected_bank_line,
+ 'name': self.statement_line.payment_ref,
+ 'partner_id': self.statement_line.partner_id.id,
+ 'debit': 0.0,
+ 'credit': 2000.0,
+ 'amount_currency': -4000.0,
+ 'currency_id': self.currency_3.id,
+ },
+ {
+ **self.expected_counterpart_line,
+ 'name': self.statement_line.payment_ref,
+ 'partner_id': self.statement_line.partner_id.id,
+ 'debit': 2000.0,
+ 'credit': 0.0,
+ 'amount_currency': 4000.0,
+ 'currency_id': self.currency_3.id,
+ },
+ ])
+
+ # -------------------------------------------------------------------------
+ # TESTS about reconciliation:
+ # - Test '_prepare_counterpart_move_line_vals': one test for each case.
+ # - Test 'reconcile': 3 cases:
+ # - Open-balance in debit.
+ # - Open-balance in credit.
+ # - No open-balance.
+ # - Test 'button_undo_reconciliation'.
+ # -------------------------------------------------------------------------
+
+ def _test_statement_line_reconciliation(
+ self,
+ journal,
+ amount, amount_currency, counterpart_amount,
+ journal_currency, foreign_currency, counterpart_currency,
+ expected_liquidity_values, expected_counterpart_values):
+ ''' Test the reconciliation of a statement line.
+ :param journal: The account.journal record that will be set on the statement line.
+ :param amount: The amount in journal's currency.
+ :param amount_currency: The amount in the foreign currency.
+ :param counterpart_amount: The amount of the invoice to reconcile.
+ :param journal_currency: The journal's currency as a res.currency record.
+ :param foreign_currency: The foreign currency as a res.currency record.
+ :param counterpart_currency: The invoice currency as a res.currency record.
+ :param expected_liquidity_values: The expected account.move.line values for the liquidity line.
+ :param expected_counterpart_values: The expected account.move.line values for the counterpart line.
+ '''
+ if journal_currency:
+ journal.currency_id = journal_currency.id
+
+ statement = self.env['account.bank.statement'].create({
+ 'name': 'test_statement',
+ 'date': '2019-01-01',
+ 'journal_id': journal.id,
+ 'line_ids': [
+ (0, 0, {
+ 'date': '2019-01-01',
+ 'payment_ref': 'line_1',
+ 'partner_id': self.partner_a.id,
+ 'foreign_currency_id': foreign_currency and foreign_currency.id,
+ 'amount': amount,
+ 'amount_currency': amount_currency,
+ }),
+ ],
+ })
+ statement_line = statement.line_ids
+
+ # - There is 3 flows to check:
+ # * The invoice will fully reconcile the statement line.
+ # * The invoice will partially reconcile the statement line and leads to an open balance in debit.
+ # * The invoice will partially reconcile the statement line and leads to an open balance in credit.
+ # - The dates are different to be sure the reconciliation will preserve the conversion rate bank side.
+ move_type = 'out_invoice' if counterpart_amount < 0.0 else 'in_invoice'
+
+ test_invoices = self.env['account.move'].create([
+ {
+ 'move_type': move_type,
+ 'invoice_date': fields.Date.from_string('2016-01-01'),
+ 'date': fields.Date.from_string('2016-01-01'),
+ 'partner_id': self.partner_a.id,
+ 'currency_id': counterpart_currency.id,
+ 'invoice_line_ids': [
+ (0, None, {
+ 'name': 'counterpart line, same amount',
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'quantity': 1,
+ 'price_unit': abs(counterpart_amount),
+ }),
+ ],
+ },
+ {
+ 'move_type': move_type,
+ 'invoice_date': fields.Date.from_string('2016-01-01'),
+ 'date': fields.Date.from_string('2016-01-01'),
+ 'partner_id': self.partner_a.id,
+ 'currency_id': counterpart_currency.id,
+ 'invoice_line_ids': [
+ (0, None, {
+ 'name': 'counterpart line, lower amount',
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'quantity': 1,
+ 'price_unit': abs(counterpart_amount / 2),
+ }),
+ ],
+ },
+ {
+ 'move_type': move_type,
+ 'invoice_date': fields.Date.from_string('2016-01-01'),
+ 'date': fields.Date.from_string('2016-01-01'),
+ 'partner_id': self.partner_a.id,
+ 'currency_id': counterpart_currency.id,
+ 'invoice_line_ids': [
+ (0, None, {
+ 'name': 'counterpart line, bigger amount',
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'quantity': 1,
+ 'price_unit': abs(counterpart_amount * 2),
+ }),
+ ],
+ },
+ ])
+ test_invoices.action_post()
+ statement.button_post()
+ counterpart_lines = test_invoices.mapped('line_ids').filtered(lambda line: line.account_internal_type in ('receivable', 'payable'))
+
+ # Check the full reconciliation.
+ statement_line.reconcile([{'id': counterpart_lines[0].id}])
+ liquidity_lines, suspense_lines, other_lines = statement_line._seek_for_lines()
+ self.assertRecordValues(liquidity_lines, [expected_liquidity_values])
+ self.assertRecordValues(other_lines, [expected_counterpart_values])
+
+ # Check the reconciliation with partial lower amount.
+ statement_line.button_undo_reconciliation()
+ statement_line.reconcile([{'id': counterpart_lines[1].id}])
+ liquidity_lines, suspense_lines, other_lines = statement_line._seek_for_lines()
+ self.assertRecordValues(liquidity_lines, [expected_liquidity_values])
+ self.assertRecordValues(other_lines.sorted('balance', reverse=amount < 0.0), [
+ {
+ **expected_counterpart_values,
+ 'debit': expected_counterpart_values.get('debit', 0.0) / 2,
+ 'credit': expected_counterpart_values.get('credit', 0.0) / 2,
+ 'amount_currency': expected_counterpart_values.get('amount_currency', 0.0) / 2,
+ },
+ {
+ 'debit': expected_counterpart_values.get('debit', 0.0) / 2,
+ 'credit': expected_counterpart_values.get('credit', 0.0) / 2,
+ 'amount_currency': expected_counterpart_values.get('amount_currency', 0.0) / 2,
+ 'currency_id': expected_counterpart_values.get('currency_id'),
+ },
+ ])
+
+ # Check the reconciliation with partial higher amount.
+ statement_line.button_undo_reconciliation()
+ statement_line.reconcile([{'id': counterpart_lines[2].id}])
+ liquidity_lines, suspense_lines, other_lines = statement_line._seek_for_lines()
+ self.assertRecordValues(liquidity_lines, [expected_liquidity_values])
+ self.assertRecordValues(other_lines.sorted('balance', reverse=amount < 0.0), [
+ {
+ **expected_counterpart_values,
+ 'debit': expected_counterpart_values.get('debit', 0.0) * 2,
+ 'credit': expected_counterpart_values.get('credit', 0.0) * 2,
+ 'amount_currency': expected_counterpart_values.get('amount_currency', 0.0) * 2,
+ },
+ {
+ 'debit': expected_counterpart_values.get('credit', 0.0),
+ 'credit': expected_counterpart_values.get('debit', 0.0),
+ 'amount_currency': -expected_counterpart_values.get('amount_currency', 0.0),
+ 'currency_id': expected_counterpart_values.get('currency_id'),
+ },
+ ])
+
+ # Make sure the statement line is still correct.
+ self.assertRecordValues(statement_line, [{
+ 'amount': amount,
+ 'amount_currency': amount_currency,
+ }])
+
+ def _test_reconciliation_customer_and_supplier_flows(
+ self,
+ amount, amount_currency, counterpart_amount,
+ journal_currency, foreign_currency, counterpart_currency,
+ expected_liquidity_values, expected_counterpart_values):
+ ''' Test '_test_statement_line_reconciliation' using the customer (positive amounts)
+ & the supplier flow (negative amounts).
+ :param amount: The amount in journal's currency.
+ :param amount_currency: The amount in the foreign currency.
+ :param counterpart_amount: The amount of the invoice to reconcile.
+ :param journal_currency: The journal's currency as a res.currency record.
+ :param foreign_currency: The foreign currency as a res.currency record.
+ :param counterpart_currency: The invoice currency as a res.currency record.
+ :param expected_liquidity_values: The expected account.move.line values for the liquidity line.
+ :param expected_counterpart_values: The expected account.move.line values for the counterpart line.
+ '''
+
+ # Check the full process with positive amount (customer process).
+ self._test_statement_line_reconciliation(
+ self.bank_journal_2,
+ amount, amount_currency, counterpart_amount,
+ journal_currency, foreign_currency, counterpart_currency,
+ expected_liquidity_values,
+ expected_counterpart_values,
+ )
+
+ # Check the full process with negative amount (supplier process).
+ self._test_statement_line_reconciliation(
+ self.bank_journal_3,
+ -amount, -amount_currency, -counterpart_amount,
+ journal_currency, foreign_currency, counterpart_currency,
+ {
+ **expected_liquidity_values,
+ 'debit': expected_liquidity_values.get('credit', 0.0),
+ 'credit': expected_liquidity_values.get('debit', 0.0),
+ 'amount_currency': -expected_liquidity_values.get('amount_currency', 0.0),
+ },
+ {
+ **expected_counterpart_values,
+ 'debit': expected_counterpart_values.get('credit', 0.0),
+ 'credit': expected_counterpart_values.get('debit', 0.0),
+ 'amount_currency': -expected_counterpart_values.get('amount_currency', 0.0),
+ },
+ )
+
+ def test_reconciliation_journal_curr_2_statement_curr_3_counterpart_curr_3(self):
+ self._test_reconciliation_customer_and_supplier_flows(
+ 80.0, 120.0, -120.0,
+ self.currency_2, self.currency_3, self.currency_3,
+ {'debit': 40.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_2.id},
+ {'debit': 0.0, 'credit': 40.0, 'amount_currency': -120.0, 'currency_id': self.currency_3.id},
+ )
+
+ def test_reconciliation_journal_curr_2_statement_curr_1_counterpart_curr_2(self):
+ self._test_reconciliation_customer_and_supplier_flows(
+ 120.0, 80.0, -120.0,
+ self.currency_2, self.currency_1, self.currency_2,
+ {'debit': 80.0, 'credit': 0.0, 'amount_currency': 120.0, 'currency_id': self.currency_2.id},
+ {'debit': 0.0, 'credit': 80.0, 'amount_currency': -80.0, 'currency_id': self.currency_1.id},
+ )
+
+ def test_reconciliation_journal_curr_2_statement_curr_3_counterpart_curr_2(self):
+ self._test_reconciliation_customer_and_supplier_flows(
+ 80.0, 120.0, -80.0,
+ self.currency_2, self.currency_3, self.currency_2,
+ {'debit': 40.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_2.id},
+ {'debit': 0.0, 'credit': 40.0, 'amount_currency': -120.0, 'currency_id': self.currency_3.id},
+ )
+
+ def test_reconciliation_journal_curr_2_statement_curr_3_counterpart_curr_4(self):
+ self._test_reconciliation_customer_and_supplier_flows(
+ 80.0, 120.0, -480.0,
+ self.currency_2, self.currency_3, self.currency_4,
+ {'debit': 40.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_2.id},
+ {'debit': 0.0, 'credit': 40.0, 'amount_currency': -120.0, 'currency_id': self.currency_3.id},
+ )
+
+ def test_reconciliation_journal_curr_1_statement_curr_2_counterpart_curr_2(self):
+ self._test_reconciliation_customer_and_supplier_flows(
+ 80.0, 120.0, -120.0,
+ self.currency_1, self.currency_2, self.currency_2,
+ {'debit': 80.0, 'credit': 0.0, 'amount_currency': 120.0, 'currency_id': self.currency_2.id},
+ {'debit': 0.0, 'credit': 80.0, 'amount_currency': -120.0, 'currency_id': self.currency_2.id},
+ )
+
+ def test_reconciliation_journal_curr_1_statement_curr_2_counterpart_curr_3(self):
+ self._test_reconciliation_customer_and_supplier_flows(
+ 80.0, 120.0, -480.0,
+ self.currency_1, self.currency_2, self.currency_3,
+ {'debit': 80.0, 'credit': 0.0, 'amount_currency': 120.0, 'currency_id': self.currency_2.id},
+ {'debit': 0.0, 'credit': 80.0, 'amount_currency': -120.0, 'currency_id': self.currency_2.id},
+ )
+
+ def test_reconciliation_journal_curr_2_statement_false_counterpart_curr_2(self):
+ self._test_reconciliation_customer_and_supplier_flows(
+ 80.0, 0.0, -80.0,
+ self.currency_2, False, self.currency_2,
+ {'debit': 40.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_2.id},
+ {'debit': 0.0, 'credit': 40.0, 'amount_currency': -80.0, 'currency_id': self.currency_2.id},
+ )
+
+ def test_reconciliation_journal_curr_2_statement_false_counterpart_curr_3(self):
+ self._test_reconciliation_customer_and_supplier_flows(
+ 80.0, 0.0, -240.0,
+ self.currency_2, False, self.currency_3,
+ {'debit': 40.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_2.id},
+ {'debit': 0.0, 'credit': 40.0, 'amount_currency': -80.0, 'currency_id': self.currency_2.id},
+ )
+
+ def test_reconciliation_journal_curr_1_statement_false_counterpart_curr_3(self):
+ self._test_reconciliation_customer_and_supplier_flows(
+ 80.0, 0.0, -480.0,
+ self.currency_1, False, self.currency_3,
+ {'debit': 80.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_1.id},
+ {'debit': 0.0, 'credit': 80.0, 'amount_currency': -80.0, 'currency_id': self.currency_1.id},
+ )
+
+ def test_reconciliation_journal_curr_2_statement_curr_1_counterpart_curr_1(self):
+ self._test_reconciliation_customer_and_supplier_flows(
+ 120.0, 80.0, -80.0,
+ self.currency_2, self.currency_1, self.currency_1,
+ {'debit': 80.0, 'credit': 0.0, 'amount_currency': 120.0, 'currency_id': self.currency_2.id},
+ {'debit': 0.0, 'credit': 80.0, 'amount_currency': -80.0, 'currency_id': self.currency_1.id},
+ )
+
+ def test_reconciliation_journal_curr_2_statement_curr_3_counterpart_curr_1(self):
+ self._test_reconciliation_customer_and_supplier_flows(
+ 80.0, 120.0, -40.0,
+ self.currency_2, self.currency_3, self.currency_1,
+ {'debit': 40.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_2.id},
+ {'debit': 0.0, 'credit': 40.0, 'amount_currency': -120.0, 'currency_id': self.currency_3.id},
+ )
+
+ def test_reconciliation_journal_curr_1_statement_curr_2_counterpart_curr_1(self):
+ self._test_reconciliation_customer_and_supplier_flows(
+ 80.0, 120.0, -80.0,
+ self.currency_1, self.currency_2, self.currency_1,
+ {'debit': 80.0, 'credit': 0.0, 'amount_currency': 120.0, 'currency_id': self.currency_2.id},
+ {'debit': 0.0, 'credit': 80.0, 'amount_currency': -120.0, 'currency_id': self.currency_2.id},
+ )
+
+ def test_reconciliation_journal_curr_2_statement_false_counterpart_curr_1(self):
+ self._test_reconciliation_customer_and_supplier_flows(
+ 80.0, 0.0, -40.0,
+ self.currency_2, False, self.currency_1,
+ {'debit': 40.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_2.id},
+ {'debit': 0.0, 'credit': 40.0, 'amount_currency': -80.0, 'currency_id': self.currency_2.id},
+ )
+
+ def test_reconciliation_journal_curr_1_statement_false_counterpart_curr_1(self):
+ self._test_reconciliation_customer_and_supplier_flows(
+ 80.0, 0.0, -80.0,
+ self.currency_1, False, self.currency_1,
+ {'debit': 80.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_1.id},
+ {'debit': 0.0, 'credit': 80.0, 'amount_currency': -80.0, 'currency_id': self.currency_1.id},
+ )
+
+ def test_reconciliation_statement_line_state(self):
+ ''' Test the reconciliation on the bank statement line with a foreign currency on the journal:
+ - Ensure the statement line is_reconciled field is well computed.
+ - Ensure the reconciliation is working well when dealing with a foreign currency at different dates.
+ - Ensure the reconciliation can be undo.
+ - Ensure the reconciliation is still possible with to_check.
+ '''
+ self.statement.button_post()
+
+ receivable_acc_1 = self.company_data['default_account_receivable']
+ receivable_acc_2 = self.copy_account(self.company_data['default_account_receivable'])
+ payment_account = self.bank_journal_1.payment_debit_account_id
+ random_acc_1 = self.company_data['default_account_revenue']
+ random_acc_2 = self.copy_account(self.company_data['default_account_revenue'])
+ test_move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': fields.Date.from_string('2016-01-01'),
+ 'line_ids': [
+ (0, None, {
+ 'name': 'counterpart of the whole move',
+ 'account_id': random_acc_1.id,
+ 'debit': 0.0,
+ 'credit': 1030.0,
+ }),
+ (0, None, {
+ 'name': 'test line 1 - receivable account',
+ 'account_id': receivable_acc_1.id,
+ 'currency_id': self.currency_2.id,
+ 'debit': 500.0,
+ 'credit': 0.0,
+ 'amount_currency': 1500.0,
+ }),
+ (0, None, {
+ 'name': 'test line 2 - another receivable account',
+ 'account_id': receivable_acc_2.id,
+ 'currency_id': self.currency_2.id,
+ 'debit': 500.0,
+ 'credit': 0.0,
+ 'amount_currency': 1500.0,
+ }),
+ (0, None, {
+ 'name': 'test line 3 - payment transfer account',
+ 'account_id': payment_account.id,
+ 'currency_id': self.currency_2.id,
+ 'debit': 30.0,
+ 'credit': 0.0,
+ 'amount_currency': 90.0,
+ }),
+ ]
+ })
+ test_move.action_post()
+
+ test_line_1 = test_move.line_ids.filtered(lambda line: line.account_id == receivable_acc_1)
+ test_line_2 = test_move.line_ids.filtered(lambda line: line.account_id == receivable_acc_2)
+ test_line_3 = test_move.line_ids.filtered(lambda line: line.account_id == payment_account)
+ self.statement_line.reconcile([
+ # test line 1
+ # Will reconcile 300.0 in balance, 600.0 in amount_currency.
+ {'id': test_line_1.id, 'balance': -600.0},
+ # test line 2
+ # Will reconcile 250.0 in balance, 500.0 in amount_currency.
+ {'id': test_line_2.id, 'balance': -500.0},
+ # test line 3
+ # Will reconcile 30.0 in balance, 90.0 in amount_currency.
+ {'id': test_line_3.id},
+ # test line 4
+ # Will reconcile 50.0 in balance, 100.0 in amount_currency.
+ {'name': 'whatever', 'account_id': random_acc_1.id, 'balance': -100.0},
+ ])
+
+ self.assertBankStatementLine(self.statement_line, {
+ **self.expected_st_line,
+ 'is_reconciled': True,
+ }, [
+ {
+ 'name': '%s: Open Balance' % self.statement_line.payment_ref,
+ 'partner_id': self.statement_line.partner_id.id,
+ 'currency_id': self.currency_2.id,
+ 'account_id': receivable_acc_1.id, # This account is retrieved on the partner.
+ 'debit': 0.0,
+ 'credit': 605.0,
+ 'amount_currency': -1210.0,
+ 'amount_residual': -605.0,
+ 'amount_residual_currency': -1210.0,
+ },
+ {
+ 'name': test_line_1.name,
+ 'partner_id': self.statement_line.partner_id.id,
+ 'currency_id': self.currency_2.id,
+ 'account_id': receivable_acc_1.id,
+ 'debit': 0.0,
+ 'credit': 300.0,
+ 'amount_currency': -600.0,
+ 'amount_residual': 0.0,
+ 'amount_residual_currency': 0.0,
+ },
+ {
+ 'name': test_line_2.name,
+ 'partner_id': self.statement_line.partner_id.id,
+ 'currency_id': self.currency_2.id,
+ 'account_id': receivable_acc_2.id,
+ 'debit': 0.0,
+ 'credit': 250.0,
+ 'amount_currency': -500.0,
+ 'amount_residual': 0.0,
+ 'amount_residual_currency': 0.0,
+ },
+ {
+ 'name': 'whatever',
+ 'partner_id': self.statement_line.partner_id.id,
+ 'currency_id': self.currency_2.id,
+ 'account_id': random_acc_1.id,
+ 'debit': 0.0,
+ 'credit': 50.0,
+ 'amount_currency': -100.0,
+ 'amount_residual': 0.0,
+ 'amount_residual_currency': 0.0,
+ },
+ {
+ 'name': test_line_3.name,
+ 'partner_id': self.statement_line.partner_id.id,
+ 'currency_id': self.currency_2.id,
+ 'account_id': test_line_3.account_id.id,
+ 'debit': 0.0,
+ 'credit': 45.0,
+ 'amount_currency': -90.0,
+ 'amount_residual': 0.0,
+ 'amount_residual_currency': 0.0,
+ },
+ {
+ **self.expected_bank_line,
+ 'amount_residual': 1250.0,
+ 'amount_residual_currency': 2500.0,
+ },
+ ])
+
+ # Undo the reconciliation to return to the initial state.
+ self.statement_line.button_undo_reconciliation()
+ self.assertBankStatementLine(self.statement_line, self.expected_st_line, [self.expected_counterpart_line, self.expected_bank_line])
+
+ # Modify the counterpart line with to_check enabled.
+ self.statement_line.reconcile([
+ {'name': 'whatever', 'account_id': random_acc_1.id, 'balance': -100.0},
+ ], to_check=True)
+
+ self.assertBankStatementLine(self.statement_line, {
+ **self.expected_st_line,
+ 'is_reconciled': True,
+ }, [
+ {
+ 'name': '%s: Open Balance' % self.statement_line.payment_ref,
+ 'partner_id': self.statement_line.partner_id.id,
+ 'currency_id': self.currency_2.id,
+ 'account_id': receivable_acc_1.id, # This account is retrieved on the partner.
+ 'debit': 0.0,
+ 'credit': 1200.0,
+ 'amount_currency': -2400.0,
+ 'amount_residual': -1200.0,
+ 'amount_residual_currency': -2400.0,
+ },
+ {
+ 'name': 'whatever',
+ 'partner_id': self.statement_line.partner_id.id,
+ 'currency_id': self.currency_2.id,
+ 'account_id': random_acc_1.id,
+ 'debit': 0.0,
+ 'credit': 50.0,
+ 'amount_currency': -100.0,
+ 'amount_residual': 0.0,
+ 'amount_residual_currency': 0.0,
+ },
+ {
+ **self.expected_bank_line,
+ 'amount_residual': 1250.0,
+ 'amount_residual_currency': 2500.0,
+ },
+ ])
+
+ # Modify the counterpart line. Should be allowed by the to_check enabled.
+ self.statement_line.reconcile([
+ {'name': 'whatever again', 'account_id': random_acc_2.id, 'balance': -500.0},
+ ])
+
+ self.assertBankStatementLine(self.statement_line, {
+ **self.expected_st_line,
+ 'is_reconciled': True,
+ }, [
+ {
+ 'name': '%s: Open Balance' % self.statement_line.payment_ref,
+ 'partner_id': self.statement_line.partner_id.id,
+ 'currency_id': self.currency_2.id,
+ 'account_id': receivable_acc_1.id, # This account is retrieved on the partner.
+ 'debit': 0.0,
+ 'credit': 1000.0,
+ 'amount_currency': -2000.0,
+ 'amount_residual': -1000.0,
+ 'amount_residual_currency': -2000.0,
+ },
+ {
+ 'name': 'whatever again',
+ 'partner_id': self.statement_line.partner_id.id,
+ 'currency_id': self.currency_2.id,
+ 'account_id': random_acc_2.id,
+ 'debit': 0.0,
+ 'credit': 250.0,
+ 'amount_currency': -500.0,
+ 'amount_residual': 0.0,
+ 'amount_residual_currency': 0.0,
+ },
+ {
+ **self.expected_bank_line,
+ 'amount_residual': 1250.0,
+ 'amount_residual_currency': 2500.0,
+ },
+ ])
+
+ # The statement line is no longer in the 'to_check' mode.
+ # Reconciling again should raise an error.
+ with self.assertRaises(UserError), self.cr.savepoint():
+ self.statement_line.reconcile([
+ {'name': 'whatever', 'account_id': random_acc_1.id, 'balance': -100.0},
+ ])
+
+ def test_reconciliation_statement_line_with_generated_payments(self):
+ self.statement.button_post()
+
+ receivable_account = self.company_data['default_account_receivable']
+ payment_account = self.bank_journal_1.payment_debit_account_id
+ random_account = self.company_data['default_account_revenue']
+ test_move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': fields.Date.from_string('2016-01-01'),
+ 'line_ids': [
+ (0, None, {
+ 'name': 'counterpart of the whole move',
+ 'account_id': random_account.id,
+ 'debit': 0.0,
+ 'credit': 1000.0,
+ }),
+ (0, None, {
+ 'name': 'test line 1',
+ 'account_id': receivable_account.id,
+ 'debit': 100.0,
+ 'credit': 0.0,
+ }),
+ (0, None, {
+ 'name': 'test line 2',
+ 'account_id': receivable_account.id,
+ 'currency_id': self.currency_2.id,
+ 'debit': 900.0,
+ 'credit': 0.0,
+ 'amount_currency': 1500.0,
+ }),
+ ]
+ })
+ test_move.action_post()
+
+ test_line_1 = test_move.line_ids.filtered(lambda line: line.name == 'test line 1')
+ test_line_2 = test_move.line_ids.filtered(lambda line: line.name == 'test line 2')
+
+ statement_line = self.statement_line
+ StatementLine_prepare_reconciliation = type(statement_line)._prepare_reconciliation
+
+ def _prepare_reconciliation(self, lines_vals_list, create_payment_for_invoice=False):
+ if self == statement_line:
+ create_payment_for_invoice = True
+ return StatementLine_prepare_reconciliation(self, lines_vals_list, create_payment_for_invoice)
+
+ with patch.object(type(statement_line), '_prepare_reconciliation', _prepare_reconciliation):
+ self.statement_line.reconcile([
+ {'id': test_line_1.id, 'balance': -50.0},
+ {'id': test_line_2.id},
+ ])
+
+ self.assertBankStatementLine(self.statement_line, {
+ **self.expected_st_line,
+ 'is_reconciled': True,
+ }, [
+ {
+ 'name': 'test line 2',
+ 'account_id': payment_account.id,
+ 'currency_id': self.currency_2.id,
+ 'debit': 0.0,
+ 'credit': 750.0,
+ 'amount_currency': -1500.0,
+ },
+ {
+ 'name': 'line_1: Open Balance',
+ 'account_id': receivable_account.id,
+ 'currency_id': self.currency_2.id,
+ 'debit': 0.0,
+ 'credit': 450.0,
+ 'amount_currency': -900.0,
+ },
+ {
+ 'name': 'test line 1',
+ 'account_id': payment_account.id,
+ 'currency_id': self.currency_2.id,
+ 'debit': 0.0,
+ 'credit': 50.0,
+ 'amount_currency': -100.0,
+ },
+ self.expected_bank_line,
+ ])
+
+ # Check generated payments.
+ self.assertRecordValues(test_line_1.matched_credit_ids.credit_move_id.payment_id.line_ids.sorted('balance'), [
+ {
+ 'partner_id': self.statement_line.partner_id.id,
+ 'currency_id': self.currency_2.id,
+ 'account_id': test_line_1.account_id.id,
+ 'debit': 0.0,
+ 'credit': 50.0,
+ 'amount_currency': -100.0,
+ 'amount_residual': 0.0,
+ 'amount_residual_currency': 50.0,
+ },
+ {
+ 'partner_id': self.statement_line.partner_id.id,
+ 'currency_id': self.currency_2.id,
+ 'account_id': payment_account.id,
+ 'debit': 50.0,
+ 'credit': 0.0,
+ 'amount_currency': 100.0,
+ 'amount_residual': 0.0,
+ 'amount_residual_currency': 0.0,
+ },
+ ])
+ self.assertRecordValues(test_line_2.matched_credit_ids.credit_move_id.payment_id.line_ids.sorted('balance'), [
+ {
+ 'partner_id': self.statement_line.partner_id.id,
+ 'currency_id': self.currency_2.id,
+ 'account_id': test_line_2.account_id.id,
+ 'debit': 0.0,
+ 'credit': 750.0,
+ 'amount_currency': -1500.0,
+ 'amount_residual': 0.0,
+ 'amount_residual_currency': 0.0,
+ },
+ {
+ 'partner_id': self.statement_line.partner_id.id,
+ 'currency_id': self.currency_2.id,
+ 'account_id': payment_account.id,
+ 'debit': 750.0,
+ 'credit': 0.0,
+ 'amount_currency': 1500.0,
+ 'amount_residual': 0.0,
+ 'amount_residual_currency': 0.0,
+ },
+ ])
+
+ def test_conversion_rate_rounding_issue(self):
+ ''' Ensure the reconciliation is well handling the rounding issue due to multiple currency conversion rates.
+
+ In this test, the resulting journal entry after reconciliation is:
+ {'amount_currency': 7541.66, 'debit': 6446.97, 'credit': 0.0}
+ {'amount_currency': 226.04, 'debit': 193.22, 'credit': 0.0}
+ {'amount_currency': -7767.70, 'debit': 0.0, 'credit': 6640.19}
+ ... but 226.04 / 1.1698 = 193.23. In this situation, 0.01 has been removed from this write-off line in order to
+ avoid an unecessary open-balance line being an exchange difference issue.
+ '''
+ self.bank_journal_2.currency_id = self.currency_2
+ self.currency_data['rates'][-1].rate = 1.1698
+
+ statement = self.env['account.bank.statement'].create({
+ 'name': 'test_statement',
+ 'date': '2017-01-01',
+ 'journal_id': self.bank_journal_2.id,
+ 'line_ids': [
+ (0, 0, {
+ 'date': '2019-01-01',
+ 'payment_ref': 'line_1',
+ 'partner_id': self.partner_a.id,
+ 'amount': 7541.66,
+ }),
+ ],
+ })
+ statement.button_post()
+ statement_line = statement.line_ids
+
+ payment = self.env['account.payment'].create({
+ 'amount': 7767.70,
+ 'date': '2019-01-01',
+ 'currency_id': self.currency_2.id,
+ 'payment_type': 'inbound',
+ 'partner_type': 'customer',
+ })
+ payment.action_post()
+ liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines()
+ self.assertRecordValues(liquidity_lines, [{'amount_currency': 7767.70}])
+
+ statement_line.reconcile([
+ {'id': liquidity_lines.id},
+ {'balance': 226.04, 'account_id': self.company_data['default_account_revenue'].id, 'name': "write-off"},
+ ])
+
+ self.assertRecordValues(statement_line.line_ids, [
+ {'amount_currency': 7541.66, 'debit': 6446.97, 'credit': 0.0},
+ {'amount_currency': 226.04, 'debit': 193.22, 'credit': 0.0},
+ {'amount_currency': -7767.70, 'debit': 0.0, 'credit': 6640.19},
+ ])
+
+ def test_zero_amount_statement_line(self):
+ ''' Ensure the statement line is directly marked as reconciled when having an amount of zero. '''
+ self.company_data['company'].account_journal_suspense_account_id.reconcile = False
+
+ statement = self.env['account.bank.statement'].with_context(skip_check_amounts_currencies=True).create({
+ 'name': 'test_statement',
+ 'date': '2017-01-01',
+ 'journal_id': self.bank_journal_2.id,
+ 'line_ids': [
+ (0, 0, {
+ 'date': '2019-01-01',
+ 'payment_ref': "Happy new year",
+ 'amount': 0.0,
+ }),
+ ],
+ })
+ statement_line = statement.line_ids
+
+ self.assertRecordValues(statement_line, [{'is_reconciled': True, 'amount_residual': 0.0}])
+
+ def test_bank_statement_line_analytic(self):
+ ''' Ensure the analytic lines are generated during the reconciliation. '''
+ analytic_account = self.env['account.analytic.account'].create({'name': 'analytic_account'})
+
+ statement = self.env['account.bank.statement'].with_context(skip_check_amounts_currencies=True).create({
+ 'name': 'test_statement',
+ 'date': '2017-01-01',
+ 'journal_id': self.bank_journal_2.id,
+ 'line_ids': [
+ (0, 0, {
+ 'date': '2019-01-01',
+ 'payment_ref': "line",
+ 'amount': 100.0,
+ }),
+ ],
+ })
+ statement_line = statement.line_ids
+
+ statement_line.reconcile([{
+ 'balance': -100.0,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'name': "write-off",
+ 'analytic_account_id': analytic_account.id,
+ }])
+
+ # Check the analytic account is there.
+ self.assertRecordValues(statement_line.line_ids.sorted('balance'), [
+ {'balance': -100.0, 'analytic_account_id': analytic_account.id},
+ {'balance': 100.0, 'analytic_account_id': False},
+ ])
+
+ # Check the analytic lines.
+ self.assertRecordValues(statement_line.line_ids.analytic_line_ids, [
+ {'amount': 100.0, 'account_id': analytic_account.id},
+ ])
diff --git a/addons/account/tests/test_account_incoming_supplier_invoice.py b/addons/account/tests/test_account_incoming_supplier_invoice.py
new file mode 100644
index 00000000..373fcbab
--- /dev/null
+++ b/addons/account/tests/test_account_incoming_supplier_invoice.py
@@ -0,0 +1,123 @@
+# -*- coding: utf-8 -*-
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests import tagged
+
+import json
+
+
+@tagged('post_install', '-at_install')
+class TestAccountIncomingSupplierInvoice(AccountTestInvoicingCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ cls.env['ir.config_parameter'].sudo().set_param('mail.catchall.domain', 'test-company.odoo.com')
+
+ cls.internal_user = cls.env['res.users'].create({
+ 'name': 'Internal User',
+ 'login': 'internal.user@test.odoo.com',
+ 'email': 'internal.user@test.odoo.com',
+ })
+
+ cls.supplier_partner = cls.env['res.partner'].create({
+ 'name': 'Your Supplier',
+ 'email': 'supplier@other.company.com',
+ 'supplier_rank': 10,
+ })
+
+ cls.journal = cls.company_data['default_journal_purchase']
+
+ journal_alias = cls.env['mail.alias'].create({
+ 'alias_name': 'test-bill',
+ 'alias_model_id': cls.env.ref('account.model_account_move').id,
+ 'alias_defaults': json.dumps({
+ 'move_type': 'in_invoice',
+ 'company_id': cls.env.user.company_id.id,
+ 'journal_id': cls.journal.id,
+ }),
+ })
+ cls.journal.write({'alias_id': journal_alias.id})
+
+ def test_supplier_invoice_mailed_from_supplier(self):
+ message_parsed = {
+ 'message_id': 'message-id-dead-beef',
+ 'subject': 'Incoming bill',
+ 'from': '%s <%s>' % (self.supplier_partner.name, self.supplier_partner.email),
+ 'to': '%s@%s' % (self.journal.alias_id.alias_name, self.journal.alias_id.alias_domain),
+ 'body': "You know, that thing that you bought.",
+ 'attachments': [b'Hello, invoice'],
+ }
+
+ invoice = self.env['account.move'].message_new(message_parsed, {'move_type': 'in_invoice', 'journal_id': self.journal.id})
+
+ message_ids = invoice.message_ids
+ self.assertEqual(len(message_ids), 1, 'Only one message should be posted in the chatter')
+ self.assertEqual(message_ids.body, '<p>Vendor Bill Created</p>', 'Only the invoice creation should be posted')
+
+ following_partners = invoice.message_follower_ids.mapped('partner_id')
+ self.assertEqual(following_partners, self.env.user.partner_id)
+ self.assertRegex(invoice.name, 'BILL/\d{4}/\d{2}/0001')
+
+ def test_supplier_invoice_forwarded_by_internal_user_without_supplier(self):
+ """ In this test, the bill was forwarded by an employee,
+ but no partner email address is found in the body."""
+ message_parsed = {
+ 'message_id': 'message-id-dead-beef',
+ 'subject': 'Incoming bill',
+ 'from': '%s <%s>' % (self.internal_user.name, self.internal_user.email),
+ 'to': '%s@%s' % (self.journal.alias_id.alias_name, self.journal.alias_id.alias_domain),
+ 'body': "You know, that thing that you bought.",
+ 'attachments': [b'Hello, invoice'],
+ }
+
+ invoice = self.env['account.move'].message_new(message_parsed, {'move_type': 'in_invoice', 'journal_id': self.journal.id})
+
+ message_ids = invoice.message_ids
+ self.assertEqual(len(message_ids), 1, 'Only one message should be posted in the chatter')
+ self.assertEqual(message_ids.body, '<p>Vendor Bill Created</p>', 'Only the invoice creation should be posted')
+
+ following_partners = invoice.message_follower_ids.mapped('partner_id')
+ self.assertEqual(following_partners, self.env.user.partner_id | self.internal_user.partner_id)
+
+ def test_supplier_invoice_forwarded_by_internal_with_supplier_in_body(self):
+ """ In this test, the bill was forwarded by an employee,
+ and the partner email address is found in the body."""
+ message_parsed = {
+ 'message_id': 'message-id-dead-beef',
+ 'subject': 'Incoming bill',
+ 'from': '%s <%s>' % (self.internal_user.name, self.internal_user.email),
+ 'to': '%s@%s' % (self.journal.alias_id.alias_name, self.journal.alias_id.alias_domain),
+ 'body': "Mail sent by %s <%s>:\nYou know, that thing that you bought." % (self.supplier_partner.name, self.supplier_partner.email),
+ 'attachments': [b'Hello, invoice'],
+ }
+
+ invoice = self.env['account.move'].message_new(message_parsed, {'move_type': 'in_invoice', 'journal_id': self.journal.id})
+
+ message_ids = invoice.message_ids
+ self.assertEqual(len(message_ids), 1, 'Only one message should be posted in the chatter')
+ self.assertEqual(message_ids.body, '<p>Vendor Bill Created</p>', 'Only the invoice creation should be posted')
+
+ following_partners = invoice.message_follower_ids.mapped('partner_id')
+ self.assertEqual(following_partners, self.env.user.partner_id | self.internal_user.partner_id)
+
+ def test_supplier_invoice_forwarded_by_internal_with_internal_in_body(self):
+ """ In this test, the bill was forwarded by an employee,
+ and the internal user email address is found in the body."""
+ message_parsed = {
+ 'message_id': 'message-id-dead-beef',
+ 'subject': 'Incoming bill',
+ 'from': '%s <%s>' % (self.internal_user.name, self.internal_user.email),
+ 'to': '%s@%s' % (self.journal.alias_id.alias_name, self.journal.alias_id.alias_domain),
+ 'body': "Mail sent by %s <%s>:\nYou know, that thing that you bought." % (self.internal_user.name, self.internal_user.email),
+ 'attachments': [b'Hello, invoice'],
+ }
+
+ invoice = self.env['account.move'].message_new(message_parsed, {'move_type': 'in_invoice', 'journal_id': self.journal.id})
+
+ message_ids = invoice.message_ids
+ self.assertEqual(len(message_ids), 1, 'Only one message should be posted in the chatter')
+ self.assertEqual(message_ids.body, '<p>Vendor Bill Created</p>', 'Only the invoice creation should be posted')
+
+ following_partners = invoice.message_follower_ids.mapped('partner_id')
+ self.assertEqual(following_partners, self.env.user.partner_id | self.internal_user.partner_id)
diff --git a/addons/account/tests/test_account_invoice_report.py b/addons/account/tests/test_account_invoice_report.py
new file mode 100644
index 00000000..e136ef71
--- /dev/null
+++ b/addons/account/tests/test_account_invoice_report.py
@@ -0,0 +1,118 @@
+# -*- coding: utf-8 -*-
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests import tagged
+from odoo import fields
+
+
+@tagged('post_install', '-at_install')
+class TestAccountInvoiceReport(AccountTestInvoicingCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ cls.invoices = cls.env['account.move'].create([
+ {
+ 'move_type': 'out_invoice',
+ 'partner_id': cls.partner_a.id,
+ 'invoice_date': fields.Date.from_string('2016-01-01'),
+ 'currency_id': cls.currency_data['currency'].id,
+ 'invoice_line_ids': [
+ (0, None, {
+ 'product_id': cls.product_a.id,
+ 'quantity': 3,
+ 'price_unit': 750,
+ }),
+ (0, None, {
+ 'product_id': cls.product_a.id,
+ 'quantity': 1,
+ 'price_unit': 3000,
+ }),
+ ]
+ },
+ {
+ 'move_type': 'out_receipt',
+ 'invoice_date': fields.Date.from_string('2016-01-01'),
+ 'currency_id': cls.currency_data['currency'].id,
+ 'invoice_line_ids': [
+ (0, None, {
+ 'product_id': cls.product_a.id,
+ 'quantity': 1,
+ 'price_unit': 6000,
+ }),
+ ]
+ },
+ {
+ 'move_type': 'out_refund',
+ 'partner_id': cls.partner_a.id,
+ 'invoice_date': fields.Date.from_string('2017-01-01'),
+ 'currency_id': cls.currency_data['currency'].id,
+ 'invoice_line_ids': [
+ (0, None, {
+ 'product_id': cls.product_a.id,
+ 'quantity': 1,
+ 'price_unit': 1200,
+ }),
+ ]
+ },
+ {
+ 'move_type': 'in_invoice',
+ 'partner_id': cls.partner_a.id,
+ 'invoice_date': fields.Date.from_string('2016-01-01'),
+ 'currency_id': cls.currency_data['currency'].id,
+ 'invoice_line_ids': [
+ (0, None, {
+ 'product_id': cls.product_a.id,
+ 'quantity': 1,
+ 'price_unit': 60,
+ }),
+ ]
+ },
+ {
+ 'move_type': 'in_receipt',
+ 'partner_id': cls.partner_a.id,
+ 'invoice_date': fields.Date.from_string('2016-01-01'),
+ 'currency_id': cls.currency_data['currency'].id,
+ 'invoice_line_ids': [
+ (0, None, {
+ 'product_id': cls.product_a.id,
+ 'quantity': 1,
+ 'price_unit': 60,
+ }),
+ ]
+ },
+ {
+ 'move_type': 'in_refund',
+ 'partner_id': cls.partner_a.id,
+ 'invoice_date': fields.Date.from_string('2017-01-01'),
+ 'currency_id': cls.currency_data['currency'].id,
+ 'invoice_line_ids': [
+ (0, None, {
+ 'product_id': cls.product_a.id,
+ 'quantity': 1,
+ 'price_unit': 12,
+ }),
+ ]
+ },
+ ])
+
+ def assertInvoiceReportValues(self, expected_values_list):
+ reports = self.env['account.invoice.report'].search([('company_id', '=', self.company_data['company'].id)], order='price_subtotal DESC, quantity ASC')
+ expected_values_dict = [{
+ 'price_average': vals[0],
+ 'price_subtotal': vals[1],
+ 'quantity': vals[2],
+ } for vals in expected_values_list]
+ self.assertRecordValues(reports, expected_values_dict)
+
+ def test_invoice_report_multiple_types(self):
+ self.assertInvoiceReportValues([
+ #price_average price_subtotal quantity
+ [2000, 2000, 1],
+ [1000, 1000, 1],
+ [250, 750, 3],
+ [6, 6, 1],
+ [-20, -20, -1],
+ [-20, -20, -1],
+ [-600, -600, -1],
+ ])
diff --git a/addons/account/tests/test_account_journal.py b/addons/account/tests/test_account_journal.py
new file mode 100644
index 00000000..b8644db0
--- /dev/null
+++ b/addons/account/tests/test_account_journal.py
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests import tagged
+from odoo.exceptions import UserError, ValidationError
+
+
+@tagged('post_install', '-at_install')
+class TestAccountJournal(AccountTestInvoicingCommon):
+
+ def test_constraint_currency_consistency_with_accounts(self):
+ ''' The accounts linked to a bank/cash journal must share the same foreign currency
+ if specified.
+ '''
+ journal_bank = self.company_data['default_journal_bank']
+ journal_bank.currency_id = self.currency_data['currency']
+
+ # Try to set a different currency on the 'debit' account.
+ with self.assertRaises(ValidationError), self.cr.savepoint():
+ journal_bank.default_account_id.currency_id = self.company_data['currency']
+
+ def test_changing_journal_company(self):
+ ''' Ensure you can't change the company of an account.journal if there are some journal entries '''
+
+ self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2019-01-01',
+ 'journal_id': self.company_data['default_journal_sale'].id,
+ })
+
+ with self.assertRaises(UserError), self.cr.savepoint():
+ self.company_data['default_journal_sale'].company_id = self.company_data_2['company']
+
+ def test_account_control_create_journal_entry(self):
+ move_vals = {
+ 'line_ids': [
+ (0, 0, {
+ 'name': 'debit',
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'debit': 100.0,
+ 'credit': 0.0,
+ }),
+ (0, 0, {
+ 'name': 'credit',
+ 'account_id': self.company_data['default_account_expense'].id,
+ 'debit': 0.0,
+ 'credit': 100.0,
+ }),
+ ],
+ }
+
+ # Should fail because 'default_account_expense' is not allowed.
+ self.company_data['default_journal_misc'].account_control_ids |= self.company_data['default_account_revenue']
+ with self.assertRaises(UserError), self.cr.savepoint():
+ self.env['account.move'].create(move_vals)
+
+ # Should be allowed because both accounts are accepted.
+ self.company_data['default_journal_misc'].account_control_ids |= self.company_data['default_account_expense']
+ self.env['account.move'].create(move_vals)
+
+ def test_account_control_existing_journal_entry(self):
+ self.env['account.move'].create({
+ 'line_ids': [
+ (0, 0, {
+ 'name': 'debit',
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'debit': 100.0,
+ 'credit': 0.0,
+ }),
+ (0, 0, {
+ 'name': 'credit',
+ 'account_id': self.company_data['default_account_expense'].id,
+ 'debit': 0.0,
+ 'credit': 100.0,
+ }),
+ ],
+ })
+
+ # There is already an other line using the 'default_account_expense' account.
+ with self.assertRaises(ValidationError), self.cr.savepoint():
+ self.company_data['default_journal_misc'].account_control_ids |= self.company_data['default_account_revenue']
+
+ # Assigning both should be allowed
+ self.company_data['default_journal_misc'].account_control_ids = \
+ self.company_data['default_account_revenue'] + self.company_data['default_account_expense']
diff --git a/addons/account/tests/test_account_journal_dashboard.py b/addons/account/tests/test_account_journal_dashboard.py
new file mode 100644
index 00000000..6bb127b6
--- /dev/null
+++ b/addons/account/tests/test_account_journal_dashboard.py
@@ -0,0 +1,94 @@
+# -*- coding: utf-8 -*-
+from freezegun import freeze_time
+
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests import tagged
+
+@tagged('post_install', '-at_install')
+class TestAccountJournalDashboard(AccountTestInvoicingCommon):
+
+ @freeze_time("2019-01-22")
+ def test_customer_invoice_dashboard(self):
+ journal = self.company_data['default_journal_sale']
+
+ invoice = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'journal_id': journal.id,
+ 'partner_id': self.partner_a.id,
+ 'invoice_date': '2019-01-21',
+ 'date': '2019-01-21',
+ 'invoice_line_ids': [(0, 0, {
+ 'product_id': self.product_a.id,
+ 'quantity': 40.0,
+ 'name': 'product test 1',
+ 'discount': 10.00,
+ 'price_unit': 2.27,
+ })]
+ })
+ refund = self.env['account.move'].create({
+ 'move_type': 'out_refund',
+ 'journal_id': journal.id,
+ 'partner_id': self.partner_a.id,
+ 'invoice_date': '2019-01-21',
+ 'date': '2019-01-21',
+ 'invoice_line_ids': [(0, 0, {
+ 'product_id': self.product_a.id,
+ 'quantity': 1.0,
+ 'name': 'product test 1',
+ 'price_unit': 13.3,
+ })]
+ })
+
+ # Check Draft
+ dashboard_data = journal.get_journal_dashboard_datas()
+
+ self.assertEqual(dashboard_data['number_draft'], 2)
+ self.assertIn('68.42', dashboard_data['sum_draft'])
+
+ self.assertEqual(dashboard_data['number_waiting'], 0)
+ self.assertIn('0.00', dashboard_data['sum_waiting'])
+
+ # Check Both
+ invoice.action_post()
+
+ dashboard_data = journal.get_journal_dashboard_datas()
+ self.assertEqual(dashboard_data['number_draft'], 1)
+ self.assertIn('-13.30', dashboard_data['sum_draft'])
+
+ self.assertEqual(dashboard_data['number_waiting'], 1)
+ self.assertIn('81.72', dashboard_data['sum_waiting'])
+
+ # Check waiting payment
+ refund.action_post()
+
+ dashboard_data = journal.get_journal_dashboard_datas()
+ self.assertEqual(dashboard_data['number_draft'], 0)
+ self.assertIn('0.00', dashboard_data['sum_draft'])
+
+ self.assertEqual(dashboard_data['number_waiting'], 2)
+ self.assertIn('68.42', dashboard_data['sum_waiting'])
+
+ # Check partial
+ receivable_account = refund.line_ids.mapped('account_id').filtered(lambda a: a.internal_type == 'receivable')
+ payment = self.env['account.payment'].create({
+ 'amount': 10.0,
+ 'payment_type': 'outbound',
+ 'partner_type': 'customer',
+ 'partner_id': self.partner_a.id,
+ })
+ payment.action_post()
+
+ (refund + payment.move_id).line_ids\
+ .filtered(lambda line: line.account_internal_type == 'receivable')\
+ .reconcile()
+
+ dashboard_data = journal.get_journal_dashboard_datas()
+ self.assertEqual(dashboard_data['number_draft'], 0)
+ self.assertIn('0.00', dashboard_data['sum_draft'])
+
+ self.assertEqual(dashboard_data['number_waiting'], 2)
+ self.assertIn('78.42', dashboard_data['sum_waiting'])
+
+ dashboard_data = journal.get_journal_dashboard_datas()
+ self.assertEqual(dashboard_data['number_late'], 2)
+ self.assertIn('78.42', dashboard_data['sum_late'])
diff --git a/addons/account/tests/test_account_move_entry.py b/addons/account/tests/test_account_move_entry.py
new file mode 100644
index 00000000..336deab9
--- /dev/null
+++ b/addons/account/tests/test_account_move_entry.py
@@ -0,0 +1,490 @@
+# -*- coding: utf-8 -*-
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests import tagged, new_test_user
+from odoo.tests.common import Form
+from odoo import fields, api, SUPERUSER_ID
+from odoo.exceptions import ValidationError, UserError, RedirectWarning
+from odoo.tools import mute_logger
+
+from dateutil.relativedelta import relativedelta
+from functools import reduce
+import json
+import psycopg2
+
+
+@tagged('post_install', '-at_install')
+class TestAccountMove(AccountTestInvoicingCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ tax_repartition_line = cls.company_data['default_tax_sale'].refund_repartition_line_ids\
+ .filtered(lambda line: line.repartition_type == 'tax')
+ cls.test_move = cls.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': fields.Date.from_string('2016-01-01'),
+ 'line_ids': [
+ (0, None, {
+ 'name': 'revenue line 1',
+ 'account_id': cls.company_data['default_account_revenue'].id,
+ 'debit': 500.0,
+ 'credit': 0.0,
+ }),
+ (0, None, {
+ 'name': 'revenue line 2',
+ 'account_id': cls.company_data['default_account_revenue'].id,
+ 'debit': 1000.0,
+ 'credit': 0.0,
+ 'tax_ids': [(6, 0, cls.company_data['default_tax_sale'].ids)],
+ }),
+ (0, None, {
+ 'name': 'tax line',
+ 'account_id': cls.company_data['default_account_tax_sale'].id,
+ 'debit': 150.0,
+ 'credit': 0.0,
+ 'tax_repartition_line_id': tax_repartition_line.id,
+ }),
+ (0, None, {
+ 'name': 'counterpart line',
+ 'account_id': cls.company_data['default_account_expense'].id,
+ 'debit': 0.0,
+ 'credit': 1650.0,
+ }),
+ ]
+ })
+
+ def test_custom_currency_on_account_1(self):
+ custom_account = self.company_data['default_account_revenue'].copy()
+
+ # The currency set on the account is not the same as the one set on the company.
+ # It should raise an error.
+ custom_account.currency_id = self.currency_data['currency']
+
+ with self.assertRaises(UserError), self.cr.savepoint():
+ self.test_move.line_ids[0].account_id = custom_account
+
+ # The currency set on the account is the same as the one set on the company.
+ # It should not raise an error.
+ custom_account.currency_id = self.company_data['currency']
+
+ self.test_move.line_ids[0].account_id = custom_account
+
+ def test_misc_fiscalyear_lock_date_1(self):
+ self.test_move.action_post()
+
+ # Set the lock date after the journal entry date.
+ self.test_move.company_id.fiscalyear_lock_date = fields.Date.from_string('2017-01-01')
+
+ # lines[0] = 'counterpart line'
+ # lines[1] = 'tax line'
+ # lines[2] = 'revenue line 1'
+ # lines[3] = 'revenue line 2'
+ lines = self.test_move.line_ids.sorted('debit')
+
+ # Editing the reference should be allowed.
+ self.test_move.ref = 'whatever'
+
+ # Try to edit a line into a locked fiscal year.
+ with self.assertRaises(UserError), self.cr.savepoint():
+ self.test_move.write({
+ 'line_ids': [
+ (1, lines[0].id, {'credit': lines[0].credit + 100.0}),
+ (1, lines[2].id, {'debit': lines[2].debit + 100.0}),
+ ],
+ })
+
+ # Try to edit the account of a line.
+ with self.assertRaises(UserError), self.cr.savepoint():
+ self.test_move.line_ids[0].write({'account_id': self.test_move.line_ids[0].account_id.copy().id})
+
+ # Try to edit a line.
+ with self.assertRaises(UserError), self.cr.savepoint():
+ self.test_move.write({
+ 'line_ids': [
+ (1, lines[0].id, {'credit': lines[0].credit + 100.0}),
+ (1, lines[3].id, {'debit': lines[3].debit + 100.0}),
+ ],
+ })
+
+ # Try to add a new tax on a line.
+ with self.assertRaises(UserError), self.cr.savepoint():
+ self.test_move.write({
+ 'line_ids': [
+ (1, lines[2].id, {'tax_ids': [(6, 0, self.company_data['default_tax_purchase'].ids)]}),
+ ],
+ })
+
+ # Try to create a new line.
+ with self.assertRaises(UserError), self.cr.savepoint():
+ self.test_move.write({
+ 'line_ids': [
+ (1, lines[0].id, {'credit': lines[0].credit + 100.0}),
+ (0, None, {
+ 'name': 'revenue line 1',
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'debit': 100.0,
+ 'credit': 0.0,
+ }),
+ ],
+ })
+
+ # You can't remove the journal entry from a locked period.
+ with self.assertRaises(UserError), self.cr.savepoint():
+ self.test_move.date = fields.Date.from_string('2018-01-01')
+
+ with self.assertRaises(UserError), self.cr.savepoint():
+ self.test_move.unlink()
+
+ with self.assertRaises(UserError), self.cr.savepoint():
+ self.test_move.button_draft()
+
+ # Try to add a new journal entry prior to the lock date.
+ copy_move = self.test_move.copy({'date': '2017-01-01'})
+ # The date has been changed to the first valid date.
+ self.assertEqual(copy_move.date, copy_move.company_id.fiscalyear_lock_date + relativedelta(days=1))
+
+ def test_misc_fiscalyear_lock_date_2(self):
+ self.test_move.action_post()
+
+ # Create a bank statement to get a balance in the suspense account.
+ statement = self.env['account.bank.statement'].create({
+ 'journal_id': self.company_data['default_journal_bank'].id,
+ 'date': '2016-01-01',
+ 'line_ids': [
+ (0, 0, {'payment_ref': 'test', 'amount': 10.0})
+ ],
+ })
+ statement.button_post()
+
+ # You can't lock the fiscal year if there is some unreconciled statement.
+ with self.assertRaises(RedirectWarning), self.cr.savepoint():
+ self.test_move.company_id.fiscalyear_lock_date = fields.Date.from_string('2017-01-01')
+
+ def test_misc_tax_lock_date_1(self):
+ self.test_move.action_post()
+
+ # Set the tax lock date after the journal entry date.
+ self.test_move.company_id.tax_lock_date = fields.Date.from_string('2017-01-01')
+
+ # lines[0] = 'counterpart line'
+ # lines[1] = 'tax line'
+ # lines[2] = 'revenue line 1'
+ # lines[3] = 'revenue line 2'
+ lines = self.test_move.line_ids.sorted('debit')
+
+ # Try to edit a line not affecting the taxes.
+ self.test_move.write({
+ 'line_ids': [
+ (1, lines[0].id, {'credit': lines[0].credit + 100.0}),
+ (1, lines[2].id, {'debit': lines[2].debit + 100.0}),
+ ],
+ })
+
+ # Try to edit the account of a line.
+ self.test_move.line_ids[0].write({'account_id': self.test_move.line_ids[0].account_id.copy().id})
+
+ # Try to edit a line having some taxes.
+ with self.assertRaises(UserError), self.cr.savepoint():
+ self.test_move.write({
+ 'line_ids': [
+ (1, lines[0].id, {'credit': lines[0].credit + 100.0}),
+ (1, lines[3].id, {'debit': lines[3].debit + 100.0}),
+ ],
+ })
+
+ # Try to add a new tax on a line.
+ with self.assertRaises(UserError), self.cr.savepoint():
+ self.test_move.write({
+ 'line_ids': [
+ (1, lines[2].id, {'tax_ids': [(6, 0, self.company_data['default_tax_purchase'].ids)]}),
+ ],
+ })
+
+ # Try to edit a tax line.
+ with self.assertRaises(UserError), self.cr.savepoint():
+ self.test_move.write({
+ 'line_ids': [
+ (1, lines[0].id, {'credit': lines[0].credit + 100.0}),
+ (1, lines[1].id, {'debit': lines[1].debit + 100.0}),
+ ],
+ })
+
+ # Try to create a line not affecting the taxes.
+ self.test_move.write({
+ 'line_ids': [
+ (1, lines[0].id, {'credit': lines[0].credit + 100.0}),
+ (0, None, {
+ 'name': 'revenue line 1',
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'debit': 100.0,
+ 'credit': 0.0,
+ }),
+ ],
+ })
+
+ # Try to create a line affecting the taxes.
+ with self.assertRaises(UserError), self.cr.savepoint():
+ self.test_move.write({
+ 'line_ids': [
+ (1, lines[0].id, {'credit': lines[0].credit + 100.0}),
+ (0, None, {
+ 'name': 'revenue line 2',
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'debit': 1000.0,
+ 'credit': 0.0,
+ 'tax_ids': [(6, 0, self.company_data['default_tax_sale'].ids)],
+ }),
+ ],
+ })
+
+ # You can't remove the journal entry from a locked period.
+ with self.assertRaises(UserError), self.cr.savepoint():
+ self.test_move.date = fields.Date.from_string('2018-01-01')
+
+ with self.assertRaises(UserError), self.cr.savepoint():
+ self.test_move.unlink()
+
+ with self.assertRaises(UserError), self.cr.savepoint():
+ self.test_move.button_draft()
+
+ copy_move = self.test_move.copy({'date': self.test_move.date})
+
+ # /!\ The date is changed automatically to the next available one during the post.
+ copy_move.action_post()
+
+ # You can't change the date to one being in a locked period.
+ with self.assertRaises(UserError), self.cr.savepoint():
+ copy_move.date = fields.Date.from_string('2017-01-01')
+
+ def test_misc_draft_reconciled_entries_1(self):
+ draft_moves = self.env['account.move'].create([
+ {
+ 'move_type': 'entry',
+ 'line_ids': [
+ (0, None, {
+ 'name': 'move 1 receivable line',
+ 'account_id': self.company_data['default_account_receivable'].id,
+ 'debit': 1000.0,
+ 'credit': 0.0,
+ }),
+ (0, None, {
+ 'name': 'move 1 counterpart line',
+ 'account_id': self.company_data['default_account_expense'].id,
+ 'debit': 0.0,
+ 'credit': 1000.0,
+ }),
+ ]
+ },
+ {
+ 'move_type': 'entry',
+ 'line_ids': [
+ (0, None, {
+ 'name': 'move 2 receivable line',
+ 'account_id': self.company_data['default_account_receivable'].id,
+ 'debit': 0.0,
+ 'credit': 2000.0,
+ }),
+ (0, None, {
+ 'name': 'move 2 counterpart line',
+ 'account_id': self.company_data['default_account_expense'].id,
+ 'debit': 2000.0,
+ 'credit': 0.0,
+ }),
+ ]
+ },
+ ])
+
+ # lines[0] = 'move 2 receivable line'
+ # lines[1] = 'move 1 counterpart line'
+ # lines[2] = 'move 1 receivable line'
+ # lines[3] = 'move 2 counterpart line'
+ draft_moves.action_post()
+ lines = draft_moves.mapped('line_ids').sorted('balance')
+
+ (lines[0] + lines[2]).reconcile()
+
+ # You can't write something impacting the reconciliation on an already reconciled line.
+ with self.assertRaises(UserError), self.cr.savepoint():
+ draft_moves[0].write({
+ 'line_ids': [
+ (1, lines[1].id, {'credit': lines[1].credit + 100.0}),
+ (1, lines[2].id, {'debit': lines[2].debit + 100.0}),
+ ]
+ })
+
+ # The write must not raise anything because the rounding of the monetary field should ignore such tiny amount.
+ draft_moves[0].write({
+ 'line_ids': [
+ (1, lines[1].id, {'credit': lines[1].credit + 0.0000001}),
+ (1, lines[2].id, {'debit': lines[2].debit + 0.0000001}),
+ ]
+ })
+
+ # You can't unlink an already reconciled line.
+ with self.assertRaises(UserError), self.cr.savepoint():
+ draft_moves.unlink()
+
+ def test_misc_always_balanced_move(self):
+ ''' Ensure there is no way to make '''
+ # You can't remove a journal item making the journal entry unbalanced.
+ with self.assertRaises(UserError), self.cr.savepoint():
+ self.test_move.line_ids[0].unlink()
+
+ # Same check using write instead of unlink.
+ with self.assertRaises(UserError), self.cr.savepoint():
+ balance = self.test_move.line_ids[0].balance + 5
+ self.test_move.line_ids[0].write({
+ 'debit': balance if balance > 0.0 else 0.0,
+ 'credit': -balance if balance < 0.0 else 0.0,
+ })
+
+ # You can remove journal items if the related journal entry is still balanced.
+ self.test_move.line_ids.unlink()
+
+ def test_add_followers_on_post(self):
+ # Add some existing partners, some from another company
+ company = self.env['res.company'].create({'name': 'Oopo'})
+ company.flush()
+ existing_partners = self.env['res.partner'].create([{
+ 'name': 'Jean',
+ 'company_id': company.id,
+ },{
+ 'name': 'Paulus',
+ }])
+ self.test_move.message_subscribe(existing_partners.ids)
+
+ user = new_test_user(self.env, login='jag', groups='account.group_account_invoice')
+
+ move = self.test_move.with_user(user)
+ partner = self.env['res.partner'].create({'name': 'Belouga'})
+ move.partner_id = partner
+
+ move.action_post()
+ self.assertEqual(move.message_partner_ids, self.env.user.partner_id | existing_partners | partner)
+
+ def test_misc_move_onchange(self):
+ ''' Test the behavior on onchanges for account.move having 'entry' as type. '''
+
+ move_form = Form(self.env['account.move'])
+ # Rate 1:3
+ move_form.date = fields.Date.from_string('2016-01-01')
+
+ # New line that should get 400.0 as debit.
+ with move_form.line_ids.new() as line_form:
+ line_form.name = 'debit_line'
+ line_form.account_id = self.company_data['default_account_revenue']
+ line_form.currency_id = self.currency_data['currency']
+ line_form.amount_currency = 1200.0
+
+ # New line that should get 400.0 as credit.
+ with move_form.line_ids.new() as line_form:
+ line_form.name = 'credit_line'
+ line_form.account_id = self.company_data['default_account_revenue']
+ line_form.currency_id = self.currency_data['currency']
+ line_form.amount_currency = -1200.0
+ move = move_form.save()
+
+ self.assertRecordValues(
+ move.line_ids.sorted('debit'),
+ [
+ {
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -1200.0,
+ 'debit': 0.0,
+ 'credit': 400.0,
+ },
+ {
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 1200.0,
+ 'debit': 400.0,
+ 'credit': 0.0,
+ },
+ ],
+ )
+
+ # === Change the date to change the currency conversion's rate ===
+
+ with Form(move) as move_form:
+ move_form.date = fields.Date.from_string('2017-01-01')
+
+ self.assertRecordValues(
+ move.line_ids.sorted('debit'),
+ [
+ {
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -1200.0,
+ 'debit': 0.0,
+ 'credit': 600.0,
+ },
+ {
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 1200.0,
+ 'debit': 600.0,
+ 'credit': 0.0,
+ },
+ ],
+ )
+
+ def test_included_tax(self):
+ '''
+ Test an account.move.line is created automatically when adding a tax.
+ This test uses the following scenario:
+ - Create manually a debit line of 1000 having an included tax.
+ - Assume a line containing the tax amount is created automatically.
+ - Create manually a credit line to balance the two previous lines.
+ - Save the move.
+
+ included tax = 20%
+
+ Name | Debit | Credit | Tax_ids | Tax_line_id's name
+ -----------------------|-----------|-----------|---------------|-------------------
+ debit_line_1 | 1000 | | tax |
+ included_tax_line | 200 | | | included_tax_line
+ credit_line_1 | | 1200 | |
+ '''
+
+ self.included_percent_tax = self.env['account.tax'].create({
+ 'name': 'included_tax_line',
+ 'amount_type': 'percent',
+ 'amount': 20,
+ 'price_include': True,
+ 'include_base_amount': False,
+ })
+ self.account = self.company_data['default_account_revenue']
+
+ move_form = Form(self.env['account.move'].with_context(default_move_type='entry'))
+
+ # Create a new account.move.line with debit amount.
+ with move_form.line_ids.new() as debit_line:
+ debit_line.name = 'debit_line_1'
+ debit_line.account_id = self.account
+ debit_line.debit = 1000
+ debit_line.tax_ids.clear()
+ debit_line.tax_ids.add(self.included_percent_tax)
+
+ self.assertTrue(debit_line.recompute_tax_line)
+
+ # Create a third account.move.line with credit amount.
+ with move_form.line_ids.new() as credit_line:
+ credit_line.name = 'credit_line_1'
+ credit_line.account_id = self.account
+ credit_line.credit = 1200
+
+ move = move_form.save()
+
+ self.assertRecordValues(move.line_ids, [
+ {'name': 'debit_line_1', 'debit': 1000.0, 'credit': 0.0, 'tax_ids': [self.included_percent_tax.id], 'tax_line_id': False},
+ {'name': 'included_tax_line', 'debit': 200.0, 'credit': 0.0, 'tax_ids': [], 'tax_line_id': self.included_percent_tax.id},
+ {'name': 'credit_line_1', 'debit': 0.0, 'credit': 1200.0, 'tax_ids': [], 'tax_line_id': False},
+ ])
+
+ def test_misc_prevent_unlink_posted_items(self):
+ # You cannot remove journal items if the related journal entry is posted.
+ self.test_move.action_post()
+ with self.assertRaises(UserError), self.cr.savepoint():
+ self.test_move.line_ids.unlink()
+
+ # You can remove journal items if the related journal entry is draft.
+ self.test_move.button_draft()
+ self.test_move.line_ids.unlink()
diff --git a/addons/account/tests/test_account_move_in_invoice.py b/addons/account/tests/test_account_move_in_invoice.py
new file mode 100644
index 00000000..4b43a431
--- /dev/null
+++ b/addons/account/tests/test_account_move_in_invoice.py
@@ -0,0 +1,1774 @@
+# -*- coding: utf-8 -*-
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests.common import Form
+from odoo.tests import tagged
+from odoo import fields
+from odoo.exceptions import UserError, ValidationError
+
+
+@tagged('post_install', '-at_install')
+class TestAccountMoveInInvoiceOnchanges(AccountTestInvoicingCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ cls.invoice = cls.init_invoice('in_invoice', products=cls.product_a+cls.product_b)
+
+ cls.product_line_vals_1 = {
+ 'name': cls.product_a.name,
+ 'product_id': cls.product_a.id,
+ 'account_id': cls.product_a.property_account_expense_id.id,
+ 'partner_id': cls.partner_a.id,
+ 'product_uom_id': cls.product_a.uom_id.id,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 800.0,
+ 'price_subtotal': 800.0,
+ 'price_total': 920.0,
+ 'tax_ids': cls.product_a.supplier_taxes_id.ids,
+ 'tax_line_id': False,
+ 'currency_id': cls.company_data['currency'].id,
+ 'amount_currency': 800.0,
+ 'debit': 800.0,
+ 'credit': 0.0,
+ 'date_maturity': False,
+ 'tax_exigible': True,
+ }
+ cls.product_line_vals_2 = {
+ 'name': cls.product_b.name,
+ 'product_id': cls.product_b.id,
+ 'account_id': cls.product_b.property_account_expense_id.id,
+ 'partner_id': cls.partner_a.id,
+ 'product_uom_id': cls.product_b.uom_id.id,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 160.0,
+ 'price_subtotal': 160.0,
+ 'price_total': 208.0,
+ 'tax_ids': cls.product_b.supplier_taxes_id.ids,
+ 'tax_line_id': False,
+ 'currency_id': cls.company_data['currency'].id,
+ 'amount_currency': 160.0,
+ 'debit': 160.0,
+ 'credit': 0.0,
+ 'date_maturity': False,
+ 'tax_exigible': True,
+ }
+ cls.tax_line_vals_1 = {
+ 'name': cls.tax_purchase_a.name,
+ 'product_id': False,
+ 'account_id': cls.company_data['default_account_tax_purchase'].id,
+ 'partner_id': cls.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 144.0,
+ 'price_subtotal': 144.0,
+ 'price_total': 144.0,
+ 'tax_ids': [],
+ 'tax_line_id': cls.tax_purchase_a.id,
+ 'currency_id': cls.company_data['currency'].id,
+ 'amount_currency': 144.0,
+ 'debit': 144.0,
+ 'credit': 0.0,
+ 'date_maturity': False,
+ 'tax_exigible': True,
+ }
+ cls.tax_line_vals_2 = {
+ 'name': cls.tax_purchase_b.name,
+ 'product_id': False,
+ 'account_id': cls.company_data['default_account_tax_purchase'].id,
+ 'partner_id': cls.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 24.0,
+ 'price_subtotal': 24.0,
+ 'price_total': 24.0,
+ 'tax_ids': [],
+ 'tax_line_id': cls.tax_purchase_b.id,
+ 'currency_id': cls.company_data['currency'].id,
+ 'amount_currency': 24.0,
+ 'debit': 24.0,
+ 'credit': 0.0,
+ 'date_maturity': False,
+ 'tax_exigible': True,
+ }
+ cls.term_line_vals_1 = {
+ 'name': '',
+ 'product_id': False,
+ 'account_id': cls.company_data['default_account_payable'].id,
+ 'partner_id': cls.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': -1128.0,
+ 'price_subtotal': -1128.0,
+ 'price_total': -1128.0,
+ 'tax_ids': [],
+ 'tax_line_id': False,
+ 'currency_id': cls.company_data['currency'].id,
+ 'amount_currency': -1128.0,
+ 'debit': 0.0,
+ 'credit': 1128.0,
+ 'date_maturity': fields.Date.from_string('2019-01-01'),
+ 'tax_exigible': True,
+ }
+ cls.move_vals = {
+ 'partner_id': cls.partner_a.id,
+ 'currency_id': cls.company_data['currency'].id,
+ 'journal_id': cls.company_data['default_journal_purchase'].id,
+ 'date': fields.Date.from_string('2019-01-01'),
+ 'fiscal_position_id': False,
+ 'payment_reference': '',
+ 'invoice_payment_term_id': cls.pay_terms_a.id,
+ 'amount_untaxed': 960.0,
+ 'amount_tax': 168.0,
+ 'amount_total': 1128.0,
+ }
+
+ def setUp(self):
+ super(TestAccountMoveInInvoiceOnchanges, self).setUp()
+ self.assertInvoiceValues(self.invoice, [
+ self.product_line_vals_1,
+ self.product_line_vals_2,
+ self.tax_line_vals_1,
+ self.tax_line_vals_2,
+ self.term_line_vals_1,
+ ], self.move_vals)
+
+ def test_in_invoice_onchange_invoice_date(self):
+ for tax_date, invoice_date, accounting_date in [
+ ('2019-03-31', '2019-05-12', '2019-05-31'),
+ ('2019-03-31', '2019-02-10', '2019-04-30'),
+ ('2019-05-31', '2019-06-15', '2019-06-30'),
+ ]:
+ self.invoice.company_id.tax_lock_date = tax_date
+ with Form(self.invoice) as move_form:
+ move_form.invoice_date = invoice_date
+ self.assertEqual(self.invoice.date, fields.Date.to_date(accounting_date))
+
+ def test_in_invoice_line_onchange_product_1(self):
+ move_form = Form(self.invoice)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ line_form.product_id = self.product_b
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'name': self.product_b.name,
+ 'product_id': self.product_b.id,
+ 'product_uom_id': self.product_b.uom_id.id,
+ 'account_id': self.product_b.property_account_expense_id.id,
+ 'price_unit': 160.0,
+ 'price_subtotal': 160.0,
+ 'price_total': 208.0,
+ 'tax_ids': self.product_b.supplier_taxes_id.ids,
+ 'amount_currency': 160.0,
+ 'debit': 160.0,
+ },
+ self.product_line_vals_2,
+ {
+ **self.tax_line_vals_1,
+ 'price_unit': 48.0,
+ 'price_subtotal': 48.0,
+ 'price_total': 48.0,
+ 'amount_currency': 48.0,
+ 'debit': 48.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'price_unit': 48.0,
+ 'price_subtotal': 48.0,
+ 'price_total': 48.0,
+ 'amount_currency': 48.0,
+ 'debit': 48.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'price_unit': -416.0,
+ 'price_subtotal': -416.0,
+ 'price_total': -416.0,
+ 'amount_currency': -416.0,
+ 'credit': 416.0,
+ },
+ ], {
+ **self.move_vals,
+ 'amount_untaxed': 320.0,
+ 'amount_tax': 96.0,
+ 'amount_total': 416.0,
+ })
+
+ def test_in_invoice_line_onchange_product_2_with_fiscal_pos(self):
+ ''' Test mapping a price-included tax (10%) with a price-excluded tax (20%) on a price_unit of 110.0.
+ The price_unit should be 100.0 after applying the fiscal position.
+ '''
+ tax_price_include = self.env['account.tax'].create({
+ 'name': '10% incl',
+ 'type_tax_use': 'purchase',
+ 'amount_type': 'percent',
+ 'amount': 10,
+ 'price_include': True,
+ 'include_base_amount': True,
+ })
+ tax_price_exclude = self.env['account.tax'].create({
+ 'name': '15% excl',
+ 'type_tax_use': 'purchase',
+ 'amount_type': 'percent',
+ 'amount': 15,
+ })
+
+ fiscal_position = self.env['account.fiscal.position'].create({
+ 'name': 'fiscal_pos_a',
+ 'tax_ids': [
+ (0, None, {
+ 'tax_src_id': tax_price_include.id,
+ 'tax_dest_id': tax_price_exclude.id,
+ }),
+ ],
+ })
+
+ product = self.env['product.product'].create({
+ 'name': 'product',
+ 'uom_id': self.env.ref('uom.product_uom_unit').id,
+ 'standard_price': 110.0,
+ 'supplier_taxes_id': [(6, 0, tax_price_include.ids)],
+ })
+
+ move_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice'))
+ move_form.partner_id = self.partner_a
+ move_form.invoice_date = fields.Date.from_string('2019-01-01')
+ move_form.currency_id = self.currency_data['currency']
+ move_form.fiscal_position_id = fiscal_position
+ with move_form.invoice_line_ids.new() as line_form:
+ line_form.product_id = product
+ invoice = move_form.save()
+
+ self.assertInvoiceValues(invoice, [
+ {
+ 'product_id': product.id,
+ 'price_unit': 200.0,
+ 'price_subtotal': 200.0,
+ 'price_total': 230.0,
+ 'tax_ids': tax_price_exclude.ids,
+ 'tax_line_id': False,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 200.0,
+ 'debit': 100.0,
+ 'credit': 0.0,
+ },
+ {
+ 'product_id': False,
+ 'price_unit': 30.0,
+ 'price_subtotal': 30.0,
+ 'price_total': 30.0,
+ 'tax_ids': [],
+ 'tax_line_id': tax_price_exclude.id,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 30.0,
+ 'debit': 15.0,
+ 'credit': 0.0,
+ },
+ {
+ 'product_id': False,
+ 'price_unit': -230.0,
+ 'price_subtotal': -230.0,
+ 'price_total': -230.0,
+ 'tax_ids': [],
+ 'tax_line_id': False,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -230.0,
+ 'debit': 0.0,
+ 'credit': 115.0,
+ },
+ ], {
+ 'currency_id': self.currency_data['currency'].id,
+ 'fiscal_position_id': fiscal_position.id,
+ 'amount_untaxed': 200.0,
+ 'amount_tax': 30.0,
+ 'amount_total': 230.0,
+ })
+
+ uom_dozen = self.env.ref('uom.product_uom_dozen')
+ with Form(invoice) as move_form:
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ line_form.product_uom_id = uom_dozen
+
+ self.assertInvoiceValues(invoice, [
+ {
+ 'product_id': product.id,
+ 'product_uom_id': uom_dozen.id,
+ 'price_unit': 2400.0,
+ 'price_subtotal': 2400.0,
+ 'price_total': 2760.0,
+ 'tax_ids': tax_price_exclude.ids,
+ 'tax_line_id': False,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 2400.0,
+ 'debit': 1200.0,
+ 'credit': 0.0,
+ },
+ {
+ 'product_id': False,
+ 'product_uom_id': False,
+ 'price_unit': 360.0,
+ 'price_subtotal': 360.0,
+ 'price_total': 360.0,
+ 'tax_ids': [],
+ 'tax_line_id': tax_price_exclude.id,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 360.0,
+ 'debit': 180.0,
+ 'credit': 0.0,
+ },
+ {
+ 'product_id': False,
+ 'product_uom_id': False,
+ 'price_unit': -2760.0,
+ 'price_subtotal': -2760.0,
+ 'price_total': -2760.0,
+ 'tax_ids': [],
+ 'tax_line_id': False,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -2760.0,
+ 'debit': 0.0,
+ 'credit': 1380.0,
+ },
+ ], {
+ 'currency_id': self.currency_data['currency'].id,
+ 'fiscal_position_id': fiscal_position.id,
+ 'amount_untaxed': 2400.0,
+ 'amount_tax': 360.0,
+ 'amount_total': 2760.0,
+ })
+
+ def test_in_invoice_line_onchange_product_2_with_fiscal_pos_2(self):
+ ''' Test mapping a price-included tax (10%) with another price-included tax (20%) on a price_unit of 110.0.
+ The price_unit should be 120.0 after applying the fiscal position.
+ '''
+ tax_price_include_1 = self.env['account.tax'].create({
+ 'name': '10% incl',
+ 'type_tax_use': 'purchase',
+ 'amount_type': 'percent',
+ 'amount': 10,
+ 'price_include': True,
+ 'include_base_amount': True,
+ })
+ tax_price_include_2 = self.env['account.tax'].create({
+ 'name': '20% incl',
+ 'type_tax_use': 'purchase',
+ 'amount_type': 'percent',
+ 'amount': 20,
+ 'price_include': True,
+ 'include_base_amount': True,
+ })
+
+ fiscal_position = self.env['account.fiscal.position'].create({
+ 'name': 'fiscal_pos_a',
+ 'tax_ids': [
+ (0, None, {
+ 'tax_src_id': tax_price_include_1.id,
+ 'tax_dest_id': tax_price_include_2.id,
+ }),
+ ],
+ })
+
+ product = self.env['product.product'].create({
+ 'name': 'product',
+ 'uom_id': self.env.ref('uom.product_uom_unit').id,
+ 'standard_price': 110.0,
+ 'supplier_taxes_id': [(6, 0, tax_price_include_1.ids)],
+ })
+
+ move_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice'))
+ move_form.partner_id = self.partner_a
+ move_form.invoice_date = fields.Date.from_string('2019-01-01')
+ move_form.currency_id = self.currency_data['currency']
+ move_form.fiscal_position_id = fiscal_position
+ with move_form.invoice_line_ids.new() as line_form:
+ line_form.product_id = product
+ invoice = move_form.save()
+
+ self.assertInvoiceValues(invoice, [
+ {
+ 'product_id': product.id,
+ 'price_unit': 240.0,
+ 'price_subtotal': 200.0,
+ 'price_total': 240.0,
+ 'tax_ids': tax_price_include_2.ids,
+ 'tax_line_id': False,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 200.0,
+ 'debit': 100.0,
+ 'credit': 0.0,
+ },
+ {
+ 'product_id': False,
+ 'price_unit': 40.0,
+ 'price_subtotal': 40.0,
+ 'price_total': 40.0,
+ 'tax_ids': [],
+ 'tax_line_id': tax_price_include_2.id,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 40.0,
+ 'debit': 20.0,
+ 'credit': 0.0,
+ },
+ {
+ 'product_id': False,
+ 'price_unit': -240.0,
+ 'price_subtotal': -240.0,
+ 'price_total': -240.0,
+ 'tax_ids': [],
+ 'tax_line_id': False,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -240.0,
+ 'debit': 0.0,
+ 'credit': 120.0,
+ },
+ ], {
+ 'currency_id': self.currency_data['currency'].id,
+ 'fiscal_position_id': fiscal_position.id,
+ 'amount_untaxed': 200.0,
+ 'amount_tax': 40.0,
+ 'amount_total': 240.0,
+ })
+
+ uom_dozen = self.env.ref('uom.product_uom_dozen')
+ with Form(invoice) as move_form:
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ line_form.product_uom_id = uom_dozen
+
+ self.assertInvoiceValues(invoice, [
+ {
+ 'product_id': product.id,
+ 'product_uom_id': uom_dozen.id,
+ 'price_unit': 2880.0,
+ 'price_subtotal': 2400.0,
+ 'price_total': 2880.0,
+ 'tax_ids': tax_price_include_2.ids,
+ 'tax_line_id': False,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 2400.0,
+ 'debit': 1200.0,
+ 'credit': 0.0,
+ },
+ {
+ 'product_id': False,
+ 'product_uom_id': False,
+ 'price_unit': 480.0,
+ 'price_subtotal': 480.0,
+ 'price_total': 480.0,
+ 'tax_ids': [],
+ 'tax_line_id': tax_price_include_2.id,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 480.0,
+ 'debit': 240.0,
+ 'credit': 0.0,
+ },
+ {
+ 'product_id': False,
+ 'product_uom_id': False,
+ 'price_unit': -2880.0,
+ 'price_subtotal': -2880.0,
+ 'price_total': -2880.0,
+ 'tax_ids': [],
+ 'tax_line_id': False,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -2880.0,
+ 'debit': 0.0,
+ 'credit': 1440.0,
+ },
+ ], {
+ 'currency_id': self.currency_data['currency'].id,
+ 'fiscal_position_id': fiscal_position.id,
+ 'amount_untaxed': 2400.0,
+ 'amount_tax': 480.0,
+ 'amount_total': 2880.0,
+ })
+
+ def test_in_invoice_line_onchange_business_fields_1(self):
+ move_form = Form(self.invoice)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ # Current price_unit is 800.
+ # We set quantity = 4, discount = 50%, price_unit = 400. The debit/credit fields don't change because (4 * 400) * 0.5 = 800.
+ line_form.quantity = 4
+ line_form.discount = 50
+ line_form.price_unit = 400
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'quantity': 4,
+ 'discount': 50.0,
+ 'price_unit': 400.0,
+ },
+ self.product_line_vals_2,
+ self.tax_line_vals_1,
+ self.tax_line_vals_2,
+ self.term_line_vals_1,
+ ], self.move_vals)
+
+ move_form = Form(self.invoice)
+ with move_form.line_ids.edit(2) as line_form:
+ # Reset field except the discount that becomes 100%.
+ # /!\ The modification is made on the accounting tab.
+ line_form.quantity = 1
+ line_form.discount = 100
+ line_form.price_unit = 800
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'discount': 100.0,
+ 'price_subtotal': 0.0,
+ 'price_total': 0.0,
+ 'amount_currency': 0.0,
+ 'debit': 0.0,
+ },
+ self.product_line_vals_2,
+ {
+ **self.tax_line_vals_1,
+ 'price_unit': 24.0,
+ 'price_subtotal': 24.0,
+ 'price_total': 24.0,
+ 'amount_currency': 24.0,
+ 'debit': 24.0,
+ },
+ self.tax_line_vals_2,
+ {
+ **self.term_line_vals_1,
+ 'price_unit': -208.0,
+ 'price_subtotal': -208.0,
+ 'price_total': -208.0,
+ 'amount_currency': -208.0,
+ 'credit': 208.0,
+ },
+ ], {
+ **self.move_vals,
+ 'amount_untaxed': 160.0,
+ 'amount_tax': 48.0,
+ 'amount_total': 208.0,
+ })
+
+ def test_in_invoice_line_onchange_accounting_fields_1(self):
+ move_form = Form(self.invoice)
+ with move_form.line_ids.edit(2) as line_form:
+ # Custom debit on the first product line.
+ line_form.debit = 3000
+ with move_form.line_ids.edit(3) as line_form:
+ # Custom credit on the second product line. Credit should be reset by onchange.
+ # /!\ It's a negative line.
+ line_form.credit = 500
+ with move_form.line_ids.edit(0) as line_form:
+ # Custom debit on the first tax line.
+ line_form.debit = 800
+ with move_form.line_ids.edit(4) as line_form:
+ # Custom debit on the second tax line.
+ line_form.debit = 250
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'price_unit': 3000.0,
+ 'price_subtotal': 3000.0,
+ 'price_total': 3450.0,
+ 'amount_currency': 3000.0,
+ 'debit': 3000.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'price_unit': -500.0,
+ 'price_subtotal': -500.0,
+ 'price_total': -650.0,
+ 'amount_currency': -500.0,
+ 'debit': 0.0,
+ 'credit': 500.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'price_unit': 800.0,
+ 'price_subtotal': 800.0,
+ 'price_total': 800.0,
+ 'amount_currency': 800.0,
+ 'debit': 800.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'price_unit': 250.0,
+ 'price_subtotal': 250.0,
+ 'price_total': 250.0,
+ 'amount_currency': 250.0,
+ 'debit': 250.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'price_unit': -3550.0,
+ 'price_subtotal': -3550.0,
+ 'price_total': -3550.0,
+ 'amount_currency': -3550.0,
+ 'credit': 3550.0,
+ },
+ ], {
+ **self.move_vals,
+ 'amount_untaxed': 2500.0,
+ 'amount_tax': 1050.0,
+ 'amount_total': 3550.0,
+ })
+
+ def test_in_invoice_line_onchange_partner_1(self):
+ move_form = Form(self.invoice)
+ move_form.partner_id = self.partner_b
+ move_form.payment_reference = 'turlututu'
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'partner_id': self.partner_b.id,
+ },
+ {
+ **self.product_line_vals_2,
+ 'partner_id': self.partner_b.id,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'partner_id': self.partner_b.id,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'partner_id': self.partner_b.id,
+ },
+ {
+ **self.term_line_vals_1,
+ 'name': 'turlututu',
+ 'partner_id': self.partner_b.id,
+ 'account_id': self.partner_b.property_account_payable_id.id,
+ 'price_unit': -789.6,
+ 'price_subtotal': -789.6,
+ 'price_total': -789.6,
+ 'amount_currency': -789.6,
+ 'credit': 789.6,
+ 'date_maturity': fields.Date.from_string('2019-02-28'),
+ },
+ {
+ **self.term_line_vals_1,
+ 'name': 'turlututu',
+ 'partner_id': self.partner_b.id,
+ 'account_id': self.partner_b.property_account_payable_id.id,
+ 'price_unit': -338.4,
+ 'price_subtotal': -338.4,
+ 'price_total': -338.4,
+ 'amount_currency': -338.4,
+ 'credit': 338.4,
+ },
+ ], {
+ **self.move_vals,
+ 'partner_id': self.partner_b.id,
+ 'payment_reference': 'turlututu',
+ 'fiscal_position_id': self.fiscal_pos_a.id,
+ 'invoice_payment_term_id': self.pay_terms_b.id,
+ 'amount_untaxed': 960.0,
+ 'amount_tax': 168.0,
+ 'amount_total': 1128.0,
+ })
+
+ # Remove lines and recreate them to apply the fiscal position.
+ move_form = Form(self.invoice)
+ move_form.invoice_line_ids.remove(0)
+ move_form.invoice_line_ids.remove(0)
+ with move_form.invoice_line_ids.new() as line_form:
+ line_form.product_id = self.product_a
+ with move_form.invoice_line_ids.new() as line_form:
+ line_form.product_id = self.product_b
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'account_id': self.product_b.property_account_expense_id.id,
+ 'partner_id': self.partner_b.id,
+ 'tax_ids': self.tax_purchase_b.ids,
+ },
+ {
+ **self.product_line_vals_2,
+ 'partner_id': self.partner_b.id,
+ 'price_total': 184.0,
+ 'tax_ids': self.tax_purchase_b.ids,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'name': self.tax_purchase_b.name,
+ 'partner_id': self.partner_b.id,
+ 'tax_line_id': self.tax_purchase_b.id,
+ },
+ {
+ **self.term_line_vals_1,
+ 'name': 'turlututu',
+ 'account_id': self.partner_b.property_account_payable_id.id,
+ 'partner_id': self.partner_b.id,
+ 'price_unit': -772.8,
+ 'price_subtotal': -772.8,
+ 'price_total': -772.8,
+ 'amount_currency': -772.8,
+ 'credit': 772.8,
+ 'date_maturity': fields.Date.from_string('2019-02-28'),
+ },
+ {
+ **self.term_line_vals_1,
+ 'name': 'turlututu',
+ 'account_id': self.partner_b.property_account_payable_id.id,
+ 'partner_id': self.partner_b.id,
+ 'price_unit': -331.2,
+ 'price_subtotal': -331.2,
+ 'price_total': -331.2,
+ 'amount_currency': -331.2,
+ 'credit': 331.2,
+ },
+ ], {
+ **self.move_vals,
+ 'partner_id': self.partner_b.id,
+ 'payment_reference': 'turlututu',
+ 'fiscal_position_id': self.fiscal_pos_a.id,
+ 'invoice_payment_term_id': self.pay_terms_b.id,
+ 'amount_untaxed': 960.0,
+ 'amount_tax': 144.0,
+ 'amount_total': 1104.0,
+ })
+
+ def test_in_invoice_line_onchange_taxes_1(self):
+ move_form = Form(self.invoice)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ line_form.price_unit = 960
+ line_form.tax_ids.add(self.tax_armageddon)
+ move_form.save()
+
+ child_tax_1 = self.tax_armageddon.children_tax_ids[0]
+ child_tax_2 = self.tax_armageddon.children_tax_ids[1]
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'price_unit': 960.0,
+ 'price_subtotal': 800.0,
+ 'price_total': 1176.0,
+ 'tax_ids': (self.tax_purchase_a + self.tax_armageddon).ids,
+ 'tax_exigible': False,
+ },
+ self.product_line_vals_2,
+ self.tax_line_vals_1,
+ self.tax_line_vals_2,
+ {
+ 'name': child_tax_1.name,
+ 'product_id': False,
+ 'account_id': self.company_data['default_account_tax_sale'].id,
+ 'partner_id': self.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 64.0,
+ 'price_subtotal': 64.0,
+ 'price_total': 70.4,
+ 'tax_ids': child_tax_2.ids,
+ 'tax_line_id': child_tax_1.id,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': 64.0,
+ 'debit': 64.0,
+ 'credit': 0.0,
+ 'date_maturity': False,
+ 'tax_exigible': False,
+ },
+ {
+ 'name': child_tax_1.name,
+ 'product_id': False,
+ 'account_id': self.company_data['default_account_expense'].id,
+ 'partner_id': self.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 96.0,
+ 'price_subtotal': 96.0,
+ 'price_total': 105.6,
+ 'tax_ids': child_tax_2.ids,
+ 'tax_line_id': child_tax_1.id,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': 96.0,
+ 'debit': 96.0,
+ 'credit': 0.0,
+ 'date_maturity': False,
+ 'tax_exigible': False,
+ },
+ {
+ 'name': child_tax_2.name,
+ 'product_id': False,
+ 'account_id': child_tax_2.cash_basis_transition_account_id.id,
+ 'partner_id': self.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 96.0,
+ 'price_subtotal': 96.0,
+ 'price_total': 96.0,
+ 'tax_ids': [],
+ 'tax_line_id': child_tax_2.id,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': 96.0,
+ 'debit': 96.0,
+ 'credit': 0.0,
+ 'date_maturity': False,
+ 'tax_exigible': False,
+ },
+ {
+ **self.term_line_vals_1,
+ 'price_unit': -1384.0,
+ 'price_subtotal': -1384.0,
+ 'price_total': -1384.0,
+ 'amount_currency': -1384.0,
+ 'credit': 1384.0,
+ },
+ ], {
+ **self.move_vals,
+ 'amount_untaxed': 960.0,
+ 'amount_tax': 424.0,
+ 'amount_total': 1384.0,
+ })
+
+ def test_in_invoice_line_onchange_cash_rounding_1(self):
+ move_form = Form(self.invoice)
+ # Add a cash rounding having 'add_invoice_line'.
+ move_form.invoice_cash_rounding_id = self.cash_rounding_a
+ move_form.save()
+
+ # The cash rounding does nothing as the total is already rounded.
+ self.assertInvoiceValues(self.invoice, [
+ self.product_line_vals_1,
+ self.product_line_vals_2,
+ self.tax_line_vals_1,
+ self.tax_line_vals_2,
+ self.term_line_vals_1,
+ ], self.move_vals)
+
+ move_form = Form(self.invoice)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ line_form.price_unit = 799.99
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ 'name': 'add_invoice_line',
+ 'product_id': False,
+ 'account_id': self.cash_rounding_a.loss_account_id.id,
+ 'partner_id': self.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 0.01,
+ 'price_subtotal': 0.01,
+ 'price_total': 0.01,
+ 'tax_ids': [],
+ 'tax_line_id': False,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': 0.01,
+ 'debit': 0.01,
+ 'credit': 0.0,
+ 'date_maturity': False,
+ 'tax_exigible': True,
+ },
+ {
+ **self.product_line_vals_1,
+ 'price_unit': 799.99,
+ 'price_subtotal': 799.99,
+ 'price_total': 919.99,
+ 'amount_currency': 799.99,
+ 'debit': 799.99,
+ },
+ self.product_line_vals_2,
+ self.tax_line_vals_1,
+ self.tax_line_vals_2,
+ self.term_line_vals_1,
+ ], self.move_vals)
+
+ move_form = Form(self.invoice)
+ # Change the cash rounding to one having 'biggest_tax'.
+ move_form.invoice_cash_rounding_id = self.cash_rounding_b
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'price_unit': 799.99,
+ 'price_subtotal': 799.99,
+ 'price_total': 919.99,
+ 'amount_currency': 799.99,
+ 'debit': 799.99,
+ },
+ self.product_line_vals_2,
+ self.tax_line_vals_1,
+ self.tax_line_vals_2,
+ {
+ 'name': '%s (rounding)' % self.tax_purchase_a.name,
+ 'product_id': False,
+ 'account_id': self.company_data['default_account_tax_purchase'].id,
+ 'partner_id': self.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': -0.04,
+ 'price_subtotal': -0.04,
+ 'price_total': -0.04,
+ 'tax_ids': [],
+ 'tax_line_id': self.tax_purchase_a.id,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': -0.04,
+ 'debit': 0.0,
+ 'credit': 0.04,
+ 'date_maturity': False,
+ 'tax_exigible': True,
+ },
+ {
+ **self.term_line_vals_1,
+ 'price_unit': -1127.95,
+ 'price_subtotal': -1127.95,
+ 'price_total': -1127.95,
+ 'amount_currency': -1127.95,
+ 'credit': 1127.95,
+ },
+ ], {
+ **self.move_vals,
+ 'amount_untaxed': 959.99,
+ 'amount_tax': 167.96,
+ 'amount_total': 1127.95,
+ })
+
+ def test_in_invoice_line_onchange_currency_1(self):
+ move_form = Form(self.invoice)
+ move_form.currency_id = self.currency_data['currency']
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 800.0,
+ 'debit': 400.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 160.0,
+ 'debit': 80.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 144.0,
+ 'debit': 72.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 24.0,
+ 'debit': 12.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -1128.0,
+ 'credit': 564.0,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ })
+
+ move_form = Form(self.invoice)
+ # Change the date to get another rate: 1/3 instead of 1/2.
+ move_form.date = fields.Date.from_string('2016-01-01')
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 800.0,
+ 'debit': 266.67,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 160.0,
+ 'debit': 53.33,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 144.0,
+ 'debit': 48.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 24.0,
+ 'debit': 8.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -1128.0,
+ 'credit': 376.0,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ 'date': fields.Date.from_string('2016-01-01'),
+ })
+
+ move_form = Form(self.invoice)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ # 0.045 * 0.1 = 0.0045. As the foreign currency has a 0.001 rounding,
+ # the result should be 0.005 after rounding.
+ line_form.quantity = 0.1
+ line_form.price_unit = 0.045
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'quantity': 0.1,
+ 'price_unit': 0.05,
+ 'price_subtotal': 0.005,
+ 'price_total': 0.006,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 0.005,
+ 'debit': 0.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 160.0,
+ 'debit': 53.33,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'price_unit': 24.0,
+ 'price_subtotal': 24.001,
+ 'price_total': 24.001,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 24.001,
+ 'debit': 8.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 24.0,
+ 'debit': 8.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'price_unit': -208.01,
+ 'price_subtotal': -208.006,
+ 'price_total': -208.006,
+ 'amount_currency': -208.006,
+ 'credit': 69.33,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ 'date': fields.Date.from_string('2016-01-01'),
+ 'amount_untaxed': 160.005,
+ 'amount_tax': 48.001,
+ 'amount_total': 208.006,
+ })
+
+ # Exit the multi-currencies.
+ move_form = Form(self.invoice)
+ move_form.currency_id = self.company_data['currency']
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'quantity': 0.1,
+ 'price_unit': 0.05,
+ 'price_subtotal': 0.01,
+ 'price_total': 0.01,
+ 'amount_currency': 0.01,
+ 'debit': 0.01,
+ },
+ self.product_line_vals_2,
+ {
+ **self.tax_line_vals_1,
+ 'price_unit': 24.0,
+ 'price_subtotal': 24.0,
+ 'price_total': 24.0,
+ 'amount_currency': 24.0,
+ 'debit': 24.0,
+ },
+ self.tax_line_vals_2,
+ {
+ **self.term_line_vals_1,
+ 'price_unit': -208.01,
+ 'price_subtotal': -208.01,
+ 'price_total': -208.01,
+ 'amount_currency': -208.01,
+ 'credit': 208.01,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.company_data['currency'].id,
+ 'date': fields.Date.from_string('2016-01-01'),
+ 'amount_untaxed': 160.01,
+ 'amount_tax': 48.0,
+ 'amount_total': 208.01,
+ })
+
+ def test_in_invoice_onchange_past_invoice_1(self):
+ copy_invoice = self.invoice.copy()
+
+ move_form = Form(self.invoice)
+ move_form.invoice_line_ids.remove(0)
+ move_form.invoice_line_ids.remove(0)
+ move_form.invoice_vendor_bill_id = copy_invoice
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ self.product_line_vals_1,
+ self.product_line_vals_2,
+ self.tax_line_vals_1,
+ self.tax_line_vals_2,
+ self.term_line_vals_1,
+ ], self.move_vals)
+
+ def test_in_invoice_create_refund(self):
+ self.invoice.action_post()
+
+ move_reversal = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=self.invoice.ids).create({
+ 'date': fields.Date.from_string('2019-02-01'),
+ 'reason': 'no reason',
+ 'refund_method': 'refund',
+ })
+ reversal = move_reversal.reverse_moves()
+ reverse_move = self.env['account.move'].browse(reversal['res_id'])
+
+ self.assertEqual(self.invoice.payment_state, 'not_paid', "Refunding with a draft credit note should keep the invoice 'not_paid'.")
+ self.assertInvoiceValues(reverse_move, [
+ {
+ **self.product_line_vals_1,
+ 'amount_currency': -800.0,
+ 'debit': 0.0,
+ 'credit': 800.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'amount_currency': -160.0,
+ 'debit': 0.0,
+ 'credit': 160.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'amount_currency': -144.0,
+ 'debit': 0.0,
+ 'credit': 144.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'amount_currency': -24.0,
+ 'debit': 0.0,
+ 'credit': 24.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'name': '',
+ 'amount_currency': 1128.0,
+ 'debit': 1128.0,
+ 'credit': 0.0,
+ 'date_maturity': move_reversal.date,
+ },
+ ], {
+ **self.move_vals,
+ 'invoice_payment_term_id': None,
+ 'date': move_reversal.date,
+ 'state': 'draft',
+ 'ref': 'Reversal of: %s, %s' % (self.invoice.name, move_reversal.reason),
+ 'payment_state': 'not_paid',
+ })
+
+ move_reversal = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=self.invoice.ids).create({
+ 'date': fields.Date.from_string('2019-02-01'),
+ 'reason': 'no reason again',
+ 'refund_method': 'cancel',
+ })
+ reversal = move_reversal.reverse_moves()
+ reverse_move = self.env['account.move'].browse(reversal['res_id'])
+
+ self.assertEqual(self.invoice.payment_state, 'reversed', "After cancelling it with a reverse invoice, an invoice should be in 'reversed' state.")
+ self.assertInvoiceValues(reverse_move, [
+ {
+ **self.product_line_vals_1,
+ 'amount_currency': -800.0,
+ 'debit': 0.0,
+ 'credit': 800.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'amount_currency': -160.0,
+ 'debit': 0.0,
+ 'credit': 160.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'amount_currency': -144.0,
+ 'debit': 0.0,
+ 'credit': 144.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'amount_currency': -24.0,
+ 'debit': 0.0,
+ 'credit': 24.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'name': '',
+ 'amount_currency': 1128.0,
+ 'debit': 1128.0,
+ 'credit': 0.0,
+ 'date_maturity': move_reversal.date,
+ },
+ ], {
+ **self.move_vals,
+ 'invoice_payment_term_id': None,
+ 'date': move_reversal.date,
+ 'state': 'posted',
+ 'ref': 'Reversal of: %s, %s' % (self.invoice.name, move_reversal.reason),
+ 'payment_state': 'paid',
+ })
+
+ def test_in_invoice_create_refund_multi_currency(self):
+ ''' Test the account.move.reversal takes care about the currency rates when setting
+ a custom reversal date.
+ '''
+ move_form = Form(self.invoice)
+ move_form.date = '2016-01-01'
+ move_form.currency_id = self.currency_data['currency']
+ move_form.save()
+
+ self.invoice.action_post()
+
+ # The currency rate changed from 1/3 to 1/2.
+ move_reversal = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=self.invoice.ids).create({
+ 'date': fields.Date.from_string('2017-01-01'),
+ 'reason': 'no reason',
+ 'refund_method': 'refund',
+ })
+ reversal = move_reversal.reverse_moves()
+ reverse_move = self.env['account.move'].browse(reversal['res_id'])
+
+ self.assertEqual(self.invoice.payment_state, 'not_paid', "Refunding with a draft credit note should keep the invoice 'not_paid'.")
+ self.assertInvoiceValues(reverse_move, [
+ {
+ **self.product_line_vals_1,
+ 'amount_currency': -800.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'debit': 0.0,
+ 'credit': 400.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'amount_currency': -160.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'debit': 0.0,
+ 'credit': 80.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'amount_currency': -144.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'debit': 0.0,
+ 'credit': 72.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'amount_currency': -24.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'debit': 0.0,
+ 'credit': 12.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'name': '',
+ 'amount_currency': 1128.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'debit': 564.0,
+ 'credit': 0.0,
+ 'date_maturity': move_reversal.date,
+ },
+ ], {
+ **self.move_vals,
+ 'invoice_payment_term_id': None,
+ 'currency_id': self.currency_data['currency'].id,
+ 'date': move_reversal.date,
+ 'state': 'draft',
+ 'ref': 'Reversal of: %s, %s' % (self.invoice.name, move_reversal.reason),
+ 'payment_state': 'not_paid',
+ })
+
+ move_reversal = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=self.invoice.ids).create({
+ 'date': fields.Date.from_string('2017-01-01'),
+ 'reason': 'no reason again',
+ 'refund_method': 'cancel',
+ })
+ reversal = move_reversal.reverse_moves()
+ reverse_move = self.env['account.move'].browse(reversal['res_id'])
+
+ self.assertEqual(self.invoice.payment_state, 'reversed', "After cancelling it with a reverse invoice, an invoice should be in 'reversed' state.")
+ self.assertInvoiceValues(reverse_move, [
+ {
+ **self.product_line_vals_1,
+ 'amount_currency': -800.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'debit': 0.0,
+ 'credit': 400.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'amount_currency': -160.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'debit': 0.0,
+ 'credit': 80.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'amount_currency': -144.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'debit': 0.0,
+ 'credit': 72.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'amount_currency': -24.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'debit': 0.0,
+ 'credit': 12.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'name': '',
+ 'amount_currency': 1128.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'debit': 564.0,
+ 'credit': 0.0,
+ 'date_maturity': move_reversal.date,
+ },
+ ], {
+ **self.move_vals,
+ 'invoice_payment_term_id': None,
+ 'currency_id': self.currency_data['currency'].id,
+ 'date': move_reversal.date,
+ 'state': 'posted',
+ 'ref': 'Reversal of: %s, %s' % (self.invoice.name, move_reversal.reason),
+ 'payment_state': 'paid',
+ })
+
+ def test_in_invoice_create_1(self):
+ # Test creating an account_move with the least information.
+ move = self.env['account.move'].create({
+ 'move_type': 'in_invoice',
+ 'partner_id': self.partner_a.id,
+ 'invoice_date': fields.Date.from_string('2019-01-01'),
+ 'currency_id': self.currency_data['currency'].id,
+ 'invoice_payment_term_id': self.pay_terms_a.id,
+ 'invoice_line_ids': [
+ (0, None, self.product_line_vals_1),
+ (0, None, self.product_line_vals_2),
+ ]
+ })
+
+ self.assertInvoiceValues(move, [
+ {
+ **self.product_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 800.0,
+ 'debit': 400.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 160.0,
+ 'debit': 80.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 144.0,
+ 'debit': 72.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 24.0,
+ 'debit': 12.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -1128.0,
+ 'credit': 564.0,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ })
+
+ def test_in_invoice_write_1(self):
+ # Test creating an account_move with the least information.
+ move = self.env['account.move'].create({
+ 'move_type': 'in_invoice',
+ 'partner_id': self.partner_a.id,
+ 'invoice_date': fields.Date.from_string('2019-01-01'),
+ 'currency_id': self.currency_data['currency'].id,
+ 'invoice_payment_term_id': self.pay_terms_a.id,
+ 'invoice_line_ids': [
+ (0, None, self.product_line_vals_1),
+ ]
+ })
+ move.write({
+ 'invoice_line_ids': [
+ (0, None, self.product_line_vals_2),
+ ]
+ })
+
+ self.assertInvoiceValues(move, [
+ {
+ **self.product_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 800.0,
+ 'debit': 400.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 160.0,
+ 'debit': 80.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 144.0,
+ 'debit': 72.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 24.0,
+ 'debit': 12.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -1128.0,
+ 'credit': 564.0,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ })
+
+ def test_in_invoice_duplicate_supplier_reference(self):
+ ''' Ensure two vendor bills can't share the same vendor reference. '''
+ self.invoice.ref = 'a supplier reference'
+ invoice2 = self.invoice.copy(default={'invoice_date': self.invoice.invoice_date})
+
+ with self.assertRaises(ValidationError):
+ invoice2.ref = 'a supplier reference'
+
+ def test_in_invoice_switch_in_refund_1(self):
+ # Test creating an account_move with an in_invoice_type and switch it in an in_refund.
+ move = self.env['account.move'].create({
+ 'move_type': 'in_invoice',
+ 'partner_id': self.partner_a.id,
+ 'invoice_date': fields.Date.from_string('2019-01-01'),
+ 'currency_id': self.currency_data['currency'].id,
+ 'invoice_payment_term_id': self.pay_terms_a.id,
+ 'invoice_line_ids': [
+ (0, None, self.product_line_vals_1),
+ (0, None, self.product_line_vals_2),
+ ]
+ })
+ move.action_switch_invoice_into_refund_credit_note()
+
+ self.assertRecordValues(move, [{'move_type': 'in_refund'}])
+ self.assertInvoiceValues(move, [
+ {
+ **self.product_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -800.0,
+ 'credit': 400.0,
+ 'debit': 0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -160.0,
+ 'credit': 80.0,
+ 'debit': 0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -144.0,
+ 'credit': 72.0,
+ 'debit': 0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -24.0,
+ 'credit': 12.0,
+ 'debit': 0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 1128.0,
+ 'debit': 564.0,
+ 'credit': 0,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ })
+
+ def test_in_invoice_switch_in_refund_2(self):
+ # Test creating an account_move with an in_invoice_type and switch it in an in_refund and a negative quantity.
+ modified_product_line_vals_1 = self.product_line_vals_1.copy()
+ modified_product_line_vals_1.update({'quantity': -modified_product_line_vals_1['quantity']})
+ modified_product_line_vals_2 = self.product_line_vals_2.copy()
+ modified_product_line_vals_2.update({'quantity': -modified_product_line_vals_2['quantity']})
+ move = self.env['account.move'].create({
+ 'move_type': 'in_invoice',
+ 'partner_id': self.partner_a.id,
+ 'invoice_date': fields.Date.from_string('2019-01-01'),
+ 'currency_id': self.currency_data['currency'].id,
+ 'invoice_payment_term_id': self.pay_terms_a.id,
+ 'invoice_line_ids': [
+ (0, None, modified_product_line_vals_1),
+ (0, None, modified_product_line_vals_2),
+ ]
+ })
+
+ self.assertInvoiceValues(move, [
+ {
+ **self.product_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -800.0,
+ 'price_subtotal': -800.0,
+ 'price_total': -920.0,
+ 'credit': 400.0,
+ 'debit': 0,
+ 'quantity': -1.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -160.0,
+ 'price_subtotal': -160.0,
+ 'price_total': -208.0,
+ 'credit': 80.0,
+ 'debit': 0,
+ 'quantity': -1.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -144.0,
+ 'price_subtotal': -144.0,
+ 'price_total': -144.0,
+ 'price_unit': -144.0,
+ 'credit': 72.0,
+ 'debit': 0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -24.0,
+ 'price_subtotal': -24.0,
+ 'price_total': -24.0,
+ 'price_unit': -24.0,
+ 'credit': 12.0,
+ 'debit': 0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 1128.0,
+ 'price_subtotal': 1128.0,
+ 'price_total': 1128.0,
+ 'price_unit': 1128.0,
+ 'debit': 564.0,
+ 'credit': 0,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_tax' : -self.move_vals['amount_tax'],
+ 'amount_total' : -self.move_vals['amount_total'],
+ 'amount_untaxed' : -self.move_vals['amount_untaxed'],
+ })
+ move.action_switch_invoice_into_refund_credit_note()
+
+ self.assertRecordValues(move, [{'move_type': 'in_refund'}])
+ self.assertInvoiceValues(move, [
+ {
+ **self.product_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -800.0,
+ 'credit': 400.0,
+ 'debit': 0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -160.0,
+ 'credit': 80.0,
+ 'debit': 0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -144.0,
+ 'credit': 72.0,
+ 'debit': 0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -24.0,
+ 'credit': 12.0,
+ 'debit': 0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 1128.0,
+ 'debit': 564.0,
+ 'credit': 0,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_tax' : self.move_vals['amount_tax'],
+ 'amount_total' : self.move_vals['amount_total'],
+ 'amount_untaxed' : self.move_vals['amount_untaxed'],
+ })
+
+ def test_in_invoice_change_period_accrual_1(self):
+ move = self.env['account.move'].create({
+ 'move_type': 'in_invoice',
+ 'date': '2017-01-01',
+ 'partner_id': self.partner_a.id,
+ 'invoice_date': fields.Date.from_string('2017-01-01'),
+ 'currency_id': self.currency_data['currency'].id,
+ 'invoice_payment_term_id': self.pay_terms_a.id,
+ 'invoice_line_ids': [
+ (0, None, {
+ 'name': self.product_line_vals_1['name'],
+ 'product_id': self.product_line_vals_1['product_id'],
+ 'product_uom_id': self.product_line_vals_1['product_uom_id'],
+ 'quantity': self.product_line_vals_1['quantity'],
+ 'price_unit': self.product_line_vals_1['price_unit'],
+ 'tax_ids': self.product_line_vals_1['tax_ids'],
+ }),
+ (0, None, {
+ 'name': self.product_line_vals_2['name'],
+ 'product_id': self.product_line_vals_2['product_id'],
+ 'product_uom_id': self.product_line_vals_2['product_uom_id'],
+ 'quantity': self.product_line_vals_2['quantity'],
+ 'price_unit': self.product_line_vals_2['price_unit'],
+ 'tax_ids': self.product_line_vals_2['tax_ids'],
+ }),
+ ]
+ })
+ move.action_post()
+
+ wizard = self.env['account.automatic.entry.wizard']\
+ .with_context(active_model='account.move.line', active_ids=move.invoice_line_ids.ids).create({
+ 'action': 'change_period',
+ 'date': '2018-01-01',
+ 'percentage': 60,
+ 'journal_id': self.company_data['default_journal_misc'].id,
+ 'expense_accrual_account': self.env['account.account'].create({
+ 'name': 'Accrual Expense Account',
+ 'code': '234567',
+ 'user_type_id': self.env.ref('account.data_account_type_expenses').id,
+ 'reconcile': True,
+ }).id,
+ 'revenue_accrual_account': self.env['account.account'].create({
+ 'name': 'Accrual Revenue Account',
+ 'code': '765432',
+ 'user_type_id': self.env.ref('account.data_account_type_expenses').id,
+ 'reconcile': True,
+ }).id,
+ })
+ wizard_res = wizard.do_action()
+
+ self.assertInvoiceValues(move, [
+ {
+ **self.product_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 800.0,
+ 'debit': 400.0,
+ 'credit': 0.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 160.0,
+ 'debit': 80.0,
+ 'credit': 0.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 144.0,
+ 'debit': 72.0,
+ 'credit': 0.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 24.0,
+ 'debit': 12.0,
+ 'credit': 0.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -1128.0,
+ 'debit': 0.0,
+ 'credit': 564.0,
+ 'date_maturity': fields.Date.from_string('2017-01-01'),
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ 'date': fields.Date.from_string('2017-01-01'),
+ })
+
+ accrual_lines = self.env['account.move'].browse(wizard_res['domain'][0][2]).line_ids.sorted('date')
+ self.assertRecordValues(accrual_lines, [
+ {'amount_currency': -480.0, 'debit': 0.0, 'credit': 240.0, 'account_id': self.product_line_vals_1['account_id'], 'reconciled': False},
+ {'amount_currency': 480.0, 'debit': 240.0, 'credit': 0.0, 'account_id': wizard.expense_accrual_account.id, 'reconciled': True},
+ {'amount_currency': -96.0, 'debit': 0.0, 'credit': 48.0, 'account_id': self.product_line_vals_2['account_id'], 'reconciled': False},
+ {'amount_currency': 96.0, 'debit': 48.0, 'credit': 0.0, 'account_id': wizard.expense_accrual_account.id, 'reconciled': True},
+ {'amount_currency': 480.0, 'debit': 240.0, 'credit': 0.0, 'account_id': self.product_line_vals_1['account_id'], 'reconciled': False},
+ {'amount_currency': -480.0, 'debit': 0.0, 'credit': 240.0, 'account_id': wizard.expense_accrual_account.id, 'reconciled': True},
+ {'amount_currency': 96.0, 'debit': 48.0, 'credit': 0.0, 'account_id': self.product_line_vals_2['account_id'], 'reconciled': False},
+ {'amount_currency': -96.0, 'debit': 0.0, 'credit': 48.0, 'account_id': wizard.expense_accrual_account.id, 'reconciled': True},
+ ])
diff --git a/addons/account/tests/test_account_move_in_refund.py b/addons/account/tests/test_account_move_in_refund.py
new file mode 100644
index 00000000..593cdde3
--- /dev/null
+++ b/addons/account/tests/test_account_move_in_refund.py
@@ -0,0 +1,950 @@
+# -*- coding: utf-8 -*-
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests.common import Form
+from odoo.tests import tagged
+from odoo import fields
+
+
+@tagged('post_install', '-at_install')
+class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ cls.invoice = cls.init_invoice('in_refund', products=cls.product_a+cls.product_b)
+
+ cls.product_line_vals_1 = {
+ 'name': cls.product_a.name,
+ 'product_id': cls.product_a.id,
+ 'account_id': cls.product_a.property_account_expense_id.id,
+ 'partner_id': cls.partner_a.id,
+ 'product_uom_id': cls.product_a.uom_id.id,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 800.0,
+ 'price_subtotal': 800.0,
+ 'price_total': 920.0,
+ 'tax_ids': cls.product_a.supplier_taxes_id.ids,
+ 'tax_line_id': False,
+ 'currency_id': cls.company_data['currency'].id,
+ 'amount_currency': -800.0,
+ 'debit': 0.0,
+ 'credit': 800.0,
+ 'date_maturity': False,
+ 'tax_exigible': True,
+ }
+ cls.product_line_vals_2 = {
+ 'name': cls.product_b.name,
+ 'product_id': cls.product_b.id,
+ 'account_id': cls.product_b.property_account_expense_id.id,
+ 'partner_id': cls.partner_a.id,
+ 'product_uom_id': cls.product_b.uom_id.id,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 160.0,
+ 'price_subtotal': 160.0,
+ 'price_total': 208.0,
+ 'tax_ids': cls.product_b.supplier_taxes_id.ids,
+ 'tax_line_id': False,
+ 'currency_id': cls.company_data['currency'].id,
+ 'amount_currency': -160.0,
+ 'debit': 0.0,
+ 'credit': 160.0,
+ 'date_maturity': False,
+ 'tax_exigible': True,
+ }
+ cls.tax_line_vals_1 = {
+ 'name': cls.tax_purchase_a.name,
+ 'product_id': False,
+ 'account_id': cls.company_data['default_account_tax_purchase'].id,
+ 'partner_id': cls.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 144.0,
+ 'price_subtotal': 144.0,
+ 'price_total': 144.0,
+ 'tax_ids': [],
+ 'tax_line_id': cls.tax_purchase_a.id,
+ 'currency_id': cls.company_data['currency'].id,
+ 'amount_currency': -144.0,
+ 'debit': 0.0,
+ 'credit': 144.0,
+ 'date_maturity': False,
+ 'tax_exigible': True,
+ }
+ cls.tax_line_vals_2 = {
+ 'name': cls.tax_purchase_b.name,
+ 'product_id': False,
+ 'account_id': cls.company_data['default_account_tax_purchase'].id,
+ 'partner_id': cls.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 24.0,
+ 'price_subtotal': 24.0,
+ 'price_total': 24.0,
+ 'tax_ids': [],
+ 'tax_line_id': cls.tax_purchase_b.id,
+ 'currency_id': cls.company_data['currency'].id,
+ 'amount_currency': -24.0,
+ 'debit': 0.0,
+ 'credit': 24.0,
+ 'date_maturity': False,
+ 'tax_exigible': True,
+ }
+ cls.term_line_vals_1 = {
+ 'name': '',
+ 'product_id': False,
+ 'account_id': cls.company_data['default_account_payable'].id,
+ 'partner_id': cls.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': -1128.0,
+ 'price_subtotal': -1128.0,
+ 'price_total': -1128.0,
+ 'tax_ids': [],
+ 'tax_line_id': False,
+ 'currency_id': cls.company_data['currency'].id,
+ 'amount_currency': 1128.0,
+ 'debit': 1128.0,
+ 'credit': 0.0,
+ 'date_maturity': fields.Date.from_string('2019-01-01'),
+ 'tax_exigible': True,
+ }
+ cls.move_vals = {
+ 'partner_id': cls.partner_a.id,
+ 'currency_id': cls.company_data['currency'].id,
+ 'journal_id': cls.company_data['default_journal_purchase'].id,
+ 'date': fields.Date.from_string('2019-01-01'),
+ 'fiscal_position_id': False,
+ 'payment_reference': '',
+ 'invoice_payment_term_id': cls.pay_terms_a.id,
+ 'amount_untaxed': 960.0,
+ 'amount_tax': 168.0,
+ 'amount_total': 1128.0,
+ }
+
+ def setUp(self):
+ super(TestAccountMoveInRefundOnchanges, self).setUp()
+ self.assertInvoiceValues(self.invoice, [
+ self.product_line_vals_1,
+ self.product_line_vals_2,
+ self.tax_line_vals_1,
+ self.tax_line_vals_2,
+ self.term_line_vals_1,
+ ], self.move_vals)
+
+ def test_in_refund_line_onchange_product_1(self):
+ move_form = Form(self.invoice)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ line_form.product_id = self.product_b
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'name': self.product_b.name,
+ 'product_id': self.product_b.id,
+ 'product_uom_id': self.product_b.uom_id.id,
+ 'account_id': self.product_b.property_account_expense_id.id,
+ 'price_unit': 160.0,
+ 'price_subtotal': 160.0,
+ 'price_total': 208.0,
+ 'tax_ids': self.product_b.supplier_taxes_id.ids,
+ 'amount_currency': -160.0,
+ 'credit': 160.0,
+ },
+ self.product_line_vals_2,
+ {
+ **self.tax_line_vals_1,
+ 'price_unit': 48.0,
+ 'price_subtotal': 48.0,
+ 'price_total': 48.0,
+ 'amount_currency': -48.0,
+ 'credit': 48.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'price_unit': 48.0,
+ 'price_subtotal': 48.0,
+ 'price_total': 48.0,
+ 'amount_currency': -48.0,
+ 'credit': 48.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'price_unit': -416.0,
+ 'price_subtotal': -416.0,
+ 'price_total': -416.0,
+ 'amount_currency': 416.0,
+ 'debit': 416.0,
+ },
+ ], {
+ **self.move_vals,
+ 'amount_untaxed': 320.0,
+ 'amount_tax': 96.0,
+ 'amount_total': 416.0,
+ })
+
+ def test_in_refund_line_onchange_business_fields_1(self):
+ move_form = Form(self.invoice)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ # Current price_unit is 800.
+ # We set quantity = 4, discount = 50%, price_unit = 400. The debit/credit fields don't change because (4 * 400) * 0.5 = 800.
+ line_form.quantity = 4
+ line_form.discount = 50
+ line_form.price_unit = 400
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'quantity': 4,
+ 'discount': 50.0,
+ 'price_unit': 400.0,
+ },
+ self.product_line_vals_2,
+ self.tax_line_vals_1,
+ self.tax_line_vals_2,
+ self.term_line_vals_1,
+ ], self.move_vals)
+
+ move_form = Form(self.invoice)
+ with move_form.line_ids.edit(2) as line_form:
+ # Reset field except the discount that becomes 100%.
+ # /!\ The modification is made on the accounting tab.
+ line_form.quantity = 1
+ line_form.discount = 100
+ line_form.price_unit = 800
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'discount': 100.0,
+ 'price_subtotal': 0.0,
+ 'price_total': 0.0,
+ 'amount_currency': 0.0,
+ 'credit': 0.0,
+ },
+ self.product_line_vals_2,
+ {
+ **self.tax_line_vals_1,
+ 'price_unit': 24.0,
+ 'price_subtotal': 24.0,
+ 'price_total': 24.0,
+ 'amount_currency': -24.0,
+ 'credit': 24.0,
+ },
+ self.tax_line_vals_2,
+ {
+ **self.term_line_vals_1,
+ 'price_unit': -208.0,
+ 'price_subtotal': -208.0,
+ 'price_total': -208.0,
+ 'amount_currency': 208.0,
+ 'debit': 208.0,
+ },
+ ], {
+ **self.move_vals,
+ 'amount_untaxed': 160.0,
+ 'amount_tax': 48.0,
+ 'amount_total': 208.0,
+ })
+
+ def test_in_refund_line_onchange_accounting_fields_1(self):
+ move_form = Form(self.invoice)
+ with move_form.line_ids.edit(2) as line_form:
+ # Custom credit on the first product line.
+ line_form.credit = 3000
+ with move_form.line_ids.edit(3) as line_form:
+ # Custom debit on the second product line. Credit should be reset by onchange.
+ # /!\ It's a negative line.
+ line_form.debit = 500
+ with move_form.line_ids.edit(0) as line_form:
+ # Custom credit on the first tax line.
+ line_form.credit = 800
+ with move_form.line_ids.edit(4) as line_form:
+ # Custom credit on the second tax line.
+ line_form.credit = 250
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'price_unit': 3000.0,
+ 'price_subtotal': 3000.0,
+ 'price_total': 3450.0,
+ 'amount_currency': -3000.0,
+ 'credit': 3000.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'price_unit': -500.0,
+ 'price_subtotal': -500.0,
+ 'price_total': -650.0,
+ 'amount_currency': 500.0,
+ 'debit': 500.0,
+ 'credit': 0.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'price_unit': 800.0,
+ 'price_subtotal': 800.0,
+ 'price_total': 800.0,
+ 'amount_currency': -800.0,
+ 'credit': 800.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'price_unit': 250.0,
+ 'price_subtotal': 250.0,
+ 'price_total': 250.0,
+ 'amount_currency': -250.0,
+ 'credit': 250.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'price_unit': -3550.0,
+ 'price_subtotal': -3550.0,
+ 'price_total': -3550.0,
+ 'amount_currency': 3550.0,
+ 'debit': 3550.0,
+ },
+ ], {
+ **self.move_vals,
+ 'amount_untaxed': 2500.0,
+ 'amount_tax': 1050.0,
+ 'amount_total': 3550.0,
+ })
+
+ def test_in_refund_line_onchange_partner_1(self):
+ move_form = Form(self.invoice)
+ move_form.partner_id = self.partner_b
+ move_form.payment_reference = 'turlututu'
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'partner_id': self.partner_b.id,
+ },
+ {
+ **self.product_line_vals_2,
+ 'partner_id': self.partner_b.id,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'partner_id': self.partner_b.id,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'partner_id': self.partner_b.id,
+ },
+ {
+ **self.term_line_vals_1,
+ 'name': 'turlututu',
+ 'partner_id': self.partner_b.id,
+ 'account_id': self.partner_b.property_account_payable_id.id,
+ 'price_unit': -338.4,
+ 'price_subtotal': -338.4,
+ 'price_total': -338.4,
+ 'amount_currency': 338.4,
+ 'debit': 338.4,
+ },
+ {
+ **self.term_line_vals_1,
+ 'name': 'turlututu',
+ 'partner_id': self.partner_b.id,
+ 'account_id': self.partner_b.property_account_payable_id.id,
+ 'price_unit': -789.6,
+ 'price_subtotal': -789.6,
+ 'price_total': -789.6,
+ 'amount_currency': 789.6,
+ 'debit': 789.6,
+ 'date_maturity': fields.Date.from_string('2019-02-28'),
+ },
+ ], {
+ **self.move_vals,
+ 'partner_id': self.partner_b.id,
+ 'payment_reference': 'turlututu',
+ 'fiscal_position_id': self.fiscal_pos_a.id,
+ 'invoice_payment_term_id': self.pay_terms_b.id,
+ 'amount_untaxed': 960.0,
+ 'amount_tax': 168.0,
+ 'amount_total': 1128.0,
+ })
+
+ # Remove lines and recreate them to apply the fiscal position.
+ move_form = Form(self.invoice)
+ move_form.invoice_line_ids.remove(0)
+ move_form.invoice_line_ids.remove(0)
+ with move_form.invoice_line_ids.new() as line_form:
+ line_form.product_id = self.product_a
+ with move_form.invoice_line_ids.new() as line_form:
+ line_form.product_id = self.product_b
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'account_id': self.product_b.property_account_expense_id.id,
+ 'partner_id': self.partner_b.id,
+ 'tax_ids': self.tax_purchase_b.ids,
+ },
+ {
+ **self.product_line_vals_2,
+ 'partner_id': self.partner_b.id,
+ 'price_total': 184.0,
+ 'tax_ids': self.tax_purchase_b.ids,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'name': self.tax_purchase_b.name,
+ 'partner_id': self.partner_b.id,
+ 'tax_line_id': self.tax_purchase_b.id,
+ },
+ {
+ **self.term_line_vals_1,
+ 'name': 'turlututu',
+ 'account_id': self.partner_b.property_account_payable_id.id,
+ 'partner_id': self.partner_b.id,
+ 'price_unit': -331.2,
+ 'price_subtotal': -331.2,
+ 'price_total': -331.2,
+ 'amount_currency': 331.2,
+ 'debit': 331.2,
+ },
+ {
+ **self.term_line_vals_1,
+ 'name': 'turlututu',
+ 'account_id': self.partner_b.property_account_payable_id.id,
+ 'partner_id': self.partner_b.id,
+ 'price_unit': -772.8,
+ 'price_subtotal': -772.8,
+ 'price_total': -772.8,
+ 'amount_currency': 772.8,
+ 'debit': 772.8,
+ 'date_maturity': fields.Date.from_string('2019-02-28'),
+ },
+ ], {
+ **self.move_vals,
+ 'partner_id': self.partner_b.id,
+ 'payment_reference': 'turlututu',
+ 'fiscal_position_id': self.fiscal_pos_a.id,
+ 'invoice_payment_term_id': self.pay_terms_b.id,
+ 'amount_untaxed': 960.0,
+ 'amount_tax': 144.0,
+ 'amount_total': 1104.0,
+ })
+
+ def test_in_refund_line_onchange_taxes_1(self):
+ move_form = Form(self.invoice)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ line_form.price_unit = 960
+ line_form.tax_ids.add(self.tax_armageddon)
+ move_form.save()
+
+ child_tax_1 = self.tax_armageddon.children_tax_ids[0]
+ child_tax_2 = self.tax_armageddon.children_tax_ids[1]
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'price_unit': 960.0,
+ 'price_subtotal': 800.0,
+ 'price_total': 1176.0,
+ 'tax_ids': (self.tax_purchase_a + self.tax_armageddon).ids,
+ 'tax_exigible': False,
+ },
+ self.product_line_vals_2,
+ self.tax_line_vals_1,
+ self.tax_line_vals_2,
+ {
+ 'name': child_tax_1.name,
+ 'product_id': False,
+ 'account_id': self.company_data['default_account_expense'].id,
+ 'partner_id': self.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 96.0,
+ 'price_subtotal': 96.0,
+ 'price_total': 105.6,
+ 'tax_ids': child_tax_2.ids,
+ 'tax_line_id': child_tax_1.id,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': -96.0,
+ 'debit': 0.0,
+ 'credit': 96.0,
+ 'date_maturity': False,
+ 'tax_exigible': False,
+ },
+ {
+ 'name': child_tax_1.name,
+ 'product_id': False,
+ 'account_id': self.company_data['default_account_tax_sale'].id,
+ 'partner_id': self.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 64.0,
+ 'price_subtotal': 64.0,
+ 'price_total': 70.4,
+ 'tax_ids': child_tax_2.ids,
+ 'tax_line_id': child_tax_1.id,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': -64.0,
+ 'debit': 0.0,
+ 'credit': 64.0,
+ 'date_maturity': False,
+ 'tax_exigible': False,
+ },
+ {
+ 'name': child_tax_2.name,
+ 'product_id': False,
+ 'account_id': child_tax_2.cash_basis_transition_account_id.id,
+ 'partner_id': self.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 96.0,
+ 'price_subtotal': 96.0,
+ 'price_total': 96.0,
+ 'tax_ids': [],
+ 'tax_line_id': child_tax_2.id,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': -96.0,
+ 'debit': 0.0,
+ 'credit': 96.0,
+ 'date_maturity': False,
+ 'tax_exigible': False,
+ },
+ {
+ **self.term_line_vals_1,
+ 'price_unit': -1384.0,
+ 'price_subtotal': -1384.0,
+ 'price_total': -1384.0,
+ 'amount_currency': 1384.0,
+ 'debit': 1384.0,
+ },
+ ], {
+ **self.move_vals,
+ 'amount_untaxed': 960.0,
+ 'amount_tax': 424.0,
+ 'amount_total': 1384.0,
+ })
+
+ def test_in_refund_line_onchange_cash_rounding_1(self):
+ move_form = Form(self.invoice)
+ # Add a cash rounding having 'add_invoice_line'.
+ move_form.invoice_cash_rounding_id = self.cash_rounding_a
+ move_form.save()
+
+ # The cash rounding does nothing as the total is already rounded.
+ self.assertInvoiceValues(self.invoice, [
+ self.product_line_vals_1,
+ self.product_line_vals_2,
+ self.tax_line_vals_1,
+ self.tax_line_vals_2,
+ self.term_line_vals_1,
+ ], self.move_vals)
+
+ move_form = Form(self.invoice)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ line_form.price_unit = 799.99
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ 'name': 'add_invoice_line',
+ 'product_id': False,
+ 'account_id': self.cash_rounding_a.profit_account_id.id,
+ 'partner_id': self.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 0.01,
+ 'price_subtotal': 0.01,
+ 'price_total': 0.01,
+ 'tax_ids': [],
+ 'tax_line_id': False,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': -0.01,
+ 'debit': 0.0,
+ 'credit': 0.01,
+ 'date_maturity': False,
+ 'tax_exigible': True,
+ },
+ {
+ **self.product_line_vals_1,
+ 'price_unit': 799.99,
+ 'price_subtotal': 799.99,
+ 'price_total': 919.99,
+ 'amount_currency': -799.99,
+ 'credit': 799.99,
+ },
+ self.product_line_vals_2,
+ self.tax_line_vals_1,
+ self.tax_line_vals_2,
+ self.term_line_vals_1,
+ ], self.move_vals)
+
+ move_form = Form(self.invoice)
+ # Change the cash rounding to one having 'biggest_tax'.
+ move_form.invoice_cash_rounding_id = self.cash_rounding_b
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'price_unit': 799.99,
+ 'price_subtotal': 799.99,
+ 'price_total': 919.99,
+ 'amount_currency': -799.99,
+ 'credit': 799.99,
+ },
+ self.product_line_vals_2,
+ self.tax_line_vals_1,
+ self.tax_line_vals_2,
+ {
+ 'name': '%s (rounding)' % self.tax_purchase_a.name,
+ 'product_id': False,
+ 'account_id': self.company_data['default_account_tax_purchase'].id,
+ 'partner_id': self.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': -0.04,
+ 'price_subtotal': -0.04,
+ 'price_total': -0.04,
+ 'tax_ids': [],
+ 'tax_line_id': self.tax_purchase_a.id,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': 0.04,
+ 'debit': 0.04,
+ 'credit': 0.0,
+ 'date_maturity': False,
+ 'tax_exigible': True,
+ },
+ {
+ **self.term_line_vals_1,
+ 'price_unit': -1127.95,
+ 'price_subtotal': -1127.95,
+ 'price_total': -1127.95,
+ 'amount_currency': 1127.95,
+ 'debit': 1127.95,
+ },
+ ], {
+ **self.move_vals,
+ 'amount_untaxed': 959.99,
+ 'amount_tax': 167.96,
+ 'amount_total': 1127.95,
+ })
+
+ def test_in_refund_line_onchange_currency_1(self):
+ move_form = Form(self.invoice)
+ move_form.currency_id = self.currency_data['currency']
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -800.0,
+ 'credit': 400.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -160.0,
+ 'credit': 80.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -144.0,
+ 'credit': 72.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -24.0,
+ 'credit': 12.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 1128.0,
+ 'debit': 564.0,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ })
+
+ move_form = Form(self.invoice)
+ # Change the date to get another rate: 1/3 instead of 1/2.
+ move_form.date = fields.Date.from_string('2016-01-01')
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -800.0,
+ 'credit': 266.67,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -160.0,
+ 'credit': 53.33,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -144.0,
+ 'credit': 48.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -24.0,
+ 'credit': 8.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 1128.0,
+ 'debit': 376.0,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ 'date': fields.Date.from_string('2016-01-01'),
+ })
+
+ move_form = Form(self.invoice)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ # 0.045 * 0.1 = 0.0045. As the foreign currency has a 0.001 rounding,
+ # the result should be 0.005 after rounding.
+ line_form.quantity = 0.1
+ line_form.price_unit = 0.045
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'quantity': 0.1,
+ 'price_unit': 0.05,
+ 'price_subtotal': 0.005,
+ 'price_total': 0.006,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -0.005,
+ 'credit': 0.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -160.0,
+ 'credit': 53.33,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'price_unit': 24.0,
+ 'price_subtotal': 24.001,
+ 'price_total': 24.001,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -24.001,
+ 'credit': 8.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -24.0,
+ 'credit': 8.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'price_unit': -208.01,
+ 'price_subtotal': -208.006,
+ 'price_total': -208.006,
+ 'amount_currency': 208.006,
+ 'debit': 69.33,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ 'date': fields.Date.from_string('2016-01-01'),
+ 'amount_untaxed': 160.005,
+ 'amount_tax': 48.001,
+ 'amount_total': 208.006,
+ })
+
+ # Exit the multi-currencies.
+ move_form = Form(self.invoice)
+ move_form.currency_id = self.company_data['currency']
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'quantity': 0.1,
+ 'price_unit': 0.05,
+ 'price_subtotal': 0.01,
+ 'price_total': 0.01,
+ 'amount_currency': -0.01,
+ 'credit': 0.01,
+ },
+ self.product_line_vals_2,
+ {
+ **self.tax_line_vals_1,
+ 'price_unit': 24.0,
+ 'price_subtotal': 24.0,
+ 'price_total': 24.0,
+ 'amount_currency': -24.0,
+ 'credit': 24.0,
+ },
+ self.tax_line_vals_2,
+ {
+ **self.term_line_vals_1,
+ 'price_unit': -208.01,
+ 'price_subtotal': -208.01,
+ 'price_total': -208.01,
+ 'amount_currency': 208.01,
+ 'debit': 208.01,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.company_data['currency'].id,
+ 'date': fields.Date.from_string('2016-01-01'),
+ 'amount_untaxed': 160.01,
+ 'amount_tax': 48.0,
+ 'amount_total': 208.01,
+ })
+
+ def test_in_refund_onchange_past_invoice_1(self):
+ copy_invoice = self.invoice.copy()
+
+ move_form = Form(self.invoice)
+ move_form.invoice_line_ids.remove(0)
+ move_form.invoice_line_ids.remove(0)
+ move_form.invoice_vendor_bill_id = copy_invoice
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ self.product_line_vals_1,
+ self.product_line_vals_2,
+ self.tax_line_vals_1,
+ self.tax_line_vals_2,
+ self.term_line_vals_1,
+ ], self.move_vals)
+
+ def test_in_refund_create_1(self):
+ # Test creating an account_move with the least information.
+ move = self.env['account.move'].create({
+ 'move_type': 'in_refund',
+ 'partner_id': self.partner_a.id,
+ 'invoice_date': fields.Date.from_string('2019-01-01'),
+ 'currency_id': self.currency_data['currency'].id,
+ 'invoice_payment_term_id': self.pay_terms_a.id,
+ 'invoice_line_ids': [
+ (0, None, self.product_line_vals_1),
+ (0, None, self.product_line_vals_2),
+ ]
+ })
+
+ self.assertInvoiceValues(move, [
+ {
+ **self.product_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -800.0,
+ 'credit': 400.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -160.0,
+ 'credit': 80.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -144.0,
+ 'credit': 72.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -24.0,
+ 'credit': 12.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 1128.0,
+ 'debit': 564.0,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ })
+
+ def test_in_refund_write_1(self):
+ # Test creating an account_move with the least information.
+ move = self.env['account.move'].create({
+ 'move_type': 'in_refund',
+ 'partner_id': self.partner_a.id,
+ 'invoice_date': fields.Date.from_string('2019-01-01'),
+ 'currency_id': self.currency_data['currency'].id,
+ 'invoice_payment_term_id': self.pay_terms_a.id,
+ 'invoice_line_ids': [
+ (0, None, self.product_line_vals_1),
+ ]
+ })
+ move.write({
+ 'invoice_line_ids': [
+ (0, None, self.product_line_vals_2),
+ ]
+ })
+
+ self.assertInvoiceValues(move, [
+ {
+ **self.product_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -800.0,
+ 'credit': 400.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -160.0,
+ 'credit': 80.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -144.0,
+ 'credit': 72.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -24.0,
+ 'credit': 12.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 1128.0,
+ 'debit': 564.0,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ })
diff --git a/addons/account/tests/test_account_move_out_invoice.py b/addons/account/tests/test_account_move_out_invoice.py
new file mode 100644
index 00000000..ce8fe166
--- /dev/null
+++ b/addons/account/tests/test_account_move_out_invoice.py
@@ -0,0 +1,2933 @@
+# -*- coding: utf-8 -*-
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests.common import Form
+from odoo.tests import tagged
+from odoo import fields
+from odoo.exceptions import UserError
+
+from unittest.mock import patch
+from datetime import timedelta
+
+
+@tagged('post_install', '-at_install')
+class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ cls.invoice = cls.init_invoice('out_invoice', products=cls.product_a+cls.product_b)
+
+ cls.product_line_vals_1 = {
+ 'name': cls.product_a.name,
+ 'product_id': cls.product_a.id,
+ 'account_id': cls.product_a.property_account_income_id.id,
+ 'partner_id': cls.partner_a.id,
+ 'product_uom_id': cls.product_a.uom_id.id,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 1000.0,
+ 'price_subtotal': 1000.0,
+ 'price_total': 1150.0,
+ 'tax_ids': cls.product_a.taxes_id.ids,
+ 'tax_line_id': False,
+ 'currency_id': cls.company_data['currency'].id,
+ 'amount_currency': -1000.0,
+ 'debit': 0.0,
+ 'credit': 1000.0,
+ 'date_maturity': False,
+ 'tax_exigible': True,
+ }
+ cls.product_line_vals_2 = {
+ 'name': cls.product_b.name,
+ 'product_id': cls.product_b.id,
+ 'account_id': cls.product_b.property_account_income_id.id,
+ 'partner_id': cls.partner_a.id,
+ 'product_uom_id': cls.product_b.uom_id.id,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 200.0,
+ 'price_subtotal': 200.0,
+ 'price_total': 260.0,
+ 'tax_ids': cls.product_b.taxes_id.ids,
+ 'tax_line_id': False,
+ 'currency_id': cls.company_data['currency'].id,
+ 'amount_currency': -200.0,
+ 'debit': 0.0,
+ 'credit': 200.0,
+ 'date_maturity': False,
+ 'tax_exigible': True,
+ }
+ cls.tax_line_vals_1 = {
+ 'name': cls.tax_sale_a.name,
+ 'product_id': False,
+ 'account_id': cls.company_data['default_account_tax_sale'].id,
+ 'partner_id': cls.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 180.0,
+ 'price_subtotal': 180.0,
+ 'price_total': 180.0,
+ 'tax_ids': [],
+ 'tax_line_id': cls.tax_sale_a.id,
+ 'currency_id': cls.company_data['currency'].id,
+ 'amount_currency': -180.0,
+ 'debit': 0.0,
+ 'credit': 180.0,
+ 'date_maturity': False,
+ 'tax_exigible': True,
+ }
+ cls.tax_line_vals_2 = {
+ 'name': cls.tax_sale_b.name,
+ 'product_id': False,
+ 'account_id': cls.company_data['default_account_tax_sale'].id,
+ 'partner_id': cls.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 30.0,
+ 'price_subtotal': 30.0,
+ 'price_total': 30.0,
+ 'tax_ids': [],
+ 'tax_line_id': cls.tax_sale_b.id,
+ 'currency_id': cls.company_data['currency'].id,
+ 'amount_currency': -30.0,
+ 'debit': 0.0,
+ 'credit': 30.0,
+ 'date_maturity': False,
+ 'tax_exigible': True,
+ }
+ cls.term_line_vals_1 = {
+ 'name': '',
+ 'product_id': False,
+ 'account_id': cls.company_data['default_account_receivable'].id,
+ 'partner_id': cls.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': -1410.0,
+ 'price_subtotal': -1410.0,
+ 'price_total': -1410.0,
+ 'tax_ids': [],
+ 'tax_line_id': False,
+ 'currency_id': cls.company_data['currency'].id,
+ 'amount_currency': 1410.0,
+ 'debit': 1410.0,
+ 'credit': 0.0,
+ 'date_maturity': fields.Date.from_string('2019-01-01'),
+ 'tax_exigible': True,
+ }
+ cls.move_vals = {
+ 'partner_id': cls.partner_a.id,
+ 'currency_id': cls.company_data['currency'].id,
+ 'journal_id': cls.company_data['default_journal_sale'].id,
+ 'date': fields.Date.from_string('2019-01-01'),
+ 'fiscal_position_id': False,
+ 'payment_reference': '',
+ 'invoice_payment_term_id': cls.pay_terms_a.id,
+ 'amount_untaxed': 1200.0,
+ 'amount_tax': 210.0,
+ 'amount_total': 1410.0,
+ }
+
+ def setUp(self):
+ super(TestAccountMoveOutInvoiceOnchanges, self).setUp()
+ self.assertInvoiceValues(self.invoice, [
+ self.product_line_vals_1,
+ self.product_line_vals_2,
+ self.tax_line_vals_1,
+ self.tax_line_vals_2,
+ self.term_line_vals_1,
+ ], self.move_vals)
+
+ def test_out_invoice_onchange_invoice_date(self):
+ for tax_date, invoice_date, accounting_date in [
+ ('2019-03-31', '2019-05-12', '2019-05-12'),
+ ('2019-03-31', '2019-02-10', '2019-04-1'),
+ ('2019-05-31', '2019-06-15', '2019-06-15'),
+ ]:
+ self.invoice.company_id.tax_lock_date = tax_date
+ with Form(self.invoice) as move_form:
+ move_form.invoice_date = invoice_date
+ self.assertEqual(self.invoice.date, fields.Date.to_date(accounting_date))
+
+ def test_out_invoice_line_onchange_product_1(self):
+ move_form = Form(self.invoice)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ line_form.product_id = self.product_b
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'name': self.product_b.name,
+ 'product_id': self.product_b.id,
+ 'product_uom_id': self.product_b.uom_id.id,
+ 'account_id': self.product_b.property_account_income_id.id,
+ 'price_unit': 200.0,
+ 'price_subtotal': 200.0,
+ 'price_total': 260.0,
+ 'tax_ids': self.product_b.taxes_id.ids,
+ 'amount_currency': -200.0,
+ 'credit': 200.0,
+ },
+ self.product_line_vals_2,
+ {
+ **self.tax_line_vals_1,
+ 'price_unit': 60.0,
+ 'price_subtotal': 60.0,
+ 'price_total': 60.0,
+ 'amount_currency': -60.0,
+ 'credit': 60.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'price_unit': 60.0,
+ 'price_subtotal': 60.0,
+ 'price_total': 60.0,
+ 'amount_currency': -60.0,
+ 'credit': 60.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'price_unit': -520.0,
+ 'price_subtotal': -520.0,
+ 'price_total': -520.0,
+ 'amount_currency': 520.0,
+ 'debit': 520.0,
+ },
+ ], {
+ **self.move_vals,
+ 'amount_untaxed': 400.0,
+ 'amount_tax': 120.0,
+ 'amount_total': 520.0,
+ })
+
+ def test_out_invoice_line_onchange_product_2_with_fiscal_pos_1(self):
+ ''' Test mapping a price-included tax (10%) with a price-excluded tax (20%) on a price_unit of 110.0.
+ The price_unit should be 100.0 after applying the fiscal position.
+ '''
+ tax_price_include = self.env['account.tax'].create({
+ 'name': '10% incl',
+ 'type_tax_use': 'sale',
+ 'amount_type': 'percent',
+ 'amount': 10,
+ 'price_include': True,
+ 'include_base_amount': True,
+ })
+ tax_price_exclude = self.env['account.tax'].create({
+ 'name': '15% excl',
+ 'type_tax_use': 'sale',
+ 'amount_type': 'percent',
+ 'amount': 15,
+ })
+
+ fiscal_position = self.env['account.fiscal.position'].create({
+ 'name': 'fiscal_pos_a',
+ 'tax_ids': [
+ (0, None, {
+ 'tax_src_id': tax_price_include.id,
+ 'tax_dest_id': tax_price_exclude.id,
+ }),
+ ],
+ })
+
+ product = self.env['product.product'].create({
+ 'name': 'product',
+ 'uom_id': self.env.ref('uom.product_uom_unit').id,
+ 'lst_price': 110.0,
+ 'taxes_id': [(6, 0, tax_price_include.ids)],
+ })
+
+ move_form = Form(self.env['account.move'].with_context(default_move_type='out_invoice'))
+ move_form.partner_id = self.partner_a
+ move_form.invoice_date = fields.Date.from_string('2019-01-01')
+ move_form.currency_id = self.currency_data['currency']
+ move_form.fiscal_position_id = fiscal_position
+ with move_form.invoice_line_ids.new() as line_form:
+ line_form.product_id = product
+ invoice = move_form.save()
+
+ self.assertInvoiceValues(invoice, [
+ {
+ 'product_id': product.id,
+ 'price_unit': 200.0,
+ 'price_subtotal': 200.0,
+ 'price_total': 230.0,
+ 'tax_ids': tax_price_exclude.ids,
+ 'tax_line_id': False,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -200.0,
+ 'debit': 0.0,
+ 'credit': 100.0,
+ },
+ {
+ 'product_id': False,
+ 'price_unit': 30.0,
+ 'price_subtotal': 30.0,
+ 'price_total': 30.0,
+ 'tax_ids': [],
+ 'tax_line_id': tax_price_exclude.id,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -30.0,
+ 'debit': 0.0,
+ 'credit': 15.0,
+ },
+ {
+ 'product_id': False,
+ 'price_unit': -230.0,
+ 'price_subtotal': -230.0,
+ 'price_total': -230.0,
+ 'tax_ids': [],
+ 'tax_line_id': False,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 230.0,
+ 'debit': 115.0,
+ 'credit': 0.0,
+ },
+ ], {
+ 'currency_id': self.currency_data['currency'].id,
+ 'fiscal_position_id': fiscal_position.id,
+ 'amount_untaxed': 200.0,
+ 'amount_tax': 30.0,
+ 'amount_total': 230.0,
+ })
+
+ uom_dozen = self.env.ref('uom.product_uom_dozen')
+ with Form(invoice) as move_form:
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ line_form.product_uom_id = uom_dozen
+
+ self.assertInvoiceValues(invoice, [
+ {
+ 'product_id': product.id,
+ 'product_uom_id': uom_dozen.id,
+ 'price_unit': 2400.0,
+ 'price_subtotal': 2400.0,
+ 'price_total': 2760.0,
+ 'tax_ids': tax_price_exclude.ids,
+ 'tax_line_id': False,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -2400.0,
+ 'debit': 0.0,
+ 'credit': 1200.0,
+ },
+ {
+ 'product_id': False,
+ 'product_uom_id': False,
+ 'price_unit': 360.0,
+ 'price_subtotal': 360.0,
+ 'price_total': 360.0,
+ 'tax_ids': [],
+ 'tax_line_id': tax_price_exclude.id,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -360.0,
+ 'debit': 0.0,
+ 'credit': 180.0,
+ },
+ {
+ 'product_id': False,
+ 'product_uom_id': False,
+ 'price_unit': -2760.0,
+ 'price_subtotal': -2760.0,
+ 'price_total': -2760.0,
+ 'tax_ids': [],
+ 'tax_line_id': False,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 2760.0,
+ 'debit': 1380.0,
+ 'credit': 0.0,
+ },
+ ], {
+ 'currency_id': self.currency_data['currency'].id,
+ 'fiscal_position_id': fiscal_position.id,
+ 'amount_untaxed': 2400.0,
+ 'amount_tax': 360.0,
+ 'amount_total': 2760.0,
+ })
+
+ def test_out_invoice_line_onchange_product_2_with_fiscal_pos_2(self):
+ ''' Test mapping a price-included tax (10%) with another price-included tax (20%) on a price_unit of 110.0.
+ The price_unit should be 120.0 after applying the fiscal position.
+ '''
+ tax_price_include_1 = self.env['account.tax'].create({
+ 'name': '10% incl',
+ 'type_tax_use': 'sale',
+ 'amount_type': 'percent',
+ 'amount': 10,
+ 'price_include': True,
+ 'include_base_amount': True,
+ })
+ tax_price_include_2 = self.env['account.tax'].create({
+ 'name': '20% incl',
+ 'type_tax_use': 'sale',
+ 'amount_type': 'percent',
+ 'amount': 20,
+ 'price_include': True,
+ 'include_base_amount': True,
+ })
+
+ fiscal_position = self.env['account.fiscal.position'].create({
+ 'name': 'fiscal_pos_a',
+ 'tax_ids': [
+ (0, None, {
+ 'tax_src_id': tax_price_include_1.id,
+ 'tax_dest_id': tax_price_include_2.id,
+ }),
+ ],
+ })
+
+ product = self.env['product.product'].create({
+ 'name': 'product',
+ 'uom_id': self.env.ref('uom.product_uom_unit').id,
+ 'lst_price': 110.0,
+ 'taxes_id': [(6, 0, tax_price_include_1.ids)],
+ })
+
+ move_form = Form(self.env['account.move'].with_context(default_move_type='out_invoice'))
+ move_form.partner_id = self.partner_a
+ move_form.invoice_date = fields.Date.from_string('2019-01-01')
+ move_form.currency_id = self.currency_data['currency']
+ move_form.fiscal_position_id = fiscal_position
+ with move_form.invoice_line_ids.new() as line_form:
+ line_form.product_id = product
+ invoice = move_form.save()
+
+ self.assertInvoiceValues(invoice, [
+ {
+ 'product_id': product.id,
+ 'price_unit': 240.0,
+ 'price_subtotal': 200.0,
+ 'price_total': 240.0,
+ 'tax_ids': tax_price_include_2.ids,
+ 'tax_line_id': False,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -200.0,
+ 'debit': 0.0,
+ 'credit': 100.0,
+ },
+ {
+ 'product_id': False,
+ 'price_unit': 40.0,
+ 'price_subtotal': 40.0,
+ 'price_total': 40.0,
+ 'tax_ids': [],
+ 'tax_line_id': tax_price_include_2.id,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -40.0,
+ 'debit': 0.0,
+ 'credit': 20.0,
+ },
+ {
+ 'product_id': False,
+ 'price_unit': -240.0,
+ 'price_subtotal': -240.0,
+ 'price_total': -240.0,
+ 'tax_ids': [],
+ 'tax_line_id': False,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 240.0,
+ 'debit': 120.0,
+ 'credit': 0.0,
+ },
+ ], {
+ 'currency_id': self.currency_data['currency'].id,
+ 'fiscal_position_id': fiscal_position.id,
+ 'amount_untaxed': 200.0,
+ 'amount_tax': 40.0,
+ 'amount_total': 240.0,
+ })
+
+ uom_dozen = self.env.ref('uom.product_uom_dozen')
+ with Form(invoice) as move_form:
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ line_form.product_uom_id = uom_dozen
+
+ self.assertInvoiceValues(invoice, [
+ {
+ 'product_id': product.id,
+ 'product_uom_id': uom_dozen.id,
+ 'price_unit': 2880.0,
+ 'price_subtotal': 2400.0,
+ 'price_total': 2880.0,
+ 'tax_ids': tax_price_include_2.ids,
+ 'tax_line_id': False,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -2400.0,
+ 'debit': 0.0,
+ 'credit': 1200.0,
+ },
+ {
+ 'product_id': False,
+ 'product_uom_id': False,
+ 'price_unit': 480.0,
+ 'price_subtotal': 480.0,
+ 'price_total': 480.0,
+ 'tax_ids': [],
+ 'tax_line_id': tax_price_include_2.id,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -480.0,
+ 'debit': 0.0,
+ 'credit': 240.0,
+ },
+ {
+ 'product_id': False,
+ 'product_uom_id': False,
+ 'price_unit': -2880.0,
+ 'price_subtotal': -2880.0,
+ 'price_total': -2880.0,
+ 'tax_ids': [],
+ 'tax_line_id': False,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 2880.0,
+ 'debit': 1440.0,
+ 'credit': 0.0,
+ },
+ ], {
+ 'currency_id': self.currency_data['currency'].id,
+ 'fiscal_position_id': fiscal_position.id,
+ 'amount_untaxed': 2400.0,
+ 'amount_tax': 480.0,
+ 'amount_total': 2880.0,
+ })
+
+ def test_out_invoice_line_onchange_business_fields_1(self):
+ move_form = Form(self.invoice)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ # Current price_unit is 1000.
+ # We set quantity = 4, discount = 50%, price_unit = 400. The debit/credit fields don't change because (4 * 500) * 0.5 = 1000.
+ line_form.quantity = 4
+ line_form.discount = 50
+ line_form.price_unit = 500
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'quantity': 4,
+ 'discount': 50.0,
+ 'price_unit': 500.0,
+ },
+ self.product_line_vals_2,
+ self.tax_line_vals_1,
+ self.tax_line_vals_2,
+ self.term_line_vals_1,
+ ], self.move_vals)
+
+ move_form = Form(self.invoice)
+ with move_form.line_ids.edit(2) as line_form:
+ # Reset field except the discount that becomes 100%.
+ # /!\ The modification is made on the accounting tab.
+ line_form.quantity = 1
+ line_form.discount = 100
+ line_form.price_unit = 1000
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'discount': 100.0,
+ 'price_subtotal': 0.0,
+ 'price_total': 0.0,
+ 'amount_currency': 0.0,
+ 'credit': 0.0,
+ },
+ self.product_line_vals_2,
+ {
+ **self.tax_line_vals_1,
+ 'price_unit': 30.0,
+ 'price_subtotal': 30.0,
+ 'price_total': 30.0,
+ 'amount_currency': -30.0,
+ 'credit': 30.0,
+ },
+ self.tax_line_vals_2,
+ {
+ **self.term_line_vals_1,
+ 'price_unit': -260.0,
+ 'price_subtotal': -260.0,
+ 'price_total': -260.0,
+ 'amount_currency': 260.0,
+ 'debit': 260.0,
+ },
+ ], {
+ **self.move_vals,
+ 'amount_untaxed': 200.0,
+ 'amount_tax': 60.0,
+ 'amount_total': 260.0,
+ })
+
+ def test_out_invoice_line_onchange_accounting_fields_1(self):
+ move_form = Form(self.invoice)
+ with move_form.line_ids.edit(2) as line_form:
+ # Custom credit on the first product line.
+ line_form.credit = 3000
+ with move_form.line_ids.edit(3) as line_form:
+ # Custom debit on the second product line. Credit should be reset by onchange.
+ # /!\ It's a negative line.
+ line_form.debit = 500
+ with move_form.line_ids.edit(0) as line_form:
+ # Custom credit on the first tax line.
+ line_form.credit = 800
+ with move_form.line_ids.edit(4) as line_form:
+ # Custom credit on the second tax line.
+ line_form.credit = 250
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'price_unit': 3000.0,
+ 'price_subtotal': 3000.0,
+ 'price_total': 3450.0,
+ 'amount_currency': -3000.0,
+ 'credit': 3000.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'price_unit': -500.0,
+ 'price_subtotal': -500.0,
+ 'price_total': -650.0,
+ 'amount_currency': 500.0,
+ 'debit': 500.0,
+ 'credit': 0.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'price_unit': 800.0,
+ 'price_subtotal': 800.0,
+ 'price_total': 800.0,
+ 'amount_currency': -800.0,
+ 'credit': 800.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'price_unit': 250.0,
+ 'price_subtotal': 250.0,
+ 'price_total': 250.0,
+ 'amount_currency': -250.0,
+ 'credit': 250.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'price_unit': -3550.0,
+ 'price_subtotal': -3550.0,
+ 'price_total': -3550.0,
+ 'amount_currency': 3550.0,
+ 'debit': 3550.0,
+ },
+ ], {
+ **self.move_vals,
+ 'amount_untaxed': 2500.0,
+ 'amount_tax': 1050.0,
+ 'amount_total': 3550.0,
+ })
+
+ def test_out_invoice_line_onchange_partner_1(self):
+ move_form = Form(self.invoice)
+ move_form.partner_id = self.partner_b
+ move_form.payment_reference = 'turlututu'
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'partner_id': self.partner_b.id,
+ },
+ {
+ **self.product_line_vals_2,
+ 'partner_id': self.partner_b.id,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'partner_id': self.partner_b.id,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'partner_id': self.partner_b.id,
+ },
+ {
+ **self.term_line_vals_1,
+ 'name': 'turlututu',
+ 'account_id': self.partner_b.property_account_receivable_id.id,
+ 'partner_id': self.partner_b.id,
+ 'price_unit': -423.0,
+ 'price_subtotal': -423.0,
+ 'price_total': -423.0,
+ 'amount_currency': 423.0,
+ 'debit': 423.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'name': 'turlututu',
+ 'account_id': self.partner_b.property_account_receivable_id.id,
+ 'partner_id': self.partner_b.id,
+ 'price_unit': -987.0,
+ 'price_subtotal': -987.0,
+ 'price_total': -987.0,
+ 'amount_currency': 987.0,
+ 'debit': 987.0,
+ 'date_maturity': fields.Date.from_string('2019-02-28'),
+ },
+ ], {
+ **self.move_vals,
+ 'partner_id': self.partner_b.id,
+ 'payment_reference': 'turlututu',
+ 'fiscal_position_id': self.fiscal_pos_a.id,
+ 'invoice_payment_term_id': self.pay_terms_b.id,
+ 'amount_untaxed': 1200.0,
+ 'amount_tax': 210.0,
+ 'amount_total': 1410.0,
+ })
+
+ # Remove lines and recreate them to apply the fiscal position.
+ move_form = Form(self.invoice)
+ move_form.invoice_line_ids.remove(0)
+ move_form.invoice_line_ids.remove(0)
+ with move_form.invoice_line_ids.new() as line_form:
+ line_form.product_id = self.product_a
+ with move_form.invoice_line_ids.new() as line_form:
+ line_form.product_id = self.product_b
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'account_id': self.product_b.property_account_income_id.id,
+ 'partner_id': self.partner_b.id,
+ 'tax_ids': self.tax_sale_b.ids,
+ },
+ {
+ **self.product_line_vals_2,
+ 'partner_id': self.partner_b.id,
+ 'price_total': 230.0,
+ 'tax_ids': self.tax_sale_b.ids,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'name': self.tax_sale_b.name,
+ 'partner_id': self.partner_b.id,
+ 'tax_line_id': self.tax_sale_b.id,
+ },
+ {
+ **self.term_line_vals_1,
+ 'name': 'turlututu',
+ 'account_id': self.partner_b.property_account_receivable_id.id,
+ 'partner_id': self.partner_b.id,
+ 'price_unit': -414.0,
+ 'price_subtotal': -414.0,
+ 'price_total': -414.0,
+ 'amount_currency': 414.0,
+ 'debit': 414.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'name': 'turlututu',
+ 'account_id': self.partner_b.property_account_receivable_id.id,
+ 'partner_id': self.partner_b.id,
+ 'price_unit': -966.0,
+ 'price_subtotal': -966.0,
+ 'price_total': -966.0,
+ 'amount_currency': 966.0,
+ 'debit': 966.0,
+ 'date_maturity': fields.Date.from_string('2019-02-28'),
+ },
+ ], {
+ **self.move_vals,
+ 'partner_id': self.partner_b.id,
+ 'payment_reference': 'turlututu',
+ 'fiscal_position_id': self.fiscal_pos_a.id,
+ 'invoice_payment_term_id': self.pay_terms_b.id,
+ 'amount_untaxed': 1200.0,
+ 'amount_tax': 180.0,
+ 'amount_total': 1380.0,
+ })
+
+ def test_out_invoice_line_onchange_taxes_1(self):
+ move_form = Form(self.invoice)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ line_form.price_unit = 1200
+ line_form.tax_ids.add(self.tax_armageddon)
+ move_form.save()
+
+ child_tax_1 = self.tax_armageddon.children_tax_ids[0]
+ child_tax_2 = self.tax_armageddon.children_tax_ids[1]
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'price_unit': 1200.0,
+ 'price_subtotal': 1000.0,
+ 'price_total': 1470.0,
+ 'tax_ids': (self.tax_sale_a + self.tax_armageddon).ids,
+ 'tax_exigible': False,
+ },
+ self.product_line_vals_2,
+ self.tax_line_vals_1,
+ self.tax_line_vals_2,
+ {
+ 'name': child_tax_1.name,
+ 'product_id': False,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'partner_id': self.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 120.0,
+ 'price_subtotal': 120.0,
+ 'price_total': 132.0,
+ 'tax_ids': child_tax_2.ids,
+ 'tax_line_id': child_tax_1.id,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': -120.0,
+ 'debit': 0.0,
+ 'credit': 120.0,
+ 'date_maturity': False,
+ 'tax_exigible': False,
+ },
+ {
+ 'name': child_tax_1.name,
+ 'product_id': False,
+ 'account_id': self.company_data['default_account_tax_sale'].id,
+ 'partner_id': self.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 80.0,
+ 'price_subtotal': 80.0,
+ 'price_total': 88.0,
+ 'tax_ids': child_tax_2.ids,
+ 'tax_line_id': child_tax_1.id,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': -80.0,
+ 'debit': 0.0,
+ 'credit': 80.0,
+ 'date_maturity': False,
+ 'tax_exigible': False,
+ },
+ {
+ 'name': child_tax_2.name,
+ 'product_id': False,
+ 'account_id': child_tax_2.cash_basis_transition_account_id.id,
+ 'partner_id': self.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 120.0,
+ 'price_subtotal': 120.0,
+ 'price_total': 120.0,
+ 'tax_ids': [],
+ 'tax_line_id': child_tax_2.id,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': -120.0,
+ 'debit': 0.0,
+ 'credit': 120.0,
+ 'date_maturity': False,
+ 'tax_exigible': False,
+ },
+ {
+ **self.term_line_vals_1,
+ 'price_unit': -1730.0,
+ 'price_subtotal': -1730.0,
+ 'price_total': -1730.0,
+ 'amount_currency': 1730.0,
+ 'debit': 1730.0,
+ },
+ ], {
+ **self.move_vals,
+ 'amount_untaxed': 1200.0,
+ 'amount_tax': 530.0,
+ 'amount_total': 1730.0,
+ })
+
+ def test_out_invoice_line_onchange_rounding_price_subtotal_1(self):
+ ''' Seek for rounding issue on the price_subtotal when dealing with a price_unit having more digits than the
+ foreign currency one.
+ '''
+ decimal_precision_name = self.env['account.move.line']._fields['price_unit']._digits
+ decimal_precision = self.env['decimal.precision'].search([('name', '=', decimal_precision_name)])
+
+ self.assertTrue(decimal_precision, "Decimal precision '%s' not found" % decimal_precision_name)
+
+ self.currency_data['currency'].rounding = 0.01
+ decimal_precision.digits = 4
+
+ def check_invoice_values(invoice):
+ self.assertInvoiceValues(invoice, [
+ {
+ 'quantity': 1.0,
+ 'price_unit': 0.025,
+ 'price_subtotal': 0.03,
+ 'debit': 0.0,
+ 'credit': 0.02,
+ 'currency_id': self.currency_data['currency'].id,
+ },
+ {
+ 'quantity': 1.0,
+ 'price_unit': -0.03,
+ 'price_subtotal': -0.03,
+ 'debit': 0.02,
+ 'credit': 0.0,
+ 'currency_id': self.currency_data['currency'].id,
+ },
+ ], {
+ 'amount_untaxed': 0.03,
+ 'amount_tax': 0.0,
+ 'amount_total': 0.03,
+ })
+
+ # == Test at the creation of the invoice ==
+
+ invoice_1 = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'invoice_date': '2017-01-01',
+ 'date': '2017-01-01',
+ 'partner_id': self.partner_a.id,
+ 'currency_id': self.currency_data['currency'].id,
+ 'invoice_line_ids': [(0, 0, {
+ 'name': 'test line',
+ 'price_unit': 0.025,
+ 'quantity': 1,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ })],
+ })
+
+ check_invoice_values(invoice_1)
+
+ # == Test when writing on the invoice ==
+
+ invoice_2 = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'invoice_date': '2017-01-01',
+ 'date': '2017-01-01',
+ 'partner_id': self.partner_a.id,
+ 'currency_id': self.currency_data['currency'].id,
+ })
+ invoice_2.write({
+ 'invoice_line_ids': [(0, 0, {
+ 'name': 'test line',
+ 'price_unit': 0.025,
+ 'quantity': 1,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ })],
+ })
+
+ check_invoice_values(invoice_2)
+
+ def test_out_invoice_line_onchange_rounding_price_subtotal_2(self):
+ """ Ensure the cyclic computations implemented using onchanges are not leading to rounding issues when using
+ price-included taxes.
+ For example:
+ 100 / 1.21 ~= 82.64 but 82.64 * 1.21 ~= 99.99 != 100.0.
+ """
+
+ def check_invoice_values(invoice):
+ self.assertInvoiceValues(invoice, [
+ {
+ 'price_unit': 100.0,
+ 'price_subtotal': 82.64,
+ 'debit': 0.0,
+ 'credit': 82.64,
+ },
+ {
+ 'price_unit': 17.36,
+ 'price_subtotal': 17.36,
+ 'debit': 0.0,
+ 'credit': 17.36,
+ },
+ {
+ 'price_unit': -100.0,
+ 'price_subtotal': -100.0,
+ 'debit': 100.0,
+ 'credit': 0.0,
+ },
+ ], {
+ 'amount_untaxed': 82.64,
+ 'amount_tax': 17.36,
+ 'amount_total': 100.0,
+ })
+
+ tax = self.env['account.tax'].create({
+ 'name': '21%',
+ 'amount': 21.0,
+ 'price_include': True,
+ 'include_base_amount': True,
+ })
+
+ # == Test assigning tax directly ==
+
+ invoice_create = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'invoice_date': '2017-01-01',
+ 'date': '2017-01-01',
+ 'partner_id': self.partner_a.id,
+ 'invoice_line_ids': [(0, 0, {
+ 'name': 'test line',
+ 'price_unit': 100.0,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'tax_ids': [(6, 0, tax.ids)],
+ })],
+ })
+
+ check_invoice_values(invoice_create)
+
+ move_form = Form(self.env['account.move'].with_context(default_move_type='out_invoice'))
+ move_form.invoice_date = fields.Date.from_string('2017-01-01')
+ move_form.partner_id = self.partner_a
+ with move_form.invoice_line_ids.new() as line_form:
+ line_form.name = 'test line'
+ line_form.price_unit = 100.0
+ line_form.account_id = self.company_data['default_account_revenue']
+ line_form.tax_ids.clear()
+ line_form.tax_ids.add(tax)
+ invoice_onchange = move_form.save()
+
+ check_invoice_values(invoice_onchange)
+
+ # == Test when the tax is set on a product ==
+
+ product = self.env['product.product'].create({
+ 'name': 'product',
+ 'lst_price': 100.0,
+ 'property_account_income_id': self.company_data['default_account_revenue'].id,
+ 'taxes_id': [(6, 0, tax.ids)],
+ })
+
+ move_form = Form(self.env['account.move'].with_context(default_move_type='out_invoice'))
+ move_form.invoice_date = fields.Date.from_string('2017-01-01')
+ move_form.partner_id = self.partner_a
+ with move_form.invoice_line_ids.new() as line_form:
+ line_form.product_id = product
+ invoice_onchange = move_form.save()
+
+ check_invoice_values(invoice_onchange)
+
+ # == Test with a fiscal position ==
+
+ fiscal_position = self.env['account.fiscal.position'].create({'name': 'fiscal_position'})
+
+ move_form = Form(self.env['account.move'].with_context(default_move_type='out_invoice'))
+ move_form.invoice_date = fields.Date.from_string('2017-01-01')
+ move_form.partner_id = self.partner_a
+ move_form.fiscal_position_id = fiscal_position
+ with move_form.invoice_line_ids.new() as line_form:
+ line_form.product_id = product
+ invoice_onchange = move_form.save()
+
+ check_invoice_values(invoice_onchange)
+
+ def test_out_invoice_line_onchange_taxes_2_price_unit_tax_included(self):
+ ''' Seek for rounding issue in the price unit. Suppose a price_unit of 2300 with a 5.5% price-included tax
+ applied on it.
+
+ The computed balance will be computed as follow: 2300.0 / 1.055 = 2180.0948 ~ 2180.09.
+ Since accounting / business fields are synchronized, the inverse computation will try to recompute the
+ price_unit based on the balance: 2180.09 * 1.055 = 2299.99495 ~ 2299.99.
+
+ This test ensures the price_unit is not overridden in such case.
+ '''
+ tax_price_include = self.env['account.tax'].create({
+ 'name': 'Tax 5.5% price included',
+ 'amount': 5.5,
+ 'amount_type': 'percent',
+ 'price_include': True,
+ })
+
+ # == Single-currency ==
+
+ # price_unit=2300 with 15% tax (excluded) + 5.5% tax (included).
+ move_form = Form(self.invoice)
+ move_form.invoice_line_ids.remove(1)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ line_form.price_unit = 2300
+ line_form.tax_ids.add(tax_price_include)
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'price_unit': 2300.0,
+ 'price_subtotal': 2180.09,
+ 'price_total': 2627.01,
+ 'tax_ids': (self.product_a.taxes_id + tax_price_include).ids,
+ 'amount_currency': -2180.09,
+ 'credit': 2180.09,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'price_unit': 327.01,
+ 'price_subtotal': 327.01,
+ 'price_total': 327.01,
+ 'amount_currency': -327.01,
+ 'credit': 327.01,
+ },
+ {
+ 'name': tax_price_include.name,
+ 'product_id': False,
+ 'account_id': self.product_line_vals_1['account_id'],
+ 'partner_id': self.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 119.91,
+ 'price_subtotal': 119.91,
+ 'price_total': 119.91,
+ 'tax_ids': [],
+ 'tax_line_id': tax_price_include.id,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': -119.91,
+ 'debit': 0.0,
+ 'credit': 119.91,
+ 'date_maturity': False,
+ 'tax_exigible': True,
+ },
+ {
+ **self.term_line_vals_1,
+ 'price_unit': -2627.01,
+ 'price_subtotal': -2627.01,
+ 'price_total': -2627.01,
+ 'amount_currency': 2627.01,
+ 'debit': 2627.01,
+ },
+ ], {
+ **self.move_vals,
+ 'amount_untaxed': 2180.09,
+ 'amount_tax': 446.92,
+ 'amount_total': 2627.01,
+ })
+
+ move_form = Form(self.invoice)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ line_form.price_unit = -2300
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'price_unit': -2300.0,
+ 'price_subtotal': -2180.09,
+ 'price_total': -2627.01,
+ 'tax_ids': (self.product_a.taxes_id + tax_price_include).ids,
+ 'amount_currency': 2180.09,
+ 'debit': 2180.09,
+ 'credit': 0.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'price_unit': -327.01,
+ 'price_subtotal': -327.01,
+ 'price_total': -327.01,
+ 'amount_currency': 327.01,
+ 'debit': 327.01,
+ 'credit': 0.0,
+ },
+ {
+ 'name': tax_price_include.name,
+ 'product_id': False,
+ 'account_id': self.product_line_vals_1['account_id'],
+ 'partner_id': self.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': -119.91,
+ 'price_subtotal': -119.91,
+ 'price_total': -119.91,
+ 'tax_ids': [],
+ 'tax_line_id': tax_price_include.id,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': 119.91,
+ 'debit': 119.91,
+ 'credit': 0.0,
+ 'date_maturity': False,
+ 'tax_exigible': True,
+ },
+ {
+ **self.term_line_vals_1,
+ 'price_unit': 2627.01,
+ 'price_subtotal': 2627.01,
+ 'price_total': 2627.01,
+ 'amount_currency': -2627.01,
+ 'debit': 0.0,
+ 'credit': 2627.01,
+ },
+ ], {
+ **self.move_vals,
+ 'amount_untaxed': -2180.09,
+ 'amount_tax': -446.92,
+ 'amount_total': -2627.01,
+ })
+
+ # == Multi-currencies ==
+
+ move_form = Form(self.invoice)
+ move_form.currency_id = self.currency_data['currency']
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ line_form.price_unit = 2300
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'price_unit': 2300.0,
+ 'price_subtotal': 2180.095,
+ 'price_total': 2627.014,
+ 'tax_ids': (self.product_a.taxes_id + tax_price_include).ids,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -2180.095,
+ 'credit': 1090.05,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'price_unit': 327.014,
+ 'price_subtotal': 327.014,
+ 'price_total': 327.014,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -327.014,
+ 'credit': 163.51,
+ },
+ {
+ 'name': tax_price_include.name,
+ 'product_id': False,
+ 'account_id': self.product_line_vals_1['account_id'],
+ 'partner_id': self.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 119.905,
+ 'price_subtotal': 119.905,
+ 'price_total': 119.905,
+ 'tax_ids': [],
+ 'tax_line_id': tax_price_include.id,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -119.905,
+ 'debit': 0.0,
+ 'credit': 59.95,
+ 'date_maturity': False,
+ 'tax_exigible': True,
+ },
+ {
+ **self.term_line_vals_1,
+ 'price_unit': -2627.014,
+ 'price_subtotal': -2627.014,
+ 'price_total': -2627.014,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 2627.014,
+ 'debit': 1313.51,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_untaxed': 2180.095,
+ 'amount_tax': 446.919,
+ 'amount_total': 2627.014,
+ })
+
+ move_form = Form(self.invoice)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ line_form.price_unit = -2300
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'price_unit': -2300.0,
+ 'price_subtotal': -2180.095,
+ 'price_total': -2627.014,
+ 'tax_ids': (self.product_a.taxes_id + tax_price_include).ids,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 2180.095,
+ 'debit': 1090.05,
+ 'credit': 0.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'price_unit': -327.014,
+ 'price_subtotal': -327.014,
+ 'price_total': -327.014,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 327.014,
+ 'debit': 163.51,
+ 'credit': 0.0,
+ },
+ {
+ 'name': tax_price_include.name,
+ 'product_id': False,
+ 'account_id': self.product_line_vals_1['account_id'],
+ 'partner_id': self.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': -119.905,
+ 'price_subtotal': -119.905,
+ 'price_total': -119.905,
+ 'tax_ids': [],
+ 'tax_line_id': tax_price_include.id,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 119.905,
+ 'debit': 59.95,
+ 'credit': 0.0,
+ 'date_maturity': False,
+ 'tax_exigible': True,
+ },
+ {
+ **self.term_line_vals_1,
+ 'price_unit': 2627.014,
+ 'price_subtotal': 2627.014,
+ 'price_total': 2627.014,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -2627.014,
+ 'debit': 0.0,
+ 'credit': 1313.51,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_untaxed': -2180.095,
+ 'amount_tax': -446.919,
+ 'amount_total': -2627.014,
+ })
+
+ 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_account = self.env['account.analytic.account'].create({
+ 'name': 'test_analytic_account',
+ 'partner_id': self.invoice.partner_id.id,
+ 'code': 'TEST'
+ })
+
+ 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)
+ 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,
+ },
+ {
+ **self.product_line_vals_2,
+ 'analytic_account_id': False,
+ 'analytic_tag_ids': [],
+ },
+ {
+ **self.tax_line_vals_1,
+ 'analytic_account_id': False,
+ 'analytic_tag_ids': [],
+ },
+ {
+ **self.tax_line_vals_2,
+ 'analytic_account_id': False,
+ 'analytic_tag_ids': [],
+ },
+ {
+ **self.term_line_vals_1,
+ 'analytic_account_id': False,
+ 'analytic_tag_ids': [],
+ },
+ ], 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()
+ move_form.save()
+
+ # Enable the analytic
+ self.tax_sale_a.analytic = True
+
+ 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)
+ move_form.save()
+
+ # The tax is flagged as an analytic one.
+ # A new tax line must be generated.
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'analytic_account_id': analytic_account.id,
+ 'analytic_tag_ids': analytic_tag.ids,
+ },
+ {
+ **self.product_line_vals_2,
+ 'analytic_account_id': False,
+ 'analytic_tag_ids': [],
+ },
+ {
+ **self.tax_line_vals_1,
+ 'price_unit': 150.0,
+ 'price_subtotal': 150.0,
+ 'price_total': 150.0,
+ 'amount_currency': -150.0,
+ 'credit': 150.0,
+ 'analytic_account_id': analytic_account.id,
+ 'analytic_tag_ids': analytic_tag.ids,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'price_unit': 30.0,
+ 'price_subtotal': 30.0,
+ 'price_total': 30.0,
+ 'amount_currency': -30.0,
+ 'credit': 30.0,
+ 'analytic_account_id': False,
+ 'analytic_tag_ids': [],
+ },
+ {
+ **self.tax_line_vals_2,
+ 'analytic_account_id': False,
+ 'analytic_tag_ids': [],
+ },
+ {
+ **self.term_line_vals_1,
+ 'analytic_account_id': False,
+ 'analytic_tag_ids': [],
+ },
+ ], 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()
+ 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': [],
+ },
+ {
+ **self.product_line_vals_2,
+ 'analytic_account_id': False,
+ 'analytic_tag_ids': [],
+ },
+ {
+ **self.tax_line_vals_1,
+ 'analytic_account_id': False,
+ 'analytic_tag_ids': [],
+ },
+ {
+ **self.tax_line_vals_2,
+ 'analytic_account_id': False,
+ 'analytic_tag_ids': [],
+ },
+ {
+ **self.term_line_vals_1,
+ 'analytic_account_id': False,
+ 'analytic_tag_ids': [],
+ },
+ ], 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_account = self.env['account.analytic.account'].create({
+ 'name': 'test_analytic_account1',
+ 'code': 'TEST1'
+ })
+
+ self.invoice.write({'invoice_line_ids': [(1, self.invoice.invoice_line_ids.ids[0], {
+ 'analytic_account_id': analytic_account.id,
+ })]})
+
+ self.assertRecordValues(self.invoice.invoice_line_ids, [
+ {'analytic_account_id': analytic_account.id},
+ {'analytic_account_id': 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,
+ })]})
+
+ self.assertRecordValues(self.invoice.invoice_line_ids, [
+ {'analytic_account_id': False},
+ {'analytic_account_id': False},
+ ])
+
+ def test_out_invoice_line_onchange_cash_rounding_1(self):
+ move_form = Form(self.invoice)
+ # Add a cash rounding having 'add_invoice_line'.
+ move_form.invoice_cash_rounding_id = self.cash_rounding_a
+ move_form.save()
+
+ # The cash rounding does nothing as the total is already rounded.
+ self.assertInvoiceValues(self.invoice, [
+ self.product_line_vals_1,
+ self.product_line_vals_2,
+ self.tax_line_vals_1,
+ self.tax_line_vals_2,
+ self.term_line_vals_1,
+ ], self.move_vals)
+
+ move_form = Form(self.invoice)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ line_form.price_unit = 999.99
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ 'name': 'add_invoice_line',
+ 'product_id': False,
+ 'account_id': self.cash_rounding_a.profit_account_id.id,
+ 'partner_id': self.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 0.01,
+ 'price_subtotal': 0.01,
+ 'price_total': 0.01,
+ 'tax_ids': [],
+ 'tax_line_id': False,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': -0.01,
+ 'debit': 0.0,
+ 'credit': 0.01,
+ 'date_maturity': False,
+ 'tax_exigible': True,
+ },
+ {
+ **self.product_line_vals_1,
+ 'price_unit': 999.99,
+ 'price_subtotal': 999.99,
+ 'price_total': 1149.99,
+ 'amount_currency': -999.99,
+ 'credit': 999.99,
+ },
+ self.product_line_vals_2,
+ self.tax_line_vals_1,
+ self.tax_line_vals_2,
+ self.term_line_vals_1,
+ ], self.move_vals)
+
+ move_form = Form(self.invoice)
+ # Change the cash rounding to one having 'biggest_tax'.
+ move_form.invoice_cash_rounding_id = self.cash_rounding_b
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'price_unit': 999.99,
+ 'price_subtotal': 999.99,
+ 'price_total': 1149.99,
+ 'amount_currency': -999.99,
+ 'credit': 999.99,
+ },
+ self.product_line_vals_2,
+ self.tax_line_vals_1,
+ self.tax_line_vals_2,
+ {
+ 'name': '%s (rounding)' % self.tax_sale_a.name,
+ 'product_id': False,
+ 'account_id': self.company_data['default_account_tax_sale'].id,
+ 'partner_id': self.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': -0.04,
+ 'price_subtotal': -0.04,
+ 'price_total': -0.04,
+ 'tax_ids': [],
+ 'tax_line_id': self.tax_sale_a.id,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': 0.04,
+ 'debit': 0.04,
+ 'credit': 0.0,
+ 'date_maturity': False,
+ 'tax_exigible': True,
+ },
+ {
+ **self.term_line_vals_1,
+ 'price_unit': -1409.95,
+ 'price_subtotal': -1409.95,
+ 'price_total': -1409.95,
+ 'amount_currency': 1409.95,
+ 'debit': 1409.95,
+ },
+ ], {
+ **self.move_vals,
+ 'amount_untaxed': 1199.99,
+ 'amount_tax': 209.96,
+ 'amount_total': 1409.95,
+ })
+
+ def test_out_invoice_line_onchange_currency_1(self):
+ move_form = Form(self.invoice.with_context(dudu=True))
+ move_form.currency_id = self.currency_data['currency']
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -1000.0,
+ 'credit': 500.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -200.0,
+ 'credit': 100.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -180.0,
+ 'credit': 90.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -30.0,
+ 'credit': 15.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 1410.0,
+ 'debit': 705.0,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ })
+
+ move_form = Form(self.invoice)
+ # Change the date to get another rate: 1/3 instead of 1/2.
+ move_form.date = fields.Date.from_string('2016-01-01')
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -1000.0,
+ 'credit': 333.33,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -200.0,
+ 'credit': 66.67,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -180.0,
+ 'credit': 60.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -30.0,
+ 'credit': 10.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 1410.0,
+ 'debit': 470.0,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ 'date': fields.Date.from_string('2016-01-01'),
+ })
+
+ move_form = Form(self.invoice)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ # 0.045 * 0.1 = 0.0045. As the foreign currency has a 0.001 rounding,
+ # the result should be 0.005 after rounding.
+ line_form.quantity = 0.1
+ line_form.price_unit = 0.045
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'quantity': 0.1,
+ 'price_unit': 0.05,
+ 'price_subtotal': 0.005,
+ 'price_total': 0.006,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -0.005,
+ 'credit': 0.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -200.0,
+ 'credit': 66.67,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'price_unit': 30.0,
+ 'price_subtotal': 30.001,
+ 'price_total': 30.001,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -30.001,
+ 'credit': 10.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -30.0,
+ 'credit': 10.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'price_unit': -260.01,
+ 'price_subtotal': -260.006,
+ 'price_total': -260.006,
+ 'amount_currency': 260.006,
+ 'debit': 86.67,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ 'date': fields.Date.from_string('2016-01-01'),
+ 'amount_untaxed': 200.005,
+ 'amount_tax': 60.001,
+ 'amount_total': 260.006,
+ })
+
+ # Exit the multi-currencies.
+ move_form = Form(self.invoice)
+ move_form.currency_id = self.company_data['currency']
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'quantity': 0.1,
+ 'price_unit': 0.05,
+ 'price_subtotal': 0.01,
+ 'price_total': 0.01,
+ 'amount_currency': -0.01,
+ 'credit': 0.01,
+ },
+ self.product_line_vals_2,
+ {
+ **self.tax_line_vals_1,
+ 'price_unit': 30.0,
+ 'price_subtotal': 30.0,
+ 'price_total': 30.0,
+ 'amount_currency': -30.0,
+ 'credit': 30.0,
+ },
+ self.tax_line_vals_2,
+ {
+ **self.term_line_vals_1,
+ 'price_unit': -260.01,
+ 'price_subtotal': -260.01,
+ 'price_total': -260.01,
+ 'amount_currency': 260.01,
+ 'debit': 260.01,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.company_data['currency'].id,
+ 'date': fields.Date.from_string('2016-01-01'),
+ 'amount_untaxed': 200.01,
+ 'amount_tax': 60.0,
+ 'amount_total': 260.01,
+ })
+
+ def test_out_invoice_create_refund(self):
+ self.invoice.action_post()
+
+ move_reversal = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=self.invoice.ids).create({
+ 'date': fields.Date.from_string('2019-02-01'),
+ 'reason': 'no reason',
+ 'refund_method': 'refund',
+ })
+ reversal = move_reversal.reverse_moves()
+ reverse_move = self.env['account.move'].browse(reversal['res_id'])
+
+ self.assertEqual(self.invoice.payment_state, 'not_paid', "Refunding with a draft credit note should keep the invoice 'not_paid'.")
+ self.assertInvoiceValues(reverse_move, [
+ {
+ **self.product_line_vals_1,
+ 'amount_currency': 1000.0,
+ 'debit': 1000.0,
+ 'credit': 0.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'amount_currency': 200.0,
+ 'debit': 200.0,
+ 'credit': 0.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'amount_currency': 180.0,
+ 'debit': 180.0,
+ 'credit': 0.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'amount_currency': 30.0,
+ 'debit': 30.0,
+ 'credit': 0.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'name': '',
+ 'amount_currency': -1410.0,
+ 'debit': 0.0,
+ 'credit': 1410.0,
+ 'date_maturity': move_reversal.date,
+ },
+ ], {
+ **self.move_vals,
+ 'invoice_payment_term_id': None,
+ 'name': 'RINV/2019/02/0001',
+ 'date': move_reversal.date,
+ 'state': 'draft',
+ 'ref': 'Reversal of: %s, %s' % (self.invoice.name, move_reversal.reason),
+ 'payment_state': 'not_paid',
+ })
+
+ move_reversal = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=self.invoice.ids).create({
+ 'date': fields.Date.from_string('2019-02-01'),
+ 'reason': 'no reason',
+ 'refund_method': 'cancel',
+ })
+ reversal = move_reversal.reverse_moves()
+ reverse_move = self.env['account.move'].browse(reversal['res_id'])
+
+ self.assertEqual(self.invoice.payment_state, 'reversed', "After cancelling it with a reverse invoice, an invoice should be in 'reversed' state.")
+ self.assertInvoiceValues(reverse_move, [
+ {
+ **self.product_line_vals_1,
+ 'amount_currency': 1000.0,
+ 'debit': 1000.0,
+ 'credit': 0.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'amount_currency': 200.0,
+ 'debit': 200.0,
+ 'credit': 0.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'amount_currency': 180.0,
+ 'debit': 180.0,
+ 'credit': 0.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'amount_currency': 30.0,
+ 'debit': 30.0,
+ 'credit': 0.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'name': '',
+ 'amount_currency': -1410.0,
+ 'debit': 0.0,
+ 'credit': 1410.0,
+ 'date_maturity': move_reversal.date,
+ },
+ ], {
+ **self.move_vals,
+ 'invoice_payment_term_id': None,
+ 'date': move_reversal.date,
+ 'state': 'posted',
+ 'ref': 'Reversal of: %s, %s' % (self.invoice.name, move_reversal.reason),
+ 'payment_state': 'paid',
+ })
+
+ def test_out_invoice_create_refund_multi_currency(self):
+ ''' Test the account.move.reversal takes care about the currency rates when setting
+ a custom reversal date.
+ '''
+ move_form = Form(self.invoice)
+ move_form.date = '2016-01-01'
+ move_form.currency_id = self.currency_data['currency']
+ move_form.save()
+
+ self.invoice.action_post()
+
+ # The currency rate changed from 1/3 to 1/2.
+ move_reversal = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=self.invoice.ids).create({
+ 'date': fields.Date.from_string('2017-01-01'),
+ 'reason': 'no reason',
+ 'refund_method': 'refund',
+ })
+ reversal = move_reversal.reverse_moves()
+ reverse_move = self.env['account.move'].browse(reversal['res_id'])
+
+ self.assertEqual(self.invoice.payment_state, 'not_paid', "Refunding with a draft credit note should keep the invoice 'not_paid'.")
+ self.assertInvoiceValues(reverse_move, [
+ {
+ **self.product_line_vals_1,
+ 'amount_currency': 1000.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'debit': 500.0,
+ 'credit': 0.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'amount_currency': 200.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'debit': 100.0,
+ 'credit': 0.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'amount_currency': 180.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'debit': 90.0,
+ 'credit': 0.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'amount_currency': 30.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'debit': 15.0,
+ 'credit': 0.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'name': '',
+ 'amount_currency': -1410.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'debit': 0.0,
+ 'credit': 705.0,
+ 'date_maturity': move_reversal.date,
+ },
+ ], {
+ **self.move_vals,
+ 'invoice_payment_term_id': None,
+ 'currency_id': self.currency_data['currency'].id,
+ 'date': move_reversal.date,
+ 'state': 'draft',
+ 'ref': 'Reversal of: %s, %s' % (self.invoice.name, move_reversal.reason),
+ 'payment_state': 'not_paid',
+ })
+
+ move_reversal = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=self.invoice.ids).create({
+ 'date': fields.Date.from_string('2017-01-01'),
+ 'reason': 'no reason',
+ 'refund_method': 'cancel',
+ })
+ reversal = move_reversal.reverse_moves()
+ reverse_move = self.env['account.move'].browse(reversal['res_id'])
+
+ self.assertEqual(self.invoice.payment_state, 'reversed', "After cancelling it with a reverse invoice, an invoice should be in 'reversed' state.")
+ self.assertInvoiceValues(reverse_move, [
+ {
+ **self.product_line_vals_1,
+ 'amount_currency': 1000.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'debit': 500.0,
+ 'credit': 0.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'amount_currency': 200.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'debit': 100.0,
+ 'credit': 0.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'amount_currency': 180.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'debit': 90.0,
+ 'credit': 0.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'amount_currency': 30.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'debit': 15.0,
+ 'credit': 0.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'name': '',
+ 'amount_currency': -1410.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'debit': 0.0,
+ 'credit': 705.0,
+ 'date_maturity': move_reversal.date,
+ },
+ ], {
+ **self.move_vals,
+ 'invoice_payment_term_id': None,
+ 'currency_id': self.currency_data['currency'].id,
+ 'date': move_reversal.date,
+ 'state': 'posted',
+ 'ref': 'Reversal of: %s, %s' % (self.invoice.name, move_reversal.reason),
+ 'payment_state': 'paid',
+ })
+
+ def test_out_invoice_create_refund_auto_post(self):
+ self.invoice.action_post()
+
+ move_reversal = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=self.invoice.ids).create({
+ 'date': fields.Date.today() + timedelta(days=7),
+ 'reason': 'no reason',
+ 'refund_method': 'modify',
+ })
+ move_reversal.reverse_moves()
+ refund = self.env['account.move'].search([('move_type', '=', 'out_refund'), ('company_id', '=', self.invoice.company_id.id)])
+
+ self.assertRecordValues(refund, [{
+ 'state': 'draft',
+ 'auto_post': True,
+ }])
+
+ def test_out_invoice_create_1(self):
+ # Test creating an account_move with the least information.
+ move = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'partner_id': self.partner_a.id,
+ 'invoice_date': fields.Date.from_string('2019-01-01'),
+ 'currency_id': self.currency_data['currency'].id,
+ 'invoice_payment_term_id': self.pay_terms_a.id,
+ 'invoice_line_ids': [
+ (0, None, {
+ 'product_id': self.product_a.id,
+ 'product_uom_id': self.product_a.uom_id.id,
+ 'quantity': 1.0,
+ 'price_unit': 1000.0,
+ 'tax_ids': [(6, 0, self.product_a.taxes_id.ids)],
+ }),
+ (0, None, {
+ 'product_id': self.product_b.id,
+ 'product_uom_id': self.product_b.uom_id.id,
+ 'quantity': 1.0,
+ 'price_unit': 200.0,
+ 'tax_ids': [(6, 0, self.product_b.taxes_id.ids)],
+ }),
+ ]
+ })
+
+ self.assertInvoiceValues(move, [
+ {
+ **self.product_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -1000.0,
+ 'credit': 500.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -200.0,
+ 'credit': 100.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -180.0,
+ 'credit': 90.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -30.0,
+ 'credit': 15.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 1410.0,
+ 'debit': 705.0,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ })
+
+ def test_out_invoice_create_child_partner(self):
+ # Test creating an account_move on a child partner.
+ # This needs to attach the lines to the parent partner id.
+ partner_a_child = self.env['res.partner'].create({
+ 'name': 'partner_a_child',
+ 'parent_id': self.partner_a.id
+ })
+ move = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'partner_id': partner_a_child.id,
+ 'invoice_date': fields.Date.from_string('2019-01-01'),
+ 'currency_id': self.currency_data['currency'].id,
+ 'invoice_payment_term_id': self.pay_terms_a.id,
+ 'invoice_line_ids': [
+ (0, None, self.product_line_vals_1),
+ (0, None, self.product_line_vals_2),
+ ]
+ })
+
+ self.assertEqual(partner_a_child.id, move.partner_id.id, 'Keep child partner on the account move record')
+ self.assertEqual(self.partner_a.id, move.line_ids[0].partner_id.id, 'Set parent partner on the account move line records')
+
+ def test_out_invoice_write_1(self):
+ # Test creating an account_move with the least information.
+ move = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'partner_id': self.partner_a.id,
+ 'invoice_date': fields.Date.from_string('2019-01-01'),
+ 'currency_id': self.currency_data['currency'].id,
+ 'invoice_payment_term_id': self.pay_terms_a.id,
+ 'invoice_line_ids': [
+ (0, None, self.product_line_vals_1),
+ ]
+ })
+ move.write({
+ 'invoice_line_ids': [
+ (0, None, self.product_line_vals_2),
+ ]
+ })
+
+ self.assertInvoiceValues(move, [
+ {
+ **self.product_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -1000.0,
+ 'credit': 500.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -200.0,
+ 'credit': 100.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -180.0,
+ 'credit': 90.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -30.0,
+ 'credit': 15.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 1410.0,
+ 'debit': 705.0,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ })
+
+ def test_out_invoice_write_2(self):
+ ''' Ensure to not messing the invoice when writing a bad account type. '''
+ move = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'partner_id': self.partner_a.id,
+ 'invoice_line_ids': [
+ (0, None, {
+ 'name': 'test_out_invoice_write_2',
+ 'quantity': 1.0,
+ 'price_unit': 2000,
+ }),
+ ]
+ })
+
+ receivable_lines = move.line_ids.filtered(lambda line: line.account_id.user_type_id.type == 'receivable')
+ not_receivable_lines = move.line_ids - receivable_lines
+
+ # Write a receivable account on a not-receivable line.
+ with self.assertRaises(UserError), self.cr.savepoint():
+ not_receivable_lines.write({'account_id': receivable_lines[0].account_id.copy().id})
+
+ # Write a not-receivable account on a receivable line.
+ with self.assertRaises(UserError), self.cr.savepoint():
+ receivable_lines.write({'account_id': not_receivable_lines[0].account_id.copy().id})
+
+ # Write another receivable account on a receivable line.
+ receivable_lines.write({'account_id': receivable_lines[0].account_id.copy().id})
+
+ def test_out_invoice_post_1(self):
+ ''' Check the invoice_date will be set automatically at the post date. '''
+ frozen_today = fields.Date.today()
+ with patch.object(fields.Date, 'today', lambda *args, **kwargs: frozen_today), patch.object(fields.Date, 'context_today', lambda *args, **kwargs: frozen_today):
+ # Create an invoice with rate 1/3.
+ move = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'partner_id': self.partner_a.id,
+ 'invoice_date': fields.Date.from_string('2016-01-01'),
+ 'currency_id': self.currency_data['currency'].id,
+ 'invoice_payment_term_id': self.pay_terms_a.id,
+ 'invoice_line_ids': [
+ (0, None, self.product_line_vals_1),
+ (0, None, self.product_line_vals_2),
+ ]
+ })
+
+ # Remove the invoice_date to check:
+ # - The invoice_date must be set automatically at today during the post.
+ # - As the invoice_date changed, date did too so the currency rate has changed (1/3 => 1/2).
+ # - A different invoice_date implies also a new date_maturity.
+ # Add a manual edition of a tax line:
+ # - The modification must be preserved in the business fields.
+ # - The journal entry must be balanced before / after the post.
+ move.write({
+ 'invoice_date': False,
+ 'line_ids': [
+ (1, move.line_ids.filtered(lambda line: line.tax_line_id.id == self.tax_line_vals_1['tax_line_id']).id, {
+ 'amount_currency': -200.0,
+ }),
+ (1, move.line_ids.filtered(lambda line: line.date_maturity).id, {
+ 'amount_currency': 1430.0,
+ }),
+ ],
+ })
+
+ move.action_post()
+
+ self.assertInvoiceValues(move, [
+ {
+ **self.product_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -1000.0,
+ 'credit': 500.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -200.0,
+ 'credit': 100.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'price_unit': 200.0,
+ 'price_subtotal': 200.0,
+ 'price_total': 200.0,
+ 'amount_currency': -200.0,
+ 'credit': 100.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -30.0,
+ 'credit': 15.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'name': move.name,
+ 'currency_id': self.currency_data['currency'].id,
+ 'price_unit': -1430.0,
+ 'price_subtotal': -1430.0,
+ 'price_total': -1430.0,
+ 'amount_currency': 1430.0,
+ 'debit': 715.0,
+ 'date_maturity': frozen_today,
+ },
+ ], {
+ **self.move_vals,
+ 'payment_reference': move.name,
+ 'currency_id': self.currency_data['currency'].id,
+ 'date': frozen_today,
+ 'invoice_date': frozen_today,
+ 'invoice_date_due': frozen_today,
+ 'amount_tax': 230.0,
+ 'amount_total': 1430.0,
+ })
+
+ def test_out_invoice_post_2(self):
+ ''' Check the date will be set automatically at the next available post date due to the tax lock date. '''
+ # Create an invoice with rate 1/3.
+ move = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'partner_id': self.partner_a.id,
+ 'invoice_date': fields.Date.from_string('2016-01-01'),
+ 'date': fields.Date.from_string('2015-01-01'),
+ 'currency_id': self.currency_data['currency'].id,
+ 'invoice_payment_term_id': self.pay_terms_a.id,
+ 'invoice_line_ids': [
+ (0, None, {
+ 'name': self.product_line_vals_1['name'],
+ 'product_id': self.product_line_vals_1['product_id'],
+ 'product_uom_id': self.product_line_vals_1['product_uom_id'],
+ 'quantity': self.product_line_vals_1['quantity'],
+ 'price_unit': self.product_line_vals_1['price_unit'],
+ 'tax_ids': self.product_line_vals_1['tax_ids'],
+ }),
+ (0, None, {
+ 'name': self.product_line_vals_2['name'],
+ 'product_id': self.product_line_vals_2['product_id'],
+ 'product_uom_id': self.product_line_vals_2['product_uom_id'],
+ 'quantity': self.product_line_vals_2['quantity'],
+ 'price_unit': self.product_line_vals_2['price_unit'],
+ 'tax_ids': self.product_line_vals_2['tax_ids'],
+ }),
+ ],
+ })
+
+ # Add a manual edition of a tax line:
+ # - The modification must be preserved in the business fields.
+ # - The journal entry must be balanced before / after the post.
+ move.write({
+ 'line_ids': [
+ (1, move.line_ids.filtered(lambda line: line.tax_line_id.id == self.tax_line_vals_1['tax_line_id']).id, {
+ 'amount_currency': -200.0,
+ }),
+ (1, move.line_ids.filtered(lambda line: line.date_maturity).id, {
+ 'amount_currency': 1430.0,
+ }),
+ ],
+ })
+
+ # Set the tax lock date:
+ # - The date must be set automatically at the date after the tax_lock_date.
+ # - As the date changed, the currency rate has changed (1/3 => 1/2).
+ move.company_id.tax_lock_date = fields.Date.from_string('2016-12-31')
+
+ move.action_post()
+
+ self.assertInvoiceValues(move, [
+ {
+ **self.product_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -1000.0,
+ 'debit': 0.0,
+ 'credit': 500.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -200.0,
+ 'debit': 0.0,
+ 'credit': 100.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'price_unit': 200.0,
+ 'price_subtotal': 200.0,
+ 'price_total': 200.0,
+ 'amount_currency': -200.0,
+ 'debit': 0.0,
+ 'credit': 100.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -30.0,
+ 'debit': 0.0,
+ 'credit': 15.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'name': move.name,
+ 'currency_id': self.currency_data['currency'].id,
+ 'price_unit': -1430.0,
+ 'price_subtotal': -1430.0,
+ 'price_total': -1430.0,
+ 'amount_currency': 1430.0,
+ 'debit': 715.0,
+ 'credit': 0.0,
+ 'date_maturity': fields.Date.from_string('2016-01-01'),
+ },
+ ], {
+ **self.move_vals,
+ 'payment_reference': move.name,
+ 'currency_id': self.currency_data['currency'].id,
+ 'date': fields.Date.from_string('2017-01-01'),
+ 'amount_untaxed': 1200.0,
+ 'amount_tax': 230.0,
+ 'amount_total': 1430.0,
+ })
+
+ def test_out_invoice_switch_out_refund_1(self):
+ # Test creating an account_move with an out_invoice_type and switch it in an out_refund.
+ move = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'partner_id': self.partner_a.id,
+ 'invoice_date': fields.Date.from_string('2019-01-01'),
+ 'currency_id': self.currency_data['currency'].id,
+ 'invoice_payment_term_id': self.pay_terms_a.id,
+ 'invoice_line_ids': [
+ (0, None, self.product_line_vals_1),
+ (0, None, self.product_line_vals_2),
+ ]
+ })
+ move.action_switch_invoice_into_refund_credit_note()
+
+ self.assertRecordValues(move, [{'move_type': 'out_refund'}])
+ self.assertInvoiceValues(move, [
+ {
+ **self.product_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 1000.0,
+ 'debit': 500.0,
+ 'credit': 0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 200.0,
+ 'debit': 100.0,
+ 'credit': 0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 180.0,
+ 'debit': 90.0,
+ 'credit': 0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 30.0,
+ 'debit': 15.0,
+ 'credit': 0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -1410.0,
+ 'credit': 705.0,
+ 'debit': 0,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ })
+
+ def test_out_invoice_switch_out_refund_2(self):
+ # Test creating an account_move with an out_invoice_type and switch it in an out_refund and a negative quantity.
+ modified_product_line_vals_1 = self.product_line_vals_1.copy()
+ modified_product_line_vals_1.update({'quantity': -modified_product_line_vals_1['quantity']})
+ modified_product_line_vals_2 = self.product_line_vals_2.copy()
+ modified_product_line_vals_2.update({'quantity': -modified_product_line_vals_2['quantity']})
+ move = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'partner_id': self.partner_a.id,
+ 'invoice_date': fields.Date.from_string('2019-01-01'),
+ 'currency_id': self.currency_data['currency'].id,
+ 'invoice_payment_term_id': self.pay_terms_a.id,
+ 'invoice_line_ids': [
+ (0, None, modified_product_line_vals_1),
+ (0, None, modified_product_line_vals_2),
+ ]
+ })
+
+ self.assertInvoiceValues(move, [
+ {
+ **self.product_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 1000.0,
+ 'price_subtotal': -1000.0,
+ 'price_total': -1150.0,
+ 'debit': 500.0,
+ 'credit': 0,
+ 'quantity': -1.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 200.0,
+ 'price_subtotal': -200.0,
+ 'price_total': -260.0,
+ 'debit': 100.0,
+ 'credit': 0,
+ 'quantity': -1.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 180.0,
+ 'price_subtotal': -180.0,
+ 'price_total': -180.0,
+ 'price_unit': -180.0,
+ 'debit': 90.0,
+ 'credit': 0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 30.0,
+ 'price_subtotal': -30.0,
+ 'price_total': -30.0,
+ 'price_unit': -30.0,
+ 'debit': 15.0,
+ 'credit': 0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -1410.0,
+ 'price_subtotal': 1410.0,
+ 'price_total': 1410.0,
+ 'price_unit': 1410.0,
+ 'credit': 705.0,
+ 'debit': 0,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_tax' : -self.move_vals['amount_tax'],
+ 'amount_total' : -self.move_vals['amount_total'],
+ 'amount_untaxed' : -self.move_vals['amount_untaxed'],
+ })
+
+ move.action_switch_invoice_into_refund_credit_note()
+
+ self.assertRecordValues(move, [{'move_type': 'out_refund'}])
+ self.assertInvoiceValues(move, [
+ {
+ **self.product_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 1000.0,
+ 'debit': 500.0,
+ 'credit': 0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 200.0,
+ 'debit': 100.0,
+ 'credit': 0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 180.0,
+ 'debit': 90.0,
+ 'credit': 0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 30.0,
+ 'debit': 15.0,
+ 'credit': 0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -1410.0,
+ 'credit': 705.0,
+ 'debit': 0,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_tax' : self.move_vals['amount_tax'],
+ 'amount_total' : self.move_vals['amount_total'],
+ 'amount_untaxed' : self.move_vals['amount_untaxed'],
+ })
+
+ def test_out_invoice_reverse_move_tags(self):
+ country = self.env.ref('base.us')
+ tags = self.env['account.account.tag'].create([{
+ 'name': "Test tag %s" % i,
+ 'applicability': 'taxes',
+ 'country_id': country.id,
+ } for i in range(8)])
+
+ taxes = self.env['account.tax'].create([{
+ 'name': "Test tax include_base_amount = %s" % include_base_amount,
+ 'amount': 10.0,
+ 'include_base_amount': include_base_amount,
+ 'invoice_repartition_line_ids': [
+ (0, 0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'tag_ids': [(6, 0, tags[(i * 4)].ids)],
+ }),
+ (0, 0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'tag_ids': [(6, 0, tags[(i * 4) + 1].ids)],
+ }),
+ ],
+ 'refund_repartition_line_ids': [
+ (0, 0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'tag_ids': [(6, 0, tags[(i * 4) + 2].ids)],
+ }),
+ (0, 0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'tag_ids': [(6, 0, tags[(i * 4) + 3].ids)],
+ }),
+ ],
+ } for i, include_base_amount in enumerate((True, False))])
+
+ invoice = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'partner_id': self.partner_a.id,
+ 'invoice_date': fields.Date.from_string('2019-01-01'),
+ 'invoice_payment_term_id': self.pay_terms_a.id,
+ 'invoice_line_ids': [
+ (0, 0, {
+ 'product_id': self.product_a.id,
+ 'price_unit': 1000.0,
+ 'tax_ids': [(6, 0, taxes.ids)],
+ }),
+ ]
+ })
+ invoice.action_post()
+
+ self.assertRecordValues(invoice.line_ids.sorted('tax_line_id'), [
+ # Product line
+ {'tax_line_id': False, 'tax_ids': taxes.ids, 'tax_tag_ids': (tags[0] + tags[4]).ids},
+ # Receivable line
+ {'tax_line_id': False, 'tax_ids': [], 'tax_tag_ids': []},
+ # Tax lines
+ {'tax_line_id': taxes[0].id, 'tax_ids': taxes[1].ids, 'tax_tag_ids': (tags[1] + tags[4]).ids},
+ {'tax_line_id': taxes[1].id, 'tax_ids': [], 'tax_tag_ids': tags[5].ids},
+ ])
+
+ refund = invoice._reverse_moves(cancel=True)
+
+ self.assertRecordValues(refund.line_ids.sorted('tax_line_id'), [
+ # Product line
+ {'tax_line_id': False, 'tax_ids': taxes.ids, 'tax_tag_ids': (tags[2] + tags[6]).ids},
+ # Receivable line
+ {'tax_line_id': False, 'tax_ids': [], 'tax_tag_ids': []},
+ # Tax lines
+ {'tax_line_id': taxes[0].id, 'tax_ids': taxes[1].ids, 'tax_tag_ids': (tags[3] + tags[6]).ids},
+ {'tax_line_id': taxes[1].id, 'tax_ids': [], 'tax_tag_ids': tags[7].ids},
+ ])
+
+ def test_out_invoice_change_period_accrual_1(self):
+ move = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'date': '2017-01-01',
+ 'partner_id': self.partner_a.id,
+ 'invoice_date': fields.Date.from_string('2017-01-01'),
+ 'currency_id': self.currency_data['currency'].id,
+ 'invoice_payment_term_id': self.pay_terms_a.id,
+ 'invoice_line_ids': [
+ (0, None, {
+ 'name': self.product_line_vals_1['name'],
+ 'product_id': self.product_line_vals_1['product_id'],
+ 'product_uom_id': self.product_line_vals_1['product_uom_id'],
+ 'quantity': self.product_line_vals_1['quantity'],
+ 'price_unit': self.product_line_vals_1['price_unit'],
+ 'tax_ids': self.product_line_vals_1['tax_ids'],
+ }),
+ (0, None, {
+ 'name': self.product_line_vals_2['name'],
+ 'product_id': self.product_line_vals_2['product_id'],
+ 'product_uom_id': self.product_line_vals_2['product_uom_id'],
+ 'quantity': self.product_line_vals_2['quantity'],
+ 'price_unit': self.product_line_vals_2['price_unit'],
+ 'tax_ids': self.product_line_vals_2['tax_ids'],
+ }),
+ ]
+ })
+ move.action_post()
+
+ wizard = self.env['account.automatic.entry.wizard']\
+ .with_context(active_model='account.move.line', active_ids=move.invoice_line_ids.ids).create({
+ 'action': 'change_period',
+ 'date': '2018-01-01',
+ 'percentage': 60,
+ 'journal_id': self.company_data['default_journal_misc'].id,
+ 'expense_accrual_account': self.env['account.account'].create({
+ 'name': 'Accrual Expense Account',
+ 'code': '234567',
+ 'user_type_id': self.env.ref('account.data_account_type_expenses').id,
+ 'reconcile': True,
+ }).id,
+ 'revenue_accrual_account': self.env['account.account'].create({
+ 'name': 'Accrual Revenue Account',
+ 'code': '765432',
+ 'user_type_id': self.env.ref('account.data_account_type_expenses').id,
+ 'reconcile': True,
+ }).id,
+ })
+ wizard_res = wizard.do_action()
+
+ self.assertInvoiceValues(move, [
+ {
+ **self.product_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -1000.0,
+ 'debit': 0.0,
+ 'credit': 500.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -200.0,
+ 'debit': 0.0,
+ 'credit': 100.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -180.0,
+ 'debit': 0.0,
+ 'credit': 90.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -30.0,
+ 'debit': 0.0,
+ 'credit': 15.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'name': 'INV/2017/01/0001',
+ 'amount_currency': 1410.0,
+ 'debit': 705.0,
+ 'credit': 0.0,
+ 'date_maturity': fields.Date.from_string('2017-01-01'),
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ 'date': fields.Date.from_string('2017-01-01'),
+ 'payment_reference': 'INV/2017/01/0001',
+ })
+
+ accrual_lines = self.env['account.move'].browse(wizard_res['domain'][0][2]).line_ids.sorted('date')
+ self.assertRecordValues(accrual_lines, [
+ {'amount_currency': 600.0, 'debit': 300.0, 'credit': 0.0, 'account_id': self.product_line_vals_1['account_id'], 'reconciled': False},
+ {'amount_currency': -600.0, 'debit': 0.0, 'credit': 300.0, 'account_id': wizard.revenue_accrual_account.id, 'reconciled': True},
+ {'amount_currency': 120.0, 'debit': 60.0, 'credit': 0.0, 'account_id': self.product_line_vals_2['account_id'], 'reconciled': False},
+ {'amount_currency': -120.0, 'debit': 0.0, 'credit': 60.0, 'account_id': wizard.revenue_accrual_account.id, 'reconciled': True},
+ {'amount_currency': -600.0, 'debit': 0.0, 'credit': 300.0, 'account_id': self.product_line_vals_1['account_id'], 'reconciled': False},
+ {'amount_currency': 600.0, 'debit': 300.0, 'credit': 0.0, 'account_id': wizard.revenue_accrual_account.id, 'reconciled': True},
+ {'amount_currency': -120.0, 'debit': 0.0, 'credit': 60.0, 'account_id': self.product_line_vals_2['account_id'], 'reconciled': False},
+ {'amount_currency': 120.0, 'debit': 60.0, 'credit': 0.0, 'account_id': wizard.revenue_accrual_account.id, 'reconciled': True},
+ ])
+
+ def test_out_invoice_filter_zero_balance_lines(self):
+ zero_balance_payment_term = self.env['account.payment.term'].create({
+ 'name': 'zero_balance_payment_term',
+ 'line_ids': [
+ (0, 0, {
+ 'value': 'percent',
+ 'value_amount': 100.0,
+ 'sequence': 10,
+ 'days': 0,
+ 'option': 'day_after_invoice_date',
+ }),
+ (0, 0, {
+ 'value': 'balance',
+ 'value_amount': 0.0,
+ 'sequence': 20,
+ 'days': 0,
+ 'option': 'day_after_invoice_date',
+ }),
+ ],
+ })
+
+ zero_balance_tax = self.env['account.tax'].create({
+ 'name': 'zero_balance_tax',
+ 'amount_type': 'percent',
+ 'amount': 0.0,
+ })
+
+ invoice = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'partner_id': self.partner_a.id,
+ 'invoice_date': fields.Date.from_string('2019-01-01'),
+ 'invoice_payment_term_id': zero_balance_payment_term.id,
+ 'invoice_line_ids': [(0, None, {
+ 'name': 'whatever',
+ 'quantity': 1.0,
+ 'price_unit': 1000.0,
+ 'tax_ids': [(6, 0, zero_balance_tax.ids)],
+ })]
+ })
+
+ self.assertEqual(len(invoice.invoice_line_ids), 1)
+ self.assertEqual(len(invoice.line_ids), 2)
+
+ def test_out_invoice_recomputation_receivable_lines(self):
+ ''' Test a tricky specific case caused by some framework limitations. Indeed, when
+ saving a record, some fields are written to the records even if the value is the same
+ as the previous one. It could lead to an unbalanced journal entry when the recomputed
+ line is the receivable/payable one.
+
+ For example, the computed price_subtotal are the following:
+ 1471.95 / 0.14 = 10513.93
+ 906468.18 / 0.14 = 6474772.71
+ 1730.84 / 0.14 = 12363.14
+ 17.99 / 0.14 = 128.50
+ SUM = 6497778.28
+
+ But when recomputing the receivable line:
+ 909688.96 / 0.14 = 6497778.285714286 => 6497778.29
+
+ This recomputation was made because the framework was writing the same 'price_unit'
+ as the previous value leading to a recomputation of the debit/credit.
+ '''
+ self.env['decimal.precision'].search([
+ ('name', '=', self.env['account.move.line']._fields['price_unit']._digits),
+ ]).digits = 5
+
+ self.env['res.currency.rate'].create({
+ 'name': '2019-01-01',
+ 'rate': 0.14,
+ 'currency_id': self.currency_data['currency'].id,
+ 'company_id': self.company_data['company'].id,
+ })
+
+ invoice = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'invoice_date': '2019-01-01',
+ 'date': '2019-01-01',
+ 'partner_id': self.partner_a.id,
+ 'currency_id': self.currency_data['currency'].id,
+ 'invoice_payment_term_id': self.env.ref('account.account_payment_term_immediate').id,
+ 'invoice_line_ids': [
+ (0, 0, {'name': 'line1', 'price_unit': 38.73553, 'quantity': 38.0}),
+ (0, 0, {'name': 'line2', 'price_unit': 4083.19000, 'quantity': 222.0}),
+ (0, 0, {'name': 'line3', 'price_unit': 49.45257, 'quantity': 35.0}),
+ (0, 0, {'name': 'line4', 'price_unit': 17.99000, 'quantity': 1.0}),
+ ],
+ })
+
+ # assertNotUnbalancedEntryWhenSaving
+ with Form(invoice) as move_form:
+ move_form.invoice_payment_term_id = self.env.ref('account.account_payment_term_30days')
+
+ def test_out_invoice_rounding_recomputation_receivable_lines(self):
+ ''' Test rounding error due to the fact that subtracting then rounding is different from
+ rounding then subtracting.
+ '''
+ self.env['decimal.precision'].search([
+ ('name', '=', self.env['account.move.line']._fields['price_unit']._digits),
+ ]).digits = 5
+
+ self.env['res.currency.rate'].search([]).unlink()
+
+ invoice = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'invoice_date': '2019-01-01',
+ 'date': '2019-01-01',
+ 'partner_id': self.partner_a.id,
+ 'invoice_payment_term_id': self.env.ref('account.account_payment_term_immediate').id,
+ })
+
+ # assertNotUnbalancedEntryWhenSaving
+ with Form(invoice) as move_form:
+ with move_form.invoice_line_ids.new() as line_form:
+ line_form.name = 'line1'
+ line_form.account_id = self.company_data['default_account_revenue']
+ line_form.tax_ids.clear()
+ line_form.price_unit = 0.89500
+ move_form.save()
+
+ def test_out_invoice_multi_company(self):
+ ''' Ensure the properties are found on the right company.
+ '''
+
+ product = self.env['product.product'].create({
+ 'name': 'product',
+ 'uom_id': self.env.ref('uom.product_uom_unit').id,
+ 'lst_price': 1000.0,
+ 'standard_price': 800.0,
+ 'company_id': False,
+ })
+
+ partner = self.env['res.partner'].create({
+ 'name': 'partner',
+ 'company_id': False,
+ })
+
+ journal = self.env['account.journal'].create({
+ 'name': 'test_out_invoice_multi_company',
+ 'code': 'XXXXX',
+ 'type': 'sale',
+ 'company_id': self.company_data_2['company'].id,
+ })
+
+ product.with_company(self.company_data['company']).write({
+ 'property_account_income_id': self.company_data['default_account_revenue'].id,
+ })
+
+ partner.with_company(self.company_data['company']).write({
+ 'property_account_receivable_id': self.company_data['default_account_receivable'].id,
+ })
+
+ product.with_company(self.company_data_2['company']).write({
+ 'property_account_income_id': self.company_data_2['default_account_revenue'].id,
+ })
+
+ partner.with_company(self.company_data_2['company']).write({
+ 'property_account_receivable_id': self.company_data_2['default_account_receivable'].id,
+ })
+
+ def _check_invoice_values(invoice):
+ self.assertInvoiceValues(invoice, [
+ {
+ 'product_id': product.id,
+ 'account_id': self.company_data_2['default_account_revenue'].id,
+ 'debit': 0.0,
+ 'credit': 1000.0,
+ },
+ {
+ 'product_id': False,
+ 'account_id': self.company_data_2['default_account_receivable'].id,
+ 'debit': 1000.0,
+ 'credit': 0.0,
+ },
+ ], {
+ 'amount_untaxed': 1000.0,
+ 'amount_total': 1000.0,
+ })
+
+ invoice_create = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'invoice_date': '2017-01-01',
+ 'date': '2017-01-01',
+ 'partner_id': partner.id,
+ 'journal_id': journal.id,
+ 'invoice_line_ids': [(0, 0, {
+ 'product_id': product.id,
+ 'price_unit': 1000.0,
+ })],
+ })
+
+ _check_invoice_values(invoice_create)
+
+ move_form = Form(self.env['account.move'].with_context(default_move_type='out_invoice'))
+ move_form.journal_id = journal
+ move_form.partner_id = partner
+ move_form.invoice_date = fields.Date.from_string('2017-01-01')
+ with move_form.invoice_line_ids.new() as line_form:
+ line_form.product_id = product
+ line_form.tax_ids.clear()
+ invoice_onchange = move_form.save()
+
+ _check_invoice_values(invoice_onchange)
+
+ def test_out_invoice_multiple_switch_payment_terms(self):
+ ''' When switching immediate payment term to 30% advance then back to immediate payment term, ensure the
+ receivable line is back to its previous value. If some business fields are not well updated, it could lead to a
+ recomputation of debit/credit when writing and then, an unbalanced journal entry.
+ '''
+ # assertNotUnbalancedEntryWhenSaving
+ with Form(self.invoice) as move_form:
+ move_form.invoice_payment_term_id = self.pay_terms_b # Switch to 30% in advance payment terms
+ move_form.invoice_payment_term_id = self.pay_terms_a # Back to immediate payment term
diff --git a/addons/account/tests/test_account_move_out_refund.py b/addons/account/tests/test_account_move_out_refund.py
new file mode 100644
index 00000000..967eb232
--- /dev/null
+++ b/addons/account/tests/test_account_move_out_refund.py
@@ -0,0 +1,933 @@
+# -*- coding: utf-8 -*-
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests.common import Form
+from odoo.tests import tagged
+from odoo import fields
+
+
+@tagged('post_install', '-at_install')
+class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ cls.invoice = cls.init_invoice('out_refund', products=cls.product_a+cls.product_b)
+
+ cls.product_line_vals_1 = {
+ 'name': cls.product_a.name,
+ 'product_id': cls.product_a.id,
+ 'account_id': cls.product_a.property_account_income_id.id,
+ 'partner_id': cls.partner_a.id,
+ 'product_uom_id': cls.product_a.uom_id.id,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 1000.0,
+ 'price_subtotal': 1000.0,
+ 'price_total': 1150.0,
+ 'tax_ids': cls.product_a.taxes_id.ids,
+ 'tax_line_id': False,
+ 'currency_id': cls.company_data['currency'].id,
+ 'amount_currency': 1000.0,
+ 'debit': 1000.0,
+ 'credit': 0.0,
+ 'date_maturity': False,
+ 'tax_exigible': True,
+ }
+ cls.product_line_vals_2 = {
+ 'name': cls.product_b.name,
+ 'product_id': cls.product_b.id,
+ 'account_id': cls.product_b.property_account_income_id.id,
+ 'partner_id': cls.partner_a.id,
+ 'product_uom_id': cls.product_b.uom_id.id,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 200.0,
+ 'price_subtotal': 200.0,
+ 'price_total': 260.0,
+ 'tax_ids': cls.product_b.taxes_id.ids,
+ 'tax_line_id': False,
+ 'currency_id': cls.company_data['currency'].id,
+ 'amount_currency': 200.0,
+ 'debit': 200.0,
+ 'credit': 0.0,
+ 'date_maturity': False,
+ 'tax_exigible': True,
+ }
+ cls.tax_line_vals_1 = {
+ 'name': cls.tax_sale_a.name,
+ 'product_id': False,
+ 'account_id': cls.company_data['default_account_tax_sale'].id,
+ 'partner_id': cls.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 180.0,
+ 'price_subtotal': 180.0,
+ 'price_total': 180.0,
+ 'tax_ids': [],
+ 'tax_line_id': cls.tax_sale_a.id,
+ 'currency_id': cls.company_data['currency'].id,
+ 'amount_currency': 180.0,
+ 'debit': 180.0,
+ 'credit': 0.0,
+ 'date_maturity': False,
+ 'tax_exigible': True,
+ }
+ cls.tax_line_vals_2 = {
+ 'name': cls.tax_sale_b.name,
+ 'product_id': False,
+ 'account_id': cls.company_data['default_account_tax_sale'].id,
+ 'partner_id': cls.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 30.0,
+ 'price_subtotal': 30.0,
+ 'price_total': 30.0,
+ 'tax_ids': [],
+ 'tax_line_id': cls.tax_sale_b.id,
+ 'currency_id': cls.company_data['currency'].id,
+ 'amount_currency': 30.0,
+ 'debit': 30.0,
+ 'credit': 0.0,
+ 'date_maturity': False,
+ 'tax_exigible': True,
+ }
+ cls.term_line_vals_1 = {
+ 'name': '',
+ 'product_id': False,
+ 'account_id': cls.company_data['default_account_receivable'].id,
+ 'partner_id': cls.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': -1410.0,
+ 'price_subtotal': -1410.0,
+ 'price_total': -1410.0,
+ 'tax_ids': [],
+ 'tax_line_id': False,
+ 'currency_id': cls.company_data['currency'].id,
+ 'amount_currency': -1410.0,
+ 'debit': 0.0,
+ 'credit': 1410.0,
+ 'date_maturity': fields.Date.from_string('2019-01-01'),
+ 'tax_exigible': True,
+ }
+ cls.move_vals = {
+ 'partner_id': cls.partner_a.id,
+ 'currency_id': cls.company_data['currency'].id,
+ 'journal_id': cls.company_data['default_journal_sale'].id,
+ 'date': fields.Date.from_string('2019-01-01'),
+ 'fiscal_position_id': False,
+ 'payment_reference': '',
+ 'invoice_payment_term_id': cls.pay_terms_a.id,
+ 'amount_untaxed': 1200.0,
+ 'amount_tax': 210.0,
+ 'amount_total': 1410.0,
+ }
+
+ def setUp(self):
+ super(TestAccountMoveOutRefundOnchanges, self).setUp()
+ self.assertInvoiceValues(self.invoice, [
+ self.product_line_vals_1,
+ self.product_line_vals_2,
+ self.tax_line_vals_1,
+ self.tax_line_vals_2,
+ self.term_line_vals_1,
+ ], self.move_vals)
+
+ def test_out_refund_line_onchange_product_1(self):
+ move_form = Form(self.invoice)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ line_form.product_id = self.product_b
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'name': self.product_b.name,
+ 'product_id': self.product_b.id,
+ 'product_uom_id': self.product_b.uom_id.id,
+ 'account_id': self.product_b.property_account_income_id.id,
+ 'price_unit': 200.0,
+ 'price_subtotal': 200.0,
+ 'price_total': 260.0,
+ 'tax_ids': self.product_b.taxes_id.ids,
+ 'amount_currency': 200.0,
+ 'debit': 200.0,
+ },
+ self.product_line_vals_2,
+ {
+ **self.tax_line_vals_1,
+ 'price_unit': 60.0,
+ 'price_subtotal': 60.0,
+ 'price_total': 60.0,
+ 'amount_currency': 60.0,
+ 'debit': 60.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'price_unit': 60.0,
+ 'price_subtotal': 60.0,
+ 'price_total': 60.0,
+ 'amount_currency': 60.0,
+ 'debit': 60.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'price_unit': -520.0,
+ 'price_subtotal': -520.0,
+ 'price_total': -520.0,
+ 'amount_currency': -520.0,
+ 'credit': 520.0,
+ },
+ ], {
+ **self.move_vals,
+ 'amount_untaxed': 400.0,
+ 'amount_tax': 120.0,
+ 'amount_total': 520.0,
+ })
+
+ def test_out_refund_line_onchange_business_fields_1(self):
+ move_form = Form(self.invoice)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ # Current price_unit is 1000.
+ # We set quantity = 4, discount = 50%, price_unit = 400. The debit/credit fields don't change because (4 * 500) * 0.5 = 1000.
+ line_form.quantity = 4
+ line_form.discount = 50
+ line_form.price_unit = 500
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'quantity': 4,
+ 'discount': 50.0,
+ 'price_unit': 500.0,
+ },
+ self.product_line_vals_2,
+ self.tax_line_vals_1,
+ self.tax_line_vals_2,
+ self.term_line_vals_1,
+ ], self.move_vals)
+
+ move_form = Form(self.invoice)
+ with move_form.line_ids.edit(2) as line_form:
+ # Reset field except the discount that becomes 100%.
+ # /!\ The modification is made on the accounting tab.
+ line_form.quantity = 1
+ line_form.discount = 100
+ line_form.price_unit = 1000
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'discount': 100.0,
+ 'price_subtotal': 0.0,
+ 'price_total': 0.0,
+ 'amount_currency': 0.0,
+ 'debit': 0.0,
+ },
+ self.product_line_vals_2,
+ {
+ **self.tax_line_vals_1,
+ 'price_unit': 30.0,
+ 'price_subtotal': 30.0,
+ 'price_total': 30.0,
+ 'amount_currency': 30.0,
+ 'debit': 30.0,
+ },
+ self.tax_line_vals_2,
+ {
+ **self.term_line_vals_1,
+ 'price_unit': -260.0,
+ 'price_subtotal': -260.0,
+ 'price_total': -260.0,
+ 'amount_currency': -260.0,
+ 'credit': 260.0,
+ },
+ ], {
+ **self.move_vals,
+ 'amount_untaxed': 200.0,
+ 'amount_tax': 60.0,
+ 'amount_total': 260.0,
+ })
+
+ def test_out_refund_line_onchange_accounting_fields_1(self):
+ move_form = Form(self.invoice)
+ with move_form.line_ids.edit(2) as line_form:
+ # Custom debit on the first product line.
+ line_form.debit = 3000
+ with move_form.line_ids.edit(3) as line_form:
+ # Custom credit on the second product line. Credit should be reset by onchange.
+ # /!\ It's a negative line.
+ line_form.credit = 500
+ with move_form.line_ids.edit(0) as line_form:
+ # Custom debit on the first tax line.
+ line_form.debit = 800
+ with move_form.line_ids.edit(4) as line_form:
+ # Custom debit on the second tax line.
+ line_form.debit = 250
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'price_unit': 3000.0,
+ 'price_subtotal': 3000.0,
+ 'price_total': 3450.0,
+ 'amount_currency': 3000.0,
+ 'debit': 3000.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'price_unit': -500.0,
+ 'price_subtotal': -500.0,
+ 'price_total': -650.0,
+ 'amount_currency': -500.0,
+ 'debit': 0.0,
+ 'credit': 500.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'price_unit': 800.0,
+ 'price_subtotal': 800.0,
+ 'price_total': 800.0,
+ 'amount_currency': 800.0,
+ 'debit': 800.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'price_unit': 250.0,
+ 'price_subtotal': 250.0,
+ 'price_total': 250.0,
+ 'amount_currency': 250.0,
+ 'debit': 250.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'price_unit': -3550.0,
+ 'price_subtotal': -3550.0,
+ 'price_total': -3550.0,
+ 'amount_currency': -3550.0,
+ 'credit': 3550.0,
+ },
+ ], {
+ **self.move_vals,
+ 'amount_untaxed': 2500.0,
+ 'amount_tax': 1050.0,
+ 'amount_total': 3550.0,
+ })
+
+ def test_out_refund_line_onchange_partner_1(self):
+ move_form = Form(self.invoice)
+ move_form.partner_id = self.partner_b
+ move_form.payment_reference = 'turlututu'
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'partner_id': self.partner_b.id,
+ },
+ {
+ **self.product_line_vals_2,
+ 'partner_id': self.partner_b.id,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'partner_id': self.partner_b.id,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'partner_id': self.partner_b.id,
+ },
+ {
+ **self.term_line_vals_1,
+ 'name': 'turlututu',
+ 'partner_id': self.partner_b.id,
+ 'account_id': self.partner_b.property_account_receivable_id.id,
+ 'price_unit': -987.0,
+ 'price_subtotal': -987.0,
+ 'price_total': -987.0,
+ 'amount_currency': -987.0,
+ 'credit': 987.0,
+ 'date_maturity': fields.Date.from_string('2019-02-28'),
+ },
+ {
+ **self.term_line_vals_1,
+ 'name': 'turlututu',
+ 'partner_id': self.partner_b.id,
+ 'account_id': self.partner_b.property_account_receivable_id.id,
+ 'price_unit': -423.0,
+ 'price_subtotal': -423.0,
+ 'price_total': -423.0,
+ 'amount_currency': -423.0,
+ 'credit': 423.0,
+ },
+ ], {
+ **self.move_vals,
+ 'partner_id': self.partner_b.id,
+ 'payment_reference': 'turlututu',
+ 'fiscal_position_id': self.fiscal_pos_a.id,
+ 'invoice_payment_term_id': self.pay_terms_b.id,
+ 'amount_untaxed': 1200.0,
+ 'amount_tax': 210.0,
+ 'amount_total': 1410.0,
+ })
+
+ # Remove lines and recreate them to apply the fiscal position.
+ move_form = Form(self.invoice)
+ move_form.invoice_line_ids.remove(0)
+ move_form.invoice_line_ids.remove(0)
+ with move_form.invoice_line_ids.new() as line_form:
+ line_form.product_id = self.product_a
+ with move_form.invoice_line_ids.new() as line_form:
+ line_form.product_id = self.product_b
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'account_id': self.product_b.property_account_income_id.id,
+ 'partner_id': self.partner_b.id,
+ 'tax_ids': self.tax_sale_b.ids,
+ },
+ {
+ **self.product_line_vals_2,
+ 'partner_id': self.partner_b.id,
+ 'price_total': 230.0,
+ 'tax_ids': self.tax_sale_b.ids,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'name': self.tax_sale_b.name,
+ 'partner_id': self.partner_b.id,
+ 'tax_line_id': self.tax_sale_b.id,
+ },
+ {
+ **self.term_line_vals_1,
+ 'name': 'turlututu',
+ 'account_id': self.partner_b.property_account_receivable_id.id,
+ 'partner_id': self.partner_b.id,
+ 'price_unit': -966.0,
+ 'price_subtotal': -966.0,
+ 'price_total': -966.0,
+ 'amount_currency': -966.0,
+ 'credit': 966.0,
+ 'date_maturity': fields.Date.from_string('2019-02-28'),
+ },
+ {
+ **self.term_line_vals_1,
+ 'name': 'turlututu',
+ 'account_id': self.partner_b.property_account_receivable_id.id,
+ 'partner_id': self.partner_b.id,
+ 'price_unit': -414.0,
+ 'price_subtotal': -414.0,
+ 'price_total': -414.0,
+ 'amount_currency': -414.0,
+ 'credit': 414.0,
+ },
+ ], {
+ **self.move_vals,
+ 'partner_id': self.partner_b.id,
+ 'payment_reference': 'turlututu',
+ 'fiscal_position_id': self.fiscal_pos_a.id,
+ 'invoice_payment_term_id': self.pay_terms_b.id,
+ 'amount_untaxed': 1200.0,
+ 'amount_tax': 180.0,
+ 'amount_total': 1380.0,
+ })
+
+ def test_out_refund_line_onchange_taxes_1(self):
+ move_form = Form(self.invoice)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ line_form.price_unit = 1200
+ line_form.tax_ids.add(self.tax_armageddon)
+ move_form.save()
+
+ child_tax_1 = self.tax_armageddon.children_tax_ids[0]
+ child_tax_2 = self.tax_armageddon.children_tax_ids[1]
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'price_unit': 1200.0,
+ 'price_subtotal': 1000.0,
+ 'price_total': 1470.0,
+ 'tax_ids': (self.tax_sale_a + self.tax_armageddon).ids,
+ 'tax_exigible': False,
+ },
+ self.product_line_vals_2,
+ self.tax_line_vals_1,
+ self.tax_line_vals_2,
+ {
+ 'name': child_tax_1.name,
+ 'product_id': False,
+ 'account_id': self.company_data['default_account_tax_sale'].id,
+ 'partner_id': self.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 80.0,
+ 'price_subtotal': 80.0,
+ 'price_total': 88.0,
+ 'tax_ids': child_tax_2.ids,
+ 'tax_line_id': child_tax_1.id,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': 80.0,
+ 'debit': 80.0,
+ 'credit': 0.0,
+ 'date_maturity': False,
+ 'tax_exigible': False,
+ },
+ {
+ 'name': child_tax_1.name,
+ 'product_id': False,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'partner_id': self.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 120.0,
+ 'price_subtotal': 120.0,
+ 'price_total': 132.0,
+ 'tax_ids': child_tax_2.ids,
+ 'tax_line_id': child_tax_1.id,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': 120.0,
+ 'debit': 120.0,
+ 'credit': 0.0,
+ 'date_maturity': False,
+ 'tax_exigible': False,
+ },
+ {
+ 'name': child_tax_2.name,
+ 'product_id': False,
+ 'account_id': child_tax_2.cash_basis_transition_account_id.id,
+ 'partner_id': self.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 120.0,
+ 'price_subtotal': 120.0,
+ 'price_total': 120.0,
+ 'tax_ids': [],
+ 'tax_line_id': child_tax_2.id,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': 120.0,
+ 'debit': 120.0,
+ 'credit': 0.0,
+ 'date_maturity': False,
+ 'tax_exigible': False,
+ },
+ {
+ **self.term_line_vals_1,
+ 'price_unit': -1730.0,
+ 'price_subtotal': -1730.0,
+ 'price_total': -1730.0,
+ 'amount_currency': -1730.0,
+ 'credit': 1730.0,
+ },
+ ], {
+ **self.move_vals,
+ 'amount_untaxed': 1200.0,
+ 'amount_tax': 530.0,
+ 'amount_total': 1730.0,
+ })
+
+ def test_out_refund_line_onchange_cash_rounding_1(self):
+ move_form = Form(self.invoice)
+ # Add a cash rounding having 'add_invoice_line'.
+ move_form.invoice_cash_rounding_id = self.cash_rounding_a
+ move_form.save()
+
+ # The cash rounding does nothing as the total is already rounded.
+ self.assertInvoiceValues(self.invoice, [
+ self.product_line_vals_1,
+ self.product_line_vals_2,
+ self.tax_line_vals_1,
+ self.tax_line_vals_2,
+ self.term_line_vals_1,
+ ], self.move_vals)
+
+ move_form = Form(self.invoice)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ line_form.price_unit = 999.99
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ 'name': 'add_invoice_line',
+ 'product_id': False,
+ 'account_id': self.cash_rounding_a.loss_account_id.id,
+ 'partner_id': self.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': 0.01,
+ 'price_subtotal': 0.01,
+ 'price_total': 0.01,
+ 'tax_ids': [],
+ 'tax_line_id': False,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': 0.01,
+ 'debit': 0.01,
+ 'credit': 0.0,
+ 'date_maturity': False,
+ 'tax_exigible': True,
+ },
+ {
+ **self.product_line_vals_1,
+ 'price_unit': 999.99,
+ 'price_subtotal': 999.99,
+ 'price_total': 1149.99,
+ 'amount_currency': 999.99,
+ 'debit': 999.99,
+ },
+ self.product_line_vals_2,
+ self.tax_line_vals_1,
+ self.tax_line_vals_2,
+ self.term_line_vals_1,
+ ], self.move_vals)
+
+ move_form = Form(self.invoice)
+ # Change the cash rounding to one having 'biggest_tax'.
+ move_form.invoice_cash_rounding_id = self.cash_rounding_b
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'price_unit': 999.99,
+ 'price_subtotal': 999.99,
+ 'price_total': 1149.99,
+ 'amount_currency': 999.99,
+ 'debit': 999.99,
+ },
+ self.product_line_vals_2,
+ self.tax_line_vals_1,
+ self.tax_line_vals_2,
+ {
+ 'name': '%s (rounding)' % self.tax_sale_a.name,
+ 'product_id': False,
+ 'account_id': self.company_data['default_account_tax_sale'].id,
+ 'partner_id': self.partner_a.id,
+ 'product_uom_id': False,
+ 'quantity': 1.0,
+ 'discount': 0.0,
+ 'price_unit': -0.04,
+ 'price_subtotal': -0.04,
+ 'price_total': -0.04,
+ 'tax_ids': [],
+ 'tax_line_id': self.tax_sale_a.id,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': -0.04,
+ 'debit': 0.0,
+ 'credit': 0.04,
+ 'date_maturity': False,
+ 'tax_exigible': True,
+ },
+ {
+ **self.term_line_vals_1,
+ 'price_unit': -1409.95,
+ 'price_subtotal': -1409.95,
+ 'price_total': -1409.95,
+ 'amount_currency': -1409.95,
+ 'credit': 1409.95,
+ },
+ ], {
+ **self.move_vals,
+ 'amount_untaxed': 1199.99,
+ 'amount_tax': 209.96,
+ 'amount_total': 1409.95,
+ })
+
+ def test_out_refund_line_onchange_currency_1(self):
+ move_form = Form(self.invoice)
+ move_form.currency_id = self.currency_data['currency']
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 1000.0,
+ 'debit': 500.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 200.0,
+ 'debit': 100.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 180.0,
+ 'debit': 90.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 30.0,
+ 'debit': 15.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -1410.0,
+ 'credit': 705.0,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ })
+
+ move_form = Form(self.invoice)
+ # Change the date to get another rate: 1/3 instead of 1/2.
+ move_form.date = fields.Date.from_string('2016-01-01')
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 1000.0,
+ 'debit': 333.33,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 200.0,
+ 'debit': 66.67,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 180.0,
+ 'debit': 60.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 30.0,
+ 'debit': 10.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -1410.0,
+ 'credit': 470.0,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ 'date': fields.Date.from_string('2016-01-01'),
+ })
+
+ move_form = Form(self.invoice)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ # 0.045 * 0.1 = 0.0045. As the foreign currency has a 0.001 rounding,
+ # the result should be 0.005 after rounding.
+ line_form.quantity = 0.1
+ line_form.price_unit = 0.045
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'quantity': 0.1,
+ 'price_unit': 0.05,
+ 'price_subtotal': 0.005,
+ 'price_total': 0.006,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 0.005,
+ 'debit': 0.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 200.0,
+ 'debit': 66.67,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'price_unit': 30.0,
+ 'price_subtotal': 30.001,
+ 'price_total': 30.001,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 30.001,
+ 'debit': 10.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 30.0,
+ 'debit': 10.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'price_unit': -260.01,
+ 'price_subtotal': -260.006,
+ 'price_total': -260.006,
+ 'amount_currency': -260.006,
+ 'credit': 86.67,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ 'date': fields.Date.from_string('2016-01-01'),
+ 'amount_untaxed': 200.005,
+ 'amount_tax': 60.001,
+ 'amount_total': 260.006,
+ })
+
+ # Exit the multi-currencies.
+ move_form = Form(self.invoice)
+ move_form.currency_id = self.company_data['currency']
+ move_form.save()
+
+ self.assertInvoiceValues(self.invoice, [
+ {
+ **self.product_line_vals_1,
+ 'quantity': 0.1,
+ 'price_unit': 0.05,
+ 'price_subtotal': 0.01,
+ 'price_total': 0.01,
+ 'amount_currency': 0.01,
+ 'debit': 0.01,
+ },
+ self.product_line_vals_2,
+ {
+ **self.tax_line_vals_1,
+ 'price_unit': 30.0,
+ 'price_subtotal': 30.0,
+ 'price_total': 30.0,
+ 'amount_currency': 30.0,
+ 'debit': 30.0,
+ },
+ self.tax_line_vals_2,
+ {
+ **self.term_line_vals_1,
+ 'price_unit': -260.01,
+ 'price_subtotal': -260.01,
+ 'price_total': -260.01,
+ 'amount_currency': -260.01,
+ 'credit': 260.01,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.company_data['currency'].id,
+ 'date': fields.Date.from_string('2016-01-01'),
+ 'amount_untaxed': 200.01,
+ 'amount_tax': 60.0,
+ 'amount_total': 260.01,
+ })
+
+ def test_out_refund_create_1(self):
+ # Test creating an account_move with the least information.
+ move = self.env['account.move'].create({
+ 'move_type': 'out_refund',
+ 'partner_id': self.partner_a.id,
+ 'invoice_date': fields.Date.from_string('2019-01-01'),
+ 'currency_id': self.currency_data['currency'].id,
+ 'invoice_payment_term_id': self.pay_terms_a.id,
+ 'invoice_line_ids': [
+ (0, None, self.product_line_vals_1),
+ (0, None, self.product_line_vals_2),
+ ]
+ })
+
+ self.assertInvoiceValues(move, [
+ {
+ **self.product_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 1000.0,
+ 'debit': 500.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 200.0,
+ 'debit': 100.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 180.0,
+ 'debit': 90.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 30.0,
+ 'debit': 15.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -1410.0,
+ 'credit': 705.0,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ })
+
+ def test_out_refund_write_1(self):
+ # Test creating an account_move with the least information.
+ move = self.env['account.move'].create({
+ 'move_type': 'out_refund',
+ 'partner_id': self.partner_a.id,
+ 'invoice_date': fields.Date.from_string('2019-01-01'),
+ 'currency_id': self.currency_data['currency'].id,
+ 'invoice_payment_term_id': self.pay_terms_a.id,
+ 'invoice_line_ids': [
+ (0, None, self.product_line_vals_1),
+ ]
+ })
+ move.write({
+ 'invoice_line_ids': [
+ (0, None, self.product_line_vals_2),
+ ]
+ })
+
+ self.assertInvoiceValues(move, [
+ {
+ **self.product_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 1000.0,
+ 'debit': 500.0,
+ },
+ {
+ **self.product_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 200.0,
+ 'debit': 100.0,
+ },
+ {
+ **self.tax_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 180.0,
+ 'debit': 90.0,
+ },
+ {
+ **self.tax_line_vals_2,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 30.0,
+ 'debit': 15.0,
+ },
+ {
+ **self.term_line_vals_1,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -1410.0,
+ 'credit': 705.0,
+ },
+ ], {
+ **self.move_vals,
+ 'currency_id': self.currency_data['currency'].id,
+ })
diff --git a/addons/account/tests/test_account_move_partner_count.py b/addons/account/tests/test_account_move_partner_count.py
new file mode 100644
index 00000000..88eab61d
--- /dev/null
+++ b/addons/account/tests/test_account_move_partner_count.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+
+from odoo.tests import tagged
+
+
+@tagged('post_install', '-at_install')
+class TestAccountMovePartnerCount(AccountTestInvoicingCommon):
+
+ def test_account_move_count(self):
+ self.env['account.move'].create([
+ {
+ 'move_type': 'out_invoice',
+ 'date': '2017-01-01',
+ 'invoice_date': '2017-01-01',
+ 'partner_id': self.partner_a.id,
+ 'invoice_line_ids': [(0, 0, {'name': 'aaaa', 'price_unit': 100.0})],
+ },
+ {
+ 'move_type': 'in_invoice',
+ 'date': '2017-01-01',
+ 'invoice_date': '2017-01-01',
+ 'partner_id': self.partner_a.id,
+ 'invoice_line_ids': [(0, 0, {'name': 'aaaa', 'price_unit': 100.0})],
+ },
+ ]).action_post()
+
+ self.assertEqual(self.partner_a.supplier_rank, 1)
+ self.assertEqual(self.partner_a.customer_rank, 1)
diff --git a/addons/account/tests/test_account_move_payments_widget.py b/addons/account/tests/test_account_move_payments_widget.py
new file mode 100644
index 00000000..b4a49515
--- /dev/null
+++ b/addons/account/tests/test_account_move_payments_widget.py
@@ -0,0 +1,184 @@
+# -*- coding: utf-8 -*-
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests import tagged
+
+import json
+
+
+@tagged('post_install', '-at_install')
+class TestAccountMovePaymentsWidget(AccountTestInvoicingCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ cls.receivable_account = cls.company_data['default_account_receivable']
+ cls.payable_account = cls.company_data['default_account_payable']
+
+ cls.currency_data_2 = cls.setup_multi_currency_data(default_values={
+ 'name': 'Stars',
+ 'symbol': '☆',
+ 'currency_unit_label': 'Stars',
+ 'currency_subunit_label': 'Little Stars',
+ }, rate2016=6.0, rate2017=4.0)
+
+ cls.curr_1 = cls.company_data['currency']
+ cls.curr_2 = cls.currency_data['currency']
+ cls.curr_3 = cls.currency_data_2['currency']
+
+ cls.payment_2016_curr_1 = cls.env['account.move'].create({
+ 'date': '2016-01-01',
+ 'line_ids': [
+ (0, 0, {'debit': 0.0, 'credit': 500.0, 'amount_currency': -500.0, 'currency_id': cls.curr_1.id, 'account_id': cls.receivable_account.id, 'partner_id': cls.partner_a.id}),
+ (0, 0, {'debit': 500.0, 'credit': 0.0, 'amount_currency': 500.0, 'currency_id': cls.curr_1.id, 'account_id': cls.payable_account.id, 'partner_id': cls.partner_a.id}),
+ ],
+ })
+ cls.payment_2016_curr_1.action_post()
+
+ cls.payment_2016_curr_2 = cls.env['account.move'].create({
+ 'date': '2016-01-01',
+ 'line_ids': [
+ (0, 0, {'debit': 0.0, 'credit': 500.0, 'amount_currency': -1550.0, 'currency_id': cls.curr_2.id, 'account_id': cls.receivable_account.id, 'partner_id': cls.partner_a.id}),
+ (0, 0, {'debit': 500.0, 'credit': 0.0, 'amount_currency': 1550.0, 'currency_id': cls.curr_2.id, 'account_id': cls.payable_account.id, 'partner_id': cls.partner_a.id}),
+ ],
+ })
+ cls.payment_2016_curr_2.action_post()
+
+ cls.payment_2017_curr_2 = cls.env['account.move'].create({
+ 'date': '2017-01-01',
+ 'line_ids': [
+ (0, 0, {'debit': 0.0, 'credit': 500.0, 'amount_currency': -950.0, 'currency_id': cls.curr_2.id, 'account_id': cls.receivable_account.id, 'partner_id': cls.partner_a.id}),
+ (0, 0, {'debit': 500.0, 'credit': 0.0, 'amount_currency': 950.0, 'currency_id': cls.curr_2.id, 'account_id': cls.payable_account.id, 'partner_id': cls.partner_a.id}),
+ ],
+ })
+ cls.payment_2017_curr_2.action_post()
+
+ cls.payment_2016_curr_3 = cls.env['account.move'].create({
+ 'date': '2016-01-01',
+ 'line_ids': [
+ (0, 0, {'debit': 0.0, 'credit': 500.0, 'amount_currency': -3050.0, 'currency_id': cls.curr_3.id, 'account_id': cls.receivable_account.id, 'partner_id': cls.partner_a.id}),
+ (0, 0, {'debit': 500.0, 'credit': 0.0, 'amount_currency': 3050.0, 'currency_id': cls.curr_3.id, 'account_id': cls.payable_account.id, 'partner_id': cls.partner_a.id}),
+ ],
+ })
+ cls.payment_2016_curr_3.action_post()
+
+ cls.payment_2017_curr_3 = cls.env['account.move'].create({
+ 'date': '2017-01-01',
+ 'line_ids': [
+ (0, 0, {'debit': 0.0, 'credit': 500.0, 'amount_currency': -1950.0, 'currency_id': cls.curr_3.id, 'account_id': cls.receivable_account.id, 'partner_id': cls.partner_a.id}),
+ (0, 0, {'debit': 500.0, 'credit': 0.0, 'amount_currency': 1950.0, 'currency_id': cls.curr_3.id, 'account_id': cls.payable_account.id, 'partner_id': cls.partner_a.id}),
+ ],
+ })
+ cls.payment_2017_curr_3.action_post()
+
+ # -------------------------------------------------------------------------
+ # HELPERS
+ # -------------------------------------------------------------------------
+
+ def _test_all_outstanding_payments(self, invoice, expected_amounts):
+ ''' Check the outstanding payments widget before/after the reconciliation.
+ :param invoice: An account.move record.
+ :param expected_amounts: A map <move_id> -> <amount>
+ '''
+
+ # Check suggested outstanding payments.
+ to_reconcile_payments_widget_vals = json.loads(invoice.invoice_outstanding_credits_debits_widget)
+
+ self.assertTrue(to_reconcile_payments_widget_vals)
+
+ current_amounts = {vals['move_id']: vals['amount'] for vals in to_reconcile_payments_widget_vals['content']}
+ self.assertDictEqual(current_amounts, expected_amounts)
+
+ # Reconcile
+ pay_term_lines = invoice.line_ids\
+ .filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
+ to_reconcile = self.env['account.move'].browse(list(current_amounts.keys()))\
+ .line_ids\
+ .filtered(lambda line: line.account_id == pay_term_lines.account_id)
+ (pay_term_lines + to_reconcile).reconcile()
+
+ # Check payments after reconciliation.
+ reconciled_payments_widget_vals = json.loads(invoice.invoice_payments_widget)
+
+ self.assertTrue(reconciled_payments_widget_vals)
+
+ current_amounts = {vals['move_id']: vals['amount'] for vals in reconciled_payments_widget_vals['content']}
+ self.assertDictEqual(current_amounts, expected_amounts)
+
+ # -------------------------------------------------------------------------
+ # TESTS
+ # -------------------------------------------------------------------------
+
+ def test_outstanding_payments_single_currency(self):
+ ''' Test the outstanding payments widget on invoices having the same currency
+ as the company one.
+ '''
+
+ # Customer invoice of 2500.0 in curr_1.
+ out_invoice = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'date': '2017-01-01',
+ 'invoice_date': '2017-01-01',
+ 'partner_id': self.partner_a.id,
+ 'currency_id': self.curr_1.id,
+ 'invoice_line_ids': [(0, 0, {'name': '/', 'price_unit': 2500.0})],
+ })
+ out_invoice.action_post()
+
+ # Vendor bill of 2500.0 in curr_1.
+ in_invoice = self.env['account.move'].create({
+ 'move_type': 'in_invoice',
+ 'date': '2017-01-01',
+ 'invoice_date': '2017-01-01',
+ 'partner_id': self.partner_a.id,
+ 'currency_id': self.curr_1.id,
+ 'invoice_line_ids': [(0, 0, {'name': '/', 'price_unit': 2500.0})],
+ })
+ in_invoice.action_post()
+
+ expected_amounts = {
+ self.payment_2016_curr_1.id: 500.0,
+ self.payment_2016_curr_2.id: 500.0,
+ self.payment_2017_curr_2.id: 500.0,
+ self.payment_2016_curr_3.id: 500.0,
+ self.payment_2017_curr_3.id: 500.0,
+ }
+
+ self._test_all_outstanding_payments(out_invoice, expected_amounts)
+ self._test_all_outstanding_payments(in_invoice, expected_amounts)
+
+ def test_outstanding_payments_foreign_currency(self):
+ ''' Test the outstanding payments widget on invoices having a foreign currency. '''
+
+ # Customer invoice of 2500.0 in curr_1.
+ out_invoice = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'date': '2017-01-01',
+ 'invoice_date': '2017-01-01',
+ 'partner_id': self.partner_a.id,
+ 'currency_id': self.curr_2.id,
+ 'invoice_line_ids': [(0, 0, {'name': '/', 'price_unit': 7500.0})],
+ })
+ out_invoice.action_post()
+
+ # Vendor bill of 2500.0 in curr_1.
+ in_invoice = self.env['account.move'].create({
+ 'move_type': 'in_invoice',
+ 'date': '2017-01-01',
+ 'invoice_date': '2017-01-01',
+ 'partner_id': self.partner_a.id,
+ 'currency_id': self.curr_2.id,
+ 'invoice_line_ids': [(0, 0, {'name': '/', 'price_unit': 7500.0})],
+ })
+ in_invoice.action_post()
+
+ expected_amounts = {
+ self.payment_2016_curr_1.id: 1500.0,
+ self.payment_2016_curr_2.id: 1550.0,
+ self.payment_2017_curr_2.id: 950.0,
+ self.payment_2016_curr_3.id: 1500.0,
+ self.payment_2017_curr_3.id: 1000.0,
+ }
+
+ self._test_all_outstanding_payments(out_invoice, expected_amounts)
+ self._test_all_outstanding_payments(in_invoice, expected_amounts)
diff --git a/addons/account/tests/test_account_move_reconcile.py b/addons/account/tests/test_account_move_reconcile.py
new file mode 100644
index 00000000..140d97d0
--- /dev/null
+++ b/addons/account/tests/test_account_move_reconcile.py
@@ -0,0 +1,2457 @@
+# -*- coding: utf-8 -*-
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests import tagged
+from odoo.tests.common import Form
+from odoo import fields
+
+
+@tagged('post_install', '-at_install')
+class TestAccountMoveReconcile(AccountTestInvoicingCommon):
+ ''' Tests about the account.partial.reconcile model, not the reconciliation itself but mainly the computation of
+ the residual amounts on account.move.line.
+ '''
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ cls.extra_receivable_account_1 = cls.copy_account(cls.company_data['default_account_receivable'])
+ cls.extra_receivable_account_2 = cls.copy_account(cls.company_data['default_account_receivable'])
+ cls.extra_payable_account_1 = cls.copy_account(cls.company_data['default_account_payable'])
+ cls.extra_payable_account_2 = cls.copy_account(cls.company_data['default_account_payable'])
+
+ # ==== Multi-currency setup ====
+
+ cls.currency_data_2 = cls.setup_multi_currency_data(default_values={
+ 'name': 'Diamond',
+ 'symbol': '💎',
+ 'currency_unit_label': 'Diamond',
+ 'currency_subunit_label': 'Carbon',
+ }, rate2016=6.0, rate2017=4.0)
+
+ # ==== Cash Basis Taxes setup ====
+
+ cls.cash_basis_base_account = cls.env['account.account'].create({
+ 'code': 'cash_basis_base_account',
+ 'name': 'cash_basis_base_account',
+ 'user_type_id': cls.env.ref('account.data_account_type_revenue').id,
+ 'company_id': cls.company_data['company'].id,
+ })
+ cls.company_data['company'].account_cash_basis_base_account_id = cls.cash_basis_base_account
+
+ cls.cash_basis_transfer_account = cls.env['account.account'].create({
+ 'code': 'cash_basis_transfer_account',
+ 'name': 'cash_basis_transfer_account',
+ 'user_type_id': cls.env.ref('account.data_account_type_revenue').id,
+ 'company_id': cls.company_data['company'].id,
+ })
+
+ cls.tax_account_1 = cls.env['account.account'].create({
+ 'code': 'tax_account_1',
+ 'name': 'tax_account_1',
+ 'user_type_id': cls.env.ref('account.data_account_type_revenue').id,
+ 'company_id': cls.company_data['company'].id,
+ })
+
+ cls.tax_account_2 = cls.env['account.account'].create({
+ 'code': 'tax_account_2',
+ 'name': 'tax_account_2',
+ 'user_type_id': cls.env.ref('account.data_account_type_revenue').id,
+ 'company_id': cls.company_data['company'].id,
+ })
+
+ cls.fake_country = cls.env['res.country'].create({
+ 'name': "The Island of the Fly",
+ 'code': 'YY',
+ })
+
+ cls.tax_tags = cls.env['account.account.tag'].create({
+ 'name': 'tax_tag_%s' % str(i),
+ 'applicability': 'taxes',
+ 'country_id': cls.fake_country.id,
+ } for i in range(8))
+
+ cls.cash_basis_tax_a_third_amount = cls.env['account.tax'].create({
+ 'name': 'tax_1',
+ 'amount': 33.3333,
+ 'company_id': cls.company_data['company'].id,
+ 'cash_basis_transition_account_id': cls.cash_basis_transfer_account.id,
+ 'tax_exigibility': 'on_payment',
+ 'invoice_repartition_line_ids': [
+ (0, 0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'tag_ids': [(6, 0, cls.tax_tags[0].ids)],
+ }),
+
+ (0, 0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'account_id': cls.tax_account_1.id,
+ 'tag_ids': [(6, 0, cls.tax_tags[1].ids)],
+ }),
+ ],
+ 'refund_repartition_line_ids': [
+ (0, 0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'tag_ids': [(6, 0, cls.tax_tags[2].ids)],
+ }),
+
+ (0, 0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'account_id': cls.tax_account_1.id,
+ 'tag_ids': [(6, 0, cls.tax_tags[3].ids)],
+ }),
+ ],
+ })
+
+ cls.cash_basis_tax_tiny_amount = cls.env['account.tax'].create({
+ 'name': 'cash_basis_tax_tiny_amount',
+ 'amount': 0.0001,
+ 'company_id': cls.company_data['company'].id,
+ 'cash_basis_transition_account_id': cls.cash_basis_transfer_account.id,
+ 'tax_exigibility': 'on_payment',
+ 'invoice_repartition_line_ids': [
+ (0, 0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'tag_ids': [(6, 0, cls.tax_tags[4].ids)],
+ }),
+
+ (0, 0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'account_id': cls.tax_account_2.id,
+ 'tag_ids': [(6, 0, cls.tax_tags[5].ids)],
+ }),
+ ],
+ 'refund_repartition_line_ids': [
+ (0, 0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'tag_ids': [(6, 0, cls.tax_tags[6].ids)],
+ }),
+
+ (0, 0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'account_id': cls.tax_account_2.id,
+ 'tag_ids': [(6, 0, cls.tax_tags[7].ids)],
+ }),
+ ],
+ })
+
+ # -------------------------------------------------------------------------
+ # HELPERS
+ # -------------------------------------------------------------------------
+
+ def assertFullReconcile(self, full_reconcile, lines):
+ exchange_difference_move = full_reconcile.exchange_move_id
+ partials = lines.mapped('matched_debit_ids') + lines.mapped('matched_credit_ids')
+
+ if full_reconcile.exchange_move_id:
+ lines += exchange_difference_move.line_ids.filtered(lambda line: line.account_id == lines[0].account_id)
+
+ # Use sets to not depend of the order.
+ self.assertEqual(set(full_reconcile.partial_reconcile_ids), set(partials))
+ self.assertEqual(set(full_reconcile.reconciled_line_ids), set(lines))
+
+ # Ensure there is no residual amount left.
+ self.assertRecordValues(lines, [{
+ 'amount_residual': 0.0,
+ 'amount_residual_currency': 0.0,
+ 'reconciled': bool(line.account_id.reconcile),
+ } for line in lines])
+
+ def assertPartialReconcile(self, partials, expected_vals_list):
+ partials = partials.sorted(lambda part: (
+ part.amount,
+ part.debit_amount_currency,
+ part.credit_amount_currency,
+ ))
+ self.assertRecordValues(partials, expected_vals_list)
+
+ def assertAmountsGroupByAccount(self, amount_per_account):
+ expected_values = {account.id: (account, balance, amount_currency) for account, balance, amount_currency in amount_per_account}
+
+ if not expected_values:
+ return
+
+ self.cr.execute('''
+ SELECT
+ line.account_id,
+ COALESCE(SUM(line.balance), 0.0) AS total_balance,
+ COALESCE(SUM(line.amount_currency), 0.0) AS total_amount_currency
+ FROM account_move_line line
+ WHERE line.account_id IN %s
+ GROUP BY line.account_id
+ ''', [tuple(expected_values.keys())])
+ for account_id, total_balance, total_amount_currency in self.cr.fetchall():
+ account, expected_balance, expected_amount_currency = expected_values[account_id]
+ self.assertEqual(
+ total_balance,
+ expected_balance,
+ "Balance of %s is incorrect" % account.name,
+ )
+ self.assertEqual(
+ total_amount_currency,
+ expected_amount_currency,
+ "Amount currency of %s is incorrect" % account.name,
+ )
+
+ def assertTaxGridAmounts(self, amount_per_tag):
+ expected_values = {tag.id: (tag, balance) for tag, balance in amount_per_tag}
+
+ if not expected_values:
+ return
+
+ self.cr.execute('''
+ SELECT
+ rel.account_account_tag_id,
+ SUM(line.balance)
+ FROM account_account_tag_account_move_line_rel rel
+ JOIN account_move_line line ON line.id = rel.account_move_line_id
+ WHERE line.tax_exigible IS TRUE
+ AND line.company_id IN %(company_ids)s
+ GROUP BY rel.account_account_tag_id
+ ''', {
+ 'company_ids': tuple(self.env.companies.ids),
+ })
+
+ for tag_id, total_balance in self.cr.fetchall():
+ tag, expected_balance = expected_values[tag_id]
+ self.assertEqual(
+ total_balance,
+ expected_balance,
+ "Balance of %s is incorrect" % tag.name,
+ )
+
+ # -------------------------------------------------------------------------
+ # Test creation of account.partial.reconcile/account.full.reconcile
+ # during the reconciliation.
+ # -------------------------------------------------------------------------
+
+ def test_reconcile_single_currency(self):
+ account_id = self.company_data['default_account_receivable'].id
+
+ move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2016-01-01',
+ 'line_ids': [
+ (0, 0, {'debit': 1000.0, 'credit': 0.0, 'account_id': account_id}),
+ (0, 0, {'debit': 200.0, 'credit': 0.0, 'account_id': account_id}),
+ (0, 0, {'debit': 0.0, 'credit': 300.0, 'account_id': account_id}),
+ (0, 0, {'debit': 0.0, 'credit': 400.0, 'account_id': account_id}),
+ (0, 0, {'debit': 0.0, 'credit': 500.0, 'account_id': account_id}),
+ ]
+ })
+ move.action_post()
+
+ line_1 = move.line_ids.filtered(lambda line: line.debit == 1000.0)
+ line_2 = move.line_ids.filtered(lambda line: line.debit == 200.0)
+ line_3 = move.line_ids.filtered(lambda line: line.credit == 300.0)
+ line_4 = move.line_ids.filtered(lambda line: line.credit == 400.0)
+ line_5 = move.line_ids.filtered(lambda line: line.credit == 500.0)
+
+ self.assertRecordValues(line_1 + line_2 + line_3 + line_4 + line_5, [
+ {'amount_residual': 1000.0, 'amount_residual_currency': 1000.0, 'reconciled': False},
+ {'amount_residual': 200.0, 'amount_residual_currency': 200.0, 'reconciled': False},
+ {'amount_residual': -300.0, 'amount_residual_currency': -300.0, 'reconciled': False},
+ {'amount_residual': -400.0, 'amount_residual_currency': -400.0, 'reconciled': False},
+ {'amount_residual': -500.0, 'amount_residual_currency': -500.0, 'reconciled': False},
+ ])
+
+ res = (line_1 + line_3).reconcile()
+
+ self.assertPartialReconcile(res['partials'], [{
+ 'amount': 300.0,
+ 'debit_amount_currency': 300.0,
+ 'credit_amount_currency': 300.0,
+ 'debit_move_id': line_1.id,
+ 'credit_move_id': line_3.id,
+ }])
+
+ self.assertRecordValues(line_1 + line_3, [
+ {'amount_residual': 700.0, 'amount_residual_currency': 700.0, 'reconciled': False},
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ ])
+
+ res = (line_1 + line_4).reconcile()
+
+ self.assertPartialReconcile(res['partials'], [{
+ 'amount': 400.0,
+ 'debit_amount_currency': 400.0,
+ 'credit_amount_currency': 400.0,
+ 'debit_move_id': line_1.id,
+ 'credit_move_id': line_4.id,
+ }])
+
+ self.assertRecordValues(line_1 + line_4, [
+ {'amount_residual': 300.0, 'amount_residual_currency': 300.0, 'reconciled': False},
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ ])
+
+ res = (line_1 + line_5).reconcile()
+
+ self.assertPartialReconcile(res['partials'], [{
+ 'amount': 300.0,
+ 'debit_amount_currency': 300.0,
+ 'credit_amount_currency': 300.0,
+ 'debit_move_id': line_1.id,
+ 'credit_move_id': line_5.id,
+ }])
+
+ self.assertRecordValues(line_1 + line_5, [
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ {'amount_residual': -200.0, 'amount_residual_currency': -200.0, 'reconciled': False},
+ ])
+
+ res = (line_2 + line_5).reconcile()
+
+ self.assertPartialReconcile(res['partials'], [{
+ 'amount': 200.0,
+ 'debit_amount_currency': 200.0,
+ 'credit_amount_currency': 200.0,
+ 'debit_move_id': line_2.id,
+ 'credit_move_id': line_5.id,
+ }])
+
+ self.assertRecordValues(res['full_reconcile'], [{'exchange_move_id': False}])
+ self.assertFullReconcile(res['full_reconcile'], line_1 + line_2 + line_3 + line_4 + line_5)
+
+ def test_reconcile_same_foreign_currency(self):
+ account_id = self.company_data['default_account_receivable'].id
+
+ # Rate is 3.0 in 2016, 2.0 in 2017.
+ currency_id = self.currency_data['currency'].id
+
+ moves = self.env['account.move'].create([
+ {
+ 'move_type': 'entry',
+ 'date': '2016-01-01',
+ 'line_ids': [
+ (0, 0, {'debit': 1200.0, 'credit': 0.0, 'amount_currency': 3600.0, 'account_id': account_id, 'currency_id': currency_id}),
+ (0, 0, {'debit': 120.0, 'credit': 0.0, 'amount_currency': 360.0, 'account_id': account_id, 'currency_id': currency_id}),
+
+ (0, 0, {'debit': 0.0, 'credit': 1320.0, 'account_id': account_id}),
+ ]
+ },
+ {
+ 'move_type': 'entry',
+ 'date': '2017-01-01',
+ 'line_ids': [
+ (0, 0, {'debit': 0.0, 'credit': 240.0, 'amount_currency': -480.0, 'account_id': account_id, 'currency_id': currency_id}),
+ (0, 0, {'debit': 0.0, 'credit': 720.0, 'amount_currency': -1440.0, 'account_id': account_id, 'currency_id': currency_id}),
+ (0, 0, {'debit': 0.0, 'credit': 1020.0, 'amount_currency': -2040.0, 'account_id': account_id, 'currency_id': currency_id}),
+
+ (0, 0, {'debit': 1980.0, 'credit': 0.0, 'account_id': account_id}),
+ ]
+ }
+ ])
+
+ moves.action_post()
+
+ line_1 = moves.line_ids.filtered(lambda line: line.debit == 1200.0)
+ line_2 = moves.line_ids.filtered(lambda line: line.debit == 120.0)
+ line_3 = moves.line_ids.filtered(lambda line: line.credit == 240.0)
+ line_4 = moves.line_ids.filtered(lambda line: line.credit == 720.0)
+ line_5 = moves.line_ids.filtered(lambda line: line.credit == 1020.0)
+
+ self.assertRecordValues(line_1 + line_2 + line_3 + line_4 + line_5, [
+ {'amount_residual': 1200.0, 'amount_residual_currency': 3600.0, 'reconciled': False},
+ {'amount_residual': 120.0, 'amount_residual_currency': 360.0, 'reconciled': False},
+ {'amount_residual': -240.0, 'amount_residual_currency': -480.0, 'reconciled': False},
+ {'amount_residual': -720.0, 'amount_residual_currency': -1440.0, 'reconciled': False},
+ {'amount_residual': -1020.0, 'amount_residual_currency': -2040.0, 'reconciled': False},
+ ])
+
+ res = (line_1 + line_3 + line_4).reconcile()
+
+ self.assertPartialReconcile(res['partials'], [
+ # Partial generated when reconciling line_1 & line_3:
+ {
+ 'amount': 240.0,
+ 'debit_amount_currency': 480.0,
+ 'credit_amount_currency': 480.0,
+ 'debit_move_id': line_1.id,
+ 'credit_move_id': line_3.id,
+ },
+ # Partial generated when reconciling line_1 & line_4:
+ {
+ 'amount': 720.0,
+ 'debit_amount_currency': 1440.0,
+ 'credit_amount_currency': 1440.0,
+ 'debit_move_id': line_1.id,
+ 'credit_move_id': line_4.id,
+ },
+ ])
+
+ self.assertRecordValues(line_1 + line_3 + line_4, [
+ {'amount_residual': 240.0, 'amount_residual_currency': 1680.0, 'reconciled': False},
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ ])
+
+ res = (line_1 + line_5).reconcile()
+
+ self.assertPartialReconcile(res['partials'], [{
+ 'amount': 240.0,
+ 'debit_amount_currency': 1680.0,
+ 'credit_amount_currency': 1680.0,
+ 'debit_move_id': line_1.id,
+ 'credit_move_id': line_5.id,
+ }])
+
+ self.assertRecordValues(line_1 + line_5, [
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ {'amount_residual': -780.0, 'amount_residual_currency': -360.0, 'reconciled': False},
+ ])
+
+ res = (line_2 + line_5).reconcile()
+
+ exchange_diff = res['full_reconcile'].exchange_move_id
+ exchange_diff_lines = exchange_diff.line_ids.sorted(lambda line: (line.currency_id, abs(line.amount_currency), -line.amount_currency))
+
+ self.assertRecordValues(exchange_diff_lines, [
+ # Fix line_2:
+ {
+ 'debit': 660.0,
+ 'credit': 0.0,
+ 'amount_currency': 0.0,
+ 'currency_id': currency_id,
+ 'account_id': account_id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 660.0,
+ 'amount_currency': 0.0,
+ 'currency_id': currency_id,
+ 'account_id': exchange_diff.journal_id.company_id.income_currency_exchange_account_id.id,
+ },
+ ])
+
+ self.assertPartialReconcile(res['partials'], [
+ # Partial generated when reconciling line_2 & line_5:
+ {
+ 'amount': 120.0,
+ 'debit_amount_currency': 360.0,
+ 'credit_amount_currency': 360.0,
+ 'debit_move_id': line_2.id,
+ 'credit_move_id': line_5.id,
+ },
+ # Partial fixing line_4 (exchange difference):
+ {
+ 'amount': 660.0,
+ 'debit_amount_currency': 0.0,
+ 'credit_amount_currency': 0.0,
+ 'debit_move_id': exchange_diff_lines[0].id,
+ 'credit_move_id': line_5.id,
+ },
+ ])
+
+ self.assertFullReconcile(res['full_reconcile'], line_1 + line_2 + line_3 + line_4 + line_5)
+
+ def test_reconcile_multiple_currencies(self):
+ account_id = self.company_data['default_account_receivable'].id
+
+ # Rate is 3.0 in 2016, 2.0 in 2017.
+ currency1_id = self.currency_data['currency'].id
+ # Rate is 6.0 in 2016, 4.0 in 2017.
+ currency2_id = self.currency_data_2['currency'].id
+
+ moves = self.env['account.move'].create([
+ {
+ 'move_type': 'entry',
+ 'date': '2016-01-01',
+ 'line_ids': [
+ (0, 0, {'debit': 1200.0, 'credit': 0.0, 'amount_currency': 3600.0, 'account_id': account_id, 'currency_id': currency1_id}),
+ (0, 0, {'debit': 780.0, 'credit': 0.0, 'amount_currency': 2340.0, 'account_id': account_id, 'currency_id': currency1_id}),
+
+ (0, 0, {'debit': 0.0, 'credit': 1980.0, 'account_id': account_id}),
+ ]
+ },
+ {
+ 'move_type': 'entry',
+ 'date': '2017-01-01',
+ 'line_ids': [
+ (0, 0, {'debit': 0.0, 'credit': 240.0, 'amount_currency': -960.0, 'account_id': account_id, 'currency_id': currency2_id}),
+ (0, 0, {'debit': 0.0, 'credit': 720.0, 'amount_currency': -2880.0, 'account_id': account_id, 'currency_id': currency2_id}),
+ (0, 0, {'debit': 0.0, 'credit': 1020.0, 'amount_currency': -4080.0, 'account_id': account_id, 'currency_id': currency2_id}),
+
+ (0, 0, {'debit': 1980.0, 'credit': 0.0, 'account_id': account_id}),
+ ]
+ }
+ ])
+
+ moves.action_post()
+
+ line_1 = moves.line_ids.filtered(lambda line: line.debit == 1200.0)
+ line_2 = moves.line_ids.filtered(lambda line: line.debit == 780.0)
+ line_3 = moves.line_ids.filtered(lambda line: line.credit == 240.0)
+ line_4 = moves.line_ids.filtered(lambda line: line.credit == 720.0)
+ line_5 = moves.line_ids.filtered(lambda line: line.credit == 1020.0)
+
+ self.assertRecordValues(line_1 + line_2 + line_3 + line_4 + line_5, [
+ {'amount_residual': 1200.0, 'amount_residual_currency': 3600.0, 'reconciled': False},
+ {'amount_residual': 780.0, 'amount_residual_currency': 2340.0, 'reconciled': False},
+ {'amount_residual': -240.0, 'amount_residual_currency': -960.0, 'reconciled': False},
+ {'amount_residual': -720.0, 'amount_residual_currency': -2880.0, 'reconciled': False},
+ {'amount_residual': -1020.0, 'amount_residual_currency': -4080.0, 'reconciled': False},
+ ])
+
+ res = (line_1 + line_3 + line_4).reconcile()
+
+ self.assertPartialReconcile(res['partials'], [
+ # Partial generated when reconciling line_1 & line_3:
+ {
+ 'amount': 240.0,
+ 'debit_amount_currency': 480.0,
+ 'credit_amount_currency': 1440.0,
+ 'debit_move_id': line_1.id,
+ 'credit_move_id': line_3.id,
+ },
+ # Partial generated when reconciling line_1 & line_4:
+ {
+ 'amount': 720.0,
+ 'debit_amount_currency': 1440.0,
+ 'credit_amount_currency': 4320.0,
+ 'debit_move_id': line_1.id,
+ 'credit_move_id': line_4.id,
+ },
+ ])
+
+ self.assertRecordValues(line_1 + line_3 + line_4, [
+ {'amount_residual': 240.0, 'amount_residual_currency': 1680.0, 'reconciled': False},
+ {'amount_residual': 0.0, 'amount_residual_currency': 480.0, 'reconciled': False},
+ {'amount_residual': 0.0, 'amount_residual_currency': 1440.0, 'reconciled': False},
+ ])
+
+ res = (line_1 + line_5).reconcile()
+
+ self.assertPartialReconcile(res['partials'], [{
+ 'amount': 240.0,
+ 'debit_amount_currency': 480.0,
+ 'credit_amount_currency': 1440.0,
+ 'debit_move_id': line_1.id,
+ 'credit_move_id': line_5.id,
+ }])
+
+ self.assertRecordValues(line_1 + line_5, [
+ {'amount_residual': 0.0, 'amount_residual_currency': 1200.0, 'reconciled': False},
+ {'amount_residual': -780.0, 'amount_residual_currency': -2640.0, 'reconciled': False},
+ ])
+
+ res = (line_2 + line_5).reconcile()
+
+ exchange_diff = res['full_reconcile'].exchange_move_id
+ exchange_diff_lines = exchange_diff.line_ids.sorted(lambda line: (line.currency_id, abs(line.amount_currency), -line.amount_currency))
+
+ self.assertRecordValues(exchange_diff_lines, [
+ # Fix line_2:
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': 780.0,
+ 'currency_id': currency1_id,
+ 'account_id': exchange_diff.journal_id.company_id.expense_currency_exchange_account_id.id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': -780.0,
+ 'currency_id': currency1_id,
+ 'account_id': account_id,
+ },
+ # Fix line_3:
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': 480.0,
+ 'currency_id': currency2_id,
+ 'account_id': exchange_diff.journal_id.company_id.expense_currency_exchange_account_id.id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': -480.0,
+ 'currency_id': currency2_id,
+ 'account_id': account_id,
+ },
+ # Fix line_4:
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': 1440.0,
+ 'currency_id': currency2_id,
+ 'account_id': exchange_diff.journal_id.company_id.expense_currency_exchange_account_id.id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': -1440.0,
+ 'currency_id': currency2_id,
+ 'account_id': account_id,
+ },
+ # Fix line_5:
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': 2040.0,
+ 'currency_id': currency2_id,
+ 'account_id': exchange_diff.journal_id.company_id.expense_currency_exchange_account_id.id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': -2040.0,
+ 'currency_id': currency2_id,
+ 'account_id': account_id,
+ },
+ # Fix line_1:
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': 1200.0,
+ 'currency_id': currency1_id,
+ 'account_id': exchange_diff.journal_id.company_id.expense_currency_exchange_account_id.id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': -1200.0,
+ 'currency_id': currency1_id,
+ 'account_id': account_id,
+ },
+ ])
+
+ self.assertPartialReconcile(res['partials'], [
+ # Partial fixing line_3 (exchange difference):
+ {
+ 'amount': 0.0,
+ 'debit_amount_currency': 480.0,
+ 'credit_amount_currency': 480.0,
+ 'debit_move_id': line_3.id,
+ 'credit_move_id': exchange_diff_lines[3].id,
+ },
+ # Partial fixing line_2 (exchange difference):
+ {
+ 'amount': 0.0,
+ 'debit_amount_currency': 780.0,
+ 'credit_amount_currency': 780.0,
+ 'debit_move_id': line_2.id,
+ 'credit_move_id': exchange_diff_lines[1].id,
+ },
+ # Partial fixing line_1 (exchange difference):
+ {
+ 'amount': 0.0,
+ 'debit_amount_currency': 1200.0,
+ 'credit_amount_currency': 1200.0,
+ 'debit_move_id': line_1.id,
+ 'credit_move_id': exchange_diff_lines[9].id,
+ },
+ # Partial fixing line_4 (exchange difference):
+ {
+ 'amount': 0.0,
+ 'debit_amount_currency': 1440.0,
+ 'credit_amount_currency': 1440.0,
+ 'debit_move_id': line_4.id,
+ 'credit_move_id': exchange_diff_lines[5].id,
+ },
+ # Partial fixing line_5 (exchange difference):
+ {
+ 'amount': 0.0,
+ 'debit_amount_currency': 2040.0,
+ 'credit_amount_currency': 2040.0,
+ 'debit_move_id': line_5.id,
+ 'credit_move_id': exchange_diff_lines[7].id,
+ },
+ # Partial generated when reconciling line_2 & line_5:
+ {
+ 'amount': 780.0,
+ 'debit_amount_currency': 1560.0,
+ 'credit_amount_currency': 4680.0,
+ 'debit_move_id': line_2.id,
+ 'credit_move_id': line_5.id,
+ },
+ ])
+
+ self.assertFullReconcile(res['full_reconcile'], line_1 + line_2 + line_3 + line_4 + line_5)
+
+ def test_reconcile_asymetric_rate_change(self):
+ account_id = self.company_data['default_account_receivable'].id
+
+ # Rate is 3.0 in 2016, 2.0 in 2017.
+ currency1_id = self.currency_data['currency'].id
+ # Rate is 6.0 in 2016, 4.0 in 2017.
+ currency2_id = self.currency_data_2['currency'].id
+
+ # Create rate changes for 2018: currency1 rate increases while currency2 rate decreases.
+ self.env['res.currency.rate'].create({
+ 'name': '2018-01-01',
+ 'rate': 8.0,
+ 'currency_id': currency1_id,
+ 'company_id': self.company_data['company'].id,
+ })
+
+ self.env['res.currency.rate'].create({
+ 'name': '2018-01-01',
+ 'rate': 2.0,
+ 'currency_id': currency2_id,
+ 'company_id': self.company_data['company'].id,
+ })
+
+ moves = self.env['account.move'].create([
+ {
+ 'move_type': 'entry',
+ 'date': '2018-01-01',
+ 'line_ids': [
+ (0, 0, {
+ 'debit': 1200.0,
+ 'credit': 0.0,
+ 'amount_currency': 9600.0,
+ 'account_id': account_id,
+ 'currency_id': currency1_id,
+ }),
+ (0, 0, {
+ 'debit': 960.0,
+ 'credit': 0.0,
+ 'amount_currency': 1920.0,
+ 'account_id': account_id,
+ 'currency_id': currency2_id,
+ }),
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 2160.0,
+ 'account_id': account_id,
+ }),
+ ]
+ },
+ {
+ 'move_type': 'entry',
+ 'date': '2017-01-01',
+ 'line_ids': [
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 1200.0,
+ 'amount_currency': -4800.0,
+ 'account_id': account_id,
+ 'currency_id': currency2_id,
+ }),
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 960.0,
+ 'amount_currency': -1920.0,
+ 'account_id': account_id,
+ 'currency_id': currency1_id,
+ }),
+ (0, 0, {
+ 'debit': 2160.0,
+ 'credit': 0.0,
+ 'account_id': account_id,
+ }),
+ ]
+ }
+ ])
+
+ moves.action_post()
+
+ line_1 = moves.line_ids.filtered(lambda line: line.debit == 1200.0)
+ line_2 = moves.line_ids.filtered(lambda line: line.debit == 960.0)
+ line_3 = moves.line_ids.filtered(lambda line: line.credit == 1200.0)
+ line_4 = moves.line_ids.filtered(lambda line: line.credit == 960.0)
+
+ self.assertRecordValues(line_1 + line_2 + line_3 + line_4, [
+ {'amount_residual': 1200.0, 'amount_residual_currency': 9600.0, 'reconciled': False},
+ {'amount_residual': 960.0, 'amount_residual_currency': 1920.0, 'reconciled': False},
+ {'amount_residual': -1200.0, 'amount_residual_currency': -4800.0, 'reconciled': False},
+ {'amount_residual': -960.0, 'amount_residual_currency': -1920.0, 'reconciled': False},
+ ])
+
+ # Reconcile with debit_line currency rate increased and credit_line currency rate decreased between
+ # credit_line.date and debit_line.date.
+
+ res = (line_1 + line_3).reconcile()
+
+ exchange_diff = res['full_reconcile'].exchange_move_id
+ exchange_diff_lines = exchange_diff.line_ids.sorted(lambda line: (line.currency_id, abs(line.amount_currency), -line.amount_currency))
+
+ self.assertRecordValues(exchange_diff_lines, [
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': 2400.0,
+ 'currency_id': currency2_id,
+ 'account_id': account_id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': -2400.0,
+ 'currency_id': currency2_id,
+ 'account_id': exchange_diff.journal_id.company_id.income_currency_exchange_account_id.id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': 7200.0,
+ 'currency_id': currency1_id,
+ 'account_id': exchange_diff.journal_id.company_id.expense_currency_exchange_account_id.id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': -7200.0,
+ 'currency_id': currency1_id,
+ 'account_id': account_id,
+ }
+ ])
+
+ self.assertPartialReconcile(res['partials'], [
+ {
+ 'amount': 0.0,
+ 'debit_amount_currency': 2400.0,
+ 'credit_amount_currency': 2400.0,
+ 'debit_move_id': exchange_diff_lines[0].id,
+ 'credit_move_id': line_3.id,
+ },
+ {
+ 'amount': 0.0,
+ 'debit_amount_currency': 7200.0,
+ 'credit_amount_currency': 7200.0,
+ 'debit_move_id': line_1.id,
+ 'credit_move_id': exchange_diff_lines[3].id,
+ },
+ {
+ 'amount': 1200.0,
+ 'debit_amount_currency': 2400.0,
+ 'credit_amount_currency': 2400.0,
+ 'debit_move_id': line_1.id,
+ 'credit_move_id': line_3.id,
+ },
+ ])
+
+ self.assertRecordValues(line_1 + line_3, [
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ ])
+
+ # Reconcile with debit_line currency rate decreased and credit_line currency rate increased between
+ # credit_line.date and debit_line.date.
+
+ res = (line_2 + line_4).reconcile()
+
+ exchange_diff = res['full_reconcile'].exchange_move_id
+ exchange_diff_lines = exchange_diff.line_ids.sorted(lambda line: (line.currency_id, abs(line.amount_currency), -line.amount_currency))
+
+ self.assertRecordValues(exchange_diff_lines, [
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': 5760.0,
+ 'currency_id': currency1_id,
+ 'account_id': exchange_diff.journal_id.company_id.expense_currency_exchange_account_id.id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': -5760.0,
+ 'currency_id': currency1_id,
+ 'account_id': account_id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': 1920.0,
+ 'currency_id': currency2_id,
+ 'account_id': account_id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': -1920.0,
+ 'currency_id': currency2_id,
+ 'account_id': exchange_diff.journal_id.company_id.income_currency_exchange_account_id.id,
+ }
+ ])
+
+ self.assertPartialReconcile(res['partials'], [
+ {
+ 'amount': 0.0,
+ 'debit_amount_currency': 1920.0,
+ 'credit_amount_currency': 1920.0,
+ 'debit_move_id': exchange_diff_lines[2].id,
+ 'credit_move_id': line_2.id,
+ },
+ {
+ 'amount': 0.0,
+ 'debit_amount_currency': 5760.0,
+ 'credit_amount_currency': 5760.0,
+ 'debit_move_id': line_4.id,
+ 'credit_move_id': exchange_diff_lines[1].id,
+ },
+ {
+ 'amount': 960.0,
+ 'debit_amount_currency': 3840.0,
+ 'credit_amount_currency': 7680.0,
+ 'debit_move_id': line_2.id,
+ 'credit_move_id': line_4.id,
+ },
+ ])
+
+ self.assertRecordValues(line_2 + line_4, [
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ ])
+
+ def test_reverse_exchange_difference_same_foreign_currency(self):
+ move_2016 = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2016-01-01',
+ 'line_ids': [
+ (0, 0, {
+ 'debit': 1200.0,
+ 'credit': 0.0,
+ 'amount_currency': 3600.0,
+ 'account_id': self.company_data['default_account_receivable'].id,
+ 'currency_id': self.currency_data['currency'].id,
+ }),
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 1200.0,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ }),
+ ],
+ })
+ move_2017 = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2017-01-01',
+ 'line_ids': [
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 1800.0,
+ 'amount_currency': -3600.0,
+ 'account_id': self.company_data['default_account_receivable'].id,
+ 'currency_id': self.currency_data['currency'].id,
+ }),
+ (0, 0, {
+ 'debit': 1800.0,
+ 'credit': 0.0,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ }),
+ ],
+ })
+ (move_2016 + move_2017).action_post()
+
+ rec_line_2016 = move_2016.line_ids.filtered(lambda line: line.account_id.internal_type == 'receivable')
+ rec_line_2017 = move_2017.line_ids.filtered(lambda line: line.account_id.internal_type == 'receivable')
+
+ self.assertRecordValues(rec_line_2016 + rec_line_2017, [
+ {'amount_residual': 1200.0, 'amount_residual_currency': 3600.0, 'reconciled': False},
+ {'amount_residual': -1800.0, 'amount_residual_currency': -3600.0, 'reconciled': False},
+ ])
+
+ # Reconcile.
+
+ res = (rec_line_2016 + rec_line_2017).reconcile()
+
+ self.assertRecordValues(rec_line_2016 + rec_line_2017, [
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ ])
+
+ exchange_diff = res['full_reconcile'].exchange_move_id
+ exchange_diff_lines = exchange_diff.line_ids.sorted('balance')
+
+ self.assertRecordValues(exchange_diff_lines, [
+ {
+ 'debit': 0.0,
+ 'credit': 600.0,
+ 'amount_currency': 0.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'account_id': exchange_diff.journal_id.company_id.income_currency_exchange_account_id.id,
+ },
+ {
+ 'debit': 600.0,
+ 'credit': 0.0,
+ 'amount_currency': 0.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'account_id': self.company_data['default_account_receivable'].id,
+ },
+ ])
+
+ self.assertRecordValues(exchange_diff_lines, [
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': False},
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ ])
+
+ # Unreconcile.
+ # A reversal is created to cancel the exchange difference journal entry.
+
+ (rec_line_2016 + rec_line_2017).remove_move_reconcile()
+
+ reverse_exchange_diff = exchange_diff_lines[1].matched_credit_ids.credit_move_id.move_id
+ reverse_exchange_diff_lines = reverse_exchange_diff.line_ids.sorted('balance')
+
+ self.assertRecordValues(reverse_exchange_diff_lines, [
+ {
+ 'debit': 0.0,
+ 'credit': 600.0,
+ 'amount_currency': 0.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'account_id': self.company_data['default_account_receivable'].id,
+ },
+ {
+ 'debit': 600.0,
+ 'credit': 0.0,
+ 'amount_currency': 0.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'account_id': exchange_diff.journal_id.company_id.income_currency_exchange_account_id.id,
+ },
+ ])
+
+ self.assertRecordValues(exchange_diff_lines + reverse_exchange_diff_lines, [
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': False},
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': False},
+ ])
+
+ partials = reverse_exchange_diff_lines.matched_debit_ids + reverse_exchange_diff_lines.matched_credit_ids
+ self.assertPartialReconcile(partials, [{
+ 'amount': 600.0,
+ 'debit_amount_currency': 0.0,
+ 'credit_amount_currency': 0.0,
+ 'debit_move_id': exchange_diff_lines[1].id,
+ 'credit_move_id': reverse_exchange_diff_lines[0].id,
+ }])
+
+ def test_reverse_exchange_multiple_foreign_currencies(self):
+ move_2016 = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2016-01-01',
+ 'line_ids': [
+ (0, 0, {
+ 'debit': 1200.0,
+ 'credit': 0.0,
+ 'amount_currency': 7200.0,
+ 'account_id': self.company_data['default_account_receivable'].id,
+ 'currency_id': self.currency_data_2['currency'].id,
+ }),
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 1200.0,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ }),
+ ],
+ })
+ move_2017 = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2017-01-01',
+ 'line_ids': [
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 1200.0,
+ 'amount_currency': -2400.0,
+ 'account_id': self.company_data['default_account_receivable'].id,
+ 'currency_id': self.currency_data['currency'].id,
+ }),
+ (0, 0, {
+ 'debit': 1200.0,
+ 'credit': 0.0,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ }),
+ ],
+ })
+ (move_2016 + move_2017).action_post()
+
+ rec_line_2016 = move_2016.line_ids.filtered(lambda line: line.account_id.internal_type == 'receivable')
+ rec_line_2017 = move_2017.line_ids.filtered(lambda line: line.account_id.internal_type == 'receivable')
+
+ self.assertRecordValues(rec_line_2016 + rec_line_2017, [
+ {'amount_residual': 1200.0, 'amount_residual_currency': 7200.0, 'reconciled': False},
+ {'amount_residual': -1200.0, 'amount_residual_currency': -2400.0, 'reconciled': False},
+ ])
+
+ # Reconcile.
+
+ res = (rec_line_2016 + rec_line_2017).reconcile()
+
+ self.assertRecordValues(rec_line_2016 + rec_line_2017, [
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ ])
+
+ exchange_diff = res['full_reconcile'].exchange_move_id
+ exchange_diff_lines = exchange_diff.line_ids.sorted('amount_currency')
+
+ self.assertRecordValues(exchange_diff_lines, [
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': -2400.0,
+ 'currency_id': self.currency_data_2['currency'].id,
+ 'account_id': self.company_data['default_account_receivable'].id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': -1200.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'account_id': self.company_data['default_account_receivable'].id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': 1200.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'account_id': exchange_diff.journal_id.company_id.expense_currency_exchange_account_id.id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': 2400.0,
+ 'currency_id': self.currency_data_2['currency'].id,
+ 'account_id': exchange_diff.journal_id.company_id.expense_currency_exchange_account_id.id,
+ },
+ ])
+
+ self.assertRecordValues(exchange_diff_lines, [
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': False},
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': False},
+ ])
+
+ # Unreconcile.
+ # A reversal is created to cancel the exchange difference journal entry.
+
+ (rec_line_2016 + rec_line_2017).remove_move_reconcile()
+
+ reverse_exchange_diff = exchange_diff_lines[1].matched_debit_ids.debit_move_id.move_id
+ reverse_exchange_diff_lines = reverse_exchange_diff.line_ids.sorted('amount_currency')
+
+ self.assertRecordValues(reverse_exchange_diff_lines, [
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': -2400.0,
+ 'currency_id': self.currency_data_2['currency'].id,
+ 'account_id': exchange_diff.journal_id.company_id.expense_currency_exchange_account_id.id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': -1200.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'account_id': exchange_diff.journal_id.company_id.expense_currency_exchange_account_id.id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': 1200.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'account_id': self.company_data['default_account_receivable'].id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': 2400.0,
+ 'currency_id': self.currency_data_2['currency'].id,
+ 'account_id': self.company_data['default_account_receivable'].id,
+ },
+ ])
+
+ self.assertRecordValues(exchange_diff_lines + reverse_exchange_diff_lines, [
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': False},
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': False},
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': False},
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': False},
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ ])
+
+ partials = reverse_exchange_diff_lines.matched_debit_ids + reverse_exchange_diff_lines.matched_credit_ids
+ self.assertPartialReconcile(partials, [
+ {
+ 'amount': 0.0,
+ 'debit_amount_currency': 1200.0,
+ 'credit_amount_currency': 1200.0,
+ 'debit_move_id': reverse_exchange_diff_lines[2].id,
+ 'credit_move_id': exchange_diff_lines[1].id,
+ },
+ {
+ 'amount': 0.0,
+ 'debit_amount_currency': 2400.0,
+ 'credit_amount_currency': 2400.0,
+ 'debit_move_id': reverse_exchange_diff_lines[3].id,
+ 'credit_move_id': exchange_diff_lines[0].id,
+ },
+ ])
+
+ # -------------------------------------------------------------------------
+ # Test creation of extra journal entries during the reconciliation to
+ # deal with taxes that are exigible on payment (cash basis).
+ # -------------------------------------------------------------------------
+
+ def test_reconcile_cash_basis_workflow_single_currency(self):
+ ''' Test the generated journal entries during the reconciliation to manage the cash basis taxes.
+ Also,
+ - Test the case when there is multiple receivable/payable accounts.
+ - Test the reconciliation with tiny amounts.
+ - Check there is no rounding issue when making the percentage.
+ - Check there is no lost cents when the journal entry is fully reconciled.
+ '''
+ cash_basis_move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2016-01-01',
+ 'line_ids': [
+ # Base Tax line
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 100.0,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'tax_ids': [(6, 0, (self.cash_basis_tax_a_third_amount + self.cash_basis_tax_tiny_amount).ids)],
+ 'tax_exigible': False,
+ }),
+
+ # Tax lines
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 33.33,
+ 'account_id': self.cash_basis_transfer_account.id,
+ 'tax_repartition_line_id': self.cash_basis_tax_a_third_amount.invoice_repartition_line_ids.filtered(lambda line: line.repartition_type == 'tax').id,
+ 'tax_exigible': False,
+ }),
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 0.01,
+ 'account_id': self.cash_basis_transfer_account.id,
+ 'tax_repartition_line_id': self.cash_basis_tax_tiny_amount.invoice_repartition_line_ids.filtered(lambda line: line.repartition_type == 'tax').id,
+ 'tax_exigible': False,
+ }),
+
+ # Receivable lines
+ (0, 0, {
+ 'debit': 44.45,
+ 'credit': 0.0,
+ 'account_id': self.extra_receivable_account_1.id,
+ }),
+ (0, 0, {
+ 'debit': 44.45,
+ 'credit': 0.0,
+ 'account_id': self.extra_receivable_account_2.id,
+ }),
+ (0, 0, {
+ 'debit': 44.45,
+ 'credit': 0.0,
+ 'account_id': self.extra_receivable_account_2.id,
+ }),
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 0.01,
+ 'account_id': self.extra_payable_account_1.id,
+ }),
+ ]
+ })
+
+ payment_move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2017-01-01',
+ 'line_ids': [
+ (0, 0, {'debit': 0.0, 'credit': 33.34, 'account_id': self.extra_receivable_account_1.id}),
+ (0, 0, {'debit': 0.0, 'credit': 11.11, 'account_id': self.extra_receivable_account_1.id}),
+ (0, 0, {'debit': 0.0, 'credit': 88.89, 'account_id': self.extra_receivable_account_2.id}),
+ (0, 0, {'debit': 0.0, 'credit': 0.01, 'account_id': self.extra_receivable_account_2.id}),
+ (0, 0, {'debit': 0.01, 'credit': 0.0, 'account_id': self.extra_payable_account_1.id}),
+ (0, 0, {'debit': 133.34, 'credit': 0.0, 'account_id': self.company_data['default_account_revenue'].id}),
+ ]
+ })
+
+ (cash_basis_move + payment_move).action_post()
+
+ # Initial amounts by accounts:
+
+ self.assertAmountsGroupByAccount([
+ # Account Balance Amount Currency
+ (self.cash_basis_transfer_account, -33.34, -33.34),
+ (self.tax_account_1, 0.0, 0.0),
+ (self.tax_account_2, 0.0, 0.0),
+ (self.cash_basis_base_account, 0.0, 0.0),
+ ])
+
+ # There is 44.45 + 44.45 + 44.45 + 0.01 = 133.36 to reconcile on 'cash_basis_move'.
+ # Reconciling all the amount in extra_receivable_account_1 should compute 2 percentages:
+ # 33.34 / 133.36 = 0.25
+ # 11.11 / 133.36 = 0.083308338
+
+ receivable_lines_1 = (cash_basis_move + payment_move).line_ids\
+ .filtered(lambda line: line.account_id == self.extra_receivable_account_1)
+ res = receivable_lines_1.reconcile()
+
+ self.assertFullReconcile(res['full_reconcile'], receivable_lines_1)
+ self.assertEqual(len(res.get('tax_cash_basis_moves', [])), 2)
+ self.assertRecordValues(res['tax_cash_basis_moves'][0].line_ids, [
+ # Base amount of tax_1 & tax_2:
+ {'debit': 25.0, 'credit': 0.0, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 25.0, 'account_id': self.cash_basis_base_account.id},
+ # tax_1:
+ {'debit': 8.33, 'credit': 0.0, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 8.33, 'account_id': self.tax_account_1.id},
+ # tax_2:
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.tax_account_2.id},
+ ])
+ self.assertRecordValues(res['tax_cash_basis_moves'][1].line_ids, [
+ # Base amount of tax_1 & tax_2:
+ {'debit': 8.33, 'credit': 0.0, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 8.33, 'account_id': self.cash_basis_base_account.id},
+ # tax_1:
+ {'debit': 2.78, 'credit': 0.0, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 2.78, 'account_id': self.tax_account_1.id},
+ # tax_2:
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.tax_account_2.id},
+ ])
+
+ self.assertAmountsGroupByAccount([
+ # Account Balance Amount Currency
+ (self.cash_basis_transfer_account, -22.23, -22.23),
+ (self.tax_account_1, -11.11, -11.11),
+ (self.tax_account_2, 0.0, 0.0),
+ ])
+
+ # Reconciling all the amount in extra_receivable_account_2 should compute 3 percentages:
+ # 44.45 / 133.36 = 0.333308338
+ # 44.44 / 133.36 = 0.333233353
+ # 0.01 / 133.36 = 0.000074985
+
+ receivable_lines_2 = (cash_basis_move + payment_move).line_ids\
+ .filtered(lambda line: line.account_id == self.extra_receivable_account_2)
+ res = receivable_lines_2.reconcile()
+
+ self.assertFullReconcile(res['full_reconcile'], receivable_lines_2)
+ self.assertEqual(len(res.get('tax_cash_basis_moves', [])), 3)
+ self.assertRecordValues(res['tax_cash_basis_moves'][0].line_ids, [
+ # Base amount of tax_1 & tax_2:
+ {'debit': 33.33, 'credit': 0.0, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 33.33, 'account_id': self.cash_basis_base_account.id},
+ # tax_1:
+ {'debit': 11.11, 'credit': 0.0, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 11.11, 'account_id': self.tax_account_1.id},
+ # tax_2:
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.tax_account_2.id},
+ ])
+ self.assertRecordValues(res['tax_cash_basis_moves'][1].line_ids, [
+ # Base amount of tax_1 & tax_2:
+ {'debit': 33.32, 'credit': 0.0, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 33.32, 'account_id': self.cash_basis_base_account.id},
+ # tax_1:
+ {'debit': 11.11, 'credit': 0.0, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 11.11, 'account_id': self.tax_account_1.id},
+ # tax_2:
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.tax_account_2.id},
+ ])
+ self.assertRecordValues(res['tax_cash_basis_moves'][2].line_ids, [
+ # Base amount of tax_1 & tax_2:
+ {'debit': 0.01, 'credit': 0.0, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 0.01, 'account_id': self.cash_basis_base_account.id},
+ # tax_1:
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.tax_account_1.id},
+ # tax_2:
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.tax_account_2.id},
+ ])
+
+ self.assertAmountsGroupByAccount([
+ # Account Balance Amount Currency
+ (self.cash_basis_transfer_account, -0.01, -0.01),
+ (self.tax_account_1, -33.33, -33.33),
+ (self.tax_account_2, 0.0, 0.0),
+ ])
+
+ # Reconciling all the amount in extra_payable_account_1 should trigger the matching number and ensure all
+ # the base amount has been covered without any rounding issue.
+
+ payable_lines_1 = (cash_basis_move + payment_move).line_ids\
+ .filtered(lambda line: line.account_id == self.extra_payable_account_1)
+ res = payable_lines_1.reconcile()
+
+ self.assertFullReconcile(res['full_reconcile'], payable_lines_1)
+ self.assertEqual(len(res.get('tax_cash_basis_moves', [])), 1)
+ self.assertRecordValues(res['tax_cash_basis_moves'].line_ids, [
+ # Base amount of tax_1 & tax_2:
+ {'debit': 0.01, 'credit': 0.0, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 0.01, 'account_id': self.cash_basis_base_account.id},
+ # tax_1:
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.tax_account_1.id},
+ # tax_2:
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.tax_account_2.id},
+ ])
+
+ self.assertRecordValues(res['full_reconcile'].exchange_move_id.line_ids, [
+ {'account_id': self.tax_account_2.id, 'debit': 0.0, 'credit': 0.01, 'tax_ids': [], 'tax_line_id': self.cash_basis_tax_tiny_amount.id},
+ {'account_id': self.cash_basis_transfer_account.id, 'debit': 0.01, 'credit': 0.0, 'tax_ids': [], 'tax_line_id': False},
+ ])
+
+ self.assertAmountsGroupByAccount([
+ # Account Balance Amount Currency
+ (self.cash_basis_transfer_account, 0.0, 0.0),
+ (self.tax_account_1, -33.33, -33.33),
+ (self.tax_account_2, -0.01, -0.01),
+ ])
+
+ def test_reconcile_cash_basis_workflow_multi_currency(self):
+ ''' Same as before with a foreign currency. '''
+
+ currency_id = self.currency_data['currency'].id
+ taxes = self.cash_basis_tax_a_third_amount + self.cash_basis_tax_tiny_amount
+
+ cash_basis_move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2016-01-01',
+ 'line_ids': [
+ # Base Tax line
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 33.34,
+ 'amount_currency': -100.0,
+ 'currency_id': currency_id,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'tax_ids': [(6, 0, taxes.ids)],
+ 'tax_exigible': False,
+ }),
+
+ # Tax lines
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 11.10,
+ 'amount_currency': -33.33,
+ 'currency_id': currency_id,
+ 'account_id': self.cash_basis_transfer_account.id,
+ 'tax_repartition_line_id': self.cash_basis_tax_a_third_amount.invoice_repartition_line_ids.filtered(lambda line: line.repartition_type == 'tax').id,
+ 'tax_exigible': False,
+ }),
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 0.01,
+ 'amount_currency': -0.01,
+ 'currency_id': currency_id,
+ 'account_id': self.cash_basis_transfer_account.id,
+ 'tax_repartition_line_id': self.cash_basis_tax_tiny_amount.invoice_repartition_line_ids.filtered(lambda line: line.repartition_type == 'tax').id,
+ 'tax_exigible': False,
+ }),
+
+ # Receivable lines
+ (0, 0, {
+ 'debit': 14.82,
+ 'credit': 0.0,
+ 'amount_currency': 44.45,
+ 'currency_id': currency_id,
+ 'account_id': self.extra_receivable_account_1.id,
+ }),
+ (0, 0, {
+ 'debit': 14.82,
+ 'credit': 0.0,
+ 'amount_currency': 44.45,
+ 'currency_id': currency_id,
+ 'account_id': self.extra_receivable_account_2.id,
+ }),
+ (0, 0, {
+ 'debit': 14.82,
+ 'credit': 0.0,
+ 'amount_currency': 44.45,
+ 'currency_id': currency_id,
+ 'account_id': self.extra_receivable_account_2.id,
+ }),
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 0.01,
+ 'amount_currency': -0.01,
+ 'currency_id': currency_id,
+ 'account_id': self.extra_payable_account_1.id,
+ }),
+ ]
+ })
+
+ payment_move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2017-01-01',
+ 'line_ids': [
+ (0, 0, {'debit': 0.0, 'credit': 16.67, 'amount_currency': -33.34, 'currency_id': currency_id, 'account_id': self.extra_receivable_account_1.id}),
+ (0, 0, {'debit': 0.0, 'credit': 5.6, 'amount_currency': -11.11, 'currency_id': currency_id, 'account_id': self.extra_receivable_account_1.id}),
+ (0, 0, {'debit': 0.0, 'credit': 44.45, 'amount_currency': -88.89, 'currency_id': currency_id, 'account_id': self.extra_receivable_account_2.id}),
+ (0, 0, {'debit': 0.0, 'credit': 0.01, 'amount_currency': -0.01, 'currency_id': currency_id, 'account_id': self.extra_receivable_account_2.id}),
+ (0, 0, {'debit': 0.01, 'credit': 0.0, 'amount_currency': 0.01, 'currency_id': currency_id, 'account_id': self.extra_payable_account_1.id}),
+ (0, 0, {'debit': 66.72, 'credit': 0.0, 'account_id': self.company_data['default_account_revenue'].id}),
+ ]
+ })
+
+ (cash_basis_move + payment_move).action_post()
+
+ # Initial amounts by accounts:
+
+ self.assertAmountsGroupByAccount([
+ # Account Balance Amount Currency
+ (self.cash_basis_transfer_account, -11.11, -33.34),
+ (self.tax_account_1, 0.0, 0.0),
+ (self.tax_account_2, 0.0, 0.0),
+ ])
+
+ # There is 44.45 + 44.45 + 44.45 + 0.01 = 133.36 to reconcile on 'cash_basis_move'.
+ # Reconciling all the amount in extra_receivable_account_1 should compute 2 percentages:
+ # 33.34 / 133.36 = 0.25
+ # 11.11 / 133.36 = 0.083308338
+
+ receivable_lines_1 = (cash_basis_move + payment_move).line_ids\
+ .filtered(lambda line: line.account_id == self.extra_receivable_account_1)
+ res = receivable_lines_1.reconcile()
+
+ self.assertFullReconcile(res['full_reconcile'], receivable_lines_1)
+ self.assertEqual(len(res.get('tax_cash_basis_moves', [])), 2)
+ self.assertRecordValues(res['tax_cash_basis_moves'][0].line_ids, [
+ # Base amount of tax_1 & tax_2:
+ {'debit': 12.5, 'credit': 0.0, 'amount_currency': 25.0, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 12.5, 'amount_currency': -25.0, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ # tax_1:
+ {'debit': 4.17, 'credit': 0.0, 'amount_currency': 8.333, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 4.17, 'amount_currency': -8.333, 'currency_id': currency_id, 'account_id': self.tax_account_1.id},
+ # tax_2:
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': 0.003, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': -0.003, 'currency_id': currency_id, 'account_id': self.tax_account_2.id},
+ ])
+ self.assertRecordValues(res['tax_cash_basis_moves'][1].line_ids, [
+ # Base amount of tax_1 & tax_2:
+ {'debit': 4.2, 'credit': 0.0, 'amount_currency': 8.331, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 4.2, 'amount_currency': -8.331, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ # tax_1:
+ {'debit': 1.4, 'credit': 0.0, 'amount_currency': 2.777, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 1.4, 'amount_currency': -2.777, 'currency_id': currency_id, 'account_id': self.tax_account_1.id},
+ # tax_2:
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': 0.001, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': -0.001, 'currency_id': currency_id, 'account_id': self.tax_account_2.id},
+ ])
+
+ self.assertAmountsGroupByAccount([
+ # Account Balance Amount Currency
+ (self.cash_basis_transfer_account, -5.54, -22.226),
+ (self.tax_account_1, -5.57, -11.11),
+ (self.tax_account_2, 0.0, -0.004),
+ ])
+
+ # Reconciling all the amount in extra_receivable_account_2 should compute 3 percentages:
+ # 44.45 / 133.36 = 0.333308338
+ # 44.44 / 133.36 = 0.333233353
+ # 0.01 / 133.36 = 0.000074985
+
+ receivable_lines_2 = (cash_basis_move + payment_move).line_ids\
+ .filtered(lambda line: line.account_id == self.extra_receivable_account_2)
+ res = receivable_lines_2.reconcile()
+
+ self.assertFullReconcile(res['full_reconcile'], receivable_lines_2)
+ self.assertEqual(len(res.get('tax_cash_basis_moves', [])), 3)
+ self.assertRecordValues(res['tax_cash_basis_moves'][0].line_ids, [
+ # Base amount of tax_1 & tax_2:
+ {'debit': 16.67, 'credit': 0.0, 'amount_currency': 33.331, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 16.67, 'amount_currency': -33.331, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ # tax_1:
+ {'debit': 5.56, 'credit': 0.0, 'amount_currency': 11.109, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 5.56, 'amount_currency': -11.109, 'currency_id': currency_id, 'account_id': self.tax_account_1.id},
+ # tax_2:
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': 0.003, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': -0.003, 'currency_id': currency_id, 'account_id': self.tax_account_2.id},
+ ])
+ self.assertRecordValues(res['tax_cash_basis_moves'][1].line_ids, [
+ # Base amount of tax_1 & tax_2:
+ {'debit': 16.66, 'credit': 0.0, 'amount_currency': 33.323, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 16.66, 'amount_currency': -33.323, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ # tax_1:
+ {'debit': 5.55, 'credit': 0.0, 'amount_currency': 11.107, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 5.55, 'amount_currency': -11.107, 'currency_id': currency_id, 'account_id': self.tax_account_1.id},
+ # tax_2:
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': 0.003, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': -0.003, 'currency_id': currency_id, 'account_id': self.tax_account_2.id},
+ ])
+ self.assertRecordValues(res['tax_cash_basis_moves'][2].line_ids, [
+ # Base amount of tax_1 & tax_2:
+ {'debit': 0.01, 'credit': 0.0, 'amount_currency': 0.007, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 0.01, 'amount_currency': -0.007, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ # tax_1:
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': 0.002, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': -0.002, 'currency_id': currency_id, 'account_id': self.tax_account_1.id},
+ # tax_2:
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': currency_id, 'account_id': self.tax_account_2.id},
+ ])
+
+ self.assertAmountsGroupByAccount([
+ # Account Balance Amount Currency
+ (self.cash_basis_transfer_account, 5.57, -0.002),
+ (self.tax_account_1, -16.68, -33.328),
+ (self.tax_account_2, 0.0, -0.01),
+ ])
+
+ # Reconciling all the amount in extra_payable_account_1 should trigger the matching number and ensure all
+ # the base amount has been covered without any rounding issue.
+
+ payable_lines_1 = (cash_basis_move + payment_move).line_ids\
+ .filtered(lambda line: line.account_id == self.extra_payable_account_1)
+ res = payable_lines_1.reconcile()
+
+ self.assertFullReconcile(res['full_reconcile'], payable_lines_1)
+ self.assertEqual(len(res.get('tax_cash_basis_moves', [])), 1)
+ self.assertRecordValues(res['tax_cash_basis_moves'].line_ids, [
+ # Base amount of tax_1 & tax_2:
+ {'debit': 0.01, 'credit': 0.0, 'amount_currency': 0.007, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 0.01, 'amount_currency': -0.007, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ # tax_1:
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': 0.002, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': -0.002, 'currency_id': currency_id, 'account_id': self.tax_account_1.id},
+ # tax_2:
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': currency_id, 'account_id': self.tax_account_2.id},
+ ])
+
+ self.assertRecordValues(res['full_reconcile'].exchange_move_id.line_ids, [
+ {'account_id': self.cash_basis_base_account.id, 'debit': 16.71, 'credit': 0.0, 'tax_ids': taxes.ids, 'tax_line_id': False},
+ {'account_id': self.cash_basis_base_account.id, 'debit': 0.0, 'credit': 16.71, 'tax_ids': [], 'tax_line_id': False},
+ {'account_id': self.tax_account_1.id, 'debit': 5.58, 'credit': 0.0, 'tax_ids': [], 'tax_line_id': self.cash_basis_tax_a_third_amount.id},
+ {'account_id': self.cash_basis_transfer_account.id, 'debit': 0.0, 'credit': 5.58, 'tax_ids': [], 'tax_line_id': False},
+ {'account_id': self.tax_account_2.id, 'debit': 0.0, 'credit': 0.01, 'tax_ids': [], 'tax_line_id': self.cash_basis_tax_tiny_amount.id},
+ {'account_id': self.cash_basis_transfer_account.id, 'debit': 0.01, 'credit': 0.0, 'tax_ids': [], 'tax_line_id': False},
+ ])
+
+ self.assertAmountsGroupByAccount([
+ # Account Balance Amount Currency
+ (self.cash_basis_transfer_account, 0.0, 0.0),
+ (self.tax_account_1, -11.1, -33.33),
+ (self.tax_account_2, -0.01, -0.01),
+ ])
+
+ def test_reconcile_cash_basis_exchange_difference_transfer_account_check_entries_1(self):
+ ''' Test the generation of the exchange difference for a tax cash basis journal entry when the transfer
+ account is not a reconcile one.
+ '''
+ currency_id = self.currency_data['currency'].id
+
+ # Rate 1/3 in 2016.
+ cash_basis_move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2016-01-01',
+ 'line_ids': [
+ # Base Tax line
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 100.0,
+ 'amount_currency': -300.0,
+ 'currency_id': currency_id,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'tax_ids': [(6, 0, self.cash_basis_tax_a_third_amount.ids)],
+ 'tax_exigible': False,
+ }),
+
+ # Tax line
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 33.33,
+ 'amount_currency': -100.0,
+ 'currency_id': currency_id,
+ 'account_id': self.cash_basis_transfer_account.id,
+ 'tax_repartition_line_id': self.cash_basis_tax_a_third_amount.invoice_repartition_line_ids.filtered(lambda line: line.repartition_type == 'tax').id,
+ 'tax_exigible': False,
+ }),
+
+ # Receivable lines
+ (0, 0, {
+ 'debit': 133.33,
+ 'credit': 0.0,
+ 'amount_currency': 400.0,
+ 'currency_id': currency_id,
+ 'account_id': self.extra_receivable_account_1.id,
+ }),
+ ]
+ })
+
+ # Rate 1/2 in 2017.
+ payment_move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2017-01-01',
+ 'line_ids': [
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 201.0,
+ 'amount_currency': -402.0, # Don't create the full reconcile directly.
+ 'currency_id': currency_id,
+ 'account_id': self.extra_receivable_account_1.id,
+ }),
+ (0, 0, {
+ 'debit': 201.0,
+ 'credit': 0.0,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ }),
+ ]
+ })
+
+ # Move making the payment fully paid.
+ end_move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2017-01-01',
+ 'line_ids': [
+ (0, 0, {
+ 'debit': 1.0,
+ 'credit': 0.0,
+ 'amount_currency': 2.0,
+ 'currency_id': currency_id,
+ 'account_id': self.extra_receivable_account_1.id,
+ }),
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 1.0,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ }),
+ ]
+ })
+
+ (cash_basis_move + payment_move + end_move).action_post()
+
+ self.assertAmountsGroupByAccount([
+ # Account Balance Amount Currency
+ (self.cash_basis_transfer_account, -33.33, -100.0),
+ (self.tax_account_1, 0.0, 0.0),
+ ])
+
+ receivable_lines = (cash_basis_move + payment_move).line_ids\
+ .filtered(lambda line: line.account_id == self.extra_receivable_account_1)
+ res = receivable_lines.reconcile()
+
+ self.assertEqual(len(res.get('tax_cash_basis_moves', [])), 1)
+ self.assertRecordValues(res['tax_cash_basis_moves'].line_ids, [
+ # Base amount:
+ {'debit': 150.0, 'credit': 0.0, 'amount_currency': 300.0, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 150.0, 'amount_currency': -300.0, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ # tax:
+ {'debit': 50.0, 'credit': 0.0, 'amount_currency': 100.0, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 50.0, 'amount_currency': -100.0, 'currency_id': currency_id, 'account_id': self.tax_account_1.id},
+ ])
+
+ receivable_lines2 = (payment_move + end_move).line_ids\
+ .filtered(lambda line: line.account_id == self.extra_receivable_account_1)
+ res = receivable_lines2.reconcile()
+
+ self.assertFullReconcile(res['full_reconcile'], receivable_lines + receivable_lines2)
+
+ exchange_diff = res['full_reconcile'].exchange_move_id
+ exchange_diff_lines = exchange_diff.line_ids\
+ .filtered(lambda line: line.account_id == self.cash_basis_transfer_account)\
+ .sorted(lambda line: (line.account_id, line.debit, line.credit))
+
+ self.assertRecordValues(exchange_diff_lines, [
+ {
+ 'debit': 0.0,
+ 'credit': 16.67,
+ 'amount_currency': 0.0,
+ 'currency_id': currency_id,
+ 'account_id': self.cash_basis_transfer_account.id,
+ },
+ ])
+
+ self.assertAmountsGroupByAccount([
+ # Account Balance Amount Currency
+ (self.cash_basis_transfer_account, 0.0, 0.0),
+ (self.tax_account_1, -33.33, -100.0),
+ ])
+
+ def test_reconcile_cash_basis_exchange_difference_transfer_account_check_entries_2(self):
+ ''' Test the generation of the exchange difference for a tax cash basis journal entry when the transfer
+ account is not a reconcile one.
+ '''
+ currency_id = self.setup_multi_currency_data(default_values={
+ 'name': 'bitcoin',
+ 'symbol': 'bc',
+ 'currency_unit_label': 'Bitcoin',
+ 'currency_subunit_label': 'Tiny bitcoin',
+ }, rate2016=0.5, rate2017=0.66666666666666)['currency'].id
+
+ # Rate 2/1 in 2016.
+ caba_inv = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2016-01-01',
+ 'line_ids': [
+ # Base Tax line
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 200.0,
+ 'amount_currency': -100.0,
+ 'currency_id': currency_id,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'tax_ids': [(6, 0, self.cash_basis_tax_a_third_amount.ids)],
+ 'tax_exigible': False,
+ }),
+
+ # Tax line
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 20.0,
+ 'amount_currency': -10.0,
+ 'currency_id': currency_id,
+ 'account_id': self.cash_basis_transfer_account.id,
+ 'tax_repartition_line_id': self.cash_basis_tax_a_third_amount.invoice_repartition_line_ids.filtered(lambda line: line.repartition_type == 'tax').id,
+ 'tax_exigible': False,
+ }),
+
+ # Receivable lines
+ (0, 0, {
+ 'debit': 220.0,
+ 'credit': 0.0,
+ 'amount_currency': 110.0,
+ 'currency_id': currency_id,
+ 'account_id': self.extra_receivable_account_1.id,
+ }),
+ ]
+ })
+ caba_inv.action_post()
+
+ # Rate 3/2 in 2017. Full payment of 110 in foreign currency
+ pmt_wizard = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=caba_inv.ids).create({
+ 'payment_date': '2017-01-01',
+ 'journal_id': self.company_data['default_journal_bank'].id,
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
+ })
+ pmt_wizard._create_payments()
+ partial_rec = caba_inv.mapped('line_ids.matched_credit_ids')
+ caba_move = self.env['account.move'].search([('tax_cash_basis_rec_id', 'in', partial_rec.ids)])
+
+ self.assertRecordValues(caba_move.line_ids, [
+ {'account_id': self.cash_basis_base_account.id, 'debit': 150.0, 'credit': 0.0, 'amount_currency': 100.0, 'tax_ids': [], 'tax_line_id': False},
+ {'account_id': self.cash_basis_base_account.id, 'debit': 0.0, 'credit': 150.0, 'amount_currency': -100.0, 'tax_ids': self.cash_basis_tax_a_third_amount.ids, 'tax_line_id': False},
+ {'account_id': self.cash_basis_transfer_account.id, 'debit': 15.0, 'credit': 0.0, 'amount_currency': 10.0, 'tax_ids': [], 'tax_line_id': False},
+ {'account_id': self.tax_account_1.id, 'debit': 0.0, 'credit': 15.0, 'amount_currency': -10.0, 'tax_ids': [], 'tax_line_id': self.cash_basis_tax_a_third_amount.id},
+ ])
+
+ receivable_line = caba_inv.line_ids.filtered(lambda x: x.account_id.internal_type == 'receivable')
+ self.assertTrue(receivable_line.full_reconcile_id, "Invoice should be fully paid")
+
+ exchange_move = receivable_line.full_reconcile_id.exchange_move_id
+ self.assertTrue(exchange_move, "There should be an exchange difference move created")
+ self.assertRecordValues(exchange_move.line_ids, [
+ {'account_id': receivable_line.account_id.id, 'debit': 0.0, 'credit': 55.0, 'amount_currency': 0.0, 'tax_ids': [], 'tax_line_id': False},
+ {'account_id': caba_move.company_id.expense_currency_exchange_account_id.id, 'debit': 55.0, 'credit': 0.0, 'amount_currency': 0.0, 'tax_ids': [], 'tax_line_id': False},
+ {'account_id': self.cash_basis_base_account.id, 'debit': 0.0, 'credit': 50.0, 'amount_currency': 0.0, 'tax_ids': self.cash_basis_tax_a_third_amount.ids, 'tax_line_id': False},
+ {'account_id': self.cash_basis_base_account.id, 'debit': 50.0, 'credit': 0.0, 'amount_currency': 0.0, 'tax_ids': [], 'tax_line_id': False},
+ {'account_id': self.tax_account_1.id, 'debit': 0.0, 'credit': 5.0, 'amount_currency': 0.0, 'tax_ids': [], 'tax_line_id': self.cash_basis_tax_a_third_amount.id},
+ {'account_id': self.cash_basis_transfer_account.id, 'debit': 5.0, 'credit': 0.0, 'amount_currency': 0.0, 'tax_ids': [], 'tax_line_id': False},
+ ])
+
+ self.assertAmountsGroupByAccount([
+ # Account Balance Amount Currency
+ (self.cash_basis_transfer_account, 0.0, 0.0),
+ (self.tax_account_1, -20.0, -10.0),
+ ])
+
+ def test_reconcile_cash_basis_exchange_difference_transfer_account_check_entries_3(self):
+ ''' Test the generation of the exchange difference for a tax cash basis journal entry when the transfer
+ account is not a reconcile one.
+ '''
+ currency_id = self.setup_multi_currency_data(default_values={
+ 'name': 'bitcoin',
+ 'symbol': 'bc',
+ 'currency_unit_label': 'Bitcoin',
+ 'currency_subunit_label': 'Tiny bitcoin',
+ 'rounding': 0.01,
+ }, rate2016=0.5, rate2017=0.66666666666666)['currency'].id
+
+ # Rate 2/1 in 2016.
+ caba_inv = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2016-01-01',
+ 'line_ids': [
+ # Base Tax line
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 200.0,
+ 'amount_currency': -100.0,
+ 'currency_id': currency_id,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'tax_ids': [(6, 0, self.cash_basis_tax_a_third_amount.ids)],
+ 'tax_exigible': False,
+ }),
+
+ # Tax line
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 20.0,
+ 'amount_currency': -10.0,
+ 'currency_id': currency_id,
+ 'account_id': self.cash_basis_transfer_account.id,
+ 'tax_repartition_line_id': self.cash_basis_tax_a_third_amount.invoice_repartition_line_ids.filtered(lambda line: line.repartition_type == 'tax').id,
+ 'tax_exigible': False,
+ }),
+
+ # Receivable lines
+ (0, 0, {
+ 'debit': 220.0,
+ 'credit': 0.0,
+ 'amount_currency': 110.0,
+ 'currency_id': currency_id,
+ 'account_id': self.extra_receivable_account_1.id,
+ }),
+ ]
+ })
+ caba_inv.action_post()
+
+ # Rate 3/2 in 2017. Full payment of 220 in company currency
+ pmt_wizard = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=caba_inv.ids).create({
+ 'payment_date': '2017-01-01',
+ 'journal_id': self.company_data['default_journal_bank'].id,
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount': 220.0,
+ })
+ pmt_wizard._create_payments()
+
+ caba_move = self.env['account.move'].search([('tax_cash_basis_rec_id', 'in', caba_inv.line_ids.matched_credit_ids.ids)])
+ self.assertRecordValues(caba_move.line_ids, [
+ {'account_id': self.cash_basis_base_account.id, 'debit': 200.01, 'credit': 0.0, 'amount_currency': 133.34, 'tax_ids': [], 'tax_line_id': False},
+ {'account_id': self.cash_basis_base_account.id, 'debit': 0.0, 'credit': 200.01, 'amount_currency': -133.34, 'tax_ids': self.cash_basis_tax_a_third_amount.ids, 'tax_line_id': False},
+ {'account_id': self.cash_basis_transfer_account.id, 'debit': 20.0, 'credit': 0.0, 'amount_currency': 13.33, 'tax_ids': [], 'tax_line_id': False},
+ {'account_id': self.tax_account_1.id, 'debit': 0.0, 'credit': 20.0, 'amount_currency': -13.33, 'tax_ids': [], 'tax_line_id': self.cash_basis_tax_a_third_amount.id},
+ ])
+
+ receivable_line = caba_inv.line_ids.filtered(lambda x: x.account_id.internal_type == 'receivable')
+ self.assertTrue(receivable_line.full_reconcile_id, "Invoice should be fully paid")
+
+ exchange_move = receivable_line.full_reconcile_id.exchange_move_id
+ self.assertRecordValues(exchange_move.line_ids, [
+ {'account_id': self.extra_receivable_account_1.id, 'debit': 0.0, 'credit': 0.0, 'amount_currency': 36.67, 'tax_ids': [], 'tax_line_id': False},
+ {'account_id': caba_move.company_id.income_currency_exchange_account_id.id, 'debit': 0.0, 'credit': 0.0, 'amount_currency': -36.67, 'tax_ids': [], 'tax_line_id': False},
+ {'account_id': self.cash_basis_base_account.id, 'debit': 0.01, 'credit': 0.0, 'amount_currency': 0.0, 'tax_ids': self.cash_basis_tax_a_third_amount.ids, 'tax_line_id': False},
+ {'account_id': self.cash_basis_base_account.id, 'debit': 0.0, 'credit': 0.01, 'amount_currency': 0.0, 'tax_ids': [], 'tax_line_id': False},
+ ])
+
+ self.assertAmountsGroupByAccount([
+ # Account Balance Amount Currency
+ (self.cash_basis_transfer_account, 0.0, 3.33),
+ (self.tax_account_1, -20.0, -13.33),
+ ])
+
+ def test_reconcile_cash_basis_revert(self):
+ ''' Ensure the cash basis journal entry can be reverted. '''
+ self.cash_basis_transfer_account.reconcile = True
+
+ invoice_move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2016-01-01',
+ 'line_ids': [
+ # Base Tax line
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 100.0,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'tax_ids': [(6, 0, self.cash_basis_tax_a_third_amount.ids)],
+ 'tax_exigible': False,
+ }),
+
+ # Tax line
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 33.33,
+ 'account_id': self.cash_basis_transfer_account.id,
+ 'tax_repartition_line_id': self.cash_basis_tax_a_third_amount.invoice_repartition_line_ids.filtered(lambda line: line.repartition_type == 'tax').id,
+ 'tax_exigible': False,
+ }),
+
+ # Receivable line
+ (0, 0, {
+ 'debit': 133.33,
+ 'credit': 0.0,
+ 'account_id': self.extra_receivable_account_1.id,
+ }),
+ ]
+ })
+
+ payment_move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2016-01-01',
+ 'line_ids': [
+ (0, 0, {'debit': 0.0, 'credit': 133.33, 'account_id': self.extra_receivable_account_1.id}),
+ (0, 0, {'debit': 133.33, 'credit': 0.0, 'account_id': self.company_data['default_account_revenue'].id}),
+ ]
+ })
+
+ (invoice_move + payment_move).action_post()
+
+ receivable_lines = (invoice_move + payment_move).line_ids\
+ .filtered(lambda line: line.account_id == self.extra_receivable_account_1)
+ res = receivable_lines.reconcile()
+
+ # == Check reconciliation of invoice with payment ==
+
+ self.assertFullReconcile(res['full_reconcile'], receivable_lines)
+ self.assertEqual(len(res.get('tax_cash_basis_moves', [])), 1)
+
+ # == Check the reconciliation of invoice with tax cash basis journal entry.
+ # /!\ We make the assumption the tax cash basis journal entry is well created.
+
+ tax_cash_basis_move = res['tax_cash_basis_moves']
+
+ taxes_lines = (invoice_move.line_ids + tax_cash_basis_move.line_ids.filtered('debit'))\
+ .filtered(lambda line: line.account_id == self.cash_basis_transfer_account)
+ taxes_full_reconcile = taxes_lines.matched_debit_ids.full_reconcile_id
+
+ self.assertTrue(taxes_full_reconcile)
+ self.assertFullReconcile(taxes_full_reconcile, taxes_lines)
+
+ # == Check the reconciliation after the reverse ==
+
+ tax_cash_basis_move_reverse = tax_cash_basis_move._reverse_moves(cancel=True)
+
+ self.assertFullReconcile(res['full_reconcile'], receivable_lines)
+
+ # == Check the reconciliation of the tax cash basis journal entry with its reverse ==
+
+ reversed_taxes_lines = (tax_cash_basis_move + tax_cash_basis_move_reverse).line_ids\
+ .filtered(lambda line: line.account_id == self.cash_basis_transfer_account)
+
+ reversed_taxes_full_reconcile = reversed_taxes_lines.matched_debit_ids.full_reconcile_id
+
+ self.assertTrue(reversed_taxes_full_reconcile)
+ self.assertFullReconcile(reversed_taxes_full_reconcile, reversed_taxes_lines)
+
+ def test_reconcile_cash_basis_tax_grid_refund(self):
+ invoice_move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2016-01-01',
+ 'line_ids': [
+ # Base Tax line
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 100.0,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'tax_ids': [(6, 0, self.cash_basis_tax_a_third_amount.ids)],
+ 'tax_tag_ids': [(6, 0, self.tax_tags[0].ids)],
+ 'tax_exigible': False,
+ }),
+
+ # Tax line
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 33.33,
+ 'account_id': self.cash_basis_transfer_account.id,
+ 'tax_repartition_line_id': self.cash_basis_tax_a_third_amount.invoice_repartition_line_ids.filtered(lambda line: line.repartition_type == 'tax').id,
+ 'tax_tag_ids': [(6, 0, self.tax_tags[1].ids)],
+ 'tax_exigible': False,
+ }),
+
+ # Receivable line
+ (0, 0, {
+ 'debit': 133.33,
+ 'credit': 0.0,
+ 'account_id': self.extra_receivable_account_1.id,
+ }),
+ ]
+ })
+
+ refund_move = self.env['account.move'].create({
+ 'move_type': 'out_refund',
+ 'partner_id': self.partner_a.id,
+ 'invoice_date': '2016-01-01',
+ 'date': '2016-01-01',
+ 'line_ids': [
+ # Base Tax line
+ (0, 0, {
+ 'debit': 100.0,
+ 'credit': 0.0,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'tax_ids': [(6, 0, self.cash_basis_tax_a_third_amount.ids)],
+ 'tax_tag_ids': [(6, 0, self.tax_tags[2].ids)],
+ 'tax_exigible': False,
+ }),
+
+ # Tax line
+ (0, 0, {
+ 'debit': 33.33,
+ 'credit': 0.0,
+ 'account_id': self.cash_basis_transfer_account.id,
+ 'tax_repartition_line_id': self.cash_basis_tax_a_third_amount.invoice_repartition_line_ids.filtered(lambda line: line.repartition_type == 'tax').id,
+ 'tax_tag_ids': [(6, 0, self.tax_tags[3].ids)],
+ 'tax_exigible': False,
+ }),
+
+ # Receivable line
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 133.33,
+ 'account_id': self.extra_receivable_account_1.id,
+ }),
+ ]
+ })
+
+ (invoice_move + refund_move).action_post()
+
+ receivable_lines = (invoice_move + refund_move).line_ids\
+ .filtered(lambda line: line.account_id == self.extra_receivable_account_1)
+ res = receivable_lines.reconcile()
+
+ self.assertFullReconcile(res['full_reconcile'], receivable_lines)
+ self.assertEqual(len(res.get('tax_cash_basis_moves', [])), 2)
+
+ tax_cash_basis_moves = res['tax_cash_basis_moves'].sorted(lambda move: move.tax_cash_basis_move_id.id)
+
+ # Invoice:
+ cb_lines = tax_cash_basis_moves[0].line_ids.sorted(lambda line: (-abs(line.balance), -line.debit, line.account_id))
+ self.assertRecordValues(cb_lines, [
+ # Base amount:
+ {'debit': 100.0, 'credit': 0.0, 'tax_tag_ids': [], 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 100.0, 'tax_tag_ids': self.tax_tags[0].ids, 'account_id': self.cash_basis_base_account.id},
+ # tax:
+ {'debit': 33.33, 'credit': 0.0, 'tax_tag_ids': [], 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 33.33, 'tax_tag_ids': self.tax_tags[1].ids, 'account_id': self.tax_account_1.id},
+ ])
+
+ # Refund:
+ cb_lines = tax_cash_basis_moves[1].line_ids.sorted(lambda line: (-abs(line.balance), -line.debit, line.account_id))
+ self.assertRecordValues(cb_lines, [
+ # Base amount:
+ {'debit': 100.0, 'credit': 0.0, 'tax_tag_ids': self.tax_tags[2].ids, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 100.0, 'tax_tag_ids': [], 'account_id': self.cash_basis_base_account.id},
+ # tax:
+ {'debit': 33.33, 'credit': 0.0, 'tax_tag_ids': self.tax_tags[3].ids, 'account_id': self.tax_account_1.id},
+ {'debit': 0.0, 'credit': 33.33, 'tax_tag_ids': [], 'account_id': self.cash_basis_transfer_account.id},
+ ])
+
+ self.assertTaxGridAmounts([
+ # Tag Balance
+ (self.tax_tags[0], -100.0),
+ (self.tax_tags[1], -33.33),
+ (self.tax_tags[2], 100.0),
+ (self.tax_tags[3], 33.33),
+ ])
+
+ def test_reconcile_cash_basis_tax_grid_multi_taxes(self):
+ ''' Test the tax grid when reconciling an invoice with multiple taxes/tax repartition. '''
+ base_taxes = self.cash_basis_tax_a_third_amount + self.cash_basis_tax_tiny_amount
+ base_tags = self.tax_tags[0] + self.tax_tags[4]
+
+ # An invoice with 2 taxes:
+ invoice_move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2016-01-01',
+ 'line_ids': [
+ # Base Tax line
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 100.0,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'tax_ids': [(6, 0, base_taxes.ids)],
+ 'tax_tag_ids': [(6, 0, base_tags.ids)],
+ 'tax_exigible': False,
+ }),
+
+ # Tax lines
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 33.33,
+ 'account_id': self.cash_basis_transfer_account.id,
+ 'tax_repartition_line_id': self.cash_basis_tax_a_third_amount.invoice_repartition_line_ids.filtered(lambda line: line.repartition_type == 'tax').id,
+ 'tax_tag_ids': [(6, 0, self.tax_tags[1].ids)],
+ 'tax_exigible': False,
+ }),
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 0.01,
+ 'account_id': self.cash_basis_transfer_account.id,
+ 'tax_repartition_line_id': self.cash_basis_tax_tiny_amount.invoice_repartition_line_ids.filtered(lambda line: line.repartition_type == 'tax').id,
+ 'tax_tag_ids': [(6, 0, self.tax_tags[5].ids)],
+ 'tax_exigible': False,
+ }),
+
+ # Receivable lines
+ (0, 0, {
+ 'debit': 133.34,
+ 'credit': 0.0,
+ 'account_id': self.extra_receivable_account_1.id,
+ }),
+ ]
+ })
+
+ # A payment paying the full invoice amount.
+ payment_move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2017-01-01',
+ 'line_ids': [
+ (0, 0, {'debit': 0.0, 'credit': 133.34, 'account_id': self.extra_receivable_account_1.id}),
+ (0, 0, {'debit': 133.34, 'credit': 0.0, 'account_id': self.company_data['default_account_revenue'].id}),
+ ]
+ })
+
+ (invoice_move + payment_move).action_post()
+
+ receivable_lines = (invoice_move + payment_move).line_ids\
+ .filtered(lambda line: line.account_id == self.extra_receivable_account_1)
+ res = receivable_lines.reconcile()
+
+ self.assertFullReconcile(res['full_reconcile'], receivable_lines)
+ self.assertEqual(len(res.get('tax_cash_basis_moves', [])), 1)
+
+ self.assertRecordValues(res['tax_cash_basis_moves'].line_ids, [
+ # Base amount x 2 because there is two taxes:
+ {'debit': 100.0, 'credit': 0.0, 'tax_ids': [], 'tax_tag_ids': [], 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 100.0, 'tax_ids': base_taxes.ids, 'tax_tag_ids': base_tags.ids, 'account_id': self.cash_basis_base_account.id},
+ # tax_1:
+ {'debit': 33.33, 'credit': 0.0, 'tax_ids': [], 'tax_tag_ids': [], 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 33.33, 'tax_ids': [], 'tax_tag_ids': self.tax_tags[1].ids, 'account_id': self.tax_account_1.id},
+ # tax_2:
+ {'debit': 0.01, 'credit': 0.0, 'tax_ids': [], 'tax_tag_ids': [], 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.01, 'tax_ids': [], 'tax_tag_ids': self.tax_tags[5].ids, 'account_id': self.tax_account_2.id},
+ ])
+
+ self.assertTaxGridAmounts([
+ # Tag Balance
+ (self.tax_tags[0], -100.0),
+ (self.tax_tags[1], -33.33),
+ (self.tax_tags[4], -100.0),
+ (self.tax_tags[5], -0.01),
+ ])
+
+ def test_caba_mix_reconciliation(self):
+ """ Test the reconciliation of tax lines (when using a reconcilable tax account)
+ for cases mixing taxes exigible on payment and on invoices.
+ """
+
+ # Make the tax account reconcilable
+ self.tax_account_1.reconcile = True
+
+ # Create a tax using the same accounts as the CABA one
+ non_caba_tax = self.env['account.tax'].create({
+ 'name': 'tax 20%',
+ 'type_tax_use': 'purchase',
+ 'company_id': self.company_data['company'].id,
+ 'amount': 20,
+ 'tax_exigibility': 'on_invoice',
+ 'invoice_repartition_line_ids': [
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'account_id': self.tax_account_1.id,
+ }),
+ ],
+ 'refund_repartition_line_ids': [
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'account_id': self.tax_account_1.id,
+ }),
+ ],
+ })
+
+ # Create an invoice with a non-CABA tax
+ non_caba_inv = self.init_invoice('in_invoice', amounts=[1000], post=True, taxes=non_caba_tax)
+
+ # Create an invoice with a CABA tax using the same tax account and pay it
+ caba_inv = self.init_invoice('in_invoice', amounts=[300], post=True, taxes=self.cash_basis_tax_a_third_amount)
+
+ pmt_wizard = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=caba_inv.ids).create({
+ 'payment_date': caba_inv.date,
+ 'journal_id': self.company_data['default_journal_bank'].id,
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
+ })
+ pmt_wizard._create_payments()
+
+ partial_rec = caba_inv.mapped('line_ids.matched_debit_ids')
+ caba_move = self.env['account.move'].search([('tax_cash_basis_rec_id', '=', partial_rec.id)])
+
+ # Create a misc operation with a line on the tax account, for full reconcile of those tax lines
+ misc_move = self.env['account.move'].create({
+ 'name': "Misc move",
+ 'journal_id': self.company_data['default_journal_misc'].id,
+ 'line_ids': [
+ (0, 0, {
+ 'name': 'line 1',
+ 'account_id': self.tax_account_1.id,
+ 'credit': 300,
+ }),
+ (0, 0, {
+ 'name': 'line 2',
+ 'account_id': self.company_data['default_account_expense'].id, # Whatever the account here
+ 'debit': 300,
+ })
+ ],
+ })
+
+ misc_move.action_post()
+
+ lines_to_reconcile = (misc_move + caba_move + non_caba_inv).mapped('line_ids').filtered(lambda x: x.account_id == self.tax_account_1)
+ lines_to_reconcile.reconcile()
+
+ # Check full reconciliation
+ self.assertTrue(all(line.full_reconcile_id for line in lines_to_reconcile), "All tax lines should be fully reconciled")
+
+ def test_caba_double_tax(self):
+ """ Test the CABA entries generated from an invoice with almost
+ equal lines, different only on analytic accounting
+ """
+ # Make the tax account reconcilable
+ self.tax_account_1.reconcile = True
+
+ # Create an invoice with a CABA tax using 'Include in analytic cost'
+ move_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice', account_predictive_bills_disable_prediction=True))
+ 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'})
+
+ tax = self.cash_basis_tax_a_third_amount
+
+ # line with analytic account, will generate 2 lines in CABA move
+ with move_form.invoice_line_ids.new() as line_form:
+ line_form.name = "test line with analytic account"
+ 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.price_unit = 100
+
+ # line with analytic account, will generate other 2 lines in CABA move
+ # even if the tax is the same
+ with move_form.invoice_line_ids.new() as line_form:
+ line_form.name = "test line"
+ line_form.product_id = self.product_a
+ line_form.tax_ids.clear()
+ line_form.tax_ids.add(tax)
+ line_form.price_unit = 100
+
+ rslt = move_form.save()
+ rslt.action_post()
+
+ pmt_wizard = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=rslt.ids).create({
+ 'amount': rslt.amount_total,
+ 'payment_date': rslt.date,
+ 'journal_id': self.company_data['default_journal_bank'].id,
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
+ })
+ pmt_wizard._create_payments()
+
+ partial_rec = rslt.mapped('line_ids.matched_debit_ids')
+ caba_move = self.env['account.move'].search([('tax_cash_basis_rec_id', '=', partial_rec.id)])
+ self.assertEqual(len(caba_move.line_ids), 4, "All lines should be there")
+ self.assertEqual(caba_move.line_ids.filtered(lambda x: x.tax_line_id).balance, 66.66, "Tax amount should take into account both lines")
+
+ def test_caba_double_tax_negative_line(self):
+ """ Tests making a cash basis invoice with 2 lines using the same tax: a positive and a negative one.
+ """
+ invoice = self.init_invoice('in_invoice', amounts=[300, -60], post=True, taxes=self.cash_basis_tax_a_third_amount)
+
+ pmt_wizard = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=invoice.ids).create({
+ 'amount': 320,
+ 'payment_date': invoice.date,
+ 'journal_id': self.company_data['default_journal_bank'].id,
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
+ })
+
+ pmt_wizard._create_payments()
+
+ partial_rec = invoice.mapped('line_ids.matched_debit_ids')
+ caba_move = self.env['account.move'].search([('tax_cash_basis_rec_id', '=', partial_rec.id)])
+
+ self.assertRecordValues(caba_move.line_ids.sorted(lambda line: (-abs(line.balance), -line.debit, line.account_id)), [
+ # Base amount:
+ {'debit': 240.0, 'credit': 0.0, 'tax_tag_ids': self.tax_tags[0].ids, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 240.0, 'tax_tag_ids': [], 'account_id': self.cash_basis_base_account.id},
+ # tax:
+ {'debit': 80.0, 'credit': 0.0, 'tax_tag_ids': self.tax_tags[1].ids, 'account_id': self.tax_account_1.id},
+ {'debit': 0.0, 'credit': 80.0, 'tax_tag_ids': [], 'account_id': self.cash_basis_transfer_account.id},
+ ])
+
+ def test_caba_dest_acc_reconciliation_partial_pmt(self):
+ """ Test the reconciliation of tax lines (when using a reconcilable tax account)
+ for partially paid invoices with cash basis taxes.
+ This test is especially useful to check the implementation of the use case tested by
+ test_reconciliation_cash_basis_foreign_currency_low_values does not have unwanted side effects.
+ """
+
+ # Make the tax account reconcilable
+ self.tax_account_1.reconcile = True
+
+ # Create an invoice with a CABA tax using the same tax account and pay half of it
+ caba_inv = self.init_invoice('in_invoice', amounts=[900], post=True, taxes=self.cash_basis_tax_a_third_amount)
+
+ pmt_wizard = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=caba_inv.ids).create({
+ 'amount': 600,
+ 'payment_date': caba_inv.date,
+ 'journal_id': self.company_data['default_journal_bank'].id,
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
+ })
+ pmt_wizard._create_payments()
+
+ partial_rec = caba_inv.mapped('line_ids.matched_debit_ids')
+ caba_move = self.env['account.move'].search([('tax_cash_basis_rec_id', '=', partial_rec.id)])
+
+ # Create a misc operation with a line on the tax account, for full reconcile with the tax line
+ misc_move = self.env['account.move'].create({
+ 'name': "Misc move",
+ 'journal_id': self.company_data['default_journal_misc'].id,
+ 'line_ids': [
+ (0, 0, {
+ 'name': 'line 1',
+ 'account_id': self.tax_account_1.id,
+ 'credit': 150,
+ }),
+ (0, 0, {
+ 'name': 'line 2',
+ 'account_id': self.company_data['default_account_expense'].id, # Whatever the account here
+ 'debit': 150,
+ })
+ ],
+ })
+
+ misc_move.action_post()
+
+ lines_to_reconcile = (misc_move + caba_move).mapped('line_ids').filtered(lambda x: x.account_id == self.tax_account_1)
+ lines_to_reconcile.reconcile()
+
+ # Check full reconciliation
+ self.assertTrue(all(line.full_reconcile_id for line in lines_to_reconcile), "All tax lines should be fully reconciled")
+
+ def test_caba_undo_reconciliation(self):
+ ''' Make sure there is no traceback like "Record has already been deleted" during the deletion of partials. '''
+ self.cash_basis_transfer_account.reconcile = True
+
+ bill = self.env['account.move'].create({
+ 'move_type': 'in_invoice',
+ 'partner_id': self.partner_a.id,
+ 'invoice_date': '2019-01-01',
+ 'date': '2019-01-01',
+ 'invoice_line_ids': [(0, 0, {
+ 'name': 'line',
+ 'account_id': self.company_data['default_account_expense'].id,
+ 'price_unit': 1000.0,
+ 'tax_ids': [(6, 0, self.cash_basis_tax_a_third_amount.ids)],
+ })],
+ })
+ bill.action_post()
+
+ # Register a payment creating the CABA journal entry on the fly and reconcile it with the tax line.
+ self.env['account.payment.register']\
+ .with_context(active_ids=bill.ids, active_model='account.move')\
+ .create({})\
+ ._create_payments()
+
+ bill.button_draft()
diff --git a/addons/account/tests/test_account_move_rounding.py b/addons/account/tests/test_account_move_rounding.py
new file mode 100644
index 00000000..92f76b7c
--- /dev/null
+++ b/addons/account/tests/test_account_move_rounding.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests import tagged
+
+
+@tagged('post_install', '-at_install')
+class TestAccountMoveRounding(AccountTestInvoicingCommon):
+
+ def test_move_line_rounding(self):
+ """Whatever arguments we give to the creation of an account move,
+ in every case the amounts should be properly rounded to the currency's precision.
+ In other words, we don't fall victim of the limitation introduced by 9d87d15db6dd40
+
+ Here the rounding should be done according to company_currency_id, which is a related
+ on move_id.company_id.currency_id.
+ In principle, it should not be necessary to add it to the create values,
+ since it is supposed to be computed by the ORM...
+ """
+ move = self.env['account.move'].create({
+ 'line_ids': [
+ (0, 0, {'debit': 100.0 / 3, 'account_id': self.company_data['default_account_revenue'].id}),
+ (0, 0, {'credit': 100.0 / 3, 'account_id': self.company_data['default_account_revenue'].id}),
+ ],
+ })
+
+ self.assertEqual(
+ [(33.33, 0.0), (0.0, 33.33)],
+ move.line_ids.mapped(lambda x: (x.debit, x.credit)),
+ "Quantities should have been rounded according to the currency."
+ )
diff --git a/addons/account/tests/test_account_onboarding.py b/addons/account/tests/test_account_onboarding.py
new file mode 100644
index 00000000..c2b9c472
--- /dev/null
+++ b/addons/account/tests/test_account_onboarding.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo.addons.account.tests.common import AccountTestInvoicingHttpCommon
+from odoo.tests.common import tagged
+
+
+@tagged('post_install', '-at_install')
+class TestTourRenderInvoiceReport(AccountTestInvoicingHttpCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ cls.env.user.write({
+ 'groups_id': [
+ (6, 0, (cls.env.ref('account.group_account_invoice') + cls.env.ref('base.group_system')).ids),
+ ],
+ })
+
+ cls.out_invoice = cls.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'partner_id': cls.partner_a.id,
+ 'invoice_date': '2019-05-01',
+ 'date': '2019-05-01',
+ 'invoice_line_ids': [
+ (0, 0, {'name': 'line1', 'price_unit': 100.0}),
+ ],
+ })
+ cls.out_invoice.action_post()
+
+ report_layout = cls.env.ref('web.report_layout_standard')
+
+ cls.company_data['company'].write({
+ 'primary_color': '#123456',
+ 'secondary_color': '#789101',
+ 'external_report_layout_id': report_layout.view_id.id,
+ })
+
+ cls.env.ref('account.account_invoices_without_payment').report_type = 'qweb-html'
+
+ def test_render_account_document_layout(self):
+ self.start_tour('/web', 'account_render_report', login=self.env.user.login, timeout=200)
diff --git a/addons/account/tests/test_account_payment.py b/addons/account/tests/test_account_payment.py
new file mode 100644
index 00000000..b2a0df04
--- /dev/null
+++ b/addons/account/tests/test_account_payment.py
@@ -0,0 +1,709 @@
+# -*- coding: utf-8 -*-
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests import tagged, new_test_user
+from odoo.tests.common import Form
+
+
+@tagged('post_install', '-at_install')
+class TestAccountPayment(AccountTestInvoicingCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ cls.payment_debit_account_id = cls.copy_account(cls.company_data['default_journal_bank'].payment_debit_account_id)
+ cls.payment_credit_account_id = cls.copy_account(cls.company_data['default_journal_bank'].payment_credit_account_id)
+
+ cls.partner_bank_account = cls.env['res.partner.bank'].create({
+ 'acc_number': 'BE32707171912447',
+ 'partner_id': cls.partner_a.id,
+ 'acc_type': 'bank',
+ })
+
+ cls.company_data['default_journal_bank'].write({
+ 'payment_debit_account_id': cls.payment_debit_account_id.id,
+ 'payment_credit_account_id': cls.payment_credit_account_id.id,
+ 'inbound_payment_method_ids': [(6, 0, cls.env.ref('account.account_payment_method_manual_in').ids)],
+ 'outbound_payment_method_ids': [(6, 0, cls.env.ref('account.account_payment_method_manual_out').ids)],
+ })
+
+ cls.partner_a.write({
+ 'bank_ids': [(6, 0, cls.partner_bank_account.ids)],
+ })
+
+ def test_payment_move_sync_create_write(self):
+ copy_receivable = self.copy_account(self.company_data['default_account_receivable'])
+
+ payment = self.env['account.payment'].create({
+ 'amount': 50.0,
+ 'payment_type': 'inbound',
+ 'partner_type': 'customer',
+ 'destination_account_id': copy_receivable.id,
+ })
+
+ expected_payment_values = {
+ 'amount': 50.0,
+ 'payment_type': 'inbound',
+ 'partner_type': 'customer',
+ 'payment_reference': False,
+ 'is_reconciled': False,
+ 'currency_id': self.company_data['currency'].id,
+ 'partner_id': False,
+ 'destination_account_id': copy_receivable.id,
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
+ 'partner_bank_id': False,
+ }
+ expected_move_values = {
+ 'currency_id': self.company_data['currency'].id,
+ 'partner_id': False,
+ 'partner_bank_id': False,
+ }
+ expected_liquidity_line = {
+ 'debit': 50.0,
+ 'credit': 0.0,
+ 'amount_currency': 50.0,
+ 'currency_id': self.company_data['currency'].id,
+ 'account_id': self.payment_debit_account_id.id,
+ }
+ expected_counterpart_line = {
+ 'debit': 0.0,
+ 'credit': 50.0,
+ 'amount_currency': -50.0,
+ 'currency_id': self.company_data['currency'].id,
+ 'account_id': copy_receivable.id,
+ }
+
+ self.assertRecordValues(payment, [expected_payment_values])
+ self.assertRecordValues(payment.move_id, [expected_move_values])
+ self.assertRecordValues(payment.line_ids.sorted('balance'), [
+ expected_counterpart_line,
+ expected_liquidity_line,
+ ])
+
+ # ==== Check editing the account.payment ====
+
+ payment.write({
+ 'partner_type': 'supplier',
+ 'currency_id': self.currency_data['currency'].id,
+ 'partner_id': self.partner_a.id,
+ })
+
+ self.assertRecordValues(payment, [{
+ **expected_payment_values,
+ 'partner_type': 'supplier',
+ 'destination_account_id': self.partner_a.property_account_payable_id.id,
+ 'currency_id': self.currency_data['currency'].id,
+ 'partner_id': self.partner_a.id,
+ 'partner_bank_id': self.partner_bank_account.id,
+ }])
+ self.assertRecordValues(payment.move_id, [{
+ **expected_move_values,
+ 'currency_id': self.currency_data['currency'].id,
+ 'partner_id': self.partner_a.id,
+ 'partner_bank_id': self.partner_bank_account.id,
+ }])
+ self.assertRecordValues(payment.line_ids.sorted('balance'), [
+ {
+ **expected_counterpart_line,
+ 'debit': 0.0,
+ 'credit': 25.0,
+ 'amount_currency': -50.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'account_id': self.partner_a.property_account_payable_id.id,
+ },
+ {
+ **expected_liquidity_line,
+ 'debit': 25.0,
+ 'credit': 0.0,
+ 'amount_currency': 50.0,
+ 'currency_id': self.currency_data['currency'].id,
+ },
+ ])
+
+ # ==== Check editing the account.move.line ====
+
+ liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines()
+ payment.move_id.write({
+ 'partner_bank_id': False,
+ 'line_ids': [
+ (1, counterpart_lines.id, {
+ 'debit': 0.0,
+ 'credit': 75.0,
+ 'amount_currency': -75.0,
+ 'currency_id': self.company_data['currency'].id,
+ 'account_id': copy_receivable.id,
+ 'partner_id': self.partner_b.id,
+ }),
+ (1, liquidity_lines.id, {
+ 'debit': 100.0,
+ 'credit': 0.0,
+ 'amount_currency': 100.0,
+ 'currency_id': self.company_data['currency'].id,
+ 'partner_id': self.partner_b.id,
+ }),
+
+ # Additional write-off:
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 25.0,
+ 'amount_currency': -25.0,
+ 'currency_id': self.company_data['currency'].id,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'partner_id': self.partner_b.id,
+ }),
+ ]
+ })
+
+ self.assertRecordValues(payment, [{
+ **expected_payment_values,
+ 'amount': 100.0,
+ 'partner_id': self.partner_b.id,
+ }])
+ self.assertRecordValues(payment.move_id, [{
+ **expected_move_values,
+ 'partner_id': self.partner_b.id,
+ }])
+ self.assertRecordValues(payment.line_ids.sorted('balance'), [
+ {
+ **expected_counterpart_line,
+ 'debit': 0.0,
+ 'credit': 75.0,
+ 'amount_currency': -75.0,
+ 'partner_id': self.partner_b.id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 25.0,
+ 'amount_currency': -25.0,
+ 'currency_id': self.company_data['currency'].id,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'partner_id': self.partner_b.id,
+ },
+ {
+ **expected_liquidity_line,
+ 'debit': 100.0,
+ 'credit': 0.0,
+ 'amount_currency': 100.0,
+ 'account_id': self.payment_debit_account_id.id,
+ 'partner_id': self.partner_b.id,
+ },
+ ])
+
+ def test_payment_move_sync_onchange(self):
+ copy_receivable = self.copy_account(self.company_data['default_account_receivable'])
+
+ pay_form = Form(self.env['account.payment'].with_context(default_journal_id=self.company_data['default_journal_bank'].id))
+ pay_form.amount = 50.0
+ pay_form.payment_type = 'inbound'
+ pay_form.partner_type = 'customer'
+ pay_form.destination_account_id = copy_receivable
+ payment = pay_form.save()
+
+ expected_payment_values = {
+ 'amount': 50.0,
+ 'payment_type': 'inbound',
+ 'partner_type': 'customer',
+ 'payment_reference': False,
+ 'is_reconciled': False,
+ 'currency_id': self.company_data['currency'].id,
+ 'partner_id': False,
+ 'destination_account_id': copy_receivable.id,
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
+ }
+ expected_move_values = {
+ 'currency_id': self.company_data['currency'].id,
+ 'partner_id': False,
+ }
+ expected_liquidity_line = {
+ 'debit': 50.0,
+ 'credit': 0.0,
+ 'amount_currency': 50.0,
+ 'currency_id': self.company_data['currency'].id,
+ 'account_id': self.payment_debit_account_id.id,
+ }
+ expected_counterpart_line = {
+ 'debit': 0.0,
+ 'credit': 50.0,
+ 'amount_currency': -50.0,
+ 'currency_id': self.company_data['currency'].id,
+ 'account_id': copy_receivable.id,
+ }
+
+ self.assertRecordValues(payment, [expected_payment_values])
+ self.assertRecordValues(payment.move_id, [expected_move_values])
+ self.assertRecordValues(payment.line_ids.sorted('balance'), [
+ expected_counterpart_line,
+ expected_liquidity_line,
+ ])
+
+ # ==== Check editing the account.payment ====
+
+ pay_form = Form(payment)
+ pay_form.partner_type = 'supplier'
+ pay_form.currency_id = self.currency_data['currency']
+ pay_form.partner_id = self.partner_a
+ payment = pay_form.save()
+
+ self.assertRecordValues(payment, [{
+ **expected_payment_values,
+ 'partner_type': 'supplier',
+ 'destination_account_id': self.partner_a.property_account_payable_id.id,
+ 'currency_id': self.currency_data['currency'].id,
+ 'partner_id': self.partner_a.id,
+ }])
+ self.assertRecordValues(payment.move_id, [{
+ **expected_move_values,
+ 'currency_id': self.currency_data['currency'].id,
+ 'partner_id': self.partner_a.id,
+ }])
+ self.assertRecordValues(payment.line_ids.sorted('balance'), [
+ {
+ **expected_counterpart_line,
+ 'debit': 0.0,
+ 'credit': 25.0,
+ 'amount_currency': -50.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'account_id': self.partner_a.property_account_payable_id.id,
+ },
+ {
+ **expected_liquidity_line,
+ 'debit': 25.0,
+ 'credit': 0.0,
+ 'amount_currency': 50.0,
+ 'currency_id': self.currency_data['currency'].id,
+ },
+ ])
+
+ # ==== Check editing the account.move.line ====
+
+ move_form = Form(payment.move_id)
+ with move_form.line_ids.edit(0) as line_form:
+ line_form.currency_id = self.company_data['currency']
+ line_form.amount_currency = 100.0
+ line_form.partner_id = self.partner_b
+ with move_form.line_ids.edit(1) as line_form:
+ line_form.currency_id = self.company_data['currency']
+ line_form.amount_currency = -75.0
+ line_form.account_id = copy_receivable
+ line_form.partner_id = self.partner_b
+ with move_form.line_ids.new() as line_form:
+ line_form.currency_id = self.company_data['currency']
+ line_form.amount_currency = -25.0
+ line_form.account_id = self.company_data['default_account_revenue']
+ line_form.partner_id = self.partner_b
+ move_form.save()
+
+ self.assertRecordValues(payment, [{
+ **expected_payment_values,
+ 'amount': 100.0,
+ 'partner_id': self.partner_b.id,
+ }])
+ self.assertRecordValues(payment.line_ids.sorted('balance'), [
+ {
+ **expected_counterpart_line,
+ 'debit': 0.0,
+ 'credit': 75.0,
+ 'amount_currency': -75.0,
+ 'partner_id': self.partner_b.id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 25.0,
+ 'amount_currency': -25.0,
+ 'currency_id': self.company_data['currency'].id,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'partner_id': self.partner_b.id,
+ },
+ {
+ **expected_liquidity_line,
+ 'debit': 100.0,
+ 'credit': 0.0,
+ 'amount_currency': 100.0,
+ 'account_id': self.payment_debit_account_id.id,
+ 'partner_id': self.partner_b.id,
+ },
+ ])
+
+ def test_inbound_payment_sync_writeoff_debit_sign(self):
+ payment = self.env['account.payment'].create({
+ 'amount': 100.0,
+ 'payment_type': 'inbound',
+ 'partner_type': 'customer',
+ })
+
+ # ==== Edit the account.move.line ====
+
+ liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines()
+ payment.move_id.write({
+ 'line_ids': [
+ (1, liquidity_lines.id, {'debit': 100.0}),
+ (1, counterpart_lines.id, {'credit': 125.0}),
+ (0, 0, {'debit': 25.0, 'account_id': self.company_data['default_account_revenue'].id}),
+ ],
+ })
+
+ self.assertRecordValues(payment, [{
+ 'payment_type': 'inbound',
+ 'partner_type': 'customer',
+ 'amount': 100.0,
+ }])
+
+ # ==== Edit the account.payment amount ====
+
+ payment.write({
+ 'partner_type': 'supplier',
+ 'amount': 100.1,
+ 'destination_account_id': self.company_data['default_account_payable'].id,
+ })
+
+ self.assertRecordValues(payment.line_ids.sorted('balance'), [
+ {
+ 'debit': 0.0,
+ 'credit': 125.1,
+ 'account_id': self.company_data['default_account_payable'].id,
+ },
+ {
+ 'debit': 25.0,
+ 'credit': 0.0,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ },
+ {
+ 'debit': 100.1,
+ 'credit': 0.0,
+ 'account_id': self.payment_debit_account_id.id,
+ },
+ ])
+
+ def test_inbound_payment_sync_writeoff_credit_sign(self):
+ payment = self.env['account.payment'].create({
+ 'amount': 100.0,
+ 'payment_type': 'inbound',
+ 'partner_type': 'customer',
+ })
+
+ # ==== Edit the account.move.line ====
+
+ liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines()
+ payment.move_id.write({
+ 'line_ids': [
+ (1, liquidity_lines.id, {'debit': 100.0}),
+ (1, counterpart_lines.id, {'credit': 75.0}),
+ (0, 0, {'credit': 25.0, 'account_id': self.company_data['default_account_revenue'].id}),
+ ],
+ })
+
+ self.assertRecordValues(payment, [{
+ 'payment_type': 'inbound',
+ 'partner_type': 'customer',
+ 'amount': 100.0,
+ }])
+
+ # ==== Edit the account.payment amount ====
+
+ payment.write({
+ 'partner_type': 'supplier',
+ 'amount': 100.1,
+ 'destination_account_id': self.company_data['default_account_payable'].id,
+ })
+
+ self.assertRecordValues(payment.line_ids.sorted('balance'), [
+ {
+ 'debit': 0.0,
+ 'credit': 75.1,
+ 'account_id': self.company_data['default_account_payable'].id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 25.0,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ },
+ {
+ 'debit': 100.1,
+ 'credit': 0.0,
+ 'account_id': self.payment_debit_account_id.id,
+ },
+ ])
+
+ def test_outbound_payment_sync_writeoff_debit_sign(self):
+ payment = self.env['account.payment'].create({
+ 'amount': 100.0,
+ 'payment_type': 'outbound',
+ 'partner_type': 'supplier',
+ })
+
+ # ==== Edit the account.move.line ====
+
+ liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines()
+ payment.move_id.write({
+ 'line_ids': [
+ (1, liquidity_lines.id, {'credit': 100.0}),
+ (1, counterpart_lines.id, {'debit': 75.0}),
+ (0, 0, {'debit': 25.0, 'account_id': self.company_data['default_account_revenue'].id}),
+ ],
+ })
+
+ self.assertRecordValues(payment, [{
+ 'payment_type': 'outbound',
+ 'partner_type': 'supplier',
+ 'amount': 100.0,
+ }])
+
+ # ==== Edit the account.payment amount ====
+
+ payment.write({
+ 'partner_type': 'customer',
+ 'amount': 100.1,
+ 'destination_account_id': self.company_data['default_account_receivable'].id,
+ })
+
+ self.assertRecordValues(payment.line_ids.sorted('balance'), [
+ {
+ 'debit': 0.0,
+ 'credit': 100.1,
+ 'account_id': self.payment_credit_account_id.id,
+ },
+ {
+ 'debit': 25.0,
+ 'credit': 0.0,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ },
+ {
+ 'debit': 75.1,
+ 'credit': 0.0,
+ 'account_id': self.company_data['default_account_receivable'].id,
+ },
+ ])
+
+ def test_outbound_payment_sync_writeoff_credit_sign(self):
+ payment = self.env['account.payment'].create({
+ 'amount': 100.0,
+ 'payment_type': 'outbound',
+ 'partner_type': 'supplier',
+ })
+
+ # ==== Edit the account.move.line ====
+
+ liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines()
+ payment.move_id.write({
+ 'line_ids': [
+ (1, liquidity_lines.id, {'credit': 100.0}),
+ (1, counterpart_lines.id, {'debit': 125.0}),
+ (0, 0, {'credit': 25.0, 'account_id': self.company_data['default_account_revenue'].id}),
+ ],
+ })
+
+ self.assertRecordValues(payment, [{
+ 'payment_type': 'outbound',
+ 'partner_type': 'supplier',
+ 'amount': 100.0,
+ }])
+
+ # ==== Edit the account.payment amount ====
+
+ payment.write({
+ 'partner_type': 'customer',
+ 'amount': 100.1,
+ 'destination_account_id': self.company_data['default_account_receivable'].id,
+ })
+
+ self.assertRecordValues(payment.line_ids.sorted('balance'), [
+ {
+ 'debit': 0.0,
+ 'credit': 100.1,
+ 'account_id': self.payment_credit_account_id.id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 25.0,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ },
+ {
+ 'debit': 125.1,
+ 'credit': 0.0,
+ 'account_id': self.company_data['default_account_receivable'].id,
+ },
+ ])
+
+ def test_internal_transfer(self):
+ copy_receivable = self.copy_account(self.company_data['default_account_receivable'])
+
+ payment = self.env['account.payment'].create({
+ 'amount': 50.0,
+ 'is_internal_transfer': True,
+ })
+
+ expected_payment_values = {
+ 'amount': 50.0,
+ 'payment_type': 'inbound',
+ 'currency_id': self.company_data['currency'].id,
+ 'partner_id': self.company_data['company'].partner_id.id,
+ 'destination_account_id': self.company_data['company'].transfer_account_id.id,
+ }
+ expected_move_values = {
+ 'currency_id': self.company_data['currency'].id,
+ 'partner_id': self.company_data['company'].partner_id.id,
+ }
+ expected_liquidity_line = {
+ 'debit': 50.0,
+ 'credit': 0.0,
+ 'amount_currency': 50.0,
+ 'currency_id': self.company_data['currency'].id,
+ 'account_id': self.payment_debit_account_id.id,
+ }
+ expected_counterpart_line = {
+ 'debit': 0.0,
+ 'credit': 50.0,
+ 'amount_currency': -50.0,
+ 'currency_id': self.company_data['currency'].id,
+ 'account_id': self.company_data['company'].transfer_account_id.id,
+ }
+
+ self.assertRecordValues(payment, [expected_payment_values])
+ self.assertRecordValues(payment.move_id, [expected_move_values])
+ self.assertRecordValues(payment.line_ids.sorted('balance'), [
+ expected_counterpart_line,
+ expected_liquidity_line,
+ ])
+
+ # ==== Check editing the account.payment ====
+
+ payment.write({
+ 'partner_type': 'customer',
+ 'partner_id': self.partner_a.id,
+ 'destination_account_id': copy_receivable.id,
+ })
+
+ self.assertRecordValues(payment, [{
+ **expected_payment_values,
+ 'partner_type': 'customer',
+ 'destination_account_id': copy_receivable.id,
+ 'partner_id': self.partner_a.id,
+ 'is_internal_transfer': False,
+ }])
+ self.assertRecordValues(payment.move_id, [{
+ **expected_move_values,
+ 'partner_id': self.partner_a.id,
+ }])
+ self.assertRecordValues(payment.line_ids.sorted('balance'), [
+ {
+ **expected_counterpart_line,
+ 'account_id': copy_receivable.id,
+ },
+ expected_liquidity_line,
+ ])
+
+ # ==== Check editing the account.move.line ====
+
+ liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines()
+ payment.move_id.write({
+ 'line_ids': [
+ (1, counterpart_lines.id, {
+ 'account_id': self.company_data['company'].transfer_account_id.id,
+ 'partner_id': self.company_data['company'].partner_id.id,
+ }),
+ (1, liquidity_lines.id, {
+ 'partner_id': self.company_data['company'].partner_id.id,
+ }),
+ ]
+ })
+
+ self.assertRecordValues(payment, [expected_payment_values])
+ self.assertRecordValues(payment.move_id, [expected_move_values])
+ self.assertRecordValues(payment.line_ids.sorted('balance'), [
+ expected_counterpart_line,
+ expected_liquidity_line,
+ ])
+
+ def test_compute_currency_id(self):
+ ''' When creating a new account.payment without specifying a currency, the default currency should be the one
+ set on the journal.
+ '''
+ self.company_data['default_journal_bank'].write({
+ 'currency_id': self.currency_data['currency'].id,
+ })
+
+ payment = self.env['account.payment'].create({
+ 'amount': 50.0,
+ 'payment_type': 'inbound',
+ 'partner_type': 'customer',
+ 'partner_id': self.partner_a.id,
+ 'journal_id': self.company_data['default_journal_bank'].id,
+ })
+
+ self.assertRecordValues(payment, [{
+ 'currency_id': self.currency_data['currency'].id,
+ }])
+ self.assertRecordValues(payment.move_id, [{
+ 'currency_id': self.currency_data['currency'].id,
+ }])
+ self.assertRecordValues(payment.line_ids.sorted('balance'), [
+ {
+ 'debit': 0.0,
+ 'credit': 25.0,
+ 'amount_currency': -50.0,
+ 'currency_id': self.currency_data['currency'].id,
+ },
+ {
+ 'debit': 25.0,
+ 'credit': 0.0,
+ 'amount_currency': 50.0,
+ 'currency_id': self.currency_data['currency'].id,
+ },
+ ])
+
+ def test_reconciliation_payment_states(self):
+ payment = self.env['account.payment'].create({
+ 'amount': 50.0,
+ 'payment_type': 'inbound',
+ 'partner_type': 'customer',
+ 'destination_account_id': self.company_data['default_account_receivable'].id,
+ })
+ liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines()
+
+ self.assertRecordValues(payment, [{
+ 'is_reconciled': False,
+ 'is_matched': False,
+ }])
+
+ invoice = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'partner_id': self.partner_a.id,
+ 'invoice_line_ids': [(0, 0, {
+ 'name': '50 to pay',
+ 'price_unit': 50.0,
+ 'quantity': 1,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ })],
+ })
+
+ payment.action_post()
+ invoice.action_post()
+
+ (counterpart_lines + invoice.line_ids.filtered(lambda line: line.account_internal_type == 'receivable'))\
+ .reconcile()
+
+ self.assertRecordValues(payment, [{
+ 'is_reconciled': True,
+ 'is_matched': False,
+ }])
+
+ statement = self.env['account.bank.statement'].create({
+ 'name': 'test_statement',
+ 'journal_id': self.company_data['default_journal_bank'].id,
+ 'line_ids': [
+ (0, 0, {
+ 'payment_ref': '50 to pay',
+ 'partner_id': self.partner_a.id,
+ 'amount': 50.0,
+ }),
+ ],
+ })
+ statement.button_post()
+ statement_line = statement.line_ids
+
+ statement_line.reconcile([{'id': liquidity_lines.id}])
+
+ self.assertRecordValues(payment, [{
+ 'is_reconciled': True,
+ 'is_matched': True,
+ }])
diff --git a/addons/account/tests/test_account_payment_register.py b/addons/account/tests/test_account_payment_register.py
new file mode 100644
index 00000000..1515ca48
--- /dev/null
+++ b/addons/account/tests/test_account_payment_register.py
@@ -0,0 +1,800 @@
+# -*- coding: utf-8 -*-
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.exceptions import UserError
+from odoo.tests import tagged
+
+
+@tagged('post_install', '-at_install')
+class TestAccountPaymentRegister(AccountTestInvoicingCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ cls.currency_data_3 = cls.setup_multi_currency_data({
+ 'name': "Umbrella",
+ 'symbol': '☂',
+ 'currency_unit_label': "Umbrella",
+ 'currency_subunit_label': "Broken Umbrella",
+ }, rate2017=0.01)
+
+ cls.payment_debit_account_id = cls.company_data['default_journal_bank'].payment_debit_account_id.copy()
+ cls.payment_credit_account_id = cls.company_data['default_journal_bank'].payment_credit_account_id.copy()
+
+ cls.custom_payment_method_in = cls.env['account.payment.method'].create({
+ 'name': 'custom_payment_method_in',
+ 'code': 'CUSTOMIN',
+ 'payment_type': 'inbound',
+ })
+ cls.manual_payment_method_in = cls.env.ref('account.account_payment_method_manual_in')
+
+ cls.custom_payment_method_out = cls.env['account.payment.method'].create({
+ 'name': 'custom_payment_method_out',
+ 'code': 'CUSTOMOUT',
+ 'payment_type': 'outbound',
+ })
+ cls.manual_payment_method_out = cls.env.ref('account.account_payment_method_manual_out')
+
+ cls.company_data['default_journal_bank'].write({
+ 'payment_debit_account_id': cls.payment_debit_account_id.id,
+ 'payment_credit_account_id': cls.payment_credit_account_id.id,
+ 'inbound_payment_method_ids': [(6, 0, (
+ cls.manual_payment_method_in.id,
+ cls.custom_payment_method_in.id,
+ ))],
+ 'outbound_payment_method_ids': [(6, 0, (
+ cls.env.ref('account.account_payment_method_manual_out').id,
+ cls.custom_payment_method_out.id,
+ cls.manual_payment_method_out.id,
+ ))],
+ })
+
+ # Customer invoices sharing the same batch.
+ cls.out_invoice_1 = cls.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'date': '2017-01-01',
+ 'invoice_date': '2017-01-01',
+ 'partner_id': cls.partner_a.id,
+ 'currency_id': cls.currency_data['currency'].id,
+ 'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 1000.0})],
+ })
+ cls.out_invoice_2 = cls.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'date': '2017-01-01',
+ 'invoice_date': '2017-01-01',
+ 'partner_id': cls.partner_a.id,
+ 'currency_id': cls.currency_data['currency'].id,
+ 'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 2000.0})],
+ })
+ cls.out_invoice_3 = cls.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'date': '2017-01-01',
+ 'invoice_date': '2017-01-01',
+ 'partner_id': cls.partner_a.id,
+ 'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 12.01})],
+ })
+ cls.out_invoice_4 = cls.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'date': '2017-01-01',
+ 'invoice_date': '2017-01-01',
+ 'partner_id': cls.partner_a.id,
+ 'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 11.99})],
+ })
+ (cls.out_invoice_1 + cls.out_invoice_2 + cls.out_invoice_3 + cls.out_invoice_4).action_post()
+
+ # Vendor bills, in_invoice_1 + in_invoice_2 are sharing the same batch but not in_invoice_3.
+ cls.in_invoice_1 = cls.env['account.move'].create({
+ 'move_type': 'in_invoice',
+ 'date': '2017-01-01',
+ 'invoice_date': '2017-01-01',
+ 'partner_id': cls.partner_a.id,
+ 'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 1000.0})],
+ })
+ cls.in_invoice_2 = cls.env['account.move'].create({
+ 'move_type': 'in_invoice',
+ 'date': '2017-01-01',
+ 'invoice_date': '2017-01-01',
+ 'partner_id': cls.partner_a.id,
+ 'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 2000.0})],
+ })
+ cls.in_invoice_3 = cls.env['account.move'].create({
+ 'move_type': 'in_invoice',
+ 'date': '2017-01-01',
+ 'invoice_date': '2017-01-01',
+ 'partner_id': cls.partner_b.id,
+ 'currency_id': cls.currency_data['currency'].id,
+ 'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 3000.0})],
+ })
+ (cls.in_invoice_1 + cls.in_invoice_2 + cls.in_invoice_3).action_post()
+
+ def test_register_payment_single_batch_grouped_keep_open_lower_amount(self):
+ ''' Pay 800.0 with 'open' as payment difference handling on two customer invoices (1000 + 2000). '''
+ active_ids = (self.out_invoice_1 + self.out_invoice_2).ids
+ payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
+ 'amount': 800.0,
+ 'group_payment': True,
+ 'payment_difference_handling': 'open',
+ 'currency_id': self.currency_data['currency'].id,
+ 'payment_method_id': self.custom_payment_method_in.id,
+ })._create_payments()
+
+ self.assertRecordValues(payments, [{
+ 'ref': 'INV/2017/01/0001 INV/2017/01/0002',
+ 'payment_method_id': self.custom_payment_method_in.id,
+ }])
+ self.assertRecordValues(payments.line_ids.sorted('balance'), [
+ # Receivable line:
+ {
+ 'debit': 0.0,
+ 'credit': 400.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -800.0,
+ 'reconciled': True,
+ },
+ # Liquidity line:
+ {
+ 'debit': 400.0,
+ 'credit': 0.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 800.0,
+ 'reconciled': False,
+ },
+ ])
+
+ def test_register_payment_single_batch_grouped_keep_open_higher_amount(self):
+ ''' Pay 3100.0 with 'open' as payment difference handling on two customer invoices (1000 + 2000). '''
+ active_ids = (self.out_invoice_1 + self.out_invoice_2).ids
+ payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
+ 'amount': 3100.0,
+ 'group_payment': True,
+ 'payment_difference_handling': 'open',
+ 'currency_id': self.currency_data['currency'].id,
+ 'payment_method_id': self.custom_payment_method_in.id,
+ })._create_payments()
+
+ self.assertRecordValues(payments, [{
+ 'ref': 'INV/2017/01/0001 INV/2017/01/0002',
+ 'payment_method_id': self.custom_payment_method_in.id,
+ }])
+ self.assertRecordValues(payments.line_ids.sorted('balance'), [
+ # Receivable line:
+ {
+ 'debit': 0.0,
+ 'credit': 1550.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -3100.0,
+ 'reconciled': False,
+ },
+ # Liquidity line:
+ {
+ 'debit': 1550.0,
+ 'credit': 0.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 3100.0,
+ 'reconciled': False,
+ },
+ ])
+
+ def test_register_payment_single_batch_grouped_writeoff_lower_amount_debit(self):
+ ''' Pay 800.0 with 'reconcile' as payment difference handling on two customer invoices (1000 + 2000). '''
+ active_ids = (self.out_invoice_1 + self.out_invoice_2).ids
+ payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
+ 'amount': 800.0,
+ 'group_payment': True,
+ 'payment_difference_handling': 'reconcile',
+ 'writeoff_account_id': self.company_data['default_account_revenue'].id,
+ 'writeoff_label': 'writeoff',
+ 'payment_method_id': self.custom_payment_method_in.id,
+ })._create_payments()
+
+ self.assertRecordValues(payments, [{
+ 'ref': 'INV/2017/01/0001 INV/2017/01/0002',
+ 'payment_method_id': self.custom_payment_method_in.id,
+ }])
+ self.assertRecordValues(payments.line_ids.sorted('balance'), [
+ # Receivable line:
+ {
+ 'debit': 0.0,
+ 'credit': 1500.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -3000.0,
+ 'reconciled': True,
+ },
+ # Liquidity line:
+ {
+ 'debit': 400.0,
+ 'credit': 0.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 800.0,
+ 'reconciled': False,
+ },
+ # Writeoff line:
+ {
+ 'debit': 1100.0,
+ 'credit': 0.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 2200.0,
+ 'reconciled': False,
+ },
+ ])
+
+ def test_register_payment_single_batch_grouped_writeoff_higher_amount_debit(self):
+ ''' Pay 3100.0 with 'reconcile' as payment difference handling on two customer invoices (1000 + 2000). '''
+ active_ids = (self.out_invoice_1 + self.out_invoice_2).ids
+ payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
+ 'amount': 3100.0,
+ 'group_payment': True,
+ 'payment_difference_handling': 'reconcile',
+ 'writeoff_account_id': self.company_data['default_account_revenue'].id,
+ 'writeoff_label': 'writeoff',
+ 'payment_method_id': self.custom_payment_method_in.id,
+ })._create_payments()
+
+ self.assertRecordValues(payments, [{
+ 'ref': 'INV/2017/01/0001 INV/2017/01/0002',
+ 'payment_method_id': self.custom_payment_method_in.id,
+ }])
+ self.assertRecordValues(payments.line_ids.sorted('balance'), [
+ # Receivable line:
+ {
+ 'debit': 0.0,
+ 'credit': 1500.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -3000.0,
+ 'reconciled': True,
+ },
+ # Writeoff line:
+ {
+ 'debit': 0.0,
+ 'credit': 50.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -100.0,
+ 'reconciled': False,
+ },
+ # Liquidity line:
+ {
+ 'debit': 1550.0,
+ 'credit': 0.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 3100.0,
+ 'reconciled': False,
+ },
+ ])
+
+ def test_register_payment_single_batch_grouped_writeoff_lower_amount_credit(self):
+ ''' Pay 800.0 with 'reconcile' as payment difference handling on two vendor billes (1000 + 2000). '''
+ active_ids = (self.in_invoice_1 + self.in_invoice_2).ids
+ payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
+ 'amount': 800.0,
+ 'group_payment': True,
+ 'payment_difference_handling': 'reconcile',
+ 'writeoff_account_id': self.company_data['default_account_revenue'].id,
+ 'writeoff_label': 'writeoff',
+ 'payment_method_id': self.custom_payment_method_in.id,
+ })._create_payments()
+
+ self.assertRecordValues(payments, [{
+ 'ref': 'BILL/2017/01/0001 BILL/2017/01/0002',
+ 'payment_method_id': self.custom_payment_method_in.id,
+ }])
+ self.assertRecordValues(payments.line_ids.sorted('balance'), [
+ # Writeoff line:
+ {
+ 'debit': 0.0,
+ 'credit': 2200.0,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': -2200.0,
+ 'reconciled': False,
+ },
+ # Liquidity line:
+ {
+ 'debit': 0.0,
+ 'credit': 800.0,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': -800.0,
+ 'reconciled': False,
+ },
+ # Payable line:
+ {
+ 'debit': 3000.0,
+ 'credit': 0.0,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': 3000.0,
+ 'reconciled': True,
+ },
+ ])
+
+ def test_register_payment_single_batch_grouped_writeoff_higher_amount_credit(self):
+ ''' Pay 3100.0 with 'reconcile' as payment difference handling on two vendor billes (1000 + 2000). '''
+ active_ids = (self.in_invoice_1 + self.in_invoice_2).ids
+ payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
+ 'amount': 3100.0,
+ 'group_payment': True,
+ 'payment_difference_handling': 'reconcile',
+ 'writeoff_account_id': self.company_data['default_account_revenue'].id,
+ 'writeoff_label': 'writeoff',
+ 'payment_method_id': self.custom_payment_method_in.id,
+ })._create_payments()
+
+ self.assertRecordValues(payments, [{
+ 'ref': 'BILL/2017/01/0001 BILL/2017/01/0002',
+ 'payment_method_id': self.custom_payment_method_in.id,
+ }])
+ self.assertRecordValues(payments.line_ids.sorted('balance'), [
+ # Liquidity line:
+ {
+ 'debit': 0.0,
+ 'credit': 3100.0,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': -3100.0,
+ 'reconciled': False,
+ },
+ # Writeoff line:
+ {
+ 'debit': 100.0,
+ 'credit': 0.0,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': 100.0,
+ 'reconciled': False,
+ },
+ # Payable line:
+ {
+ 'debit': 3000.0,
+ 'credit': 0.0,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': 3000.0,
+ 'reconciled': True,
+ },
+ ])
+
+ def test_register_payment_single_batch_not_grouped(self):
+ ''' Choose to pay two customer invoices with separated payments (1000 + 2000). '''
+ active_ids = (self.out_invoice_1 + self.out_invoice_2).ids
+ payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
+ 'group_payment': False,
+ })._create_payments()
+
+ self.assertRecordValues(payments, [
+ {
+ 'ref': 'INV/2017/01/0001',
+ 'payment_method_id': self.manual_payment_method_in.id,
+ },
+ {
+ 'ref': 'INV/2017/01/0002',
+ 'payment_method_id': self.manual_payment_method_in.id,
+ },
+ ])
+ self.assertRecordValues(payments[0].line_ids.sorted('balance') + payments[1].line_ids.sorted('balance'), [
+ # == Payment 1: to pay out_invoice_1 ==
+ # Receivable line:
+ {
+ 'debit': 0.0,
+ 'credit': 500.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -1000.0,
+ 'reconciled': True,
+ },
+ # Liquidity line:
+ {
+ 'debit': 500.0,
+ 'credit': 0.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 1000.0,
+ 'reconciled': False,
+ },
+ # == Payment 2: to pay out_invoice_2 ==
+ # Receivable line:
+ {
+ 'debit': 0.0,
+ 'credit': 1000.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -2000.0,
+ 'reconciled': True,
+ },
+ # Liquidity line:
+ {
+ 'debit': 1000.0,
+ 'credit': 0.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 2000.0,
+ 'reconciled': False,
+ },
+ ])
+
+ def test_register_payment_multi_batches_grouped(self):
+ ''' Choose to pay multiple batches, one with two customer invoices (1000 + 2000)
+ and one with a vendor bill of 600, by grouping payments.
+ '''
+ active_ids = (self.in_invoice_1 + self.in_invoice_2 + self.in_invoice_3).ids
+ payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
+ 'group_payment': True,
+ })._create_payments()
+
+ self.assertRecordValues(payments, [
+ {
+ 'ref': 'BILL/2017/01/0001 BILL/2017/01/0002',
+ 'payment_method_id': self.manual_payment_method_out.id,
+ },
+ {
+ 'ref': 'BILL/2017/01/0003',
+ 'payment_method_id': self.manual_payment_method_out.id,
+ },
+ ])
+ self.assertRecordValues(payments[0].line_ids.sorted('balance') + payments[1].line_ids.sorted('balance'), [
+ # == Payment 1: to pay in_invoice_1 & in_invoice_2 ==
+ # Liquidity line:
+ {
+ 'debit': 0.0,
+ 'credit': 3000.0,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': -3000.0,
+ 'reconciled': False,
+ },
+ # Payable line:
+ {
+ 'debit': 3000.0,
+ 'credit': 0.0,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': 3000.0,
+ 'reconciled': True,
+ },
+ # == Payment 2: to pay in_invoice_3 ==
+ # Liquidity line:
+ {
+ 'debit': 0.0,
+ 'credit': 1500.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -3000.0,
+ 'reconciled': False,
+ },
+ # Payable line:
+ {
+ 'debit': 1500.0,
+ 'credit': 0.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 3000.0,
+ 'reconciled': True,
+ },
+ ])
+
+ def test_register_payment_multi_batches_not_grouped(self):
+ ''' Choose to pay multiple batches, one with two customer invoices (1000 + 2000)
+ and one with a vendor bill of 600, by splitting payments.
+ '''
+ active_ids = (self.in_invoice_1 + self.in_invoice_2 + self.in_invoice_3).ids
+ payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
+ 'group_payment': False,
+ })._create_payments()
+
+ self.assertRecordValues(payments, [
+ {
+ 'ref': 'BILL/2017/01/0001',
+ 'payment_method_id': self.manual_payment_method_out.id,
+ },
+ {
+ 'ref': 'BILL/2017/01/0002',
+ 'payment_method_id': self.manual_payment_method_out.id,
+ },
+ {
+ 'ref': 'BILL/2017/01/0003',
+ 'payment_method_id': self.manual_payment_method_out.id,
+ },
+ ])
+ self.assertRecordValues(payments[0].line_ids.sorted('balance') + payments[1].line_ids.sorted('balance') + payments[2].line_ids.sorted('balance'), [
+ # == Payment 1: to pay in_invoice_1 ==
+ # Liquidity line:
+ {
+ 'debit': 0.0,
+ 'credit': 1000.0,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': -1000.0,
+ 'reconciled': False,
+ },
+ # Payable line:
+ {
+ 'debit': 1000.0,
+ 'credit': 0.0,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': 1000.0,
+ 'reconciled': True,
+ },
+ # == Payment 2: to pay in_invoice_2 ==
+ # Liquidity line:
+ {
+ 'debit': 0.0,
+ 'credit': 2000.0,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': -2000.0,
+ 'reconciled': False,
+ },
+ # Payable line:
+ {
+ 'debit': 2000.0,
+ 'credit': 0.0,
+ 'currency_id': self.company_data['currency'].id,
+ 'amount_currency': 2000.0,
+ 'reconciled': True,
+ },
+ # == Payment 3: to pay in_invoice_3 ==
+ # Liquidity line:
+ {
+ 'debit': 0.0,
+ 'credit': 1500.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': -3000.0,
+ 'reconciled': False,
+ },
+ # Payable line:
+ {
+ 'debit': 1500.0,
+ 'credit': 0.0,
+ 'currency_id': self.currency_data['currency'].id,
+ 'amount_currency': 3000.0,
+ 'reconciled': True,
+ },
+ ])
+
+ def test_register_payment_constraints(self):
+ # Test to register a payment for a draft journal entry.
+ self.out_invoice_1.button_draft()
+ with self.assertRaises(UserError), self.cr.savepoint():
+ self.env['account.payment.register']\
+ .with_context(active_model='account.move', active_ids=self.out_invoice_1.ids)\
+ .create({})
+
+ # Test to register a payment for an already fully reconciled journal entry.
+ self.env['account.payment.register']\
+ .with_context(active_model='account.move', active_ids=self.out_invoice_2.ids)\
+ .create({})\
+ ._create_payments()
+ with self.assertRaises(UserError), self.cr.savepoint():
+ self.env['account.payment.register']\
+ .with_context(active_model='account.move', active_ids=self.out_invoice_2.ids)\
+ .create({})
+
+ def test_register_payment_multi_currency_rounding_issue_positive_delta(self):
+ ''' When registering a payment using a different currency than the invoice one, the invoice must be fully paid
+ at the end whatever the currency rate.
+ '''
+ payment = self.env['account.payment.register']\
+ .with_context(active_model='account.move', active_ids=self.out_invoice_3.ids)\
+ .create({
+ 'currency_id': self.currency_data_3['currency'].id,
+ 'amount': 0.12,
+ })\
+ ._create_payments()
+
+ self.assertRecordValues(payment.line_ids.sorted('balance'), [
+ # Receivable line:
+ {
+ 'debit': 0.0,
+ 'credit': 12.01,
+ 'currency_id': self.currency_data_3['currency'].id,
+ 'amount_currency': -0.12,
+ 'reconciled': True,
+ },
+ # Liquidity line:
+ {
+ 'debit': 12.01,
+ 'credit': 0.0,
+ 'currency_id': self.currency_data_3['currency'].id,
+ 'amount_currency': 0.12,
+ 'reconciled': False,
+ },
+ ])
+
+ def test_register_payment_multi_currency_rounding_issue_negative_delta(self):
+ ''' When registering a payment using a different currency than the invoice one, the invoice must be fully paid
+ at the end whatever the currency rate.
+ '''
+ payment = self.env['account.payment.register']\
+ .with_context(active_model='account.move', active_ids=self.out_invoice_4.ids)\
+ .create({
+ 'currency_id': self.currency_data_3['currency'].id,
+ 'amount': 0.12,
+ })\
+ ._create_payments()
+
+ self.assertRecordValues(payment.line_ids.sorted('balance'), [
+ # Receivable line:
+ {
+ 'debit': 0.0,
+ 'credit': 11.99,
+ 'currency_id': self.currency_data_3['currency'].id,
+ 'amount_currency': -0.12,
+ 'reconciled': True,
+ },
+ # Liquidity line:
+ {
+ 'debit': 11.99,
+ 'credit': 0.0,
+ 'currency_id': self.currency_data_3['currency'].id,
+ 'amount_currency': 0.12,
+ 'reconciled': False,
+ },
+ ])
+
+ def test_register_payment_multi_currency_rounding_issue_writeoff_lower_amount_keep_open(self):
+ payment = self.env['account.payment.register']\
+ .with_context(active_model='account.move', active_ids=self.out_invoice_3.ids)\
+ .create({
+ 'currency_id': self.currency_data_3['currency'].id,
+ 'amount': 0.08,
+ 'payment_difference_handling': 'open',
+ })\
+ ._create_payments()
+
+ self.assertRecordValues(payment.line_ids.sorted('balance'), [
+ # Receivable line:
+ {
+ 'debit': 0.0,
+ 'credit': 8.0,
+ 'currency_id': self.currency_data_3['currency'].id,
+ 'amount_currency': -0.08,
+ 'reconciled': True,
+ },
+ # Liquidity line:
+ {
+ 'debit': 8.0,
+ 'credit': 0.0,
+ 'currency_id': self.currency_data_3['currency'].id,
+ 'amount_currency': 0.08,
+ 'reconciled': False,
+ },
+ ])
+
+ def test_register_payment_multi_currency_rounding_issue_writeoff_lower_amount_reconcile_positive_delta(self):
+ payment = self.env['account.payment.register']\
+ .with_context(active_model='account.move', active_ids=self.out_invoice_3.ids)\
+ .create({
+ 'currency_id': self.currency_data_3['currency'].id,
+ 'amount': 0.08,
+ 'payment_difference_handling': 'reconcile',
+ 'writeoff_account_id': self.company_data['default_account_revenue'].id,
+ 'writeoff_label': 'writeoff',
+ })\
+ ._create_payments()
+
+ self.assertRecordValues(payment.line_ids.sorted('balance'), [
+ # Receivable line:
+ {
+ 'debit': 0.0,
+ 'credit': 12.01,
+ 'currency_id': self.currency_data_3['currency'].id,
+ 'amount_currency': -0.12,
+ 'reconciled': True,
+ },
+ # Write-off line:
+ {
+ 'debit': 4.0,
+ 'credit': 0.0,
+ 'currency_id': self.currency_data_3['currency'].id,
+ 'amount_currency': 0.04,
+ 'reconciled': False,
+ },
+ # Liquidity line:
+ {
+ 'debit': 8.01,
+ 'credit': 0.0,
+ 'currency_id': self.currency_data_3['currency'].id,
+ 'amount_currency': 0.08,
+ 'reconciled': False,
+ },
+ ])
+
+ def test_register_payment_multi_currency_rounding_issue_writeoff_lower_amount_reconcile_negative_delta(self):
+ payment = self.env['account.payment.register']\
+ .with_context(active_model='account.move', active_ids=self.out_invoice_4.ids)\
+ .create({
+ 'currency_id': self.currency_data_3['currency'].id,
+ 'amount': 0.08,
+ 'payment_difference_handling': 'reconcile',
+ 'writeoff_account_id': self.company_data['default_account_revenue'].id,
+ 'writeoff_label': 'writeoff',
+ })\
+ ._create_payments()
+
+ self.assertRecordValues(payment.line_ids.sorted('balance'), [
+ # Receivable line:
+ {
+ 'debit': 0.0,
+ 'credit': 11.99,
+ 'currency_id': self.currency_data_3['currency'].id,
+ 'amount_currency': -0.12,
+ 'reconciled': True,
+ },
+ # Write-off line:
+ {
+ 'debit': 4.0,
+ 'credit': 0.0,
+ 'currency_id': self.currency_data_3['currency'].id,
+ 'amount_currency': 0.04,
+ 'reconciled': False,
+ },
+ # Liquidity line:
+ {
+ 'debit': 7.99,
+ 'credit': 0.0,
+ 'currency_id': self.currency_data_3['currency'].id,
+ 'amount_currency': 0.08,
+ 'reconciled': False,
+ },
+ ])
+
+ def test_register_payment_multi_currency_rounding_issue_writeoff_higher_amount_reconcile_positive_delta(self):
+ payment = self.env['account.payment.register']\
+ .with_context(active_model='account.move', active_ids=self.out_invoice_3.ids)\
+ .create({
+ 'currency_id': self.currency_data_3['currency'].id,
+ 'amount': 0.16,
+ 'payment_difference_handling': 'reconcile',
+ 'writeoff_account_id': self.company_data['default_account_revenue'].id,
+ 'writeoff_label': 'writeoff',
+ })\
+ ._create_payments()
+
+ self.assertRecordValues(payment.line_ids.sorted('balance'), [
+ # Receivable line:
+ {
+ 'debit': 0.0,
+ 'credit': 12.01,
+ 'currency_id': self.currency_data_3['currency'].id,
+ 'amount_currency': -0.12,
+ 'reconciled': True,
+ },
+ # Write-off line:
+ {
+ 'debit': 0.0,
+ 'credit': 4.0,
+ 'currency_id': self.currency_data_3['currency'].id,
+ 'amount_currency': -0.04,
+ 'reconciled': False,
+ },
+ # Liquidity line:
+ {
+ 'debit': 16.01,
+ 'credit': 0.0,
+ 'currency_id': self.currency_data_3['currency'].id,
+ 'amount_currency': 0.16,
+ 'reconciled': False,
+ },
+ ])
+
+ def test_register_payment_multi_currency_rounding_issue_writeoff_higher_amount_reconcile_negative_delta(self):
+ payment = self.env['account.payment.register']\
+ .with_context(active_model='account.move', active_ids=self.out_invoice_4.ids)\
+ .create({
+ 'currency_id': self.currency_data_3['currency'].id,
+ 'amount': 0.16,
+ 'payment_difference_handling': 'reconcile',
+ 'writeoff_account_id': self.company_data['default_account_revenue'].id,
+ 'writeoff_label': 'writeoff',
+ })\
+ ._create_payments()
+
+ self.assertRecordValues(payment.line_ids.sorted('balance'), [
+ # Receivable line:
+ {
+ 'debit': 0.0,
+ 'credit': 11.99,
+ 'currency_id': self.currency_data_3['currency'].id,
+ 'amount_currency': -0.12,
+ 'reconciled': True,
+ },
+ # Write-off line:
+ {
+ 'debit': 0.0,
+ 'credit': 4.0,
+ 'currency_id': self.currency_data_3['currency'].id,
+ 'amount_currency': -0.04,
+ 'reconciled': False,
+ },
+ # Liquidity line:
+ {
+ 'debit': 15.99,
+ 'credit': 0.0,
+ 'currency_id': self.currency_data_3['currency'].id,
+ 'amount_currency': 0.16,
+ 'reconciled': False,
+ },
+ ])
diff --git a/addons/account/tests/test_account_tax.py b/addons/account/tests/test_account_tax.py
new file mode 100644
index 00000000..94bd2a16
--- /dev/null
+++ b/addons/account/tests/test_account_tax.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests import tagged
+from odoo.exceptions import UserError
+
+
+@tagged('post_install', '-at_install')
+class TestAccountTax(AccountTestInvoicingCommon):
+
+ def test_changing_tax_company(self):
+ ''' Ensure you can't change the company of an account.tax if there are some journal entries '''
+
+ # Avoid duplicate key value violates unique constraint "account_tax_name_company_uniq".
+ self.company_data['default_tax_sale'].name = 'test_changing_account_company'
+
+ self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'date': '2019-01-01',
+ 'invoice_line_ids': [
+ (0, 0, {
+ 'name': 'invoice_line',
+ 'quantity': 1.0,
+ 'price_unit': 100.0,
+ 'tax_ids': [(6, 0, self.company_data['default_tax_sale'].ids)],
+ }),
+ ],
+ })
+
+ with self.assertRaises(UserError), self.cr.savepoint():
+ self.company_data['default_tax_sale'].company_id = self.company_data_2['company']
diff --git a/addons/account/tests/test_fiscal_position.py b/addons/account/tests/test_fiscal_position.py
new file mode 100644
index 00000000..96032e75
--- /dev/null
+++ b/addons/account/tests/test_fiscal_position.py
@@ -0,0 +1,168 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests import common
+
+
+class TestFiscalPosition(common.SavepointCase):
+ """Tests for fiscal positions in auto apply (account.fiscal.position).
+ If a partner has a vat number, the fiscal positions with "vat_required=True"
+ are preferred.
+ """
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestFiscalPosition, cls).setUpClass()
+ cls.fp = cls.env['account.fiscal.position']
+
+ # reset any existing FP
+ cls.fp.search([]).write({'auto_apply': False})
+
+ cls.res_partner = cls.env['res.partner']
+ cls.be = be = cls.env.ref('base.be')
+ cls.fr = fr = cls.env.ref('base.fr')
+ cls.mx = mx = cls.env.ref('base.mx')
+ cls.eu = eu = cls.env.ref('base.europe')
+ cls.state_fr = cls.env['res.country.state'].create(dict(
+ name="State",
+ code="ST",
+ country_id=fr.id))
+ cls.jc = cls.res_partner.create(dict(
+ name="JCVD",
+ vat="BE0477472701",
+ country_id=be.id))
+ cls.ben = cls.res_partner.create(dict(
+ name="BP",
+ country_id=be.id))
+ cls.george = cls.res_partner.create(dict(
+ name="George",
+ vat="BE0477472701",
+ country_id=fr.id))
+ cls.alberto = cls.res_partner.create(dict(
+ name="Alberto",
+ vat="BE0477472701",
+ country_id=mx.id))
+ cls.be_nat = cls.fp.create(dict(
+ name="BE-NAT",
+ auto_apply=True,
+ country_id=be.id,
+ vat_required=False,
+ sequence=10))
+ cls.fr_b2c = cls.fp.create(dict(
+ name="EU-VAT-FR-B2C",
+ auto_apply=True,
+ country_id=fr.id,
+ vat_required=False,
+ sequence=40))
+ cls.fr_b2b = cls.fp.create(dict(
+ name="EU-VAT-FR-B2B",
+ auto_apply=True,
+ country_id=fr.id,
+ vat_required=True,
+ sequence=50))
+
+ def test_10_fp_country(self):
+ def assert_fp(partner, expected_pos, message):
+ self.assertEqual(
+ self.fp.get_fiscal_position(partner.id).id,
+ expected_pos.id,
+ message)
+
+ george, jc, ben, alberto = self.george, self.jc, self.ben, self.alberto
+
+ # B2B has precedence over B2C for same country even when sequence gives lower precedence
+ self.assertGreater(self.fr_b2b.sequence, self.fr_b2c.sequence)
+ assert_fp(george, self.fr_b2b, "FR-B2B should have precedence over FR-B2C")
+ self.fr_b2b.auto_apply = False
+ assert_fp(george, self.fr_b2c, "FR-B2C should match now")
+ self.fr_b2b.auto_apply = True
+
+ # Create positions matching on Country Group and on NO country at all
+ self.eu_intra_b2b = self.fp.create(dict(
+ name="EU-INTRA B2B",
+ auto_apply=True,
+ country_group_id=self.eu.id,
+ vat_required=True,
+ sequence=20))
+ self.world = self.fp.create(dict(
+ name="WORLD-EXTRA",
+ auto_apply=True,
+ vat_required=False,
+ sequence=30))
+
+ # Country match has higher precedence than group match or sequence
+ self.assertGreater(self.fr_b2b.sequence, self.eu_intra_b2b.sequence)
+ assert_fp(george, self.fr_b2b, "FR-B2B should have precedence over EU-INTRA B2B")
+
+ # B2B has precedence regardless of country or group match
+ self.assertGreater(self.eu_intra_b2b.sequence, self.be_nat.sequence)
+ assert_fp(jc, self.eu_intra_b2b, "EU-INTRA B2B should match before BE-NAT")
+
+ # Lower sequence = higher precedence if country/group and VAT matches
+ self.assertFalse(ben.vat) # No VAT set
+ assert_fp(ben, self.be_nat, "BE-NAT should match before EU-INTRA due to lower sequence")
+
+ # Remove BE from EU group, now BE-NAT should be the fallback match before the wildcard WORLD
+ self.be.write({'country_group_ids': [(3, self.eu.id)]})
+ self.assertTrue(jc.vat) # VAT set
+ assert_fp(jc, self.be_nat, "BE-NAT should match as fallback even w/o VAT match")
+
+ # No country = wildcard match only if nothing else matches
+ self.assertTrue(alberto.vat) # with VAT
+ assert_fp(alberto, self.world, "WORLD-EXTRA should match anything else (1)")
+ alberto.vat = False # or without
+ assert_fp(alberto, self.world, "WORLD-EXTRA should match anything else (2)")
+
+ # Zip range
+ self.fr_b2b_zip100 = self.fr_b2b.copy(dict(zip_from=0, zip_to=5000, sequence=60))
+ george.zip = 6000
+ assert_fp(george, self.fr_b2b, "FR-B2B with wrong zip range should not match")
+ george.zip = 3000
+ assert_fp(george, self.fr_b2b_zip100, "FR-B2B with zip range should have precedence")
+
+ # States
+ self.fr_b2b_state = self.fr_b2b.copy(dict(state_ids=[(4, self.state_fr.id)], sequence=70))
+ george.state_id = self.state_fr
+ assert_fp(george, self.fr_b2b_zip100, "FR-B2B with zip should have precedence over states")
+ george.zip = False
+ assert_fp(george, self.fr_b2b_state, "FR-B2B with states should have precedence")
+
+ # Dedicated position has max precedence
+ george.property_account_position_id = self.be_nat
+ assert_fp(george, self.be_nat, "Forced position has max precedence")
+
+
+ def test_20_fp_one_tax_2m(self):
+
+ self.src_tax = self.env['account.tax'].create({'name': "SRC", 'amount': 0.0})
+ self.dst1_tax = self.env['account.tax'].create({'name': "DST1", 'amount': 0.0})
+ self.dst2_tax = self.env['account.tax'].create({'name': "DST2", 'amount': 0.0})
+
+ self.fp2m = self.fp.create({
+ 'name': "FP-TAX2TAXES",
+ 'tax_ids': [
+ (0,0,{
+ 'tax_src_id': self.src_tax.id,
+ 'tax_dest_id': self.dst1_tax.id
+ }),
+ (0,0,{
+ 'tax_src_id': self.src_tax.id,
+ 'tax_dest_id': self.dst2_tax.id
+ })
+ ]
+ })
+ mapped_taxes = self.fp2m.map_tax(self.src_tax)
+
+ self.assertEqual(mapped_taxes, self.dst1_tax | self.dst2_tax)
+
+ def test_30_fp_country_delivery(self):
+ """
+ Customer is in Belgium
+ Delivery is in France
+ Check if fiscal position is France
+ """
+ self.george.vat = False
+ self.assertEqual(
+ self.fp.get_fiscal_position(self.ben.id, self.george.id).id,
+ self.fr_b2c.id,
+ "FR B2C should be set")
diff --git a/addons/account/tests/test_invoice_tax_amount_by_group.py b/addons/account/tests/test_invoice_tax_amount_by_group.py
new file mode 100644
index 00000000..e4f76f14
--- /dev/null
+++ b/addons/account/tests/test_invoice_tax_amount_by_group.py
@@ -0,0 +1,209 @@
+# -*- coding: utf-8 -*-
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests import tagged
+
+
+@tagged('post_install', '-at_install')
+class TestInvoiceTaxAmountByGroup(AccountTestInvoicingCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ cls.tax_group1 = cls.env['account.tax.group'].create({'name': '1'})
+ cls.tax_group2 = cls.env['account.tax.group'].create({'name': '2'})
+
+ def assertAmountByTaxGroup(self, invoice, expected_values):
+ current_values = [(x[6], x[2], x[1]) for x in invoice.amount_by_group]
+ self.assertEqual(current_values, expected_values)
+
+ def test_multiple_tax_lines(self):
+ tax_10 = self.env['account.tax'].create({
+ 'name': "tax_10",
+ 'amount_type': 'percent',
+ 'amount': 10.0,
+ 'tax_group_id': self.tax_group1.id,
+ })
+ tax_20 = self.env['account.tax'].create({
+ 'name': "tax_20",
+ 'amount_type': 'percent',
+ 'amount': 20.0,
+ 'tax_group_id': self.tax_group2.id,
+ })
+
+ invoice = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'partner_id': self.partner_a.id,
+ 'invoice_date': '2019-01-01',
+ 'invoice_line_ids': [
+ (0, 0, {
+ 'name': 'line',
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'price_unit': 1000.0,
+ 'tax_ids': [(6, 0, (tax_10 + tax_20).ids)],
+ }),
+ (0, 0, {
+ 'name': 'line',
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'price_unit': 1000.0,
+ 'tax_ids': [(6, 0, tax_10.ids)],
+ }),
+ (0, 0, {
+ 'name': 'line',
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'price_unit': 1000.0,
+ 'tax_ids': [(6, 0, tax_20.ids)],
+ }),
+ ]
+ })
+
+ self.assertAmountByTaxGroup(invoice, [
+ (self.tax_group1.id, 2000.0, 200.0),
+ (self.tax_group2.id, 2000.0, 400.0),
+ ])
+
+ # Same but both are sharing the same tax group.
+
+ tax_20.tax_group_id = self.tax_group1
+ invoice.invalidate_cache(['amount_by_group'])
+
+ self.assertAmountByTaxGroup(invoice, [
+ (self.tax_group1.id, 3000.0, 600.0),
+ ])
+
+ def test_zero_tax_lines(self):
+ tax_0 = self.env['account.tax'].create({
+ 'name': "tax_0",
+ 'amount_type': 'percent',
+ 'amount': 0.0,
+ })
+
+ invoice = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'partner_id': self.partner_a.id,
+ 'invoice_date': '2019-01-01',
+ 'invoice_line_ids': [
+ (0, 0, {
+ 'name': 'line',
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'price_unit': 1000.0,
+ 'tax_ids': [(6, 0, tax_0.ids)],
+ }),
+ ]
+ })
+
+ self.assertAmountByTaxGroup(invoice, [
+ (tax_0.tax_group_id.id, 1000.0, 0.0),
+ ])
+
+ def test_tax_affect_base_1(self):
+ tax_10 = self.env['account.tax'].create({
+ 'name': "tax_10",
+ 'amount_type': 'percent',
+ 'amount': 10.0,
+ 'tax_group_id': self.tax_group1.id,
+ 'price_include': True,
+ 'include_base_amount': True,
+ })
+ tax_20 = self.env['account.tax'].create({
+ 'name': "tax_20",
+ 'amount_type': 'percent',
+ 'amount': 20.0,
+ 'tax_group_id': self.tax_group2.id,
+ })
+
+ invoice = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'partner_id': self.partner_a.id,
+ 'invoice_date': '2019-01-01',
+ 'invoice_line_ids': [
+ (0, 0, {
+ 'name': 'line',
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'price_unit': 1100.0,
+ 'tax_ids': [(6, 0, (tax_10 + tax_20).ids)],
+ }),
+ (0, 0, {
+ 'name': 'line',
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'price_unit': 1100.0,
+ 'tax_ids': [(6, 0, tax_10.ids)],
+ }),
+ (0, 0, {
+ 'name': 'line',
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'price_unit': 1000.0,
+ 'tax_ids': [(6, 0, tax_20.ids)],
+ }),
+ ]
+ })
+
+ self.assertAmountByTaxGroup(invoice, [
+ (self.tax_group1.id, 2000.0, 200.0),
+ (self.tax_group2.id, 2100.0, 420.0),
+ ])
+
+ # Same but both are sharing the same tax group.
+
+ tax_20.tax_group_id = self.tax_group1
+ invoice.invalidate_cache(['amount_by_group'])
+
+ self.assertAmountByTaxGroup(invoice, [
+ (self.tax_group1.id, 3000.0, 620.0),
+ ])
+
+ def test_tax_affect_base_2(self):
+ tax_10 = self.env['account.tax'].create({
+ 'name': "tax_10",
+ 'amount_type': 'percent',
+ 'amount': 10.0,
+ 'tax_group_id': self.tax_group1.id,
+ 'include_base_amount': True,
+ })
+ tax_20 = self.env['account.tax'].create({
+ 'name': "tax_20",
+ 'amount_type': 'percent',
+ 'amount': 20.0,
+ 'tax_group_id': self.tax_group1.id,
+ })
+ tax_30 = self.env['account.tax'].create({
+ 'name': "tax_30",
+ 'amount_type': 'percent',
+ 'amount': 30.0,
+ 'tax_group_id': self.tax_group2.id,
+ 'include_base_amount': True,
+ })
+
+ invoice = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'partner_id': self.partner_a.id,
+ 'invoice_date': '2019-01-01',
+ 'invoice_line_ids': [
+ (0, 0, {
+ 'name': 'line',
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'price_unit': 1000.0,
+ 'tax_ids': [(6, 0, (tax_10 + tax_20).ids)],
+ }),
+ (0, 0, {
+ 'name': 'line',
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'price_unit': 1000.0,
+ 'tax_ids': [(6, 0, (tax_30 + tax_10).ids)],
+ }),
+ ]
+ })
+
+ self.assertAmountByTaxGroup(invoice, [
+ (self.tax_group1.id, 2300.0, 450.0),
+ (self.tax_group2.id, 1000.0, 300.0),
+ ])
+
+ # Same but both are sharing the same tax group.
+
+ tax_30.tax_group_id = self.tax_group1
+ invoice.invalidate_cache(['amount_by_group'])
+
+ self.assertAmountByTaxGroup(invoice, [
+ (self.tax_group1.id, 2000.0, 750.0),
+ ])
diff --git a/addons/account/tests/test_invoice_taxes.py b/addons/account/tests/test_invoice_taxes.py
new file mode 100644
index 00000000..83b9faa2
--- /dev/null
+++ b/addons/account/tests/test_invoice_taxes.py
@@ -0,0 +1,669 @@
+# -*- coding: utf-8 -*-
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests import tagged, Form
+
+
+@tagged('post_install', '-at_install')
+class TestInvoiceTaxes(AccountTestInvoicingCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ cls.company_data['company'].country_id = cls.env.ref('base.us')
+
+ cls.percent_tax_1 = cls.env['account.tax'].create({
+ 'name': '21%',
+ 'amount_type': 'percent',
+ 'amount': 21,
+ 'sequence': 10,
+ })
+ cls.percent_tax_1_incl = cls.env['account.tax'].create({
+ 'name': '21% incl',
+ 'amount_type': 'percent',
+ 'amount': 21,
+ 'price_include': True,
+ 'include_base_amount': True,
+ 'sequence': 20,
+ })
+ cls.percent_tax_2 = cls.env['account.tax'].create({
+ 'name': '12%',
+ 'amount_type': 'percent',
+ 'amount': 12,
+ 'sequence': 30,
+ })
+ cls.percent_tax_3_incl = cls.env['account.tax'].create({
+ 'name': '5% incl',
+ 'amount_type': 'percent',
+ 'amount': 5,
+ 'price_include': True,
+ 'include_base_amount': True,
+ 'sequence': 40,
+ })
+ cls.group_tax = cls.env['account.tax'].create({
+ 'name': 'group 12% + 21%',
+ 'amount_type': 'group',
+ 'amount': 21,
+ 'children_tax_ids': [
+ (4, cls.percent_tax_1_incl.id),
+ (4, cls.percent_tax_2.id)
+ ],
+ 'sequence': 40,
+ })
+
+ cls.tax_report = cls.env['account.tax.report'].create({
+ 'name': "Tax report",
+ 'country_id': cls.company_data['company'].country_id.id,
+ })
+
+ cls.tax_report_line = cls.env['account.tax.report.line'].create({
+ 'name': 'test_tax_report_line',
+ 'tag_name': 'test_tax_report_line',
+ 'report_id': cls.tax_report.id,
+ 'sequence': 10,
+ })
+ cls.tax_tag_pos = cls.tax_report_line.tag_ids.filtered(lambda x: not x.tax_negate)
+ cls.tax_tag_neg = cls.tax_report_line.tag_ids.filtered(lambda x: x.tax_negate)
+ cls.base_tax_report_line = cls.env['account.tax.report.line'].create({
+ 'name': 'base_test_tax_report_line',
+ 'tag_name': 'base_test_tax_report_line',
+ 'report_id': cls.tax_report.id,
+ 'sequence': 10,
+ })
+ cls.base_tag_pos = cls.base_tax_report_line.tag_ids.filtered(lambda x: not x.tax_negate)
+ cls.base_tag_neg = cls.base_tax_report_line.tag_ids.filtered(lambda x: x.tax_negate)
+
+ def _create_invoice(self, taxes_per_line, inv_type='out_invoice', currency_id=False, invoice_payment_term_id=False):
+ ''' Create an invoice on the fly.
+
+ :param taxes_per_line: A list of tuple (price_unit, account.tax recordset)
+ '''
+ vals = {
+ 'move_type': inv_type,
+ 'partner_id': self.partner_a.id,
+ 'invoice_line_ids': [(0, 0, {
+ 'name': 'xxxx',
+ 'quantity': 1,
+ 'price_unit': amount,
+ 'tax_ids': [(6, 0, taxes.ids)],
+ }) for amount, taxes in taxes_per_line],
+ }
+ if currency_id:
+ vals['currency_id'] = currency_id.id
+ if invoice_payment_term_id:
+ vals['invoice_payment_term_id'] = invoice_payment_term_id.id
+ return self.env['account.move'].create(vals)
+
+ def test_one_tax_per_line(self):
+ ''' Test:
+ price_unit | Taxes
+ ------------------
+ 100 | 21%
+ 121 | 21% incl
+ 100 | 12%
+
+ Expected:
+ Tax | Taxes | Base | Amount
+ --------------------------------------------
+ 21% | / | 100 | 21
+ 21% incl | / | 100 | 21
+ 12% | / | 100 | 12
+ '''
+ invoice = self._create_invoice([
+ (100, self.percent_tax_1),
+ (121, self.percent_tax_1_incl),
+ (100, self.percent_tax_2),
+ ])
+ invoice.action_post()
+ self.assertRecordValues(invoice.line_ids.filtered('tax_line_id'), [
+ {'name': self.percent_tax_1.name, 'tax_base_amount': 100, 'price_unit': 21, 'tax_ids': []},
+ {'name': self.percent_tax_1_incl.name, 'tax_base_amount': 100, 'price_unit': 21, 'tax_ids': []},
+ {'name': self.percent_tax_2.name, 'tax_base_amount': 100, 'price_unit': 12, 'tax_ids': []},
+ ])
+
+ def test_affecting_base_amount(self):
+ ''' Test:
+ price_unit | Taxes
+ ------------------
+ 121 | 21% incl, 12%
+ 100 | 12%
+
+ Expected:
+ Tax | Taxes | Base | Amount
+ --------------------------------------------
+ 21% incl | 12% | 100 | 21
+ 12% | / | 121 | 14.52
+ 12% | / | 100 | 12
+ '''
+ invoice = self._create_invoice([
+ (121, self.percent_tax_1_incl + self.percent_tax_2),
+ (100, self.percent_tax_2),
+ ])
+ invoice.action_post()
+ self.assertRecordValues(invoice.line_ids.filtered('tax_line_id').sorted(lambda x: x.price_unit), [
+ {'name': self.percent_tax_1_incl.name, 'tax_base_amount': 100, 'price_unit': 21, 'tax_ids': [self.percent_tax_2.id]},
+ {'name': self.percent_tax_2.name, 'tax_base_amount': 221, 'price_unit': 26.52, 'tax_ids': []},
+ ])
+
+ def test_group_of_taxes(self):
+ ''' Test:
+ price_unit | Taxes
+ ------------------
+ 121 | 21% incl + 12%
+ 100 | 12%
+
+ Expected:
+ Tax | Taxes | Base | Amount
+ --------------------------------------------
+ 21% incl | / | 100 | 21
+ 12% | 21% incl | 121 | 14.52
+ 12% | / | 100 | 12
+ '''
+ invoice = self._create_invoice([
+ (121, self.group_tax),
+ (100, self.percent_tax_2),
+ ])
+ invoice.action_post()
+ self.assertRecordValues(invoice.line_ids.filtered('tax_line_id').sorted(lambda x: x.price_unit), [
+ {'name': self.percent_tax_1_incl.name, 'tax_base_amount': 100, 'price_unit': 21, 'tax_ids': [self.percent_tax_2.id]},
+ {'name': self.percent_tax_2.name, 'tax_base_amount': 221, 'price_unit': 26.52, 'tax_ids': []},
+ ])
+
+ def _create_tax_tag(self, tag_name):
+ return self.env['account.account.tag'].create({
+ 'name': tag_name,
+ 'applicability': 'taxes',
+ 'country_id': self.env.company.country_id.id,
+ })
+
+ def test_tax_repartition(self):
+ inv_base_tag = self._create_tax_tag('invoice_base')
+ inv_tax_tag_10 = self._create_tax_tag('invoice_tax_10')
+ inv_tax_tag_90 = self._create_tax_tag('invoice_tax_90')
+ ref_base_tag = self._create_tax_tag('refund_base')
+ ref_tax_tag = self._create_tax_tag('refund_tax')
+
+ user_type = self.env.ref('account.data_account_type_current_assets')
+ account_1 = self.env['account.account'].create({'name': 'test1', 'code': 'test1', 'user_type_id': user_type.id})
+ account_2 = self.env['account.account'].create({'name': 'test2', 'code': 'test2', 'user_type_id': user_type.id})
+
+ tax = self.env['account.tax'].create({
+ 'name': "Tax with account",
+ 'amount_type': 'fixed',
+ 'type_tax_use': 'sale',
+ 'amount': 42,
+ 'invoice_repartition_line_ids': [
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'tag_ids': [(4, inv_base_tag.id, 0)],
+ }),
+
+ (0,0, {
+ 'factor_percent': 10,
+ 'repartition_type': 'tax',
+ 'account_id': account_1.id,
+ 'tag_ids': [(4, inv_tax_tag_10.id, 0)],
+ }),
+
+ (0,0, {
+ 'factor_percent': 90,
+ 'repartition_type': 'tax',
+ 'account_id': account_2.id,
+ 'tag_ids': [(4, inv_tax_tag_90.id, 0)],
+ }),
+ ],
+ 'refund_repartition_line_ids': [
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'tag_ids': [(4, ref_base_tag.id, 0)],
+ }),
+
+ (0,0, {
+ 'factor_percent': 10,
+ 'repartition_type': 'tax',
+ 'tag_ids': [(4, ref_tax_tag.id, 0)],
+ }),
+
+ (0,0, {
+ 'factor_percent': 90,
+ 'repartition_type': 'tax',
+ 'account_id': account_1.id,
+ 'tag_ids': [(4, ref_tax_tag.id, 0)],
+ }),
+ ],
+ })
+
+ # Test invoice repartition
+ invoice = self._create_invoice([(100, tax)], inv_type='out_invoice')
+ invoice.action_post()
+
+ self.assertEqual(len(invoice.line_ids), 4, "There should be 4 account move lines created for the invoice: payable, base and 2 tax lines")
+ inv_base_line = invoice.line_ids.filtered(lambda x: not x.tax_repartition_line_id and x.account_id.user_type_id.type != 'receivable')
+ self.assertEqual(len(inv_base_line), 1, "There should be only one base line generated")
+ self.assertEqual(abs(inv_base_line.balance), 100, "Base amount should be 100")
+ self.assertEqual(inv_base_line.tax_tag_ids, inv_base_tag, "Base line should have received base tag")
+ inv_tax_lines = invoice.line_ids.filtered(lambda x: x.tax_repartition_line_id.repartition_type == 'tax')
+ self.assertEqual(len(inv_tax_lines), 2, "There should be two tax lines, one for each repartition line.")
+ self.assertEqual(abs(inv_tax_lines.filtered(lambda x: x.account_id == account_1).balance), 4.2, "Tax line on account 1 should amount to 4.2 (10% of 42)")
+ self.assertEqual(inv_tax_lines.filtered(lambda x: x.account_id == account_1).tax_tag_ids, inv_tax_tag_10, "Tax line on account 1 should have 10% tag")
+ self.assertAlmostEqual(abs(inv_tax_lines.filtered(lambda x: x.account_id == account_2).balance), 37.8, 2, "Tax line on account 2 should amount to 37.8 (90% of 42)")
+ self.assertEqual(inv_tax_lines.filtered(lambda x: x.account_id == account_2).tax_tag_ids, inv_tax_tag_90, "Tax line on account 2 should have 90% tag")
+
+ # Test refund repartition
+ refund = self._create_invoice([(100, tax)], inv_type='out_refund')
+ refund.action_post()
+
+ self.assertEqual(len(refund.line_ids), 4, "There should be 4 account move lines created for the refund: payable, base and 2 tax lines")
+ ref_base_line = refund.line_ids.filtered(lambda x: not x.tax_repartition_line_id and x.account_id.user_type_id.type != 'receivable')
+ self.assertEqual(len(ref_base_line), 1, "There should be only one base line generated")
+ self.assertEqual(abs(ref_base_line.balance), 100, "Base amount should be 100")
+ self.assertEqual(ref_base_line.tax_tag_ids, ref_base_tag, "Base line should have received base tag")
+ ref_tax_lines = refund.line_ids.filtered(lambda x: x.tax_repartition_line_id.repartition_type == 'tax')
+ self.assertEqual(len(ref_tax_lines), 2, "There should be two refund tax lines")
+ self.assertEqual(abs(ref_tax_lines.filtered(lambda x: x.account_id == ref_base_line.account_id).balance), 4.2, "Refund tax line on base account should amount to 4.2 (10% of 42)")
+ self.assertAlmostEqual(abs(ref_tax_lines.filtered(lambda x: x.account_id == account_1).balance), 37.8, 2, "Refund tax line on account 1 should amount to 37.8 (90% of 42)")
+ self.assertEqual(ref_tax_lines.mapped('tax_tag_ids'), ref_tax_tag, "Refund tax lines should have the right tag")
+
+ def test_division_tax(self):
+ '''
+ Test that when using division tax, with percentage amount
+ 100% any change on price unit is correctly reflected on
+ the whole move.
+
+ Complete scenario:
+ - Create a division tax, 100% amount, included in price.
+ - Create an invoice, with only the mentioned tax
+ - Change price_unit of the aml
+ - Total price of the move should change as well
+ '''
+
+ sale_tax = self.env['account.tax'].create({
+ 'name': 'tax',
+ 'type_tax_use': 'sale',
+ 'amount_type': 'division',
+ 'amount': 100,
+ 'price_include': True,
+ 'include_base_amount': True,
+ })
+ invoice = self._create_invoice([(100, sale_tax)])
+ self.assertRecordValues(invoice.line_ids.filtered('tax_line_id'), [{
+ 'name': sale_tax.name,
+ 'tax_base_amount': 0.0,
+ 'balance': -100,
+ }])
+ # change price unit, everything should change as well
+ with Form(invoice) as invoice_form:
+ with invoice_form.line_ids.edit(0) as line_edit:
+ line_edit.price_unit = 200
+
+ self.assertRecordValues(invoice.line_ids.filtered('tax_line_id'), [{
+ 'name': sale_tax.name,
+ 'tax_base_amount': 0.0,
+ 'balance': -200,
+ }])
+
+ def test_misc_journal_entry_tax_tags_sale(self):
+ sale_tax = self.env['account.tax'].create({
+ 'name': 'tax',
+ 'type_tax_use': 'sale',
+ 'amount_type': 'percent',
+ 'amount': 10,
+ '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,
+ '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)],
+ }),
+ ],
+ })
+
+ inv_tax_rep_ln = sale_tax.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax')
+ ref_tax_rep_ln = sale_tax.refund_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax')
+
+ # === Tax in debit ===
+
+ move_form = Form(self.env['account.move'], view='account.view_move_form')
+ move_form.ref = 'azerty'
+
+ # Debit base tax line.
+ with move_form.line_ids.new() as credit_line:
+ credit_line.name = 'debit_line_1'
+ credit_line.account_id = self.company_data['default_account_revenue']
+ credit_line.debit = 1000.0
+ credit_line.tax_ids.clear()
+ credit_line.tax_ids.add(sale_tax)
+
+ self.assertTrue(credit_line.recompute_tax_line)
+
+ # Balance the journal entry.
+ with move_form.line_ids.new() as credit_line:
+ credit_line.name = 'balance'
+ credit_line.account_id = self.company_data['default_account_revenue']
+ credit_line.credit = 1100.0
+
+ move = move_form.save()
+
+ self.assertRecordValues(move.line_ids.sorted('balance'), [
+ {'balance': -1100.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0, 'tax_repartition_line_id': False},
+ {'balance': 100.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_neg.ids, 'tax_base_amount': 1000, 'tax_repartition_line_id': ref_tax_rep_ln.id},
+ {'balance': 1000.0, 'tax_ids': sale_tax.ids, 'tax_tag_ids': self.base_tag_neg.ids, 'tax_base_amount': 0, 'tax_repartition_line_id': False},
+ ])
+
+ # === Tax in credit ===
+
+ move_form = Form(self.env['account.move'], view='account.view_move_form')
+ move_form.ref = 'azerty'
+
+ # Debit base tax line.
+ with move_form.line_ids.new() as credit_line:
+ credit_line.name = 'debit_line_1'
+ credit_line.account_id = self.company_data['default_account_revenue']
+ credit_line.credit = 1000.0
+ credit_line.tax_ids.clear()
+ credit_line.tax_ids.add(sale_tax)
+
+ self.assertTrue(credit_line.recompute_tax_line)
+
+ # Balance the journal entry.
+ with move_form.line_ids.new() as debit_line:
+ debit_line.name = 'balance'
+ debit_line.account_id = self.company_data['default_account_revenue']
+ debit_line.debit = 1100.0
+
+ move = move_form.save()
+
+ self.assertRecordValues(move.line_ids.sorted('balance'), [
+ {'balance': -1000.0, 'tax_ids': sale_tax.ids, 'tax_tag_ids': self.base_tag_neg.ids, 'tax_base_amount': 0, 'tax_repartition_line_id': False},
+ {'balance': -100.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_neg.ids, 'tax_base_amount': 1000, 'tax_repartition_line_id': inv_tax_rep_ln.id},
+ {'balance': 1100.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0, 'tax_repartition_line_id': False},
+ ])
+
+ def test_misc_journal_entry_tax_tags_purchase(self):
+ purch_tax = self.env['account.tax'].create({
+ 'name': 'tax',
+ 'type_tax_use': 'purchase',
+ 'amount_type': 'percent',
+ 'amount': 10,
+ '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,
+ '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)],
+ }),
+ ],
+ })
+
+ inv_tax_rep_ln = purch_tax.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax')
+ ref_tax_rep_ln = purch_tax.refund_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax')
+
+ # === Tax in debit ===
+
+ move_form = Form(self.env['account.move'])
+ move_form.ref = 'azerty'
+
+ # Debit base tax line.
+ with move_form.line_ids.new() as credit_line:
+ credit_line.name = 'debit_line_1'
+ credit_line.account_id = self.company_data['default_account_revenue']
+ credit_line.debit = 1000.0
+ credit_line.tax_ids.clear()
+ credit_line.tax_ids.add(purch_tax)
+
+ self.assertTrue(credit_line.recompute_tax_line)
+
+ # Balance the journal entry.
+ with move_form.line_ids.new() as credit_line:
+ credit_line.name = 'balance'
+ credit_line.account_id = self.company_data['default_account_revenue']
+ credit_line.credit = 1100.0
+
+ move = move_form.save()
+
+ self.assertRecordValues(move.line_ids.sorted('balance'), [
+ {'balance': -1100.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0, 'tax_repartition_line_id': False},
+ {'balance': 100.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_pos.ids, 'tax_base_amount': 1000, 'tax_repartition_line_id': inv_tax_rep_ln.id},
+ {'balance': 1000.0, 'tax_ids': purch_tax.ids, 'tax_tag_ids': self.base_tag_pos.ids, 'tax_base_amount': 0, 'tax_repartition_line_id': False},
+ ])
+
+ # === Tax in credit ===
+
+ move_form = Form(self.env['account.move'])
+ move_form.ref = 'azerty'
+
+ # Debit base tax line.
+ with move_form.line_ids.new() as credit_line:
+ credit_line.name = 'debit_line_1'
+ credit_line.account_id = self.company_data['default_account_revenue']
+ credit_line.credit = 1000.0
+ credit_line.tax_ids.clear()
+ credit_line.tax_ids.add(purch_tax)
+
+ self.assertTrue(credit_line.recompute_tax_line)
+
+ # Balance the journal entry.
+ with move_form.line_ids.new() as debit_line:
+ debit_line.name = 'balance'
+ debit_line.account_id = self.company_data['default_account_revenue']
+ debit_line.debit = 1100.0
+
+ move = move_form.save()
+
+ self.assertRecordValues(move.line_ids.sorted('balance'), [
+ {'balance': -1000.0, 'tax_ids': purch_tax.ids, 'tax_tag_ids': self.base_tag_pos.ids, 'tax_base_amount': 0, 'tax_repartition_line_id': False},
+ {'balance': -100.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_pos.ids, 'tax_base_amount': 1000, 'tax_repartition_line_id': ref_tax_rep_ln.id},
+ {'balance': 1100.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_base_amount': 0, 'tax_repartition_line_id': 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 and invert tags
+ 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_neg.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_neg.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 and invert tags
+ 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_pos.ids, 'tax_base_amount': 0},
+ {'balance': -100.0, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_pos.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%
+ price_unit | Quantity | Taxes
+ ------------------
+ 2.82 | 20000 | 21% not incl
+ '''
+ self.env['res.currency.rate'].create({
+ 'name': '2018-01-01',
+ 'rate': 1.1726,
+ 'currency_id': self.currency_data['currency'].id,
+ 'company_id': self.env.company.id,
+ })
+ self.currency_data['currency'].rounding = 0.05
+
+ invoice = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'partner_id': self.partner_a.id,
+ 'currency_id': self.currency_data['currency'].id,
+ 'invoice_date': '2018-01-01',
+ 'date': '2018-01-01',
+ 'invoice_line_ids': [(0, 0, {
+ 'name': 'xxxx',
+ 'quantity': 20000,
+ 'price_unit': 2.82,
+ 'tax_ids': [(6, 0, self.percent_tax_1.ids)],
+ })]
+ })
+
+ self.assertRecordValues(invoice.line_ids.filtered('tax_line_id'), [{
+ 'tax_base_amount': 48098.24, # 20000 * 2.82 / 1.1726
+ 'credit': 10100.63, # tax_base_amount * 0.21
+ }])
+
+ def test_ensure_no_unbalanced_entry(self):
+ ''' Ensure to not create an unbalanced journal entry when saving. '''
+ self.env['res.currency.rate'].create({
+ 'name': '2018-01-01',
+ 'rate': 0.654065014,
+ 'currency_id': self.currency_data['currency'].id,
+ 'company_id': self.env.company.id,
+ })
+ self.currency_data['currency'].rounding = 0.05
+
+ invoice = self._create_invoice([
+ (5, self.percent_tax_3_incl),
+ (10, self.percent_tax_3_incl),
+ (50, self.percent_tax_3_incl),
+ ], currency_id=self.currency_data['currency'], invoice_payment_term_id=self.pay_terms_a)
+ invoice.action_post()
diff --git a/addons/account/tests/test_payment_term.py b/addons/account/tests/test_payment_term.py
new file mode 100644
index 00000000..a3133247
--- /dev/null
+++ b/addons/account/tests/test_payment_term.py
@@ -0,0 +1,98 @@
+# -*- coding: utf-8 -*-
+
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests import tagged
+from odoo import fields
+from odoo.tests.common import Form
+
+
+@tagged('post_install', '-at_install')
+class TestAccountInvoiceRounding(AccountTestInvoicingCommon):
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+ cls.pay_term_today = cls.env['account.payment.term'].create({
+ 'name': 'Today',
+ 'line_ids': [
+ (0, 0, {
+ 'value': 'balance',
+ 'days': 0,
+ 'option': 'day_after_invoice_date',
+ }),
+ ],
+ })
+
+ cls.pay_term_min_31days_15th = cls.env['account.payment.term'].create({
+ 'name': 'the 15th of the month, min 31 days from now',
+ 'line_ids': [
+ (0, 0, {
+ 'value': 'balance',
+ 'days': 31,
+ 'day_of_the_month': 15,
+ 'option': 'day_after_invoice_date',
+ }),
+ ],
+ })
+
+ cls.pay_term_45_end_month = cls.env['account.payment.term'].create({
+ 'name': '45 Days from End of Month',
+ 'line_ids': [
+ (0, 0, {
+ 'value': 'balance',
+ 'days': 45,
+ 'option': 'after_invoice_month',
+ }),
+ ],
+ })
+
+ cls.pay_term_last_day_of_month = cls.env['account.payment.term'].create({
+ 'name': 'Last Day of month',
+ 'line_ids': [
+ (0, 0, {
+ 'value': 'balance',
+ 'days': 31,
+ 'option': 'day_current_month',
+ }),
+ ],
+ })
+
+ cls.pay_term_first_day_next_month = cls.env['account.payment.term'].create({
+ 'name': 'First day next month',
+ 'line_ids': [
+ (0, 0, {
+ 'value': 'balance',
+ 'days': 1,
+ 'option': 'day_following_month',
+ }),
+ ],
+ })
+
+ cls.invoice = cls.init_invoice('out_refund', products=cls.product_a+cls.product_b)
+
+ def assertPaymentTerm(self, pay_term, invoice_date, dates):
+ with Form(self.invoice) as move_form:
+ move_form.invoice_payment_term_id = pay_term
+ move_form.invoice_date = invoice_date
+ self.assertEqual(
+ self.invoice.line_ids.filtered(
+ lambda l: l.account_id == self.company_data['default_account_receivable']
+ ).mapped('date_maturity'),
+ [fields.Date.from_string(date) for date in dates],
+ )
+
+ def test_payment_term(self):
+ self.assertPaymentTerm(self.pay_term_today, '2019-01-01', ['2019-01-01'])
+ self.assertPaymentTerm(self.pay_term_today, '2019-01-15', ['2019-01-15'])
+ self.assertPaymentTerm(self.pay_term_today, '2019-01-31', ['2019-01-31'])
+ self.assertPaymentTerm(self.pay_term_45_end_month, '2019-01-01', ['2019-03-17'])
+ self.assertPaymentTerm(self.pay_term_45_end_month, '2019-01-15', ['2019-03-17'])
+ self.assertPaymentTerm(self.pay_term_45_end_month, '2019-01-31', ['2019-03-17'])
+ self.assertPaymentTerm(self.pay_term_min_31days_15th, '2019-01-01', ['2019-02-15'])
+ self.assertPaymentTerm(self.pay_term_min_31days_15th, '2019-01-15', ['2019-02-15'])
+ self.assertPaymentTerm(self.pay_term_min_31days_15th, '2019-01-31', ['2019-03-15'])
+ self.assertPaymentTerm(self.pay_term_last_day_of_month, '2019-01-01', ['2019-01-31'])
+ self.assertPaymentTerm(self.pay_term_last_day_of_month, '2019-01-15', ['2019-01-31'])
+ self.assertPaymentTerm(self.pay_term_last_day_of_month, '2019-01-31', ['2019-01-31'])
+ self.assertPaymentTerm(self.pay_term_first_day_next_month, '2019-01-01', ['2019-02-01'])
+ self.assertPaymentTerm(self.pay_term_first_day_next_month, '2019-01-15', ['2019-02-01'])
+ self.assertPaymentTerm(self.pay_term_first_day_next_month, '2019-01-31', ['2019-02-01'])
diff --git a/addons/account/tests/test_portal_attachment.py b/addons/account/tests/test_portal_attachment.py
new file mode 100644
index 00000000..2340fc75
--- /dev/null
+++ b/addons/account/tests/test_portal_attachment.py
@@ -0,0 +1,272 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo.addons.account.tests.common import AccountTestInvoicingHttpCommon
+from odoo.tests.common import tagged
+
+import json
+
+from odoo import http
+from odoo.tools import mute_logger
+
+
+@tagged('post_install', '-at_install')
+class TestPortalAttachment(AccountTestInvoicingHttpCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ cls.out_invoice = cls.env['account.move'].with_context(tracking_disable=True).create({
+ 'move_type': 'out_invoice',
+ 'partner_id': cls.partner_a.id,
+ 'invoice_date': '2019-05-01',
+ 'date': '2019-05-01',
+ 'invoice_line_ids': [
+ (0, 0, {'name': 'line1', 'price_unit': 100.0}),
+ ],
+ })
+
+ cls.base_url = cls.env['ir.config_parameter'].sudo().get_param('web.base.url')
+
+ @mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http')
+ def test_01_portal_attachment(self):
+ """Test the portal chatter attachment route."""
+
+ self.authenticate(None, None)
+
+ # Test public user can't create attachment without token of document
+ res = self.url_open(
+ url='%s/portal/attachment/add' % self.base_url,
+ data={
+ 'name': "new attachment",
+ 'res_model': self.out_invoice._name,
+ 'res_id': self.out_invoice.id,
+ 'csrf_token': http.WebRequest.csrf_token(self),
+ },
+ files=[('file', ('test.txt', b'test', 'plain/text'))],
+ )
+ self.assertEqual(res.status_code, 400)
+ self.assertIn("you do not have the rights", res.text)
+
+ # Test public user can create attachment with token
+ res = self.url_open(
+ url='%s/portal/attachment/add' % self.base_url,
+ data={
+ 'name': "new attachment",
+ 'res_model': self.out_invoice._name,
+ 'res_id': self.out_invoice.id,
+ 'csrf_token': http.WebRequest.csrf_token(self),
+ 'access_token': self.out_invoice._portal_ensure_token(),
+ },
+ files=[('file', ('test.txt', b'test', 'plain/text'))],
+ )
+ self.assertEqual(res.status_code, 200)
+ create_res = json.loads(res.content.decode('utf-8'))
+ self.assertTrue(self.env['ir.attachment'].sudo().search([('id', '=', create_res['id'])]))
+
+ # Test created attachment is private
+ res_binary = self.url_open('/web/content/%d' % create_res['id'])
+ self.assertEqual(res_binary.status_code, 404)
+
+ # Test created access_token is working
+ res_binary = self.url_open('/web/content/%d?access_token=%s' % (create_res['id'], create_res['access_token']))
+ self.assertEqual(res_binary.status_code, 200)
+
+ # Test mimetype is neutered as non-admin
+ res = self.url_open(
+ url='%s/portal/attachment/add' % self.base_url,
+ data={
+ 'name': "new attachment",
+ 'res_model': self.out_invoice._name,
+ 'res_id': self.out_invoice.id,
+ 'csrf_token': http.WebRequest.csrf_token(self),
+ 'access_token': self.out_invoice._portal_ensure_token(),
+ },
+ files=[('file', ('test.svg', b'<svg></svg>', 'image/svg+xml'))],
+ )
+ self.assertEqual(res.status_code, 200)
+ create_res = json.loads(res.content.decode('utf-8'))
+ self.assertEqual(create_res['mimetype'], 'text/plain')
+
+ res_binary = self.url_open('/web/content/%d?access_token=%s' % (create_res['id'], create_res['access_token']))
+ self.assertEqual(res_binary.headers['Content-Type'], 'text/plain')
+ self.assertEqual(res_binary.content, b'<svg></svg>')
+
+ res_image = self.url_open('/web/image/%d?access_token=%s' % (create_res['id'], create_res['access_token']))
+ self.assertEqual(res_image.headers['Content-Type'], 'text/plain')
+ self.assertEqual(res_image.content, b'<svg></svg>')
+
+ # Test attachment can't be removed without valid token
+ res = self.opener.post(
+ url='%s/portal/attachment/remove' % self.base_url,
+ json={
+ 'params': {
+ 'attachment_id': create_res['id'],
+ 'access_token': "wrong",
+ },
+ },
+ )
+ self.assertEqual(res.status_code, 200)
+ self.assertTrue(self.env['ir.attachment'].sudo().search([('id', '=', create_res['id'])]))
+ self.assertIn("you do not have the rights", res.text)
+
+ # Test attachment can be removed with token if "pending" state
+ res = self.opener.post(
+ url='%s/portal/attachment/remove' % self.base_url,
+ json={
+ 'params': {
+ 'attachment_id': create_res['id'],
+ 'access_token': create_res['access_token'],
+ },
+ },
+ )
+ self.assertEqual(res.status_code, 200)
+ remove_res = json.loads(res.content.decode('utf-8'))['result']
+ self.assertFalse(self.env['ir.attachment'].sudo().search([('id', '=', create_res['id'])]))
+ self.assertTrue(remove_res is True)
+
+ # Test attachment can't be removed if not "pending" state
+ attachment = self.env['ir.attachment'].create({
+ 'name': 'an attachment',
+ 'access_token': self.env['ir.attachment']._generate_access_token(),
+ })
+ res = self.opener.post(
+ url='%s/portal/attachment/remove' % self.base_url,
+ json={
+ 'params': {
+ 'attachment_id': attachment.id,
+ 'access_token': attachment.access_token,
+ },
+ },
+ )
+ self.assertEqual(res.status_code, 200)
+ self.assertTrue(self.env['ir.attachment'].sudo().search([('id', '=', attachment.id)]))
+ self.assertIn("not in a pending state", res.text)
+
+ # Test attachment can't be removed if attached to a message
+ attachment.write({
+ 'res_model': 'mail.compose.message',
+ 'res_id': 0,
+ })
+ attachment.flush()
+ message = self.env['mail.message'].create({
+ 'attachment_ids': [(6, 0, attachment.ids)],
+ })
+ res = self.opener.post(
+ url='%s/portal/attachment/remove' % self.base_url,
+ json={
+ 'params': {
+ 'attachment_id': attachment.id,
+ 'access_token': attachment.access_token,
+ },
+ },
+ )
+ self.assertEqual(res.status_code, 200)
+ self.assertTrue(attachment.exists())
+ self.assertIn("it is linked to a message", res.text)
+ message.sudo().unlink()
+
+ # Test attachment can't be associated if no attachment token.
+ res = self.url_open(
+ url='%s/mail/chatter_post' % self.base_url,
+ data={
+ 'res_model': self.out_invoice._name,
+ 'res_id': self.out_invoice.id,
+ 'message': "test message 1",
+ 'attachment_ids': attachment.id,
+ 'attachment_tokens': 'false',
+ 'csrf_token': http.WebRequest.csrf_token(self),
+ },
+ )
+ self.assertEqual(res.status_code, 400)
+ self.assertIn("The attachment %s does not exist or you do not have the rights to access it." % attachment.id, res.text)
+
+ # Test attachment can't be associated if no main document token
+ res = self.url_open(
+ url='%s/mail/chatter_post' % self.base_url,
+ data={
+ 'res_model': self.out_invoice._name,
+ 'res_id': self.out_invoice.id,
+ 'message': "test message 1",
+ 'attachment_ids': attachment.id,
+ 'attachment_tokens': attachment.access_token,
+ 'csrf_token': http.WebRequest.csrf_token(self),
+ },
+ )
+ self.assertEqual(res.status_code, 403)
+ self.assertIn("You are not allowed to access 'Journal Entry' (account.move) records.", res.text)
+
+ # Test attachment can't be associated if not "pending" state
+ self.assertFalse(self.out_invoice.message_ids)
+ attachment.write({'res_model': 'model'})
+ res = self.url_open(
+ url='%s/mail/chatter_post' % self.base_url,
+ data={
+ 'res_model': self.out_invoice._name,
+ 'res_id': self.out_invoice.id,
+ 'message': "test message 1",
+ 'attachment_ids': attachment.id,
+ 'attachment_tokens': attachment.access_token,
+ 'csrf_token': http.WebRequest.csrf_token(self),
+ 'token': self.out_invoice._portal_ensure_token(),
+ },
+ )
+ self.assertEqual(res.status_code, 200)
+ self.out_invoice.invalidate_cache(fnames=['message_ids'], ids=self.out_invoice.ids)
+ self.assertEqual(len(self.out_invoice.message_ids), 1)
+ self.assertEqual(self.out_invoice.message_ids.body, "<p>test message 1</p>")
+ self.assertFalse(self.out_invoice.message_ids.attachment_ids)
+
+ # Test attachment can't be associated if not correct user
+ attachment.write({'res_model': 'mail.compose.message'})
+ res = self.url_open(
+ url='%s/mail/chatter_post' % self.base_url,
+ data={
+ 'res_model': self.out_invoice._name,
+ 'res_id': self.out_invoice.id,
+ 'message': "test message 2",
+ 'attachment_ids': attachment.id,
+ 'attachment_tokens': attachment.access_token,
+ 'csrf_token': http.WebRequest.csrf_token(self),
+ 'token': self.out_invoice._portal_ensure_token(),
+ },
+ )
+ self.assertEqual(res.status_code, 200)
+ self.out_invoice.invalidate_cache(fnames=['message_ids'], ids=self.out_invoice.ids)
+ self.assertEqual(len(self.out_invoice.message_ids), 2)
+ self.assertEqual(self.out_invoice.message_ids[0].body, "<p>test message 2</p>")
+ self.assertFalse(self.out_invoice.message_ids.attachment_ids)
+
+ # Test attachment can be associated if all good (complete flow)
+ res = self.url_open(
+ url='%s/portal/attachment/add' % self.base_url,
+ data={
+ 'name': "final attachment",
+ 'res_model': self.out_invoice._name,
+ 'res_id': self.out_invoice.id,
+ 'csrf_token': http.WebRequest.csrf_token(self),
+ 'access_token': self.out_invoice._portal_ensure_token(),
+ },
+ files=[('file', ('test.txt', b'test', 'plain/text'))],
+ )
+ self.assertEqual(res.status_code, 200)
+ create_res = json.loads(res.content.decode('utf-8'))
+ self.assertEqual(create_res['name'], "final attachment")
+
+ res = self.url_open(
+ url='%s/mail/chatter_post' % self.base_url,
+ data={
+ 'res_model': self.out_invoice._name,
+ 'res_id': self.out_invoice.id,
+ 'message': "test message 3",
+ 'attachment_ids': create_res['id'],
+ 'attachment_tokens': create_res['access_token'],
+ 'csrf_token': http.WebRequest.csrf_token(self),
+ 'token': self.out_invoice._portal_ensure_token(),
+ },
+ )
+ self.assertEqual(res.status_code, 200)
+ self.out_invoice.invalidate_cache(fnames=['message_ids'], ids=self.out_invoice.ids)
+ self.assertEqual(len(self.out_invoice.message_ids), 3)
+ self.assertEqual(self.out_invoice.message_ids[0].body, "<p>test message 3</p>")
+ self.assertEqual(len(self.out_invoice.message_ids[0].attachment_ids), 1)
diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py
new file mode 100644
index 00000000..98559eab
--- /dev/null
+++ b/addons/account/tests/test_reconciliation.py
@@ -0,0 +1,1208 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import time
+import unittest
+from datetime import timedelta
+
+from odoo import api, fields
+from odoo.addons.account.tests.common import TestAccountReconciliationCommon
+from odoo.tests import Form, tagged
+
+
+@tagged('post_install', '-at_install')
+class TestReconciliationExec(TestAccountReconciliationCommon):
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.env['res.currency.rate'].search([]).unlink()
+
+ def test_statement_euro_invoice_usd_transaction_euro_full(self):
+ self.env['res.currency.rate'].create({
+ 'name': '%s-07-01' % time.strftime('%Y'),
+ 'rate': 1.5289,
+ 'currency_id': self.currency_usd_id,
+ })
+ # Create a customer invoice of 50 USD.
+ partner = self.env['res.partner'].create({'name': 'test'})
+ move = self.env['account.move'].with_context(default_move_type='out_invoice').create({
+ 'move_type': 'out_invoice',
+ 'partner_id': partner.id,
+ 'invoice_date': '%s-07-01' % time.strftime('%Y'),
+ 'date': '%s-07-01' % time.strftime('%Y'),
+ 'currency_id': self.currency_usd_id,
+ 'invoice_line_ids': [
+ (0, 0, {'quantity': 1, 'price_unit': 50.0, 'name': 'test'})
+ ],
+ })
+ move.action_post()
+
+ # Create a bank statement of 40 EURO.
+ bank_stmt = self.env['account.bank.statement'].create({
+ 'journal_id': self.bank_journal_euro.id,
+ 'date': '%s-01-01' % time.strftime('%Y'),
+ 'line_ids': [
+ (0, 0, {
+ 'payment_ref': 'test',
+ 'partner_id': partner.id,
+ 'amount': 40.0,
+ 'date': '%s-01-01' % time.strftime('%Y')
+ })
+ ],
+ })
+
+ # Reconcile the bank statement with the invoice.
+ receivable_line = move.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
+ bank_stmt.button_post()
+ bank_stmt.line_ids[0].reconcile([
+ {'id': receivable_line.id},
+ {'name': 'exchange difference', 'balance': -7.3, 'account_id': self.diff_income_account.id},
+ ])
+
+ self.assertRecordValues(bank_stmt.line_ids.line_ids, [
+ {'debit': 40.0, 'credit': 0.0, 'amount_currency': 40.0, 'currency_id': self.currency_euro_id},
+ {'debit': 0.0, 'credit': 7.3, 'amount_currency': -7.3, 'currency_id': self.currency_euro_id},
+ {'debit': 0.0, 'credit': 32.7, 'amount_currency': -32.7, 'currency_id': self.currency_euro_id},
+ ])
+
+ # The invoice should be paid, as the payments totally cover its total
+ self.assertEqual(move.payment_state, 'paid', 'The invoice should be paid by now')
+ self.assertTrue(receivable_line.reconciled, 'The invoice should be totally reconciled')
+ self.assertTrue(receivable_line.full_reconcile_id, 'The invoice should have a full reconcile number')
+ self.assertEqual(receivable_line.amount_residual, 0, 'The invoice should be totally reconciled')
+ self.assertEqual(receivable_line.amount_residual_currency, 0, 'The invoice should be totally reconciled')
+
+ @unittest.skip('adapt to new accounting')
+ def test_balanced_exchanges_gain_loss(self):
+ # The point of this test is to show that we handle correctly the gain/loss exchanges during reconciliations in foreign currencies.
+ # For instance, with a company set in EUR, and a USD rate set to 0.033,
+ # the reconciliation of an invoice of 2.00 USD (60.61 EUR) and a bank statement of two lines of 1.00 USD (30.30 EUR)
+ # will lead to an exchange loss, that should be handled correctly within the journal items.
+ env = api.Environment(self.cr, self.uid, {})
+ # We update the currency rate of the currency USD in order to force the gain/loss exchanges in next steps
+ rateUSDbis = env.ref("base.rateUSDbis")
+ rateUSDbis.write({
+ 'name': time.strftime('%Y-%m-%d') + ' 00:00:00',
+ 'rate': 0.033,
+ })
+ # We create a customer invoice of 2.00 USD
+ invoice = self.account_invoice_model.create({
+ 'partner_id': self.partner_agrolait_id,
+ 'currency_id': self.currency_usd_id,
+ 'name': 'Foreign invoice with exchange gain',
+ 'account_id': self.account_rcv_id,
+ 'move_type': 'out_invoice',
+ 'invoice_date': time.strftime('%Y-%m-%d'),
+ 'date': time.strftime('%Y-%m-%d'),
+ 'journal_id': self.bank_journal_usd_id,
+ 'invoice_line': [
+ (0, 0, {
+ 'name': 'line that will lead to an exchange gain',
+ 'quantity': 1,
+ 'price_unit': 2,
+ })
+ ]
+ })
+ invoice.action_post()
+ # We create a bank statement with two lines of 1.00 USD each.
+ statement = self.env['account.bank.statement'].create({
+ 'journal_id': self.bank_journal_usd_id,
+ 'date': time.strftime('%Y-%m-%d'),
+ 'line_ids': [
+ (0, 0, {
+ 'name': 'half payment',
+ 'partner_id': self.partner_agrolait_id,
+ 'amount': 1.0,
+ 'date': time.strftime('%Y-%m-%d')
+ }),
+ (0, 0, {
+ 'name': 'second half payment',
+ 'partner_id': self.partner_agrolait_id,
+ 'amount': 1.0,
+ 'date': time.strftime('%Y-%m-%d')
+ })
+ ]
+ })
+
+ # We process the reconciliation of the invoice line with the two bank statement lines
+ line_id = None
+ for l in invoice.line_id:
+ if l.account_id.id == self.account_rcv_id:
+ line_id = l
+ break
+ for statement_line in statement.line_ids:
+ statement_line.reconcile([{'id': line_id.id}])
+
+ # The invoice should be paid, as the payments totally cover its total
+ self.assertEqual(invoice.state, 'paid', 'The invoice should be paid by now')
+ reconcile = None
+ for payment in invoice.payment_ids:
+ reconcile = payment.reconcile_model_id
+ break
+ # The invoice should be reconciled (entirely, not a partial reconciliation)
+ self.assertTrue(reconcile, 'The invoice should be totally reconciled')
+ result = {}
+ exchange_loss_line = None
+ for line in reconcile.line_id:
+ res_account = result.setdefault(line.account_id, {'debit': 0.0, 'credit': 0.0, 'count': 0})
+ res_account['debit'] = res_account['debit'] + line.debit
+ res_account['credit'] = res_account['credit'] + line.credit
+ res_account['count'] += 1
+ if line.credit == 0.01:
+ exchange_loss_line = line
+ # We should be able to find a move line of 0.01 EUR on the Debtors account, being the cent we lost during the currency exchange
+ self.assertTrue(exchange_loss_line, 'There should be one move line of 0.01 EUR in credit')
+ # The journal items of the reconciliation should have their debit and credit total equal
+ # Besides, the total debit and total credit should be 60.61 EUR (2.00 USD)
+ self.assertEqual(sum(res['debit'] for res in result.values()), 60.61)
+ self.assertEqual(sum(res['credit'] for res in result.items()), 60.61)
+ counterpart_exchange_loss_line = None
+ for line in exchange_loss_line.move_id.line_id:
+ if line.account_id.id == self.account_fx_expense_id:
+ counterpart_exchange_loss_line = line
+ # We should be able to find a move line of 0.01 EUR on the Foreign Exchange Loss account
+ self.assertTrue(counterpart_exchange_loss_line, 'There should be one move line of 0.01 EUR on account "Foreign Exchange Loss"')
+
+ def test_manual_reconcile_wizard_opw678153(self):
+
+ def create_move(name, amount, amount_currency, currency_id):
+ debit_line_vals = {
+ 'name': name,
+ 'debit': amount > 0 and amount or 0.0,
+ 'credit': amount < 0 and -amount or 0.0,
+ 'account_id': self.account_rcv.id,
+ 'amount_currency': amount_currency,
+ 'currency_id': currency_id,
+ }
+ credit_line_vals = debit_line_vals.copy()
+ credit_line_vals['debit'] = debit_line_vals['credit']
+ credit_line_vals['credit'] = debit_line_vals['debit']
+ credit_line_vals['account_id'] = self.account_rsa.id
+ credit_line_vals['amount_currency'] = -debit_line_vals['amount_currency']
+ vals = {
+ 'journal_id': self.bank_journal_euro.id,
+ 'line_ids': [(0,0, debit_line_vals), (0, 0, credit_line_vals)]
+ }
+ move = self.env['account.move'].create(vals)
+ move.action_post()
+ return move.id
+ move_list_vals = [
+ ('1', -1.83, 0, self.currency_swiss_id),
+ ('2', 728.35, 795.05, self.currency_swiss_id),
+ ('3', -4.46, 0, self.currency_swiss_id),
+ ('4', 0.32, 0, self.currency_swiss_id),
+ ('5', 14.72, 16.20, self.currency_swiss_id),
+ ('6', -737.10, -811.25, self.currency_swiss_id),
+ ]
+ move_ids = []
+ for name, amount, amount_currency, currency_id in move_list_vals:
+ move_ids.append(create_move(name, amount, amount_currency, currency_id))
+ aml_recs = self.env['account.move.line'].search([('move_id', 'in', move_ids), ('account_id', '=', self.account_rcv.id), ('reconciled', '=', False)])
+ aml_recs.reconcile()
+ for aml in aml_recs:
+ self.assertTrue(aml.reconciled, 'The journal item should be totally reconciled')
+ self.assertEqual(aml.amount_residual, 0, 'The journal item should be totally reconciled')
+ self.assertEqual(aml.amount_residual_currency, 0, 'The journal item should be totally reconciled')
+
+ def test_partial_reconcile_currencies_01(self):
+ # client Account (payable, rsa)
+ # Debit Credit
+ # --------------------------------------------------------
+ # Pay a : 25/0.5 = 50 | Inv a : 50/0.5 = 100
+ # Pay b: 50/0.75 = 66.66 | Inv b : 50/0.75 = 66.66
+ # Pay c: 25/0.8 = 31.25 |
+ #
+ # Debit_currency = 100 | Credit currency = 100
+ # Debit = 147.91 | Credit = 166.66
+ # Balance Debit = 18.75
+ # Counterpart Credit goes in Exchange diff
+
+ dest_journal_id = self.env['account.journal'].create({
+ 'name': 'dest_journal_id',
+ 'type': 'bank',
+ })
+
+ # Setting up rates for USD (main_company is in EUR)
+ self.env['res.currency.rate'].create({'name': time.strftime('%Y') + '-' + '07' + '-01',
+ 'rate': 0.5,
+ 'currency_id': self.currency_usd_id,
+ 'company_id': self.company.id})
+
+ self.env['res.currency.rate'].create({'name': time.strftime('%Y') + '-' + '08' + '-01',
+ 'rate': 0.75,
+ 'currency_id': self.currency_usd_id,
+ 'company_id': self.company.id})
+
+ self.env['res.currency.rate'].create({'name': time.strftime('%Y') + '-' + '09' + '-01',
+ 'rate': 0.80,
+ 'currency_id': self.currency_usd_id,
+ 'company_id': self.company.id})
+
+ # Preparing Invoices (from vendor)
+ invoice_a = self.env['account.move'].with_context(default_move_type='in_invoice').create({
+ 'move_type': 'in_invoice',
+ 'partner_id': self.partner_agrolait_id,
+ 'currency_id': self.currency_usd_id,
+ 'invoice_date': '%s-07-01' % time.strftime('%Y'),
+ 'date': '%s-07-01' % time.strftime('%Y'),
+ 'invoice_line_ids': [
+ (0, 0, {'product_id': self.product.id, 'quantity': 1, 'price_unit': 50.0})
+ ],
+ })
+ invoice_b = self.env['account.move'].with_context(default_move_type='in_invoice').create({
+ 'move_type': 'in_invoice',
+ 'partner_id': self.partner_agrolait_id,
+ 'currency_id': self.currency_usd_id,
+ 'invoice_date': '%s-08-01' % time.strftime('%Y'),
+ 'date': '%s-08-01' % time.strftime('%Y'),
+ 'invoice_line_ids': [
+ (0, 0, {'product_id': self.product.id, 'quantity': 1, 'price_unit': 50.0})
+ ],
+ })
+ (invoice_a + invoice_b).action_post()
+
+ # Preparing Payments
+ # One partial for invoice_a (fully assigned to it)
+ payment_a = self.env['account.payment'].create({'payment_type': 'outbound',
+ 'amount': 25,
+ 'currency_id': self.currency_usd_id,
+ 'journal_id': self.bank_journal_euro.id,
+ 'company_id': self.company.id,
+ 'date': time.strftime('%Y') + '-' + '07' + '-01',
+ 'partner_id': self.partner_agrolait_id,
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id,
+ 'partner_type': 'supplier'})
+
+ # One that will complete the payment of a, the rest goes to b
+ payment_b = self.env['account.payment'].create({'payment_type': 'outbound',
+ 'amount': 50,
+ 'currency_id': self.currency_usd_id,
+ 'journal_id': self.bank_journal_euro.id,
+ 'company_id': self.company.id,
+ 'date': time.strftime('%Y') + '-' + '08' + '-01',
+ 'partner_id': self.partner_agrolait_id,
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id,
+ 'partner_type': 'supplier'})
+
+ # The last one will complete the payment of b
+ payment_c = self.env['account.payment'].create({'payment_type': 'outbound',
+ 'amount': 25,
+ 'currency_id': self.currency_usd_id,
+ 'journal_id': self.bank_journal_euro.id,
+ 'company_id': self.company.id,
+ 'date': time.strftime('%Y') + '-' + '09' + '-01',
+ 'partner_id': self.partner_agrolait_id,
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id,
+ 'partner_type': 'supplier'})
+
+ payment_a.action_post()
+ payment_b.action_post()
+ payment_c.action_post()
+
+ # Assigning payments to invoices
+ debit_line_a = payment_a.line_ids.filtered(lambda l: l.debit and l.account_id == self.account_rsa)
+ debit_line_b = payment_b.line_ids.filtered(lambda l: l.debit and l.account_id == self.account_rsa)
+ debit_line_c = payment_c.line_ids.filtered(lambda l: l.debit and l.account_id == self.account_rsa)
+
+ invoice_a.js_assign_outstanding_line(debit_line_a.id)
+ invoice_a.js_assign_outstanding_line(debit_line_b.id)
+ invoice_b.js_assign_outstanding_line(debit_line_b.id)
+ invoice_b.js_assign_outstanding_line(debit_line_c.id)
+
+ # Asserting correctness (only in the payable account)
+ full_reconcile = False
+ reconciled_amls = (debit_line_a + debit_line_b + debit_line_c + (invoice_a + invoice_b).mapped('line_ids'))\
+ .filtered(lambda l: l.account_id == self.account_rsa)
+ for aml in reconciled_amls:
+ self.assertEqual(aml.amount_residual, 0.0)
+ self.assertEqual(aml.amount_residual_currency, 0.0)
+ self.assertTrue(aml.reconciled)
+ if not full_reconcile:
+ full_reconcile = aml.full_reconcile_id
+ else:
+ self.assertTrue(aml.full_reconcile_id == full_reconcile)
+
+ full_rec_move = full_reconcile.exchange_move_id
+ # Globally check whether the amount is correct
+ self.assertEqual(sum(full_rec_move.mapped('line_ids.debit')), 18.75)
+
+ # Checking if the direction of the move is correct
+ full_rec_payable = full_rec_move.line_ids.filtered(lambda l: l.account_id == self.account_rsa)
+ self.assertEqual(full_rec_payable.balance, 18.75)
+
+ def test_unreconcile(self):
+ # Use case:
+ # 2 invoices paid with a single payment. Unreconcile the payment with one invoice, the
+ # other invoice should remain reconciled.
+ inv1 = self.create_invoice(invoice_amount=10, currency_id=self.currency_usd_id)
+ inv2 = self.create_invoice(invoice_amount=20, currency_id=self.currency_usd_id)
+ payment = self.env['account.payment'].create({
+ 'payment_type': 'inbound',
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
+ 'partner_type': 'customer',
+ 'partner_id': self.partner_agrolait_id,
+ 'amount': 100,
+ 'currency_id': self.currency_usd_id,
+ 'journal_id': self.bank_journal_usd.id,
+ })
+ payment.action_post()
+ credit_aml = payment.line_ids.filtered('credit')
+
+ # Check residual before assignation
+ self.assertAlmostEqual(inv1.amount_residual, 10)
+ self.assertAlmostEqual(inv2.amount_residual, 20)
+
+ # Assign credit and residual
+ inv1.js_assign_outstanding_line(credit_aml.id)
+ inv2.js_assign_outstanding_line(credit_aml.id)
+ self.assertAlmostEqual(inv1.amount_residual, 0)
+ self.assertAlmostEqual(inv2.amount_residual, 0)
+
+ # Unreconcile one invoice at a time and check residual
+ credit_aml.remove_move_reconcile()
+ self.assertAlmostEqual(inv1.amount_residual, 10)
+ self.assertAlmostEqual(inv2.amount_residual, 20)
+
+ def test_unreconcile_exchange(self):
+ # Use case:
+ # - Company currency in EUR
+ # - Create 2 rates for USD:
+ # 1.0 on 2018-01-01
+ # 0.5 on 2018-02-01
+ # - Create an invoice on 2018-01-02 of 111 USD
+ # - Register a payment on 2018-02-02 of 111 USD
+ # - Unreconcile the payment
+
+ self.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y') + '-07-01',
+ 'rate': 1.0,
+ 'currency_id': self.currency_usd_id,
+ 'company_id': self.company.id
+ })
+ self.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y') + '-08-01',
+ 'rate': 0.5,
+ 'currency_id': self.currency_usd_id,
+ 'company_id': self.company.id
+ })
+ inv = self.create_invoice(invoice_amount=111, currency_id=self.currency_usd_id)
+ payment = self.env['account.payment'].create({
+ 'payment_type': 'inbound',
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
+ 'partner_type': 'customer',
+ 'partner_id': self.partner_agrolait_id,
+ 'amount': 111,
+ 'currency_id': self.currency_usd_id,
+ 'journal_id': self.bank_journal_usd.id,
+ 'date': time.strftime('%Y') + '-08-01',
+ })
+ payment.action_post()
+ credit_aml = payment.line_ids.filtered('credit')
+
+ # Check residual before assignation
+ self.assertAlmostEqual(inv.amount_residual, 111)
+
+ # Assign credit, check exchange move and residual
+ inv.js_assign_outstanding_line(credit_aml.id)
+ self.assertEqual(len(payment.line_ids.mapped('full_reconcile_id').exchange_move_id), 1)
+ self.assertAlmostEqual(inv.amount_residual, 0)
+
+ # Unreconcile invoice and check residual
+ credit_aml.remove_move_reconcile()
+ self.assertAlmostEqual(inv.amount_residual, 111)
+
+ def test_revert_payment_and_reconcile(self):
+ payment = self.env['account.payment'].create({
+ 'payment_method_id': self.inbound_payment_method.id,
+ 'payment_type': 'inbound',
+ 'partner_type': 'customer',
+ 'partner_id': self.partner_agrolait_id,
+ 'journal_id': self.bank_journal_usd.id,
+ 'date': '2018-06-04',
+ 'amount': 666,
+ })
+ payment.action_post()
+
+ self.assertEqual(len(payment.line_ids), 2)
+
+ bank_line = payment.line_ids.filtered(lambda l: l.account_id.id == self.bank_journal_usd.payment_debit_account_id.id)
+ customer_line = payment.line_ids - bank_line
+
+ self.assertEqual(len(bank_line), 1)
+ self.assertEqual(len(customer_line), 1)
+ self.assertNotEqual(bank_line.id, customer_line.id)
+
+ self.assertEqual(bank_line.move_id.id, customer_line.move_id.id)
+ move = bank_line.move_id
+
+ # Reversing the payment's move
+ reversed_move = move._reverse_moves([{'date': '2018-06-04'}])
+ self.assertEqual(len(reversed_move), 1)
+
+ self.assertEqual(len(reversed_move.line_ids), 2)
+
+ # Testing the reconciliation matching between the move lines and their reversed counterparts
+ reversed_bank_line = reversed_move.line_ids.filtered(lambda l: l.account_id.id == self.bank_journal_usd.payment_debit_account_id.id)
+ reversed_customer_line = reversed_move.line_ids - reversed_bank_line
+
+ self.assertEqual(len(reversed_bank_line), 1)
+ self.assertEqual(len(reversed_customer_line), 1)
+ self.assertNotEqual(reversed_bank_line.id, reversed_customer_line.id)
+ self.assertEqual(reversed_bank_line.move_id.id, reversed_customer_line.move_id.id)
+
+ self.assertEqual(reversed_bank_line.full_reconcile_id.id, bank_line.full_reconcile_id.id)
+ self.assertEqual(reversed_customer_line.full_reconcile_id.id, customer_line.full_reconcile_id.id)
+
+
+ def test_revert_payment_and_reconcile_exchange(self):
+
+ # A reversal of a reconciled payment which created a currency exchange entry, should create reversal moves
+ # which move lines should be reconciled two by two with the original move's lines
+
+ def _determine_debit_credit_line(move):
+ line_ids_reconciliable = move.line_ids.filtered(lambda l: l.account_id.reconcile or l.account_id.internal_type == 'liquidity')
+ return line_ids_reconciliable.filtered(lambda l: l.debit), line_ids_reconciliable.filtered(lambda l: l.credit)
+
+ def _move_revert_test_pair(move, revert):
+ self.assertTrue(move.line_ids)
+ self.assertTrue(revert.line_ids)
+
+ move_lines = _determine_debit_credit_line(move)
+ revert_lines = _determine_debit_credit_line(revert)
+
+ # in the case of the exchange entry, only one pair of lines will be found
+ if move_lines[0] and revert_lines[1]:
+ self.assertTrue(move_lines[0].full_reconcile_id.exists())
+ self.assertEqual(move_lines[0].full_reconcile_id.id, revert_lines[1].full_reconcile_id.id)
+
+ if move_lines[1] and revert_lines[0]:
+ self.assertTrue(move_lines[1].full_reconcile_id.exists())
+ self.assertEqual(move_lines[1].full_reconcile_id.id, revert_lines[0].full_reconcile_id.id)
+
+ self.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y') + '-07-01',
+ 'rate': 1.0,
+ 'currency_id': self.currency_usd_id,
+ 'company_id': self.company.id
+ })
+ self.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y') + '-08-01',
+ 'rate': 0.5,
+ 'currency_id': self.currency_usd_id,
+ 'company_id': self.company.id
+ })
+ inv = self.create_invoice(invoice_amount=111, currency_id=self.currency_usd_id)
+ payment = self.env['account.payment'].create({
+ 'payment_type': 'inbound',
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
+ 'partner_type': 'customer',
+ 'partner_id': self.partner_agrolait_id,
+ 'amount': 111,
+ 'currency_id': self.currency_usd_id,
+ 'journal_id': self.bank_journal_usd.id,
+ 'date': time.strftime('%Y') + '-08-01',
+ })
+ payment.action_post()
+
+ credit_aml = payment.line_ids.filtered('credit')
+ inv.js_assign_outstanding_line(credit_aml.id)
+ self.assertTrue(inv.payment_state in ('in_payment', 'paid'), "Invoice should be paid")
+
+ exchange_reconcile = payment.line_ids.mapped('full_reconcile_id')
+ exchange_move = exchange_reconcile.exchange_move_id
+ payment_move = payment.line_ids[0].move_id
+
+ reverted_payment_move = payment_move._reverse_moves([{'date': time.strftime('%Y') + '-08-01'}], cancel=True)
+
+ # After reversal of payment, the invoice should be open
+ self.assertTrue(inv.state == 'posted', 'The invoice should be open again')
+ self.assertFalse(exchange_reconcile.exists())
+
+ reverted_exchange_move = self.env['account.move'].search([('journal_id', '=', exchange_move.journal_id.id), ('ref', 'ilike', exchange_move.name)], limit=1)
+ _move_revert_test_pair(payment_move, reverted_payment_move)
+ _move_revert_test_pair(exchange_move, reverted_exchange_move)
+
+ def test_partial_reconcile_currencies_02(self):
+ ####
+ # Day 1: Invoice Cust/001 to customer (expressed in USD)
+ # Market value of USD (day 1): 1 USD = 0.5 EUR
+ # * Dr. 100 USD / 50 EUR - Accounts receivable
+ # * Cr. 100 USD / 50 EUR - Revenue
+ ####
+ dest_journal_id = self.env['account.journal'].create({
+ 'name': 'turlututu',
+ 'type': 'bank',
+ 'company_id': self.env.company.id,
+ })
+
+ self.env['res.currency.rate'].create({
+ 'currency_id': self.currency_usd_id,
+ 'name': time.strftime('%Y') + '-01-01',
+ 'rate': 2,
+ })
+
+ invoice_cust_1 = self.env['account.move'].with_context(default_move_type='out_invoice').create({
+ 'move_type': 'out_invoice',
+ 'partner_id': self.partner_agrolait_id,
+ 'invoice_date': '%s-01-01' % time.strftime('%Y'),
+ 'date': '%s-01-01' % time.strftime('%Y'),
+ 'currency_id': self.currency_usd_id,
+ 'invoice_line_ids': [
+ (0, 0, {'quantity': 1, 'price_unit': 100.0, 'name': 'product that cost 100'})
+ ],
+ })
+ invoice_cust_1.action_post()
+ aml = invoice_cust_1.invoice_line_ids[0]
+ self.assertEqual(aml.credit, 50.0)
+ #####
+ # Day 2: Receive payment for half invoice Cust/1 (in USD)
+ # -------------------------------------------------------
+ # Market value of USD (day 2): 1 USD = 1 EUR
+
+ # Payment transaction:
+ # * Dr. 50 USD / 50 EUR - EUR Bank (valued at market price
+ # at the time of receiving the money)
+ # * Cr. 50 USD / 50 EUR - Accounts Receivable
+ #####
+ self.env['res.currency.rate'].create({
+ 'currency_id': self.currency_usd_id,
+ 'name': time.strftime('%Y') + '-01-02',
+ 'rate': 1,
+ })
+
+ payment = self.env['account.payment.register']\
+ .with_context(active_model='account.move', active_ids=invoice_cust_1.ids)\
+ .create({
+ 'payment_date': time.strftime('%Y') + '-01-02',
+ 'amount': 50,
+ 'journal_id': dest_journal_id.id,
+ 'currency_id': self.currency_usd_id,
+ })\
+ ._create_payments()
+
+ # We expect at this point that the invoice should still be open, in 'partial' state,
+ # because they owe us still 50 CC.
+ self.assertEqual(invoice_cust_1.payment_state, 'partial', 'Invoice is in status %s' % invoice_cust_1.state)
+
+ def test_multiple_term_reconciliation_opw_1906665(self):
+ '''Test that when registering a payment to an invoice with multiple
+ payment term lines the reconciliation happens against the line
+ with the earliest date_maturity
+ '''
+
+ payment_term = self.env['account.payment.term'].create({
+ 'name': 'Pay in 2 installments',
+ 'line_ids': [
+ # Pay 50% immediately
+ (0, 0, {
+ 'value': 'percent',
+ 'value_amount': 50,
+ }),
+ # Pay the rest after 14 days
+ (0, 0, {
+ 'value': 'balance',
+ 'days': 14,
+ })
+ ],
+ })
+
+ # can't use self.create_invoice because it validates and we need to set payment_term_id
+ invoice = self.create_invoice_partner(
+ partner_id=self.partner_agrolait_id,
+ payment_term_id=payment_term.id,
+ currency_id=self.currency_usd_id,
+ )
+
+ payment = self.env['account.payment'].create({
+ 'date': time.strftime('%Y') + '-07-15',
+ 'payment_type': 'inbound',
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
+ 'partner_type': 'customer',
+ 'partner_id': self.partner_agrolait_id,
+ 'amount': 25,
+ 'currency_id': self.currency_usd_id,
+ 'journal_id': self.bank_journal_usd.id,
+ })
+ payment.action_post()
+
+ receivable_line = payment.line_ids.filtered('credit')
+ invoice.js_assign_outstanding_line(receivable_line.id)
+
+ self.assertTrue(receivable_line.matched_debit_ids)
+
+ def test_reconciliation_with_currency(self):
+ #reconciliation on an account having a foreign currency being
+ #the same as the company one
+ account_rcv = self.account_rcv
+ account_rcv.currency_id = self.currency_euro_id
+ aml_obj = self.env['account.move.line'].with_context(
+ check_move_validity=False)
+ general_move1 = self.env['account.move'].create({
+ 'name': 'general1',
+ 'journal_id': self.general_journal.id,
+ })
+ aml_obj.create({
+ 'name': 'debit1',
+ 'account_id': account_rcv.id,
+ 'debit': 11,
+ 'move_id': general_move1.id,
+ })
+ aml_obj.create({
+ 'name': 'credit1',
+ 'account_id': self.account_rsa.id,
+ 'credit': 11,
+ 'move_id': general_move1.id,
+ })
+ general_move1.action_post()
+ general_move2 = self.env['account.move'].create({
+ 'name': 'general2',
+ 'journal_id': self.general_journal.id,
+ })
+ aml_obj.create({
+ 'name': 'credit2',
+ 'account_id': account_rcv.id,
+ 'credit': 10,
+ 'move_id': general_move2.id,
+ })
+ aml_obj.create({
+ 'name': 'debit2',
+ 'account_id': self.account_rsa.id,
+ 'debit': 10,
+ 'move_id': general_move2.id,
+ })
+ general_move2.action_post()
+ general_move3 = self.env['account.move'].create({
+ 'name': 'general3',
+ 'journal_id': self.general_journal.id,
+ })
+ aml_obj.create({
+ 'name': 'credit3',
+ 'account_id': account_rcv.id,
+ 'credit': 1,
+ 'move_id': general_move3.id,
+ })
+ aml_obj.create({
+ 'name': 'debit3',
+ 'account_id': self.account_rsa.id,
+ 'debit': 1,
+ 'move_id': general_move3.id,
+ })
+ general_move3.action_post()
+ to_reconcile = ((general_move1 + general_move2 + general_move3)
+ .mapped('line_ids')
+ .filtered(lambda l: l.account_id.id == account_rcv.id))
+ to_reconcile.reconcile()
+ for aml in to_reconcile:
+ self.assertEqual(aml.amount_residual, 0.0)
+
+ def test_inv_refund_foreign_payment_writeoff_domestic2(self):
+ company = self.company
+ self.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y') + '-07-01',
+ 'rate': 1.0,
+ 'currency_id': self.currency_euro_id,
+ 'company_id': company.id
+ })
+ self.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y') + '-07-01',
+ 'rate': 1.110600, # Don't change this !
+ 'currency_id': self.currency_usd_id,
+ 'company_id': self.company.id
+ })
+ inv1 = self.create_invoice(invoice_amount=800, currency_id=self.currency_usd_id)
+ inv2 = self.create_invoice(move_type="out_refund", invoice_amount=400, currency_id=self.currency_usd_id)
+
+ payment = self.env['account.payment'].create({
+ 'date': time.strftime('%Y') + '-07-15',
+ 'payment_method_id': self.inbound_payment_method.id,
+ 'payment_type': 'inbound',
+ 'partner_type': 'customer',
+ 'partner_id': inv1.partner_id.id,
+ 'amount': 200.00,
+ 'journal_id': self.bank_journal_euro.id,
+ 'company_id': company.id,
+ })
+ payment.action_post()
+
+ inv1_receivable = inv1.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+ inv2_receivable = inv2.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+ pay_receivable = payment.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+
+ move_balance = self.env['account.move'].create({
+ 'partner_id': inv1.partner_id.id,
+ 'date': time.strftime('%Y') + '-07-01',
+ 'journal_id': self.bank_journal_euro.id,
+ 'line_ids': [
+ (0, False, {'credit': 160.16, 'account_id': inv1_receivable.account_id.id, 'name': 'Balance WriteOff'}),
+ (0, False, {'debit': 160.16, 'account_id': self.diff_expense_account.id, 'name': 'Balance WriteOff'}),
+ ]
+ })
+
+ move_balance.action_post()
+ move_balance_receiv = move_balance.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+
+ (inv1_receivable + inv2_receivable + pay_receivable + move_balance_receiv).reconcile()
+
+ self.assertTrue(inv1_receivable.full_reconcile_id.exists())
+ self.assertEqual(inv1_receivable.full_reconcile_id, inv2_receivable.full_reconcile_id)
+ self.assertEqual(inv1_receivable.full_reconcile_id, pay_receivable.full_reconcile_id)
+ self.assertEqual(inv1_receivable.full_reconcile_id, move_balance_receiv.full_reconcile_id)
+
+ self.assertTrue(inv1.payment_state in ('in_payment', 'paid'), "Invoice should be paid")
+ self.assertEqual(inv2.payment_state, 'paid')
+
+ def test_inv_refund_foreign_payment_writeoff_domestic3(self):
+ """
+ Receivable
+ Domestic (Foreign)
+ 592.47 (658.00) | INV 1 > Done in foreign
+ | 202.59 (225.00) INV 2 > Done in foreign
+ | 372.10 (413.25) PAYMENT > Done in domestic (the 413.25 is virtual, non stored)
+ | 17.78 (19.75) WriteOff > Done in domestic (the 19.75 is virtual, non stored)
+ Reconciliation should be full
+ Invoices should be marked as paid
+ """
+ company = self.company
+ self.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y') + '-07-01',
+ 'rate': 1.0,
+ 'currency_id': self.currency_euro_id,
+ 'company_id': company.id
+ })
+ self.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y') + '-07-01',
+ 'rate': 1.110600, # Don't change this !
+ 'currency_id': self.currency_usd_id,
+ 'company_id': company.id
+ })
+ inv1 = self.create_invoice(invoice_amount=658, currency_id=self.currency_usd_id)
+ inv2 = self.create_invoice(move_type="out_refund", invoice_amount=225, currency_id=self.currency_usd_id)
+
+ payment = self.env['account.payment'].create({
+ 'payment_method_id': self.inbound_payment_method.id,
+ 'payment_type': 'inbound',
+ 'partner_type': 'customer',
+ 'partner_id': inv1.partner_id.id,
+ 'amount': 372.10,
+ 'date': time.strftime('%Y') + '-07-01',
+ 'journal_id': self.bank_journal_euro.id,
+ 'company_id': company.id,
+ })
+ payment.action_post()
+
+ inv1_receivable = inv1.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+ inv2_receivable = inv2.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+ pay_receivable = payment.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+
+ move_balance = self.env['account.move'].create({
+ 'partner_id': inv1.partner_id.id,
+ 'date': time.strftime('%Y') + '-07-01',
+ 'journal_id': self.bank_journal_euro.id,
+ 'line_ids': [
+ (0, False, {'credit': 17.78, 'account_id': inv1_receivable.account_id.id, 'name': 'Balance WriteOff'}),
+ (0, False, {'debit': 17.78, 'account_id': self.diff_expense_account.id, 'name': 'Balance WriteOff'}),
+ ]
+ })
+
+ move_balance.action_post()
+ move_balance_receiv = move_balance.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+
+ (inv1_receivable + inv2_receivable + pay_receivable + move_balance_receiv).reconcile()
+
+ self.assertTrue(inv1_receivable.full_reconcile_id.exists())
+ self.assertEqual(inv1_receivable.full_reconcile_id, inv2_receivable.full_reconcile_id)
+ self.assertEqual(inv1_receivable.full_reconcile_id, pay_receivable.full_reconcile_id)
+ self.assertEqual(inv1_receivable.full_reconcile_id, move_balance_receiv.full_reconcile_id)
+
+ self.assertFalse(inv1_receivable.full_reconcile_id.exchange_move_id)
+
+ self.assertTrue(inv1.payment_state in ('in_payment', 'paid'), "Invoice should be paid")
+ self.assertEqual(inv2.payment_state, 'paid')
+
+ def test_inv_refund_foreign_payment_writeoff_domestic4(self):
+ """
+ Receivable
+ Domestic (Foreign)
+ 658.00 (658.00) | INV 1 > Done in foreign
+ | 202.59 (225.00) INV 2 > Done in foreign
+ | 372.10 (413.25) PAYMENT > Done in domestic (the 413.25 is virtual, non stored)
+ | 83.31 (92.52) WriteOff > Done in domestic (the 92.52 is virtual, non stored)
+ Reconciliation should be full
+ Invoices should be marked as paid
+ """
+ company = self.company
+ self.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y') + '-07-01',
+ 'rate': 1.0,
+ 'currency_id': self.currency_euro_id,
+ 'company_id': company.id
+ })
+ self.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y') + '-07-01',
+ 'rate': 1.0, # Don't change this !
+ 'currency_id': self.currency_usd_id,
+ 'company_id': company.id
+ })
+ self.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y') + '-07-15',
+ 'rate': 1.110600, # Don't change this !
+ 'currency_id': self.currency_usd_id,
+ 'company_id': company.id
+ })
+ inv1 = self._create_invoice(invoice_amount=658, currency_id=self.currency_usd_id, date_invoice=time.strftime('%Y') + '-07-01', auto_validate=True)
+ inv2 = self._create_invoice(move_type="out_refund", invoice_amount=225, currency_id=self.currency_usd_id, date_invoice=time.strftime('%Y') + '-07-15', auto_validate=True)
+
+ payment = self.env['account.payment'].create({
+ 'date': time.strftime('%Y') + '-07-15',
+ 'payment_method_id': self.inbound_payment_method.id,
+ 'payment_type': 'inbound',
+ 'partner_type': 'customer',
+ 'partner_id': inv1.partner_id.id,
+ 'amount': 372.10,
+ 'journal_id': self.bank_journal_euro.id,
+ 'company_id': company.id,
+ 'currency_id': self.currency_euro_id,
+ })
+ payment.action_post()
+
+ inv1_receivable = inv1.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+ inv2_receivable = inv2.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+ pay_receivable = payment.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+
+ self.assertEqual(inv1_receivable.balance, 658)
+ self.assertEqual(inv2_receivable.balance, -202.59)
+ self.assertEqual(pay_receivable.balance, -372.1)
+
+ move_balance = self.env['account.move'].create({
+ 'partner_id': inv1.partner_id.id,
+ 'date': time.strftime('%Y') + '-07-15',
+ 'journal_id': self.bank_journal_usd.id,
+ 'line_ids': [
+ (0, False, {'credit': 83.31, 'account_id': inv1_receivable.account_id.id, 'name': 'Balance WriteOff'}),
+ (0, False, {'debit': 83.31, 'account_id': self.diff_expense_account.id, 'name': 'Balance WriteOff'}),
+ ]
+ })
+
+ move_balance.action_post()
+ move_balance_receiv = move_balance.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+
+ (inv1_receivable + inv2_receivable + pay_receivable + move_balance_receiv).reconcile()
+
+ self.assertTrue(inv1_receivable.full_reconcile_id.exists())
+ self.assertEqual(inv1_receivable.full_reconcile_id, inv2_receivable.full_reconcile_id)
+ self.assertEqual(inv1_receivable.full_reconcile_id, pay_receivable.full_reconcile_id)
+ self.assertEqual(inv1_receivable.full_reconcile_id, move_balance_receiv.full_reconcile_id)
+
+ self.assertTrue(inv1.payment_state in ('in_payment', 'paid'), "Invoice should be paid")
+ self.assertEqual(inv2.payment_state, 'paid')
+
+ def test_inv_refund_foreign_payment_writeoff_domestic5(self):
+ """
+ Receivable
+ Domestic (Foreign)
+ 600.00 (600.00) | INV 1 > Done in foreign
+ | 250.00 (250.00) INV 2 > Done in foreign
+ | 314.07 (314.07) PAYMENT > Done in domestic (foreign non stored)
+ | 35.93 (60.93) WriteOff > Done in domestic (foreign non stored). WriteOff is included in payment
+ Reconciliation should be full, without exchange difference
+ Invoices should be marked as paid
+ """
+ company = self.company
+ self.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y') + '-07-01',
+ 'rate': 1.0,
+ 'currency_id': self.currency_euro_id,
+ 'company_id': company.id
+ })
+ self.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y') + '-07-01',
+ 'rate': 1.0, # Don't change this !
+ 'currency_id': self.currency_usd_id,
+ 'company_id': company.id
+ })
+
+ inv1 = self._create_invoice(invoice_amount=600, currency_id=self.currency_usd_id, date_invoice=time.strftime('%Y') + '-07-15', auto_validate=True)
+ inv2 = self._create_invoice(move_type="out_refund", invoice_amount=250, currency_id=self.currency_usd_id, date_invoice=time.strftime('%Y') + '-07-15', auto_validate=True)
+
+ inv1_receivable = inv1.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+ inv2_receivable = inv2.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+
+ self.assertEqual(inv1_receivable.balance, 600.00)
+ self.assertEqual(inv2_receivable.balance, -250)
+
+ # partially pay the invoice with the refund
+ inv1.js_assign_outstanding_line(inv2_receivable.id)
+ self.assertEqual(inv1.amount_residual, 350)
+
+ payment = self.env['account.payment.register']\
+ .with_context(active_model='account.move', active_ids=inv1.ids)\
+ .create({
+ 'payment_date': time.strftime('%Y') + '-07-15',
+ 'amount': 314.07,
+ 'journal_id': self.bank_journal_euro.id,
+ 'currency_id': self.currency_euro_id,
+ 'payment_difference_handling': 'reconcile',
+ 'writeoff_account_id': self.diff_income_account.id,
+ })\
+ ._create_payments()
+
+ payment_receivable = payment.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+ self.assertEqual(payment_receivable.balance, -350)
+
+ self.assertTrue(inv1_receivable.full_reconcile_id.exists())
+ self.assertEqual(inv1_receivable.full_reconcile_id, inv2_receivable.full_reconcile_id)
+ self.assertEqual(inv1_receivable.full_reconcile_id, payment_receivable.full_reconcile_id)
+
+ self.assertFalse(inv1_receivable.full_reconcile_id.exchange_move_id)
+
+ self.assertTrue(inv1.payment_state in ('in_payment', 'paid'), "Invoice should be paid")
+ self.assertEqual(inv2.payment_state, 'paid')
+
+ def test_inv_refund_foreign_payment_writeoff_domestic6(self):
+ """
+ Receivable
+ Domestic (Foreign)
+ 540.25 (600.00) | INV 1 > Done in foreign
+ | 225.10 (250.00) INV 2 > Done in foreign
+ | 315.15 (350.00) PAYMENT > Done in domestic (the 350.00 is virtual, non stored)
+ """
+ company = self.company
+ self.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y') + '-07-01',
+ 'rate': 1.0,
+ 'currency_id': self.currency_euro_id,
+ 'company_id': company.id
+ })
+ self.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y') + '-07-01',
+ 'rate': 1.1106, # Don't change this !
+ 'currency_id': self.currency_usd_id,
+ 'company_id': company.id
+ })
+ inv1 = self._create_invoice(invoice_amount=600, currency_id=self.currency_usd_id, date_invoice=time.strftime('%Y') + '-07-15', auto_validate=True)
+ inv2 = self._create_invoice(move_type="out_refund", invoice_amount=250, currency_id=self.currency_usd_id, date_invoice=time.strftime('%Y') + '-07-15', auto_validate=True)
+
+ inv1_receivable = inv1.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+ inv2_receivable = inv2.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+
+ self.assertEqual(inv1_receivable.balance, 540.25)
+ self.assertEqual(inv2_receivable.balance, -225.10)
+
+ # partially pay the invoice with the refund
+ inv1.js_assign_outstanding_line(inv2_receivable.id)
+ self.assertAlmostEqual(inv1.amount_residual, 350)
+ self.assertAlmostEqual(inv1_receivable.amount_residual, 315.15)
+
+ payment = self.env['account.payment.register']\
+ .with_context(active_model='account.move', active_ids=inv1.ids)\
+ .create({
+ 'payment_date': time.strftime('%Y') + '-07-15',
+ 'amount': 314.07,
+ 'journal_id': self.bank_journal_euro.id,
+ 'currency_id': self.currency_euro_id,
+ 'payment_difference_handling': 'reconcile',
+ 'writeoff_account_id': self.diff_income_account.id,
+ })\
+ ._create_payments()
+
+ payment_receivable = payment.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+
+ self.assertTrue(inv1_receivable.full_reconcile_id.exists())
+ self.assertEqual(inv1_receivable.full_reconcile_id, inv2_receivable.full_reconcile_id)
+ self.assertEqual(inv1_receivable.full_reconcile_id, payment_receivable.full_reconcile_id)
+
+ exchange_rcv = inv1_receivable.full_reconcile_id.exchange_move_id.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+ self.assertEqual(exchange_rcv.amount_currency, 0.01)
+
+ self.assertTrue(inv1.payment_state in ('in_payment', 'paid'), "Invoice should be paid")
+ self.assertEqual(inv2.payment_state, 'paid')
+
+ def test_inv_refund_foreign_payment_writeoff_domestic6bis(self):
+ """
+ Same as domestic6, but only in foreign currencies
+ Obviously, it should lead to the same kind of results
+ Here there is no exchange difference entry though
+ """
+ foreign_0 = self.env['res.currency'].create({
+ 'name': 'foreign0',
+ 'symbol': 'F0'
+ })
+ foreign_1 = self.env['res.currency'].browse(self.currency_usd_id)
+
+ company = self.company
+ self.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y') + '-07-01',
+ 'rate': 1.0,
+ 'currency_id': self.currency_euro_id,
+ 'company_id': company.id
+ })
+
+ self.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y') + '-07-01',
+ 'rate': 1.0,
+ 'currency_id': foreign_0.id,
+ 'company_id': company.id
+ })
+ self.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y') + '-07-01',
+ 'rate': 1.1106, # Don't change this !
+ 'currency_id': foreign_1.id,
+ 'company_id': company.id
+ })
+ inv1 = self._create_invoice(invoice_amount=600, currency_id=foreign_1.id, date_invoice=time.strftime('%Y') + '-07-15', auto_validate=True)
+ inv2 = self._create_invoice(move_type="out_refund", invoice_amount=250, currency_id=foreign_1.id, date_invoice=time.strftime('%Y') + '-07-15', auto_validate=True)
+
+ inv1_receivable = inv1.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+ inv2_receivable = inv2.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+
+ self.assertEqual(inv1_receivable.balance, 540.25)
+ self.assertEqual(inv2_receivable.balance, -225.10)
+
+ # partially pay the invoice with the refund
+ inv1.js_assign_outstanding_line(inv2_receivable.id)
+ self.assertAlmostEqual(inv1.amount_residual, 350)
+ self.assertAlmostEqual(inv1_receivable.amount_residual, 315.15)
+
+ payment = self.env['account.payment.register']\
+ .with_context(active_model='account.move', active_ids=inv1.ids)\
+ .create({
+ 'payment_date': time.strftime('%Y') + '-07-15',
+ 'amount': 314.07,
+ 'journal_id': self.bank_journal_euro.id,
+ 'currency_id': foreign_0.id,
+ 'payment_difference_handling': 'reconcile',
+ 'writeoff_account_id': self.diff_income_account.id,
+ })\
+ ._create_payments()
+
+ payment_receivable = payment.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+
+ self.assertTrue(inv1_receivable.full_reconcile_id.exists())
+ self.assertEqual(inv1_receivable.full_reconcile_id, inv2_receivable.full_reconcile_id)
+ self.assertEqual(inv1_receivable.full_reconcile_id, payment_receivable.full_reconcile_id)
+
+ # Before saas-13.4, there was no exchange difference entry generated because the amount was
+ # wrongly converted in the _amount_residual method at the invoice date like this:
+ # 315.15 * (600.0 / 540.25) = 515.15 * 1.110596946 = 350.004627487 ~= 350.0
+ # Now, the conversion is made using the payment rate using the _convert method and the
+ # encoded currency rate:
+ # 315.15 * 1.1106 = 350.00559 ~= 350.01
+ self.assertTrue(inv1_receivable.full_reconcile_id.exchange_move_id)
+
+ self.assertTrue(inv1.payment_state in ('in_payment', 'paid'), "Invoice should be paid")
+ self.assertEqual(inv2.payment_state, 'paid')
+
+ def test_inv_refund_foreign_payment_writeoff_domestic7(self):
+ """
+ Receivable
+ Domestic (Foreign)
+ 5384.48 (5980.00) | INV 1 > Done in foreign
+ | 5384.43 (5979.95) PAYMENT > Done in domestic (foreign non stored)
+ | 0.05 (0.00) WriteOff > Done in domestic (foreign non stored). WriteOff is included in payment,
+ so, the amount in currency is irrelevant
+ Reconciliation should be full, without exchange difference
+ Invoices should be marked as paid
+ """
+ company = self.company
+ self.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y') + '-07-01',
+ 'rate': 1.0,
+ 'currency_id': self.currency_euro_id,
+ 'company_id': company.id
+ })
+ self.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y') + '-07-01',
+ 'rate': 1.1106, # Don't change this !
+ 'currency_id': self.currency_usd_id,
+ 'company_id': company.id
+ })
+ inv1 = self._create_invoice(invoice_amount=5980, currency_id=self.currency_usd_id, date_invoice=time.strftime('%Y') + '-07-15', auto_validate=True)
+
+ inv1_receivable = inv1.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+
+ self.assertAlmostEqual(inv1_receivable.balance, 5384.48)
+
+ payment = self.env['account.payment.register']\
+ .with_context(active_model='account.move', active_ids=inv1.ids)\
+ .create({
+ 'payment_date': time.strftime('%Y') + '-07-15',
+ 'amount': 5384.43,
+ 'journal_id': self.bank_journal_euro.id,
+ 'currency_id': self.currency_euro_id,
+ 'payment_difference_handling': 'reconcile',
+ 'writeoff_account_id': self.diff_income_account.id,
+ })\
+ ._create_payments()
+
+ payment_receivable = payment.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+
+ self.assertTrue(inv1_receivable.full_reconcile_id.exists())
+ self.assertEqual(inv1_receivable.full_reconcile_id, payment_receivable.full_reconcile_id)
+
+ self.assertFalse(inv1_receivable.full_reconcile_id.exchange_move_id)
+
+ self.assertTrue(inv1.payment_state in ('in_payment', 'paid'), "Invoice should be paid")
+
+ def test_inv_refund_foreign_payment_writeoff_domestic8(self):
+ """
+ Roughly the same as *_domestic7
+ Though it simulates going through the reconciliation widget
+ Because the WriteOff is on a different line than the payment
+ """
+ company = self.company
+ self.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y') + '-07-01',
+ 'rate': 1.0,
+ 'currency_id': self.currency_euro_id,
+ 'company_id': company.id
+ })
+ self.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y') + '-07-01',
+ 'rate': 1.1106, # Don't change this !
+ 'currency_id': self.currency_usd_id,
+ 'company_id': company.id
+ })
+ inv1 = self._create_invoice(invoice_amount=5980, currency_id=self.currency_usd_id, date_invoice=time.strftime('%Y') + '-07-15', auto_validate=True)
+
+ inv1_receivable = inv1.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+
+ self.assertAlmostEqual(inv1_receivable.balance, 5384.48)
+
+ Payment = self.env['account.payment']
+ payment = Payment.create({
+ 'date': time.strftime('%Y') + '-07-15',
+ 'payment_method_id': self.inbound_payment_method.id,
+ 'payment_type': 'inbound',
+ 'partner_type': 'customer',
+ 'partner_id': inv1.partner_id.id,
+ 'amount': 5384.43,
+ 'journal_id': self.bank_journal_euro.id,
+ 'company_id': company.id,
+ 'currency_id': self.currency_euro_id,
+ })
+ payment.action_post()
+ payment_receivable = payment.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+
+ move_balance = self.env['account.move'].create({
+ 'partner_id': inv1.partner_id.id,
+ 'date': time.strftime('%Y') + '-07-15',
+ 'journal_id': self.bank_journal_usd.id,
+ 'line_ids': [
+ (0, False, {'credit': 0.05, 'account_id': inv1_receivable.account_id.id, 'name': 'Balance WriteOff'}),
+ (0, False, {'debit': 0.05, 'account_id': self.diff_expense_account.id, 'name': 'Balance WriteOff'}),
+ ]
+ })
+ move_balance.action_post()
+ move_balance_receiv = move_balance.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+
+ (inv1_receivable + payment_receivable + move_balance_receiv).reconcile()
+
+ self.assertTrue(inv1_receivable.full_reconcile_id.exists())
+ self.assertEqual(inv1_receivable.full_reconcile_id, payment_receivable.full_reconcile_id)
+ self.assertEqual(move_balance_receiv.full_reconcile_id, inv1_receivable.full_reconcile_id)
+
+ exchange_rcv = inv1_receivable.full_reconcile_id.exchange_move_id.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+ self.assertEqual(exchange_rcv.amount_currency, 0.01)
+
+ self.assertTrue(inv1.payment_state in ('in_payment', 'paid'), "Invoice should be paid")
diff --git a/addons/account/tests/test_reconciliation_matching_rules.py b/addons/account/tests/test_reconciliation_matching_rules.py
new file mode 100644
index 00000000..ffe18d63
--- /dev/null
+++ b/addons/account/tests/test_reconciliation_matching_rules.py
@@ -0,0 +1,1061 @@
+# -*- coding: utf-8 -*-
+from freezegun import freeze_time
+
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests.common import Form
+from odoo.tests import tagged
+
+
+@tagged('post_install', '-at_install')
+class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ #################
+ # Company setup #
+ #################
+ cls.currency_data_2 = cls.setup_multi_currency_data({
+ 'name': 'Dark Chocolate Coin',
+ 'symbol': '🍫',
+ 'currency_unit_label': 'Dark Choco',
+ 'currency_subunit_label': 'Dark Cacao Powder',
+ }, rate2016=10.0, rate2017=20.0)
+
+ cls.company = cls.company_data['company']
+
+ cls.account_pay = cls.company_data['default_account_payable']
+ cls.current_assets_account = cls.env['account.account'].search([
+ ('user_type_id', '=', cls.env.ref('account.data_account_type_current_assets').id),
+ ('company_id', '=', cls.company.id)], limit=1)
+
+ cls.bank_journal = cls.env['account.journal'].search([('type', '=', 'bank'), ('company_id', '=', cls.company.id)], limit=1)
+ cls.cash_journal = cls.env['account.journal'].search([('type', '=', 'cash'), ('company_id', '=', cls.company.id)], limit=1)
+
+ cls.tax21 = cls.env['account.tax'].create({
+ 'name': '21%',
+ 'type_tax_use': 'purchase',
+ 'amount': 21,
+ })
+
+ cls.tax12 = cls.env['account.tax'].create({
+ 'name': '12%',
+ 'type_tax_use': 'purchase',
+ 'amount': 12,
+ })
+
+ cls.partner_1 = cls.env['res.partner'].create({'name': 'partner_1', 'company_id': cls.company.id})
+ cls.partner_2 = cls.env['res.partner'].create({'name': 'partner_2', 'company_id': cls.company.id})
+ cls.partner_3 = cls.env['res.partner'].create({'name': 'partner_3', 'company_id': cls.company.id})
+
+ ###############
+ # Rules setup #
+ ###############
+ cls.rule_1 = cls.env['account.reconcile.model'].create({
+ 'name': 'Invoices Matching Rule',
+ 'sequence': '1',
+ 'rule_type': 'invoice_matching',
+ 'auto_reconcile': False,
+ 'match_nature': 'both',
+ 'match_same_currency': True,
+ 'match_total_amount': True,
+ 'match_total_amount_param': 100,
+ 'match_partner': True,
+ 'match_partner_ids': [(6, 0, (cls.partner_1 + cls.partner_2 + cls.partner_3).ids)],
+ 'company_id': cls.company.id,
+ 'line_ids': [(0, 0, {'account_id': cls.current_assets_account.id})],
+ })
+ cls.rule_2 = cls.env['account.reconcile.model'].create({
+ 'name': 'write-off model',
+ 'rule_type': 'writeoff_suggestion',
+ 'match_partner': True,
+ 'match_partner_ids': [],
+ 'line_ids': [(0, 0, {'account_id': cls.current_assets_account.id})],
+ })
+
+ ##################
+ # Invoices setup #
+ ##################
+ cls.invoice_line_1 = cls._create_invoice_line(100, cls.partner_1, 'out_invoice')
+ cls.invoice_line_2 = cls._create_invoice_line(200, cls.partner_1, 'out_invoice')
+ cls.invoice_line_3 = cls._create_invoice_line(300, cls.partner_1, 'in_refund', name="RBILL/2019/09/0013")
+ cls.invoice_line_4 = cls._create_invoice_line(1000, cls.partner_2, 'in_invoice')
+ cls.invoice_line_5 = cls._create_invoice_line(600, cls.partner_3, 'out_invoice')
+ cls.invoice_line_6 = cls._create_invoice_line(600, cls.partner_3, 'out_invoice', ref="RF12 3456")
+ cls.invoice_line_7 = cls._create_invoice_line(200, cls.partner_3, 'out_invoice', pay_reference="RF12 3456")
+
+ ####################
+ # Statements setup #
+ ####################
+ # TODO : account_number, partner_name, transaction_type, narration
+ invoice_number = cls.invoice_line_1.move_id.name
+ cls.bank_st, cls.bank_st_2, cls.cash_st = cls.env['account.bank.statement'].create([
+ {
+ 'name': 'test bank journal',
+ 'journal_id': cls.bank_journal.id,
+ 'line_ids': [
+ (0, 0, {
+ 'date': '2020-01-01',
+ 'payment_ref': 'invoice %s-%s-%s' % tuple(invoice_number.split('/')[1:]),
+ 'partner_id': cls.partner_1.id,
+ 'amount': 100,
+ 'sequence': 1,
+ }),
+ (0, 0, {
+ 'date': '2020-01-01',
+ 'payment_ref': 'xxxxx',
+ 'partner_id': cls.partner_1.id,
+ 'amount': 600,
+ 'sequence': 2,
+ }),
+ ],
+ }, {
+ 'name': 'second test bank journal',
+ 'journal_id': cls.bank_journal.id,
+ 'line_ids': [
+ (0, 0, {
+ 'date': '2020-01-01',
+ 'payment_ref': 'nawak',
+ 'narration': 'Communication: RF12 3456',
+ 'partner_id': cls.partner_3.id,
+ 'amount': 600,
+ 'sequence': 1,
+ }),
+ (0, 0, {
+ 'date': '2020-01-01',
+ 'payment_ref': 'RF12 3456',
+ 'partner_id': cls.partner_3.id,
+ 'amount': 600,
+ 'sequence': 2,
+ }),
+ (0, 0, {
+ 'date': '2020-01-01',
+ 'payment_ref': 'baaaaah',
+ 'ref': 'RF12 3456',
+ 'partner_id': cls.partner_3.id,
+ 'amount': 600,
+ 'sequence': 2,
+ }),
+ ],
+ }, {
+ 'name': 'test cash journal',
+ 'journal_id': cls.cash_journal.id,
+ 'line_ids': [
+ (0, 0, {
+ 'date': '2020-01-01',
+ 'payment_ref': 'yyyyy',
+ 'partner_id': cls.partner_2.id,
+ 'amount': -1000,
+ 'sequence': 1,
+ }),
+ ],
+ }
+ ])
+
+ cls.bank_line_1, cls.bank_line_2 = cls.bank_st.line_ids
+ cls.bank_line_3, cls.bank_line_4, cls.bank_line_5 = cls.bank_st_2.line_ids
+ cls.cash_line_1 = cls.cash_st.line_ids
+ cls._post_statements(cls)
+
+ @classmethod
+ def _create_invoice_line(cls, amount, partner, type, currency=None, pay_reference=None, ref=None, name=None):
+ ''' Create an invoice on the fly.'''
+ invoice_form = Form(cls.env['account.move'].with_context(default_move_type=type, default_invoice_date='2019-09-01', default_date='2019-09-01'))
+ invoice_form.partner_id = partner
+ if currency:
+ invoice_form.currency_id = currency
+ if pay_reference:
+ invoice_form.payment_reference = pay_reference
+ if ref:
+ invoice_form.ref = ref
+ if name:
+ invoice_form.name = name
+ with invoice_form.invoice_line_ids.new() as invoice_line_form:
+ invoice_line_form.name = 'xxxx'
+ invoice_line_form.quantity = 1
+ invoice_line_form.price_unit = amount
+ invoice_line_form.tax_ids.clear()
+ invoice = invoice_form.save()
+ invoice.action_post()
+ lines = invoice.line_ids
+ return lines.filtered(lambda l: l.account_id.user_type_id.type in ('receivable', 'payable'))
+
+ def _post_statements(self):
+ self.bank_st.balance_end_real = self.bank_st.balance_end
+ self.bank_st_2.balance_end_real = self.bank_st_2.balance_end
+ self.cash_st.balance_end_real = self.cash_st.balance_end
+ (self.bank_st + self.bank_st_2 + self.cash_st).button_post()
+
+ @freeze_time('2020-01-01')
+ def _check_statement_matching(self, rules, expected_values, statements=None):
+ if statements is None:
+ statements = self.bank_st + self.cash_st
+ statement_lines = statements.mapped('line_ids').sorted()
+ matching_values = rules._apply_rules(statement_lines, None)
+
+ for st_line_id, values in matching_values.items():
+ values.pop('reconciled_lines', None)
+ values.pop('write_off_vals', None)
+ self.assertDictEqual(values, expected_values[st_line_id])
+
+ def test_matching_fields(self):
+ # Check without restriction.
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id},
+ self.bank_line_2.id: {'aml_ids': [
+ self.invoice_line_2.id,
+ self.invoice_line_3.id,
+ self.invoice_line_1.id,
+ ], 'model': self.rule_1,
+ 'partner': self.bank_line_2.partner_id},
+ self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id},
+ })
+
+ def test_matching_fields_match_text_location(self):
+ self.rule_1.match_text_location_label = True
+ self.rule_1.match_text_location_reference = False
+ self.rule_1.match_text_location_note = False
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_3.id: {'aml_ids': [self.invoice_line_5.id], 'model': self.rule_1, 'partner': self.bank_line_3.partner_id},
+ self.bank_line_4.id: {'aml_ids': [self.invoice_line_7.id], 'model': self.rule_1, 'partner': self.bank_line_4.partner_id},
+ self.bank_line_5.id: {'aml_ids': [self.invoice_line_6.id], 'model': self.rule_1, 'partner': self.bank_line_5.partner_id},
+ }, statements=self.bank_st_2)
+
+ self.rule_1.match_text_location_label = True
+ self.rule_1.match_text_location_reference = False
+ self.rule_1.match_text_location_note = True
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_3.id: {'aml_ids': [self.invoice_line_6.id], 'model': self.rule_1, 'partner': self.bank_line_3.partner_id},
+ self.bank_line_4.id: {'aml_ids': [self.invoice_line_7.id], 'model': self.rule_1, 'partner': self.bank_line_4.partner_id},
+ self.bank_line_5.id: {'aml_ids': [self.invoice_line_5.id], 'model': self.rule_1, 'partner': self.bank_line_5.partner_id},
+ }, statements=self.bank_st_2)
+
+ self.rule_1.match_text_location_label = True
+ self.rule_1.match_text_location_reference = True
+ self.rule_1.match_text_location_note = False
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_3.id: {'aml_ids': [self.invoice_line_5.id], 'model': self.rule_1, 'partner': self.bank_line_3.partner_id},
+ self.bank_line_4.id: {'aml_ids': [self.invoice_line_7.id], 'model': self.rule_1, 'partner': self.bank_line_4.partner_id},
+ self.bank_line_5.id: {'aml_ids': [self.invoice_line_7.id], 'model': self.rule_1, 'partner': self.bank_line_5.partner_id},
+ }, statements=self.bank_st_2)
+
+ self.rule_1.match_text_location_label = True
+ self.rule_1.match_text_location_reference = True
+ self.rule_1.match_text_location_note = True
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_3.id: {'aml_ids': [self.invoice_line_6.id], 'model': self.rule_1, 'partner': self.bank_line_3.partner_id},
+ self.bank_line_4.id: {'aml_ids': [self.invoice_line_7.id], 'model': self.rule_1, 'partner': self.bank_line_4.partner_id},
+ self.bank_line_5.id: {'aml_ids': [self.invoice_line_7.id], 'model': self.rule_1, 'partner': self.bank_line_5.partner_id},
+ }, statements=self.bank_st_2)
+
+ self.rule_1.match_text_location_label = False
+ self.rule_1.match_text_location_reference = False
+ self.rule_1.match_text_location_note = False
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_3.id: {'aml_ids': [self.invoice_line_5.id], 'model': self.rule_1, 'partner': self.bank_line_3.partner_id},
+ self.bank_line_4.id: {'aml_ids': [self.invoice_line_5.id], 'model': self.rule_1, 'partner': self.bank_line_4.partner_id},
+ self.bank_line_5.id: {'aml_ids': [self.invoice_line_6.id], 'model': self.rule_1, 'partner': self.bank_line_5.partner_id},
+ }, statements=self.bank_st_2)
+
+ def test_matching_fields_match_journal_ids(self):
+ self.rule_1.match_journal_ids |= self.cash_st.journal_id
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_1.id: {'aml_ids': []},
+ self.bank_line_2.id: {'aml_ids': []},
+ self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id},
+ })
+ self.rule_1.match_journal_ids |= self.bank_st.journal_id + self.cash_st.journal_id
+
+ def test_matching_fields_match_nature(self):
+ self.rule_1.match_nature = 'amount_received'
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id},
+ self.bank_line_2.id: {'aml_ids': [
+ self.invoice_line_2.id,
+ self.invoice_line_3.id,
+ self.invoice_line_1.id,
+ ], 'model': self.rule_1, 'partner': self.bank_line_2.partner_id},
+ self.cash_line_1.id: {'aml_ids': []},
+ })
+ self.rule_1.match_nature = 'amount_paid'
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_1.id: {'aml_ids': []},
+ self.bank_line_2.id: {'aml_ids': []},
+ self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id},
+ })
+ self.rule_1.match_nature = 'both'
+
+ def test_matching_fields_match_amount(self):
+ self.rule_1.match_amount = 'lower'
+ self.rule_1.match_amount_max = 150
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id},
+ self.bank_line_2.id: {'aml_ids': []},
+ self.cash_line_1.id: {'aml_ids': []},
+ })
+ self.rule_1.match_amount = 'greater'
+ self.rule_1.match_amount_min = 200
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_1.id: {'aml_ids': []},
+ self.bank_line_2.id: {'aml_ids': [
+ self.invoice_line_1.id,
+ self.invoice_line_2.id,
+ self.invoice_line_3.id,
+ ], 'model': self.rule_1, 'partner': self.bank_line_2.partner_id},
+ self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id},
+ })
+ self.rule_1.match_amount = 'between'
+ self.rule_1.match_amount_min = 200
+ self.rule_1.match_amount_max = 800
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_1.id: {'aml_ids': []},
+ self.bank_line_2.id: {'aml_ids': [
+ self.invoice_line_1.id,
+ self.invoice_line_2.id,
+ self.invoice_line_3.id,
+ ], 'model': self.rule_1, 'partner': self.bank_line_2.partner_id},
+ self.cash_line_1.id: {'aml_ids': []},
+ })
+ self.rule_1.match_amount = False
+
+ def test_matching_fields_match_label(self):
+ self.rule_1.match_label = 'contains'
+ self.rule_1.match_label_param = 'yyyyy'
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_1.id: {'aml_ids': []},
+ self.bank_line_2.id: {'aml_ids': []},
+ self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id},
+ })
+ self.rule_1.match_label = 'not_contains'
+ self.rule_1.match_label_param = 'xxxxx'
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id},
+ self.bank_line_2.id: {'aml_ids': []},
+ self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id},
+ })
+ self.rule_1.match_label = 'match_regex'
+ self.rule_1.match_label_param = 'xxxxx|yyyyy'
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_1.id: {'aml_ids': []},
+ self.bank_line_2.id: {'aml_ids': [
+ self.invoice_line_1.id,
+ self.invoice_line_2.id,
+ self.invoice_line_3.id,
+ ], 'model': self.rule_1, 'partner': self.bank_line_2.partner_id},
+ self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id},
+ })
+ self.rule_1.match_label = False
+
+ def test_matching_fields_match_total_amount(self):
+ # Check match_total_amount: line amount >= total residual amount.
+ self.rule_1.match_total_amount_param = 90.0
+ self.bank_line_1.amount += 10
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'status': 'write_off', 'partner': self.bank_line_1.partner_id},
+ self.bank_line_2.id: {'aml_ids': [
+ self.invoice_line_2.id,
+ self.invoice_line_3.id,
+ self.invoice_line_1.id,
+ ], 'model': self.rule_1, 'partner': self.bank_line_2.partner_id},
+ self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id},
+ })
+ self.rule_1.match_total_amount_param = 100.0
+ self.bank_line_1.amount -= 10
+
+ # Check match_total_amount: line amount <= total residual amount.
+ self.rule_1.match_total_amount_param = 90.0
+ self.bank_line_1.amount -= 10
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'status': 'write_off', 'partner': self.bank_line_1.partner_id},
+ self.bank_line_2.id: {'aml_ids': [
+ self.invoice_line_2.id,
+ self.invoice_line_3.id,
+ self.invoice_line_1.id,
+ ], 'model': self.rule_1, 'partner': self.bank_line_2.partner_id},
+ self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id},
+ })
+ self.rule_1.match_total_amount_param = 100.0
+ self.bank_line_1.amount += 10
+
+ # Check match_total_amount: line amount >= total residual amount, match_total_amount_param just not matched.
+ self.rule_1.match_total_amount_param = 90.0
+ self.bank_line_1.amount += 10.01
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_1.id: {'aml_ids': []},
+ self.bank_line_2.id: {'aml_ids': [
+ self.invoice_line_1.id,
+ self.invoice_line_2.id,
+ self.invoice_line_3.id,
+ ], 'model': self.rule_1, 'partner': self.bank_line_2.partner_id},
+ self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id},
+ })
+ self.rule_1.match_total_amount_param = 100.0
+ self.bank_line_1.amount -= 10.01
+
+ # Check match_total_amount: line amount <= total residual amount, match_total_amount_param just not matched.
+ self.rule_1.match_total_amount_param = 90.0
+ self.bank_line_1.amount -= 10.01
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_1.id: {'aml_ids': []},
+ self.bank_line_2.id: {'aml_ids': [
+ self.invoice_line_1.id,
+ self.invoice_line_2.id,
+ self.invoice_line_3.id,
+ ], 'model': self.rule_1, 'partner': self.bank_line_2.partner_id},
+ self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id},
+ })
+ self.rule_1.match_total_amount_param = 100.0
+ self.bank_line_1.amount += 10.01
+
+ def test_matching_fields_match_partner_category_ids(self):
+ test_category = self.env['res.partner.category'].create({'name': 'Consulting Services'})
+ self.partner_2.category_id = test_category
+ self.rule_1.match_partner_category_ids |= test_category
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_1.id: {'aml_ids': []},
+ self.bank_line_2.id: {'aml_ids': []},
+ self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id},
+ })
+ self.rule_1.match_partner_category_ids = False
+
+ def test_mixin_rules(self):
+ ''' Test usage of rules together.'''
+ # rule_1 is used before rule_2.
+ self.rule_1.sequence = 1
+ self.rule_2.sequence = 2
+
+ self._check_statement_matching(self.rule_1 + self.rule_2, {
+ self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id},
+ self.bank_line_2.id: {'aml_ids': [
+ self.invoice_line_2.id,
+ self.invoice_line_3.id,
+ self.invoice_line_1.id,
+ ], 'model': self.rule_1, 'partner': self.bank_line_2.partner_id},
+ self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id},
+ })
+
+ # rule_2 is used before rule_1.
+ self.rule_1.sequence = 2
+ self.rule_2.sequence = 1
+
+ self._check_statement_matching(self.rule_1 + self.rule_2, {
+ self.bank_line_1.id: {'aml_ids': [], 'model': self.rule_2, 'status': 'write_off', 'partner': self.bank_line_1.partner_id},
+ self.bank_line_2.id: {'aml_ids': [], 'model': self.rule_2, 'status': 'write_off', 'partner': self.bank_line_2.partner_id},
+ self.cash_line_1.id: {'aml_ids': [], 'model': self.rule_2, 'status': 'write_off', 'partner': self.cash_line_1.partner_id},
+ })
+
+ # rule_2 is used before rule_1 but only on partner_1.
+ self.rule_2.match_partner_ids |= self.partner_1
+
+ self._check_statement_matching(self.rule_1 + self.rule_2, {
+ self.bank_line_1.id: {'aml_ids': [], 'model': self.rule_2, 'status': 'write_off', 'partner': self.bank_line_1.partner_id},
+ self.bank_line_2.id: {'aml_ids': [], 'model': self.rule_2, 'status': 'write_off', 'partner': self.bank_line_2.partner_id},
+ self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id},
+ })
+
+ def test_auto_reconcile(self):
+ ''' Test auto reconciliation.'''
+ self.bank_line_1.amount += 5
+
+ self.rule_1.sequence = 2
+ self.rule_1.auto_reconcile = True
+ self.rule_1.match_total_amount_param = 90
+ self.rule_2.sequence = 1
+ self.rule_2.match_partner_ids |= self.partner_2
+ self.rule_2.auto_reconcile = True
+
+ self._check_statement_matching(self.rule_1 + self.rule_2, {
+ self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'status': 'reconciled', 'partner': self.bank_line_1.partner_id},
+ self.bank_line_2.id: {'aml_ids': []},
+ self.cash_line_1.id: {'aml_ids': [], 'model': self.rule_2, 'status': 'reconciled', 'partner': self.cash_line_1.partner_id},
+ })
+
+ # Check first line has been well reconciled.
+ self.assertRecordValues(self.bank_line_1.line_ids, [
+ {'partner_id': self.partner_1.id, 'debit': 105.0, 'credit': 0.0},
+ {'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 5.0},
+ {'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 100.0},
+ ])
+
+ # Check second line has been well reconciled.
+ self.assertRecordValues(self.cash_line_1.line_ids, [
+ {'partner_id': self.partner_2.id, 'debit': 0.0, 'credit': 1000.0},
+ {'partner_id': self.partner_2.id, 'debit': 1000.0, 'credit': 0.0},
+ ])
+
+ def test_larger_invoice_auto_reconcile(self):
+ ''' Test auto reconciliation with an invoice with larger amount than the
+ statement line's, for rules without write-offs.'''
+ self.bank_line_1.amount = 40
+ self.invoice_line_1.move_id.payment_reference = self.bank_line_1.payment_ref
+
+ self.rule_1.sequence = 2
+ self.rule_1.auto_reconcile = True
+ self.rule_1.line_ids = [(5, 0, 0)]
+
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'status': 'reconciled', 'partner': self.bank_line_1.partner_id},
+ self.bank_line_2.id: {'aml_ids': []},
+ }, statements=self.bank_st)
+
+ # Check first line has been well reconciled.
+ self.assertRecordValues(self.bank_line_1.line_ids, [
+ {'partner_id': self.partner_1.id, 'debit': 40.0, 'credit': 0.0},
+ {'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 40.0},
+ ])
+
+ self.assertEqual(self.invoice_line_1.amount_residual, 60.0, "The invoice should have been partially reconciled")
+
+ def test_auto_reconcile_with_duplicate_match(self):
+ """ If multiple bank statement lines match with the same invoice, ensure the
+ correct line is auto-validated and no crashing happens.
+ """
+
+ # Only the invoice defined in this test should have this partner
+ partner = self.env['res.partner'].create({'name': "The Only One"})
+ invoice_line = self._create_invoice_line(
+ 2000, partner, 'out_invoice', ref="REF 7788")
+
+ # Enable auto-validation and don't restrict the partners that can be matched on
+ # so our newly created partner can be matched.
+ self.rule_1.write({
+ 'auto_reconcile': True,
+ 'match_partner_ids': [(5, 0, 0)],
+ })
+ self.rule_1.match_partner_ids = []
+
+ # This line has a matching payment reference and the exact amount of the
+ # invoice. As a result it should auto-validate.
+ self.bank_line_1.amount = 2000
+ self.bank_line_1.partner_id = partner
+ self.bank_line_1.payment_ref = "REF 7788"
+
+ # This line doesn't have a matching amount or reference, but it does have a
+ # matching partner.
+ self.bank_line_2.amount = 1800
+ self.bank_line_2.partner_id = partner
+ self.bank_line_2.payment_ref = "something"
+
+ # Verify the auto-validation happens with the first line, and no exceptions.
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_1.id: {
+ 'aml_ids': [invoice_line.id],
+ 'model': self.rule_1,
+ 'status': 'reconciled',
+ 'partner': self.bank_line_1.partner_id
+ },
+ self.bank_line_2.id: {
+ 'aml_ids': []
+ },
+ }, statements=self.bank_st)
+
+ def test_auto_reconcile_with_tax(self):
+ ''' Test auto reconciliation with a tax amount included in the bank statement line'''
+ self.rule_1.write({
+ 'auto_reconcile': True,
+ 'rule_type': 'writeoff_suggestion',
+ 'line_ids': [(1, self.rule_1.line_ids.id, {
+ 'amount': 50,
+ 'force_tax_included': True,
+ 'tax_ids': [(6, 0, self.tax21.ids)],
+ }), (0, 0, {
+ 'amount': 100,
+ 'force_tax_included': False,
+ 'tax_ids': [(6, 0, self.tax12.ids)],
+ 'account_id': self.current_assets_account.id,
+ })]
+ })
+
+ self.bank_line_1.amount = -121
+
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_1.id: {'aml_ids': [], 'model': self.rule_1, 'status': 'reconciled', 'partner': self.bank_line_1.partner_id},
+ self.bank_line_2.id: {'aml_ids': [], 'model': self.rule_1, 'status': 'reconciled', 'partner': self.bank_line_2.partner_id},
+ }, statements=self.bank_st)
+
+ # Check first line has been well reconciled.
+ self.assertRecordValues(self.bank_line_1.line_ids, [
+ {'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 121.0, 'tax_ids': [], 'tax_line_id': False},
+ {'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 7.26, 'tax_ids': [], 'tax_line_id': False},
+ {'partner_id': self.partner_1.id, 'debit': 50.0, 'credit': 0.0, 'tax_ids': [self.tax21.id], 'tax_line_id': False},
+ {'partner_id': self.partner_1.id, 'debit': 10.5, 'credit': 0.0, 'tax_ids': [], 'tax_line_id': self.tax21.id},
+ {'partner_id': self.partner_1.id, 'debit': 60.5, 'credit': 0.0, 'tax_ids': [self.tax12.id], 'tax_line_id': False},
+ {'partner_id': self.partner_1.id, 'debit': 7.26, 'credit': 0.0, 'tax_ids': [], 'tax_line_id': self.tax12.id},
+ ])
+
+ def test_reverted_move_matching(self):
+ partner = self.partner_1
+ AccountMove = self.env['account.move']
+ move = AccountMove.create({
+ 'journal_id': self.bank_journal.id,
+ 'line_ids': [
+ (0, 0, {
+ 'account_id': self.account_pay.id,
+ 'partner_id': partner.id,
+ 'name': 'One of these days',
+ 'debit': 10,
+ }),
+ (0, 0, {
+ 'account_id': self.bank_journal.payment_credit_account_id.id,
+ 'partner_id': partner.id,
+ 'name': 'I\'m gonna cut you into little pieces',
+ 'credit': 10,
+ })
+ ],
+ })
+
+ payment_bnk_line = move.line_ids.filtered(lambda l: l.account_id == self.bank_journal.payment_credit_account_id)
+
+ move.action_post()
+ move_reversed = move._reverse_moves()
+ self.assertTrue(move_reversed.exists())
+
+ self.bank_line_1.write({
+ 'payment_ref': '8',
+ 'partner_id': partner.id,
+ 'amount': -10,
+ })
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_1.id: {'aml_ids': [payment_bnk_line.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id},
+ self.bank_line_2.id: {'aml_ids': []},
+ }, statements=self.bank_st)
+
+ def test_match_different_currencies(self):
+ partner = self.env['res.partner'].create({'name': 'Bernard Gagnant'})
+ self.rule_1.write({'match_partner_ids': [(6, 0, partner.ids)], 'match_same_currency': False})
+
+ currency_inv = self.env.ref('base.EUR')
+ currency_statement = self.env.ref('base.JPY')
+
+ currency_statement.active = True
+
+ invoice_line = self._create_invoice_line(100, partner, 'out_invoice', currency=currency_inv)
+
+ self.bank_line_1.write({'partner_id': partner.id, 'foreign_currency_id': currency_statement.id, 'amount_currency': 100, 'payment_ref': 'test'})
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_1.id: {'aml_ids': invoice_line.ids, 'model': self.rule_1, 'partner': self.bank_line_1.partner_id},
+ self.bank_line_2.id: {'aml_ids': []},
+ }, statements=self.bank_st)
+
+ def test_invoice_matching_rule_no_partner(self):
+ """ Tests that a statement line without any partner can be matched to the
+ right invoice if they have the same payment reference.
+ """
+ self.invoice_line_1.move_id.write({'payment_reference': 'Tournicoti66'})
+
+ self.bank_line_1.write({
+ 'payment_ref': 'Tournicoti66',
+ 'partner_id': None,
+ 'amount': 95,
+ })
+
+ self.rule_1.write({
+ 'line_ids': [(5, 0, 0)],
+ 'match_partner': False,
+ 'match_label': 'contains',
+ 'match_label_param': 'Tournicoti', # So that we only match what we want to test
+ })
+
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id},
+ self.bank_line_2.id: {'aml_ids': []},
+ }, self.bank_st)
+
+ def test_inv_matching_rule_auto_rec_no_partner_with_writeoff(self):
+ self.invoice_line_1.move_id.write({'payment_reference': 'doudlidou355'})
+
+ self.bank_line_1.write({
+ 'payment_ref': 'doudlidou355',
+ 'partner_id': None,
+ 'amount': 95,
+ })
+
+ self.rule_1.write({
+ 'match_partner': False,
+ 'match_label': 'contains',
+ 'match_label_param': 'doudlidou', # So that we only match what we want to test
+ 'match_total_amount_param': 90,
+ 'auto_reconcile': True,
+ })
+
+ # Check bank reconciliation
+
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id, 'status': 'reconciled'},
+ self.bank_line_2.id: {'aml_ids': []},
+ }, self.bank_st)
+
+ # Check invoice line has been fully reconciled, with a write-off.
+ self.assertRecordValues(self.bank_line_1.line_ids, [
+ {'partner_id': self.partner_1.id, 'debit': 95.0, 'credit': 0.0, 'account_id': self.bank_journal.default_account_id.id, 'reconciled': False},
+ {'partner_id': self.partner_1.id, 'debit': 5.0, 'credit': 0.0, 'account_id': self.current_assets_account.id, 'reconciled': False},
+ {'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 100.0, 'account_id': self.invoice_line_1.account_id.id, 'reconciled': True},
+ ])
+
+ self.assertEqual(self.invoice_line_1.amount_residual, 0.0, "The invoice should have been fully reconciled")
+
+ def test_partner_mapping_rule(self):
+ self.bank_line_1.write({'partner_id': None, 'payment_ref': 'toto42', 'narration': None})
+ self.bank_line_2.write({'partner_id': None})
+
+ # Do the test for both rule 1 and 2, so that we check invoice matching and write-off rules
+ for rule in (self.rule_1 + self.rule_2):
+
+ # To cope for minor differences in rule results
+ matching_amls = rule.rule_type == 'invoice_matching' and self.invoice_line_1.ids or []
+ result_status = rule.rule_type == 'writeoff_suggestion' and {'status': 'write_off'} or {}
+
+ match_result = {**result_status, 'aml_ids': matching_amls, 'model': rule, 'partner': self.partner_1}
+ no_match_result = {'aml_ids': []}
+
+ # Without mapping, there should be no match
+ self._check_statement_matching(rule, {
+ self.bank_line_1.id: no_match_result,
+ self.bank_line_2.id: no_match_result,
+ }, self.bank_st)
+
+ # We add some mapping for payment reference to rule_1
+ rule.write({
+ 'partner_mapping_line_ids': [(0, 0, {
+ 'partner_id': self.partner_1.id,
+ 'payment_ref_regex': 'toto.*',
+ })]
+ })
+
+ # bank_line_1 should now match
+ self._check_statement_matching(rule, {
+ self.bank_line_1.id: match_result,
+ self.bank_line_2.id: no_match_result,
+ }, self.bank_st)
+
+ # If we now add a narration regex to the same mapping line, nothing should match
+ rule.partner_mapping_line_ids.write({'narration_regex': ".*coincoin"})
+ self.bank_line_1.write({'narration': None}) # Reset from possible previous iteration
+
+ self._check_statement_matching(rule, {
+ self.bank_line_1.id: no_match_result,
+ self.bank_line_2.id: no_match_result,
+ }, self.bank_st)
+
+ # If we set the narration so that it matches the new mapping criterium, line_1 matches
+ self.bank_line_1.write({'narration': "42coincoin"})
+
+ self._check_statement_matching(rule, {
+ self.bank_line_1.id: match_result,
+ self.bank_line_2.id: no_match_result,
+ }, self.bank_st)
+
+ def test_partner_name_in_communication(self):
+ self.invoice_line_1.partner_id.write({'name': "Archibald Haddock"})
+ self.bank_line_1.write({'partner_id': None, 'payment_ref': '1234//HADDOCK-Archibald'})
+ self.bank_line_2.write({'partner_id': None})
+ self.rule_1.write({'match_partner': False})
+
+ # bank_line_1 should match, as its communication contains the invoice's partner name
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id},
+ self.bank_line_2.id: {'aml_ids': []},
+ }, self.bank_st)
+
+ def test_partner_name_with_regexp_chars(self):
+ self.invoice_line_1.partner_id.write({'name': "Archibald + Haddock"})
+ self.bank_line_1.write({'partner_id': None, 'payment_ref': '1234//HADDOCK+Archibald'})
+ self.bank_line_2.write({'partner_id': None})
+ self.rule_1.write({'match_partner': False})
+
+ # The query should still work
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id},
+ self.bank_line_2.id: {'aml_ids': []},
+ }, self.bank_st)
+
+ def test_match_multi_currencies(self):
+ ''' Ensure the matching of candidates is made using the right statement line currency.
+
+ In this test, the value of the statement line is 100 USD = 300 GOL = 900 DAR and we want to match two journal
+ items of:
+ - 100 USD = 200 GOL (= 600 DAR from the statement line point of view)
+ - 14 USD = 280 DAR
+
+ Both journal items should be suggested to the user because they represents 98% of the statement line amount
+ (DAR).
+ '''
+ partner = self.env['res.partner'].create({'name': 'Bernard Perdant'})
+
+ journal = self.env['account.journal'].create({
+ 'name': 'test_match_multi_currencies',
+ 'code': 'xxxx',
+ 'type': 'bank',
+ 'currency_id': self.currency_data['currency'].id,
+ })
+
+ matching_rule = self.env['account.reconcile.model'].create({
+ 'name': 'test_match_multi_currencies',
+ 'rule_type': 'invoice_matching',
+ 'match_partner': True,
+ 'match_partner_ids': [(6, 0, partner.ids)],
+ 'match_total_amount': True,
+ 'match_total_amount_param': 95.0,
+ 'match_same_currency': False,
+ 'company_id': self.company_data['company'].id,
+ 'past_months_limit': False,
+ })
+
+ statement = self.env['account.bank.statement'].create({
+ 'name': 'test_match_multi_currencies',
+ 'journal_id': journal.id,
+ 'line_ids': [
+ (0, 0, {
+ 'journal_id': journal.id,
+ 'date': '2016-01-01',
+ 'payment_ref': 'line',
+ 'partner_id': partner.id,
+ 'foreign_currency_id': self.currency_data_2['currency'].id,
+ 'amount': 300.0, # Rate is 3 GOL = 1 USD in 2016.
+ 'amount_currency': 900.0, # Rate is 10 DAR = 1 USD in 2016 but the rate used by the bank is 9:1.
+ }),
+ ],
+ })
+ statement_line = statement.line_ids
+
+ statement.button_post()
+
+ move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2017-01-01',
+ 'journal_id': self.company_data['default_journal_misc'].id,
+ 'line_ids': [
+ # Rate is 2 GOL = 1 USD in 2017.
+ # The statement line will consider this line equivalent to 600 DAR.
+ (0, 0, {
+ 'account_id': self.company_data['default_account_receivable'].id,
+ 'partner_id': partner.id,
+ 'currency_id': self.currency_data['currency'].id,
+ 'debit': 100.0,
+ 'credit': 0.0,
+ 'amount_currency': 200.0,
+ }),
+ # Rate is 20 GOL = 1 USD in 2017.
+ (0, 0, {
+ 'account_id': self.company_data['default_account_receivable'].id,
+ 'partner_id': partner.id,
+ 'currency_id': self.currency_data_2['currency'].id,
+ 'debit': 14.0,
+ 'credit': 0.0,
+ 'amount_currency': 280.0,
+ }),
+ # Line to balance the journal entry:
+ (0, 0, {
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'debit': 0.0,
+ 'credit': 114.0,
+ }),
+ ],
+ })
+ move.action_post()
+
+ move_line_1 = move.line_ids.filtered(lambda line: line.debit == 100.0)
+ move_line_2 = move.line_ids.filtered(lambda line: line.debit == 14.0)
+
+ self._check_statement_matching(matching_rule, {
+ statement_line.id: {'aml_ids': (move_line_1 + move_line_2).ids, 'model': matching_rule, 'partner': statement_line.partner_id}
+ }, statements=statement)
+
+ @freeze_time('2020-01-01')
+ def test_inv_matching_with_write_off(self):
+ self.rule_1.match_total_amount_param = 90
+ self.bank_st.line_ids[1].unlink() # We don't need this one here
+ statement_line = self.bank_st.line_ids[0]
+ statement_line.write({
+ 'payment_ref': self.invoice_line_1.move_id.payment_reference,
+ 'amount': 90,
+ })
+
+ # Test the invoice-matching part
+ self._check_statement_matching(self.rule_1, {
+ statement_line.id: {'aml_ids': self.invoice_line_1.ids, 'model': self.rule_1, 'partner': self.invoice_line_1.partner_id, 'status': 'write_off'},
+ }, self.bank_st)
+
+ # Test the write-off part
+ expected_write_off = {
+ 'balance': 10,
+ 'currency_id': False,
+ 'reconcile_model_id': self.rule_1.id,
+ 'account_id': self.current_assets_account.id,
+ }
+
+ matching_result = self.rule_1._apply_rules(statement_line)
+
+ self.assertEqual(len(matching_result[statement_line.id].get('write_off_vals', [])), 1, "Exactly one write-off line should be proposed.")
+
+ full_write_off_dict = matching_result[statement_line.id]['write_off_vals'][0]
+ to_compare = {
+ key: full_write_off_dict[key]
+ for key in expected_write_off.keys()
+ }
+
+ self.assertDictEqual(expected_write_off, to_compare)
+
+ def test_inv_matching_with_write_off_autoreconcile(self):
+ self.bank_line_1.amount = 95
+
+ self.rule_1.sequence = 2
+ self.rule_1.auto_reconcile = True
+ self.rule_1.match_total_amount_param = 90
+
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'status': 'reconciled', 'partner': self.bank_line_1.partner_id},
+ self.bank_line_2.id: {'aml_ids': []},
+ }, statements=self.bank_st)
+
+ # Check first line has been properly reconciled.
+ self.assertRecordValues(self.bank_line_1.line_ids, [
+ {'partner_id': self.partner_1.id, 'debit': 95.0, 'credit': 0.0, 'account_id': self.bank_journal.default_account_id.id, 'reconciled': False},
+ {'partner_id': self.partner_1.id, 'debit': 5.0, 'credit': 0.0, 'account_id': self.current_assets_account.id, 'reconciled': False},
+ {'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 100.0, 'account_id': self.invoice_line_1.account_id.id, 'reconciled': True},
+ ])
+
+ self.assertEqual(self.invoice_line_1.amount_residual, 0.0, "The invoice should have been fully reconciled")
+
+ def test_avoid_amount_matching_bypass(self):
+ """ By the default, if the label of statement lines exactly matches a payment reference, it bypasses any kind of amount verification.
+ This is annoying in some setups, so a config parameter was introduced to handle that.
+ """
+ self.env['ir.config_parameter'].set_param('account.disable_rec_models_bypass', '1')
+ self.rule_1.match_total_amount_param = 90
+ second_inv_matching_rule = self.env['account.reconcile.model'].create({
+ 'name': 'Invoices Matching Rule',
+ 'sequence': 2,
+ 'rule_type': 'invoice_matching',
+ 'auto_reconcile': False,
+ 'match_nature': 'both',
+ 'match_same_currency': False,
+ 'match_total_amount': False,
+ 'match_partner': True,
+ 'company_id': self.company.id,
+ })
+
+ self.bank_line_1.write({
+ 'payment_ref': self.invoice_line_1.move_id.payment_reference,
+ 'amount': 99,
+ })
+ self.bank_line_2.write({
+ 'payment_ref': self.invoice_line_2.move_id.payment_reference,
+ 'amount': 1,
+ })
+
+ self._check_statement_matching(self.rule_1 + second_inv_matching_rule, {
+ self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'status': 'write_off', 'partner': self.bank_line_1.partner_id},
+ self.bank_line_2.id: {'aml_ids': [self.invoice_line_2.id], 'model': second_inv_matching_rule, 'partner': self.bank_line_2.partner_id}
+ }, statements=self.bank_st)
+
+ def test_payment_similar_communications(self):
+ def create_payment_line(amount, memo, partner):
+ payment = self.env['account.payment'].create({
+ 'amount': amount,
+ 'payment_type': 'inbound',
+ 'partner_type': 'customer',
+ 'partner_id': partner.id,
+ 'ref': memo,
+ 'destination_account_id': self.company_data['default_account_receivable'].id,
+ })
+ payment.action_post()
+
+ return payment.line_ids.filtered(lambda x: x.account_id.user_type_id.type not in {'receivable', 'payable'})
+
+ payment_partner = self.env['res.partner'].create({
+ 'name': "Bernard Gagnant",
+ })
+
+ self.rule_1.match_partner_ids = [(6, 0, payment_partner.ids)]
+
+ pmt_line_1 = create_payment_line(500, 'a1b2c3', payment_partner)
+ pmt_line_2 = create_payment_line(500, 'a1b2c3', payment_partner)
+ pmt_line_3 = create_payment_line(500, 'd1e2f3', payment_partner)
+
+ self.bank_line_1.write({
+ 'amount': 1000,
+ 'payment_ref': 'a1b2c3',
+ 'partner_id': payment_partner.id,
+ })
+ self.bank_line_2.unlink()
+ self.rule_1.match_total_amount = False
+
+ self._check_statement_matching(self.rule_1, {
+ self.bank_line_1.id: {'aml_ids': (pmt_line_1 + pmt_line_2).ids, 'model': self.rule_1, 'partner': payment_partner},
+ }, statements=self.bank_line_1.statement_id)
+
+ def test_tax_tags_inversion_with_reconciliation_model(self):
+ country = self.env.ref('base.us')
+ tax_report = self.env['account.tax.report'].create({
+ 'name': "Tax report",
+ 'country_id': country.id,
+ })
+ tax_report_line = self.env['account.tax.report.line'].create({
+ 'name': 'test_tax_report_line',
+ 'tag_name': 'test_tax_report_line',
+ 'report_id': tax_report.id,
+ 'sequence': 10,
+ })
+ tax_tag_pos = tax_report_line.tag_ids.filtered(lambda x: not x.tax_negate)
+ tax_tag_neg = tax_report_line.tag_ids.filtered(lambda x: x.tax_negate)
+ tax = self.env['account.tax'].create({
+ 'name': 'Test Tax',
+ 'amount': 10,
+ 'invoice_repartition_line_ids': [
+ (0, 0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'tag_ids': [(6, 0, tax_tag_pos.ids)],
+ }),
+ (0, 0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'tag_ids': [(6, 0, tax_tag_neg.ids)],
+ }),
+ ],
+ 'refund_repartition_line_ids': [
+ (0, 0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'tag_ids': [(6, 0, tax_tag_neg.ids)],
+ }),
+ (0, 0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'tag_ids': [(6, 0, tax_tag_pos.ids)],
+ }),
+ ],
+ })
+ reconciliation_model = self.env['account.reconcile.model'].create({
+ 'name': 'Charge with Tax',
+ 'line_ids': [(0, 0, {
+ 'account_id': self.company_data['default_account_expense'].id,
+ 'amount_type': 'percentage',
+ 'amount_string': '100',
+ 'tax_ids': [(6, 0, tax.ids)]
+ })]
+ })
+ bank_stmt = self.env['account.bank.statement'].create({
+ 'journal_id': self.bank_journal.id,
+ 'date': '2020-07-15',
+ 'name': 'test',
+ 'line_ids': [(0, 0, {
+ 'payment_ref': 'testLine',
+ 'amount': 5,
+ 'date': '2020-07-15',
+ })]
+ })
+ res = reconciliation_model._get_write_off_move_lines_dict(bank_stmt.line_ids, 5)
+ self.assertEqual(len(res), 2)
+ self.assertEqual(
+ res[0]['tax_tag_ids'],
+ [(6, 0, tax.refund_repartition_line_ids[0].tag_ids.ids)],
+ 'The tags of the first repartition line are not inverted'
+ )
+ self.assertEqual(
+ res[1]['tax_tag_ids'],
+ [(6, 0, tax.refund_repartition_line_ids[1].tag_ids.ids)],
+ 'The tags of the second repartition line are not inverted'
+ )
diff --git a/addons/account/tests/test_sequence_mixin.py b/addons/account/tests/test_sequence_mixin.py
new file mode 100644
index 00000000..6280533b
--- /dev/null
+++ b/addons/account/tests/test_sequence_mixin.py
@@ -0,0 +1,396 @@
+# -*- coding: utf-8 -*-
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests import tagged
+from odoo.tests.common import Form
+from odoo import fields, api, SUPERUSER_ID
+from odoo.exceptions import ValidationError
+from odoo.tools import mute_logger
+
+from dateutil.relativedelta import relativedelta
+from functools import reduce
+import json
+import psycopg2
+
+
+@tagged('post_install', '-at_install')
+class TestSequenceMixin(AccountTestInvoicingCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+ cls.test_move = cls.create_move()
+
+ @classmethod
+ def create_move(cls, move_type=None, date=None, journal=None, name=None, post=False):
+ move = cls.env['account.move'].create({
+ 'move_type': move_type or 'entry',
+ 'date': date or '2016-01-01',
+ 'line_ids': [
+ (0, None, {
+ 'name': 'line',
+ 'account_id': cls.company_data['default_account_revenue'].id,
+ }),
+ ]
+ })
+ if journal:
+ move.name = False
+ move.journal_id = journal
+ if name:
+ move.name = name
+ if post:
+ move.action_post()
+ return move
+
+ def test_sequence_change_date(self):
+ """Change the sequence when we change the date iff it has never been posted."""
+ # Check setup
+ self.assertEqual(self.test_move.state, 'draft')
+ self.assertEqual(self.test_move.name, 'MISC/2016/01/0001')
+ self.assertEqual(fields.Date.to_string(self.test_move.date), '2016-01-01')
+
+ # Never posetd, the number must change if we change the date
+ self.test_move.date = '2020-02-02'
+ self.assertEqual(self.test_move.name, 'MISC/2020/02/0001')
+
+ # We don't recompute user's input when posting
+ self.test_move.name = 'MyMISC/2020/0000001'
+ self.test_move.action_post()
+ self.assertEqual(self.test_move.name, 'MyMISC/2020/0000001')
+
+ # Has been posted, and it doesn't change anymore
+ self.test_move.button_draft()
+ self.test_move.date = '2020-01-02'
+ self.test_move.action_post()
+ self.assertEqual(self.test_move.name, 'MyMISC/2020/0000001')
+
+ def test_journal_sequence(self):
+ self.assertEqual(self.test_move.name, 'MISC/2016/01/0001')
+ self.test_move.action_post()
+ self.assertEqual(self.test_move.name, 'MISC/2016/01/0001')
+
+ copy1 = self.create_move(date=self.test_move.date)
+ self.assertEqual(copy1.name, '/')
+ copy1.action_post()
+ self.assertEqual(copy1.name, 'MISC/2016/01/0002')
+
+ copy2 = self.create_move(date=self.test_move.date)
+ new_journal = self.test_move.journal_id.copy()
+ new_journal.code = "MISC2"
+ copy2.journal_id = new_journal
+ self.assertEqual(copy2.name, 'MISC2/2016/01/0001')
+ with Form(copy2) as move_form: # It is editable in the form
+ with mute_logger('odoo.tests.common.onchange'):
+ move_form.name = 'MyMISC/2016/0001'
+ self.assertIn(
+ 'The sequence will restart at 1 at the start of every year',
+ move_form._perform_onchange(['name'])['warning']['message'],
+ )
+ move_form.journal_id = self.test_move.journal_id
+ self.assertEqual(move_form.name, '/')
+ move_form.journal_id = new_journal
+ self.assertEqual(move_form.name, 'MISC2/2016/01/0001')
+ with mute_logger('odoo.tests.common.onchange'):
+ move_form.name = 'MyMISC/2016/0001'
+ self.assertIn(
+ 'The sequence will restart at 1 at the start of every year',
+ move_form._perform_onchange(['name'])['warning']['message'],
+ )
+ copy2.action_post()
+ self.assertEqual(copy2.name, 'MyMISC/2016/0001')
+
+ copy3 = self.create_move(date=copy2.date, journal=new_journal)
+ self.assertEqual(copy3.name, '/')
+ with self.assertRaises(AssertionError):
+ with Form(copy2) as move_form: # It is not editable in the form
+ move_form.name = 'MyMISC/2016/0002'
+ copy3.action_post()
+ self.assertEqual(copy3.name, 'MyMISC/2016/0002')
+ copy3.name = 'MISC2/2016/00002'
+
+ copy4 = self.create_move(date=copy2.date, journal=new_journal)
+ copy4.action_post()
+ self.assertEqual(copy4.name, 'MISC2/2016/00003')
+
+ copy5 = self.create_move(date=copy2.date, journal=new_journal)
+ copy5.date = '2021-02-02'
+ copy5.action_post()
+ self.assertEqual(copy5.name, 'MISC2/2021/00001')
+ copy5.name = 'N\'importe quoi?'
+
+ copy6 = self.create_move(date=copy5.date, journal=new_journal)
+ copy6.action_post()
+ self.assertEqual(copy6.name, 'N\'importe quoi?1')
+
+ def test_journal_sequence_format(self):
+ """Test different format of sequences and what it becomes on another period"""
+ sequences = [
+ ('JRNL/2016/00001', 'JRNL/2016/00002', 'JRNL/2016/00003', 'JRNL/2017/00001'),
+ ('1234567', '1234568', '1234569', '1234570'),
+ ('20190910', '20190911', '20190912', '20190913'),
+ ('2016-0910', '2016-0911', '2016-0912', '2017-0001'),
+ ('201603-10', '201603-11', '201604-01', '201703-01'),
+ ('16-03-10', '16-03-11', '16-04-01', '17-03-01'),
+ ('2016-10', '2016-11', '2016-12', '2017-01'),
+ ('045-001-000002', '045-001-000003', '045-001-000004', '045-001-000005'),
+ ('JRNL/2016/00001suffix', 'JRNL/2016/00002suffix', 'JRNL/2016/00003suffix', 'JRNL/2017/00001suffix'),
+ ]
+
+ init_move = self.create_move(date='2016-03-12')
+ next_move = self.create_move(date='2016-03-12')
+ next_move_month = self.create_move(date='2016-04-12')
+ next_move_year = self.create_move(date='2017-03-12')
+ next_moves = (next_move + next_move_month + next_move_year)
+ next_moves.action_post()
+
+ for sequence_init, sequence_next, sequence_next_month, sequence_next_year in sequences:
+ init_move.name = sequence_init
+ next_moves.name = False
+ next_moves._compute_name()
+ self.assertEqual(
+ [next_move.name, next_move_month.name, next_move_year.name],
+ [sequence_next, sequence_next_month, sequence_next_year],
+ )
+
+ def test_journal_next_sequence(self):
+ """Sequences behave correctly even when there is not enough padding."""
+ prefix = "TEST_ORDER/2016/"
+ self.test_move.name = f"{prefix}1"
+ for c in range(2, 25):
+ copy = self.create_move(date=self.test_move.date)
+ copy.name = "/"
+ copy.action_post()
+ self.assertEqual(copy.name, f"{prefix}{c}")
+
+ def test_journal_sequence_multiple_type(self):
+ """Domain is computed accordingly to different types."""
+ entry, entry2, invoice, invoice2, refund, refund2 = (
+ self.create_move(date='2016-01-01')
+ for i in range(6)
+ )
+ (invoice + invoice2 + refund + refund2).write({
+ 'journal_id': self.company_data['default_journal_sale'],
+ 'partner_id': 1,
+ 'invoice_date': '2016-01-01',
+ })
+ (invoice + invoice2).move_type = 'out_invoice'
+ (refund + refund2).move_type = 'out_refund'
+ all = (entry + entry2 + invoice + invoice2 + refund + refund2)
+ all.name = False
+ all.action_post()
+ self.assertEqual(entry.name, 'MISC/2016/01/0002')
+ self.assertEqual(entry2.name, 'MISC/2016/01/0003')
+ self.assertEqual(invoice.name, 'INV/2016/01/0001')
+ self.assertEqual(invoice2.name, 'INV/2016/01/0002')
+ self.assertEqual(refund.name, 'RINV/2016/01/0001')
+ self.assertEqual(refund2.name, 'RINV/2016/01/0002')
+
+ def test_journal_sequence_groupby_compute(self):
+ """The grouping optimization is correctly done."""
+ # Setup two journals with a sequence that resets yearly
+ journals = self.env['account.journal'].create([{
+ 'name': f'Journal{i}',
+ 'code': f'J{i}',
+ 'type': 'general',
+ } for i in range(2)])
+ account = self.env['account.account'].search([], limit=1)
+ moves = self.env['account.move'].create([{
+ 'journal_id': journals[i].id,
+ 'line_ids': [(0, 0, {'account_id': account.id, 'name': 'line'})],
+ 'date': '2010-01-01',
+ } for i in range(2)])._post()
+ for i in range(2):
+ moves[i].name = f'J{i}/2010/00001'
+
+ # Check that the moves are correctly batched
+ moves = self.env['account.move'].create([{
+ 'journal_id': journals[journal_index].id,
+ 'line_ids': [(0, 0, {'account_id': account.id, 'name': 'line'})],
+ 'date': f'2010-{month}-01',
+ } for journal_index, month in [(1, 1), (0, 1), (1, 2), (1, 1)]])._post()
+ self.assertEqual(
+ moves.mapped('name'),
+ ['J1/2010/00002', 'J0/2010/00002', 'J1/2010/00004', 'J1/2010/00003'],
+ )
+
+ journals[0].code = 'OLD'
+ journals.flush()
+ journal_same_code = self.env['account.journal'].create([{
+ 'name': 'Journal0',
+ 'code': 'J0',
+ 'type': 'general',
+ }])
+ moves = (
+ self.create_move(date='2010-01-01', journal=journal_same_code, name='J0/2010/00001')
+ + self.create_move(date='2010-01-01', journal=journal_same_code)
+ + self.create_move(date='2010-01-01', journal=journal_same_code)
+ + self.create_move(date='2010-01-01', journal=journals[0])
+ )._post()
+ self.assertEqual(
+ moves.mapped('name'),
+ ['J0/2010/00001', 'J0/2010/00002', 'J0/2010/00003', 'J0/2010/00003'],
+ )
+
+ def test_journal_override_sequence_regex(self):
+ """There is a possibility to override the regex and change the order of the paramters."""
+ self.create_move(date='2020-01-01', name='00000876-G 0002/2020')
+ next = self.create_move(date='2020-01-01')
+ next.action_post()
+ self.assertEqual(next.name, '00000876-G 0002/2021') # Wait, I didn't want this!
+
+ next.button_draft()
+ next.name = False
+ next.journal_id.sequence_override_regex = r'^(?P<seq>\d*)(?P<suffix1>.*?)(?P<year>(\d{4})?)(?P<suffix2>)$'
+ next.action_post()
+ self.assertEqual(next.name, '00000877-G 0002/2020') # Pfew, better!
+ next = self.create_move(date='2020-01-01')
+ next.action_post()
+ self.assertEqual(next.name, '00000878-G 0002/2020')
+
+ next = self.create_move(date='2017-05-02')
+ next.action_post()
+ self.assertEqual(next.name, '00000001-G 0002/2017')
+
+ def test_journal_sequence_ordering(self):
+ """Entries are correctly sorted when posting multiple at once."""
+ self.test_move.name = 'XMISC/2016/00001'
+ copies = reduce((lambda x, y: x+y), [
+ self.create_move(date=self.test_move.date)
+ for i in range(6)
+ ])
+
+ copies[0].date = '2019-03-05'
+ copies[1].date = '2019-03-06'
+ copies[2].date = '2019-03-07'
+ copies[3].date = '2019-03-04'
+ copies[4].date = '2019-03-05'
+ copies[5].date = '2019-03-05'
+ # that entry is actualy the first one of the period, so it already has a name
+ # set it to '/' so that it is recomputed at post to be ordered correctly.
+ copies[0].name = '/'
+ copies.action_post()
+
+ # Ordered by date
+ self.assertEqual(copies[0].name, 'XMISC/2019/00002')
+ self.assertEqual(copies[1].name, 'XMISC/2019/00005')
+ self.assertEqual(copies[2].name, 'XMISC/2019/00006')
+ self.assertEqual(copies[3].name, 'XMISC/2019/00001')
+ self.assertEqual(copies[4].name, 'XMISC/2019/00003')
+ self.assertEqual(copies[5].name, 'XMISC/2019/00004')
+
+ # Can't have twice the same name
+ with self.assertRaises(ValidationError):
+ copies[0].name = 'XMISC/2019/00001'
+
+ # Lets remove the order by date
+ copies[0].name = 'XMISC/2019/10001'
+ copies[1].name = 'XMISC/2019/10002'
+ copies[2].name = 'XMISC/2019/10003'
+ copies[3].name = 'XMISC/2019/10004'
+ copies[4].name = 'XMISC/2019/10005'
+ copies[5].name = 'XMISC/2019/10006'
+
+ copies[4].button_draft()
+ copies[4].with_context(force_delete=True).unlink()
+ copies[5].button_draft()
+
+ wizard = Form(self.env['account.resequence.wizard'].with_context(
+ active_ids=set(copies.ids) - set(copies[4].ids),
+ active_model='account.move'),
+ )
+
+ new_values = json.loads(wizard.new_values)
+ self.assertEqual(new_values[str(copies[0].id)]['new_by_date'], 'XMISC/2019/10002')
+ self.assertEqual(new_values[str(copies[0].id)]['new_by_name'], 'XMISC/2019/10001')
+
+ self.assertEqual(new_values[str(copies[1].id)]['new_by_date'], 'XMISC/2019/10004')
+ self.assertEqual(new_values[str(copies[1].id)]['new_by_name'], 'XMISC/2019/10002')
+
+ self.assertEqual(new_values[str(copies[2].id)]['new_by_date'], 'XMISC/2019/10005')
+ self.assertEqual(new_values[str(copies[2].id)]['new_by_name'], 'XMISC/2019/10003')
+
+ self.assertEqual(new_values[str(copies[3].id)]['new_by_date'], 'XMISC/2019/10001')
+ self.assertEqual(new_values[str(copies[3].id)]['new_by_name'], 'XMISC/2019/10004')
+
+ self.assertEqual(new_values[str(copies[5].id)]['new_by_date'], 'XMISC/2019/10003')
+ self.assertEqual(new_values[str(copies[5].id)]['new_by_name'], 'XMISC/2019/10005')
+
+ wizard.save().resequence()
+
+ self.assertEqual(copies[3].state, 'posted')
+ self.assertEqual(copies[5].name, 'XMISC/2019/10005')
+ self.assertEqual(copies[5].state, 'draft')
+
+ def test_sequence_get_more_specific(self):
+ """There is the ability to change the format (i.e. from yearly to montlhy)."""
+ def test_date(date, name):
+ test = self.create_move(date=date)
+ test.action_post()
+ self.assertEqual(test.name, name)
+
+ def set_sequence(date, name):
+ return self.create_move(date=date, name=name)._post()
+
+ # Start with a continuous sequence
+ self.test_move.name = 'MISC/00001'
+
+ # Change the prefix to reset every year starting in 2017
+ new_year = set_sequence(self.test_move.date + relativedelta(years=1), 'MISC/2017/00001')
+
+ # Change the prefix to reset every month starting in February 2017
+ new_month = set_sequence(new_year.date + relativedelta(months=1), 'MISC/2017/02/00001')
+
+ test_date(self.test_move.date, 'MISC/00002') # Keep the old prefix in 2016
+ test_date(new_year.date, 'MISC/2017/00002') # Keep the new prefix in 2017
+ test_date(new_month.date, 'MISC/2017/02/00002') # Keep the new prefix in February 2017
+
+ # Change the prefix to never reset (again) year starting in 2018 (Please don't do that)
+ reset_never = set_sequence(self.test_move.date + relativedelta(years=2), 'MISC/00100')
+ test_date(reset_never.date, 'MISC/00101') # Keep the new prefix in 2018
+
+ def test_sequence_concurency(self):
+ """Computing the same name in concurent transactions is not allowed."""
+ with self.env.registry.cursor() as cr0,\
+ self.env.registry.cursor() as cr1,\
+ self.env.registry.cursor() as cr2:
+ env0 = api.Environment(cr0, SUPERUSER_ID, {})
+ env1 = api.Environment(cr1, SUPERUSER_ID, {})
+ env2 = api.Environment(cr2, SUPERUSER_ID, {})
+
+ journal = env0['account.journal'].create({
+ 'name': 'concurency_test',
+ 'code': 'CT',
+ 'type': 'general',
+ })
+ account = env0['account.account'].create({
+ 'code': 'CT',
+ 'name': 'CT',
+ 'user_type_id': env0.ref('account.data_account_type_fixed_assets').id,
+ })
+ moves = env0['account.move'].create([{
+ 'journal_id': journal.id,
+ 'date': fields.Date.from_string('2016-01-01'),
+ 'line_ids': [(0, 0, {'name': 'name', 'account_id': account.id})]
+ }] * 3)
+ moves.name = '/'
+ moves[0].action_post()
+ self.assertEqual(moves.mapped('name'), ['CT/2016/01/0001', '/', '/'])
+ env0.cr.commit()
+
+ # start the transactions here on cr2 to simulate concurency with cr1
+ env2.cr.execute('SELECT 1')
+
+ move = env1['account.move'].browse(moves[1].id)
+ move.action_post()
+ env1.cr.commit()
+
+ move = env2['account.move'].browse(moves[2].id)
+ with self.assertRaises(psycopg2.OperationalError), env2.cr.savepoint(), mute_logger('odoo.sql_db'):
+ move.action_post()
+
+ self.assertEqual(moves.mapped('name'), ['CT/2016/01/0001', 'CT/2016/01/0002', '/'])
+ moves.button_draft()
+ moves.posted_before = False
+ moves.unlink()
+ journal.unlink()
+ account.unlink()
+ env0.cr.commit()
diff --git a/addons/account/tests/test_settings.py b/addons/account/tests/test_settings.py
new file mode 100644
index 00000000..2aabe334
--- /dev/null
+++ b/addons/account/tests/test_settings.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests import tagged
+
+
+@tagged('post_install', '-at_install')
+class TestSettings(AccountTestInvoicingCommon):
+
+ def test_switch_taxB2B_taxB2C(self):
+ """
+ Since having users both in the tax B2B and tax B2C groups raise,
+ modifications of the settings must be done in the right order;
+ otherwise it is impossible to change the settings.
+ """
+ # at each setting change, all users should be removed from one group and added to the other
+ # so picking an arbitrary witness should be equivalent to checking that everything worked.
+ config = self.env['res.config.settings'].create({})
+ self.switch_tax_settings(config)
+
+ def switch_tax_settings(self, config):
+ config.show_line_subtotals_tax_selection = "tax_excluded"
+ config._onchange_sale_tax()
+ config.flush()
+ config.execute()
+ self.assertEqual(self.env.user.has_group('account.group_show_line_subtotals_tax_excluded'), True)
+ self.assertEqual(self.env.user.has_group('account.group_show_line_subtotals_tax_included'), False)
+
+ config.show_line_subtotals_tax_selection = "tax_included"
+ config._onchange_sale_tax()
+ config.flush()
+ config.execute()
+ self.assertEqual(self.env.user.has_group('account.group_show_line_subtotals_tax_excluded'), False)
+ self.assertEqual(self.env.user.has_group('account.group_show_line_subtotals_tax_included'), True)
+
+ config.show_line_subtotals_tax_selection = "tax_excluded"
+ config._onchange_sale_tax()
+ config.flush()
+ config.execute()
+ self.assertEqual(self.env.user.has_group('account.group_show_line_subtotals_tax_excluded'), True)
+ self.assertEqual(self.env.user.has_group('account.group_show_line_subtotals_tax_included'), False)
+
+ def test_switch_taxB2B_taxB2C_multicompany(self):
+ """
+ Contrarily to the (apparently reasonable) assumption that adding users
+ to group and removing them was symmetrical, it may not be the case
+ if one is done in SQL and the other via the ORM.
+ Because the latter automatically takes into account record rules that
+ might make some users invisible.
+
+ This one is identical to the previous, except that we do the actions
+ with a non-superuser user, and in a new company with one user in common
+ with another company which has a different taxB2X setting.
+ """
+ user = self.env.ref('base.user_admin')
+ company = self.env['res.company'].create({'name': 'oobO'})
+ user.write({'company_ids': [(4, company.id)], 'company_id': company.id})
+ Settings = self.env['res.config.settings'].with_user(user.id)
+ config = Settings.create({})
+
+ self.switch_tax_settings(config)
diff --git a/addons/account/tests/test_tax.py b/addons/account/tests/test_tax.py
new file mode 100644
index 00000000..068031ce
--- /dev/null
+++ b/addons/account/tests/test_tax.py
@@ -0,0 +1,1075 @@
+# -*- coding: utf-8 -*-
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests import tagged
+
+
+@tagged('post_install', '-at_install')
+class TestTaxCommon(AccountTestInvoicingCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ # Setup another company having a rounding of 1.0.
+ cls.currency_data['currency'].rounding = 1.0
+ cls.currency_no_decimal = cls.currency_data['currency']
+ cls.company_data_2 = cls.setup_company_data('company_2', currency_id=cls.currency_no_decimal.id)
+ cls.env.user.company_id = cls.company_data['company']
+
+ cls.fixed_tax = cls.env['account.tax'].create({
+ 'name': "Fixed tax",
+ 'amount_type': 'fixed',
+ 'amount': 10,
+ 'sequence': 1,
+ })
+ cls.fixed_tax_bis = cls.env['account.tax'].create({
+ 'name': "Fixed tax bis",
+ 'amount_type': 'fixed',
+ 'amount': 15,
+ 'sequence': 2,
+ })
+ cls.percent_tax = cls.env['account.tax'].create({
+ 'name': "Percent tax",
+ 'amount_type': 'percent',
+ 'amount': 10,
+ 'sequence': 3,
+ })
+ cls.percent_tax_bis = cls.env['account.tax'].create({
+ 'name': "Percent tax bis",
+ 'amount_type': 'percent',
+ 'amount': 10,
+ 'sequence': 4,
+ })
+ cls.division_tax = cls.env['account.tax'].create({
+ 'name': "Division tax",
+ 'amount_type': 'division',
+ 'amount': 10,
+ 'sequence': 4,
+ })
+ cls.group_tax = cls.env['account.tax'].create({
+ 'name': "Group tax",
+ 'amount_type': 'group',
+ 'amount': 0,
+ 'sequence': 5,
+ 'children_tax_ids': [
+ (4, cls.fixed_tax.id, 0),
+ (4, cls.percent_tax.id, 0)
+ ]
+ })
+ cls.group_tax_bis = cls.env['account.tax'].create({
+ 'name': "Group tax bis",
+ 'amount_type': 'group',
+ 'amount': 0,
+ 'sequence': 6,
+ 'children_tax_ids': [
+ (4, cls.fixed_tax.id, 0),
+ (4, cls.percent_tax.id, 0)
+ ]
+ })
+ cls.group_tax_percent = cls.env['account.tax'].create({
+ 'name': "Group tax percent",
+ 'amount_type': 'group',
+ 'amount': 0,
+ 'sequence': 6,
+ 'children_tax_ids': [
+ (4, cls.percent_tax.id, 0),
+ (4, cls.percent_tax_bis.id, 0)
+ ]
+ })
+ cls.group_of_group_tax = cls.env['account.tax'].create({
+ 'name': "Group of group tax",
+ 'amount_type': 'group',
+ 'amount': 0,
+ 'sequence': 7,
+ 'children_tax_ids': [
+ (4, cls.group_tax.id, 0),
+ (4, cls.group_tax_bis.id, 0)
+ ]
+ })
+ cls.tax_with_no_account = cls.env['account.tax'].create({
+ 'name': "Tax with no account",
+ 'amount_type': 'fixed',
+ 'amount': 0,
+ 'sequence': 8,
+ })
+ some_account = cls.env['account.account'].search([], limit=1)
+ cls.tax_with_account = cls.env['account.tax'].create({
+ 'name': "Tax with account",
+ 'amount_type': 'fixed',
+ 'amount': 0,
+ 'sequence': 8,
+ 'invoice_repartition_line_ids': [
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'account_id': some_account.id,
+ }),
+ ],
+ 'refund_repartition_line_ids': [
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'account_id': some_account.id,
+ }),
+ ],
+ })
+
+ cls.tax_0_percent = cls.env['account.tax'].with_company(cls.company_data['company']).create({
+ 'name': "test_0_percent",
+ 'amount_type': 'percent',
+ 'amount': 0,
+ })
+
+ cls.tax_5_percent = cls.env['account.tax'].with_company(cls.company_data['company']).create({
+ 'name': "test_5_percent",
+ 'amount_type': 'percent',
+ 'amount': 5,
+ })
+
+ cls.tax_8_percent = cls.env['account.tax'].with_company(cls.company_data['company']).create({
+ 'name': "test_8_percent",
+ 'amount_type': 'percent',
+ 'amount': 8,
+ })
+ cls.tax_12_percent = cls.env['account.tax'].with_company(cls.company_data['company']).create({
+ 'name': "test_12_percent",
+ 'amount_type': 'percent',
+ 'amount': 12,
+ })
+
+ cls.tax_19_percent = cls.env['account.tax'].with_company(cls.company_data_2['company']).create({
+ 'name': "test_19_percent",
+ 'amount_type': 'percent',
+ 'amount': 19,
+ })
+
+ cls.tax_21_percent = cls.env['account.tax'].with_company(cls.company_data['company']).create({
+ 'name': "test_21_percent",
+ 'amount_type': 'percent',
+ 'amount': 19,
+ })
+
+ cls.tax_21_percent = cls.env['account.tax'].with_company(cls.company_data['company']).create({
+ 'name': "test_rounding_methods_2",
+ 'amount_type': 'percent',
+ 'amount': 21,
+ })
+
+ cls.bank_journal = cls.company_data['default_journal_bank']
+ cls.bank_account = cls.bank_journal.default_account_id
+ cls.expense_account = cls.company_data['default_account_expense']
+
+ def _check_compute_all_results(self, total_included, total_excluded, taxes, res):
+ self.assertAlmostEqual(res['total_included'], total_included)
+ self.assertAlmostEqual(res['total_excluded'], total_excluded)
+ for i in range(0, len(taxes)):
+ self.assertAlmostEqual(res['taxes'][i]['base'], taxes[i][0])
+ self.assertAlmostEqual(res['taxes'][i]['amount'], taxes[i][1])
+
+
+@tagged('post_install', '-at_install')
+class TestTax(TestTaxCommon):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestTax, cls).setUpClass()
+
+ def test_tax_group_of_group_tax(self):
+ self.fixed_tax.include_base_amount = True
+ res = self.group_of_group_tax.compute_all(200.0)
+ self._check_compute_all_results(
+ 263, # 'total_included'
+ 200, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (200.0, 10.0), # | 1 | 10 | | t
+ (210.0, 21.0), # | 3 | 10% | |
+ (210.0, 10.0), # | 1 | 10 | | t
+ (220.0, 22.0), # | 3 | 10% | |
+ # ---------------------------------------------------
+ ],
+ res
+ )
+
+ def test_tax_group(self):
+ res = self.group_tax.compute_all(200.0)
+ self._check_compute_all_results(
+ 230, # 'total_included'
+ 200, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (200.0, 10.0), # | 1 | 10 | |
+ (200.0, 20.0), # | 3 | 10% | |
+ # ---------------------------------------------------
+ ],
+ res
+ )
+
+ def test_tax_group_percent(self):
+ res = self.group_tax_percent.with_context({'force_price_include':True}).compute_all(100.0)
+ self._check_compute_all_results(
+ 100, # 'total_included'
+ 83.33, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (83.33, 8.33), # | 1 | 10% | |
+ (83.33, 8.34), # | 2 | 10% | |
+ # ---------------------------------------------------
+ ],
+ res
+ )
+
+ def test_tax_percent_division(self):
+ self.division_tax.price_include = True
+ self.division_tax.include_base_amount = True
+ res_division = self.division_tax.compute_all(200.0)
+ self._check_compute_all_results(
+ 200, # 'total_included'
+ 180, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (180.0, 20.0), # | 4 | 10/ | t | t
+ # ---------------------------------------------------
+ ],
+ res_division
+ )
+ self.percent_tax.price_include = False
+ self.percent_tax.include_base_amount = False
+ res_percent = self.percent_tax.compute_all(100.0)
+ self._check_compute_all_results(
+ 110, # 'total_included'
+ 100, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (100.0, 10.0), # | 3 | 10% | |
+ # ---------------------------------------------------
+ ],
+ res_percent
+ )
+ self.division_tax.price_include = False
+ self.division_tax.include_base_amount = False
+ res_division = self.division_tax.compute_all(180.0)
+ self._check_compute_all_results(
+ 200, # 'total_included'
+ 180, # 'total_excluded'
+ [
+ # base, amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (180.0, 20.0), # | 4 | 10/ | |
+ # ---------------------------------------------------
+ ],
+ res_division
+ )
+ self.percent_tax.price_include = True
+ self.percent_tax.include_base_amount = True
+ res_percent = self.percent_tax.compute_all(110.0)
+ self._check_compute_all_results(
+ 110, # 'total_included'
+ 100, # 'total_excluded'
+ [
+ # base, amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (100.0, 10.0), # | 3 | 10% | t | t
+ # ---------------------------------------------------
+ ],
+ res_percent
+ )
+ self.percent_tax_bis.price_include = True
+ self.percent_tax_bis.include_base_amount = True
+ self.percent_tax_bis.amount = 21
+ res_percent = self.percent_tax_bis.compute_all(7.0)
+ self._check_compute_all_results(
+ 7.0, # 'total_included'
+ 5.79, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (5.79, 1.21), # | 3 | 21% | t | t
+ # ---------------------------------------------------
+ ],
+ res_percent
+ )
+
+ def test_tax_sequence_normalized_set(self):
+ self.division_tax.sequence = 1
+ self.fixed_tax.sequence = 2
+ self.percent_tax.sequence = 3
+ taxes_set = (self.group_tax | self.division_tax)
+ res = taxes_set.compute_all(200.0)
+ self._check_compute_all_results(
+ 252.22, # 'total_included'
+ 200, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (200.0, 22.22), # | 1 | 10/ | |
+ (200.0, 10.0), # | 2 | 10 | |
+ (200.0, 20.0), # | 3 | 10% | |
+ # ---------------------------------------------------
+ ],
+ res
+ )
+
+ def test_fixed_tax_include_base_amount(self):
+ self.fixed_tax.include_base_amount = True
+ res = self.group_tax.compute_all(200.0)
+ self._check_compute_all_results(
+ 231, # 'total_included'
+ 200, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (200.0, 10.0), # | 1 | 10 | | t
+ (210.0, 21.0), # | 3 | 10% | |
+ # ---------------------------------------------------
+ ],
+ res
+ )
+
+ self.fixed_tax.price_include = True
+ self.fixed_tax.include_base_amount = False
+ res = self.fixed_tax.compute_all(100.0, quantity=2.0)
+ self._check_compute_all_results(
+ 200, # 'total_included'
+ 180, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (180.0, 20.0), # | 1 | 20 | | t
+ # ---------------------------------------------------
+ ],
+ res
+ )
+
+ def test_percent_tax_include_base_amount(self):
+ self.percent_tax.price_include = True
+ self.percent_tax.amount = 21.0
+ res = self.percent_tax.compute_all(7.0)
+ self._check_compute_all_results(
+ 7.0, # 'total_included'
+ 5.79, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (5.79, 1.21), # | 3 | 21% | t |
+ # ---------------------------------------------------
+ ],
+ res
+ )
+
+ self.percent_tax.price_include = True
+ self.percent_tax.amount = 20.0
+ res = self.percent_tax.compute_all(399.99)
+ self._check_compute_all_results(
+ 399.99, # 'total_included'
+ 333.33, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (333.33, 66.66), # | 3 | 20% | t |
+ # ---------------------------------------------------
+ ],
+ res
+ )
+
+ def test_tax_decimals(self):
+ """Test the rounding of taxes up to 6 decimals (maximum decimals places allowed for currencies)"""
+ self.env.user.company_id.currency_id.rounding = 0.000001
+
+ self.percent_tax.price_include = True
+ self.percent_tax.amount = 21.0
+ res = self.percent_tax.compute_all(7.0)
+ self._check_compute_all_results(
+ 7.0, # 'total_included'
+ 5.785124, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # --------------------------------------------------------
+ (5.785124, 1.214876), # | 3 | 21% | t |
+ # --------------------------------------------------------
+ ],
+ res
+ )
+
+ self.percent_tax.price_include = True
+ self.percent_tax.amount = 20.0
+ res = self.percent_tax.compute_all(399.999999)
+ self._check_compute_all_results(
+ 399.999999, # 'total_included'
+ 333.333333, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # -----------------------------------------------------------
+ (333.333333, 66.666666), # | 3 | 20% | t |
+ # -----------------------------------------------------------
+ ],
+ res
+ )
+
+ def test_advanced_taxes_computation_0(self):
+ '''Test more advanced taxes computation (see issue 34471).'''
+ tax_1 = self.env['account.tax'].create({
+ 'name': 'test_advanced_taxes_computation_0_1',
+ 'amount_type': 'percent',
+ 'amount': 10,
+ 'price_include': True,
+ 'include_base_amount': True,
+ 'sequence': 1,
+ 'invoice_repartition_line_ids': [
+ (0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ ],
+ 'refund_repartition_line_ids': [
+ (0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ ],
+ })
+ tax_2 = self.env['account.tax'].create({
+ 'name': 'test_advanced_taxes_computation_0_2',
+ 'amount_type': 'percent',
+ 'amount': 10,
+ 'sequence': 2,
+ 'invoice_repartition_line_ids': [
+ (0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ ],
+ 'refund_repartition_line_ids': [
+ (0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ ],
+ })
+ tax_3 = self.env['account.tax'].create({
+ 'name': 'test_advanced_taxes_computation_0_3',
+ 'amount_type': 'percent',
+ 'amount': 10,
+ 'price_include': True,
+ 'sequence': 3,
+ 'invoice_repartition_line_ids': [
+ (0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ ],
+ 'refund_repartition_line_ids': [
+ (0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ ],
+ })
+ tax_4 = self.env['account.tax'].create({
+ 'name': 'test_advanced_taxes_computation_0_4',
+ 'amount_type': 'percent',
+ 'amount': 10,
+ 'sequence': 4,
+ 'invoice_repartition_line_ids': [
+ (0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ ],
+ 'refund_repartition_line_ids': [
+ (0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ ],
+ })
+ tax_5 = self.env['account.tax'].create({
+ 'name': 'test_advanced_taxes_computation_0_5',
+ 'amount_type': 'percent',
+ 'amount': 10,
+ 'price_include': True,
+ 'sequence': 5,
+ 'invoice_repartition_line_ids': [
+ (0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ ],
+ 'refund_repartition_line_ids': [
+ (0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ ],
+ })
+ taxes = tax_1 + tax_2 + tax_3 + tax_4 + tax_5
+
+ # Test with positive amount.
+ self._check_compute_all_results(
+ 154, # 'total_included'
+ 100, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (100.0, 5.0), # | 1 | 10% | t | t
+ (100.0, 5.0), # | 1 | 10% | t | t
+ (110.0, 5.5), # | 2 | 10% | |
+ (110.0, 5.5), # | 2 | 10% | |
+ (110.0, 5.5), # | 3 | 10% | t |
+ (110.0, 5.5), # | 3 | 10% | t |
+ (110.0, 5.5), # | 4 | 10% | |
+ (110.0, 5.5), # | 4 | 10% | |
+ (110.0, 5.5), # | 5 | 10% | t |
+ (110.0, 5.5), # | 5 | 10% | t |
+ # ---------------------------------------------------
+ ],
+ taxes.compute_all(132.0)
+ )
+
+ # Test with negative amount.
+ self._check_compute_all_results(
+ -154, # 'total_included'
+ -100, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (-100.0, -5.0), # | 1 | 10% | t | t
+ (-100.0, -5.0), # | 1 | 10% | t | t
+ (-110.0, -5.5), # | 2 | 10% | |
+ (-110.0, -5.5), # | 2 | 10% | |
+ (-110.0, -5.5), # | 3 | 10% | t |
+ (-110.0, -5.5), # | 3 | 10% | t |
+ (-110.0, -5.5), # | 4 | 10% | |
+ (-110.0, -5.5), # | 4 | 10% | |
+ (-110.0, -5.5), # | 5 | 10% | t |
+ (-110.0, -5.5), # | 5 | 10% | t |
+ # ---------------------------------------------------
+ ],
+ taxes.compute_all(-132.0)
+ )
+
+ def test_intracomm_taxes_computation_0(self):
+ ''' Test usage of intracomm taxes having e.g.+100%, -100% as repartition lines. '''
+ intracomm_tax = self.env['account.tax'].create({
+ 'name': 'test_intracomm_taxes_computation_0_1',
+ 'amount_type': 'percent',
+ 'amount': 21,
+ 'invoice_repartition_line_ids': [
+ (0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
+ (0, 0, {'repartition_type': 'tax', '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}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 100.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': -100.0}),
+ ],
+ })
+
+ # Test with positive amount.
+ self._check_compute_all_results(
+ 100, # 'total_included'
+ 100, # 'total_excluded'
+ [
+ # base , amount
+ # ---------------
+ (100.0, 21.0),
+ (100.0, -21.0),
+ # ---------------
+ ],
+ intracomm_tax.compute_all(100.0)
+ )
+
+ # Test with negative amount.
+ self._check_compute_all_results(
+ -100, # 'total_included'
+ -100, # 'total_excluded'
+ [
+ # base , amount
+ # ---------------
+ (-100.0, -21.0),
+ (-100.0, 21.0),
+ # ---------------
+ ],
+ intracomm_tax.compute_all(-100.0)
+ )
+
+ def test_rounding_issues_0(self):
+ ''' Test taxes having a complex setup of repartition lines. '''
+ tax = self.env['account.tax'].create({
+ 'name': 'test_rounding_issues_0',
+ 'amount_type': 'percent',
+ 'amount': 3,
+ 'invoice_repartition_line_ids': [
+ (0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ ],
+ 'refund_repartition_line_ids': [
+ (0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ ],
+ })
+
+ # Test with positive amount.
+ self._check_compute_all_results(
+ 1.09, # 'total_included'
+ 1, # 'total_excluded'
+ [
+ # base , amount
+ # ---------------
+ (1.0, 0.01),
+ (1.0, 0.01),
+ (1.0, 0.01),
+ (1.0, 0.02),
+ (1.0, 0.02),
+ (1.0, 0.02),
+ # ---------------
+ ],
+ tax.compute_all(1.0)
+ )
+
+ # Test with negative amount.
+ self._check_compute_all_results(
+ -1.09, # 'total_included'
+ -1, # 'total_excluded'
+ [
+ # base , amount
+ # ---------------
+ (-1.0, -0.01),
+ (-1.0, -0.01),
+ (-1.0, -0.01),
+ (-1.0, -0.02),
+ (-1.0, -0.02),
+ (-1.0, -0.02),
+ # ---------------
+ ],
+ tax.compute_all(-1.0)
+ )
+
+ def test_rounding_issues_1(self):
+ ''' Test taxes having a complex setup of repartition lines. '''
+ tax = self.env['account.tax'].create({
+ 'name': 'test_advanced_taxes_repartition_lines_computation_1',
+ 'amount_type': 'percent',
+ 'amount': 3,
+ 'invoice_repartition_line_ids': [
+ (0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': -50.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 25.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 25.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': -25.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': -25.0}),
+ ],
+ 'refund_repartition_line_ids': [
+ (0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': -50.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 25.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': 25.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': -25.0}),
+ (0, 0, {'repartition_type': 'tax', 'factor_percent': -25.0}),
+ ],
+ })
+
+ # Test with positive amount.
+ self._check_compute_all_results(
+ 1, # 'total_included'
+ 1, # 'total_excluded'
+ [
+ # base , amount
+ # ---------------
+ (1.0, 0.02),
+ (1.0, -0.02),
+ (1.0, 0.01),
+ (1.0, 0.01),
+ (1.0, -0.01),
+ (1.0, -0.01),
+ # ---------------
+ ],
+ tax.compute_all(1.0)
+ )
+
+ # Test with negative amount.
+ self._check_compute_all_results(
+ -1, # 'total_included'
+ -1, # 'total_excluded'
+ [
+ # base , amount
+ # ---------------
+ (-1.0, -0.02),
+ (-1.0, 0.02),
+ (-1.0, -0.01),
+ (-1.0, -0.01),
+ (-1.0, 0.01),
+ (-1.0, 0.01),
+ # ---------------
+ ],
+ tax.compute_all(-1.0)
+ )
+
+ def test_rounding_tax_excluded_round_per_line_01(self):
+ ''' Test the rounding of a 19% price excluded tax in an invoice having 22689 and 9176 as lines.
+ The decimal precision is set to zero.
+ The computation must be similar to round(22689 * 0.19) + round(9176 * 0.19).
+ '''
+ self.tax_19_percent.company_id.currency_id.rounding = 1.0
+ self.tax_19_percent.company_id.tax_calculation_rounding_method = 'round_per_line'
+
+ res1 = self.tax_19_percent.compute_all(22689)
+ self._check_compute_all_results(
+ 27000, # 'total_included'
+ 22689, # 'total_excluded'
+ [
+ # base, amount
+ # ---------------
+ (22689, 4311),
+ # ---------------
+ ],
+ res1
+ )
+
+ res2 = self.tax_19_percent.compute_all(9176)
+ self._check_compute_all_results(
+ 10919, # 'total_included'
+ 9176, # 'total_excluded'
+ [
+ # base , amount
+ # ---------------
+ (9176, 1743),
+ # ---------------
+ ],
+ res2
+ )
+
+ def test_rounding_tax_excluded_round_globally(self):
+ ''' Test the rounding of a 19% price excluded tax in an invoice having 22689 and 9176 as lines.
+ The decimal precision is set to zero.
+ The computation must be similar to round((22689 + 9176) * 0.19).
+ '''
+ self.tax_19_percent.company_id.tax_calculation_rounding_method = 'round_globally'
+
+ res1 = self.tax_19_percent.compute_all(22689)
+ self._check_compute_all_results(
+ 27000, # 'total_included'
+ 22689, # 'total_excluded'
+ [
+ # base, amount
+ # ---------------
+ (22689, 4310.91),
+ # ---------------
+ ],
+ res1
+ )
+
+ res2 = self.tax_19_percent.compute_all(9176)
+ self._check_compute_all_results(
+ 10919, # 'total_included'
+ 9176, # 'total_excluded'
+ [
+ # base , amount
+ # ---------------
+ (9176, 1743.44),
+ # ---------------
+ ],
+ res2
+ )
+
+ def test_rounding_tax_included_round_per_line_01(self):
+ ''' Test the rounding of a 19% price included tax in an invoice having 27000 and 10920 as lines.
+ The decimal precision is set to zero.
+ The computation must be similar to round(27000 / 1.19) + round(10920 / 1.19).
+ '''
+ self.tax_19_percent.price_include = True
+ self.tax_19_percent.company_id.currency_id.rounding = 1.0
+ self.tax_19_percent.company_id.tax_calculation_rounding_method = 'round_per_line'
+
+ res1 = self.tax_19_percent.compute_all(27000)
+ self._check_compute_all_results(
+ 27000, # 'total_included'
+ 22689, # 'total_excluded'
+ [
+ # base , amount
+ # ---------------
+ (22689, 4311),
+ # ---------------
+ ],
+ res1
+ )
+
+ res2 = self.tax_19_percent.compute_all(10920)
+ self._check_compute_all_results(
+ 10920, # 'total_included'
+ 9176, # 'total_excluded'
+ [
+ # base , amount
+ # ---------------
+ (9176, 1744),
+ # ---------------
+ ],
+ res2
+ )
+
+ def test_rounding_tax_included_round_per_line_02(self):
+ ''' Test the rounding of a 12% price included tax in an invoice having 52.50 as line.
+ The decimal precision is set to 2.
+ '''
+ self.tax_12_percent.price_include = True
+ self.tax_12_percent.company_id.currency_id.rounding = 0.01
+
+ res1 = self.tax_12_percent.compute_all(52.50)
+ self._check_compute_all_results(
+ 52.50, # 'total_included'
+ 46.88, # 'total_excluded'
+ [
+ # base , amount
+ # -------------
+ (46.88, 5.62),
+ # -------------
+ ],
+ res1
+ )
+
+ def test_rounding_tax_included_round_per_line_03(self):
+ ''' Test the rounding of a 8% and 0% price included tax in an invoice having 8 * 15.55 as line.
+ The decimal precision is set to 2.
+ '''
+ self.tax_0_percent.company_id.currency_id.rounding = 0.01
+ self.tax_0_percent.price_include = True
+ self.tax_8_percent.price_include = True
+
+ self.group_tax.children_tax_ids = [(6, 0, self.tax_0_percent.ids)]
+ self.group_tax_bis.children_tax_ids = [(6, 0, self.tax_8_percent.ids)]
+
+ res1 = (self.tax_8_percent | self.tax_0_percent).compute_all(15.55, quantity=8.0)
+ self._check_compute_all_results(
+ 124.40, # 'total_included'
+ 115.19, # 'total_excluded'
+ [
+ # base , amount
+ # -------------
+ (115.19, 9.21),
+ (115.19, 0.00),
+ # -------------
+ ],
+ res1
+ )
+
+ res2 = (self.tax_0_percent | self.tax_8_percent).compute_all(15.55, quantity=8.0)
+ self._check_compute_all_results(
+ 124.40, # 'total_included'
+ 115.19, # 'total_excluded'
+ [
+ # base , amount
+ # -------------
+ (115.19, 0.00),
+ (115.19, 9.21),
+ # -------------
+ ],
+ res2
+ )
+
+ def test_rounding_tax_included_round_per_line_04(self):
+ ''' Test the rounding of a 5% price included tax.
+ The decimal precision is set to 0.05.
+ '''
+ self.tax_5_percent.price_include = True
+ self.tax_5_percent.company_id.currency_id.rounding = 0.05
+ self.tax_5_percent.company_id.tax_calculation_rounding_method = 'round_per_line'
+
+ res1 = self.tax_5_percent.compute_all(5)
+ self._check_compute_all_results(
+ 5, # 'total_included'
+ 4.75, # 'total_excluded'
+ [
+ # base , amount
+ # ---------------
+ (4.75, 0.25),
+ # ---------------
+ ],
+ res1
+ )
+
+ res2 = self.tax_5_percent.compute_all(10)
+ self._check_compute_all_results(
+ 10, # 'total_included'
+ 9.5, # 'total_excluded'
+ [
+ # base , amount
+ # ---------------
+ (9.5, 0.5),
+ # ---------------
+ ],
+ res2
+ )
+
+ res3 = self.tax_5_percent.compute_all(50)
+ self._check_compute_all_results(
+ 50, # 'total_included'
+ 47.6, # 'total_excluded'
+ [
+ # base , amount
+ # ---------------
+ (47.6, 2.4),
+ # ---------------
+ ],
+ res3
+ )
+
+ def test_rounding_tax_included_round_globally_01(self):
+ ''' Test the rounding of a 19% price included tax in an invoice having 27000 and 10920 as lines.
+ The decimal precision is set to zero.
+ The computation must be similar to round((27000 + 10920) / 1.19).
+ '''
+ self.tax_19_percent.price_include = True
+ self.tax_19_percent.company_id.tax_calculation_rounding_method = 'round_globally'
+
+ res1 = self.tax_19_percent.compute_all(27000)
+ self._check_compute_all_results(
+ 27000, # 'total_included'
+ 22689, # 'total_excluded'
+ [
+ # base , amount
+ # ---------------
+ (22689, 4311),
+ # ---------------
+ ],
+ res1
+ )
+
+ res2 = self.tax_19_percent.compute_all(10920)
+ self._check_compute_all_results(
+ 10920, # 'total_included'
+ 9176, # 'total_excluded'
+ [
+ # base , amount
+ # ---------------
+ (9176, 1744),
+ # ---------------
+ ],
+ res2
+ )
+
+ def test_rounding_tax_included_round_globally_02(self):
+ ''' Test the rounding of a 21% price included tax in an invoice having 11.90 and 2.80 as lines.
+ The decimal precision is set to 2.
+ '''
+ self.tax_21_percent.price_include = True
+ self.tax_21_percent.company_id.currency_id.rounding = 0.01
+ self.tax_21_percent.company_id.tax_calculation_rounding_method = 'round_globally'
+
+ res1 = self.tax_21_percent.compute_all(11.90)
+ self._check_compute_all_results(
+ 11.90, # 'total_included'
+ 9.83, # 'total_excluded'
+ [
+ # base , amount
+ # ---------------
+ (9.83, 2.07),
+ # ---------------
+ ],
+ res1
+ )
+
+ res2 = self.tax_21_percent.compute_all(2.80)
+ self._check_compute_all_results(
+ 2.80, # 'total_included'
+ 2.31, # 'total_excluded'
+ [
+ # base , amount
+ # ---------------
+ (2.31, 0.49),
+ # ---------------
+ ],
+ res2
+ )
+
+ def test_rounding_tax_included_round_globally_03(self):
+ ''' Test the rounding of a 5% price included tax.
+ The decimal precision is set to 0.05.
+ '''
+ self.tax_5_percent.price_include = True
+ self.tax_5_percent.company_id.currency_id.rounding = 0.05
+ self.tax_5_percent.company_id.tax_calculation_rounding_method = 'round_globally'
+
+ res1 = self.tax_5_percent.compute_all(5)
+ self._check_compute_all_results(
+ 5, # 'total_included'
+ 4.75, # 'total_excluded'
+ [
+ # base , amount
+ # ---------------
+ (4.75, 0.25),
+ # ---------------
+ ],
+ res1
+ )
+
+ res2 = self.tax_5_percent.compute_all(10)
+ self._check_compute_all_results(
+ 10, # 'total_included'
+ 9.5, # 'total_excluded'
+ [
+ # base , amount
+ # ---------------
+ (9.50, 0.50),
+ # ---------------
+ ],
+ res2
+ )
+
+ res3 = self.tax_5_percent.compute_all(50)
+ self._check_compute_all_results(
+ 50, # 'total_included'
+ 47.6, # 'total_excluded'
+ [
+ # base , amount
+ # ---------------
+ (47.60, 2.40),
+ # ---------------
+ ],
+ res3
+ )
+
+ def test_mixing_price_included_excluded_with_affect_base(self):
+ tax_10_fix = self.env['account.tax'].create({
+ 'name': "tax_10_fix",
+ 'amount_type': 'fixed',
+ 'amount': 10.0,
+ 'include_base_amount': True,
+ })
+ tax_21 = self.env['account.tax'].create({
+ 'name': "tax_21",
+ 'amount_type': 'percent',
+ 'amount': 21.0,
+ 'price_include': True,
+ 'include_base_amount': True,
+ })
+
+ self._check_compute_all_results(
+ 1222.1, # 'total_included'
+ 1000.0, # 'total_excluded'
+ [
+ # base , amount
+ # ---------------
+ (1000.0, 10.0),
+ (1010.0, 212.1),
+ # ---------------
+ ],
+ (tax_10_fix + tax_21).compute_all(1210),
+ )
diff --git a/addons/account/tests/test_tax_report.py b/addons/account/tests/test_tax_report.py
new file mode 100644
index 00000000..23014084
--- /dev/null
+++ b/addons/account/tests/test_tax_report.py
@@ -0,0 +1,285 @@
+# -*- coding: utf-8 -*-
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests import tagged
+
+
+@tagged('post_install', '-at_install')
+class TaxReportTest(AccountTestInvoicingCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+ cls.test_country_1 = cls.env['res.country'].create({
+ 'name': "The Old World",
+ 'code': 'YY',
+ })
+
+ cls.test_country_2 = cls.env['res.country'].create({
+ 'name': "The Principality of Zeon",
+ 'code': 'ZZ',
+ })
+ cls.test_country_3 = cls.env['res.country'].create({
+ 'name': "Alagaësia",
+ 'code': 'QQ',
+ })
+
+ cls.tax_report_1 = cls.env['account.tax.report'].create({
+ 'name': "Tax report 1",
+ 'country_id': cls.test_country_1.id,
+ })
+
+ cls.tax_report_line_1_1 = cls.env['account.tax.report.line'].create({
+ 'name': "[01] Line 01",
+ 'tag_name': '01',
+ 'report_id': cls.tax_report_1.id,
+ 'sequence': 2,
+ })
+
+ cls.tax_report_line_1_2 = cls.env['account.tax.report.line'].create({
+ 'name': "[01] Line 02",
+ 'tag_name': '02',
+ 'report_id': cls.tax_report_1.id,
+ 'sequence': 3,
+ })
+
+ cls.tax_report_line_1_3 = cls.env['account.tax.report.line'].create({
+ 'name': "[03] Line 03",
+ 'tag_name': '03',
+ 'report_id': cls.tax_report_1.id,
+ 'sequence': 4,
+ })
+
+ cls.tax_report_line_1_4 = cls.env['account.tax.report.line'].create({
+ 'name': "[04] Line 04",
+ 'report_id': cls.tax_report_1.id,
+ 'sequence': 5,
+ })
+
+ cls.tax_report_line_1_5 = cls.env['account.tax.report.line'].create({
+ 'name': "[05] Line 05",
+ 'report_id': cls.tax_report_1.id,
+ 'sequence': 6,
+ })
+
+ cls.tax_report_line_1_55 = cls.env['account.tax.report.line'].create({
+ 'name': "[55] Line 55",
+ 'tag_name': '55',
+ 'report_id': cls.tax_report_1.id,
+ 'sequence': 7,
+ })
+
+ cls.tax_report_line_1_6 = cls.env['account.tax.report.line'].create({
+ 'name': "[100] Line 100",
+ 'tag_name': '100',
+ 'report_id': cls.tax_report_1.id,
+ 'sequence': 8,
+ })
+
+ cls.tax_report_2 = cls.env['account.tax.report'].create({
+ 'name': "Tax report 2",
+ 'country_id': cls.test_country_1.id,
+ })
+
+ cls.tax_report_line_2_1 = cls.env['account.tax.report.line'].create({
+ 'name': "[01] Line 01, but in report 2",
+ 'tag_name': '01',
+ 'report_id': cls.tax_report_2.id,
+ 'sequence': 1,
+ })
+
+ cls.tax_report_line_2_2 = cls.env['account.tax.report.line'].create({
+ 'name': "[02] Line 02, but in report 2",
+ 'report_id': cls.tax_report_2.id,
+ 'sequence': 2,
+ })
+
+ cls.tax_report_line_2_42 = cls.env['account.tax.report.line'].create({
+ 'name': "[42] Line 42",
+ 'tag_name': '42',
+ 'report_id': cls.tax_report_2.id,
+ 'sequence': 3,
+ })
+
+ cls.tax_report_line_2_6 = cls.env['account.tax.report.line'].create({
+ 'name': "[100] Line 100",
+ 'tag_name': '100',
+ 'report_id': cls.tax_report_2.id,
+ 'sequence': 4,
+ })
+
+ def _get_tax_tags(self, tag_name=None):
+ domain = [('country_id', '=', self.test_country_1.id), ('applicability', '=', 'taxes')]
+ if tag_name:
+ domain.append(('name', 'like', '_' + tag_name ))
+ return self.env['account.account.tag'].search(domain)
+
+ def test_write_add_tagname(self):
+ """ Adding a tag_name to a line without any should create new tags.
+ """
+ tags_before = self._get_tax_tags()
+ self.tax_report_line_2_2.tag_name = 'tournicoti'
+ tags_after = self._get_tax_tags()
+
+ self.assertEqual(len(tags_after), len(tags_before) + 2, "Two tags should have been created, +tournicoti and -tournicoti.")
+
+ def test_write_single_line_tagname(self):
+ """ Writing on the tag_name of a line with a non-null tag_name used in
+ no other line should overwrite the name of the existing tags.
+ """
+ start_tags = self._get_tax_tags()
+ original_tag_name = self.tax_report_line_1_55.tag_name
+ original_tags = self.tax_report_line_1_55.tag_ids
+ self.tax_report_line_1_55.tag_name = 'Mille sabords !'
+
+ self.assertEqual(len(self._get_tax_tags(original_tag_name)), 0, "The original tag name of the line should not correspond to any tag anymore.")
+ self.assertEqual(original_tags, self.tax_report_line_1_55.tag_ids, "The tax report line should still be linked to the same tags.")
+ self.assertEqual(len(self._get_tax_tags()), len(start_tags), "No new tag should have been created.")
+
+ def test_write_single_line_remove_tagname(self):
+ """ Setting None as the tag_name of a line with a non-null tag_name used
+ in a unique line should delete the tags, also removing all the references to it
+ from tax repartition lines and account move lines
+ """
+
+ test_tax = self.env['account.tax'].create({
+ 'name': "Test tax",
+ 'amount_type': 'percent',
+ 'amount': 25,
+ 'type_tax_use': 'sale',
+ 'invoice_repartition_line_ids': [
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ 'tag_ids': [(6, 0, self.tax_report_line_1_55.tag_ids[0].ids)],
+ }),
+ ],
+ 'refund_repartition_line_ids': [
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ }),
+
+ (0,0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'tax',
+ }),
+ ],
+ })
+
+ test_invoice = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'partner_id': self.partner_a.id,
+ 'date': '1992-12-22',
+ 'invoice_line_ids': [
+ (0, 0, {'quantity': 1, 'price_unit': 42, 'tax_ids': [(6, 0, test_tax.ids)]}),
+ ],
+ })
+ test_invoice.action_post()
+
+ self.assertTrue(any(line.tax_tag_ids == self.tax_report_line_1_55.tag_ids[0] for line in test_invoice.line_ids), "The test invoice should contain a tax line with tag 55")
+ tag_name_before = self.tax_report_line_1_55.tag_name
+ tag_nber_before = len(self._get_tax_tags())
+ self.tax_report_line_1_55.tag_name = None
+ self.assertFalse(self.tax_report_line_1_55.tag_name, "The tag name for line 55 should now be None")
+ self.assertEqual(len(self._get_tax_tags(tag_name_before)), 0, "None of the original tags for this line should be left after setting tag_name to None if no other line was using this tag_name.")
+ self.assertEqual(len(self._get_tax_tags()), tag_nber_before - 2, "No new tag should have been created, and the two that were assigned to the report line should have been removed.")
+ self.assertFalse(test_tax.mapped('invoice_repartition_line_ids.tag_ids'), "There should be no tag left on test tax's repartition lines after the removal of tag 55.")
+ self.assertFalse(test_invoice.mapped('line_ids.tax_tag_ids'), "The link between test invoice and tag 55 should have been broken. There should be no tag left on the invoice's lines.")
+
+ def test_write_multi_no_change(self):
+ """ Writing the same tag_name as they already use on a set of tax report
+ lines with the same tag_name should not do anything.
+ """
+ tags_before = self._get_tax_tags().ids
+ (self.tax_report_line_1_1 + self.tax_report_line_2_1).write({'tag_name': '01'})
+ tags_after = self._get_tax_tags().ids
+ self.assertEqual(tags_before, tags_after, "Re-assigning the same tag_name should keep the same tags.")
+
+ def test_edit_line_shared_tags(self):
+ """ Setting the tag_name of a tax report line sharing its tags with another line
+ should edit the tags' name and the tag_name of this other report line, to
+ keep consistency.
+ """
+ original_tag_name = self.tax_report_line_1_1.tag_name
+ self.tax_report_line_1_1.tag_name = 'Groucha'
+ self.assertEqual(self.tax_report_line_2_1.tag_name, self.tax_report_line_1_1.tag_name, "Modifying the tag name of a tax report line sharing it with another one should also modify the other's.")
+
+ def test_edit_multi_line_tagname_all_different_new(self):
+ """ Writing a tag_name on multiple lines with distinct tag_names should
+ delete all the former tags and replace them by new ones (also on lines
+ sharing tags with them).
+ """
+ lines = self.tax_report_line_1_1 + self.tax_report_line_2_2 + self.tax_report_line_2_42
+ previous_tag_ids = lines.mapped('tag_ids.id')
+ lines.write({'tag_name': 'crabe'})
+ new_tags = lines.mapped('tag_ids')
+
+ self.assertNotEqual(new_tags.ids, previous_tag_ids, "All the tags should have changed")
+ self.assertEqual(len(new_tags), 2, "Only two distinct tags should be assigned to all the lines after writing tag_name on them all")
+ surviving_tags = self.env['account.account.tag'].search([('id', 'in', previous_tag_ids)])
+ self.assertEqual(len(surviving_tags), 0, "All former tags should have been deleted")
+ self.assertEqual(self.tax_report_line_1_1.tag_ids, self.tax_report_line_2_1.tag_ids, "The report lines initially sharing their tag_name with the written-on lines should also have been impacted")
+
+ def test_remove_line_dependency(self):
+ """ Setting to None the tag_name of a report line sharing its tags with
+ other lines should only impact this line ; the other ones should keep their
+ link to the initial tags (their tag_name will hence differ in the end).
+ """
+ tags_before = self.tax_report_line_1_1.tag_ids
+ self.tax_report_line_1_1.tag_name = None
+ self.assertEqual(len(self.tax_report_line_1_1.tag_ids), 0, "Setting the tag_name to None should have removed the tags.")
+ self.assertEqual(self.tax_report_line_2_1.tag_ids, tags_before, "Setting tag_name to None on a line linked to another one via tag_name should break this link.")
+
+ def test_tax_report_change_country(self):
+ """ Tests that duplicating and modifying the country of a tax report works
+ as intended (countries wanting to use the tax report of another
+ country need that).
+ """
+ # Copy our first report
+ tags_before = self._get_tax_tags().ids
+ copied_report_1 = self.tax_report_1.copy()
+ copied_report_2 = self.tax_report_1.copy()
+ tags_after = self._get_tax_tags().ids
+ self.assertEqual(tags_before, tags_after, "Report duplication should not create or remove any tag")
+
+ for original, copy in zip(self.tax_report_1.get_lines_in_hierarchy(), copied_report_1.get_lines_in_hierarchy()):
+ self.assertEqual(original.tag_ids, copy.tag_ids, "Copying the lines of a tax report should keep the same tags on lines")
+
+ # Assign another country to one of the copies
+ copied_report_1.country_id = self.test_country_2
+ for original, copy in zip(self.tax_report_1.get_lines_in_hierarchy(), copied_report_1.get_lines_in_hierarchy()):
+ if original.tag_ids or copy.tag_ids:
+ self.assertNotEqual(original.tag_ids, copy.tag_ids, "Changing the country of a copied report should create brand new tags for all of its lines")
+
+ for original, copy in zip(self.tax_report_1.get_lines_in_hierarchy(), copied_report_2.get_lines_in_hierarchy()):
+ self.assertEqual(original.tag_ids, copy.tag_ids, "Changing the country of a copied report should not impact the other copies or the original report")
+
+
+ # Direclty change the country of a report without copying it first (some of its tags are shared, but not all)
+ original_report_2_tags = {line.id: line.tag_ids.ids for line in self.tax_report_2.get_lines_in_hierarchy()}
+ self.tax_report_2.country_id = self.test_country_2
+ for line in self.tax_report_2.get_lines_in_hierarchy():
+ if line == self.tax_report_line_2_42:
+ # This line is the only one of the report not sharing its tags
+ self.assertEqual(line.tag_ids.ids, original_report_2_tags[line.id], "The tax report lines not sharing their tags with any other report should keep the same tags when the country of their report is changed")
+ elif line.tag_ids or original_report_2_tags[line.id]:
+ self.assertNotEqual(line.tag_ids.ids, original_report_2_tags[line.id], "The tax report lines sharing their tags with other report should receive new tags when the country of their report is changed")
+
+ def test_unlink_report_line_tags(self):
+ """ Under certain circumstances, unlinking a tax report line should also unlink
+ the tags that are linked to it. We test those cases here.
+ """
+ def check_tags_unlink(tag_name, report_lines, unlinked, error_message):
+ report_lines.unlink()
+ surviving_tags = self._get_tax_tags(tag_name)
+ required_len = 0 if unlinked else 2 # 2 for + and - tag
+ self.assertEqual(len(surviving_tags), required_len, error_message)
+
+ check_tags_unlink('42', self.tax_report_line_2_42, True, "Unlinking one line not sharing its tags should also unlink them")
+ check_tags_unlink('01', self.tax_report_line_1_1, False, "Unlinking one line sharing its tags with others should keep the tags")
+ check_tags_unlink('100', self.tax_report_line_1_6 + self.tax_report_line_2_6, True, "Unlinkink all the lines sharing the same tags should also unlink them")
diff --git a/addons/account/tests/test_templates_consistency.py b/addons/account/tests/test_templates_consistency.py
new file mode 100644
index 00000000..ba6ec81d
--- /dev/null
+++ b/addons/account/tests/test_templates_consistency.py
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+from odoo.tests.common import TransactionCase
+
+
+class AccountingTestTemplConsistency(TransactionCase):
+ '''Test the templates consistency between some objects like account.account when account.account.template.
+ '''
+
+ def check_fields_consistency(self, model_from, model_to, exceptions=[]):
+ '''Check the consistency of fields from one model to another by comparing if all fields
+ in the model_from are present in the model_to.
+ :param model_from: The model to compare.
+ :param model_to: The compared model.
+ :param exceptions: Not copied model's fields.
+ '''
+
+ def get_fields(model, extra_domain=None):
+ # Retrieve fields to compare
+ domain = [('model', '=', model), ('state', '=', 'base'), ('related', '=', False),
+ ('compute', '=', False), ('store', '=', True)]
+ if extra_domain:
+ domain += extra_domain
+ return self.env['ir.model.fields'].search(domain)
+
+ from_fields = get_fields(model_from, extra_domain=[('name', 'not in', exceptions)])
+ to_fields_set = set([f.name for f in get_fields(model_to)])
+ for field in from_fields:
+ assert field.name in to_fields_set,\
+ 'Missing field "%s" from "%s" in model "%s".' % (field.name, model_from, model_to)
+
+ def test_account_account_fields(self):
+ '''Test fields consistency for ('account.account', 'account.account.template')
+ '''
+ self.check_fields_consistency(
+ 'account.account.template', 'account.account', exceptions=['chart_template_id', 'nocreate'])
+ self.check_fields_consistency(
+ 'account.account', 'account.account.template', exceptions=['company_id', 'deprecated', 'opening_debit', 'opening_credit', 'allowed_journal_ids', 'group_id', 'root_id', 'is_off_balance'])
+
+ def test_account_tax_fields(self):
+ '''Test fields consistency for ('account.tax', 'account.tax.template')
+ '''
+ self.check_fields_consistency('account.tax.template', 'account.tax', exceptions=['chart_template_id'])
+ self.check_fields_consistency('account.tax', 'account.tax.template', exceptions=['company_id'])
+ self.check_fields_consistency('account.tax.repartition.line.template', 'account.tax.repartition.line', exceptions=['plus_report_line_ids', 'minus_report_line_ids'])
+ self.check_fields_consistency('account.tax.repartition.line', 'account.tax.repartition.line.template', exceptions=['tag_ids', 'country_id', 'company_id', 'sequence'])
+
+ def test_fiscal_position_fields(self):
+ '''Test fields consistency for ('account.fiscal.position', 'account.fiscal.position.template')
+ '''
+ #main
+ self.check_fields_consistency('account.fiscal.position.template', 'account.fiscal.position', exceptions=['chart_template_id'])
+ self.check_fields_consistency('account.fiscal.position', 'account.fiscal.position.template', exceptions=['active', 'company_id', 'states_count'])
+ #taxes
+ self.check_fields_consistency('account.fiscal.position.tax.template', 'account.fiscal.position.tax')
+ self.check_fields_consistency('account.fiscal.position.tax', 'account.fiscal.position.tax.template')
+ #accounts
+ self.check_fields_consistency('account.fiscal.position.account.template', 'account.fiscal.position.account')
+ self.check_fields_consistency('account.fiscal.position.account', 'account.fiscal.position.account.template')
+
+ def test_reconcile_model_fields(self):
+ '''Test fields consistency for ('account.reconcile.model', 'account.reconcile.model.template')
+ '''
+ self.check_fields_consistency('account.reconcile.model.template', 'account.reconcile.model', exceptions=['chart_template_id'])
+ self.check_fields_consistency('account.reconcile.model', 'account.reconcile.model.template', exceptions=['active', 'company_id', 'past_months_limit', 'partner_mapping_line_ids'])
+ # 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'])
+
+ def test_account_group_fields(self):
+ '''Test fields consistency for ('account.group', 'account.group.template')
+ '''
+ self.check_fields_consistency('account.group', 'account.group.template', exceptions=['company_id', 'parent_path'])
+ self.check_fields_consistency('account.group.template', 'account.group', exceptions=['chart_template_id'])
diff --git a/addons/account/tests/test_tour.py b/addons/account/tests/test_tour.py
new file mode 100644
index 00000000..8dc70195
--- /dev/null
+++ b/addons/account/tests/test_tour.py
@@ -0,0 +1,15 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import odoo.tests
+
+
+@odoo.tests.tagged('post_install', '-at_install')
+class TestUi(odoo.tests.HttpCase):
+
+ def test_01_account_tour(self):
+ # This tour doesn't work with demo data on runbot
+ all_moves = self.env['account.move'].search([('move_type', '!=', 'entry')])
+ all_moves.button_draft()
+ all_moves.posted_before = False
+ all_moves.unlink()
+ self.start_tour("/web", 'account_tour', login="admin")
diff --git a/addons/account/tests/test_transfer_wizard.py b/addons/account/tests/test_transfer_wizard.py
new file mode 100644
index 00000000..eb78a3da
--- /dev/null
+++ b/addons/account/tests/test_transfer_wizard.py
@@ -0,0 +1,291 @@
+# -*- coding: utf-8 -*-
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests import tagged, Form
+import time
+
+@tagged('post_install', '-at_install')
+class TestTransferWizard(AccountTestInvoicingCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ cls.company = cls.company_data['company']
+ cls.receivable_account = cls.company_data['default_account_receivable']
+ cls.payable_account = cls.company_data['default_account_payable']
+ cls.accounts = cls.env['account.account'].search([('reconcile', '=', False), ('company_id', '=', cls.company.id)], limit=5)
+ cls.journal = cls.company_data['default_journal_misc']
+
+ # Set rate for base currency to 1
+ cls.env['res.currency.rate'].search([('company_id', '=', cls.company.id), ('currency_id', '=', cls.company.currency_id.id)]).write({'rate': 1})
+
+ # Create test currencies
+ cls.test_currency_1 = cls.env['res.currency'].create({
+ 'name': "PMK",
+ 'symbol':'P',
+ })
+
+ cls.test_currency_2 = cls.env['res.currency'].create({
+ 'name': "toto",
+ 'symbol':'To',
+ })
+
+ cls.test_currency_3 = cls.env['res.currency'].create({
+ 'name': "titi",
+ 'symbol':'Ti',
+ })
+
+ # Create test rates
+ cls.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y') + '-' + '01' + '-01',
+ 'rate': 0.5,
+ 'currency_id': cls.test_currency_1.id,
+ 'company_id': cls.company.id
+ })
+
+ cls.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y') + '-' + '01' + '-01',
+ 'rate': 2,
+ 'currency_id': cls.test_currency_2.id,
+ 'company_id': cls.company.id
+ })
+
+ cls.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y') + '-' + '01' + '-01',
+ 'rate': 10,
+ 'currency_id': cls.test_currency_3.id,
+ 'company_id': cls.company.id
+ })
+
+ # Create an account using a foreign currency
+ cls.test_currency_account = cls.env['account.account'].create({
+ 'name': 'test destination account',
+ 'code': 'test_dest_acc',
+ 'user_type_id': cls.env['ir.model.data'].xmlid_to_res_id('account.data_account_type_current_assets'),
+ 'currency_id': cls.test_currency_3.id,
+ })
+
+ # Create test account.move
+ cls.move_1 = cls.env['account.move'].create({
+ 'journal_id': cls.journal.id,
+ 'line_ids': [
+ (0, 0, {
+ 'name': "test1_1",
+ 'account_id': cls.receivable_account.id,
+ 'debit': 500,
+ }),
+ (0, 0, {
+ 'name': "test1_2",
+ 'account_id': cls.accounts[0].id,
+ 'credit': 500,
+ }),
+ (0, 0, {
+ 'name': "test1_3",
+ 'account_id': cls.accounts[0].id,
+ 'debit': 800,
+ 'partner_id': cls.partner_a.id,
+ }),
+ (0, 0, {
+ 'name': "test1_4",
+ 'account_id': cls.accounts[1].id,
+ 'credit': 500,
+ }),
+ (0, 0, {
+ 'name': "test1_5",
+ 'account_id': cls.accounts[2].id,
+ 'credit': 300,
+ 'partner_id': cls.partner_a.id,
+ }),
+ (0, 0, {
+ 'name': "test1_6",
+ 'account_id': cls.accounts[0].id,
+ 'debit': 270,
+ 'currency_id': cls.test_currency_1.id,
+ 'amount_currency': 540,
+ }),
+ (0, 0, {
+ 'name': "test1_7",
+ 'account_id': cls.accounts[1].id,
+ 'credit': 140,
+ }),
+ (0, 0, {
+ 'name': "test1_8",
+ 'account_id': cls.accounts[2].id,
+ 'credit': 160,
+ }),
+ (0, 0, {
+ 'name': "test1_9",
+ 'account_id': cls.accounts[2].id,
+ 'debit': 30,
+ 'currency_id': cls.test_currency_2.id,
+ 'amount_currency': 15,
+ }),
+ ]
+ })
+ cls.move_1.action_post()
+
+ cls.move_2 = cls.env['account.move'].create({
+ 'journal_id': cls.journal.id,
+ 'line_ids': [
+ (0, 0, {
+ 'name': "test2_1",
+ 'account_id': cls.accounts[1].id,
+ 'debit': 400,
+ }),
+ (0, 0, {
+ 'name': "test2_2",
+ 'account_id': cls.payable_account.id,
+ 'credit': 400,
+ }),
+ (0, 0, {
+ 'name': "test2_3",
+ 'account_id': cls.accounts[3].id,
+ 'debit': 250,
+ 'partner_id': cls.partner_a.id,
+ }),
+ (0, 0, {
+ 'name': "test2_4",
+ 'account_id': cls.accounts[1].id,
+ 'debit': 480,
+ 'partner_id': cls.partner_b.id,
+ }),
+ (0, 0, {
+ 'name': "test2_5",
+ 'account_id': cls.accounts[2].id,
+ 'credit': 730,
+ 'partner_id': cls.partner_a.id,
+ }),
+ (0, 0, {
+ 'name': "test2_6",
+ 'account_id': cls.accounts[2].id,
+ 'credit': 412,
+ 'partner_id': cls.partner_a.id,
+ 'currency_id': cls.test_currency_2.id,
+ 'amount_currency': -633,
+ }),
+ (0, 0, {
+ 'name': "test2_7",
+ 'account_id': cls.accounts[1].id,
+ 'debit': 572,
+ }),
+ (0, 0, {
+ 'name': "test2_8",
+ 'account_id': cls.accounts[2].id,
+ 'credit': 100,
+ 'partner_id': cls.partner_a.id,
+ 'currency_id': cls.test_currency_2.id,
+ 'amount_currency': -123,
+ }),
+ (0, 0, {
+ 'name': "test2_9",
+ 'account_id': cls.accounts[2].id,
+ 'credit': 60,
+ 'partner_id': cls.partner_a.id,
+ 'currency_id': cls.test_currency_1.id,
+ 'amount_currency': -10,
+ }),
+ ]
+ })
+ cls.move_2.action_post()
+
+
+ def test_transfer_wizard_reconcile(self):
+ """ Tests reconciliation when doing a transfer with the wizard
+ """
+ active_move_lines = (self.move_1 + self.move_2).mapped('line_ids').filtered(lambda x: x.account_id.user_type_id.type in ('receivable', 'payable'))
+
+ # We use a form to pass the context properly to the depends_context move_line_ids field
+ context = {'active_model': 'account.move.line', 'active_ids': active_move_lines.ids}
+ with Form(self.env['account.automatic.entry.wizard'].with_context(context)) as wizard_form:
+ wizard_form.action = 'change_account'
+ wizard_form.destination_account_id = self.receivable_account
+ wizard_form.journal_id = self.journal
+ wizard = wizard_form.save()
+
+ transfer_move_id = wizard.do_action()['res_id']
+ transfer_move = self.env['account.move'].browse(transfer_move_id)
+
+ payable_transfer = transfer_move.line_ids.filtered(lambda x: x.account_id == self.payable_account)
+ receivable_transfer = transfer_move.line_ids.filtered(lambda x: x.account_id == self.receivable_account)
+
+ self.assertTrue(payable_transfer.reconciled, "Payable line of the transfer move should be fully reconciled")
+ self.assertAlmostEqual(self.move_1.line_ids.filtered(lambda x: x.account_id == self.receivable_account).amount_residual, 100, self.company.currency_id.decimal_places, "Receivable line of the original move should be partially reconciled, and still have a residual amount of 100 (500 - 400 from payable account)")
+ self.assertTrue(self.move_2.line_ids.filtered(lambda x: x.account_id == self.payable_account).reconciled, "Payable line of the original move should be fully reconciled")
+ self.assertAlmostEqual(receivable_transfer.amount_residual, 0, self.company.currency_id.decimal_places, "Receivable line from the transfer move should have nothing left to reconcile")
+ self.assertAlmostEqual(payable_transfer.debit, 400, self.company.currency_id.decimal_places, "400 should have been debited from payable account to apply the transfer")
+ self.assertAlmostEqual(receivable_transfer.credit, 400, self.company.currency_id.decimal_places, "400 should have been credited to receivable account to apply the transfer")
+
+ def test_transfer_wizard_grouping(self):
+ """ Tests grouping (by account and partner) when doing a transfer with the wizard
+ """
+ active_move_lines = (self.move_1 + self.move_2).mapped('line_ids').filtered(lambda x: x.name in ('test1_3', 'test1_4', 'test1_5', 'test2_3', 'test2_4', 'test2_5', 'test2_6', 'test2_8'))
+
+ # We use a form to pass the context properly to the depends_context move_line_ids field
+ context = {'active_model': 'account.move.line', 'active_ids': active_move_lines.ids}
+ with Form(self.env['account.automatic.entry.wizard'].with_context(context)) as wizard_form:
+ wizard_form.action = 'change_account'
+ wizard_form.destination_account_id = self.accounts[4]
+ wizard_form.journal_id = self.journal
+ wizard = wizard_form.save()
+
+ transfer_move_id = wizard.do_action()['res_id']
+ transfer_move = self.env['account.move'].browse(transfer_move_id)
+
+ groups = {}
+ for line in transfer_move.line_ids:
+ key = (line.account_id, line.partner_id or None, line.currency_id)
+ self.assertFalse(groups.get(key), "There should be only one line per (account, partner, currency) group in the transfer move.")
+ groups[key] = line
+
+ self.assertAlmostEqual(groups[(self.accounts[0], self.partner_a, self.company_data['currency'])].balance, -800, self.company.currency_id.decimal_places)
+ self.assertAlmostEqual(groups[(self.accounts[1], None, self.company_data['currency'])].balance, 500, self.company.currency_id.decimal_places)
+ self.assertAlmostEqual(groups[(self.accounts[1], self.partner_b, self.company_data['currency'])].balance, -480, self.company.currency_id.decimal_places)
+ self.assertAlmostEqual(groups[(self.accounts[2], self.partner_a, self.company_data['currency'])].balance, 1030, self.company.currency_id.decimal_places)
+ self.assertAlmostEqual(groups[(self.accounts[2], self.partner_a, self.test_currency_2)].balance, 512, self.company.currency_id.decimal_places)
+ self.assertAlmostEqual(groups[(self.accounts[3], self.partner_a, self.company_data['currency'])].balance, -250, self.company.currency_id.decimal_places)
+
+
+ def test_transfer_wizard_currency_conversion(self):
+ """ Tests multi currency use of the transfer wizard, checking the conversion
+ is propperly done when using a destination account with a currency_id set.
+ """
+ active_move_lines = self.move_1.mapped('line_ids').filtered(lambda x: x.name in ('test1_6', 'test1_9'))
+
+ # We use a form to pass the context properly to the depends_context move_line_ids field
+ context = {'active_model': 'account.move.line', 'active_ids': active_move_lines.ids}
+ with Form(self.env['account.automatic.entry.wizard'].with_context(context)) as wizard_form:
+ wizard_form.action = 'change_account'
+ wizard_form.destination_account_id = self.test_currency_account
+ wizard_form.journal_id = self.journal
+ wizard = wizard_form.save()
+
+ transfer_move_id = wizard.do_action()['res_id']
+ transfer_move = self.env['account.move'].browse(transfer_move_id)
+
+ destination_line = transfer_move.line_ids.filtered(lambda x: x.account_id == self.test_currency_account)
+ self.assertEqual(destination_line.currency_id, self.test_currency_3, "Transferring to an account with a currency set should keep this currency on the transfer line.")
+ self.assertAlmostEqual(destination_line.amount_currency, 3000, self.company.currency_id.decimal_places, "Transferring two lines with different currencies (and the same partner) on an account with a currency set should convert the balance of these lines into this account's currency (here (270 + 30) * 10 = 3000)")
+
+
+ def test_transfer_wizard_no_currency_conversion(self):
+ """ Tests multi currency use of the transfer wizard, verifying that
+ currency amounts are kept on distinct lines when transferring to an
+ account without any currency specified.
+ """
+ active_move_lines = self.move_2.mapped('line_ids').filtered(lambda x: x.name in ('test2_9', 'test2_6', 'test2_8'))
+
+ # We use a form to pass the context properly to the depends_context move_line_ids field
+ context = {'active_model': 'account.move.line', 'active_ids': active_move_lines.ids}
+ with Form(self.env['account.automatic.entry.wizard'].with_context(context)) as wizard_form:
+ wizard_form.action = 'change_account'
+ wizard_form.destination_account_id = self.receivable_account
+ wizard_form.journal_id = self.journal
+ wizard = wizard_form.save()
+
+ transfer_move_id = wizard.do_action()['res_id']
+ transfer_move = self.env['account.move'].browse(transfer_move_id)
+
+ destination_lines = transfer_move.line_ids.filtered(lambda x: x.account_id == self.receivable_account)
+ self.assertEqual(len(destination_lines), 2, "Two lines should have been created on destination account: one for each currency (the lines with same partner and currency should have been aggregated)")
+ self.assertAlmostEqual(destination_lines.filtered(lambda x: x.currency_id == self.test_currency_1).amount_currency, -10, self.test_currency_1.decimal_places)
+ self.assertAlmostEqual(destination_lines.filtered(lambda x: x.currency_id == self.test_currency_2).amount_currency, -756, self.test_currency_2.decimal_places)