feat: NamingSeries class
Single class to group together everything required related to naming series
This commit is contained in:
parent
6930822a20
commit
5590cb0be8
4 changed files with 98 additions and 61 deletions
|
|
@ -1,13 +1,14 @@
|
|||
# Copyright (c) 2022, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import re
|
||||
from typing import List
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.core.doctype.doctype.doctype import validate_series
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.naming import get_naming_series_prefix, make_autoname
|
||||
from frappe.model.naming import NamingSeries, make_autoname
|
||||
from frappe.permissions import get_doctypes_with_read
|
||||
from frappe.utils import cint
|
||||
|
||||
|
|
@ -80,8 +81,8 @@ class DocumentNamingSettings(Document):
|
|||
options = self.get_options_list(options)
|
||||
|
||||
# validate names
|
||||
for i in options:
|
||||
self.validate_series_name(i)
|
||||
for series in options:
|
||||
self.validate_series_name(series)
|
||||
|
||||
if options and self.user_must_always_select:
|
||||
options = [""] + options
|
||||
|
|
@ -125,13 +126,8 @@ class DocumentNamingSettings(Document):
|
|||
frappe.throw(_("Series {0} already used in {1}").format(series, existing_series[series]))
|
||||
validate_series(dt, series)
|
||||
|
||||
def validate_series_name(self, n):
|
||||
import re
|
||||
|
||||
if not re.match(r"^[\w\- \/.#{}]+$", n, re.UNICODE):
|
||||
frappe.throw(
|
||||
_('Special Characters except "-", "#", ".", "/", "{" and "}" not allowed in naming series')
|
||||
)
|
||||
def validate_series_name(self, series):
|
||||
NamingSeries(series).validate()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_options(self, doctype=None):
|
||||
|
|
@ -146,7 +142,7 @@ class DocumentNamingSettings(Document):
|
|||
def get_current(self):
|
||||
"""get series current"""
|
||||
if self.prefix:
|
||||
prefix = get_naming_series_prefix(self.prefix)
|
||||
prefix = NamingSeries(self.prefix).get_prefix()
|
||||
self.current_value = frappe.db.get_value("Series", prefix, "current", order_by="name")
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
@ -156,7 +152,7 @@ class DocumentNamingSettings(Document):
|
|||
|
||||
series = frappe.qb.DocType("Series")
|
||||
|
||||
db_prefix = get_naming_series_prefix(self.prefix)
|
||||
db_prefix = NamingSeries(self.prefix).get_prefix()
|
||||
|
||||
if frappe.db.get_value("Series", db_prefix, "name", order_by="name") is None:
|
||||
series.insert(db_prefix, 0).columns(series.name, series.current).run()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import frappe
|
||||
from frappe.core.doctype.document_naming_settings.document_naming_settings import (
|
||||
NAMING_SERIES_PATTERN,
|
||||
DocumentNamingSettings,
|
||||
)
|
||||
from frappe.model.naming import get_default_naming_series
|
||||
|
|
@ -11,24 +12,24 @@ from frappe.tests.utils import FrappeTestCase
|
|||
|
||||
class TestNamingSeries(FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.ns: DocumentNamingSettings = frappe.get_doc("Document Naming Settings")
|
||||
self.dns: DocumentNamingSettings = frappe.get_doc("Document Naming Settings")
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_naming_preview(self):
|
||||
self.ns.transaction_type = "Webhook"
|
||||
self.dns.transaction_type = "Webhook"
|
||||
|
||||
self.ns.try_naming_series = "AXBZ.####"
|
||||
serieses = self.ns.preview_series().split("\n")
|
||||
self.dns.try_naming_series = "AXBZ.####"
|
||||
serieses = self.dns.preview_series().split("\n")
|
||||
self.assertEqual(["AXBZ0001", "AXBZ0002", "AXBZ0003"], serieses)
|
||||
|
||||
self.ns.try_naming_series = "AXBZ-.{currency}.-"
|
||||
serieses = self.ns.preview_series().split("\n")
|
||||
self.dns.try_naming_series = "AXBZ-.{currency}.-"
|
||||
serieses = self.dns.preview_series().split("\n")
|
||||
|
||||
def test_get_transactions(self):
|
||||
|
||||
naming_info = self.ns.get_transactions_and_prefixes()
|
||||
naming_info = self.dns.get_transactions_and_prefixes()
|
||||
self.assertIn("Webhook", naming_info["transactions"])
|
||||
|
||||
existing_naming_series = frappe.get_meta("Webhook").get_field("naming_series").options
|
||||
|
|
|
|||
|
|
@ -19,6 +19,67 @@ if TYPE_CHECKING:
|
|||
# whether `log_types` have autoincremented naming set for the site or not.
|
||||
autoincremented_site_status_map = {}
|
||||
|
||||
NAMING_SERIES_PATTERN = re.compile(r"^[\w\- \/.#{}]+$", re.UNICODE)
|
||||
|
||||
|
||||
class InvalidNamingSeriesError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class NamingSeries:
|
||||
__slots__ = ("series",)
|
||||
|
||||
def __init__(self, series: str):
|
||||
self.series = series
|
||||
|
||||
# Add default number part if missing
|
||||
if "#" not in self.series:
|
||||
self.series += ".#####"
|
||||
|
||||
def validate(self):
|
||||
if "." not in self.series:
|
||||
frappe.throw(
|
||||
_("Invalid naming series {}: dot (.) missing").format(frappe.bold(self.series)),
|
||||
exc=InvalidNamingSeriesError,
|
||||
)
|
||||
|
||||
if not NAMING_SERIES_PATTERN.match(self.series):
|
||||
frappe.throw(
|
||||
_(
|
||||
'Special Characters except "-", "#", ".", "/", "{" and "}" not allowed in naming series',
|
||||
),
|
||||
exc=InvalidNamingSeriesError,
|
||||
)
|
||||
|
||||
def generate_next_name(self, doc: "Document") -> str:
|
||||
self.validate()
|
||||
parts = self.series.split(".")
|
||||
return parse_naming_series(parts, doc)
|
||||
|
||||
def get_prefix(self) -> str:
|
||||
"""Naming series stores prefix to maintain a counter in DB. This prefix can be used to update counter or validations.
|
||||
|
||||
e.g. `SINV-.YY.-.####` has prefix of `SINV-22-` in database for year 2022.
|
||||
"""
|
||||
|
||||
prefix = None
|
||||
|
||||
def fake_counter_backend(partial_series, digits):
|
||||
nonlocal prefix
|
||||
prefix = partial_series
|
||||
return "#" * digits
|
||||
|
||||
# This function evaluates all parts till we hit numerical parts and then
|
||||
# sends prefix + digits to DB to find next number.
|
||||
# Instead of reimplementing the whole parsing logic in multiple places we
|
||||
# can just ask this function to give us the prefix.
|
||||
parse_naming_series(self.series, number_generator=fake_counter_backend)
|
||||
|
||||
if prefix is None:
|
||||
frappe.throw(_("Invalid Naming Series"))
|
||||
|
||||
return prefix
|
||||
|
||||
|
||||
def set_new_name(doc):
|
||||
"""
|
||||
|
|
@ -176,18 +237,8 @@ def make_autoname(key="", doctype="", doc=""):
|
|||
if key == "hash":
|
||||
return frappe.generate_hash(doctype, 10)
|
||||
|
||||
if "#" not in key:
|
||||
key = key + ".#####"
|
||||
elif "." not in key:
|
||||
error_message = _("Invalid naming series (. missing)")
|
||||
if doctype:
|
||||
error_message = _("Invalid naming series (. missing) for {0}").format(doctype)
|
||||
|
||||
frappe.throw(error_message)
|
||||
|
||||
parts = key.split(".")
|
||||
n = parse_naming_series(parts, doctype, doc)
|
||||
return n
|
||||
series = NamingSeries(key)
|
||||
return series.generate_next_name(doc)
|
||||
|
||||
|
||||
def parse_naming_series(
|
||||
|
|
@ -249,34 +300,6 @@ def parse_naming_series(
|
|||
return name
|
||||
|
||||
|
||||
def get_naming_series_prefix(series: str) -> str:
|
||||
"""Naming series stores prefix to maintain a counter in DB. This prefix can be used to update counter and/or other validations.
|
||||
|
||||
e.g. `SINV-.YY.-.####` has prefix of `SINV-22-` in database for year 2022.
|
||||
"""
|
||||
|
||||
prefix = None
|
||||
|
||||
if "#" not in series:
|
||||
series += ".#####"
|
||||
|
||||
def fake_counter_backend(partial_series, digits):
|
||||
nonlocal prefix
|
||||
prefix = partial_series
|
||||
return "#" * digits
|
||||
|
||||
# This function evaluates all parts till we hit numerical parts and then
|
||||
# sends prefix + digits to DB to find next number.
|
||||
# Instead of reimplemnted the whole parsing logic in multiple places we can
|
||||
# just ask this function to give us the prefix.
|
||||
parse_naming_series(series, number_generator=fake_counter_backend)
|
||||
|
||||
if prefix is None:
|
||||
frappe.throw(_("Invalid Naming Series"))
|
||||
|
||||
return prefix
|
||||
|
||||
|
||||
def determine_consecutive_week_number(datetime):
|
||||
"""Determines the consecutive calendar week"""
|
||||
m = datetime.month
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@
|
|||
import frappe
|
||||
from frappe.core.doctype.doctype.test_doctype import new_doctype
|
||||
from frappe.model.naming import (
|
||||
InvalidNamingSeriesError,
|
||||
NamingSeries,
|
||||
append_number_if_name_exists,
|
||||
determine_consecutive_week_number,
|
||||
get_naming_series_prefix,
|
||||
getseries,
|
||||
revert_series_if_last,
|
||||
)
|
||||
|
|
@ -300,7 +301,23 @@ class TestNaming(FrappeTestCase):
|
|||
}
|
||||
|
||||
for series, prefix in prefix_test_cases.items():
|
||||
self.assertEqual(prefix, get_naming_series_prefix(series))
|
||||
self.assertEqual(prefix, NamingSeries(series).get_prefix())
|
||||
|
||||
def test_naming_series_validation(self):
|
||||
dns = frappe.get_doc("Document Naming Settings")
|
||||
exisiting_series = dns.get_transactions_and_prefixes()["prefixes"]
|
||||
valid = ["SINV-", "SI-.{field}.", "SI-#.###", ""] + exisiting_series
|
||||
invalid = ["$INV-", r"WINDOWS\NAMING"]
|
||||
|
||||
for series in valid:
|
||||
if series.strip():
|
||||
try:
|
||||
NamingSeries(series).validate()
|
||||
except Exception as e:
|
||||
self.fail(f"{series} should be valid\n{e}")
|
||||
|
||||
for series in invalid:
|
||||
self.assertRaises(InvalidNamingSeriesError, NamingSeries(series).validate)
|
||||
|
||||
|
||||
def make_invalid_todo():
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue