From 38c886db8b156c2bbb64d6a8a5be578f3d224488 Mon Sep 17 00:00:00 2001 From: Imesha Sudasingha Date: Thu, 7 Aug 2025 04:51:54 +0530 Subject: [PATCH 01/29] fix: Pick List barcode scanner and manual picking issues - Fix barcode scanner serial_no_field configuration from 'not_supported' to 'serial_no' - Add conditional serial assignment in create_pick_list to respect pick_manually flag - Add client-side handler to clear auto-assigned data when switching to manual picking - Add server-side validation to ensure data consistency --- .../doctype/sales_order/sales_order.py | 4 +++- erpnext/stock/doctype/pick_list/pick_list.js | 22 ++++++++++++++++++- erpnext/stock/doctype/pick_list/pick_list.py | 14 ++++++++---- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index b6a979dbc61..9b14616373b 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1785,7 +1785,9 @@ def create_pick_list(source_name, target_doc=None): doc.purpose = "Delivery" - doc.set_item_locations() + # Only auto-assign serial numbers if not picking manually + if not doc.pick_manually: + doc.set_item_locations() return doc diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index c72fa864960..252a5d2872f 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -81,6 +81,26 @@ frappe.ui.form.on("Pick List", { }; }); }, + pick_manually: (frm) => { + // Clear auto-assigned serial numbers and related fields when switching to manual picking + if (frm.doc.pick_manually && frm.doc.locations) { + let has_changes = false; + frm.doc.locations.forEach((row) => { + if (row.serial_no || row.batch_no || row.serial_and_batch_bundle) { + row.serial_no = ""; + row.batch_no = ""; + row.serial_and_batch_bundle = ""; + row.picked_qty = 0; + has_changes = true; + } + }); + + if (has_changes) { + frappe.show_alert(__("Cleared auto-assigned serial numbers and batch numbers for manual picking"), 3); + frm.refresh_field("locations"); + } + } + }, set_item_locations: (frm, save) => { if (!(frm.doc.locations && frm.doc.locations.length)) { frappe.msgprint(__("Add items in the Item Locations table")); @@ -273,7 +293,7 @@ frappe.ui.form.on("Pick List", { max_qty_field: "qty", dont_allow_new_row: true, prompt_qty: frm.doc.prompt_qty, - serial_no_field: "not_supported", // doesn't make sense for picklist without a separate field. + serial_no_field: "serial_no", }; const barcode_scanner = new erpnext.utils.BarcodeScanner(opts); barcode_scanner.process_scan(); diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index f554f4bd140..24fecce135a 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -572,10 +572,16 @@ class PickList(TransactionBase): if not item.item_code: frappe.throw(f"Row #{item.idx}: Item Code is Mandatory") - if not cint( - frappe.get_cached_value("Item", item.item_code, "is_stock_item") - ) and not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code, "disabled": 0}): - continue + + # Check if item is stock item or product bundle + is_stock_item = cint(frappe.get_cached_value("Item", item.item_code, "is_stock_item")) + is_product_bundle = frappe.db.exists("Product Bundle", {"new_item_code": item.item_code, "disabled": 0}) + + # Include non-stock items for delivery purposes, but skip them for warehouse assignment + if not is_stock_item and not is_product_bundle: + # For non-stock items, set warehouse to None and continue processing + item.warehouse = None + item_code = item.item_code reference = item.sales_order_item or item.material_request_item key = (item_code, item.uom, item.warehouse, item.batch_no, reference) From eebeb36b6cbd925ff3633cc803efa1237b1ea5a2 Mon Sep 17 00:00:00 2001 From: Imesha Sudasingha Date: Thu, 7 Aug 2025 05:09:46 +0530 Subject: [PATCH 02/29] refactor: combine duplicate pick_manually handlers in pick_list.js --- erpnext/stock/doctype/pick_list/pick_list.js | 41 ++++++++++---------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index 252a5d2872f..7de3ef7d51a 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -81,26 +81,7 @@ frappe.ui.form.on("Pick List", { }; }); }, - pick_manually: (frm) => { - // Clear auto-assigned serial numbers and related fields when switching to manual picking - if (frm.doc.pick_manually && frm.doc.locations) { - let has_changes = false; - frm.doc.locations.forEach((row) => { - if (row.serial_no || row.batch_no || row.serial_and_batch_bundle) { - row.serial_no = ""; - row.batch_no = ""; - row.serial_and_batch_bundle = ""; - row.picked_qty = 0; - has_changes = true; - } - }); - - if (has_changes) { - frappe.show_alert(__("Cleared auto-assigned serial numbers and batch numbers for manual picking"), 3); - frm.refresh_field("locations"); - } - } - }, + set_item_locations: (frm, save) => { if (!(frm.doc.locations && frm.doc.locations.length)) { frappe.msgprint(__("Add items in the Item Locations table")); @@ -121,11 +102,31 @@ frappe.ui.form.on("Pick List", { }, pick_manually: function (frm) { + // Update warehouse field read-only property frm.fields_dict.locations.grid.update_docfield_property( "warehouse", "read_only", !frm.doc.pick_manually ); + + // Clear auto-assigned serial numbers and related fields when switching to manual picking + if (frm.doc.pick_manually && frm.doc.locations) { + let has_changes = false; + frm.doc.locations.forEach((row) => { + if (row.serial_no || row.batch_no || row.serial_and_batch_bundle) { + row.serial_no = ""; + row.batch_no = ""; + row.serial_and_batch_bundle = ""; + row.picked_qty = 0; + has_changes = true; + } + }); + + if (has_changes) { + frappe.show_alert(__("Cleared auto-assigned serial numbers and batch numbers for manual picking"), 3); + frm.refresh_field("locations"); + } + } }, get_item_locations: (frm) => { From dab6bb8e05cd9c3f90638f0562deb9766e9e6a19 Mon Sep 17 00:00:00 2001 From: Imesha Sudasingha Date: Thu, 7 Aug 2025 05:15:42 +0530 Subject: [PATCH 03/29] style: apply pre-commit formatting fixes --- erpnext/stock/doctype/pick_list/pick_list.js | 5 ++++- erpnext/stock/doctype/pick_list/pick_list.py | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index 7de3ef7d51a..d021973a01f 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -123,7 +123,10 @@ frappe.ui.form.on("Pick List", { }); if (has_changes) { - frappe.show_alert(__("Cleared auto-assigned serial numbers and batch numbers for manual picking"), 3); + frappe.show_alert( + __("Cleared auto-assigned serial numbers and batch numbers for manual picking"), + 3 + ); frm.refresh_field("locations"); } } diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 24fecce135a..2420c166161 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -575,7 +575,9 @@ class PickList(TransactionBase): # Check if item is stock item or product bundle is_stock_item = cint(frappe.get_cached_value("Item", item.item_code, "is_stock_item")) - is_product_bundle = frappe.db.exists("Product Bundle", {"new_item_code": item.item_code, "disabled": 0}) + is_product_bundle = frappe.db.exists( + "Product Bundle", {"new_item_code": item.item_code, "disabled": 0} + ) # Include non-stock items for delivery purposes, but skip them for warehouse assignment if not is_stock_item and not is_product_bundle: From eafe33a1768f1a1d047c1fa118f747160adef089 Mon Sep 17 00:00:00 2001 From: navinrc Date: Sat, 9 Aug 2025 01:06:51 +0530 Subject: [PATCH 04/29] feat: add item_name column to Material Request dialog in Purchase Order (cherry picked from commit 43127190102e906e2014e9f9ced6bd2142d82d9e) --- erpnext/buying/doctype/purchase_order/purchase_order.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index bdd5e89dda0..5bdc9900abf 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -578,7 +578,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends ( }, allow_child_item_selection: true, child_fieldname: "items", - child_columns: ["item_code", "qty", "ordered_qty"], + child_columns: ["item_code", "item_name", "qty", "ordered_qty"], }); }, __("Get Items From") From 15e8fa31894d0720fb97b164eb8d10e1003bc2de Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 10:25:21 +0530 Subject: [PATCH 05/29] Merge pull request #49081 from frappe/mergify/bp/version-15-hotfix/pr-49068 fix(job-card): Add filter to item_code query for scrap_items to exclude disabled (backport #49068) --- erpnext/manufacturing/doctype/job_card/job_card.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index f95946a1724..c3b0bb10fe6 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -23,6 +23,14 @@ frappe.ui.form.on("Job Card", { }; }); + frm.set_query("item_code", "scrap_items", () => { + return { + filters: { + disabled: 0, + }, + }; + }); + frm.set_indicator_formatter("sub_operation", function (doc) { if (doc.status == "Pending") { return "red"; From 9da2be23252e205f25758361de5b3cc9c174b700 Mon Sep 17 00:00:00 2001 From: Vishist16 <101823906+Vishist16@users.noreply.github.com> Date: Mon, 11 Aug 2025 10:29:27 +0530 Subject: [PATCH 06/29] fix: handle negative inventory check (#48558) (#48691) * fix: handle negative inventory check (#48558) * fix: updated DocField via Desk UI as suggested * fix: update DocField via Desk UI and fix linting issues (cherry picked from commit 3ee23d9ee80b8ac669a74ffbb06997ce9ae4c089) --- erpnext/stock/doctype/item/item.json | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 16ac4fd1017..f0923061e93 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -318,7 +318,8 @@ "fieldname": "shelf_life_in_days", "fieldtype": "Int", "label": "Shelf Life In Days", - "mandatory_depends_on": "eval:doc.has_batch_no && doc.has_expiry_date" + "mandatory_depends_on": "eval:doc.has_batch_no && doc.has_expiry_date", + "non_negative": 1 }, { "default": "2099-12-31", @@ -362,7 +363,8 @@ "depends_on": "is_stock_item", "fieldname": "weight_per_unit", "fieldtype": "Float", - "label": "Weight Per Unit" + "label": "Weight Per Unit", + "non_negative": 1 }, { "depends_on": "eval:doc.is_stock_item", @@ -534,13 +536,15 @@ "fieldname": "min_order_qty", "fieldtype": "Float", "label": "Minimum Order Qty", + "non_negative": 1, "oldfieldname": "min_order_qty", "oldfieldtype": "Currency" }, { "fieldname": "safety_stock", "fieldtype": "Float", - "label": "Safety Stock" + "label": "Safety Stock", + "non_negative": 1 }, { "fieldname": "purchase_details_cb", @@ -551,6 +555,7 @@ "fieldname": "lead_time_days", "fieldtype": "Int", "label": "Lead Time in days", + "non_negative": 1, "oldfieldname": "lead_time_days", "oldfieldtype": "Int" }, @@ -559,6 +564,7 @@ "fieldtype": "Float", "label": "Last Purchase Rate", "no_copy": 1, + "non_negative": 1, "oldfieldname": "last_purchase_rate", "oldfieldtype": "Currency", "read_only": 1 @@ -889,7 +895,7 @@ "image_field": "image", "links": [], "make_attachments_public": 1, - "modified": "2025-02-03 23:43:57.253667", + "modified": "2025-08-08 14:58:48.674193", "modified_by": "Administrator", "module": "Stock", "name": "Item", @@ -954,6 +960,7 @@ } ], "quick_entry": 1, + "row_format": "Dynamic", "search_fields": "item_name,description,item_group,customer_code", "show_name_in_global_search": 1, "show_preview_popup": 1, From 9df6424a20bd3391509e0fa33b40d26f441f76fd Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Mon, 21 Jul 2025 14:22:05 +0000 Subject: [PATCH 07/29] fix(regional-uae): mark export items as zero rated (cherry picked from commit b8224693c49a21ddfa4a57a0a5ed2a8a963e06aa) # Conflicts: # erpnext/patches.txt --- erpnext/patches.txt | 4 ++++ .../patches/v15_0/update_uae_zero_rated_fetch.py | 9 +++++++++ erpnext/regional/report/uae_vat_201/uae_vat_201.py | 2 +- erpnext/regional/united_arab_emirates/setup.py | 1 + erpnext/regional/united_arab_emirates/utils.py | 13 +++++++++---- 5 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 erpnext/patches/v15_0/update_uae_zero_rated_fetch.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 74b899555c0..bf3326cd976 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -416,6 +416,10 @@ erpnext.patches.v15_0.update_payment_ledger_entries_against_advance_doctypes erpnext.patches.v15_0.rename_price_list_to_buying_price_list erpnext.patches.v15_0.patch_missing_buying_price_list_in_material_request erpnext.patches.v15_0.remove_sales_partner_from_consolidated_sales_invoice +<<<<<<< HEAD erpnext.patches.v15_0.repost_gl_entries_with_no_account_subcontracting #2025-08-04 execute:frappe.db.set_single_value("Accounts Settings", "fetch_valuation_rate_for_internal_transaction", 1) erpnext.patches.v15_0.add_company_payment_gateway_account +======= +erpnext.patches.v15_0.update_uae_zero_rated_fetch +>>>>>>> b8224693c4 (fix(regional-uae): mark export items as zero rated) diff --git a/erpnext/patches/v15_0/update_uae_zero_rated_fetch.py b/erpnext/patches/v15_0/update_uae_zero_rated_fetch.py new file mode 100644 index 00000000000..a769cc0a85e --- /dev/null +++ b/erpnext/patches/v15_0/update_uae_zero_rated_fetch.py @@ -0,0 +1,9 @@ +import frappe +from frappe.custom.doctype.property_setter.property_setter import make_property_setter + + +def execute(): + if not frappe.db.get_value("Company", {"country": "United Arab Emirates"}): + return + + make_property_setter("Sales Invoice Item", "is_zero_rated", "fetch_if_empty", 1, "Check") diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py index 1a68d7dec6a..7cf86adbe01 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -143,7 +143,7 @@ def get_total_emiratewise(filters): on i.parent = s.name where - s.docstatus = 1 and i.is_exempt != 1 and i.is_zero_rated != 1 + s.docstatus = 1 and i.is_exempt != 1 and i.is_zero_rated != 1 {conditions} group by s.vat_emirate; diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index 36a079546e5..6a8c7b9438b 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -20,6 +20,7 @@ def make_custom_fields(): label="Is Zero Rated", fieldtype="Check", fetch_from="item_code.is_zero_rated", + fetch_if_empty=1, insert_after="description", print_hide=1, ) diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index d71b87bd903..1fa4d070d12 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -7,10 +7,6 @@ from erpnext.controllers.taxes_and_totals import get_itemised_tax def update_itemised_tax_data(doc): - # maybe this should be a standard function rather than a regional one - if not doc.taxes: - return - if not doc.items: return @@ -19,6 +15,14 @@ def update_itemised_tax_data(doc): return itemised_tax = get_itemised_tax(doc.taxes) + is_export = 0 + + if doc.customer_address and doc.company_address: + company_country = frappe.get_cached_value("Address", doc.company_address, "country") + customer_country = frappe.db.get_value("Address", doc.customer_address, "country") + + if company_country != customer_country: + is_export = 1 for row in doc.items: tax_rate, tax_amount = 0.0, 0.0 @@ -30,6 +34,7 @@ def update_itemised_tax_data(doc): tax_amount += flt((row.net_amount * _tax_rate) / 100, row.precision("tax_amount")) tax_rate += _tax_rate + row.is_zero_rated = is_export row.tax_rate = flt(tax_rate, row.precision("tax_rate")) row.tax_amount = flt(tax_amount, row.precision("tax_amount")) row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount")) From 106b83e9f94c5bcd3725d957070a6aff7e3f8dc0 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Mon, 21 Jul 2025 14:47:14 +0000 Subject: [PATCH 08/29] fix(regional-uae): split export determination (cherry picked from commit dc72e6cf36f5b144b092efc60b2ebb4947a0d197) --- .../regional/united_arab_emirates/utils.py | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index 1fa4d070d12..0f7336af596 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -15,14 +15,25 @@ def update_itemised_tax_data(doc): return itemised_tax = get_itemised_tax(doc.taxes) - is_export = 0 + is_export = False - if doc.customer_address and doc.company_address: - company_country = frappe.get_cached_value("Address", doc.company_address, "country") - customer_country = frappe.db.get_value("Address", doc.customer_address, "country") + def determine_if_export(doc): + if doc.flags.export_determined is not None: + return doc.flags.export_determined - if company_country != customer_country: - is_export = 1 + doc.flags.export_determined = False + if doc.customer_address and doc.company_address: + company_country = frappe.get_cached_value("Address", doc.company_address, "country") + customer_country = frappe.db.get_value("Address", doc.customer_address, "country") + + if company_country != customer_country: + doc.flags.export_determined = True + return doc.flags.export_determined + else: + frappe.msgprint( + _("Please set Customer and Company Address to determine if the transaction is an export."), + alert=True, + ) for row in doc.items: tax_rate, tax_amount = 0.0, 0.0 @@ -34,11 +45,16 @@ def update_itemised_tax_data(doc): tax_amount += flt((row.net_amount * _tax_rate) / 100, row.precision("tax_amount")) tax_rate += _tax_rate - row.is_zero_rated = is_export + if not row.is_zero_rated and not tax_rate: + is_export = is_export or determine_if_export(doc) + row.is_zero_rated = is_export + row.tax_rate = flt(tax_rate, row.precision("tax_rate")) row.tax_amount = flt(tax_amount, row.precision("tax_amount")) row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount")) + doc.flags.export_determined = None + def get_account_currency(account): """Helper function to get account currency.""" From 62db42cf2f65341be2d71e706b5ff7e5bbcaa5bb Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Mon, 28 Jul 2025 05:32:04 +0000 Subject: [PATCH 09/29] fix(regional-uae): restrict zero rated export to invoice (cherry picked from commit 1170e4fb2c067f7c44a06cac89f80b667ffe0e49) --- erpnext/regional/united_arab_emirates/utils.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index 0f7336af596..ecf8226b2db 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -10,14 +10,22 @@ def update_itemised_tax_data(doc): if not doc.items: return + has_zero_rated_field = False meta = frappe.get_meta(doc.items[0].doctype) if not meta.has_field("tax_rate"): return + if meta.has_field("is_zero_rated"): + has_zero_rated_field = True + itemised_tax = get_itemised_tax(doc.taxes) + doc.flags.export_determined = None is_export = False def determine_if_export(doc): + if doc.doctype != "Sales Invoice": + return False + if doc.flags.export_determined is not None: return doc.flags.export_determined @@ -45,7 +53,7 @@ def update_itemised_tax_data(doc): tax_amount += flt((row.net_amount * _tax_rate) / 100, row.precision("tax_amount")) tax_rate += _tax_rate - if not row.is_zero_rated and not tax_rate: + if not tax_rate and has_zero_rated_field and not row.is_zero_rated: is_export = is_export or determine_if_export(doc) row.is_zero_rated = is_export From 68c65866bf6e56fcdc4b55a3b49d3ae0c4feddf8 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Mon, 4 Aug 2025 15:45:43 +0530 Subject: [PATCH 10/29] fix: simplify export determination logic (cherry picked from commit d25846f383affe7ca4dfeba444cb5cc9daaec5ea) --- .../regional/united_arab_emirates/utils.py | 39 +++++++------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index ecf8226b2db..504651882cf 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -10,38 +10,32 @@ def update_itemised_tax_data(doc): if not doc.items: return - has_zero_rated_field = False meta = frappe.get_meta(doc.items[0].doctype) if not meta.has_field("tax_rate"): return - if meta.has_field("is_zero_rated"): - has_zero_rated_field = True - itemised_tax = get_itemised_tax(doc.taxes) - doc.flags.export_determined = None - is_export = False def determine_if_export(doc): if doc.doctype != "Sales Invoice": return False - if doc.flags.export_determined is not None: - return doc.flags.export_determined - - doc.flags.export_determined = False - if doc.customer_address and doc.company_address: - company_country = frappe.get_cached_value("Address", doc.company_address, "country") - customer_country = frappe.db.get_value("Address", doc.customer_address, "country") - - if company_country != customer_country: - doc.flags.export_determined = True - return doc.flags.export_determined - else: + if not doc.customer_address: frappe.msgprint( - _("Please set Customer and Company Address to determine if the transaction is an export."), + _("Please set Customer Address to determine if the transaction is an export."), alert=True, ) + return False + + company_country = frappe.get_cached_value("Company", doc.company, "country") + customer_country = frappe.db.get_value("Address", doc.customer_address, "country") + + if company_country != customer_country: + return True + + return False + + is_export = determine_if_export(doc) for row in doc.items: tax_rate, tax_amount = 0.0, 0.0 @@ -53,16 +47,13 @@ def update_itemised_tax_data(doc): tax_amount += flt((row.net_amount * _tax_rate) / 100, row.precision("tax_amount")) tax_rate += _tax_rate - if not tax_rate and has_zero_rated_field and not row.is_zero_rated: - is_export = is_export or determine_if_export(doc) - row.is_zero_rated = is_export + if not tax_rate and is_export: + row.is_zero_rated = 1 row.tax_rate = flt(tax_rate, row.precision("tax_rate")) row.tax_amount = flt(tax_amount, row.precision("tax_amount")) row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount")) - doc.flags.export_determined = None - def get_account_currency(account): """Helper function to get account currency.""" From faae7347971c0515044e8004e380c80fb8296a77 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Mon, 4 Aug 2025 15:52:22 +0530 Subject: [PATCH 11/29] fix: avoid property setter for custom field (cherry picked from commit 0c15b65756b2c3dac09a705cf2a08bc8cd459c20) --- erpnext/patches/v15_0/update_uae_zero_rated_fetch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/patches/v15_0/update_uae_zero_rated_fetch.py b/erpnext/patches/v15_0/update_uae_zero_rated_fetch.py index a769cc0a85e..4ccdbb4229f 100644 --- a/erpnext/patches/v15_0/update_uae_zero_rated_fetch.py +++ b/erpnext/patches/v15_0/update_uae_zero_rated_fetch.py @@ -1,9 +1,9 @@ import frappe -from frappe.custom.doctype.property_setter.property_setter import make_property_setter +from erpnext.regional.united_arab_emirates.setup import make_custom_fields def execute(): if not frappe.db.get_value("Company", {"country": "United Arab Emirates"}): return - make_property_setter("Sales Invoice Item", "is_zero_rated", "fetch_if_empty", 1, "Check") + make_custom_fields() From 534b27afa52d4ee0c9068fe4245ab5044e44db44 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Wed, 6 Aug 2025 11:40:33 +0530 Subject: [PATCH 12/29] chore: linters (cherry picked from commit eb6c8d8938226f12d517a59e2c9777f479001888) --- erpnext/patches/v15_0/update_uae_zero_rated_fetch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/patches/v15_0/update_uae_zero_rated_fetch.py b/erpnext/patches/v15_0/update_uae_zero_rated_fetch.py index 4ccdbb4229f..57b8db59f97 100644 --- a/erpnext/patches/v15_0/update_uae_zero_rated_fetch.py +++ b/erpnext/patches/v15_0/update_uae_zero_rated_fetch.py @@ -1,4 +1,5 @@ import frappe + from erpnext.regional.united_arab_emirates.setup import make_custom_fields From 614d38d0e63f3f4eee838af0faa8248187119817 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Fri, 8 Aug 2025 09:58:16 +0530 Subject: [PATCH 13/29] fix: show message only if no tax is applied (cherry picked from commit 38471995e76313b31cc37a52c8b2834744e2c46f) --- erpnext/regional/united_arab_emirates/utils.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index 504651882cf..a975c1539a5 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -21,10 +21,12 @@ def update_itemised_tax_data(doc): return False if not doc.customer_address: - frappe.msgprint( - _("Please set Customer Address to determine if the transaction is an export."), - alert=True, - ) + if not doc.total_taxes_and_charges: + frappe.msgprint( + _("Please set Customer Address to determine if the transaction is an export."), + alert=True, + ) + return False company_country = frappe.get_cached_value("Company", doc.company, "country") @@ -47,8 +49,10 @@ def update_itemised_tax_data(doc): tax_amount += flt((row.net_amount * _tax_rate) / 100, row.precision("tax_amount")) tax_rate += _tax_rate - if not tax_rate and is_export: - row.is_zero_rated = 1 + if not tax_rate: + row.is_zero_rated = ( + is_export or frappe.get_cached_value("Item", row.item_code, "is_zero_rated") + ) row.tax_rate = flt(tax_rate, row.precision("tax_rate")) row.tax_amount = flt(tax_amount, row.precision("tax_amount")) From d47c25287d5485525bbfe73fe887dd813cd211f6 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Fri, 8 Aug 2025 10:02:35 +0530 Subject: [PATCH 14/29] chore: code styling (cherry picked from commit c8940a39b357836e36095d07ef91f53789d5eff5) --- erpnext/regional/united_arab_emirates/utils.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index a975c1539a5..6a89944c4ba 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -50,9 +50,7 @@ def update_itemised_tax_data(doc): tax_rate += _tax_rate if not tax_rate: - row.is_zero_rated = ( - is_export or frappe.get_cached_value("Item", row.item_code, "is_zero_rated") - ) + row.is_zero_rated = is_export or frappe.get_cached_value("Item", row.item_code, "is_zero_rated") row.tax_rate = flt(tax_rate, row.precision("tax_rate")) row.tax_amount = flt(tax_amount, row.precision("tax_amount")) From 90913c66aeaf0d6de4ce18fe1e33e07d84a68f88 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Fri, 8 Aug 2025 11:01:59 +0530 Subject: [PATCH 15/29] fix: handle case where taxes is added invoice changed to non-export later (cherry picked from commit 29c3ef8280a36376cbb4ecc07d8467ecdd85ec41) --- erpnext/regional/united_arab_emirates/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index 6a89944c4ba..19c00a6c798 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -49,7 +49,7 @@ def update_itemised_tax_data(doc): tax_amount += flt((row.net_amount * _tax_rate) / 100, row.precision("tax_amount")) tax_rate += _tax_rate - if not tax_rate: + if not tax_rate or row.get("is_zero_rated"): row.is_zero_rated = is_export or frappe.get_cached_value("Item", row.item_code, "is_zero_rated") row.tax_rate = flt(tax_rate, row.precision("tax_rate")) From faba5230862d02131c5ad6cf60e5b500e7daa2cb Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 11 Aug 2025 11:41:10 +0530 Subject: [PATCH 16/29] chore: resolve conflict --- erpnext/patches.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index bf3326cd976..1087f64276d 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -416,10 +416,7 @@ erpnext.patches.v15_0.update_payment_ledger_entries_against_advance_doctypes erpnext.patches.v15_0.rename_price_list_to_buying_price_list erpnext.patches.v15_0.patch_missing_buying_price_list_in_material_request erpnext.patches.v15_0.remove_sales_partner_from_consolidated_sales_invoice -<<<<<<< HEAD erpnext.patches.v15_0.repost_gl_entries_with_no_account_subcontracting #2025-08-04 execute:frappe.db.set_single_value("Accounts Settings", "fetch_valuation_rate_for_internal_transaction", 1) erpnext.patches.v15_0.add_company_payment_gateway_account -======= erpnext.patches.v15_0.update_uae_zero_rated_fetch ->>>>>>> b8224693c4 (fix(regional-uae): mark export items as zero rated) From c462219dd7cc8967a70cb92b6ad28d363fd0dba1 Mon Sep 17 00:00:00 2001 From: Assem Bahnasy Date: Mon, 11 Aug 2025 11:55:56 +0300 Subject: [PATCH 17/29] refactor: Use parameterized SQL queries to prevent injection and handle None values (cherry picked from commit a08c7f37d33e3f8fdc10939e4a801ea864a91423) --- .../setup/doctype/party_type/party_type.py | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/erpnext/setup/doctype/party_type/party_type.py b/erpnext/setup/doctype/party_type/party_type.py index be8652f153b..09dd6c67050 100644 --- a/erpnext/setup/doctype/party_type/party_type.py +++ b/erpnext/setup/doctype/party_type/party_type.py @@ -26,13 +26,31 @@ class PartyType(Document): @frappe.validate_and_sanitize_search_inputs def get_party_type(doctype, txt, searchfield, start, page_len, filters): cond = "" + account_type = None + if filters and filters.get("account"): account_type = frappe.db.get_value("Account", filters.get("account"), "account_type") - cond = "and account_type = '%s'" % account_type + if account_type: + cond = "and account_type = %(account_type)s" - return frappe.db.sql( + # Build parameters dictionary + params = {"txt": "%" + txt + "%", "start": start, "page_len": page_len} + if account_type: + params["account_type"] = account_type + + result = frappe.db.sql( f"""select name from `tabParty Type` - where `{searchfield}` LIKE %(txt)s {cond} - order by name limit %(page_len)s offset %(start)s""", - {"txt": "%" + txt + "%", "start": start, "page_len": page_len}, + where `{searchfield}` LIKE %(txt)s {cond} + order by name limit %(page_len)s offset %(start)s""", + params, ) + + # Convert to list and append Employee if not already present + result = list(result) if result else [] + + # Only append Employee for Receivable or Payable account types + if account_type in ["Receivable", "Payable"]: + if not any(row[0] == "Employee" for row in result): + result.append(("Employee",)) # Using tuple format like SQL returns + + return result From e762007e0e9e29dc6a5af2281b7352603b48eab1 Mon Sep 17 00:00:00 2001 From: Assem Bahnasy Date: Mon, 11 Aug 2025 12:41:34 +0300 Subject: [PATCH 18/29] refactor: Move Employee inclusion to SQL level to preserve search semantics (cherry picked from commit 8a9bf166c6971d0d2b9d9fe81f6dffb04338a5ed) --- erpnext/setup/doctype/party_type/party_type.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/erpnext/setup/doctype/party_type/party_type.py b/erpnext/setup/doctype/party_type/party_type.py index 09dd6c67050..6730d1cbdce 100644 --- a/erpnext/setup/doctype/party_type/party_type.py +++ b/erpnext/setup/doctype/party_type/party_type.py @@ -31,7 +31,11 @@ def get_party_type(doctype, txt, searchfield, start, page_len, filters): if filters and filters.get("account"): account_type = frappe.db.get_value("Account", filters.get("account"), "account_type") if account_type: - cond = "and account_type = %(account_type)s" + if account_type in ["Receivable", "Payable"]: + # Include Employee regardless of its configured account_type, but still respect the text filter + cond = "and (account_type = %(account_type)s or name = 'Employee')" + else: + cond = "and account_type = %(account_type)s" # Build parameters dictionary params = {"txt": "%" + txt + "%", "start": start, "page_len": page_len} @@ -45,12 +49,4 @@ def get_party_type(doctype, txt, searchfield, start, page_len, filters): params, ) - # Convert to list and append Employee if not already present - result = list(result) if result else [] - - # Only append Employee for Receivable or Payable account types - if account_type in ["Receivable", "Payable"]: - if not any(row[0] == "Employee" for row in result): - result.append(("Employee",)) # Using tuple format like SQL returns - - return result + return result or [] From 36197afa192b94f72ec7695510aba896af292b86 Mon Sep 17 00:00:00 2001 From: Wolfram Schmidt Date: Sun, 20 Jul 2025 16:20:48 +0200 Subject: [PATCH 19/29] refactor: remove default value for opportunity type removing default value as this leads to an error in non-english setups --- erpnext/crm/doctype/opportunity/opportunity.js | 3 +++ erpnext/crm/doctype/opportunity/opportunity.json | 3 +-- erpnext/crm/doctype/opportunity/opportunity.py | 5 +++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js index b78eae5f109..fcea1cafab9 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.js +++ b/erpnext/crm/doctype/opportunity/opportunity.js @@ -284,6 +284,9 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller { this.frm.set_value("currency", frappe.defaults.get_user_default("Currency")); } + if (this.frm.is_new() && this.frm.doc.opportunity_type === undefined) { + this.frm.doc.opportunity_type = __("Sales"); + } this.setup_queries(); } diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json index f56286a636c..10df9d91209 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.json +++ b/erpnext/crm/doctype/opportunity/opportunity.json @@ -152,7 +152,6 @@ "no_copy": 1 }, { - "default": "Sales", "fieldname": "opportunity_type", "fieldtype": "Link", "in_list_view": 1, @@ -670,4 +669,4 @@ "title_field": "title", "track_seen": 1, "track_views": 1 -} \ No newline at end of file +} diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 9229ab22227..91d60c924bf 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -126,6 +126,7 @@ class Opportunity(TransactionBase, CRMNote): link_communications(self.opportunity_from, self.party_name, self) def validate(self): + self.set_opportunity_type() self.make_new_lead_if_required() self.validate_item_details() self.validate_uom_is_integer("uom", "qty") @@ -150,6 +151,10 @@ class Opportunity(TransactionBase, CRMNote): except Exception: continue + def set_opportunity_type(self): + if self.is_new() and not self.opportunity_type: + self.opportunity_type = _("Sales") + def set_exchange_rate(self): company_currency = frappe.get_cached_value("Company", self.company, "default_currency") if self.currency == company_currency: From c2ad5d0fc7e7e828631158858cc84ef9ab3e2243 Mon Sep 17 00:00:00 2001 From: Lakshit Jain <108322669+ljain112@users.noreply.github.com> Date: Mon, 11 Aug 2025 17:07:03 +0530 Subject: [PATCH 20/29] Merge pull request #49093 from ljain112/fix-account-coa fix: allow creation of root accounts in account tree view (cherry picked from commit d45cd5af2bee982b069a8288db74c6314a7c508c) --- erpnext/accounts/doctype/account/account.py | 6 +++--- erpnext/accounts/doctype/account/account_tree.js | 7 +------ 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index b9cde43653a..f81afbd1297 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -169,7 +169,7 @@ class Account(NestedSet): if par.root_type: self.root_type = par.root_type - if self.is_group: + if cint(self.is_group): db_value = self.get_doc_before_save() if db_value: if self.report_type != db_value.report_type: @@ -212,7 +212,7 @@ class Account(NestedSet): if doc_before_save and not doc_before_save.parent_account: throw(_("Root cannot be edited."), RootNotEditable) - if not self.parent_account and not self.is_group: + if not self.parent_account and not cint(self.is_group): throw(_("The root account {0} must be a group").format(frappe.bold(self.name))) def validate_root_company_and_sync_account_to_children(self): @@ -261,7 +261,7 @@ class Account(NestedSet): if self.check_gle_exists(): throw(_("Account with existing transaction cannot be converted to ledger")) - elif self.is_group: + elif cint(self.is_group): if self.account_type and not self.flags.exclude_account_type_check: throw(_("Cannot covert to Group because Account Type is selected.")) elif self.check_if_child_exists(): diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js index 84b6239a392..1687466096c 100644 --- a/erpnext/accounts/doctype/account/account_tree.js +++ b/erpnext/accounts/doctype/account/account_tree.js @@ -252,10 +252,6 @@ frappe.treeview_settings["Account"] = { root_company, ]); } else { - const node = treeview.tree.get_selected_node(); - if (node.is_root) { - frappe.throw(__("Cannot create root account.")); - } treeview.new_node(); } }, @@ -274,8 +270,7 @@ frappe.treeview_settings["Account"] = { ].treeview.page.fields_dict.root_company.get_value() || frappe.flags.ignore_root_company_validation) && node.expandable && - !node.hide_add && - !node.is_root + !node.hide_add ); }, click: function () { From 680fa3b8f36789da70c6cace94c1ed49ac0c2045 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Wed, 30 Jul 2025 16:56:12 +0530 Subject: [PATCH 21/29] feat: add party name column in general ledger report (cherry picked from commit 5f24061dd46c931b783f8d3b1b4e7cf43007aa3c) --- .../report/general_ledger/general_ledger.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 2bec888729a..219a564b94e 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -701,6 +701,19 @@ def get_columns(filters): {"label": _("Party"), "fieldname": "party", "width": 100}, ] + supplier_master_name = frappe.db.get_single_value("Buying Settings", "supp_master_name") + customer_master_name = frappe.db.get_single_value("Selling Settings", "cust_master_name") + + if supplier_master_name != "Supplier Name" or customer_master_name != "Customer Name": + columns.append( + { + "label": _("Party Name"), + "fieldname": "party_name", + "fieldtype": "Data", + "width": 150, + } + ) + if filters.get("include_dimensions"): columns.append({"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100}) From 3763ad451b1e0f7dbca301b703240cc2aa8bba77 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Wed, 30 Jul 2025 16:58:38 +0530 Subject: [PATCH 22/29] feat: add party name in GL entries (cherry picked from commit 3d94a7cf2c189175694a50ab0c6a5f3da42f031a) --- .../report/general_ledger/general_ledger.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 219a564b94e..c4dd07d5341 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -203,6 +203,12 @@ def get_gl_entries(filters, accounting_dimensions): as_dict=1, ) + party_name_map = get_party_name_map() + + for gl_entry in gl_entries: + if gl_entry.party_type and gl_entry.party: + gl_entry.party_name = party_name_map.get(gl_entry.party_type, {}).get(gl_entry.party) + if filters.get("presentation_currency"): return convert_to_presentation_currency(gl_entries, currency_map, filters) else: @@ -337,6 +343,17 @@ def get_conditions(filters): return "and {}".format(" and ".join(conditions)) if conditions else "" +def get_party_name_map(): + party_map = {} + + customers = frappe.get_all("Customer", fields=["name", "customer_name"]) + party_map["Customer"] = {c.name: c.customer_name for c in customers} + + suppliers = frappe.get_all("Supplier", fields=["name", "supplier_name"]) + party_map["Supplier"] = {s.name: s.supplier_name for s in suppliers} + return party_map + + def get_accounts_with_children(accounts): if not isinstance(accounts, list): accounts = [d.strip() for d in accounts.strip().split(",") if d] From 9cd60531d21f806d220c95668a7844ca5d865fc4 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Wed, 30 Jul 2025 17:00:33 +0530 Subject: [PATCH 23/29] feat: add customer name column in gross profit report (cherry picked from commit 9dee411eb563f52b40dcaffa05199c7998b6a8a7) --- .../report/gross_profit/gross_profit.py | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index fe17924d527..188e97ff3c0 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -33,6 +33,7 @@ def execute(filters=None): "invoice_or_item", "customer", "customer_group", + "customer_name", "posting_date", "item_code", "item_name", @@ -95,6 +96,7 @@ def execute(filters=None): "customer": [ "customer", "customer_group", + "customer_name", "qty", "base_rate", "buying_rate", @@ -250,6 +252,9 @@ def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_ def get_columns(group_wise_columns, filters): columns = [] + + supplier_master_name = frappe.db.get_single_value("Buying Settings", "supp_master_name") + customer_master_name = frappe.db.get_single_value("Selling Settings", "cust_master_name") column_map = frappe._dict( { "parent": { @@ -395,6 +400,12 @@ def get_columns(group_wise_columns, filters): "options": "Customer Group", "width": 100, }, + "customer_name": { + "label": _("Customer Name"), + "fieldname": "customer_name", + "fieldtype": "Data", + "width": 150, + }, "territory": { "label": _("Territory"), "fieldname": "territory", @@ -419,6 +430,10 @@ def get_columns(group_wise_columns, filters): ) for col in group_wise_columns.get(scrub(filters.group_by)): + if col == "customer_name" and ( + supplier_master_name == "Supplier Name" and customer_master_name == "Customer Name" + ): + continue columns.append(column_map.get(col)) columns.append( @@ -440,6 +455,7 @@ def get_column_names(): "invoice_or_item": "sales_invoice", "customer": "customer", "customer_group": "customer_group", + "customer_name": "customer_name", "posting_date": "posting_date", "item_code": "item_code", "item_name": "item_name", @@ -905,7 +921,7 @@ class GrossProfitGenerator: `tabSales Invoice Item`.parenttype, `tabSales Invoice Item`.parent, `tabSales Invoice`.posting_date, `tabSales Invoice`.posting_time, `tabSales Invoice`.project, `tabSales Invoice`.update_stock, - `tabSales Invoice`.customer, `tabSales Invoice`.customer_group, + `tabSales Invoice`.customer, `tabSales Invoice`.customer_group, `tabSales Invoice`.customer_name, `tabSales Invoice`.territory, `tabSales Invoice Item`.item_code, `tabSales Invoice`.base_net_total as "invoice_base_net_total", `tabSales Invoice Item`.item_name, `tabSales Invoice Item`.description, @@ -1003,6 +1019,7 @@ class GrossProfitGenerator: "update_stock": row.update_stock, "customer": row.customer, "customer_group": row.customer_group, + "customer_name": row.customer_name, "item_code": None, "item_name": None, "description": None, @@ -1032,6 +1049,7 @@ class GrossProfitGenerator: "project": row.project, "customer": row.customer, "customer_group": row.customer_group, + "customer_name": row.customer_name, "item_code": item.item_code, "item_name": item.item_name, "description": item.description, From d6e9216a451e74734e28f7624ef10dde317eca4a Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Wed, 30 Jul 2025 17:01:44 +0530 Subject: [PATCH 24/29] chore: code format (cherry picked from commit b0c0a86fcf060a2e029ed6aabc5d6878c365baf6) --- erpnext/accounts/report/gross_profit/gross_profit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 188e97ff3c0..add7a90b74b 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -255,6 +255,7 @@ def get_columns(group_wise_columns, filters): supplier_master_name = frappe.db.get_single_value("Buying Settings", "supp_master_name") customer_master_name = frappe.db.get_single_value("Selling Settings", "cust_master_name") + column_map = frappe._dict( { "parent": { From 0e7f778b3f50b6fdc81fadd1504087bfc3cbc540 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Wed, 30 Jul 2025 17:59:06 +0530 Subject: [PATCH 25/29] fix: show name of the employee in general ledger report (cherry picked from commit 70411ec086b2c826646ce0e66fd8533a3bc4dff6) --- erpnext/accounts/report/general_ledger/general_ledger.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index c4dd07d5341..94331ef1b92 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -351,6 +351,9 @@ def get_party_name_map(): suppliers = frappe.get_all("Supplier", fields=["name", "supplier_name"]) party_map["Supplier"] = {s.name: s.supplier_name for s in suppliers} + + employees = frappe.get_all("Employee", fields=["name", "employee_name"]) + party_map["Employee"] = {e.name: e.employee_name for e in employees} return party_map From 52db89f73fa6e663477edde2fd7e080758bc004c Mon Sep 17 00:00:00 2001 From: l0gesh29 Date: Mon, 11 Aug 2025 16:52:29 +0530 Subject: [PATCH 26/29] fix: table render issue on pop-up edit (cherry picked from commit 3431c6c90e47136bc4413976af7004156911ea59) --- erpnext/selling/doctype/sales_order/sales_order.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 28756fb1eb6..78b819dc46a 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -1219,6 +1219,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex read_only: 1, fieldname: "uom", label: __("UOM"), + options: "UOM", in_list_view: 1, }, { @@ -1292,7 +1293,6 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex let pending_qty = (flt(d.stock_qty) - ordered_qty) / flt(d.conversion_factor); if (pending_qty > 0) { po_items.push({ - doctype: "Sales Order Item", name: d.name, item_name: d.item_name, item_code: d.item_code, From 6ad3461953aa45ca44c33ec56fef7ed0c33af458 Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Mon, 11 Aug 2025 12:17:02 +0530 Subject: [PATCH 27/29] fix(process statement of accounts): use date instead of formatted date (cherry picked from commit aa3f50ab778318f0067cf40e8289424c2c934b23) --- .../process_statement_of_accounts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index bbf59aa7f02..76f1e573c41 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -550,7 +550,7 @@ def send_auto_email(): selected = frappe.get_list( "Process Statement Of Accounts", filters={"enable_auto_email": 1}, - or_filters={"to_date": format_date(today()), "posting_date": format_date(today())}, + or_filters={"to_date": today(), "posting_date": today()}, ) for entry in selected: send_emails(entry.name, from_scheduler=True) From 6e7fb2ea01275344e0cc0bb4d5065bb4d787bef7 Mon Sep 17 00:00:00 2001 From: Logesh Periyasamy Date: Tue, 5 Aug 2025 20:55:44 +0530 Subject: [PATCH 28/29] Merge pull request #48761 from aerele/exchange-gain-or-loss-on-repost fix: prevent gain or loss entry cancellation upon reposting (cherry picked from commit a8d17b7590265151c24550acfe14b7c2b2961306) --- .../repost_accounting_ledger.py | 4 +- .../doctype/sales_invoice/sales_invoice.py | 1 - .../sales_invoice/test_sales_invoice.py | 53 +++++++++++++++++++ erpnext/controllers/stock_controller.py | 5 +- 4 files changed, 58 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py index 37863036492..11614467472 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py @@ -178,7 +178,7 @@ def start_repost(account_repost_doc=str) -> None: if doc.doctype in ["Sales Invoice", "Purchase Invoice"]: if not repost_doc.delete_cancelled_entries: doc.docstatus = 2 - doc.make_gl_entries_on_cancel() + doc.make_gl_entries_on_cancel(from_repost=True) doc.docstatus = 1 if doc.doctype == "Sales Invoice": @@ -190,7 +190,7 @@ def start_repost(account_repost_doc=str) -> None: elif doc.doctype == "Purchase Receipt": if not repost_doc.delete_cancelled_entries: doc.docstatus = 2 - doc.make_gl_entries_on_cancel() + doc.make_gl_entries_on_cancel(from_repost=True) doc.docstatus = 1 doc.make_gl_entries(from_repost=True) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 97ded97dd2e..386d28fd804 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1206,7 +1206,6 @@ class SalesInvoice(SellingController): self.make_exchange_gain_loss_journal() elif self.docstatus == 2: - cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name)) make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) if update_outstanding == "No": diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 39faa24c16a..aeb266b6d87 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -4668,6 +4668,59 @@ class TestSalesInvoice(FrappeTestCase): doc.db_set("do_not_use_batchwise_valuation", original_value) + def test_system_generated_exchange_gain_or_loss_je_after_repost(self): + from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry + from erpnext.accounts.doctype.repost_accounting_ledger.test_repost_accounting_ledger import ( + update_repost_settings, + ) + + update_repost_settings() + + si = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable USD - _TC", + currency="USD", + conversion_rate=80, + ) + + pe = get_payment_entry("Sales Invoice", si.name) + pe.reference_no = "10" + pe.reference_date = nowdate() + pe.paid_from_account_currency = si.currency + pe.paid_to_account_currency = "INR" + pe.source_exchange_rate = 85 + pe.target_exchange_rate = 1 + pe.paid_amount = si.outstanding_amount + pe.insert() + pe.submit() + + ral = frappe.new_doc("Repost Accounting Ledger") + ral.company = si.company + ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name}) + ral.save() + ral.submit() + + je = frappe.qb.DocType("Journal Entry") + jea = frappe.qb.DocType("Journal Entry Account") + q = ( + ( + frappe.qb.from_(je) + .join(jea) + .on(je.name == jea.parent) + .select(je.docstatus) + .where( + (je.voucher_type == "Exchange Gain Or Loss") + & (jea.reference_name == si.name) + & (jea.reference_type == "Sales Invoice") + & (je.is_system_generated == 1) + ) + ) + .limit(1) + .run() + ) + + self.assertEqual(q[0][0], 1) + def make_item_for_si(item_code, properties=None): from erpnext.stock.doctype.item.test_item import make_item diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index e375c2c362c..2369c39f508 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -959,8 +959,9 @@ class StockController(AccountsController): make_sl_entries(sl_entries, allow_negative_stock, via_landed_cost_voucher) update_batch_qty(self.doctype, self.name, via_landed_cost_voucher=via_landed_cost_voucher) - def make_gl_entries_on_cancel(self): - cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name)) + def make_gl_entries_on_cancel(self, from_repost=False): + if not from_repost: + cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name)) if frappe.db.sql( """select name from `tabGL Entry` where voucher_type=%s and voucher_no=%s""", From 76b0f4fb2554e92688768c2e96fa6cc3895ebeed Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:43:43 +0000 Subject: [PATCH 29/29] perf: multiple performance fixes in `get_item_warehouse` (backport #49118) Co-authored-by: Sagar Vora <16315650+sagarvora@users.noreply.github.com> --- erpnext/setup/doctype/brand/brand.py | 2 +- erpnext/setup/doctype/item_group/item_group.py | 2 +- erpnext/stock/doctype/item/item.py | 2 +- erpnext/stock/get_item_details.py | 15 +++++---------- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/erpnext/setup/doctype/brand/brand.py b/erpnext/setup/doctype/brand/brand.py index cf3f4744391..ccb8b71e64f 100644 --- a/erpnext/setup/doctype/brand/brand.py +++ b/erpnext/setup/doctype/brand/brand.py @@ -35,7 +35,7 @@ def get_brand_defaults(item, company): for d in brand.brand_defaults or []: if d.company == company: - row = copy.deepcopy(d.as_dict()) + row = d.as_dict(no_private_properties=True) row.pop("name") return row diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 54f57e707af..f68f0eed493 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -90,7 +90,7 @@ def get_item_group_defaults(item, company): for d in item_group.item_group_defaults or []: if d.company == company: - row = copy.deepcopy(d.as_dict()) + row = d.as_dict(no_private_properties=True) row.pop("name") return row diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 3409e9a559a..0ea35ddc1fb 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -1281,7 +1281,7 @@ def get_item_defaults(item_code, company): for d in item.item_defaults: if d.company == company: - row = copy.deepcopy(d.as_dict()) + row = d.as_dict(no_private_properties=True) row.pop("name") out.update(row) return out diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 8d04141d78c..e31ba966653 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -567,20 +567,15 @@ def get_item_warehouse(item, args, overwrite_warehouse, defaults=None): or args.get("warehouse") ) - if not warehouse: - defaults = frappe.defaults.get_defaults() or {} - warehouse_exists = frappe.db.exists( - "Warehouse", {"name": defaults.default_warehouse, "company": args.company} - ) - if defaults.get("default_warehouse") and warehouse_exists: - warehouse = defaults.default_warehouse - else: warehouse = args.get("warehouse") if not warehouse: - default_warehouse = frappe.db.get_single_value("Stock Settings", "default_warehouse") - if frappe.db.get_value("Warehouse", default_warehouse, "company") == args.company: + default_warehouse = frappe.get_single_value("Stock Settings", "default_warehouse") + if ( + default_warehouse + and frappe.get_cached_value("Warehouse", default_warehouse, "company") == args.company + ): return default_warehouse return warehouse