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