[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:
@@ -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()
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}])
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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",
|
||||
}]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user