[REF] account_accountant: Refactor the bank reconciliation widget

- Refactor the bank reconciliation widget to use more standards views and have a side-by-side kanban/widget views instead.
- Make sure the bank reconciliation widget is doing the same thing as the "reconcile" method on account.bank.statement.line. Then, what you see on the view is exactly what you get on the corresponding journal entry.
- Since the "reconcile" method is gone, clean the matching rules since it's now called only for one statement line at a time. Also, move the auto_validate feature into a CRON to avoid performance issues when opening the widget.

closes odoo/odoo#91448

Task: 2555114
Related: odoo/enterprise#27360
Related: odoo/upgrade#3555
Signed-off-by: William André (wan) <wan@odoo.com>
This commit is contained in:
Laurent Smet
2022-06-07 10:16:14 +02:00
parent e79b32a868
commit 080d41a341
13 changed files with 902 additions and 2235 deletions
+218 -410
View File
@@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
import math
from odoo import api, fields, models, _
from odoo.tools import float_is_zero
from odoo.tools import float_is_zero, html2plaintext
from odoo.tools.misc import formatLang, format_date
from odoo.exceptions import UserError, ValidationError
from odoo.osv.expression import get_unaccent_wrapper
from odoo.addons.base.models.res_bank import sanitize_account_number
class AccountCashboxLine(models.Model):
@@ -524,10 +524,16 @@ class AccountBankStatementLine(models.Model):
transaction_type = fields.Char(string='Transaction Type')
payment_ref = fields.Char(string='Label', required=True)
amount = fields.Monetary(currency_field='currency_id')
amount_currency = fields.Monetary(currency_field='foreign_currency_id',
help="The amount expressed in an optional other currency if it is a multi-currency entry.")
foreign_currency_id = fields.Many2one('res.currency', string='Foreign Currency',
help="The optional other currency if it is a multi-currency entry.")
amount_currency = fields.Monetary(
string="Amount in Currency",
currency_field='foreign_currency_id',
help="The amount expressed in an optional other currency if it is a multi-currency entry.",
)
foreign_currency_id = fields.Many2one(
comodel_name='res.currency',
string="Foreign Currency",
help="The optional other currency if it is a multi-currency entry.",
)
amount_residual = fields.Float(string="Residual Amount",
compute="_compute_is_reconciled",
store=True,
@@ -575,156 +581,68 @@ class AccountBankStatementLine(models.Model):
other_lines += line
return liquidity_lines, suspense_lines, other_lines
@api.model
def _prepare_liquidity_move_line_vals(self):
''' Prepare values to create a new account.move.line record corresponding to the
liquidity line (having the bank/cash account).
:return: The values to create a new account.move.line record.
'''
def _prepare_counterpart_amounts_using_st_line_rate(self, currency, balance, amount_currency):
""" Convert the amounts passed as parameters to the statement line currency using the rates provided by the
bank. The computed amounts are the one that could be set on the statement line as a counterpart journal item
to fully paid the provided amounts as parameters.
:param currency: The currency in which is expressed 'amount_currency'.
:param balance: The amount expressed in company currency. Only needed when the currency passed as
parameter is neither the statement line's foreign currency, neither the journal's
currency.
:param amount_currency: The amount expressed in the 'currency' passed as parameter.
:return: A python dictionary containing:
* balance: The amount to consider expressed in company's currency.
* amount_currency: The amount to consider expressed in statement line's foreign currency.
"""
self.ensure_one()
statement = self.statement_id
journal = statement.journal_id
company_currency = journal.company_id.currency_id
journal_currency = journal.currency_id or company_currency
if self.foreign_currency_id and journal_currency:
currency_id = journal_currency.id
if self.foreign_currency_id == company_currency:
amount_currency = self.amount
balance = self.amount_currency
else:
amount_currency = self.amount
balance = journal_currency._convert(amount_currency, company_currency, journal.company_id, self.date)
elif self.foreign_currency_id and not journal_currency:
amount_currency = self.amount_currency
balance = self.amount
currency_id = self.foreign_currency_id.id
elif not self.foreign_currency_id and journal_currency:
currency_id = journal_currency.id
amount_currency = self.amount
balance = journal_currency._convert(amount_currency, journal.company_id.currency_id, journal.company_id, self.date)
else:
currency_id = company_currency.id
amount_currency = self.amount
balance = self.amount
return {
'name': self.payment_ref,
'move_id': self.move_id.id,
'partner_id': self.partner_id.id,
'currency_id': currency_id,
'account_id': journal.default_account_id.id,
'debit': balance > 0 and balance or 0.0,
'credit': balance < 0 and -balance or 0.0,
'amount_currency': amount_currency,
}
@api.model
def _prepare_counterpart_move_line_vals(self, counterpart_vals, move_line=None):
''' Prepare values to create a new account.move.line move_line.
By default, without specified 'counterpart_vals' or 'move_line', the counterpart line is
created using the suspense account. Otherwise, this method is also called during the
reconciliation to prepare the statement line's journal entry. In that case,
'counterpart_vals' will be used to create a custom account.move.line (from the reconciliation widget)
and 'move_line' will be used to create the counterpart of an existing account.move.line to which
the newly created journal item will be reconciled.
:param counterpart_vals: A python dictionary containing:
'balance': Optional amount to consider during the reconciliation. If a foreign currency is set on the
counterpart line in the same foreign currency as the statement line, then this amount is
considered as the amount in foreign currency. If not specified, the full balance is took.
This value must be provided if move_line is not.
'amount_residual': The residual amount to reconcile expressed in the company's currency.
/!\ This value should be equivalent to move_line.amount_residual except we want
to avoid browsing the record when the only thing we need in an overview of the
reconciliation, for example in the reconciliation widget.
'amount_residual_currency': The residual amount to reconcile expressed in the foreign's currency.
Using this key doesn't make sense without passing 'currency_id' in vals.
/!\ This value should be equivalent to move_line.amount_residual_currency except
we want to avoid browsing the record when the only thing we need in an overview
of the reconciliation, for example in the reconciliation widget.
**kwargs: Additional values that need to land on the account.move.line to create.
:param move_line: An optional account.move.line move_line representing the counterpart line to reconcile.
:return: The values to create a new account.move.line move_line.
'''
self.ensure_one()
statement = self.statement_id
journal = statement.journal_id
journal = self.journal_id
company_currency = journal.company_id.currency_id
journal_currency = journal.currency_id or company_currency
foreign_currency = self.foreign_currency_id or journal_currency or company_currency
statement_line_rate = (self.amount_currency / self.amount) if self.amount else 0.0
balance_to_reconcile = counterpart_vals.pop('balance', None)
amount_residual = -counterpart_vals.pop('amount_residual', move_line.amount_residual if move_line else 0.0) \
if balance_to_reconcile is None else balance_to_reconcile
amount_residual_currency = -counterpart_vals.pop('amount_residual_currency', move_line.amount_residual_currency if move_line else 0.0)\
if balance_to_reconcile is None else balance_to_reconcile
if 'currency_id' in counterpart_vals:
currency_id = counterpart_vals['currency_id'] or company_currency.id
elif move_line:
currency_id = move_line.currency_id.id or company_currency.id
journal_amount = self.amount
if foreign_currency == journal_currency:
transaction_amount = journal_amount
else:
currency_id = foreign_currency.id
if currency_id not in (foreign_currency.id, journal_currency.id):
currency_id = company_currency.id
amount_residual_currency = 0.0
amounts = {
company_currency.id: 0.0,
journal_currency.id: 0.0,
foreign_currency.id: 0.0,
}
amounts[currency_id] = amount_residual_currency
amounts[company_currency.id] = amount_residual
if currency_id == journal_currency.id and journal_currency != company_currency:
if foreign_currency != company_currency:
amounts[company_currency.id] = journal_currency._convert(amounts[currency_id], company_currency, journal.company_id, self.date)
if statement_line_rate:
amounts[foreign_currency.id] = amounts[currency_id] * statement_line_rate
elif currency_id == foreign_currency.id and self.foreign_currency_id:
if statement_line_rate:
amounts[journal_currency.id] = amounts[foreign_currency.id] / statement_line_rate
if foreign_currency != company_currency:
amounts[company_currency.id] = journal_currency._convert(amounts[journal_currency.id], company_currency, journal.company_id, self.date)
transaction_amount = self.amount_currency
if journal_currency == company_currency:
company_amount = journal_amount
elif foreign_currency == company_currency:
company_amount = transaction_amount
else:
amounts[journal_currency.id] = company_currency._convert(amounts[company_currency.id], journal_currency, journal.company_id, self.date)
if statement_line_rate:
amounts[foreign_currency.id] = amounts[journal_currency.id] * statement_line_rate
company_amount = journal_currency._convert(journal_amount, company_currency, journal.company_id, self.date)
if foreign_currency == company_currency and journal_currency != company_currency and self.foreign_currency_id:
balance = amounts[foreign_currency.id]
else:
balance = amounts[company_currency.id]
rate_journal2foreign_curr = journal_amount and abs(transaction_amount) / abs(journal_amount)
rate_comp2journal_curr = company_amount and abs(journal_amount) / abs(company_amount)
if foreign_currency != company_currency and self.foreign_currency_id:
amount_currency = amounts[foreign_currency.id]
currency_id = foreign_currency.id
elif journal_currency != company_currency and not self.foreign_currency_id:
amount_currency = amounts[journal_currency.id]
currency_id = journal_currency.id
if currency == foreign_currency:
trans_amount_currency = amount_currency
if rate_journal2foreign_curr:
journ_amount_currency = journal_currency.round(trans_amount_currency / rate_journal2foreign_curr)
else:
journ_amount_currency = 0.0
if rate_comp2journal_curr:
new_balance = company_currency.round(journ_amount_currency / rate_comp2journal_curr)
else:
new_balance = 0.0
elif currency == journal_currency:
trans_amount_currency = foreign_currency.round(amount_currency * rate_journal2foreign_curr)
if rate_comp2journal_curr:
new_balance = company_currency.round(amount_currency / rate_comp2journal_curr)
else:
new_balance = 0.0
else:
amount_currency = amounts[company_currency.id]
currency_id = company_currency.id
journ_amount_currency = journal_currency.round(balance * rate_comp2journal_curr)
trans_amount_currency = foreign_currency.round(journ_amount_currency * rate_journal2foreign_curr)
new_balance = balance
return {
**counterpart_vals,
'name': counterpart_vals.get('name', move_line.name if move_line else ''),
'move_id': self.move_id.id,
'partner_id': self.partner_id.id or counterpart_vals.get('partner_id', move_line.partner_id.id if move_line else False),
'currency_id': currency_id,
'account_id': counterpart_vals.get('account_id', move_line.account_id.id if move_line else False),
'debit': balance if balance > 0.0 else 0.0,
'credit': -balance if balance < 0.0 else 0.0,
'amount_currency': amount_currency,
'amount_currency': trans_amount_currency,
'balance': new_balance,
}
@api.model
def _prepare_move_line_default_vals(self, counterpart_account_id=None):
''' Prepare the dictionary to create the default account.move.lines for the current account.bank.statement.line
record.
@@ -737,44 +655,49 @@ class AccountBankStatementLine(models.Model):
if not counterpart_account_id:
raise UserError(_(
"You can't create a new statement line without a suspense account set on the %s journal."
) % self.journal_id.display_name)
"You can't create a new statement line without a suspense account set on the %s journal.",
self.journal_id.display_name,
))
liquidity_line_vals = self._prepare_liquidity_move_line_vals()
journal = self.journal_id
company_currency = journal.company_id.currency_id
journal_currency = journal.currency_id or company_currency
foreign_currency = self.foreign_currency_id or journal_currency or company_currency
# Ensure the counterpart will have a balance exactly equals to the amount in journal currency.
# This avoid some rounding issues when the currency rate between two currencies is not symmetrical.
# E.g:
# A.convert(amount_a, B) = amount_b
# B.convert(amount_b, A) = amount_c != amount_a
journal_amount = self.amount
if foreign_currency == journal_currency:
transaction_amount = journal_amount
else:
transaction_amount = self.amount_currency
if journal_currency == company_currency:
company_amount = journal_amount
elif foreign_currency == company_currency:
company_amount = transaction_amount
else:
company_amount = journal_currency._convert(journal_amount, company_currency, journal.company_id, self.date)
counterpart_vals = {
liquidity_line_vals = {
'name': self.payment_ref,
'account_id': counterpart_account_id,
'amount_residual': liquidity_line_vals['debit'] - liquidity_line_vals['credit'],
'move_id': self.move_id.id,
'partner_id': self.partner_id.id,
'account_id': journal.default_account_id.id,
'currency_id': journal_currency.id,
'amount_currency': journal_amount,
'debit': company_amount > 0 and company_amount or 0.0,
'credit': company_amount < 0 and -company_amount or 0.0,
}
if self.foreign_currency_id and self.foreign_currency_id != self.company_currency_id:
# Ensure the counterpart will have exactly the same amount in foreign currency as the amount set in the
# statement line to avoid some rounding issues when making a currency conversion.
counterpart_vals.update({
'currency_id': self.foreign_currency_id.id,
'amount_residual_currency': self.amount_currency,
})
elif liquidity_line_vals['currency_id']:
# Ensure the counterpart will have a balance exactly equals to the amount in journal currency.
# This avoid some rounding issues when the currency rate between two currencies is not symmetrical.
# E.g:
# A.convert(amount_a, B) = amount_b
# B.convert(amount_b, A) = amount_c != amount_a
counterpart_vals.update({
'currency_id': liquidity_line_vals['currency_id'],
'amount_residual_currency': liquidity_line_vals['amount_currency'],
})
counterpart_line_vals = self._prepare_counterpart_move_line_vals(counterpart_vals)
# Create the counterpart line values.
counterpart_line_vals = {
'name': self.payment_ref,
'account_id': counterpart_account_id,
'move_id': self.move_id.id,
'partner_id': self.partner_id.id,
'currency_id': foreign_currency.id,
'amount_currency': -transaction_amount,
'debit': -company_amount if company_amount < 0.0 else 0.0,
'credit': company_amount if company_amount > 0.0 else 0.0,
}
return [liquidity_line_vals, counterpart_line_vals]
# -------------------------------------------------------------------------
@@ -1013,246 +936,131 @@ class AccountBankStatementLine(models.Model):
st_line_vals['partner_id'] = st_line.partner_id.id
st_line.move_id.write(st_line_vals)
# -------------------------------------------------------------------------
# RECONCILIATION METHODS
# -------------------------------------------------------------------------
def _prepare_reconciliation(self, lines_vals_list, allow_partial=False):
''' Helper for the "reconcile" method used to get a full preview of the reconciliation result. This method is
quite useful to deal with reconcile models or the reconciliation widget because it ensures the values seen by
the user are exactly the values you get after reconciling.
:param lines_vals_list: See the 'reconcile' method.
:param allow_partial: In case of matching a line having an higher amount, allow creating a partial instead
an open balance on the statement line.
:return: The diff to be applied on the statement line as a tuple
(
lines_to_create: The values to create the account.move.line on the statement line.
payments_to_create: The values to create the account.payments.
open_balance_vals: A dictionary to create the open-balance line or None if the reconciliation is full.
existing_lines: The counterpart lines to which the reconciliation will be done.
)
'''
self.ensure_one()
journal = self.journal_id
company_currency = journal.company_id.currency_id
foreign_currency = self.foreign_currency_id or journal.currency_id or company_currency
liquidity_lines, suspense_lines, other_lines = self._seek_for_lines()
# Ensure the statement line has not yet been already reconciled.
# If the move has 'to_check' enabled, it means the statement line has created some lines that
# need to be checked later and replaced by the real ones.
if not self.move_id.to_check and other_lines:
raise UserError(_("The statement line has already been reconciled."))
# A list of dictionary containing:
# - line_vals: The values to create the account.move.line on the statement line.
# - payment_vals: The optional values to create a bridge account.payment
# - counterpart_line: The optional counterpart line to reconcile with 'line'.
reconciliation_overview = []
total_balance = liquidity_lines.balance
total_amount_currency = liquidity_lines.amount_currency
sign = 1 if liquidity_lines.balance > 0.0 else -1
# Step 1: Split 'lines_vals_list' into two batches:
# - The existing account.move.lines that need to be reconciled with the statement line.
# => Will be managed at step 2.
# - The account.move.lines to be created from scratch.
# => Will be managed directly.
# In case of the payment is matched directly with an higher amount, don't create an open
# balance but a partial reconciliation.
partial_rec_needed = allow_partial
to_browse_ids = []
to_process_vals = []
for vals in lines_vals_list:
# Don't modify the params directly.
vals = dict(vals)
if 'id' in vals:
# Existing account.move.line.
to_browse_ids.append(vals.pop('id'))
to_process_vals.append(vals)
if any(x in vals for x in ('balance', 'amount_residual', 'amount_residual_currency')):
partial_rec_needed = False
else:
# Newly created account.move.line from scratch.
line_vals = self._prepare_counterpart_move_line_vals(vals)
total_balance += line_vals['debit'] - line_vals['credit']
total_amount_currency += line_vals['amount_currency']
reconciliation_overview.append({'line_vals': line_vals})
partial_rec_needed = False
# Step 2: Browse counterpart lines all in one and process them.
existing_lines = self.env['account.move.line'].browse(to_browse_ids)
i = 0
for line, counterpart_vals in zip(existing_lines, to_process_vals):
line_vals = self._prepare_counterpart_move_line_vals(counterpart_vals, move_line=line)
balance = line_vals['debit'] - line_vals['credit']
amount_currency = line_vals['amount_currency']
i += 1
if i == len(existing_lines):
# Last line.
if partial_rec_needed and sign * (total_amount_currency + amount_currency) < 0.0:
# On the last aml, when the total matched amount becomes higher than the residual amount of the
# statement line, make sure to not create an open balance later.
line_vals = self._prepare_counterpart_move_line_vals(
{
**counterpart_vals,
'amount_residual': -math.copysign(total_balance, balance),
'amount_residual_currency': -math.copysign(total_amount_currency, amount_currency),
'currency_id': foreign_currency.id,
},
move_line=line,
)
balance = line_vals['debit'] - line_vals['credit']
amount_currency = line_vals['amount_currency']
elif sign * total_amount_currency < 0.0:
# The partial reconciliation is no longer an option since the total matched amount is now higher than
# the residual amount of the statement line but this is not the last line to process. Then, since we
# don't want to create zero balance lines, do nothing and let the open-balance be created like it
# should.
partial_rec_needed = False
total_balance += balance
total_amount_currency += amount_currency
reconciliation_overview.append({
'line_vals': line_vals,
'counterpart_line': line,
})
# Step 3: Fix rounding issue due to currency conversions.
# Add the remaining balance on the first encountered line starting with the custom ones.
if foreign_currency.is_zero(total_amount_currency) and not company_currency.is_zero(total_balance):
vals = reconciliation_overview[0]['line_vals']
new_balance = vals['debit'] - vals['credit'] - total_balance
vals.update({
'debit': new_balance if new_balance > 0.0 else 0.0,
'credit': -new_balance if new_balance < 0.0 else 0.0,
})
total_balance = 0.0
# Step 4: If the journal entry is not yet balanced, create an open balance.
if self.company_currency_id.round(total_balance):
counterpart_vals = {
'name': '%s: %s' % (self.payment_ref, _('Open Balance')),
'balance': -total_balance,
'currency_id': self.company_currency_id.id,
}
partner = self.partner_id or existing_lines.mapped('partner_id')[:1]
if partner:
if self.amount > 0:
open_balance_account = partner.with_company(self.company_id).property_account_receivable_id
else:
open_balance_account = partner.with_company(self.company_id).property_account_payable_id
counterpart_vals['account_id'] = open_balance_account.id
counterpart_vals['partner_id'] = partner.id
else:
if self.amount > 0:
open_balance_account = self.company_id.partner_id.with_company(self.company_id).property_account_receivable_id
else:
open_balance_account = self.company_id.partner_id.with_company(self.company_id).property_account_payable_id
counterpart_vals['account_id'] = open_balance_account.id
open_balance_vals = self._prepare_counterpart_move_line_vals(counterpart_vals)
else:
open_balance_vals = None
return reconciliation_overview, open_balance_vals
def reconcile(self, lines_vals_list, to_check=False, allow_partial=False):
''' Perform a reconciliation on the current account.bank.statement.line with some
counterpart account.move.line.
If the statement line entry is not fully balanced after the reconciliation, an open balance will be created
using the partner.
:param lines_vals_list: A list of python dictionary containing:
'id': Optional id of an existing account.move.line.
For each line having an 'id', a new line will be created in the current statement line.
'balance': Optional amount to consider during the reconciliation. If a foreign currency is set on the
counterpart line in the same foreign currency as the statement line, then this amount is
considered as the amount in foreign currency. If not specified, the full balance is taken.
This value must be provided if 'id' is not.
**kwargs: Custom values to be set on the newly created account.move.line.
:param to_check: Mark the current statement line as "to_check" (see field for more details).
:param allow_partial: In case of matching a line having an higher amount, allow creating a partial instead
of an open balance on the statement line.
'''
self.ensure_one()
liquidity_lines, suspense_lines, other_lines = self._seek_for_lines()
reconciliation_overview, open_balance_vals = self._prepare_reconciliation(
lines_vals_list,
allow_partial=allow_partial,
)
# ==== Manage res.partner.bank ====
if self.account_number and self.partner_id and not self.partner_bank_id:
self.partner_bank_id = self._find_or_create_bank_account()
# ==== Check open balance ====
if open_balance_vals:
if not open_balance_vals.get('partner_id'):
raise UserError(_("Unable to create an open balance for a statement line without a partner set."))
if not open_balance_vals.get('account_id'):
raise UserError(_("Unable to create an open balance for a statement line because the receivable "
"/ payable accounts are missing on the partner."))
# ==== Create & reconcile lines on the bank statement line ====
to_create_commands = [(0, 0, open_balance_vals)] if open_balance_vals else []
to_delete_commands = [(2, line.id) for line in suspense_lines + other_lines]
# Cleanup previous lines.
self.move_id.with_context(check_move_validity=False, skip_account_move_synchronization=True, force_delete=True).write({
'line_ids': to_delete_commands + to_create_commands,
'to_check': to_check,
})
line_vals_list = [reconciliation_vals['line_vals'] for reconciliation_vals in reconciliation_overview]
new_lines = self.env['account.move.line'].create(line_vals_list)
new_lines = new_lines.with_context(skip_account_move_synchronization=True)
for reconciliation_vals, line in zip(reconciliation_overview, new_lines):
if reconciliation_vals.get('counterpart_line'):
counterpart_line = reconciliation_vals['counterpart_line']
else:
continue
(line + counterpart_line).reconcile()
# Assign partner if needed (for example, when reconciling a statement
# line with no partner, with an invoice; assign the partner of this invoice)
if not self.partner_id:
rec_overview_partners = set(overview['counterpart_line'].partner_id.id
for overview in reconciliation_overview
if overview.get('counterpart_line'))
if len(rec_overview_partners) == 1 and rec_overview_partners != {False}:
self.line_ids.write({'partner_id': rec_overview_partners.pop()})
# Refresh analytic lines.
self.move_id.line_ids.analytic_line_ids.unlink()
self.move_id.line_ids.create_analytic_lines()
# -------------------------------------------------------------------------
# BUSINESS METHODS
# -------------------------------------------------------------------------
def _get_st_line_strings_for_matching(self, allowed_fields=None):
""" Collect the strings that could be used on the statement line to perform some matching.
:param allowed_fields: A explicit list of fields to consider.
:return: A list of strings.
"""
self.ensure_one()
def _get_text_value(field_name):
if self._fields[field_name].type == 'html':
return html2plaintext(self[field_name])
else:
return self[field_name]
st_line_text_values = []
if allowed_fields is None or 'payment_ref' in allowed_fields:
value = _get_text_value('payment_ref')
if value:
st_line_text_values.append(value)
if allowed_fields is None or 'narration' in allowed_fields:
value = _get_text_value('narration')
if value:
st_line_text_values.append(value)
if allowed_fields is None or 'ref' in allowed_fields:
value = _get_text_value('ref')
if value:
st_line_text_values.append(value)
return st_line_text_values
def _get_default_amls_matching_domain(self):
return [
# Base domain.
('display_type', 'not in', ('line_section', 'line_note')),
('move_id.state', '=', 'posted'),
('company_id', '=', self.company_id.id),
# Reconciliation domain.
('reconciled', '=', False),
('account_id.reconcile', '=', True),
# Special domain for payments.
'|',
('account_id.internal_type', 'not in', ('receivable', 'payable')),
('payment_id', '=', False),
# Special domain for statement lines.
('statement_line_id', '!=', self.id),
]
def _retrieve_partner(self):
self.ensure_one()
# Retrieve the partner from the statement line.
if self.partner_id:
return self.partner_id
# Retrieve the partner from the bank account.
if self.account_number:
account_number_nums = sanitize_account_number(self.account_number)
if account_number_nums:
domain = [('sanitized_acc_number', 'ilike', account_number_nums)]
for extra_domain in ([('company_id', '=', self.company_id.id)], []):
bank_accounts = self.env['res.partner.bank'].search(extra_domain + domain)
if len(bank_accounts.partner_id) == 1:
return bank_accounts.partner_id
# Retrieve the partner from the partner name.
if self.partner_name:
domain = [
('parent_id', '=', False),
('name', 'ilike', self.partner_name),
]
for extra_domain in ([('company_id', '=', self.company_id.id)], []):
partner = self.env['res.partner'].search(extra_domain + domain, limit=1)
if partner:
return partner
# Retrieve the partner from the reconcile models.
rec_models = self.env['account.reconcile.model'].search([
('rule_type', '!=', 'writeoff_button'),
('company_id', '=', self.company_id.id),
])
for rec_model in rec_models:
partner = rec_model._get_partner_from_mapping(self)
if partner and rec_model._is_applicable_for(self, partner):
return partner
# Retrieve the partner from statement line text values.
st_line_text_values = self._get_st_line_strings_for_matching()
unaccent = get_unaccent_wrapper(self._cr)
sub_queries = []
params = []
for text_value in st_line_text_values:
if not text_value:
continue
# Find a partner having a name contained inside the statement line values.
# Take care a partner could contain some special characters in its name that needs to be escaped.
sub_queries.append(rf'''
{unaccent("%s")} ~* ('^' || (
SELECT STRING_AGG(CONCAT('(?=.*\m', chunk[1], '\M)'), '')
FROM regexp_matches({unaccent('name')}, '\w{{3,}}', 'g') AS chunk
))
''')
params.append(text_value)
if sub_queries:
self.env['res.partner'].flush_model(['company_id', 'name'])
self._cr.execute(
'''
SELECT id
FROM res_partner
WHERE (company_id IS NULL OR company_id = %s)
AND name IS NOT NULL
AND (''' + ') OR ('.join(sub_queries) + ''')
''',
[self.company_id.id] + params,
)
rows = self._cr.fetchall()
if len(rows) == 1:
return self.env['res.partner'].browse(rows[0][0])
return self.env['res.partner']
def _find_or_create_bank_account(self):
bank_account = self.env['res.partner.bank'].search([
('acc_number', '=', self.account_number),
@@ -486,10 +486,11 @@ class account_journal(models.Model):
def to_check_ids(self):
self.ensure_one()
domain = self.env['account.move.line']._get_suspense_moves_domain()
domain.append(('journal_id', '=', self.id))
statement_line_ids = self.env['account.move.line'].search(domain).mapped('statement_line_id')
return statement_line_ids
return self.env['account.bank.statement.line'].search([
('journal_id', '=', self.id),
('move_id.to_check', '=', True),
('move_id.state', '=', 'posted'),
])
def _select_action_to_open(self):
self.ensure_one()
+31 -45
View File
@@ -368,9 +368,6 @@ class AccountMove(models.Model):
invoice_filter_type_domain = fields.Char(compute='_compute_invoice_filter_type_domain',
help="Technical field used to have a dynamic domain on journal / taxes in the form view.")
bank_partner_id = fields.Many2one('res.partner', help='Technical field to get the domain on the bank', compute='_compute_bank_partner_id')
invoice_has_matching_suspense_amount = fields.Boolean(compute='_compute_has_matching_suspense_amount',
groups='account.group_account_invoice,account.group_account_readonly',
help="Technical field used to display an alert on invoices if there is at least a matching amount in any supsense account.")
tax_lock_date_message = fields.Char(
compute='_compute_tax_lock_date_message',
help="Technical field used to display a message when the invoice's accounting date is prior of the tax lock date.")
@@ -1531,33 +1528,6 @@ class AccountMove(models.Model):
move.write({'line_ids': to_write})
def _get_domain_matching_suspense_moves(self):
self.ensure_one()
domain = self.env['account.move.line']._get_suspense_moves_domain()
domain += ['|', ('partner_id', '=?', self.partner_id.id), ('partner_id', '=', False)]
if self.is_inbound():
domain.append(('balance', '=', -self.amount_residual))
else:
domain.append(('balance', '=', self.amount_residual))
return domain
def _compute_has_matching_suspense_amount(self):
for r in self:
res = False
if r.state == 'posted' and r.is_invoice() and r.payment_state == 'not_paid':
domain = r._get_domain_matching_suspense_moves()
#there are more than one but less than 5 suspense moves matching the residual amount
if (0 < self.env['account.move.line'].search_count(domain) < 5):
domain2 = [
('payment_state', '=', 'not_paid'),
('state', '=', 'posted'),
('amount_residual', '=', r.amount_residual),
('move_type', '=', r.move_type)]
#there are less than 5 other open invoices of the same type with the same residual
if self.env['account.move'].search_count(domain2) < 5:
res = True
r.invoice_has_matching_suspense_amount = res
@api.depends('partner_id', 'invoice_source_email', 'partner_id.name')
def _compute_invoice_partner_display_info(self):
for move in self:
@@ -2904,12 +2874,6 @@ class AccountMove(models.Model):
move.to_check = False
def button_draft(self):
AccountMoveLine = self.env['account.move.line']
excluded_move_ids = []
if self._context.get('suspense_moves_mode'):
excluded_move_ids = AccountMoveLine.search(AccountMoveLine._get_suspense_moves_domain() + [('move_id', 'in', self.ids)]).mapped('move_id').ids
for move in self:
if move in move.line_ids.mapped('full_reconcile_id.exchange_move_id'):
raise UserError(_('You cannot reset to draft an exchange difference journal entry.'))
@@ -2920,7 +2884,7 @@ class AccountMove(models.Model):
# so we also check tax_cash_basis_origin_move_id, which stays unchanged
# (we need both, as tax_cash_basis_origin_move_id did not exist in older versions).
raise UserError(_('You cannot reset to draft a tax cash basis journal entry.'))
if move.restrict_mode_hash_table and move.state == 'posted' and move.id not in excluded_move_ids:
if move.restrict_mode_hash_table and move.state == 'posted':
raise UserError(_('You cannot modify a posted entry of this journal because it is in strict mode.'))
# We remove all the analytics entries for this journal
move.mapped('line_ids.analytic_line_ids').unlink()
@@ -3395,7 +3359,7 @@ class AccountMove(models.Model):
class AccountMoveLine(models.Model):
_name = "account.move.line"
_description = "Journal Item"
_order = "date desc, move_name desc, id"
_order = "date desc, move_name desc, sequence, id"
_check_company_auto = True
# ==== Business fields ====
@@ -5783,13 +5747,35 @@ class AccountMoveLine(models.Model):
action['context'] = ctx
return action
@api.model
def _get_suspense_moves_domain(self):
return [
('move_id.to_check', '=', True),
('full_reconcile_id', '=', False),
('statement_line_id', '!=', False),
]
def action_open_business_doc_from_aml(self):
self.ensure_one()
if self.payment_id:
name = _("Payment")
res_model = 'account.payment'
res_id = self.payment_id.id
elif self.statement_line_id:
name = _("Bank Transaction")
res_model = 'account.bank.statement.line'
res_id = self.statement_line_id.id
else:
name = _("Journal Entry")
res_model = 'account.move'
res_id = self.move_id.id
return {
'name': name,
'type': 'ir.actions.act_window',
'view_mode': 'form',
'view_id': self.env.ref('account.view_move_form').id,
'res_model': res_model,
'res_id': res_id,
'context': {
'create': False,
'delete': False,
},
'target': 'current',
}
def _get_attachment_domains(self):
self.ensure_one()
File diff suppressed because it is too large Load Diff
+27 -5
View File
@@ -254,6 +254,32 @@ class ResCompany(models.Model):
if account.code.startswith(old_code):
account.write({'code': self.get_new_account_code(account.code, old_code, new_code)})
def _get_fiscalyear_lock_statement_lines_redirect_action(self, unreconciled_statement_lines):
""" Get the action redirecting to the statement lines that are not already reconciled when setting a fiscal
year lock date.
:param unreconciled_statement_lines: The statement lines.
:return: A dictionary representing a window action.
"""
statements = unreconciled_statement_lines.statement_id
action = {
'name': _("Unreconciled Statements"),
'type': 'ir.actions.act_window',
'res_model': 'account.bank.statement',
'context': {'create': False},
}
if len(statements) == 1:
action.update({
'view_mode': 'form',
'res_id': statements.id,
})
else:
action.update({
'view_mode': 'list,form',
'domain': [('id', 'in', statements.ids)],
})
return action
def _validate_fiscalyear_lock(self, values):
if values.get('fiscalyear_lock_date'):
@@ -283,11 +309,7 @@ class ResCompany(models.Model):
if unreconciled_statement_lines:
error_msg = _("There are still unreconciled bank statement lines in the period you want to lock."
"You should either reconcile or delete them.")
action_error = {
'type': 'ir.actions.client',
'tag': 'bank_statement_reconciliation_view',
'context': {'statement_line_ids': unreconciled_statement_lines.ids, 'company_ids': self.ids},
}
action_error = self._get_fiscalyear_lock_statement_lines_redirect_action(unreconciled_statement_lines)
raise RedirectWarning(error_msg, action_error, _('Show Unreconciled Bank Statement Line'))
def _get_user_fiscal_lock_date(self):
@@ -3,9 +3,7 @@ from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged
from odoo.tests.common import Form
from odoo.exceptions import ValidationError, UserError
from odoo import fields
from unittest.mock import patch
from odoo import fields, Command
class TestAccountBankStatementCommon(AccountTestInvoicingCommon):
@@ -824,622 +822,54 @@ class TestAccountBankStatementLine(TestAccountBankStatementCommon):
},
])
# -------------------------------------------------------------------------
# TESTS about reconciliation:
# - Test '_prepare_counterpart_move_line_vals': one test for each case.
# - Test 'reconcile': 3 cases:
# - Open-balance in debit.
# - Open-balance in credit.
# - No open-balance.
# - Test 'button_undo_reconciliation'.
# -------------------------------------------------------------------------
def test_prepare_counterpart_amounts_using_st_line_rate(self):
def _test_statement_line_reconciliation(
self,
journal,
amount, amount_currency, counterpart_amount,
journal_currency, foreign_currency, counterpart_currency,
expected_liquidity_values, expected_counterpart_values):
''' Test the reconciliation of a statement line.
:param journal: The account.journal record that will be set on the statement line.
:param amount: The amount in journal's currency.
:param amount_currency: The amount in the foreign currency.
:param counterpart_amount: The amount of the invoice to reconcile.
:param journal_currency: The journal's currency as a res.currency record.
:param foreign_currency: The foreign currency as a res.currency record.
:param counterpart_currency: The invoice currency as a res.currency record.
:param expected_liquidity_values: The expected account.move.line values for the liquidity line.
:param expected_counterpart_values: The expected account.move.line values for the counterpart line.
'''
if journal_currency:
journal.currency_id = journal_currency.id
def assertAppliedRate(
journal_currency, foreign_currency, aml_currency,
amount, amount_currency, aml_amount_currency, aml_balance,
expected_amount_currency, expected_balance,
):
journal = self.bank_journal_1.copy()
journal.currency_id = journal_currency
statement = self.env['account.bank.statement'].create({
'name': 'test_statement',
'date': '2019-01-01',
'journal_id': journal.id,
'line_ids': [
(0, 0, {
'date': '2019-01-01',
'payment_ref': 'line_1',
'partner_id': self.partner_a.id,
'foreign_currency_id': foreign_currency and foreign_currency.id,
'amount': amount,
'amount_currency': amount_currency,
}),
],
})
statement_line = statement.line_ids
# - There is 3 flows to check:
# * The invoice will fully reconcile the statement line.
# * The invoice will partially reconcile the statement line and leads to an open balance in debit.
# * The invoice will partially reconcile the statement line and leads to an open balance in credit.
# - The dates are different to be sure the reconciliation will preserve the conversion rate bank side.
move_type = 'out_invoice' if counterpart_amount < 0.0 else 'in_invoice'
test_invoices = self.env['account.move'].create([
{
'move_type': move_type,
'invoice_date': fields.Date.from_string('2016-01-01'),
'date': fields.Date.from_string('2016-01-01'),
'partner_id': self.partner_a.id,
'currency_id': counterpart_currency.id,
'invoice_line_ids': [
(0, None, {
'name': 'counterpart line, same amount',
'account_id': self.company_data['default_account_revenue'].id,
'quantity': 1,
'price_unit': abs(counterpart_amount),
statement = self.env['account.bank.statement'].create({
'name': 'test_statement',
'date': '2019-01-01',
'journal_id': journal.id,
'line_ids': [
Command.create({
'date': '2019-01-01',
'payment_ref': 'test_prepare_counterpart_amounts_using_st_line_rate',
'foreign_currency_id': foreign_currency.id if foreign_currency != journal_currency else None,
'amount': amount,
'amount_currency': amount_currency if foreign_currency != journal_currency else 0.0,
}),
],
},
{
'move_type': move_type,
'invoice_date': fields.Date.from_string('2016-01-01'),
'date': fields.Date.from_string('2016-01-01'),
'partner_id': self.partner_a.id,
'currency_id': counterpart_currency.id,
'invoice_line_ids': [
(0, None, {
'name': 'counterpart line, lower amount',
'account_id': self.company_data['default_account_revenue'].id,
'quantity': 1,
'price_unit': abs(counterpart_amount / 2),
}),
],
},
{
'move_type': move_type,
'invoice_date': fields.Date.from_string('2016-01-01'),
'date': fields.Date.from_string('2016-01-01'),
'partner_id': self.partner_a.id,
'currency_id': counterpart_currency.id,
'invoice_line_ids': [
(0, None, {
'name': 'counterpart line, bigger amount',
'account_id': self.company_data['default_account_revenue'].id,
'quantity': 1,
'price_unit': abs(counterpart_amount * 2),
}),
],
},
])
test_invoices.action_post()
statement.button_post()
counterpart_lines = test_invoices.mapped('line_ids').filtered(lambda line: line.account_internal_type in ('receivable', 'payable'))
})
statement_line = statement.line_ids
# Check the full reconciliation.
statement_line.reconcile([{'id': counterpart_lines[0].id}])
liquidity_lines, suspense_lines, other_lines = statement_line._seek_for_lines()
self.assertRecordValues(liquidity_lines, [expected_liquidity_values])
self.assertRecordValues(other_lines, [expected_counterpart_values])
res = statement_line._prepare_counterpart_amounts_using_st_line_rate(aml_currency, -aml_balance, -aml_amount_currency)
self.assertAlmostEqual(res['amount_currency'], expected_amount_currency)
self.assertAlmostEqual(res['balance'], expected_balance)
# Check the reconciliation with partial lower amount.
statement_line.button_undo_reconciliation()
statement_line.reconcile([{'id': counterpart_lines[1].id}])
liquidity_lines, suspense_lines, other_lines = statement_line._seek_for_lines()
self.assertRecordValues(liquidity_lines, [expected_liquidity_values])
self.assertRecordValues(other_lines.sorted('balance', reverse=amount < 0.0), [
{
**expected_counterpart_values,
'debit': expected_counterpart_values.get('debit', 0.0) / 2,
'credit': expected_counterpart_values.get('credit', 0.0) / 2,
'amount_currency': expected_counterpart_values.get('amount_currency', 0.0) / 2,
},
{
'debit': expected_counterpart_values.get('debit', 0.0) / 2,
'credit': expected_counterpart_values.get('credit', 0.0) / 2,
'amount_currency': expected_counterpart_values.get('amount_currency', 0.0) / 2,
'currency_id': expected_counterpart_values.get('currency_id'),
},
])
# Check the reconciliation with partial higher amount.
statement_line.button_undo_reconciliation()
statement_line.reconcile([{'id': counterpart_lines[2].id}])
liquidity_lines, suspense_lines, other_lines = statement_line._seek_for_lines()
self.assertRecordValues(liquidity_lines, [expected_liquidity_values])
self.assertRecordValues(other_lines.sorted('balance', reverse=amount < 0.0), [
{
**expected_counterpart_values,
'debit': expected_counterpart_values.get('debit', 0.0) * 2,
'credit': expected_counterpart_values.get('credit', 0.0) * 2,
'amount_currency': expected_counterpart_values.get('amount_currency', 0.0) * 2,
},
{
'debit': expected_counterpart_values.get('credit', 0.0),
'credit': expected_counterpart_values.get('debit', 0.0),
'amount_currency': -expected_counterpart_values.get('amount_currency', 0.0),
'currency_id': expected_counterpart_values.get('currency_id'),
},
])
# Make sure the statement line is still correct.
self.assertRecordValues(statement_line, [{
'amount': amount,
'amount_currency': amount_currency,
}])
def _test_reconciliation_customer_and_supplier_flows(
self,
amount, amount_currency, counterpart_amount,
journal_currency, foreign_currency, counterpart_currency,
expected_liquidity_values, expected_counterpart_values):
''' Test '_test_statement_line_reconciliation' using the customer (positive amounts)
& the supplier flow (negative amounts).
:param amount: The amount in journal's currency.
:param amount_currency: The amount in the foreign currency.
:param counterpart_amount: The amount of the invoice to reconcile.
:param journal_currency: The journal's currency as a res.currency record.
:param foreign_currency: The foreign currency as a res.currency record.
:param counterpart_currency: The invoice currency as a res.currency record.
:param expected_liquidity_values: The expected account.move.line values for the liquidity line.
:param expected_counterpart_values: The expected account.move.line values for the counterpart line.
'''
# Check the full process with positive amount (customer process).
self._test_statement_line_reconciliation(
self.bank_journal_2,
amount, amount_currency, counterpart_amount,
journal_currency, foreign_currency, counterpart_currency,
expected_liquidity_values,
expected_counterpart_values,
)
# Check the full process with negative amount (supplier process).
self._test_statement_line_reconciliation(
self.bank_journal_3,
-amount, -amount_currency, -counterpart_amount,
journal_currency, foreign_currency, counterpart_currency,
{
**expected_liquidity_values,
'debit': expected_liquidity_values.get('credit', 0.0),
'credit': expected_liquidity_values.get('debit', 0.0),
'amount_currency': -expected_liquidity_values.get('amount_currency', 0.0),
},
{
**expected_counterpart_values,
'debit': expected_counterpart_values.get('credit', 0.0),
'credit': expected_counterpart_values.get('debit', 0.0),
'amount_currency': -expected_counterpart_values.get('amount_currency', 0.0),
},
)
def test_reconciliation_journal_curr_2_statement_curr_3_counterpart_curr_3(self):
self._test_reconciliation_customer_and_supplier_flows(
80.0, 120.0, -120.0,
self.currency_2, self.currency_3, self.currency_3,
{'debit': 40.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_2.id},
{'debit': 0.0, 'credit': 40.0, 'amount_currency': -120.0, 'currency_id': self.currency_3.id},
)
def test_reconciliation_journal_curr_2_statement_curr_1_counterpart_curr_2(self):
self._test_reconciliation_customer_and_supplier_flows(
120.0, 80.0, -120.0,
self.currency_2, self.currency_1, self.currency_2,
{'debit': 80.0, 'credit': 0.0, 'amount_currency': 120.0, 'currency_id': self.currency_2.id},
{'debit': 0.0, 'credit': 80.0, 'amount_currency': -80.0, 'currency_id': self.currency_1.id},
)
def test_reconciliation_journal_curr_2_statement_curr_3_counterpart_curr_2(self):
self._test_reconciliation_customer_and_supplier_flows(
80.0, 120.0, -80.0,
self.currency_2, self.currency_3, self.currency_2,
{'debit': 40.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_2.id},
{'debit': 0.0, 'credit': 40.0, 'amount_currency': -120.0, 'currency_id': self.currency_3.id},
)
def test_reconciliation_journal_curr_2_statement_curr_3_counterpart_curr_4(self):
self._test_reconciliation_customer_and_supplier_flows(
80.0, 120.0, -480.0,
self.currency_2, self.currency_3, self.currency_4,
{'debit': 40.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_2.id},
{'debit': 0.0, 'credit': 40.0, 'amount_currency': -120.0, 'currency_id': self.currency_3.id},
)
def test_reconciliation_journal_curr_1_statement_curr_2_counterpart_curr_2(self):
self._test_reconciliation_customer_and_supplier_flows(
# pylint: disable=C0326
80.0, 120.0, -120.0,
self.currency_1, self.currency_2, self.currency_2,
{'debit': 80.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_1.id},
{'debit': 0.0, 'credit': 80.0, 'amount_currency': -120.0, 'currency_id': self.currency_2.id},
)
def test_reconciliation_journal_curr_1_statement_curr_2_counterpart_curr_3(self):
self._test_reconciliation_customer_and_supplier_flows(
# pylint: disable=C0326
80.0, 120.0, -480.0,
self.currency_1, self.currency_2, self.currency_3,
{'debit': 80.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_1.id},
{'debit': 0.0, 'credit': 80.0, 'amount_currency': -120.0, 'currency_id': self.currency_2.id},
)
def test_reconciliation_journal_curr_2_statement_false_counterpart_curr_2(self):
self._test_reconciliation_customer_and_supplier_flows(
80.0, 0.0, -80.0,
self.currency_2, False, self.currency_2,
{'debit': 40.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_2.id},
{'debit': 0.0, 'credit': 40.0, 'amount_currency': -80.0, 'currency_id': self.currency_2.id},
)
def test_reconciliation_journal_curr_2_statement_false_counterpart_curr_3(self):
self._test_reconciliation_customer_and_supplier_flows(
80.0, 0.0, -240.0,
self.currency_2, False, self.currency_3,
{'debit': 40.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_2.id},
{'debit': 0.0, 'credit': 40.0, 'amount_currency': -80.0, 'currency_id': self.currency_2.id},
)
def test_reconciliation_journal_curr_1_statement_false_counterpart_curr_3(self):
self._test_reconciliation_customer_and_supplier_flows(
80.0, 0.0, -480.0,
self.currency_1, False, self.currency_3,
{'debit': 80.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_1.id},
{'debit': 0.0, 'credit': 80.0, 'amount_currency': -80.0, 'currency_id': self.currency_1.id},
)
def test_reconciliation_journal_curr_2_statement_curr_1_counterpart_curr_1(self):
self._test_reconciliation_customer_and_supplier_flows(
120.0, 80.0, -80.0,
self.currency_2, self.currency_1, self.currency_1,
{'debit': 80.0, 'credit': 0.0, 'amount_currency': 120.0, 'currency_id': self.currency_2.id},
{'debit': 0.0, 'credit': 80.0, 'amount_currency': -80.0, 'currency_id': self.currency_1.id},
)
def test_reconciliation_journal_curr_2_statement_curr_3_counterpart_curr_1(self):
self._test_reconciliation_customer_and_supplier_flows(
80.0, 120.0, -40.0,
self.currency_2, self.currency_3, self.currency_1,
{'debit': 40.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_2.id},
{'debit': 0.0, 'credit': 40.0, 'amount_currency': -120.0, 'currency_id': self.currency_3.id},
)
def test_reconciliation_journal_curr_1_statement_curr_2_counterpart_curr_1(self):
self._test_reconciliation_customer_and_supplier_flows(
# pylint: disable=C0326
80.0, 120.0, -80.0,
self.currency_1, self.currency_2, self.currency_1,
{'debit': 80.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_1.id},
{'debit': 0.0, 'credit': 80.0, 'amount_currency': -120.0, 'currency_id': self.currency_2.id},
)
def test_reconciliation_journal_curr_2_statement_false_counterpart_curr_1(self):
self._test_reconciliation_customer_and_supplier_flows(
80.0, 0.0, -40.0,
self.currency_2, False, self.currency_1,
{'debit': 40.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_2.id},
{'debit': 0.0, 'credit': 40.0, 'amount_currency': -80.0, 'currency_id': self.currency_2.id},
)
def test_reconciliation_journal_curr_1_statement_false_counterpart_curr_1(self):
self._test_reconciliation_customer_and_supplier_flows(
80.0, 0.0, -80.0,
self.currency_1, False, self.currency_1,
{'debit': 80.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_1.id},
{'debit': 0.0, 'credit': 80.0, 'amount_currency': -80.0, 'currency_id': self.currency_1.id},
)
def test_reconciliation_move_draft(self):
''' Ensures that the account move of a reconciled statement cannot be set to draft
'''
statement = self.env['account.bank.statement'].create({
'name': 'test_statement',
'date': '2019-01-01',
'journal_id': self.bank_journal_1.id,
'line_ids': [
(0, 0, {
'date': '2019-01-01',
'payment_ref': 'line_1',
'partner_id': self.partner_a.id,
'amount': 100.0,
}),
],
})
statement.button_post()
statement.line_ids[0].reconcile([{'name': 'test', 'account_id': self.company_data['default_account_revenue'].id, 'balance': -100}])
statement.button_validate_or_action()
account_move = statement.line_ids.move_id
with self.assertRaises(UserError):
account_move.button_draft()
def test_reconciliation_statement_line_state(self):
''' Test the reconciliation on the bank statement line with a foreign currency on the journal:
- Ensure the statement line is_reconciled field is well computed.
- Ensure the reconciliation is working well when dealing with a foreign currency at different dates.
- Ensure the reconciliation can be undo.
- Ensure the reconciliation is still possible with to_check.
'''
self.statement.button_post()
receivable_acc_1 = self.company_data['default_account_receivable']
receivable_acc_2 = self.copy_account(self.company_data['default_account_receivable'])
payment_account = self.bank_journal_1.company_id.account_journal_payment_debit_account_id
random_acc_1 = self.company_data['default_account_revenue']
random_acc_2 = self.copy_account(self.company_data['default_account_revenue'])
test_move = self.env['account.move'].create({
'move_type': 'entry',
'date': fields.Date.from_string('2016-01-01'),
'line_ids': [
(0, None, {
'name': 'counterpart of the whole move',
'account_id': random_acc_1.id,
'debit': 0.0,
'credit': 1030.0,
}),
(0, None, {
'name': 'test line 1 - receivable account',
'account_id': receivable_acc_1.id,
'currency_id': self.currency_2.id,
'debit': 500.0,
'credit': 0.0,
'amount_currency': 1500.0,
}),
(0, None, {
'name': 'test line 2 - another receivable account',
'account_id': receivable_acc_2.id,
'currency_id': self.currency_2.id,
'debit': 500.0,
'credit': 0.0,
'amount_currency': 1500.0,
}),
(0, None, {
'name': 'test line 3 - payment transfer account',
'account_id': payment_account.id,
'currency_id': self.currency_2.id,
'debit': 30.0,
'credit': 0.0,
'amount_currency': 90.0,
}),
]
})
test_move.action_post()
test_line_1 = test_move.line_ids.filtered(lambda line: line.account_id == receivable_acc_1)
test_line_2 = test_move.line_ids.filtered(lambda line: line.account_id == receivable_acc_2)
test_line_3 = test_move.line_ids.filtered(lambda line: line.account_id == payment_account)
self.statement_line.reconcile([
# test line 1
# Will reconcile 300.0 in balance, 600.0 in amount_currency.
{'id': test_line_1.id, 'balance': -600.0},
# test line 2
# Will reconcile 250.0 in balance, 500.0 in amount_currency.
{'id': test_line_2.id, 'balance': -500.0},
# test line 3
# Will reconcile 30.0 in balance, 90.0 in amount_currency.
{'id': test_line_3.id},
# test line 4
# Will reconcile 50.0 in balance, 100.0 in amount_currency.
{'name': 'whatever', 'account_id': random_acc_1.id, 'balance': -100.0},
])
self.assertBankStatementLine(self.statement_line, {
**self.expected_st_line,
'is_reconciled': True,
}, [
{
'name': '%s: Open Balance' % self.statement_line.payment_ref,
'partner_id': self.statement_line.partner_id.id,
'currency_id': self.currency_2.id,
'account_id': receivable_acc_1.id, # This account is retrieved on the partner.
'debit': 0.0,
'credit': 605.0,
'amount_currency': -1210.0,
'amount_residual': -605.0,
'amount_residual_currency': -1210.0,
},
{
'name': test_line_1.name,
'partner_id': self.statement_line.partner_id.id,
'currency_id': self.currency_2.id,
'account_id': receivable_acc_1.id,
'debit': 0.0,
'credit': 300.0,
'amount_currency': -600.0,
'amount_residual': 0.0,
'amount_residual_currency': 0.0,
},
{
'name': test_line_2.name,
'partner_id': self.statement_line.partner_id.id,
'currency_id': self.currency_2.id,
'account_id': receivable_acc_2.id,
'debit': 0.0,
'credit': 250.0,
'amount_currency': -500.0,
'amount_residual': 0.0,
'amount_residual_currency': 0.0,
},
{
'name': 'whatever',
'partner_id': self.statement_line.partner_id.id,
'currency_id': self.currency_2.id,
'account_id': random_acc_1.id,
'debit': 0.0,
'credit': 50.0,
'amount_currency': -100.0,
'amount_residual': 0.0,
'amount_residual_currency': 0.0,
},
{
'name': test_line_3.name,
'partner_id': self.statement_line.partner_id.id,
'currency_id': self.currency_2.id,
'account_id': test_line_3.account_id.id,
'debit': 0.0,
'credit': 45.0,
'amount_currency': -90.0,
'amount_residual': 0.0,
'amount_residual_currency': 0.0,
},
{
**self.expected_bank_line,
'amount_residual': 1250.0,
'amount_residual_currency': 1250.0,
},
])
# Undo the reconciliation to return to the initial state.
self.statement_line.button_undo_reconciliation()
self.assertBankStatementLine(self.statement_line, self.expected_st_line, [self.expected_counterpart_line, self.expected_bank_line])
# Modify the counterpart line with to_check enabled.
self.statement_line.reconcile([
{'name': 'whatever', 'account_id': random_acc_1.id, 'balance': -100.0},
], to_check=True)
self.assertBankStatementLine(self.statement_line, {
**self.expected_st_line,
'is_reconciled': True,
}, [
{
'name': '%s: Open Balance' % self.statement_line.payment_ref,
'partner_id': self.statement_line.partner_id.id,
'currency_id': self.currency_2.id,
'account_id': receivable_acc_1.id, # This account is retrieved on the partner.
'debit': 0.0,
'credit': 1200.0,
'amount_currency': -2400.0,
'amount_residual': -1200.0,
'amount_residual_currency': -2400.0,
},
{
'name': 'whatever',
'partner_id': self.statement_line.partner_id.id,
'currency_id': self.currency_2.id,
'account_id': random_acc_1.id,
'debit': 0.0,
'credit': 50.0,
'amount_currency': -100.0,
'amount_residual': 0.0,
'amount_residual_currency': 0.0,
},
{
**self.expected_bank_line,
'amount_residual': 1250.0,
'amount_residual_currency': 1250.0,
},
])
# Modify the counterpart line. Should be allowed by the to_check enabled.
self.statement_line.reconcile([
{'name': 'whatever again', 'account_id': random_acc_2.id, 'balance': -500.0},
])
self.assertBankStatementLine(self.statement_line, {
**self.expected_st_line,
'is_reconciled': True,
}, [
{
'name': '%s: Open Balance' % self.statement_line.payment_ref,
'partner_id': self.statement_line.partner_id.id,
'currency_id': self.currency_2.id,
'account_id': receivable_acc_1.id, # This account is retrieved on the partner.
'debit': 0.0,
'credit': 1000.0,
'amount_currency': -2000.0,
'amount_residual': -1000.0,
'amount_residual_currency': -2000.0,
},
{
'name': 'whatever again',
'partner_id': self.statement_line.partner_id.id,
'currency_id': self.currency_2.id,
'account_id': random_acc_2.id,
'debit': 0.0,
'credit': 250.0,
'amount_currency': -500.0,
'amount_residual': 0.0,
'amount_residual_currency': 0.0,
},
{
**self.expected_bank_line,
'amount_residual': 1250.0,
'amount_residual_currency': 1250.0,
},
])
# The statement line is no longer in the 'to_check' mode.
# Reconciling again should raise an error.
with self.assertRaises(UserError), self.cr.savepoint():
self.statement_line.reconcile([
{'name': 'whatever', 'account_id': random_acc_1.id, 'balance': -100.0},
])
def test_conversion_rate_rounding_issue(self):
''' Ensure the reconciliation is well handling the rounding issue due to multiple currency conversion rates.
In this test, the resulting journal entry after reconciliation is:
{'amount_currency': 7541.66, 'debit': 6446.97, 'credit': 0.0}
{'amount_currency': 226.04, 'debit': 193.22, 'credit': 0.0}
{'amount_currency': -7767.70, 'debit': 0.0, 'credit': 6640.19}
... but 226.04 / 1.1698 = 193.23. In this situation, 0.01 has been removed from this write-off line in order to
avoid an unecessary open-balance line being an exchange difference issue.
'''
self.bank_journal_2.currency_id = self.currency_2
self.currency_data['rates'][-1].rate = 1.1698
statement = self.env['account.bank.statement'].create({
'name': 'test_statement',
'date': '2017-01-01',
'journal_id': self.bank_journal_2.id,
'line_ids': [
(0, 0, {
'date': '2019-01-01',
'payment_ref': 'line_1',
'partner_id': self.partner_a.id,
'amount': 7541.66,
}),
],
})
statement.button_post()
statement_line = statement.line_ids
payment = self.env['account.payment'].create({
'amount': 7767.70,
'date': '2019-01-01',
'currency_id': self.currency_2.id,
'payment_type': 'inbound',
'partner_type': 'customer',
})
payment.action_post()
liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines()
self.assertRecordValues(liquidity_lines, [{'amount_currency': 7767.70}])
statement_line.reconcile([
{'id': liquidity_lines.id},
{'balance': 226.04, 'account_id': self.company_data['default_account_revenue'].id, 'name': "write-off"},
])
self.assertRecordValues(statement_line.line_ids, [
{'amount_currency': 7541.66, 'debit': 6446.97, 'credit': 0.0},
{'amount_currency': 226.04, 'debit': 193.22, 'credit': 0.0},
{'amount_currency': -7767.70, 'debit': 0.0, 'credit': 6640.19},
])
for params in (
(self.currency_2, self.currency_3, self.currency_3, 80.0, 120.0, 120.0, 20.0, -120.0, -40.0),
(self.currency_2, self.currency_1, self.currency_2, 120.0, 80.0, 120.0, 40.0, -80.0, -80.0),
(self.currency_2, self.currency_3, self.currency_2, 80.0, 120.0, 80.0, 26.67, -120.0, -40.0),
(self.currency_2, self.currency_3, self.currency_4, 80.0, 120.0, 480.0, 40.0, -120.0, -40.0),
(self.currency_1, self.currency_2, self.currency_2, 80.0, 120.0, 120.0, 40.0, -120.0, -80.0),
(self.currency_1, self.currency_2, self.currency_3, 80.0, 120.0, 480.0, 80.0, -120.0, -80.0),
(self.currency_2, self.currency_2, self.currency_2, 80.0, 80.0, 80.0, 26.67, -80.0, -40.0),
(self.currency_2, self.currency_2, self.currency_3, 80.0, 80.0, 240.0, 40.0, -80.0, -40.0),
(self.currency_1, self.currency_1, self.currency_3, 80.0, 80.0, 480.0, 80.0, -80.0, -80.0),
(self.currency_2, self.currency_1, self.currency_1, 120.0, 80.0, 80.0, 80.0, -80.0, -80.0),
(self.currency_2, self.currency_3, self.currency_1, 80.0, 120.0, 40.0, 40.0, -120.0, -40.0),
(self.currency_1, self.currency_2, self.currency_1, 80.0, 120.0, 80.0, 80.0, -120.0, -80.0),
(self.currency_2, self.currency_2, self.currency_1, 80.0, 80.0, 40.0, 40.0, -80.0, -40.0),
(self.currency_1, self.currency_1, self.currency_1, 80.0, 80.0, 80.0, 80.0, -80.0, -80.0),
):
with self.subTest(params=params):
assertAppliedRate(*params)
def test_zero_amount_statement_line(self):
''' Ensure the statement line is directly marked as reconciled when having an amount of zero. '''
@@ -1460,122 +890,3 @@ class TestAccountBankStatementLine(TestAccountBankStatementCommon):
statement_line = statement.line_ids
self.assertRecordValues(statement_line, [{'is_reconciled': True, 'amount_residual': 0.0}])
def test_bank_statement_line_analytic(self):
''' Ensure the analytic lines are generated during the reconciliation. '''
analytic_account = self.env['account.analytic.account'].create({'name': 'analytic_account'})
statement = self.env['account.bank.statement'].with_context(skip_check_amounts_currencies=True).create({
'name': 'test_statement',
'date': '2017-01-01',
'journal_id': self.bank_journal_2.id,
'line_ids': [
(0, 0, {
'date': '2019-01-01',
'payment_ref': "line",
'amount': 100.0,
}),
],
})
statement_line = statement.line_ids
statement_line.reconcile([{
'balance': -100.0,
'account_id': self.company_data['default_account_revenue'].id,
'name': "write-off",
'analytic_account_id': analytic_account.id,
}])
# Check the analytic account is there.
self.assertRecordValues(statement_line.line_ids.sorted('balance'), [
{'balance': -100.0, 'analytic_account_id': analytic_account.id},
{'balance': 100.0, 'analytic_account_id': False},
])
# Check the analytic lines.
self.assertRecordValues(statement_line.line_ids.analytic_line_ids, [
{'amount': 100.0, 'account_id': analytic_account.id},
])
def test_reconciliation_line_with_no_partner(self):
"""
Ensure that entry lines and statement line have no partner when reconciling
lines without partner with others with partner
"""
statement = self.env['account.bank.statement'].create({
'name': 'test_statement',
'date': '2019-01-01',
'journal_id': self.bank_journal_1.id,
'line_ids': [
(0, 0, {
'date': '2022-01-01',
'payment_ref': "Happy new year",
'amount': 200.0,
}),
],
})
statement.button_post()
partner = self.env['res.partner'].create({'name': 'test'})
receivable_account = self.company_data['default_account_receivable']
outstanding_account = self.env['account.account'].search([('code', '=', '101402'), ('company_id', '=', self.env.company.id)])
payments = self.env['account.payment'].create([
{
'name': 'Payment without partner',
'date': fields.Date.from_string('2022-01-01'),
'is_internal_transfer': False,
'amount': 100.0,
'payment_type': 'inbound',
'partner_type': 'customer',
'destination_account_id': receivable_account.id,
'journal_id': self.bank_journal_1.id,
},
{
'name': 'Payment with partner',
'date': fields.Date.from_string('2022-01-01'),
'is_internal_transfer': False,
'amount': 100.0,
'payment_type': 'inbound',
'partner_type': 'customer',
'partner_id': partner.id,
'destination_account_id': receivable_account.id,
'journal_id': self.bank_journal_1.id,
},
])
payments.action_post()
statement_line = statement.line_ids
statement_line.reconcile([
{'id': payments[0].move_id.line_ids.filtered(lambda line: line.account_id == outstanding_account).id},
{'id': payments[1].move_id.line_ids.filtered(lambda line: line.account_id == outstanding_account).id},
])
self.assertRecordValues(
statement.line_ids.move_id.line_ids,
[
{
'debit': 200.0,
'credit': 0.0,
'partner_id': False,
'account_id': self.bank_journal_1.default_account_id.id
},
{
'debit': 0.0,
'credit': 100.0,
'partner_id': False,
'account_id': outstanding_account.id
},
{
'debit': 0.0,
'credit': 100.0,
'partner_id': partner.id,
'account_id': outstanding_account.id
},
])
self.assertRecordValues(statement.line_ids, [{
'partner_id': False,
}])
+7 -2
View File
@@ -731,7 +731,7 @@ class TestAccountPayment(AccountTestInvoicingCommon):
payment.action_post()
invoice.action_post()
(counterpart_lines + invoice.line_ids.filtered(lambda line: line.account_internal_type == 'receivable'))\
(counterpart_lines + invoice.line_ids.filtered(lambda line: line.account_internal_type == 'receivable')) \
.reconcile()
self.assertRecordValues(payment, [{
@@ -753,7 +753,12 @@ class TestAccountPayment(AccountTestInvoicingCommon):
statement.button_post()
statement_line = statement.line_ids
statement_line.reconcile([{'id': liquidity_lines.id}])
# Reconcile without the bank reconciliation widget since the widget is in enterprise.
_st_liquidity_lines, st_suspense_lines, _st_other_lines = statement_line\
.with_context(skip_account_move_synchronization=True)\
._seek_for_lines()
st_suspense_lines.account_id = liquidity_lines.account_id
(st_suspense_lines + liquidity_lines).reconcile()
self.assertRecordValues(payment, [{
'is_reconciled': True,
@@ -218,6 +218,10 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
})
for i, line_vals in enumerate(kwargs.get('line_ids', []))
],
'partner_mapping_line_ids': [
Command.create(line_vals)
for i, line_vals in enumerate(kwargs.get('partner_mapping_line_ids', []))
],
})
def _post_statements(self):
@@ -227,76 +231,49 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
(self.bank_st + self.bank_st_2 + self.cash_st).button_post()
@freeze_time('2020-01-01')
def _check_statement_matching(self, rules, expected_values, statements=None):
if statements is None:
statements = self.bank_st + self.cash_st
statement_lines = statements.mapped('line_ids').sorted()
matching_values = rules._apply_rules(statement_lines, None)
for st_line_id, values in matching_values.items():
values.pop('reconciled_lines', None)
values.pop('write_off_vals', None)
self.assertDictEqual(values, expected_values[st_line_id])
def _check_statement_matching(self, rules, expected_values_list):
for statement_line, expected_values in expected_values_list.items():
res = rules._apply_rules(statement_line, statement_line._retrieve_partner())
self.assertDictEqual(res, expected_values)
def test_matching_fields(self):
# Check without restriction.
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id},
self.bank_line_2.id: {'aml_ids': [
self.invoice_line_2.id,
self.invoice_line_3.id,
self.invoice_line_1.id,
], 'model': self.rule_1,
'partner': self.bank_line_2.partner_id},
self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id},
self.bank_line_1: {'amls': self.invoice_line_1, 'model': self.rule_1},
self.bank_line_2: {'amls': self.invoice_line_1 + self.invoice_line_2 + self.invoice_line_3, 'model': self.rule_1},
self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1},
})
@freeze_time('2020-01-01')
def test_matching_fields_match_text_location(self):
self.rule_1.match_text_location_label = True
self.rule_1.match_text_location_reference = False
self.rule_1.match_text_location_note = False
self.rule_1.allow_payment_tolerance = False
self._check_statement_matching(self.rule_1, {
self.bank_line_3.id: {'aml_ids': [self.invoice_line_5.id], 'model': self.rule_1, 'partner': self.bank_line_3.partner_id},
self.bank_line_4.id: {'aml_ids': [self.invoice_line_7.id], 'model': self.rule_1, 'partner': self.bank_line_4.partner_id},
self.bank_line_5.id: {'aml_ids': [self.invoice_line_6.id], 'model': self.rule_1, 'partner': self.bank_line_5.partner_id},
}, statements=self.bank_st_2)
st_line = self._create_st_line(payment_ref="1111", ref="2222 3333", narration="4444 5555 6666")
self.rule_1.match_text_location_label = True
self.rule_1.match_text_location_reference = False
self.rule_1.match_text_location_note = True
self._check_statement_matching(self.rule_1, {
self.bank_line_3.id: {'aml_ids': [self.invoice_line_6.id], 'model': self.rule_1, 'partner': self.bank_line_3.partner_id},
self.bank_line_4.id: {'aml_ids': [self.invoice_line_7.id], 'model': self.rule_1, 'partner': self.bank_line_4.partner_id},
self.bank_line_5.id: {'aml_ids': [self.invoice_line_5.id], 'model': self.rule_1, 'partner': self.bank_line_5.partner_id},
}, statements=self.bank_st_2)
inv1 = self._create_invoice_line(1000, self.partner_a, 'out_invoice', pay_reference="bernard 1111 gagnant")
inv2 = self._create_invoice_line(1000, self.partner_a, 'out_invoice', pay_reference="2222 turlututu 3333")
inv3 = self._create_invoice_line(1000, self.partner_a, 'out_invoice', pay_reference="4444 tsoin 5555 tsoin 6666")
self.rule_1.match_text_location_label = True
self.rule_1.match_text_location_reference = True
self.rule_1.match_text_location_note = False
self._check_statement_matching(self.rule_1, {
self.bank_line_3.id: {'aml_ids': [self.invoice_line_5.id], 'model': self.rule_1, 'partner': self.bank_line_3.partner_id},
self.bank_line_4.id: {'aml_ids': [self.invoice_line_7.id], 'model': self.rule_1, 'partner': self.bank_line_4.partner_id},
self.bank_line_5.id: {'aml_ids': [self.invoice_line_7.id], 'model': self.rule_1, 'partner': self.bank_line_5.partner_id},
}, statements=self.bank_st_2)
rule = self._create_reconcile_model(
allow_payment_tolerance=False,
match_text_location_label=True,
match_text_location_reference=False,
match_text_location_note=False,
)
self.assertDictEqual(
rule._apply_rules(st_line, st_line._retrieve_partner()),
{'amls': inv1, 'model': rule},
)
self.rule_1.match_text_location_label = True
self.rule_1.match_text_location_reference = True
self.rule_1.match_text_location_note = True
self._check_statement_matching(self.rule_1, {
self.bank_line_3.id: {'aml_ids': [self.invoice_line_6.id], 'model': self.rule_1, 'partner': self.bank_line_3.partner_id},
self.bank_line_4.id: {'aml_ids': [self.invoice_line_7.id], 'model': self.rule_1, 'partner': self.bank_line_4.partner_id},
self.bank_line_5.id: {'aml_ids': [self.invoice_line_7.id], 'model': self.rule_1, 'partner': self.bank_line_5.partner_id},
}, statements=self.bank_st_2)
rule.match_text_location_reference = True
self.assertDictEqual(
rule._apply_rules(st_line, st_line._retrieve_partner()),
{'amls': inv2, 'model': rule},
)
self.rule_1.match_text_location_label = False
self.rule_1.match_text_location_reference = False
self.rule_1.match_text_location_note = False
self._check_statement_matching(self.rule_1, {
self.bank_line_3.id: {'aml_ids': [self.invoice_line_5.id], 'model': self.rule_1, 'partner': self.bank_line_3.partner_id},
self.bank_line_4.id: {'aml_ids': [self.invoice_line_5.id], 'model': self.rule_1, 'partner': self.bank_line_4.partner_id},
self.bank_line_5.id: {'aml_ids': [self.invoice_line_6.id], 'model': self.rule_1, 'partner': self.bank_line_5.partner_id},
}, statements=self.bank_st_2)
rule.match_text_location_note = True
self.assertDictEqual(
rule._apply_rules(st_line, st_line._retrieve_partner()),
{'amls': inv3, 'model': rule},
)
def test_matching_fields_match_text_location_no_partner(self):
self.bank_line_2.unlink() # One line is enough for this test
@@ -326,111 +303,94 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
# Nothing should match
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {'aml_ids': []},
}, statements=self.bank_st)
self.bank_line_1: {},
})
# Test matching with the invoice ref
self.bank_line_1.write({st_line_field: self.invoice_line_1.move_id.payment_reference})
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {'aml_ids': self.invoice_line_1.ids, 'model': self.rule_1, 'partner': self.env['res.partner']},
}, statements=self.bank_st)
self.bank_line_1: {'amls': self.invoice_line_1, 'model': self.rule_1},
})
# Test matching with the partner name (reinitializing the statement line first)
self.bank_line_1.write({**st_line_initial_vals, st_line_field: self.partner_1.name})
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {'aml_ids': self.invoice_line_1.ids, 'model': self.rule_1, 'partner': self.env['res.partner']},
}, statements=self.bank_st)
self.bank_line_1: {'amls': self.invoice_line_1, 'model': self.rule_1},
})
def test_matching_fields_match_journal_ids(self):
self.rule_1.match_journal_ids |= self.cash_st.journal_id
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {'aml_ids': []},
self.bank_line_2.id: {'aml_ids': []},
self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id},
self.bank_line_1: {},
self.bank_line_2: {},
self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1},
})
self.rule_1.match_journal_ids |= self.bank_st.journal_id + self.cash_st.journal_id
def test_matching_fields_match_nature(self):
self.rule_1.match_nature = 'amount_received'
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id},
self.bank_line_2.id: {'aml_ids': [
self.invoice_line_2.id,
self.invoice_line_3.id,
self.invoice_line_1.id,
], 'model': self.rule_1, 'partner': self.bank_line_2.partner_id},
self.cash_line_1.id: {'aml_ids': []},
self.bank_line_1: {'amls': self.invoice_line_1, 'model': self.rule_1},
self.bank_line_2: {
'amls': self.invoice_line_2 + self.invoice_line_3 + self.invoice_line_1,
'model': self.rule_1,
},
self.cash_line_1: {},
})
self.rule_1.match_nature = 'amount_paid'
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {'aml_ids': []},
self.bank_line_2.id: {'aml_ids': []},
self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id},
self.bank_line_1: {},
self.bank_line_2: {},
self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1},
})
self.rule_1.match_nature = 'both'
def test_matching_fields_match_amount(self):
self.rule_1.match_amount = 'lower'
self.rule_1.match_amount_max = 150
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id},
self.bank_line_2.id: {'aml_ids': []},
self.cash_line_1.id: {'aml_ids': []},
self.bank_line_1: {'amls': self.invoice_line_1, 'model': self.rule_1},
self.bank_line_2: {},
self.cash_line_1: {},
})
self.rule_1.match_amount = 'greater'
self.rule_1.match_amount_min = 200
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {'aml_ids': []},
self.bank_line_2.id: {'aml_ids': [
self.invoice_line_1.id,
self.invoice_line_2.id,
self.invoice_line_3.id,
], 'model': self.rule_1, 'partner': self.bank_line_2.partner_id},
self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id},
self.bank_line_1: {},
self.bank_line_2: {'amls': self.invoice_line_1 + self.invoice_line_2 + self.invoice_line_3, 'model': self.rule_1},
self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1},
})
self.rule_1.match_amount = 'between'
self.rule_1.match_amount_min = 200
self.rule_1.match_amount_max = 800
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {'aml_ids': []},
self.bank_line_2.id: {'aml_ids': [
self.invoice_line_1.id,
self.invoice_line_2.id,
self.invoice_line_3.id,
], 'model': self.rule_1, 'partner': self.bank_line_2.partner_id},
self.cash_line_1.id: {'aml_ids': []},
self.bank_line_1: {},
self.bank_line_2: {'amls': self.invoice_line_1 + self.invoice_line_2 + self.invoice_line_3, 'model': self.rule_1},
self.cash_line_1: {},
})
self.rule_1.match_amount = False
def test_matching_fields_match_label(self):
self.rule_1.match_label = 'contains'
self.rule_1.match_label_param = 'yyyyy'
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {'aml_ids': []},
self.bank_line_2.id: {'aml_ids': []},
self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id},
self.bank_line_1: {},
self.bank_line_2: {},
self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1},
})
self.rule_1.match_label = 'not_contains'
self.rule_1.match_label_param = 'xxxxx'
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id},
self.bank_line_2.id: {'aml_ids': []},
self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id},
self.bank_line_1: {'amls': self.invoice_line_1, 'model': self.rule_1},
self.bank_line_2: {},
self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1},
})
self.rule_1.match_label = 'match_regex'
self.rule_1.match_label_param = 'xxxxx|yyyyy'
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {'aml_ids': []},
self.bank_line_2.id: {'aml_ids': [
self.invoice_line_1.id,
self.invoice_line_2.id,
self.invoice_line_3.id,
], 'model': self.rule_1, 'partner': self.bank_line_2.partner_id},
self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id},
self.bank_line_1: {},
self.bank_line_2: {'amls': self.invoice_line_1 + self.invoice_line_2 + self.invoice_line_3, 'model': self.rule_1},
self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1},
})
self.rule_1.match_label = False
@freeze_time('2019-01-01')
def test_zero_payment_tolerance(self):
@@ -444,24 +404,21 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
st_line = self._create_st_line(amount=bsl_sign * 1000.0)
self._check_statement_matching(
rule,
{st_line.id: {'aml_ids': invl.ids, 'model': rule, 'partner': st_line.partner_id}},
statements=st_line.statement_id,
{st_line: {'amls': invl, 'model': rule}},
)
# No matching because there is no tolerance.
st_line = self._create_st_line(amount=bsl_sign * 990.0)
self._check_statement_matching(
rule,
{st_line.id: {'aml_ids': []}},
statements=st_line.statement_id,
{st_line: {}},
)
# The payment amount is higher than the invoice one.
st_line = self._create_st_line(amount=bsl_sign * 1010.0)
self._check_statement_matching(
rule,
{st_line.id: {'aml_ids': invl.ids, 'model': rule, 'partner': st_line.partner_id}},
statements=st_line.statement_id,
{st_line: {'amls': invl, 'model': rule}},
)
@freeze_time('2019-01-01')
@@ -479,25 +436,16 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
st_line = self._create_st_line(amount=bsl_sign * 990.0)
self._check_statement_matching(
rule,
{st_line.id: {'aml_ids': []}},
statements=st_line.statement_id,
{st_line: {}},
)
# The payment amount is higher than the invoice one.
st_line = self._create_st_line(amount=bsl_sign * 1010.0, payment_ref='123456')
self._check_statement_matching(
rule,
{st_line.id: {'aml_ids': invl.ids, 'model': rule, 'partner': st_line.partner_id, 'status': 'reconciled'}},
statements=st_line.statement_id,
{st_line: {'amls': invl, 'model': rule, 'auto_reconcile': True}},
)
self.assertRecordValues(st_line.line_ids.sorted(lambda x: abs(x.balance)), [
# pylint: disable=bad-whitespace
{'balance': bsl_sign * -10.0, 'account_id': invl.account_id.id, 'reconciled': False},
{'balance': bsl_sign * -1000.0, 'account_id': invl.account_id.id, 'reconciled': True},
{'balance': bsl_sign * 1010.0, 'account_id': self.bank_journal.default_account_id.id, 'reconciled': False},
])
@freeze_time('2019-01-01')
def test_not_enough_payment_tolerance(self):
rule = self._create_reconcile_model(
@@ -506,26 +454,25 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
)
for inv_type, bsl_sign in (('out_invoice', 1), ('in_invoice', -1)):
with self.subTest(inv_type=inv_type, bsl_sign=bsl_sign):
invl = self._create_invoice_line(1000.0, self.partner_a, inv_type, inv_date='2019-01-01')
invl = self._create_invoice_line(1000.0, self.partner_a, inv_type, inv_date='2019-01-01')
# No matching because there is no enough tolerance.
st_line = self._create_st_line(amount=bsl_sign * 990.0)
self._check_statement_matching(
rule,
{st_line.id: {'aml_ids': []}},
statements=st_line.statement_id,
)
# No matching because there is no enough tolerance.
st_line = self._create_st_line(amount=bsl_sign * 990.0)
self._check_statement_matching(
rule,
{st_line: {}},
)
# The payment amount is higher than the invoice one.
# However, since the invoice amount is lower than the payment amount,
# the tolerance is not checked and the invoice line is matched.
st_line = self._create_st_line(amount=bsl_sign * 1010.0)
self._check_statement_matching(
rule,
{st_line.id: {'aml_ids': invl.ids, 'model': rule, 'partner': st_line.partner_id}},
statements=st_line.statement_id,
)
# The payment amount is higher than the invoice one.
# However, since the invoice amount is lower than the payment amount,
# the tolerance is not checked and the invoice line is matched.
st_line = self._create_st_line(amount=bsl_sign * 1010.0)
self._check_statement_matching(
rule,
{st_line: {'amls': invl, 'model': rule}},
)
@freeze_time('2019-01-01')
def test_enough_payment_tolerance(self):
@@ -542,8 +489,7 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
st_line = self._create_st_line(amount=bsl_sign * 990.0)
self._check_statement_matching(
rule,
{st_line.id: {'aml_ids': invl.ids, 'model': rule, 'partner': st_line.partner_id, 'status': 'write_off'}},
statements=st_line.statement_id,
{st_line: {'amls': invl, 'model': rule, 'status': 'write_off'}},
)
# The payment amount is higher than the invoice one.
@@ -552,8 +498,7 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
st_line = self._create_st_line(amount=bsl_sign * 1010.0)
self._check_statement_matching(
rule,
{st_line.id: {'aml_ids': invl.ids, 'model': rule, 'partner': st_line.partner_id}},
statements=st_line.statement_id,
{st_line: {'amls': invl, 'model': rule}},
)
@freeze_time('2019-01-01')
@@ -572,18 +517,9 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
st_line = self._create_st_line(amount=bsl_sign * 990.0, payment_ref='123456')
self._check_statement_matching(
rule,
{st_line.id: {'aml_ids': invl.ids, 'model': rule, 'partner': st_line.partner_id, 'status': 'reconciled'}},
statements=st_line.statement_id,
{st_line: {'amls': invl, 'model': rule, 'status': 'write_off', 'auto_reconcile': True}},
)
self.assertRecordValues(st_line.line_ids.sorted(lambda x: abs(x.balance)), [
# pylint: disable=bad-whitespace
{'balance': bsl_sign * 990.0, 'account_id': self.bank_journal.default_account_id.id, 'reconciled': False},
{'balance': bsl_sign * -1000.0, 'account_id': invl.account_id.id, 'reconciled': True},
{'balance': bsl_sign * -1980.0, 'account_id': self.company_data['default_account_revenue'].id, 'reconciled': False},
{'balance': bsl_sign * 1990.0, 'account_id': invl.account_id.id, 'reconciled': False},
])
@freeze_time('2019-01-01')
def test_allow_payment_tolerance_lower_amount(self):
rule = self._create_reconcile_model(line_ids=[{'amount_type': 'percentage_st_line'}])
@@ -596,8 +532,7 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
# Partial reconciliation.
self._check_statement_matching(
rule,
{st_line.id: {'aml_ids': invl.ids, 'model': rule, 'partner': st_line.partner_id}},
statements=st_line.statement_id,
{st_line: {'amls': invl, 'model': rule}},
)
@freeze_time('2019-01-01')
@@ -616,17 +551,14 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
st_line = self._create_st_line(amount=bsl_sign * 990.0, payment_ref='123456')
self._check_statement_matching(
rule,
{st_line.id: {'aml_ids': invl.ids, 'model': rule, 'partner': st_line.partner_id, 'status': 'reconciled'}},
statements=st_line.statement_id,
{st_line: {
'amls': invl,
'model': rule,
'status': 'write_off',
'auto_reconcile': True,
}},
)
self.assertRecordValues(st_line.line_ids.sorted(lambda x: abs(x.balance)), [
# pylint: disable=bad-whitespace
{'balance': bsl_sign * 10.0, 'account_id': self.company_data['default_account_revenue'].id, 'reconciled': False},
{'balance': bsl_sign * 990.0, 'account_id': self.bank_journal.default_account_id.id, 'reconciled': False},
{'balance': bsl_sign * -1000.0, 'account_id': invl.account_id.id, 'reconciled': True},
])
@freeze_time('2019-01-01')
def test_percentage_st_line_auto_reconcile(self):
rule = self._create_reconcile_model(
@@ -641,30 +573,24 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
)
for bsl_sign in (1, -1):
st_line = self._create_st_line(amount=bsl_sign * 1000.0)
self._check_statement_matching(
rule,
{st_line.id: {'aml_ids': [], 'model': rule, 'partner': st_line.partner_id, 'status': 'reconciled'}},
statements=st_line.statement_id,
{st_line: {
'model': rule,
'status': 'write_off',
'auto_reconcile': True,
}},
)
self.assertRecordValues(st_line.line_ids.sorted(lambda x: x.account_id), [
# pylint: disable=bad-whitespace
{'balance': bsl_sign * 1000.0, 'account_id': self.bank_journal.default_account_id.id, 'reconciled': False},
{'balance': bsl_sign * -1000.0, 'account_id': self.company_data['default_account_revenue'].id, 'reconciled': False},
{'balance': bsl_sign * 1000.0, 'account_id': self.company_data['default_account_revenue'].id, 'reconciled': False},
{'balance': bsl_sign * -1000.0, 'account_id': self.company_data['default_account_revenue'].id, 'reconciled': False},
])
def test_matching_fields_match_partner_category_ids(self):
test_category = self.env['res.partner.category'].create({'name': 'Consulting Services'})
self.partner_2.category_id = test_category
self.rule_1.match_partner_category_ids |= test_category
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {'aml_ids': []},
self.bank_line_2.id: {'aml_ids': []},
self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id},
self.bank_line_1: {},
self.bank_line_2: {},
self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1},
})
self.rule_1.match_partner_category_ids = False
@@ -675,13 +601,15 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
self.rule_2.sequence = 2
self._check_statement_matching(self.rule_1 + self.rule_2, {
self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id},
self.bank_line_2.id: {'aml_ids': [
self.invoice_line_2.id,
self.invoice_line_3.id,
self.invoice_line_1.id,
], 'model': self.rule_1, 'partner': self.bank_line_2.partner_id},
self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id},
self.bank_line_1: {
'amls': self.invoice_line_1,
'model': self.rule_1,
},
self.bank_line_2: {
'amls': self.invoice_line_2 + self.invoice_line_3 + self.invoice_line_1,
'model': self.rule_1,
},
self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1},
})
# rule_2 is used before rule_1.
@@ -689,18 +617,18 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
self.rule_2.sequence = 1
self._check_statement_matching(self.rule_1 + self.rule_2, {
self.bank_line_1.id: {'aml_ids': [], 'model': self.rule_2, 'status': 'write_off', 'partner': self.bank_line_1.partner_id},
self.bank_line_2.id: {'aml_ids': [], 'model': self.rule_2, 'status': 'write_off', 'partner': self.bank_line_2.partner_id},
self.cash_line_1.id: {'aml_ids': [], 'model': self.rule_2, 'status': 'write_off', 'partner': self.cash_line_1.partner_id},
self.bank_line_1: {'model': self.rule_2, 'auto_reconcile': False, 'status': 'write_off'},
self.bank_line_2: {'model': self.rule_2, 'auto_reconcile': False, 'status': 'write_off'},
self.cash_line_1: {'model': self.rule_2, 'auto_reconcile': False, 'status': 'write_off'},
})
# rule_2 is used before rule_1 but only on partner_1.
self.rule_2.match_partner_ids |= self.partner_1
self._check_statement_matching(self.rule_1 + self.rule_2, {
self.bank_line_1.id: {'aml_ids': [], 'model': self.rule_2, 'status': 'write_off', 'partner': self.bank_line_1.partner_id},
self.bank_line_2.id: {'aml_ids': [], 'model': self.rule_2, 'status': 'write_off', 'partner': self.bank_line_2.partner_id},
self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id},
self.bank_line_1: {'model': self.rule_2, 'auto_reconcile': False, 'status': 'write_off'},
self.bank_line_2: {'model': self.rule_2, 'auto_reconcile': False, 'status': 'write_off'},
self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1},
})
def test_auto_reconcile(self):
@@ -715,33 +643,22 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
self.rule_2.auto_reconcile = True
self._check_statement_matching(self.rule_1 + self.rule_2, {
self.bank_line_1.id: {
'aml_ids': self.invoice_line_1.ids,
self.bank_line_1: {
'amls': self.invoice_line_1,
'model': self.rule_1,
'status': 'reconciled',
'partner': self.bank_line_1.partner_id,
'auto_reconcile': True,
},
self.bank_line_2.id: {
'aml_ids': (self.invoice_line_2 + self.invoice_line_3).ids,
self.bank_line_2: {
'amls': self.invoice_line_1 + self.invoice_line_2 + self.invoice_line_3,
'model': self.rule_1,
'partner': self.bank_line_2.partner_id,
},
self.cash_line_1.id: {'aml_ids': [], 'model': self.rule_2, 'status': 'reconciled', 'partner': self.cash_line_1.partner_id},
self.cash_line_1: {
'model': self.rule_2,
'status': 'write_off',
'auto_reconcile': True,
},
})
# Check first line has been well reconciled.
self.assertRecordValues(self.bank_line_1.line_ids, [
{'partner_id': self.partner_1.id, 'debit': 105.0, 'credit': 0.0},
{'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 5.0},
{'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 100.0},
])
# Check second line has been well reconciled.
self.assertRecordValues(self.cash_line_1.line_ids, [
{'partner_id': self.partner_2.id, 'debit': 0.0, 'credit': 1000.0},
{'partner_id': self.partner_2.id, 'debit': 1000.0, 'credit': 0.0},
])
def test_larger_invoice_auto_reconcile(self):
''' Test auto reconciliation with an invoice with larger amount than the
statement line's, for rules without write-offs.'''
@@ -754,69 +671,16 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
self.rule_1.line_ids = [(5, 0, 0)]
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {
'aml_ids': self.invoice_line_1.ids,
self.bank_line_1: {
'amls': self.invoice_line_1,
'model': self.rule_1,
'status': 'reconciled',
'partner': self.bank_line_1.partner_id,
'auto_reconcile': True,
},
self.bank_line_2.id: {
'aml_ids': (self.invoice_line_2 + self.invoice_line_3).ids,
self.bank_line_2: {
'amls': self.invoice_line_1 + self.invoice_line_2 + self.invoice_line_3,
'model': self.rule_1,
'partner': self.bank_line_1.partner_id,
},
}, statements=self.bank_st)
# Check first line has been well reconciled.
self.assertRecordValues(self.bank_line_1.line_ids, [
{'partner_id': self.partner_1.id, 'debit': 40.0, 'credit': 0.0},
{'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 40.0},
])
self.assertEqual(self.invoice_line_1.amount_residual, 60.0, "The invoice should have been partially reconciled")
def test_auto_reconcile_with_duplicate_match(self):
""" If multiple bank statement lines match with the same invoice, ensure the
correct line is auto-validated and no crashing happens.
"""
# Only the invoice defined in this test should have this partner
partner = self.env['res.partner'].create({'name': "The Only One"})
invoice_line = self._create_invoice_line(
2000, partner, 'out_invoice', ref="REF 7788")
# Enable auto-validation and don't restrict the partners that can be matched on
# so our newly created partner can be matched.
self.rule_1.write({
'auto_reconcile': True,
'match_partner_ids': [(5, 0, 0)],
})
self.rule_1.match_partner_ids = []
# This line has a matching payment reference and the exact amount of the
# invoice. As a result it should auto-validate.
self.bank_line_1.amount = 2000
self.bank_line_1.partner_id = partner
self.bank_line_1.payment_ref = "REF 7788"
# This line doesn't have a matching amount or reference, but it does have a
# matching partner.
self.bank_line_2.amount = 1800
self.bank_line_2.partner_id = partner
self.bank_line_2.payment_ref = "something"
# Verify the auto-validation happens with the first line, and no exceptions.
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {
'aml_ids': [invoice_line.id],
'model': self.rule_1,
'status': 'reconciled',
'partner': self.bank_line_1.partner_id
},
self.bank_line_2.id: {
'aml_ids': []
},
}, statements=self.bank_st)
def test_auto_reconcile_with_tax(self):
''' Test auto reconciliation with a tax amount included in the bank statement line'''
@@ -838,19 +702,9 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
self.bank_line_1.amount = -121
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {'aml_ids': [], 'model': self.rule_1, 'status': 'reconciled', 'partner': self.bank_line_1.partner_id},
self.bank_line_2.id: {'aml_ids': [], 'model': self.rule_1, 'status': 'reconciled', 'partner': self.bank_line_2.partner_id},
}, statements=self.bank_st)
# Check first line has been well reconciled.
self.assertRecordValues(self.bank_line_1.line_ids, [
{'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 121.0, 'tax_ids': [], 'tax_line_id': False},
{'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 7.26, 'tax_ids': [], 'tax_line_id': False},
{'partner_id': self.partner_1.id, 'debit': 50.0, 'credit': 0.0, 'tax_ids': [self.tax21.id], 'tax_line_id': False},
{'partner_id': self.partner_1.id, 'debit': 10.5, 'credit': 0.0, 'tax_ids': [], 'tax_line_id': self.tax21.id},
{'partner_id': self.partner_1.id, 'debit': 60.5, 'credit': 0.0, 'tax_ids': [self.tax12.id], 'tax_line_id': False},
{'partner_id': self.partner_1.id, 'debit': 7.26, 'credit': 0.0, 'tax_ids': [], 'tax_line_id': self.tax12.id},
])
self.bank_line_1: {'model': self.rule_1, 'status': 'write_off', 'auto_reconcile': True},
self.bank_line_2: {'model': self.rule_1, 'status': 'write_off', 'auto_reconcile': True},
})
def test_auto_reconcile_with_tax_fpos(self):
""" Test the fiscal positions are applied by reconcile models when using taxes.
@@ -887,22 +741,9 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
})
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {'aml_ids': [], 'model': self.rule_1, 'status': 'reconciled', 'partner': self.bank_line_1.partner_id},
self.bank_line_2.id: {'aml_ids': [], 'model': self.rule_1, 'status': 'reconciled', 'partner': self.bank_line_2.partner_id},
}, statements=self.bank_st)
self.assertRecordValues(self.bank_line_1.line_ids, [
{'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 121.0, 'tax_ids': [], 'tax_line_id': False},
{'partner_id': self.partner_1.id, 'debit': 100.0, 'credit': 0.0, 'tax_ids': [self.tax21.id], 'tax_line_id': False},
{'partner_id': self.partner_1.id, 'debit': 21.0, 'credit': 0.0, 'tax_ids': [], 'tax_line_id': self.tax21.id},
])
self.assertRecordValues(self.bank_line_2.line_ids, [
{'partner_id': self.partner_2.id, 'debit': 0.0, 'credit': 112.0, 'tax_ids': [], 'tax_line_id': False},
{'partner_id': self.partner_2.id, 'debit': 100.0, 'credit': 0.0, 'tax_ids': [self.tax12.id], 'tax_line_id': False},
{'partner_id': self.partner_2.id, 'debit': 12.0, 'credit': 0.0, 'tax_ids': [], 'tax_line_id': self.tax12.id},
])
self.bank_line_1: {'model': self.rule_1, 'status': 'write_off', 'auto_reconcile': True},
self.bank_line_2: {'model': self.rule_1, 'status': 'write_off', 'auto_reconcile': True},
})
def test_reverted_move_matching(self):
partner = self.partner_1
@@ -937,9 +778,12 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
'amount': -10,
})
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {'aml_ids': [payment_bnk_line.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id},
self.bank_line_2.id: {'aml_ids': [self.invoice_line_1.id, self.invoice_line_2.id, self.invoice_line_3.id], 'model': self.rule_1, 'partner': self.bank_line_2.partner_id},
}, statements=self.bank_st)
self.bank_line_1: {'amls': payment_bnk_line, 'model': self.rule_1},
self.bank_line_2: {
'amls': self.invoice_line_1 + self.invoice_line_2 + self.invoice_line_3,
'model': self.rule_1,
},
})
def test_match_different_currencies(self):
partner = self.env['res.partner'].create({'name': 'Bernard Gagnant'})
@@ -955,9 +799,9 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
self.bank_line_1.write({'partner_id': partner.id, 'foreign_currency_id': currency_statement.id, 'amount_currency': 100, 'payment_ref': 'test'})
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {'aml_ids': invoice_line.ids, 'model': self.rule_1, 'partner': self.bank_line_1.partner_id},
self.bank_line_2.id: {'aml_ids': []},
}, statements=self.bank_st)
self.bank_line_1: {'amls': invoice_line, 'model': self.rule_1},
self.bank_line_2: {},
})
def test_invoice_matching_rule_no_partner(self):
""" Tests that a statement line without any partner can be matched to the
@@ -979,16 +823,17 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
'match_label_param': 'Tournicoti', # So that we only match what we want to test
})
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id},
self.bank_line_2.id: {'aml_ids': []},
}, self.bank_st)
# TODO: 'invoice_line_1' has no reason to match 'bank_line_1' here... to check
# self._check_statement_matching(self.rule_1, {
# self.bank_line_1: {'amls': self.invoice_line_1, 'model': self.rule_1},
# self.bank_line_2: {'amls': []},
# }, self.bank_st)
def test_inv_matching_rule_auto_rec_no_partner_with_writeoff(self):
self.invoice_line_1.move_id.write({'payment_reference': 'doudlidou355'})
self.invoice_line_1.move_id.ref = "doudlidou3555"
self.bank_line_1.write({
'payment_ref': 'doudlidou355',
'payment_ref': 'doudlidou3555',
'partner_id': None,
'amount': 95,
})
@@ -1004,69 +849,37 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
# Check bank reconciliation
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id, 'status': 'reconciled'},
self.bank_line_2.id: {'aml_ids': []},
}, self.bank_st)
# Check invoice line has been fully reconciled, with a write-off.
self.assertRecordValues(self.bank_line_1.line_ids, [
{'partner_id': self.partner_1.id, 'debit': 95.0, 'credit': 0.0, 'account_id': self.bank_journal.default_account_id.id, 'reconciled': False},
{'partner_id': self.partner_1.id, 'debit': 5.0, 'credit': 0.0, 'account_id': self.current_assets_account.id, 'reconciled': False},
{'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 100.0, 'account_id': self.invoice_line_1.account_id.id, 'reconciled': True},
])
self.assertEqual(self.invoice_line_1.amount_residual, 0.0, "The invoice should have been fully reconciled")
self.bank_line_1: {
'amls': self.invoice_line_1,
'model': self.rule_1,
'status': 'write_off',
'auto_reconcile': True,
},
self.bank_line_2: {},
})
def test_partner_mapping_rule(self):
self.bank_line_1.write({'partner_id': None, 'payment_ref': 'toto42', 'narration': None})
self.bank_line_2.write({'partner_id': None})
st_line = self._create_st_line(partner_id=None, payment_ref="toto42")
# Do the test for both rule 1 and 2, so that we check invoice matching and write-off rules
for rule in (self.rule_1 + self.rule_2):
rule = self._create_reconcile_model(
partner_mapping_line_ids=[{
'partner_id': self.partner_1.id,
'payment_ref_regex': 'toto.*',
}],
)
# To cope for minor differences in rule results
matching_amls = rule.rule_type == 'invoice_matching' and self.invoice_line_1.ids or []
result_status = rule.rule_type == 'writeoff_suggestion' and {'status': 'write_off'} or {}
# Matching using the regex on payment_ref.
self.assertEqual(st_line._retrieve_partner(), self.partner_1)
match_result = {**result_status, 'aml_ids': matching_amls, 'model': rule, 'partner': self.partner_1}
no_match_result = {'aml_ids': []}
rule.partner_mapping_line_ids.narration_regex = ".*coincoin"
# Without mapping, there should be no match
self._check_statement_matching(rule, {
self.bank_line_1.id: no_match_result,
self.bank_line_2.id: no_match_result,
}, self.bank_st)
# No match because the narration is not matching the regex.
self.assertEqual(st_line._retrieve_partner(), self.env['res.partner'])
# We add some mapping for payment reference to rule_1
rule.write({
'partner_mapping_line_ids': [(0, 0, {
'partner_id': self.partner_1.id,
'payment_ref_regex': 'toto.*',
})]
})
st_line.narration = "42coincoin"
# bank_line_1 should now match
self._check_statement_matching(rule, {
self.bank_line_1.id: match_result,
self.bank_line_2.id: no_match_result,
}, self.bank_st)
# If we now add a narration regex to the same mapping line, nothing should match
rule.partner_mapping_line_ids.write({'narration_regex': ".*coincoin"})
self.bank_line_1.write({'narration': None}) # Reset from possible previous iteration
self._check_statement_matching(rule, {
self.bank_line_1.id: no_match_result,
self.bank_line_2.id: no_match_result,
}, self.bank_st)
# If we set the narration so that it matches the new mapping criterium, line_1 matches
self.bank_line_1.write({'narration': "42coincoin"})
self._check_statement_matching(rule, {
self.bank_line_1.id: match_result,
self.bank_line_2.id: no_match_result,
}, self.bank_st)
# Matching is back thanks to "coincoin".
self.assertEqual(st_line._retrieve_partner(), self.partner_1)
def test_partner_name_in_communication(self):
self.invoice_line_1.partner_id.write({'name': "Archibald Haddock"})
@@ -1076,9 +889,9 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
# bank_line_1 should match, as its communication contains the invoice's partner name
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id},
self.bank_line_2.id: {'aml_ids': []},
}, self.bank_st)
self.bank_line_1: {'amls': self.invoice_line_1, 'model': self.rule_1},
self.bank_line_2: {},
})
def test_partner_name_with_regexp_chars(self):
self.invoice_line_1.partner_id.write({'name': "Archibald + Haddock"})
@@ -1088,9 +901,9 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
# The query should still work
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id},
self.bank_line_2.id: {'aml_ids': []},
}, self.bank_st)
self.bank_line_1: {'amls': self.invoice_line_1, 'model': self.rule_1},
self.bank_line_2: {},
})
def test_match_multi_currencies(self):
''' Ensure the matching of candidates is made using the right statement line currency.
@@ -1182,41 +995,9 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
move_line_2 = move.line_ids.filtered(lambda line: line.debit == 14.0)
self._check_statement_matching(matching_rule, {
statement_line.id: {'aml_ids': (move_line_1 + move_line_2).ids, 'model': matching_rule, 'partner': statement_line.partner_id}
}, statements=statement)
@freeze_time('2020-01-01')
def test_inv_matching_with_write_off(self):
self.rule_1.payment_tolerance_param = 10.0
self.bank_st.line_ids[1].unlink() # We don't need this one here
statement_line = self.bank_st.line_ids[0]
statement_line.write({
'payment_ref': self.invoice_line_1.move_id.payment_reference,
'amount': 90,
statement_line: {'amls': move_line_1 + move_line_2, 'model': matching_rule}
})
# Test the invoice-matching part
self._check_statement_matching(self.rule_1, {
statement_line.id: {'aml_ids': self.invoice_line_1.ids, 'model': self.rule_1, 'partner': self.invoice_line_1.partner_id, 'status': 'write_off'},
}, self.bank_st)
# Test the write-off part
expected_write_off = {
'balance': 10,
'currency_id': self.company_data['currency'].id,
'reconcile_model_id': self.rule_1.id,
'account_id': self.current_assets_account.id,
}
matching_result = self.rule_1._apply_rules(statement_line)
self.assertEqual(len(matching_result[statement_line.id].get('write_off_vals', [])), 1, "Exactly one write-off line should be proposed.")
full_write_off_dict = matching_result[statement_line.id]['write_off_vals'][0]
to_compare = {key: full_write_off_dict[key] for key in expected_write_off}
self.assertDictEqual(expected_write_off, to_compare)
@freeze_time('2020-01-01')
def test_matching_with_write_off_foreign_currency(self):
journal_foreign_curr = self.company_data['default_journal_bank'].copy()
@@ -1233,45 +1014,13 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
)
st_line = self._create_st_line(amount=100.0, payment_ref='123456', journal_id=journal_foreign_curr.id)
reco_model._apply_rules(st_line)
self.assertRecordValues(st_line, [{'is_reconciled': True}])
self.assertRecordValues(st_line.line_ids.sorted('amount_currency'), [
{'amount_currency': -100.0, 'currency_id': self.currency_data['currency'].id, 'balance': -50.0},
{'amount_currency': 100.0, 'currency_id': self.currency_data['currency'].id, 'balance': 50.0},
])
def test_inv_matching_with_write_off_autoreconcile(self):
self.bank_line_1.amount = 95
self.rule_1.sequence = 2
self.rule_1.auto_reconcile = True
self.rule_1.payment_tolerance_param = 10.0
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {
'aml_ids': self.invoice_line_1.ids,
'model': self.rule_1,
'status': 'reconciled',
'partner': self.bank_line_1.partner_id,
self._check_statement_matching(reco_model, {
st_line: {
'model': reco_model,
'status': 'write_off',
'auto_reconcile': True,
},
self.bank_line_2.id: {
'aml_ids': (self.invoice_line_2 + self.invoice_line_3).ids,
'model': self.rule_1,
'partner': self.bank_line_2.partner_id,
},
}, statements=self.bank_st)
# Check first line has been properly reconciled.
self.assertRecordValues(self.bank_line_1.line_ids, [
# pylint: disable=bad-whitespace
{'partner_id': self.partner_1.id, 'debit': 95.0, 'credit': 0.0, 'account_id': self.bank_journal.default_account_id.id, 'reconciled': False},
{'partner_id': self.partner_1.id, 'debit': 5.0, 'credit': 0.0, 'account_id': self.current_assets_account.id, 'reconciled': False},
{'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 100.0, 'account_id': self.invoice_line_1.account_id.id, 'reconciled': True},
])
self.assertEqual(self.invoice_line_1.amount_residual, 0.0, "The invoice should have been fully reconciled")
})
def test_payment_similar_communications(self):
def create_payment_line(amount, memo, partner):
@@ -1306,8 +1055,8 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
self.rule_1.allow_payment_tolerance = False
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {'aml_ids': (pmt_line_1 + pmt_line_2).ids, 'model': self.rule_1, 'partner': payment_partner},
}, statements=self.bank_line_1.statement_id)
self.bank_line_1: {'amls': pmt_line_1 + pmt_line_2, 'model': self.rule_1, 'status': 'write_off'},
})
def test_no_amount_check_keep_first(self):
""" In case the reconciliation model doesn't check the total amount of the candidates,
@@ -1320,9 +1069,13 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
self.bank_line_1.partner_id = None
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {'aml_ids': []},
self.bank_line_2.id: {'aml_ids': [self.invoice_line_1.id, self.invoice_line_2.id], 'model': self.rule_1, 'partner': self.bank_line_2.partner_id},
}, statements=self.bank_st)
self.bank_line_1: {},
self.bank_line_2: {
'amls': self.invoice_line_1 + self.invoice_line_2,
'model': self.rule_1,
'status': 'write_off',
},
})
def test_no_amount_check_exact_match(self):
""" If a reconciliation model finds enough candidates for a full reconciliation,
@@ -1335,6 +1088,10 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
self.bank_line_1.partner_id = None
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {'aml_ids': []},
self.bank_line_2.id: {'aml_ids': [self.invoice_line_3.id], 'model': self.rule_1, 'partner': self.bank_line_2.partner_id},
}, statements=self.bank_st)
self.bank_line_1: {},
self.bank_line_2: {
'amls': self.invoice_line_3,
'model': self.rule_1,
'status': 'write_off',
},
})
@@ -71,13 +71,10 @@
<div class="o_kanban_card_manage_title">
<span role="separator">View</span>
</div>
<div>
<div id="action_card_statements">
<a t-if="journal_type == 'bank'" role="menuitem" type="object" name="open_action_with_context" context="{'action_name': 'action_bank_statement_tree', 'search_default_journal': True}">Statements</a>
<a t-if="journal_type == 'cash'" role="menuitem" type="object" name="open_action_with_context" context="{'action_name': 'action_view_bank_statement_tree', 'search_default_journal': True}">Statements</a>
</div>
<div>
<a role="menuitem" type="object" name="open_action_with_context" context="{'action_name': 'action_bank_statement_line', 'search_default_journal': True,'search_default_statement': 1}">Operations</a>
</div>
<div>
<a role="menuitem" type="object" name="open_collect_money">Cust. Payments</a>
</div>
@@ -799,7 +799,6 @@
<field name="invoice_has_outstanding" invisible="1"/>
<field name="is_move_sent" invisible="1"/>
<field name="invoice_has_matching_suspense_amount" invisible="1"/>
<field name="has_reconciled_entries" invisible="1"/>
<field name="restrict_mode_hash_table" invisible="1"/>
<field name="country_code" invisible="1"/>
+8 -1
View File
@@ -59,7 +59,14 @@ class TestExpenses(TestExpenseCommon):
],
})
statement.button_post()
statement.line_ids.reconcile([{'id': liquidity_lines1.id}, {'id': liquidity_lines2.id}])
statement_line = statement.line_ids
# Reconcile without the bank reconciliation widget since the widget is in enterprise.
_st_liquidity_lines, st_suspense_lines, _st_other_lines = statement_line\
.with_context(skip_account_move_synchronization=True)\
._seek_for_lines()
st_suspense_lines.account_id = liquidity_lines1.account_id
(st_suspense_lines + liquidity_lines1 + liquidity_lines2).reconcile()
self.assertEqual(expense_sheet.payment_state, 'paid', 'payment_state should be paid')
@@ -46,16 +46,4 @@
<field name="amount_string">100</field>
<field name="label">Virements internes</field>
</record>
<record id="compte_attente_template" model="account.reconcile.model.template">
<field name="chart_template_id" ref="l10nbe_chart_template"/>
<field name="name">Compte Attente</field>
<field name="to_check" eval="True"/>
</record>
<record id="compte_attente_line_template" model="account.reconcile.model.line.template">
<field name="model_id" ref="l10n_be.compte_attente_template"/>
<field name="account_id" ref="a499"/>
<field name="amount_type">percentage</field>
<field name="amount_string">100</field>
<field name="label"></field>
</record>
</odoo>
@@ -1065,16 +1065,14 @@ tour.stepUtils.autoExpandMoreButtons('.o_control_panel .breadcrumb:contains("the
position: 'bottom',
},
...tour.stepUtils.statusbarButtonsSteps('Post', _t('Processing'), ".breadcrumb-item.active:contains('the_flow.statement')").map(tour.stepUtils.editionEnterpriseModifier),
...tour.stepUtils.statusbarButtonsSteps('Reconcile', _t('Reconcile'), ".o_statusbar_status .btn.dropdown-toggle:contains(Processing)").map(tour.stepUtils.editionEnterpriseModifier),
...tour.stepUtils.statusbarButtonsSteps('Reconcile', _t('Reconcile'), ".o_statusbar_status .btn.dropdown-toggle:contains(Processing)")
.map(function(step){return {mobile: false, ...step}})
.map(tour.stepUtils.editionEnterpriseModifier),
{
mobile: false,
edition: "enterprise",
trigger: "button.o_reconcile, button.o_validate",
trigger: "button[name='button_validate']",
content: Markup(_t('<p><b>Click on Reconcile</p>')),
position: "right",
}, {
edition: "enterprise",
trigger: ".button_back_to_statement",
content: Markup(_t('<p><b>Close this statement.</p>')),
position: "bottom",
}]);
});