Merge pull request #38561 from blaggacao/feat/add-delivery-cutoff-date-on-so

feat(delivery): add cutoff item date for so delivery items
diff --git a/erpnext/public/js/bulk_transaction_processing.js b/erpnext/public/js/bulk_transaction_processing.js
index 0e42b47..3ae5a5d 100644
--- a/erpnext/public/js/bulk_transaction_processing.js
+++ b/erpnext/public/js/bulk_transaction_processing.js
@@ -1,7 +1,7 @@
 frappe.provide("erpnext.bulk_transaction_processing");
 
 $.extend(erpnext.bulk_transaction_processing, {
-	create: function(listview, from_doctype, to_doctype) {
+	create: function(listview, from_doctype, to_doctype, args) {
 		let checked_items = listview.get_checked_items();
 		const doc_name = [];
 		checked_items.forEach((Item)=> {
@@ -15,7 +15,7 @@
 			if (doc_name.length == 0) {
 				frappe.call({
 					method: "erpnext.utilities.bulk_transaction.transaction_processing",
-					args: {data: checked_items, from_doctype: from_doctype, to_doctype: to_doctype}
+					args: {data: checked_items, from_doctype: from_doctype, to_doctype: to_doctype, args: args}
 				}).then(()=> {
 
 				});
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index 2bb093d..1713a7b 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -855,6 +855,8 @@
 		var delivery_dates = this.frm.doc.items.map(i => i.delivery_date);
 		delivery_dates = [ ...new Set(delivery_dates) ];
 
+		var today = new Date();
+
 		var item_grid = this.frm.fields_dict["items"].grid;
 		if(!item_grid.get_selected().length && delivery_dates.length > 1) {
 			var dialog = new frappe.ui.Dialog({
@@ -873,7 +875,11 @@
 						<div class="list-item">
 							<div class="list-item__content list-item__content--flex-2">
 								<label>
-								<input type="checkbox" data-date="${date}" checked="checked"/>
+								<input
+									type="checkbox"
+									data-date="${date}"
+									${frappe.datetime.get_day_diff(new Date(date), today) > 0 ? "" : 'checked="checked"'}
+								/>
 								${frappe.datetime.str_to_user(date)}
 								</label>
 							</div>
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index cad1453..4956f29 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -937,6 +937,9 @@
 		if frappe.flags.args and frappe.flags.args.delivery_dates:
 			if cstr(doc.delivery_date) not in frappe.flags.args.delivery_dates:
 				return False
+		if frappe.flags.args and frappe.flags.args.until_delivery_date:
+			if cstr(doc.delivery_date) > frappe.flags.args.until_delivery_date:
+				return False
 
 		return abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier != 1
 
@@ -1012,6 +1015,11 @@
 				for idx, item in enumerate(target_doc.items):
 					item.idx = idx + 1
 
+	if not kwargs.skip_item_mapping and frappe.flags.bulk_transaction and not target_doc.items:
+		# the (date) condition filter resulted in an unintendedly created empty DN; remove it
+		del target_doc
+		return
+
 	# Should be called after mapping items.
 	set_missing_values(so, target_doc)
 
diff --git a/erpnext/selling/doctype/sales_order/sales_order_list.js b/erpnext/selling/doctype/sales_order/sales_order_list.js
index 37686a8..53de329 100644
--- a/erpnext/selling/doctype/sales_order/sales_order_list.js
+++ b/erpnext/selling/doctype/sales_order/sales_order_list.js
@@ -55,7 +55,24 @@
 		});
 
 		listview.page.add_action_item(__("Delivery Note"), ()=>{
-			erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Delivery Note");
+			frappe.db.get_single_value("Selling Settings", "enable_cutoff_date_on_bulk_delivery_note_creation").then((value) => {
+				if (value) {
+					var dialog = new frappe.ui.Dialog({
+						title: __("Select Items up to Delivery Date"),
+						fields: [{fieldtype: "Date", fieldname: "delivery_date", default: frappe.datetime.add_days(frappe.datetime.nowdate(), 1)}]
+					});
+					dialog.set_primary_action(__("Select"), function(values) {
+						var until_delivery_date = values.delivery_date;
+						erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Delivery Note", {
+							until_delivery_date
+						});
+						dialog.hide();
+					});
+					dialog.show();
+				} else {
+					erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Delivery Note");
+				}
+			})
 		});
 
 		listview.page.add_action_item(__("Advance Payment"), ()=>{
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json
index d6829ce..ee3d2bf 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.json
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.json
@@ -32,7 +32,8 @@
   "allow_sales_order_creation_for_expired_quotation",
   "dont_reserve_sales_order_qty_on_sales_return",
   "hide_tax_id",
-  "enable_discount_accounting"
+  "enable_discount_accounting",
+  "enable_cutoff_date_on_bulk_delivery_note_creation"
  ],
  "fields": [
   {
@@ -200,6 +201,12 @@
    "fieldname": "blanket_order_allowance",
    "fieldtype": "Float",
    "label": "Blanket Order Allowance (%)"
+  },
+  {
+   "default": "0",
+   "fieldname": "enable_cutoff_date_on_bulk_delivery_note_creation",
+   "fieldtype": "Check",
+   "label": "Enable Cut-Off Date on Bulk Delivery Note Creation"
   }
  ],
  "icon": "fa fa-cog",
@@ -207,7 +214,7 @@
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2023-10-25 14:03:03.966701",
+ "modified": "2024-03-01 12:07:39.994520",
  "modified_by": "Administrator",
  "module": "Selling",
  "name": "Selling Settings",
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py
index 24fe909..a488177 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.py
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.py
@@ -30,6 +30,7 @@
 		dont_reserve_sales_order_qty_on_sales_return: DF.Check
 		editable_bundle_item_rates: DF.Check
 		editable_price_list_rate: DF.Check
+		enable_cutoff_date_on_bulk_delivery_note_creation: DF.Check
 		enable_discount_accounting: DF.Check
 		hide_tax_id: DF.Check
 		maintain_same_rate_action: DF.Literal["Stop", "Warn"]
diff --git a/erpnext/utilities/bulk_transaction.py b/erpnext/utilities/bulk_transaction.py
index 9678488..3538c24 100644
--- a/erpnext/utilities/bulk_transaction.py
+++ b/erpnext/utilities/bulk_transaction.py
@@ -7,12 +7,15 @@
 
 
 @frappe.whitelist()
-def transaction_processing(data, from_doctype, to_doctype):
+def transaction_processing(data, from_doctype, to_doctype, args=None):
 	if isinstance(data, str):
 		deserialized_data = json.loads(data)
 	else:
 		deserialized_data = data
 
+	if isinstance(args, str):
+		args = frappe._dict(json.loads(args))
+
 	length_of_data = len(deserialized_data)
 
 	frappe.msgprint(
@@ -23,6 +26,7 @@
 		deserialized_data=deserialized_data,
 		from_doctype=from_doctype,
 		to_doctype=to_doctype,
+		args=args,
 	)
 
 
@@ -71,8 +75,13 @@
 		frappe.db.set_value("Bulk Transaction Log Detail", log_name, "error_description", err)
 
 
-def job(deserialized_data, from_doctype, to_doctype):
+def job(deserialized_data, from_doctype, to_doctype, args):
 	fail_count = 0
+
+	if args:
+		# currently: flag-based transport to `task`
+		frappe.flags.args = args
+
 	for d in deserialized_data:
 		try:
 			doc_name = d.get("name")
@@ -147,9 +156,12 @@
 	else:
 		obj = mapper[from_doctype][to_doctype](doc_name)
 
-	obj.flags.ignore_validate = True
-	obj.set_title_field()
-	obj.insert(ignore_mandatory=True)
+	if obj:
+		obj.flags.ignore_validate = True
+		obj.set_title_field()
+		obj.insert(ignore_mandatory=True)
+
+	del obj
 	del frappe.flags.bulk_transaction