diff --git a/addons/account/models/account_reconcile_model.py b/addons/account/models/account_reconcile_model.py index 42138f74e7b..5fbb039ed13 100644 --- a/addons/account/models/account_reconcile_model.py +++ b/addons/account/models/account_reconcile_model.py @@ -2,13 +2,29 @@ from odoo import api, fields, models, _ from odoo.tools import float_compare, float_is_zero -from odoo.exceptions import UserError +from odoo.exceptions import UserError, ValidationError import re from math import copysign import itertools from collections import defaultdict from dateutil.relativedelta import relativedelta +class AccountReconcileModelPartnerMapping(models.Model): + _name = 'account.reconcile.model.partner.mapping' + _description = 'Partner mapping for reconciliation models' + + model_id = fields.Many2one(comodel_name='account.reconcile.model', readonly=True, required=True) + partner_id = fields.Many2one(comodel_name='res.partner', string="Partner", required=True) + payment_ref_regex = fields.Char(string="Find Text in Label") + narration_regex = fields.Char(string="Find Text in Notes") + + @api.constrains('narration_regex', 'payment_ref_regex') + def validate_regex(self): + for record in self: + if not (record.narration_regex or record.payment_ref_regex): + raise ValidationError(_("Please set at least one of the match texts to create a partner mapping.")) + + class AccountReconcileModelLine(models.Model): _name = 'account.reconcile.model.line' _description = 'Rules for the reconciliation model' @@ -173,6 +189,13 @@ class AccountReconcileModel(models.Model): help='The reconciliation model will only be applied to the selected customer/vendor categories.') line_ids = fields.One2many('account.reconcile.model.line', 'model_id') + partner_mapping_line_ids = fields.One2many(string="Partner Mapping Lines", + comodel_name='account.reconcile.model.partner.mapping', + inverse_name='model_id', + help="The mapping uses regular expressions.\n" + "- To Match the text at the beginning of the line (in label or notes), simply fill in your text.\n" + "- To Match the text anywhere (in label or notes), put your text between .*\n" + " e.g: .*N°48748 abc123.*") past_months_limit = fields.Integer(string="Past Months Limit", default=18, help="Number of months in the past to consider entries from when applying this model.") @@ -389,6 +412,10 @@ class AccountReconcileModel(models.Model): partner = (partner_map and partner_map.get(st_line.id) and self.env['res.partner'].browse(partner_map[st_line.id])) or st_line.partner_id for rec_model in available_models: + if not partner: + # The function takes care of returning something only for the rules supporting partner mapping + partner = rec_model._get_partner_from_mapping(st_line) + if rec_model._is_applicable_for(st_line, partner): lines_with_partner_per_model[rec_model].append((st_line, partner)) @@ -627,6 +654,25 @@ class AccountReconcileModel(models.Model): return query, params + def _get_partner_from_mapping(self, st_line): + """ 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. + """ + self.ensure_one() + + if self.rule_type != 'invoice_matching': + # Only invoice_matching rules support this option + return None + + 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, st_line.narration or '') if partner_mapping.narration_regex else True + + if match_payment_ref and match_narration: + return partner_mapping.partner_id + return None + def _get_writeoff_suggestion_query(self, st_lines_with_partner, excluded_ids=None): ''' Returns the query applying the current writeoff_suggestion reconciliation model to the provided statement lines. diff --git a/addons/account/security/ir.model.access.csv b/addons/account/security/ir.model.access.csv index d95559d7270..fbf5f631df1 100644 --- a/addons/account/security/ir.model.access.csv +++ b/addons/account/security/ir.model.access.csv @@ -104,6 +104,9 @@ access_account_reconcile_model,account.reconcile.model,model_account_reconcile_m access_account_reconcile_model_line_readonly,account.reconcile.model.line.readonly,model_account_reconcile_model_line,account.group_account_readonly,1,0,0,0 access_account_reconcile_model_line_billing,account.reconcile.model.line.billing,model_account_reconcile_model_line,account.group_account_invoice,1,0,1,0 access_account_reconcile_model_line,account.reconcile.model.line,model_account_reconcile_model_line,account.group_account_user,1,1,1,1 +access_account_reconcile_model_partner_mapping_readonly,account.reconcile.model.partner.mapping.readonly,model_account_reconcile_model_partner_mapping,account.group_account_readonly,1,0,0,0 +access_account_reconcile_model_partner_mapping_billing,account.reconcile.model.partner.mapping.billing,model_account_reconcile_model_partner_mapping,account.group_account_invoice,1,0,1,0 +access_account_reconcile_model_partner_mapping,account.reconcile.model.partner.mapping,model_account_reconcile_model_partner_mapping,account.group_account_user,1,1,1,1 access_account_partial_reconcile_readonly,account.partial.reconcile.readonly,model_account_partial_reconcile,account.group_account_readonly,1,0,0,0 access_account_partial_reconcile_group_invoice,account.partial.reconcile.group.invoice,model_account_partial_reconcile,account.group_account_invoice,1,1,1,0 access_account_partial_reconcile,account.partial.reconcile,model_account_partial_reconcile,account.group_account_user,1,1,1,1 diff --git a/addons/account/tests/test_reconciliation_matching_rules.py b/addons/account/tests/test_reconciliation_matching_rules.py index 177a5464651..e5ebb776ad3 100644 --- a/addons/account/tests/test_reconciliation_matching_rules.py +++ b/addons/account/tests/test_reconciliation_matching_rules.py @@ -458,3 +458,44 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon): self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1}, self.bank_line_2.id: {'aml_ids': []}, }, self.bank_st) + + def test_partner_mapping_rule(self): + self.bank_line_1.write({'partner_id': None, 'payment_ref': 'toto42', 'narration': None}) + self.bank_line_2.write({'partner_id': None}) + + # Without mapping, there should be no match + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': []}, + self.bank_line_2.id: {'aml_ids': []}, + }, self.bank_st) + + # We add some mapping for payment reference to rule_1 + self.rule_1.write({ + 'partner_mapping_line_ids': [(0, 0, { + 'partner_id': self.partner_1.id, + 'payment_ref_regex': 'toto.*', + })] + }) + + # bank_line_1 should now match + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1}, + self.bank_line_2.id: {'aml_ids': []}, + }, self.bank_st) + + # If we now add a narration regex to the same mapping line, nothing should match + self.rule_1.partner_mapping_line_ids.write({'narration_regex': ".*coincoin"}) + + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': []}, + self.bank_line_2.id: {'aml_ids': []}, + }, self.bank_st) + + + # If we set the narration so that it matches the new mapping criterium, line_1 matches + self.bank_line_1.write({'narration': "42coincoin"}) + + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1}, + self.bank_line_2.id: {'aml_ids': []}, + }, self.bank_st) diff --git a/addons/account/tests/test_templates_consistency.py b/addons/account/tests/test_templates_consistency.py index 2e41123db5a..aabf0f2d765 100644 --- a/addons/account/tests/test_templates_consistency.py +++ b/addons/account/tests/test_templates_consistency.py @@ -61,7 +61,7 @@ class AccountingTestTemplConsistency(TransactionCase): '''Test fields consistency for ('account.reconcile.model', 'account.reconcile.model.template') ''' self.check_fields_consistency('account.reconcile.model.template', 'account.reconcile.model', exceptions=['chart_template_id']) - self.check_fields_consistency('account.reconcile.model', 'account.reconcile.model.template', exceptions=['company_id', 'past_months_limit']) + self.check_fields_consistency('account.reconcile.model', 'account.reconcile.model.template', exceptions=['company_id', 'past_months_limit', 'partner_mapping_line_ids']) # lines self.check_fields_consistency('account.reconcile.model.line.template', 'account.reconcile.model.line', exceptions=['chart_template_id']) self.check_fields_consistency('account.reconcile.model.line', 'account.reconcile.model.line.template', exceptions=['company_id', 'journal_id', 'analytic_account_id', 'analytic_tag_ids', 'amount']) diff --git a/addons/account/views/account_reconcile_model_views.xml b/addons/account/views/account_reconcile_model_views.xml index 3f5c4ff4ac2..191f41e4fa9 100644 --- a/addons/account/views/account_reconcile_model_views.xml +++ b/addons/account/views/account_reconcile_model_views.xml @@ -176,6 +176,16 @@ + + + + + + + + + +