Repository: frappe/frappe

Issue:
[Proposal] Add calandar week placeholder for series names
<!--
Welcome to the Frappe Framework issue tracker! Before creating an issue, please heed the following:

1. This tracker should only be used to report bugs and request features / enhancements to Frappe
    - For questions and general support, refer to https://stackoverflow.com/questions/tagged/frappe
    - For documentation issues, use https://frappeframework.com/docs/user/en or the developer cheetsheet https://frappeframework.com/docs/user/en/bench/resources/bench-commands-cheatsheet
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
   the original discussion.
3. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the     greater the drive to make it happen.
-->

**Is your feature request related to a problem? Please describe.**
Placeholders for generating series names are currently limited small number of date related properties including day, month, year, fiscal year and timestamp. In my company, many processes and documents are assigned to a week and accordingly we use the calendar week in numbering. This can currently not be implemented with Frappe/ERPNext.

**Describe the solution you'd like**
It would be useful to have a new placeholder based on the calendar week:
* placeholder `WW`
* calendar week based on ISO-8601, `strftime`'s `%V`
* use `00` for the first week (instead of `53`) if the week is part of the previous year (according to ISO-8601) for continuity per year, and conversely `53` instead of `01` for the last week if the week is part of the next year.

**Additional context**
I'm already testing a possible implementation and would be happy to create a PR if the proposal is accepted.


Discussion:

Resolving patch:
diff --git a/frappe/model/naming.py b/frappe/model/naming.py
--- a/frappe/model/naming.py
+++ b/frappe/model/naming.py
@@ -177,6 +177,8 @@ def parse_naming_series(parts, doctype='', doc=''):
 			part = today.strftime("%d")
 		elif e == 'YYYY':
 			part = today.strftime('%Y')
+		elif e == 'WW':
+			part = determine_consecutive_week_number(today)
 		elif e == 'timestamp':
 			part = str(today)
 		elif e == 'FY':
@@ -195,6 +197,19 @@ def parse_naming_series(parts, doctype='', doc=''):
 	return n
 
 
+def determine_consecutive_week_number(datetime):
+	"""Determines the consecutive calendar week"""
+	m = datetime.month
+	# ISO 8601 calandar week
+	w = datetime.strftime('%V')
+	# Ensure consecutiveness for the first and last days of a year
+	if m == 1 and int(w) >= 52:
+		w = '00'
+	elif m == 12 and int(w) <= 1:
+		w = '53'
+	return w
+
+
 def getseries(key, digits):
 	# series created ?
 	current = frappe.db.sql("SELECT `current` FROM `tabSeries` WHERE `name`=%s FOR UPDATE", (key,))


Test patch:
diff --git a/frappe/tests/test_naming.py b/frappe/tests/test_naming.py
--- a/frappe/tests/test_naming.py
+++ b/frappe/tests/test_naming.py
@@ -9,6 +9,7 @@
 
 from frappe.model.naming import getseries
 from frappe.model.naming import append_number_if_name_exists, revert_series_if_last
+from frappe.model.naming import determine_consecutive_week_number, parse_naming_series
 
 class TestNaming(unittest.TestCase):
 	def tearDown(self):
@@ -62,6 +63,34 @@ def test_format_autoname(self):
 		self.assertEqual(todo.name, 'TODO-{month}-{status}-{series}'.format(
 			month=now_datetime().strftime('%m'), status=todo.status, series=series))
 
+	def test_format_autoname_for_consecutive_week_number(self):
+		'''
+		Test if braced params are replaced for consecutive week number in format autoname
+		'''
+		doctype = 'ToDo'
+
+		todo_doctype = frappe.get_doc('DocType', doctype)
+		todo_doctype.autoname = 'format:TODO-{WW}-{##}'
+		todo_doctype.save()
+
+		description = 'Format'
+
+		todo = frappe.new_doc(doctype)
+		todo.description = description
+		todo.insert()
+
+		series = getseries('', 2)
+
+		series = str(int(series)-1)
+
+		if len(series) < 2:
+			series = '0' + series
+
+		week = determine_consecutive_week_number(now_datetime())
+
+		self.assertEqual(todo.name, 'TODO-{week}-{series}'.format(
+			week=week, series=series))
+
 	def test_revert_series(self):
 		from datetime import datetime
 		year = datetime.now().year
@@ -132,3 +161,32 @@ def test_naming_for_cancelled_and_amended_doc(self):
 		self.assertEqual(amended_doc.name, "{}-CANC-1".format(original_name))
 
 		submittable_doctype.delete()
+
+	def test_parse_naming_series_for_consecutive_week_number(self):
+		week = determine_consecutive_week_number(now_datetime())
+		name = parse_naming_series('PREFIX-.WW.-SUFFIX')
+		expected_name = 'PREFIX-{}-SUFFIX'.format(week)
+		self.assertEqual(name, expected_name)
+
+	def test_determine_consecutive_week_number(self):
+		from datetime import datetime
+
+		dt = datetime.fromisoformat("2019-12-31")
+		w = determine_consecutive_week_number(dt)
+		self.assertEqual(w, "53")
+
+		dt = datetime.fromisoformat("2020-01-01")
+		w = determine_consecutive_week_number(dt)
+		self.assertEqual(w, "01")
+
+		dt = datetime.fromisoformat("2020-01-15")
+		w = determine_consecutive_week_number(dt)
+		self.assertEqual(w, "03")
+
+		dt = datetime.fromisoformat("2021-01-01")
+		w = determine_consecutive_week_number(dt)
+		self.assertEqual(w, "00")
+
+		dt = datetime.fromisoformat("2021-12-31")
+		w = determine_consecutive_week_number(dt)
+		self.assertEqual(w, "52")