[IMP] account: reconciliation models: introduce partner mapping

Partner mapping is a new field defined on invoice matching reconciliation models. It allows defining a mapping between partners and regular expressions. When ran, on a statement line without any partner set, those rules use this mapping to assign a partner to statement lines without any one set, using the regular expressions to match the payment reference of the statement line.

It is important to note that the mapped partner will only be set to the statement line when the reconciliation is actually performed (so, when clicking the validate button, if the matching rule is not auto_reconcile = True), just like with the partner_map.
This commit is contained in:
oco-odoo
2020-06-12 16:26:42 +00:00
parent 02f7023b54
commit faed88d209
5 changed files with 102 additions and 2 deletions
@@ -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.
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
104 access_account_payment_method_readonly access_account_payment_term_manager account.payment.method account.payment.term model_account_payment_method model_account_payment_term account.group_account_readonly account.group_account_manager 1 0 1 0 1 0 1
105 access_account_payment_method access_account_payment_term_line_partner_manager account.payment.method account.payment.term.line partner manager model_account_payment_method model_account_payment_term_line account.group_account_invoice base.group_user 1 1 0 1 0 1 0
106 access_account_payment_readonly access_account_payment_term_line_manager account.payment account.payment.term.line model_account_payment model_account_payment_term_line account.group_account_readonly account.group_account_manager 1 0 1 0 1 0 1
107 access_account_payment_method_readonly account.payment.method model_account_payment_method account.group_account_readonly 1 0 0 0
108 access_account_payment_method account.payment.method model_account_payment_method account.group_account_invoice 1 1 1 1
109 access_account_payment_readonly account.payment model_account_payment account.group_account_readonly 1 0 0 0
110 access_account_payment account.payment model_account_payment account.group_account_invoice 1 1 1 1
111 access_account_payment_register access.account.payment.register model_account_payment_register account.group_account_invoice 1 1 1 0
112 access_account_bank_statement_closebalance access.account.bank.statement.closebalance model_account_bank_statement_closebalance account.group_account_user 1 1 1 0
@@ -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)
@@ -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'])
@@ -176,6 +176,16 @@
</field>
</group>
<group string="Partner Mapping" attrs="{'invisible': [('rule_type', '!=', 'invoice_matching')]}">
<field name="partner_mapping_line_ids" nolabel="1">
<tree editable="bottom">
<field name="payment_ref_regex" attrs="{'required': [('narration_regex', '=', False)]}"/>
<field name="narration_regex" attrs="{'required': [('payment_ref_regex', '=', False)]}"/>
<field name="partner_id"/>
</tree>
</field>
</group>
</sheet>
</form>
</field>