You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

562 lines
16 KiB

  1. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. # License: MIT. See LICENSE
  3. import datetime
  4. import re
  5. from typing import TYPE_CHECKING, Callable, Optional
  6. import frappe
  7. from frappe import _
  8. from frappe.model import log_types
  9. from frappe.query_builder import DocType
  10. from frappe.utils import cint, cstr, now_datetime
  11. if TYPE_CHECKING:
  12. from frappe.model.document import Document
  13. from frappe.model.meta import Meta
  14. # NOTE: This is used to keep track of status of sites
  15. # whether `log_types` have autoincremented naming set for the site or not.
  16. autoincremented_site_status_map = {}
  17. NAMING_SERIES_PATTERN = re.compile(r"^[\w\- \/.#{}]+$", re.UNICODE)
  18. BRACED_PARAMS_PATTERN = re.compile(r"(\{[\w | #]+\})")
  19. # Types that can be using in naming series fields
  20. NAMING_SERIES_PART_TYPES = (
  21. int,
  22. str,
  23. datetime.datetime,
  24. datetime.date,
  25. datetime.time,
  26. datetime.timedelta,
  27. )
  28. class InvalidNamingSeriesError(frappe.ValidationError):
  29. pass
  30. class NamingSeries:
  31. __slots__ = ("series",)
  32. def __init__(self, series: str):
  33. self.series = series
  34. # Add default number part if missing
  35. if "#" not in self.series:
  36. self.series += ".#####"
  37. def validate(self):
  38. if "." not in self.series:
  39. frappe.throw(
  40. _("Invalid naming series {}: dot (.) missing").format(frappe.bold(self.series)),
  41. exc=InvalidNamingSeriesError,
  42. )
  43. if not NAMING_SERIES_PATTERN.match(self.series):
  44. frappe.throw(
  45. _(
  46. 'Special Characters except "-", "#", ".", "/", "{" and "}" not allowed in naming series',
  47. ),
  48. exc=InvalidNamingSeriesError,
  49. )
  50. def generate_next_name(self, doc: "Document") -> str:
  51. self.validate()
  52. parts = self.series.split(".")
  53. return parse_naming_series(parts, doc=doc)
  54. def get_prefix(self) -> str:
  55. """Naming series stores prefix to maintain a counter in DB. This prefix can be used to update counter or validations.
  56. e.g. `SINV-.YY.-.####` has prefix of `SINV-22-` in database for year 2022.
  57. """
  58. prefix = None
  59. def fake_counter_backend(partial_series, digits):
  60. nonlocal prefix
  61. prefix = partial_series
  62. return "#" * digits
  63. # This function evaluates all parts till we hit numerical parts and then
  64. # sends prefix + digits to DB to find next number.
  65. # Instead of reimplementing the whole parsing logic in multiple places we
  66. # can just ask this function to give us the prefix.
  67. parse_naming_series(self.series, number_generator=fake_counter_backend)
  68. if prefix is None:
  69. frappe.throw(_("Invalid Naming Series: {}").format(self.series))
  70. return prefix
  71. def get_preview(self, doc=None) -> list[str]:
  72. """Generate preview of naming series without using DB counters"""
  73. generated_names = []
  74. for count in range(1, 4):
  75. def fake_counter(_prefix, digits):
  76. # ignore B023: binding `count` is not necessary because
  77. # function is evaluated immediately and it can not be done
  78. # because of function signature requirement
  79. return str(count).zfill(digits) # noqa: B023
  80. generated_names.append(parse_naming_series(self.series, doc=doc, number_generator=fake_counter))
  81. return generated_names
  82. def update_counter(self, new_count: int) -> None:
  83. """Warning: Incorrectly updating series can result in unusable transactions"""
  84. Series = frappe.qb.DocType("Series")
  85. prefix = self.get_prefix()
  86. # Initialize if not present in DB
  87. if frappe.db.get_value("Series", prefix, "name", order_by="name") is None:
  88. frappe.qb.into(Series).insert(prefix, 0).columns("name", "current").run()
  89. (
  90. frappe.qb.update(Series).set(Series.current, cint(new_count)).where(Series.name == prefix)
  91. ).run()
  92. def get_current_value(self) -> int:
  93. prefix = self.get_prefix()
  94. return cint(frappe.db.get_value("Series", prefix, "current", order_by="name"))
  95. def set_new_name(doc):
  96. """
  97. Sets the `name` property for the document based on various rules.
  98. 1. If amended doc, set suffix.
  99. 2. If `autoname` method is declared, then call it.
  100. 3. If `autoname` property is set in the DocType (`meta`), then build it using the `autoname` property.
  101. 4. If no rule defined, use hash.
  102. :param doc: Document to be named.
  103. """
  104. doc.run_method("before_naming")
  105. meta = frappe.get_meta(doc.doctype)
  106. autoname = meta.autoname or ""
  107. if autoname.lower() != "prompt" and not frappe.flags.in_import:
  108. doc.name = None
  109. if is_autoincremented(doc.doctype, meta):
  110. doc.name = frappe.db.get_next_sequence_val(doc.doctype)
  111. return
  112. if getattr(doc, "amended_from", None):
  113. _set_amended_name(doc)
  114. return
  115. elif getattr(doc.meta, "issingle", False):
  116. doc.name = doc.doctype
  117. if not doc.name:
  118. set_naming_from_document_naming_rule(doc)
  119. if not doc.name:
  120. doc.run_method("autoname")
  121. if not doc.name and autoname:
  122. set_name_from_naming_options(autoname, doc)
  123. # if the autoname option is 'field:' and no name was derived, we need to
  124. # notify
  125. if not doc.name and autoname.startswith("field:"):
  126. fieldname = autoname[6:]
  127. frappe.throw(_("{0} is required").format(doc.meta.get_label(fieldname)))
  128. # at this point, we fall back to name generation with the hash option
  129. if not doc.name and autoname == "hash":
  130. doc.name = make_autoname("hash", doc.doctype)
  131. if not doc.name:
  132. doc.name = make_autoname("hash", doc.doctype)
  133. doc.name = validate_name(doc.doctype, doc.name, meta.get_field("name_case"))
  134. def is_autoincremented(doctype: str, meta: Optional["Meta"] = None) -> bool:
  135. """Checks if the doctype has autoincrement autoname set"""
  136. if doctype in log_types:
  137. if autoincremented_site_status_map.get(frappe.local.site) is None:
  138. if (
  139. frappe.db.sql(
  140. f"""select data_type FROM information_schema.columns
  141. where column_name = 'name' and table_name = 'tab{doctype}'"""
  142. )[0][0]
  143. == "bigint"
  144. ):
  145. autoincremented_site_status_map[frappe.local.site] = 1
  146. return True
  147. else:
  148. autoincremented_site_status_map[frappe.local.site] = 0
  149. elif autoincremented_site_status_map[frappe.local.site]:
  150. return True
  151. else:
  152. if not meta:
  153. meta = frappe.get_meta(doctype)
  154. if not getattr(meta, "issingle", False) and meta.autoname == "autoincrement":
  155. return True
  156. return False
  157. def set_name_from_naming_options(autoname, doc):
  158. """
  159. Get a name based on the autoname field option
  160. """
  161. _autoname = autoname.lower()
  162. if _autoname.startswith("field:"):
  163. doc.name = _field_autoname(autoname, doc)
  164. elif _autoname.startswith("naming_series:"):
  165. set_name_by_naming_series(doc)
  166. elif _autoname.startswith("prompt"):
  167. _prompt_autoname(autoname, doc)
  168. elif _autoname.startswith("format:"):
  169. doc.name = _format_autoname(autoname, doc)
  170. elif "#" in autoname:
  171. doc.name = make_autoname(autoname, doc=doc)
  172. def set_naming_from_document_naming_rule(doc):
  173. """
  174. Evaluate rules based on "Document Naming Series" doctype
  175. """
  176. if doc.doctype in log_types:
  177. return
  178. # ignore_ddl if naming is not yet bootstrapped
  179. for d in frappe.get_all(
  180. "Document Naming Rule",
  181. dict(document_type=doc.doctype, disabled=0),
  182. order_by="priority desc",
  183. ignore_ddl=True,
  184. ):
  185. frappe.get_cached_doc("Document Naming Rule", d.name).apply(doc)
  186. if doc.name:
  187. break
  188. def set_name_by_naming_series(doc):
  189. """Sets name by the `naming_series` property"""
  190. if not doc.naming_series:
  191. doc.naming_series = get_default_naming_series(doc.doctype)
  192. if not doc.naming_series:
  193. frappe.throw(frappe._("Naming Series mandatory"))
  194. doc.name = make_autoname(doc.naming_series + ".#####", "", doc)
  195. def make_autoname(key="", doctype="", doc=""):
  196. """
  197. Creates an autoname from the given key:
  198. **Autoname rules:**
  199. * The key is separated by '.'
  200. * '####' represents a series. The string before this part becomes the prefix:
  201. Example: ABC.#### creates a series ABC0001, ABC0002 etc
  202. * 'MM' represents the current month
  203. * 'YY' and 'YYYY' represent the current year
  204. *Example:*
  205. * DE/./.YY./.MM./.##### will create a series like
  206. DE/09/01/0001 where 09 is the year, 01 is the month and 0001 is the series
  207. """
  208. if key == "hash":
  209. return frappe.generate_hash(doctype, 10)
  210. series = NamingSeries(key)
  211. return series.generate_next_name(doc)
  212. def parse_naming_series(
  213. parts: list[str] | str,
  214. doctype=None,
  215. doc: Optional["Document"] = None,
  216. number_generator: Callable[[str, int], str] | None = None,
  217. ) -> str:
  218. """Parse the naming series and get next name.
  219. args:
  220. parts: naming series parts (split by `.`)
  221. doc: document to use for series that have parts using fieldnames
  222. number_generator: Use different counter backend other than `tabSeries`. Primarily used for testing.
  223. """
  224. name = ""
  225. if isinstance(parts, str):
  226. parts = parts.split(".")
  227. if not number_generator:
  228. number_generator = getseries
  229. series_set = False
  230. today = now_datetime()
  231. for e in parts:
  232. if not e:
  233. continue
  234. part = ""
  235. if e.startswith("#"):
  236. if not series_set:
  237. digits = len(e)
  238. part = number_generator(name, digits)
  239. series_set = True
  240. elif e == "YY":
  241. part = today.strftime("%y")
  242. elif e == "MM":
  243. part = today.strftime("%m")
  244. elif e == "DD":
  245. part = today.strftime("%d")
  246. elif e == "YYYY":
  247. part = today.strftime("%Y")
  248. elif e == "WW":
  249. part = determine_consecutive_week_number(today)
  250. elif e == "timestamp":
  251. part = str(today)
  252. elif e == "FY":
  253. part = frappe.defaults.get_user_default("fiscal_year")
  254. elif e.startswith("{") and doc:
  255. e = e.replace("{", "").replace("}", "")
  256. part = doc.get(e)
  257. elif doc and doc.get(e):
  258. part = doc.get(e)
  259. else:
  260. part = e
  261. if isinstance(part, str):
  262. name += part
  263. elif isinstance(part, NAMING_SERIES_PART_TYPES):
  264. name += cstr(part).strip()
  265. return name
  266. def determine_consecutive_week_number(datetime):
  267. """Determines the consecutive calendar week"""
  268. m = datetime.month
  269. # ISO 8601 calandar week
  270. w = datetime.strftime("%V")
  271. # Ensure consecutiveness for the first and last days of a year
  272. if m == 1 and int(w) >= 52:
  273. w = "00"
  274. elif m == 12 and int(w) <= 1:
  275. w = "53"
  276. return w
  277. def getseries(key, digits):
  278. # series created ?
  279. # Using frappe.qb as frappe.get_values does not allow order_by=None
  280. series = DocType("Series")
  281. current = (frappe.qb.from_(series).where(series.name == key).for_update().select("current")).run()
  282. if current and current[0][0] is not None:
  283. current = current[0][0]
  284. # yes, update it
  285. frappe.db.sql("UPDATE `tabSeries` SET `current` = `current` + 1 WHERE `name`=%s", (key,))
  286. current = cint(current) + 1
  287. else:
  288. # no, create it
  289. frappe.db.sql("INSERT INTO `tabSeries` (`name`, `current`) VALUES (%s, 1)", (key,))
  290. current = 1
  291. return ("%0" + str(digits) + "d") % current
  292. def revert_series_if_last(key, name, doc=None):
  293. """
  294. Reverts the series for particular naming series:
  295. * key is naming series - SINV-.YYYY-.####
  296. * name is actual name - SINV-2021-0001
  297. 1. This function split the key into two parts prefix (SINV-YYYY) & hashes (####).
  298. 2. Use prefix to get the current index of that naming series from Series table
  299. 3. Then revert the current index.
  300. *For custom naming series:*
  301. 1. hash can exist anywhere, if it exist in hashes then it take normal flow.
  302. 2. If hash doesn't exit in hashes, we get the hash from prefix, then update name and prefix accordingly.
  303. *Example:*
  304. 1. key = SINV-.YYYY.-
  305. * If key doesn't have hash it will add hash at the end
  306. * prefix will be SINV-YYYY based on this will get current index from Series table.
  307. 2. key = SINV-.####.-2021
  308. * now prefix = SINV-#### and hashes = 2021 (hash doesn't exist)
  309. * will search hash in key then accordingly get prefix = SINV-
  310. 3. key = ####.-2021
  311. * prefix = #### and hashes = 2021 (hash doesn't exist)
  312. * will search hash in key then accordingly get prefix = ""
  313. """
  314. if ".#" in key:
  315. prefix, hashes = key.rsplit(".", 1)
  316. if "#" not in hashes:
  317. # get the hash part from the key
  318. hash = re.search("#+", key)
  319. if not hash:
  320. return
  321. name = name.replace(hashes, "")
  322. prefix = prefix.replace(hash.group(), "")
  323. else:
  324. prefix = key
  325. if "." in prefix:
  326. prefix = parse_naming_series(prefix.split("."), doc=doc)
  327. count = cint(name.replace(prefix, ""))
  328. series = DocType("Series")
  329. current = (
  330. frappe.qb.from_(series).where(series.name == prefix).for_update().select("current")
  331. ).run()
  332. if current and current[0][0] == count:
  333. frappe.db.sql("UPDATE `tabSeries` SET `current` = `current` - 1 WHERE `name`=%s", prefix)
  334. def get_default_naming_series(doctype: str) -> str | None:
  335. """get default value for `naming_series` property"""
  336. naming_series_options = frappe.get_meta(doctype).get_naming_series_options()
  337. # Return first truthy options
  338. # Empty strings are used to avoid populating forms by default
  339. for option in naming_series_options:
  340. if option:
  341. return option
  342. def validate_name(doctype: str, name: int | str, case: str | None = None):
  343. if not name:
  344. frappe.throw(_("No Name Specified for {0}").format(doctype))
  345. if isinstance(name, int):
  346. if is_autoincremented(doctype):
  347. # this will set the sequence value to be the provided name/value and set it to be used
  348. # so that the sequence will start from the next value
  349. frappe.db.set_next_sequence_val(doctype, name, is_val_used=True)
  350. return name
  351. frappe.throw(_("Invalid name type (integer) for varchar name column"), frappe.NameError)
  352. if name.startswith("New " + doctype):
  353. frappe.throw(
  354. _("There were some errors setting the name, please contact the administrator"), frappe.NameError
  355. )
  356. if case == "Title Case":
  357. name = name.title()
  358. if case == "UPPER CASE":
  359. name = name.upper()
  360. name = name.strip()
  361. if not frappe.get_meta(doctype).get("issingle") and (doctype == name) and (name != "DocType"):
  362. frappe.throw(_("Name of {0} cannot be {1}").format(doctype, name), frappe.NameError)
  363. special_characters = "<>"
  364. if re.findall(f"[{special_characters}]+", name):
  365. message = ", ".join(f"'{c}'" for c in special_characters)
  366. frappe.throw(
  367. _("Name cannot contain special characters like {0}").format(message), frappe.NameError
  368. )
  369. return name
  370. def append_number_if_name_exists(doctype, value, fieldname="name", separator="-", filters=None):
  371. if not filters:
  372. filters = dict()
  373. filters.update({fieldname: value})
  374. exists = frappe.db.exists(doctype, filters)
  375. regex = f"^{re.escape(value)}{separator}\\d+$"
  376. if exists:
  377. last = frappe.db.sql(
  378. """SELECT `{fieldname}` FROM `tab{doctype}`
  379. WHERE `{fieldname}` {regex_character} %s
  380. ORDER BY length({fieldname}) DESC,
  381. `{fieldname}` DESC LIMIT 1""".format(
  382. doctype=doctype, fieldname=fieldname, regex_character=frappe.db.REGEX_CHARACTER
  383. ),
  384. regex,
  385. )
  386. if last:
  387. count = str(cint(last[0][0].rsplit(separator, 1)[1]) + 1)
  388. else:
  389. count = "1"
  390. value = f"{value}{separator}{count}"
  391. return value
  392. def _set_amended_name(doc):
  393. am_id = 1
  394. am_prefix = doc.amended_from
  395. if frappe.db.get_value(doc.doctype, doc.amended_from, "amended_from"):
  396. am_id = cint(doc.amended_from.split("-")[-1]) + 1
  397. am_prefix = "-".join(doc.amended_from.split("-")[:-1]) # except the last hyphen
  398. doc.name = am_prefix + "-" + str(am_id)
  399. return doc.name
  400. def _field_autoname(autoname, doc, skip_slicing=None):
  401. """
  402. Generate a name using `DocType` field. This is called when the doctype's
  403. `autoname` field starts with 'field:'
  404. """
  405. fieldname = autoname if skip_slicing else autoname[6:]
  406. name = (cstr(doc.get(fieldname)) or "").strip()
  407. return name
  408. def _prompt_autoname(autoname, doc):
  409. """
  410. Generate a name using Prompt option. This simply means the user will have to set the name manually.
  411. This is called when the doctype's `autoname` field starts with 'prompt'.
  412. """
  413. # set from __newname in save.py
  414. if not doc.name:
  415. frappe.throw(_("Please set the document name"))
  416. def _format_autoname(autoname, doc):
  417. """
  418. Generate autoname by replacing all instances of braced params (fields, date params ('DD', 'MM', 'YY'), series)
  419. Independent of remaining string or separators.
  420. Example pattern: 'format:LOG-{MM}-{fieldname1}-{fieldname2}-{#####}'
  421. """
  422. first_colon_index = autoname.find(":")
  423. autoname_value = autoname[first_colon_index + 1 :]
  424. def get_param_value_for_match(match):
  425. param = match.group()
  426. # trim braces
  427. trimmed_param = param[1:-1]
  428. return parse_naming_series([trimmed_param], doc=doc)
  429. # Replace braced params with their parsed value
  430. name = BRACED_PARAMS_PATTERN.sub(get_param_value_for_match, autoname_value)
  431. return name