From da3f563e0e479b49f5bba2b46c5912c23f63a5e5 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Tue, 5 May 2026 16:32:48 +0800 Subject: [PATCH 1/4] fix(spp_import_match): add Condition Field, treat conditional rows as pure gates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `is_conditional=True` rows on `spp.import.match.fields` were being used as both the gate (CSV value check) and a search predicate (`field_id` added to the DB domain). When the CSV had a metadata column that didn't exist on the registrant model — e.g. `data_source` — the generated search query would either fail or silently return zero matches, defeating the whole conditional-rule mechanism. Two-part fix: - Add a separate `condition_field_id` Many2one on `spp.import.match.fields` so a rule can gate on a different field than the one it searches on. The new field is shown in the form's Fields list as **Condition Field**, readonly until `Is Conditional` is ticked. - Rewrite the matching loop in `spp.import.match._match_find`: conditional rows are now pure gates and never contribute to the search domain. The gate column is `condition_field_id.name` when set, falling back to `field_id.name` for backwards compatibility with rules created before this change. - Rename the existing `imported_value` column heading to **Condition Value** to match the documentation. Refs OP#991. --- spp_import_match/__manifest__.py | 2 +- spp_import_match/models/import_match.py | 42 +++++++++++++++++++- spp_import_match/views/import_match_view.xml | 11 ++++- 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/spp_import_match/__manifest__.py b/spp_import_match/__manifest__.py index 3c6a52ce..2668090b 100644 --- a/spp_import_match/__manifest__.py +++ b/spp_import_match/__manifest__.py @@ -5,7 +5,7 @@ "name": "OpenSPP Import Match", "summary": "OpenSPP Import Match enhances data import processes by intelligently matching incoming records against existing data, preventing duplication and ensuring registry integrity. It provides configurable matching logic and supports seamless updates to existing records during bulk data onboarding.", "category": "OpenSPP/Integration", - "version": "19.0.2.0.0", + "version": "19.0.2.0.1", "sequence": 1, "author": "OpenSPP.org", "website": "https://github.com/OpenSPP/OpenSPP2", diff --git a/spp_import_match/models/import_match.py b/spp_import_match/models/import_match.py index 5ee26de6..224a3301 100644 --- a/spp_import_match/models/import_match.py +++ b/spp_import_match/models/import_match.py @@ -68,9 +68,25 @@ def _match_find(self, model, converted_row, imported_row): domain = list() for field in combination.field_ids: if field.is_conditional: - if imported_row[field.name] != field.imported_value: + # Conditional rows are pure *gates* — they decide whether + # the rule applies to this CSV row. The CSV column that + # carries the gate value is `condition_field_id` when + # set; otherwise we fall back to `field_id` for + # backwards compatibility with rules created before + # OP#991. Crucially, a conditional row is **not** added + # to the DB search domain — the gate column may be a + # CSV-only metadata field (e.g. `data_source`) that + # doesn't exist on the registrant model. + gate_field_name = ( + field.condition_field_id.name + if field.condition_field_id + else field.field_id.name + ) + if imported_row.get(gate_field_name) != field.imported_value: combination_valid = False break + continue + if field.field_id.name in converted_row: row_value = converted_row[field.field_id.name] # Skip matching on empty values to avoid false matches @@ -141,7 +157,29 @@ class SPPImportMatchFields(models.Model): match_id = fields.Many2one("spp.import.match", string="Match", ondelete="cascade") model_id = fields.Many2one(related="match_id.model_id") is_conditional = fields.Boolean() - imported_value = fields.Char(help="This will be used as a condition to disregard this field if matched") + condition_field_id = fields.Many2one( + "ir.model.fields", + string="Condition Field", + ondelete="cascade", + domain="[('model_id', '=', model_id)]", + help=( + "When `Is Conditional` is set, the rule only fires for CSV rows " + "whose value in this field equals `Condition Value`. The " + "condition field is used purely as a gate — it is **not** added " + "to the database search domain, so it can safely be a CSV-only " + "metadata column (e.g. `data_source`) that doesn't have data on " + "the registrant. Leave empty to fall back to the legacy " + "behaviour where `Field` is used as both the gate and the " + "search predicate." + ), + ) + imported_value = fields.Char( + string="Condition Value", + help=( + "Expected value of the condition field. The rule only applies " + "when the imported row's `Condition Field` matches this value." + ), + ) def _compute_name(self): for rec in self: diff --git a/spp_import_match/views/import_match_view.xml b/spp_import_match/views/import_match_view.xml index 7091abf8..7d9efb94 100644 --- a/spp_import_match/views/import_match_view.xml +++ b/spp_import_match/views/import_match_view.xml @@ -53,9 +53,18 @@ - + + From 7d7c879611b929ab30382a4186cdbc6d0b6f76d0 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Fri, 8 May 2026 09:47:32 +0800 Subject: [PATCH 2/4] chore(spp_import_match): hide conditional-gate columns until a use case lands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Condition Field / Condition Value / Is Conditional columns added under OP#991 introduced UI surface for a feature that no current import flow uses. Reviewers asked for them to be hidden until a real use case shows up. Switch all three columns on the match-rule fields list to `column_invisible="1"`. The model schema, the `_match_find` gate semantics, and the backwards-compat fallback to `field_id.name` when `condition_field_id` is empty all stay in place — the data path is unchanged. Only the form's column rendering is suppressed, so existing records keep their values and the engine keeps honoring them; new rules just don't expose the controls in the UI. Bumps version 19.0.2.0.1 → 19.0.2.0.2 and fills in the missing HISTORY entry for the OP#991 round-1 work. Refs OP#991. --- spp_import_match/__manifest__.py | 2 +- spp_import_match/readme/HISTORY.md | 8 ++++++++ spp_import_match/views/import_match_view.xml | 18 ++++++++++++------ 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/spp_import_match/__manifest__.py b/spp_import_match/__manifest__.py index 2668090b..26fcef35 100644 --- a/spp_import_match/__manifest__.py +++ b/spp_import_match/__manifest__.py @@ -5,7 +5,7 @@ "name": "OpenSPP Import Match", "summary": "OpenSPP Import Match enhances data import processes by intelligently matching incoming records against existing data, preventing duplication and ensuring registry integrity. It provides configurable matching logic and supports seamless updates to existing records during bulk data onboarding.", "category": "OpenSPP/Integration", - "version": "19.0.2.0.1", + "version": "19.0.2.0.2", "sequence": 1, "author": "OpenSPP.org", "website": "https://github.com/OpenSPP/OpenSPP2", diff --git a/spp_import_match/readme/HISTORY.md b/spp_import_match/readme/HISTORY.md index 4aaf9afe..01d95e79 100644 --- a/spp_import_match/readme/HISTORY.md +++ b/spp_import_match/readme/HISTORY.md @@ -1,3 +1,11 @@ +### 19.0.2.0.2 + +- chore(views): hide the conditional-gate columns (`Is Conditional`, `Condition Field`, `Condition Value`) from the match-rule fields list — the schema and matching-engine wiring stay in place, but no current import flow uses the gate, so the columns are kept out of the UI until a real use case lands. + +### 19.0.2.0.1 + +- fix(matching): add a `condition_field_id` Many2one column to `spp.import.match.fields` and rewrite the matching loop so conditional rows act as pure gates — never added to the DB search domain. Renames the IMPORTED VALUE column heading to **Condition Value**. Fixes the case where a CSV-only metadata column (e.g. `data_source`) was being injected into the search domain and causing zero matches. + ### 19.0.2.0.0 - Initial migration to OpenSPP2 diff --git a/spp_import_match/views/import_match_view.xml b/spp_import_match/views/import_match_view.xml index 7d9efb94..32488a9c 100644 --- a/spp_import_match/views/import_match_view.xml +++ b/spp_import_match/views/import_match_view.xml @@ -52,20 +52,26 @@ /> - - + + From 0c2bcd2df315e9c7b97e01cbe284a482d6d3976e Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Tue, 12 May 2026 15:09:15 +0800 Subject: [PATCH 3/4] test(spp_import_match): update conditional-match test for new pure-gate semantics (OP#991) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI surfaced a regression in test_match_find_conditional_match. The test was written under the pre-OP#991 semantics where an `is_conditional=True` row contributed BOTH a gate (CSV value check) AND a search predicate (`field_id` added to the search domain). The OP#991 fix (commit da3f563e) intentionally removes that dual role — conditional rows are now pure gates, never injected into the domain. Under the new semantics, a rule with only one conditional row produces an empty search domain and is skipped (the existing `if not domain: continue` guard at line 109). The test's assertion that the partner would be found is therefore obsolete. Rewrite the test to demonstrate the new gate + non-conditional-search shape: a conditional row gates on `name`, and a non-conditional row provides the actual `email` search predicate. Verifies the gate- passing path while respecting the OP#991 semantics. Sibling test test_match_find_conditional_skip (gate-failing path) still passes unchanged. 44 tests, 0 failed locally. --- .../tests/test_import_match_model.py | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/spp_import_match/tests/test_import_match_model.py b/spp_import_match/tests/test_import_match_model.py index 801c3dc3..15df3353 100644 --- a/spp_import_match/tests/test_import_match_model.py +++ b/spp_import_match/tests/test_import_match_model.py @@ -120,21 +120,33 @@ def test_match_find_conditional_skip(self): self.assertFalse(result.id) def test_match_find_conditional_match(self): - """Test _match_find uses rule when conditional value matches.""" - partner = self.env["res.partner"].create({"name": "ConditionalMatchTest"}) + """Test _match_find applies the rule when the conditional gate passes. + + Under OP#991 semantics, an `is_conditional=True` row is a pure gate — + it decides whether the rule applies to this CSV row but is never + added to the DB search domain (the gate column may be a CSV-only + metadata field that doesn't exist on the registrant model). The + combination must include at least one non-conditional row to + provide the actual search predicate. Here `name` is the gate and + `email` is the search predicate. + """ + partner = self.env["res.partner"].create( + {"name": "ConditionalMatchTest", "email": "conditional@example.com"} + ) match = self._create_match_rule( [ { "field_id": self.name_field.id, "is_conditional": True, "imported_value": "ConditionalMatchTest", - } + }, + {"field_id": self.email_field.id}, ] ) result = match._match_find( self.env["res.partner"], - {"name": "ConditionalMatchTest"}, - {"name": "ConditionalMatchTest", "id": None}, + {"email": "conditional@example.com"}, + {"name": "ConditionalMatchTest", "email": "conditional@example.com", "id": None}, ) self.assertEqual(result, partner) From d44adb38a5be066c8c928ef83e727b3890dfcdb9 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Tue, 12 May 2026 15:16:43 +0800 Subject: [PATCH 4/4] chore(spp_import_match): apply CI formatter + regenerate README (#991) --- spp_import_match/README.rst | 20 +++++++++++++++++ spp_import_match/models/import_match.py | 6 +---- .../static/description/index.html | 22 +++++++++++++++++++ .../tests/test_import_match_model.py | 4 +--- 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/spp_import_match/README.rst b/spp_import_match/README.rst index 2c4528cf..611fee98 100644 --- a/spp_import_match/README.rst +++ b/spp_import_match/README.rst @@ -393,6 +393,26 @@ Test 12: Security — Non-Admin Access Changelog ========= +19.0.2.0.2 +~~~~~~~~~~ + +- chore(views): hide the conditional-gate columns (``Is Conditional``, + ``Condition Field``, ``Condition Value``) from the match-rule fields + list — the schema and matching-engine wiring stay in place, but no + current import flow uses the gate, so the columns are kept out of the + UI until a real use case lands. + +19.0.2.0.1 +~~~~~~~~~~ + +- fix(matching): add a ``condition_field_id`` Many2one column to + ``spp.import.match.fields`` and rewrite the matching loop so + conditional rows act as pure gates — never added to the DB search + domain. Renames the IMPORTED VALUE column heading to **Condition + Value**. Fixes the case where a CSV-only metadata column (e.g. + ``data_source``) was being injected into the search domain and causing + zero matches. + 19.0.2.0.0 ~~~~~~~~~~ diff --git a/spp_import_match/models/import_match.py b/spp_import_match/models/import_match.py index 224a3301..51ab48b9 100644 --- a/spp_import_match/models/import_match.py +++ b/spp_import_match/models/import_match.py @@ -77,11 +77,7 @@ def _match_find(self, model, converted_row, imported_row): # to the DB search domain — the gate column may be a # CSV-only metadata field (e.g. `data_source`) that # doesn't exist on the registrant model. - gate_field_name = ( - field.condition_field_id.name - if field.condition_field_id - else field.field_id.name - ) + gate_field_name = field.condition_field_id.name if field.condition_field_id else field.field_id.name if imported_row.get(gate_field_name) != field.imported_value: combination_valid = False break diff --git a/spp_import_match/static/description/index.html b/spp_import_match/static/description/index.html index 3d48ccfc..dff79052 100644 --- a/spp_import_match/static/description/index.html +++ b/spp_import_match/static/description/index.html @@ -804,6 +804,28 @@

