summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurent Smet <las@odoo.com>2020-04-17 07:43:33 +0000
committerLaurent Smet <las@odoo.com>2020-06-15 08:36:04 +0000
commitbeccf82e09d536255d9d9cb9bfe58ebae2559843 (patch)
treee031e879cb607248f437acc351540324c2f27b31
parentf074772d259656a08376bd0519e5648997e700ba (diff)
[REF] account: journal items reconciliation (part 1/2)
This commits refactors the code of the reconciliation, in order to facilitate the process of complex use cases, namely multi-currencies or cash-basis-taxes related (see details below). It also prepares the code for a second refactoring where we will save on each journal items the amount_currency and currency_id field (even in case of operation made in company currency), also in the sake of simplification. 1) Multi-currencies: - the account.partial.reconcile model now will have dedicated columns to specify the amount of the partial reconciliation in the debit_line_id currency and the credit_line_id currency. That comes in handy when dealing with journal items having different secondary currencies, but also allows some simplification. - residual_amount_currency computation changed accordingly - moved models account.full.reconcile and account.partial.reconcile in their dedicated .py file 2) Cash basis taxes - cash basis entries now handle correctly rounding errors to make sure the exact amount of gets reported when the reconciliation becomes full. - the account for the base amount of cash basis entries has to be set, now, on the company instead of on each cash basis tax. - that new 'property' field can be set at the CoA installation via the field property_cash_basis_base_account_id of account.chart.template, or going through the accounting settings. Was task task: 2243420 Was PR #50308 Related: odoo/upgrade#1121 Related: odoo/enterprise#10252 Signed-off-by: Quentin De Paoli (qdp) <qdp@openerp.com>
-rw-r--r--addons/account/models/__init__.py2
-rw-r--r--addons/account/models/account_full_reconcile.py37
-rw-r--r--addons/account/models/account_move.py1472
-rw-r--r--addons/account/models/account_partial_reconcile.py612
-rw-r--r--addons/account/models/account_tax.py6
-rw-r--r--addons/account/models/chart_template.py16
-rw-r--r--addons/account/models/company.py12
-rw-r--r--addons/account/models/res_config_settings.py6
-rw-r--r--addons/account/static/src/js/account_payment_field.js26
-rw-r--r--addons/account/static/src/xml/account_payment.xml2
-rw-r--r--addons/account/static/tests/account_payment_field_tests.js9
-rw-r--r--addons/account/tests/__init__.py2
-rw-r--r--addons/account/tests/common.py42
-rw-r--r--addons/account/tests/test_account_move_payments_widget.py184
-rw-r--r--addons/account/tests/test_account_move_reconcile.py1635
-rw-r--r--addons/account/tests/test_reconciliation.py720
-rw-r--r--addons/account/views/account_chart_template_views.xml1
-rw-r--r--addons/account/views/account_tax_views.xml1
-rw-r--r--addons/account/views/report_payment_receipt_templates.xml2
-rw-r--r--addons/account/views/res_config_settings_views.xml4
-rw-r--r--addons/hr_expense/models/account_move_line.py28
-rw-r--r--addons/l10n_mx/data/account_tax_data.xml8
-rw-r--r--addons/l10n_mx/data/l10n_mx_chart_post_data.xml1
-rw-r--r--addons/sale_stock/tests/test_anglo_saxon_valuation.py2
24 files changed, 3262 insertions, 1568 deletions
diff --git a/addons/account/models/__init__.py b/addons/account/models/__init__.py
index 951796b3282..d9e2695b020 100644
--- a/addons/account/models/__init__.py
+++ b/addons/account/models/__init__.py
@@ -12,6 +12,8 @@ from . import account_reconcile_model
from . import account_payment_term
from . import account_move
from . import account_analytic_default
+from . import account_partial_reconcile
+from . import account_full_reconcile
from . import account_payment
from . import account_bank_statement
from . import chart_template
diff --git a/addons/account/models/account_full_reconcile.py b/addons/account/models/account_full_reconcile.py
new file mode 100644
index 00000000000..b8ef91ec1fa
--- /dev/null
+++ b/addons/account/models/account_full_reconcile.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+from odoo import api, fields, models, _
+
+
+class AccountFullReconcile(models.Model):
+ _name = "account.full.reconcile"
+ _description = "Full Reconcile"
+
+ 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')
+
+ 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 completely
+ cancel the currency difference entry on the partner account (otherwise it will still appear on the aged balance
+ for example).
+ """
+ # Avoid cyclic unlink calls when removing partials.
+ if not self:
+ return True
+
+ moves_to_reverse = self.exchange_move_id
+
+ res = super().unlink()
+
+ # Reverse all exchange moves at once.
+ today = fields.Date.today()
+ default_values_list = [{
+ 'date': today,
+ 'ref': _('Reversal of: %s') % move.name,
+ } for move in moves_to_reverse]
+ moves_to_reverse._reverse_moves(default_values_list, cancel=True)
+
+ return res
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index ddd9a822dfb..e197d28d037 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -216,6 +216,10 @@ class AccountMove(models.Model):
string='Tax Cash Basis Entry of',
help="Technical field used to keep track of the tax cash basis reconciliation. "
"This is needed when cancelling the source: it will post the inverse journal entry to cancel that part too.")
+ tax_cash_basis_move_id = fields.Many2one(
+ comodel_name='account.move',
+ string="Origin Tax Cash Basis Entry",
+ help="The journal entry from which this tax cash basis journal entry has been created.")
# ==== Auto-post feature fields ====
auto_post = fields.Boolean(string='Post Automatically', default=False,
@@ -266,10 +270,10 @@ class AccountMove(models.Model):
# ==== Payment widget fields ====
invoice_outstanding_credits_debits_widget = fields.Text(groups="account.group_account_invoice,account.group_account_readonly",
compute='_compute_payments_widget_to_reconcile_info')
- invoice_payments_widget = fields.Text(groups="account.group_account_invoice,account.group_account_readonly",
- compute='_compute_payments_widget_reconciled_info')
invoice_has_outstanding = fields.Boolean(groups="account.group_account_invoice,account.group_account_readonly",
compute='_compute_payments_widget_to_reconcile_info')
+ invoice_payments_widget = fields.Text(groups="account.group_account_invoice,account.group_account_readonly",
+ compute='_compute_payments_widget_reconciled_info')
# ==== Vendor bill fields ====
invoice_vendor_bill_id = fields.Many2one('account.move', store=False,
@@ -1289,71 +1293,82 @@ class AccountMove(models.Model):
move.invoice_outstanding_credits_debits_widget = json.dumps(False)
move.invoice_has_outstanding = False
- if move.state != 'posted' or move.payment_state not in ('not_paid', 'partial') or not move.is_invoice(include_receipts=True):
+ if move.state != 'posted' \
+ or move.payment_state not in ('not_paid', 'partial') \
+ or not move.is_invoice(include_receipts=True):
continue
- pay_term_line_ids = move.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
- domain = [('account_id', 'in', pay_term_line_ids.mapped('account_id').ids),
- ('move_id.state', '=', 'posted'),
- ('partner_id', '=', move.commercial_partner_id.id),
- ('reconciled', '=', False), '|', ('amount_residual', '!=', 0.0),
- ('amount_residual_currency', '!=', 0.0)]
+ pay_term_lines = move.line_ids\
+ .filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
+
+ domain = [
+ ('account_id', 'in', pay_term_lines.account_id.ids),
+ ('move_id.state', '=', 'posted'),
+ ('partner_id', '=', move.commercial_partner_id.id),
+ ('reconciled', '=', False),
+ '|', ('amount_residual', '!=', 0.0), ('amount_residual_currency', '!=', 0.0),
+ ]
+
+ payments_widget_vals = {'outstanding': True, 'content': [], 'move_id': move.id}
if move.is_inbound():
- domain.extend([('credit', '>', 0), ('debit', '=', 0)])
- type_payment = _('Outstanding credits')
+ domain.append(('balance', '<', 0.0))
+ payments_widget_vals['title'] = _('Outstanding credits')
else:
- domain.extend([('credit', '=', 0), ('debit', '>', 0)])
- type_payment = _('Outstanding debits')
- info = {'title': '', 'outstanding': True, 'content': [], 'move_id': move.id}
- lines = self.env['account.move.line'].search(domain)
- currency_id = move.currency_id
- if len(lines) != 0:
- for line in lines:
- # get the outstanding residual value in invoice currency
- if line.currency_id and line.currency_id == move.currency_id:
- amount_to_show = abs(line.amount_residual_currency)
- else:
- currency = line.company_id.currency_id
- amount_to_show = currency._convert(abs(line.amount_residual), move.currency_id, move.company_id,
- line.date or fields.Date.today())
- if float_is_zero(amount_to_show, precision_rounding=move.currency_id.rounding):
- continue
- info['content'].append({
- 'journal_name': line.ref or line.move_id.name,
- 'amount': amount_to_show,
- 'currency': currency_id.symbol,
- 'id': line.id,
- 'position': currency_id.position,
- 'digits': [69, move.currency_id.decimal_places],
- 'payment_date': fields.Date.to_string(line.date),
- })
- info['title'] = type_payment
- move.invoice_outstanding_credits_debits_widget = json.dumps(info)
- move.invoice_has_outstanding = True
+ domain.append(('balance', '>', 0.0))
+ payments_widget_vals['title'] = _('Outstanding debits')
- def _get_reconciled_info_JSON_values(self):
- self.ensure_one()
- foreign_currency = self.currency_id if self.currency_id != self.company_id.currency_id else False
-
- reconciled_vals = []
- pay_term_line_ids = self.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
- partials = pay_term_line_ids.mapped('matched_debit_ids') + pay_term_line_ids.mapped('matched_credit_ids')
- for partial in partials:
- counterpart_lines = partial.debit_move_id + partial.credit_move_id
- counterpart_line = counterpart_lines.filtered(lambda line: line not in self.line_ids)
-
- if foreign_currency and partial.currency_id == foreign_currency:
- amount = partial.amount_currency
+ if move.currency_id == move.company_currency_id:
+ currency = False
else:
- amount = partial.company_currency_id._convert(partial.amount, self.currency_id, self.company_id, self.date)
+ currency = move.currency_id
+
+ for line in self.env['account.move.line'].search(domain):
+
+ if currency and line.currency_id == currency:
+ # Same foreign currency.
+ amount = abs(line.amount_residual_currency)
+ elif currency:
+ # Different foreign currencies.
+ amount = move.company_currency_id._convert(
+ abs(line.amount_residual),
+ move.currency_id,
+ move.company_id,
+ line.date,
+ )
+ else:
+ # Company's currency.
+ amount = abs(line.amount_residual)
- if float_is_zero(amount, precision_rounding=self.currency_id.rounding):
+ if move.currency_id.is_zero(amount):
+ continue
+
+ payments_widget_vals['content'].append({
+ 'journal_name': line.ref or line.move_id.name,
+ 'amount': amount,
+ 'currency': move.currency_id.symbol,
+ 'id': line.id,
+ 'move_id': line.move_id.id,
+ 'position': move.currency_id.position,
+ 'digits': [69, move.currency_id.decimal_places],
+ 'payment_date': fields.Date.to_string(line.date),
+ })
+
+ if not payments_widget_vals['content']:
continue
- ref = counterpart_line.move_id.name
+ move.invoice_outstanding_credits_debits_widget = json.dumps(payments_widget_vals)
+ move.invoice_has_outstanding = True
+
+ def _get_reconciled_info_JSON_values(self):
+ self.ensure_one()
+
+ reconciled_vals = []
+ for partial, amount, counterpart_line in self._get_reconciled_invoices_partials():
if counterpart_line.move_id.ref:
- ref += ' (' + counterpart_line.move_id.ref + ')'
+ reconciliation_ref = '%s (%s)' % (counterpart_line.move_id.name, counterpart_line.move_id.ref)
+ else:
+ reconciliation_ref = counterpart_line.move_id.name
reconciled_vals.append({
'name': counterpart_line.name,
@@ -1362,29 +1377,26 @@ class AccountMove(models.Model):
'currency': self.currency_id.symbol,
'digits': [69, self.currency_id.decimal_places],
'position': self.currency_id.position,
- 'date': counterpart_line.payment_id.date or counterpart_line.date,
+ 'date': counterpart_line.date,
'payment_id': counterpart_line.id,
+ 'partial_id': partial.id,
'account_payment_id': counterpart_line.payment_id.id,
'payment_method_name': counterpart_line.payment_id.payment_method_id.name if counterpart_line.journal_id.type == 'bank' else None,
'move_id': counterpart_line.move_id.id,
- 'ref': ref,
+ 'ref': reconciliation_ref,
})
return reconciled_vals
@api.depends('move_type', 'line_ids.amount_residual')
def _compute_payments_widget_reconciled_info(self):
for move in self:
- if move.state != 'posted' or not move.is_invoice(include_receipts=True):
- move.invoice_payments_widget = json.dumps(False)
- continue
- reconciled_vals = move._get_reconciled_info_JSON_values()
- if reconciled_vals:
- info = {
- 'title': _('Less Payment'),
- 'outstanding': False,
- 'content': reconciled_vals,
- }
- move.invoice_payments_widget = json.dumps(info, default=date_utils.json_default)
+ payments_widget_vals = {'title': _('Less Payment'), 'outstanding': False, 'content': []}
+
+ if move.state == 'posted' and move.is_invoice(include_receipts=True):
+ payments_widget_vals['content'] = move._get_reconciled_info_JSON_values()
+
+ if payments_widget_vals['content']:
+ move.invoice_payments_widget = json.dumps(payments_widget_vals, default=date_utils.json_default)
else:
move.invoice_payments_widget = json.dumps(False)
@@ -1822,6 +1834,78 @@ class AccountMove(models.Model):
}[self.move_type]
# -------------------------------------------------------------------------
+ # RECONCILIATION METHODS
+ # -------------------------------------------------------------------------
+
+ def _collect_tax_cash_basis_values(self):
+ ''' Collect all information needed to create the tax cash basis journal entries:
+ - Determine if a tax cash basis journal entry is needed.
+ - Compute the lines to be processed and the amounts needed to compute a percentage.
+ :return: A dictionary:
+ * move: The current account.move record passed as parameter.
+ * to_process_lines: An account.move.line recordset being not exigible on the tax report.
+ * currency: The currency on which the percentage has been computed.
+ * total_balance: sum(payment_term_lines.mapped('balance').
+ * total_residual: sum(payment_term_lines.mapped('amount_residual').
+ * total_amount_currency: sum(payment_term_lines.mapped('amount_currency').
+ * total_residual_currency: sum(payment_term_lines.mapped('amount_residual_currency').
+ * is_fully_paid: A flag indicating the current move is now fully paid.
+ '''
+ self.ensure_one()
+
+ values = {
+ 'move': self,
+ 'to_process_lines': self.env['account.move.line'],
+ 'payment_term_lines': self.env['account.move.line'],
+ 'total_balance': 0.0,
+ 'total_residual': 0.0,
+ 'total_amount_currency': 0.0,
+ 'total_residual_currency': 0.0,
+ }
+
+ currencies = set()
+ has_term_lines = False
+ for line in self.line_ids:
+ if line.account_internal_type in ('receivable', 'payable'):
+ sign = 1 if line.balance > 0.0 else -1
+
+ currencies.add(line.currency_id or line.company_currency_id)
+ has_term_lines = True
+ values['total_balance'] += sign * line.balance
+ values['total_residual'] += sign * line.amount_residual
+ values['total_amount_currency'] += sign * line.amount_currency
+ values['total_residual_currency'] += sign * line.amount_residual_currency
+
+ elif not line.tax_exigible:
+
+ values['to_process_lines'] += line
+ currencies.add(line.currency_id or line.company_currency_id)
+
+ if not values['to_process_lines'] or not has_term_lines:
+ return None
+
+ # Compute the currency on which made the percentage.
+ if len(currencies) == 1:
+ values['currency'] = list(currencies)[0]
+ else:
+ # Don't support the case where there is multiple involved currencies.
+ return None
+
+ if len(values['payment_term_lines'].account_id) > 1:
+ # Don't support the case where there is multiple involved receivable/payable accounts.
+ # It could lead to some weird situation regarding the cash basis exchange difference
+ # journal items.
+ return None
+
+ # Determine is the move is now fully paid.
+ if values['currency'] == self.company_id.currency_id:
+ values['is_fully_paid'] = values['currency'].is_zero(values['total_residual'])
+ else:
+ values['is_fully_paid'] = values['currency'].is_zero(values['total_residual_currency'])
+
+ return values
+
+ # -------------------------------------------------------------------------
# BUSINESS METHODS
# -------------------------------------------------------------------------
@@ -1923,7 +2007,6 @@ class AccountMove(models.Model):
else:
raise UserError(_('The combination of reference model and reference type on the journal is not implemented'))
-
def _get_move_display_name(self, show_ref=False):
''' Helper to get the display name of an invoice depending of its type.
:param show_ref: A flag indicating of the display name must include or not the journal entry reference.
@@ -1954,41 +2037,6 @@ class AccountMove(models.Model):
self.ensure_one()
return self.partner_id.address_get(['delivery'])['delivery']
- def _get_cash_basis_matched_percentage(self):
- """Compute the percentage to apply for cash basis method. This value is relevant only for moves that
- involve journal items on receivable or payable accounts.
- """
- self.ensure_one()
- query = '''
- SELECT
- (
- SELECT COALESCE(SUM(line.balance), 0.0)
- FROM account_move_line line
- JOIN account_account account ON account.id = line.account_id
- JOIN account_account_type account_type ON account_type.id = account.user_type_id
- WHERE line.move_id = %s AND account_type.type IN ('receivable', 'payable')
- ) AS total_amount,
- (
- SELECT COALESCE(SUM(partial.amount), 0.0)
- FROM account_move_line line
- JOIN account_account account ON account.id = line.account_id
- JOIN account_account_type account_type ON account_type.id = account.user_type_id
- LEFT JOIN account_partial_reconcile partial ON
- partial.debit_move_id = line.id
- OR
- partial.credit_move_id = line.id
- WHERE line.move_id = %s AND account_type.type IN ('receivable', 'payable')
- ) AS total_reconciled
- '''
- params = [self.id, self.id]
- self._cr.execute(query, params)
- total_amount, total_reconciled = self._cr.fetchone()
- currency = self.company_id.currency_id
- if float_is_zero(total_amount, precision_rounding=currency.rounding):
- return 1.0
- else:
- return abs(currency.round(total_reconciled) / currency.round(total_amount))
-
def _get_reconciled_payments(self):
"""Helper used to retrieve the reconciled payments on this journal entry"""
reconciled_lines = self.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
@@ -2012,32 +2060,17 @@ class AccountMove(models.Model):
def _get_reconciled_invoices_partials(self):
''' Helper to retrieve the details about reconciled invoices.
- :return A list of tuple (amount, rate, invoice_line).
+ :return A list of tuple (partial, amount, invoice_line).
'''
self.ensure_one()
-
- def compute_tuple(partial, payment_line, invoice_line):
- if payment_line.currency_id == invoice_line.currency_id:
- # Same currency.
- exchange_rate = None
- amount_paid = partial.amount_currency
- elif invoice_line.currency_id:
- # Different currency on the payment.
- exchange_rate = payment_line.amount_currency / payment_line.balance
- amount_paid = partial.company_currency_id._convert(partial.amount, invoice_line.currency_id, invoice_line.company_id, invoice_line.date)
- else:
- # No foreign currency on the payment, don't care about the invoice's currency.
- exchange_rate = None
- amount_paid = partial.amount
- return (amount_paid, exchange_rate, invoice_line)
-
- receivable_lines = self.line_ids.filtered(lambda line: line.account_internal_type == 'receivable')
+ pay_term_lines = self.line_ids\
+ .filtered(lambda line: line.account_internal_type in ('receivable', 'payable'))
invoice_partials = []
- for partial in receivable_lines.matched_debit_ids:
- invoice_partials.append(compute_tuple(partial, partial.credit_move_id, partial.debit_move_id))
- for partial in receivable_lines.matched_credit_ids:
- invoice_partials.append(compute_tuple(partial, partial.debit_move_id, partial.credit_move_id))
+ for partial in pay_term_lines.matched_debit_ids:
+ invoice_partials.append((partial, partial.credit_amount_currency, partial.debit_move_id))
+ for partial in pay_term_lines.matched_credit_ids:
+ invoice_partials.append((partial, partial.debit_amount_currency, partial.credit_move_id))
return invoice_partials
def _reverse_move_vals(self, default_values, cancel=True):
@@ -2346,11 +2379,25 @@ class AccountMove(models.Model):
return self.post()
def js_assign_outstanding_line(self, line_id):
+ ''' Called by the 'payment' widget to reconcile a suggested journal item to the present
+ invoice.
+
+ :param line_id: The id of the line to reconcile with the current invoice.
+ '''
self.ensure_one()
lines = self.env['account.move.line'].browse(line_id)
lines += self.line_ids.filtered(lambda line: line.account_id == lines[0].account_id and not line.reconciled)
return lines.reconcile()
+ def js_remove_outstanding_partial(self, partial_id):
+ ''' Called by the 'payment' widget to remove a reconciled entry to the present invoice.
+
+ :param partial_id: The id of an existing partial reconciled with the current invoice.
+ '''
+ self.ensure_one()
+ partial = self.env['account.partial.reconcile'].browse(partial_id)
+ return partial.unlink()
+
def button_draft(self):
AccountMoveLine = self.env['account.move.line']
excluded_move_ids = []
@@ -2688,7 +2735,7 @@ class AccountMoveLine(models.Model):
currency_field='always_set_currency_id')
price_total = fields.Monetary(string='Total', store=True, readonly=True,
currency_field='always_set_currency_id')
- reconciled = fields.Boolean(compute='_amount_residual', store=True)
+ reconciled = fields.Boolean(compute='_compute_amount_residual', store=True)
blocked = fields.Boolean(string='No Follow-up', default=False,
help="You can check this box to mark this journal item as a litigation with the associated partner")
date_maturity = fields.Date(string='Due Date', index=True, tracking=True,
@@ -2737,10 +2784,10 @@ class AccountMoveLine(models.Model):
# ==== Reconciliation fields ====
amount_residual = fields.Monetary(string='Residual Amount', store=True,
currency_field='company_currency_id',
- compute='_amount_residual',
+ compute='_compute_amount_residual',
help="The residual amount on a journal item expressed in the company currency.")
amount_residual_currency = fields.Monetary(string='Residual Amount in Currency', store=True,
- compute='_amount_residual',
+ compute='_compute_amount_residual',
help="The residual amount on a journal item expressed in its currency (possibly not the company currency).")
full_reconcile_id = fields.Many2one('account.full.reconcile', string="Matching", copy=False, index=True, readonly=True)
matched_debit_ids = fields.One2many('account.partial.reconcile', 'credit_move_id', string='Matched Debits',
@@ -3329,59 +3376,34 @@ class AccountMoveLine(models.Model):
for record in self:
record.cumulated_balance = result[record.id]
- @api.depends('debit', 'credit', 'account_id', 'amount_currency', 'currency_id', 'matched_debit_ids', 'matched_credit_ids', 'matched_debit_ids.amount', 'matched_credit_ids.amount', 'move_id.state', 'company_id')
- def _amount_residual(self):
+ @api.depends('debit', 'credit', 'amount_currency', 'account_id', 'currency_id', 'move_id.state', 'company_id',
+ 'matched_debit_ids', 'matched_credit_ids')
+ def _compute_amount_residual(self):
""" Computes the residual amount of a move line from a reconcilable account in the company currency and the line's currency.
This amount will be 0 for fully reconciled lines or lines from a non-reconcilable account, the original line amount
for unreconciled lines, and something in-between for partially reconciled lines.
"""
for line in self:
- if not line.account_id.reconcile and line.account_id.internal_type != 'liquidity':
- line.reconciled = False
- line.amount_residual = 0
- line.amount_residual_currency = 0
- continue
- #amounts in the partial reconcile table aren't signed, so we need to use abs()
- 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:
- # - subtract matched_debit_ids (partial_line.credit_move_id == line)
- # - add matched_credit_ids (partial_line.credit_move_id != line)
- # If line is a debit (sign = 1), do the opposite.
- sign_partial_line = sign if partial_line.credit_move_id == line else (-1 * sign)
-
- amount += sign_partial_line * partial_line.amount
- #getting the date of the matched item to compute the amount_residual in currency
- if line.currency_id and line.amount_currency:
- if partial_line.currency_id and partial_line.currency_id == line.currency_id:
- amount_residual_currency += sign_partial_line * partial_line.amount_currency
- else:
- if line.balance and line.amount_currency:
- rate = line.amount_currency / line.balance
- else:
- date = partial_line.credit_move_id.date if partial_line.debit_move_id == line else partial_line.debit_move_id.date
- rate = line.currency_id.with_context(date=date).rate
- amount_residual_currency += sign_partial_line * line.currency_id.round(partial_line.amount * rate)
-
- #computing the `reconciled` field.
- reconciled = False
- digits_rounding_precision = line.move_id.company_id.currency_id.rounding
- 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
+ if line.id and (line.account_id.reconcile or line.account_id.internal_type == 'liquidity'):
+ reconciled_balance = sum(line.matched_credit_ids.mapped('amount')) \
+ - sum(line.matched_debit_ids.mapped('amount'))
+ reconciled_amount_currency = sum(line.matched_credit_ids.mapped('debit_amount_currency'))\
+ - sum(line.matched_debit_ids.mapped('credit_amount_currency'))
+
+ line.amount_residual = line.balance - reconciled_balance
+
+ if line.currency_id:
+ line.amount_residual_currency = line.amount_currency - reconciled_amount_currency
else:
- reconciled = True
- line.reconciled = reconciled
+ line.amount_residual_currency = 0.0
- line.amount_residual = line.move_id.company_id.currency_id.round(amount * sign) if line.move_id.company_id else amount * sign
- line.amount_residual_currency = line.currency_id and line.currency_id.round(amount_residual_currency * sign) or 0.0
+ line.reconciled = line.company_currency_id.is_zero(line.amount_residual) \
+ and (not line.currency_id or line.currency_id.is_zero(line.amount_residual_currency))
+ else:
+ # Must not have any reconciliation since the line is not eligible for that.
+ line.amount_residual = 0.0
+ line.amount_residual_currency = 0.0
+ line.reconciled = False
@api.depends('tax_repartition_line_id.invoice_tax_id', 'tax_repartition_line_id.refund_tax_id')
def _compute_tax_line_id(self):
@@ -3847,270 +3869,505 @@ class AccountMoveLine(models.Model):
# RECONCILIATION
# -------------------------------------------------------------------------
- def check_full_reconcile(self):
- """
- This method check if a move is totally reconciled and if we need to create exchange rate entries for the move.
- In case exchange rate entries needs to be created, one will be created per currency present.
- In case of full reconciliation, all moves belonging to the reconciliation will belong to the same account_full_reconcile object.
- """
- # Get first all aml involved
- todo = self.env['account.partial.reconcile'].search_read(['|', ('debit_move_id', 'in', self.ids), ('credit_move_id', 'in', self.ids)], ['debit_move_id', 'credit_move_id'])
- amls = set(self.ids)
- seen = set()
- while todo:
- aml_ids = [rec['debit_move_id'][0] for rec in todo if rec['debit_move_id']] + [rec['credit_move_id'][0] for rec in todo if rec['credit_move_id']]
- amls |= set(aml_ids)
- seen |= set([rec['id'] for rec in todo])
- todo = self.env['account.partial.reconcile'].search_read(['&', '|', ('credit_move_id', 'in', aml_ids), ('debit_move_id', 'in', aml_ids), '!', ('id', 'in', list(seen))], ['debit_move_id', 'credit_move_id'])
-
- partial_rec_ids = list(seen)
- if not amls:
- return
- else:
- amls = self.browse(list(amls))
-
- # If we have multiple currency, we can only base ourselves on debit-credit to see if it is fully reconciled
- currency = set([a.currency_id for a in amls if a.currency_id.id != False])
- multiple_currency = False
- if len(currency) != 1:
- currency = False
- multiple_currency = True
- else:
- currency = list(currency)[0]
- # Get the sum(debit, credit, amount_currency) of all amls involved
- total_debit = 0
- total_credit = 0
- total_amount_currency = 0
- maxdate = date.min
- to_balance = {}
- cash_basis_partial = self.env['account.partial.reconcile']
- for aml in amls:
- cash_basis_partial |= aml.move_id.tax_cash_basis_rec_id
- total_debit += aml.debit
- total_credit += aml.credit
- maxdate = max(aml.date, maxdate)
- total_amount_currency += aml.amount_currency
- # Convert in currency if we only have one currency and no amount_currency
- if not aml.amount_currency and currency:
- multiple_currency = True
- total_amount_currency += aml.company_id.currency_id._convert(aml.balance, currency, aml.company_id, aml.date)
- # If we still have residual value, it means that this move might need to be balanced using an exchange rate entry
- if aml.amount_residual != 0 or aml.amount_residual_currency != 0:
- if not to_balance.get(aml.currency_id):
- to_balance[aml.currency_id] = [self.env['account.move.line'], 0]
- to_balance[aml.currency_id][0] += aml
- to_balance[aml.currency_id][1] += aml.amount_residual != 0 and aml.amount_residual or aml.amount_residual_currency
-
- # Check if reconciliation is total
- # To check if reconciliation is total we have 3 different use case:
- # 1) There are multiple currency different than company currency, in that case we check using debit-credit
- # 2) We only have one currency which is different than company currency, in that case we check using amount_currency
- # 3) We have only one currency and some entries that don't have a secundary currency, in that case we check debit-credit
- # or amount_currency.
- # 4) Cash basis full reconciliation
- # - either none of the moves are cash basis reconciled, and we proceed
- # - or some moves are cash basis reconciled and we make sure they are all fully reconciled
-
- digits_rounding_precision = amls[0].company_id.currency_id.rounding
- if (
- (
- not cash_basis_partial or (cash_basis_partial and all([p >= 1.0 for p in amls._get_matched_percentage().values()]))
- ) and
- (
- currency and float_is_zero(total_amount_currency, precision_rounding=currency.rounding) or
- multiple_currency and float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0
+ def _prepare_reconciliation_partials(self):
+ ''' Prepare the partials on the current journal items to perform the reconciliation.
+ /!\ The order of records in self is important because the journal items will be reconciled using this order.
+
+ :return: A recordset of account.partial.reconcile.
+ '''
+ debit_lines = iter(self.filtered('debit'))
+ credit_lines = iter(self.filtered('credit'))
+ debit_line = None
+ credit_line = None
+
+ debit_amount_residual = 0.0
+ debit_amount_residual_currency = 0.0
+ credit_amount_residual = 0.0
+ credit_amount_residual_currency = 0.0
+ debit_line_currency = None
+ credit_line_currency = None
+
+ partials_vals_list = []
+
+ while True:
+
+ # Move to the next available debit line.
+ if not debit_line:
+ debit_line = next(debit_lines, None)
+ if not debit_line:
+ break
+ debit_amount_residual = debit_line.amount_residual
+
+ if debit_line.currency_id:
+ debit_amount_residual_currency = debit_line.amount_residual_currency
+ debit_line_currency = debit_line.currency_id
+ else:
+ debit_amount_residual_currency = debit_amount_residual
+ debit_line_currency = debit_line.company_currency_id
+
+ # Move to the next available credit line.
+ if not credit_line:
+ credit_line = next(credit_lines, None)
+ if not credit_line:
+ break
+ credit_amount_residual = credit_line.amount_residual
+
+ if credit_line.currency_id:
+ credit_amount_residual_currency = credit_line.amount_residual_currency
+ credit_line_currency = credit_line.currency_id
+ else:
+ credit_amount_residual_currency = credit_amount_residual
+ credit_line_currency = credit_line.company_currency_id
+
+ min_amount_residual = min(debit_amount_residual, -credit_amount_residual)
+
+ if debit_line_currency == credit_line_currency:
+ # Reconcile on the same currency.
+
+ # The debit line is now fully reconciled.
+ if debit_line_currency.is_zero(debit_amount_residual_currency) or debit_amount_residual_currency < 0.0:
+ debit_line = None
+ continue
+
+ # The credit line is now fully reconciled.
+ if credit_line_currency.is_zero(credit_amount_residual_currency) or credit_amount_residual_currency > 0.0:
+ credit_line = None
+ continue
+
+ min_amount_residual_currency = min(debit_amount_residual_currency, -credit_amount_residual_currency)
+ min_debit_amount_residual_currency = min_amount_residual_currency
+ min_credit_amount_residual_currency = min_amount_residual_currency
+
+ else:
+ # Reconcile on the company's currency.
+
+ # The debit line is now fully reconciled.
+ if debit_line.company_currency_id.is_zero(debit_amount_residual) or debit_amount_residual < 0.0:
+ debit_line = None
+ continue
+
+ # The credit line is now fully reconciled.
+ if credit_line.company_currency_id.is_zero(credit_amount_residual) or credit_amount_residual > 0.0:
+ credit_line = None
+ continue
+
+ min_debit_amount_residual_currency = credit_line.company_currency_id._convert(
+ min_amount_residual,
+ debit_line.currency_id,
+ credit_line.company_id,
+ credit_line.date,
)
- ):
-
- exchange_move_id = False
- missing_exchange_difference = False
- # Eventually create a journal entry to book the difference due to foreign currency's exchange rate that fluctuates
- if to_balance and any([not float_is_zero(residual, precision_rounding=digits_rounding_precision) for aml, residual in to_balance.values()]):
- if not self.env.context.get('no_exchange_difference'):
- exchange_move_vals = self.env['account.full.reconcile']._prepare_exchange_diff_move(
- move_date=maxdate, company=amls[0].company_id)
- if len(amls.mapped('partner_id')) == 1 and amls[0].partner_id:
- exchange_move_vals['partner_id'] = amls[0].partner_id.id
- exchange_move = self.env['account.move'].with_context(default_move_type='entry').create(exchange_move_vals)
- part_reconcile = self.env['account.partial.reconcile']
- for aml_to_balance, total in to_balance.values():
- if total:
- rate_diff_amls, rate_diff_partial_rec = part_reconcile.create_exchange_rate_entry(aml_to_balance, exchange_move)
- amls += rate_diff_amls
- partial_rec_ids += rate_diff_partial_rec.ids
- else:
- aml_to_balance.reconcile()
- exchange_move.post()
- exchange_move_id = exchange_move.id
+ min_credit_amount_residual_currency = debit_line.company_currency_id._convert(
+ min_amount_residual,
+ credit_line.currency_id,
+ debit_line.company_id,
+ debit_line.date,
+ )
+
+ debit_amount_residual -= min_amount_residual
+ debit_amount_residual_currency -= min_debit_amount_residual_currency
+ credit_amount_residual += min_amount_residual
+ credit_amount_residual_currency += min_credit_amount_residual_currency
+
+ partials_vals_list.append({
+ 'amount': min_amount_residual,
+ 'debit_amount_currency': min_debit_amount_residual_currency,
+ 'credit_amount_currency': min_credit_amount_residual_currency,
+ 'debit_move_id': debit_line.id,
+ 'credit_move_id': credit_line.id,
+ })
+
+ return partials_vals_list
+
+ def _create_exchange_difference_move(self):
+ ''' Create the exchange difference journal entry on the current journal items.
+ :return: An account.move record.
+ '''
+
+ def _add_lines_to_exchange_difference_vals(lines, exchange_diff_move_vals):
+ ''' Generate the exchange difference values used to create the journal items
+ in order to fix the residual amounts and add them into 'exchange_diff_move_vals'.
+
+ 1) When reconciled on the same foreign currency, the journal items are
+ fully reconciled regarding this currency but it could be not the case
+ of the balance that is expressed using the company's currency. In that
+ case, we need to create exchange difference journal items to ensure this
+ residual amount reaches zero.
+
+ 2) When reconciled on the company currency but having different foreign
+ currencies, the journal items are fully reconciled regarding the company
+ currency but it's not always the case for the foreign currencies. In that
+ case, the exchange difference journal items are created to ensure this
+ residual amount in foreign currency reaches zero.
+
+ :param lines: The account.move.lines to which fix the residual amounts.
+ :param exchange_diff_move_vals: The current vals of the exchange difference journal entry.
+ :return: A list of pair <line, sequence> to perform the reconciliation
+ at the creation of the exchange difference move where 'line'
+ is the account.move.line to which the 'sequence'-th exchange
+ difference line will be reconciled with.
+ '''
+ journal = self.env['account.journal'].browse(exchange_diff_move_vals['journal_id'])
+ to_reconcile = []
+
+ for line in lines:
+
+ exchange_diff_move_vals['date'] = max(exchange_diff_move_vals['date'], line.date)
+
+ if not line.company_currency_id.is_zero(line.amount_residual):
+ # amount_residual_currency == 0 and amount_residual has to be fixed.
+
+ if line.amount_residual > 0.0:
+ exchange_line_account = journal.default_debit_account_id
+ else:
+ exchange_line_account = journal.default_credit_account_id
+
+ elif line.currency_id and not line.currency_id.is_zero(line.amount_residual_currency):
+ # amount_residual == 0 and amount_residual_currency has to be fixed.
+
+ if line.amount_residual_currency > 0.0:
+ exchange_line_account = journal.default_debit_account_id
+ else:
+ exchange_line_account = journal.default_credit_account_id
+
else:
- missing_exchange_difference = True
- if not missing_exchange_difference:
- #mark the reference of the full reconciliation on the exchange rate entries and on the entries
- self.env['account.full.reconcile'].create({
- 'partial_reconcile_ids': [(6, 0, partial_rec_ids)],
- 'reconciled_line_ids': [(6, 0, amls.ids)],
- 'exchange_move_id': exchange_move_id,
- })
+ continue
- def _reconcile_lines(self, debit_moves, credit_moves, field):
- """ This function loops on the 2 recordsets given as parameter as long as it
- can find a debit and a credit to reconcile together. It returns the recordset of the
- account move lines that were not reconciled during the process.
- """
- (debit_moves + credit_moves).read([field])
- to_create = []
- cash_basis = debit_moves and debit_moves[0].account_id.internal_type in ('receivable', 'payable') or False
- cash_basis_percentage_before_rec = {}
- dc_vals ={}
- while (debit_moves and credit_moves):
- debit_move = debit_moves[0]
- credit_move = credit_moves[0]
- company_currency = debit_move.company_id.currency_id
- # We need those temporary value otherwise the computation might be wrong below
- temp_amount_residual = min(debit_move.amount_residual, -credit_move.amount_residual)
- temp_amount_residual_currency = min(debit_move.amount_residual_currency, -credit_move.amount_residual_currency)
- dc_vals[(debit_move.id, credit_move.id)] = (debit_move, credit_move, temp_amount_residual_currency)
- amount_reconcile = min(debit_move[field], -credit_move[field])
-
- #Remove from recordset the one(s) that will be totally reconciled
- # For optimization purpose, the creation of the partial_reconcile are done at the end,
- # therefore during the process of reconciling several move lines, there are actually no recompute performed by the orm
- # and thus the amount_residual are not recomputed, hence we have to do it manually.
- if amount_reconcile == debit_move[field]:
- debit_moves -= debit_move
+ sequence = len(exchange_diff_move_vals['line_ids'])
+ exchange_diff_move_vals['line_ids'] += [
+ (0, 0, {
+ 'name': _('Currency exchange rate difference'),
+ 'debit': -line.amount_residual if line.amount_residual < 0.0 else 0.0,
+ 'credit': line.amount_residual if line.amount_residual > 0.0 else 0.0,
+ 'amount_currency': -line.amount_residual_currency,
+ 'account_id': line.account_id.id,
+ 'currency_id': line.currency_id.id,
+ 'partner_id': line.partner_id.id,
+ 'sequence': sequence,
+ }),
+ (0, 0, {
+ 'name': _('Currency exchange rate difference'),
+ 'debit': line.amount_residual if line.amount_residual > 0.0 else 0.0,
+ 'credit': -line.amount_residual if line.amount_residual < 0.0 else 0.0,
+ 'amount_currency': line.amount_residual_currency,
+ 'account_id': exchange_line_account.id,
+ 'currency_id': line.currency_id.id,
+ 'partner_id': line.partner_id.id,
+ 'sequence': sequence + 1,
+ }),
+ ]
+
+ to_reconcile.append((line, sequence))
+
+ return to_reconcile
+
+ def _add_cash_basis_lines_to_exchange_difference_vals(lines, exchange_diff_move_vals):
+ ''' Generate the exchange difference values used to create the journal items
+ in order to fix the cash basis lines using the transfer account in a multi-currencies
+ environment when this account is not a reconcile one.
+
+ When the tax cash basis journal entries are generated and all involved
+ transfer account set on taxes are all reconcilable, the account balance
+ will be reset to zero by the exchange difference journal items generated
+ above. However, this mechanism will not work if there is any transfer
+ accounts that are not reconcile and we are generating the cash basis
+ journal items in a foreign currency. In that specific case, we need to
+ generate extra journal items at the generation of the exchange difference
+ journal entry to ensure this balance is reset to zero and then, will not
+ appear on the tax report leading to erroneous tax base amount / tax amount.
+
+ :param lines: The account.move.lines to which fix the residual amounts.
+ :param exchange_diff_move_vals: The current vals of the exchange difference journal entry.
+ '''
+ for move in lines.move_id:
+ transfer_account_vals_to_fix = {}
+
+ move_values = move._collect_tax_cash_basis_values()
+
+ # The cash basis doesn't need to be handle for this move because there is another payment term
+ # line that is not yet fully paid.
+ if not move_values or not move_values['is_fully_paid']:
+ continue
+
+ # The percentage of the tax cash basis entries are expressed using the company's currency and then,
+ # there is no exchange difference to make for such journal entry.
+ if move_values['currency'] == move.company_id.currency_id:
+ continue
+
+ # ==========================================================================
+ # Add the balance of all tax lines of the current move in order in order
+ # to compute the residual amount for each of them.
+ # ==========================================================================
+
+ is_exchange_diff_needed = False
+ for line in move_values['to_process_lines']:
+
+ if not line.tax_repartition_line_id:
+ continue
+
+ if not line.account_id.reconcile:
+ is_exchange_diff_needed = True
+ grouping_key = self.env['account.partial.reconcile']._get_cash_basis_tax_line_grouping_key_from_record(line)
+ transfer_account_vals_to_fix[grouping_key] = {
+ 'account_id': line.account_id.id,
+ 'currency_id': line.currency_id.id,
+ 'partner_id': line.partner_id.id,
+ 'tax_base_amount': line.tax_base_amount,
+ 'tax_repartition_line_id': line.tax_repartition_line_id.id,
+ 'tax_ids': [(6, 0, line.tax_ids.ids)],
+ 'tax_tag_ids': [(6, 0, line.tax_tag_ids.ids)],
+ 'debit': line.debit,
+ 'credit': line.credit,
+ }
+
+ # No tax line on the current move.
+ if not is_exchange_diff_needed:
+ continue
+
+ # ==========================================================================
+ # Subtract the balance of all previously generated cash basis journal entries
+ # in order to retrieve the residual balance of each involved transfer account.
+ # ==========================================================================
+
+ cash_basis_moves = self.env['account.move'].search([('tax_cash_basis_move_id', '=', move.id)])
+ for line in cash_basis_moves.line_ids:
+ if not line.tax_repartition_line_id:
+ continue
+
+ grouping_key = self.env['account.partial.reconcile']._get_cash_basis_tax_line_grouping_key_from_record(
+ line,
+ account=line.tax_line_id.cash_basis_transition_account_id,
+ )
+
+ if grouping_key not in transfer_account_vals_to_fix:
+ continue
+
+ transfer_account_vals_to_fix[grouping_key]['debit'] -= line.debit
+ transfer_account_vals_to_fix[grouping_key]['credit'] -= line.credit
+
+ # ==========================================================================
+ # Generate the exchange difference journal items to reset the balance of
+ # all transfer account to zero.
+ # ==========================================================================
+
+ for values in transfer_account_vals_to_fix.values():
+ balance = values['debit'] - values['credit']
+ values.update({
+ 'debit': balance if balance > 0.0 else 0.0,
+ 'credit': -balance if balance < 0.0 else 0.0,
+ })
+
+ account = self.env['account.account'].browse(values['account_id'])
+ if account.company_id.currency_id.is_zero(balance):
+ continue
+
+ journal = account.company_id.currency_exchange_journal_id
+
+ if balance > 0.0:
+ exchange_line_account = journal.default_debit_account_id
+ else:
+ exchange_line_account = journal.default_credit_account_id
+
+ sequence = len(exchange_diff_move_vals['line_ids'])
+ exchange_diff_move_vals['line_ids'] += [
+ (0, 0, {
+ **values,
+ 'name': _('Currency exchange rate difference (cash basis)'),
+ 'debit': values['credit'],
+ 'credit': values['debit'],
+ 'amount_currency': 0.0,
+ 'sequence': sequence,
+ }),
+ (0, 0, {
+ **values,
+ 'name': _('Currency exchange rate difference (cash basis)'),
+ 'amount_currency': 0.0,
+ 'account_id': exchange_line_account.id,
+ 'sequence': sequence + 1,
+ }),
+ ]
+
+ if not self:
+ return self.env['account.move']
+
+ company = self[0].company_id
+ journal = company.currency_exchange_journal_id
+
+ # Check the configuration of the exchange difference journal.
+ if not journal:
+ 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 journal.default_debit_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."))
+ if not journal.default_credit_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."))
+
+ exchange_diff_move_vals = {
+ 'move_type': 'entry',
+ 'date': date.min,
+ 'journal_id': journal.id,
+ 'line_ids': [],
+ }
+
+ # Fix residual amounts.
+ to_reconcile = _add_lines_to_exchange_difference_vals(self, exchange_diff_move_vals)
+
+ # Fix cash basis entries.
+ is_cash_basis_needed = self[0].account_internal_type in ('receivable', 'payable')
+ if is_cash_basis_needed:
+ _add_cash_basis_lines_to_exchange_difference_vals(self, exchange_diff_move_vals)
+
+ # ==========================================================================
+ # Create move and reconcile.
+ # ==========================================================================
+
+ if exchange_diff_move_vals['line_ids']:
+ exchange_diff_move_vals['date'] = max(exchange_diff_move_vals['date'], company._get_user_fiscal_lock_date())
+
+ exchange_move = self.env['account.move'].create(exchange_diff_move_vals)
+ else:
+ return None
+
+ # Reconcile lines to the newly created exchange difference journal entry by creating more partials.
+ partials_vals_list = []
+ for source_line, sequence in to_reconcile:
+ exchange_diff_line = exchange_move.line_ids[sequence]
+
+ if source_line.company_currency_id.is_zero(source_line.amount_residual):
+ exchange_field = 'amount_residual_currency'
else:
- debit_moves[0].amount_residual -= temp_amount_residual
- debit_moves[0].amount_residual_currency -= temp_amount_residual_currency
+ exchange_field = 'amount_residual'
- if amount_reconcile == -credit_move[field]:
- credit_moves -= credit_move
+ if exchange_diff_line[exchange_field] > 0.0:
+ debit_line = exchange_diff_line
+ credit_line = source_line
else:
- credit_moves[0].amount_residual += temp_amount_residual
- credit_moves[0].amount_residual_currency += temp_amount_residual_currency
- #Check for the currency and amount_currency we can set
- currency = False
- amount_reconcile_currency = 0
- if field == 'amount_residual_currency':
- currency = credit_move.currency_id.id
- amount_reconcile_currency = temp_amount_residual_currency
- amount_reconcile = temp_amount_residual
- elif bool(debit_move.currency_id) != bool(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. It also allows to compute the amount residual in foreign currency.
- currency = debit_move.currency_id or credit_move.currency_id
- currency_date = debit_move.currency_id and credit_move.date or debit_move.date
- amount_reconcile_currency = company_currency._convert(amount_reconcile, currency, debit_move.company_id, currency_date)
- currency = currency.id
-
- if cash_basis:
- tmp_set = debit_move | credit_move
- cash_basis_percentage_before_rec.update(tmp_set._get_matched_percentage())
-
- to_create.append({
- 'debit_move_id': debit_move.id,
- 'credit_move_id': credit_move.id,
- 'amount': amount_reconcile,
- 'amount_currency': amount_reconcile_currency,
- 'currency_id': currency,
+ debit_line = source_line
+ credit_line = exchange_diff_line
+
+ partials_vals_list.append({
+ 'amount': abs(source_line.amount_residual),
+ 'debit_amount_currency': abs(debit_line.amount_residual_currency),
+ 'credit_amount_currency': abs(credit_line.amount_residual_currency),
+ 'debit_move_id': debit_line.id,
+ 'credit_move_id': credit_line.id,
})
- cash_basis_subjected = []
- part_rec = self.env['account.partial.reconcile']
- for partial_rec_dict in to_create:
- debit_move, credit_move, amount_residual_currency = dc_vals[partial_rec_dict['debit_move_id'], partial_rec_dict['credit_move_id']]
- # /!\ NOTE: Exchange rate differences shouldn't create cash basis entries
- # i. e: we don't really receive/give money in a customer/provider fashion
- # Since those are not subjected to cash basis computation we process them first
- if not amount_residual_currency and debit_move.currency_id and credit_move.currency_id:
- part_rec.create(partial_rec_dict)
- else:
- cash_basis_subjected.append(partial_rec_dict)
-
- for after_rec_dict in cash_basis_subjected:
- new_rec = part_rec.create(after_rec_dict)
- # if the pair belongs to move being reverted, do not create CABA entry
- if cash_basis and not (
- new_rec.debit_move_id.move_id == new_rec.credit_move_id.move_id.reversed_entry_id
- or
- new_rec.credit_move_id.move_id == new_rec.debit_move_id.move_id.reversed_entry_id
- ):
- new_rec.create_tax_cash_basis_entry(cash_basis_percentage_before_rec)
- return debit_moves+credit_moves
-
- def auto_reconcile_lines(self):
- # Create list of debit and list of credit move ordered by date-currency
- debit_moves = self.filtered(lambda r: r.debit != 0 or r.amount_currency > 0)
- credit_moves = self.filtered(lambda r: r.credit != 0 or r.amount_currency < 0)
- debit_moves = debit_moves.sorted(key=lambda a: (a.date_maturity or a.date, a.currency_id))
- credit_moves = credit_moves.sorted(key=lambda a: (a.date_maturity or a.date, a.currency_id))
- # Compute on which field reconciliation should be based upon:
- if self[0].account_id.currency_id and self[0].account_id.currency_id != self[0].account_id.company_id.currency_id:
- field = 'amount_residual_currency'
- else:
- field = 'amount_residual'
- #if all lines share the same currency, use amount_residual_currency to avoid currency rounding error
- if self[0].currency_id and all([x.amount_currency and x.currency_id == self[0].currency_id for x in self]):
- field = 'amount_residual_currency'
- # Reconcile lines
- ret = self._reconcile_lines(debit_moves, credit_moves, field)
- return ret
-
- def _check_reconcile_validity(self):
- #Perform all checks on lines
- company_ids = set()
- all_accounts = []
+ self.env['account.partial.reconcile'].create(partials_vals_list)
+
+ return exchange_move
+
+ def reconcile(self):
+ ''' Reconcile the current move lines all together.
+ :return: A dictionary representing a summary of what has been done during the reconciliation:
+ * partials: A recorset of all account.partial.reconcile created during the reconciliation.
+ * full_reconcile: An account.full.reconcile record created when there is nothing left to reconcile
+ in the involved lines.
+ * tax_cash_basis_moves: An account.move recordset representing the tax cash basis journal entries.
+ '''
+ results = {}
+
+ if not self:
+ return results
+
+ # List unpaid invoices
+ not_paid_invoices = self.move_id.filtered(
+ lambda move: move.is_invoice(include_receipts=True) and move.payment_state not in ('paid', 'in_payment')
+ )
+
+ # ==== Check the lines can be reconciled together ====
+ company = None
+ account = None
for line in self:
- company_ids.add(line.company_id.id)
- all_accounts.append(line.account_id)
if line.reconciled:
- raise UserError(_('You are trying to reconcile some entries that are already reconciled.'))
+ raise UserError(_("You are trying to reconcile some entries that are already reconciled."))
+ if not line.account_id.reconcile and line.account_id.internal_type != 'liquidity':
+ raise UserError(_("Account %s does not allow reconciliation. First change the configuration of this account to allow it.")
+ % line.account_id.display_name)
if line.move_id.state != 'posted':
raise UserError(_('You can only reconcile posted entries.'))
- if len(company_ids) > 1:
- raise UserError(_('To reconcile the entries company should be the same for all entries.'))
- if len(set(all_accounts)) > 1:
- raise UserError(_('Entries are not from the same account.'))
- if not (all_accounts[0].reconcile or all_accounts[0].internal_type == 'liquidity'):
- raise UserError(_('Account %s (%s) does not allow reconciliation. First change the configuration of this account to allow it.') % (all_accounts[0].name, all_accounts[0].code))
-
- def reconcile(self, writeoff_acc_id=False, writeoff_journal_id=False):
- # Empty self can happen if the user tries to reconcile entries which are already reconciled.
- # The calling method might have filtered out reconciled lines.
- if not self:
- return
+ if company is None:
+ company = line.company_id
+ elif line.company_id != company:
+ raise UserError(_("Entries doesn't belong to the same company: %s != %s")
+ % (company.display_name, line.company_id.display_name))
+ if account is None:
+ account = line.account_id
+ elif line.account_id != account:
+ raise UserError(_("Entries are not from the same account: %s != %s")
+ % (account.display_name, line.account_id.display_name))
+
+ sorted_lines = self.sorted(key=lambda line: (line.date_maturity or line.date, line.currency_id))
+
+ # ==== Collect all involved lines through the existing reconciliation ====
+
+ involved_lines = sorted_lines
+ involved_partials = self.env['account.partial.reconcile']
+ current_lines = involved_lines
+ current_partials = involved_partials
+ while current_lines:
+ current_partials = (current_lines.matched_debit_ids + current_lines.matched_credit_ids) - current_partials
+ involved_partials += current_partials
+ current_lines = (current_partials.debit_move_id + current_partials.credit_move_id) - current_lines
+ involved_lines += current_lines
+
+ # ==== Create partials ====
+
+ partials = self.env['account.partial.reconcile'].create(sorted_lines._prepare_reconciliation_partials())
+
+ # Track newly created partials.
+ results['partials'] = partials
+ involved_partials += partials
+
+ # ==== Create entries for cash basis taxes ====
+
+ is_cash_basis_needed = account.user_type_id.type in ('receivable', 'payable')
+ if is_cash_basis_needed:
+ tax_cash_basis_moves = partials._create_tax_cash_basis_moves()
+ results['tax_cash_basis_moves'] = tax_cash_basis_moves
+
+ # ==== Check if a full reconcile is needed ====
+
+ if involved_lines[0].currency_id and all(line.currency_id == involved_lines[0].currency_id for line in involved_lines):
+ is_full_needed = all(line.currency_id.is_zero(line.amount_residual_currency) for line in involved_lines)
+ else:
+ is_full_needed = all(line.company_currency_id.is_zero(line.amount_residual) for line in involved_lines)
- # List unpaid invoices
- not_paid_invoices = self.mapped('move_id').filtered(
- lambda m: m.is_invoice(include_receipts=True) and m.payment_state not in ('paid', 'in_payment')
- )
+ if is_full_needed:
- self._check_reconcile_validity()
- #reconcile everything that can be
- remaining_moves = self.auto_reconcile_lines()
-
- writeoff_to_reconcile = self.env['account.move.line']
- #if writeoff_acc_id specified, then create write-off move with value the remaining amount from move in self
- if writeoff_acc_id and writeoff_journal_id and remaining_moves:
- all_aml_share_same_currency = all([x.currency_id == self[0].currency_id for x in self])
- writeoff_vals = {
- 'account_id': writeoff_acc_id.id,
- 'journal_id': writeoff_journal_id.id
- }
- if not all_aml_share_same_currency:
- writeoff_vals['amount_currency'] = False
- writeoff_to_reconcile = remaining_moves._create_writeoff([writeoff_vals])
- #add writeoff line to reconcile algorithm and finish the reconciliation
- remaining_moves = (remaining_moves + writeoff_to_reconcile).auto_reconcile_lines()
- # Check if reconciliation is total or needs an exchange rate entry to be created
- (self + writeoff_to_reconcile).check_full_reconcile()
+ # ==== Create the exchange difference move ====
+
+ if self._context.get('no_exchange_difference'):
+ exchange_move = None
+ else:
+ exchange_move = involved_lines._create_exchange_difference_move()
+ if exchange_move:
+ exchange_move_lines = exchange_move.line_ids.filtered(lambda line: line.account_id == account)
+
+ # Track newly created lines.
+ involved_lines += exchange_move_lines
+
+ # Track newly created partials.
+ exchange_diff_partials = exchange_move_lines.matched_debit_ids \
+ + exchange_move_lines.matched_credit_ids
+ involved_partials += exchange_diff_partials
+ results['partials'] += exchange_diff_partials
+
+ exchange_move.post()
+
+ # ==== Create the full reconcile ====
+
+ results['full_reconcile'] = self.env['account.full.reconcile'].create({
+ 'exchange_move_id': exchange_move and exchange_move.id,
+ 'partial_reconcile_ids': [(6, 0, involved_partials.ids)],
+ 'reconciled_line_ids': [(6, 0, involved_lines.ids)],
+ })
# Trigger action for paid invoices
- not_paid_invoices.filtered(
- lambda m: m.payment_state in ('paid', 'in_payment')
- ).action_invoice_paid()
+ not_paid_invoices\
+ .filtered(lambda move: move.payment_state in ('paid', 'in_payment'))\
+ .action_invoice_paid()
- return True
+ return results
def _create_writeoff(self, writeoff_vals):
""" Create a writeoff move per journal for the account.move.lines in self. If debit/credit is not specified in vals,
@@ -4227,47 +4484,6 @@ class AccountMoveLine(models.Model):
# MISC
# -------------------------------------------------------------------------
- def _get_matched_percentage(self):
- """ This function returns a dictionary giving for each move_id of self, the percentage to consider as cash basis factor.
- This is actually computing the same as the matched_percentage field of account.move, except in case of multi-currencies
- where we recompute the matched percentage based on the amount_currency fields.
- Note that this function is used only by the tax cash basis module since we want to consider the matched_percentage only
- based on the company currency amounts in reports.
- """
- matched_percentage_per_move = {}
- for line in self:
- if not matched_percentage_per_move.get(line.move_id.id, False):
- lines_to_consider = line.move_id.line_ids.filtered(lambda x: x.account_id.internal_type in ('receivable', 'payable'))
- total_amount_currency = 0.0
- total_reconciled_currency = 0.0
- all_same_currency = False
- #if all receivable/payable aml and their payments have the same currency, we can safely consider
- #the amount_currency fields to avoid including the exchange rate difference in the matched_percentage
- if lines_to_consider and all([x.currency_id.id == lines_to_consider[0].currency_id.id for x in lines_to_consider]):
- all_same_currency = lines_to_consider[0].currency_id.id
- for line in lines_to_consider:
- if all_same_currency:
- total_amount_currency += abs(line.amount_currency)
- for partial_line in (line.matched_debit_ids + line.matched_credit_ids):
- if partial_line.currency_id and partial_line.currency_id.id == all_same_currency:
- total_reconciled_currency += partial_line.amount_currency
- else:
- all_same_currency = False
- break
- if not all_same_currency:
- #we cannot rely on amount_currency fields as it is not present on all partial reconciliation
- matched_percentage_per_move[line.move_id.id] = line.move_id._get_cash_basis_matched_percentage()
- else:
- #we can rely on amount_currency fields, which allow us to post a tax cash basis move at the initial rate
- #to avoid currency rate difference issues.
- if total_amount_currency == 0.0:
- matched_percentage_per_move[line.move_id.id] = 1.0
- else:
- # lines_to_consider is always non-empty when total_amount_currency is 0
- currency = lines_to_consider[0].currency_id or lines_to_consider[0].company_id.currency_id
- matched_percentage_per_move[line.move_id.id] = currency.round(total_reconciled_currency) / currency.round(total_amount_currency)
- return matched_percentage_per_move
-
def _get_analytic_tag_ids(self):
self.ensure_one()
return self.analytic_tag_ids.filtered(lambda r: not r.active_analytic_distribution).ids
@@ -4463,331 +4679,3 @@ class AccountMoveLine(models.Model):
if self.payment_id:
domains.append([('res_model', '=', 'account.payment'), ('res_id', '=', self.payment_id.id)])
return domains
-
-class AccountPartialReconcile(models.Model):
- _name = "account.partial.reconcile"
- _description = "Partial Reconcile"
-
- debit_move_id = fields.Many2one('account.move.line', index=True, required=True)
- credit_move_id = fields.Many2one('account.move.line', index=True, required=True)
- amount = fields.Monetary(currency_field='company_currency_id', help="Amount concerned by this matching. Assumed to be always positive")
- amount_currency = fields.Monetary(string="Amount in Currency")
- currency_id = fields.Many2one('res.currency', string='Currency')
- company_currency_id = fields.Many2one('res.currency', string="Company Currency", related='company_id.currency_id', readonly=True,
- help='Utility field to express amount currency')
- company_id = fields.Many2one('res.company', related='debit_move_id.company_id', store=True, string='Company', readonly=False)
- full_reconcile_id = fields.Many2one('account.full.reconcile', string="Full Reconcile", copy=False)
- max_date = fields.Date(string='Max Date of Matched Lines', compute='_compute_max_date',
- readonly=True, copy=False, store=True,
- help='Technical field used to determine at which date this reconciliation needs to be shown on the aged receivable/payable reports.')
-
- @api.depends('debit_move_id.date', 'credit_move_id.date')
- def _compute_max_date(self):
- for rec in self:
- rec.max_date = max(
- rec.debit_move_id.date,
- rec.credit_move_id.date
- )
-
- @api.model
- def _prepare_exchange_diff_partial_reconcile(self, aml, line_to_reconcile, currency):
- """
- Prepares the values for the partial reconciliation between an account.move.line
- that needs to be fixed by an exchange rate entry and the account.move.line that fixes it
-
- @param {account.move.line} aml:
- The line that needs fixing with exchange difference entry
- (e.g. a receivable/payable from an invoice)
- @param {account.move.line} line_to_reconcile:
- The line that fixes the aml. it is the receivable/payable line
- of the exchange difference entry move
- @param {res.currency} currency
-
- @return {dict} values of account.partial.reconcile; ready for create()
- """
-
- # the exhange rate difference that will be fixed may be of opposite direction
- # than the original move line (i.e. the exchange difference may be negative whereas
- # the move line on which it applies may be a debit -- positive)
- # So we need to register both the move line and the exchange line
- # to either debit_move or credit_move as a function of whether the direction (debit VS credit)
- # of the exchange loss/gain is the same (or not) as the direction of the line that is fixed here
- if aml.currency_id:
- residual_same_sign = aml.amount_currency * aml.amount_residual_currency >= 0
- else:
- residual_same_sign = aml.balance * aml.amount_residual >= 0
-
- if residual_same_sign:
- debit_move_id = line_to_reconcile.id if aml.credit else aml.id
- credit_move_id = line_to_reconcile.id if aml.debit else aml.id
- else:
- debit_move_id = aml.id if aml.credit else line_to_reconcile.id
- credit_move_id = aml.id if aml.debit else line_to_reconcile.id
-
- return {
- 'debit_move_id': debit_move_id,
- 'credit_move_id': credit_move_id,
- 'amount': abs(aml.amount_residual),
- 'amount_currency': abs(aml.amount_residual_currency),
- 'currency_id': currency and currency.id or False,
- }
-
- @api.model
- def create_exchange_rate_entry(self, aml_to_fix, move):
- """
- Automatically create a journal items to book the exchange rate
- differences that can occur in multi-currencies environment. That
- new journal item will be made into the given `move` in the company
- `currency_exchange_journal_id`, and one of its journal items is
- matched with the other lines to balance the full reconciliation.
- :param aml_to_fix: recordset of account.move.line (possible several
- but sharing the same currency)
- :param move: account.move
- :return: tuple.
- [0]: account.move.line created to balance the `aml_to_fix`
- [1]: recordset of account.partial.reconcile created between the
- tuple first element and the `aml_to_fix`
- """
- partial_rec = self.env['account.partial.reconcile']
- aml_model = self.env['account.move.line']
-
- created_lines = self.env['account.move.line']
- for aml in aml_to_fix:
- #create the line that will compensate all the aml_to_fix
- line_to_rec = aml_model.with_context(check_move_validity=False).create({
- 'name': _('Currency exchange rate difference'),
- 'debit': aml.amount_residual < 0 and -aml.amount_residual or 0.0,
- 'credit': aml.amount_residual > 0 and aml.amount_residual or 0.0,
- 'account_id': aml.account_id.id,
- 'move_id': move.id,
- 'currency_id': aml.currency_id.id,
- 'amount_currency': aml.amount_residual_currency and -aml.amount_residual_currency or 0.0,
- 'partner_id': aml.partner_id.id,
- })
- #create the counterpart on exchange gain/loss account
- exchange_journal = move.company_id.currency_exchange_journal_id
- aml_model.with_context(check_move_validity=False).create({
- 'name': _('Currency exchange rate difference'),
- 'debit': aml.amount_residual > 0 and aml.amount_residual or 0.0,
- 'credit': aml.amount_residual < 0 and -aml.amount_residual or 0.0,
- 'account_id': aml.amount_residual > 0 and exchange_journal.default_debit_account_id.id or exchange_journal.default_credit_account_id.id,
- 'move_id': move.id,
- 'currency_id': aml.currency_id.id,
- 'amount_currency': aml.amount_residual_currency and aml.amount_residual_currency or 0.0,
- 'partner_id': aml.partner_id.id,
- })
-
- #reconcile all aml_to_fix
- partial_rec |= self.create(
- self._prepare_exchange_diff_partial_reconcile(
- aml=aml,
- line_to_reconcile=line_to_rec,
- currency=aml.currency_id or False)
- )
- created_lines |= line_to_rec
- return created_lines, partial_rec
-
- def _get_tax_cash_basis_base_account(self, line, tax):
- ''' Get the account of lines that will contain the base amount of taxes.
- :param line: An account.move.line record
- :param tax: An account.tax record
- :return: An account record
- '''
- return tax.cash_basis_base_account_id or line.account_id
-
- def _get_amount_tax_cash_basis(self, amount, line):
- return line.company_id.currency_id.round(amount)
-
- def _set_tax_cash_basis_entry_date(self, move_date, newly_created_move):
- if move_date > (self.company_id.period_lock_date or date.min) and newly_created_move.date != move_date:
- # 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 before the period lock date as the tax statement for this period is
- # probably already sent to the estate.
- newly_created_move.write({'date': move_date})
- newly_created_move.recompute(['name'])
-
- def create_tax_cash_basis_entry(self, percentage_before_rec):
- self.ensure_one()
- move_date = self.debit_move_id.date
- newly_created_move = self.env['account.move']
- to_reconcile = []
- # We use a set here in case the reconciled lines belong to the same move (it happens with POS)
- for move in {self.debit_move_id.move_id, self.credit_move_id.move_id}:
- #move_date is the max of the 2 reconciled items
- if move_date < move.date:
- move_date = move.date
- percentage_before = percentage_before_rec[move.id]
- percentage_after = move.line_ids[0]._get_matched_percentage()[move.id]
- # update the percentage before as the move can be part of
- # multiple partial reconciliations
- percentage_before_rec[move.id] = percentage_after
-
- for line in move.line_ids:
- if not line.tax_exigible:
- #amount is the current cash_basis amount minus the one before the reconciliation
- amount = line.balance * percentage_after - line.balance * percentage_before
- rounded_amt = self._get_amount_tax_cash_basis(amount, line)
- if float_is_zero(rounded_amt, precision_rounding=line.company_id.currency_id.rounding):
- continue
- if line.tax_line_id and line.tax_line_id.tax_exigibility == 'on_payment':
- if not newly_created_move:
- newly_created_move = self._create_tax_basis_move()
- #create cash basis entry for the tax line
- to_clear_aml = self.env['account.move.line'].with_context(check_move_validity=False).create({
- 'name': line.move_id.name,
- 'debit': abs(rounded_amt) if rounded_amt < 0 else 0.0,
- 'credit': rounded_amt if rounded_amt > 0 else 0.0,
- 'account_id': line.account_id.id,
- 'analytic_account_id': line.analytic_account_id.id,
- 'analytic_tag_ids': line.analytic_tag_ids.ids,
- 'tax_exigible': True,
- 'amount_currency': line.amount_currency and line.currency_id.round(-line.amount_currency * amount / line.balance) or 0.0,
- 'currency_id': line.currency_id.id,
- 'move_id': newly_created_move.id,
- 'partner_id': line.partner_id.id,
- 'journal_id': newly_created_move.journal_id.id,
- })
- # Group by cash basis account and tax
- self.env['account.move.line'].with_context(check_move_validity=False).create({
- 'name': line.name,
- 'debit': rounded_amt if rounded_amt > 0 else 0.0,
- 'credit': abs(rounded_amt) if rounded_amt < 0 else 0.0,
- 'account_id': line.tax_repartition_line_id.account_id.id or line.account_id.id,
- 'analytic_account_id': line.analytic_account_id.id,
- 'analytic_tag_ids': line.analytic_tag_ids.ids,
- 'tax_exigible': True,
- 'amount_currency': line.amount_currency and line.currency_id.round(line.amount_currency * amount / line.balance) or 0.0,
- 'currency_id': line.currency_id.id,
- 'move_id': newly_created_move.id,
- 'partner_id': line.partner_id.id,
- 'journal_id': newly_created_move.journal_id.id,
- 'tax_repartition_line_id': line.tax_repartition_line_id.id,
- 'tax_base_amount': line.tax_base_amount,
- 'tax_tag_ids': [(6, 0, line.tax_tag_ids.ids)],
- })
- if line.account_id.reconcile and not line.reconciled:
- to_reconcile.append(to_clear_aml + line)
-
- taxes_payment_exigible = line.tax_ids.flatten_taxes_hierarchy().filtered(lambda tax: tax.tax_exigibility == 'on_payment')
- if taxes_payment_exigible:
- if not newly_created_move:
- newly_created_move = self._create_tax_basis_move()
- #create cash basis entry for the base
- for tax in taxes_payment_exigible:
- account_id = self._get_tax_cash_basis_base_account(line, tax)
- self.env['account.move.line'].with_context(check_move_validity=False).create({
- 'name': line.name,
- 'debit': rounded_amt > 0 and rounded_amt or 0.0,
- 'credit': rounded_amt < 0 and abs(rounded_amt) or 0.0,
- 'account_id': account_id.id,
- 'tax_exigible': True,
- 'tax_ids': [(6, 0, [tax.id])],
- 'move_id': newly_created_move.id,
- 'currency_id': line.currency_id.id,
- 'amount_currency': line.currency_id.round(line.amount_currency * amount / line.balance) if line.currency_id and line.balance else 0.0,
- 'partner_id': line.partner_id.id,
- 'journal_id': newly_created_move.journal_id.id,
- 'tax_repartition_line_id': line.tax_repartition_line_id.id,
- 'tax_base_amount': line.tax_base_amount,
- 'tax_tag_ids': [(6, 0, line.tax_tag_ids.ids)],
- })
- self.env['account.move.line'].with_context(check_move_validity=False).create({
- 'name': line.name,
- 'credit': rounded_amt > 0 and rounded_amt or 0.0,
- 'debit': rounded_amt < 0 and abs(rounded_amt) or 0.0,
- 'account_id': account_id.id,
- 'tax_exigible': True,
- 'move_id': newly_created_move.id,
- 'currency_id': line.currency_id.id,
- 'amount_currency': line.currency_id.round(-line.amount_currency * amount / line.balance) if line.currency_id and line.balance else 0.0,
- 'partner_id': line.partner_id.id,
- 'journal_id': newly_created_move.journal_id.id,
- })
- if newly_created_move:
- self._set_tax_cash_basis_entry_date(move_date, newly_created_move)
- # post move
- newly_created_move.post()
- for lines in to_reconcile:
- lines.reconcile()
-
- def _create_tax_basis_move(self):
- # Check if company_journal for cash basis is set if not, raise exception
- if not self.company_id.tax_cash_basis_journal_id:
- raise UserError(_('There is no tax cash basis journal defined '
- 'for this company: "%s" \nConfigure it in Accounting/Configuration/Settings') %
- (self.company_id.name))
- move_vals = {
- 'journal_id': self.company_id.tax_cash_basis_journal_id.id,
- 'tax_cash_basis_rec_id': self.id,
- 'ref': self.credit_move_id.move_id.name if self.credit_move_id.payment_id else self.debit_move_id.move_id.name,
- }
- return self.env['account.move'].create(move_vals)
-
- def unlink(self):
- """ When removing a partial reconciliation, also unlink its full reconciliation if it exists """
- full_to_unlink = self.env['account.full.reconcile']
- for rec in self:
- if rec.full_reconcile_id:
- full_to_unlink |= rec.full_reconcile_id
- #reverse the tax basis move created at the reconciliation time
- for move in self.env['account.move'].search([('tax_cash_basis_rec_id', 'in', self._ids)]):
- if move.date > (move.company_id.period_lock_date or date.min):
- move._reverse_moves([{'ref': _('Reversal of %s') % move.name}], cancel=True)
- else:
- move._reverse_moves([{'date': fields.Date.today(), 'ref': _('Reversal of %s') % move.name}], cancel=True)
-
- # Unlink partials then the full in this order to avoid a recursive call to the same partials.
- res = super(AccountPartialReconcile, self).unlink()
-
- if full_to_unlink:
- full_to_unlink.unlink()
-
- return res
-
-
-class AccountFullReconcile(models.Model):
- _name = "account.full.reconcile"
- _description = "Full Reconcile"
-
- 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')
-
- 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 completely
- cancel the currency difference entry on the partner account (otherwise it will still appear on the aged balance
- for example).
- """
- moves_to_reverse = self.exchange_move_id
-
- res = super(AccountFullReconcile, self).unlink()
-
- # Reverse all exchange moves at once.
- today = fields.Date.today()
- default_values_list = [{
- 'date': today,
- 'ref': _('Reversal of: %s') % move.name,
- } for move in moves_to_reverse]
- moves_to_reverse._reverse_moves(default_values_list, cancel=True)
-
- return res
-
- @api.model
- def _prepare_exchange_diff_move(self, move_date, company):
- if not company.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 company.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 company.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."))
- res = {'journal_id': company.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.
- if move_date > company._get_user_fiscal_lock_date():
- res['date'] = move_date
- return res
diff --git a/addons/account/models/account_partial_reconcile.py b/addons/account/models/account_partial_reconcile.py
new file mode 100644
index 00000000000..6e445c20c85
--- /dev/null
+++ b/addons/account/models/account_partial_reconcile.py
@@ -0,0 +1,612 @@
+# -*- coding: utf-8 -*-
+from odoo import api, fields, models, _
+from odoo.exceptions import UserError, ValidationError
+
+from datetime import date
+
+
+class AccountPartialReconcile(models.Model):
+ _name = "account.partial.reconcile"
+ _description = "Partial Reconcile"
+
+ # ==== Reconciliation fields ====
+ debit_move_id = fields.Many2one(
+ comodel_name='account.move.line',
+ index=True, required=True)
+ credit_move_id = fields.Many2one(
+ comodel_name='account.move.line',
+ index=True, required=True)
+ full_reconcile_id = fields.Many2one(
+ comodel_name='account.full.reconcile',
+ string="Full Reconcile", copy=False)
+
+ # ==== Currency fields ====
+ company_currency_id = fields.Many2one(
+ comodel_name='res.currency',
+ string="Company Currency",
+ related='company_id.currency_id',
+ help="Utility field to express amount currency")
+ debit_currency_id = fields.Many2one(
+ comodel_name='res.currency',
+ store=True,
+ compute='_compute_debit_currency_id',
+ string="Currency of the debit journal item.")
+ credit_currency_id = fields.Many2one(
+ comodel_name='res.currency',
+ store=True,
+ compute='_compute_credit_currency_id',
+ string="Currency of the credit journal item.")
+
+ # ==== Amount fields ====
+ amount = fields.Monetary(
+ currency_field='company_currency_id',
+ help="Always positive amount concerned by this matching expressed in the company currency.")
+ debit_amount_currency = fields.Monetary(
+ currency_field='debit_currency_id',
+ help="Always positive amount concerned by this matching expressed in the debit line foreign currency.")
+ credit_amount_currency = fields.Monetary(
+ currency_field='credit_currency_id',
+ help="Always positive amount concerned by this matching expressed in the credit line foreign currency.")
+
+ # ==== Other fields ====
+ company_id = fields.Many2one(
+ comodel_name='res.company',
+ string="Company", store=True, readonly=False,
+ related='debit_move_id.company_id')
+ max_date = fields.Date(
+ string="Max Date of Matched Lines", store=True,
+ compute='_compute_max_date',
+ help="Technical field used to determine at which date this reconciliation needs to be shown on the "
+ "aged receivable/payable reports.")
+
+ # -------------------------------------------------------------------------
+ # CONSTRAINT METHODS
+ # -------------------------------------------------------------------------
+
+ @api.constrains('debit_currency_id', 'credit_currency_id')
+ def _check_required_computed_currencies(self):
+ bad_partials = self.filtered(lambda partial: not partial.debit_currency_id or not partial.credit_currency_id)
+ if bad_partials:
+ raise ValidationError(_("Missing foreign currencies on partials having ids: %s") % bad_partials.ids)
+
+ # -------------------------------------------------------------------------
+ # COMPUTE METHODS
+ # -------------------------------------------------------------------------
+
+ @api.depends('debit_move_id.date', 'credit_move_id.date')
+ def _compute_max_date(self):
+ for partial in self:
+ partial.max_date = max(
+ partial.debit_move_id.date,
+ partial.credit_move_id.date
+ )
+
+ @api.depends('debit_move_id')
+ def _compute_debit_currency_id(self):
+ for partial in self:
+ partial.debit_currency_id = partial.debit_move_id.currency_id \
+ or partial.debit_move_id.company_currency_id
+
+ @api.depends('credit_move_id')
+ def _compute_credit_currency_id(self):
+ for partial in self:
+ partial.credit_currency_id = partial.credit_move_id.currency_id \
+ or partial.credit_move_id.company_currency_id
+
+ # -------------------------------------------------------------------------
+ # LOW-LEVEL METHODS
+ # -------------------------------------------------------------------------
+
+ def unlink(self):
+ # OVERRIDE to unlink full reconcile linked to the current partials
+ # and reverse the tax cash basis journal entries.
+
+ # Avoid cyclic unlink calls when removing the partials that could remove some full reconcile
+ # and then, loop again and again.
+ if not self:
+ return True
+
+ # Reverse all exchange moves at once.
+ moves_to_reverse = self.env['account.move'].search([('tax_cash_basis_rec_id', 'in', self.ids)])
+ today = fields.Date.today()
+ default_values_list = [{
+ 'date': move.date if move.date > (move.company_id.period_lock_date or date.min) else today,
+ 'ref': _('Reversal of: %s') % move.name,
+ } for move in moves_to_reverse]
+ moves_to_reverse._reverse_moves(default_values_list, cancel=True)
+
+ # Unlink partials then the full in this order to avoid a recursive call to the same partials.
+ full_to_unlink = self.full_reconcile_id
+ res = super().unlink()
+ full_to_unlink.unlink()
+ return res
+
+ # -------------------------------------------------------------------------
+ # RECONCILIATION METHODS
+ # -------------------------------------------------------------------------
+
+ def _collect_tax_cash_basis_values(self):
+ ''' Collect all information needed to create the tax cash basis journal entries on the current partials.
+ :return: A dictionary mapping each move_id to the result of 'account_move._collect_tax_cash_basis_values'.
+ Also, add the 'partials' keys being a list of dictionary, one for each partial to process:
+ * partial: The account.partial.reconcile record.
+ * percentage: The reconciled percentage represented by the partial.
+ * payment_rate: The applied rate of this partial.
+ '''
+ tax_cash_basis_values_per_move = {}
+
+ if not self:
+ return {}
+
+ for partial in self:
+ for move in {partial.debit_move_id.move_id, partial.credit_move_id.move_id}:
+
+ # Collect data about cash basis.
+ if move.id not in tax_cash_basis_values_per_move:
+ tax_cash_basis_values_per_move[move.id] = move._collect_tax_cash_basis_values()
+
+ # Nothing to process on the move.
+ if not tax_cash_basis_values_per_move.get(move.id):
+ continue
+ move_values = tax_cash_basis_values_per_move[move.id]
+
+ # Check the cash basis configuration only when at least one cash basis tax entry need to be created.
+ journal = partial.company_id.tax_cash_basis_journal_id
+
+ if not journal:
+ raise UserError(_("There is no tax cash basis journal defined for the '%s' company.\n"
+ "Configure it in Accounting/Configuration/Settings") % partial.company_id.display_name)
+
+ partial_amount = 0.0
+ partial_amount_currency = 0.0
+ rate_amount = 0.0
+ rate_amount_currency = 0.0
+ if partial.debit_move_id.move_id == move:
+ partial_amount += partial.amount
+ partial_amount_currency += partial.debit_amount_currency
+ rate_amount -= partial.credit_move_id.balance
+ rate_amount_currency -= partial.credit_move_id.amount_currency
+ if partial.credit_move_id.move_id == move:
+ partial_amount += partial.amount
+ partial_amount_currency += partial.credit_amount_currency
+ rate_amount += partial.debit_move_id.balance
+ rate_amount_currency += partial.debit_move_id.amount_currency
+
+ if move_values['currency'] == move.company_id.currency_id:
+ # Percentage made on company's currency.
+ percentage = partial_amount / move_values['total_balance']
+ else:
+ # Percentage made on foreign currency.
+ percentage = partial_amount_currency / move_values['total_amount_currency']
+
+ if rate_amount:
+ payment_rate = rate_amount_currency / rate_amount
+ else:
+ payment_rate = 0.0
+
+ partial_vals = {
+ 'partial': partial,
+ 'percentage': percentage,
+ 'payment_rate': payment_rate,
+ }
+
+ # Add partials.
+ move_values.setdefault('partials', [])
+ move_values['partials'].append(partial_vals)
+
+ # Clean-up moves having nothing to process.
+ return {k: v for k, v in tax_cash_basis_values_per_move.items() if v}
+
+ @api.model
+ def _prepare_cash_basis_base_line_vals(self, base_line, balance, amount_currency):
+ ''' Prepare the values to be used to create the cash basis journal items for the tax base line
+ passed as parameter.
+
+ :param base_line: An account.move.line being the base of some taxes.
+ :param balance: The balance to consider for this line.
+ :param amount_currency: The balance in foreign currency to consider for this line.
+ :return: A python dictionary that could be passed to the create method of
+ account.move.line.
+ '''
+ account = base_line.company_id.account_cash_basis_base_account_id or base_line.account_id
+ return {
+ 'name': base_line.move_id.name,
+ 'debit': balance if balance > 0.0 else 0.0,
+ 'credit': -balance if balance < 0.0 else 0.0,
+ 'amount_currency': amount_currency,
+ 'currency_id': base_line.currency_id.id,
+ 'partner_id': base_line.partner_id.id,
+ 'account_id': account.id,
+ 'tax_ids': [(6, 0, base_line.tax_ids.ids)],
+ 'tax_tag_ids': [(6, 0, base_line.tax_tag_ids.ids)],
+ 'tax_exigible': True,
+ }
+
+ @api.model
+ def _prepare_cash_basis_counterpart_base_line_vals(self, cb_base_line_vals):
+ ''' Prepare the move line used as a counterpart of the line created by
+ _prepare_cash_basis_base_line_vals.
+
+ :param cb_base_line_vals: The line returned by _prepare_cash_basis_base_line_vals.
+ :return: A python dictionary that could be passed to the create method of
+ account.move.line.
+ '''
+ return {
+ 'name': cb_base_line_vals['name'],
+ 'debit': cb_base_line_vals['credit'],
+ 'credit': cb_base_line_vals['debit'],
+ 'account_id': cb_base_line_vals['account_id'],
+ 'amount_currency': -cb_base_line_vals['amount_currency'],
+ 'currency_id': cb_base_line_vals['currency_id'],
+ 'partner_id': cb_base_line_vals['partner_id'],
+ 'tax_exigible': True,
+ }
+
+ @api.model
+ def _prepare_cash_basis_tax_line_vals(self, tax_line, balance, amount_currency):
+ ''' Prepare the move line corresponding to a tax in the cash basis entry.
+
+ :param tax_line: An account.move.line record being a tax line.
+ :param balance: The balance to consider for this line.
+ :param amount_currency: The balance in foreign currency to consider for this line.
+ :return: A python dictionary that could be passed to the create method of
+ account.move.line.
+ '''
+ return {
+ 'name': tax_line.name,
+ 'debit': balance if balance > 0.0 else 0.0,
+ 'credit': -balance if balance < 0.0 else 0.0,
+ 'tax_base_amount': tax_line.tax_base_amount,
+ 'tax_repartition_line_id': tax_line.tax_repartition_line_id.id,
+ 'tax_ids': [(6, 0, tax_line.tax_ids.ids)],
+ 'tax_tag_ids': [(6, 0, tax_line.tax_tag_ids.ids)],
+ 'account_id': tax_line.tax_repartition_line_id.account_id.id or tax_line.account_id.id,
+ 'amount_currency': amount_currency,
+ 'currency_id': tax_line.currency_id.id,
+ 'partner_id': tax_line.partner_id.id,
+ 'tax_exigible': True,
+ }
+
+ @api.model
+ def _prepare_cash_basis_counterpart_tax_line_vals(self, tax_line, cb_tax_line_vals):
+ ''' Prepare the move line used as a counterpart of the line created by
+ _prepare_cash_basis_tax_line_vals.
+
+ :param tax_line: An account.move.line record being a tax line.
+ :param cb_tax_line_vals: The result of _prepare_cash_basis_counterpart_tax_line_vals.
+ :return: A python dictionary that could be passed to the create method of
+ account.move.line.
+ '''
+ return {
+ 'name': cb_tax_line_vals['name'],
+ 'debit': cb_tax_line_vals['credit'],
+ 'credit': cb_tax_line_vals['debit'],
+ 'account_id': tax_line.account_id.id,
+ 'amount_currency': -cb_tax_line_vals['amount_currency'],
+ 'currency_id': cb_tax_line_vals['currency_id'],
+ 'partner_id': cb_tax_line_vals['partner_id'],
+ 'tax_exigible': True,
+ }
+
+ @api.model
+ def _get_cash_basis_base_line_grouping_key_from_vals(self, base_line_vals):
+ ''' Get the grouping key of a cash basis base line that hasn't yet been created.
+ :param base_line_vals: The values to create a new account.move.line record.
+ :return: The grouping key as a tuple.
+ '''
+ return (
+ base_line_vals['currency_id'],
+ base_line_vals['partner_id'],
+ base_line_vals['account_id'],
+ tuple(base_line_vals['tax_ids'][0][2]), # Decode [(6, 0, [...])] command
+ tuple(base_line_vals['tax_tag_ids'][0][2]), # Decode [(6, 0, [...])] command
+ )
+
+ @api.model
+ def _get_cash_basis_base_line_grouping_key_from_record(self, base_line, account=None):
+ ''' Get the grouping key of a journal item being a base line.
+ :param base_line: An account.move.line record.
+ :param account: Optional account to shadow the current base_line one.
+ :return: The grouping key as a tuple.
+ '''
+ return (
+ base_line.currency_id.id,
+ base_line.partner_id.id,
+ (account or base_line.account_id).id,
+ tuple(base_line.tax_ids.ids),
+ tuple(base_line.tax_tag_ids.ids),
+ )
+
+ @api.model
+ def _get_cash_basis_tax_line_grouping_key_from_vals(self, tax_line_vals):
+ ''' Get the grouping key of a cash basis tax line that hasn't yet been created.
+ :param tax_line_vals: The values to create a new account.move.line record.
+ :return: The grouping key as a tuple.
+ '''
+ return (
+ tax_line_vals['currency_id'],
+ tax_line_vals['partner_id'],
+ tax_line_vals['account_id'],
+ tuple(tax_line_vals['tax_ids'][0][2]), # Decode [(6, 0, [...])] command
+ tuple(tax_line_vals['tax_tag_ids'][0][2]), # Decode [(6, 0, [...])] command
+ tax_line_vals['tax_repartition_line_id'],
+ )
+
+ @api.model
+ def _get_cash_basis_tax_line_grouping_key_from_record(self, tax_line, account=None):
+ ''' Get the grouping key of a journal item being a tax line.
+ :param tax_line: An account.move.line record.
+ :param account: Optional account to shadow the current tax_line one.
+ :return: The grouping key as a tuple.
+ '''
+ return (
+ tax_line.currency_id.id,
+ tax_line.partner_id.id,
+ (account or tax_line.account_id).id,
+ tuple(tax_line.tax_ids.ids),
+ tuple(tax_line.tax_tag_ids.ids),
+ tax_line.tax_repartition_line_id.id,
+ )
+
+ @api.model
+ def _fix_cash_basis_full_balance_coverage(self, move_values, partial_values, pending_cash_basis_lines, partial_lines_to_create):
+ ''' This method is used to ensure the full coverage of the current move when it becomes fully paid.
+ For example, suppose a line of 0.03 paid 50-50. Without this method, each cash basis entry will report
+ 0.03 / 0.5 = 0.015 ~ 0.02 per cash entry on the tax report as base amount, for a total of 0.04.
+ This is wrong because we expect 0.03.on the tax report as base amount. This is wrong because we expect 0.03.
+
+ :param move_values: The collected values about cash basis for the current move.
+ :param partial_values: The collected values about cash basis for the current partial.
+ :param pending_cash_basis_lines: The previously generated lines during this reconciliation but not yet created.
+ :param partial_lines_to_create: The generated lines for the current and last partial making the move fully paid.
+ '''
+ residual_amount_per_group = {}
+ move = move_values['move']
+
+ # ==========================================================================
+ # Part 1:
+ # Add the balance of all journal items that are not tax exigible in order to
+ # ensure the exact balance will be report on the Tax Report.
+ # This part is needed when the move will be fully paid after the current
+ # reconciliation.
+ # ==========================================================================
+
+ balance_field = 'balance' if move_values['currency'] == move.company_id.currency_id else 'amount_currency'
+
+ for line in move_values['to_process_lines']:
+ if line.tax_repartition_line_id:
+ # Tax line.
+ grouping_key = self._get_cash_basis_tax_line_grouping_key_from_record(
+ line,
+ account=line.tax_repartition_line_id.account_id,
+ )
+ residual_amount_per_group.setdefault(grouping_key, 0.0)
+ residual_amount_per_group[grouping_key] += line[balance_field]
+
+ elif line.tax_ids:
+ # Base line.
+ grouping_key = self._get_cash_basis_base_line_grouping_key_from_record(
+ line,
+ account=line.company_id.account_cash_basis_base_account_id,
+ )
+ residual_amount_per_group.setdefault(grouping_key, 0.0)
+ residual_amount_per_group[grouping_key] += line[balance_field]
+
+ # ==========================================================================
+ # Part 2:
+ # Subtract all previously created cash basis journal items during previous
+ # reconciliation.
+ # ==========================================================================
+
+ previous_tax_cash_basis_moves = self.env['account.move'].search([
+ '|',
+ ('tax_cash_basis_rec_id', 'in', self.ids),
+ ('tax_cash_basis_move_id', '=', move.id),
+ ])
+ for line in previous_tax_cash_basis_moves.line_ids:
+ if line.tax_repartition_line_id:
+ # Tax line.
+ grouping_key = self._get_cash_basis_tax_line_grouping_key_from_record(line)
+ elif line.tax_ids:
+ # Base line.
+ grouping_key = self._get_cash_basis_base_line_grouping_key_from_record(line)
+ else:
+ continue
+
+ if grouping_key not in residual_amount_per_group:
+ # The grouping_key is unknown regarding the current lines.
+ # Maybe this move has been created before migration and then,
+ # we are not able to ensure the full coverage of the balance.
+ return
+
+ residual_amount_per_group[grouping_key] -= line[balance_field]
+
+ # ==========================================================================
+ # Part 3:
+ # Subtract all pending cash basis journal items that will be created during
+ # this reconciliation.
+ # ==========================================================================
+
+ for grouping_key, balance in pending_cash_basis_lines:
+ residual_amount_per_group[grouping_key] -= balance
+
+ # ==========================================================================
+ # Part 4:
+ # Fix the current cash basis journal items in progress by replacing the
+ # balance by the residual one.
+ # ==========================================================================
+
+ for grouping_key, aggregated_vals in partial_lines_to_create.items():
+ line_vals = aggregated_vals['vals']
+
+ if move_values['currency'] == move.company_id.currency_id:
+ balance = residual_amount_per_group[grouping_key]
+ line_vals.update({
+ 'debit': balance if balance > 0.0 else 0.0,
+ 'credit': -balance if balance < 0.0 else 0.0,
+ })
+ else:
+ amount_currency = residual_amount_per_group[grouping_key]
+ balance = partial_values['payment_rate'] and amount_currency / partial_values['payment_rate'] or 0.0
+ line_vals.update({
+ 'debit': balance if balance > 0.0 else 0.0,
+ 'credit': -balance if balance < 0.0 else 0.0,
+ 'amount_currency': amount_currency,
+ })
+
+ def _create_tax_cash_basis_moves(self):
+ ''' Create the tax cash basis journal entries.
+ :return: The newly created journal entries.
+ '''
+ tax_cash_basis_values_per_move = self._collect_tax_cash_basis_values()
+
+ moves_to_create = []
+ to_reconcile_after = []
+ for move_values in tax_cash_basis_values_per_move.values():
+ move = move_values['move']
+ pending_cash_basis_lines = []
+
+ for i, partial_values in enumerate(move_values['partials']):
+ partial = partial_values['partial']
+ is_last_partial = i == len(move_values['partials']) - 1
+
+ # Init the journal entry.
+ move_vals = {
+ 'move_type': 'entry',
+ 'date': partial.max_date,
+ 'ref': move.name,
+ 'journal_id': partial.company_id.tax_cash_basis_journal_id.id,
+ 'line_ids': [],
+ 'tax_cash_basis_rec_id': partial.id,
+ 'tax_cash_basis_move_id': move.id,
+ }
+
+ # Tracking of lines grouped all together.
+ # Used to reduce the number of generated lines and to avoid rounding issues.
+ partial_lines_to_create = {}
+
+ for line in move_values['to_process_lines']:
+
+ # ==========================================================================
+ # Compute the balance of the current line on the cash basis entry.
+ # This balance is a percentage representing the part of the journal entry
+ # that is actually paid by the current partial.
+ # ==========================================================================
+
+ if move_values['currency'] == move.company_id.currency_id:
+ # Percentage expressed in the company's currency.
+ balance = line.company_currency_id.round(line.balance * partial_values['percentage'])
+ amount_currency = 0.0
+ else:
+ # Percentage expressed in the foreign currency.
+ amount_currency = line.currency_id.round(line.amount_currency * partial_values['percentage'])
+ balance = partial_values['payment_rate'] and amount_currency / partial_values['payment_rate'] or 0.0
+
+ # ==========================================================================
+ # Prepare the mirror cash basis journal item of the current line.
+ # Group them all together as much as possible to reduce the number of
+ # generated journal items.
+ # Also track the computed balance in order to avoid rounding issues when
+ # the journal entry will be fully paid. At that case, we expect the exact
+ # amount of each line has been covered by the cash basis journal entries
+ # and well reported in the Tax Report.
+ # ==========================================================================
+
+ if line.tax_repartition_line_id:
+ # Tax line.
+
+ cb_tax_line_vals = self._prepare_cash_basis_tax_line_vals(line, balance, amount_currency)
+ grouping_key = self._get_cash_basis_tax_line_grouping_key_from_vals(cb_tax_line_vals)
+ partial_lines_to_create[grouping_key] = {
+ 'tax_line': line,
+ 'vals': cb_tax_line_vals,
+ }
+
+ elif line.tax_ids:
+ # Base line.
+
+ cb_base_line_vals = self._prepare_cash_basis_base_line_vals(line, balance, amount_currency)
+ grouping_key = self._get_cash_basis_base_line_grouping_key_from_vals(cb_base_line_vals)
+
+ if grouping_key in partial_lines_to_create:
+ aggregated_vals = partial_lines_to_create[grouping_key]['vals']
+ balance = aggregated_vals['debit'] - aggregated_vals['credit']
+ balance += cb_base_line_vals['debit'] - cb_base_line_vals['credit']
+
+ aggregated_vals.update({
+ 'debit': balance if balance > 0.0 else 0.0,
+ 'credit': -balance if balance < 0.0 else 0.0,
+ })
+ aggregated_vals['amount_currency'] += cb_base_line_vals['amount_currency']
+ else:
+ partial_lines_to_create[grouping_key] = {
+ 'vals': cb_base_line_vals,
+ }
+
+ # ==========================================================================
+ # Ensure the full coverage by replacing the balance of the journal items
+ # created by the last partial.
+ # ==========================================================================
+
+ if move_values['is_fully_paid'] and is_last_partial:
+ self._fix_cash_basis_full_balance_coverage(
+ move_values,
+ partial_values,
+ pending_cash_basis_lines,
+ partial_lines_to_create,
+ )
+
+ # ==========================================================================
+ # Create the counterpart journal items.
+ # ==========================================================================
+
+ # To be able to retrieve the correct matching between the tax lines to reconcile
+ # later, the lines will be created using a specific sequence.
+ sequence = 0
+
+ for grouping_key, aggregated_vals in partial_lines_to_create.items():
+ line_vals = aggregated_vals['vals']
+ line_vals['sequence'] = sequence
+
+ if move_values['currency'] == move.company_id.currency_id:
+ pending_cash_basis_lines.append((grouping_key, line_vals['debit'] - line_vals['credit']))
+ else:
+ pending_cash_basis_lines.append((grouping_key, line_vals['amount_currency']))
+
+ if 'tax_repartition_line_id' in line_vals:
+ # Tax line.
+
+ tax_line = aggregated_vals['tax_line']
+ counterpart_line_vals = self._prepare_cash_basis_counterpart_tax_line_vals(tax_line, line_vals)
+ counterpart_line_vals['sequence'] = sequence + 1
+
+ if tax_line.account_id.reconcile:
+ move_index = len(moves_to_create)
+ to_reconcile_after.append((tax_line, move_index, counterpart_line_vals['sequence']))
+
+ else:
+ # Base line.
+
+ counterpart_line_vals = self._prepare_cash_basis_counterpart_base_line_vals(line_vals)
+ counterpart_line_vals['sequence'] = sequence + 1
+
+ sequence += 2
+
+ move_vals['line_ids'] += [(0, 0, counterpart_line_vals), (0, 0, line_vals)]
+
+ moves_to_create.append(move_vals)
+
+ moves = self.env['account.move'].create(moves_to_create)
+ moves.post()
+
+ # Reconcile the tax lines being on a reconcile tax basis transfer account.
+ for line, move_index, sequence in to_reconcile_after:
+ counterpart_line = moves[move_index].line_ids.filtered(lambda line: line.sequence == sequence)
+
+ # When dealing with tiny amounts, the line could have a zero amount and then, be already reconciled.
+ if counterpart_line.reconciled:
+ continue
+
+ (line + counterpart_line).reconcile()
+
+ return moves
diff --git a/addons/account/models/account_tax.py b/addons/account/models/account_tax.py
index b56e6168dab..e4cd2e88325 100644
--- a/addons/account/models/account_tax.py
+++ b/addons/account/models/account_tax.py
@@ -88,12 +88,6 @@ class AccountTax(models.Model):
domain="[('deprecated', '=', False), ('company_id', '=', company_id)]",
comodel_name='account.account',
help="Account used to transition the tax amount for cash basis taxes. It will contain the tax amount as long as the original invoice has not been reconciled ; at reconciliation, this amount cancelled on this account and put on the regular tax account.")
- cash_basis_base_account_id = fields.Many2one(
- 'account.account',
- check_company=True,
- domain="[('deprecated', '=', False), ('company_id', '=', company_id)]",
- string='Base Tax Received Account',
- help='Account that will be set on lines created in cash basis journal entry and used to keep track of the tax base amount.')
invoice_repartition_line_ids = fields.One2many(string="Repartition for Invoices", comodel_name="account.tax.repartition.line", inverse_name="invoice_tax_id", copy=True, help="Repartition when the tax is used on an invoice")
refund_repartition_line_ids = fields.One2many(string="Repartition for Refund Invoices", comodel_name="account.tax.repartition.line", inverse_name="refund_tax_id", copy=True, help="Repartition when the tax is used on a refund")
country_id = fields.Many2one(string='Country', comodel_name='res.country', related='company_id.country_id', help="Technical field used to restrict the domain of account tags for tax repartition lines created for this tax.")
diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py
index 6f12b955724..c99be6bae59 100644
--- a/addons/account/models/chart_template.py
+++ b/addons/account/models/chart_template.py
@@ -121,6 +121,12 @@ class AccountChartTemplate(models.Model):
property_tax_payable_account_id = fields.Many2one('account.account.template', string="Tax current account (payable)")
property_tax_receivable_account_id = fields.Many2one('account.account.template', string="Tax current account (receivable)")
property_advance_tax_payment_account_id = fields.Many2one('account.account.template', string="Advance tax payment account")
+ property_cash_basis_base_account_id = fields.Many2one(
+ comodel_name='account.account.template',
+ domain=[('deprecated', '=', False)],
+ string="Base Tax Received Account",
+ help="Account that will be set on lines created in cash basis journal entry and used to keep track of the "
+ "tax base amount.")
@api.model
def _prepare_transfer_account_template(self, prefix=None):
@@ -249,6 +255,7 @@ class AccountChartTemplate(models.Model):
'default_cash_difference_income_account_id': acc_template_ref.get(self.default_cash_difference_income_account_id.id, False),
'default_cash_difference_expense_account_id': acc_template_ref.get(self.default_cash_difference_expense_account_id.id, False),
'account_journal_suspense_account_id': acc_template_ref.get(self.account_journal_suspense_account_id.id),
+ 'account_cash_basis_base_account_id': acc_template_ref.get(self.property_cash_basis_base_account_id.id),
})
if not company.account_journal_suspense_account_id:
@@ -555,10 +562,9 @@ class AccountChartTemplate(models.Model):
# writing account values after creation of accounts
for key, value in generated_tax_res['account_dict']['account.tax'].items():
- if value['cash_basis_transition_account_id'] or value['cash_basis_base_account_id']:
+ if value['cash_basis_transition_account_id']:
AccountTaxObj.browse(key).write({
'cash_basis_transition_account_id': account_ref.get(value['cash_basis_transition_account_id'], False),
- 'cash_basis_base_account_id': account_ref.get(value['cash_basis_base_account_id'], False),
})
AccountTaxRepartitionLineObj = self.env['account.tax.repartition.line']
@@ -855,11 +861,6 @@ class AccountTaxTemplate(models.Model):
string="Cash Basis Transition Account",
domain=[('deprecated', '=', False)],
help="Account used to transition the tax amount for cash basis taxes. It will contain the tax amount as long as the original invoice has not been reconciled ; at reconciliation, this amount cancelled on this account and put on the regular tax account.")
- cash_basis_base_account_id = fields.Many2one(
- 'account.account.template',
- domain=[('deprecated', '=', False)],
- string='Base Tax Received Account',
- help='Account that will be set on lines created in cash basis journal entry and used to keep track of the tax base amount.')
_sql_constraints = [
('name_company_uniq', 'unique(name, type_tax_use, chart_template_id)', 'Tax names must be unique !'),
@@ -946,7 +947,6 @@ class AccountTaxTemplate(models.Model):
# Since the accounts have not been created yet, we have to wait before filling these fields
todo_dict['account.tax'][tax.id] = {
'cash_basis_transition_account_id': template.cash_basis_transition_account_id.id,
- 'cash_basis_base_account_id': template.cash_basis_base_account_id.id,
}
# We also have to delay the assignation of accounts to repartition lines
diff --git a/addons/account/models/company.py b/addons/account/models/company.py
index 11e02db2baa..853b2321252 100644
--- a/addons/account/models/company.py
+++ b/addons/account/models/company.py
@@ -55,7 +55,6 @@ class ResCompany(models.Model):
transfer_account_code_prefix = fields.Char(string='Prefix of the transfer accounts')
account_sale_tax_id = fields.Many2one('account.tax', string="Default Sale Tax")
account_purchase_tax_id = fields.Many2one('account.tax', string="Default Purchase Tax")
- tax_cash_basis_journal_id = fields.Many2one('account.journal', string="Cash Basis Journal")
tax_calculation_rounding_method = fields.Selection([
('round_per_line', 'Round per Line'),
('round_globally', 'Round Globally'),
@@ -113,6 +112,17 @@ class ResCompany(models.Model):
# Technical field to hide country specific fields in company form view
country_code = fields.Char(related='country_id.code')
+ # Cash basis taxes
+ tax_cash_basis_journal_id = fields.Many2one(
+ comodel_name='account.journal',
+ string="Cash Basis Journal")
+ account_cash_basis_base_account_id = fields.Many2one(
+ comodel_name='account.account',
+ domain=[('deprecated', '=', False)],
+ string="Base Tax Received Account",
+ help="Account that will be set on lines created in cash basis journal entry and used to keep track of the "
+ "tax base amount.")
+
@api.constrains('account_opening_move_id', 'fiscalyear_last_day', 'fiscalyear_last_month')
def _check_fiscalyear_last_day(self):
# if the user explicitly chooses the 29th of February we allow it:
diff --git a/addons/account/models/res_config_settings.py b/addons/account/models/res_config_settings.py
index eb05b59737f..95166b32454 100644
--- a/addons/account/models/res_config_settings.py
+++ b/addons/account/models/res_config_settings.py
@@ -73,6 +73,12 @@ class ResConfigSettings(models.TransientModel):
module_snailmail_account = fields.Boolean(string="Snailmail")
tax_exigibility = fields.Boolean(string='Cash Basis', related='company_id.tax_exigibility', readonly=False)
tax_cash_basis_journal_id = fields.Many2one('account.journal', related='company_id.tax_cash_basis_journal_id', string="Tax Cash Basis Journal", readonly=False)
+ account_cash_basis_base_account_id = fields.Many2one(
+ comodel_name='account.account',
+ string="Base Tax Received Account",
+ readonly=False,
+ related='company_id.account_cash_basis_base_account_id',
+ domain=[('deprecated', '=', False)])
qr_code = fields.Boolean(string='Display SEPA QR-code', related='company_id.qr_code', readonly=False)
invoice_is_print = fields.Boolean(string='Print', related='company_id.invoice_is_print', readonly=False)
diff --git a/addons/account/static/src/js/account_payment_field.js b/addons/account/static/src/js/account_payment_field.js
index 71ed1dfd272..fa56d1a1858 100644
--- a/addons/account/static/src/js/account_payment_field.js
+++ b/addons/account/static/src/js/account_payment_field.js
@@ -59,19 +59,7 @@ var ShowPaymentLineWidget = AbstractField.extend({
var content = info.content[v];
var options = {
content: function () {
- var $content = $(QWeb.render('PaymentPopOver', {
- name: content.name,
- journal_name: content.journal_name,
- date: content.date,
- amount: content.amount,
- currency: content.currency,
- position: content.position,
- payment_id: content.payment_id,
- payment_method_name: content.payment_method_name,
- move_id: content.move_id,
- ref: content.ref,
- account_payment_id: content.account_payment_id,
- }));
+ var $content = $(QWeb.render('PaymentPopOver', content));
var unreconcile_button = $content.filter('.js_unreconcile_payment').on('click', self._onRemoveMoveReconcile.bind(self));
$content.filter('.js_open_payment').on('click', self._onOpenPayment.bind(self));
@@ -145,13 +133,13 @@ var ShowPaymentLineWidget = AbstractField.extend({
*/
_onRemoveMoveReconcile: function (event) {
var self = this;
- var paymentId = parseInt($(event.target).attr('payment-id'));
- if (paymentId !== undefined && !isNaN(paymentId)){
+ var moveId = parseInt($(event.target).attr('move-id'));
+ var partialId = parseInt($(event.target).attr('partial-id'));
+ if (partialId !== undefined && !isNaN(partialId)){
this._rpc({
- model: 'account.move.line',
- method: 'remove_move_reconcile',
- args: [paymentId],
- context: {'move_id': this.res_id},
+ model: 'account.move',
+ method: 'js_remove_outstanding_partial',
+ args: [moveId, partialId],
}).then(function () {
self.trigger_up('reload');
});
diff --git a/addons/account/static/src/xml/account_payment.xml b/addons/account/static/src/xml/account_payment.xml
index 8d6f484cb6c..c752f838123 100644
--- a/addons/account/static/src/xml/account_payment.xml
+++ b/addons/account/static/src/xml/account_payment.xml
@@ -78,7 +78,7 @@
</tr>
</table>
</div>
- <button class="btn btn-sm btn-primary js_unreconcile_payment float-left" t-att-payment-id="payment_id" style="margin-top:5px; margin-bottom:5px;" groups="account.group_account_invoice">Unreconcile</button>
+ <button class="btn btn-sm btn-primary js_unreconcile_payment float-left" t-att-partial-id="partial_id" t-att-payment-id="payment_id" t-att-move-id="move_id" style="margin-top:5px; margin-bottom:5px;" groups="account.group_account_invoice">Unreconcile</button>
<button class="btn btn-sm btn-secondary js_open_payment float-right" t-att-payment-id="account_payment_id" t-att-move-id="move_id" style="margin-top:5px; margin-bottom:5px;">View</button>
</t>
diff --git a/addons/account/static/tests/account_payment_field_tests.js b/addons/account/static/tests/account_payment_field_tests.js
index 379f099d29c..48609728edd 100644
--- a/addons/account/static/tests/account_payment_field_tests.js
+++ b/addons/account/static/tests/account_payment_field_tests.js
@@ -16,7 +16,7 @@ QUnit.module('account', {
},
records: [{
id: 1,
- payments_widget: '{"content": [{"digits": [69, 2], "currency": "$", "amount": 555.0, "name": "Customer Payment: INV/2017/0004", "date": "2017-04-25", "position": "before", "ref": "BNK1/2017/0003 (INV/2017/0004)", "payment_id": 22, "move_id": 10, "journal_name": "Bank"}], "outstanding": false, "title": "Less Payment"}',
+ payments_widget: '{"content": [{"digits": [69, 2], "currency": "$", "amount": 555.0, "name": "Customer Payment: INV/2017/0004", "date": "2017-04-25", "position": "before", "ref": "BNK1/2017/0003 (INV/2017/0004)", "payment_id": 22, "move_id": 10, "partial_id": 38, "journal_name": "Bank"}], "outstanding": false, "title": "Less Payment"}',
outstanding_credits_debits_widget: '{"content": [{"digits": [69, 2], "currency": "$", "amount": 100.0, "journal_name": "INV/2017/0004", "position": "before", "id": 20}], "move_id": 4, "outstanding": true, "title": "Outstanding credits"}',
}]
},
@@ -26,7 +26,7 @@ QUnit.module('account', {
QUnit.module('Reconciliation');
QUnit.test('Reconciliation form field', async function (assert) {
- assert.expect(6);
+ assert.expect(5);
var form = await createView({
View: FormView,
@@ -38,9 +38,8 @@ QUnit.module('account', {
'</form>',
res_id: 1,
mockRPC: function (route, args) {
- if (args.method === 'remove_move_reconcile') {
- assert.deepEqual(args.args, [22], "should call remove_move_reconcile {warning: required focus}");
- assert.deepEqual(args.kwargs, {context: {"move_id": 1}}, "should call remove_move_reconcile {warning: required focus}");
+ if (args.method === 'js_remove_outstanding_partial') {
+ assert.deepEqual(args.args, [10, 38], "should call js_remove_outstanding_partial {warning: required focus}");
return Promise.resolve();
}
if (args.method === 'js_assign_outstanding_line') {
diff --git a/addons/account/tests/__init__.py b/addons/account/tests/__init__.py
index b6e8b294a63..552b40ccb12 100644
--- a/addons/account/tests/__init__.py
+++ b/addons/account/tests/__init__.py
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
+from . import test_account_move_reconcile
+from . import test_account_move_payments_widget
from . import test_account_move_out_invoice
from . import test_account_move_out_refund
from . import test_account_move_in_invoice
diff --git a/addons/account/tests/common.py b/addons/account/tests/common.py
index 15e2982c1d5..b28d51d18db 100644
--- a/addons/account/tests/common.py
+++ b/addons/account/tests/common.py
@@ -374,6 +374,24 @@ class AccountTestNoChartCommon(SavepointCaseWithUserDemo):
'code': 'GJT0',
})
+ # Exchange journal.
+ cls.exchange_journal0 = cls.env['account.journal'].create({
+ 'name': 'Exchange Difference - (test)',
+ 'type': 'general',
+ 'code': 'EXCH',
+ 'default_debit_account_id': cls.env['account.account'].create({
+ 'name': 'Foreign Exchange Gain - (test)',
+ 'code': 'NC4410',
+ 'user_type_id': cls.env.ref('account.data_account_type_revenue').id,
+ }).id,
+ 'default_credit_account_id': cls.env['account.account'].create({
+ 'name': 'Foreign Exchange Loss - (test)',
+ 'code': 'NC6410',
+ 'user_type_id': cls.env.ref('account.data_account_type_expenses').id,
+ }).id,
+ })
+ cls.env.user.company_id.currency_exchange_journal_id = cls.exchange_journal0
+
@classmethod
def setUpAdditionalAccounts(cls):
""" Set up some addionnal accounts: expenses, revenue, ... """
@@ -504,6 +522,27 @@ class AccountTestNoChartCommonMultiCompany(AccountTestNoChartCommon):
'company_id': cls.company_B.id
})
+ # Exchange journal.
+ cls.exchange_journal0 = cls.env['account.journal'].create({
+ 'name': 'Exchange Difference - (test)',
+ 'type': 'general',
+ 'code': 'EXCH',
+ 'company_id': cls.company_B.id,
+ 'default_debit_account_id': cls.env['account.account'].create({
+ 'name': 'Foreign Exchange Gain - (test)',
+ 'code': 'NC4410',
+ 'user_type_id': cls.env.ref('account.data_account_type_revenue').id,
+ 'company_id': cls.company_B.id
+ }).id,
+ 'default_credit_account_id': cls.env['account.account'].create({
+ 'name': 'Foreign Exchange Loss - (test)',
+ 'code': 'NC6410',
+ 'user_type_id': cls.env.ref('account.data_account_type_expenses').id,
+ 'company_id': cls.company_B.id
+ }).id,
+ })
+ cls.company_B.currency_exchange_journal_id = cls.exchange_journal0
+
@classmethod
def setUpAdditionalAccounts(cls):
""" Set up some addionnal accounts: expenses, revenue, ... """
@@ -1072,6 +1111,8 @@ class TestAccountReconciliationCommon(AccountTestCommon):
'user_type_id': cls.env.ref('account.data_account_type_current_assets').id,
'company_id': cls.company.id,
})
+ cls.company.account_cash_basis_base_account_id = cls.tax_base_amount_account.id
+
# Journals
cls.purchase_journal = cls.env['account.journal'].create({
@@ -1100,7 +1141,6 @@ class TestAccountReconciliationCommon(AccountTestCommon):
'amount': 20,
'tax_exigibility': 'on_payment',
'cash_basis_transition_account_id': cls.tax_waiting_account.id,
- 'cash_basis_base_account_id': cls.tax_base_amount_account.id,
'invoice_repartition_line_ids': [
(0,0, {
'factor_percent': 100,
diff --git a/addons/account/tests/test_account_move_payments_widget.py b/addons/account/tests/test_account_move_payments_widget.py
new file mode 100644
index 00000000000..9f3ba3f4674
--- /dev/null
+++ b/addons/account/tests/test_account_move_payments_widget.py
@@ -0,0 +1,184 @@
+# -*- coding: utf-8 -*-
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests import tagged
+
+import json
+
+
+@tagged('post_install', '-at_install')
+class TestAccountMovePaymentsWidget(AccountTestInvoicingCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ cls.receivable_account = cls.company_data['default_account_receivable']
+ cls.payable_account = cls.company_data['default_account_payable']
+
+ cls.currency_data_2 = cls.setup_multi_currency_data(default_values={
+ 'name': 'Stars',
+ 'symbol': '☆',
+ 'currency_unit_label': 'Stars',
+ 'currency_subunit_label': 'Little Stars',
+ }, rate2016=6.0, rate2017=4.0)
+
+ cls.curr_1 = cls.company_data['currency']
+ cls.curr_2 = cls.currency_data['currency']
+ cls.curr_3 = cls.currency_data_2['currency']
+
+ cls.payment_2016_curr_1 = cls.env['account.move'].create({
+ 'date': '2016-01-01',
+ 'line_ids': [
+ (0, 0, {'debit': 0.0, 'credit': 500.0, 'amount_currency': 0.0, 'currency_id': False, 'account_id': cls.receivable_account.id, 'partner_id': cls.partner_a.id}),
+ (0, 0, {'debit': 500.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': False, 'account_id': cls.payable_account.id, 'partner_id': cls.partner_a.id}),
+ ],
+ })
+ cls.payment_2016_curr_1.post()
+
+ cls.payment_2016_curr_2 = cls.env['account.move'].create({
+ 'date': '2016-01-01',
+ 'line_ids': [
+ (0, 0, {'debit': 0.0, 'credit': 500.0, 'amount_currency': -1550.0, 'currency_id': cls.curr_2.id, 'account_id': cls.receivable_account.id, 'partner_id': cls.partner_a.id}),
+ (0, 0, {'debit': 500.0, 'credit': 0.0, 'amount_currency': 1550.0, 'currency_id': cls.curr_2.id, 'account_id': cls.payable_account.id, 'partner_id': cls.partner_a.id}),
+ ],
+ })
+ cls.payment_2016_curr_2.post()
+
+ cls.payment_2017_curr_2 = cls.env['account.move'].create({
+ 'date': '2017-01-01',
+ 'line_ids': [
+ (0, 0, {'debit': 0.0, 'credit': 500.0, 'amount_currency': -950.0, 'currency_id': cls.curr_2.id, 'account_id': cls.receivable_account.id, 'partner_id': cls.partner_a.id}),
+ (0, 0, {'debit': 500.0, 'credit': 0.0, 'amount_currency': 950.0, 'currency_id': cls.curr_2.id, 'account_id': cls.payable_account.id, 'partner_id': cls.partner_a.id}),
+ ],
+ })
+ cls.payment_2017_curr_2.post()
+
+ cls.payment_2016_curr_3 = cls.env['account.move'].create({
+ 'date': '2016-01-01',
+ 'line_ids': [
+ (0, 0, {'debit': 0.0, 'credit': 500.0, 'amount_currency': -3050.0, 'currency_id': cls.curr_3.id, 'account_id': cls.receivable_account.id, 'partner_id': cls.partner_a.id}),
+ (0, 0, {'debit': 500.0, 'credit': 0.0, 'amount_currency': 3050.0, 'currency_id': cls.curr_3.id, 'account_id': cls.payable_account.id, 'partner_id': cls.partner_a.id}),
+ ],
+ })
+ cls.payment_2016_curr_3.post()
+
+ cls.payment_2017_curr_3 = cls.env['account.move'].create({
+ 'date': '2017-01-01',
+ 'line_ids': [
+ (0, 0, {'debit': 0.0, 'credit': 500.0, 'amount_currency': -1950.0, 'currency_id': cls.curr_3.id, 'account_id': cls.receivable_account.id, 'partner_id': cls.partner_a.id}),
+ (0, 0, {'debit': 500.0, 'credit': 0.0, 'amount_currency': 1950.0, 'currency_id': cls.curr_3.id, 'account_id': cls.payable_account.id, 'partner_id': cls.partner_a.id}),
+ ],
+ })
+ cls.payment_2017_curr_3.post()
+
+ # -------------------------------------------------------------------------
+ # HELPERS
+ # -------------------------------------------------------------------------
+
+ def _test_all_outstanding_payments(self, invoice, expected_amounts):
+ ''' Check the outstanding payments widget before/after the reconciliation.
+ :param invoice: An account.move record.
+ :param expected_amounts: A map <move_id> -> <amount>
+ '''
+
+ # Check suggested outstanding payments.
+ to_reconcile_payments_widget_vals = json.loads(invoice.invoice_outstanding_credits_debits_widget)
+
+ self.assertTrue(to_reconcile_payments_widget_vals)
+
+ current_amounts = {vals['move_id']: vals['amount'] for vals in to_reconcile_payments_widget_vals['content']}
+ self.assertDictEqual(current_amounts, expected_amounts)
+
+ # Reconcile
+ pay_term_lines = invoice.line_ids\
+ .filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
+ to_reconcile = self.env['account.move'].browse(list(current_amounts.keys()))\
+ .line_ids\
+ .filtered(lambda line: line.account_id == pay_term_lines.account_id)
+ (pay_term_lines + to_reconcile).reconcile()
+
+ # Check payments after reconciliation.
+ reconciled_payments_widget_vals = json.loads(invoice.invoice_payments_widget)
+
+ self.assertTrue(reconciled_payments_widget_vals)
+
+ current_amounts = {vals['move_id']: vals['amount'] for vals in reconciled_payments_widget_vals['content']}
+ self.assertDictEqual(current_amounts, expected_amounts)
+
+ # -------------------------------------------------------------------------
+ # TESTS
+ # -------------------------------------------------------------------------
+
+ def test_outstanding_payments_single_currency(self):
+ ''' Test the outstanding payments widget on invoices having the same currency
+ as the company one.
+ '''
+
+ # Customer invoice of 2500.0 in curr_1.
+ out_invoice = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'date': '2017-01-01',
+ 'invoice_date': '2017-01-01',
+ 'partner_id': self.partner_a.id,
+ 'currency_id': self.curr_1.id,
+ 'invoice_line_ids': [(0, 0, {'name': '/', 'price_unit': 2500.0})],
+ })
+ out_invoice.post()
+
+ # Vendor bill of 2500.0 in curr_1.
+ in_invoice = self.env['account.move'].create({
+ 'move_type': 'in_invoice',
+ 'date': '2017-01-01',
+ 'invoice_date': '2017-01-01',
+ 'partner_id': self.partner_a.id,
+ 'currency_id': self.curr_1.id,
+ 'invoice_line_ids': [(0, 0, {'name': '/', 'price_unit': 2500.0})],
+ })
+ in_invoice.post()
+
+ expected_amounts = {
+ self.payment_2016_curr_1.id: 500.0,
+ self.payment_2016_curr_2.id: 500.0,
+ self.payment_2017_curr_2.id: 500.0,
+ self.payment_2016_curr_3.id: 500.0,
+ self.payment_2017_curr_3.id: 500.0,
+ }
+
+ self._test_all_outstanding_payments(out_invoice, expected_amounts)
+ self._test_all_outstanding_payments(in_invoice, expected_amounts)
+
+ def test_outstanding_payments_foreign_currency(self):
+ ''' Test the outstanding payments widget on invoices having a foreign currency. '''
+
+ # Customer invoice of 2500.0 in curr_1.
+ out_invoice = self.env['account.move'].create({
+ 'move_type': 'out_invoice',
+ 'date': '2017-01-01',
+ 'invoice_date': '2017-01-01',
+ 'partner_id': self.partner_a.id,
+ 'currency_id': self.curr_2.id,
+ 'invoice_line_ids': [(0, 0, {'name': '/', 'price_unit': 7500.0})],
+ })
+ out_invoice.post()
+
+ # Vendor bill of 2500.0 in curr_1.
+ in_invoice = self.env['account.move'].create({
+ 'move_type': 'in_invoice',
+ 'date': '2017-01-01',
+ 'invoice_date': '2017-01-01',
+ 'partner_id': self.partner_a.id,
+ 'currency_id': self.curr_2.id,
+ 'invoice_line_ids': [(0, 0, {'name': '/', 'price_unit': 7500.0})],
+ })
+ in_invoice.post()
+
+ expected_amounts = {
+ self.payment_2016_curr_1.id: 1500.0,
+ self.payment_2016_curr_2.id: 1550.0,
+ self.payment_2017_curr_2.id: 950.0,
+ self.payment_2016_curr_3.id: 1500.0,
+ self.payment_2017_curr_3.id: 1000.0,
+ }
+
+ self._test_all_outstanding_payments(out_invoice, expected_amounts)
+ self._test_all_outstanding_payments(in_invoice, expected_amounts)
diff --git a/addons/account/tests/test_account_move_reconcile.py b/addons/account/tests/test_account_move_reconcile.py
new file mode 100644
index 00000000000..76b5b51cb17
--- /dev/null
+++ b/addons/account/tests/test_account_move_reconcile.py
@@ -0,0 +1,1635 @@
+# -*- coding: utf-8 -*-
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests import tagged
+
+
+@tagged('post_install', '-at_install')
+class TestAccountMoveReconcile(AccountTestInvoicingCommon):
+ ''' Tests about the account.partial.reconcile model, not the reconciliation itself but mainly the computation of
+ the residual amounts on account.move.line.
+ '''
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ cls.extra_receivable_account_1 = cls.copy_account(cls.company_data['default_account_receivable'])
+ cls.extra_receivable_account_2 = cls.copy_account(cls.company_data['default_account_receivable'])
+ cls.extra_payable_account_1 = cls.copy_account(cls.company_data['default_account_payable'])
+ cls.extra_payable_account_2 = cls.copy_account(cls.company_data['default_account_payable'])
+
+ # ==== Multi-currency setup ====
+
+ cls.currency_data_2 = cls.setup_multi_currency_data(default_values={
+ 'name': 'Diamond',
+ 'symbol': '💎',
+ 'currency_unit_label': 'Diamond',
+ 'currency_subunit_label': 'Carbon',
+ }, rate2016=6.0, rate2017=4.0)
+
+ # ==== Cash Basis Taxes setup ====
+
+ cls.cash_basis_base_account = cls.env['account.account'].create({
+ 'code': 'cash_basis_base_account',
+ 'name': 'cash_basis_base_account',
+ 'user_type_id': cls.env.ref('account.data_account_type_revenue').id,
+ 'company_id': cls.company_data['company'].id,
+ })
+ cls.company_data['company'].account_cash_basis_base_account_id = cls.cash_basis_base_account
+
+ cls.cash_basis_transfer_account = cls.env['account.account'].create({
+ 'code': 'cash_basis_transfer_account',
+ 'name': 'cash_basis_transfer_account',
+ 'user_type_id': cls.env.ref('account.data_account_type_revenue').id,
+ 'company_id': cls.company_data['company'].id,
+ })
+
+ cls.tax_account_1 = cls.env['account.account'].create({
+ 'code': 'tax_account_1',
+ 'name': 'tax_account_1',
+ 'user_type_id': cls.env.ref('account.data_account_type_revenue').id,
+ 'company_id': cls.company_data['company'].id,
+ })
+
+ cls.tax_account_2 = cls.env['account.account'].create({
+ 'code': 'tax_account_2',
+ 'name': 'tax_account_2',
+ 'user_type_id': cls.env.ref('account.data_account_type_revenue').id,
+ 'company_id': cls.company_data['company'].id,
+ })
+
+ cls.fake_country = cls.env['res.country'].create({
+ 'name': "The Island of the Fly",
+ 'code': 'YY',
+ })
+
+ cls.tax_tags = cls.env['account.account.tag'].create({
+ 'name': 'tax_tag_%s' % str(i),
+ 'applicability': 'taxes',
+ 'country_id': cls.fake_country.id,
+ } for i in range(8))
+
+ cls.cash_basis_tax_a_third_amount = cls.env['account.tax'].create({
+ 'name': 'tax_1',
+ 'amount': 33.3333,
+ 'company_id': cls.company_data['company'].id,
+ 'cash_basis_transition_account_id': cls.cash_basis_transfer_account.id,
+ 'invoice_repartition_line_ids': [
+ (0, 0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'tag_ids': [(6, 0, cls.tax_tags[0].ids)],
+ }),
+
+ (0, 0, {
+ 'factor_percent': 10,
+ 'repartition_type': 'tax',
+ 'account_id': cls.tax_account_1.id,
+ 'tag_ids': [(6, 0, cls.tax_tags[1].ids)],
+ }),
+ ],
+ 'refund_repartition_line_ids': [
+ (0, 0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'tag_ids': [(6, 0, cls.tax_tags[2].ids)],
+ }),
+
+ (0, 0, {
+ 'factor_percent': 10,
+ 'repartition_type': 'tax',
+ 'account_id': cls.tax_account_1.id,
+ 'tag_ids': [(6, 0, cls.tax_tags[3].ids)],
+ }),
+ ],
+ })
+
+ cls.cash_basis_tax_tiny_amount = cls.env['account.tax'].create({
+ 'name': 'cash_basis_tax_tiny_amount',
+ 'amount': 0.0001,
+ 'company_id': cls.company_data['company'].id,
+ 'cash_basis_transition_account_id': cls.cash_basis_transfer_account.id,
+ 'invoice_repartition_line_ids': [
+ (0, 0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'tag_ids': [(6, 0, cls.tax_tags[4].ids)],
+ }),
+
+ (0, 0, {
+ 'factor_percent': 10,
+ 'repartition_type': 'tax',
+ 'account_id': cls.tax_account_2.id,
+ 'tag_ids': [(6, 0, cls.tax_tags[5].ids)],
+ }),
+ ],
+ 'refund_repartition_line_ids': [
+ (0, 0, {
+ 'factor_percent': 100,
+ 'repartition_type': 'base',
+ 'tag_ids': [(6, 0, cls.tax_tags[6].ids)],
+ }),
+
+ (0, 0, {
+ 'factor_percent': 10,
+ 'repartition_type': 'tax',
+ 'account_id': cls.tax_account_2.id,
+ 'tag_ids': [(6, 0, cls.tax_tags[7].ids)],
+ }),
+ ],
+ })
+
+ # -------------------------------------------------------------------------
+ # HELPERS
+ # -------------------------------------------------------------------------
+
+ def assertFullReconcile(self, full_reconcile, lines):
+ exchange_difference_move = full_reconcile.exchange_move_id
+ partials = lines.mapped('matched_debit_ids') + lines.mapped('matched_credit_ids')
+
+ if full_reconcile.exchange_move_id:
+ lines += exchange_difference_move.line_ids.filtered(lambda line: line.account_id == lines[0].account_id)
+
+ # Use sets to not depend of the order.
+ self.assertEqual(set(full_reconcile.partial_reconcile_ids), set(partials))
+ self.assertEqual(set(full_reconcile.reconciled_line_ids), set(lines))
+
+ # Ensure there is no residual amount left.
+ self.assertRecordValues(lines, [{
+ 'amount_residual': 0.0,
+ 'amount_residual_currency': 0.0,
+ 'reconciled': bool(line.account_id.reconcile),
+ } for line in lines])
+
+ def assertPartialReconcile(self, partials, expected_vals_list):
+ partials = partials.sorted(lambda part: (
+ part.amount,
+ part.debit_amount_currency,
+ part.credit_amount_currency,
+ ))
+ self.assertRecordValues(partials, expected_vals_list)
+
+ def assertAmountsGroupByAccount(self, amount_per_account):
+ expected_values = {account.id: (account, balance, amount_currency) for account, balance, amount_currency in amount_per_account}
+
+ if not expected_values:
+ return
+
+ self.cr.execute('''
+ SELECT
+ line.account_id,
+ COALESCE(SUM(line.balance), 0.0) AS total_balance,
+ COALESCE(SUM(line.amount_currency), 0.0) AS total_amount_currency
+ FROM account_move_line line
+ WHERE line.account_id IN %s
+ GROUP BY line.account_id
+ ''', [tuple(expected_values.keys())])
+ for account_id, total_balance, total_amount_currency in self.cr.fetchall():
+ account, expected_balance, expected_amount_currency = expected_values[account_id]
+ self.assertEqual(
+ total_balance,
+ expected_balance,
+ "Balance of %s is incorrect" % account.name,
+ )
+ self.assertEqual(
+ total_amount_currency,
+ expected_amount_currency,
+ "Amount currency of %s is incorrect" % account.name,
+ )
+
+ def assertTaxGridAmounts(self, amount_per_tag):
+ expected_values = {tag.id: (tag, balance) for tag, balance in amount_per_tag}
+
+ if not expected_values:
+ return
+
+ self.cr.execute('''
+ SELECT
+ rel.account_account_tag_id,
+ SUM(line.balance)
+ FROM account_account_tag_account_move_line_rel rel
+ JOIN account_move_line line ON line.id = rel.account_move_line_id
+ WHERE line.tax_exigible IS TRUE
+ GROUP BY rel.account_account_tag_id
+ ''')
+
+ for tag_id, total_balance in self.cr.fetchall():
+ tag, expected_balance = expected_values[tag_id]
+ self.assertEqual(
+ total_balance,
+ expected_balance,
+ "Balance of %s is incorrect" % tag.name,
+ )
+
+ # -------------------------------------------------------------------------
+ # Test creation of account.partial.reconcile/account.full.reconcile
+ # during the reconciliation.
+ # -------------------------------------------------------------------------
+
+ def test_reconcile_single_currency(self):
+ account_id = self.company_data['default_account_receivable'].id
+
+ move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2016-01-01',
+ 'line_ids': [
+ (0, 0, {'debit': 1000.0, 'credit': 0.0, 'account_id': account_id}),
+ (0, 0, {'debit': 200.0, 'credit': 0.0, 'account_id': account_id}),
+ (0, 0, {'debit': 0.0, 'credit': 300.0, 'account_id': account_id}),
+ (0, 0, {'debit': 0.0, 'credit': 400.0, 'account_id': account_id}),
+ (0, 0, {'debit': 0.0, 'credit': 500.0, 'account_id': account_id}),
+ ]
+ })
+ move.post()
+
+ line_1 = move.line_ids.filtered(lambda line: line.debit == 1000.0)
+ line_2 = move.line_ids.filtered(lambda line: line.debit == 200.0)
+ line_3 = move.line_ids.filtered(lambda line: line.credit == 300.0)
+ line_4 = move.line_ids.filtered(lambda line: line.credit == 400.0)
+ line_5 = move.line_ids.filtered(lambda line: line.credit == 500.0)
+
+ self.assertRecordValues(line_1 + line_2 + line_3 + line_4 + line_5, [
+ {'amount_residual': 1000.0, 'amount_residual_currency': 0.0, 'reconciled': False},
+ {'amount_residual': 200.0, 'amount_residual_currency': 0.0, 'reconciled': False},
+ {'amount_residual': -300.0, 'amount_residual_currency': 0.0, 'reconciled': False},
+ {'amount_residual': -400.0, 'amount_residual_currency': 0.0, 'reconciled': False},
+ {'amount_residual': -500.0, 'amount_residual_currency': 0.0, 'reconciled': False},
+ ])
+
+ res = (line_1 + line_3).reconcile()
+
+ self.assertPartialReconcile(res['partials'], [{
+ 'amount': 300.0,
+ 'debit_amount_currency': 300.0,
+ 'credit_amount_currency': 300.0,
+ 'debit_move_id': line_1.id,
+ 'credit_move_id': line_3.id,
+ }])
+
+ self.assertRecordValues(line_1 + line_3, [
+ {'amount_residual': 700.0, 'amount_residual_currency': 0.0, 'reconciled': False},
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ ])
+
+ res = (line_1 + line_4).reconcile()
+
+ self.assertPartialReconcile(res['partials'], [{
+ 'amount': 400.0,
+ 'debit_amount_currency': 400.0,
+ 'credit_amount_currency': 400.0,
+ 'debit_move_id': line_1.id,
+ 'credit_move_id': line_4.id,
+ }])
+
+ self.assertRecordValues(line_1 + line_4, [
+ {'amount_residual': 300.0, 'amount_residual_currency': 0.0, 'reconciled': False},
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ ])
+
+ res = (line_1 + line_5).reconcile()
+
+ self.assertPartialReconcile(res['partials'], [{
+ 'amount': 300.0,
+ 'debit_amount_currency': 300.0,
+ 'credit_amount_currency': 300.0,
+ 'debit_move_id': line_1.id,
+ 'credit_move_id': line_5.id,
+ }])
+
+ self.assertRecordValues(line_1 + line_5, [
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ {'amount_residual': -200.0, 'amount_residual_currency': 0.0, 'reconciled': False},
+ ])
+
+ res = (line_2 + line_5).reconcile()
+
+ self.assertPartialReconcile(res['partials'], [{
+ 'amount': 200.0,
+ 'debit_amount_currency': 200.0,
+ 'credit_amount_currency': 200.0,
+ 'debit_move_id': line_2.id,
+ 'credit_move_id': line_5.id,
+ }])
+
+ self.assertRecordValues(res['full_reconcile'], [{'exchange_move_id': False}])
+ self.assertFullReconcile(res['full_reconcile'], line_1 + line_2 + line_3 + line_4 + line_5)
+
+ def test_reconcile_same_foreign_currency(self):
+ account_id = self.company_data['default_account_receivable'].id
+
+ # Rate is 3.0 in 2016, 2.0 in 2017.
+ currency_id = self.currency_data['currency'].id
+
+ moves = self.env['account.move'].create([
+ {
+ 'move_type': 'entry',
+ 'date': '2016-01-01',
+ 'line_ids': [
+ (0, 0, {'debit': 1200.0, 'credit': 0.0, 'amount_currency': 3600.0, 'account_id': account_id, 'currency_id': currency_id}),
+ (0, 0, {'debit': 120.0, 'credit': 0.0, 'amount_currency': 360.0, 'account_id': account_id, 'currency_id': currency_id}),
+
+ (0, 0, {'debit': 0.0, 'credit': 1320.0, 'account_id': account_id}),
+ ]
+ },
+ {
+ 'move_type': 'entry',
+ 'date': '2017-01-01',
+ 'line_ids': [
+ (0, 0, {'debit': 0.0, 'credit': 240.0, 'amount_currency': -480.0, 'account_id': account_id, 'currency_id': currency_id}),
+ (0, 0, {'debit': 0.0, 'credit': 720.0, 'amount_currency': -1440.0, 'account_id': account_id, 'currency_id': currency_id}),
+ (0, 0, {'debit': 0.0, 'credit': 1020.0, 'amount_currency': -2040.0, 'account_id': account_id, 'currency_id': currency_id}),
+
+ (0, 0, {'debit': 1980.0, 'credit': 0.0, 'account_id': account_id}),
+ ]
+ }
+ ])
+
+ moves.post()
+
+ line_1 = moves.line_ids.filtered(lambda line: line.debit == 1200.0)
+ line_2 = moves.line_ids.filtered(lambda line: line.debit == 120.0)
+ line_3 = moves.line_ids.filtered(lambda line: line.credit == 240.0)
+ line_4 = moves.line_ids.filtered(lambda line: line.credit == 720.0)
+ line_5 = moves.line_ids.filtered(lambda line: line.credit == 1020.0)
+
+ self.assertRecordValues(line_1 + line_2 + line_3 + line_4 + line_5, [
+ {'amount_residual': 1200.0, 'amount_residual_currency': 3600.0, 'reconciled': False},
+ {'amount_residual': 120.0, 'amount_residual_currency': 360.0, 'reconciled': False},
+ {'amount_residual': -240.0, 'amount_residual_currency': -480.0, 'reconciled': False},
+ {'amount_residual': -720.0, 'amount_residual_currency': -1440.0, 'reconciled': False},
+ {'amount_residual': -1020.0, 'amount_residual_currency': -2040.0, 'reconciled': False},
+ ])
+
+ res = (line_1 + line_3 + line_4).reconcile()
+
+ self.assertPartialReconcile(res['partials'], [
+ # Partial generated when reconciling line_1 & line_3:
+ {
+ 'amount': 240.0,
+ 'debit_amount_currency': 480.0,
+ 'credit_amount_currency': 480.0,
+ 'debit_move_id': line_1.id,
+ 'credit_move_id': line_3.id,
+ },
+ # Partial generated when reconciling line_1 & line_4:
+ {
+ 'amount': 720.0,
+ 'debit_amount_currency': 1440.0,
+ 'credit_amount_currency': 1440.0,
+ 'debit_move_id': line_1.id,
+ 'credit_move_id': line_4.id,
+ },
+ ])
+
+ self.assertRecordValues(line_1 + line_3 + line_4, [
+ {'amount_residual': 240.0, 'amount_residual_currency': 1680.0, 'reconciled': False},
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ ])
+
+ res = (line_1 + line_5).reconcile()
+
+ self.assertPartialReconcile(res['partials'], [{
+ 'amount': 240.0,
+ 'debit_amount_currency': 1680.0,
+ 'credit_amount_currency': 1680.0,
+ 'debit_move_id': line_1.id,
+ 'credit_move_id': line_5.id,
+ }])
+
+ self.assertRecordValues(line_1 + line_5, [
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ {'amount_residual': -780.0, 'amount_residual_currency': -360.0, 'reconciled': False},
+ ])
+
+ res = (line_2 + line_5).reconcile()
+
+ exchange_diff = res['full_reconcile'].exchange_move_id
+ exchange_diff_lines = exchange_diff.line_ids.sorted(lambda line: (line.currency_id, abs(line.amount_currency), -line.amount_currency))
+
+ self.assertRecordValues(exchange_diff_lines, [
+ # Fix line_2:
+ {
+ 'debit': 660.0,
+ 'credit': 0.0,
+ 'amount_currency': 0.0,
+ 'currency_id': currency_id,
+ 'account_id': account_id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 660.0,
+ 'amount_currency': 0.0,
+ 'currency_id': currency_id,
+ 'account_id': exchange_diff.journal_id.default_credit_account_id.id,
+ },
+ ])
+
+ self.assertPartialReconcile(res['partials'], [
+ # Partial generated when reconciling line_2 & line_5:
+ {
+ 'amount': 120.0,
+ 'debit_amount_currency': 360.0,
+ 'credit_amount_currency': 360.0,
+ 'debit_move_id': line_2.id,
+ 'credit_move_id': line_5.id,
+ },
+ # Partial fixing line_4 (exchange difference):
+ {
+ 'amount': 660.0,
+ 'debit_amount_currency': 0.0,
+ 'credit_amount_currency': 0.0,
+ 'debit_move_id': exchange_diff_lines[0].id,
+ 'credit_move_id': line_5.id,
+ },
+ ])
+
+ self.assertFullReconcile(res['full_reconcile'], line_1 + line_2 + line_3 + line_4 + line_5)
+
+ def test_reconcile_multiple_currencies(self):
+ account_id = self.company_data['default_account_receivable'].id
+
+ # Rate is 3.0 in 2016, 2.0 in 2017.
+ currency1_id = self.currency_data['currency'].id
+ # Rate is 6.0 in 2016, 4.0 in 2017.
+ currency2_id = self.currency_data_2['currency'].id
+
+ moves = self.env['account.move'].create([
+ {
+ 'move_type': 'entry',
+ 'date': '2016-01-01',
+ 'line_ids': [
+ (0, 0, {'debit': 1200.0, 'credit': 0.0, 'amount_currency': 3600.0, 'account_id': account_id, 'currency_id': currency1_id}),
+ (0, 0, {'debit': 780.0, 'credit': 0.0, 'amount_currency': 2340.0, 'account_id': account_id, 'currency_id': currency1_id}),
+
+ (0, 0, {'debit': 0.0, 'credit': 1980.0, 'account_id': account_id}),
+ ]
+ },
+ {
+ 'move_type': 'entry',
+ 'date': '2017-01-01',
+ 'line_ids': [
+ (0, 0, {'debit': 0.0, 'credit': 240.0, 'amount_currency': -960.0, 'account_id': account_id, 'currency_id': currency2_id}),
+ (0, 0, {'debit': 0.0, 'credit': 720.0, 'amount_currency': -2880.0, 'account_id': account_id, 'currency_id': currency2_id}),
+ (0, 0, {'debit': 0.0, 'credit': 1020.0, 'amount_currency': -4080.0, 'account_id': account_id, 'currency_id': currency2_id}),
+
+ (0, 0, {'debit': 1980.0, 'credit': 0.0, 'account_id': account_id}),
+ ]
+ }
+ ])
+
+ moves.post()
+
+ line_1 = moves.line_ids.filtered(lambda line: line.debit == 1200.0)
+ line_2 = moves.line_ids.filtered(lambda line: line.debit == 780.0)
+ line_3 = moves.line_ids.filtered(lambda line: line.credit == 240.0)
+ line_4 = moves.line_ids.filtered(lambda line: line.credit == 720.0)
+ line_5 = moves.line_ids.filtered(lambda line: line.credit == 1020.0)
+
+ self.assertRecordValues(line_1 + line_2 + line_3 + line_4 + line_5, [
+ {'amount_residual': 1200.0, 'amount_residual_currency': 3600.0, 'reconciled': False},
+ {'amount_residual': 780.0, 'amount_residual_currency': 2340.0, 'reconciled': False},
+ {'amount_residual': -240.0, 'amount_residual_currency': -960.0, 'reconciled': False},
+ {'amount_residual': -720.0, 'amount_residual_currency': -2880.0, 'reconciled': False},
+ {'amount_residual': -1020.0, 'amount_residual_currency': -4080.0, 'reconciled': False},
+ ])
+
+ res = (line_1 + line_3 + line_4).reconcile()
+
+ self.assertPartialReconcile(res['partials'], [
+ # Partial generated when reconciling line_1 & line_3:
+ {
+ 'amount': 240.0,
+ 'debit_amount_currency': 480.0,
+ 'credit_amount_currency': 1440.0,
+ 'debit_move_id': line_1.id,
+ 'credit_move_id': line_3.id,
+ },
+ # Partial generated when reconciling line_1 & line_4:
+ {
+ 'amount': 720.0,
+ 'debit_amount_currency': 1440.0,
+ 'credit_amount_currency': 4320.0,
+ 'debit_move_id': line_1.id,
+ 'credit_move_id': line_4.id,
+ },
+ ])
+
+ self.assertRecordValues(line_1 + line_3 + line_4, [
+ {'amount_residual': 240.0, 'amount_residual_currency': 1680.0, 'reconciled': False},
+ {'amount_residual': 0.0, 'amount_residual_currency': 480.0, 'reconciled': False},
+ {'amount_residual': 0.0, 'amount_residual_currency': 1440.0, 'reconciled': False},
+ ])
+
+ res = (line_1 + line_5).reconcile()
+
+ self.assertPartialReconcile(res['partials'], [{
+ 'amount': 240.0,
+ 'debit_amount_currency': 480.0,
+ 'credit_amount_currency': 1440.0,
+ 'debit_move_id': line_1.id,
+ 'credit_move_id': line_5.id,
+ }])
+
+ self.assertRecordValues(line_1 + line_5, [
+ {'amount_residual': 0.0, 'amount_residual_currency': 1200.0, 'reconciled': False},
+ {'amount_residual': -780.0, 'amount_residual_currency': -2640.0, 'reconciled': False},
+ ])
+
+ res = (line_2 + line_5).reconcile()
+
+ exchange_diff = res['full_reconcile'].exchange_move_id
+ exchange_diff_lines = exchange_diff.line_ids.sorted(lambda line: (line.currency_id, abs(line.amount_currency), -line.amount_currency))
+
+ self.assertRecordValues(exchange_diff_lines, [
+ # Fix line_2:
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': 780.0,
+ 'currency_id': currency1_id,
+ 'account_id': exchange_diff.journal_id.default_debit_account_id.id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': -780.0,
+ 'currency_id': currency1_id,
+ 'account_id': account_id,
+ },
+ # Fix line_3:
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': 480.0,
+ 'currency_id': currency2_id,
+ 'account_id': exchange_diff.journal_id.default_debit_account_id.id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': -480.0,
+ 'currency_id': currency2_id,
+ 'account_id': account_id,
+ },
+ # Fix line_4:
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': 1440.0,
+ 'currency_id': currency2_id,
+ 'account_id': exchange_diff.journal_id.default_debit_account_id.id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': -1440.0,
+ 'currency_id': currency2_id,
+ 'account_id': account_id,
+ },
+ # Fix line_5:
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': 2040.0,
+ 'currency_id': currency2_id,
+ 'account_id': exchange_diff.journal_id.default_debit_account_id.id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': -2040.0,
+ 'currency_id': currency2_id,
+ 'account_id': account_id,
+ },
+ # Fix line_1:
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': 1200.0,
+ 'currency_id': currency1_id,
+ 'account_id': exchange_diff.journal_id.default_debit_account_id.id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': -1200.0,
+ 'currency_id': currency1_id,
+ 'account_id': account_id,
+ },
+ ])
+
+ self.assertPartialReconcile(res['partials'], [
+ # Partial fixing line_3 (exchange difference):
+ {
+ 'amount': 0.0,
+ 'debit_amount_currency': 480.0,
+ 'credit_amount_currency': 480.0,
+ 'debit_move_id': line_3.id,
+ 'credit_move_id': exchange_diff_lines[3].id,
+ },
+ # Partial fixing line_2 (exchange difference):
+ {
+ 'amount': 0.0,
+ 'debit_amount_currency': 780.0,
+ 'credit_amount_currency': 780.0,
+ 'debit_move_id': line_2.id,
+ 'credit_move_id': exchange_diff_lines[1].id,
+ },
+ # Partial fixing line_1 (exchange difference):
+ {
+ 'amount': 0.0,
+ 'debit_amount_currency': 1200.0,
+ 'credit_amount_currency': 1200.0,
+ 'debit_move_id': line_1.id,
+ 'credit_move_id': exchange_diff_lines[9].id,
+ },
+ # Partial fixing line_4 (exchange difference):
+ {
+ 'amount': 0.0,
+ 'debit_amount_currency': 1440.0,
+ 'credit_amount_currency': 1440.0,
+ 'debit_move_id': line_4.id,
+ 'credit_move_id': exchange_diff_lines[5].id,
+ },
+ # Partial fixing line_5 (exchange difference):
+ {
+ 'amount': 0.0,
+ 'debit_amount_currency': 2040.0,
+ 'credit_amount_currency': 2040.0,
+ 'debit_move_id': line_5.id,
+ 'credit_move_id': exchange_diff_lines[7].id,
+ },
+ # Partial generated when reconciling line_2 & line_5:
+ {
+ 'amount': 780.0,
+ 'debit_amount_currency': 1560.0,
+ 'credit_amount_currency': 4680.0,
+ 'debit_move_id': line_2.id,
+ 'credit_move_id': line_5.id,
+ },
+ ])
+
+ self.assertFullReconcile(res['full_reconcile'], line_1 + line_2 + line_3 + line_4 + line_5)
+
+ # -------------------------------------------------------------------------
+ # Test creation of extra journal entries during the reconciliation to
+ # deal with taxes that are exigible on payment (cash basis).
+ # -------------------------------------------------------------------------
+
+ def test_reconcile_cash_basis_workflow_single_currency(self):
+ ''' Test the generated journal entries during the reconciliation to manage the cash basis taxes.
+ Also,
+ - Test the case when there is multiple receivable/payable accounts.
+ - Test the reconciliation with tiny amounts.
+ - Check there is no rounding issue when making the percentage.
+ - Check there is no lost cents when the journal entry is fully reconciled.
+ '''
+ cash_basis_move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2016-01-01',
+ 'line_ids': [
+ # Base Tax line
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 100.0,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'tax_ids': [(6, 0, (self.cash_basis_tax_a_third_amount + self.cash_basis_tax_tiny_amount).ids)],
+ 'tax_exigible': False,
+ }),
+
+ # Tax lines
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 33.33,
+ 'account_id': self.cash_basis_transfer_account.id,
+ 'tax_repartition_line_id': self.cash_basis_tax_a_third_amount.invoice_repartition_line_ids.filtered(lambda line: line.repartition_type == 'tax').id,
+ 'tax_exigible': False,
+ }),
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 0.01,
+ 'account_id': self.cash_basis_transfer_account.id,
+ 'tax_repartition_line_id': self.cash_basis_tax_tiny_amount.invoice_repartition_line_ids.filtered(lambda line: line.repartition_type == 'tax').id,
+ 'tax_exigible': False,
+ }),
+
+ # Receivable lines
+ (0, 0, {
+ 'debit': 44.45,
+ 'credit': 0.0,
+ 'account_id': self.extra_receivable_account_1.id,
+ }),
+ (0, 0, {
+ 'debit': 44.45,
+ 'credit': 0.0,
+ 'account_id': self.extra_receivable_account_2.id,
+ }),
+ (0, 0, {
+ 'debit': 44.45,
+ 'credit': 0.0,
+ 'account_id': self.extra_receivable_account_2.id,
+ }),
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 0.01,
+ 'account_id': self.extra_payable_account_1.id,
+ }),
+ ]
+ })
+
+ payment_move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2017-01-01',
+ 'line_ids': [
+ (0, 0, {'debit': 0.0, 'credit': 33.34, 'account_id': self.extra_receivable_account_1.id}),
+ (0, 0, {'debit': 0.0, 'credit': 11.11, 'account_id': self.extra_receivable_account_1.id}),
+ (0, 0, {'debit': 0.0, 'credit': 88.89, 'account_id': self.extra_receivable_account_2.id}),
+ (0, 0, {'debit': 0.0, 'credit': 0.01, 'account_id': self.extra_receivable_account_2.id}),
+ (0, 0, {'debit': 0.01, 'credit': 0.0, 'account_id': self.extra_payable_account_1.id}),
+ (0, 0, {'debit': 133.34, 'credit': 0.0, 'account_id': self.company_data['default_account_revenue'].id}),
+ ]
+ })
+
+ (cash_basis_move + payment_move).post()
+
+ # Initial amounts by accounts:
+
+ self.assertAmountsGroupByAccount([
+ # Account Balance Amount Currency
+ (self.cash_basis_transfer_account, -33.34, 0.0),
+ (self.tax_account_1, 0.0, 0.0),
+ (self.tax_account_2, 0.0, 0.0),
+ ])
+
+ # There is 44.45 + 44.45 + 44.45 + 0.01 = 133.36 to reconcile on 'cash_basis_move'.
+ # Reconciling all the amount in extra_receivable_account_1 should compute 2 percentages:
+ # 33.34 / 133.36 = 0.25
+ # 11.11 / 133.36 = 0.083308338
+
+ receivable_lines_1 = (cash_basis_move + payment_move).line_ids\
+ .filtered(lambda line: line.account_id == self.extra_receivable_account_1)
+ res = receivable_lines_1.reconcile()
+
+ self.assertFullReconcile(res['full_reconcile'], receivable_lines_1)
+ self.assertEqual(len(res.get('tax_cash_basis_moves', [])), 2)
+ self.assertRecordValues(res['tax_cash_basis_moves'][0].line_ids, [
+ # Base amount of tax_1 & tax_2:
+ {'debit': 25.0, 'credit': 0.0, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 25.0, 'account_id': self.cash_basis_base_account.id},
+ # tax_1:
+ {'debit': 8.33, 'credit': 0.0, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 8.33, 'account_id': self.tax_account_1.id},
+ # tax_2:
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.tax_account_2.id},
+ ])
+ self.assertRecordValues(res['tax_cash_basis_moves'][1].line_ids, [
+ # Base amount of tax_1 & tax_2:
+ {'debit': 8.33, 'credit': 0.0, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 8.33, 'account_id': self.cash_basis_base_account.id},
+ # tax_1:
+ {'debit': 2.78, 'credit': 0.0, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 2.78, 'account_id': self.tax_account_1.id},
+ # tax_2:
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.tax_account_2.id},
+ ])
+
+ self.assertAmountsGroupByAccount([
+ # Account Balance Amount Currency
+ (self.cash_basis_transfer_account, -22.23, 0.0),
+ (self.tax_account_1, -11.11, 0.0),
+ (self.tax_account_2, 0.0, 0.0),
+ ])
+
+ # Reconciling all the amount in extra_receivable_account_2 should compute 3 percentages:
+ # 44.45 / 133.36 = 0.333308338
+ # 44.44 / 133.36 = 0.333233353
+ # 0.01 / 133.36 = 0.000074985
+
+ receivable_lines_2 = (cash_basis_move + payment_move).line_ids\
+ .filtered(lambda line: line.account_id == self.extra_receivable_account_2)
+ res = receivable_lines_2.reconcile()
+
+ self.assertFullReconcile(res['full_reconcile'], receivable_lines_2)
+ self.assertEqual(len(res.get('tax_cash_basis_moves', [])), 3)
+ self.assertRecordValues(res['tax_cash_basis_moves'][0].line_ids, [
+ # Base amount of tax_1 & tax_2:
+ {'debit': 33.33, 'credit': 0.0, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 33.33, 'account_id': self.cash_basis_base_account.id},
+ # tax_1:
+ {'debit': 11.11, 'credit': 0.0, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 11.11, 'account_id': self.tax_account_1.id},
+ # tax_2:
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.tax_account_2.id},
+ ])
+ self.assertRecordValues(res['tax_cash_basis_moves'][1].line_ids, [
+ # Base amount of tax_1 & tax_2:
+ {'debit': 33.32, 'credit': 0.0, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 33.32, 'account_id': self.cash_basis_base_account.id},
+ # tax_1:
+ {'debit': 11.11, 'credit': 0.0, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 11.11, 'account_id': self.tax_account_1.id},
+ # tax_2:
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.tax_account_2.id},
+ ])
+ self.assertRecordValues(res['tax_cash_basis_moves'][2].line_ids, [
+ # Base amount of tax_1 & tax_2:
+ {'debit': 0.01, 'credit': 0.0, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 0.01, 'account_id': self.cash_basis_base_account.id},
+ # tax_1:
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.tax_account_1.id},
+ # tax_2:
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.tax_account_2.id},
+ ])
+
+ self.assertAmountsGroupByAccount([
+ # Account Balance Amount Currency
+ (self.cash_basis_transfer_account, -0.01, 0.0),
+ (self.tax_account_1, -33.33, 0.0),
+ (self.tax_account_2, 0.0, 0.0),
+ ])
+
+ # Reconciling all the amount in extra_payable_account_1 should trigger the matching number and ensure all
+ # the base amount has been covered without any rounding issue.
+
+ payable_lines_1 = (cash_basis_move + payment_move).line_ids\
+ .filtered(lambda line: line.account_id == self.extra_payable_account_1)
+ res = payable_lines_1.reconcile()
+
+ self.assertFullReconcile(res['full_reconcile'], payable_lines_1)
+ self.assertEqual(len(res.get('tax_cash_basis_moves', [])), 1)
+ self.assertRecordValues(res['tax_cash_basis_moves'].line_ids, [
+ # Base amount of tax_1 & tax_2:
+ {'debit': 0.01, 'credit': 0.0, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 0.01, 'account_id': self.cash_basis_base_account.id},
+ # tax_1:
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'account_id': self.tax_account_1.id},
+ # tax_2:
+ {'debit': 0.01, 'credit': 0.0, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.01, 'account_id': self.tax_account_2.id},
+ ])
+
+ self.assertAmountsGroupByAccount([
+ # Account Balance Amount Currency
+ (self.cash_basis_transfer_account, 0.0, 0.0),
+ (self.tax_account_1, -33.33, 0.0),
+ (self.tax_account_2, -0.01, 0.0),
+ ])
+
+ def test_reconcile_cash_basis_workflow_multi_currency(self):
+ ''' Same as before with a foreign currency. '''
+
+ currency_id = self.currency_data['currency'].id
+
+ cash_basis_move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2016-01-01',
+ 'line_ids': [
+ # Base Tax line
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 33.34,
+ 'amount_currency': -100.0,
+ 'currency_id': currency_id,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'tax_ids': [(6, 0, (self.cash_basis_tax_a_third_amount + self.cash_basis_tax_tiny_amount).ids)],
+ 'tax_exigible': False,
+ }),
+
+ # Tax lines
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 11.10,
+ 'amount_currency': -33.33,
+ 'currency_id': currency_id,
+ 'account_id': self.cash_basis_transfer_account.id,
+ 'tax_repartition_line_id': self.cash_basis_tax_a_third_amount.invoice_repartition_line_ids.filtered(lambda line: line.repartition_type == 'tax').id,
+ 'tax_exigible': False,
+ }),
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 0.01,
+ 'amount_currency': -0.01,
+ 'currency_id': currency_id,
+ 'account_id': self.cash_basis_transfer_account.id,
+ 'tax_repartition_line_id': self.cash_basis_tax_tiny_amount.invoice_repartition_line_ids.filtered(lambda line: line.repartition_type == 'tax').id,
+ 'tax_exigible': False,
+ }),
+
+ # Receivable lines
+ (0, 0, {
+ 'debit': 14.82,
+ 'credit': 0.0,
+ 'amount_currency': 44.45,
+ 'currency_id': currency_id,
+ 'account_id': self.extra_receivable_account_1.id,
+ }),
+ (0, 0, {
+ 'debit': 14.82,
+ 'credit': 0.0,
+ 'amount_currency': 44.45,
+ 'currency_id': currency_id,
+ 'account_id': self.extra_receivable_account_2.id,
+ }),
+ (0, 0, {
+ 'debit': 14.82,
+ 'credit': 0.0,
+ 'amount_currency': 44.45,
+ 'currency_id': currency_id,
+ 'account_id': self.extra_receivable_account_2.id,
+ }),
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 0.01,
+ 'amount_currency': -0.01,
+ 'currency_id': currency_id,
+ 'account_id': self.extra_payable_account_1.id,
+ }),
+ ]
+ })
+
+ payment_move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2017-01-01',
+ 'line_ids': [
+ (0, 0, {'debit': 0.0, 'credit': 16.67, 'amount_currency': -33.34, 'currency_id': currency_id, 'account_id': self.extra_receivable_account_1.id}),
+ (0, 0, {'debit': 0.0, 'credit': 5.6, 'amount_currency': -11.11, 'currency_id': currency_id, 'account_id': self.extra_receivable_account_1.id}),
+ (0, 0, {'debit': 0.0, 'credit': 44.45, 'amount_currency': -88.89, 'currency_id': currency_id, 'account_id': self.extra_receivable_account_2.id}),
+ (0, 0, {'debit': 0.0, 'credit': 0.01, 'amount_currency': -0.01, 'currency_id': currency_id, 'account_id': self.extra_receivable_account_2.id}),
+ (0, 0, {'debit': 0.01, 'credit': 0.0, 'amount_currency': 0.01, 'currency_id': currency_id, 'account_id': self.extra_payable_account_1.id}),
+ (0, 0, {'debit': 66.72, 'credit': 0.0, 'account_id': self.company_data['default_account_revenue'].id}),
+ ]
+ })
+
+ (cash_basis_move + payment_move).post()
+
+ # Initial amounts by accounts:
+
+ self.assertAmountsGroupByAccount([
+ # Account Balance Amount Currency
+ (self.cash_basis_transfer_account, -11.11, -33.34),
+ (self.tax_account_1, 0.0, 0.0),
+ (self.tax_account_2, 0.0, 0.0),
+ ])
+
+ # There is 44.45 + 44.45 + 44.45 + 0.01 = 133.36 to reconcile on 'cash_basis_move'.
+ # Reconciling all the amount in extra_receivable_account_1 should compute 2 percentages:
+ # 33.34 / 133.36 = 0.25
+ # 11.11 / 133.36 = 0.083308338
+
+ receivable_lines_1 = (cash_basis_move + payment_move).line_ids\
+ .filtered(lambda line: line.account_id == self.extra_receivable_account_1)
+ res = receivable_lines_1.reconcile()
+
+ self.assertFullReconcile(res['full_reconcile'], receivable_lines_1)
+ self.assertEqual(len(res.get('tax_cash_basis_moves', [])), 2)
+ self.assertRecordValues(res['tax_cash_basis_moves'][0].line_ids, [
+ # Base amount of tax_1 & tax_2:
+ {'debit': 12.5, 'credit': 0.0, 'amount_currency': 25.0, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 12.5, 'amount_currency': -25.0, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ # tax_1:
+ {'debit': 4.17, 'credit': 0.0, 'amount_currency': 8.333, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 4.17, 'amount_currency': -8.333, 'currency_id': currency_id, 'account_id': self.tax_account_1.id},
+ # tax_2:
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': 0.003, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': -0.003, 'currency_id': currency_id, 'account_id': self.tax_account_2.id},
+ ])
+ self.assertRecordValues(res['tax_cash_basis_moves'][1].line_ids, [
+ # Base amount of tax_1 & tax_2:
+ {'debit': 4.2, 'credit': 0.0, 'amount_currency': 8.331, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 4.2, 'amount_currency': -8.331, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ # tax_1:
+ {'debit': 1.4, 'credit': 0.0, 'amount_currency': 2.777, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 1.4, 'amount_currency': -2.777, 'currency_id': currency_id, 'account_id': self.tax_account_1.id},
+ # tax_2:
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': 0.001, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': -0.001, 'currency_id': currency_id, 'account_id': self.tax_account_2.id},
+ ])
+
+ self.assertAmountsGroupByAccount([
+ # Account Balance Amount Currency
+ (self.cash_basis_transfer_account, -5.54, -22.226),
+ (self.tax_account_1, -5.57, -11.11),
+ (self.tax_account_2, 0.0, -0.004),
+ ])
+
+ # Reconciling all the amount in extra_receivable_account_2 should compute 3 percentages:
+ # 44.45 / 133.36 = 0.333308338
+ # 44.44 / 133.36 = 0.333233353
+ # 0.01 / 133.36 = 0.000074985
+
+ receivable_lines_2 = (cash_basis_move + payment_move).line_ids\
+ .filtered(lambda line: line.account_id == self.extra_receivable_account_2)
+ res = receivable_lines_2.reconcile()
+
+ self.assertFullReconcile(res['full_reconcile'], receivable_lines_2)
+ self.assertEqual(len(res.get('tax_cash_basis_moves', [])), 3)
+ self.assertRecordValues(res['tax_cash_basis_moves'][0].line_ids, [
+ # Base amount of tax_1 & tax_2:
+ {'debit': 16.67, 'credit': 0.0, 'amount_currency': 33.331, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 16.67, 'amount_currency': -33.331, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ # tax_1:
+ {'debit': 5.56, 'credit': 0.0, 'amount_currency': 11.109, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 5.56, 'amount_currency': -11.109, 'currency_id': currency_id, 'account_id': self.tax_account_1.id},
+ # tax_2:
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': 0.003, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': -0.003, 'currency_id': currency_id, 'account_id': self.tax_account_2.id},
+ ])
+ self.assertRecordValues(res['tax_cash_basis_moves'][1].line_ids, [
+ # Base amount of tax_1 & tax_2:
+ {'debit': 16.66, 'credit': 0.0, 'amount_currency': 33.323, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 16.66, 'amount_currency': -33.323, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ # tax_1:
+ {'debit': 5.55, 'credit': 0.0, 'amount_currency': 11.107, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 5.55, 'amount_currency': -11.107, 'currency_id': currency_id, 'account_id': self.tax_account_1.id},
+ # tax_2:
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': 0.003, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': -0.003, 'currency_id': currency_id, 'account_id': self.tax_account_2.id},
+ ])
+ self.assertRecordValues(res['tax_cash_basis_moves'][2].line_ids, [
+ # Base amount of tax_1 & tax_2:
+ {'debit': 0.01, 'credit': 0.0, 'amount_currency': 0.007, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 0.01, 'amount_currency': -0.007, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ # tax_1:
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': 0.002, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': -0.002, 'currency_id': currency_id, 'account_id': self.tax_account_1.id},
+ # tax_2:
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': currency_id, 'account_id': self.tax_account_2.id},
+ ])
+
+ self.assertAmountsGroupByAccount([
+ # Account Balance Amount Currency
+ (self.cash_basis_transfer_account, 5.57, -0.002),
+ (self.tax_account_1, -16.68, -33.328),
+ (self.tax_account_2, 0.0, -0.01),
+ ])
+
+ # Reconciling all the amount in extra_payable_account_1 should trigger the matching number and ensure all
+ # the base amount has been covered without any rounding issue.
+
+ payable_lines_1 = (cash_basis_move + payment_move).line_ids\
+ .filtered(lambda line: line.account_id == self.extra_payable_account_1)
+ res = payable_lines_1.reconcile()
+
+ self.assertFullReconcile(res['full_reconcile'], payable_lines_1)
+ self.assertEqual(len(res.get('tax_cash_basis_moves', [])), 1)
+ self.assertRecordValues(res['tax_cash_basis_moves'].line_ids, [
+ # Base amount of tax_1 & tax_2:
+ {'debit': 0.01, 'credit': 0.0, 'amount_currency': 0.008, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 0.01, 'amount_currency': -0.008, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ # tax_1:
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': 0.002, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': -0.002, 'currency_id': currency_id, 'account_id': self.tax_account_1.id},
+ # tax_2:
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': currency_id, 'account_id': self.tax_account_2.id},
+ ])
+
+ self.assertAmountsGroupByAccount([
+ # Account Balance Amount Currency
+ (self.cash_basis_transfer_account, 0.0, 0.0),
+ (self.tax_account_1, -16.68, -33.33),
+ (self.tax_account_2, 0.0, -0.01),
+ ])
+
+ def test_reconcile_cash_basis_exchange_difference_transfer_account_not_reconcile(self):
+ ''' Test the generation of the exchange difference for a tax cash basis journal entry when the transfer
+ account is not a reconcile one.
+ '''
+ self.cash_basis_transfer_account.reconcile = False
+
+ currency_id = self.currency_data['currency'].id
+
+ # Rate 1/3 in 2016.
+ cash_basis_move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2016-01-01',
+ 'line_ids': [
+ # Base Tax line
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 100.0,
+ 'amount_currency': -300.0,
+ 'currency_id': currency_id,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'tax_ids': [(6, 0, self.cash_basis_tax_a_third_amount.ids)],
+ 'tax_exigible': False,
+ }),
+
+ # Tax line
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 33.33,
+ 'amount_currency': -100.0,
+ 'currency_id': currency_id,
+ 'account_id': self.cash_basis_transfer_account.id,
+ 'tax_repartition_line_id': self.cash_basis_tax_a_third_amount.invoice_repartition_line_ids.filtered(lambda line: line.repartition_type == 'tax').id,
+ 'tax_exigible': False,
+ }),
+
+ # Receivable lines
+ (0, 0, {
+ 'debit': 133.33,
+ 'credit': 0.0,
+ 'amount_currency': 400.0,
+ 'currency_id': currency_id,
+ 'account_id': self.extra_receivable_account_1.id,
+ }),
+ ]
+ })
+
+ # Rate 1/2 in 2017.
+ payment_move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2017-01-01',
+ 'line_ids': [
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 201.0,
+ 'amount_currency': -402.0, # Don't create the full reconcile directly.
+ 'currency_id': currency_id,
+ 'account_id': self.extra_receivable_account_1.id,
+ }),
+ (0, 0, {
+ 'debit': 201.0,
+ 'credit': 0.0,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ }),
+ ]
+ })
+
+ # Move making the payment fully paid.
+ end_move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2017-01-01',
+ 'line_ids': [
+ (0, 0, {
+ 'debit': 1.0,
+ 'credit': 0.0,
+ 'amount_currency': 2.0,
+ 'currency_id': currency_id,
+ 'account_id': self.extra_receivable_account_1.id,
+ }),
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 1.0,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ }),
+ ]
+ })
+
+ (cash_basis_move + payment_move + end_move).post()
+
+ self.assertAmountsGroupByAccount([
+ # Account Balance Amount Currency
+ (self.cash_basis_transfer_account, -33.33, -100.0),
+ (self.tax_account_1, 0.0, 0.0),
+ ])
+
+ receivable_lines = (cash_basis_move + payment_move).line_ids\
+ .filtered(lambda line: line.account_id == self.extra_receivable_account_1)
+ res = receivable_lines.reconcile()
+
+ self.assertEqual(len(res.get('tax_cash_basis_moves', [])), 1)
+ self.assertRecordValues(res['tax_cash_basis_moves'].line_ids, [
+ # Base amount:
+ {'debit': 150.0, 'credit': 0.0, 'amount_currency': 300.0, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 150.0, 'amount_currency': -300.0, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ # tax:
+ {'debit': 50.0, 'credit': 0.0, 'amount_currency': 100.0, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 50.0, 'amount_currency': -100.0, 'currency_id': currency_id, 'account_id': self.tax_account_1.id},
+ ])
+
+ receivable_lines2 = (payment_move + end_move).line_ids\
+ .filtered(lambda line: line.account_id == self.extra_receivable_account_1)
+ res = receivable_lines2.reconcile()
+
+ self.assertFullReconcile(res['full_reconcile'], receivable_lines + receivable_lines2)
+
+ exchange_diff = res['full_reconcile'].exchange_move_id
+ exchange_diff_lines = exchange_diff.line_ids\
+ .filtered(lambda line: line.account_id == self.cash_basis_transfer_account)\
+ .sorted(lambda line: (line.account_id, line.debit, line.credit))
+
+ self.assertRecordValues(exchange_diff_lines, [
+ {
+ 'debit': 0.0,
+ 'credit': 16.67,
+ 'amount_currency': 0.0,
+ 'currency_id': currency_id,
+ 'account_id': self.cash_basis_transfer_account.id,
+ },
+ ])
+
+ self.assertAmountsGroupByAccount([
+ # Account Balance Amount Currency
+ (self.cash_basis_transfer_account, 0.0, 0.0),
+ (self.tax_account_1, -50.0, -100.0),
+ ])
+
+ def test_reconcile_cash_basis_exchange_difference_transfer_account_reconcile(self):
+ ''' Test the generation of the exchange difference for a tax cash basis journal entry when the transfer
+ account is a reconcile one.
+ '''
+ self.cash_basis_transfer_account.reconcile = True
+
+ currency_id = self.currency_data['currency'].id
+
+ # Rate 1/3 in 2016.
+ cash_basis_move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2016-01-01',
+ 'line_ids': [
+ # Base Tax line
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 100.0,
+ 'amount_currency': -300.0,
+ 'currency_id': currency_id,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'tax_ids': [(6, 0, self.cash_basis_tax_a_third_amount.ids)],
+ 'tax_exigible': False,
+ }),
+
+ # Tax line
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 33.33,
+ 'amount_currency': -100.0,
+ 'currency_id': currency_id,
+ 'account_id': self.cash_basis_transfer_account.id,
+ 'tax_repartition_line_id': self.cash_basis_tax_a_third_amount.invoice_repartition_line_ids.filtered(lambda line: line.repartition_type == 'tax').id,
+ 'tax_exigible': False,
+ }),
+
+ # Receivable lines
+ (0, 0, {
+ 'debit': 133.33,
+ 'credit': 0.0,
+ 'amount_currency': 400.0,
+ 'currency_id': currency_id,
+ 'account_id': self.extra_receivable_account_1.id,
+ }),
+ ]
+ })
+
+ # Rate 1/2 in 2017.
+ payment_move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2017-01-01',
+ 'line_ids': [
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 200.0,
+ 'amount_currency': -400.0,
+ 'currency_id': currency_id,
+ 'account_id': self.extra_receivable_account_1.id,
+ }),
+ (0, 0, {
+ 'debit': 200.0,
+ 'credit': 0.0,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ }),
+ ]
+ })
+
+ (cash_basis_move + payment_move).post()
+
+ self.assertAmountsGroupByAccount([
+ # Account Balance Amount Currency
+ (self.cash_basis_transfer_account, -33.33, -100.0),
+ (self.tax_account_1, 0.0, 0.0),
+ ])
+
+ receivable_lines = (cash_basis_move + payment_move).line_ids\
+ .filtered(lambda line: line.account_id == self.extra_receivable_account_1)
+ res = receivable_lines.reconcile()
+
+ self.assertFullReconcile(res['full_reconcile'], receivable_lines)
+ self.assertEqual(len(res.get('tax_cash_basis_moves', [])), 1)
+ self.assertRecordValues(res['tax_cash_basis_moves'].line_ids, [
+ # Base amount:
+ {'debit': 150.0, 'credit': 0.0, 'amount_currency': 300.0, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 150.0, 'amount_currency': -300.0, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
+ # tax:
+ {'debit': 50.0, 'credit': 0.0, 'amount_currency': 100.0, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 50.0, 'amount_currency': -100.0, 'currency_id': currency_id, 'account_id': self.tax_account_1.id},
+ ])
+
+ transfer_lines = (cash_basis_move + res['tax_cash_basis_moves']).line_ids\
+ .filtered(lambda line: line.account_id == self.cash_basis_transfer_account)
+ self.assertTrue(transfer_lines.full_reconcile_id)
+ self.assertFullReconcile(transfer_lines.full_reconcile_id, transfer_lines)
+
+ transfer_exchange_diff = transfer_lines.full_reconcile_id.exchange_move_id
+ transfer_exchange_diff_lines = transfer_exchange_diff.line_ids.sorted(lambda line: (line.account_id, -line.balance))
+
+ self.assertRecordValues(transfer_exchange_diff_lines, [
+ {
+ 'debit': 0.0,
+ 'credit': 16.67,
+ 'amount_currency': 0.0,
+ 'currency_id': currency_id,
+ 'account_id': self.cash_basis_transfer_account.id,
+ },
+ {
+ 'debit': 16.67,
+ 'credit': 0.0,
+ 'amount_currency': 0.0,
+ 'currency_id': currency_id,
+ 'account_id': transfer_exchange_diff.journal_id.default_debit_account_id.id,
+ },
+ ])
+
+ self.assertAmountsGroupByAccount([
+ # Account Balance Amount Currency
+ (self.cash_basis_transfer_account, 0.0, 0.0),
+ (self.tax_account_1, -50.0, -100.0),
+ ])
+
+ def test_reconcile_cash_basis_revert(self):
+ ''' Ensure the cash basis journal entry can be reverted. '''
+ self.cash_basis_transfer_account.reconcile = True
+
+ invoice_move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2016-01-01',
+ 'line_ids': [
+ # Base Tax line
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 100.0,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'tax_ids': [(6, 0, self.cash_basis_tax_a_third_amount.ids)],
+ 'tax_exigible': False,
+ }),
+
+ # Tax line
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 33.33,
+ 'account_id': self.cash_basis_transfer_account.id,
+ 'tax_repartition_line_id': self.cash_basis_tax_a_third_amount.invoice_repartition_line_ids.filtered(lambda line: line.repartition_type == 'tax').id,
+ 'tax_exigible': False,
+ }),
+
+ # Receivable line
+ (0, 0, {
+ 'debit': 133.33,
+ 'credit': 0.0,
+ 'account_id': self.extra_receivable_account_1.id,
+ }),
+ ]
+ })
+
+ payment_move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2016-01-01',
+ 'line_ids': [
+ (0, 0, {'debit': 0.0, 'credit': 133.33, 'account_id': self.extra_receivable_account_1.id}),
+ (0, 0, {'debit': 133.33, 'credit': 0.0, 'account_id': self.company_data['default_account_revenue'].id}),
+ ]
+ })
+
+ (invoice_move + payment_move).post()
+
+ receivable_lines = (invoice_move + payment_move).line_ids\
+ .filtered(lambda line: line.account_id == self.extra_receivable_account_1)
+ res = receivable_lines.reconcile()
+
+ # == Check reconciliation of invoice with payment ==
+
+ self.assertFullReconcile(res['full_reconcile'], receivable_lines)
+ self.assertEqual(len(res.get('tax_cash_basis_moves', [])), 1)
+
+ # == Check the reconciliation of invoice with tax cash basis journal entry.
+ # /!\ We make the assumption the tax cash basis journal entry is well created.
+
+ tax_cash_basis_move = res['tax_cash_basis_moves']
+
+ taxes_lines = (invoice_move.line_ids + tax_cash_basis_move.line_ids.filtered('debit'))\
+ .filtered(lambda line: line.account_id == self.cash_basis_transfer_account)
+ taxes_full_reconcile = taxes_lines.matched_debit_ids.full_reconcile_id
+
+ self.assertTrue(taxes_full_reconcile)
+ self.assertFullReconcile(taxes_full_reconcile, taxes_lines)
+
+ # == Check the reconciliation after the reverse ==
+
+ tax_cash_basis_move_reverse = tax_cash_basis_move._reverse_moves(cancel=True)
+
+ self.assertFullReconcile(res['full_reconcile'], receivable_lines)
+
+ # == Check the reconciliation of the tax cash basis journal entry with its reverse ==
+
+ reversed_taxes_lines = (tax_cash_basis_move + tax_cash_basis_move_reverse).line_ids\
+ .filtered(lambda line: line.account_id == self.cash_basis_transfer_account)
+
+ reversed_taxes_full_reconcile = reversed_taxes_lines.matched_debit_ids.full_reconcile_id
+
+ self.assertTrue(reversed_taxes_full_reconcile)
+ self.assertFullReconcile(reversed_taxes_full_reconcile, reversed_taxes_lines)
+
+ def test_reconcile_cash_basis_tax_grid_refund(self):
+ invoice_move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2016-01-01',
+ 'line_ids': [
+ # Base Tax line
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 100.0,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'tax_ids': [(6, 0, self.cash_basis_tax_a_third_amount.ids)],
+ 'tax_tag_ids': [(6, 0, self.tax_tags[0].ids)],
+ 'tax_exigible': False,
+ }),
+
+ # Tax line
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 33.33,
+ 'account_id': self.cash_basis_transfer_account.id,
+ 'tax_repartition_line_id': self.cash_basis_tax_a_third_amount.invoice_repartition_line_ids.filtered(lambda line: line.repartition_type == 'tax').id,
+ 'tax_tag_ids': [(6, 0, self.tax_tags[1].ids)],
+ 'tax_exigible': False,
+ }),
+
+ # Receivable line
+ (0, 0, {
+ 'debit': 133.33,
+ 'credit': 0.0,
+ 'account_id': self.extra_receivable_account_1.id,
+ }),
+ ]
+ })
+
+ refund_move = self.env['account.move'].create({
+ 'move_type': 'out_refund',
+ 'partner_id': self.partner_a.id,
+ 'invoice_date': '2016-01-01',
+ 'date': '2016-01-01',
+ 'line_ids': [
+ # Base Tax line
+ (0, 0, {
+ 'debit': 100.0,
+ 'credit': 0.0,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'tax_ids': [(6, 0, self.cash_basis_tax_a_third_amount.ids)],
+ 'tax_tag_ids': [(6, 0, self.tax_tags[2].ids)],
+ 'tax_exigible': False,
+ }),
+
+ # Tax line
+ (0, 0, {
+ 'debit': 33.33,
+ 'credit': 0.0,
+ 'account_id': self.cash_basis_transfer_account.id,
+ 'tax_repartition_line_id': self.cash_basis_tax_a_third_amount.invoice_repartition_line_ids.filtered(lambda line: line.repartition_type == 'tax').id,
+ 'tax_tag_ids': [(6, 0, self.tax_tags[3].ids)],
+ 'tax_exigible': False,
+ }),
+
+ # Receivable line
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 133.33,
+ 'account_id': self.extra_receivable_account_1.id,
+ }),
+ ]
+ })
+
+ (invoice_move + refund_move).post()
+
+ receivable_lines = (invoice_move + refund_move).line_ids\
+ .filtered(lambda line: line.account_id == self.extra_receivable_account_1)
+ res = receivable_lines.reconcile()
+
+ self.assertFullReconcile(res['full_reconcile'], receivable_lines)
+ self.assertEqual(len(res.get('tax_cash_basis_moves', [])), 2)
+
+ tax_cash_basis_moves = res['tax_cash_basis_moves'].sorted(lambda move: move.tax_cash_basis_move_id.id)
+
+ # Invoice:
+ cb_lines = tax_cash_basis_moves[0].line_ids.sorted(lambda line: (-abs(line.balance), -line.debit, line.account_id))
+ self.assertRecordValues(cb_lines, [
+ # Base amount:
+ {'debit': 100.0, 'credit': 0.0, 'tax_tag_ids': [], 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 100.0, 'tax_tag_ids': self.tax_tags[0].ids, 'account_id': self.cash_basis_base_account.id},
+ # tax:
+ {'debit': 33.33, 'credit': 0.0, 'tax_tag_ids': [], 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 33.33, 'tax_tag_ids': self.tax_tags[1].ids, 'account_id': self.tax_account_1.id},
+ ])
+
+ # Refund:
+ cb_lines = tax_cash_basis_moves[1].line_ids.sorted(lambda line: (-abs(line.balance), -line.debit, line.account_id))
+ self.assertRecordValues(cb_lines, [
+ # Base amount:
+ {'debit': 100.0, 'credit': 0.0, 'tax_tag_ids': self.tax_tags[2].ids, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 100.0, 'tax_tag_ids': [], 'account_id': self.cash_basis_base_account.id},
+ # tax:
+ {'debit': 33.33, 'credit': 0.0, 'tax_tag_ids': self.tax_tags[3].ids, 'account_id': self.tax_account_1.id},
+ {'debit': 0.0, 'credit': 33.33, 'tax_tag_ids': [], 'account_id': self.cash_basis_transfer_account.id},
+ ])
+
+ self.assertTaxGridAmounts([
+ # Tag Balance
+ (self.tax_tags[0], -100.0),
+ (self.tax_tags[1], -33.33),
+ (self.tax_tags[2], 100.0),
+ (self.tax_tags[3], 33.33),
+ ])
+
+ def test_reconcile_cash_basis_tax_grid_multi_taxes(self):
+ ''' Test the tax grid when reconciling an invoice with multiple taxes/tax repartition. '''
+ base_taxes = self.cash_basis_tax_a_third_amount + self.cash_basis_tax_tiny_amount
+ base_tags = self.tax_tags[0] + self.tax_tags[4]
+
+ # An invoice with 2 taxes:
+ invoice_move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2016-01-01',
+ 'line_ids': [
+ # Base Tax line
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 100.0,
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'tax_ids': [(6, 0, base_taxes.ids)],
+ 'tax_tag_ids': [(6, 0, base_tags.ids)],
+ 'tax_exigible': False,
+ }),
+
+ # Tax lines
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 33.33,
+ 'account_id': self.cash_basis_transfer_account.id,
+ 'tax_repartition_line_id': self.cash_basis_tax_a_third_amount.invoice_repartition_line_ids.filtered(lambda line: line.repartition_type == 'tax').id,
+ 'tax_tag_ids': [(6, 0, self.tax_tags[1].ids)],
+ 'tax_exigible': False,
+ }),
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 0.01,
+ 'account_id': self.cash_basis_transfer_account.id,
+ 'tax_repartition_line_id': self.cash_basis_tax_tiny_amount.invoice_repartition_line_ids.filtered(lambda line: line.repartition_type == 'tax').id,
+ 'tax_tag_ids': [(6, 0, self.tax_tags[5].ids)],
+ 'tax_exigible': False,
+ }),
+
+ # Receivable lines
+ (0, 0, {
+ 'debit': 133.34,
+ 'credit': 0.0,
+ 'account_id': self.extra_receivable_account_1.id,
+ }),
+ ]
+ })
+
+ # A payment paying the full invoice amount.
+ payment_move = self.env['account.move'].create({
+ 'move_type': 'entry',
+ 'date': '2017-01-01',
+ 'line_ids': [
+ (0, 0, {'debit': 0.0, 'credit': 133.34, 'account_id': self.extra_receivable_account_1.id}),
+ (0, 0, {'debit': 133.34, 'credit': 0.0, 'account_id': self.company_data['default_account_revenue'].id}),
+ ]
+ })
+
+ (invoice_move + payment_move).post()
+
+ receivable_lines = (invoice_move + payment_move).line_ids\
+ .filtered(lambda line: line.account_id == self.extra_receivable_account_1)
+ res = receivable_lines.reconcile()
+
+ self.assertFullReconcile(res['full_reconcile'], receivable_lines)
+ self.assertEqual(len(res.get('tax_cash_basis_moves', [])), 1)
+
+ self.assertRecordValues(res['tax_cash_basis_moves'].line_ids, [
+ # Base amount x 2 because there is two taxes:
+ {'debit': 100.0, 'credit': 0.0, 'tax_ids': [], 'tax_tag_ids': [], 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 100.0, 'tax_ids': base_taxes.ids, 'tax_tag_ids': base_tags.ids, 'account_id': self.cash_basis_base_account.id},
+ # tax_1:
+ {'debit': 33.33, 'credit': 0.0, 'tax_ids': [], 'tax_tag_ids': [], 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 33.33, 'tax_ids': [], 'tax_tag_ids': self.tax_tags[1].ids, 'account_id': self.tax_account_1.id},
+ # tax_2:
+ {'debit': 0.01, 'credit': 0.0, 'tax_ids': [], 'tax_tag_ids': [], 'account_id': self.cash_basis_transfer_account.id},
+ {'debit': 0.0, 'credit': 0.01, 'tax_ids': [], 'tax_tag_ids': self.tax_tags[5].ids, 'account_id': self.tax_account_2.id},
+ ])
+
+ self.assertTaxGridAmounts([
+ # Tag Balance
+ (self.tax_tags[0], -100.0),
+ (self.tax_tags[1], -33.33),
+ (self.tax_tags[4], -100.0),
+ (self.tax_tags[5], -0.01),
+ ])
diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py
index 6658704f23a..ec11249216d 100644
--- a/addons/account/tests/test_reconciliation.py
+++ b/addons/account/tests/test_reconciliation.py
@@ -195,23 +195,6 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
self.assertEqual(aml.amount_residual, 0, 'The journal item should be totally reconciled')
self.assertEqual(aml.amount_residual_currency, 0, 'The journal item should be totally reconciled')
- move_list_vals = [
- ('2', 728.35, 795.05, self.currency_swiss_id),
- ('3', -4.46, 0, False),
- ('4', 0.32, 0, False),
- ('5', 14.72, 16.20, self.currency_swiss_id),
- ('6', -737.10, -811.25, self.currency_swiss_id),
- ]
- move_ids = []
- for name, amount, amount_currency, currency_id in move_list_vals:
- move_ids.append(create_move(name, amount, amount_currency, currency_id))
- aml_recs = self.env['account.move.line'].search([('move_id', 'in', move_ids), ('account_id', '=', self.account_rcv.id), ('reconciled', '=', False)])
- aml_recs.reconcile(self.account_rsa, self.bank_journal_usd)
- for aml in aml_recs:
- self.assertTrue(aml.reconciled, 'The journal item should be totally reconciled')
- self.assertEqual(aml.amount_residual, 0, 'The journal item should be totally reconciled')
- self.assertEqual(aml.amount_residual_currency, 0, 'The journal item should be totally reconciled')
-
def test_manual_reconcile_wizard_same_account(self):
move_ids = self.env['account.move']
debit_line_vals = {
@@ -478,7 +461,7 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
self.assertAlmostEqual(inv.amount_residual, 0)
# Unreconcile invoice and check residual
- credit_aml.with_context(invoice_id=inv.id).remove_move_reconcile()
+ credit_aml.remove_move_reconcile()
self.assertAlmostEqual(inv.amount_residual, 111)
def test_revert_payment_and_reconcile(self):
@@ -705,636 +688,6 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
self.assertTrue(receivable_line.matched_debit_ids)
- def test_reconciliation_cash_basis01(self):
- # Simulates an expense made up by 2 lines
- # one is subject to a cash basis tax
- # the other is not subject to tax
-
- company = self.company
- company.tax_cash_basis_journal_id = self.cash_basis_journal
-
- purchase_move = self.env['account.move'].create({
- 'journal_id': self.purchase_journal.id,
- 'line_ids': [
- (0, 0, {'account_id': self.account_rsa.id, 'debit': 0.0, 'credit': 100.0}),
- (0, 0, {'account_id': self.account_rsa.id, 'debit': 0.0, 'credit': 50.0}),
- (0, 0, {'account_id': self.expense_account.id, 'debit': 50.0, 'credit': 0.0}),
- (0, 0, {'account_id': self.expense_account.id, 'debit': 83.33, 'credit': 0.0, 'tax_ids': [(4, self.tax_cash_basis.id)], 'tax_exigible': False}),
- (0, 0, {
- 'account_id': self.tax_waiting_account.id,
- 'debit': 16.67,
- 'credit': 0.0,
- 'tax_repartition_line_id': self.tax_cash_basis.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id,
- 'tax_base_amount': 83.33,
- 'tax_exigible': False
- }),
- ],
- })
- purchase_move.post()
-
- payment_move = self.env['account.move'].create({
- 'journal_id': self.bank_journal_euro.id,
- 'line_ids': [
- (0, 0, {'account_id': self.account_rsa.id, 'debit': 150.0, 'credit': 0.0}),
- (0, 0, {'account_id': self.account_euro.id, 'debit': 0.0, 'credit': 150.0}),
- ],
- })
- payment_move.post()
-
- (purchase_move + payment_move).invalidate_cache(['line_ids'])
- to_reconcile = (purchase_move + payment_move).mapped('line_ids').filtered(lambda l: l.account_id.internal_type == 'payable')
- to_reconcile.reconcile()
-
- cash_basis_moves = self.env['account.move'].search([('journal_id', '=', self.cash_basis_journal.id)])
-
- self.assertEqual(len(cash_basis_moves), 2)
- self.assertTrue(cash_basis_moves.exists())
-
- # check reconciliation in Payable account
- purchase_line_ids = purchase_move.line_ids.sorted()
- payment_line_ids = payment_move.line_ids.sorted()
- self.assertTrue(purchase_line_ids[0].full_reconcile_id.exists())
- self.assertEqual(purchase_line_ids[0].full_reconcile_id.reconciled_line_ids,
- purchase_line_ids[0] + purchase_line_ids[1] + payment_line_ids[0])
-
- cash_basis_aml_ids = cash_basis_moves.mapped('line_ids')
- # check reconciliation in the tax waiting account
- self.assertTrue(purchase_line_ids[4].full_reconcile_id.exists())
- self.assertEqual(purchase_line_ids[4].full_reconcile_id.reconciled_line_ids,
- cash_basis_aml_ids.filtered(lambda l: l.account_id == self.tax_waiting_account) + purchase_line_ids[4])
-
- self.assertEqual(len(cash_basis_aml_ids), 8)
-
- # check amounts
- cash_basis_move1 = cash_basis_moves.filtered(lambda m: m.amount_total == 33.34)
- cash_basis_move2 = cash_basis_moves.filtered(lambda m: m.amount_total == 66.66)
-
- self.assertTrue(cash_basis_move1.exists())
- self.assertTrue(cash_basis_move2.exists())
-
- # For first move
- move_lines = cash_basis_move1.line_ids
- base_amount_tax_lines = move_lines.filtered(lambda l: l.account_id == self.tax_base_amount_account)
- self.assertEqual(len(base_amount_tax_lines), 2)
- self.assertAlmostEqual(sum(base_amount_tax_lines.mapped('credit')), 27.78)
- self.assertAlmostEqual(sum(base_amount_tax_lines.mapped('debit')), 27.78)
-
- self.assertAlmostEqual((move_lines - base_amount_tax_lines).filtered(lambda l: l.account_id == self.tax_waiting_account).credit,
- 5.56)
- self.assertAlmostEqual((move_lines - base_amount_tax_lines).filtered(lambda l: l.account_id == self.tax_final_account).debit,
- 5.56)
-
- # For second move
- move_lines = cash_basis_move2.line_ids
- base_amount_tax_lines = move_lines.filtered(lambda l: l.account_id == self.tax_base_amount_account)
- self.assertEqual(len(base_amount_tax_lines), 2)
- self.assertAlmostEqual(sum(base_amount_tax_lines.mapped('credit')), 55.55)
- self.assertAlmostEqual(sum(base_amount_tax_lines.mapped('debit')), 55.55)
-
- self.assertAlmostEqual((move_lines - base_amount_tax_lines).filtered(lambda l: l.account_id == self.tax_waiting_account).credit,
- 11.11)
- self.assertAlmostEqual((move_lines - base_amount_tax_lines).filtered(lambda l: l.account_id == self.tax_final_account).debit,
- 11.11)
-
- def test_reconciliation_cash_basis02(self):
- # Simulates an invoice made up by 2 lines
- # both subjected to cash basis taxes
- # with 2 payment terms
- # And partial payment not martching any payment term
-
- company = self.company
- company.tax_cash_basis_journal_id = self.cash_basis_journal
- tax_cash_basis10percent = self.tax_cash_basis.copy({'amount': 10})
- tax_waiting_account10 = self.tax_waiting_account.copy({
- 'name': 'TAX WAIT 10',
- 'code': 'TWAIT1',
- })
-
- purchase_move = self.env['account.move'].create({
- 'journal_id': self.purchase_journal.id,
- 'line_ids': [
- (0, 0, {'account_id': self.account_rsa.id, 'debit': 0.0, 'credit': 50.0}),
- (0, 0, {'account_id': self.account_rsa.id, 'debit': 0.0, 'credit': 105.0}),
- (0, 0, {'name': 'expenseTaxed 10%', 'account_id': self.expense_account.id, 'debit': 50.0, 'credit': 0.0, 'tax_ids': [(4, tax_cash_basis10percent.id)], 'tax_exigible': False}),
- (0, 0, {
- 'name': 'TaxLine0',
- 'account_id': tax_waiting_account10.id,
- 'debit': 5.0,
- 'credit': 0.0,
- 'tax_repartition_line_id': tax_cash_basis10percent.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id,
- 'tax_exigible': False,
- 'tax_base_amount': 50.00,
- }),
- (0, 0, {'name': 'expenseTaxed 20%', 'account_id': self.expense_account.id, 'debit': 83.33, 'credit': 0.0, 'tax_ids': [(4, self.tax_cash_basis.id)], 'tax_exigible': False}),
- (0, 0, {
- 'name': 'TaxLine1',
- 'account_id': self.tax_waiting_account.id,
- 'debit': 16.67,
- 'credit': 0.0,
- 'tax_repartition_line_id': self.tax_cash_basis.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id,
- 'tax_exigible': False,
- 'tax_base_amount': 83.33,
- }),
- ],
- })
- purchase_move.post()
-
- payment_move0 = self.env['account.move'].create({
- 'journal_id': self.bank_journal_euro.id,
- 'line_ids': [
- (0, 0, {'account_id': self.account_rsa.id, 'debit': 40.0, 'credit': 0.0}),
- (0, 0, {'account_id': self.account_euro.id, 'debit': 0.0, 'credit': 40.0}),
- ],
- })
- payment_move0.post()
-
- payment_move1 = self.env['account.move'].create({
- 'journal_id': self.bank_journal_euro.id,
- 'line_ids': [
- (0, 0, {'account_id': self.account_rsa.id, 'debit': 115.0, 'credit': 0.0}),
- (0, 0, {'account_id': self.account_euro.id, 'debit': 0.0, 'credit': 115.0}),
- ],
- })
- payment_move1.post()
-
- (purchase_move + payment_move0 + payment_move1).post()
- (purchase_move + payment_move0).mapped('line_ids').sorted().filtered(lambda l: l.account_id.internal_type == 'payable').reconcile()
- (purchase_move + payment_move1).mapped('line_ids').sorted().filtered(lambda l: l.account_id.internal_type == 'payable').reconcile()
-
- cash_basis_moves = self.env['account.move'].search([('journal_id', '=', self.cash_basis_journal.id)])
-
- self.assertEqual(len(cash_basis_moves), 3)
- self.assertTrue(cash_basis_moves.exists())
-
- # check reconciliation in Payable account
- purchase_line_ids = purchase_move.line_ids.sorted()
- self.assertTrue(purchase_line_ids[0].full_reconcile_id.exists())
- self.assertEqual(purchase_line_ids[0].full_reconcile_id.reconciled_line_ids,
- (purchase_move + payment_move0 + payment_move1).mapped('line_ids').filtered(lambda l: l.account_id.internal_type == 'payable'))
-
- cash_basis_aml_ids = cash_basis_moves.mapped('line_ids')
-
- # check reconciliation in the tax waiting account
- self.assertTrue(purchase_line_ids[3].full_reconcile_id.exists())
- self.assertEqual(purchase_line_ids[3].full_reconcile_id.reconciled_line_ids,
- cash_basis_aml_ids.filtered(lambda l: l.account_id == tax_waiting_account10) + purchase_line_ids[3])
-
- self.assertTrue(purchase_line_ids[5].full_reconcile_id.exists())
- self.assertEqual(purchase_line_ids[5].full_reconcile_id.reconciled_line_ids,
- cash_basis_aml_ids.filtered(lambda l: l.account_id == self.tax_waiting_account) + purchase_line_ids[5])
-
- self.assertEqual(len(cash_basis_aml_ids), 24)
-
- # check amounts
- expected_move_amounts = [
- {'base_20': 56.45, 'tax_20': 11.29, 'base_10': 33.87, 'tax_10': 3.39},
- {'base_20': 5.38, 'tax_20': 1.08, 'base_10': 3.23, 'tax_10': 0.32},
- {'base_20': 21.50, 'tax_20': 4.30, 'base_10': 12.90, 'tax_10': 1.29},
- ]
-
- index = 0
- for cb_move in cash_basis_moves:
- expected = expected_move_amounts[index]
- move_lines = cb_move.line_ids
- base_amount_tax_lines20per = move_lines.filtered(lambda l: l.account_id == self.tax_base_amount_account and '20%' in l.name)
- base_amount_tax_lines10per = move_lines.filtered(lambda l: l.account_id == self.tax_base_amount_account and '10%' in l.name)
- self.assertEqual(len(base_amount_tax_lines20per), 2)
-
- self.assertAlmostEqual(sum(base_amount_tax_lines20per.mapped('credit')), expected['base_20'])
- self.assertAlmostEqual(sum(base_amount_tax_lines20per.mapped('debit')), expected['base_20'])
-
- self.assertEqual(len(base_amount_tax_lines10per), 2)
- self.assertAlmostEqual(sum(base_amount_tax_lines10per.mapped('credit')), expected['base_10'])
- self.assertAlmostEqual(sum(base_amount_tax_lines10per.mapped('debit')), expected['base_10'])
-
- self.assertAlmostEqual(
- (move_lines - base_amount_tax_lines20per - base_amount_tax_lines10per)
- .filtered(lambda l: l.account_id == self.tax_waiting_account).credit,
- expected['tax_20']
- )
- self.assertAlmostEqual(
- (move_lines - base_amount_tax_lines20per - base_amount_tax_lines10per)
- .filtered(lambda l: 'TaxLine1' in l.name).debit,
- expected['tax_20']
- )
-
- self.assertAlmostEqual(
- (move_lines - base_amount_tax_lines20per - base_amount_tax_lines10per)
- .filtered(lambda l: l.account_id == tax_waiting_account10).credit,
- expected['tax_10']
- )
- self.assertAlmostEqual(
- (move_lines - base_amount_tax_lines20per - base_amount_tax_lines10per)
- .filtered(lambda l: 'TaxLine0' in l.name).debit,
- expected['tax_10']
- )
- index += 1
-
- def test_reconciliation_cash_basis_fx_01(self):
- """
- Company's Currency EUR
-
- Having issued an invoice at date Nov-21-2018 as:
-
- Accounts Amount Currency Debit(EUR) Credit(EUR)
- ---------------------------------------------------------------------
- Expenses 5,301.00 USD 106,841.65 0.00
- Taxes 848.16 USD 17,094.66 0.00
- Payables -6,149.16 USD 0.00 123,936.31
-
- On Dec-20-2018 user issues an FX Journal Entry as:
-
- Accounts Amount Currency Debit(EUR) Credit(EUR)
- ---------------------------------------------------------------------
- Payables 0.00 USD 167.86 0.00
- FX Gains 0.00 USD 0.00 167.86
-
- On Same day user records a payment for:
-
- Accounts Amount Currency Debit(EUR) Credit(EUR)
- ---------------------------------------------------------------------
- Payables 6,149.16 USD 123,768.45 0.00
- Bank -6,149.16 USD 0.00 123,768.45
-
- And then reconciles the Payables Items which shall render only one Tax
- Cash Basis Journal Entry because of the actual payment, i.e.
- amount_currency != 0:
-
- Accounts Amount Currency Debit(EUR) Credit(EUR)
- ---------------------------------------------------------------------
- Tax Base Acc. 0.00 USD 106,841.65 0.00
- Tax Base Acc. 0.00 USD 0.00 106,841.65
- Creditable Taxes 848.16 USD 17,094.66 0.00
- Taxes -848.16 USD 0.00 17,094.66
- """
-
- company = self.company
- company.country_id = self.ref('base.us')
- company.tax_cash_basis_journal_id = self.cash_basis_journal
-
- # Purchase
- purchase_move = self.env['account.move'].create({
- 'journal_id': self.purchase_journal.id,
- 'line_ids': [
- (0, 0, {
- 'name': 'expenseTaxed',
- 'account_id': self.expense_account.id,
- 'currency_id': self.currency_usd_id,
- 'tax_ids': [(4, self.tax_cash_basis.id)],
- 'tax_exigible': False,
- 'debit': 106841.65, 'credit': 0.0, 'amount_currency': 5301.00,
- }),
- (0, 0, {
- 'name': 'TaxLine',
- 'account_id': self.tax_waiting_account.id,
- 'currency_id': self.currency_usd_id,
- 'tax_repartition_line_id': self.tax_cash_basis.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id,
- 'tax_exigible': False,
- 'tax_base_amount': 106841.65,
- 'debit': 17094.66, 'credit': 0.0, 'amount_currency': 848.16,
- }),
- (0, 0, {
- 'name': 'Payable',
- 'account_id': self.account_rsa.id,
- 'currency_id': self.currency_usd_id,
- 'debit': 0.0, 'credit': 123936.31, 'amount_currency': -6149.16,
- }),
- ],
- })
- purchase_move.post()
-
- # FX 01 Move
- fx_move_01 = self.env['account.move'].create({
- 'journal_id': self.fx_journal.id,
- 'line_ids': [
- (0, 0, {
- 'account_id': self.account_rsa.id,
- 'currency_id': self.currency_usd_id,
- 'debit': 167.86, 'credit': 0.0, 'amount_currency': 0.00,
- }),
- (0, 0, {
- 'account_id': self.diff_income_account.id,
- 'currency_id': self.currency_usd_id,
- 'debit': 0.0, 'credit': 167.86, 'amount_currency': 0.0,
- }),
- ],
- })
- fx_move_01.post()
-
- # Payment Move
- payment_move = self.env['account.move'].create({
- 'journal_id': self.bank_journal_usd.id,
- 'line_ids': [
- (0, 0, {
- 'account_id': self.account_rsa.id,
- 'currency_id': self.currency_usd_id,
- 'debit': 123768.45, 'credit': 0.0, 'amount_currency': 6149.16,
- }),
- (0, 0, {
- 'account_id': self.account_usd.id,
- 'currency_id': self.currency_usd_id,
- 'debit': 0.0, 'credit': 123768.45, 'amount_currency': -6149.16,
- }),
- ],
- })
- payment_move.post()
-
- to_reconcile = (
- (purchase_move + payment_move + fx_move_01).mapped('line_ids')
- .filtered(lambda l: l.account_id.internal_type == 'payable'))
- to_reconcile.reconcile()
-
- # check reconciliation in Payable account
- purchase_line_ids = purchase_move.line_ids.sorted()
- fx_move_01_line_ids = fx_move_01.line_ids.sorted()
- payment_line_ids = payment_move.line_ids.sorted()
- self.assertTrue(purchase_line_ids[2].full_reconcile_id.exists())
- self.assertEqual(
- purchase_line_ids[2].full_reconcile_id.reconciled_line_ids,
- purchase_line_ids[2] + fx_move_01_line_ids[0] + payment_line_ids[0])
-
- # check cash basis
- cash_basis_moves = self.env['account.move'].search(
- [('journal_id', '=', self.cash_basis_journal.id)])
-
- self.assertEqual(len(cash_basis_moves), 1)
-
- cash_basis_aml_ids = cash_basis_moves.mapped('line_ids')
- self.assertEqual(len(cash_basis_aml_ids), 4)
-
- # check amounts
- cash_basis_move1 = cash_basis_moves.filtered(lambda m: m.currency_id.is_zero(sum(line.credit for line in m.line_ids) - 123936.31))
-
- self.assertTrue(cash_basis_move1.exists())
-
- # For first move
- move_lines = cash_basis_moves.line_ids
- base_amount_tax_lines = move_lines.filtered(
- lambda l: l.account_id == self.tax_base_amount_account)
- self.assertEqual(len(base_amount_tax_lines), 2)
- self.assertAlmostEqual(
- sum(base_amount_tax_lines.mapped('credit')), 106841.65)
- self.assertAlmostEqual(
- sum(base_amount_tax_lines.mapped('debit')), 106841.65)
-
- self.assertAlmostEqual(
- (move_lines - base_amount_tax_lines)
- .filtered(lambda l: l.account_id == self.tax_waiting_account)
- .credit, 17094.66)
- self.assertAlmostEqual(
- (move_lines - base_amount_tax_lines)
- .filtered(lambda l: l.account_id == self.tax_final_account)
- .debit, 17094.66)
-
- def test_reconciliation_cash_basis_fx_02(self):
- """
- Company's Currency EUR
-
- Having issued an invoice at date Nov-21-2018 as:
-
- Accounts Amount Currency Debit(EUR) Credit(EUR)
- ---------------------------------------------------------------------
- Expenses 5,301.00 USD 106,841.65 0.00
- Taxes 848.16 USD 17,094.66 0.00
- Payables -6,149.16 USD 0.00 123,936.31
-
- On Nov-30-2018 user issues an FX Journal Entry as:
-
- Accounts Amount Currency Debit(EUR) Credit(EUR)
- ---------------------------------------------------------------------
- FX Losses 0.00 USD 1.572.96 0.00
- Payables 0.00 USD 0.00 1.572.96
-
- On Dec-20-2018 user issues an FX Journal Entry as:
-
- Accounts Amount Currency Debit(EUR) Credit(EUR)
- ---------------------------------------------------------------------
- Payables 0.00 USD 1.740.82 0.00
- FX Gains 0.00 USD 0.00 1.740.82
-
- On Same day user records a payment for:
-
- Accounts Amount Currency Debit(EUR) Credit(EUR)
- ---------------------------------------------------------------------
- Payables 6,149.16 USD 123,768.45 0.00
- Bank -6,149.16 USD 0.00 123,768.45
-
- And then reconciles the Payables Items which shall render only one Tax
- Cash Basis Journal Entry because of the actual payment, i.e.
- amount_currency != 0:
-
- Accounts Amount Currency Debit(EUR) Credit(EUR)
- ---------------------------------------------------------------------
- Tax Base Acc. 0.00 USD 106,841.65 0.00
- Tax Base Acc. 0.00 USD 0.00 106,841.65
- Creditable Taxes 848.16 USD 17,094.66 0.00
- Taxes -848.16 USD 0.00 17,094.66
- """
-
- company = self.company
- company.country_id = self.ref('base.us')
- company.tax_cash_basis_journal_id = self.cash_basis_journal
-
- # Purchase
- purchase_move = self.env['account.move'].create({
- 'journal_id': self.purchase_journal.id,
- 'line_ids': [
- (0, 0, {
- 'name': 'expenseTaxed',
- 'account_id': self.expense_account.id,
- 'currency_id': self.currency_usd_id,
- 'tax_ids': [(4, self.tax_cash_basis.id)],
- 'tax_exigible': False,
- 'debit': 106841.65, 'credit': 0.0, 'amount_currency': 5301.00,
- }),
- (0, 0, {
- 'name': 'TaxLine',
- 'account_id': self.tax_waiting_account.id,
- 'currency_id': self.currency_usd_id,
- 'tax_repartition_line_id': self.tax_cash_basis.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id,
- 'tax_exigible': False,
- 'tax_base_amount': 106841.65,
- 'debit': 17094.66, 'credit': 0.0, 'amount_currency': 848.16,
- }),
- (0, 0, {
- 'name': 'Payable',
- 'account_id': self.account_rsa.id,
- 'currency_id': self.currency_usd_id,
- 'debit': 0.0, 'credit': 123936.31, 'amount_currency': -6149.16,
- }),
- ],
- })
- purchase_move.post()
-
- # FX 01 Move
- fx_move_01 = self.env['account.move'].create({
- 'journal_id': self.fx_journal.id,
- 'line_ids': [
- (0, 0, {
- 'account_id': self.account_rsa.id,
- 'currency_id': self.currency_usd_id,
- 'debit': 0.0, 'credit': 1572.96, 'amount_currency': 0.00,
- }),
- (0, 0, {
- 'account_id': self.diff_expense_account.id,
- 'currency_id': self.currency_usd_id,
- 'debit': 1572.96, 'credit': 0.0, 'amount_currency': 0.0,
- }),
- ],
- })
- fx_move_01.post()
-
- # FX 02 Move
- fx_move_02 = self.env['account.move'].create({
- 'journal_id': self.fx_journal.id,
- 'line_ids': [
- (0, 0, {
- 'account_id': self.account_rsa.id,
- 'currency_id': self.currency_usd_id,
- 'debit': 1740.82, 'credit': 0.0, 'amount_currency': 0.00,
- }),
- (0, 0, {
- 'account_id': self.diff_income_account.id,
- 'currency_id': self.currency_usd_id,
- 'debit': 0.0, 'credit': 1740.82, 'amount_currency': 0.0,
- }),
- ],
- })
- fx_move_02.post()
-
- # Payment Move
- payment_move = self.env['account.move'].create({
- 'journal_id': self.bank_journal_usd.id,
- 'line_ids': [
- (0, 0, {
- 'account_id': self.account_rsa.id,
- 'currency_id': self.currency_usd_id,
- 'debit': 123768.45, 'credit': 0.0, 'amount_currency': 6149.16,
- }),
- (0, 0, {
- 'account_id': self.account_usd.id,
- 'currency_id': self.currency_usd_id,
- 'debit': 0.0, 'credit': 123768.45, 'amount_currency': -6149.16,
- }),
- ],
- })
- payment_move.post()
-
- to_reconcile = (
- (purchase_move + payment_move + fx_move_01 + fx_move_02)
- .mapped('line_ids')
- .filtered(lambda l: l.account_id.internal_type == 'payable'))
- to_reconcile.reconcile()
-
- # check reconciliation in Payable account
- purchase_line_ids = purchase_move.line_ids.sorted()
- fx_move_01_line_ids = fx_move_01.line_ids.sorted()
- fx_move_02_line_ids = fx_move_02.line_ids.sorted()
- payment_line_ids = payment_move.line_ids.sorted()
- self.assertTrue(purchase_line_ids[2].full_reconcile_id.exists())
- self.assertEqual(
- purchase_line_ids[2].full_reconcile_id.reconciled_line_ids,
- purchase_line_ids[2] + fx_move_01_line_ids[0] + fx_move_02_line_ids[0] +
- payment_line_ids[0])
-
- # check cash basis
- cash_basis_moves = self.env['account.move'].search(
- [('journal_id', '=', self.cash_basis_journal.id)])
-
- self.assertEqual(len(cash_basis_moves), 1)
-
- cash_basis_aml_ids = cash_basis_moves.mapped('line_ids')
- self.assertEqual(len(cash_basis_aml_ids), 4)
-
- # check amounts
- cash_basis_move1 = cash_basis_moves.filtered(lambda m: m.currency_id.is_zero(sum(line.credit for line in m.line_ids) - 123936.31))
-
- self.assertTrue(cash_basis_move1.exists())
-
- # For first move
- move_lines = cash_basis_move1.line_ids
- base_amount_tax_lines = move_lines.filtered(
- lambda l: l.account_id == self.tax_base_amount_account)
- self.assertEqual(len(base_amount_tax_lines), 2)
- self.assertAlmostEqual(
- sum(base_amount_tax_lines.mapped('credit')), 106841.65)
- self.assertAlmostEqual(
- sum(base_amount_tax_lines.mapped('debit')), 106841.65)
-
- self.assertAlmostEqual(
- (move_lines - base_amount_tax_lines)
- .filtered(lambda l: l.account_id == self.tax_waiting_account)
- .credit, 17094.66)
- self.assertAlmostEqual(
- (move_lines - base_amount_tax_lines)
- .filtered(lambda l: l.account_id == self.tax_final_account)
- .debit, 17094.66)
-
- def test_reconciliation_cash_basis_revert(self):
- company = self.company
- company.tax_cash_basis_journal_id = self.cash_basis_journal
- tax_cash_basis10percent = self.tax_cash_basis.copy({'amount': 10})
- self.tax_waiting_account.reconcile = True
- tax_waiting_account10 = self.tax_waiting_account.copy({
- 'name': 'TAX WAIT 10',
- 'code': 'TWAIT1',
- })
-
- AccountMoveLine = self.env['account.move.line'].with_context(check_move_validity=False)
-
- # Purchase
- purchase_move = self.env['account.move'].create({
- 'name': 'invoice',
- 'journal_id': self.purchase_journal.id,
- })
-
- purchase_payable_line0 = AccountMoveLine.create({
- 'account_id': self.account_rsa.id,
- 'credit': 175,
- 'move_id': purchase_move.id,
- })
-
- AccountMoveLine.create({
- 'name': 'expenseTaxed 10%',
- 'account_id': self.expense_account.id,
- 'debit': 50,
- 'move_id': purchase_move.id,
- 'tax_ids': [(4, tax_cash_basis10percent.id, False)],
- })
- tax_line0 = AccountMoveLine.create({
- 'name': 'TaxLine0',
- 'account_id': tax_waiting_account10.id,
- 'debit': 5,
- 'move_id': purchase_move.id,
- 'tax_repartition_line_id': tax_cash_basis10percent.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id,
- 'tax_base_amount': 50,
- })
- AccountMoveLine.create({
- 'name': 'expenseTaxed 20%',
- 'account_id': self.expense_account.id,
- 'debit': 100,
- 'move_id': purchase_move.id,
- 'tax_ids': [(4, self.tax_cash_basis.id, False)],
- })
- tax_line1 = AccountMoveLine.create({
- 'name': 'TaxLine1',
- 'account_id': self.tax_waiting_account.id,
- 'debit': 20,
- 'move_id': purchase_move.id,
- 'tax_repartition_line_id': self.tax_cash_basis.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id,
- 'tax_base_amount': 100,
- })
- purchase_move.post()
-
- reverted = purchase_move._reverse_moves(cancel=True)
- self.assertTrue(reverted.exists())
-
- for inv_line in [purchase_payable_line0, tax_line0, tax_line1]:
- self.assertTrue(inv_line.full_reconcile_id.exists())
- reverted_expected = reverted.line_ids.filtered(lambda l: l.account_id == inv_line.account_id)
- self.assertEqual(len(reverted_expected), 1)
- self.assertEqual(reverted_expected.full_reconcile_id, inv_line.full_reconcile_id)
-
def test_reconciliation_revert_future(self):
company = self.company
company.tax_cash_basis_journal_id = self.cash_basis_journal
@@ -1404,69 +757,6 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
self.assertEqual(reverse_move.auto_post, True)
self.assertEqual(reverse_move.date, reverse_date)
- def test_reconciliation_cash_basis_foreign_currency_low_values(self):
- self.company.tax_cash_basis_journal_id = self.cash_basis_journal
- journal = self.env['account.journal'].create({
- 'name': 'Bank', 'type': 'bank', 'code': 'THE',
- 'currency_id': self.currency_usd_id,
- })
- usd = self.env['res.currency'].browse(self.currency_usd_id)
- usd.rate_ids.unlink()
- self.env['res.currency.rate'].create({
- 'name': time.strftime('%Y-01-01'),
- 'rate': 1/17.0,
- 'currency_id': self.currency_usd_id,
- 'company_id': self.company.id,
- })
-
- move_form = Form(self.env['account.move'].with_context(default_move_type='out_invoice'))
- move_form.partner_id = self.partner_agrolait
- move_form.currency_id = self.env.ref('base.USD')
- move_form.invoice_date = time.strftime('%Y') + '-07-01'
- with move_form.invoice_line_ids.new() as line_form:
- line_form.name = 'test line'
- line_form.price_unit = 50
- line_form.tax_ids.clear()
- invoice = move_form.save()
- move_form = Form(invoice)
- with move_form.invoice_line_ids.edit(0) as line_form:
- line_form.tax_ids.add(self.tax_cash_basis)
- invoice = move_form.save()
- invoice.post()
-
- self.assertTrue(invoice.currency_id != self.env.user.company_id.currency_id)
-
- # First Payment
- line = invoice.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
- payment0 = self.make_payment(invoice, journal, amount=59.99, reconcile_param=[{'id': line.id, 'balance': -59.99}])
- self.assertEqual(invoice.amount_residual, 0.01)
-
- tax_waiting_line = invoice.line_ids.filtered(lambda l: l.account_id == self.tax_waiting_account)
- self.assertTrue(tax_waiting_line.exists())
- self.assertFalse(tax_waiting_line.reconciled)
-
- move_caba0 = tax_waiting_line.matched_debit_ids.debit_move_id.move_id
- self.assertTrue(move_caba0.exists())
- self.assertEqual(move_caba0.journal_id, self.env.user.company_id.tax_cash_basis_journal_id)
-
- pay_receivable_line0 = payment0.move_line_ids.filtered(lambda l: l.account_id == self.account_rcv)
- self.assertTrue(pay_receivable_line0.reconciled)
- self.assertEqual(pay_receivable_line0.matched_debit_ids, move_caba0.tax_cash_basis_rec_id)
-
- # Second Payment
- payment1 = self.make_payment(invoice, journal, 0.01, reconcile_param=[{'id': line.id, 'balance': -0.01}])
- self.assertEqual(invoice.amount_residual, 0)
- self.assertEqual(invoice.payment_state, 'paid')
-
- self.assertTrue(tax_waiting_line.reconciled)
- move_caba1 = tax_waiting_line.matched_debit_ids.mapped('debit_move_id').mapped('move_id').filtered(lambda m: m != move_caba0)
- self.assertEqual(len(move_caba1.exists()), 1)
- self.assertEqual(move_caba1.journal_id, self.env.user.company_id.tax_cash_basis_journal_id)
-
- pay_receivable_line1 = payment1.move_line_ids.filtered(lambda l: l.account_id == self.account_rcv)
- self.assertTrue(pay_receivable_line1.reconciled)
- self.assertEqual(pay_receivable_line1.matched_debit_ids, move_caba1.tax_cash_basis_rec_id)
-
def test_reconciliation_with_currency(self):
#reconciliation on an account having a foreign currency being
#the same as the company one
@@ -1927,7 +1217,13 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
self.assertEqual(inv1_receivable.full_reconcile_id, inv2_receivable.full_reconcile_id)
self.assertEqual(inv1_receivable.full_reconcile_id, payment_receivable.full_reconcile_id)
- self.assertFalse(inv1_receivable.full_reconcile_id.exchange_move_id)
+ # Before saas-13.4, there was no exchange difference entry generated because the amount was
+ # wrongly converted in the _amount_residual method at the invoice date like this:
+ # 315.15 * (600.0 / 540.25) = 515.15 * 1.110596946 = 350.004627487 ~= 350.0
+ # Now, the conversion is made using the payment rate using the _convert method and the
+ # encoded currency rate:
+ # 315.15 * 1.1106 = 350.00559 ~= 350.01
+ self.assertTrue(inv1_receivable.full_reconcile_id.exchange_move_id)
self.assertTrue(inv1.payment_state in ('in_payment', 'paid'), "Invoice should be paid")
self.assertEqual(inv2.payment_state, 'paid')
diff --git a/addons/account/views/account_chart_template_views.xml b/addons/account/views/account_chart_template_views.xml
index 87ee1d4bcf8..f42f53ecdc7 100644
--- a/addons/account/views/account_chart_template_views.xml
+++ b/addons/account/views/account_chart_template_views.xml
@@ -176,7 +176,6 @@
<group name="tax_configuration">
<field name="active" groups="base.group_no_one"/>
<field name="tax_exigibility" widget="radio" attrs="{'invisible':[('amount_type','=', 'group')]}"/>
- <field name="cash_basis_base_account_id" attrs="{'invisible': [('tax_exigibility', '=', 'on_invoice')], 'required': [('tax_exigibility', '=', 'on_payment')]}"/>
<field name="cash_basis_transition_account_id" attrs="{'invisible': [('tax_exigibility', '=', 'on_invoice')], 'required': [('tax_exigibility', '=', 'on_payment')]}"/>
</group>
</group>
diff --git a/addons/account/views/account_tax_views.xml b/addons/account/views/account_tax_views.xml
index 8cf2da53e9e..87c9b015233 100644
--- a/addons/account/views/account_tax_views.xml
+++ b/addons/account/views/account_tax_views.xml
@@ -155,7 +155,6 @@
<field name="include_base_amount" attrs="{'invisible':[('amount_type','=', 'group')]}" />
<field name="hide_tax_exigibility" invisible="1"/>
<field name="tax_exigibility" widget="radio" attrs="{'invisible':['|', ('amount_type','=', 'group'), ('hide_tax_exigibility', '=', False)]}" groups="account.group_account_readonly"/>
- <field name="cash_basis_base_account_id" options="{'no_create': True}" attrs="{'invisible': [('tax_exigibility', '=', 'on_invoice')]}" groups="account.group_account_readonly"/>
<field name="cash_basis_transition_account_id" options="{'no_create': True}" attrs="{'invisible': [('tax_exigibility', '=', 'on_invoice')], 'required': [('tax_exigibility', '=', 'on_payment')]}" groups="account.group_account_readonly"/>
</group>
</group>
diff --git a/addons/account/views/report_payment_receipt_templates.xml b/addons/account/views/report_payment_receipt_templates.xml
index c472cd3ff68..bb8e3e3e454 100644
--- a/addons/account/views/report_payment_receipt_templates.xml
+++ b/addons/account/views/report_payment_receipt_templates.xml
@@ -44,7 +44,7 @@
</thead>
<tbody>
<tr t-foreach="o.move_id._get_reconciled_invoices_partials()" t-as="rec">
- <t t-set="amount" t-value="rec[0]"/>
+ <t t-set="amount" t-value="rec[1]"/>
<t t-set="inv" t-value="rec[2].move_id"/>
<td><span t-field="inv.invoice_date"/></td>
<td><span t-field="inv.name"/></td>
diff --git a/addons/account/views/res_config_settings_views.xml b/addons/account/views/res_config_settings_views.xml
index fefa89d4e4a..93e3b7b5841 100644
--- a/addons/account/views/res_config_settings_views.xml
+++ b/addons/account/views/res_config_settings_views.xml
@@ -122,6 +122,10 @@
<label for="tax_cash_basis_journal_id" class="col-lg-3 o_light_label"/>
<field name="tax_cash_basis_journal_id"/>
</div>
+ <div class="row mt16">
+ <label for="account_cash_basis_base_account_id" class="col-lg-3 o_light_label"/>
+ <field name="account_cash_basis_base_account_id"/>
+ </div>
</div>
</div>
</div>
diff --git a/addons/hr_expense/models/account_move_line.py b/addons/hr_expense/models/account_move_line.py
index 6ebc17ed523..2dd37f32e3b 100644
--- a/addons/hr_expense/models/account_move_line.py
+++ b/addons/hr_expense/models/account_move_line.py
@@ -2,7 +2,23 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
-from odoo.tools import float_compare
+
+
+class AccountMove(models.Model):
+ _inherit = 'account.move'
+
+ def action_invoice_paid(self):
+ # OVERRIDE to mark as paid the expense sheets.
+ res = super().action_invoice_paid()
+
+ if self:
+ expense_sheets = self.env['hr.expense.sheet'].search([
+ ('account_move_id', 'in', self.ids),
+ ('state', '!=', 'done'),
+ ])
+ expense_sheets.set_to_paid()
+
+ return res
class AccountMoveLine(models.Model):
@@ -15,13 +31,3 @@ class AccountMoveLine(models.Model):
if self.expense_id:
attachment_domains.append([('res_model', '=', 'hr.expense'), ('res_id', '=', self.expense_id.id)])
return attachment_domains
-
- def reconcile(self, writeoff_acc_id=False, writeoff_journal_id=False):
- res = super(AccountMoveLine, self).reconcile(writeoff_acc_id=writeoff_acc_id, writeoff_journal_id=writeoff_journal_id)
- account_move_ids = [l.move_id.id for l in self if float_compare(l.move_id._get_cash_basis_matched_percentage(), 1, precision_digits=5) == 0]
- if account_move_ids:
- expense_sheets = self.env['hr.expense.sheet'].search([
- ('account_move_id', 'in', account_move_ids), ('state', '!=', 'done')
- ])
- expense_sheets.set_to_paid()
- return res
diff --git a/addons/l10n_mx/data/account_tax_data.xml b/addons/l10n_mx/data/account_tax_data.xml
index 8d91bba6cce..0b3280f6ee3 100644
--- a/addons/l10n_mx/data/account_tax_data.xml
+++ b/addons/l10n_mx/data/account_tax_data.xml
@@ -70,7 +70,6 @@
<field name="type_tax_use">sale</field>
<field name="tax_group_id" ref="tax_group_iva_0"/>
<field name="tax_exigibility">on_payment</field>
- <field name="cash_basis_base_account_id" ref="cuenta801_01_99"/>
<field name="cash_basis_transition_account_id" ref="cuenta209_01"/>
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
@@ -110,7 +109,6 @@
<field name="type_tax_use">sale</field>
<field name="tax_group_id" ref="tax_group_iva_16"/>
<field name="tax_exigibility">on_payment</field>
- <field name="cash_basis_base_account_id" ref="cuenta801_01_99"/>
<field name="cash_basis_transition_account_id" ref="cuenta209_01"/>
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
@@ -150,7 +148,6 @@
<field name="type_tax_use">purchase</field>
<field name="tax_group_id" ref="tax_group_iva_ret_4"/>
<field name="tax_exigibility">on_payment</field>
- <field name="cash_basis_base_account_id" ref="cuenta801_01_99"/>
<field name="cash_basis_transition_account_id" ref="cuenta216_10"/>
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
@@ -190,7 +187,6 @@
<field name="type_tax_use">purchase</field>
<field name="tax_group_id" ref="tax_group_iva_ret_10"/>
<field name="tax_exigibility">on_payment</field>
- <field name="cash_basis_base_account_id" ref="cuenta801_01_99"/>
<field name="cash_basis_transition_account_id" ref="cuenta216_10"/>
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
@@ -300,7 +296,6 @@
<field name="type_tax_use">purchase</field>
<field name="tax_group_id" ref="tax_group_iva_ret_1067"/>
<field name="tax_exigibility">on_payment</field>
- <field name="cash_basis_base_account_id" ref="cuenta801_01_99"/>
<field name="cash_basis_transition_account_id" ref="cuenta216_10"/>
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
@@ -340,7 +335,6 @@
<field name="type_tax_use">purchase</field>
<field name="tax_group_id" ref="tax_group_iva_ret_1067"/>
<field name="tax_exigibility">on_payment</field>
- <field name="cash_basis_base_account_id" ref="cuenta801_01_99"/>
<field name="cash_basis_transition_account_id" ref="cuenta216_10"/>
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
@@ -380,7 +374,6 @@
<field name="type_tax_use">purchase</field>
<field name="tax_group_id" ref="tax_group_iva_0"/>
<field name="tax_exigibility">on_payment</field>
- <field name="cash_basis_base_account_id" ref="cuenta801_01_99"/>
<field name="cash_basis_transition_account_id" ref="cuenta119_01"/>
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
@@ -418,7 +411,6 @@
<field name="type_tax_use">purchase</field>
<field name="tax_group_id" ref="tax_group_iva_16"/>
<field name="tax_exigibility">on_payment</field>
- <field name="cash_basis_base_account_id" ref="cuenta801_01_99"/>
<field name="cash_basis_transition_account_id" ref="cuenta119_01"/>
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
(0,0, {
diff --git a/addons/l10n_mx/data/l10n_mx_chart_post_data.xml b/addons/l10n_mx/data/l10n_mx_chart_post_data.xml
index e95b3a1302f..9acddb7dfe2 100644
--- a/addons/l10n_mx/data/l10n_mx_chart_post_data.xml
+++ b/addons/l10n_mx/data/l10n_mx_chart_post_data.xml
@@ -11,6 +11,7 @@
<field name="property_stock_account_input_categ_id" ref="cuenta205_06_01"/>
<field name="property_stock_account_output_categ_id" ref="cuenta107_05_01"/>
<field name="property_stock_valuation_account_id" ref="cuenta115_01"/>
+ <field name="property_cash_basis_base_account_id" ref="cuenta801_01_99" />
<field name="income_currency_exchange_account_id" ref="cuenta702_01"/>
<field name="expense_currency_exchange_account_id" ref="cuenta701_01"/>
<field name="default_pos_receivable_account_id" ref="cuenta105_02" />
diff --git a/addons/sale_stock/tests/test_anglo_saxon_valuation.py b/addons/sale_stock/tests/test_anglo_saxon_valuation.py
index 2ea13237145..3b9625d5492 100644
--- a/addons/sale_stock/tests/test_anglo_saxon_valuation.py
+++ b/addons/sale_stock/tests/test_anglo_saxon_valuation.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo.tests import Form
+from odoo.tests import Form, tagged
from odoo.tests.common import SavepointCase
from odoo.exceptions import UserError