Merge PR #819 into 18.0

Signed-off-by pedrobaeza
pull/823/head
OCA-git-bot 2025-04-01 15:23:05 +00:00
commit 8dfb47944e
3 changed files with 481 additions and 235 deletions

View File

@ -118,23 +118,15 @@ class AccountBankStatementLine(models.Model):
"""
self.ensure_one()
def _get_text_value(field_name):
if self._fields[field_name].type == "html":
return self[field_name] and 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 not allowed_fields or "payment_ref" in allowed_fields:
if self.payment_ref:
st_line_text_values.append(self.payment_ref)
if not allowed_fields or "narration" in allowed_fields:
value = html2plaintext(self.narration or "")
if value:
st_line_text_values.append(value)
if not allowed_fields or "ref" in allowed_fields:
if self.ref:
st_line_text_values.append(self.ref)
return st_line_text_values

View File

@ -131,6 +131,8 @@ class AccountReconcileModel(models.Model):
balance = currency.round(
line.amount * (1 if residual_balance > 0.0 else -1)
)
else:
balance = 0.0
if currency.is_zero(balance):
continue
@ -258,7 +260,7 @@ class AccountReconcileModel(models.Model):
or (
self.match_partner
and self.match_partner_category_ids
and partner.category_id not in self.match_partner_category_ids
and not (partner.category_id & self.match_partner_category_ids)
)
):
return False
@ -314,36 +316,63 @@ class AccountReconcileModel(models.Model):
return aml_domain
def _get_st_line_text_values_for_matching(self, st_line):
"""Collect the strings that could be used on the statement line to perform
some matching.
:param st_line: The current statement line.
:return: A list of strings.
"""
self.ensure_one()
allowed_fields = []
if self.match_text_location_label:
allowed_fields.append("payment_ref")
if self.match_text_location_note:
allowed_fields.append("narration")
if self.match_text_location_reference:
allowed_fields.append("ref")
return st_line._get_st_line_strings_for_matching(allowed_fields=allowed_fields)
def _get_invoice_matching_st_line_tokens(self, st_line):
"""Parse the textual information from the statement line passed as parameter
in order to extract from it the meaningful information in order to perform the
matching.
:param st_line: A statement line.
:return: A list of tokens, each one being a string.
:return: A tuple of list of tokens, each one being a string.
The first element is a list of tokens you may match on
numerical information.
The second element is a list of tokens you may match exactly.
"""
st_line_text_values = st_line._get_st_line_strings_for_matching(
allowed_fields=(
"payment_ref" if self.match_text_location_label else None,
"narration" if self.match_text_location_note else None,
"ref" if self.match_text_location_reference else None,
)
)
st_line_text_values = self._get_st_line_text_values_for_matching(st_line)
significant_token_size = 4
tokens = []
numerical_tokens = []
exact_tokens = []
text_tokens = []
for text_value in st_line_text_values:
for token in (text_value or "").split():
tokens = [
"".join(x for x in token if re.match(r"[0-9a-zA-Z\s]", x))
for token in (text_value or "").split()
]
# Numerical tokens
for token in tokens:
# The token is too short to be significant.
if len(token) < significant_token_size:
continue
text_tokens.append(token)
formatted_token = "".join(x for x in token if x.isdecimal())
# The token is too short after formatting to be significant.
if len(formatted_token) < significant_token_size:
continue
tokens.append(formatted_token)
return tokens
numerical_tokens.append(formatted_token)
# Exact tokens.
if len(tokens) == 1:
exact_tokens.append(text_value)
return numerical_tokens, exact_tokens, text_tokens
def _get_invoice_matching_amls_candidates(self, st_line, partner):
"""Returns the match candidates for the 'invoice_matching' rule, with respect to
@ -351,58 +380,100 @@ class AccountReconcileModel(models.Model):
:param st_line: A statement line.
:param partner: The partner associated to the statement line.
"""
def get_order_by_clause(alias=None):
direction = "DESC" if self.matching_order == "new_first" else "ASC"
dotted_alias = f"{alias}." if alias else ""
return f"{dotted_alias}date_maturity {direction}, {dotted_alias}date {direction}, {dotted_alias}id {direction}" # noqa: E501
assert self.rule_type == "invoice_matching"
self.env["account.move"].flush_model()
self.env["account.move.line"].flush_model()
if self.matching_order == "new_first":
order_by = "sub.date_maturity DESC, sub.date DESC, sub.id DESC"
else:
order_by = "sub.date_maturity ASC, sub.date ASC, sub.id ASC"
aml_domain = self._get_invoice_matching_amls_domain(st_line, partner)
query = self.env["account.move.line"]._where_calc(aml_domain)
from_string, from_params = query.from_clause
where_string, where_params = query.where_clause
from_clause = from_string
where_clause = where_string
query_params = from_params + where_params
tokens = self._get_invoice_matching_st_line_tokens(st_line)
if tokens:
search_fields = [
("account_move_line", "name"),
("account_move_line__move_id", "name"),
("account_move_line__move_id", "ref"),
]
sub_queries = []
for table_alias, field in search_fields:
sub_queries = []
all_params = []
aml_cte = ""
(
numerical_tokens,
exact_tokens,
_text_tokens,
) = self._get_invoice_matching_st_line_tokens(st_line)
if numerical_tokens or exact_tokens:
aml_cte = rf"""
WITH aml_cte AS (
SELECT
account_move_line.id as account_move_line_id,
account_move_line.date as account_move_line_date,
account_move_line.date_maturity as account_move_line_date_maturity,
account_move_line.name as account_move_line_name,
account_move_line__move_id.name as account_move_line__move_id_name,
account_move_line__move_id.ref as account_move_line__move_id_ref
FROM {from_clause}
JOIN account_move account_move_line__move_id
ON account_move_line__move_id.id = account_move_line.move_id
WHERE {where_clause}
)
""" # noqa: E501
all_params += where_params
enabled_matches = []
if self.match_text_location_label:
enabled_matches.append(("account_move_line", "name"))
if self.match_text_location_note:
enabled_matches.append(("account_move_line__move_id", "name"))
if self.match_text_location_reference:
enabled_matches.append(("account_move_line__move_id", "ref"))
if numerical_tokens:
for table_alias, field in enabled_matches:
sub_queries.append(
rf"""
SELECT
account_move_line.id,
account_move_line.date,
account_move_line.date_maturity,
account_move_line_id as id,
account_move_line_date as date,
account_move_line_date_maturity as date_maturity,
UNNEST(
REGEXP_SPLIT_TO_ARRAY(
SUBSTRING(
REGEXP_REPLACE(
{table_alias}.{field}, '[^0-9\s]', '', 'g'
{table_alias}_{field}, '[^0-9\s]', '', 'g'
),
'\S(?:.*\S)*'
),
'\s+'
)
) AS token
FROM {from_clause}
JOIN account_move account_move_line__move_id
ON account_move_line__move_id.id = account_move_line.move_id
WHERE {where_clause} AND {table_alias}.{field} IS NOT NULL
FROM aml_cte
WHERE {table_alias}_{field} IS NOT NULL
"""
)
self._cr.execute(
if exact_tokens:
for table_alias, field in enabled_matches:
sub_queries.append(
rf"""
SELECT
account_move_line_id as id,
account_move_line_date as date,
account_move_line_date_maturity as date_maturity,
{table_alias}_{field} AS token
FROM aml_cte
WHERE COALESCE({table_alias}_{field}, '') != ''
"""
)
if sub_queries:
order_by = get_order_by_clause(alias="sub")
self._cr.execute(
aml_cte
+ """
SELECT
sub.id,
COUNT(*) AS nb_match
@ -416,7 +487,7 @@ class AccountReconcileModel(models.Model):
+ order_by
+ """
""",
(query_params * 3) + [tuple(tokens)],
all_params + [tuple(numerical_tokens + exact_tokens)],
)
candidate_ids = [r[0] for r in self._cr.fetchall()]
if candidate_ids:
@ -424,20 +495,58 @@ class AccountReconcileModel(models.Model):
"allow_auto_reconcile": True,
"amls": self.env["account.move.line"].browse(candidate_ids),
}
elif (
self.match_text_location_label
or self.match_text_location_note
or self.match_text_location_reference
):
# In the case any of the Label, Note or Reference matching rule has been
# toggled, and the query didn't return
# any candidates, the model should not try to mount another aml instead.
return
# Search without any matching based on textual information.
if partner:
if self.matching_order == "new_first":
order = "date_maturity DESC, date DESC, id DESC"
if not partner:
st_line_currency = (
st_line.foreign_currency_id
or st_line.journal_id.currency_id
or st_line.company_currency_id
)
if st_line_currency == self.company_id.currency_id:
aml_amount_field = "amount_residual"
else:
order = "date_maturity ASC, date ASC, id ASC"
aml_amount_field = "amount_residual_currency"
amls = self.env["account.move.line"].search(aml_domain, order=order)
if amls:
return {
"allow_auto_reconcile": False,
"amls": amls,
}
order_by = get_order_by_clause(alias="account_move_line")
self._cr.execute(
f"""
SELECT account_move_line.id
FROM {from_clause}
WHERE
{where_clause}
AND account_move_line.currency_id = %s
AND ROUND(account_move_line.{aml_amount_field}, %s) = ROUND(%s, %s)
ORDER BY {order_by}
""", # noqa: E501
where_params
+ [
st_line_currency.id,
st_line_currency.decimal_places,
-st_line.amount_residual,
st_line_currency.decimal_places,
],
)
amls = self.env["account.move.line"].browse(
[row[0] for row in self._cr.fetchall()]
)
else:
amls = self.env["account.move.line"].search(
aml_domain, order=get_order_by_clause()
)
if amls:
return {
"allow_auto_reconcile": False,
"amls": amls,
}
def _get_invoice_matching_rules_map(self):
"""Get a mapping <priority_order, rule> that could be overridden in others
@ -648,7 +757,9 @@ class AccountReconcileModel(models.Model):
for aml_values in amls_values_list
)
sign = 1 if st_line_amount_curr > 0.0 else -1
amount_curr_after_rec = sign * (amls_amount_curr + st_line_amount_curr)
amount_curr_after_rec = st_line_currency.round(
sign * (amls_amount_curr + st_line_amount_curr)
)
# The statement line will be fully reconciled.
if st_line_currency.is_zero(amount_curr_after_rec):
@ -667,7 +778,10 @@ class AccountReconcileModel(models.Model):
# amount doesn't exceed the tolerance.
if (
self.payment_tolerance_type == "fixed_amount"
and -amount_curr_after_rec <= self.payment_tolerance_param
and st_line_currency.compare_amounts(
-amount_curr_after_rec, self.payment_tolerance_param
)
<= 0
):
return {"allow_write_off", "allow_auto_reconcile"}
@ -677,7 +791,10 @@ class AccountReconcileModel(models.Model):
) * 100.0
if (
self.payment_tolerance_type == "percentage"
and reconciled_percentage_left <= self.payment_tolerance_param
and st_line_currency.compare_amounts(
reconciled_percentage_left, self.payment_tolerance_param
)
<= 0
):
return {"allow_write_off", "allow_auto_reconcile"}

View File

@ -1,3 +1,5 @@
from contextlib import contextmanager
from freezegun import freeze_time
from odoo import Command
@ -75,6 +77,8 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
"match_nature": "both",
"match_same_currency": True,
"allow_payment_tolerance": True,
"match_text_location_note": True,
"match_text_location_reference": True,
"payment_tolerance_type": "percentage",
"payment_tolerance_param": 0.0,
"match_partner": True,
@ -282,6 +286,7 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
def test_matching_fields(self):
# Check without restriction.
self.rule_1.match_text_location_label = False
self._check_statement_matching(
self.rule_1,
{
@ -296,123 +301,8 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
},
)
@freeze_time("2020-01-01")
def test_matching_fields_match_text_location(self):
st_line = self._create_st_line(
payment_ref="1111", ref="2222 3333", narration="4444 5555 6666"
)
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",
)
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},
)
rule.match_text_location_reference = True
self.assertDictEqual(
rule._apply_rules(st_line, st_line._retrieve_partner()),
{"amls": inv2, "model": rule},
)
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
self.bank_line_1.partner_id = None
self.partner_1.name = "Bernard Gagnant"
self.rule_1.write(
{
"match_partner": False,
"match_partner_ids": [(5, 0, 0)],
"line_ids": [(5, 0, 0)],
}
)
st_line_initial_vals = {
"ref": None,
"payment_ref": "nothing",
"narration": None,
}
recmod_initial_vals = {
"match_text_location_label": False,
"match_text_location_note": False,
"match_text_location_reference": False,
}
rec_mod_options_to_fields = {
"match_text_location_label": "payment_ref",
"match_text_location_note": "narration",
"match_text_location_reference": "ref",
}
for rec_mod_field, st_line_field in rec_mod_options_to_fields.items():
self.rule_1.write({**recmod_initial_vals, rec_mod_field: True})
# Fully reinitialize the statement line
self.bank_line_1.write(st_line_initial_vals)
# Nothing should match
self._check_statement_matching(
self.rule_1,
{
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: {
"amls": self.invoice_line_1,
"model": self.rule_1,
},
},
)
# Test matching with the partner name (resetting 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: {
"amls": self.invoice_line_1,
"model": self.rule_1,
},
},
)
def test_matching_fields_match_journal_ids(self):
self.rule_1.match_text_location_label = False
self.rule_1.match_journal_ids |= self.cash_line_1.journal_id
self._check_statement_matching(
self.rule_1,
@ -424,6 +314,7 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
)
def test_matching_fields_match_nature(self):
self.rule_1.match_text_location_label = False
self.rule_1.match_nature = "amount_received"
self._check_statement_matching(
self.rule_1,
@ -449,6 +340,7 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
)
def test_matching_fields_match_amount(self):
self.rule_1.match_text_location_label = False
self.rule_1.match_amount = "lower"
self.rule_1.match_amount_max = 150
self._check_statement_matching(
@ -492,6 +384,7 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
)
def test_matching_fields_match_label(self):
self.rule_1.match_text_location_label = False
self.rule_1.match_label = "contains"
self.rule_1.match_label_param = "yyyyy"
self._check_statement_matching(
@ -530,7 +423,11 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
@freeze_time("2019-01-01")
def test_zero_payment_tolerance(self):
rule = self._create_reconcile_model(line_ids=[{}])
rule = self._create_reconcile_model(
line_ids=[{}],
match_text_location_reference=True,
match_text_location_note=True,
)
for inv_type, bsl_sign in (("out_invoice", 1), ("in_invoice", -1)):
invl = self._create_invoice_line(
@ -538,21 +435,27 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
)
# Exact matching.
st_line = self._create_st_line(amount=bsl_sign * 1000.0)
st_line = self._create_st_line(
amount=bsl_sign * 1000.0, payment_ref=invl.name
)
self._check_statement_matching(
rule,
{st_line: {"amls": invl, "model": rule}},
)
# No matching because there is no tolerance.
st_line = self._create_st_line(amount=bsl_sign * 990.0)
st_line = self._create_st_line(
amount=bsl_sign * 990.0, payment_ref=invl.name
)
self._check_statement_matching(
rule,
{st_line: {}},
)
# The payment amount is higher than the invoice one.
st_line = self._create_st_line(amount=bsl_sign * 1010.0)
st_line = self._create_st_line(
amount=bsl_sign * 1010.0, payment_ref=invl.name
)
self._check_statement_matching(
rule,
{st_line: {"amls": invl, "model": rule}},
@ -575,7 +478,9 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
)
# No matching because there is no tolerance.
st_line = self._create_st_line(amount=bsl_sign * 990.0)
st_line = self._create_st_line(
amount=bsl_sign * 990.0, payment_ref="123456"
)
self._check_statement_matching(
rule,
{st_line: {}},
@ -604,7 +509,9 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
)
# No matching because there is no enough tolerance.
st_line = self._create_st_line(amount=bsl_sign * 990.0)
st_line = self._create_st_line(
amount=bsl_sign * 990.0, payment_ref=invl.name
)
self._check_statement_matching(
rule,
{st_line: {}},
@ -613,7 +520,9 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
# 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)
st_line = self._create_st_line(
amount=bsl_sign * 1010.0, payment_ref=invl.name
)
self._check_statement_matching(
rule,
{st_line: {"amls": invl, "model": rule}},
@ -622,17 +531,19 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
@freeze_time("2019-01-01")
def test_enough_payment_tolerance(self):
rule = self._create_reconcile_model(
payment_tolerance_param=1.0,
payment_tolerance_param=2.0,
line_ids=[{}],
)
for inv_type, bsl_sign in (("out_invoice", 1), ("in_invoice", -1)):
invl = self._create_invoice_line(
1000.0, self.partner_a, inv_type, inv_date="2019-01-01"
1210.0, self.partner_a, inv_type, inv_date="2019-01-01"
)
# Enough tolerance to match the invoice line.
st_line = self._create_st_line(amount=bsl_sign * 990.0)
st_line = self._create_st_line(
amount=bsl_sign * 1185.80, payment_ref=invl.name
)
self._check_statement_matching(
rule,
{st_line: {"amls": invl, "model": rule, "status": "write_off"}},
@ -641,7 +552,9 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
# 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)
st_line = self._create_st_line(
amount=bsl_sign * 1234.20, payment_ref=invl.name
)
self._check_statement_matching(
rule,
{st_line: {"amls": invl, "model": rule}},
@ -690,7 +603,9 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
invl = self._create_invoice_line(
990.0, self.partner_a, inv_type, inv_date="2019-01-01"
)
st_line = self._create_st_line(amount=bsl_sign * 1000)
st_line = self._create_st_line(
amount=bsl_sign * 1000, payment_ref=invl.name
)
# Partial reconciliation.
self._check_statement_matching(
@ -770,10 +685,14 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
)
def test_matching_fields_match_partner_category_ids(self):
self.rule_1.match_text_location_label = False
test_category = self.env["res.partner.category"].create(
{"name": "Consulting Services"}
)
self.partner_2.category_id = test_category
test_category2 = self.env["res.partner.category"].create(
{"name": "Consulting Services2"}
)
self.partner_2.category_id = test_category + test_category2
self.rule_1.match_partner_category_ids |= test_category
self._check_statement_matching(
self.rule_1,
@ -787,6 +706,7 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
def test_mixin_rules(self):
"""Test usage of rules together."""
self.rule_1.match_text_location_label = False
# rule_1 is used before rule_2.
self.rule_1.sequence = 1
self.rule_2.sequence = 2
@ -865,18 +785,24 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
self.rule_2.auto_reconcile = True
self._check_statement_matching(
self.rule_1 + self.rule_2,
self.rule_1,
{
self.bank_line_1: {
"amls": self.invoice_line_1,
"model": self.rule_1,
"auto_reconcile": True,
},
},
)
rule_3 = self.rule_1.copy({"match_text_location_label": False})
self._check_statement_matching(
self.rule_2 + rule_3,
{
self.bank_line_2: {
"amls": self.invoice_line_1
+ self.invoice_line_2
+ self.invoice_line_3,
"model": self.rule_1,
"model": rule_3,
},
self.cash_line_1: {
"model": self.rule_2,
@ -905,11 +831,17 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
"model": self.rule_1,
"auto_reconcile": True,
},
},
)
rule_3 = self.rule_1.copy({"match_text_location_label": False})
self._check_statement_matching(
rule_3,
{
self.bank_line_2: {
"amls": self.invoice_line_1
+ self.invoice_line_2
+ self.invoice_line_3,
"model": self.rule_1,
"model": rule_3,
},
},
)
@ -1063,6 +995,7 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
move_reversed = move._reverse_moves()
self.assertTrue(move_reversed.exists())
self.rule_1.match_text_location_label = False
self.bank_line_1.write(
{
"payment_ref": "8",
@ -1104,7 +1037,7 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
"partner_id": partner.id,
"foreign_currency_id": currency_statement.id,
"amount_currency": 100,
"payment_ref": "test",
"payment_ref": invoice_line.name,
}
)
self._check_statement_matching(
@ -1206,40 +1139,6 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
# 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"})
self.bank_line_1.write(
{"partner_id": None, "payment_ref": "1234//HADDOCK-Archibald"}
)
self.bank_line_2.write({"partner_id": None})
self.rule_1.write({"match_partner": False})
# bank_line_1 should match, as its communic. contains the invoice's partner name
self._check_statement_matching(
self.rule_1,
{
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"})
self.bank_line_1.write(
{"partner_id": None, "payment_ref": "1234//HADDOCK+Archibald"}
)
self.bank_line_2.write({"partner_id": None})
self.rule_1.write({"match_partner": False})
# The query should still work
self._check_statement_matching(
self.rule_1,
{
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. In this test, the value of the statement line is 100 USD = 300
@ -1272,6 +1171,7 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
"match_same_currency": False,
"company_id": self.company_data["company"].id,
"past_months_limit": False,
"match_text_location_label": False,
}
)
@ -1448,6 +1348,7 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
200 should be proposed.
"""
self.rule_1.allow_payment_tolerance = False
self.rule_1.match_text_location_label = False
self.bank_line_2.amount = 250
self.bank_line_1.partner_id = None
@ -1470,6 +1371,7 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
other ones are disregarded.
"""
self.rule_1.allow_payment_tolerance = False
self.rule_1.match_text_location_label = False
self.bank_line_2.amount = 300
self.bank_line_1.partner_id = None
@ -1484,3 +1386,238 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
},
},
)
@freeze_time("2019-01-01")
def test_invoice_matching_using_match_text_location(self):
@contextmanager
def rollback():
savepoint = self.cr.savepoint()
yield
savepoint.rollback()
rule = self._create_reconcile_model(
match_partner=False,
allow_payment_tolerance=False,
match_text_location_reference=True,
match_text_location_note=True,
)
st_line = self._create_st_line(amount=1000, partner_id=False)
invoice = self.env["account.move"].create(
{
"move_type": "out_invoice",
"partner_id": self.partner_a.id,
"invoice_date": "2019-01-01",
"invoice_line_ids": [
Command.create(
{
"product_id": self.product_a.id,
"price_unit": 100,
}
)
],
}
)
invoice.action_post()
term_line = invoice.line_ids.filtered(
lambda x: x.display_type == "payment_term"
)
# No match at all.
self.assertDictEqual(
rule._apply_rules(st_line, None),
{},
)
with rollback():
term_line.name = "1234"
st_line.payment_ref = "1234"
# Matching if no checkbox checked.
self.assertDictEqual(
rule._apply_rules(st_line, None),
{"amls": term_line, "model": rule},
)
# No matching if checkbox is unchecked.
rule.match_text_location_label = False
rule.match_text_location_reference = False
rule.match_text_location_note = False
self.assertDictEqual(
rule._apply_rules(st_line, None),
{},
)
with rollback():
# Test Matching on exact_token.
term_line.name = "PAY-123"
st_line.payment_ref = "PAY-123"
# Matching if no checkbox checked.
self.assertDictEqual(
rule._apply_rules(st_line, None),
{"amls": term_line, "model": rule},
)
with self.subTest(
rule_field="match_text_location_label", st_line_field="payment_ref"
):
with rollback():
term_line.name = ""
st_line.payment_ref = "/?"
# No exact matching when the term line name is an empty string
self.assertDictEqual(
rule._apply_rules(st_line, None),
{},
)
for rule_field, st_line_field in (
("match_text_location_label", "payment_ref"),
("match_text_location_reference", "ref"),
("match_text_location_note", "narration"),
):
with self.subTest(rule_field=rule_field, st_line_field=st_line_field):
with rollback():
rule[rule_field] = True
st_line[st_line_field] = "123456"
term_line.name = "123456"
# Matching if the corresponding flag is enabled.
self.assertDictEqual(
rule._apply_rules(st_line, None),
{"amls": term_line, "model": rule},
)
# It works also if the statement line contains the word.
st_line[st_line_field] = "payment for 123456 urgent!"
self.assertDictEqual(
rule._apply_rules(st_line, None),
{"amls": term_line, "model": rule},
)
# Not if the invoice has nothing in common even if numerical.
term_line.name = "78910"
self.assertDictEqual(
rule._apply_rules(st_line, None),
{},
)
# Exact matching on a single word.
st_line[st_line_field] = "TURLUTUTU21"
term_line.name = "TURLUTUTU21"
self.assertDictEqual(
rule._apply_rules(st_line, None),
{"amls": term_line, "model": rule},
)
# No matching if not enough numerical values.
st_line[st_line_field] = "12"
term_line.name = "selling 3 apples, 2 tomatoes and 12kg of potatoes"
self.assertDictEqual(
rule._apply_rules(st_line, None),
{},
)
invoice2 = self.env["account.move"].create(
{
"move_type": "out_invoice",
"partner_id": self.partner_a.id,
"invoice_date": "2019-01-01",
"invoice_line_ids": [
Command.create(
{
"product_id": self.product_a.id,
"price_unit": 100,
}
)
],
}
)
invoice2.action_post()
term_lines = (invoice + invoice2).line_ids.filtered(
lambda x: x.display_type == "payment_term"
)
# Matching multiple invoices.
rule.match_text_location_label = True
st_line.payment_ref = "paying invoices 1234 & 5678"
term_lines[0].name = "INV/1234"
term_lines[1].name = "INV/5678"
self.assertDictEqual(
rule._apply_rules(st_line, None),
{"amls": term_lines, "model": rule},
)
# Matching multiple invoices sharing the same reference.
term_lines[1].name = "INV/1234"
self.assertDictEqual(
rule._apply_rules(st_line, None),
{"amls": term_lines, "model": rule},
)
@freeze_time("2019-01-01")
def test_matching_exact_amount_no_partner(self):
"""In case the reconciliation model can't match via text or partner matching
we do a last check to find amls with the exact amount.
"""
self.rule_1.write(
{
"match_text_location_label": False,
"match_partner": False,
"match_partner_ids": [Command.clear()],
}
)
self.bank_line_1.partner_id = None
self.bank_line_1.payment_ref = False
with self.subTest(test="single_currency"):
st_line = self._create_st_line(
amount=100, payment_ref=None, partner_id=None
)
invl = self._create_invoice_line(100, self.partner_1, "out_invoice")
self._check_statement_matching(
self.rule_1,
{
st_line: {
"amls": invl,
"model": self.rule_1,
},
},
)
with self.subTest(test="rounding"):
st_line = self._create_st_line(
amount=-208.73, payment_ref=None, partner_id=None
)
invl = self._create_invoice_line(208.73, self.partner_1, "in_invoice")
self._check_statement_matching(
self.rule_1,
{
st_line: {
"amls": invl,
"model": self.rule_1,
},
},
)
with self.subTest(test="multi_currencies"):
foreign_curr = self.other_currency
invl = self._create_invoice_line(
300, self.partner_1, "out_invoice", currency=foreign_curr
)
st_line = self._create_st_line(
amount=15.0,
foreign_currency_id=foreign_curr.id,
amount_currency=300.0,
payment_ref=None,
partner_id=None,
)
self._check_statement_matching(
self.rule_1,
{
st_line: {
"amls": invl,
"model": self.rule_1,
},
},
)