Merge pull request #39386 from nabinhait/asset-capitalisation-calcellation

fix: Cancel asset capitalisation record on cancellation of asset and vice-versa
diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py
index 37326fd..a46642a 100644
--- a/erpnext/accounts/doctype/subscription/test_subscription.py
+++ b/erpnext/accounts/doctype/subscription/test_subscription.py
@@ -460,11 +460,13 @@
 		self.assertEqual(len(subscription.invoices), 1)
 
 	def test_multi_currency_subscription(self):
+		party = "_Test Subscription Customer"
+		frappe.db.set_value("Customer", party, "default_currency", "USD")
 		subscription = create_subscription(
 			start_date="2018-01-01",
 			generate_invoice_at="Beginning of the current subscription period",
 			plans=[{"plan": "_Test Plan Multicurrency", "qty": 1, "currency": "USD"}],
-			party="_Test Subscription Customer",
+			party=party,
 		)
 
 		subscription.process()
diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js
index 9c356bf..d6a4755 100644
--- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js
+++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js
@@ -84,10 +84,6 @@
 			options: budget_against_options,
 			default: "Cost Center",
 			reqd: 1,
-			get_data: function() {
-				console.log(this.options);
-				return ["Emacs", "Rocks"];
-			},
 			on_change: function() {
 				frappe.query_report.set_filter_value("budget_against_filter", []);
 				frappe.query_report.refresh();
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 6636b8e..f5b034a 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -449,6 +449,10 @@
 	for gle in gl_entries:
 		group_by_value = gle.get(group_by)
 		gle.voucher_type = _(gle.voucher_type)
+		gle.voucher_subtype = _(gle.voucher_subtype)
+		gle.against_voucher_type = _(gle.against_voucher_type)
+		gle.remarks = _(gle.remarks)
+		gle.party_type = _(gle.party_type)
 
 		if gle.posting_date < from_date or (cstr(gle.is_opening) == "Yes" and not show_opening_entries):
 			if not group_by_voucher_consolidated:
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
index 74d2aa7..ffb50eb 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
@@ -7,6 +7,7 @@
 from frappe.utils import (
 	add_days,
 	add_months,
+	add_years,
 	cint,
 	date_diff,
 	flt,
@@ -18,6 +19,7 @@
 )
 
 import erpnext
+from erpnext.accounts.utils import get_fiscal_year
 
 
 class AssetDepreciationSchedule(Document):
@@ -283,12 +285,20 @@
 		depreciation_amount = 0
 
 		number_of_pending_depreciations = final_number_of_depreciations - start
-
+		yearly_opening_wdv = value_after_depreciation
+		current_fiscal_year_end_date = None
 		for n in range(start, final_number_of_depreciations):
 			# If depreciation is already completed (for double declining balance)
 			if skip_row:
 				continue
 
+			schedule_date = add_months(row.depreciation_start_date, n * cint(row.frequency_of_depreciation))
+			if not current_fiscal_year_end_date:
+				current_fiscal_year_end_date = get_fiscal_year(row.depreciation_start_date)[2]
+			elif getdate(schedule_date) > getdate(current_fiscal_year_end_date):
+				current_fiscal_year_end_date = add_years(current_fiscal_year_end_date, 1)
+				yearly_opening_wdv = value_after_depreciation
+
 			if n > 0 and len(self.get("depreciation_schedule")) > n - 1:
 				prev_depreciation_amount = self.get("depreciation_schedule")[n - 1].depreciation_amount
 			else:
@@ -298,6 +308,7 @@
 				self,
 				asset_doc,
 				value_after_depreciation,
+				yearly_opening_wdv,
 				row,
 				n,
 				prev_depreciation_amount,
@@ -401,8 +412,9 @@
 
 			if not depreciation_amount:
 				continue
-			value_after_depreciation -= flt(
-				depreciation_amount, asset_doc.precision("gross_purchase_amount")
+			value_after_depreciation = flt(
+				value_after_depreciation - flt(depreciation_amount),
+				asset_doc.precision("gross_purchase_amount"),
 			)
 
 			# Adjust depreciation amount in the last period based on the expected value after useful life
@@ -582,6 +594,7 @@
 	asset_depr_schedule,
 	asset,
 	depreciable_value,
+	yearly_opening_wdv,
 	fb_row,
 	schedule_idx=0,
 	prev_depreciation_amount=0,
@@ -597,6 +610,7 @@
 			asset,
 			fb_row,
 			depreciable_value,
+			yearly_opening_wdv,
 			schedule_idx,
 			prev_depreciation_amount,
 			has_wdv_or_dd_non_yearly_pro_rata,
@@ -744,19 +758,23 @@
 	asset,
 	fb_row,
 	depreciable_value,
+	yearly_opening_wdv,
 	schedule_idx,
 	prev_depreciation_amount,
 	has_wdv_or_dd_non_yearly_pro_rata,
 	asset_depr_schedule,
 ):
-	return get_default_wdv_or_dd_depr_amount(
-		asset,
-		fb_row,
-		depreciable_value,
-		schedule_idx,
-		prev_depreciation_amount,
-		has_wdv_or_dd_non_yearly_pro_rata,
-		asset_depr_schedule,
+	return (
+		get_default_wdv_or_dd_depr_amount(
+			asset,
+			fb_row,
+			depreciable_value,
+			schedule_idx,
+			prev_depreciation_amount,
+			has_wdv_or_dd_non_yearly_pro_rata,
+			asset_depr_schedule,
+		),
+		None,
 	)
 
 
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index 8ebdcc5..e234eec 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -6,10 +6,12 @@
 from collections import OrderedDict, defaultdict
 
 import frappe
-from frappe import scrub
+from frappe import qb, scrub
 from frappe.desk.reportview import get_filters_cond, get_match_cond
-from frappe.query_builder.functions import Concat, Sum
+from frappe.query_builder import Criterion, CustomFunction
+from frappe.query_builder.functions import Concat, Locate, Sum
 from frappe.utils import nowdate, today, unique
+from pypika import Order
 
 import erpnext
 from erpnext.stock.get_item_details import _get_item_tax_template
@@ -344,37 +346,46 @@
 @frappe.whitelist()
 @frappe.validate_and_sanitize_search_inputs
 def get_project_name(doctype, txt, searchfield, start, page_len, filters):
-	doctype = "Project"
-	cond = ""
+	proj = qb.DocType("Project")
+	qb_filter_and_conditions = []
+	qb_filter_or_conditions = []
+	ifelse = CustomFunction("IF", ["condition", "then", "else"])
+
 	if filters and filters.get("customer"):
-		cond = """(`tabProject`.customer = %s or
-			ifnull(`tabProject`.customer,"")="") and""" % (
-			frappe.db.escape(filters.get("customer"))
-		)
+		qb_filter_and_conditions.append(proj.customer == filters.get("customer"))
+
+	qb_filter_and_conditions.append(proj.status.notin(["Completed", "Cancelled"]))
+
+	q = qb.from_(proj)
 
 	fields = get_fields(doctype, ["name", "project_name"])
-	searchfields = frappe.get_meta(doctype).get_search_fields()
-	searchfields = " or ".join(["`tabProject`." + field + " like %(txt)s" for field in searchfields])
+	for x in fields:
+		q = q.select(proj[x])
 
-	return frappe.db.sql(
-		"""select {fields} from `tabProject`
-		where
-			`tabProject`.status not in ('Completed', 'Cancelled')
-			and {cond} {scond} {match_cond}
-		order by
-			(case when locate(%(_txt)s, `tabProject`.name) > 0 then locate(%(_txt)s, `tabProject`.name) else 99999 end),
-			`tabProject`.idx desc,
-			`tabProject`.name asc
-		limit {page_len} offset {start}""".format(
-			fields=", ".join(["`tabProject`.{0}".format(f) for f in fields]),
-			cond=cond,
-			scond=searchfields,
-			match_cond=get_match_cond(doctype),
-			start=start,
-			page_len=page_len,
-		),
-		{"txt": "%{0}%".format(txt), "_txt": txt.replace("%", "")},
-	)
+	# don't consider 'customer' and 'status' fields for pattern search, as they must be exactly matched
+	searchfields = [
+		x for x in frappe.get_meta(doctype).get_search_fields() if x not in ["customer", "status"]
+	]
+
+	# pattern search
+	if txt:
+		for x in searchfields:
+			qb_filter_or_conditions.append(proj[x].like(f"%{txt}%"))
+
+	q = q.where(Criterion.all(qb_filter_and_conditions)).where(Criterion.any(qb_filter_or_conditions))
+
+	# ordering
+	if txt:
+		# project_name containing search string 'txt' will be given higher precedence
+		q = q.orderby(ifelse(Locate(txt, proj.project_name) > 0, Locate(txt, proj.project_name), 99999))
+	q = q.orderby(proj.idx, order=Order.desc).orderby(proj.name)
+
+	if page_len:
+		q = q.limit(page_len)
+
+	if start:
+		q = q.offset(start)
+	return q.run()
 
 
 @frappe.whitelist()
diff --git a/erpnext/controllers/tests/test_queries.py b/erpnext/controllers/tests/test_queries.py
index 60d1733..3a3bc1c 100644
--- a/erpnext/controllers/tests/test_queries.py
+++ b/erpnext/controllers/tests/test_queries.py
@@ -68,7 +68,7 @@
 		self.assertGreaterEqual(len(query(txt="_Test Item Home Desktop Manufactured")), 1)
 
 	def test_project_query(self):
-		query = add_default_params(queries.get_project_name, "BOM")
+		query = add_default_params(queries.get_project_name, "Project")
 
 		self.assertGreaterEqual(len(query(txt="_Test Project")), 1)
 
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index 598167b..de46271 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -21,7 +21,7 @@
 	},
 
 	toggle_naming_series: function() {
-		if(cur_frm.fields_dict.naming_series) {
+		if(cur_frm && cur_frm.fields_dict.naming_series) {
 			cur_frm.toggle_display("naming_series", cur_frm.doc.__islocal?true:false);
 		}
 	},
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index b206e3f..56c745c 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -94,6 +94,9 @@
 						frm.set_value("reserve_stock", 0);
 						frm.set_df_property("reserve_stock", "read_only", 1);
 						frm.set_df_property("reserve_stock", "hidden", 1);
+						frm.fields_dict.items.grid.update_docfield_property('reserve_stock', 'hidden', 1);
+						frm.fields_dict.items.grid.update_docfield_property('reserve_stock', 'default', 0);
+						frm.fields_dict.items.grid.update_docfield_property('reserve_stock', 'read_only', 1);
 					}
 				})
 			}
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 9542361..5ef2c50 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -200,6 +200,7 @@
 		self.validate_for_items()
 		self.validate_warehouse()
 		self.validate_drop_ship()
