fix: resolve code lists by URI and version (backport #58770) (#58772)

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
This commit is contained in:
mergify[bot]
2026-09-04 20:39:23 +02:00
committed by GitHub
parent 8229aeaead
commit 31319bd36e
2 changed files with 132 additions and 5 deletions

View File

@@ -1,6 +1,7 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import re
from typing import TYPE_CHECKING
import frappe
@@ -78,8 +79,48 @@ class CodeList(Document):
self.url = getattr(root.find(".//Identification/LocationUri"), "text", None)
def _version_key(version: str | None) -> list:
"""Natural sort key for the version formats publishers use: integers and ISO dates.
Orders 3 < 10 (which a lexical sort gets wrong) and 2020-01-01 < 2020-11-05.
"""
return [int(p) if p.isdigit() else p for p in re.split(r"(\d+)", version or "")]
@frappe.request_cache
def resolve_code_list(code_list: str) -> str | None:
"""Return the Code List for a document name or a canonical URI.
Code Lists are named after their CanonicalVersionUri, so one canonical URI can
map to several documents, one per version. An exact document name takes
precedence, which lets a caller request a specific version; a canonical URI
resolves to the latest version available.
"""
if frappe.db.exists("Code List", code_list):
return code_list
candidates = frappe.get_all(
"Code List",
filters={"canonical_uri": code_list},
fields=["name", "version"],
)
if not candidates:
return None
# ponytail: assumes one publisher sticks to one version format. An integer and an
# ISO date under the same canonical URI compare numerically (3 < 2020), so the date
# would win; import the genericode ValidityDate and sort on that if it ever happens.
return max(candidates, key=lambda cl: _version_key(cl.version)).name
def get_codes_for(code_list: str, doctype: str, name: str) -> tuple[str]:
"""Return the common code for a given record"""
"""Return the common code for a given record.
`code_list` may be a Code List name or a canonical URI (latest version wins).
"""
if not (code_list := resolve_code_list(code_list)):
return ()
CommonCode = frappe.qb.DocType("Common Code")
DynamicLink = frappe.qb.DocType("Dynamic Link")
@@ -101,7 +142,13 @@ def get_codes_for(code_list: str, doctype: str, name: str) -> tuple[str]:
def get_docnames_for(code_list: str, doctype: str, code: str) -> tuple[str]:
"""Return the record name for a given common code"""
"""Return the record name for a given common code.
`code_list` may be a Code List name or a canonical URI (latest version wins).
"""
if not (code_list := resolve_code_list(code_list)):
return ()
CommonCode = frappe.qb.DocType("Common Code")
DynamicLink = frappe.qb.DocType("Dynamic Link")
@@ -123,6 +170,12 @@ def get_docnames_for(code_list: str, doctype: str, code: str) -> tuple[str]:
def get_default_code(code_list: str) -> str | None:
"""Return the default common code for a given code list"""
"""Return the default common code for a given code list.
`code_list` may be a Code List name or a canonical URI (latest version wins).
"""
if not (code_list := resolve_code_list(code_list)):
return None
code_id = frappe.db.get_value("Code List", code_list, "default_common_code")
return frappe.db.get_value("Common Code", code_id, "common_code") if code_id else None

View File

@@ -1,9 +1,83 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import frappe
from erpnext.edi.doctype.code_list.code_list import (
_version_key,
get_codes_for,
get_default_code,
get_docnames_for,
resolve_code_list,
)
from erpnext.tests.utils import ERPNextTestSuite
CANONICAL_URI = "urn:test:erpnext:codeliste:resolve"
OLD_VERSION = f"{CANONICAL_URI}:3"
NEW_VERSION = f"{CANONICAL_URI}:10"
UNKNOWN_URI = "urn:test:erpnext:codeliste:missing"
class TestCodeList(ERPNextTestSuite):
pass
def setUp(self):
"""Create two versions of one code list. Test records are rolled back per test."""
for name, version in ((OLD_VERSION, "3"), (NEW_VERSION, "10")):
if not frappe.db.exists("Code List", name):
frappe.get_doc(
doctype="Code List",
name=name,
title=name,
canonical_uri=CANONICAL_URI,
version=version,
).insert()
default_code = frappe.get_doc(
doctype="Common Code",
title="Test Default",
common_code="XYZ",
code_list=NEW_VERSION,
).insert()
frappe.db.set_value("Code List", NEW_VERSION, "default_common_code", default_code.name)
# resolution is request-cached, so fixtures must not be masked by earlier lookups
frappe.local.request_cache.clear()
def test_version_key_orders_integers_and_iso_dates(self):
"""Integer and ISO date versions must both order correctly, unlike a lexical sort."""
self.assertEqual(sorted(["10", "3", None, "9"], key=_version_key), [None, "3", "9", "10"])
self.assertEqual(
sorted(["2020-11-05", "2019-12-31", "2020-01-01"], key=_version_key),
["2019-12-31", "2020-01-01", "2020-11-05"],
)
def test_canonical_uri_resolves_to_latest_version(self):
self.assertEqual(resolve_code_list(CANONICAL_URI), NEW_VERSION)
def test_name_resolves_to_itself(self):
"""Passing a version-specific name must return that version, not the latest one."""
self.assertEqual(resolve_code_list(OLD_VERSION), OLD_VERSION)
def test_name_takes_precedence_over_canonical_uri(self):
"""A document named like a canonical URI must not redirect to another version."""
frappe.get_doc(
doctype="Code List",
name=CANONICAL_URI,
title=CANONICAL_URI,
canonical_uri=CANONICAL_URI,
version="1",
).insert()
frappe.local.request_cache.clear()
self.assertEqual(resolve_code_list(CANONICAL_URI), CANONICAL_URI)
def test_unknown_uri_resolves_to_none(self):
self.assertIsNone(resolve_code_list(UNKNOWN_URI))
def test_lookups_are_empty_for_unknown_code_list(self):
"""An unresolved code list must not fall through to an unfiltered query."""
self.assertEqual(get_codes_for(UNKNOWN_URI, "UOM", "Nos"), ())
self.assertEqual(get_docnames_for(UNKNOWN_URI, "UOM", "XYZ"), ())
self.assertIsNone(get_default_code(UNKNOWN_URI))
def test_default_code_follows_latest_version(self):
self.assertEqual(get_default_code(CANONICAL_URI), "XYZ")