From 835e0c9b16bec06cd1d25795021e2018812582e5 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Thu, 8 Feb 2024 19:25:28 +0100 Subject: [PATCH 01/13] [ADD] account_reconcile_model_oca --- account_reconcile_model_oca/README.rst | 80 + account_reconcile_model_oca/__init__.py | 1 + account_reconcile_model_oca/__manifest__.py | 15 + .../models/__init__.py | 2 + .../models/account_bank_statement_line.py | 128 ++ .../models/account_reconcile_model.py | 667 ++++++++ account_reconcile_model_oca/pyproject.toml | 3 + .../readme/CONTRIBUTORS.md | 3 + .../readme/DESCRIPTION.md | 1 + .../static/description/icon.png | Bin 0 -> 9455 bytes .../static/description/index.html | 426 +++++ account_reconcile_model_oca/tests/__init__.py | 1 + account_reconcile_model_oca/tests/common.py | 243 +++ .../tests/test_reconciliation_match.py | 1491 +++++++++++++++++ 14 files changed, 3061 insertions(+) create mode 100644 account_reconcile_model_oca/README.rst create mode 100644 account_reconcile_model_oca/__init__.py create mode 100644 account_reconcile_model_oca/__manifest__.py create mode 100644 account_reconcile_model_oca/models/__init__.py create mode 100644 account_reconcile_model_oca/models/account_bank_statement_line.py create mode 100644 account_reconcile_model_oca/models/account_reconcile_model.py create mode 100644 account_reconcile_model_oca/pyproject.toml create mode 100644 account_reconcile_model_oca/readme/CONTRIBUTORS.md create mode 100644 account_reconcile_model_oca/readme/DESCRIPTION.md create mode 100644 account_reconcile_model_oca/static/description/icon.png create mode 100644 account_reconcile_model_oca/static/description/index.html create mode 100644 account_reconcile_model_oca/tests/__init__.py create mode 100644 account_reconcile_model_oca/tests/common.py create mode 100644 account_reconcile_model_oca/tests/test_reconciliation_match.py diff --git a/account_reconcile_model_oca/README.rst b/account_reconcile_model_oca/README.rst new file mode 100644 index 0000000000..5da1edf5d6 --- /dev/null +++ b/account_reconcile_model_oca/README.rst @@ -0,0 +1,80 @@ +=========================== +Account Reconcile Model Oca +=========================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:62683a913039d5afe96202b49fdd537d57edf8ac49edc89bf9c9e4512971888f + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Faccount--reconcile-lightgray.png?logo=github + :target: https://github.com/OCA/account-reconcile/tree/17.0/account_reconcile_model_oca + :alt: OCA/account-reconcile +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/account-reconcile-17-0/account-reconcile-17-0-account_reconcile_model_oca + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/account-reconcile&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module restores account reconciliation models functions moved from +Odoo community to enterpise in V. 17.0 + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Dixmit +* Odoo + +Contributors +------------ + +- Dixmit + + - Enric Tobella + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/account-reconcile `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_reconcile_model_oca/__init__.py b/account_reconcile_model_oca/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/account_reconcile_model_oca/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/account_reconcile_model_oca/__manifest__.py b/account_reconcile_model_oca/__manifest__.py new file mode 100644 index 0000000000..9dec6a1ab8 --- /dev/null +++ b/account_reconcile_model_oca/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright 2024 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Account Reconcile Model Oca", + "summary": """ + This includes the logic moved from Odoo Community to Odoo Enterprise""", + "version": "17.0.1.0.0", + "license": "LGPL-3", + "author": "Dixmit,Odoo,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/account-reconcile", + "depends": ["account"], + "data": [], + "demo": [], +} diff --git a/account_reconcile_model_oca/models/__init__.py b/account_reconcile_model_oca/models/__init__.py new file mode 100644 index 0000000000..cbaab70a65 --- /dev/null +++ b/account_reconcile_model_oca/models/__init__.py @@ -0,0 +1,2 @@ +from . import account_reconcile_model +from . import account_bank_statement_line diff --git a/account_reconcile_model_oca/models/account_bank_statement_line.py b/account_reconcile_model_oca/models/account_bank_statement_line.py new file mode 100644 index 0000000000..234f76bfe2 --- /dev/null +++ b/account_reconcile_model_oca/models/account_bank_statement_line.py @@ -0,0 +1,128 @@ +# Copyright 2023 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models +from odoo.osv.expression import get_unaccent_wrapper +from odoo.tools import html2plaintext + +from odoo.addons.base.models.res_bank import sanitize_account_number + + +class AccountBankStatementLine(models.Model): + _inherit = ("account.bank.statement.line",) + + def _retrieve_partner(self): + self.ensure_one() + + # Retrieve the partner from the statement line. + if self.partner_id: + return self.partner_id + + # Retrieve the partner from the bank account. + if self.account_number: + account_number_nums = sanitize_account_number(self.account_number) + if account_number_nums: + domain = [("sanitized_acc_number", "ilike", account_number_nums)] + for extra_domain in ([("company_id", "=", self.company_id.id)], []): + bank_accounts = self.env["res.partner.bank"].search( + extra_domain + domain + ) + if len(bank_accounts.partner_id) == 1: + return bank_accounts.partner_id + + # Retrieve the partner from the partner name. + if self.partner_name: + domain = [ + ("parent_id", "=", False), + ("name", "ilike", self.partner_name), + ] + for extra_domain in ([("company_id", "=", self.company_id.id)], []): + partner = self.env["res.partner"].search(extra_domain + domain, limit=1) + if partner: + return partner + + # Retrieve the partner from the 'reconcile models'. + rec_models = self.env["account.reconcile.model"].search( + [ + ("rule_type", "!=", "writeoff_button"), + ("company_id", "=", self.company_id.id), + ] + ) + for rec_model in rec_models: + partner = rec_model._get_partner_from_mapping(self) + if partner and rec_model._is_applicable_for(self, partner): + return partner + + # Retrieve the partner from statement line text values. + st_line_text_values = self._get_st_line_strings_for_matching() + unaccent = get_unaccent_wrapper(self._cr) + sub_queries = [] + params = [] + for text_value in st_line_text_values: + if not text_value: + continue + + # Find a partner having a name contained inside the statement line values. + # Take care a partner could contain some special characters in its name that needs to be escaped. + sub_queries.append( + rf""" + {unaccent("%s")} ~* ('^' || ( + SELECT STRING_AGG(CONCAT('(?=.*\m', chunk[1], '\M)'), '') + FROM regexp_matches({unaccent('partner.name')}, '\w{{3,}}', 'g') AS chunk + )) + """ + ) + params.append(text_value) + + if sub_queries: + self.env["res.partner"].flush_model(["company_id", "name"]) + self.env["account.move.line"].flush_model(["partner_id", "company_id"]) + self._cr.execute( + """ + SELECT aml.partner_id + FROM account_move_line aml + JOIN res_partner partner ON + aml.partner_id = partner.id + AND partner.name IS NOT NULL + AND partner.active + AND ((""" + + ") OR (".join(sub_queries) + + """)) + WHERE aml.company_id = %s + LIMIT 1 + """, + params + [self.company_id.id], + ) + row = self._cr.fetchone() + if row: + return self.env["res.partner"].browse(row[0]) + + return self.env["res.partner"] + + def _get_st_line_strings_for_matching(self, allowed_fields=None): + """Collect the strings that could be used on the statement line to perform some matching. + :param allowed_fields: A explicit list of fields to consider. + :return: A list of strings. + """ + self.ensure_one() + + def _get_text_value(field_name): + if self._fields[field_name].type == "html": + return 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 value: + st_line_text_values.append(value) + 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 new file mode 100644 index 0000000000..2ad17fa411 --- /dev/null +++ b/account_reconcile_model_oca/models/account_reconcile_model.py @@ -0,0 +1,667 @@ +import re +from collections import defaultdict + +from dateutil.relativedelta import relativedelta + +from odoo import Command, fields, models, tools + + +class AccountReconcileModel(models.Model): + _inherit = "account.reconcile.model" + + #################################################### + # RECONCILIATION PROCESS + #################################################### + + def _apply_lines_for_bank_widget(self, residual_amount_currency, partner, st_line): + """Apply the reconciliation model lines to the statement line passed as parameter. + :param residual_amount_currency: The open balance of the statement line in the bank reconciliation widget + expressed in the statement line currency. + :param partner: The partner set on the wizard. + :param st_line: The statement line processed by the bank reconciliation widget. + :return: A list of python dictionaries (one per reconcile model line) representing + the journal items to be created by the current reconcile model. + """ + self.ensure_one() + currency = ( + st_line.foreign_currency_id + or st_line.journal_id.currency_id + or st_line.company_currency_id + ) + if currency.is_zero(residual_amount_currency): + return [] + + vals_list = [] + for line in self.line_ids: + vals = line._apply_in_bank_widget( + residual_amount_currency, partner, st_line + ) + amount_currency = vals["amount_currency"] + + if currency.is_zero(amount_currency): + continue + + vals_list.append(vals) + residual_amount_currency -= amount_currency + + return vals_list + + def _get_taxes_move_lines_dict(self, tax, base_line_dict): + """Get move.lines dict (to be passed to the create()) corresponding to a tax. + :param tax: An account.tax record. + :param base_line_dict: A dict representing the move.line containing the base amount. + :return: A list of dict representing move.lines to be created corresponding to the tax. + """ + self.ensure_one() + balance = base_line_dict["balance"] + + tax_type = tax.type_tax_use + is_refund = (tax_type == "sale" and balance < 0) or ( + tax_type == "purchase" and balance > 0 + ) + + res = tax.compute_all(balance, is_refund=is_refund) + + new_aml_dicts = [] + for tax_res in res["taxes"]: + tax = self.env["account.tax"].browse(tax_res["id"]) + balance = tax_res["amount"] + name = " ".join( + [x for x in [base_line_dict.get("name", ""), tax_res["name"]] if x] + ) + new_aml_dicts.append( + { + "account_id": tax_res["account_id"] or base_line_dict["account_id"], + "journal_id": base_line_dict.get("journal_id", False), + "name": name, + "partner_id": base_line_dict.get("partner_id"), + "balance": balance, + "debit": balance > 0 and balance or 0, + "credit": balance < 0 and -balance or 0, + "analytic_distribution": tax.analytic + and base_line_dict["analytic_distribution"], + "tax_repartition_line_id": tax_res["tax_repartition_line_id"], + "tax_ids": [(6, 0, tax_res["tax_ids"])], + "tax_tag_ids": [(6, 0, tax_res["tag_ids"])], + "group_tax_id": tax_res["group"].id if tax_res["group"] else False, + "currency_id": False, + "reconcile_model_id": self.id, + } + ) + + # Handle price included taxes. + base_balance = tax_res["base"] + base_line_dict.update( + { + "balance": base_balance, + "debit": base_balance > 0 and base_balance or 0, + "credit": base_balance < 0 and -base_balance or 0, + } + ) + + base_line_dict["tax_tag_ids"] = [(6, 0, res["base_tags"])] + return new_aml_dicts + + def _get_write_off_move_lines_dict(self, residual_balance, partner_id): + """Get move.lines dict corresponding to the reconciliation model's write-off lines. + :param residual_balance: The residual balance of the account on the manual reconciliation widget. + :return: A list of dict representing move.lines to be created corresponding to the write-off lines. + """ + self.ensure_one() + + if self.rule_type == "invoice_matching" and ( + not self.allow_payment_tolerance or self.payment_tolerance_param == 0 + ): + return [] + + currency = self.company_id.currency_id + + lines_vals_list = [] + for line in self.line_ids: + balance = 0 + if line.amount_type == "percentage": + balance = currency.round(residual_balance * (line.amount / 100.0)) + elif line.amount_type == "fixed": + balance = currency.round( + line.amount * (1 if residual_balance > 0.0 else -1) + ) + + if currency.is_zero(balance): + continue + + writeoff_line = { + "name": line.label, + "balance": balance, + "debit": balance > 0 and balance or 0, + "credit": balance < 0 and -balance or 0, + "account_id": line.account_id.id, + "currency_id": currency.id, + "analytic_distribution": line.analytic_distribution, + "reconcile_model_id": self.id, + "journal_id": line.journal_id.id, + "tax_ids": [], + } + lines_vals_list.append(writeoff_line) + + residual_balance -= balance + + if line.tax_ids: + taxes = line.tax_ids + detected_fiscal_position = self.env[ + "account.fiscal.position" + ]._get_fiscal_position(self.env["res.partner"].browse(partner_id)) + if detected_fiscal_position: + taxes = detected_fiscal_position.map_tax(taxes) + writeoff_line["tax_ids"] += [Command.set(taxes.ids)] + # Multiple taxes with force_tax_included results in wrong computation, so we + # only allow to set the force_tax_included field if we have one tax selected + if line.force_tax_included: + taxes = taxes[0].with_context(force_price_include=True) + tax_vals_list = self._get_taxes_move_lines_dict(taxes, writeoff_line) + lines_vals_list += tax_vals_list + if not line.force_tax_included: + for tax_line in tax_vals_list: + residual_balance -= tax_line["balance"] + + return lines_vals_list + + #################################################### + # RECONCILIATION CRITERIA + #################################################### + + def _apply_rules(self, st_line, partner): + """Apply criteria to get candidates for all reconciliation models. + This function is called in enterprise by the reconciliation widget to match + the statement line with the available candidates (using the reconciliation models). + :param st_line: The statement line to match. + :param partner: The partner to consider. + :return: A dict mapping each statement line id with: + * aml_ids: A list of account.move.line ids. + * model: An account.reconcile.model record (optional). + * status: 'reconciled' if the lines has been already reconciled, 'write_off' if the write-off + must be applied on the statement line. + * auto_reconcile: A flag indicating if the match is enough significant to auto reconcile the candidates. + """ + available_models = self.filtered( + lambda m: m.rule_type != "writeoff_button" + ).sorted() + + for rec_model in available_models: + if not rec_model._is_applicable_for(st_line, partner): + continue + + if rec_model.rule_type == "invoice_matching": + rules_map = rec_model._get_invoice_matching_rules_map() + for rule_index in sorted(rules_map.keys()): + for rule_method in rules_map[rule_index]: + candidate_vals = rule_method(st_line, partner) + if not candidate_vals: + continue + + if candidate_vals.get("amls"): + res = rec_model._get_invoice_matching_amls_result( + st_line, partner, candidate_vals + ) + if res: + return { + **res, + "model": rec_model, + } + else: + return { + **candidate_vals, + "model": rec_model, + } + + elif rec_model.rule_type == "writeoff_suggestion": + return { + "model": rec_model, + "status": "write_off", + "auto_reconcile": rec_model.auto_reconcile, + } + return {} + + def _is_applicable_for(self, st_line, partner): + """Returns true iff this reconciliation model can be used to search for matches + for the provided statement line and partner. + """ + self.ensure_one() + + # Filter on journals, amount nature, amount and partners + # All the conditions defined in this block are non-match conditions. + if ( + ( + self.match_journal_ids + and st_line.move_id.journal_id not in self.match_journal_ids + ) + or (self.match_nature == "amount_received" and st_line.amount < 0) + or (self.match_nature == "amount_paid" and st_line.amount > 0) + or ( + self.match_amount == "lower" + and abs(st_line.amount) >= self.match_amount_max + ) + or ( + self.match_amount == "greater" + and abs(st_line.amount) <= self.match_amount_min + ) + or ( + self.match_amount == "between" + and ( + abs(st_line.amount) > self.match_amount_max + or abs(st_line.amount) < self.match_amount_min + ) + ) + or (self.match_partner and not partner) + or ( + self.match_partner + and self.match_partner_ids + and partner not in self.match_partner_ids + ) + or ( + self.match_partner + and self.match_partner_category_ids + and partner.category_id not in self.match_partner_category_ids + ) + ): + return False + + # Filter on label, note and transaction_type + for record, rule_field, record_field in [ + (st_line, "label", "payment_ref"), + (st_line.move_id, "note", "narration"), + (st_line, "transaction_type", "transaction_type"), + ]: + rule_term = (self["match_" + rule_field + "_param"] or "").lower() + record_term = (record[record_field] or "").lower() + + # This defines non-match conditions + if ( + ( + self["match_" + rule_field] == "contains" + and rule_term not in record_term + ) + or ( + self["match_" + rule_field] == "not_contains" + and rule_term in record_term + ) + or ( + self["match_" + rule_field] == "match_regex" + and not re.match(rule_term, record_term) + ) + ): + return False + + return True + + def _get_invoice_matching_amls_domain(self, st_line, partner): + aml_domain = st_line._get_default_amls_matching_domain() + + if st_line.amount > 0.0: + aml_domain.append(("balance", ">", 0.0)) + else: + aml_domain.append(("balance", "<", 0.0)) + + currency = st_line.foreign_currency_id or st_line.currency_id + if self.match_same_currency: + aml_domain.append(("currency_id", "=", currency.id)) + + if partner: + aml_domain.append(("partner_id", "=", partner.id)) + + if self.past_months_limit: + date_limit = fields.Date.context_today(self) - relativedelta( + months=self.past_months_limit + ) + aml_domain.append(("date", ">=", fields.Date.to_string(date_limit))) + + return aml_domain + + 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. + """ + 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, + ) + ) + significant_token_size = 4 + tokens = [] + for text_value in st_line_text_values: + for token in (text_value or "").split(): + # The token is too short to be significant. + if len(token) < significant_token_size: + continue + + 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 + + def _get_invoice_matching_amls_candidates(self, st_line, partner): + """Returns the match candidates for the 'invoice_matching' rule, with respect to the provided parameters. + :param st_line: A statement line. + :param partner: The partner associated to the statement line. + """ + 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.append( + rf""" + SELECT + account_move_line.id, + account_move_line.date, + account_move_line.date_maturity, + UNNEST( + REGEXP_SPLIT_TO_ARRAY( + SUBSTRING( + REGEXP_REPLACE({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 + """ + ) + + self._cr.execute( + """ + SELECT + sub.id, + COUNT(*) AS nb_match + FROM (""" + + " UNION ALL ".join(sub_queries) + + """) AS sub + WHERE sub.token IN %s + GROUP BY sub.date_maturity, sub.date, sub.id + HAVING COUNT(*) > 0 + ORDER BY nb_match DESC, """ + + order_by + + """ + """, + (where_params * 3) + [tuple(tokens)], + ) + candidate_ids = [r[0] for r in self._cr.fetchall()] + if candidate_ids: + return { + "allow_auto_reconcile": True, + "amls": self.env["account.move.line"].browse(candidate_ids), + } + + # Search without any matching based on textual information. + if partner: + if self.matching_order == "new_first": + order = "date_maturity DESC, date DESC, id DESC" + else: + order = "date_maturity ASC, date ASC, id ASC" + + amls = self.env["account.move.line"].search(aml_domain, order=order) + 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 modules. + :return: a mapping where: + * priority_order: Defines in which order the rules will be evaluated, the lowest comes first. + This is extremely important since the algorithm stops when a rule returns some candidates. + * rule: Method taking as parameters and returning the candidates journal items found. + """ + rules_map = defaultdict(list) + rules_map[10].append(self._get_invoice_matching_amls_candidates) + return rules_map + + def _get_partner_from_mapping(self, st_line): + """Find partner with mapping defined on model. + For invoice matching rules, matches the statement line against each + regex defined in partner mapping, and returns the partner corresponding + to the first one matching. + :param st_line (Model): + The statement line that needs a partner to be found + :return Model: + The partner found from the mapping. Can be empty an empty recordset + if there was nothing found from the mapping or if the function is + not applicable. + """ + self.ensure_one() + + if self.rule_type not in ("invoice_matching", "writeoff_suggestion"): + return self.env["res.partner"] + + for partner_mapping in self.partner_mapping_line_ids: + match_payment_ref = ( + re.match(partner_mapping.payment_ref_regex, st_line.payment_ref) + if partner_mapping.payment_ref_regex + else True + ) + match_narration = ( + re.match( + partner_mapping.narration_regex, + tools.html2plaintext(st_line.narration or "").rstrip(), + ) + if partner_mapping.narration_regex + else True + ) + + if match_payment_ref and match_narration: + return partner_mapping.partner_id + return self.env["res.partner"] + + def _get_invoice_matching_amls_result(self, st_line, partner, candidate_vals): # noqa: C901 + def _create_result_dict(amls_values_list, status): + if "rejected" in status: + return + + result = {"amls": self.env["account.move.line"]} + for aml_values in amls_values_list: + result["amls"] |= aml_values["aml"] + + if "allow_write_off" in status and self.line_ids: + result["status"] = "write_off" + + if ( + "allow_auto_reconcile" in status + and candidate_vals["allow_auto_reconcile"] + and self.auto_reconcile + ): + result["auto_reconcile"] = True + + return result + + st_line_currency = st_line.foreign_currency_id or st_line.currency_id + st_line_amount = st_line._prepare_move_line_default_vals()[1]["amount_currency"] + sign = 1 if st_line_amount > 0.0 else -1 + + amls = candidate_vals["amls"] + amls_values_list = [] + amls_with_epd_values_list = [] + same_currency_mode = amls.currency_id == st_line_currency + for aml in amls: + aml_values = { + "aml": aml, + "amount_residual": aml.amount_residual, + "amount_residual_currency": aml.amount_residual_currency, + } + + amls_values_list.append(aml_values) + + # Manage the early payment discount. + if ( + same_currency_mode + and aml.move_id.move_type + in ("out_invoice", "out_receipt", "in_invoice", "in_receipt") + and not aml.matched_debit_ids + and not aml.matched_credit_ids + and aml.discount_date + and st_line.date <= aml.discount_date + ): + rate = ( + abs(aml.amount_currency) / abs(aml.balance) if aml.balance else 1.0 + ) + amls_with_epd_values_list.append( + { + **aml_values, + "amount_residual": st_line.company_currency_id.round( + aml.discount_amount_currency / rate + ), + "amount_residual_currency": aml.discount_amount_currency, + } + ) + else: + amls_with_epd_values_list.append(aml_values) + + def match_batch_amls(amls_values_list): + if not same_currency_mode: + return None, [] + + kepts_amls_values_list = [] + sum_amount_residual_currency = 0.0 + for aml_values in amls_values_list: + if ( + st_line_currency.compare_amounts( + st_line_amount, -aml_values["amount_residual_currency"] + ) + == 0 + ): + # Special case: the amounts are the same, submit the line directly. + return "perfect", [aml_values] + + if ( + st_line_currency.compare_amounts( + sign * (st_line_amount + sum_amount_residual_currency), 0.0 + ) + > 0 + ): + # Here, we still have room for other candidates ; so we add the current one to the list we keep. + # Then, we continue iterating, even if there is no room anymore, just in case one of the following candidates + # is an exact match, which would then be preferred on the current candidates. + kepts_amls_values_list.append(aml_values) + sum_amount_residual_currency += aml_values[ + "amount_residual_currency" + ] + + if st_line_currency.is_zero( + sign * (st_line_amount + sum_amount_residual_currency) + ): + return "perfect", kepts_amls_values_list + elif kepts_amls_values_list: + return "partial", kepts_amls_values_list + else: + return None, [] + + # Try to match a batch with the early payment feature. Only a perfect match is allowed. + match_type, kepts_amls_values_list = match_batch_amls(amls_with_epd_values_list) + if match_type != "perfect": + kepts_amls_values_list = [] + + # Try to match the amls having the same currency as the statement line. + if not kepts_amls_values_list: + _match_type, kepts_amls_values_list = match_batch_amls(amls_values_list) + + # Try to match the whole candidates. + if not kepts_amls_values_list: + kepts_amls_values_list = amls_values_list + + # Try to match the amls having the same currency as the statement line. + if kepts_amls_values_list: + status = self._check_rule_propositions(st_line, kepts_amls_values_list) + result = _create_result_dict(kepts_amls_values_list, status) + if result: + return result + + def _check_rule_propositions(self, st_line, amls_values_list): + """Check restrictions that can't be handled for each move.line separately. + Note: Only used by models having a type equals to 'invoice_matching'. + :param st_line: The statement line. + :param amls_values_list: The candidates account.move.line as a list of dict: + * aml: The record. + * amount_residual: The amount residual to consider. + * amount_residual_currency: The amount residual in foreign currency to consider. + :return: A string representing what to do with the candidates: + * rejected: Reject candidates. + * allow_write_off: Allow to generate the write-off from the reconcile model lines if specified. + * allow_auto_reconcile: Allow to automatically reconcile entries if 'auto_validate' is enabled. + """ + self.ensure_one() + + if not self.allow_payment_tolerance: + return {"allow_write_off", "allow_auto_reconcile"} + + st_line_currency = st_line.foreign_currency_id or st_line.currency_id + st_line_amount_curr = st_line._prepare_move_line_default_vals()[1][ + "amount_currency" + ] + amls_amount_curr = sum( + st_line._prepare_counterpart_amounts_using_st_line_rate( + aml_values["aml"].currency_id, + aml_values["amount_residual"], + aml_values["amount_residual_currency"], + )["amount_currency"] + 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) + + # The statement line will be fully reconciled. + if st_line_currency.is_zero(amount_curr_after_rec): + return {"allow_auto_reconcile"} + + # The payment amount is higher than the sum of invoices. + # In that case, don't check the tolerance and don't try to generate any write-off. + if amount_curr_after_rec > 0.0: + return {"allow_auto_reconcile"} + + # No tolerance, reject the candidates. + if self.payment_tolerance_param == 0: + return {"rejected"} + + # If the tolerance is expressed as a fixed amount, check the residual payment amount doesn't exceed the + # tolerance. + if ( + self.payment_tolerance_type == "fixed_amount" + and -amount_curr_after_rec <= self.payment_tolerance_param + ): + return {"allow_write_off", "allow_auto_reconcile"} + + # The tolerance is expressed as a percentage between 0 and 100.0. + reconciled_percentage_left = ( + abs(amount_curr_after_rec / amls_amount_curr) + ) * 100.0 + if ( + self.payment_tolerance_type == "percentage" + and reconciled_percentage_left <= self.payment_tolerance_param + ): + return {"allow_write_off", "allow_auto_reconcile"} + + return {"rejected"} diff --git a/account_reconcile_model_oca/pyproject.toml b/account_reconcile_model_oca/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/account_reconcile_model_oca/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/account_reconcile_model_oca/readme/CONTRIBUTORS.md b/account_reconcile_model_oca/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..becca42792 --- /dev/null +++ b/account_reconcile_model_oca/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- Dixmit + + - Enric Tobella diff --git a/account_reconcile_model_oca/readme/DESCRIPTION.md b/account_reconcile_model_oca/readme/DESCRIPTION.md new file mode 100644 index 0000000000..f296e00083 --- /dev/null +++ b/account_reconcile_model_oca/readme/DESCRIPTION.md @@ -0,0 +1 @@ +This module restores account reconciliation models functions moved from Odoo community to enterpise in V. 17.0 diff --git a/account_reconcile_model_oca/static/description/icon.png b/account_reconcile_model_oca/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/account_reconcile_model_oca/static/description/index.html b/account_reconcile_model_oca/static/description/index.html new file mode 100644 index 0000000000..b0ab3f4085 --- /dev/null +++ b/account_reconcile_model_oca/static/description/index.html @@ -0,0 +1,426 @@ + + + + + + +Account Reconcile Model Oca + + + +
+

