diff --git a/addons/account/models/account.py b/addons/account/models/account.py index 4f6685f7fc0..e292ed69818 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -521,11 +521,16 @@ class AccountJournal(models.Model): @api.multi def write(self, vals): for journal in self: + company = journal.company_id if ('company_id' in vals and journal.company_id.id != vals['company_id']): if self.env['account.move'].search([('journal_id', 'in', self.ids)], limit=1): raise UserError(_('This journal already contains items, therefore you cannot modify its company.')) - if self.bank_account_id: - self.bank_account_id.company_id = vals['company_id'] + company = self.env['res.company'].browse(vals['company_id']) + if self.bank_account_id.company_id and self.bank_account_id.company_id != company: + self.bank_account_id.write({ + 'company_id': company.id, + 'partner_id': company.partner_id.id, + }) if ('code' in vals and journal.code != vals['code']): if self.env['account.move'].search([('journal_id', 'in', self.ids)], limit=1): raise UserError(_('This journal already contains items, therefore you cannot modify its short name.')) @@ -541,8 +546,14 @@ class AccountJournal(models.Model): self.default_credit_account_id.currency_id = vals['currency_id'] if self.bank_account_id: self.bank_account_id.currency_id = vals['currency_id'] - if 'bank_account_id' in vals and not vals.get('bank_account_id'): - raise UserError(_('You cannot remove the bank account from the journal once set.')) + if 'bank_account_id' in vals: + if not vals.get('bank_account_id'): + raise UserError(_('You cannot remove the bank account from the journal once set.')) + else: + bank_account = self.env['res.partner.bank'].browse(vals['bank_account_id']) + if bank_account.partner_id != company.partner_id: + raise UserError(_("The partners of the journal's company and the related bank account mismatch.")) + result = super(AccountJournal, self).write(vals) # Create the bank_account_id if necessary diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index dcd4159bce7..9296124eec2 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -690,23 +690,8 @@ class AccountBankStatementLine(models.Model): aml_dict['currency_id'] = statement_currency.id # Create write-offs - # When we register a payment on an invoice, the write-off line contains the amount - # currency if all related invoices have the same currency. We apply the same logic in - # the manual reconciliation. - counterpart_aml = self.env['account.move.line'] - for aml_dict in counterpart_aml_dicts: - counterpart_aml |= aml_dict.get('move_line', self.env['account.move.line']) - new_aml_currency = False - if counterpart_aml\ - and len(counterpart_aml.mapped('currency_id')) == 1\ - and counterpart_aml[0].currency_id\ - and counterpart_aml[0].currency_id != company_currency: - new_aml_currency = counterpart_aml[0].currency_id for aml_dict in new_aml_dicts: aml_dict['payment_id'] = payment and payment.id or False - if new_aml_currency and not aml_dict.get('currency_id'): - aml_dict['currency_id'] = new_aml_currency.id - aml_dict['amount_currency'] = company_currency.with_context(ctx).compute(aml_dict['debit'] - aml_dict['credit'], new_aml_currency) aml_obj.with_context(check_move_validity=False, apply_taxes=True).create(aml_dict) # Create counterpart move lines and reconcile them @@ -717,9 +702,6 @@ class AccountBankStatementLine(models.Model): aml_dict['payment_id'] = payment and payment.id or False counterpart_move_line = aml_dict.pop('move_line') - if counterpart_move_line.currency_id and counterpart_move_line.currency_id != company_currency and not aml_dict.get('currency_id'): - aml_dict['currency_id'] = counterpart_move_line.currency_id.id - aml_dict['amount_currency'] = company_currency.with_context(ctx).compute(aml_dict['debit'] - aml_dict['credit'], counterpart_move_line.currency_id) new_aml = aml_obj.with_context(check_move_validity=False).create(aml_dict) (new_aml | counterpart_move_line).reconcile() diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 47d7bcaf446..3f78e546e36 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -224,7 +224,7 @@ class AccountInvoice(models.Model): @api.depends('move_id.line_ids.amount_residual') def _compute_payments(self): payment_lines = set() - for line in self.move_id.line_ids: + for line in self.move_id.line_ids.filtered(lambda l: l.account_id.id == self.account_id.id): payment_lines.update(line.mapped('matched_credit_ids.credit_move_id.id')) payment_lines.update(line.mapped('matched_debit_ids.debit_move_id.id')) self.payment_move_line_ids = self.env['account.move.line'].browse(list(payment_lines)) @@ -267,7 +267,6 @@ class AccountInvoice(models.Model): ], string='Status', index=True, readonly=True, default='draft', track_visibility='onchange', copy=False, help=" * The 'Draft' status is used when a user is encoding a new and unconfirmed Invoice.\n" - " * The 'Pro-forma' status is used when the invoice does not have an invoice number.\n" " * The 'Open' status is used when user creates invoice, an invoice number is generated. It stays in the open status till the user pays the invoice.\n" " * The 'Paid' status is set automatically when the invoice is paid. Its related journal entries may or may not be reconciled.\n" " * The 'Cancelled' status is used when user cancel invoice.") diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index f5196eb02d6..7902ec8ceca 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -472,7 +472,7 @@ class AccountMoveLine(models.Model): tax_line_id = fields.Many2one('account.tax', string='Originator tax', ondelete='restrict') analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account') analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags') - company_id = fields.Many2one('res.company', related='account_id.company_id', string='Company', store=True) + company_id = fields.Many2one('res.company', related='account_id.company_id', string='Company', store=True, readonly=True) counterpart = fields.Char("Counterpart", compute='_get_counterpart', help="Compute the counter part accounts of this journal item for this journal entry. This can be needed in reports.") # TODO: put the invoice link and partner_id on the account_move @@ -525,7 +525,7 @@ class AccountMoveLine(models.Model): raise ValidationError(_("You cannot create journal items with a secondary currency without filling both 'currency' and 'amount currency' field.")) @api.multi - @api.constrains('amount_currency') + @api.constrains('amount_currency', 'debit', 'credit') def _check_currency_amount(self): for line in self: if line.amount_currency: @@ -558,7 +558,9 @@ 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' + company_currency_id = self[0].account_id.company_id.currency_id + account_curreny_id = self[0].account_id.currency_id + field = (account_curreny_id and company_currency_id != account_curreny_id) and 'amount_residual_currency' or 'amount_residual' #reconciliation on bank accounts are special cases as we don't want to set them as reconcilable #but we still want to reconcile entries that are reversed together in order to clear those lines #in the bank reconciliation report. @@ -596,8 +598,9 @@ class AccountMoveLine(models.Model): #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' + company_currency_id = self[0].account_id.company_id.currency_id + account_curreny_id = self[0].account_id.currency_id + field = (account_curreny_id and company_currency_id != account_curreny_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' @@ -619,9 +622,20 @@ class AccountMoveLine(models.Model): #Check for the currency and amount_currency we can set currency = False amount_reconcile_currency = 0 - if sm_debit_move.currency_id == sm_credit_move.currency_id and sm_debit_move.currency_id.id: - currency = sm_credit_move.currency_id.id - amount_reconcile_currency = min(sm_debit_move.amount_residual_currency, -sm_credit_move.amount_residual_currency) + if sm_debit_move.currency_id == sm_credit_move.currency_id: + if sm_debit_move.currency_id.id: + currency = sm_credit_move.currency_id.id + amount_reconcile_currency = min(sm_debit_move.amount_residual_currency, -sm_credit_move.amount_residual_currency) + else: + if not sm_debit_move.currency_id or not sm_credit_move.currency_id: + # If only one of debit_move or credit_move has a secondary currency, also record the converted amount + # in that secondary currency in the partial reconciliation. That allows the exchange difference entry + # to be created, in case it is needed. + company_currency = sm_debit_move.company_id.currency_id + currency = sm_debit_move.currency_id or sm_credit_move.currency_id + currency_date = sm_debit_move.currency_id and sm_credit_move.date or sm_debit_move.date + amount_reconcile_currency = company_currency.with_context(date=currency_date).compute(amount_reconcile, currency) + currency = currency.id amount_reconcile = min(sm_debit_move.amount_residual, -sm_credit_move.amount_residual) @@ -1061,7 +1075,7 @@ class AccountMoveLine(models.Model): return matched_percentage_per_move @api.model - def compute_amount_fields(self, amount, src_currency, company_currency, invoice_currency=False): + def _compute_amount_fields(self, amount, src_currency, company_currency): """ 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 @@ -1071,9 +1085,6 @@ class AccountMoveLine(models.Model): currency_id = src_currency.id debit = amount > 0 and amount or 0.0 credit = amount < 0 and -amount or 0.0 - 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 def _get_analytic_tag_ids(self): @@ -1436,7 +1447,8 @@ class AccountPartialReconcile(models.Model): total_amount_currency = 0 #make sure that all partial reconciliations share the same secondary currency otherwise it's not #possible to compute the exchange difference entry and it has to be done manually. - currency = self[0].currency_id + currency = self[0].debit_move_id.currency_id or self[0].credit_move_id.currency_id or None + more_than_1_currency = False maxdate = '0000-00-00' seen = set() @@ -1444,9 +1456,14 @@ class AccountPartialReconcile(models.Model): while todo: partial_rec = todo.pop() seen.add(partial_rec) + if partial_rec.debit_move_id.currency_id != currency or partial_rec.credit_move_id.currency_id != currency: + #There's more than 1 secondary currency involved, which means that we can use the total_debit + # and total_credit comparison (otherwise we cannot, as we have to rely only on the amount in + # secondary currency to deal with the case stated in test_partial_reconcile_currencies_02) + more_than_1_currency = True if partial_rec.currency_id != currency: #no exchange rate entry will be created - currency = None + currency = False 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: @@ -1459,9 +1476,9 @@ class AccountPartialReconcile(models.Model): total_amount_currency += aml.amount_currency elif partial_rec.currency_id and partial_rec.currency_id == currency: #if the aml has no secondary currency but is reconciled with other journal item(s) in secondary currency, the amount - #currency is recorded on the partial rec and in order to check if the reconciliation is total, we need to convert the - #aml.balance in that foreign currency - total_amount_currency += aml.company_id.currency_id.with_context(date=aml.date).compute(aml.balance, partial_rec.currency_id) + #in secondary currency is recorded on the partial rec. That allows us to consider it, in order to check if the + # reconciliation is total + total_amount_currency += aml.balance > 0 and partial_rec.amount_currency or - partial_rec.amount_currency for x in aml.matched_debit_ids | aml.matched_credit_ids: if x not in seen: @@ -1469,15 +1486,16 @@ class AccountPartialReconcile(models.Model): partial_rec_ids = [x.id for x in seen] 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 + #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 (currency and float_is_zero(total_amount_currency, precision_rounding=currency.rounding)) or float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0: + if (currency and float_is_zero(total_amount_currency, precision_rounding=currency.rounding)) or \ + ((currency is None or more_than_1_currency) and float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0): exchange_move_id = False - if currency and aml_to_balance: + if aml_to_balance: exchange_move = self.env['account.move'].create( self.env['account.full.reconcile']._prepare_exchange_diff_move(move_date=maxdate, company=aml_to_balance[0].company_id)) #eventually create a journal entry to book the difference due to foreign currency's exchange rate that fluctuates - rate_diff_amls, rate_diff_partial_rec = self.create_exchange_rate_entry(aml_to_balance, total_debit - total_credit, total_amount_currency, currency, exchange_move) + rate_diff_amls, rate_diff_partial_rec = self.create_exchange_rate_entry(aml_to_balance, total_debit - total_credit, total_amount_currency, aml_to_balance.currency_id, exchange_move) aml_ids += rate_diff_amls.ids partial_rec_ids += rate_diff_partial_rec.ids exchange_move.post() diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index ca1e13511d4..09e50feb664 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -2,6 +2,7 @@ from odoo import models, fields, api, _ from odoo.exceptions import UserError, ValidationError +from odoo.tools import float_compare from itertools import groupby @@ -544,11 +545,7 @@ class account_payment(models.Model): Return the journal entry. """ aml_obj = self.env['account.move.line'].with_context(check_move_validity=False) - 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 payment 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) + 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) move = self.env['account.move'].create(self._get_move_vals()) @@ -561,26 +558,7 @@ class account_payment(models.Model): #Reconcile with the invoices if self.payment_difference_handling == 'reconcile' and self.payment_difference: writeoff_line = self._get_shared_move_line_vals(0, 0, 0, move.id, False) - 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)[2:] - # the writeoff debit and credit must be computed from the invoice residual in company currency - # minus the payment amount in company currency, and not from the payment difference in the payment currency - # to avoid loss of precision during the currency rate computations. See revision 20935462a0cabeb45480ce70114ff2f4e91eaf79 for a detailed example. - total_residual_company_signed = sum(invoice.residual_company_signed for invoice in self.invoice_ids) - total_payment_company_signed = self.currency_id.with_context(date=self.payment_date).compute(self.amount, self.company_id.currency_id) - if self.invoice_ids[0].type in ['in_invoice', 'out_refund']: - amount_wo = total_payment_company_signed - total_residual_company_signed - else: - amount_wo = total_residual_company_signed - total_payment_company_signed - # Align the sign of the secondary currency writeoff amount with the sign of the writeoff - # amount in the company currency - if amount_wo > 0: - debit_wo = amount_wo - credit_wo = 0.0 - amount_currency_wo = abs(amount_currency_wo) - else: - debit_wo = 0.0 - credit_wo = -amount_wo - amount_currency_wo = -abs(amount_currency_wo) + 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) writeoff_line['name'] = self.writeoff_label writeoff_line['account_id'] = self.writeoff_account_id.id writeoff_line['debit'] = debit_wo @@ -614,7 +592,7 @@ class account_payment(models.Model): """ Create the journal entry corresponding to the 'incoming money' part of an internal transfer, return the reconcilable move line """ aml_obj = self.env['account.move.line'].with_context(check_move_validity=False) - 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) + 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)) @@ -669,6 +647,7 @@ class account_payment(models.Model): 'credit': credit, 'amount_currency': amount_currency or False, 'payment_id': self.id, + 'journal_id': self.journal_id.id, } def _get_counterpart_move_line_vals(self, invoice=False): @@ -695,7 +674,6 @@ class account_payment(models.Model): return { 'name': name, 'account_id': self.destination_account_id.id, - 'journal_id': self.journal_id.id, 'currency_id': self.currency_id != self.company_id.currency_id and self.currency_id.id or False, } @@ -713,7 +691,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, 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) + 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/models/chart_template.py b/addons/account/models/chart_template.py index 93f17153fa8..d035fe594dc 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -738,6 +738,9 @@ class WizardMultiChartsAccounts(models.TransientModel): res.setdefault('domain', {}) res['domain']['sale_tax_id'] = repr(sale_tax_domain) res['domain']['purchase_tax_id'] = repr(purchase_tax_domain) + else: + self.sale_tax_id = False + self.purchase_tax_id = False if self.chart_template_id.transfer_account_id: self.transfer_account_id = self.chart_template_id.transfer_account_id.id if self.chart_template_id.code_digits: diff --git a/addons/account/models/res_config_settings.py b/addons/account/models/res_config_settings.py index 68d8ef3e83b..d4593130d2c 100644 --- a/addons/account/models/res_config_settings.py +++ b/addons/account/models/res_config_settings.py @@ -36,12 +36,12 @@ class ResConfigSettings(models.TransientModel): module_account_budget = fields.Boolean(string='Budget Management') module_account_payment = fields.Boolean(string='Online Payment') module_account_reports = fields.Boolean("Dynamic Reports") - module_account_reports_followup = fields.Boolean("Enable payment followup management") + module_account_reports_followup = fields.Boolean("Follow-up Levels") module_account_check_printing = fields.Boolean("Allow check printing and deposits") module_account_batch_deposit = fields.Boolean(string='Use batch deposit', help='This allows you to group received checks before you deposit them to the bank.\n' '-This installs the module account_batch_deposit.') - module_account_sepa = fields.Boolean(string='Use SEPA payments') + module_account_sepa = fields.Boolean(string='SEPA Credit Transfer (SCT)') module_account_sepa_direct_debit = fields.Boolean(string='Use SEPA Direct Debit') module_account_plaid = fields.Boolean(string="Plaid Connector") module_account_yodlee = fields.Boolean("Bank Interface - Sync your bank feeds automatically") @@ -49,7 +49,7 @@ class ResConfigSettings(models.TransientModel): module_account_bank_statement_import_ofx = fields.Boolean("Import in .ofx format") module_account_bank_statement_import_csv = fields.Boolean("Import in .csv format") module_account_bank_statement_import_camt = fields.Boolean("Import in CAMT.053 format") - module_currency_rate_live = fields.Boolean(string="Allow Currency Rate Live") + module_currency_rate_live = fields.Boolean(string="Automatic Currency Rates") module_print_docsaway = fields.Boolean(string="Docsaway") module_product_margin = fields.Boolean(string="Allow Product Margin") module_l10n_eu_service = fields.Boolean(string="EU Digital Goods VAT") diff --git a/addons/account/report/account_aged_partner_balance.py b/addons/account/report/account_aged_partner_balance.py index 0140d489817..504a78e05e0 100644 --- a/addons/account/report/account_aged_partner_balance.py +++ b/addons/account/report/account_aged_partner_balance.py @@ -13,6 +13,10 @@ class ReportAgedPartnerBalance(models.AbstractModel): _name = 'report.account.report_agedpartnerbalance' def _get_partner_move_lines(self, account_type, date_from, target_move, period_length): + # This method can receive the context key 'include_nullified_amount' {Boolean} + # Do an invoice and a payment and unreconcile. The amount will be nullified + # By default, the partner wouldn't appear in this report. + # The context key allow it to appear periods = {} start = datetime.strptime(date_from, "%Y-%m-%d") for i in range(5)[::-1]: @@ -190,7 +194,7 @@ class ReportAgedPartnerBalance(models.AbstractModel): values['name'] = _('Unknown Partner') values['trust'] = False - if at_least_one_amount: + if at_least_one_amount or self._context.get('include_nullified_amount'): res.append(values) return res, total, lines diff --git a/addons/account/report/account_overdue_report.py b/addons/account/report/account_overdue_report.py index 4d7e4b87ede..99463c482bb 100644 --- a/addons/account/report/account_overdue_report.py +++ b/addons/account/report/account_overdue_report.py @@ -25,7 +25,7 @@ class ReportOverdue(models.AbstractModel): "FROM account_move_line l " "JOIN account_account_type at ON (l.user_type_id = at.id) " "JOIN account_move m ON (l.move_id = m.id) " - "WHERE l.partner_id IN %s AND at.type IN ('receivable', 'payable') AND NOT l.reconciled GROUP BY l.date, l.name, l.ref, l.date_maturity, l.partner_id, at.type, l.blocked, l.amount_currency, l.currency_id, l.move_id, m.name", (((fields.date.today(), ) + (tuple(partner_ids),)))) + "WHERE l.partner_id IN %s AND at.type IN ('receivable', 'payable') AND l.full_reconcile_id IS NULL GROUP BY l.date, l.name, l.ref, l.date_maturity, l.partner_id, at.type, l.blocked, l.amount_currency, l.currency_id, l.move_id, m.name", (((fields.date.today(), ) + (tuple(partner_ids),)))) for row in self.env.cr.dictfetchall(): res[row.pop('partner_id')].append(row) return res diff --git a/addons/account/report/account_partner_ledger.py b/addons/account/report/account_partner_ledger.py index e28ab7f0169..68fe77f8f40 100644 --- a/addons/account/report/account_partner_ledger.py +++ b/addons/account/report/account_partner_ledger.py @@ -14,7 +14,7 @@ class ReportPartnerLedger(models.AbstractModel): full_account = [] currency = self.env['res.currency'] query_get_data = self.env['account.move.line'].with_context(data['form'].get('used_context', {}))._query_get() - reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".reconciled = false ' + reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".full_reconcile_id IS NULL ' params = [partner.id, tuple(data['computed']['move_state']), tuple(data['computed']['account_ids'])] + query_get_data[2] query = """ SELECT "account_move_line".id, "account_move_line".date, j.code, acc.code as a_code, acc.name as a_name, "account_move_line".ref, m.name as move_name, "account_move_line".name, "account_move_line".debit, "account_move_line".credit, "account_move_line".amount_currency,"account_move_line".currency_id, c.symbol AS currency_code @@ -51,7 +51,7 @@ class ReportPartnerLedger(models.AbstractModel): return result = 0.0 query_get_data = self.env['account.move.line'].with_context(data['form'].get('used_context', {}))._query_get() - reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".reconciled = false ' + reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".full_reconcile_id IS NULL ' params = [partner.id, tuple(data['computed']['move_state']), tuple(data['computed']['account_ids'])] + query_get_data[2] query = """SELECT sum(""" + field + """) @@ -95,7 +95,7 @@ class ReportPartnerLedger(models.AbstractModel): AND NOT a.deprecated""", (tuple(data['computed']['ACCOUNT_TYPE']),)) data['computed']['account_ids'] = [a for (a,) in self.env.cr.fetchall()] params = [tuple(data['computed']['move_state']), tuple(data['computed']['account_ids'])] + query_get_data[2] - reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".reconciled = false ' + reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".full_reconcile_id IS NULL ' query = """ SELECT DISTINCT "account_move_line".partner_id FROM """ + query_get_data[0] + """, account_account AS account, account_move AS am diff --git a/addons/account/tests/test_payment.py b/addons/account/tests/test_payment.py index 3d490eedc49..af8a66df8ef 100644 --- a/addons/account/tests/test_payment.py +++ b/addons/account/tests/test_payment.py @@ -98,9 +98,9 @@ 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: - currency_diff_move = aml_rec.matched_debit_ids.full_reconcile_id.exchange_move_id + currency_diff_move = aml_rec.matched_debit_ids[0].full_reconcile_id.exchange_move_id else: - currency_diff_move = aml_rec.matched_credit_ids.full_reconcile_id.exchange_move_id + currency_diff_move = aml_rec.matched_credit_ids[0].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: @@ -139,6 +139,7 @@ class TestPayment(AccountingTestCase): {'account_id': self.account_eur.id, 'debit': 300.0, 'credit': 0.0, 'amount_currency': 0, 'currency_id': False}, {'account_id': inv_1.account_id.id, 'debit': 0.0, 'credit': 300.0, 'amount_currency': 00, 'currency_id': False}, ]) + self.assertTrue(payment.move_line_ids.filtered(lambda l: l.account_id == inv_1.account_id)[0].full_reconcile_id) liquidity_aml = payment.move_line_ids.filtered(lambda r: r.account_id == self.account_eur) bank_statement = self.reconcile(liquidity_aml, 200, 0, False) @@ -364,7 +365,7 @@ class TestPayment(AccountingTestCase): {'account_id': inv_3.account_id.id, 'debit': 0.0, 'credit': 300.0, 'amount_currency': 0.0, 'currency_id': False}, ]) - def test_payment_and_writeoff_in_other_currency(self): + def test_payment_and_writeoff_in_other_currency_1(self): # Use case: # Company is in EUR, create a customer invoice for 25 EUR and register payment of 25 USD. # Mark invoice as fully paid with a write_off @@ -413,3 +414,96 @@ class TestPayment(AccountingTestCase): {'account_id': self.account_payable.id, 'debit': 0.0, 'credit': 8.65, 'amount_currency': -13.22, 'currency_id': self.currency_usd_id}, {'account_id': self.account_receivable.id, 'debit': 0.0, 'credit': 7.7, 'amount_currency': -11.78, 'currency_id': self.currency_usd_id}, ]) + + def test_payment_and_writeoff_in_other_currency_2(self): + # Use case: + # Company is in EUR, create a supplier bill of 5325.6 USD and register payment of 5325 USD, at a different rate + # Mark invoice as fully paid with a write_off + # Check that all the aml are correctly created. + + # Set exchange rates 0.895@2017-11-01 and 0.88@2017-12-01 + self.env['res.currency.rate'].create({ + 'currency_id': self.currency_usd_id, + 'rate': 0.895, + 'name': time.strftime('%Y') + '-06-26'}) + self.env['res.currency.rate'].create({ + 'currency_id': self.currency_usd_id, + 'rate': 0.88, + 'name': time.strftime('%Y') + '-07-15'}) + + invoice = self.create_invoice(amount=5325.6, type='in_invoice', currency_id=self.currency_usd_id, partner=self.partner_agrolait.id) + self.check_journal_items(invoice.move_id.line_ids, [ + {'account_id': self.account_revenue.id, 'debit': 5950.39, 'credit': 0.0, 'amount_currency': 5325.6, 'currency_id': self.currency_usd_id}, + {'account_id': self.account_receivable.id, 'debit': 0.0, 'credit': 5950.39, 'amount_currency': -5325.6, 'currency_id': self.currency_usd_id}, + ]) + # register payment on invoice + payment = self.payment_model.create({'payment_type': 'outbound', + 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id, + 'partner_type': 'supplier', + 'partner_id': self.partner_agrolait.id, + 'amount': 5325, + 'currency_id': self.currency_usd_id, + 'payment_date': time.strftime('%Y') + '-07-15', + 'payment_difference_handling': 'reconcile', + 'writeoff_account_id': self.account_revenue.id, + 'journal_id': self.bank_journal_euro.id, + 'invoice_ids': [(4, invoice.id, None)] + }) + payment.post() + self.check_journal_items(payment.move_line_ids, [ + {'account_id': self.account_eur.id, 'debit': 0, 'credit': 6051.14, 'amount_currency': -5325.0, 'currency_id': self.currency_usd_id}, + {'account_id': self.account_revenue.id, 'debit': 0.0, 'credit': 0.68, 'amount_currency': -0.6, 'currency_id': self.currency_usd_id}, + {'account_id': self.account_receivable.id, 'debit': 6051.82, 'credit': 0.0, 'amount_currency': 5325.6, 'currency_id': self.currency_usd_id, 'currency_diff': -101.43}, + ]) + + #check the invoice status + self.assertEqual(invoice.state, 'paid') + + def test_payment_and_writeoff_in_other_currency_3(self): + # Use case related in revision 20935462a0cabeb45480ce70114ff2f4e91eaf79 + # Invoice made in secondary currency for which the rate to the company currency + # is higher than the foreign currency decimal precision. + # E.g: Company currency is EUR, create a customer invoice of 247590.40 EUR and + # register payment of 267 USD (1 USD = 948 EUR) + # Mark invoice as fully paid with a write_off + # Check that all the aml are correctly created and that the invoice is paid + + self.env['res.currency.rate'].create({ + 'currency_id': self.currency_usd_id, + 'rate': 1, + 'name': time.strftime('%Y') + '-06-26'}) + self.env['res.currency.rate'].create({ + 'currency_id': self.currency_eur_id, + 'rate': 948, + 'name': time.strftime('%Y') + '-06-26'}) + + invoice = self.create_invoice(amount=247590.4, type='out_invoice', currency_id=self.currency_eur_id, partner=self.partner_agrolait.id) + self.check_journal_items(invoice.move_id.line_ids, [ + {'account_id': self.account_receivable.id, 'debit': 247590.4, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': False}, + {'account_id': self.account_revenue.id, 'debit': 0.0, 'credit': 247590.4, 'amount_currency': 0.0, 'currency_id': False}, + ]) + # register payment on invoice + payment = self.payment_model.create({'payment_type': 'inbound', + 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id, + 'partner_type': 'customer', + 'partner_id': self.partner_agrolait.id, + 'amount': 267, + 'currency_id': self.currency_usd_id, + 'payment_date': time.strftime('%Y') + '-06-26', + 'payment_difference_handling': 'reconcile', + 'writeoff_account_id': self.account_revenue.id, + 'journal_id': self.bank_journal_euro.id, + 'invoice_ids': [(4, invoice.id, None)], + 'name': 'test_payment_and_writeoff_in_other_currency_3', + }) + payment.post() + self.check_journal_items(payment.move_line_ids, [ + {'account_id': self.account_eur.id, 'debit': 253116.0, 'credit': 0.0, 'amount_currency': 267.0, 'currency_id': self.currency_usd_id}, + {'account_id': self.account_revenue.id, 'debit': 0.0, 'credit': 5526.84, 'amount_currency': -5.83, 'currency_id': self.currency_usd_id}, + {'account_id': self.account_receivable.id, 'debit': 0.0, 'credit': 247589.16, 'amount_currency': -261.17, 'currency_id': self.currency_usd_id}, + ]) + + # Check the invoice status and the full reconciliation: the difference on the receivable account + # should have been completed by an exchange rate difference entry + self.assertEqual(invoice.state, 'paid') + self.assertTrue(invoice.move_id.line_ids.filtered(lambda l: l.account_id == self.account_receivable)[0].full_reconcile_id) diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py index 1180df172c9..4840e40f41a 100644 --- a/addons/account/tests/test_reconciliation.py +++ b/addons/account/tests/test_reconciliation.py @@ -103,6 +103,7 @@ class TestReconciliation(AccountingTestCase): self.assertEquals(move_line.currency_id.id, aml_dict[move_line.account_id.id]['currency_id']) if 'currency_diff' in aml_dict[move_line.account_id.id]: currency_diff_move = move_line.full_reconcile_id.exchange_move_id + self.assertTrue(currency_diff_move, 'The full reconciliation should have created an exchange rate journal entry') 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: @@ -196,11 +197,11 @@ class TestReconciliation(AccountingTestCase): 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}, + self.account_rcv.id: {'debit': 0.0, 'credit': 40.0, 'amount_currency': 0.0, 'currency_id': False}, }) self.check_results(supplier_move_lines, { self.account_euro.id: {'debit': 0.0, 'credit': 40.0, 'amount_currency': 0.0, 'currency_id': False}, - self.account_rcv.id: {'debit': 40.0, 'credit': 0.0, 'amount_currency': 61.16, 'currency_id': self.currency_usd_id}, + self.account_rcv.id: {'debit': 40.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': False}, }) def test_statement_euro_invoice_usd_transaction_chf(self): @@ -238,7 +239,7 @@ class TestReconciliation(AccountingTestCase): 'move_line': line_id, 'debit': 0.0, 'credit': 32.7, - 'name': line_id.name, + 'name': 'test_statement_euro_invoice_usd_transaction_euro_full', }], new_aml_dicts=[{ 'debit': 0.0, 'credit': 7.3, @@ -247,8 +248,8 @@ class TestReconciliation(AccountingTestCase): }]) 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': -9.37, 'currency_id': self.currency_usd_id}, + self.account_rcv.id: {'debit': 0.0, 'credit': 32.7, 'amount_currency': 0, 'currency_id': False, '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 @@ -480,7 +481,7 @@ class TestReconciliation(AccountingTestCase): self.assertEquals(round(aml.amount_currency, 2), line['amount_currency']) self.assertEquals(aml.currency_id.id, line['currency_id']) - def test_partial_reconcile_currencies(self): + def test_partial_reconcile_currencies_01(self): # client Account (payable, rsa) # Debit Credit # -------------------------------------------------------- @@ -623,3 +624,86 @@ class TestReconciliation(AccountingTestCase): # Checking if the direction of the move is correct full_rec_payable = full_rec_move.line_ids.filtered(lambda l: l.account_id == self.account_rsa) self.assertEqual(full_rec_payable.balance, 18.75) + + def test_partial_reconcile_currencies_02(self): + #### + # Day 1: Invoice Cust/001 to customer (expressed in USD) + # Market value of USD (day 1): 1 USD = 0.5 EUR + # * Dr. 100 USD / 50 EUR - Accounts receivable + # * Cr. 100 USD / 50 EUR - Revenue + #### + account_revenue = self.env['account.account'].search( + [('user_type_id', '=', self.env.ref( + 'account.data_account_type_revenue').id)], limit=1) + dest_journal_id = self.env['account.journal'].search( + [('type', '=', 'purchase'), + ('company_id', '=', self.env.ref('base.main_company').id)], + limit=1) + + # Delete any old rate - to make sure that we use the ones we need. + old_rates = self.env['res.currency.rate'].search( + [('currency_id', '=', self.currency_usd_id)]) + old_rates.unlink() + + self.env['res.currency.rate'].create({ + 'currency_id': self.currency_usd_id, + 'name': time.strftime('%Y') + '-01-01', + 'rate': 2, + }) + + invoice_cust_1 = self.account_invoice_model.create({ + 'partner_id': self.partner_agrolait_id, + 'account_id': self.account_rcv.id, + 'type': 'out_invoice', + 'currency_id': self.currency_usd_id, + 'date_invoice': time.strftime('%Y') + '-01-01', + }) + self.account_invoice_line_model.create({ + 'quantity': 1.0, + 'price_unit': 100.0, + 'invoice_id': invoice_cust_1.id, + 'name': 'product that cost 100', + 'account_id': account_revenue.id, + }) + invoice_cust_1.action_invoice_open() + self.assertEqual(invoice_cust_1.residual_company_signed, 50.0) + aml = invoice_cust_1.move_id.mapped('line_ids').filtered( + lambda x: x.account_id == account_revenue) + self.assertEqual(aml.credit, 50.0) + ##### + # Day 2: Receive payment for half invoice Cust/1 (in USD) + # ------------------------------------------------------- + # Market value of USD (day 2): 1 USD = 1 EUR + + # Payment transaction: + # * Dr. 50 USD / 50 EUR - EUR Bank (valued at market price + # at the time of receiving the money) + # * Cr. 50 USD / 50 EUR - Accounts Receivable + ##### + self.env['res.currency.rate'].create({ + 'currency_id': self.currency_usd_id, + 'name': time.strftime('%Y') + '-01-02', + 'rate': 1, + }) + # register payment on invoice + payment = self.env['account.payment'].create( + {'payment_type': 'inbound', + 'payment_method_id': self.env.ref( + 'account.account_payment_method_manual_in').id, + 'partner_type': 'customer', + 'partner_id': self.partner_agrolait_id, + 'amount': 50, + 'currency_id': self.currency_usd_id, + 'payment_date': time.strftime('%Y') + '-01-02', + 'journal_id': dest_journal_id.id, + }) + payment.post() + payment_move_line = False + for l in payment.move_line_ids: + if l.account_id == invoice_cust_1.account_id: + payment_move_line = l + invoice_cust_1.register_payment(payment_move_line) + # We expect at this point that the invoice should still be open, + # because they owe us still 50 CC. + self.assertEqual(invoice_cust_1.state, 'open', + 'Invoice is in status %s' % invoice_cust_1.state) diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml index 30b9382e607..c3af025c5e6 100644 --- a/addons/account/views/account_invoice_view.xml +++ b/addons/account/views/account_invoice_view.xml @@ -256,7 +256,7 @@ - + @@ -303,7 +303,7 @@ - + @@ -401,7 +401,7 @@ - +