diff --git a/message_center_compassion/models/field_to_json.py b/message_center_compassion/models/field_to_json.py
index 330ff0d54..2cff0a0b0 100644
--- a/message_center_compassion/models/field_to_json.py
+++ b/message_center_compassion/models/field_to_json.py
@@ -9,8 +9,9 @@
##############################################################################
import logging
-from odoo import api, fields, models
+from odoo import _, api, fields, models
from odoo.exceptions import UserError
+from odoo.osv import expression
from odoo.tools.safe_eval import safe_eval, wrap_module
_logger = logging.getLogger(__name__)
@@ -67,6 +68,35 @@ class FieldToJson(models.Model):
"record. If not specified, it will assume a single value is given and "
"will search according the relational_field set."
)
+ relational_domain_restrict = fields.Char(
+ help="Optional domain (Odoo domain syntax) added to the search "
+ "performed for an existing relational record. Use it to disambiguate "
+ "between several records that could otherwise match the same "
+ "search value."
+ )
+ relational_comodel_name = fields.Char(
+ related="relational_field_id.relation",
+ help="Technical field holding the model name of the relational field, "
+ "used to know which model relational_domain_restrict applies to.",
+ )
+ relational_ttype = fields.Selection(
+ related="relational_field_id.ttype",
+ help="Technical field holding the type (many2one, many2many, ...) "
+ "of the relational field, used to show Many2one-specific options "
+ "in the view.",
+ )
+ many2one_multiple_match_policy = fields.Selection(
+ [
+ ("first_match", "Take First Match"),
+ ("raise", "Raise an Error"),
+ ],
+ default="first_match",
+ help="When the search for an existing record on a Many2one field "
+ "returns several matches, decide whether to silently take the "
+ "first one found (legacy behavior) or raise an error so the "
+ "ambiguity can be resolved manually, e.g. with a domain "
+ "restriction.",
+ )
allow_relational_creation = fields.Boolean(
help="If set to true, new records will be created if no matching "
"records are found with the given JSON values",
@@ -236,18 +266,38 @@ def _search_for_relational_values(self, value, field, relational_model, orm_vals
# In that case we receive several values for the relation record
# and use one value in particular to find a matching record.
search_val = val.get(search_field)
- records = (
- relational_model.search(
- [
- "|",
- (search_field, "=", search_val),
- (search_field, "=ilike", str(search_val)),
- ]
- )
- if search_val
- else relational_model
- )
+ if search_val:
+ domain = [
+ "|",
+ (search_field, "=", search_val),
+ (search_field, "=ilike", str(search_val)),
+ ]
+ if self.relational_domain_restrict:
+ domain = expression.AND(
+ [domain, safe_eval(self.relational_domain_restrict)]
+ )
+ records = relational_model.search(domain)
+ else:
+ records = relational_model
if self.relational_field_id.ttype == "many2one":
+ if len(records) > 1 and self.many2one_multiple_match_policy == "raise":
+ raise UserError(
+ _(
+ "Found %(count)s records matching value %(value)r "
+ "for field %(field)s (mapping %(mapping)s), but "
+ "expected at most one. Restrict the search with a "
+ "domain or allow taking the first match on the "
+ "field mapping configuration. Matching records: "
+ "%(records)s"
+ )
+ % {
+ "count": len(records),
+ "value": search_val,
+ "field": self.odoo_field,
+ "mapping": self.mapping_id.name,
+ "records": records,
+ }
+ )
record = records[:1] # Only take one relation
if not record and self.allow_relational_creation:
to_create.append(val)
@@ -364,3 +414,68 @@ def _get_relational_creation_values(self, field_values):
}
)
return [(0, 0, values) for values in record_values]
+
+ def action_check_duplicate_matches(self):
+ """
+ Diagnostic action for the "Check for duplicates" button: scans the
+ actual current data of the relational model targeted by this field
+ mapping, and reports which search values currently match more than
+ one record - i.e. the exact ambiguity a Many2one lookup could hit
+ when converting an incoming GMC message (see T3276).
+
+ This lets a non-technical user self-serve the same check a
+ developer would otherwise have to run manually against the
+ database.
+ """
+ self.ensure_one()
+ if (
+ self.relational_field_id.ttype != "many2one"
+ or not self.search_relational_record
+ ):
+ raise UserError(
+ _(
+ "This check only applies to a Many2one field configured "
+ "with 'Search Relational Record' enabled."
+ )
+ )
+ target_model = self.relational_comodel_name
+ search_field = self.search_key or self.field_id.name
+ if not target_model or not search_field:
+ raise UserError(_("Cannot determine the target model/field to check."))
+
+ duplicate_groups = self.env[target_model]._read_group(
+ domain=[(search_field, "!=", False)],
+ groupby=[search_field],
+ aggregates=["__count"],
+ having=[("__count", ">", 1)],
+ )
+ if not duplicate_groups:
+ return {
+ "type": "ir.actions.client",
+ "tag": "display_notification",
+ "params": {
+ "title": _("No ambiguity found"),
+ "message": _(
+ "No duplicate values currently exist on %(model)s.%(field)s. "
+ "This lookup is safe for now, but new data could still "
+ "introduce a duplicate later."
+ )
+ % {"model": target_model, "field": search_field},
+ "type": "success",
+ "sticky": False,
+ },
+ }
+ raw_values = [group[0] for group in duplicate_groups]
+ if raw_values and isinstance(raw_values[0], models.BaseModel):
+ domain_values = [value.id for value in raw_values]
+ else:
+ domain_values = raw_values
+ return {
+ "type": "ir.actions.act_window",
+ "name": _("%(count)s value(s) of %(field)s match several records")
+ % {"count": len(domain_values), "field": search_field},
+ "res_model": target_model,
+ "view_mode": "list,form",
+ "domain": [(search_field, "in", domain_values)],
+ "context": {"group_by": [search_field]},
+ }
diff --git a/message_center_compassion/views/compassion_mapping_view.xml b/message_center_compassion/views/compassion_mapping_view.xml
index 2d38feca3..e795f0d58 100644
--- a/message_center_compassion/views/compassion_mapping_view.xml
+++ b/message_center_compassion/views/compassion_mapping_view.xml
@@ -25,6 +25,8 @@
+
+
+
+
+
+
+
+
+
+
+
+ field.to.json.many2one.review.list
+ compassion.field.to.json
+
+
+
+
+
+
+
+
+
+
+
+
+ Many2one Fields to Review
+ compassion.field.to.json
+ list,form
+
+ [('search_relational_record', '=', True), ('relational_ttype', '=', 'many2one')]
+
+
+
+
compassion.mapping.listcompassion.mapping
diff --git a/sponsorship_compassion/security/ir.model.access.csv b/sponsorship_compassion/security/ir.model.access.csv
index f2d0d5404..846f9b06a 100644
--- a/sponsorship_compassion/security/ir.model.access.csv
+++ b/sponsorship_compassion/security/ir.model.access.csv
@@ -3,7 +3,6 @@ access_recurring_contract,Full access on recurring.contract,recurring_contract.m
access_recurring_contract_line,Full access on recurring.contract.line,recurring_contract.model_recurring_contract_line,child_compassion.group_sponsorship,1,1,1,1
access_recurring_contract_group,Full access on recurring.contract.group,recurring_contract.model_recurring_contract_group,child_compassion.group_sponsorship,1,1,1,1
access_recurring_contract_origin,Full access on recurring.contract.origin,model_recurring_contract_origin,child_compassion.group_sponsorship,1,1,1,1
-access_recurring_invoicer,Full access on recurring.invoicer,recurring_contract.model_recurring_invoicer,child_compassion.group_sponsorship,1,1,1,1
access_account_move,Full access on account.move,account.model_account_move,child_compassion.group_sponsorship,1,1,1,1
access_account_move_line,Full access on account.move.line,account.model_account_move_line,child_compassion.group_sponsorship,1,1,1,1
access_account_journal,Read access on account.journal,account.model_account_journal,child_compassion.group_sponsorship,1,0,0,0
diff --git a/sponsorship_compassion/views/sponsorship_contract_view.xml b/sponsorship_compassion/views/sponsorship_contract_view.xml
index d531eafcc..236da50a4 100644
--- a/sponsorship_compassion/views/sponsorship_contract_view.xml
+++ b/sponsorship_compassion/views/sponsorship_contract_view.xml
@@ -358,13 +358,6 @@
sequence="12"
action="recurring_contract.action_invoice_automatic_generation"
/>
-