Account Reconcile Model Oca

+ + +

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

+

This module restores account reconciliation models functions moved from +Odoo community to enterpise in V. 17.0

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Dixmit
  • +
  • Odoo
  • +
+
+
+

Contributors

+
    +
  • Dixmit
      +
    • Enric Tobella
    • +
    +
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/account-reconcile project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/account_reconcile_model_oca/tests/__init__.py b/account_reconcile_model_oca/tests/__init__.py new file mode 100644 index 0000000000..4198497cdc --- /dev/null +++ b/account_reconcile_model_oca/tests/__init__.py @@ -0,0 +1 @@ +from . import test_reconciliation_match diff --git a/account_reconcile_model_oca/tests/common.py b/account_reconcile_model_oca/tests/common.py new file mode 100644 index 0000000000..7b9a804cb2 --- /dev/null +++ b/account_reconcile_model_oca/tests/common.py @@ -0,0 +1,243 @@ +import time + +from odoo import Command + +from odoo.addons.account.tests.common import AccountTestInvoicingCommon + + +class TestAccountReconciliationCommon(AccountTestInvoicingCommon): + + """Tests for reconciliation (account.tax) + + Test used to check that when doing a sale or purchase invoice in a different currency, + the result will be balanced. + """ + + @classmethod + def setUpClass(cls, chart_template_ref=None): + super().setUpClass(chart_template_ref=chart_template_ref) + + cls.company = cls.company_data["company"] + cls.company.currency_id = cls.env.ref("base.EUR") + + cls.partner_agrolait = cls.env["res.partner"].create( + { + "name": "Deco Addict", + "is_company": True, + "country_id": cls.env.ref("base.us").id, + } + ) + cls.partner_agrolait_id = cls.partner_agrolait.id + cls.currency_swiss_id = cls.env.ref("base.CHF").id + cls.currency_usd_id = cls.env.ref("base.USD").id + cls.currency_euro_id = cls.env.ref("base.EUR").id + cls.account_rcv = cls.company_data["default_account_receivable"] + cls.account_rsa = cls.company_data["default_account_payable"] + cls.product = cls.env["product.product"].create( + { + "name": "Product Product 4", + "standard_price": 500.0, + "list_price": 750.0, + "type": "consu", + "categ_id": cls.env.ref("product.product_category_all").id, + } + ) + + cls.bank_journal_euro = cls.env["account.journal"].create( + {"name": "Bank", "type": "bank", "code": "BNK67"} + ) + cls.account_euro = cls.bank_journal_euro.default_account_id + + cls.bank_journal_usd = cls.env["account.journal"].create( + { + "name": "Bank US", + "type": "bank", + "code": "BNK68", + "currency_id": cls.currency_usd_id, + } + ) + cls.account_usd = cls.bank_journal_usd.default_account_id + + cls.fx_journal = cls.company.currency_exchange_journal_id + cls.diff_income_account = cls.company.income_currency_exchange_account_id + cls.diff_expense_account = cls.company.expense_currency_exchange_account_id + + cls.expense_account = cls.company_data["default_account_expense"] + # cash basis intermediary account + cls.tax_waiting_account = cls.env["account.account"].create( + { + "name": "TAX_WAIT", + "code": "TWAIT", + "account_type": "liability_current", + "reconcile": True, + "company_id": cls.company.id, + } + ) + # cash basis final account + cls.tax_final_account = cls.env["account.account"].create( + { + "name": "TAX_TO_DEDUCT", + "code": "TDEDUCT", + "account_type": "asset_current", + "company_id": cls.company.id, + } + ) + cls.tax_base_amount_account = cls.env["account.account"].create( + { + "name": "TAX_BASE", + "code": "TBASE", + "account_type": "asset_current", + "company_id": cls.company.id, + } + ) + cls.company.account_cash_basis_base_account_id = cls.tax_base_amount_account.id + + # Journals + cls.purchase_journal = cls.company_data["default_journal_purchase"] + cls.cash_basis_journal = cls.env["account.journal"].create( + { + "name": "Test CABA", + "code": "tCABA", + "type": "general", + } + ) + cls.general_journal = cls.company_data["default_journal_misc"] + + # Tax Cash Basis + cls.tax_cash_basis = cls.env["account.tax"].create( + { + "name": "cash basis 20%", + "type_tax_use": "purchase", + "company_id": cls.company.id, + "country_id": cls.company.account_fiscal_country_id.id, + "amount": 20, + "tax_exigibility": "on_payment", + "cash_basis_transition_account_id": cls.tax_waiting_account.id, + "invoice_repartition_line_ids": [ + ( + 0, + 0, + { + "repartition_type": "base", + }, + ), + ( + 0, + 0, + { + "repartition_type": "tax", + "account_id": cls.tax_final_account.id, + }, + ), + ], + "refund_repartition_line_ids": [ + ( + 0, + 0, + { + "repartition_type": "base", + }, + ), + ( + 0, + 0, + { + "repartition_type": "tax", + "account_id": cls.tax_final_account.id, + }, + ), + ], + } + ) + cls.env["res.currency.rate"].create( + [ + { + "currency_id": cls.env.ref("base.EUR").id, + "name": "2010-01-02", + "rate": 1.0, + }, + { + "currency_id": cls.env.ref("base.USD").id, + "name": "2010-01-02", + "rate": 1.2834, + }, + { + "currency_id": cls.env.ref("base.USD").id, + "name": time.strftime("%Y-06-05"), + "rate": 1.5289, + }, + ] + ) + + def _create_invoice( + self, + move_type="out_invoice", + invoice_amount=50, + currency_id=None, + partner_id=None, + date_invoice=None, + payment_term_id=False, + auto_validate=False, + ): + date_invoice = date_invoice or time.strftime("%Y") + "-07-01" + + invoice_vals = { + "move_type": move_type, + "partner_id": partner_id or self.partner_agrolait_id, + "invoice_date": date_invoice, + "date": date_invoice, + "invoice_line_ids": [ + ( + 0, + 0, + { + "name": "product that cost %s" % invoice_amount, + "quantity": 1, + "price_unit": invoice_amount, + "tax_ids": [Command.set([])], + }, + ) + ], + } + + if payment_term_id: + invoice_vals["invoice_payment_term_id"] = payment_term_id + + if currency_id: + invoice_vals["currency_id"] = currency_id + + invoice = ( + self.env["account.move"] + .with_context(default_move_type=move_type) + .create(invoice_vals) + ) + if auto_validate: + invoice.action_post() + return invoice + + def create_invoice( + self, move_type="out_invoice", invoice_amount=50, currency_id=None + ): + return self._create_invoice( + move_type=move_type, + invoice_amount=invoice_amount, + currency_id=currency_id, + auto_validate=True, + ) + + def create_invoice_partner( + self, + move_type="out_invoice", + invoice_amount=50, + currency_id=None, + partner_id=False, + payment_term_id=False, + ): + return self._create_invoice( + move_type=move_type, + invoice_amount=invoice_amount, + currency_id=currency_id, + partner_id=partner_id, + payment_term_id=payment_term_id, + auto_validate=True, + ) diff --git a/account_reconcile_model_oca/tests/test_reconciliation_match.py b/account_reconcile_model_oca/tests/test_reconciliation_match.py new file mode 100644 index 0000000000..26704d15e3 --- /dev/null +++ b/account_reconcile_model_oca/tests/test_reconciliation_match.py @@ -0,0 +1,1491 @@ +from freezegun import freeze_time + +from odoo import Command +from odoo.tests import tagged +from odoo.tests.common import Form + +from odoo.addons.account.tests.common import AccountTestInvoicingCommon + + +@tagged("post_install", "-at_install") +class TestReconciliationMatchingRules(AccountTestInvoicingCommon): + @classmethod + def setUpClass(cls, chart_template_ref=None): + super().setUpClass(chart_template_ref=chart_template_ref) + + ################# + # Company setup # + ################# + cls.currency_data_2 = cls.setup_multi_currency_data( + { + "name": "Dark Chocolate Coin", + "symbol": "🍫", + "currency_unit_label": "Dark Choco", + "currency_subunit_label": "Dark Cacao Powder", + }, + rate2016=10.0, + rate2017=20.0, + ) + + cls.company = cls.company_data["company"] + + cls.account_pay = cls.company_data["default_account_payable"] + cls.current_assets_account = cls.env["account.account"].search( + [ + ("account_type", "=", "asset_current"), + ("company_id", "=", cls.company.id), + ], + limit=1, + ) + + cls.bank_journal = cls.env["account.journal"].search( + [("type", "=", "bank"), ("company_id", "=", cls.company.id)], limit=1 + ) + cls.cash_journal = cls.env["account.journal"].search( + [("type", "=", "cash"), ("company_id", "=", cls.company.id)], limit=1 + ) + + cls.tax21 = cls.env["account.tax"].create( + { + "name": "21%", + "type_tax_use": "purchase", + "amount": 21, + } + ) + + cls.tax12 = cls.env["account.tax"].create( + { + "name": "12%", + "type_tax_use": "purchase", + "amount": 12, + } + ) + + cls.partner_1 = cls.env["res.partner"].create( + {"name": "partner_1", "company_id": cls.company.id} + ) + cls.partner_2 = cls.env["res.partner"].create( + {"name": "partner_2", "company_id": cls.company.id} + ) + cls.partner_3 = cls.env["res.partner"].create( + {"name": "partner_3", "company_id": cls.company.id} + ) + + ############### + # Rules setup # + ############### + cls.rule_1 = cls.env["account.reconcile.model"].create( + { + "name": "Invoices Matching Rule", + "sequence": "1", + "rule_type": "invoice_matching", + "auto_reconcile": False, + "match_nature": "both", + "match_same_currency": True, + "allow_payment_tolerance": True, + "payment_tolerance_type": "percentage", + "payment_tolerance_param": 0.0, + "match_partner": True, + "match_partner_ids": [ + (6, 0, (cls.partner_1 + cls.partner_2 + cls.partner_3).ids) + ], + "company_id": cls.company.id, + "line_ids": [(0, 0, {"account_id": cls.current_assets_account.id})], + } + ) + cls.rule_2 = cls.env["account.reconcile.model"].create( + { + "name": "write-off model", + "rule_type": "writeoff_suggestion", + "match_partner": True, + "match_partner_ids": [], + "line_ids": [(0, 0, {"account_id": cls.current_assets_account.id})], + } + ) + + ################## + # Invoices setup # + ################## + cls.invoice_line_1 = cls._create_invoice_line(100, cls.partner_1, "out_invoice") + cls.invoice_line_2 = cls._create_invoice_line(200, cls.partner_1, "out_invoice") + cls.invoice_line_3 = cls._create_invoice_line( + 300, cls.partner_1, "in_refund", name="RBILL/2019/09/0013" + ) + cls.invoice_line_4 = cls._create_invoice_line(1000, cls.partner_2, "in_invoice") + cls.invoice_line_5 = cls._create_invoice_line(600, cls.partner_3, "out_invoice") + cls.invoice_line_6 = cls._create_invoice_line( + 600, cls.partner_3, "out_invoice", ref="RF12 3456" + ) + cls.invoice_line_7 = cls._create_invoice_line( + 200, cls.partner_3, "out_invoice", pay_reference="RF12 3456" + ) + + #################### + # Statements setup # + #################### + # TODO : account_number, partner_name, transaction_type, narration + invoice_number = cls.invoice_line_1.move_id.name + ( + cls.bank_line_1, + cls.bank_line_2, + cls.bank_line_3, + cls.bank_line_4, + cls.bank_line_5, + cls.cash_line_1, + ) = cls.env["account.bank.statement.line"].create( + [ + { + "journal_id": cls.bank_journal.id, + "date": "2020-01-01", + "payment_ref": "invoice {}-{}".format( + *invoice_number.split("/")[1:] + ), + "partner_id": cls.partner_1.id, + "amount": 100, + "sequence": 1, + }, + { + "journal_id": cls.bank_journal.id, + "date": "2020-01-01", + "payment_ref": "xxxxx", + "partner_id": cls.partner_1.id, + "amount": 600, + "sequence": 2, + }, + { + "journal_id": cls.bank_journal.id, + "date": "2020-01-01", + "payment_ref": "nawak", + "narration": "Communication: RF12 3456", + "partner_id": cls.partner_3.id, + "amount": 600, + "sequence": 1, + }, + { + "journal_id": cls.bank_journal.id, + "date": "2020-01-01", + "payment_ref": "RF12 3456", + "partner_id": cls.partner_3.id, + "amount": 600, + "sequence": 2, + }, + { + "journal_id": cls.bank_journal.id, + "date": "2020-01-01", + "payment_ref": "baaaaah", + "ref": "RF12 3456", + "partner_id": cls.partner_3.id, + "amount": 600, + "sequence": 2, + }, + { + "journal_id": cls.cash_journal.id, + "date": "2020-01-01", + "payment_ref": "yyyyy", + "partner_id": cls.partner_2.id, + "amount": -1000, + "sequence": 1, + }, + ] + ) + + @classmethod + def _create_invoice_line( + cls, + amount, + partner, + move_type, + currency=None, + pay_reference=None, + ref=None, + name=None, + inv_date="2019-09-01", + ): + """Create an invoice on the fly.""" + invoice_form = Form( + cls.env["account.move"].with_context( + default_move_type=move_type, + default_invoice_date=inv_date, + default_date=inv_date, + ) + ) + invoice_form.partner_id = partner + if currency: + invoice_form.currency_id = currency + if pay_reference: + invoice_form.payment_reference = pay_reference + if ref: + invoice_form.ref = ref + if name: + invoice_form.name = name + with invoice_form.invoice_line_ids.new() as invoice_line_form: + invoice_line_form.name = "xxxx" + invoice_line_form.quantity = 1 + invoice_line_form.price_unit = amount + invoice_line_form.tax_ids.clear() + invoice = invoice_form.save() + invoice.action_post() + lines = invoice.line_ids + return lines.filtered( + lambda line: line.account_id.account_type + in ("asset_receivable", "liability_payable") + ) + + @classmethod + def _create_st_line( + cls, amount=1000.0, date="2019-01-01", payment_ref="turlututu", **kwargs + ): + st_line = cls.env["account.bank.statement.line"].create( + { + "journal_id": kwargs.get("journal_id", cls.bank_journal.id), + "amount": amount, + "date": date, + "payment_ref": payment_ref, + "partner_id": cls.partner_a.id, + **kwargs, + } + ) + return st_line + + @classmethod + def _create_reconcile_model(cls, **kwargs): + return cls.env["account.reconcile.model"].create( + { + "name": "test", + "rule_type": "invoice_matching", + "allow_payment_tolerance": True, + "payment_tolerance_type": "percentage", + "payment_tolerance_param": 0.0, + **kwargs, + "line_ids": [ + Command.create( + { + "account_id": cls.company_data[ + "default_account_revenue" + ].id, + "amount_type": "percentage", + "label": f"test {i}", + **line_vals, + } + ) + for i, line_vals in enumerate(kwargs.get("line_ids", [])) + ], + "partner_mapping_line_ids": [ + Command.create(line_vals) + for i, line_vals in enumerate( + kwargs.get("partner_mapping_line_ids", []) + ) + ], + } + ) + + @freeze_time("2020-01-01") + def _check_statement_matching(self, rules, expected_values_list): + for statement_line, expected_values in expected_values_list.items(): + res = rules._apply_rules(statement_line, statement_line._retrieve_partner()) + self.assertDictEqual(res, expected_values) + + def test_matching_fields(self): + # Check without restriction. + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {"amls": self.invoice_line_1, "model": self.rule_1}, + self.bank_line_2: { + "amls": self.invoice_line_1 + + self.invoice_line_2 + + self.invoice_line_3, + "model": self.rule_1, + }, + self.cash_line_1: {"amls": self.invoice_line_4, "model": self.rule_1}, + }, + ) + + @freeze_time("2020-01-01") + def test_matching_fields_match_text_location(self): + 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 (reinitializing the statement line first) + self.bank_line_1.write( + {**st_line_initial_vals, st_line_field: self.partner_1.name} + ) + + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: { + "amls": self.invoice_line_1, + "model": self.rule_1, + }, + }, + ) + + def test_matching_fields_match_journal_ids(self): + self.rule_1.match_journal_ids |= self.cash_line_1.journal_id + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {}, + self.bank_line_2: {}, + self.cash_line_1: {"amls": self.invoice_line_4, "model": self.rule_1}, + }, + ) + + def test_matching_fields_match_nature(self): + self.rule_1.match_nature = "amount_received" + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {"amls": self.invoice_line_1, "model": self.rule_1}, + self.bank_line_2: { + "amls": self.invoice_line_2 + + self.invoice_line_3 + + self.invoice_line_1, + "model": self.rule_1, + }, + self.cash_line_1: {}, + }, + ) + self.rule_1.match_nature = "amount_paid" + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {}, + self.bank_line_2: {}, + self.cash_line_1: {"amls": self.invoice_line_4, "model": self.rule_1}, + }, + ) + + def test_matching_fields_match_amount(self): + self.rule_1.match_amount = "lower" + self.rule_1.match_amount_max = 150 + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {"amls": self.invoice_line_1, "model": self.rule_1}, + self.bank_line_2: {}, + self.cash_line_1: {}, + }, + ) + self.rule_1.match_amount = "greater" + self.rule_1.match_amount_min = 200 + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {}, + self.bank_line_2: { + "amls": self.invoice_line_1 + + self.invoice_line_2 + + self.invoice_line_3, + "model": self.rule_1, + }, + self.cash_line_1: {"amls": self.invoice_line_4, "model": self.rule_1}, + }, + ) + self.rule_1.match_amount = "between" + self.rule_1.match_amount_min = 200 + self.rule_1.match_amount_max = 800 + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {}, + self.bank_line_2: { + "amls": self.invoice_line_1 + + self.invoice_line_2 + + self.invoice_line_3, + "model": self.rule_1, + }, + self.cash_line_1: {}, + }, + ) + + def test_matching_fields_match_label(self): + self.rule_1.match_label = "contains" + self.rule_1.match_label_param = "yyyyy" + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {}, + self.bank_line_2: {}, + self.cash_line_1: {"amls": self.invoice_line_4, "model": self.rule_1}, + }, + ) + self.rule_1.match_label = "not_contains" + self.rule_1.match_label_param = "xxxxx" + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {"amls": self.invoice_line_1, "model": self.rule_1}, + self.bank_line_2: {}, + self.cash_line_1: {"amls": self.invoice_line_4, "model": self.rule_1}, + }, + ) + self.rule_1.match_label = "match_regex" + self.rule_1.match_label_param = "xxxxx|yyyyy" + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {}, + self.bank_line_2: { + "amls": self.invoice_line_1 + + self.invoice_line_2 + + self.invoice_line_3, + "model": self.rule_1, + }, + self.cash_line_1: {"amls": self.invoice_line_4, "model": self.rule_1}, + }, + ) + + @freeze_time("2019-01-01") + def test_zero_payment_tolerance(self): + rule = self._create_reconcile_model(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" + ) + + # Exact matching. + st_line = self._create_st_line(amount=bsl_sign * 1000.0) + 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) + 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) + self._check_statement_matching( + rule, + {st_line: {"amls": invl, "model": rule}}, + ) + + @freeze_time("2019-01-01") + def test_zero_payment_tolerance_auto_reconcile(self): + rule = self._create_reconcile_model( + auto_reconcile=True, + 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, + pay_reference="123456", + inv_date="2019-01-01", + ) + + # No matching because there is no tolerance. + st_line = self._create_st_line(amount=bsl_sign * 990.0) + self._check_statement_matching( + rule, + {st_line: {}}, + ) + + # The payment amount is higher than the invoice one. + st_line = self._create_st_line( + amount=bsl_sign * 1010.0, payment_ref="123456" + ) + self._check_statement_matching( + rule, + {st_line: {"amls": invl, "model": rule, "auto_reconcile": True}}, + ) + + @freeze_time("2019-01-01") + def test_not_enough_payment_tolerance(self): + rule = self._create_reconcile_model( + payment_tolerance_param=0.5, + line_ids=[{}], + ) + + for inv_type, bsl_sign in (("out_invoice", 1), ("in_invoice", -1)): + with self.subTest(inv_type=inv_type, bsl_sign=bsl_sign): + invl = self._create_invoice_line( + 1000.0, self.partner_a, inv_type, inv_date="2019-01-01" + ) + + # No matching because there is no enough tolerance. + st_line = self._create_st_line(amount=bsl_sign * 990.0) + self._check_statement_matching( + rule, + {st_line: {}}, + ) + + # The payment amount is higher than the invoice one. + # However, since the invoice amount is lower than the payment amount, + # the tolerance is not checked and the invoice line is matched. + st_line = self._create_st_line(amount=bsl_sign * 1010.0) + self._check_statement_matching( + rule, + {st_line: {"amls": invl, "model": rule}}, + ) + + @freeze_time("2019-01-01") + def test_enough_payment_tolerance(self): + rule = self._create_reconcile_model( + payment_tolerance_param=1.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" + ) + + # Enough tolerance to match the invoice line. + st_line = self._create_st_line(amount=bsl_sign * 990.0) + self._check_statement_matching( + rule, + {st_line: {"amls": invl, "model": rule, "status": "write_off"}}, + ) + + # The payment amount is higher than the invoice one. + # However, since the invoice amount is lower than the payment amount, + # the tolerance is not checked and the invoice line is matched. + st_line = self._create_st_line(amount=bsl_sign * 1010.0) + self._check_statement_matching( + rule, + {st_line: {"amls": invl, "model": rule}}, + ) + + @freeze_time("2019-01-01") + def test_enough_payment_tolerance_auto_reconcile_not_full(self): + rule = self._create_reconcile_model( + payment_tolerance_param=1.0, + auto_reconcile=True, + line_ids=[{"amount_type": "percentage_st_line", "amount_string": "200.0"}], + ) + + for inv_type, bsl_sign in (("out_invoice", 1), ("in_invoice", -1)): + invl = self._create_invoice_line( + 1000.0, + self.partner_a, + inv_type, + pay_reference="123456", + inv_date="2019-01-01", + ) + + # Enough tolerance to match the invoice line. + st_line = self._create_st_line( + amount=bsl_sign * 990.0, payment_ref="123456" + ) + self._check_statement_matching( + rule, + { + st_line: { + "amls": invl, + "model": rule, + "status": "write_off", + "auto_reconcile": True, + } + }, + ) + + @freeze_time("2019-01-01") + def test_allow_payment_tolerance_lower_amount(self): + rule = self._create_reconcile_model( + line_ids=[{"amount_type": "percentage_st_line"}] + ) + + for inv_type, bsl_sign in (("out_invoice", 1), ("in_invoice", -1)): + 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) + + # Partial reconciliation. + self._check_statement_matching( + rule, + {st_line: {"amls": invl, "model": rule}}, + ) + + @freeze_time("2019-01-01") + def test_enough_payment_tolerance_auto_reconcile(self): + rule = self._create_reconcile_model( + payment_tolerance_param=1.0, + auto_reconcile=True, + 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, + pay_reference="123456", + inv_date="2019-01-01", + ) + + # Enough tolerance to match the invoice line. + st_line = self._create_st_line( + amount=bsl_sign * 990.0, payment_ref="123456" + ) + self._check_statement_matching( + rule, + { + st_line: { + "amls": invl, + "model": rule, + "status": "write_off", + "auto_reconcile": True, + } + }, + ) + + @freeze_time("2019-01-01") + def test_percentage_st_line_auto_reconcile(self): + rule = self._create_reconcile_model( + payment_tolerance_param=1.0, + rule_type="writeoff_suggestion", + auto_reconcile=True, + line_ids=[ + { + "amount_type": "percentage_st_line", + "amount_string": "100.0", + "label": "A", + }, + { + "amount_type": "percentage_st_line", + "amount_string": "-100.0", + "label": "B", + }, + { + "amount_type": "percentage_st_line", + "amount_string": "100.0", + "label": "C", + }, + ], + ) + + for bsl_sign in (1, -1): + st_line = self._create_st_line(amount=bsl_sign * 1000.0) + self._check_statement_matching( + rule, + { + st_line: { + "model": rule, + "status": "write_off", + "auto_reconcile": True, + } + }, + ) + + def test_matching_fields_match_partner_category_ids(self): + test_category = self.env["res.partner.category"].create( + {"name": "Consulting Services"} + ) + self.partner_2.category_id = test_category + self.rule_1.match_partner_category_ids |= test_category + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {}, + self.bank_line_2: {}, + self.cash_line_1: {"amls": self.invoice_line_4, "model": self.rule_1}, + }, + ) + self.rule_1.match_partner_category_ids = False + + def test_mixin_rules(self): + """Test usage of rules together.""" + # rule_1 is used before rule_2. + self.rule_1.sequence = 1 + self.rule_2.sequence = 2 + + self._check_statement_matching( + self.rule_1 + self.rule_2, + { + self.bank_line_1: { + "amls": self.invoice_line_1, + "model": self.rule_1, + }, + self.bank_line_2: { + "amls": self.invoice_line_2 + + self.invoice_line_3 + + self.invoice_line_1, + "model": self.rule_1, + }, + self.cash_line_1: {"amls": self.invoice_line_4, "model": self.rule_1}, + }, + ) + + # rule_2 is used before rule_1. + self.rule_1.sequence = 2 + self.rule_2.sequence = 1 + + self._check_statement_matching( + self.rule_1 + self.rule_2, + { + self.bank_line_1: { + "model": self.rule_2, + "auto_reconcile": False, + "status": "write_off", + }, + self.bank_line_2: { + "model": self.rule_2, + "auto_reconcile": False, + "status": "write_off", + }, + self.cash_line_1: { + "model": self.rule_2, + "auto_reconcile": False, + "status": "write_off", + }, + }, + ) + + # rule_2 is used before rule_1 but only on partner_1. + self.rule_2.match_partner_ids |= self.partner_1 + + self._check_statement_matching( + self.rule_1 + self.rule_2, + { + self.bank_line_1: { + "model": self.rule_2, + "auto_reconcile": False, + "status": "write_off", + }, + self.bank_line_2: { + "model": self.rule_2, + "auto_reconcile": False, + "status": "write_off", + }, + self.cash_line_1: {"amls": self.invoice_line_4, "model": self.rule_1}, + }, + ) + + def test_auto_reconcile(self): + """Test auto reconciliation.""" + self.bank_line_1.amount += 5 + + self.rule_1.sequence = 2 + self.rule_1.auto_reconcile = True + self.rule_1.payment_tolerance_param = 10.0 + self.rule_2.sequence = 1 + self.rule_2.match_partner_ids |= self.partner_2 + self.rule_2.auto_reconcile = True + + self._check_statement_matching( + self.rule_1 + self.rule_2, + { + self.bank_line_1: { + "amls": self.invoice_line_1, + "model": self.rule_1, + "auto_reconcile": True, + }, + self.bank_line_2: { + "amls": self.invoice_line_1 + + self.invoice_line_2 + + self.invoice_line_3, + "model": self.rule_1, + }, + self.cash_line_1: { + "model": self.rule_2, + "status": "write_off", + "auto_reconcile": True, + }, + }, + ) + + def test_larger_invoice_auto_reconcile(self): + """Test auto reconciliation with an invoice with larger amount than the + statement line's, for rules without write-offs.""" + self.bank_line_1.amount = 40 + self.invoice_line_1.move_id.payment_reference = self.bank_line_1.payment_ref + + self.rule_1.sequence = 2 + self.rule_1.allow_payment_tolerance = False + self.rule_1.auto_reconcile = True + self.rule_1.line_ids = [(5, 0, 0)] + + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: { + "amls": self.invoice_line_1, + "model": self.rule_1, + "auto_reconcile": True, + }, + self.bank_line_2: { + "amls": self.invoice_line_1 + + self.invoice_line_2 + + self.invoice_line_3, + "model": self.rule_1, + }, + }, + ) + + def test_auto_reconcile_with_tax(self): + """Test auto reconciliation with a tax amount included in the bank statement line""" + self.rule_1.write( + { + "auto_reconcile": True, + "rule_type": "writeoff_suggestion", + "line_ids": [ + ( + 1, + self.rule_1.line_ids.id, + { + "amount": 50, + "force_tax_included": True, + "tax_ids": [(6, 0, self.tax21.ids)], + }, + ), + ( + 0, + 0, + { + "amount": 100, + "force_tax_included": False, + "tax_ids": [(6, 0, self.tax12.ids)], + "account_id": self.current_assets_account.id, + }, + ), + ], + } + ) + + self.bank_line_1.amount = -121 + + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: { + "model": self.rule_1, + "status": "write_off", + "auto_reconcile": True, + }, + self.bank_line_2: { + "model": self.rule_1, + "status": "write_off", + "auto_reconcile": True, + }, + }, + ) + + def test_auto_reconcile_with_tax_fpos(self): + """Test the fiscal positions are applied by reconcile models when using taxes.""" + self.rule_1.write( + { + "auto_reconcile": True, + "rule_type": "writeoff_suggestion", + "line_ids": [ + ( + 1, + self.rule_1.line_ids.id, + { + "amount": 100, + "force_tax_included": True, + "tax_ids": [(6, 0, self.tax21.ids)], + }, + ) + ], + } + ) + + self.partner_1.country_id = self.env.ref("base.lu") + belgium = self.env.ref("base.be") + self.partner_2.country_id = belgium + + self.bank_line_2.partner_id = self.partner_2 + + self.bank_line_1.amount = -121 + self.bank_line_2.amount = -112 + + self.env["account.fiscal.position"].create( + { + "name": "Test", + "country_id": belgium.id, + "auto_apply": True, + "tax_ids": [ + Command.create( + { + "tax_src_id": self.tax21.id, + "tax_dest_id": self.tax12.id, + } + ), + ], + } + ) + + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: { + "model": self.rule_1, + "status": "write_off", + "auto_reconcile": True, + }, + self.bank_line_2: { + "model": self.rule_1, + "status": "write_off", + "auto_reconcile": True, + }, + }, + ) + + def test_reverted_move_matching(self): + partner = self.partner_1 + AccountMove = self.env["account.move"] + move = AccountMove.create( + { + "journal_id": self.bank_journal.id, + "line_ids": [ + ( + 0, + 0, + { + "account_id": self.account_pay.id, + "partner_id": partner.id, + "name": "One of these days", + "debit": 10, + }, + ), + ( + 0, + 0, + { + "account_id": self.bank_journal.company_id.account_journal_payment_credit_account_id.id, + "partner_id": partner.id, + "name": "I'm gonna cut you into little pieces", + "credit": 10, + }, + ), + ], + } + ) + + payment_bnk_line = move.line_ids.filtered( + lambda line: line.account_id + == self.bank_journal.company_id.account_journal_payment_credit_account_id + ) + + move.action_post() + move_reversed = move._reverse_moves() + self.assertTrue(move_reversed.exists()) + + self.bank_line_1.write( + { + "payment_ref": "8", + "partner_id": partner.id, + "amount": -10, + } + ) + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {"amls": payment_bnk_line, "model": self.rule_1}, + self.bank_line_2: { + "amls": self.invoice_line_1 + + self.invoice_line_2 + + self.invoice_line_3, + "model": self.rule_1, + }, + }, + ) + + def test_match_different_currencies(self): + partner = self.env["res.partner"].create({"name": "Bernard Gagnant"}) + self.rule_1.write( + {"match_partner_ids": [(6, 0, partner.ids)], "match_same_currency": False} + ) + + currency_inv = self.env.ref("base.EUR") + currency_inv.active = True + currency_statement = self.env.ref("base.JPY") + + currency_statement.active = True + + invoice_line = self._create_invoice_line( + 100, partner, "out_invoice", currency=currency_inv + ) + + self.bank_line_1.write( + { + "partner_id": partner.id, + "foreign_currency_id": currency_statement.id, + "amount_currency": 100, + "payment_ref": "test", + } + ) + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {"amls": invoice_line, "model": self.rule_1}, + self.bank_line_2: {}, + }, + ) + + def test_invoice_matching_rule_no_partner(self): + """Tests that a statement line without any partner can be matched to the + right invoice if they have the same payment reference. + """ + self.invoice_line_1.move_id.write({"payment_reference": "Tournicoti66"}) + self.rule_1.allow_payment_tolerance = False + + self.bank_line_1.write( + { + "payment_ref": "Tournicoti66", + "partner_id": None, + "amount": 95, + } + ) + + self.rule_1.write( + { + "line_ids": [(5, 0, 0)], + "match_partner": False, + "match_label": "contains", + "match_label_param": "Tournicoti", # So that we only match what we want to test + } + ) + + # TODO: 'invoice_line_1' has no reason to match 'bank_line_1' here... to check + # self._check_statement_matching(self.rule_1, { + # self.bank_line_1: {'amls': self.invoice_line_1, 'model': self.rule_1}, + # self.bank_line_2: {'amls': []}, + # }, self.bank_st) + + def test_inv_matching_rule_auto_rec_no_partner_with_writeoff(self): + self.invoice_line_1.move_id.ref = "doudlidou3555" + + self.bank_line_1.write( + { + "payment_ref": "doudlidou3555", + "partner_id": None, + "amount": 95, + } + ) + + self.rule_1.write( + { + "match_partner": False, + "match_label": "contains", + "match_label_param": "doudlidou", # So that we only match what we want to test + "payment_tolerance_param": 10.0, + "auto_reconcile": True, + } + ) + + # Check bank reconciliation + + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: { + "amls": self.invoice_line_1, + "model": self.rule_1, + "status": "write_off", + "auto_reconcile": True, + }, + self.bank_line_2: {}, + }, + ) + + def test_partner_mapping_rule(self): + st_line = self._create_st_line(partner_id=None, payment_ref="toto42") + + rule = self._create_reconcile_model( + partner_mapping_line_ids=[ + { + "partner_id": self.partner_1.id, + "payment_ref_regex": "toto.*", + } + ], + ) + + # Matching using the regex on payment_ref. + self.assertEqual(st_line._retrieve_partner(), self.partner_1) + + rule.partner_mapping_line_ids.narration_regex = ".*coincoin" + + # No match because the narration is not matching the regex. + self.assertEqual(st_line._retrieve_partner(), self.env["res.partner"]) + + st_line.narration = "42coincoin" + + # 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 communication 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 GOL = 900 DAR and we want to match two journal + items of: + - 100 USD = 200 GOL (= 600 DAR from the statement line point of view) + - 14 USD = 280 DAR + Both journal items should be suggested to the user because they represents 98% of the statement line amount + (DAR). + """ + partner = self.env["res.partner"].create({"name": "Bernard Perdant"}) + + journal = self.env["account.journal"].create( + { + "name": "test_match_multi_currencies", + "code": "xxxx", + "type": "bank", + "currency_id": self.currency_data["currency"].id, + } + ) + + matching_rule = self.env["account.reconcile.model"].create( + { + "name": "test_match_multi_currencies", + "rule_type": "invoice_matching", + "match_partner": True, + "match_partner_ids": [(6, 0, partner.ids)], + "allow_payment_tolerance": True, + "payment_tolerance_type": "percentage", + "payment_tolerance_param": 5.0, + "match_same_currency": False, + "company_id": self.company_data["company"].id, + "past_months_limit": False, + } + ) + + statement_line = self.env[ + "account.bank.statement.line" + ].create( + { + "journal_id": journal.id, + "date": "2016-01-01", + "payment_ref": "line", + "partner_id": partner.id, + "foreign_currency_id": self.currency_data_2["currency"].id, + "amount": 300.0, # Rate is 3 GOL = 1 USD in 2016. + "amount_currency": 900.0, # Rate is 10 DAR = 1 USD in 2016 but the rate used by the bank is 9:1. + } + ) + + move = self.env["account.move"].create( + { + "move_type": "entry", + "date": "2017-01-01", + "journal_id": self.company_data["default_journal_misc"].id, + "line_ids": [ + # Rate is 2 GOL = 1 USD in 2017. + # The statement line will consider this line equivalent to 600 DAR. + ( + 0, + 0, + { + "account_id": self.company_data[ + "default_account_receivable" + ].id, + "partner_id": partner.id, + "currency_id": self.currency_data["currency"].id, + "debit": 100.0, + "credit": 0.0, + "amount_currency": 200.0, + }, + ), + # Rate is 20 GOL = 1 USD in 2017. + ( + 0, + 0, + { + "account_id": self.company_data[ + "default_account_receivable" + ].id, + "partner_id": partner.id, + "currency_id": self.currency_data_2["currency"].id, + "debit": 14.0, + "credit": 0.0, + "amount_currency": 280.0, + }, + ), + # Line to balance the journal entry: + ( + 0, + 0, + { + "account_id": self.company_data[ + "default_account_revenue" + ].id, + "debit": 0.0, + "credit": 114.0, + }, + ), + ], + } + ) + move.action_post() + + move_line_1 = move.line_ids.filtered(lambda line: line.debit == 100.0) + move_line_2 = move.line_ids.filtered(lambda line: line.debit == 14.0) + + self._check_statement_matching( + matching_rule, + { + statement_line: { + "amls": move_line_1 + move_line_2, + "model": matching_rule, + } + }, + ) + + @freeze_time("2020-01-01") + def test_matching_with_write_off_foreign_currency(self): + journal_foreign_curr = self.company_data["default_journal_bank"].copy() + journal_foreign_curr.currency_id = self.currency_data["currency"] + + reco_model = self._create_reconcile_model( + auto_reconcile=True, + rule_type="writeoff_suggestion", + line_ids=[ + { + "amount_type": "percentage", + "amount": 100.0, + "account_id": self.company_data["default_account_revenue"].id, + } + ], + ) + + st_line = self._create_st_line( + amount=100.0, payment_ref="123456", journal_id=journal_foreign_curr.id + ) + self._check_statement_matching( + reco_model, + { + st_line: { + "model": reco_model, + "status": "write_off", + "auto_reconcile": True, + }, + }, + ) + + def test_payment_similar_communications(self): + def create_payment_line(amount, memo, partner): + payment = self.env["account.payment"].create( + { + "amount": amount, + "payment_type": "inbound", + "partner_type": "customer", + "partner_id": partner.id, + "ref": memo, + "destination_account_id": self.company_data[ + "default_account_receivable" + ].id, + } + ) + payment.action_post() + + return payment.line_ids.filtered( + lambda x: x.account_id.account_type + not in {"asset_receivable", "liability_payable"} + ) + + payment_partner = self.env["res.partner"].create( + { + "name": "Bernard Gagnant", + } + ) + + self.rule_1.match_partner_ids = [(6, 0, payment_partner.ids)] + + pmt_line_1 = create_payment_line(500, "a1b2c3", payment_partner) + pmt_line_2 = create_payment_line(500, "a1b2c3", payment_partner) + create_payment_line(500, "d1e2f3", payment_partner) + + self.bank_line_1.write( + { + "amount": 1000, + "payment_ref": "a1b2c3", + "partner_id": payment_partner.id, + } + ) + self.bank_line_2.unlink() + self.rule_1.allow_payment_tolerance = False + + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: { + "amls": pmt_line_1 + pmt_line_2, + "model": self.rule_1, + "status": "write_off", + }, + }, + ) + + def test_no_amount_check_keep_first(self): + """In case the reconciliation model doesn't check the total amount of the candidates, + we still don't want to suggest more than are necessary to match the statement. + For example, if a statement line amounts to 250 and is to be matched with three invoices + of 100, 200 and 300 (retrieved in this order), only 100 and 200 should be proposed. + """ + self.rule_1.allow_payment_tolerance = False + self.bank_line_2.amount = 250 + self.bank_line_1.partner_id = None + + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {}, + self.bank_line_2: { + "amls": self.invoice_line_1 + self.invoice_line_2, + "model": self.rule_1, + "status": "write_off", + }, + }, + ) + + def test_no_amount_check_exact_match(self): + """If a reconciliation model finds enough candidates for a full reconciliation, + it should still check the following candidates, in case one of them exactly + matches the amount of the statement line. If such a candidate exist, all the + other ones are disregarded. + """ + self.rule_1.allow_payment_tolerance = False + self.bank_line_2.amount = 300 + self.bank_line_1.partner_id = None + + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1: {}, + self.bank_line_2: { + "amls": self.invoice_line_3, + "model": self.rule_1, + "status": "write_off", + }, + }, + ) From b1384a3b7f7da10ab44200c46679fb840720cfcb Mon Sep 17 00:00:00 2001 From: oca-ci Date: Fri, 26 Apr 2024 08:23:19 +0000 Subject: [PATCH 02/13] [UPD] Update account_reconcile_model_oca.pot --- .../i18n/account_reconcile_model_oca.pot | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 account_reconcile_model_oca/i18n/account_reconcile_model_oca.pot diff --git a/account_reconcile_model_oca/i18n/account_reconcile_model_oca.pot b/account_reconcile_model_oca/i18n/account_reconcile_model_oca.pot new file mode 100644 index 0000000000..a75b7011f3 --- /dev/null +++ b/account_reconcile_model_oca/i18n/account_reconcile_model_oca.pot @@ -0,0 +1,25 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_reconcile_model_oca +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: account_reconcile_model_oca +#: model:ir.model,name:account_reconcile_model_oca.model_account_bank_statement_line +msgid "Bank Statement Line" +msgstr "" + +#. module: account_reconcile_model_oca +#: model:ir.model,name:account_reconcile_model_oca.model_account_reconcile_model +msgid "" +"Preset to create journal entries during a invoices and payments matching" +msgstr "" From 0f4ddf5a5b16fa657d4776eed9113a28b8935a73 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 26 Apr 2024 08:26:12 +0000 Subject: [PATCH 03/13] [BOT] post-merge updates --- account_reconcile_model_oca/README.rst | 2 +- account_reconcile_model_oca/static/description/index.html | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/account_reconcile_model_oca/README.rst b/account_reconcile_model_oca/README.rst index 5da1edf5d6..cbfd833b6d 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:62683a913039d5afe96202b49fdd537d57edf8ac49edc89bf9c9e4512971888f + !! source digest: sha256:4ee52a99a09664c961d76a060be72ca1c3eb7dbe3129b539196f9e7b901a0825 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/account_reconcile_model_oca/static/description/index.html b/account_reconcile_model_oca/static/description/index.html index b0ab3f4085..5d9c8a64d6 100644 --- a/account_reconcile_model_oca/static/description/index.html +++ b/account_reconcile_model_oca/static/description/index.html @@ -1,4 +1,3 @@ - @@ -367,7 +366,7 @@

