Merge branch 'OCA:17.0' into 17.0

pull/800/head
Antonio A. 2025-04-10 21:47:40 +02:00 committed by GitHub
commit 36cefd3ee6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 518 additions and 255 deletions

View File

@ -23,8 +23,8 @@ addon | version | maintainers | summary
--- | --- | --- | ---
[account_in_payment](account_in_payment/) | 17.0.1.0.0 | | This module enables in-payment mode for your accounting
[account_mass_reconcile](account_mass_reconcile/) | 17.0.1.0.1 | | Account Mass Reconcile
[account_reconcile_model_oca](account_reconcile_model_oca/) | 17.0.1.0.2 | | This includes the logic moved from Odoo Community to Odoo Enterprise
[account_reconcile_oca](account_reconcile_oca/) | 17.0.1.5.5 | [![etobella](https://github.com/etobella.png?size=30px)](https://github.com/etobella) | Reconcile addons for Odoo CE accounting
[account_reconcile_model_oca](account_reconcile_model_oca/) | 17.0.1.0.4 | | This includes the logic moved from Odoo Community to Odoo Enterprise
[account_reconcile_oca](account_reconcile_oca/) | 17.0.1.5.8 | [![etobella](https://github.com/etobella.png?size=30px)](https://github.com/etobella) | Reconcile addons for Odoo CE accounting
[account_statement_base](account_statement_base/) | 17.0.1.5.0 | [![alexis-via](https://github.com/alexis-via.png?size=30px)](https://github.com/alexis-via) | Base module for Bank Statements
[//]: # (end addons)

View File

@ -7,7 +7,7 @@ Account Reconcile Model Oca
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:80bb08dc3058116c364563a7014c16787db3ac0b12afadbde03716f7277fa298
!! source digest: sha256:f0554ce70e9ac90badf0a4082aecd5af7011cf1461fe0cc6678577d2b7f87e21
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png

View File

@ -5,11 +5,12 @@
"name": "Account Reconcile Model Oca",
"summary": """
This includes the logic moved from Odoo Community to Odoo Enterprise""",
"version": "17.0.1.0.2",
"version": "17.0.1.0.4",
"license": "LGPL-3",
"author": "Dixmit,Odoo,Odoo Community Association (OCA)",
"website": "https://github.com/OCA/account-reconcile",
"depends": ["account"],
"excludes": ["account_accountant"],
"data": [],
"demo": [],
}

View File

@ -109,23 +109,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
@ -260,7 +262,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
@ -316,36 +318,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
@ -353,53 +382,97 @@ 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)
tables, where_clause, where_params = query.get_sql()
tokens = self._get_invoice_matching_st_line_tokens(st_line)
if tokens:
sub_queries = []
for table_alias, field in (
("account_move_line", "name"),
("account_move_line__move_id", "name"),
("account_move_line__move_id", "ref"),
):
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 {tables}
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 {tables}
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
@ -413,7 +486,7 @@ class AccountReconcileModel(models.Model):
+ order_by
+ """
""",
(where_params * 3) + [tuple(tokens)],
all_params + [tuple(numerical_tokens + exact_tokens)],
)
candidate_ids = [r[0] for r in self._cr.fetchall()]
if candidate_ids:
@ -421,15 +494,53 @@ 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)
order_by = get_order_by_clause(alias="account_move_line")
self._cr.execute(
f"""
SELECT account_move_line.id
FROM {tables}
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,
@ -645,7 +756,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):
@ -664,7 +777,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"}
@ -674,7 +790,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

@ -367,7 +367,7 @@ ul.auto-toc {
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:80bb08dc3058116c364563a7014c16787db3ac0b12afadbde03716f7277fa298
!! source digest: sha256:f0554ce70e9ac90badf0a4082aecd5af7011cf1461fe0cc6678577d2b7f87e21
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/licence-LGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/account-reconcile/tree/17.0/account_reconcile_model_oca"><img alt="OCA/account-reconcile" src="https://img.shields.io/badge/github-OCA%2Faccount--reconcile-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/account-reconcile-17-0/account-reconcile-17-0-account_reconcile_model_oca"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/account-reconcile&amp;target_branch=17.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module restores account reconciliation models functions moved from

View File

@ -1,3 +1,5 @@
from contextlib import contextmanager
from freezegun import freeze_time
from odoo import Command
@ -83,6 +85,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,
@ -287,6 +291,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,
{
@ -301,123 +306,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,
@ -429,6 +319,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,
@ -454,6 +345,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(
@ -497,6 +389,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(
@ -535,7 +428,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(
@ -543,21 +440,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}},
@ -580,7 +483,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: {}},
@ -609,7 +514,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: {}},
@ -618,7 +525,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}},
@ -627,17 +536,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"}},
@ -646,7 +557,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}},
@ -695,7 +608,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(
@ -775,10 +690,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,
@ -792,6 +711,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
@ -870,18 +790,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,
@ -910,11 +836,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,
},
},
)
@ -1069,6 +1001,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",
@ -1110,7 +1043,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(
@ -1212,40 +1145,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
@ -1278,6 +1177,7 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
"match_same_currency": False,
"company_id": self.company_data["company"].id,
"past_months_limit": False,
"match_text_location_label": False,
}
)
@ -1454,6 +1354,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
@ -1476,6 +1377,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
@ -1490,3 +1392,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.currency_data_2["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,
},
},
)

View File

@ -7,7 +7,7 @@ Account Reconcile Oca
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:cf0d778067ac722c5a6d7f65f8fa4b0766076829ba9c9c147cc3718782cc85b0
!! source digest: sha256:a1d19f7cb36b1b53954b505f964b766f7237615fc66c01368256cc9c5f390fd2
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png

View File

@ -5,7 +5,7 @@
"name": "Account Reconcile Oca",
"summary": """
Reconcile addons for Odoo CE accounting""",
"version": "17.0.1.5.5",
"version": "17.0.1.5.8",
"license": "AGPL-3",
"author": "CreuBlanca,Dixmit,Odoo Community Association (OCA)",
"maintainers": ["etobella"],

View File

@ -613,7 +613,10 @@ class AccountBankStatementLine(models.Model):
self.env["res.partner"].browse(line["partner_id"]).display_name,
)
elif self.partner_id:
new_line["partner_id"] = self.partner_id.name_get()[0]
new_line["partner_id"] = (
self.partner_id.id,
self.partner_id.display_name,
)
new_data.append(new_line)
return new_data, reconcile_auxiliary_id
@ -680,9 +683,12 @@ class AccountBankStatementLine(models.Model):
if (
reconciled_line.move_id.journal_id
== self.company_id.currency_exchange_journal_id
):
for rl_item in (
reconciled_line.move_id.line_ids - reconciled_line
):
reconcile_auxiliary_id, lines = self._get_reconcile_line(
reconciled_line.move_id.line_ids - reconciled_line,
rl_item,
"other",
from_unreconcile=False,
move=True,
@ -1017,7 +1023,7 @@ class AccountBankStatementLine(models.Model):
suspense_lines,
_other_lines,
) = st_line._seek_for_lines()
line_vals = {"partner_id": st_line.partner_id}
line_vals = {"partner_id": st_line.partner_id.id}
line_ids_commands = [(1, liquidity_lines.id, line_vals)]
if suspense_lines:
line_ids_commands.append((1, suspense_lines.id, line_vals))
@ -1270,3 +1276,11 @@ class AccountBankStatementLine(models.Model):
for line in lines:
self._add_account_move_line(line, keep_current=True)
return res
def _retrieve_partner(self):
if self.env.context.get("skip_retrieve_partner"):
# This hook can be used, for example, when importing files.
# With large databases, we already have the information, moreover,
# the data might be preloaded, so it has no sense to import it again
return self.partner_id
return super()._retrieve_partner()

View File

@ -367,7 +367,7 @@ ul.auto-toc {
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:cf0d778067ac722c5a6d7f65f8fa4b0766076829ba9c9c147cc3718782cc85b0
!! source digest: sha256:a1d19f7cb36b1b53954b505f964b766f7237615fc66c01368256cc9c5f390fd2
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/account-reconcile/tree/17.0/account_reconcile_oca"><img alt="OCA/account-reconcile" src="https://img.shields.io/badge/github-OCA%2Faccount--reconcile-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/account-reconcile-17-0/account-reconcile-17-0-account_reconcile_oca"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/account-reconcile&amp;target_branch=17.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This addon allows to reconcile bank statements and account marked as

View File

@ -6,15 +6,15 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2023-07-29 12:10+0000\n"
"Last-Translator: Ivorra78 <informatica@totmaterial.es>\n"
"PO-Revision-Date: 2025-02-13 20:35+0000\n"
"Last-Translator: \"Pedro M. Baeza\" <pedro.baeza@tecnativa.com>\n"
"Language-Team: none\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.17\n"
"X-Generator: Weblate 5.6.2\n"
#. module: account_statement_base
#: model_terms:ir.ui.view,arch_db:account_statement_base.view_bank_statement_form
@ -63,7 +63,7 @@ msgstr "Etiqueta, referencia o notas"
#. module: account_statement_base
#: model_terms:ir.ui.view,arch_db:account_statement_base.account_bank_statement_line_search
msgid "Not Reconciled"
msgstr "No reconciliado"
msgstr "No conciliado"
#. module: account_statement_base
#: model_terms:ir.ui.view,arch_db:account_statement_base.account_bank_statement_line_form
@ -89,7 +89,7 @@ msgstr "Socio"
#. module: account_statement_base
#: model_terms:ir.ui.view,arch_db:account_statement_base.account_bank_statement_line_search
msgid "Reconciled"
msgstr "Reconciliado"
msgstr "Conciliado"
#. module: account_statement_base
#: model_terms:ir.ui.view,arch_db:account_statement_base.account_bank_statement_line_tree