diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 488a01d7d11..1a9a2a250da 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -115,18 +115,16 @@ class AccountInvoice(models.Model): type_payment = _('Outstanding debits') info = {'title': '', 'outstanding': True, 'content': [], 'invoice_id': self.id} lines = self.env['account.move.line'].search(domain) + currency_id = self.currency_id if len(lines) != 0: for line in lines: # get the outstanding residual value in invoice currency - # get the outstanding residual value in its currency. We don't want to show it - # in the invoice currency since the exchange rate between the invoice date and - # the payment date might have changed. - if line.currency_id: - currency_id = line.currency_id + if line.currency_id and line.currency_id == self.currency_id: amount_to_show = abs(line.amount_residual_currency) else: - currency_id = line.company_id.currency_id - amount_to_show = abs(line.amount_residual) + amount_to_show = line.company_id.currency_id.with_context(date=line.date).compute(abs(line.amount_residual), self.currency_id) + if float_is_zero(amount_to_show, precision_rounding=self.currency_id.rounding): + continue info['content'].append({ 'journal_name': line.ref or line.move_id.name, 'amount': amount_to_show, @@ -145,25 +143,24 @@ class AccountInvoice(models.Model): self.payments_widget = json.dumps(False) if self.payment_move_line_ids: info = {'title': _('Less Payment'), 'outstanding': False, 'content': []} + currency_id = self.currency_id for payment in self.payment_move_line_ids: - #we don't take into account the movement created due to a change difference - if payment.currency_id and payment.move_id.rate_diff_partial_rec_id: - continue if self.type in ('out_invoice', 'in_refund'): amount = sum([p.amount for p in payment.matched_debit_ids if p.debit_move_id in self.move_id.line_ids]) amount_currency = sum([p.amount_currency for p in payment.matched_debit_ids if p.debit_move_id in self.move_id.line_ids]) elif self.type in ('in_invoice', 'out_refund'): amount = sum([p.amount for p in payment.matched_credit_ids if p.credit_move_id in self.move_id.line_ids]) amount_currency = sum([p.amount_currency for p in payment.matched_credit_ids if p.credit_move_id in self.move_id.line_ids]) - # Get the payment value in its currency. We don't want to show it in the invoice - # currency since the exchange rate between the invoice date and the payment date - # might have changed. - if payment.currency_id and amount_currency != 0: - currency_id = payment.currency_id - amount_to_show = -amount_currency + # get the payment value in invoice currency + if payment.currency_id and payment.currency_id == self.currency_id: + amount_to_show = amount_currency else: - currency_id = payment.company_id.currency_id - amount_to_show = -amount + amount_to_show = payment.company_id.currency_id.with_context(date=payment.date).compute(amount, self.currency_id) + if float_is_zero(amount_to_show, precision_rounding=self.currency_id.rounding): + continue + payment_ref = payment.move_id.name + if payment.move_id.ref: + payment_ref += ' (' + payment.move_id.ref + ')' info['content'].append({ 'name': payment.name, 'journal_name': payment.journal_id.name, @@ -174,7 +171,7 @@ class AccountInvoice(models.Model): 'date': payment.date, 'payment_id': payment.id, 'move_id': payment.move_id.id, - 'ref': payment.move_id.ref, + 'ref': payment_ref, }) self.payments_widget = json.dumps(info) @@ -543,9 +540,14 @@ class AccountInvoice(models.Model): @api.v7 def assign_outstanding_credit(self, cr, uid, id, credit_aml_id, context=None): credit_aml = self.pool.get('account.move.line').browse(cr, uid, credit_aml_id, context=context) + inv = self.browse(cr, uid, id, context=context) + if not credit_aml.currency_id and inv.currency_id != inv.company_id.currency_id: + credit_aml.with_context(allow_amount_currency=True).write({ + 'amount_currency': inv.company_id.currency_id.with_context(date=credit_aml.date).compute(credit_aml.balance, inv.currency_id), + 'currency_id': inv.currency_id.id}) if credit_aml.payment_id: credit_aml.payment_id.write({'invoice_ids': [(4, id, None)]}) - return self.browse(cr, uid, id, context=context).register_payment(credit_aml) + return inv.register_payment(credit_aml) @api.multi def action_date_assign(self): diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 56f7adf6cee..59ae9883a9c 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -5,9 +5,10 @@ from openerp import api, fields, models, _ from openerp.osv import expression from openerp.exceptions import RedirectWarning, UserError from openerp.tools.misc import formatLang -from openerp.tools import float_is_zero +from openerp.tools import float_compare, float_is_zero from openerp.tools.safe_eval import safe_eval +from collections import OrderedDict #---------------------------------------------------------- # Entries @@ -239,6 +240,9 @@ class AccountMoveLine(models.Model): amount = abs(line.debit - line.credit) amount_residual_currency = abs(line.amount_currency) or 0.0 sign = 1 if (line.debit - line.credit) > 0 else -1 + if not line.debit and not line.credit and line.amount_currency and line.currency_id: + #residual for exchange rate entries + sign = 1 if float_compare(line.amount_currency, 0, precision_rounding=line.currency_id.rounding) == 1 else -1 for partial_line in (line.matched_debit_ids + line.matched_credit_ids): # If line is a credit (sign = -1) we: @@ -260,8 +264,12 @@ class AccountMoveLine(models.Model): #we can only check the amount in company currency reconciled = False digits_rounding_precision = line.company_id.currency_id.rounding - if float_is_zero(amount, precision_rounding=digits_rounding_precision) and (line.debit or line.credit): - reconciled = True + if float_is_zero(amount, precision_rounding=digits_rounding_precision): + if line.currency_id and line.amount_currency: + if float_is_zero(amount_residual_currency, precision_rounding=line.currency_id.rounding): + reconciled = True + else: + reconciled = True line.reconciled = reconciled line.amount_residual = line.company_id.currency_id.round(amount * sign) @@ -727,15 +735,20 @@ class AccountMoveLine(models.Model): def _get_pair_to_reconcile(self): #field is either 'amount_residual' or 'amount_residual_currency' (if the reconciled account has a secondary currency set) field = self[0].account_id.currency_id and 'amount_residual_currency' or 'amount_residual' + rounding = self[0].company_id.currency_id.rounding + if self[0].currency_id and all([x.amount_currency and x.currency_id == self[0].currency_id for x in self]): + #or if all lines share the same currency + field = 'amount_residual_currency' + rounding = self[0].currency_id.rounding #target the pair of move in self that are the oldest sorted_moves = sorted(self, key=lambda a: a.date) debit = credit = False for aml in sorted_moves: - if credit != False and debit != False: + if credit and debit: break - if aml[field] > 0 and debit == False: + if float_compare(aml[field], 0, precision_rounding=rounding) == 1 and not debit: debit = aml - elif aml[field] < 0 and credit == False: + elif float_compare(aml[field], 0, precision_rounding=rounding) == -1 and not credit: credit = aml return debit, credit @@ -746,12 +759,18 @@ class AccountMoveLine(models.Model): """ if not self.ids: return self - field = self[0].account_id.currency_id and 'amount_residual_currency' or 'amount_residual' sm_debit_move, sm_credit_move = self._get_pair_to_reconcile() #there is no more pair to reconcile so return what move_line are left if not sm_credit_move or not sm_debit_move: return self + field = self[0].account_id.currency_id and 'amount_residual_currency' or 'amount_residual' + if not sm_debit_move.debit and not sm_debit_move.credit: + #both debit and credit field are 0, consider the amount_residual_currency field because it's an exchange difference entry + field = 'amount_residual_currency' + if self[0].currency_id and all([x.currency_id == self[0].currency_id for x in self]): + #all the lines have the same currency, so we consider the amount_residual_currency field + field = 'amount_residual_currency' #Reconcile the pair together amount_reconcile = min(sm_debit_move[field], -sm_credit_move[field]) #Remove from recordset the one(s) that will be totally reconciled @@ -1022,7 +1041,11 @@ class AccountMoveLine(models.Model): raise UserError(_('You cannot change the tax, you should remove and recreate lines.')) if ('account_id' in vals) and self.env['account.account'].browse(vals['account_id']).deprecated: raise UserError(_('You cannot use deprecated account.')) - if any(key in vals for key in ('account_id', 'journal_id', 'date', 'move_id', 'debit', 'credit', 'amount_currency', 'currency_id')): + if any(key in vals for key in ('account_id', 'journal_id', 'date', 'move_id', 'debit', 'credit')): + self._update_check() + if not self._context.get('allow_amount_currency') and any(key in vals for key in ('amount_currency', 'currency_id')): + #hackish workaround to write the amount_currency when assigning a payment to an invoice through the 'add' button + #this is needed to compute the correct amount_residual_currency and potentially create an exchange difference entry self._update_check() #when we set the expected payment date, log a note on the invoice_id related (if any) if vals.get('expected_pay_date') and self.invoice_id: @@ -1074,15 +1097,20 @@ class AccountMoveLine(models.Model): return result @api.model - def compute_amount_fields(self, amount, src_currency, company_currency): + def compute_amount_fields(self, amount, src_currency, company_currency, invoice_currency=False): """ Helper function to compute value for fields debit/credit/amount_currency based on an amount and the currencies given in parameter""" amount_currency = False + currency_id = False if src_currency and src_currency != company_currency: amount_currency = amount amount = src_currency.with_context(self._context).compute(amount, company_currency) + currency_id = src_currency.id debit = amount > 0 and amount or 0.0 credit = amount < 0 and -amount or 0.0 - return debit, credit, amount_currency + if invoice_currency and invoice_currency != company_currency and not amount_currency: + amount_currency = src_currency.with_context(self._context).compute(amount, invoice_currency) + currency_id = invoice_currency.id + return debit, credit, amount_currency, currency_id @api.multi def create_analytic_lines(self): @@ -1184,69 +1212,88 @@ class AccountPartialReconcile(models.Model): help='Utility field to express amount currency') company_id = fields.Many2one('res.company', related='debit_move_id.company_id', store=True, string='Currency') - def create_exchange_rate_entry(self): - """ Automatically create a journal entry to book the exchange rate difference between the `debit_move_id` - and the `credit_move_id`, if both share the same currency, and at the prorata of the amount matched - between them. + def create_exchange_rate_entry(self, aml, amount_diff, diff_in_currency, currency, move_date): + """ Automatically create a journal entry to book the exchange rate difference. That new journal entry is made in the company `currency_exchange_journal_id` and one of its journal - items is matched with the other lines to ensure having the residual amount in company currency that - reflects perfectly the residual amount in foreign currency. + items is matched with the other lines to balance the full reconciliation. """ for rec in self: - if rec.currency_id and rec.debit_move_id.amount_currency and rec.credit_move_id.amount_currency: - #create exchange rate difference journal entry - rate_diff = rec.debit_move_id.debit / rec.debit_move_id.amount_currency - rec.credit_move_id.credit / -rec.credit_move_id.amount_currency - if rec.amount_currency and rec.company_id.currency_id.round(rec.amount_currency * rate_diff): - if not rec.company_id.currency_exchange_journal_id: - raise UserError(_("You should configure the 'Exchange Rate Journal' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates.")) - if not self.company_id.income_currency_exchange_account_id.id: - raise UserError(_("You should configure the 'Gain Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates.")) - if not self.company_id.expense_currency_exchange_account_id.id: - raise UserError(_("You should configure the 'Loss Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates.")) - amount_diff = rec.company_id.currency_id.round(rec.amount_currency * rate_diff) - move_vals = {'journal_id': rec.company_id.currency_exchange_journal_id.id, 'rate_diff_partial_rec_id': rec.id} + if not rec.company_id.currency_exchange_journal_id: + raise UserError(_("You should configure the 'Exchange Rate Journal' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates.")) + if not self.company_id.income_currency_exchange_account_id.id: + raise UserError(_("You should configure the 'Gain Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates.")) + if not self.company_id.expense_currency_exchange_account_id.id: + raise UserError(_("You should configure the 'Loss Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates.")) + move_vals = {'journal_id': rec.company_id.currency_exchange_journal_id.id, 'rate_diff_partial_rec_id': rec.id} - # The move date should be the maximum date between payment and invoice (in case - # of payment in advance). However, we should make sure the move date is not - # recorded after the end of year closing. - move_date = max(rec.debit_move_id.date, rec.credit_move_id.date) - if move_date > rec.company_id.fiscalyear_lock_date: - move_vals['date'] = move_date - - move = rec.env['account.move'].create(move_vals) - line_to_reconcile = rec.env['account.move.line'].with_context(check_move_validity=False).create({ - 'name': _('Currency exchange rate difference'), - 'debit': amount_diff < 0 and -amount_diff or 0.0, - 'credit': amount_diff > 0 and amount_diff or 0.0, - 'account_id': rec.debit_move_id.account_id.id, - 'move_id': move.id, - 'currency_id': rec.currency_id.id, - 'amount_currency': 0.0, - 'partner_id': rec.debit_move_id.partner_id.id, - }) - rec.env['account.move.line'].create({ - 'name': _('Currency exchange rate difference'), - 'debit': amount_diff > 0 and amount_diff or 0.0, - 'credit': amount_diff < 0 and -amount_diff or 0.0, - 'account_id': amount_diff > 0 and rec.company_id.currency_exchange_journal_id.default_debit_account_id.id or rec.company_id.currency_exchange_journal_id.default_credit_account_id.id, - 'move_id': move.id, - 'currency_id': rec.currency_id.id, - 'partner_id': rec.debit_move_id.partner_id.id, - }) - rec.env['account.partial.reconcile'].create({ - 'debit_move_id': amount_diff < 0 and line_to_reconcile.id or rec.debit_move_id.id, - 'credit_move_id': amount_diff > 0 and line_to_reconcile.id or rec.credit_move_id.id, - 'amount': abs(amount_diff), - 'amount_currency': 0.0, - 'currency_id': rec.debit_move_id.currency_id.id, - }) - move.post() + # The move date should be the maximum date between payment and invoice (in case + # of payment in advance). However, we should make sure the move date is not + # recorded after the end of year closing. + if move_date > rec.company_id.fiscalyear_lock_date: + move_vals['date'] = move_date + move = rec.env['account.move'].create(move_vals) + line_to_reconcile = rec.env['account.move.line'].with_context(check_move_validity=False).create({ + 'name': _('Currency exchange rate difference'), + 'debit': amount_diff < 0 and -amount_diff or 0.0, + 'credit': amount_diff > 0 and amount_diff or 0.0, + 'account_id': rec.debit_move_id.account_id.id, + 'move_id': move.id, + 'currency_id': currency.id, + 'amount_currency': -diff_in_currency, + 'partner_id': rec.debit_move_id.partner_id.id, + }) + rec.env['account.move.line'].create({ + 'name': _('Currency exchange rate difference'), + 'debit': amount_diff > 0 and amount_diff or 0.0, + 'credit': amount_diff < 0 and -amount_diff or 0.0, + 'account_id': amount_diff > 0 and rec.company_id.currency_exchange_journal_id.default_debit_account_id.id or rec.company_id.currency_exchange_journal_id.default_credit_account_id.id, + 'move_id': move.id, + 'currency_id': currency.id, + 'amount_currency': diff_in_currency, + 'partner_id': rec.debit_move_id.partner_id.id, + }) + partial_rec = rec.env['account.partial.reconcile'].create({ + 'debit_move_id': aml.credit and line_to_reconcile.id or aml.id, + 'credit_move_id': aml.debit and line_to_reconcile.id or aml.id, + 'amount': abs(amount_diff), + 'amount_currency': abs(diff_in_currency), + 'currency_id': currency.id, + }) + move.post() + return line_to_reconcile.id, partial_rec.id @api.model def create(self, vals): res = super(AccountPartialReconcile, self).create(vals) - #eventually create a journal entry to book the difference due to foreign currency's exchange rate that fluctuates - res.create_exchange_rate_entry() + #check if the reconcilation is full + #first, gather all journal items involved in the reconciliation just created + partial_rec_set = OrderedDict.fromkeys([x for x in res]) + aml_set = self.env['account.move.line'] + total_debit = 0 + total_credit = 0 + total_amount_currency = 0 + currency = None + maxdate = None + aml_to_balance = None + for partial_rec in partial_rec_set: + if currency is None: + currency = partial_rec.currency_id + for aml in [partial_rec.debit_move_id, partial_rec.credit_move_id]: + if aml not in aml_set: + if aml.amount_residual or aml.amount_residual_currency: + aml_to_balance = aml + maxdate = max(aml.date, maxdate) + total_debit += aml.debit + total_credit += aml.credit + aml_set |= aml + if aml.currency_id and aml.currency_id == currency: + total_amount_currency += aml.amount_currency + for x in aml.matched_debit_ids | aml.matched_credit_ids: + partial_rec_set[x] = None + #then, if the total debit and credit are equal, or the total amount in currency is 0, the reconciliation is full + if currency and aml_to_balance and (float_is_zero(total_amount_currency, precision_rounding=currency.rounding) or float_compare(total_debit, total_credit, precision_rounding=currency.rounding) == 0): + #eventually create a journal entry to book the difference due to foreign currency's exchange rate that fluctuates + aml_id, partial_rec_id = partial_rec.create_exchange_rate_entry(aml_to_balance, total_debit - total_credit, total_amount_currency, currency, maxdate) return res @api.multi @@ -1260,6 +1307,31 @@ class AccountPartialReconcile(models.Model): # we must unlink the reconciliation with the exchange rate difference entry as well, to be able to reconcile it with its reversal entry to_unlink = self exchange_rate_entries = self.env['account.move'].search([('rate_diff_partial_rec_id', 'in', self.ids)]) + for rec in self: + partial_rec_set = OrderedDict.fromkeys([rec]) + aml_set = self.env['account.move.line'] + total_debit = 0 + total_credit = 0 + total_amount_currency = 0 + currency = None + for partial_rec in partial_rec_set: + if currency is None: + currency = partial_rec.currency_id + for aml in [partial_rec.debit_move_id, partial_rec.credit_move_id]: + if aml not in aml_set: + total_debit += aml.debit + total_credit += aml.credit + aml_set |= aml + if aml.currency_id and aml.currency_id == currency: + total_amount_currency += aml.amount_currency + for x in aml.matched_debit_ids | aml.matched_credit_ids: + partial_rec_set[x] = None + digits_rounding_precision = aml_set[0].company_id.currency_id.rounding + if float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0 \ + or (currency and float_is_zero(total_amount_currency, precision_rounding=currency.rounding)): + #if the reconciliation is full, also unlink any currency rate diffence entry created + exchange_rate_entries = self.env['account.move'].search([('rate_diff_partial_rec_id', 'in', [x.id for x in partial_rec_set.keys()])]) + # revert the currency difference entry reversed_moves = exchange_rate_entries.reverse_moves() # find the origin currency difference line on the partner account and its newly created reversal, and store them in a list @@ -1275,6 +1347,8 @@ class AccountPartialReconcile(models.Model): to_unlink |= origin_line.matched_debit_ids | origin_line.matched_credit_ids to_rec = origin_line + acm_line pairs_to_rec.append(to_rec) + #make sure that the exchange_rate_entries aren't linked to any partial reconciliation anymore + exchange_rate_entries.write({'rate_diff_partial_rec_id': False}) # the call to super() had to be delayed in order to mark the move lines to reconcile together (to use 'rate_diff_partial_rec_id') res = super(AccountPartialReconcile, to_unlink).unlink() # now that the origin currency difference line is not reconciled anymore, we can reconcile it with its reversal entry to cancel it completly diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index 345b6ec1f20..65d75d139e3 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -353,23 +353,41 @@ class account_payment(models.Model): Return the journal entry. """ aml_obj = self.env['account.move.line'].with_context(check_move_validity=False) - debit, credit, amount_currency = aml_obj.with_context(date=self.payment_date).compute_amount_fields(amount, self.currency_id, self.company_id.currency_id) + invoice_currency = False + if self.invoice_ids and all([x.currency_id == self.invoice_ids[0].currency_id for x in self.invoice_ids]): + #if all the invoices selected share the same currency, record the paiement in that currency too + invoice_currency = self.invoice_ids[0].currency_id + debit, credit, amount_currency, currency_id = aml_obj.with_context(date=self.payment_date).compute_amount_fields(amount, self.currency_id, self.company_id.currency_id, invoice_currency) move = self.env['account.move'].create(self._get_move_vals()) #Write line corresponding to invoice payment counterpart_aml_dict = self._get_shared_move_line_vals(debit, credit, amount_currency, move.id, False) counterpart_aml_dict.update(self._get_counterpart_move_line_vals(self.invoice_ids)) - counterpart_aml_dict.update({'currency_id': self.currency_id != self.company_id.currency_id and self.currency_id.id or False}) + counterpart_aml_dict.update({'currency_id': currency_id}) counterpart_aml = aml_obj.create(counterpart_aml_dict) #Reconcile with the invoices - if self.payment_difference_handling == 'reconcile': - self.invoice_ids.register_payment(counterpart_aml, self.writeoff_account_id, self.journal_id) - else: - self.invoice_ids.register_payment(counterpart_aml) + if self.payment_difference_handling == 'reconcile' and self.payment_difference: + writeoff_line = self._get_shared_move_line_vals(0, 0, 0, move.id, False) + debit_wo, credit_wo, amount_currency_wo, currency_id = aml_obj.with_context(date=self.payment_date).compute_amount_fields(self.payment_difference, self.currency_id, self.company_id.currency_id, invoice_currency) + writeoff_line['name'] = _('Write-Off') + writeoff_line['account_id'] = self.writeoff_account_id.id + writeoff_line['debit'] = debit_wo + writeoff_line['credit'] = credit_wo + writeoff_line['amount_currency'] = amount_currency_wo + writeoff_line['currency_id'] = currency_id + writeoff_line = aml_obj.create(writeoff_line) + if counterpart_aml['debit']: + counterpart_aml['debit'] += credit_wo - debit_wo + if counterpart_aml['credit']: + counterpart_aml['credit'] += debit_wo - credit_wo + counterpart_aml['amount_currency'] -= amount_currency_wo + self.invoice_ids.register_payment(counterpart_aml) #Write counterpart lines + if not self.currency_id != self.company_id.currency_id: + amount_currency = 0 liquidity_aml_dict = self._get_shared_move_line_vals(credit, debit, -amount_currency, move.id, False) liquidity_aml_dict.update(self._get_liquidity_move_line_vals(-amount)) aml_obj.create(liquidity_aml_dict) @@ -381,7 +399,7 @@ class account_payment(models.Model): """ Create the journal entry corresponding to the 'incoming money' part of an internal transfer, return the reconciliable move line """ aml_obj = self.env['account.move.line'].with_context(check_move_validity=False) - debit, credit, amount_currency = aml_obj.with_context(date=self.payment_date).compute_amount_fields(amount, self.currency_id, self.company_id.currency_id) + debit, credit, amount_currency, dummy = aml_obj.with_context(date=self.payment_date).compute_amount_fields(amount, self.currency_id, self.company_id.currency_id) amount_currency = self.destination_journal_id.currency_id and self.currency_id.with_context(date=self.payment_date).compute(amount, self.destination_journal_id.currency_id) or 0 dst_move = self.env['account.move'].create(self._get_move_vals(self.destination_journal_id)) @@ -482,7 +500,7 @@ class account_payment(models.Model): # If the journal has a currency specified, the journal item need to be expressed in this currency if self.journal_id.currency_id and self.currency_id != self.journal_id.currency_id: amount = self.currency_id.with_context(date=self.payment_date).compute(amount, self.journal_id.currency_id) - debit, credit, amount_currency = self.env['account.move.line'].with_context(date=self.payment_date).compute_amount_fields(amount, self.journal_id.currency_id, self.company_id.currency_id) + debit, credit, amount_currency, dummy = self.env['account.move.line'].with_context(date=self.payment_date).compute_amount_fields(amount, self.journal_id.currency_id, self.company_id.currency_id) vals.update({ 'amount_currency': amount_currency, 'currency_id': self.journal_id.currency_id.id, diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py index 73b42c102cb..5dab1ba6a65 100644 --- a/addons/account/tests/test_reconciliation.py +++ b/addons/account/tests/test_reconciliation.py @@ -37,7 +37,7 @@ class TestReconciliation(AccountingTestCase): self.diff_income_account = self.env['res.users'].browse(self.env.uid).company_id.income_currency_exchange_account_id self.diff_expense_account = self.env['res.users'].browse(self.env.uid).company_id.expense_currency_exchange_account_id - def create_invoice(self, type='out_invoice', currency_id=None): + def create_invoice(self, type='out_invoice', invoice_amount=50, currency_id=None): #we create an invoice in given currency invoice = self.account_invoice_model.create({'partner_id': self.partner_agrolait_id, 'reference_type': 'none', @@ -49,9 +49,9 @@ class TestReconciliation(AccountingTestCase): }) self.account_invoice_line_model.create({'product_id': self.product.id, 'quantity': 1, - 'price_unit': 100, + 'price_unit': invoice_amount, 'invoice_id': invoice.id, - 'name': 'product that cost 100', + 'name': 'product that cost ' + str(invoice_amount), 'account_id': self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_revenue').id)], limit=1).id, }) @@ -95,7 +95,7 @@ class TestReconciliation(AccountingTestCase): self.assertEquals(round(move_line.credit, 2), aml_dict[move_line.account_id.id]['credit']) self.assertEquals(round(move_line.amount_currency, 2), aml_dict[move_line.account_id.id]['amount_currency']) self.assertEquals(move_line.currency_id.id, aml_dict[move_line.account_id.id]['currency_id']) - if aml_dict[move_line.account_id.id].get('currency_diff'): + if 'currency_diff' in aml_dict[move_line.account_id.id]: if move_line.credit: rec_ids = [r.id for r in move_line.matched_debit_ids] else: @@ -103,37 +103,38 @@ class TestReconciliation(AccountingTestCase): currency_diff_move = self.env['account.move'].search([('rate_diff_partial_rec_id', 'in', rec_ids)]) self.assertEqual(len(currency_diff_move), 1) for currency_diff_line in currency_diff_move[0].line_ids: + if aml_dict[move_line.account_id.id].get('currency_diff') == 0: + if currency_diff_line.account_id.id == move_line.account_id.id: + self.assertAlmostEquals(currency_diff_line.amount_currency, aml_dict[move_line.account_id.id].get('amount_currency_diff')) if aml_dict[move_line.account_id.id].get('currency_diff') > 0: if currency_diff_line.account_id.id == move_line.account_id.id: self.assertAlmostEquals(currency_diff_line.debit, aml_dict[move_line.account_id.id].get('currency_diff')) else: self.assertAlmostEquals(currency_diff_line.credit, aml_dict[move_line.account_id.id].get('currency_diff')) self.assertIn(currency_diff_line.account_id.id, [self.diff_expense_account.id, self.diff_income_account.id], 'The difference accounts should be used correctly. ') - #self.assertEquals(currency_diff_line.account_id.id, self.account_rsa.id) else: if currency_diff_line.account_id.id == move_line.account_id.id: self.assertAlmostEquals(currency_diff_line.credit, abs(aml_dict[move_line.account_id.id].get('currency_diff'))) else: self.assertAlmostEquals(currency_diff_line.debit, abs(aml_dict[move_line.account_id.id].get('currency_diff'))) self.assertIn(currency_diff_line.account_id.id, [self.diff_expense_account.id, self.diff_income_account.id], 'The difference accounts should be used correctly. ') - #self.assertEquals(currency_diff_line.account_id.id, self.account_rsa.id) - def make_customer_and_supplier_flows(self, invoice_currency_id, bank_journal, amount, amount_currency, transaction_currency_id): + 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(type='out_invoice', currency_id=invoice_currency_id) + invoice_record = self.create_invoice(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 bank_stmt = self.make_payment(invoice_record, bank_journal, amount=amount, amount_currency=amount_currency, currency_id=transaction_currency_id) customer_move_lines = bank_stmt.move_line_ids #we create a supplier bill in given invoice_currency - invoice_record = self.create_invoice(type='in_invoice', currency_id=invoice_currency_id) + invoice_record = self.create_invoice(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 bank_stmt = self.make_payment(invoice_record, bank_journal, amount=-amount, amount_currency=-amount_currency, currency_id=transaction_currency_id) supplier_move_lines = bank_stmt.move_line_ids return customer_move_lines, supplier_move_lines def test_statement_usd_invoice_eur_transaction_eur(self): - customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_euro_id, self.bank_journal_usd, 42, 30, self.currency_euro_id) + customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_euro_id, 30, self.bank_journal_usd, 42, 30, self.currency_euro_id) self.check_results(customer_move_lines, { self.account_usd.id: {'debit': 30.0, 'credit': 0.0, 'amount_currency': 42, 'currency_id': self.currency_usd_id}, self.account_rcv.id: {'debit': 0.0, 'credit': 30.0, 'amount_currency': -42, 'currency_id': self.currency_usd_id}, @@ -144,7 +145,7 @@ class TestReconciliation(AccountingTestCase): }) def test_statement_usd_invoice_usd_transaction_usd(self): - customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_usd_id, self.bank_journal_usd, 50, 0, False) + customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_usd_id, 50, self.bank_journal_usd, 50, 0, False) self.check_results(customer_move_lines, { self.account_usd.id: {'debit': 32.70, 'credit': 0.0, 'amount_currency': 50, 'currency_id': self.currency_usd_id}, self.account_rcv.id: {'debit': 0.0, 'credit': 32.70, 'amount_currency': -50, 'currency_id': self.currency_usd_id}, @@ -155,7 +156,7 @@ class TestReconciliation(AccountingTestCase): }) def test_statement_usd_invoice_usd_transaction_eur(self): - customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_usd_id, self.bank_journal_usd, 50, 40, self.currency_euro_id) + customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_usd_id, 50, self.bank_journal_usd, 50, 40, self.currency_euro_id) self.check_results(customer_move_lines, { self.account_usd.id: {'debit': 40.0, 'credit': 0.0, 'amount_currency': 50, 'currency_id': self.currency_usd_id}, self.account_rcv.id: {'debit': 0.0, 'credit': 40.0, 'amount_currency': -50, 'currency_id': self.currency_usd_id, 'currency_diff': 7.30}, @@ -166,7 +167,7 @@ class TestReconciliation(AccountingTestCase): }) def test_statement_usd_invoice_chf_transaction_chf(self): - customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_swiss_id, self.bank_journal_usd, 42, 50, self.currency_swiss_id) + customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_swiss_id, 50, self.bank_journal_usd, 42, 50, self.currency_swiss_id) self.check_results(customer_move_lines, { self.account_usd.id: {'debit': 27.47, 'credit': 0.0, 'amount_currency': 42, 'currency_id': self.currency_usd_id}, self.account_rcv.id: {'debit': 0.0, 'credit': 27.47, 'amount_currency': -50, 'currency_id': self.currency_swiss_id, 'currency_diff': -10.74}, @@ -177,7 +178,7 @@ class TestReconciliation(AccountingTestCase): }) def test_statement_eur_invoice_usd_transaction_usd(self): - customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_usd_id, self.bank_journal_euro, 40, 50, self.currency_usd_id) + customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_usd_id, 50, self.bank_journal_euro, 40, 50, self.currency_usd_id) self.check_results(customer_move_lines, { self.account_euro.id: {'debit': 40.0, 'credit': 0.0, 'amount_currency': 50, 'currency_id': self.currency_usd_id}, self.account_rcv.id: {'debit': 0.0, 'credit': 40.0, 'amount_currency': -50, 'currency_id': self.currency_usd_id, 'currency_diff': 7.30}, @@ -188,7 +189,7 @@ class TestReconciliation(AccountingTestCase): }) def test_statement_eur_invoice_usd_transaction_eur(self): - customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_usd_id, self.bank_journal_euro, 40, 0.0, False) + customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_usd_id, 50, self.bank_journal_euro, 40, 0.0, False) self.check_results(customer_move_lines, { self.account_euro.id: {'debit': 40.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': False}, self.account_rcv.id: {'debit': 0.0, 'credit': 40.0, 'amount_currency': -61.16, 'currency_id': self.currency_usd_id}, @@ -199,7 +200,7 @@ class TestReconciliation(AccountingTestCase): }) def test_statement_euro_invoice_usd_transaction_chf(self): - customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_usd_id, self.bank_journal_euro, 42, 50, self.currency_swiss_id) + customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_usd_id, 50, self.bank_journal_euro, 42, 50, self.currency_swiss_id) self.check_results(customer_move_lines, { self.account_euro.id: {'debit': 42.0, 'credit': 0.0, 'amount_currency': 50, 'currency_id': self.currency_swiss_id}, self.account_rcv.id: {'debit': 0.0, 'credit': 42.0, 'amount_currency': -50, 'currency_id': self.currency_swiss_id}, @@ -209,6 +210,50 @@ class TestReconciliation(AccountingTestCase): self.account_rcv.id: {'debit': 42.0, 'credit': 0.0, 'amount_currency': 50, 'currency_id': self.currency_swiss_id}, }) + def test_statement_euro_invoice_usd_transaction_euro_full(self): + #we create an invoice in given invoice_currency + invoice_record = self.create_invoice(type='out_invoice', invoice_amount=50, currency_id=self.currency_usd_id) + #we encode a payment on it, on the given bank_journal with amount, amount_currency and transaction_currency given + bank_stmt = self.acc_bank_stmt_model.create({ + 'journal_id': self.bank_journal_euro.id, + 'date': time.strftime('%Y') + '-01-01', + }) + + bank_stmt_line = self.acc_bank_stmt_line_model.create({'name': 'payment', + 'statement_id': bank_stmt.id, + 'partner_id': self.partner_agrolait_id, + 'amount': 40, + 'date': time.strftime('%Y') + '-01-01',}) + + #reconcile the payment with the invoice + for l in invoice_record.move_id.line_ids: + if l.account_id.id == self.account_rcv.id: + line_id = l + break + bank_stmt_line.process_reconciliation(counterpart_aml_dicts=[{ + 'move_line': line_id, + 'debit': 0.0, + 'credit': 32.7, + 'name': line_id.name, + }], new_aml_dicts=[{ + 'debit': 0.0, + 'credit': 7.3, + 'name': 'exchange difference', + 'account_id': self.diff_income_account.id + }]) + self.check_results(bank_stmt.move_line_ids, { + self.account_euro.id: {'debit': 40.0, 'credit': 0.0, 'amount_currency': 0, 'currency_id': False}, + self.account_rcv.id: {'debit': 0.0, 'credit': 32.7, 'amount_currency': -41.97, 'currency_id': self.currency_usd_id, 'currency_diff': 0, 'amount_currency_diff': -8.03}, + self.diff_income_account.id: {'debit': 0.0, 'credit': 7.3, 'amount_currency': 0, 'currency_id': False}, + }) + + # The invoice should be paid, as the payments totally cover its total + self.assertEquals(invoice_record.state, 'paid', 'The invoice should be paid by now') + invoice_rec_line = invoice_record.move_id.line_ids.filtered(lambda x: x.account_id.reconcile) + self.assertTrue(invoice_rec_line.reconciled, 'The invoice should be totally reconciled') + self.assertEquals(invoice_rec_line.amount_residual, 0, 'The invoice should be totally reconciled') + self.assertEquals(invoice_rec_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. diff --git a/addons/account_full_reconcile/models/account_full_reconcile.py b/addons/account_full_reconcile/models/account_full_reconcile.py index 4b666f88371..f4141a3a88f 100644 --- a/addons/account_full_reconcile/models/account_full_reconcile.py +++ b/addons/account_full_reconcile/models/account_full_reconcile.py @@ -2,7 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from collections import OrderedDict -from openerp.tools import float_compare +from openerp.tools import float_compare, float_is_zero from openerp import api, fields, models @@ -25,19 +25,27 @@ class AccountPartialReconcile(models.Model): aml_set = self.env['account.move.line'] total_debit = 0 total_credit = 0 + total_amount_currency = 0 + currency = None for partial_rec in partial_rec_set: + if currency is None: + currency = partial_rec.currency_id for aml in [partial_rec.debit_move_id, partial_rec.credit_move_id]: if aml not in aml_set: total_debit += aml.debit total_credit += aml.credit aml_set |= aml + if aml.currency_id and aml.currency_id == currency: + total_amount_currency += aml.amount_currency for x in aml.matched_debit_ids | aml.matched_credit_ids: partial_rec_set[x] = None partial_rec_ids = [x.id for x in partial_rec_set.keys()] aml_ids = [x.id for x in aml_set] - #then, if the total debit and credit are equal, the reconciliation is full + #then, if the total debit and credit are equal, or the total amount in currency is 0, the reconciliation is full digits_rounding_precision = aml_set[0].company_id.currency_id.rounding - if float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0: + if float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0 \ + or (currency and float_is_zero(total_amount_currency, precision_rounding=currency.rounding)): + #in that case, mark the reference on the partial reconciliations and the entries self.env['account.full.reconcile'].with_context(check_move_validity=False).create({ 'partial_reconcile_ids': [(6, 0, partial_rec_ids)], @@ -47,11 +55,9 @@ class AccountPartialReconcile(models.Model): @api.multi def unlink(self): """When removing a partial reconciliation, also unlink its full reconciliation if it exists""" - full_rec = self.env['account.full.reconcile'] for rec in self: if rec.full_reconcile_id: - full_rec |= rec.full_reconcile_id - full_rec.unlink() + rec.full_reconcile_id.unlink() return super(AccountPartialReconcile, self).unlink()