Account Reconcile Model Oca

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:62683a913039d5afe96202b49fdd537d57edf8ac49edc89bf9c9e4512971888f +!! source digest: sha256:4ee52a99a09664c961d76a060be72ca1c3eb7dbe3129b539196f9e7b901a0825 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

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

This module restores account reconciliation models functions moved from From 4f954dc1f7ccbe33fc850cbaedf47f95fecf6962 Mon Sep 17 00:00:00 2001 From: mymage Date: Mon, 29 Apr 2024 06:15:36 +0000 Subject: [PATCH 04/13] Added translation using Weblate (Italian) --- account_reconcile_model_oca/i18n/it.po | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 account_reconcile_model_oca/i18n/it.po diff --git a/account_reconcile_model_oca/i18n/it.po b/account_reconcile_model_oca/i18n/it.po new file mode 100644 index 0000000000..4de38dfc5d --- /dev/null +++ b/account_reconcile_model_oca/i18n/it.po @@ -0,0 +1,26 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_reconcile_model_oca +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: it\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" + +#. module: account_reconcile_model_oca +#: model:ir.model,name:account_reconcile_model_oca.model_account_bank_statement_line +msgid "Bank Statement Line" +msgstr "" + +#. module: account_reconcile_model_oca +#: model:ir.model,name:account_reconcile_model_oca.model_account_reconcile_model +msgid "" +"Preset to create journal entries during a invoices and payments matching" +msgstr "" From 96d7d66deb2d7d874f9638afb7a2157d88b0cbf5 Mon Sep 17 00:00:00 2001 From: mymage Date: Mon, 29 Apr 2024 06:19:08 +0000 Subject: [PATCH 05/13] Translated using Weblate (Italian) Currently translated at 100.0% (2 of 2 strings) Translation: account-reconcile-17.0/account-reconcile-17.0-account_reconcile_model_oca Translate-URL: https://translation.odoo-community.org/projects/account-reconcile-17-0/account-reconcile-17-0-account_reconcile_model_oca/it/ --- account_reconcile_model_oca/i18n/it.po | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/account_reconcile_model_oca/i18n/it.po b/account_reconcile_model_oca/i18n/it.po index 4de38dfc5d..b4901c9a1d 100644 --- a/account_reconcile_model_oca/i18n/it.po +++ b/account_reconcile_model_oca/i18n/it.po @@ -6,21 +6,25 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 17.0\n" "Report-Msgid-Bugs-To: \n" -"Last-Translator: Automatically generated\n" +"PO-Revision-Date: 2024-04-29 08:38+0000\n" +"Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\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" #. module: account_reconcile_model_oca #: model:ir.model,name:account_reconcile_model_oca.model_account_bank_statement_line msgid "Bank Statement Line" -msgstr "" +msgstr "Riga estratto conto" #. module: account_reconcile_model_oca #: model:ir.model,name:account_reconcile_model_oca.model_account_reconcile_model msgid "" "Preset to create journal entries during a invoices and payments matching" msgstr "" +"Preimpostazione per creare registrazioni contabili durante la corrispondenza " +"tra fatture e pagamenti" From 44764223fd084ceb9c56b98c8bc3bffe9e5ef053 Mon Sep 17 00:00:00 2001 From: "Pedro M. Baeza" Date: Wed, 1 May 2024 19:31:30 +0200 Subject: [PATCH 06/13] [FIX] *: pre-commit fixes Due to latest copier template with all the checks. --- .../models/account_bank_statement_line.py | 9 +- .../models/account_reconcile_model.py | 115 +++++++++++------- account_reconcile_model_oca/tests/common.py | 4 +- .../tests/test_reconciliation_match.py | 41 ++++--- 4 files changed, 98 insertions(+), 71 deletions(-) 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 234f76bfe2..652a3aa690 100644 --- a/account_reconcile_model_oca/models/account_bank_statement_line.py +++ b/account_reconcile_model_oca/models/account_bank_statement_line.py @@ -63,12 +63,14 @@ def _retrieve_partner(self): continue # Find a partner having a name contained inside the statement line values. - # Take care a partner could contain some special characters in its name that needs to be escaped. + # Take care a partner could contain some special characters in its name that + # needs to be escaped. sub_queries.append( rf""" {unaccent("%s")} ~* ('^' || ( SELECT STRING_AGG(CONCAT('(?=.*\m', chunk[1], '\M)'), '') - FROM regexp_matches({unaccent('partner.name')}, '\w{{3,}}', 'g') AS chunk + FROM regexp_matches({unaccent('partner.name')}, '\w{{3,}}', 'g') + AS chunk )) """ ) @@ -100,7 +102,8 @@ def _retrieve_partner(self): return self.env["res.partner"] def _get_st_line_strings_for_matching(self, allowed_fields=None): - """Collect the strings that could be used on the statement line to perform some matching. + """Collect the strings that could be used on the statement line to perform some + matching. :param allowed_fields: A explicit list of fields to consider. :return: A list of strings. """ diff --git a/account_reconcile_model_oca/models/account_reconcile_model.py b/account_reconcile_model_oca/models/account_reconcile_model.py index 2ad17fa411..58e6a3c2a3 100644 --- a/account_reconcile_model_oca/models/account_reconcile_model.py +++ b/account_reconcile_model_oca/models/account_reconcile_model.py @@ -14,13 +14,14 @@ class AccountReconcileModel(models.Model): #################################################### def _apply_lines_for_bank_widget(self, residual_amount_currency, partner, st_line): - """Apply the reconciliation model lines to the statement line passed as parameter. - :param residual_amount_currency: The open balance of the statement line in the bank reconciliation widget - expressed in the statement line currency. - :param partner: The partner set on the wizard. - :param st_line: The statement line processed by the bank reconciliation widget. - :return: A list of python dictionaries (one per reconcile model line) representing - the journal items to be created by the current reconcile model. + """Apply the reconciliation model lines to the statement line passed as + parameter. + :param residual_amount_currency: The open balance of the statement line in the + bank reconciliation widget expressed in the statement line currency. + :param partner: The partner set on the wizard. + :param st_line: The statement line processed by the bank reconciliation widget. + :return: A list of python dictionaries (one per reconcile model line) + representing the journal items to be created by the current reconcile model. """ self.ensure_one() currency = ( @@ -48,9 +49,11 @@ def _apply_lines_for_bank_widget(self, residual_amount_currency, partner, st_lin def _get_taxes_move_lines_dict(self, tax, base_line_dict): """Get move.lines dict (to be passed to the create()) corresponding to a tax. - :param tax: An account.tax record. - :param base_line_dict: A dict representing the move.line containing the base amount. - :return: A list of dict representing move.lines to be created corresponding to the tax. + :param tax: An account.tax record. + :param base_line_dict: A dict representing the move.line containing the base + amount. + :return: A list of dict representing move.lines to be created corresponding to + the tax. """ self.ensure_one() balance = base_line_dict["balance"] @@ -103,9 +106,12 @@ def _get_taxes_move_lines_dict(self, tax, base_line_dict): return new_aml_dicts def _get_write_off_move_lines_dict(self, residual_balance, partner_id): - """Get move.lines dict corresponding to the reconciliation model's write-off lines. - :param residual_balance: The residual balance of the account on the manual reconciliation widget. - :return: A list of dict representing move.lines to be created corresponding to the write-off lines. + """Get move.lines dict corresponding to the reconciliation model's write-off + lines. + :param residual_balance: The residual balance of the account on the manual + reconciliation widget. + :return: A list of dict representing move.lines to be created corresponding to + the write-off lines. """ self.ensure_one() @@ -153,8 +159,9 @@ def _get_write_off_move_lines_dict(self, residual_balance, partner_id): if detected_fiscal_position: taxes = detected_fiscal_position.map_tax(taxes) writeoff_line["tax_ids"] += [Command.set(taxes.ids)] - # Multiple taxes with force_tax_included results in wrong computation, so we - # only allow to set the force_tax_included field if we have one tax selected + # Multiple taxes with force_tax_included results in wrong computation, + # so we only allow to set the force_tax_included field if we have one + # tax selected if line.force_tax_included: taxes = taxes[0].with_context(force_price_include=True) tax_vals_list = self._get_taxes_move_lines_dict(taxes, writeoff_line) @@ -172,15 +179,17 @@ def _get_write_off_move_lines_dict(self, residual_balance, partner_id): def _apply_rules(self, st_line, partner): """Apply criteria to get candidates for all reconciliation models. This function is called in enterprise by the reconciliation widget to match - the statement line with the available candidates (using the reconciliation models). + the statement line with the available candidates (using the reconciliation + models). :param st_line: The statement line to match. :param partner: The partner to consider. - :return: A dict mapping each statement line id with: - * aml_ids: A list of account.move.line ids. - * model: An account.reconcile.model record (optional). - * status: 'reconciled' if the lines has been already reconciled, 'write_off' if the write-off - must be applied on the statement line. - * auto_reconcile: A flag indicating if the match is enough significant to auto reconcile the candidates. + :return: A dict mapping each statement line id with: + * aml_ids: A list of account.move.line ids. + * model: An account.reconcile.model record (optional). + * status: 'reconciled' if the lines has been already reconciled, 'write_off' + if the write-off must be applied on the statement line. + * auto_reconcile: A flag indicating if the match is enough significant to + auto reconcile the candidates. """ available_models = self.filtered( lambda m: m.rule_type != "writeoff_button" @@ -318,7 +327,8 @@ def _get_invoice_matching_amls_domain(self, st_line, partner): 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. + 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. """ @@ -347,7 +357,8 @@ def _get_invoice_matching_st_line_tokens(self, st_line): return tokens def _get_invoice_matching_amls_candidates(self, st_line, partner): - """Returns the match candidates for the 'invoice_matching' rule, with respect to the provided parameters. + """Returns the match candidates for the 'invoice_matching' rule, with respect to + the provided parameters. :param st_line: A statement line. :param partner: The partner associated to the statement line. """ @@ -381,14 +392,17 @@ def _get_invoice_matching_amls_candidates(self, st_line, partner): UNNEST( REGEXP_SPLIT_TO_ARRAY( SUBSTRING( - REGEXP_REPLACE({table_alias}.{field}, '[^0-9\s]', '', 'g'), + REGEXP_REPLACE( + {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 + 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 """ ) @@ -432,11 +446,14 @@ def _get_invoice_matching_amls_candidates(self, st_line, partner): } def _get_invoice_matching_rules_map(self): - """Get a mapping that could be overridden in others modules. + """Get a mapping that could be overridden in others + modules. :return: a mapping where: - * priority_order: Defines in which order the rules will be evaluated, the lowest comes first. - This is extremely important since the algorithm stops when a rule returns some candidates. - * rule: Method taking as parameters and returning the candidates journal items found. + * priority_order: Defines in which order the rules will be evaluated, the + lowest comes first. This is extremely important since the algorithm stops + when a rule returns some candidates. + * rule: Method taking as parameters and returning the + candidates journal items found. """ rules_map = defaultdict(list) rules_map[10].append(self._get_invoice_matching_amls_candidates) @@ -563,9 +580,11 @@ def match_batch_amls(amls_values_list): ) > 0 ): - # Here, we still have room for other candidates ; so we add the current one to the list we keep. - # Then, we continue iterating, even if there is no room anymore, just in case one of the following candidates - # is an exact match, which would then be preferred on the current candidates. + # Here, we still have room for other candidates ; so we add the + # current one to the list we keep. Then, we continue iterating, even + # if there is no room anymore, just in case one of the following + # candidates is an exact match, which would then be preferred on the + # current candidates. kepts_amls_values_list.append(aml_values) sum_amount_residual_currency += aml_values[ "amount_residual_currency" @@ -580,7 +599,8 @@ def match_batch_amls(amls_values_list): else: return None, [] - # Try to match a batch with the early payment feature. Only a perfect match is allowed. + # Try to match a batch with the early payment feature. Only a perfect match is + # allowed. match_type, kepts_amls_values_list = match_batch_amls(amls_with_epd_values_list) if match_type != "perfect": kepts_amls_values_list = [] @@ -603,15 +623,18 @@ def match_batch_amls(amls_values_list): def _check_rule_propositions(self, st_line, amls_values_list): """Check restrictions that can't be handled for each move.line separately. Note: Only used by models having a type equals to 'invoice_matching'. - :param st_line: The statement line. - :param amls_values_list: The candidates account.move.line as a list of dict: - * aml: The record. - * amount_residual: The amount residual to consider. - * amount_residual_currency: The amount residual in foreign currency to consider. + :param st_line: The statement line. + :param amls_values_list: The candidates account.move.line as a list of dict: + * aml: The record. + * amount_residual: The amount residual to consider. + * amount_residual_currency: The amount residual in foreign currency to + consider. :return: A string representing what to do with the candidates: - * rejected: Reject candidates. - * allow_write_off: Allow to generate the write-off from the reconcile model lines if specified. - * allow_auto_reconcile: Allow to automatically reconcile entries if 'auto_validate' is enabled. + * rejected: Reject candidates. + * allow_write_off: Allow to generate the write-off from the reconcile model + lines if specified. + * allow_auto_reconcile: Allow to automatically reconcile entries if + 'auto_validate' is enabled. """ self.ensure_one() @@ -637,8 +660,8 @@ def _check_rule_propositions(self, st_line, amls_values_list): if st_line_currency.is_zero(amount_curr_after_rec): return {"allow_auto_reconcile"} - # The payment amount is higher than the sum of invoices. - # In that case, don't check the tolerance and don't try to generate any write-off. + # The payment amount is higher than the sum of invoices. In that case, don't + # check the tolerance and don't try to generate any write-off. if amount_curr_after_rec > 0.0: return {"allow_auto_reconcile"} @@ -646,8 +669,8 @@ def _check_rule_propositions(self, st_line, amls_values_list): if self.payment_tolerance_param == 0: return {"rejected"} - # If the tolerance is expressed as a fixed amount, check the residual payment amount doesn't exceed the - # tolerance. + # If the tolerance is expressed as a fixed amount, check the residual payment + # amount doesn't exceed the tolerance. if ( self.payment_tolerance_type == "fixed_amount" and -amount_curr_after_rec <= self.payment_tolerance_param diff --git a/account_reconcile_model_oca/tests/common.py b/account_reconcile_model_oca/tests/common.py index 7b9a804cb2..7ddac2fbdc 100644 --- a/account_reconcile_model_oca/tests/common.py +++ b/account_reconcile_model_oca/tests/common.py @@ -9,8 +9,8 @@ class TestAccountReconciliationCommon(AccountTestInvoicingCommon): """Tests for reconciliation (account.tax) - Test used to check that when doing a sale or purchase invoice in a different currency, - the result will be balanced. + Test used to check that when doing a sale or purchase invoice in a different + currency, the result will be balanced. """ @classmethod diff --git a/account_reconcile_model_oca/tests/test_reconciliation_match.py b/account_reconcile_model_oca/tests/test_reconciliation_match.py index 26704d15e3..9c99f101f9 100644 --- a/account_reconcile_model_oca/tests/test_reconciliation_match.py +++ b/account_reconcile_model_oca/tests/test_reconciliation_match.py @@ -402,7 +402,7 @@ def test_matching_fields_match_text_location_no_partner(self): }, ) - # Test matching with the partner name (reinitializing the statement line first) + # 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} ) @@ -920,7 +920,7 @@ def test_larger_invoice_auto_reconcile(self): ) def test_auto_reconcile_with_tax(self): - """Test auto reconciliation with a tax amount included in the bank statement line""" + """Test auto reconciliation with a tax amount included in the bank stat. line""" self.rule_1.write( { "auto_reconcile": True, @@ -968,7 +968,7 @@ def test_auto_reconcile_with_tax(self): ) def test_auto_reconcile_with_tax_fpos(self): - """Test the fiscal positions are applied by reconcile models when using taxes.""" + """Test the fiscal positions are applied by reconcile models when using taxes""" self.rule_1.write( { "auto_reconcile": True, @@ -1031,6 +1031,7 @@ def test_auto_reconcile_with_tax_fpos(self): def test_reverted_move_matching(self): partner = self.partner_1 AccountMove = self.env["account.move"] + account = self.bank_journal.company_id.account_journal_payment_credit_account_id move = AccountMove.create( { "journal_id": self.bank_journal.id, @@ -1049,7 +1050,7 @@ def test_reverted_move_matching(self): 0, 0, { - "account_id": self.bank_journal.company_id.account_journal_payment_credit_account_id.id, + "account_id": account.id, "partner_id": partner.id, "name": "I'm gonna cut you into little pieces", "credit": 10, @@ -1140,7 +1141,7 @@ def test_invoice_matching_rule_no_partner(self): "line_ids": [(5, 0, 0)], "match_partner": False, "match_label": "contains", - "match_label_param": "Tournicoti", # So that we only match what we want to test + "match_label_param": "Tournicoti", # match what we want to test } ) @@ -1165,7 +1166,7 @@ def test_inv_matching_rule_auto_rec_no_partner_with_writeoff(self): { "match_partner": False, "match_label": "contains", - "match_label_param": "doudlidou", # So that we only match what we want to test + "match_label_param": "doudlidou", # match what we want to test "payment_tolerance_param": 10.0, "auto_reconcile": True, } @@ -1219,7 +1220,7 @@ def test_partner_name_in_communication(self): self.bank_line_2.write({"partner_id": None}) self.rule_1.write({"match_partner": False}) - # bank_line_1 should match, as its communication contains the invoice's partner name + # bank_line_1 should match, as its communic. contains the invoice's partner name self._check_statement_matching( self.rule_1, { @@ -1246,13 +1247,13 @@ def test_partner_name_with_regexp_chars(self): ) 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 GOL = 900 DAR and we want to match two journal - items of: + """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 + GOL = 900 DAR and we want to match two journal items of: - 100 USD = 200 GOL (= 600 DAR from the statement line point of view) - 14 USD = 280 DAR - Both journal items should be suggested to the user because they represents 98% of the statement line amount - (DAR). + Both journal items should be suggested to the user because they represents 98% + of the statement line amount (DAR). """ partner = self.env["res.partner"].create({"name": "Bernard Perdant"}) @@ -1280,9 +1281,7 @@ def test_match_multi_currencies(self): } ) - statement_line = self.env[ - "account.bank.statement.line" - ].create( + statement_line = self.env["account.bank.statement.line"].create( { "journal_id": journal.id, "date": "2016-01-01", @@ -1290,7 +1289,8 @@ def test_match_multi_currencies(self): "partner_id": partner.id, "foreign_currency_id": self.currency_data_2["currency"].id, "amount": 300.0, # Rate is 3 GOL = 1 USD in 2016. - "amount_currency": 900.0, # Rate is 10 DAR = 1 USD in 2016 but the rate used by the bank is 9:1. + # Rate is 10 DAR = 1 USD in 2016 but the rate used by the bank is 9:1. + "amount_currency": 900.0, } ) @@ -1447,10 +1447,11 @@ def create_payment_line(amount, memo, partner): ) def test_no_amount_check_keep_first(self): - """In case the reconciliation model doesn't check the total amount of the candidates, - we still don't want to suggest more than are necessary to match the statement. - For example, if a statement line amounts to 250 and is to be matched with three invoices - of 100, 200 and 300 (retrieved in this order), only 100 and 200 should be proposed. + """In case the reconciliation model doesn't check the total amount of the + candidates, we still don't want to suggest more than are necessary to match the + statement. For example, if a statement line amounts to 250 and is to be matched + with three invoices of 100, 200 and 300 (retrieved in this order), only 100 and + 200 should be proposed. """ self.rule_1.allow_payment_tolerance = False self.bank_line_2.amount = 250 From 4d26871debb3086b1c260864b705b0429e20321b Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 1 May 2024 18:52:00 +0000 Subject: [PATCH 07/13] [BOT] post-merge updates --- account_reconcile_model_oca/README.rst | 2 +- account_reconcile_model_oca/__manifest__.py | 2 +- account_reconcile_model_oca/static/description/index.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/account_reconcile_model_oca/README.rst b/account_reconcile_model_oca/README.rst index cbfd833b6d..df9483c6c1 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:4ee52a99a09664c961d76a060be72ca1c3eb7dbe3129b539196f9e7b901a0825 + !! source digest: sha256:cead010f67ccc5d4b9700540c9d8d0b4fb20d497a38d36cea80cf6f78563a756 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |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 9dec6a1ab8..306e1ace34 100644 --- a/account_reconcile_model_oca/__manifest__.py +++ b/account_reconcile_model_oca/__manifest__.py @@ -5,7 +5,7 @@ "name": "Account Reconcile Model Oca", "summary": """ This includes the logic moved from Odoo Community to Odoo Enterprise""", - "version": "17.0.1.0.0", + "version": "17.0.1.0.1", "license": "LGPL-3", "author": "Dixmit,Odoo,Odoo Community Association (OCA)", "website": "https://github.com/OCA/account-reconcile", diff --git a/account_reconcile_model_oca/static/description/index.html b/account_reconcile_model_oca/static/description/index.html index 5d9c8a64d6..846d951a6a 100644 --- a/account_reconcile_model_oca/static/description/index.html +++ b/account_reconcile_model_oca/static/description/index.html @@ -366,7 +366,7 @@