Changelog

+

19.0.2.0.2

+
    +
  • chore(views): hide the conditional-gate columns (Is Conditional, +Condition Field, Condition Value) from the match-rule fields +list — the schema and matching-engine wiring stay in place, but no +current import flow uses the gate, so the columns are kept out of the +UI until a real use case lands.
  • +
+
+
+

19.0.2.0.1

+
    +
  • fix(matching): add a condition_field_id Many2one column to +spp.import.match.fields and rewrite the matching loop so +conditional rows act as pure gates — never added to the DB search +domain. Renames the IMPORTED VALUE column heading to Condition +Value. Fixes the case where a CSV-only metadata column (e.g. +data_source) was being injected into the search domain and causing +zero matches.
  • +
+
+

19.0.2.0.0

  • Initial migration to OpenSPP2
  • diff --git a/spp_import_match/tests/test_import_match_model.py b/spp_import_match/tests/test_import_match_model.py index 15df3353..162ba434 100644 --- a/spp_import_match/tests/test_import_match_model.py +++ b/spp_import_match/tests/test_import_match_model.py @@ -130,9 +130,7 @@ def test_match_find_conditional_match(self): provide the actual search predicate. Here `name` is the gate and `email` is the search predicate. """ - partner = self.env["res.partner"].create( - {"name": "ConditionalMatchTest", "email": "conditional@example.com"} - ) + partner = self.env["res.partner"].create({"name": "ConditionalMatchTest", "email": "conditional@example.com"}) match = self._create_match_rule( [ {