mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-09 15:29:30 +00:00
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import re
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
@@ -78,8 +79,48 @@ class CodeList(Document):
|
|||||||
self.url = getattr(root.find(".//Identification/LocationUri"), "text", None)
|
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]:
|
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")
|
CommonCode = frappe.qb.DocType("Common Code")
|
||||||
DynamicLink = frappe.qb.DocType("Dynamic Link")
|
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]:
|
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")
|
CommonCode = frappe.qb.DocType("Common Code")
|
||||||
DynamicLink = frappe.qb.DocType("Dynamic Link")
|
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:
|
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")
|
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
|
return frappe.db.get_value("Common Code", code_id, "common_code") if code_id else None
|
||||||
|
|||||||
@@ -1,9 +1,83 @@
|
|||||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# See license.txt
|
# 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
|
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):
|
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")
|
||||||
|
|||||||
Reference in New Issue
Block a user