Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions partner_identification/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"views/res_partner_id_category_view.xml",
"views/res_partner_id_number_view.xml",
"views/res_partner_view.xml",
"views/res_company_view.xml",
],
"author": "ChriCar Beteiligungs- und Beratungs- GmbH,"
"Tecnativa,"
Expand Down
1 change: 1 addition & 0 deletions partner_identification/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from . import res_partner_id_number
from . import res_partner_id_category
from . import res_partner
from . import res_company
15 changes: 15 additions & 0 deletions partner_identification/models/res_company.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright 2026 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from odoo import fields, models


class ResCompany(models.Model):
_inherit = "res.company"

id_category_scheme_unique_check = fields.Boolean(
string="Enforce unique ID category scheme",
default=False,
help="When enabled, two Partner ID Categories cannot share the "
"same external `scheme` code.",
)
27 changes: 27 additions & 0 deletions partner_identification/models/res_partner_id_category.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ def _get_default_color(self):
help="Abbreviation or acronym of this ID type. For example, "
"'driver_license'",
)
scheme = fields.Char(
help="Code identifying this ID type in an external coding scheme "
"(eg. the Peppol ICD code '0088' for GLN, for EDI/UBL output). "
"Falls back to `code` when not set - only needed when `code` "
"(used for other purposes, eg. internal categorization) differs "
"from the code an external system expects.",
)
name = fields.Char(
string="ID name",
required=True,
Expand All @@ -40,6 +47,26 @@ def _get_default_color(self):
"Python validation code", help="Python code called to validate an id number."
)

@api.constrains("scheme")
def _check_scheme_unique(self):
if not self.env.company.id_category_scheme_unique_check:
Comment thread
simahawk marked this conversation as resolved.
Outdated
return
for rec in self:
if not rec.scheme:
continue
duplicate = self.search(
[("scheme", "=", rec.scheme), ("id", "!=", rec.id)], limit=1
)
if duplicate:
raise ValidationError(
self.env._(
"The scheme code '%(scheme)s' is already used by "
"another ID category (%(other)s).",
scheme=rec.scheme,
other=duplicate.display_name,
)
)

@api.model
def _search_duplicate(self, category_id, id_number, force_active=False):
"""Find duplicates for the given category and number."""
Expand Down
14 changes: 14 additions & 0 deletions partner_identification/readme/CONFIGURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ Code:
Code, abbreviation or acronym of this ID type. For example,
'driver_license'

Scheme:
Optional code identifying this ID type in an external coding scheme (for
example the Peppol ICD code '0088' for GLN, used for EDI/UBL output).
Falls back to `Code` when not set - only needed when `Code` (used for
other purposes, e.g. internal categorization) differs from the code an
external system expects.

By default, two different ID Categories can share the same `Scheme`. To
forbid this, enable "Enforce unique ID category scheme" on the company
(Settings \> Companies \> a company \> Partner Identification). Once
enabled, saving an ID Category whose `Scheme` is already used by another
one raises a validation error. Categories with no `Scheme` set never
conflict, even when this is enabled.

Python validation code:
Optional python code called to validate ID numbers of this ID type. This
functionality can be overridden by setting `id_no_validate` to `True` in
Expand Down
30 changes: 30 additions & 0 deletions partner_identification/tests/test_partner_identification.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,36 @@ def test_update_partner_with_category(self):
self.assertEqual(len(partner_1.id_numbers), 0)


class TestPartnerIdCategorySchemeUnique(common.TransactionCase):
def test_duplicate_scheme_allowed_by_default(self):
# The check is off by default - duplicates are allowed.
self.assertFalse(self.env.company.id_category_scheme_unique_check)
self.env["res.partner.id_category"].create(
{"code": "cat1", "name": "Cat 1", "scheme": "TESTSCHEME"}
)
# Does not raise.
self.env["res.partner.id_category"].create(
{"code": "cat2", "name": "Cat 2", "scheme": "TESTSCHEME"}
)

def test_duplicate_scheme_blocked_when_enabled(self):
self.env.company.id_category_scheme_unique_check = True
self.env["res.partner.id_category"].create(
{"code": "cat1", "name": "Cat 1", "scheme": "TESTSCHEME"}
)
with self.assertRaises(ValidationError), self.cr.savepoint():
self.env["res.partner.id_category"].create(
{"code": "cat2", "name": "Cat 2", "scheme": "TESTSCHEME"}
)

def test_no_scheme_never_blocked(self):
# Categories with no `scheme` set never conflict, even when enabled.
self.env.company.id_category_scheme_unique_check = True
self.env["res.partner.id_category"].create({"code": "cat1", "name": "Cat 1"})
# Does not raise.
self.env["res.partner.id_category"].create({"code": "cat2", "name": "Cat 2"})


class TestPartnerCategoryValidation(common.TransactionCase):
def test_partner_id_number_validation(self):
partner_id_category = self.env["res.partner.id_category"].create(
Expand Down
15 changes: 15 additions & 0 deletions partner_identification/views/res_company_view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_company_form_id_category_scheme" model="ir.ui.view">
<field name="name">res.company.form.id_category_scheme_unique_check</field>
<field name="model">res.company</field>
<field name="inherit_id" ref="base.view_company_form" />
<field name="arch" type="xml">
<xpath expr="//div[hasclass('oe_title')]" position="after">
<group string="Partner Identification">
<field name="id_category_scheme_unique_check" />
</group>
</xpath>
</field>
</record>
</odoo>
13 changes: 13 additions & 0 deletions partner_identification/views/res_partner_id_category_view.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<group>
<field name="name" />
<field name="code" />
<field name="scheme" />
<field name="validation_code" />
</group>
<group>
Expand Down Expand Up @@ -60,6 +61,18 @@ failed = not id_number.name.startswith(self.code) and True or False
</list>
</field>
</record>
<record id="view_partner_id_category_search" model="ir.ui.view">
<field name="name">res.partner.id_category.search</field>
<field name="model">res.partner.id_category</field>
<field name="type">search</field>
<field name="arch" type="xml">
<search string="Partner ID Categories">
<field name="name" />
<field name="code" />
<field name="scheme" />
</search>
</field>
</record>
<record id="action_partner_id_category" model="ir.actions.act_window">
<field name="name">Partner ID Categories</field>
<field name="res_model">res.partner.id_category</field>
Expand Down
22 changes: 22 additions & 0 deletions partner_identification/views/res_partner_id_number_view.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,28 @@
</list>
</field>
</record>
<record id="view_partner_id_numbers_search" model="ir.ui.view">
<field name="name">res.partner.id_number.search</field>
<field name="model">res.partner.id_number</field>
<field name="type">search</field>
<field name="arch" type="xml">
<search string="Partner ID Numbers">
<field name="name" />
<field name="partner_id" />
<field name="category_id" />
<field
name="name"
string="Category Code"
filter_domain="[('category_id.code', 'ilike', self)]"
/>
<field
name="name"
string="Category Scheme"
filter_domain="[('category_id.scheme', 'ilike', self)]"
/>
</search>
</field>
</record>
<record id="action_partner_id_numbers_form" model="ir.actions.act_window">
<field name="name">Partner ID Numbers</field>
<field name="res_model">res.partner.id_number</field>
Expand Down
Loading