Account Reconcile Model Oca

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:4ee52a99a09664c961d76a060be72ca1c3eb7dbe3129b539196f9e7b901a0825 +!! source digest: sha256:cead010f67ccc5d4b9700540c9d8d0b4fb20d497a38d36cea80cf6f78563a756 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

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

This module restores account reconciliation models functions moved from From 755655c96b28aaf37a1249cce9e19afc34ba909c Mon Sep 17 00:00:00 2001 From: xtanuiha Date: Wed, 12 Jun 2024 03:22:30 +0000 Subject: [PATCH 08/13] Added translation using Weblate (Chinese (Simplified) (zh_CN)) --- account_reconcile_model_oca/i18n/zh_CN.po | 26 +++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 account_reconcile_model_oca/i18n/zh_CN.po diff --git a/account_reconcile_model_oca/i18n/zh_CN.po b/account_reconcile_model_oca/i18n/zh_CN.po new file mode 100644 index 0000000000..50df04d327 --- /dev/null +++ b/account_reconcile_model_oca/i18n/zh_CN.po @@ -0,0 +1,26 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_reconcile_model_oca +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. module: account_reconcile_model_oca +#: model:ir.model,name:account_reconcile_model_oca.model_account_bank_statement_line +msgid "Bank Statement Line" +msgstr "" + +#. module: account_reconcile_model_oca +#: model:ir.model,name:account_reconcile_model_oca.model_account_reconcile_model +msgid "" +"Preset to create journal entries during a invoices and payments matching" +msgstr "" From 8dad84e3af7bc33f21bf884a36d7c33fcfc45914 Mon Sep 17 00:00:00 2001 From: xtanuiha Date: Wed, 12 Jun 2024 03:22:53 +0000 Subject: [PATCH 09/13] Translated using Weblate (Chinese (Simplified) (zh_CN)) Currently translated at 100.0% (2 of 2 strings) Translation: account-reconcile-17.0/account-reconcile-17.0-account_reconcile_model_oca Translate-URL: https://translation.odoo-community.org/projects/account-reconcile-17-0/account-reconcile-17-0-account_reconcile_model_oca/zh_CN/ --- account_reconcile_model_oca/i18n/zh_CN.po | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/account_reconcile_model_oca/i18n/zh_CN.po b/account_reconcile_model_oca/i18n/zh_CN.po index 50df04d327..b9db9d8245 100644 --- a/account_reconcile_model_oca/i18n/zh_CN.po +++ b/account_reconcile_model_oca/i18n/zh_CN.po @@ -6,21 +6,23 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 17.0\n" "Report-Msgid-Bugs-To: \n" -"Last-Translator: Automatically generated\n" +"PO-Revision-Date: 2024-06-12 03:25+0000\n" +"Last-Translator: xtanuiha \n" "Language-Team: none\n" "Language: zh_CN\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.17\n" #. module: account_reconcile_model_oca #: model:ir.model,name:account_reconcile_model_oca.model_account_bank_statement_line msgid "Bank Statement Line" -msgstr "" +msgstr "银行对账单明细" #. module: account_reconcile_model_oca #: model:ir.model,name:account_reconcile_model_oca.model_account_reconcile_model msgid "" "Preset to create journal entries during a invoices and payments matching" -msgstr "" +msgstr "在发票和付款匹配期间创建会计分录的预置信息" From 10dcdcbc81afb5b7d76940c665ce1e8acd553220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Duy=20=28=C4=90=E1=BB=97=20Anh=29?= Date: Tue, 12 Nov 2024 10:29:55 +0700 Subject: [PATCH 10/13] [IMP] account_reconcile_model_oca: excludes account_accountant module --- account_reconcile_model_oca/__manifest__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/account_reconcile_model_oca/__manifest__.py b/account_reconcile_model_oca/__manifest__.py index 306e1ace34..1a3478dea7 100644 --- a/account_reconcile_model_oca/__manifest__.py +++ b/account_reconcile_model_oca/__manifest__.py @@ -10,6 +10,7 @@ "author": "Dixmit,Odoo,Odoo Community Association (OCA)", "website": "https://github.com/OCA/account-reconcile", "depends": ["account"], + "excludes": ["account_accountant"], "data": [], "demo": [], } From c10afe5cb8e56b0e8cb045b83b0b6dd50a180e47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Mart=C3=ADnez?= Date: Tue, 28 Jan 2025 09:16:19 +0100 Subject: [PATCH 11/13] [IMP] account_reconcile_model_oca: Create _get_write_off_move_move_line_dict() method to allow extension FWP from 16.0: https://github.com/odoo/odoo/pull/188808 Related to OCA/account-reconcile#761 TT51885 --- .../models/account_reconcile_model.py | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/account_reconcile_model_oca/models/account_reconcile_model.py b/account_reconcile_model_oca/models/account_reconcile_model.py index 58e6a3c2a3..a3ec70e517 100644 --- a/account_reconcile_model_oca/models/account_reconcile_model.py +++ b/account_reconcile_model_oca/models/account_reconcile_model.py @@ -135,18 +135,7 @@ def _get_write_off_move_lines_dict(self, residual_balance, partner_id): if currency.is_zero(balance): continue - writeoff_line = { - "name": line.label, - "balance": balance, - "debit": balance > 0 and balance or 0, - "credit": balance < 0 and -balance or 0, - "account_id": line.account_id.id, - "currency_id": currency.id, - "analytic_distribution": line.analytic_distribution, - "reconcile_model_id": self.id, - "journal_id": line.journal_id.id, - "tax_ids": [], - } + writeoff_line = line._get_write_off_move_line_dict(balance, currency) lines_vals_list.append(writeoff_line) residual_balance -= balance @@ -688,3 +677,22 @@ def _check_rule_propositions(self, st_line, amls_values_list): return {"allow_write_off", "allow_auto_reconcile"} return {"rejected"} + + +class AccountReconcileModelLine(models.Model): + _inherit = "account.reconcile.model.line" + + def _get_write_off_move_line_dict(self, balance, currency): + self.ensure_one() + return { + "name": self.label, + "balance": balance, + "debit": balance > 0 and balance or 0, + "credit": balance < 0 and -balance or 0, + "account_id": self.account_id.id, + "currency_id": currency.id, + "analytic_distribution": self.analytic_distribution, + "reconcile_model_id": self.model_id.id, + "journal_id": self.journal_id.id, + "tax_ids": [], + } From 3cc2fe2765ce68e069931c61137a87533288d393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Duy=20=28=C4=90=E1=BB=97=20Anh=29?= Date: Tue, 5 Nov 2024 14:11:06 +0700 Subject: [PATCH 12/13] [IMP] account_reconcile_model_oca: pre-commit auto fixes --- account_reconcile_model_oca/tests/common.py | 1 - 1 file changed, 1 deletion(-) diff --git a/account_reconcile_model_oca/tests/common.py b/account_reconcile_model_oca/tests/common.py index 7ddac2fbdc..14e968a9d2 100644 --- a/account_reconcile_model_oca/tests/common.py +++ b/account_reconcile_model_oca/tests/common.py @@ -6,7 +6,6 @@ class TestAccountReconciliationCommon(AccountTestInvoicingCommon): - """Tests for reconciliation (account.tax) Test used to check that when doing a sale or purchase invoice in a different From 7d3576c02eb06a94dd8049d1dd4803ef42e680c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Duy=20=28=C4=90=E1=BB=97=20Anh=29?= Date: Tue, 5 Nov 2024 18:03:13 +0700 Subject: [PATCH 13/13] [MIG] account_reconcile_model_oca: Migration to 18.0 --- account_reconcile_model_oca/README.rst | 20 +++++-- account_reconcile_model_oca/__manifest__.py | 2 +- .../models/account_bank_statement_line.py | 59 +++++++++++-------- .../models/account_reconcile_model.py | 17 ++++-- .../readme/CONTRIBUTORS.md | 3 + account_reconcile_model_oca/readme/CREDITS.md | 1 + .../static/description/index.html | 31 +++++++--- account_reconcile_model_oca/tests/common.py | 12 ++-- .../tests/test_reconciliation_match.py | 46 +++++++-------- 9 files changed, 113 insertions(+), 78 deletions(-) create mode 100644 account_reconcile_model_oca/readme/CREDITS.md diff --git a/account_reconcile_model_oca/README.rst b/account_reconcile_model_oca/README.rst index df9483c6c1..f6976d4726 100644 --- a/account_reconcile_model_oca/README.rst +++ b/account_reconcile_model_oca/README.rst @@ -17,13 +17,13 @@ Account Reconcile Model Oca :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html :alt: License: LGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Faccount--reconcile-lightgray.png?logo=github - :target: https://github.com/OCA/account-reconcile/tree/17.0/account_reconcile_model_oca + :target: https://github.com/OCA/account-reconcile/tree/18.0/account_reconcile_model_oca :alt: OCA/account-reconcile .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/account-reconcile-17-0/account-reconcile-17-0-account_reconcile_model_oca + :target: https://translation.odoo-community.org/projects/account-reconcile-18-0/account-reconcile-18-0-account_reconcile_model_oca :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png - :target: https://runboat.odoo-community.org/builds?repo=OCA/account-reconcile&target_branch=17.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/account-reconcile&target_branch=18.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -42,7 +42,7 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -62,6 +62,16 @@ Contributors - Enric Tobella +- Trobz + + - Do Anh Duy + +Other credits +------------- + +The migration of this module from 17.0 to 18.0 was financially supported +by Camptocamp. + Maintainers ----------- @@ -75,6 +85,6 @@ OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use. -This module is part of the `OCA/account-reconcile `_ project on GitHub. +This module is part of the `OCA/account-reconcile `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_reconcile_model_oca/__manifest__.py b/account_reconcile_model_oca/__manifest__.py index 1a3478dea7..7e5f4ac9d5 100644 --- a/account_reconcile_model_oca/__manifest__.py +++ b/account_reconcile_model_oca/__manifest__.py @@ -5,7 +5,7 @@ "name": "Account Reconcile Model Oca", "summary": """ This includes the logic moved from Odoo Community to Odoo Enterprise""", - "version": "17.0.1.0.1", + "version": "18.0.1.0.0", "license": "LGPL-3", "author": "Dixmit,Odoo,Odoo Community Association (OCA)", "website": "https://github.com/OCA/account-reconcile", 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 652a3aa690..b807e3f6ee 100644 --- a/account_reconcile_model_oca/models/account_bank_statement_line.py +++ b/account_reconcile_model_oca/models/account_bank_statement_line.py @@ -2,14 +2,13 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import models -from odoo.osv.expression import get_unaccent_wrapper -from odoo.tools import html2plaintext +from odoo.tools import SQL, html2plaintext from odoo.addons.base.models.res_bank import sanitize_account_number class AccountBankStatementLine(models.Model): - _inherit = ("account.bank.statement.line",) + _inherit = "account.bank.statement.line" def _retrieve_partner(self): self.ensure_one() @@ -55,7 +54,7 @@ def _retrieve_partner(self): # Retrieve the partner from statement line text values. st_line_text_values = self._get_st_line_strings_for_matching() - unaccent = get_unaccent_wrapper(self._cr) + unaccent = self.env.registry.unaccent sub_queries = [] params = [] for text_value in st_line_text_values: @@ -66,35 +65,45 @@ def _retrieve_partner(self): # Take care a partner could contain some special characters in its name that # needs to be escaped. sub_queries.append( - rf""" - {unaccent("%s")} ~* ('^' || ( - SELECT STRING_AGG(CONCAT('(?=.*\m', chunk[1], '\M)'), '') - FROM regexp_matches({unaccent('partner.name')}, '\w{{3,}}', 'g') - AS chunk - )) - """ + SQL( + rf""" + {unaccent("%s")} ~* ('^' || ( + SELECT STRING_AGG(CONCAT('(?=.*\m', chunk[1], '\M)'), '') + FROM regexp_matches({unaccent('partner.name')}, '\w{{3,}}', 'g') + AS chunk + )) + """, + text_value, + ) ) params.append(text_value) if sub_queries: self.env["res.partner"].flush_model(["company_id", "name"]) self.env["account.move.line"].flush_model(["partner_id", "company_id"]) - self._cr.execute( + query = SQL(""" + SELECT aml.partner_id + FROM account_move_line aml + JOIN res_partner partner ON + aml.partner_id = partner.id + AND partner.name IS NOT NULL + AND partner.active + AND ( + """) + query_parts = SQL(") OR (").join(sub_queries) + final_query = SQL( """ - SELECT aml.partner_id - FROM account_move_line aml - JOIN res_partner partner ON - aml.partner_id = partner.id - AND partner.name IS NOT NULL - AND partner.active - AND ((""" - + ") OR (".join(sub_queries) - + """)) - WHERE aml.company_id = %s - LIMIT 1 - """, - params + [self.company_id.id], + %s + %s + ) + WHERE aml.company_id = %s + LIMIT 1 + """, + query, + query_parts, + self.company_id.id, ) + self._cr.execute(final_query) row = self._cr.fetchone() if row: return self.env["res.partner"].browse(row[0]) diff --git a/account_reconcile_model_oca/models/account_reconcile_model.py b/account_reconcile_model_oca/models/account_reconcile_model.py index a3ec70e517..1fe0f39225 100644 --- a/account_reconcile_model_oca/models/account_reconcile_model.py +++ b/account_reconcile_model_oca/models/account_reconcile_model.py @@ -362,16 +362,21 @@ def _get_invoice_matching_amls_candidates(self, st_line, partner): 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() + 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: - sub_queries = [] - for table_alias, field in ( + 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.append( rf""" SELECT @@ -389,7 +394,7 @@ def _get_invoice_matching_amls_candidates(self, st_line, partner): '\s+' ) ) AS token - FROM {tables} + 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 @@ -411,7 +416,7 @@ def _get_invoice_matching_amls_candidates(self, st_line, partner): + order_by + """ """, - (where_params * 3) + [tuple(tokens)], + (query_params * 3) + [tuple(tokens)], ) candidate_ids = [r[0] for r in self._cr.fetchall()] if candidate_ids: diff --git a/account_reconcile_model_oca/readme/CONTRIBUTORS.md b/account_reconcile_model_oca/readme/CONTRIBUTORS.md index becca42792..8baa4dbef5 100644 --- a/account_reconcile_model_oca/readme/CONTRIBUTORS.md +++ b/account_reconcile_model_oca/readme/CONTRIBUTORS.md @@ -1,3 +1,6 @@ - Dixmit - Enric Tobella + +- Trobz \<\> + - Do Anh Duy \<\> diff --git a/account_reconcile_model_oca/readme/CREDITS.md b/account_reconcile_model_oca/readme/CREDITS.md new file mode 100644 index 0000000000..83b3ec91f7 --- /dev/null +++ b/account_reconcile_model_oca/readme/CREDITS.md @@ -0,0 +1 @@ +The migration of this module from 17.0 to 18.0 was financially supported by Camptocamp. diff --git a/account_reconcile_model_oca/static/description/index.html b/account_reconcile_model_oca/static/description/index.html index 846d951a6a..6c40c3029b 100644 --- a/account_reconcile_model_oca/static/description/index.html +++ b/account_reconcile_model_oca/static/description/index.html @@ -8,10 +8,11 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ +:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. +Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -274,7 +275,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: grey; } /* line numbers */ +pre.code .ln { color: gray; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -300,7 +301,7 @@ span.pre { white-space: pre } -span.problematic { +span.problematic, pre.problematic { color: red } span.section-subtitle { @@ -368,7 +369,7 @@

