diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/account/tests/test_reconciliation.py | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/account/tests/test_reconciliation.py')
| -rw-r--r-- | addons/account/tests/test_reconciliation.py | 1208 |
1 files changed, 1208 insertions, 0 deletions
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") |
