diff --git a/README.md b/README.md index 120ac789..cc787d91 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/account_reconcile_model_oca/README.rst b/account_reconcile_model_oca/README.rst index d76f5538..c266798b 100644 --- a/account_reconcile_model_oca/README.rst +++ b/account_reconcile_model_oca/README.rst @@ -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 diff --git a/account_reconcile_model_oca/__manifest__.py b/account_reconcile_model_oca/__manifest__.py index 7d13a691..997b00f6 100644 --- a/account_reconcile_model_oca/__manifest__.py +++ b/account_reconcile_model_oca/__manifest__.py @@ -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": [], } diff --git a/account_reconcile_model_oca/models/account_bank_statement_line.py b/account_reconcile_model_oca/models/account_bank_statement_line.py index 652a3aa6..9dd16137 100644 --- a/account_reconcile_model_oca/models/account_bank_statement_line.py +++ b/account_reconcile_model_oca/models/account_bank_statement_line.py @@ -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 diff --git a/account_reconcile_model_oca/models/account_reconcile_model.py b/account_reconcile_model_oca/models/account_reconcile_model.py index 3627dc13..d95f0e62 100644 --- a/account_reconcile_model_oca/models/account_reconcile_model.py +++ b/account_reconcile_model_oca/models/account_reconcile_model.py @@ -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"), - ): + 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 {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,20 +494,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 {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, + "amls": amls, + } def _get_invoice_matching_rules_map(self): """Get a mapping that could be overridden in others @@ -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"} diff --git a/account_reconcile_model_oca/static/description/index.html b/account_reconcile_model_oca/static/description/index.html index a7e54853..ef60142c 100644 --- a/account_reconcile_model_oca/static/description/index.html +++ b/account_reconcile_model_oca/static/description/index.html @@ -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 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Beta License: LGPL-3 OCA/account-reconcile Translate me on Weblate Try me on Runboat

This module restores account reconciliation models functions moved from diff --git a/account_reconcile_model_oca/tests/test_reconciliation_match.py b/account_reconcile_model_oca/tests/test_reconciliation_match.py index 9c99f101..fc8f5324 100644 --- a/account_reconcile_model_oca/tests/test_reconciliation_match.py +++ b/account_reconcile_model_oca/tests/test_reconciliation_match.py @@ -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, + }, + }, + ) diff --git a/account_reconcile_oca/README.rst b/account_reconcile_oca/README.rst index ac588432..b0a0dd00 100644 --- a/account_reconcile_oca/README.rst +++ b/account_reconcile_oca/README.rst @@ -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 diff --git a/account_reconcile_oca/__manifest__.py b/account_reconcile_oca/__manifest__.py index 06df5d9b..90bf3ebc 100644 --- a/account_reconcile_oca/__manifest__.py +++ b/account_reconcile_oca/__manifest__.py @@ -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"], diff --git a/account_reconcile_oca/models/account_bank_statement_line.py b/account_reconcile_oca/models/account_bank_statement_line.py index ffb78029..5a046b33 100644 --- a/account_reconcile_oca/models/account_bank_statement_line.py +++ b/account_reconcile_oca/models/account_bank_statement_line.py @@ -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 @@ -681,13 +684,16 @@ class AccountBankStatementLine(models.Model): reconciled_line.move_id.journal_id == self.company_id.currency_exchange_journal_id ): - reconcile_auxiliary_id, lines = self._get_reconcile_line( - reconciled_line.move_id.line_ids - reconciled_line, - "other", - from_unreconcile=False, - move=True, - ) - data += lines + for rl_item in ( + reconciled_line.move_id.line_ids - reconciled_line + ): + reconcile_auxiliary_id, lines = self._get_reconcile_line( + rl_item, + "other", + from_unreconcile=False, + move=True, + ) + data += lines continue partial = partial_lines.filtered( lambda r, line=reconciled_line: r.debit_move_id == line @@ -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() diff --git a/account_reconcile_oca/static/description/index.html b/account_reconcile_oca/static/description/index.html index 0e3f271c..73472519 100644 --- a/account_reconcile_oca/static/description/index.html +++ b/account_reconcile_oca/static/description/index.html @@ -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 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Beta License: AGPL-3 OCA/account-reconcile Translate me on Weblate Try me on Runboat

This addon allows to reconcile bank statements and account marked as diff --git a/account_statement_base/i18n/es.po b/account_statement_base/i18n/es.po index 28052d01..888bbafb 100644 --- a/account_statement_base/i18n/es.po +++ b/account_statement_base/i18n/es.po @@ -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 \n" +"PO-Revision-Date: 2025-02-13 20:35+0000\n" +"Last-Translator: \"Pedro M. Baeza\" \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