+		self.validate_reserved_stock()
 		self.validate_serial_no_based_delivery()
 		validate_against_blanket_order(self)
 		validate_inter_company_party(
@@ -660,6 +661,17 @@
 					).format(item.item_code)
 				)
 
+	def validate_reserved_stock(self):
+		"""Clean reserved stock flag for non-stock Item"""
+
+		enable_stock_reservation = frappe.db.get_single_value(
+			"Stock Settings", "enable_stock_reservation"
+		)
+
+		for item in self.items:
+			if item.reserve_stock and (not enable_stock_reservation or not cint(item.is_stock_item)):
+				item.reserve_stock = 0
+
 	def has_unreserved_stock(self) -> bool:
 		"""Returns True if there is any unreserved item in the Sales Order."""
 
diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
index d4ccfc4..87aeeac 100644
--- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json
+++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
@@ -10,6 +10,7 @@
   "item_code",
   "customer_item_code",
   "ensure_delivery_based_on_produced_serial_no",
+  "is_stock_item",
   "reserve_stock",
   "col_break1",
   "delivery_date",
@@ -867,6 +868,7 @@
   {
    "allow_on_submit": 1,
    "default": "1",
+   "depends_on": "eval:doc.is_stock_item",
    "fieldname": "reserve_stock",
    "fieldtype": "Check",
    "label": "Reserve Stock",
@@ -891,6 +893,16 @@
    "label": "Production Plan Qty",
    "no_copy": 1,
    "read_only": 1
+  },
+  {
+   "default": "0",
+   "fetch_from": "item_code.is_stock_item",
+   "fieldname": "is_stock_item",
+   "fieldtype": "Check",
+   "hidden": 1,
+   "label": "Is Stock Item",
+   "print_hide": 1,
+   "report_hide": 1
   }
  ],
  "idx": 1,
diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.js b/erpnext/selling/page/sales_funnel/sales_funnel.js
index e3d0a55..a0a1222 100644
--- a/erpnext/selling/page/sales_funnel/sales_funnel.js
+++ b/erpnext/selling/page/sales_funnel/sales_funnel.js
@@ -126,9 +126,9 @@
 		if (me.options.chart == 'sales_funnel'){
 			me.render_funnel();
 		} else if (me.options.chart == 'opp_by_lead_source'){
-			me.render_chart("Sales Opportunities by Source");
+			me.render_chart(__("Sales Opportunities by Source"));
 		} else if (me.options.chart == 'sales_pipeline'){
-			me.render_chart("Sales Pipeline by Stage");
+			me.render_chart(__("Sales Pipeline by Stage"));
 		}
 	}
 
diff --git a/erpnext/setup/doctype/vehicle/vehicle.json b/erpnext/setup/doctype/vehicle/vehicle.json
index b19d459..bf1a8c1 100644
--- a/erpnext/setup/doctype/vehicle/vehicle.json
+++ b/erpnext/setup/doctype/vehicle/vehicle.json
@@ -57,7 +57,7 @@
    "in_global_search": 0,
    "in_list_view": 0,
    "in_standard_filter": 0,
-   "label": "Make",
+   "label": "Manufacturer",
    "length": 0,
    "no_copy": 0,
    "permlevel": 0,
