[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:
@@ -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
|
||||
|
||||
|
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user