From 22bd6a54b24129403e0b399938bddcaa9d630cae Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 15 Jan 2024 16:54:19 +0530 Subject: [PATCH 1/7] fix: WDV as per IT Act: calculate yearly amount first and then split it based on months --- .../asset_depreciation_schedule.py | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) 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 74d2aa71ff..ffb50ebe45 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.model.document import Document from frappe.utils import ( add_days, add_months, + add_years, cint, date_diff, flt, @@ -18,6 +19,7 @@ from frappe.utils import ( ) import erpnext +from erpnext.accounts.utils import get_fiscal_year class AssetDepreciationSchedule(Document): @@ -283,12 +285,20 @@ class AssetDepreciationSchedule(Document): 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 @@ class AssetDepreciationSchedule(Document): self, asset_doc, value_after_depreciation, + yearly_opening_wdv, row, n, prev_depreciation_amount, @@ -401,8 +412,9 @@ class AssetDepreciationSchedule(Document): 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 @@ def get_depreciation_amount( asset_depr_schedule, asset, depreciable_value, + yearly_opening_wdv, fb_row, schedule_idx=0, prev_depreciation_amount=0, @@ -597,6 +610,7 @@ def get_depreciation_amount( 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 @@ def get_wdv_or_dd_depr_amount( 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, ) From efe9f6656f01c46a6ac02e3bb61851564670d6bc Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 15 Jan 2024 17:54:47 +0530 Subject: [PATCH 2/7] fix: Cancel asset capitalisation record on cancellation of asset and vice-versa --- erpnext/assets/doctype/asset/asset.json | 5 ++--- erpnext/assets/doctype/asset/asset.py | 11 +++++++++++ .../asset_capitalization/asset_capitalization.py | 8 ++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index d0c9350d77..39a0867d98 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -202,8 +202,7 @@ "fieldname": "purchase_date", "fieldtype": "Date", "label": "Purchase Date", - "mandatory_depends_on": "eval:!doc.is_existing_asset", - "read_only": 1, + "mandatory_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset", "read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset" }, { @@ -590,7 +589,7 @@ "link_fieldname": "target_asset" } ], - "modified": "2024-01-05 17:36:53.131512", + "modified": "2024-01-15 17:35:49.226603", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 5f448987a5..652c75fba7 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -162,6 +162,7 @@ class Asset(AccountsController): def on_cancel(self): self.validate_cancellation() self.cancel_movement_entries() + self.cancel_capitalization() self.delete_depreciation_entries() cancel_asset_depr_schedules(self) self.set_status() @@ -517,6 +518,16 @@ class Asset(AccountsController): movement = frappe.get_doc("Asset Movement", movement.get("name")) movement.cancel() + def cancel_capitalization(self): + asset_capitalization = frappe.db.get_value( + "Asset Capitalization", + {"target_asset": self.name, "docstatus": 1, "entry_type": "Capitalization"}, + ) + + if asset_capitalization: + asset_capitalization = frappe.get_doc("Asset Capitalization", asset_capitalization) + asset_capitalization.cancel() + def delete_depreciation_entries(self): if self.calculate_depreciation: for row in self.get("finance_books"): diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index de758419e0..5e5b62870b 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -136,11 +136,19 @@ class AssetCapitalization(StockController): "Stock Ledger Entry", "Repost Item Valuation", "Serial and Batch Bundle", + "Asset", ) + self.cancel_target_asset() self.update_stock_ledger() self.make_gl_entries() self.restore_consumed_asset_items() + def cancel_target_asset(self): + if self.entry_type == "Capitalization" and self.target_asset: + asset_doc = frappe.get_doc("Asset", self.target_asset) + if asset_doc.docstatus == 1: + asset_doc.cancel() + def set_title(self): self.title = self.target_asset_name or self.target_item_name or self.target_item_code From 4eefb445a748100f3c36094188e38c127ad80051 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 16 Jan 2024 13:38:53 +0530 Subject: [PATCH 3/7] fix: project query controller logic --- erpnext/controllers/queries.py | 54 ++++++++++++++++------------------ 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 8ebdcc5875..66541ae678 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -6,8 +6,9 @@ import json 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 import Criterion from frappe.query_builder.functions import Concat, Sum from frappe.utils import nowdate, today, unique @@ -344,37 +345,32 @@ def bom(doctype, txt, searchfield, start, page_len, filters): @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 = [] 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")) - 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]) + qb_filter_and_conditions.append(proj.status.notin(["Completed", "Cancelled"])) - 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("%", "")}, - ) + q = qb.from_(proj) + + fields = get_fields("Project", ["name", "project_name"]) + for x in fields: + q = q.select(proj[x]) + + # ignore 'customer' and 'status' on searchfields as they must be exactly matched + searchfields = [ + x for x in frappe.get_meta(doctype).get_search_fields() if x not in ["customer", "status"] + ] + 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)) + if page_len: + q = q.limit(page_len) + return q.run() @frappe.whitelist() From 3349dde5e2914bd9e2dbe0ce4de94023bfee2e7f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 16 Jan 2024 14:28:09 +0530 Subject: [PATCH 4/7] fix(test): test case for project query --- erpnext/controllers/queries.py | 2 +- erpnext/controllers/tests/test_queries.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 66541ae678..d88782bbcb 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -355,7 +355,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters): q = qb.from_(proj) - fields = get_fields("Project", ["name", "project_name"]) + fields = get_fields(doctype, ["name", "project_name"]) for x in fields: q = q.select(proj[x]) diff --git a/erpnext/controllers/tests/test_queries.py b/erpnext/controllers/tests/test_queries.py index 60d1733021..3a3bc1cd72 100644 --- a/erpnext/controllers/tests/test_queries.py +++ b/erpnext/controllers/tests/test_queries.py @@ -68,7 +68,7 @@ class TestQueries(unittest.TestCase): 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) From 6e4d4a55cd70964d8ab8870105591abaa1b17b9e Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 16 Jan 2024 15:12:28 +0530 Subject: [PATCH 5/7] fix: permission issue for the BIN --- erpnext/stock/doctype/bin/bin.json | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/bin/bin.json b/erpnext/stock/doctype/bin/bin.json index 10d9511357..39e0917ce6 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, From bfe42fdccb13ab797ac7252ada58df49af43ad54 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 16 Jan 2024 14:35:06 +0530 Subject: [PATCH 6/7] refactor: better ordering of query result --- erpnext/controllers/queries.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index d88782bbcb..e234eec1a6 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -8,9 +8,10 @@ from collections import OrderedDict, defaultdict import frappe from frappe import qb, scrub from frappe.desk.reportview import get_filters_cond, get_match_cond -from frappe.query_builder import Criterion -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 @@ -348,6 +349,8 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters): proj = qb.DocType("Project") qb_filter_and_conditions = [] qb_filter_or_conditions = [] + ifelse = CustomFunction("IF", ["condition", "then", "else"]) + if filters and filters.get("customer"): qb_filter_and_conditions.append(proj.customer == filters.get("customer")) @@ -359,17 +362,29 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters): for x in fields: q = q.select(proj[x]) - # ignore 'customer' and 'status' on searchfields as they must be exactly matched + # 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() From af80d253dbf5d1502f3df4a18b234d392bd96556 Mon Sep 17 00:00:00 2001 From: HENRY Florian Date: Wed, 17 Jan 2024 06:13:36 +0100 Subject: [PATCH 7/7] fix: consistency in display reserved_stock checkbox on Sales Order Item according global settings and item.is_stock_item (#38322) * fix: consistency in display reserved_stock checkbox on Sales Order Item according global settings and item.is_stock_item * fix: evaluate depends_on for fdata visibility in grid * fix: evaluate depends_on for fdata visibility in grid * chore: change after review * chore: change for review --- erpnext/selling/doctype/sales_order/sales_order.js | 3 +++ erpnext/selling/doctype/sales_order/sales_order.py | 12 ++++++++++++ .../doctype/sales_order_item/sales_order_item.json | 12 ++++++++++++ 3 files changed, 27 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index b206e3fe33..56c745c00a 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -94,6 +94,9 @@ frappe.ui.form.on("Sales Order", { 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 95423612c8..5ef2c50146 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -200,6 +200,7 @@ class SalesOrder(SellingController): 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 @@ class SalesOrder(SellingController): ).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 d4ccfc4753..87aeeac368 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,