diff --git a/addons/account/account_bank_statement.py b/addons/account/account_bank_statement.py index 72a907876b9..ffcaf99106d 100644 --- a/addons/account/account_bank_statement.py +++ b/addons/account/account_bank_statement.py @@ -711,6 +711,60 @@ class account_bank_statement_line(osv.osv): 'account_id': account_id } + def _get_exchange_lines(self, cr, uid, st_line, mv_line, currency_diff, currency_id, move_id, context=None): + ''' + Prepare the two lines in company currency due to currency rate difference. + + :param line: browse record of the voucher.line for which we want to create currency rate difference accounting + entries + :param move_id: Account move wher the move lines will be. + :param currency_diff: Amount to be posted. + :param company_currency: id of currency of the company to which the voucher belong + :param current_currency: id of currency of the voucher + :return: the account move line and its counterpart to create, depicted as mapping between fieldname and value + :rtype: tuple of dict + ''' + if currency_diff > 0: + exchange_account_id = st_line.company_id.expense_currency_exchange_account_id.id + else: + exchange_account_id = st_line.company_id.income_currency_exchange_account_id.id + # Even if the amount_currency is never filled, we need to pass the foreign currency because otherwise + # the receivable/payable account may have a secondary currency, which render this field mandatory + if mv_line.account_id.currency_id: + account_currency_id = mv_line.account_id.currency_id.id + else: + account_currency_id = st_line.company_id.currency_id.id != currency_id and currency_id or False + move_line = { + 'journal_id': st_line.journal_id.id, + 'period_id': st_line.statement_id.period_id.id, + 'name': _('change') + ': ' + (st_line.name or '/'), + 'account_id': mv_line.account_id.id, + 'move_id': move_id, + 'partner_id': st_line.partner_id.id, + 'currency_id': account_currency_id, + 'amount_currency': 0.0, + 'quantity': 1, + 'credit': currency_diff > 0 and currency_diff or 0.0, + 'debit': currency_diff < 0 and -currency_diff or 0.0, + 'date': st_line.date, + 'counterpart_move_line_id': mv_line.id, + } + move_line_counterpart = { + 'journal_id': st_line.journal_id.id, + 'period_id': st_line.statement_id.period_id.id, + 'name': _('change') + ': ' + (st_line.name or '/'), + 'account_id': exchange_account_id, + 'move_id': move_id, + 'amount_currency': 0.0, + 'partner_id': st_line.partner_id.id, + 'currency_id': account_currency_id, + 'quantity': 1, + 'debit': currency_diff > 0 and currency_diff or 0.0, + 'credit': currency_diff < 0 and -currency_diff or 0.0, + 'date': st_line.date, + } + return (move_line, move_line_counterpart) + def process_reconciliations(self, cr, uid, data, context=None): for datum in data: self.process_reconciliation(cr, uid, datum[0], datum[1], context=context) @@ -809,6 +863,21 @@ class account_bank_statement_line(osv.osv): if credit_at_old_rate - credit_at_current_rate: currency_diff = credit_at_current_rate - credit_at_old_rate to_create.append(self.get_currency_rate_line(cr, uid, st_line, currency_diff, move_id, context=context)) + if mv_line.currency_id and mv_line_dict['currency_id'] == mv_line.currency_id.id: + amount_unreconciled = mv_line.amount_residual_currency + else: + amount_unreconciled = currency_obj.compute(cr, uid, company_currency.id, mv_line_dict['currency_id'] , mv_line.amount_residual, context=ctx) + if float_is_zero(mv_line_dict['amount_currency'] + amount_unreconciled, precision_rounding=mv_line.currency_id.rounding): + import pudb + pu.db + amount = mv_line_dict['debit'] or mv_line_dict['credit'] + sign = -1 if mv_line_dict['debit'] else 1 + currency_rate_difference = sign * (mv_line.amount_residual - amount) + if not company_currency.is_zero(currency_rate_difference): + exchange_lines = self._get_exchange_lines(cr, uid, st_line, mv_line, currency_rate_difference, mv_line_dict['currency_id'], move_id, context=context) + for exchange_line in exchange_lines: + to_create.append(exchange_line) + else: mv_line_dict['debit'] = debit_at_current_rate mv_line_dict['credit'] = credit_at_current_rate diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py index b674350edbd..767b4f77edc 100644 --- a/addons/account/tests/test_reconciliation.py +++ b/addons/account/tests/test_reconciliation.py @@ -14,20 +14,25 @@ class TestReconciliation(TransactionCase): self.account_invoice_line_model = self.registry('account.invoice.line') self.acc_bank_stmt_model = self.registry('account.bank.statement') self.acc_bank_stmt_line_model = self.registry('account.bank.statement.line') + self.res_currency_model = self.registry('res.currency') + self.res_currency_rate_model = self.registry('res.currency.rate') self.partner_agrolait_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "base", "res_partner_2")[1] self.currency_swiss_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "base", "CHF")[1] self.currency_usd_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "base", "USD")[1] self.account_rcv_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "account", "a_recv")[1] - self.account_rsa_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "account", "rsa")[1] + self.account_fx_income_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "account", "income_fx_income")[1] + self.account_fx_expense_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "account", "income_fx_expense")[1] + self.product_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "product", "product_product_4")[1] self.bank_journal_usd_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "account", "bank_journal_usd")[1] self.account_usd_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "account", "usd_bnk")[1] self.company_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "base", "main_company")[1] - #set expense_currency_exchange_account_id and income_currency_exchange_account_id to a random account - self.registry("res.company").write(self.cr, self.uid, [self.company_id], {'expense_currency_exchange_account_id': self.account_rsa_id, 'income_currency_exchange_account_id':self.account_rsa_id}) + + #set expense_currency_exchange_account_id and income_currency_exchange_account_id to the according accounts + self.registry("res.company").write(self.cr, self.uid, [self.company_id], {'expense_currency_exchange_account_id': self.account_fx_expense_id, 'income_currency_exchange_account_id':self.account_fx_income_id}) def test_balanced_customer_invoice(self): cr, uid = self.cr, self.uid @@ -92,7 +97,7 @@ class TestReconciliation(TransactionCase): self.assertEquals(move_line.currency_id.id, self.currency_swiss_id) checked_line += 1 continue - if move_line.account_id.id == self.account_rsa_id: + if move_line.account_id.id == self.account_fx_expense_id: self.assertEquals(move_line.debit, 10.74) self.assertEquals(move_line.credit, 0.0) checked_line += 1 @@ -164,10 +169,103 @@ class TestReconciliation(TransactionCase): self.assertEquals(move_line.currency_id.id, self.currency_swiss_id) checked_line += 1 continue - if move_line.account_id.id == self.account_rsa_id: + if move_line.account_id.id == self.account_fx_income_id: self.assertEquals(move_line.debit, 0.0) self.assertEquals(move_line.credit, 10.74) checked_line += 1 continue self.assertEquals(checked_line, 3) + 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. + cr, uid = self.cr, self.uid + # We update the currency rate of the currency USD in order to force the gain/loss exchanges in next steps + self.res_currency_rate_model.create(cr, uid, { + 'name': time.strftime('%Y-%m-%d') + ' 00:00:00', + 'currency_id': self.currency_usd_id, + 'rate': 0.033, + }) + # We create a customer invoice of 2.00 USD + invoice_id = self.account_invoice_model.create(cr, uid, { + 'partner_id': self.partner_agrolait_id, + 'currency_id': self.currency_usd_id, + 'name': 'Foreign invoice with exchange gain', + 'account_id': self.account_rcv_id, + 'type': 'out_invoice', + 'date_invoice': 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, + }) + ] + }) + self.registry('account.invoice').signal_workflow(cr, uid, [invoice_id], 'invoice_open') + invoice = self.account_invoice_model.browse(cr, uid, invoice_id) + # We create a bank statement with two lines of 1.00 USD each. + bank_stmt_id = self.acc_bank_stmt_model.create(cr, uid, { + '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') + }) + ] + }) + + statement = self.acc_bank_stmt_model.browse(cr, uid, bank_stmt_id) + + # We process the reconciliation of the invoice line with the two bank statement lines + line_id = None + for l in invoice.move_id.line_id: + if l.account_id.id == self.account_rcv_id: + line_id = l + break + for statement_line in statement.line_ids: + self.acc_bank_stmt_line_model.process_reconciliation(cr, uid, statement_line.id, [ + {'counterpart_move_line_id': line_id.id, 'credit': 1.0, 'debit': 0.0, 'name': line_id.name} + ]) + + # The invoice should be paid, as the payments totally cover its total + self.assertEquals(invoice.state, 'paid', 'The invoice should be paid by now') + reconcile = None + for payment in invoice.payment_ids: + reconcile = payment.reconcile_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.assertEquals(sum([res['debit'] for res in result.values()]), 60.61) + self.assertEquals(sum([res['credit'] for res in result.values()]), 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"')