Account Reconcile Model Oca

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! source digest: sha256:cead010f67ccc5d4b9700540c9d8d0b4fb20d497a38d36cea80cf6f78563a756 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

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

+

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

This module restores account reconciliation models functions moved from Odoo community to enterpise in V. 17.0

Table of contents

@@ -378,7 +379,8 @@

Account Reconcile Model Oca

  • Credits
  • @@ -388,7 +390,7 @@

    Bug Tracker

    Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -feedback.

    +feedback.

    Do not contact contributors directly about support or help with technical issues.

    @@ -407,16 +409,27 @@

    Contributors

  • Enric Tobella
  • +
  • Trobz <https://www.trobz.com/> +
  • + +
    +
    +

    Other credits

    +

    The migration of this module from 17.0 to 18.0 was financially supported +by Camptocamp.

    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    -Odoo Community Association + +Odoo Community Association +

    OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

    -

    This module is part of the OCA/account-reconcile project on GitHub.

    +

    This module is part of the OCA/account-reconcile project on GitHub.

    You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

    diff --git a/account_reconcile_model_oca/tests/common.py b/account_reconcile_model_oca/tests/common.py index 14e968a9d2..58d8c2ae71 100644 --- a/account_reconcile_model_oca/tests/common.py +++ b/account_reconcile_model_oca/tests/common.py @@ -13,8 +13,8 @@ class TestAccountReconciliationCommon(AccountTestInvoicingCommon): """ @classmethod - def setUpClass(cls, chart_template_ref=None): - super().setUpClass(chart_template_ref=chart_template_ref) + def setUpClass(cls): + super().setUpClass() cls.company = cls.company_data["company"] cls.company.currency_id = cls.env.ref("base.EUR") @@ -69,7 +69,7 @@ def setUpClass(cls, chart_template_ref=None): "code": "TWAIT", "account_type": "liability_current", "reconcile": True, - "company_id": cls.company.id, + "company_ids": cls.company.ids, } ) # cash basis final account @@ -78,7 +78,7 @@ def setUpClass(cls, chart_template_ref=None): "name": "TAX_TO_DEDUCT", "code": "TDEDUCT", "account_type": "asset_current", - "company_id": cls.company.id, + "company_ids": cls.company.ids, } ) cls.tax_base_amount_account = cls.env["account.account"].create( @@ -86,7 +86,7 @@ def setUpClass(cls, chart_template_ref=None): "name": "TAX_BASE", "code": "TBASE", "account_type": "asset_current", - "company_id": cls.company.id, + "company_ids": cls.company.ids, } ) cls.company.account_cash_basis_base_account_id = cls.tax_base_amount_account.id @@ -190,7 +190,7 @@ def _create_invoice( 0, 0, { - "name": "product that cost %s" % invoice_amount, + "name": f"product that cost {invoice_amount}", "quantity": 1, "price_unit": invoice_amount, "tax_ids": [Command.set([])], diff --git a/account_reconcile_model_oca/tests/test_reconciliation_match.py b/account_reconcile_model_oca/tests/test_reconciliation_match.py index 9c99f101f9..595175c7b6 100644 --- a/account_reconcile_model_oca/tests/test_reconciliation_match.py +++ b/account_reconcile_model_oca/tests/test_reconciliation_match.py @@ -1,8 +1,7 @@ from freezegun import freeze_time from odoo import Command -from odoo.tests import tagged -from odoo.tests.common import Form +from odoo.tests import Form, tagged from odoo.addons.account.tests.common import AccountTestInvoicingCommon @@ -10,21 +9,14 @@ @tagged("post_install", "-at_install") class TestReconciliationMatchingRules(AccountTestInvoicingCommon): @classmethod - def setUpClass(cls, chart_template_ref=None): - super().setUpClass(chart_template_ref=chart_template_ref) + def setUpClass(cls): + super().setUpClass() ################# # Company setup # ################# - cls.currency_data_2 = cls.setup_multi_currency_data( - { - "name": "Dark Chocolate Coin", - "symbol": "🍫", - "currency_unit_label": "Dark Choco", - "currency_subunit_label": "Dark Cacao Powder", - }, - rate2016=10.0, - rate2017=20.0, + cls.other_currency = cls.setup_other_currency( + "EUR", rates=[("2016-01-01", 10.0), ("2017-01-01", 20.0)] ) cls.company = cls.company_data["company"] @@ -33,7 +25,7 @@ def setUpClass(cls, chart_template_ref=None): cls.current_assets_account = cls.env["account.account"].search( [ ("account_type", "=", "asset_current"), - ("company_id", "=", cls.company.id), + ("company_ids", "in", cls.company.id), ], limit=1, ) @@ -188,6 +180,9 @@ def setUpClass(cls, chart_template_ref=None): }, ] ) + cls.payment_credit_account_id = ( + cls.outbound_payment_method_line.payment_account_id + ) @classmethod def _create_invoice_line( @@ -212,8 +207,6 @@ def _create_invoice_line( invoice_form.partner_id = partner if currency: invoice_form.currency_id = currency - if pay_reference: - invoice_form.payment_reference = pay_reference if ref: invoice_form.ref = ref if name: @@ -224,6 +217,8 @@ def _create_invoice_line( invoice_line_form.price_unit = amount invoice_line_form.tax_ids.clear() invoice = invoice_form.save() + if pay_reference: + invoice.payment_reference = pay_reference invoice.action_post() lines = invoice.line_ids return lines.filtered( @@ -1031,7 +1026,7 @@ def test_auto_reconcile_with_tax_fpos(self): def test_reverted_move_matching(self): partner = self.partner_1 AccountMove = self.env["account.move"] - account = self.bank_journal.company_id.account_journal_payment_credit_account_id + account = self.payment_credit_account_id move = AccountMove.create( { "journal_id": self.bank_journal.id, @@ -1061,8 +1056,7 @@ def test_reverted_move_matching(self): ) payment_bnk_line = move.line_ids.filtered( - lambda line: line.account_id - == self.bank_journal.company_id.account_journal_payment_credit_account_id + lambda line: line.account_id == self.payment_credit_account_id ) move.action_post() @@ -1262,7 +1256,7 @@ def test_match_multi_currencies(self): "name": "test_match_multi_currencies", "code": "xxxx", "type": "bank", - "currency_id": self.currency_data["currency"].id, + "currency_id": self.company_data["currency"].id, } ) @@ -1287,7 +1281,7 @@ def test_match_multi_currencies(self): "date": "2016-01-01", "payment_ref": "line", "partner_id": partner.id, - "foreign_currency_id": self.currency_data_2["currency"].id, + "foreign_currency_id": self.other_currency.id, "amount": 300.0, # Rate is 3 GOL = 1 USD in 2016. # Rate is 10 DAR = 1 USD in 2016 but the rate used by the bank is 9:1. "amount_currency": 900.0, @@ -1310,7 +1304,7 @@ def test_match_multi_currencies(self): "default_account_receivable" ].id, "partner_id": partner.id, - "currency_id": self.currency_data["currency"].id, + "currency_id": self.other_currency.id, "debit": 100.0, "credit": 0.0, "amount_currency": 200.0, @@ -1325,7 +1319,7 @@ def test_match_multi_currencies(self): "default_account_receivable" ].id, "partner_id": partner.id, - "currency_id": self.currency_data_2["currency"].id, + "currency_id": self.other_currency.id, "debit": 14.0, "credit": 0.0, "amount_currency": 280.0, @@ -1364,7 +1358,7 @@ def test_match_multi_currencies(self): @freeze_time("2020-01-01") def test_matching_with_write_off_foreign_currency(self): journal_foreign_curr = self.company_data["default_journal_bank"].copy() - journal_foreign_curr.currency_id = self.currency_data["currency"] + journal_foreign_curr.currency_id = self.company_data["currency"] reco_model = self._create_reconcile_model( auto_reconcile=True, @@ -1400,7 +1394,7 @@ def create_payment_line(amount, memo, partner): "payment_type": "inbound", "partner_type": "customer", "partner_id": partner.id, - "ref": memo, + "memo": memo, "destination_account_id": self.company_data[ "default_account_receivable" ].id, @@ -1408,7 +1402,7 @@ def create_payment_line(amount, memo, partner): ) payment.action_post() - return payment.line_ids.filtered( + return payment.move_id.line_ids.filtered( lambda x: x.account_id.account_type not in {"asset_receivable", "liability_payable"} )