diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index ff79875cd28..1d56d570217 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 80742a209c1..5165388bbf5 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -9,7 +9,6 @@ from openerp.tools.misc import formatLang from openerp.tools import float_is_zero, float_compare from openerp.tools.safe_eval import safe_eval - #---------------------------------------------------------- # Entries #---------------------------------------------------------- @@ -75,7 +74,6 @@ class AccountMove(models.Model): date = fields.Date(required=True, states={'posted': [('readonly', True)]}, index=True, default=fields.Date.context_today) journal_id = fields.Many2one('account.journal', string='Journal', required=True, states={'posted': [('readonly', True)]}, default=_get_default_journal) currency_id = fields.Many2one('res.currency', compute='_compute_currency', store=True, string="Currency") - rate_diff_partial_rec_id = fields.Many2one('account.partial.reconcile', string='Exchange Rate Entry of', help="Technical field used to keep track of the origin of journal entries created in case of fluctuation of the currency exchange rate. This is needed when cancelling the source: it will post the inverse journal entry to cancel that part too.") state = fields.Selection([('draft', 'Unposted'), ('posted', 'Posted')], string='Status', required=True, readonly=True, copy=False, default='draft', help='All manually created new journal entries are usually in the status \'Unposted\', ' @@ -240,6 +238,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: @@ -261,8 +262,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) @@ -729,15 +734,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 @@ -748,12 +758,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 @@ -1023,7 +1039,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: @@ -1075,15 +1095,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): @@ -1186,132 +1211,125 @@ class AccountPartialReconcile(models.Model): company_id = fields.Many2one('res.company', related='debit_move_id.company_id', store=True, string='Currency') full_reconcile_id = fields.Many2one('account.full.reconcile', string="Full Reconcile") - 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} - # 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, partial_rec @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 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 + aml_ids = aml_set.ids + #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: - #in that case, mark the reference on the partial reconciliations and the entries + if float_is_zero(total_amount_currency, precision_rounding=digits_rounding_precision) or float_compare(total_debit, total_credit, precision_rounding=currency.rounding) == 0: + exchange_move_id = False + exchange_partial_rec_id = False + if currency and aml_to_balance: + #eventually create a journal entry to book the difference due to foreign currency's exchange rate that fluctuates + rate_diff_aml, rate_diff_partial_rec = partial_rec.create_exchange_rate_entry(aml_to_balance, total_debit - total_credit, total_amount_currency, currency, maxdate) + aml_ids.append(rate_diff_aml.id) + partial_rec_ids.append(rate_diff_partial_rec.id) + exchange_move_id = rate_diff_aml.move_id.id + exchange_partial_rec_id = rate_diff_partial_rec.id + #mark the reference of the full reconciliation on the partial ones and on the entries self.env['account.full.reconcile'].with_context(check_move_validity=False).create({ 'partial_reconcile_ids': [(6, 0, partial_rec_ids)], - 'reconciled_line_ids': [(6, 0, aml_ids)]}) + 'reconciled_line_ids': [(6, 0, aml_ids)], + 'exchange_move_id': exchange_move_id, + 'exchange_partial_rec_id': exchange_partial_rec_id, + }) return res @api.multi def unlink(self): - """ When removing a link between entries, we need to revert the eventual journal entries we created to book the - fluctuation of the foreign currency's exchange rate. - We need also to reconcile together the origin currency difference line and its reversal in order to completly - cancel the currency difference entry on the partner account (otherwise it will still appear on the aged balance - for example). - When removing a partial reconciliation, also unlink its full reconciliation if it exists - """ - #remove full reconciliation - 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() - - # revert the currency difference entry - exchange_rate_entries = self.env['account.move'].search([('rate_diff_partial_rec_id', 'in', self.ids)]) - 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 - pairs_to_rec = [] - for rev_move in self.env['account.move'].browse(reversed_moves): - if not rev_move.rate_diff_partial_rec_id: - continue - origin_move = exchange_rate_entries.filtered(lambda x: x.rate_diff_partial_rec_id == rev_move.rate_diff_partial_rec_id) - for acm_line in rev_move.line_ids: - if acm_line.account_id.reconcile: - for origin_line in origin_move.line_ids: - if origin_line.account_id == acm_line.account_id and origin_line.debit == acm_line.credit and origin_line.credit == acm_line.debit: - to_rec = origin_line + acm_line - pairs_to_rec.append(to_rec) - # 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, self).unlink() - # now that the origin currency difference line is not reconciled anymore, we can reconcile it with its reversal entry to cancel it completly - for to_rec in pairs_to_rec: - to_rec.reconcile() - return res + """ When removing a partial reconciliation, also unlink its full reconciliation if it exists """ + to_unlink = self + if self._context.get('full_rec_lookup', True): + full_to_unlink = self.env['account.full.reconcile'] + for rec in self: + #exclude partial reconciliations related to an exchange rate entry, because the unlink of the full reconciliation will already do it + if self.env['account.full.reconcile'].search([('exchange_partial_rec_id', '=', rec.id)]): + to_unlink = to_unlink - rec + #without the deleted partial reconciliations, the full reconciliation won't be full anymore + if rec.full_reconcile_id: + full_to_unlink |= rec.full_reconcile_id + if full_to_unlink: + full_to_unlink.unlink() + if to_unlink: + return super(AccountPartialReconcile, to_unlink).unlink() + return True class AccountFullReconcile(models.Model): _name = "account.full.reconcile" @@ -1320,3 +1338,34 @@ class AccountFullReconcile(models.Model): name = fields.Char(string='Number', required=True, copy=False, default=lambda self: self.env['ir.sequence'].next_by_code('account.reconcile')) partial_reconcile_ids = fields.One2many('account.partial.reconcile', 'full_reconcile_id', string='Reconciliation Parts') reconciled_line_ids = fields.One2many('account.move.line', 'full_reconcile_id', string='Matched Journal Items') + exchange_move_id = fields.Many2one('account.move') + exchange_partial_rec_id = fields.Many2one('account.partial.reconcile') + + @api.multi + def unlink(self): + """ When removing a full reconciliation, we need to revert the eventual journal entries we created to book the + fluctuation of the foreign currency's exchange rate. + We need also to reconcile together the origin currency difference line and its reversal in order to completly + cancel the currency difference entry on the partner account (otherwise it will still appear on the aged balance + for example). + """ + for rec in self: + if not rec.exchange_move_id or not rec.exchange_partial_rec_id: + continue + #reverse the exchange rate entry + reversed_move_id = rec.exchange_move_id.reverse_moves()[0] + reversed_move = self.env['account.move'].browse(reversed_move_id) + #search the original line and its newly created reversal + for aml in reversed_move.line_ids: + if aml.account_id.reconcile: + break + if aml: + if aml.debit: + pair_to_rec = aml | rec.exchange_partial_rec_id.credit_move_id + else: + pair_to_rec = aml | rec.exchange_partial_rec_id.debit_move_id + #remove the partial reconciliation of the exchange rate entry as well + rec.exchange_partial_rec_id.with_context(full_rec_lookup=False).unlink() + #reconcile together the original exchange rate line and its reversal + pair_to_rec.reconcile() + return super(AccountFullReconcile, self).unlink() diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index d9202d319fe..e9ea35c1617 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_payment.py b/addons/account/tests/test_payment.py index 3e9971709ac..b8e2cc3379b 100644 --- a/addons/account/tests/test_payment.py +++ b/addons/account/tests/test_payment.py @@ -94,12 +94,10 @@ class TestPayment(AccountingTestCase): self.assertEqual(len(aml_rec), 1, "Expected a move line with values : %s" % str(aml_dict)) if aml_dict.get('currency_diff'): if aml_rec.credit: - rec_ids = [r.id for r in aml_rec.matched_debit_ids] + currency_diff_move = aml_rec.matched_debit_ids.full_reconcile_id.exchange_move_id else: - rec_ids = [r.id for r in aml_rec.matched_credit_ids] - 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: + currency_diff_move = aml_rec.matched_credit_ids.full_reconcile_id.exchange_move_id + for currency_diff_line in currency_diff_move.line_ids: if aml_dict.get('currency_diff') > 0: if currency_diff_line.account_id.id == aml_rec.account_id.id: self.assertAlmostEquals(currency_diff_line.debit, aml_dict.get('currency_diff')) diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py index 73b42c102cb..65a4c0fdd1a 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,45 +95,41 @@ 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 move_line.credit: - rec_ids = [r.id for r in move_line.matched_debit_ids] - else: - rec_ids = [r.id for r in move_line.matched_credit_ids] - 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 'currency_diff' in aml_dict[move_line.account_id.id]: + currency_diff_move = move_line.full_reconcile_id.exchange_move_id + for currency_diff_line in currency_diff_move.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 +140,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 +151,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 +162,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 +173,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 +184,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 +195,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 +205,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.