diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 969f4ad39f6..b8e5e965f14 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -11,6 +11,7 @@ import frappe.defaults from frappe import _, qb, throw from frappe.desk.reportview import build_match_conditions from frappe.model.meta import get_field_precision +from frappe.model.naming import determine_consecutive_week_number from frappe.query_builder import AliasedQuery, Case, Criterion, Field, Table from frappe.query_builder.functions import Count, IfNull, Max, Round, Sum from frappe.query_builder.utils import DocType @@ -25,6 +26,7 @@ from frappe.utils import ( get_number_format_info, getdate, now, + now_datetime, nowdate, ) from frappe.utils.caching import site_cache @@ -66,6 +68,7 @@ def get_fiscal_year( as_dict=False, boolean=None, raise_on_missing=True, + truncate=False, ): if isinstance(raise_on_missing, str): raise_on_missing = loads(raise_on_missing) @@ -79,7 +82,14 @@ def get_fiscal_year( fiscal_years = get_fiscal_years( date, fiscal_year, label, verbose, company, as_dict=as_dict, raise_on_missing=raise_on_missing ) - return False if not fiscal_years else fiscal_years[0] + + if fiscal_years: + fiscal_year = fiscal_years[0] + if truncate: + return ("-".join(y[-2:] for y in fiscal_year[0].split("-")), fiscal_year[1], fiscal_year[2]) + return fiscal_year + + return False def get_fiscal_years( @@ -1503,14 +1513,14 @@ def get_autoname_with_number(number_value, doc_title, company): def parse_naming_series_variable(doc, variable): - if variable == "FY": + if variable in ["FY", "TFY"]: if doc: date = doc.get("posting_date") or doc.get("transaction_date") or getdate() company = doc.get("company") else: date = getdate() company = None - return get_fiscal_year(date=date, company=company)[0] + return get_fiscal_year(date=date, company=company, truncate=variable == "TFY")[0] elif variable == "ABBR": if doc: @@ -1520,6 +1530,18 @@ def parse_naming_series_variable(doc, variable): return frappe.db.get_value("Company", company, "abbr") if company else "" + else: + data = {"YY": "%y", "YYYY": "%Y", "MM": "%m", "DD": "%d", "JJJ": "%j"} + date = ( + ( + getdate(doc.get("posting_date") or doc.get("transaction_date") or doc.get("posting_datetime")) + or now_datetime() + ) + if frappe.get_single_value("Global Defaults", "use_posting_datetime_for_naming_documents") + else now_datetime() + ) + return date.strftime(data[variable]) if variable in data else determine_consecutive_week_number(date) + @frappe.whitelist() def get_coa(doctype, parent, is_root=None, chart=None): diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 43aaec4bb96..553a7246fea 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -2478,3 +2478,21 @@ class TestAccountsController(IntegrationTestCase): self.assertRaises(frappe.ValidationError, po.save) po.items[0].delivered_by_supplier = 1 po.save() + + @IntegrationTestCase.change_settings("Global Defaults", {"use_posting_datetime_for_naming_documents": 1}) + def test_document_naming_rule_based_on_posting_date(self): + frappe.new_doc( + "Document Naming Rule", document_type="Sales Invoice", prefix="SI-.MM.-.YYYY.-" + ).submit() + + si = create_sales_invoice(do_not_save=True) + si.set_posting_time = 1 + si.posting_date = "2025-12-31" + si.save() + self.assertEqual(si.name, "SI-12-2025-00001") + + si = create_sales_invoice(do_not_save=True) + si.set_posting_time = 1 + si.posting_date = "2026-01-01" + si.save() + self.assertEqual(si.name, "SI-01-2026-00002") diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 55d9021e63d..8617cdb9fe3 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -402,9 +402,10 @@ doc_events = { } # function should expect the variable and doc as arguments +naming_series_variables_list = ["FY", "TFY", "ABBR", "MM", "DD", "YY", "YYYY", "JJJ", "WW"] naming_series_variables = { - "FY": "erpnext.accounts.utils.parse_naming_series_variable", - "ABBR": "erpnext.accounts.utils.parse_naming_series_variable", + variable: "erpnext.accounts.utils.parse_naming_series_variable" + for variable in naming_series_variables_list } # On cancel event Payment Entry will be exempted and all linked submittable doctype will get cancelled. diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.json b/erpnext/setup/doctype/global_defaults/global_defaults.json index cb43c97ae4c..92bff6d4fbe 100644 --- a/erpnext/setup/doctype/global_defaults/global_defaults.json +++ b/erpnext/setup/doctype/global_defaults/global_defaults.json @@ -13,6 +13,7 @@ "hide_currency_symbol", "disable_rounded_total", "disable_in_words", + "use_posting_datetime_for_naming_documents", "demo_company" ], "fields": [ @@ -80,6 +81,13 @@ "label": "Demo Company", "options": "Company", "read_only": 1 + }, + { + "default": "0", + "description": "When checked, the system will use the posting datetime of the document for naming the document instead of the creation datetime of the document.", + "fieldname": "use_posting_datetime_for_naming_documents", + "fieldtype": "Check", + "label": "Use Posting Datetime for Naming Documents" } ], "grid_page_length": 50, @@ -89,7 +97,7 @@ "in_create": 1, "issingle": 1, "links": [], - "modified": "2026-01-02 18:13:13.421866", + "modified": "2026-01-12 09:45:59.819161", "modified_by": "Administrator", "module": "Setup", "name": "Global Defaults", diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.py b/erpnext/setup/doctype/global_defaults/global_defaults.py index 4b7f642747e..99174c9a0d1 100644 --- a/erpnext/setup/doctype/global_defaults/global_defaults.py +++ b/erpnext/setup/doctype/global_defaults/global_defaults.py @@ -39,6 +39,7 @@ class GlobalDefaults(Document): disable_in_words: DF.Check disable_rounded_total: DF.Check hide_currency_symbol: DF.Literal["", "No", "Yes"] + use_posting_datetime_for_naming_documents: DF.Check # end: auto-generated types def on_update(self):