From 5883f0a84cd3cd2330cc5ff34261252dacd8c5b9 Mon Sep 17 00:00:00 2001 From: Laurent Smet Date: Sat, 20 Nov 2021 10:40:34 +0000 Subject: [PATCH] [IMP] account: foreign currency reconciliation (Exchange diff entries on partials) * Previously, when reconciling journal items in multi-currencies, the exchange difference entry was created only on the full reconciliation. It will now be created directly at each partial to ensure the ratio between amount_residual_currency and amount_residual is always kept identical. * Also when reconciling two lines, one with a foreign currency and one expressed in company currency, the reconciliation is now made based on the foreign currency. ==== RATIONALE ==== This patch allows to fix the following use cases (among others) 1) When everything is expressed in foreign currency, the reconciliation is made in that currency: Suppose EUR is the foreign currency and USD is the company currency. Reconciling: L1: 120 EUR 60 USD (rate 2:1) L2: 240 EUR 80 USD (rate 3:1) ..leads to a partial of 120 EUR and min(80, 60) = 60 USD After the reconciliation, L1 is fully matched but L2 is still open with 120 EUR but only 20 USD. This is the first problem is the current reconciliation because L2 is supposed to have a rate 3:1 so the residual amount should be 120 / 3 = 40 USD. Since the rate is no longer consistent on L2, the user will probably close the reconciliation by using another line in EUR or will close manually the reconciliation with 20 USD but without any additional exchange difference journal items explaining where this unconsistency comes from. 2) When the current lines are mixing multiple currencies, the reconciliation is made using the company's currency. In some countries like Mexico, Ethiopia or Costa Rica, the customer is free to pay an invoice using the company's currency instead of the foreign one. So, suppose USD is the foreign currency and MXN is the company currency. The invoice is expressed by: L1: 120 USD 60 MXN (rate 2:1) If the customer is paying at a date in which the rate is 3:1, he is free to pay either L2: 120 USD 40 MXN (rate 3:1), either L2: 40 MXN. In the second case, he is expecting the invoice to be fully paid because its paiement is equivalent to 120 USD at the payment date. In Odoo, the second case led to an open balance of 20 MXN and the invoice was not completely paid. Even this situation could be easily fixed by a manual write-off, this makes the Mexican payment EDI very complicated to fullfil correctly because the government is expecting a complete matching between the invoice and the payment. When the customer is paying multiple invoices or the invoices are paid using multiple payments, the currently generated mexican EDI file in Odoo was wrong. ==== REFERENCES ==== Original idea suggested by hbto@vauxoo.com. Thanks for the contribution and patience of the many persons having, at some point, helped on that. github issue: https://github.com/odoo/odoo/issues/37469 closes odoo/odoo#84201 Task: 2669371 Related: odoo/enterprise#24268 Signed-off-by: Quentin De Paoli --- addons/account/models/account_move.py | 1122 ++++--- .../models/account_partial_reconcile.py | 28 +- addons/account/tests/__init__.py | 1 - addons/account/tests/common.py | 66 +- .../test_account_move_payments_widget.py | 44 +- .../tests/test_account_move_reconcile.py | 2968 +++++++++++------ .../tests/test_account_payment_register.py | 56 +- addons/account/tests/test_reconciliation.py | 1208 ------- .../wizard/account_payment_register.py | 73 +- .../tests/test_print_check.py | 4 +- .../tests/test_stockvaluation.py | 141 +- odoo/addons/base/models/res_currency.py | 5 +- 12 files changed, 2931 insertions(+), 2785 deletions(-) delete mode 100644 addons/account/tests/test_reconciliation.py diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index e8f85d38b47..bb3a83bd23c 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -1363,6 +1363,43 @@ class AccountMove(models.Model): 'line_ids.payment_id.state', 'line_ids.full_reconcile_id') def _compute_amount(self): + stored_ids = tuple(self.ids) + if stored_ids: + self.env['account.partial.reconcile'].flush(self.env['account.partial.reconcile']._fields) + self.env['account.payment'].flush(fnames=['is_matched']) + + queries = [] + for source_field, counterpart_field in (('debit', 'credit'), ('credit', 'debit')): + queries.append(f''' + SELECT + source_line.id AS source_line_id, + source_line.move_id AS source_move_id, + account_type.type AS source_line_account_type, + ARRAY_AGG(counterpart_move.reversed_entry_id) + FILTER (WHERE counterpart_move.reversed_entry_id IS NOT NULL) AS counterpart_reversed_entry_ids, + ARRAY_AGG(counterpart_move.move_type) + FILTER (WHERE counterpart_move.reversed_entry_id IS NOT NULL) AS counterpart_move_types, + COALESCE(BOOL_AND(COALESCE(pay.is_matched, FALSE)) + FILTER (WHERE counterpart_move.payment_id IS NOT NULL), TRUE) AS all_payments_matched + FROM account_partial_reconcile part + JOIN account_move_line source_line ON source_line.id = part.{source_field}_move_id + JOIN account_account account ON account.id = source_line.account_id + JOIN account_account_type account_type ON account_type.id = account.user_type_id + JOIN account_move_line counterpart_line ON counterpart_line.id = part.{counterpart_field}_move_id + JOIN account_move counterpart_move ON counterpart_move.id = counterpart_line.move_id + LEFT JOIN account_payment pay ON pay.id = counterpart_move.payment_id + WHERE source_line.move_id IN %s AND counterpart_line.move_id != source_line.move_id + GROUP BY source_line_id, source_move_id, source_line_account_type + ''') + + self._cr.execute(' UNION ALL '.join(queries), [stored_ids, stored_ids]) + + payment_data = defaultdict(lambda: []) + for row in self._cr.dictfetchall(): + payment_data[row['source_move_id']].append(row) + else: + payment_data = {} + for move in self: if move.payment_state == 'invoicing_legacy': @@ -1427,27 +1464,41 @@ class AccountMove(models.Model): currency = currencies if len(currencies) == 1 else move.company_id.currency_id - # Compute 'payment_state'. - new_pmt_state = 'not_paid' if move.move_type != 'entry' else False + # === Compute 'payment_state' === + reconciliation_vals = payment_data.get(move.id, []) + payment_state_matters = move._payment_state_matters() - if move._payment_state_matters() and move.state == 'posted': - if currency.is_zero(move.amount_residual): - reconciled_payments = move._get_reconciled_payments() - if not reconciled_payments or all(payment.is_matched for payment in reconciled_payments): - new_pmt_state = 'paid' - else: - new_pmt_state = move._get_invoice_in_payment_state() - elif currency.compare_amounts(total_to_pay, total_residual) != 0: - new_pmt_state = 'partial' + # Restrict on 'receivable'/'payable' lines for invoices/expense entries. + if payment_state_matters: + reconciliation_vals = [x for x in reconciliation_vals if x['source_line_account_type'] in ('receivable', 'payable')] - if new_pmt_state == 'paid' and move.move_type in ('in_invoice', 'out_invoice', 'entry'): - reverse_type = move.move_type == 'in_invoice' and 'in_refund' or move.move_type == 'out_invoice' and 'out_refund' or 'entry' - reverse_moves = self.env['account.move'].search([('reversed_entry_id', '=', move.id), ('state', '=', 'posted'), ('move_type', '=', reverse_type)]) + new_pmt_state = 'not_paid' + if move.state == 'posted': - # We only set 'reversed' state in cas of 1 to 1 full reconciliation with a reverse entry; otherwise, we use the regular 'paid' state - reverse_moves_full_recs = reverse_moves.mapped('line_ids.full_reconcile_id') - if reverse_moves_full_recs.mapped('reconciled_line_ids.move_id').filtered(lambda x: x not in (reverse_moves + reverse_moves_full_recs.mapped('exchange_move_id'))) == move: - new_pmt_state = 'reversed' + # Posted invoice/expense entry. + if payment_state_matters: + + if currency.is_zero(move.amount_residual): + # Check if the invoice/expense entry is fully paid or 'in_payment'. + if all(x['all_payments_matched'] for x in reconciliation_vals): + new_pmt_state = 'paid' + else: + new_pmt_state = move._get_invoice_in_payment_state() + elif reconciliation_vals: + new_pmt_state = 'partial' + + # Check if the journal entry is 'reversed' (1 on 1 full reconciliation with entries being of the opposite types) + if new_pmt_state == 'paid': + reverse_move_types = [] + for x in reconciliation_vals: + for rec_move_type, rec_reversed_entry_id in zip(x['counterpart_move_types'] or [], x['counterpart_reversed_entry_ids'] or []): + if rec_reversed_entry_id == move.id: + reverse_move_types.append(rec_move_type) + + if (move.move_type in ('in_invoice', 'in_receipt') and reverse_move_types == ['in_refund']) \ + or (move.move_type in ('out_invoice', 'out_receipt') and reverse_move_types == ['out_refund']) \ + or (move.move_type in ('entry', 'out_refund', 'in_refund') and reverse_move_types == ['entry']): + new_pmt_state = 'reversed' move.payment_state = new_pmt_state @@ -1551,7 +1602,7 @@ class AccountMove(models.Model): amount = abs(line.amount_residual_currency) else: # Different foreign currencies. - amount = move.company_currency_id._convert( + amount = line.company_currency_id._convert( abs(line.amount_residual), move.currency_id, move.company_id, @@ -4063,27 +4114,61 @@ class AccountMoveLine(models.Model): This amount will be 0 for fully reconciled lines or lines from a non-reconcilable account, the original line amount for unreconciled lines, and something in-between for partially reconciled lines. """ - for line in self: - if line.id and (line.account_id.reconcile or line.account_id.internal_type == 'liquidity'): - reconciled_balance = sum(line.matched_credit_ids.mapped('amount')) \ - - sum(line.matched_debit_ids.mapped('amount')) - reconciled_amount_currency = sum(line.matched_credit_ids.mapped('debit_amount_currency'))\ - - sum(line.matched_debit_ids.mapped('credit_amount_currency')) + need_residual_lines = self.filtered(lambda x: x.account_id.reconcile or x.account_id.internal_type == 'liquidity') + stored_lines = need_residual_lines.filtered('id') - line.amount_residual = line.balance - reconciled_balance + if stored_lines: + self.env['account.partial.reconcile'].flush(self.env['account.partial.reconcile']._fields) + self.env['res.currency'].flush(['decimal_places']) - if line.currency_id: - line.amount_residual_currency = line.amount_currency - reconciled_amount_currency - else: - line.amount_residual_currency = 0.0 + aml_ids = tuple(stored_lines.ids) + self._cr.execute(''' + SELECT + part.debit_move_id AS line_id, + 'debit' AS flag, + COALESCE(SUM(part.amount), 0.0) AS amount, + ROUND(SUM(part.debit_amount_currency), curr.decimal_places) AS amount_currency + FROM account_partial_reconcile part + JOIN res_currency curr ON curr.id = part.debit_currency_id + WHERE part.debit_move_id IN %s + GROUP BY part.debit_move_id, curr.decimal_places - line.reconciled = line.company_currency_id.is_zero(line.amount_residual) \ - and (not line.currency_id or line.currency_id.is_zero(line.amount_residual_currency)) - else: - # Must not have any reconciliation since the line is not eligible for that. - line.amount_residual = 0.0 - line.amount_residual_currency = 0.0 - line.reconciled = False + UNION ALL + + SELECT + part.credit_move_id AS line_id, + 'credit' AS flag, + COALESCE(SUM(part.amount), 0.0) AS amount, + ROUND(SUM(part.credit_amount_currency), curr.decimal_places) AS amount_currency + FROM account_partial_reconcile part + JOIN res_currency curr ON curr.id = part.credit_currency_id + WHERE part.credit_move_id IN %s + GROUP BY part.credit_move_id, curr.decimal_places + ''', [aml_ids, aml_ids]) + a = self._cr.fetchall() + amounts_map = dict(((r[0], r[1]), (r[2], r[3])) for r in a) + else: + amounts_map = {} + + # Lines that can't be reconciled with anything since the account doesn't allow that. + for line in self - need_residual_lines: + line.amount_residual = 0.0 + line.amount_residual_currency = 0.0 + line.reconciled = False + + for line in need_residual_lines: + # Since this part could be call on 'new' records, 'company_currency_id'/'currency_id' could be not set. + comp_curr = line.company_currency_id or self.env.company.currency_id + foreign_curr = line.currency_id or comp_curr + + # Retrieve the amounts in both foreign/company currencies. If the record is 'new', the amounts_map is empty. + debit_amount, debit_amount_currency = amounts_map.get((line.id, 'debit'), (0.0, 0.0)) + credit_amount, credit_amount_currency = amounts_map.get((line.id, 'credit'), (0.0, 0.0)) + + # Subtract the values from the account.partial.reconcile to compute the residual amounts. + line.amount_residual = comp_curr.round(line.balance - debit_amount + credit_amount) + line.amount_residual_currency = foreign_curr.round(line.amount_currency - debit_amount_currency + credit_amount_currency) + line.reconciled = comp_curr.is_zero(line.amount_residual) and foreign_curr.is_zero(line.amount_residual_currency) @api.depends('tax_repartition_line_id.invoice_tax_id', 'tax_repartition_line_id.refund_tax_id') def _compute_tax_line_id(self): @@ -4598,450 +4683,585 @@ class AccountMoveLine(models.Model): ''' Prepare the partials on the current journal items to perform the reconciliation. /!\ The order of records in self is important because the journal items will be reconciled using this order. - :return: A recordset of account.partial.reconcile. + :return: a tuple of 1) list of vals for partial reconcilation creation, 2) the list of vals for the exchange difference entries to be created ''' + def get_odoo_rate(line): + return recon_currency._get_conversion_rate(company_currency, recon_currency, line.company_id, line.date) + + def get_accounting_rate(line): + if company_currency.is_zero(line.balance) or line.currency_id.is_zero(line.amount_currency): + return None + else: + return abs(line.amount_currency) / abs(line.balance) + debit_lines = iter(self.filtered(lambda line: line.balance > 0.0 or line.amount_currency > 0.0)) credit_lines = iter(self.filtered(lambda line: line.balance < 0.0 or line.amount_currency < 0.0)) debit_line = None credit_line = None - debit_amount_residual = 0.0 - debit_amount_residual_currency = 0.0 - credit_amount_residual = 0.0 - credit_amount_residual_currency = 0.0 - debit_line_currency = None - credit_line_currency = None + remaining_debit_amount_curr = 0.0 + remaining_credit_amount_curr = 0.0 + remaining_debit_amount = 0.0 + remaining_credit_amount = 0.0 partials_vals_list = [] + exchange_data = {} while True: + # ==== Find the next available lines ==== + # For performance reasons, the partials are created all at once meaning the residual amounts can't be + # trusted from one iteration to another. That's the reason why all residual amounts are kept as variables + # and reduced "manually" every time we append a dictionary to 'partials_vals_list'. + # Move to the next available debit line. if not debit_line: debit_line = next(debit_lines, None) if not debit_line: break - debit_amount_residual = debit_line.amount_residual - - if debit_line.currency_id: - debit_amount_residual_currency = debit_line.amount_residual_currency - debit_line_currency = debit_line.currency_id - else: - debit_amount_residual_currency = debit_amount_residual - debit_line_currency = debit_line.company_currency_id + remaining_debit_amount_curr = debit_line.amount_residual_currency + remaining_debit_amount = debit_line.amount_residual # Move to the next available credit line. if not credit_line: credit_line = next(credit_lines, None) if not credit_line: break - credit_amount_residual = credit_line.amount_residual + remaining_credit_amount_curr = credit_line.amount_residual_currency + remaining_credit_amount = credit_line.amount_residual - if credit_line.currency_id: - credit_amount_residual_currency = credit_line.amount_residual_currency - credit_line_currency = credit_line.currency_id + # ==== Determine the currency in which the reconciliation will be done ==== + # In this part, we retrieve the residual amounts, check if they are zero or not and determine in which + # currency and at which rate the reconciliation will be done. + + company_currency = debit_line.company_currency_id + has_debit_zero_residual = debit_line.company_currency_id.is_zero(remaining_debit_amount) + has_credit_zero_residual = credit_line.company_currency_id.is_zero(remaining_credit_amount) + has_debit_zero_residual_currency = debit_line.currency_id.is_zero(remaining_debit_amount_curr) + has_credit_zero_residual_currency = credit_line.currency_id.is_zero(remaining_credit_amount_curr) + + if debit_line.currency_id == credit_line.currency_id == company_currency \ + and not has_debit_zero_residual \ + and not has_credit_zero_residual: + # Everything is expressed in company's currency and there is something left to reconcile. + recon_currency = company_currency + debit_rate = credit_rate = 1.0 + recon_debit_amount = remaining_debit_amount + recon_credit_amount = -remaining_credit_amount + elif debit_line.currency_id == company_currency \ + and not has_debit_zero_residual \ + and credit_line.currency_id != company_currency \ + and not has_credit_zero_residual_currency: + # The credit line is using a foreign currency but not the opposite line. + # In that case, convert the amount in company currency to the foreign currency one. + recon_currency = credit_line.currency_id + debit_rate = get_odoo_rate(debit_line) + credit_rate = get_accounting_rate(credit_line) + recon_debit_amount = recon_currency.round(remaining_debit_amount * debit_rate) + recon_credit_amount = -remaining_credit_amount_curr + elif debit_line.currency_id != company_currency \ + and not has_debit_zero_residual_currency \ + and credit_line.currency_id == company_currency \ + and not has_credit_zero_residual: + # The debit line is using a foreign currency but not the opposite line. + # In that case, convert the amount in company currency to the foreign currency one. + recon_currency = debit_line.currency_id + debit_rate = get_accounting_rate(debit_line) + credit_rate = get_odoo_rate(credit_line) + recon_debit_amount = remaining_debit_amount_curr + recon_credit_amount = recon_currency.round(-remaining_credit_amount * credit_rate) + elif debit_line.currency_id == credit_line.currency_id \ + and debit_line.currency_id != company_currency \ + and not has_debit_zero_residual_currency \ + and not has_credit_zero_residual_currency: + # Both lines are sharing the same foreign currency. + recon_currency = debit_line.currency_id + debit_rate = get_accounting_rate(debit_line) + credit_rate = get_accounting_rate(credit_line) + recon_debit_amount = remaining_debit_amount_curr + recon_credit_amount = -remaining_credit_amount_curr + elif debit_line.currency_id == credit_line.currency_id \ + and debit_line.currency_id != company_currency \ + and (has_debit_zero_residual_currency or has_credit_zero_residual_currency): + # Special case for exchange difference lines. In that case, both lines are sharing the same foreign + # currency but at least one has no amount in foreign currency. + # In that case, we don't want a rate for the opposite line because the exchange difference is supposed + # to reduce only the amount in company currency but not the foreign one. + recon_currency = company_currency + debit_rate = None + credit_rate = None + recon_debit_amount = remaining_debit_amount + recon_credit_amount = -remaining_credit_amount + else: + # Multiple involved foreign currencies. The reconciliation is done using the currency of the company. + recon_currency = company_currency + debit_rate = get_accounting_rate(debit_line) + credit_rate = get_accounting_rate(credit_line) + recon_debit_amount = remaining_debit_amount + recon_credit_amount = -remaining_credit_amount + + # Check if there is something left to reconcile. Move to the next loop iteration if not. + skip_reconciliation = False + if recon_currency.is_zero(recon_debit_amount): + debit_line = None + skip_reconciliation = True + if recon_currency.is_zero(recon_credit_amount): + credit_line = None + skip_reconciliation = True + if skip_reconciliation: + continue + + # ==== Match both lines together and compute amounts to reconcile ==== + + # Determine which line is fully matched by the other. + compare_amounts = recon_currency.compare_amounts(recon_debit_amount, recon_credit_amount) + min_recon_amount = min(recon_debit_amount, recon_credit_amount) + debit_fully_matched = compare_amounts <= 0 + credit_fully_matched = compare_amounts >= 0 + + # ==== Computation of partial amounts ==== + if recon_currency == company_currency: + # Compute the partial amount expressed in company currency. + partial_amount = min_recon_amount + + # Compute the partial amount expressed in foreign currency. + if debit_rate: + partial_debit_amount_currency = debit_line.currency_id.round(debit_rate * min_recon_amount) + partial_debit_amount_currency = min(partial_debit_amount_currency, remaining_debit_amount_curr) else: - credit_amount_residual_currency = credit_amount_residual - credit_line_currency = credit_line.company_currency_id - - min_amount_residual = min(debit_amount_residual, -credit_amount_residual) - has_debit_residual_left = not debit_line.company_currency_id.is_zero(debit_amount_residual) and debit_amount_residual > 0.0 - has_credit_residual_left = not credit_line.company_currency_id.is_zero(credit_amount_residual) and credit_amount_residual < 0.0 - has_debit_residual_curr_left = not debit_line_currency.is_zero(debit_amount_residual_currency) and debit_amount_residual_currency > 0.0 - has_credit_residual_curr_left = not credit_line_currency.is_zero(credit_amount_residual_currency) and credit_amount_residual_currency < 0.0 - - if debit_line_currency == credit_line_currency: - # Reconcile on the same currency. - - # The debit line is now fully reconciled because: - # - either amount_residual & amount_residual_currency are at 0. - # - either the credit_line is not an exchange difference one. - if not has_debit_residual_curr_left and (has_credit_residual_curr_left or not has_debit_residual_left): - debit_line = None - continue - - # The credit line is now fully reconciled because: - # - either amount_residual & amount_residual_currency are at 0. - # - either the debit is not an exchange difference one. - if not has_credit_residual_curr_left and (has_debit_residual_curr_left or not has_credit_residual_left): - credit_line = None - continue - - min_amount_residual_currency = min(debit_amount_residual_currency, -credit_amount_residual_currency) - min_debit_amount_residual_currency = min_amount_residual_currency - min_credit_amount_residual_currency = min_amount_residual_currency + partial_debit_amount_currency = 0.0 + if credit_rate: + partial_credit_amount_currency = credit_line.currency_id.round(credit_rate * min_recon_amount) + partial_credit_amount_currency = min(partial_credit_amount_currency, -remaining_credit_amount_curr) + else: + partial_credit_amount_currency = 0.0 else: - # Reconcile on the company's currency. + # recon_currency != company_currency + # Compute the partial amount expressed in company currency. + if debit_rate: + partial_debit_amount = company_currency.round(min_recon_amount / debit_rate) + partial_debit_amount = min(partial_debit_amount, remaining_debit_amount) + else: + partial_debit_amount = 0.0 + if credit_rate: + partial_credit_amount = company_currency.round(min_recon_amount / credit_rate) + partial_credit_amount = min(partial_credit_amount, -remaining_credit_amount) + else: + partial_credit_amount = 0.0 + partial_amount = min(partial_debit_amount, partial_credit_amount) - # The debit line is now fully reconciled since amount_residual is 0. - if not has_debit_residual_left: - debit_line = None - continue + # Compute the partial amount expressed in foreign currency. + # Take care to handle the case when a line expressed in company currency is mimicking the foreign + # currency of the opposite line. + if debit_line.currency_id == company_currency: + partial_debit_amount_currency = partial_amount + else: + partial_debit_amount_currency = min_recon_amount + if credit_line.currency_id == company_currency: + partial_credit_amount_currency = partial_amount + else: + partial_credit_amount_currency = min_recon_amount - # The credit line is now fully reconciled since amount_residual is 0. - if not has_credit_residual_left: - credit_line = None - continue + # Computation of the partial exchange difference. You can skip this part using the + # `no_exchange_difference` context key (when reconciling an exchange difference for example). + if not self._context.get('no_exchange_difference'): + exchange_lines_to_fix = self.env['account.move.line'] + amounts_list = [] + if recon_currency == company_currency: + if debit_fully_matched: + debit_exchange_amount = remaining_debit_amount_curr - partial_debit_amount_currency + if not debit_line.currency_id.is_zero(debit_exchange_amount): + exchange_lines_to_fix += debit_line + amounts_list.append({'amount_residual_currency': debit_exchange_amount}) + remaining_credit_amount_curr -= debit_exchange_amount + if credit_fully_matched: + credit_exchange_amount = remaining_credit_amount_curr + partial_credit_amount_currency + if not credit_line.currency_id.is_zero(credit_exchange_amount): + exchange_lines_to_fix += credit_line + amounts_list.append({'amount_residual_currency': credit_exchange_amount}) + remaining_credit_amount_curr += credit_exchange_amount - min_debit_amount_residual_currency = credit_line.company_currency_id._convert( - min_amount_residual, - debit_line.currency_id, - credit_line.company_id, - credit_line.date, - ) - min_credit_amount_residual_currency = debit_line.company_currency_id._convert( - min_amount_residual, - credit_line.currency_id, - debit_line.company_id, - debit_line.date, - ) + else: + if debit_fully_matched: + # Create an exchange difference on the remaining amount expressed in company's currency. + debit_exchange_amount = remaining_debit_amount - partial_amount + if not debit_line.company_currency_id.is_zero(debit_exchange_amount): + exchange_lines_to_fix += debit_line + amounts_list.append({'amount_residual': debit_exchange_amount}) + remaining_debit_amount -= debit_exchange_amount + else: + # Create an exchange difference ensuring the rate between the residual amounts expressed in + # both foreign and company's currency is still consistent regarding the rate between + # 'amount_currency' & 'balance'. + debit_exchange_amount = partial_debit_amount - partial_amount + if debit_line.company_currency_id.compare_amounts(debit_exchange_amount, 0.0) > 0: + exchange_lines_to_fix += debit_line + amounts_list.append({'amount_residual': debit_exchange_amount}) + remaining_debit_amount -= debit_exchange_amount - debit_amount_residual -= min_amount_residual - debit_amount_residual_currency -= min_debit_amount_residual_currency - credit_amount_residual += min_amount_residual - credit_amount_residual_currency += min_credit_amount_residual_currency + if credit_fully_matched: + # Create an exchange difference on the remaining amount expressed in company's currency. + credit_exchange_amount = remaining_credit_amount + partial_amount + if not credit_line.company_currency_id.is_zero(credit_exchange_amount): + exchange_lines_to_fix += credit_line + amounts_list.append({'amount_residual': credit_exchange_amount}) + remaining_credit_amount += credit_exchange_amount + else: + # Create an exchange difference ensuring the rate between the residual amounts expressed in + # both foreign and company's currency is still consistent regarding the rate between + # 'amount_currency' & 'balance'. + credit_exchange_amount = partial_amount - partial_credit_amount + if credit_line.company_currency_id.compare_amounts(credit_exchange_amount, 0.0) < 0: + exchange_lines_to_fix += credit_line + amounts_list.append({'amount_residual': credit_exchange_amount}) + remaining_credit_amount -= credit_exchange_amount + + if exchange_lines_to_fix: + exchange_vals = exchange_lines_to_fix._prepare_exchange_difference_move_vals( + amounts_list, + exchange_date=max(debit_line.date, credit_line.date), + ) + exchange_data[len(partials_vals_list)] = exchange_vals + + # ==== Create partials ==== + + remaining_debit_amount -= partial_amount + remaining_credit_amount += partial_amount + remaining_debit_amount_curr -= partial_debit_amount_currency + remaining_credit_amount_curr += partial_credit_amount_currency partials_vals_list.append({ - 'amount': min_amount_residual, - 'debit_amount_currency': min_debit_amount_residual_currency, - 'credit_amount_currency': min_credit_amount_residual_currency, + 'amount': partial_amount, + 'debit_amount_currency': partial_debit_amount_currency, + 'credit_amount_currency': partial_credit_amount_currency, 'debit_move_id': debit_line.id, 'credit_move_id': credit_line.id, }) - return partials_vals_list + if debit_fully_matched: + debit_line = None + if credit_fully_matched: + credit_line = None + return partials_vals_list, exchange_data - def _create_exchange_difference_move(self): - ''' Create the exchange difference journal entry on the current journal items. - :return: An account.move record. + def _create_reconciliation_partials(self): + '''create the partial reconciliation between all the records in self + :return: A recordset of account.partial.reconcile. ''' + partials_vals_list, exchange_data = self._prepare_reconciliation_partials() + partials = self.env['account.partial.reconcile'].create(partials_vals_list) - def _add_lines_to_exchange_difference_vals(lines, exchange_diff_move_vals): - ''' Generate the exchange difference values used to create the journal items - in order to fix the residual amounts and add them into 'exchange_diff_move_vals'. + # ==== Create exchange difference moves ==== + for index, exchange_vals in exchange_data.items(): + partials[index].exchange_move_id = self._create_exchange_difference_move(exchange_vals) - 1) When reconciled on the same foreign currency, the journal items are - fully reconciled regarding this currency but it could be not the case - of the balance that is expressed using the company's currency. In that - case, we need to create exchange difference journal items to ensure this - residual amount reaches zero. + return partials - 2) When reconciled on the company currency but having different foreign - currencies, the journal items are fully reconciled regarding the company - currency but it's not always the case for the foreign currencies. In that - case, the exchange difference journal items are created to ensure this - residual amount in foreign currency reaches zero. + def _prepare_exchange_difference_move_vals(self, amounts_list, company=None, exchange_date=None): + """ Prepare values to create later the exchange difference journal entry. + The exchange difference journal entry is there to fix the debit/credit of lines when the journal items are + fully reconciled in foreign currency. - :param lines: The account.move.lines to which fix the residual amounts. - :param exchange_diff_move_vals: The current vals of the exchange difference journal entry. - :return: A list of pair to perform the reconciliation - at the creation of the exchange difference move where 'line' - is the account.move.line to which the 'sequence'-th exchange - difference line will be reconciled with. - ''' - journal = self.env['account.journal'].browse(exchange_diff_move_vals['journal_id']) - to_reconcile = [] + :param amounts_list: A list of dict, one for each aml. + :param company: The company in case there is no aml in self. + :param exchange_date: Optional date object providing the date to consider for the exchange difference. + :return: A python dictionary containing: + * move_vals: A dictionary to be passed to the account.move.create method. + * to_reconcile: A list of tuple in order to perform the reconciliation after the move + creation. + """ + company = self.company_id or company + if not company: + return - for line in lines: - - exchange_diff_move_vals['date'] = max(exchange_diff_move_vals['date'], line.date) - - if not line.company_currency_id.is_zero(line.amount_residual): - # amount_residual_currency == 0 and amount_residual has to be fixed. - - if line.amount_residual > 0.0: - exchange_line_account = journal.company_id.expense_currency_exchange_account_id - else: - exchange_line_account = journal.company_id.income_currency_exchange_account_id - - elif line.currency_id and not line.currency_id.is_zero(line.amount_residual_currency): - # amount_residual == 0 and amount_residual_currency has to be fixed. - - if line.amount_residual_currency > 0.0: - exchange_line_account = journal.company_id.expense_currency_exchange_account_id - else: - exchange_line_account = journal.company_id.income_currency_exchange_account_id - else: - continue - - sequence = len(exchange_diff_move_vals['line_ids']) - exchange_diff_move_vals['line_ids'] += [ - (0, 0, { - 'name': _('Currency exchange rate difference'), - 'debit': -line.amount_residual if line.amount_residual < 0.0 else 0.0, - 'credit': line.amount_residual if line.amount_residual > 0.0 else 0.0, - 'amount_currency': -line.amount_residual_currency, - 'account_id': line.account_id.id, - 'currency_id': line.currency_id.id, - 'partner_id': line.partner_id.id, - 'sequence': sequence, - }), - (0, 0, { - 'name': _('Currency exchange rate difference'), - 'debit': line.amount_residual if line.amount_residual > 0.0 else 0.0, - 'credit': -line.amount_residual if line.amount_residual < 0.0 else 0.0, - 'amount_currency': line.amount_residual_currency, - 'account_id': exchange_line_account.id, - 'currency_id': line.currency_id.id, - 'partner_id': line.partner_id.id, - 'sequence': sequence + 1, - }), - ] - - to_reconcile.append((line, sequence)) - - return to_reconcile - - def _add_cash_basis_lines_to_exchange_difference_vals(lines, exchange_diff_move_vals): - ''' Generate the exchange difference values used to create the journal items - in order to fix the cash basis lines using the transfer account in a multi-currencies - environment when this account is not a reconcile one. - - When the tax cash basis journal entries are generated and all involved - transfer account set on taxes are all reconcilable, the account balance - will be reset to zero by the exchange difference journal items generated - above. However, this mechanism will not work if there is any transfer - accounts that are not reconcile and we are generating the cash basis - journal items in a foreign currency. In that specific case, we need to - generate extra journal items at the generation of the exchange difference - journal entry to ensure this balance is reset to zero and then, will not - appear on the tax report leading to erroneous tax base amount / tax amount. - - :param lines: The account.move.lines to which fix the residual amounts. - :param exchange_diff_move_vals: The current vals of the exchange difference journal entry. - ''' - for move in lines.move_id: - account_vals_to_fix = {} - - move_values = move._collect_tax_cash_basis_values() - - # The cash basis doesn't need to be handle for this move because there is another payment term - # line that is not yet fully paid. - if not move_values or not move_values['is_fully_paid']: - continue - - # ========================================================================== - # Add the balance of all tax lines of the current move in order in order - # to compute the residual amount for each of them. - # ========================================================================== - - for caba_treatment, line in move_values['to_process_lines']: - - vals = { - 'currency_id': line.currency_id.id, - 'partner_id': line.partner_id.id, - 'tax_ids': [(6, 0, line.tax_ids.ids)], - 'tax_tag_ids': [(6, 0, line.tax_tag_ids.ids)], - 'debit': line.debit, - 'credit': line.credit, - } - - if caba_treatment == 'tax': - # Tax line. - grouping_key = self.env['account.partial.reconcile']._get_cash_basis_tax_line_grouping_key_from_record(line) - if grouping_key in account_vals_to_fix: - debit = account_vals_to_fix[grouping_key]['debit'] + vals['debit'] - credit = account_vals_to_fix[grouping_key]['credit'] + vals['credit'] - balance = debit - credit - - account_vals_to_fix[grouping_key].update({ - 'debit': balance if balance > 0 else 0, - 'credit': -balance if balance < 0 else 0, - 'tax_base_amount': account_vals_to_fix[grouping_key]['tax_base_amount'] + line.tax_base_amount, - }) - else: - account_vals_to_fix[grouping_key] = { - **vals, - 'account_id': line.account_id.id, - 'tax_base_amount': line.tax_base_amount, - 'tax_repartition_line_id': line.tax_repartition_line_id.id, - } - elif caba_treatment == 'base': - # Base line. - account_to_fix = line.company_id.account_cash_basis_base_account_id - if not account_to_fix: - continue - - grouping_key = self.env['account.partial.reconcile']._get_cash_basis_base_line_grouping_key_from_record(line, account=account_to_fix) - - if grouping_key not in account_vals_to_fix: - account_vals_to_fix[grouping_key] = { - **vals, - 'account_id': account_to_fix.id, - } - else: - # Multiple base lines could share the same key, if the same - # cash basis tax is used alone on several lines of the invoices - account_vals_to_fix[grouping_key]['debit'] += vals['debit'] - account_vals_to_fix[grouping_key]['credit'] += vals['credit'] - - # ========================================================================== - # Subtract the balance of all previously generated cash basis journal entries - # in order to retrieve the residual balance of each involved transfer account. - # ========================================================================== - - cash_basis_moves = self.env['account.move'].search([('tax_cash_basis_origin_move_id', '=', move.id)]) - for line in cash_basis_moves.line_ids: - grouping_key = None - if line.tax_repartition_line_id: - # Tax line. - grouping_key = self.env['account.partial.reconcile']._get_cash_basis_tax_line_grouping_key_from_record( - line, - account=line.tax_line_id.cash_basis_transition_account_id, - ) - elif line.tax_ids: - # Base line. - grouping_key = self.env['account.partial.reconcile']._get_cash_basis_base_line_grouping_key_from_record( - line, - account=line.company_id.account_cash_basis_base_account_id, - ) - - if grouping_key not in account_vals_to_fix: - continue - - account_vals_to_fix[grouping_key]['debit'] -= line.debit - account_vals_to_fix[grouping_key]['credit'] -= line.credit - - # ========================================================================== - # Generate the exchange difference journal items: - # - to reset the balance of all transfer account to zero. - # - fix rounding issues on the tax account/base tax account. - # ========================================================================== - - for values in account_vals_to_fix.values(): - balance = values['debit'] - values['credit'] - - if move.company_currency_id.is_zero(balance): - continue - - if values.get('tax_repartition_line_id'): - # Tax line. - tax_repartition_line = self.env['account.tax.repartition.line'].browse(values['tax_repartition_line_id']) - account = tax_repartition_line.account_id or self.env['account.account'].browse(values['account_id']) - - sequence = len(exchange_diff_move_vals['line_ids']) - exchange_diff_move_vals['line_ids'] += [ - (0, 0, { - **values, - 'name': _('Currency exchange rate difference (cash basis)'), - 'debit': balance if balance > 0.0 else 0.0, - 'credit': -balance if balance < 0.0 else 0.0, - 'account_id': account.id, - 'sequence': sequence, - }), - (0, 0, { - **values, - 'name': _('Currency exchange rate difference (cash basis)'), - 'debit': -balance if balance < 0.0 else 0.0, - 'credit': balance if balance > 0.0 else 0.0, - 'account_id': values['account_id'], - 'tax_ids': [], - 'tax_tag_ids': [], - 'tax_repartition_line_id': False, - 'sequence': sequence + 1, - }), - ] - else: - # Base line. - sequence = len(exchange_diff_move_vals['line_ids']) - exchange_diff_move_vals['line_ids'] += [ - (0, 0, { - **values, - 'name': _('Currency exchange rate difference (cash basis)'), - 'debit': balance if balance > 0.0 else 0.0, - 'credit': -balance if balance < 0.0 else 0.0, - 'sequence': sequence, - }), - (0, 0, { - **values, - 'name': _('Currency exchange rate difference (cash basis)'), - 'debit': -balance if balance < 0.0 else 0.0, - 'credit': balance if balance > 0.0 else 0.0, - 'tax_ids': [], - 'tax_tag_ids': [], - 'sequence': sequence + 1, - }), - ] - - if not self: - return self.env['account.move'] - - company = self[0].company_id journal = company.currency_exchange_journal_id + expense_exchange_account = company.expense_currency_exchange_account_id + income_exchange_account = company.income_currency_exchange_account_id - exchange_diff_move_vals = { + move_vals = { 'move_type': 'entry', - 'date': date.min, + 'date': max(exchange_date or date.min, company._get_user_fiscal_lock_date()), 'journal_id': journal.id, 'line_ids': [], } + to_reconcile = [] - # Fix residual amounts. - to_reconcile = _add_lines_to_exchange_difference_vals(self, exchange_diff_move_vals) + for line, amounts in zip(self, amounts_list): + move_vals['date'] = max(move_vals['date'], line.date) - # Fix cash basis entries. - is_cash_basis_needed = self[0].account_internal_type in ('receivable', 'payable') - if is_cash_basis_needed: - _add_cash_basis_lines_to_exchange_difference_vals(self, exchange_diff_move_vals) + if 'amount_residual' in amounts: + amount_residual = amounts['amount_residual'] + amount_residual_currency = 0.0 + amount_residual_to_fix = amount_residual + if line.company_currency_id.is_zero(amount_residual): + continue + elif 'amount_residual_currency' in amounts: + amount_residual = 0.0 + amount_residual_currency = amounts['amount_residual_currency'] + amount_residual_to_fix = amount_residual_currency + if line.currency_id.is_zero(amount_residual_currency): + continue + else: + continue - # ========================================================================== - # Create move and reconcile. - # ========================================================================== + if amount_residual_to_fix > 0.0: + exchange_line_account = expense_exchange_account + else: + exchange_line_account = income_exchange_account - if exchange_diff_move_vals['line_ids']: - # Check the configuration of the exchange difference journal. - if not journal: - raise UserError(_("You should configure the 'Exchange Gain or Loss Journal' in your company settings, to manage automatically the booking of accounting entries related to differences between exchange rates.")) - if not journal.company_id.expense_currency_exchange_account_id: - raise UserError(_("You should configure the 'Loss Exchange Rate Account' in your company settings, to manage automatically the booking of accounting entries related to differences between exchange rates.")) - if not journal.company_id.income_currency_exchange_account_id.id: - raise UserError(_("You should configure the 'Gain Exchange Rate Account' in your company settings, to manage automatically the booking of accounting entries related to differences between exchange rates.")) + sequence = len(move_vals['line_ids']) + move_vals['line_ids'] += [ + Command.create({ + 'name': _('Currency exchange rate difference'), + 'debit': -amount_residual if amount_residual < 0.0 else 0.0, + 'credit': amount_residual if amount_residual > 0.0 else 0.0, + 'amount_currency': -amount_residual_currency, + 'account_id': line.account_id.id, + 'currency_id': line.currency_id.id, + 'partner_id': line.partner_id.id, + 'sequence': sequence, + }), + Command.create({ + 'name': _('Currency exchange rate difference'), + 'debit': amount_residual if amount_residual > 0.0 else 0.0, + 'credit': -amount_residual if amount_residual < 0.0 else 0.0, + 'amount_currency': amount_residual_currency, + 'account_id': exchange_line_account.id, + 'currency_id': line.currency_id.id, + 'partner_id': line.partner_id.id, + 'sequence': sequence + 1, + }), + ] + to_reconcile.append((line, sequence)) - exchange_diff_move_vals['date'] = max(exchange_diff_move_vals['date'], company._get_user_fiscal_lock_date()) + return {'move_vals': move_vals, 'to_reconcile': to_reconcile} - exchange_move = self.env['account.move'].create(exchange_diff_move_vals) - else: - return None + @api.model + def _create_exchange_difference_move(self, exchange_diff_vals): + """ Create the exchange difference journal entry on the current journal items. + :param exchange_diff_vals: The current vals of the exchange difference journal entry created by the + '_prepare_exchange_difference_move_vals' method. + :return: An account.move record. + """ + move_vals = exchange_diff_vals['move_vals'] + if not move_vals['line_ids']: + return + + # Check the configuration of the exchange difference journal. + journal = self.env['account.journal'].browse(move_vals['journal_id']) + if not journal: + raise UserError(_( + "You should configure the 'Exchange Gain or Loss Journal' in your company settings, to manage" + " automatically the booking of accounting entries related to differences between exchange rates." + )) + if not journal.company_id.expense_currency_exchange_account_id: + raise UserError(_( + "You should configure the 'Loss Exchange Rate Account' in your company settings, to manage" + " automatically the booking of accounting entries related to differences between exchange rates." + )) + if not journal.company_id.income_currency_exchange_account_id.id: + raise UserError(_( + "You should configure the 'Gain Exchange Rate Account' in your company settings, to manage" + " automatically the booking of accounting entries related to differences between exchange rates." + )) + + # Create the move. + exchange_move = self.env['account.move'].create(move_vals) + exchange_move._post(soft=False) # Reconcile lines to the newly created exchange difference journal entry by creating more partials. - partials_vals_list = [] - for source_line, sequence in to_reconcile: + for source_line, sequence in exchange_diff_vals['to_reconcile']: exchange_diff_line = exchange_move.line_ids[sequence] - - if source_line.company_currency_id.is_zero(source_line.amount_residual): - exchange_field = 'amount_residual_currency' - else: - exchange_field = 'amount_residual' - - if exchange_diff_line[exchange_field] > 0.0: - debit_line = exchange_diff_line - credit_line = source_line - else: - debit_line = source_line - credit_line = exchange_diff_line - - partials_vals_list.append({ - 'amount': abs(source_line.amount_residual), - 'debit_amount_currency': abs(debit_line.amount_residual_currency), - 'credit_amount_currency': abs(credit_line.amount_residual_currency), - 'debit_move_id': debit_line.id, - 'credit_move_id': credit_line.id, - }) - - self.env['account.partial.reconcile'].create(partials_vals_list) + (exchange_diff_line + source_line).with_context(no_exchange_difference=True).reconcile() return exchange_move + def _add_exchange_difference_cash_basis_vals(self, exchange_diff_vals): + """ Generate the exchange difference values used to create the journal items + in order to fix the cash basis lines using the transfer account in a multi-currencies + environment when this account is not a reconcile one. + + When the tax cash basis journal entries are generated and all involved + transfer account set on taxes are all reconcilable, the account balance + will be reset to zero by the exchange difference journal items generated + above. However, this mechanism will not work if there is any transfer + accounts that are not reconcile and we are generating the cash basis + journal items in a foreign currency. In that specific case, we need to + generate extra journal items at the generation of the exchange difference + journal entry to ensure this balance is reset to zero and then, will not + appear on the tax report leading to erroneous tax base amount / tax amount. + + :param exchange_diff_vals: The current vals of the exchange difference journal entry created by the + '_prepare_exchange_difference_move_vals' method. + """ + move_vals = exchange_diff_vals['move_vals'] + + for move in self.move_id: + account_vals_to_fix = {} + + move_values = move._collect_tax_cash_basis_values() + + # The cash basis doesn't need to be handled for this move because there is another payment term + # line that is not yet fully paid. + if not move_values or not move_values['is_fully_paid']: + continue + + # ========================================================================== + # Add the balance of all tax lines of the current move in order in order + # to compute the residual amount for each of them. + # ========================================================================== + + move_vals['date'] = max(move_vals['date'], move.date) + for caba_treatment, line in move_values['to_process_lines']: + + vals = { + 'currency_id': line.currency_id.id, + 'partner_id': line.partner_id.id, + 'tax_ids': [Command.set(line.tax_ids.ids)], + 'tax_tag_ids': [Command.set(line.tax_tag_ids.ids)], + 'debit': line.debit, + 'credit': line.credit, + } + + if caba_treatment == 'tax': + # Tax line. + grouping_key = self.env['account.partial.reconcile']._get_cash_basis_tax_line_grouping_key_from_record(line) + if grouping_key in account_vals_to_fix: + debit = account_vals_to_fix[grouping_key]['debit'] + vals['debit'] + credit = account_vals_to_fix[grouping_key]['credit'] + vals['credit'] + balance = debit - credit + + account_vals_to_fix[grouping_key].update({ + 'debit': balance if balance > 0 else 0, + 'credit': -balance if balance < 0 else 0, + 'tax_base_amount': account_vals_to_fix[grouping_key]['tax_base_amount'] + line.tax_base_amount, + }) + else: + account_vals_to_fix[grouping_key] = { + **vals, + 'account_id': line.account_id.id, + 'tax_base_amount': line.tax_base_amount, + 'tax_repartition_line_id': line.tax_repartition_line_id.id, + } + elif caba_treatment == 'base': + # Base line. + account_to_fix = line.company_id.account_cash_basis_base_account_id + if not account_to_fix: + continue + + grouping_key = self.env['account.partial.reconcile']._get_cash_basis_base_line_grouping_key_from_record(line, account=account_to_fix) + + if grouping_key not in account_vals_to_fix: + account_vals_to_fix[grouping_key] = { + **vals, + 'account_id': account_to_fix.id, + } + else: + # Multiple base lines could share the same key, if the same + # cash basis tax is used alone on several lines of the invoices + account_vals_to_fix[grouping_key]['debit'] += vals['debit'] + account_vals_to_fix[grouping_key]['credit'] += vals['credit'] + + # ========================================================================== + # Subtract the balance of all previously generated cash basis journal entries + # in order to retrieve the residual balance of each involved transfer account. + # ========================================================================== + + cash_basis_moves = self.env['account.move'].search([('tax_cash_basis_origin_move_id', '=', move.id)]) + for line in cash_basis_moves.line_ids: + grouping_key = None + if line.tax_repartition_line_id: + # Tax line. + grouping_key = self.env['account.partial.reconcile']._get_cash_basis_tax_line_grouping_key_from_record( + line, + account=line.tax_line_id.cash_basis_transition_account_id, + ) + elif line.tax_ids: + # Base line. + grouping_key = self.env['account.partial.reconcile']._get_cash_basis_base_line_grouping_key_from_record( + line, + account=line.company_id.account_cash_basis_base_account_id, + ) + + if grouping_key not in account_vals_to_fix: + continue + + account_vals_to_fix[grouping_key]['debit'] -= line.debit + account_vals_to_fix[grouping_key]['credit'] -= line.credit + + # ========================================================================== + # Generate the exchange difference journal items: + # - to reset the balance of all transfer account to zero. + # - fix rounding issues on the tax account/base tax account. + # ========================================================================== + + for values in account_vals_to_fix.values(): + balance = values['debit'] - values['credit'] + + if move.company_currency_id.is_zero(balance): + continue + + if values.get('tax_repartition_line_id'): + # Tax line. + tax_repartition_line = self.env['account.tax.repartition.line'].browse(values['tax_repartition_line_id']) + account = tax_repartition_line.account_id or self.env['account.account'].browse(values['account_id']) + + sequence = len(move_vals['line_ids']) + move_vals['line_ids'] += [ + Command.create({ + **values, + 'name': _('Currency exchange rate difference (cash basis)'), + 'debit': balance if balance > 0.0 else 0.0, + 'credit': -balance if balance < 0.0 else 0.0, + 'account_id': account.id, + 'sequence': sequence, + }), + Command.create({ + **values, + 'name': _('Currency exchange rate difference (cash basis)'), + 'debit': -balance if balance < 0.0 else 0.0, + 'credit': balance if balance > 0.0 else 0.0, + 'account_id': values['account_id'], + 'tax_ids': [], + 'tax_tag_ids': [], + 'tax_repartition_line_id': False, + 'sequence': sequence + 1, + }), + ] + else: + # Base line. + sequence = len(move_vals['line_ids']) + move_vals['line_ids'] += [ + Command.create({ + **values, + 'name': _('Currency exchange rate difference (cash basis)'), + 'debit': balance if balance > 0.0 else 0.0, + 'credit': -balance if balance < 0.0 else 0.0, + 'sequence': sequence, + }), + Command.create({ + **values, + 'name': _('Currency exchange rate difference (cash basis)'), + 'debit': -balance if balance < 0.0 else 0.0, + 'credit': balance if balance > 0.0 else 0.0, + 'tax_ids': [], + 'tax_tag_ids': [], + 'sequence': sequence + 1, + }), + ] + def reconcile(self): ''' Reconcile the current move lines all together. :return: A dictionary representing a summary of what has been done during the reconciliation: * partials: A recorset of all account.partial.reconcile created during the reconciliation. + * exchange_partials: A recorset of all account.partial.reconcile created during the reconciliation + with the exchange difference journal entries. * full_reconcile: An account.full.reconcile record created when there is nothing left to reconcile in the involved lines. * tax_cash_basis_moves: An account.move recordset representing the tax cash basis journal entries. ''' - results = {} + results = {'exchange_partials': self.env['account.partial.reconcile']} if not self: return results @@ -5089,11 +5309,16 @@ class AccountMoveLine(models.Model): # ==== Create partials ==== - partials = self.env['account.partial.reconcile'].create(sorted_lines._prepare_reconciliation_partials()) - - # Track newly created partials. + partial_no_exch_diff = bool(self.env['ir.config_parameter'].sudo().get_param('account.disable_partial_exchange_diff')) + sorted_lines_ctx = sorted_lines.with_context(no_exchange_difference=self._context.get('no_exchange_difference') or partial_no_exch_diff) + partials = sorted_lines_ctx._create_reconciliation_partials() results['partials'] = partials involved_partials += partials + exchange_move_lines = partials.exchange_move_id.line_ids.filtered(lambda line: line.account_id == account) + involved_lines += exchange_move_lines + exchange_diff_partials = exchange_move_lines.matched_debit_ids + exchange_move_lines.matched_credit_ids + involved_partials += exchange_diff_partials + results['exchange_partials'] += exchange_diff_partials # ==== Create entries for cash basis taxes ==== @@ -5104,32 +5329,61 @@ class AccountMoveLine(models.Model): # ==== Check if a full reconcile is needed ==== - if involved_lines[0].currency_id and all(line.currency_id == involved_lines[0].currency_id for line in involved_lines): - is_full_needed = all(line.currency_id.is_zero(line.amount_residual_currency) for line in involved_lines) - else: - is_full_needed = all(line.company_currency_id.is_zero(line.amount_residual) for line in involved_lines) + def is_line_reconciled(line): + # Check if the journal item passed as parameter is now fully reconciled. + return line.reconciled \ + or line.currency_id.is_zero(line.amount_residual_currency) \ + or line.company_currency_id.is_zero(line.amount_residual) - if is_full_needed: + if all(is_line_reconciled(line) for line in involved_lines): # ==== Create the exchange difference move ==== + # This part could be bypassed using the 'no_exchange_difference' key inside the context. This is useful + # when importing a full accounting including the reconciliation like Winbooks. - if self._context.get('no_exchange_difference'): - exchange_move = None - else: - exchange_move = involved_lines._create_exchange_difference_move() - if exchange_move: - exchange_move_lines = exchange_move.line_ids.filtered(lambda line: line.account_id == account) + exchange_move = None + if not self._context.get('no_exchange_difference'): + # In normal cases, the exchange differences are already generated by the partial at this point meaning + # there is no journal item left with a zero amount residual in one currency but not in the other. + # However, after a migration coming from an older version with an older partial reconciliation or due to + # some rounding issues (when dealing with different decimal places for example), we could need an extra + # exchange difference journal entry to handle them. + exchange_lines_to_fix = self.env['account.move.line'] + amounts_list = [] + exchange_max_date = date.min + for line in involved_lines: + if not line.company_currency_id.is_zero(line.amount_residual): + exchange_lines_to_fix += line + amounts_list.append({'amount_residual': line.amount_residual}) + elif not line.currency_id.is_zero(line.amount_residual_currency): + exchange_lines_to_fix += line + amounts_list.append({'amount_residual_currency': line.amount_residual_currency}) + exchange_max_date = max(exchange_max_date, line.date) + exchange_diff_vals = exchange_lines_to_fix._prepare_exchange_difference_move_vals( + amounts_list, + company=involved_lines[0].company_id, + exchange_date=exchange_max_date, + ) - # Track newly created lines. - involved_lines += exchange_move_lines + # Exchange difference for cash basis entries. + is_cash_basis_needed = account.internal_type in ('receivable', 'payable') + if is_cash_basis_needed: + involved_lines._add_exchange_difference_cash_basis_vals(exchange_diff_vals) - # Track newly created partials. - exchange_diff_partials = exchange_move_lines.matched_debit_ids \ - + exchange_move_lines.matched_credit_ids - involved_partials += exchange_diff_partials - results['partials'] += exchange_diff_partials + # Create the exchange difference. + if exchange_diff_vals['move_vals']['line_ids']: + exchange_move = involved_lines._create_exchange_difference_move(exchange_diff_vals) + if exchange_move: + exchange_move_lines = exchange_move.line_ids.filtered(lambda line: line.account_id == account) - exchange_move._post(soft=False) + # Track newly created lines. + involved_lines += exchange_move_lines + + # Track newly created partials. + exchange_diff_partials = exchange_move_lines.matched_debit_ids \ + + exchange_move_lines.matched_credit_ids + involved_partials += exchange_diff_partials + results['exchange_partials'] += exchange_diff_partials # ==== Create the full reconcile ==== diff --git a/addons/account/models/account_partial_reconcile.py b/addons/account/models/account_partial_reconcile.py index 07f807e710d..9ebac7e8ea3 100644 --- a/addons/account/models/account_partial_reconcile.py +++ b/addons/account/models/account_partial_reconcile.py @@ -20,6 +20,7 @@ class AccountPartialReconcile(models.Model): full_reconcile_id = fields.Many2one( comodel_name='account.full.reconcile', string="Full Reconcile", copy=False) + exchange_move_id = fields.Many2one(comodel_name='account.move') # ==== Currency fields ==== company_currency_id = fields.Many2one( @@ -112,10 +113,15 @@ class AccountPartialReconcile(models.Model): # Retrieve the CABA entries to reverse. moves_to_reverse = self.env['account.move'].search([('tax_cash_basis_rec_id', 'in', self.ids)]) + # Same for the exchange difference entries. + moves_to_reverse += self.exchange_move_id # Unlink partials before doing anything else to avoid 'Record has already been deleted' due to the recursion. res = super().unlink() + # Remove the matching numbers before reversing the moves to avoid trying to remove the full twice. + full_to_unlink.unlink() + # Reverse CABA entries. today = fields.Date.context_today(self) default_values_list = [{ @@ -124,9 +130,6 @@ class AccountPartialReconcile(models.Model): } for move in moves_to_reverse] moves_to_reverse._reverse_moves(default_values_list, cancel=True) - # Remove the matching numbers. - full_to_unlink.unlink() - return res # ------------------------------------------------------------------------- @@ -150,13 +153,14 @@ class AccountPartialReconcile(models.Model): for move in {partial.debit_move_id.move_id, partial.credit_move_id.move_id}: # Collect data about cash basis. - if move.id not in tax_cash_basis_values_per_move: - tax_cash_basis_values_per_move[move.id] = move._collect_tax_cash_basis_values() + if move.id in tax_cash_basis_values_per_move: + move_values = tax_cash_basis_values_per_move[move.id] + else: + move_values = move._collect_tax_cash_basis_values() # Nothing to process on the move. - if not tax_cash_basis_values_per_move.get(move.id): + if not move_values: continue - move_values = tax_cash_basis_values_per_move[move.id] # Check the cash basis configuration only when at least one cash basis tax entry need to be created. journal = partial.company_id.tax_cash_basis_journal_id @@ -185,9 +189,17 @@ class AccountPartialReconcile(models.Model): counterpart_line = partial.debit_move_id if move_values['currency'] == move.company_id.currency_id: + # Ignore the exchange difference. + if move.company_currency_id.is_zero(partial_amount): + continue + # Percentage made on company's currency. percentage = partial_amount / move_values['total_balance'] else: + # Ignore the exchange difference. + if move.currency_id.is_zero(partial_amount_currency): + continue + # Percentage made on foreign currency. percentage = partial_amount_currency / move_values['total_amount_currency'] @@ -205,6 +217,8 @@ class AccountPartialReconcile(models.Model): else: payment_rate = 0.0 + tax_cash_basis_values_per_move[move.id] = move_values + partial_vals = { 'partial': partial, 'percentage': percentage, diff --git a/addons/account/tests/__init__.py b/addons/account/tests/__init__.py index 03fdc4b9f43..0f599db0c0b 100644 --- a/addons/account/tests/__init__.py +++ b/addons/account/tests/__init__.py @@ -20,7 +20,6 @@ from . import test_account_invoice_report from . import test_account_move_line_tax_details 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 diff --git a/addons/account/tests/common.py b/addons/account/tests/common.py index 66e0978346a..7f01e6e8e31 100644 --- a/addons/account/tests/common.py +++ b/addons/account/tests/common.py @@ -3,10 +3,12 @@ from odoo import fields from odoo.tests.common import TransactionCase, HttpCase, tagged, Form +import json import time import base64 from lxml import etree + @tagged('post_install', '-at_install') class AccountTestInvoicingCommon(TransactionCase): @@ -412,6 +414,35 @@ class AccountTestInvoicingCommon(TransactionCase): self.assertRecordValues(sort_lines(move.invoice_line_ids.sorted()), expected_lines_values[:len(move.invoice_line_ids)]) self.assertRecordValues(move, [expected_move_values]) + def assert_invoice_outstanding_to_reconcile_widget(self, invoice, expected_amounts): + """ Check the outstanding widget before the reconciliation. + :param invoice: An invoice. + :param expected_amounts: A map -> + """ + invoice.invalidate_cache(['invoice_outstanding_credits_debits_widget']) + widget_vals = json.loads(invoice.invoice_outstanding_credits_debits_widget) + + if widget_vals: + current_amounts = {vals['move_id']: vals['amount'] for vals in widget_vals['content']} + else: + current_amounts = {} + self.assertDictEqual(current_amounts, expected_amounts) + + def assert_invoice_outstanding_reconciled_widget(self, invoice, expected_amounts): + """ Check the outstanding widget after the reconciliation. + :param invoice: An invoice. + :param expected_amounts: A map -> + """ + invoice.invalidate_cache(['invoice_payments_widget']) + widget_vals = json.loads(invoice.invoice_payments_widget) + + if widget_vals: + current_amounts = {vals['move_id']: vals['amount'] for vals in widget_vals['content']} + else: + current_amounts = {} + formatted_expected_amounts = {k: invoice.currency_id.round(v) for k, v in expected_amounts.items()} + self.assertDictEqual(current_amounts, formatted_expected_amounts) + #################################################### # Xml Comparison #################################################### @@ -676,38 +707,3 @@ class TestAccountReconciliationCommon(AccountTestInvoicingCommon): 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=None): - reconcile_param = reconcile_param or [] - 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_move_payments_widget.py b/addons/account/tests/test_account_move_payments_widget.py index b4a49515ec5..06584e26cc8 100644 --- a/addons/account/tests/test_account_move_payments_widget.py +++ b/addons/account/tests/test_account_move_payments_widget.py @@ -2,8 +2,6 @@ from odoo.addons.account.tests.common import AccountTestInvoicingCommon from odoo.tests import tagged -import json - @tagged('post_install', '-at_install') class TestAccountMovePaymentsWidget(AccountTestInvoicingCommon): @@ -71,40 +69,6 @@ class TestAccountMovePaymentsWidget(AccountTestInvoicingCommon): }) 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 -> - ''' - - # 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 # ------------------------------------------------------------------------- @@ -144,8 +108,8 @@ class TestAccountMovePaymentsWidget(AccountTestInvoicingCommon): 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) + self.assert_invoice_outstanding_to_reconcile_widget(out_invoice, expected_amounts) + self.assert_invoice_outstanding_to_reconcile_widget(in_invoice, expected_amounts) def test_outstanding_payments_foreign_currency(self): ''' Test the outstanding payments widget on invoices having a foreign currency. ''' @@ -180,5 +144,5 @@ class TestAccountMovePaymentsWidget(AccountTestInvoicingCommon): 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) + self.assert_invoice_outstanding_to_reconcile_widget(out_invoice, expected_amounts) + self.assert_invoice_outstanding_to_reconcile_widget(in_invoice, expected_amounts) diff --git a/addons/account/tests/test_account_move_reconcile.py b/addons/account/tests/test_account_move_reconcile.py index 7e25366da68..3063e0329e1 100644 --- a/addons/account/tests/test_account_move_reconcile.py +++ b/addons/account/tests/test_account_move_reconcile.py @@ -16,11 +16,15 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): 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.receivable_account = cls.company_data['default_account_receivable'] + cls.extra_receivable_account_1 = cls.copy_account(cls.receivable_account) + cls.extra_receivable_account_2 = cls.copy_account(cls.receivable_account) 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']) + cls.exch_income_account = cls.env.company.income_currency_exchange_account_id + cls.exch_expense_account = cls.env.company.expense_currency_exchange_account_id + # ==== Multi-currency setup ==== cls.currency_data_2 = cls.setup_multi_currency_data(default_values={ @@ -166,13 +170,8 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): '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 assertFullReconcileAccount(self, full_reconcile, account): + self.assertFullReconcile(full_reconcile, self.env['account.move.line'].search([('account_id', '=', account.id)])) def assertAmountsGroupByAccount(self, amount_per_account): expected_values = {account.id: (account, balance, amount_currency) for account, balance, amount_currency in amount_per_account} @@ -207,963 +206,2035 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): # during the reconciliation. # ------------------------------------------------------------------------- - def test_reconcile_single_currency(self): - account_id = self.company_data['default_account_receivable'].id - + def _create_line_for_reconciliation(self, balance, amount_currency, currency, move_date): move = self.env['account.move'].create({ 'move_type': 'entry', - 'date': '2016-01-01', + 'date': move_date, '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}), - ] + Command.create({ + 'debit': balance if balance > 0.0 else 0.0, + 'credit': -balance if balance < 0.0 else 0.0, + 'amount_currency': amount_currency, + 'account_id': self.receivable_account.id, + 'currency_id': currency.id, + }), + Command.create({ + 'debit': -balance if balance < 0.0 else 0.0, + 'credit': balance if balance > 0.0 else 0.0, + 'amount_currency': -amount_currency, + 'account_id': self.company_data['default_account_revenue'].id, + 'currency_id': currency.id, + }), + ], }) move.action_post() + line = move.line_ids.filtered(lambda x: x.account_id == self.receivable_account) - 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, [{ + 'amount_residual': balance, + 'amount_residual_currency': amount_currency, + 'reconciled': False, }]) - 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}, - ]) + return line - res = (line_1 + line_4).reconcile() + def test_full_reconcile_bunch_lines(self): + """ Test the reconciliation with multiple lines at the same time and ensure the result is always a full + reconcile whatever the number of involved currencies. + """ + comp_curr = self.company_data['currency'] + foreign_curr1 = self.currency_data['currency'] + foreign_curr2 = self.currency_data_2['currency'] - 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, - }]) + line_1 = self._create_line_for_reconciliation(1000.0, 1000.0, comp_curr, '2016-01-01') + line_2 = self._create_line_for_reconciliation(-300.0, -300.0, comp_curr, '2016-01-01') + line_3 = self._create_line_for_reconciliation(-400.0, -400.0, comp_curr, '2016-01-01') + line_4 = self._create_line_for_reconciliation(-500.0, -500.0, comp_curr, '2016-01-01') + line_5 = self._create_line_for_reconciliation(200.0, 200.0, comp_curr, '2016-01-01') + comp_curr_batch = line_1 + line_2 + line_3 + line_4 + line_5 - 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}, - ]) + line_1 = self._create_line_for_reconciliation(1200.0, 3600.0, foreign_curr1, '2016-01-01') + line_2 = self._create_line_for_reconciliation(-240.0, -480.0, foreign_curr1, '2017-01-01') + line_3 = self._create_line_for_reconciliation(-720.0, -1440.0, foreign_curr1, '2017-01-01') + line_4 = self._create_line_for_reconciliation(-1020.0, -2040.0, foreign_curr1, '2017-01-01') + line_5 = self._create_line_for_reconciliation(120.0, 360.0, foreign_curr1, '2016-01-01') + same_curr_batch = line_1 + line_2 + line_3 + line_4 + line_5 - res = (line_1 + line_5).reconcile() + line_1 = self._create_line_for_reconciliation(1200.0, 3600.0, foreign_curr1, '2016-01-01') + line_2 = self._create_line_for_reconciliation(780.0, 2340.0, foreign_curr1, '2016-01-01') + line_3 = self._create_line_for_reconciliation(-240.0, -960.0, foreign_curr2, '2017-01-01') + line_4 = self._create_line_for_reconciliation(-720.0, -2880.0, foreign_curr2, '2017-01-01') + line_5 = self._create_line_for_reconciliation(-1020.0, -4080.0, foreign_curr2, '2017-01-01') + multi_curr_batch = line_1 + line_2 + line_3 + line_4 + line_5 - 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, - }]) + for batch, sub_test_name in ( + (comp_curr_batch, "Batch in company currency"), + (same_curr_batch, "Batch in foreign currency"), + (multi_curr_batch, "Batch with multiple currencies"), + (comp_curr_batch + same_curr_batch + multi_curr_batch, "All batches"), + ): + with self.subTest(sub_test_name=sub_test_name): - 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 = batch.reconcile() + self.assertTrue(res.get('full_reconcile')) + self.assertRecordValues( + batch, + [{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}] * len(batch), + ) + batch.remove_move_reconcile() - res = (line_2 + line_5).reconcile() + def test_reconcile_lines_multiple_in_foreign_currency(self): + currency = self.currency_data['currency'] - self.assertPartialReconcile(res['partials'], [{ - 'amount': 200.0, - 'debit_amount_currency': 200.0, - 'credit_amount_currency': 200.0, + rates = (1/3, 1/2) + for rate1 in rates: + for rate2 in rates: + for rate3 in rates: + with self.subTest(rate1=rate1, rate2=rate2, rate3=rate3): + line_1 = self._create_line_for_reconciliation(120.0 * rate1, 120.0, currency, '2017-01-01') + line_2 = self._create_line_for_reconciliation(120.0 * rate2, 120.0, currency, '2017-01-01') + line_3 = self._create_line_for_reconciliation(-240.0 * rate3, -240.0, currency, '2017-01-01') + + (line_1 + line_2 + line_3).reconcile() + self.assertTrue(line_1.full_reconcile_id) + + def test_reverse_exchange_difference(self): + """ Test the reversing of the exchange difference to ensure there is no unexpected recursion inside the + 'reconcile' method. + """ + comp_curr = self.company_data['currency'] + foreign_curr = self.currency_data['currency'] + + for line1_vals, line2_vals in ( + ((60.0, 120.0, foreign_curr, '2017-01-01'), (-40.0, -120.0, foreign_curr, '2016-01-01')), + ((-60.0, -120.0, foreign_curr, '2017-01-01'), (40.0, 120.0, foreign_curr, '2016-01-01')), + ((60.0, 60.0, comp_curr, '2017-01-01'), (-40.0, -120.0, foreign_curr, '2016-01-01')), + ((-60.0, -60.0, comp_curr, '2017-01-01'), (40.0, 120.0, foreign_curr, '2016-01-01')), + ): + line1 = self._create_line_for_reconciliation(*line1_vals) + line2 = self._create_line_for_reconciliation(*line2_vals) + with self.subTest(line1_vals=line1_vals, line2_vals=line2_vals): + + res = (line1 + line2).reconcile() + + # Reconcile. + # Don't check the result since this is already checked by another tests. + exchange_diff = res['partials'].exchange_move_id + self.assertTrue(exchange_diff) + + # Unreconcile. + # A reversal is created to cancel the exchange difference journal entry. + line1.remove_move_reconcile() + reverse_exchange_diff_lines = exchange_diff.line_ids.matched_debit_ids.debit_move_id \ + + exchange_diff.line_ids.matched_credit_ids.credit_move_id + reverse_exchange_diff = reverse_exchange_diff_lines.move_id + self.assertTrue(reverse_exchange_diff) + + self.assertRecordValues(exchange_diff.line_ids + reverse_exchange_diff.line_ids, [ + {'reconciled': True}, + {'reconciled': False}, + {'reconciled': True}, + {'reconciled': False}, + ]) + + def test_reconcile_lines_corner_case_1_zero_balance_same_foreign_currency(self): + """ Test the reconciliation of lines having a zero balance but the same foreign currency. """ + currency = self.currency_data['currency'] + + line_1 = self._create_line_for_reconciliation(0.0, -0.01, currency, '2017-01-01') + line_2 = self._create_line_for_reconciliation(0.0, 0.02, currency, '2016-01-01') + + res = (line_1 + line_2).reconcile() + + self.assertRecordValues(res['partials'], [{ + 'amount': 0.0, + 'debit_amount_currency': 0.01, + 'credit_amount_currency': 0.01, 'debit_move_id': line_2.id, - 'credit_move_id': line_5.id, + 'credit_move_id': line_1.id, + 'exchange_move_id': None, }]) + self.assertTrue(res.get('full_reconcile')) + self.assertRecordValues(res['full_reconcile'].exchange_move_id, [{'date': fields.Date.from_string('2017-01-01')}]) + self.assertRecordValues(res['full_reconcile'].exchange_move_id.line_ids, [ + { + 'debit': 0.0, + 'credit': 0.0, + 'amount_currency': -0.01, + 'currency_id': currency.id, + 'account_id': line_2.account_id.id, + }, + { + 'debit': 0.0, + 'credit': 0.0, + 'amount_currency': 0.01, + 'currency_id': currency.id, + 'account_id': self.exch_expense_account.id, + }, + ]) + self.assertRecordValues(res['exchange_partials'], [{ + 'amount': 0.0, + 'debit_amount_currency': 0.01, + 'credit_amount_currency': 0.01, + 'debit_move_id': line_2.id, + 'credit_move_id': res['full_reconcile'].exchange_move_id.line_ids[0].id, + }]) + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + ]) + def test_reconcile_lines_corner_case_2_zero_amount_currency_same_foreign_currency(self): + """ Test a corner case when both lines have something to reconcile in company currency but nothing + in foreign currency. It could be due to: + - a bad usage of the `no_exchange_difference` context key + - a partial reconciliation made before migrating to this version + - some rounding error when dealing with currencies having != decimal places + - strange journal items made by the user + """ + currency = self.currency_data['currency'] + + line_1 = self._create_line_for_reconciliation(-0.01, 0.0, currency, '2017-01-01') + line_2 = self._create_line_for_reconciliation(0.02, 0.0, currency, '2016-01-01') + + res = (line_1 + line_2).reconcile() + + self.assertRecordValues(res['partials'], [{ + 'amount': 0.01, + 'debit_amount_currency': 0.0, + 'credit_amount_currency': 0.0, + 'debit_move_id': line_2.id, + 'credit_move_id': line_1.id, + 'exchange_move_id': None, + }]) + self.assertTrue(res.get('full_reconcile')) + self.assertRecordValues(res['full_reconcile'].exchange_move_id, [{'date': fields.Date.from_string('2017-01-01')}]) + self.assertRecordValues(res['full_reconcile'].exchange_move_id.line_ids, [ + { + 'debit': 0.0, + 'credit': 0.01, + 'amount_currency': 0.0, + 'currency_id': currency.id, + 'account_id': line_1.account_id.id, + }, + { + 'debit': 0.01, + 'credit': 0.0, + 'amount_currency': 0.0, + 'currency_id': currency.id, + 'account_id': self.exch_expense_account.id, + }, + ]) + self.assertRecordValues(res['exchange_partials'], [{ + 'amount': 0.01, + 'debit_amount_currency': 0.0, + 'credit_amount_currency': 0.0, + 'debit_move_id': line_2.id, + 'credit_move_id': res['full_reconcile'].exchange_move_id.line_ids[0].id, + }]) + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + ]) + + def test_reconcile_lines_corner_case_3_zero_balance_one_foreign_currency(self): + """ Test the special case when one line (credit) has a zero residual amount in company currency probably due to + some rounding issues or accumulated rounding due to multiple reconciliations. + This line is matched with another line (debit) using the company currency. + """ + comp_curr = self.company_data['currency'] + foreign_curr = self.currency_data['currency'] + + line_1 = self._create_line_for_reconciliation(-0.01, -0.01, comp_curr, '2017-01-01') + line_2 = self._create_line_for_reconciliation(0.0, 0.03, foreign_curr, '2016-01-01') + + res = (line_1 + line_2).reconcile() + + self.assertRecordValues(res['partials'], [{ + 'amount': 0.0, + 'debit_amount_currency': 0.02, + 'credit_amount_currency': 0.0, + 'debit_move_id': line_2.id, + 'credit_move_id': line_1.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2017-01-01')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ + { + 'debit': 0.01, + 'credit': 0.0, + 'amount_currency': 0.01, + 'currency_id': comp_curr.id, + 'account_id': line_1.account_id.id, + }, + { + 'debit': 0.0, + 'credit': 0.01, + 'amount_currency': -0.01, + 'currency_id': comp_curr.id, + 'account_id': self.exch_income_account.id, + }, + ]) + self.assertTrue(res.get('full_reconcile')) + self.assertRecordValues(res['full_reconcile'].exchange_move_id, [{'date': fields.Date.from_string('2017-01-01')}]) + self.assertRecordValues(res['full_reconcile'].exchange_move_id.line_ids, [ + { + 'debit': 0.0, + 'credit': 0.0, + 'amount_currency': -0.01, + 'currency_id': foreign_curr.id, + 'account_id': line_2.account_id.id, + }, + { + 'debit': 0.0, + 'credit': 0.0, + 'amount_currency': 0.01, + 'currency_id': foreign_curr.id, + 'account_id': self.exch_expense_account.id, + }, + ]) + self.assertRecordValues(res['exchange_partials'], [ + { + 'amount': 0.01, + 'debit_amount_currency': 0.01, + 'credit_amount_currency': 0.01, + 'debit_move_id': res['partials'].exchange_move_id.line_ids[0].id, + 'credit_move_id': line_1.id, + }, + { + 'amount': 0.0, + 'debit_amount_currency': 0.01, + 'credit_amount_currency': 0.01, + 'debit_move_id': line_2.id, + 'credit_move_id': res['full_reconcile'].exchange_move_id.line_ids[0].id, + }, + ]) + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + ]) + + def test_reconcile_lines_corner_case_4_zero_amount_currency_multiple_currencies(self): + foreign_curr1 = self.currency_data['currency'] + foreign_curr2 = self.currency_data_2['currency'] + + line_1 = self._create_line_for_reconciliation(-0.01, 0.0, foreign_curr2, '2017-01-01') + line_2 = self._create_line_for_reconciliation(0.01, 0.03, foreign_curr1, '2016-01-01') + + res = (line_1 + line_2).reconcile() + + self.assertRecordValues(res['partials'], [{ + 'amount': 0.01, + 'debit_amount_currency': 0.03, + 'credit_amount_currency': 0.0, + 'debit_move_id': line_2.id, + 'credit_move_id': line_1.id, + 'exchange_move_id': None, + }]) + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + ]) + + def test_reconcile_exchange_difference_on_partial_same_foreign_currency_debit_expense_partial_payment(self): + currency = self.currency_data['currency'] + + line_1 = self._create_line_for_reconciliation(60.0, 120.0, currency, '2017-01-01') + line_2 = self._create_line_for_reconciliation(-80.0, -240.0, currency, '2016-01-01') + + res = (line_1 + line_2).reconcile() + + self.assertRecordValues(res['partials'], [{ + 'amount': 40.0, + 'debit_amount_currency': 120.0, + 'credit_amount_currency': 120.0, + 'debit_move_id': line_1.id, + 'credit_move_id': line_2.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2017-01-01')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ + { + 'debit': 0.0, + 'credit': 20.0, + 'amount_currency': 0.0, + 'currency_id': currency.id, + 'account_id': line_1.account_id.id, + }, + { + 'debit': 20.0, + 'credit': 0.0, + 'amount_currency': 0.0, + 'currency_id': currency.id, + 'account_id': self.exch_expense_account.id, + }, + ]) + self.assertRecordValues(res['exchange_partials'], [{ + 'amount': 20.0, + 'debit_amount_currency': 0.0, + 'credit_amount_currency': 0.0, + 'debit_move_id': line_1.id, + 'credit_move_id': res['partials'].exchange_move_id.line_ids[0].id, + }]) + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': -40.0, 'amount_residual_currency': -120.0, 'reconciled': False}, + ]) + + def test_reconcile_exchange_difference_on_partial_same_foreign_currency_debit_income_partial_payment(self): + currency = self.currency_data['currency'] + + line_1 = self._create_line_for_reconciliation(40.0, 120.0, currency, '2016-01-01') + line_2 = self._create_line_for_reconciliation(-120.0, -240.0, currency, '2017-01-01') + + res = (line_1 + line_2).reconcile() + + self.assertRecordValues(res['partials'], [{ + 'amount': 40.0, + 'debit_amount_currency': 120.0, + 'credit_amount_currency': 120.0, + 'debit_move_id': line_1.id, + 'credit_move_id': line_2.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2017-01-01')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ + { + 'debit': 20.0, + 'credit': 0.0, + 'amount_currency': 0.0, + 'currency_id': currency.id, + 'account_id': line_2.account_id.id, + }, + { + 'debit': 0.0, + 'credit': 20.0, + 'amount_currency': 0.0, + 'currency_id': currency.id, + 'account_id': self.exch_income_account.id, + }, + ]) + self.assertRecordValues(res['exchange_partials'], [{ + 'amount': 20.0, + 'debit_amount_currency': 0.0, + 'credit_amount_currency': 0.0, + 'debit_move_id': res['partials'].exchange_move_id.line_ids[0].id, + 'credit_move_id': line_2.id, + }]) + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': -60.0, 'amount_residual_currency': -120.0, 'reconciled': False}, + ]) + + def test_reconcile_exchange_difference_on_partial_same_foreign_currency_credit_expense_partial_payment(self): + currency = self.currency_data['currency'] + + line_1 = self._create_line_for_reconciliation(-40.0, -120.0, currency, '2016-01-01') + line_2 = self._create_line_for_reconciliation(120.0, 240.0, currency, '2017-01-01') + + res = (line_1 + line_2).reconcile() + + self.assertRecordValues(res['partials'], [{ + 'amount': 40.0, + 'debit_amount_currency': 120.0, + 'credit_amount_currency': 120.0, + 'debit_move_id': line_2.id, + 'credit_move_id': line_1.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2017-01-01')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ + { + 'debit': 0.0, + 'credit': 20.0, + 'amount_currency': 0.0, + 'currency_id': currency.id, + 'account_id': line_2.account_id.id, + }, + { + 'debit': 20.0, + 'credit': 0.0, + 'amount_currency': 0.0, + 'currency_id': currency.id, + 'account_id': self.exch_expense_account.id, + }, + ]) + self.assertRecordValues(res['exchange_partials'], [{ + 'amount': 20.0, + 'debit_amount_currency': 0.0, + 'credit_amount_currency': 0.0, + 'debit_move_id': line_2.id, + 'credit_move_id': res['partials'].exchange_move_id.line_ids[0].id, + }]) + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 60.0, 'amount_residual_currency': 120.0, 'reconciled': False}, + ]) + + def test_reconcile_exchange_difference_on_partial_same_foreign_currency_credit_income_partial_payment(self): + currency = self.currency_data['currency'] + + line_1 = self._create_line_for_reconciliation(-60.0, -120.0, currency, '2017-01-01') + line_2 = self._create_line_for_reconciliation(80.0, 240.0, currency, '2016-01-01') + + res = (line_1 + line_2).reconcile() + + self.assertRecordValues(res['partials'], [{ + 'amount': 40.0, + 'debit_amount_currency': 120.0, + 'credit_amount_currency': 120.0, + 'debit_move_id': line_2.id, + 'credit_move_id': line_1.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2017-01-01')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ + { + 'debit': 20.0, + 'credit': 0.0, + 'amount_currency': 0.0, + 'currency_id': currency.id, + 'account_id': line_1.account_id.id, + }, + { + 'debit': 0.0, + 'credit': 20.0, + 'amount_currency': 0.0, + 'currency_id': currency.id, + 'account_id': self.exch_income_account.id, + }, + ]) + self.assertRecordValues(res['exchange_partials'], [{ + 'amount': 20.0, + 'debit_amount_currency': 0.0, + 'credit_amount_currency': 0.0, + 'debit_move_id': res['partials'].exchange_move_id.line_ids[0].id, + 'credit_move_id': line_1.id, + }]) + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 40.0, 'amount_residual_currency': 120.0, 'reconciled': False}, + ]) + + def test_reconcile_exchange_difference_on_partial_one_debit_foreign_currency_debit_expense_partial_payment(self): + comp_curr = self.company_data['currency'] + foreign_curr = self.currency_data['currency'] + + line_1 = self._create_line_for_reconciliation(60.0, 120.0, foreign_curr, '2017-01-01') + line_2 = self._create_line_for_reconciliation(-80.0, -80.0, comp_curr, '2016-01-01') + + res = (line_1 + line_2).reconcile() + + self.assertRecordValues(res['partials'], [{ + 'amount': 40.0, + 'debit_amount_currency': 120.0, + 'credit_amount_currency': 40.0, + 'debit_move_id': line_1.id, + 'credit_move_id': line_2.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2017-01-01')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ + { + 'debit': 0.0, + 'credit': 20.0, + 'amount_currency': 0.0, + 'currency_id': foreign_curr.id, + 'account_id': line_1.account_id.id, + }, + { + 'debit': 20.0, + 'credit': 0.0, + 'amount_currency': 0.0, + 'currency_id': foreign_curr.id, + 'account_id': self.exch_expense_account.id, + }, + ]) + self.assertRecordValues(res['exchange_partials'], [{ + 'amount': 20.0, + 'debit_amount_currency': 0.0, + 'credit_amount_currency': 0.0, + 'debit_move_id': line_1.id, + 'credit_move_id': res['partials'].exchange_move_id.line_ids[0].id, + }]) + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': -40.0, 'amount_residual_currency': -40.0, 'reconciled': False}, + ]) + + def test_reconcile_exchange_difference_on_partial_one_credit_foreign_currency_debit_expense_partial_payment(self): + comp_curr = self.company_data['currency'] + foreign_curr = self.currency_data['currency'] + + line_1 = self._create_line_for_reconciliation(60.0, 60.0, comp_curr, '2017-01-01') + line_2 = self._create_line_for_reconciliation(-80.0, -240.0, foreign_curr, '2016-01-01') + + res = (line_1 + line_2).reconcile() + + self.assertRecordValues(res['partials'], [{ + 'amount': 40.0, + 'debit_amount_currency': 40.0, + 'credit_amount_currency': 120.0, + 'debit_move_id': line_1.id, + 'credit_move_id': line_2.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2017-01-01')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ + { + 'debit': 0.0, + 'credit': 20.0, + 'amount_currency': -20.0, + 'currency_id': comp_curr.id, + 'account_id': line_1.account_id.id, + }, + { + 'debit': 20.0, + 'credit': 0.0, + 'amount_currency': 20.0, + 'currency_id': comp_curr.id, + 'account_id': self.exch_expense_account.id, + }, + ]) + self.assertRecordValues(res['exchange_partials'], [{ + 'amount': 20.0, + 'debit_amount_currency': 20.0, + 'credit_amount_currency': 20.0, + 'debit_move_id': line_1.id, + 'credit_move_id': res['partials'].exchange_move_id.line_ids[0].id, + }]) + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': -40.0, 'amount_residual_currency': -120.0, 'reconciled': False}, + ]) + + def test_reconcile_exchange_difference_on_partial_one_debit_foreign_currency_debit_income_partial_payment(self): + comp_curr = self.company_data['currency'] + foreign_curr = self.currency_data['currency'] + + line_1 = self._create_line_for_reconciliation(40.0, 120.0, foreign_curr, '2016-01-01') + line_2 = self._create_line_for_reconciliation(-120.0, -120.0, comp_curr, '2017-01-01') + + res = (line_1 + line_2).reconcile() + + self.assertRecordValues(res['partials'], [{ + 'amount': 40.0, + 'debit_amount_currency': 120.0, + 'credit_amount_currency': 40.0, + 'debit_move_id': line_1.id, + 'credit_move_id': line_2.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2017-01-01')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ + { + 'debit': 20.0, + 'credit': 0.0, + 'amount_currency': 20.0, + 'currency_id': comp_curr.id, + 'account_id': line_2.account_id.id, + }, + { + 'debit': 0.0, + 'credit': 20.0, + 'amount_currency': -20.0, + 'currency_id': comp_curr.id, + 'account_id': self.exch_income_account.id, + }, + ]) + self.assertRecordValues(res['exchange_partials'], [{ + 'amount': 20.0, + 'debit_amount_currency': 20.0, + 'credit_amount_currency': 20.0, + 'debit_move_id': res['partials'].exchange_move_id.line_ids[0].id, + 'credit_move_id': line_2.id, + }]) + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': -60.0, 'amount_residual_currency': -60.0, 'reconciled': False}, + ]) + + def test_reconcile_exchange_difference_on_partial_one_credit_foreign_currency_debit_income_partial_payment(self): + comp_curr = self.company_data['currency'] + foreign_curr = self.currency_data['currency'] + + line_1 = self._create_line_for_reconciliation(40.0, 40.0, comp_curr, '2016-01-01') + line_2 = self._create_line_for_reconciliation(-120.0, -240.0, foreign_curr, '2017-01-01') + + res = (line_1 + line_2).reconcile() + + self.assertRecordValues(res['partials'], [{ + 'amount': 40.0, + 'debit_amount_currency': 40.0, + 'credit_amount_currency': 120.0, + 'debit_move_id': line_1.id, + 'credit_move_id': line_2.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2017-01-01')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ + { + 'debit': 20.0, + 'credit': 0.0, + 'amount_currency': 0.0, + 'currency_id': foreign_curr.id, + 'account_id': line_2.account_id.id, + }, + { + 'debit': 0.0, + 'credit': 20.0, + 'amount_currency': 0.0, + 'currency_id': foreign_curr.id, + 'account_id': self.exch_income_account.id, + }, + ]) + self.assertRecordValues(res['exchange_partials'], [{ + 'amount': 20.0, + 'debit_amount_currency': 0.0, + 'credit_amount_currency': 0.0, + 'debit_move_id': res['partials'].exchange_move_id.line_ids[0].id, + 'credit_move_id': line_2.id, + }]) + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': -60.0, 'amount_residual_currency': -120.0, 'reconciled': False}, + ]) + + def test_reconcile_exchange_difference_on_partial_one_credit_foreign_currency_credit_expense_partial_payment(self): + comp_curr = self.company_data['currency'] + foreign_curr = self.currency_data['currency'] + + line_1 = self._create_line_for_reconciliation(-40.0, -120.0, foreign_curr, '2016-01-01') + line_2 = self._create_line_for_reconciliation(120.0, 120.0, comp_curr, '2017-01-01') + + res = (line_1 + line_2).reconcile() + + self.assertRecordValues(res['partials'], [{ + 'amount': 40.0, + 'debit_amount_currency': 40.0, + 'credit_amount_currency': 120.0, + 'debit_move_id': line_2.id, + 'credit_move_id': line_1.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2017-01-01')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ + { + 'debit': 0.0, + 'credit': 20.0, + 'amount_currency': -20.0, + 'currency_id': comp_curr.id, + 'account_id': line_2.account_id.id, + }, + { + 'debit': 20.0, + 'credit': 0.0, + 'amount_currency': 20.0, + 'currency_id': comp_curr.id, + 'account_id': self.exch_expense_account.id, + }, + ]) + self.assertRecordValues(res['exchange_partials'], [{ + 'amount': 20.0, + 'debit_amount_currency': 20.0, + 'credit_amount_currency': 20.0, + 'debit_move_id': line_2.id, + 'credit_move_id': res['partials'].exchange_move_id.line_ids[0].id, + }]) + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 60.0, 'amount_residual_currency': 60.0, 'reconciled': False}, + ]) + + def test_reconcile_exchange_difference_on_partial_one_debit_foreign_currency_credit_expense_partial_payment(self): + comp_curr = self.company_data['currency'] + foreign_curr = self.currency_data['currency'] + + line_1 = self._create_line_for_reconciliation(-40.0, -40.0, comp_curr, '2016-01-01') + line_2 = self._create_line_for_reconciliation(120.0, 240.0, foreign_curr, '2017-01-01') + + res = (line_1 + line_2).reconcile() + + self.assertRecordValues(res['partials'], [{ + 'amount': 40.0, + 'debit_amount_currency': 120.0, + 'credit_amount_currency': 40.0, + 'debit_move_id': line_2.id, + 'credit_move_id': line_1.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2017-01-01')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ + { + 'debit': 0.0, + 'credit': 20.0, + 'amount_currency': 0.0, + 'currency_id': foreign_curr.id, + 'account_id': line_2.account_id.id, + }, + { + 'debit': 20.0, + 'credit': 0.0, + 'amount_currency': 0.0, + 'currency_id': foreign_curr.id, + 'account_id': self.exch_expense_account.id, + }, + ]) + self.assertRecordValues(res['exchange_partials'], [{ + 'amount': 20.0, + 'debit_amount_currency': 0.0, + 'credit_amount_currency': 0.0, + 'debit_move_id': line_2.id, + 'credit_move_id': res['partials'].exchange_move_id.line_ids[0].id, + }]) + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 60.0, 'amount_residual_currency': 120.0, 'reconciled': False}, + ]) + + def test_reconcile_exchange_difference_on_partial_one_debit_foreign_currency_credit_income_partial_payment(self): + comp_curr = self.company_data['currency'] + foreign_curr = self.currency_data['currency'] + + line_1 = self._create_line_for_reconciliation(-60.0, -60.0, comp_curr, '2017-01-01') + line_2 = self._create_line_for_reconciliation(80.0, 240.0, foreign_curr, '2016-01-01') + + res = (line_1 + line_2).reconcile() + + self.assertRecordValues(res['partials'], [{ + 'amount': 40.0, + 'debit_amount_currency': 120.0, + 'credit_amount_currency': 40.0, + 'debit_move_id': line_2.id, + 'credit_move_id': line_1.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2017-01-01')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ + { + 'debit': 20.0, + 'credit': 0.0, + 'amount_currency': 20.0, + 'currency_id': comp_curr.id, + 'account_id': line_1.account_id.id, + }, + { + 'debit': 0.0, + 'credit': 20.0, + 'amount_currency': -20.0, + 'currency_id': comp_curr.id, + 'account_id': self.exch_income_account.id, + }, + ]) + self.assertRecordValues(res['exchange_partials'], [{ + 'amount': 20.0, + 'debit_amount_currency': 20.0, + 'credit_amount_currency': 20.0, + 'debit_move_id': res['partials'].exchange_move_id.line_ids[0].id, + 'credit_move_id': line_1.id, + }]) + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 40.0, 'amount_residual_currency': 120.0, 'reconciled': False}, + ]) + + def test_reconcile_exchange_difference_on_partial_one_credit_foreign_currency_credit_income_partial_payment(self): + comp_curr = self.company_data['currency'] + foreign_curr = self.currency_data['currency'] + + line_1 = self._create_line_for_reconciliation(-60.0, -120.0, foreign_curr, '2017-01-01') + line_2 = self._create_line_for_reconciliation(80.0, 80.0, comp_curr, '2016-01-01') + + res = (line_1 + line_2).reconcile() + + self.assertRecordValues(res['partials'], [{ + 'amount': 40.0, + 'debit_amount_currency': 40.0, + 'credit_amount_currency': 120.0, + 'debit_move_id': line_2.id, + 'credit_move_id': line_1.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2017-01-01')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ + { + 'debit': 20.0, + 'credit': 0.0, + 'amount_currency': 0.0, + 'currency_id': foreign_curr.id, + 'account_id': line_1.account_id.id, + }, + { + 'debit': 0.0, + 'credit': 20.0, + 'amount_currency': 0.0, + 'currency_id': foreign_curr.id, + 'account_id': self.exch_income_account.id, + }, + ]) + self.assertRecordValues(res['exchange_partials'], [{ + 'amount': 20.0, + 'debit_amount_currency': 0.0, + 'credit_amount_currency': 0.0, + 'debit_move_id': res['partials'].exchange_move_id.line_ids[0].id, + 'credit_move_id': line_1.id, + }]) + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 40.0, 'amount_residual_currency': 40.0, 'reconciled': False}, + ]) + + def test_reconcile_exchange_difference_on_partial_same_foreign_currency_debit_expense_full_payment(self): + currency = self.currency_data['currency'] + + line_1 = self._create_line_for_reconciliation(60.0, 120.0, currency, '2017-01-01') + line_2 = self._create_line_for_reconciliation(-40.0, -120.0, currency, '2016-01-01') + + res = (line_1 + line_2).reconcile() + + self.assertTrue(res.get('full_reconcile')) 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, + self.assertRecordValues(res['partials'], [{ + 'amount': 40.0, + 'debit_amount_currency': 120.0, + 'credit_amount_currency': 120.0, 'debit_move_id': line_1.id, - 'credit_move_id': line_5.id, + 'credit_move_id': line_2.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: + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2017-01-01')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ { - 'debit': 660.0, + 'debit': 0.0, + 'credit': 20.0, + 'amount_currency': 0.0, + 'currency_id': currency.id, + 'account_id': line_1.account_id.id, + }, + { + 'debit': 20.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, + 'currency_id': currency.id, + 'account_id': self.exch_expense_account.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, + self.assertRecordValues(res['exchange_partials'], [{ + 'amount': 20.0, + 'debit_amount_currency': 0.0, + 'credit_amount_currency': 0.0, 'debit_move_id': line_1.id, - 'credit_move_id': line_5.id, + 'credit_move_id': res['partials'].exchange_move_id.line_ids[0].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}, + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, ]) - res = (line_2 + line_5).reconcile() + def test_reconcile_exchange_difference_on_partial_same_foreign_currency_debit_income_full_payment(self): + currency = self.currency_data['currency'] - 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)) + line_1 = self._create_line_for_reconciliation(40.0, 120.0, currency, '2016-01-01') + line_2 = self._create_line_for_reconciliation(-60.0, -120.0, currency, '2017-01-01') - self.assertRecordValues(exchange_diff_lines, [ - # Fix line_2: + res = (line_1 + line_2).reconcile() + + self.assertTrue(res.get('full_reconcile')) + self.assertRecordValues(res['full_reconcile'], [{'exchange_move_id': False}]) + self.assertRecordValues(res['partials'], [{ + 'amount': 40.0, + 'debit_amount_currency': 120.0, + 'credit_amount_currency': 120.0, + 'debit_move_id': line_1.id, + 'credit_move_id': line_2.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2017-01-01')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ { - 'debit': 0.0, + 'debit': 20.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, + 'amount_currency': 0.0, + 'currency_id': currency.id, + 'account_id': line_2.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, + 'credit': 20.0, + 'amount_currency': 0.0, + 'currency_id': currency.id, + 'account_id': self.exch_income_account.id, }, ]) + self.assertRecordValues(res['exchange_partials'], [{ + 'amount': 20.0, + 'debit_amount_currency': 0.0, + 'credit_amount_currency': 0.0, + 'debit_move_id': res['partials'].exchange_move_id.line_ids[0].id, + 'credit_move_id': line_2.id, + }]) + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + ]) - self.assertPartialReconcile(res['partials'], [ - # Partial fixing line_3 (exchange difference): + def test_reconcile_exchange_difference_on_partial_same_foreign_currency_credit_expense_full_payment(self): + currency = self.currency_data['currency'] + + line_1 = self._create_line_for_reconciliation(-40.0, -120.0, currency, '2016-01-01') + line_2 = self._create_line_for_reconciliation(60.0, 120.0, currency, '2017-01-01') + + res = (line_1 + line_2).reconcile() + + self.assertTrue(res.get('full_reconcile')) + self.assertRecordValues(res['full_reconcile'], [{'exchange_move_id': False}]) + self.assertRecordValues(res['partials'], [{ + 'amount': 40.0, + 'debit_amount_currency': 120.0, + 'credit_amount_currency': 120.0, + 'debit_move_id': line_2.id, + 'credit_move_id': line_1.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2017-01-01')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ { - '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, + 'debit': 0.0, + 'credit': 20.0, + 'amount_currency': 0.0, + 'currency_id': currency.id, + 'account_id': line_2.account_id.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, + 'debit': 20.0, + 'credit': 0.0, + 'amount_currency': 0.0, + 'currency_id': currency.id, + 'account_id': self.exch_expense_account.id, }, - # Partial fixing line_1 (exchange difference): + ]) + self.assertRecordValues(res['exchange_partials'], [{ + 'amount': 20.0, + 'debit_amount_currency': 0.0, + 'credit_amount_currency': 0.0, + 'debit_move_id': line_2.id, + 'credit_move_id': res['partials'].exchange_move_id.line_ids[0].id, + }]) + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + ]) + + def test_reconcile_exchange_difference_on_partial_same_foreign_currency_credit_income_full_payment(self): + currency = self.currency_data['currency'] + + line_1 = self._create_line_for_reconciliation(-60.0, -120.0, currency, '2017-01-01') + line_2 = self._create_line_for_reconciliation(40.0, 120.0, currency, '2016-01-01') + + res = (line_1 + line_2).reconcile() + + self.assertTrue(res.get('full_reconcile')) + self.assertRecordValues(res['full_reconcile'], [{'exchange_move_id': False}]) + self.assertRecordValues(res['partials'], [{ + 'amount': 40.0, + 'debit_amount_currency': 120.0, + 'credit_amount_currency': 120.0, + 'debit_move_id': line_2.id, + 'credit_move_id': line_1.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2017-01-01')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ { - 'amount': 0.0, + 'debit': 20.0, + 'credit': 0.0, + 'amount_currency': 0.0, + 'currency_id': currency.id, + 'account_id': line_1.account_id.id, + }, + { + 'debit': 0.0, + 'credit': 20.0, + 'amount_currency': 0.0, + 'currency_id': currency.id, + 'account_id': self.exch_income_account.id, + }, + ]) + self.assertRecordValues(res['exchange_partials'], [{ + 'amount': 20.0, + 'debit_amount_currency': 0.0, + 'credit_amount_currency': 0.0, + 'debit_move_id': res['partials'].exchange_move_id.line_ids[0].id, + 'credit_move_id': line_1.id, + }]) + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + ]) + + def test_reconcile_exchange_difference_on_partial_one_debit_foreign_currency_debit_expense_full_payment(self): + comp_curr = self.company_data['currency'] + foreign_curr = self.currency_data['currency'] + + line_1 = self._create_line_for_reconciliation(60.0, 120.0, foreign_curr, '2017-01-01') + line_2 = self._create_line_for_reconciliation(-40.0, -40.0, comp_curr, '2016-01-01') + + res = (line_1 + line_2).reconcile() + + self.assertTrue(res.get('full_reconcile')) + self.assertRecordValues(res['full_reconcile'], [{'exchange_move_id': False}]) + self.assertRecordValues(res['partials'], [{ + 'amount': 40.0, + 'debit_amount_currency': 120.0, + 'credit_amount_currency': 40.0, + 'debit_move_id': line_1.id, + 'credit_move_id': line_2.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2017-01-01')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ + { + 'debit': 0.0, + 'credit': 20.0, + 'amount_currency': 0.0, + 'currency_id': foreign_curr.id, + 'account_id': line_1.account_id.id, + }, + { + 'debit': 20.0, + 'credit': 0.0, + 'amount_currency': 0.0, + 'currency_id': foreign_curr.id, + 'account_id': self.exch_expense_account.id, + }, + ]) + self.assertRecordValues(res['exchange_partials'], [{ + 'amount': 20.0, + 'debit_amount_currency': 0.0, + 'credit_amount_currency': 0.0, + 'debit_move_id': line_1.id, + 'credit_move_id': res['partials'].exchange_move_id.line_ids[0].id, + }]) + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + ]) + + def test_reconcile_exchange_difference_on_partial_one_credit_foreign_currency_debit_expense_full_payment(self): + comp_curr = self.company_data['currency'] + foreign_curr = self.currency_data['currency'] + + line_1 = self._create_line_for_reconciliation(60.0, 60.0, comp_curr, '2017-01-01') + line_2 = self._create_line_for_reconciliation(-40.0, -120.0, foreign_curr, '2016-01-01') + + res = (line_1 + line_2).reconcile() + + self.assertTrue(res.get('full_reconcile')) + self.assertRecordValues(res['full_reconcile'], [{'exchange_move_id': False}]) + self.assertRecordValues(res['partials'], [{ + 'amount': 40.0, + 'debit_amount_currency': 40.0, + 'credit_amount_currency': 120.0, + 'debit_move_id': line_1.id, + 'credit_move_id': line_2.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2017-01-01')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ + { + 'debit': 0.0, + 'credit': 20.0, + 'amount_currency': -20.0, + 'currency_id': comp_curr.id, + 'account_id': line_1.account_id.id, + }, + { + 'debit': 20.0, + 'credit': 0.0, + 'amount_currency': 20.0, + 'currency_id': comp_curr.id, + 'account_id': self.exch_expense_account.id, + }, + ]) + self.assertRecordValues(res['exchange_partials'], [{ + 'amount': 20.0, + 'debit_amount_currency': 20.0, + 'credit_amount_currency': 20.0, + 'debit_move_id': line_1.id, + 'credit_move_id': res['partials'].exchange_move_id.line_ids[0].id, + }]) + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + ]) + + def test_reconcile_exchange_difference_on_partial_one_debit_foreign_currency_debit_income_full_payment(self): + comp_curr = self.company_data['currency'] + foreign_curr = self.currency_data['currency'] + + line_1 = self._create_line_for_reconciliation(40.0, 120.0, foreign_curr, '2016-01-01') + line_2 = self._create_line_for_reconciliation(-60.0, -60.0, comp_curr, '2017-01-01') + + res = (line_1 + line_2).reconcile() + + self.assertTrue(res.get('full_reconcile')) + self.assertRecordValues(res['full_reconcile'], [{'exchange_move_id': False}]) + self.assertRecordValues(res['partials'], [{ + 'amount': 40.0, + 'debit_amount_currency': 120.0, + 'credit_amount_currency': 40.0, + 'debit_move_id': line_1.id, + 'credit_move_id': line_2.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2017-01-01')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ + { + 'debit': 20.0, + 'credit': 0.0, + 'amount_currency': 20.0, + 'currency_id': comp_curr.id, + 'account_id': line_2.account_id.id, + }, + { + 'debit': 0.0, + 'credit': 20.0, + 'amount_currency': -20.0, + 'currency_id': comp_curr.id, + 'account_id': self.exch_income_account.id, + }, + ]) + self.assertRecordValues(res['exchange_partials'], [{ + 'amount': 20.0, + 'debit_amount_currency': 20.0, + 'credit_amount_currency': 20.0, + 'debit_move_id': res['partials'].exchange_move_id.line_ids[0].id, + 'credit_move_id': line_2.id, + }]) + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + ]) + + def test_reconcile_exchange_difference_on_partial_one_credit_foreign_currency_debit_income_full_payment(self): + comp_curr = self.company_data['currency'] + foreign_curr = self.currency_data['currency'] + + line_1 = self._create_line_for_reconciliation(40.0, 40.0, comp_curr, '2016-01-01') + line_2 = self._create_line_for_reconciliation(-60.0, -120.0, foreign_curr, '2017-01-01') + + res = (line_1 + line_2).reconcile() + + self.assertTrue(res.get('full_reconcile')) + self.assertRecordValues(res['full_reconcile'], [{'exchange_move_id': False}]) + self.assertRecordValues(res['partials'], [{ + 'amount': 40.0, + 'debit_amount_currency': 40.0, + 'credit_amount_currency': 120.0, + 'debit_move_id': line_1.id, + 'credit_move_id': line_2.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2017-01-01')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ + { + 'debit': 20.0, + 'credit': 0.0, + 'amount_currency': 0.0, + 'currency_id': foreign_curr.id, + 'account_id': line_2.account_id.id, + }, + { + 'debit': 0.0, + 'credit': 20.0, + 'amount_currency': 0.0, + 'currency_id': foreign_curr.id, + 'account_id': self.exch_income_account.id, + }, + ]) + self.assertRecordValues(res['exchange_partials'], [{ + 'amount': 20.0, + 'debit_amount_currency': 0.0, + 'credit_amount_currency': 0.0, + 'debit_move_id': res['partials'].exchange_move_id.line_ids[0].id, + 'credit_move_id': line_2.id, + }]) + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + ]) + + def test_reconcile_exchange_difference_on_partial_one_debit_foreign_currency_credit_expense_full_payment(self): + comp_curr = self.company_data['currency'] + foreign_curr = self.currency_data['currency'] + + line_1 = self._create_line_for_reconciliation(-40.0, -40.0, comp_curr, '2016-01-01') + line_2 = self._create_line_for_reconciliation(60.0, 120.0, foreign_curr, '2017-01-01') + + res = (line_1 + line_2).reconcile() + + self.assertTrue(res.get('full_reconcile')) + self.assertRecordValues(res['full_reconcile'], [{'exchange_move_id': False}]) + self.assertRecordValues(res['partials'], [{ + 'amount': 40.0, + 'debit_amount_currency': 120.0, + 'credit_amount_currency': 40.0, + 'debit_move_id': line_2.id, + 'credit_move_id': line_1.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2017-01-01')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ + { + 'debit': 0.0, + 'credit': 20.0, + 'amount_currency': 0.0, + 'currency_id': foreign_curr.id, + 'account_id': line_2.account_id.id, + }, + { + 'debit': 20.0, + 'credit': 0.0, + 'amount_currency': 0.0, + 'currency_id': foreign_curr.id, + 'account_id': self.exch_expense_account.id, + }, + ]) + self.assertRecordValues(res['exchange_partials'], [{ + 'amount': 20.0, + 'debit_amount_currency': 0.0, + 'credit_amount_currency': 0.0, + 'debit_move_id': line_2.id, + 'credit_move_id': res['partials'].exchange_move_id.line_ids[0].id, + }]) + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + ]) + + def test_reconcile_exchange_difference_on_partial_one_credit_foreign_currency_credit_expense_full_payment(self): + comp_curr = self.company_data['currency'] + foreign_curr = self.currency_data['currency'] + + line_1 = self._create_line_for_reconciliation(-40.0, -120.0, foreign_curr, '2016-01-01') + line_2 = self._create_line_for_reconciliation(60.0, 60.0, comp_curr, '2017-01-01') + + res = (line_1 + line_2).reconcile() + + self.assertTrue(res.get('full_reconcile')) + self.assertRecordValues(res['full_reconcile'], [{'exchange_move_id': False}]) + self.assertRecordValues(res['partials'], [{ + 'amount': 40.0, + 'debit_amount_currency': 40.0, + 'credit_amount_currency': 120.0, + 'debit_move_id': line_2.id, + 'credit_move_id': line_1.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2017-01-01')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ + { + 'debit': 0.0, + 'credit': 20.0, + 'amount_currency': -20.0, + 'currency_id': comp_curr.id, + 'account_id': line_2.account_id.id, + }, + { + 'debit': 20.0, + 'credit': 0.0, + 'amount_currency': 20.0, + 'currency_id': comp_curr.id, + 'account_id': self.exch_expense_account.id, + }, + ]) + self.assertRecordValues(res['exchange_partials'], [{ + 'amount': 20.0, + 'debit_amount_currency': 20.0, + 'credit_amount_currency': 20.0, + 'debit_move_id': line_2.id, + 'credit_move_id': res['partials'].exchange_move_id.line_ids[0].id, + }]) + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + ]) + + def test_reconcile_exchange_difference_on_partial_one_debit_foreign_currency_credit_income_full_payment(self): + comp_curr = self.company_data['currency'] + foreign_curr = self.currency_data['currency'] + + line_1 = self._create_line_for_reconciliation(-60.0, -60.0, comp_curr, '2017-01-01') + line_2 = self._create_line_for_reconciliation(40.0, 120.0, foreign_curr, '2016-01-01') + + res = (line_1 + line_2).reconcile() + + self.assertTrue(res.get('full_reconcile')) + self.assertRecordValues(res['full_reconcile'], [{'exchange_move_id': False}]) + self.assertRecordValues(res['partials'], [{ + 'amount': 40.0, + 'debit_amount_currency': 120.0, + 'credit_amount_currency': 40.0, + 'debit_move_id': line_2.id, + 'credit_move_id': line_1.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2017-01-01')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ + { + 'debit': 20.0, + 'credit': 0.0, + 'amount_currency': 20.0, + 'currency_id': comp_curr.id, + 'account_id': line_1.account_id.id, + }, + { + 'debit': 0.0, + 'credit': 20.0, + 'amount_currency': -20.0, + 'currency_id': comp_curr.id, + 'account_id': self.exch_income_account.id, + }, + ]) + self.assertRecordValues(res['exchange_partials'], [{ + 'amount': 20.0, + 'debit_amount_currency': 20.0, + 'credit_amount_currency': 20.0, + 'debit_move_id': res['partials'].exchange_move_id.line_ids[0].id, + 'credit_move_id': line_1.id, + }]) + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + ]) + + def test_reconcile_exchange_difference_on_partial_one_credit_foreign_currency_credit_income_full_payment(self): + comp_curr = self.company_data['currency'] + foreign_curr = self.currency_data['currency'] + + line_1 = self._create_line_for_reconciliation(-60.0, -120.0, foreign_curr, '2017-01-01') + line_2 = self._create_line_for_reconciliation(40.0, 40.0, comp_curr, '2016-01-01') + + res = (line_1 + line_2).reconcile() + + self.assertTrue(res.get('full_reconcile')) + self.assertRecordValues(res['full_reconcile'], [{'exchange_move_id': False}]) + self.assertRecordValues(res['partials'], [{ + 'amount': 40.0, + 'debit_amount_currency': 40.0, + 'credit_amount_currency': 120.0, + 'debit_move_id': line_2.id, + 'credit_move_id': line_1.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2017-01-01')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ + { + 'debit': 20.0, + 'credit': 0.0, + 'amount_currency': 0.0, + 'currency_id': foreign_curr.id, + 'account_id': line_1.account_id.id, + }, + { + 'debit': 0.0, + 'credit': 20.0, + 'amount_currency': 0.0, + 'currency_id': foreign_curr.id, + 'account_id': self.exch_income_account.id, + }, + ]) + self.assertRecordValues(res['exchange_partials'], [{ + 'amount': 20.0, + 'debit_amount_currency': 0.0, + 'credit_amount_currency': 0.0, + 'debit_move_id': res['partials'].exchange_move_id.line_ids[0].id, + 'credit_move_id': line_1.id, + }]) + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + ]) + + def test_reconcile_special_mexican_workflow_1(self): + comp_curr = self.company_data['currency'] + foreign_curr = self.env['res.currency'].create({ + 'name': "Sushi", + 'symbol': '🍣', + 'rounding': 0.01, + 'rate_ids': [ + Command.create({'name': '2019-09-24', 'rate': 0.050800000000}), + Command.create({'name': '2019-06-28', 'rate': 0.052235000000}), + Command.create({'name': '2019-06-24', 'rate': 0.052686000000}), + Command.create({'name': '2019-06-20', 'rate': 0.052353000000}), + Command.create({'name': '2019-06-12', 'rate': 0.052072000000}), + ], + }) + + refund1 = self.env['account.move'].create({ + 'move_type': 'out_refund', + 'invoice_date': '2019-06-12', + 'date': '2019-06-12', + 'partner_id': self.partner_a.id, + 'currency_id': self.company_data['currency'].id, + 'invoice_line_ids': [Command.create({ + 'product_id': self.product_a.id, + 'price_unit': 1385.92, + })], + }) + refund1.action_post() + refund1_rec_line = refund1.line_ids.filtered(lambda x: x.account_id.internal_type == 'receivable') + + inv1 = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'invoice_date': '2019-06-20', + 'date': '2019-06-20', + 'partner_id': self.partner_a.id, + 'currency_id': self.company_data['currency'].id, + 'invoice_line_ids': [Command.create({ + 'product_id': self.product_a.id, + 'price_unit': 839.40, + })], + }) + inv1.action_post() + inv1_rec_line = inv1.line_ids.filtered(lambda x: x.account_id.internal_type == 'receivable') + + inv2 = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'invoice_date': '2019-06-24', + 'date': '2019-06-24', + 'partner_id': self.partner_a.id, + 'currency_id': foreign_curr.id, + 'invoice_line_ids': [Command.create({ + 'product_id': self.product_a.id, + 'price_unit': 1935.72, + })], + }) + inv2.action_post() + inv2_rec_line = inv2.line_ids.filtered(lambda x: x.account_id.internal_type == 'receivable') + + pay1 = self.env['account.payment'].create({ + 'partner_type': 'customer', + 'payment_type': 'inbound', + 'date': '2019-06-28', + 'amount': 1907.17, + 'partner_id': self.partner_a.id, + 'currency_id': foreign_curr.id, + }) + pay1_liquidity_line = pay1.line_ids.filtered(lambda x: x.account_id.internal_type != 'receivable') + pay1_rec_line = pay1.line_ids.filtered(lambda x: x.account_id.internal_type == 'receivable') + pay1.action_post() + pay1.write({'line_ids': [ + Command.update(pay1_liquidity_line.id, {'debit': 36511.34}), + Command.update(pay1_rec_line.id, {'credit': 36511.34}), + ]}) + + pay2 = self.env['account.payment'].create({ + 'partner_type': 'customer', + 'payment_type': 'inbound', + 'date': '2019-09-24', + 'amount': 0.09, + 'partner_id': self.partner_a.id, + 'currency_id': foreign_curr.id, + }) + pay2.action_post() + pay2_rec_line = pay2.line_ids.filtered(lambda x: x.account_id.internal_type == 'receivable') + + # 1st reconciliation refund1 + inv1 + self.assert_invoice_outstanding_to_reconcile_widget(refund1, { + inv1.id: 839.40, + inv2.id: 36740.69, + }) + self.assertRecordValues(refund1_rec_line + inv1_rec_line, [ + {'amount_residual': -1385.92, 'amount_residual_currency': -1385.92, 'reconciled': False}, + {'amount_residual': 839.40, 'amount_residual_currency': 839.40, 'reconciled': False}, + ]) + + res = (refund1_rec_line + inv1_rec_line).reconcile() + + self.assertRecordValues(res['partials'], [{ + 'amount': 839.4, + 'debit_amount_currency': 839.4, + 'credit_amount_currency': 839.4, + 'debit_move_id': inv1_rec_line.id, + 'credit_move_id': refund1_rec_line.id, + 'exchange_move_id': None, + }]) + self.assertRecordValues(refund1_rec_line + inv1_rec_line, [ + {'amount_residual': -546.52, 'amount_residual_currency': -546.52, 'reconciled': False}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + ]) + self.assert_invoice_outstanding_reconciled_widget(refund1, { + inv1.id: 839.40, + }) + + # 2th reconciliation refund1 + inv2 + self.assert_invoice_outstanding_to_reconcile_widget(refund1, { + inv2.id: 36740.69, + }) + self.assertRecordValues(refund1_rec_line + inv2_rec_line, [ + {'amount_residual': -546.52, 'amount_residual_currency': -546.52, 'reconciled': False}, + {'amount_residual': 36740.69, 'amount_residual_currency': 1935.72, 'reconciled': False}, + ]) + + res = (refund1_rec_line + inv2_rec_line).reconcile() + + self.assertRecordValues(res['partials'], [{ + 'amount': 540.18, + 'debit_amount_currency': 28.46, + 'credit_amount_currency': 540.18, + 'debit_move_id': inv2_rec_line.id, + 'credit_move_id': refund1_rec_line.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2019-06-24')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ + { + 'debit': 6.34, + 'credit': 0.0, + 'amount_currency': 6.34, + 'currency_id': comp_curr.id, + 'account_id': refund1_rec_line.account_id.id, + }, + { + 'debit': 0.0, + 'credit': 6.34, + 'amount_currency': -6.34, + 'currency_id': comp_curr.id, + 'account_id': self.exch_income_account.id, + }, + ]) + self.assertRecordValues(res['exchange_partials'], [{ + 'amount': 6.34, + 'debit_amount_currency': 6.34, + 'credit_amount_currency': 6.34, + 'debit_move_id': res['partials'].exchange_move_id.line_ids[0].id, + 'credit_move_id': refund1_rec_line.id, + }]) + self.assertRecordValues(refund1_rec_line + inv2_rec_line, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 36200.51, 'amount_residual_currency': 1907.26, 'reconciled': False}, + ]) + self.assert_invoice_outstanding_reconciled_widget(refund1, { + inv1.id: 839.40, + inv2.id: 540.18, + res['partials'].exchange_move_id.id: 6.34, + }) + self.assert_invoice_outstanding_to_reconcile_widget(refund1, {}) + + # 3th reconciliation inv1 + pay1 + self.assert_invoice_outstanding_reconciled_widget(inv2, { + refund1.id: 28.46, + }) + self.assert_invoice_outstanding_to_reconcile_widget(inv2, { + pay1.move_id.id: 1907.17, + pay2.move_id.id: 0.09, + }) + self.assertRecordValues(inv2_rec_line + pay1_rec_line, [ + {'amount_residual': 36200.51, 'amount_residual_currency': 1907.26, 'reconciled': False}, + {'amount_residual': -36511.34, 'amount_residual_currency': -1907.17, 'reconciled': False}, + ]) + + res = (inv2_rec_line + pay1_rec_line).reconcile() + + self.assertRecordValues(res['partials'], [{ + 'amount': 36198.80, + 'debit_amount_currency': 1907.17, + 'credit_amount_currency': 1907.17, + 'debit_move_id': inv2_rec_line.id, + 'credit_move_id': pay1_rec_line.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2019-06-28')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ + { + 'debit': 312.54, + 'credit': 0.0, + 'amount_currency': 0.0, + 'currency_id': foreign_curr.id, + 'account_id': pay1_rec_line.account_id.id, + }, + { + 'debit': 0.0, + 'credit': 312.54, + 'amount_currency': 0.0, + 'currency_id': foreign_curr.id, + 'account_id': self.exch_income_account.id, + }, + ]) + self.assertRecordValues(inv2_rec_line + pay1_rec_line, [ + {'amount_residual': 1.71, 'amount_residual_currency': 0.09, 'reconciled': False}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + ]) + self.assert_invoice_outstanding_reconciled_widget(inv2, { + refund1.id: 28.46, + pay1.move_id.id: 1907.17, + }) + + # 4th reconciliation inv2 + pay2 + self.assert_invoice_outstanding_to_reconcile_widget(inv2, { + pay2.move_id.id: 0.09, + }) + self.assertRecordValues(inv2_rec_line + pay2_rec_line, [ + {'amount_residual': 1.71, 'amount_residual_currency': 0.09, 'reconciled': False}, + {'amount_residual': -1.77, 'amount_residual_currency': -0.09, 'reconciled': False}, + ]) + + res = (inv2_rec_line + pay2_rec_line).reconcile() + + self.assertRecordValues(res['partials'], [{ + 'amount': 1.71, + 'debit_amount_currency': 0.09, + 'credit_amount_currency': 0.09, + 'debit_move_id': inv2_rec_line.id, + 'credit_move_id': pay2_rec_line.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2019-09-24')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ + { + 'debit': 0.06, + 'credit': 0.0, + 'amount_currency': 0.0, + 'currency_id': foreign_curr.id, + 'account_id': inv2_rec_line.account_id.id, + }, + { + 'debit': 0.0, + 'credit': 0.06, + 'amount_currency': 0.0, + 'currency_id': foreign_curr.id, + 'account_id': self.exch_income_account.id, + }, + ]) + self.assertRecordValues(inv2_rec_line + pay2_rec_line, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + ]) + self.assert_invoice_outstanding_reconciled_widget(inv2, { + refund1.id: 28.46, + pay1.move_id.id: 1907.17, + pay2.move_id.id: 0.09, + }) + self.assert_invoice_outstanding_to_reconcile_widget(inv2, {}) + + self.assertTrue(res.get('full_reconcile')) + self.assertRecordValues(res['full_reconcile'], [{'exchange_move_id': None}]) + + def test_reconcile_special_mexican_workflow_2(self): + comp_curr = self.company_data['currency'] + foreign_curr = self.env['res.currency'].create({ + 'name': "Sushi", + 'symbol': '🍣', + 'rounding': 0.01, + 'rate_ids': [ + Command.create({'name': '2019-09-24', 'rate': 0.050800000000}), + Command.create({'name': '2019-06-28', 'rate': 0.052235000000}), + Command.create({'name': '2019-06-24', 'rate': 0.052686000000}), + Command.create({'name': '2019-06-20', 'rate': 0.052353000000}), + Command.create({'name': '2019-06-12', 'rate': 0.052072000000}), + ], + }) + + refund1 = self.env['account.move'].create({ + 'move_type': 'out_refund', + 'invoice_date': '2019-06-12', + 'date': '2019-06-12', + 'partner_id': self.partner_a.id, + 'currency_id': self.company_data['currency'].id, + 'invoice_line_ids': [Command.create({ + 'product_id': self.product_a.id, + 'price_unit': 1385.92, + })], + }) + refund1.action_post() + refund1_rec_line = refund1.line_ids.filtered(lambda x: x.account_id.internal_type == 'receivable') + + inv1 = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'invoice_date': '2019-06-20', + 'date': '2019-06-20', + 'partner_id': self.partner_a.id, + 'currency_id': self.company_data['currency'].id, + 'invoice_line_ids': [Command.create({ + 'product_id': self.product_a.id, + 'price_unit': 839.40, + })], + }) + inv1.action_post() + inv1_rec_line = inv1.line_ids.filtered(lambda x: x.account_id.internal_type == 'receivable') + + inv2 = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'invoice_date': '2019-06-24', + 'date': '2019-06-24', + 'partner_id': self.partner_a.id, + 'currency_id': foreign_curr.id, + 'invoice_line_ids': [Command.create({ + 'product_id': self.product_a.id, + 'price_unit': 1935.72, + })], + }) + inv2.action_post() + inv2_rec_line = inv2.line_ids.filtered(lambda x: x.account_id.internal_type == 'receivable') + + pay1 = self.env['account.payment'].create({ + 'partner_type': 'customer', + 'payment_type': 'inbound', + 'date': '2019-06-28', + 'amount': 1907.17, + 'partner_id': self.partner_a.id, + 'currency_id': foreign_curr.id, + }) + pay1_liquidity_line = pay1.line_ids.filtered(lambda x: x.account_id.internal_type != 'receivable') + pay1_rec_line = pay1.line_ids.filtered(lambda x: x.account_id.internal_type == 'receivable') + pay1.action_post() + pay1.write({'line_ids': [ + Command.update(pay1_liquidity_line.id, {'debit': 36511.34}), + Command.update(pay1_rec_line.id, {'credit': 36511.34}), + ]}) + + pay2 = self.env['account.payment'].create({ + 'partner_type': 'customer', + 'payment_type': 'inbound', + 'date': '2019-09-24', + 'amount': 0.09, + 'partner_id': self.partner_a.id, + 'currency_id': foreign_curr.id, + }) + pay2.action_post() + pay2_rec_line = pay2.line_ids.filtered(lambda x: x.account_id.internal_type == 'receivable') + + self.assertRecordValues(refund1_rec_line + inv1_rec_line + inv2_rec_line + pay1_rec_line + pay2_rec_line, [ + {'amount_residual': -1385.92, 'amount_residual_currency': -1385.92}, + {'amount_residual': 839.40, 'amount_residual_currency': 839.40}, + {'amount_residual': 36740.69, 'amount_residual_currency': 1935.72}, + {'amount_residual': -36511.34, 'amount_residual_currency': -1907.17}, + {'amount_residual': -1.77, 'amount_residual_currency': -0.09}, + ]) + + # 1st reconciliation refund1 + inv1 + self.assert_invoice_outstanding_to_reconcile_widget(refund1, { + inv1.id: 839.40, + inv2.id: 36740.69, + }) + self.assertRecordValues(refund1_rec_line + inv1_rec_line, [ + {'amount_residual': -1385.92, 'amount_residual_currency': -1385.92, 'reconciled': False}, + {'amount_residual': 839.40, 'amount_residual_currency': 839.40, 'reconciled': False}, + ]) + + res = (refund1_rec_line + inv1_rec_line).reconcile() + + self.assertRecordValues(res['partials'], [{ + 'amount': 839.4, + 'debit_amount_currency': 839.4, + 'credit_amount_currency': 839.4, + 'debit_move_id': inv1_rec_line.id, + 'credit_move_id': refund1_rec_line.id, + 'exchange_move_id': None, + }]) + self.assertRecordValues(refund1_rec_line + inv1_rec_line, [ + {'amount_residual': -546.52, 'amount_residual_currency': -546.52, 'reconciled': False}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + ]) + self.assert_invoice_outstanding_reconciled_widget(refund1, { + inv1.id: 839.40, + }) + + # 2th reconciliation refund1 + inv2 + self.assert_invoice_outstanding_to_reconcile_widget(inv2, { + refund1.id: 28.46, + pay1.move_id.id: 1907.17, + pay2.move_id.id: 0.09, + }) + self.assertRecordValues(refund1_rec_line + inv2_rec_line, [ + {'amount_residual': -546.52, 'amount_residual_currency': -546.52, 'reconciled': False}, + {'amount_residual': 36740.69, 'amount_residual_currency': 1935.72, 'reconciled': False}, + ]) + + res = (inv2_rec_line + pay1_rec_line).reconcile() + + self.assertRecordValues(res['partials'], [{ + 'amount': 36198.8, + 'debit_amount_currency': 1907.17, + 'credit_amount_currency': 1907.17, + 'debit_move_id': inv2_rec_line.id, + 'credit_move_id': pay1_rec_line.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2019-06-28')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ + { + 'debit': 312.54, + 'credit': 0.0, + 'amount_currency': 0.0, + 'currency_id': foreign_curr.id, + 'account_id': inv2_rec_line.account_id.id, + }, + { + 'debit': 0.0, + 'credit': 312.54, + 'amount_currency': 0.0, + 'currency_id': foreign_curr.id, + 'account_id': self.exch_income_account.id, + }, + ]) + self.assertRecordValues(inv2_rec_line + pay1_rec_line, [ + {'amount_residual': 541.89, 'amount_residual_currency': 28.55, 'reconciled': False}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + ]) + self.assert_invoice_outstanding_reconciled_widget(inv2, { + pay1.move_id.id: 1907.17, + }) + self.assert_invoice_outstanding_to_reconcile_widget(inv2, { + refund1.id: 28.46, + pay2.move_id.id: 0.09, + }) + + # 3th reconciliation refund1 + inv2 + self.assert_invoice_outstanding_to_reconcile_widget(refund1, { + inv2.id: 541.89, + }) + self.assertRecordValues(refund1_rec_line + inv2_rec_line, [ + {'amount_residual': -546.52, 'amount_residual_currency': -546.52, 'reconciled': False}, + {'amount_residual': 541.89, 'amount_residual_currency': 28.55, 'reconciled': False}, + ]) + + res = (refund1_rec_line + inv2_rec_line).reconcile() + + self.assertRecordValues(res['partials'], [{ + 'amount': 540.18, + 'debit_amount_currency': 28.46, + 'credit_amount_currency': 540.18, + 'debit_move_id': inv2_rec_line.id, + 'credit_move_id': refund1_rec_line.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2019-06-24')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ + { + 'debit': 6.34, + 'credit': 0.0, + 'amount_currency': 6.34, + 'currency_id': comp_curr.id, + 'account_id': refund1_rec_line.account_id.id, + }, + { + 'debit': 0.0, + 'credit': 6.34, + 'amount_currency': -6.34, + 'currency_id': comp_curr.id, + 'account_id': self.exch_income_account.id, + }, + ]) + self.assertRecordValues(res['exchange_partials'], [{ + 'amount': 6.34, + 'debit_amount_currency': 6.34, + 'credit_amount_currency': 6.34, + 'debit_move_id': res['partials'].exchange_move_id.line_ids[0].id, + 'credit_move_id': refund1_rec_line.id, + }]) + self.assertRecordValues(refund1_rec_line + inv2_rec_line, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 1.71, 'amount_residual_currency': 0.09, 'reconciled': False}, + ]) + self.assert_invoice_outstanding_reconciled_widget(refund1, { + inv1.id: 839.40, + inv2.id: 540.18, + res['partials'].exchange_move_id.id: 6.34, + }) + self.assert_invoice_outstanding_to_reconcile_widget(refund1, {}) + + self.assert_invoice_outstanding_to_reconcile_widget(inv2, { + pay2.move_id.id: 0.09, + }) + + res = (inv2_rec_line + pay2_rec_line).reconcile() + self.assertRecordValues(res['partials'], [{ + 'amount': 1.71, + 'debit_amount_currency': 0.09, + 'credit_amount_currency': 0.09, + 'debit_move_id': inv2_rec_line.id, + 'credit_move_id': pay2_rec_line.id, + }]) + self.assertRecordValues(res['partials'].exchange_move_id, [{'date': fields.Date.from_string('2019-09-24')}]) + self.assertRecordValues(res['partials'].exchange_move_id.line_ids, [ + { + 'debit': 0.06, + 'credit': 0.0, + 'amount_currency': 0.0, + 'currency_id': foreign_curr.id, + 'account_id': inv2_rec_line.account_id.id, + }, + { + 'debit': 0.0, + 'credit': 0.06, + 'amount_currency': 0.0, + 'currency_id': foreign_curr.id, + 'account_id': self.exch_income_account.id, + }, + ]) + self.assertRecordValues(inv2_rec_line + pay2_rec_line, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + ]) + + self.assert_invoice_outstanding_reconciled_widget(inv2, { + refund1.id: 28.46, + pay1.move_id.id: 1907.17, + pay2.move_id.id: 0.09, + }) + self.assert_invoice_outstanding_to_reconcile_widget(inv2, {}) + + self.assertTrue(res.get('full_reconcile')) + self.assertRecordValues(res['full_reconcile'], [{'exchange_move_id': None}]) + + def test_migration_to_new_reconciliation_same_foreign_currency(self): + foreign_curr = self.currency_data['currency'] + + line_1 = self._create_line_for_reconciliation(-60.0, -120.0, foreign_curr, '2017-01-01') + line_2 = self._create_line_for_reconciliation(80.0, 240.0, foreign_curr, '2016-01-01') + + # Create the partial as it should be created in previous version. + self.env['account.partial.reconcile'].create({ + 'amount': 60.0, + 'debit_amount_currency': 120.0, + 'credit_amount_currency': 120.0, + 'debit_move_id': line_2.id, + 'credit_move_id': line_1.id, + }) + self.assertRecordValues(line_1 + line_2, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 20.0, 'amount_residual_currency': 120.0, 'reconciled': False}, + ]) + + # Reconcile using the "new" reconciliation. + line_3 = self._create_line_for_reconciliation(-15.0, -30.0, foreign_curr, '2017-01-01') + (line_2 + line_3).reconcile() + self.assertRecordValues(line_2 + line_3, [ + {'amount_residual': 10.0, 'amount_residual_currency': 90.0, 'reconciled': False}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + ]) + + line_4 = self._create_line_for_reconciliation(-30.0, -90.0, foreign_curr, '2016-01-01') + (line_2 + line_4).reconcile() + 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_migration_to_new_reconciliation_multiple_currencies_fix_residual_with_writeoff(self): + comp_curr = self.company_data['currency'] + foreign_curr1 = self.currency_data['currency'] + + line_1 = self._create_line_for_reconciliation(600.0, 1200.0, foreign_curr1, '2017-01-01') + line_2 = self._create_line_for_reconciliation(-800.0, -2400.0, foreign_curr1, '2016-01-01') + line_3 = self._create_line_for_reconciliation(400.0, 400.0, comp_curr, '2016-01-01') + + # Create the partials as it should be created in previous version. + self.env['account.partial.reconcile'].create([ + { + 'amount': 600.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, + 'amount': 200.0, + 'debit_amount_currency': 200.0, + 'credit_amount_currency': 600.0, + 'debit_move_id': line_3.id, + 'credit_move_id': line_2.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}, + self.assertRecordValues(line_1 + line_2 + line_3, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': -600.0, 'reconciled': False}, + {'amount_residual': 200.0, 'amount_residual_currency': 200.0, 'reconciled': False}, ]) - 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() + # Fix 'line_2' & 'line_4' using the "new" reconciliation. + line_4 = self._create_line_for_reconciliation(0.0, 600.0, foreign_curr1, '2016-01-01') + line_5 = self._create_line_for_reconciliation(-200.0, -200.0, comp_curr, '2016-01-01') + (line_2 + line_3 + line_4 + line_5).reconcile() - 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, - }, + self.assertRecordValues(line_1 + line_2 + line_3 + line_4 + line_5, [ + {'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': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, ]) # ------------------------------------------------------------------------- @@ -1483,7 +2554,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): .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.assertFullReconcileAccount(res['full_reconcile'], self.extra_receivable_account_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: @@ -1524,7 +2595,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): .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.assertFullReconcileAccount(res['full_reconcile'], self.extra_receivable_account_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: @@ -1712,8 +2783,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): .filtered(lambda line: line.account_id == self.extra_receivable_account_1) res = receivable_lines2.reconcile() - self.assertFullReconcile(res['full_reconcile'], receivable_lines + receivable_lines2) - + self.assertTrue(res.get('full_reconcile')) 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)\ @@ -1803,11 +2873,14 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): 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") + self.assertRecordValues(partial_rec.exchange_move_id.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}, + ]) + 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}, @@ -1869,39 +2942,44 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): }) 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_line_id': self.inbound_payment_method_line.id, - 'currency_id': self.company_data['currency'].id, - 'amount': 220.0, - }) - pmt_wizard._create_payments() + self.env['account.payment.register']\ + .with_context(active_model='account.move', active_ids=caba_inv.ids)\ + .create({ + 'payment_date': '2017-01-01', + 'currency_id': currency_id, + 'amount': 110.0, + })\ + ._create_payments() + + receivable_line = caba_inv.line_ids.filtered(lambda x: x.account_id.internal_type == 'receivable') + partial_rec = caba_inv.line_ids.matched_credit_ids + self.assertTrue(receivable_line.full_reconcile_id, "Invoice should be fully paid") 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}, + {'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") + self.assertRecordValues(partial_rec.exchange_move_id.line_ids, [ + {'account_id': self.extra_receivable_account_1.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}, + ]) 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}, + {'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, 3.33), - (self.tax_account_1, -20.0, -13.33), + (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_4(self): @@ -2005,14 +3083,16 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): {'debit': 0.0, 'credit': 50.0, 'amount_currency': -100.0, 'currency_id': currency_id, 'account_id': self.tax_account_1.id}, ]) - exchange_diff = res['full_reconcile'].exchange_move_id + partial_rec = cash_basis_move.line_ids.matched_credit_ids + self.assertRecordValues(partial_rec.exchange_move_id.line_ids, [ + {'debit': 66.67, 'credit': 0.0, 'currency_id': currency_id, 'account_id': self.extra_receivable_account_1.id}, + {'debit': 0.0, 'credit': 66.67, 'currency_id': currency_id, 'account_id': self.company_data['company'].income_currency_exchange_account_id.id}, + ]) # Exchange difference # 66.67 amount residual on the payment line after reconciling receivable line of the cash basis move with the payment counterpart # 50.00 difference of the cash_basis_move base line and the CABA entry created by the system - self.assertRecordValues(exchange_diff.line_ids, [ - {'debit': 66.67, 'credit': 0.0, 'currency_id': currency_id, 'account_id': self.extra_receivable_account_1.id}, - {'debit': 0.0, 'credit': 66.67, 'currency_id': currency_id, 'account_id': self.company_data['company'].income_currency_exchange_account_id.id}, + self.assertRecordValues(res['full_reconcile'].exchange_move_id.line_ids, [ {'debit': 50.0, 'credit': 0.0, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id}, {'debit': 0.0, 'credit': 50.0, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id}, ]) @@ -2320,7 +3400,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): 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)]) + caba_move = self.env['account.move'].search([('tax_cash_basis_rec_id', 'in', partial_rec.ids)]) # 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({ @@ -2394,7 +3474,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): 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)]) + caba_move = self.env['account.move'].search([('tax_cash_basis_rec_id', 'in', partial_rec.ids)]) 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") @@ -2413,7 +3493,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): 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)]) + caba_move = self.env['account.move'].search([('tax_cash_basis_rec_id', 'in', partial_rec.ids)]) self.assertRecordValues(caba_move.line_ids.sorted(lambda line: (-abs(line.balance), -line.debit, line.account_id)), [ # Base amount: @@ -2446,7 +3526,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): 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)]) + caba_move = self.env['account.move'].search([('tax_cash_basis_rec_id', 'in', partial_rec.ids)]) # 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({ diff --git a/addons/account/tests/test_account_payment_register.py b/addons/account/tests/test_account_payment_register.py index 18abfbfa36b..18054f75a4a 100644 --- a/addons/account/tests/test_account_payment_register.py +++ b/addons/account/tests/test_account_payment_register.py @@ -67,14 +67,16 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon): '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})], + 'currency_id': cls.currency_data['currency'].id, + 'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 24.02})], }) 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})], + 'currency_id': cls.currency_data['currency'].id, + 'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 23.98})], }) (cls.out_invoice_1 + cls.out_invoice_2 + cls.out_invoice_3 + cls.out_invoice_4).action_post() @@ -1006,3 +1008,53 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon): 'partner_bank_id': self.comp_bank_account2.id, }, ]) + + def test_register_payment_invoice_foreign_curr_payment_comp_curr(self): + # Invoice 1200 Gol = 400 USD + invoice = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'date': '2016-01-01', + 'invoice_date': '2016-01-01', + 'partner_id': self.partner_a.id, + 'currency_id': self.currency_data['currency'].id, + 'invoice_line_ids': [(0, 0, {'product_id': self.product_a.id, 'price_unit': 1200.0})], + }) + invoice.action_post() + + # Payment of 600 USD (equivalent to 1200 Gol in 2017). + # 600.0 USD should be computed correctly to fully paid the invoices. + payment = self.env['account.payment.register']\ + .with_context(active_model='account.move', active_ids=invoice.ids)\ + .create({'currency_id': self.company_data['currency'].id})\ + ._create_payments() + + lines = (invoice + payment.move_id).line_ids.filtered(lambda x: x.account_internal_type == 'receivable') + self.assertRecordValues(lines, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'currency_id': self.currency_data['currency'].id, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'currency_id': self.company_data['currency'].id, 'reconciled': True}, + ]) + + def test_register_payment_invoice_comp_curr_payment_foreign_curr(self): + # Invoice of 400 USD (equivalent to 1200 Gol in 2016). + invoice = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'date': '2016-01-01', + 'invoice_date': '2016-01-01', + 'partner_id': self.partner_a.id, + 'currency_id': self.company_data['currency'].id, + 'invoice_line_ids': [(0, 0, {'product_id': self.product_a.id, 'price_unit': 400.0})], + }) + invoice.action_post() + + # Payment of 600 USD = 1200 Gol. + # 1200.0 Gol should be computed correctly to fully paid the invoices. + payment = self.env['account.payment.register']\ + .with_context(active_model='account.move', active_ids=invoice.ids)\ + .create({'currency_id': self.currency_data['currency'].id})\ + ._create_payments() + + lines = (invoice + payment.move_id).line_ids.filtered(lambda x: x.account_internal_type == 'receivable') + self.assertRecordValues(lines, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'currency_id': self.company_data['currency'].id, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'currency_id': self.currency_data['currency'].id, 'reconciled': True}, + ]) diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py deleted file mode 100644 index f0c6e86dfd2..00000000000 --- a/addons/account/tests/test_reconciliation.py +++ /dev/null @@ -1,1208 +0,0 @@ -# -*- 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_line_id': self.inbound_payment_method_line.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_line_id': self.outbound_payment_method_line.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_line_id': self.outbound_payment_method_line.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_line_id': self.inbound_payment_method_line.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_line_id': self.inbound_payment_method_line.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_line_id': self.inbound_payment_method_line.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.company_id.account_journal_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.company_id.account_journal_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_line_id': self.inbound_payment_method_line.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_line_id': self.inbound_payment_method_line.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_line_id': self.inbound_payment_method_line.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_line_id': self.inbound_payment_method_line.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_line_id': self.inbound_payment_method_line.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_line_id': self.inbound_payment_method_line.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/wizard/account_payment_register.py b/addons/account/wizard/account_payment_register.py index 1c1d9bd8cb2..ecaedbea5f3 100644 --- a/addons/account/wizard/account_payment_register.py +++ b/addons/account/wizard/account_payment_register.py @@ -414,29 +414,74 @@ class AccountPaymentRegister(models.TransientModel): wizard.show_partner_bank_account = wizard.payment_method_line_id.code in self.env['account.payment']._get_method_codes_using_bank_account() wizard.require_partner_bank_account = wizard.payment_method_line_id.code in self.env['account.payment']._get_method_codes_needing_bank_account() - @api.depends('source_amount', 'source_amount_currency', 'source_currency_id', 'company_id', 'currency_id', 'payment_date') + @api.depends('can_edit_wizard', 'source_amount', 'source_amount_currency', 'source_currency_id', 'company_id', 'currency_id', 'payment_date') def _compute_amount(self): for wizard in self: - if wizard.source_currency_id == wizard.currency_id: - # Same currency. - wizard.amount = wizard.source_amount_currency - elif wizard.currency_id == wizard.company_id.currency_id: - # Payment expressed on the company's currency. - wizard.amount = wizard.source_amount + if wizard.source_currency_id and wizard.can_edit_wizard: + comp_curr = wizard.company_id.currency_id + if wizard.source_currency_id == wizard.currency_id: + # Same currency. + wizard.amount = wizard.source_amount_currency + elif wizard.source_currency_id != comp_curr and wizard.currency_id == comp_curr: + # Foreign currency on source line but the company currency one on the opposite line. + wizard.amount = wizard.source_currency_id._convert( + wizard.source_amount_currency, + comp_curr, + wizard.company_id, + wizard.payment_date, + ) + elif wizard.source_currency_id == comp_curr and wizard.currency_id != comp_curr: + # Company currency on source line but a foreign currency one on the opposite line. + batch = wizard._get_batches()[0] + wizard.amount = abs(sum( + comp_curr._convert( + aml.amount_residual, + wizard.currency_id, + wizard.company_id, + aml.date, + ) + for aml in batch['lines'] + )) + else: + # Foreign currency on payment different than the one set on the journal entries. + wizard.amount = comp_curr._convert( + wizard.source_amount, + wizard.currency_id, + wizard.company_id, + wizard.payment_date, + ) else: - # Foreign currency on payment different than the one set on the journal entries. - amount_payment_currency = wizard.company_id.currency_id._convert(wizard.source_amount, wizard.currency_id, wizard.company_id, wizard.payment_date) - wizard.amount = amount_payment_currency + # The wizard is not editable so no partial payment allowed and then, 'amount' is not used. + wizard.amount = None - @api.depends('amount') + @api.depends('can_edit_wizard', 'amount') def _compute_payment_difference(self): for wizard in self: + comp_curr = wizard.company_id.currency_id if wizard.source_currency_id == wizard.currency_id: # Same currency. wizard.payment_difference = wizard.source_amount_currency - wizard.amount - elif wizard.currency_id == wizard.company_id.currency_id: - # Payment expressed on the company's currency. - wizard.payment_difference = wizard.source_amount - wizard.amount + elif wizard.source_currency_id != comp_curr and wizard.currency_id == comp_curr: + # Foreign currency on source line but the company currency one on the opposite line. + rate = self.env['res.currency']._get_conversion_rate( + wizard.company_id.currency_id, + wizard.source_currency_id, + wizard.company_id, + wizard.payment_date, + ) + amount_payment_foreign_curr = wizard.source_currency_id.round(wizard.amount * rate) + residual_foreign_curr = wizard.source_amount_currency - amount_payment_foreign_curr + wizard.payment_difference = wizard.company_id.currency_id.round(residual_foreign_curr / rate) + elif wizard.source_currency_id == comp_curr and wizard.currency_id != comp_curr: + # Company currency on source line but a foreign currency one on the opposite line. + rate = self.env['res.currency']._get_conversion_rate( + wizard.company_id.currency_id, + wizard.source_currency_id, + wizard.company_id, + wizard.payment_date, + ) + amount_to_pay_foreign_curr = wizard.currency_id.round(wizard.source_amount * rate) + wizard.payment_difference = amount_to_pay_foreign_curr - wizard.amount else: # Foreign currency on payment different than the one set on the journal entries. amount_payment_currency = wizard.company_id.currency_id._convert(wizard.source_amount, wizard.currency_id, wizard.company_id, wizard.payment_date) diff --git a/addons/account_check_printing/tests/test_print_check.py b/addons/account_check_printing/tests/test_print_check.py index b7d310adce7..c3b343e2485 100644 --- a/addons/account_check_printing/tests/test_print_check.py +++ b/addons/account_check_printing/tests/test_print_check.py @@ -111,11 +111,11 @@ class TestPrintCheck(AccountTestInvoicingCommon): }) invoice.action_post() - # Partial payment in foreign currency: 100Gol = 33.33$. + # Partial payment in foreign currency. payment = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=invoice.ids).create({ 'payment_method_line_id': self.payment_method_line_check.id, 'currency_id': self.currency_data['currency'].id, - 'amount': 100.0, + 'amount': 150.0, 'payment_date': '2017-01-01', })._create_payments() diff --git a/addons/purchase_stock/tests/test_stockvaluation.py b/addons/purchase_stock/tests/test_stockvaluation.py index 0a3c943f474..bfac6f40052 100644 --- a/addons/purchase_stock/tests/test_stockvaluation.py +++ b/addons/purchase_stock/tests/test_stockvaluation.py @@ -910,41 +910,17 @@ class TestStockValuationWithCOA(AccountTestInvoicingCommon): for p in patchers: p.stop() - move_lines = inv.line_ids - self.assertEqual(len(move_lines), 2) - - # PAYABLE CHECK - payable_line = move_lines.filtered(lambda l: l.account_id.internal_type == 'payable') - self.assertEqual(payable_line.amount_currency, -30.0) - self.assertAlmostEqual(payable_line.balance, -15.00) - - # PRODUCTS CHECKS - - # DELIVERY DIFFERENCE (AVERAGE) - # We ordered a product at 30 EUR valued at 20 USD - # We received it when the exchange rate has appreciated - # So, the actualized 20 USD are now 20*1.5/0.7 = 42.86 USD - product_lines = move_lines.filtered(lambda l: l.product_id == product_avg) - - # Although those 42.86 USD are just due to the exchange difference - stock_line = product_lines.filtered(lambda l: l.account_id == self.stock_input_account) - self.assertEqual(stock_line.journal_id, inv.journal_id) - self.assertEqual(stock_line.amount_currency, 30.00) - self.assertAlmostEqual(stock_line.balance, 15.00) - full_reconcile = stock_line.full_reconcile_id - self.assertTrue(full_reconcile.exists()) - - reconciled_lines = full_reconcile.reconciled_line_ids - stock_line - self.assertEqual(len(reconciled_lines), 2) - - stock_journal_line = reconciled_lines.filtered(lambda l: l.journal_id == self.stock_journal) - self.assertEqual(stock_journal_line.amount_currency, -30.00) - self.assertAlmostEqual(stock_journal_line.balance, -42.86) - - exhange_diff_journal = company.currency_exchange_journal_id.exists() - exchange_stock_line = reconciled_lines.filtered(lambda l: l.journal_id == exhange_diff_journal) - self.assertEqual(exchange_stock_line.amount_currency, 0.00) - self.assertAlmostEqual(exchange_stock_line.balance, 27.86) + self.assertRecordValues(inv.line_ids, [ + # pylint: disable=C0326 + {'balance': 15.0, 'amount_currency': 30.0, 'account_id': self.stock_input_account.id}, + {'balance': -15.0, 'amount_currency': -30.0, 'account_id': self.company_data['default_account_payable'].id}, + ]) + self.assertRecordValues(inv.line_ids.full_reconcile_id.reconciled_line_ids, [ + # pylint: disable=C0326 + {'balance': -42.86, 'amount_currency': -30.0, 'account_id': self.stock_input_account.id}, + {'balance': 15.0, 'amount_currency': 30.0, 'account_id': self.stock_input_account.id}, + {'balance': 27.86, 'amount_currency': 0.0, 'account_id': self.stock_input_account.id}, + ]) def test_average_realtime_with_two_delivery_anglo_saxon_valuation_multicurrency_different_dates(self): """ @@ -1119,76 +1095,47 @@ class TestStockValuationWithCOA(AccountTestInvoicingCommon): ########################## # Invoice 0 # ########################## - move_lines = inv.line_ids - self.assertEqual(len(move_lines), 4) - # PAYABLE CHECK - payable_line = move_lines.filtered(lambda l: l.account_id.internal_type == 'payable') - self.assertEqual(payable_line.amount_currency, -100.0) - self.assertAlmostEqual(payable_line.balance, -50.00) + self.assertRecordValues(inv.line_ids, [ + # pylint: disable=C0326 + {'balance': 50.0, 'amount_currency': 100.0, 'account_id': self.stock_input_account.id}, + {'balance': -50.0, 'amount_currency': -100.0, 'account_id': self.company_data['default_account_payable'].id}, + {'balance': -25.0, 'amount_currency': -50.0, 'account_id': self.price_diff_account.id}, + {'balance': 25.0, 'amount_currency': 50.0, 'account_id': self.stock_input_account.id}, + ]) - # # PRODUCTS CHECKS - - # DELIVERY DIFFERENCE (AVERAGE) - stock_lines = move_lines.filtered(lambda l: l.account_id == self.stock_input_account) - self.assertEqual(len(stock_lines), 2) - self.assertAlmostEqual(sum(stock_lines.mapped('amount_currency')), 150.00) - self.assertAlmostEqual(sum(stock_lines.mapped('balance')), 75.00) - - price_diff_line = move_lines.filtered(lambda l: l.account_id == self.price_diff_account) - self.assertAlmostEqual(price_diff_line.amount_currency, -50.00) - self.assertAlmostEqual(price_diff_line.balance, -25.00) - - full_reconcile = stock_lines.mapped('full_reconcile_id') - self.assertTrue(full_reconcile.exists()) - - reconciled_lines = full_reconcile.reconciled_line_ids - stock_lines - self.assertEqual(len(reconciled_lines), 2) - - stock_journal_line = reconciled_lines.filtered(lambda l: l.journal_id == self.stock_journal) - self.assertEqual(stock_journal_line.amount_currency, -150) - self.assertAlmostEqual(stock_journal_line.balance, -214.29) - - exchange_stock_line = reconciled_lines.filtered(lambda l: l.journal_id == exchange_diff_journal) - self.assertEqual(exchange_stock_line.amount_currency, 0.00) - self.assertAlmostEqual(exchange_stock_line.balance, 139.29) + self.assertRecordValues(inv.line_ids.full_reconcile_id.reconciled_line_ids, [ + # pylint: disable=C0326 + # Exchange difference lines: + {'balance': 46.43, 'amount_currency': 0.0}, + {'balance': 92.86, 'amount_currency': 0.0}, + # Other lines: + {'balance': 50.0, 'amount_currency': 100.0}, + {'balance': 25.0, 'amount_currency': 50.0}, + {'balance': -214.29, 'amount_currency': -150.0}, + ]) ########################## # Invoice 1 # ########################## - move_lines = inv1.line_ids - self.assertEqual(len(move_lines), 4) - # PAYABLE CHECK - payable_line = move_lines.filtered(lambda l: l.account_id.internal_type == 'payable') - self.assertEqual(payable_line.amount_currency, -200.0) - self.assertAlmostEqual(payable_line.balance, -90.91) + self.assertRecordValues(inv1.line_ids, [ + # pylint: disable=C0326 + {'balance': 90.91, 'amount_currency': 200.0, 'account_id': self.stock_input_account.id}, + {'balance': -90.91, 'amount_currency': -200.0, 'account_id': self.company_data['default_account_payable'].id}, + {'balance': 22.73, 'amount_currency': 50.0, 'account_id': self.price_diff_account.id}, + {'balance': -22.73, 'amount_currency': -50.0, 'account_id': self.stock_input_account.id}, + ]) - # # PRODUCTS CHECKS - - # DELIVERY DIFFERENCE (AVERAGE) - stock_lines = move_lines.filtered(lambda l: l.account_id == self.stock_input_account) - self.assertEqual(stock_lines.mapped('journal_id'), inv.journal_id) - self.assertAlmostEqual(sum(stock_lines.mapped('amount_currency')), 150.00) - self.assertAlmostEqual(sum(stock_lines.mapped('balance')), 68.18) - - price_diff_line = move_lines.filtered(lambda l: l.account_id == self.price_diff_account) - self.assertEqual(price_diff_line.amount_currency, 50.00) - self.assertAlmostEqual(price_diff_line.balance, 22.73) - - full_reconcile = stock_lines.mapped('full_reconcile_id') - self.assertTrue(full_reconcile.exists()) - - reconciled_lines = full_reconcile.reconciled_line_ids - stock_lines - self.assertEqual(len(reconciled_lines), 3) - - stock_journal_line = reconciled_lines.filtered(lambda l: l.journal_id == self.stock_journal) - self.assertEqual(stock_journal_line.amount_currency, -150) - self.assertAlmostEqual(stock_journal_line.balance, -187.5) - - exchange_stock_lines = reconciled_lines.filtered(lambda l: l.journal_id == exchange_diff_journal) - self.assertAlmostEqual(sum(exchange_stock_lines.mapped('amount_currency')), 0.00) - self.assertAlmostEqual(sum(exchange_stock_lines.mapped('balance')), 119.32) + self.assertRecordValues(inv1.line_ids.full_reconcile_id.reconciled_line_ids, [ + # pylint: disable=C0326 + # Other lines: + {'balance': -187.5, 'amount_currency': -150.0}, + {'balance': 90.91, 'amount_currency': 200.0}, + {'balance': -22.73, 'amount_currency': -50.0}, + # Exchange difference lines: + {'balance': 119.32, 'amount_currency': 0.0}, + ]) def test_anglosaxon_valuation_price_total_diff_discount(self): """ diff --git a/odoo/addons/base/models/res_currency.py b/odoo/addons/base/models/res_currency.py index ad28f97e2da..ff84f3cbd26 100644 --- a/odoo/addons/base/models/res_currency.py +++ b/odoo/addons/base/models/res_currency.py @@ -255,8 +255,11 @@ class Currency(models.Model): # apply conversion rate if self == to_currency: to_amount = from_amount - else: + elif from_amount: to_amount = from_amount * self._get_conversion_rate(self, to_currency, company, date) + else: + return 0.0 + # apply rounding return to_currency.round(to_amount) if round else to_amount