diff --git a/erpnext/stock/doctype/bin/bin.json b/erpnext/stock/doctype/bin/bin.json
index 10d9511..39e0917 100644
--- a/erpnext/stock/doctype/bin/bin.json
+++ b/erpnext/stock/doctype/bin/bin.json
@@ -186,7 +186,7 @@
  "idx": 1,
  "in_create": 1,
  "links": [],
- "modified": "2023-11-01 16:51:17.079107",
+ "modified": "2024-01-16 15:11:46.140323",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Bin",
@@ -213,6 +213,21 @@
    "read": 1,
    "report": 1,
    "role": "Stock User"
+  },
+  {
+   "read": 1,
+   "report": 1,
+   "role": "Stock Manager"
+  },
+  {
+   "read": 1,
+   "report": 1,
+   "role": "Purchase Manager"
+  },
+  {
+   "read": 1,
+   "report": 1,
+   "role": "Sales Manager"
   }
  ],
  "quick_entry": 1,
diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js
index 61927f5..c175a4a 100644
--- a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js
+++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js
@@ -1,7 +1,7 @@
 frappe.pages['warehouse-capacity-summary'].on_page_load = function(wrapper) {
 	var page = frappe.ui.make_app_page({
 		parent: wrapper,
-		title: 'Warehouse Capacity Summary',
+		title: __('Warehouse Capacity Summary'),
 		single_column: true
 	});
 	page.set_secondary_action('Refresh', () => page.capacity_dashboard.refresh(), 'refresh');
diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html
index 1183ad4..1883004 100644
--- a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html
+++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html
@@ -1,19 +1,19 @@
 <div class="dashboard-list-item" style="padding: 12px 15px;">
 	<div class="row">
 		<div class="col-sm-2 text-muted" style="margin-top: 8px;">
-			Warehouse
+			{{ __("Warehouse") }}
 		</div>
 		<div class="col-sm-2 text-muted" style="margin-top: 8px;">
-			Item
+			{{ __("Item") }}
 		</div>
 		<div class="col-sm-1 text-muted" style="margin-top: 8px;">
-			Stock Capacity
+			{{ __("Stock Capacity") }}
 		</div>
 		<div class="col-sm-2 text-muted" style="margin-top: 8px;">
-			Balance Stock Qty
+			{{ __("Balance Stock Qty") }}
 		</div>
 		<div class="col-sm-2 text-muted" style="margin-top: 8px;">
-			% Occupied
+			{{ __("% Occupied") }}
 		</div>
 	</div>
 </div>
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py
index f055c6c..02ac8c6 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/stock_ageing.py
@@ -233,28 +233,30 @@
 		"""
 
 		stock_ledger_entries = self.sle
-		if stock_ledger_entries is None:
-			stock_ledger_entries = self.__get_stock_ledger_entries()
 
-		for d in stock_ledger_entries:
-			key, fifo_queue, transferred_item_key = self.__init_key_stores(d)
+		with frappe.db.unbuffered_cursor():
+			if stock_ledger_entries is None:
+				stock_ledger_entries = self.__get_stock_ledger_entries()
 
-			if d.voucher_type == "Stock Reconciliation":
-				# get difference in qty shift as actual qty
-				prev_balance_qty = self.item_details[key].get("qty_after_transaction", 0)
-				d.actual_qty = flt(d.qty_after_transaction) - flt(prev_balance_qty)
+			for d in stock_ledger_entries:
+				key, fifo_queue, transferred_item_key = self.__init_key_stores(d)
 
-			serial_nos = get_serial_nos(d.serial_no) if d.serial_no else []
+				if d.voucher_type == "Stock Reconciliation":
+					# get difference in qty shift as actual qty
+					prev_balance_qty = self.item_details[key].get("qty_after_transaction", 0)
+					d.actual_qty = flt(d.qty_after_transaction) - flt(prev_balance_qty)
 
-			if d.actual_qty > 0:
-				self.__compute_incoming_stock(d, fifo_queue, transferred_item_key, serial_nos)
-			else:
-				self.__compute_outgoing_stock(d, fifo_queue, transferred_item_key, serial_nos)
+				serial_nos = get_serial_nos(d.serial_no) if d.serial_no else []
 
-			self.__update_balances(d, key)
+				if d.actual_qty > 0:
+					self.__compute_incoming_stock(d, fifo_queue, transferred_item_key, serial_nos)
+				else:
+					self.__compute_outgoing_stock(d, fifo_queue, transferred_item_key, serial_nos)
 
-		# Note that stock_ledger_entries is an iterator, you can not reuse it  like a list
-		del stock_ledger_entries
+				self.__update_balances(d, key)
+
+			# Note that stock_ledger_entries is an iterator, you can not reuse it  like a list
+			del stock_ledger_entries
 
 		if not self.filters.get("show_warehouse_wise_stock"):
 			# (Item 1, WH 1), (Item 1, WH 2) => (Item 1)