From 5f06d87f01309a09dca4dad4e473a2388b404172 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 12 Aug 2025 20:27:41 +0530 Subject: [PATCH 01/45] feat: select child item when creating one document from another (cherry picked from commit a9936ae133aaadc1681fa061c23e2c07477fea60) --- .../purchase_invoice/purchase_invoice.js | 6 +++ .../purchase_invoice/purchase_invoice.py | 16 +++++++- .../doctype/sales_invoice/sales_invoice.js | 9 +++++ .../doctype/purchase_order/purchase_order.py | 34 ++++++++++++++--- .../selling/doctype/quotation/quotation.py | 38 +++++++++++++++---- .../doctype/sales_order/sales_order.js | 3 ++ .../doctype/sales_order/sales_order.py | 22 +++++++++-- .../doctype/delivery_note/delivery_note.js | 6 +++ .../doctype/delivery_note/delivery_note.py | 13 +++++++ erpnext/stock/doctype/pick_list/pick_list.py | 22 ++++++++--- .../purchase_receipt/purchase_receipt.js | 7 ++++ .../purchase_receipt/purchase_receipt.py | 19 ++++++++-- 12 files changed, 168 insertions(+), 27 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 5bf15ac325b..17fad2eca43 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -153,6 +153,9 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. per_billed: ["<", 99.99], company: me.frm.doc.company, }, + allow_child_item_selection: true, + child_fieldname: "items", + child_columns: ["item_code", "item_name", "qty", "amount", "billed_amt"], }); }, __("Get Items From") @@ -175,6 +178,9 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. company: me.frm.doc.company, is_return: 0, }, + allow_child_item_selection: true, + child_fieldname: "items", + child_columns: ["item_code", "item_name", "qty", "amount", "billed_amt"], }); }, __("Get Items From") diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index abbdd5523c8..4f0d6a64c36 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -2,6 +2,8 @@ # License: GNU General Public License v3. See license.txt +import json + import frappe from frappe import _, qb, throw from frappe.model.mapper import get_mapped_doc @@ -2070,7 +2072,12 @@ def make_inter_company_sales_invoice(source_name, target_doc=None): @frappe.whitelist() -def make_purchase_receipt(source_name, target_doc=None): +def make_purchase_receipt(source_name, target_doc=None, args=None): + if args is None: + args = {} + if isinstance(args, str): + args = json.loads(args) + def update_item(obj, target, source_parent): target.qty = flt(obj.qty) - flt(obj.received_qty) target.received_qty = flt(obj.qty) - flt(obj.received_qty) @@ -2080,6 +2087,11 @@ def make_purchase_receipt(source_name, target_doc=None): (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) * flt(source_parent.conversion_rate) ) + def select_item(d): + filtered_items = args.get("filtered_children", []) + child_filter = d.name in filtered_items if filtered_items else True + return child_filter + doc = get_mapped_doc( "Purchase Invoice", source_name, @@ -2103,7 +2115,7 @@ def make_purchase_receipt(source_name, target_doc=None): "wip_composite_asset": "wip_composite_asset", }, "postprocess": update_item, - "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty), + "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) and select_item(doc), }, "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges"}, }, diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index db9083a9f91..4978f95a132 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -259,6 +259,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( per_billed: ["<", 99.99], company: me.frm.doc.company, }, + allow_child_item_selection: true, + child_fieldname: "items", + child_columns: ["item_code", "item_name", "qty", "amount", "billed_amt"], }); }, __("Get Items From") @@ -288,6 +291,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( status: ["!=", "Lost"], company: me.frm.doc.company, }, + allow_child_item_selection: true, + child_fieldname: "items", + child_columns: ["item_code", "item_name", "qty", "rate", "amount"], }); }, __("Get Items From") @@ -319,6 +325,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( filters: filters, }; }, + allow_child_item_selection: true, + child_fieldname: "items", + child_columns: ["item_code", "item_name", "qty", "amount", "billed_amt"], }); }, __("Get Items From") diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 246690c3e39..0f4d7cf496c 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -724,7 +724,12 @@ def set_missing_values(source, target): @frappe.whitelist() -def make_purchase_receipt(source_name, target_doc=None): +def make_purchase_receipt(source_name, target_doc=None, args=None): + if args is None: + args = {} + if isinstance(args, str): + args = json.loads(args) + has_unit_price_items = frappe.db.get_value("Purchase Order", source_name, "has_unit_price_items") def is_unit_price_row(source): @@ -738,6 +743,11 @@ def make_purchase_receipt(source_name, target_doc=None): (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) * flt(source_parent.conversion_rate) ) + def select_item(d): + filtered_items = args.get("filtered_children", []) + child_filter = d.name in filtered_items if filtered_items else True + return child_filter + doc = get_mapped_doc( "Purchase Order", source_name, @@ -765,7 +775,8 @@ def make_purchase_receipt(source_name, target_doc=None): "condition": lambda doc: ( True if is_unit_price_row(doc) else abs(doc.received_qty) < abs(doc.qty) ) - and doc.delivered_by_supplier != 1, + and doc.delivered_by_supplier != 1 + and select_item(doc), }, "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True}, }, @@ -777,8 +788,8 @@ def make_purchase_receipt(source_name, target_doc=None): @frappe.whitelist() -def make_purchase_invoice(source_name, target_doc=None): - return get_mapped_purchase_invoice(source_name, target_doc) +def make_purchase_invoice(source_name, target_doc=None, args=None): + return get_mapped_purchase_invoice(source_name, target_doc, args=args) @frappe.whitelist() @@ -792,7 +803,12 @@ def make_purchase_invoice_from_portal(purchase_order_name): frappe.response.location = "/purchase-invoices/" + doc.name -def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions=False): +def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions=False, args=None): + if args is None: + args = {} + if isinstance(args, str): + args = json.loads(args) + def postprocess(source, target): target.flags.ignore_permissions = ignore_permissions set_missing_values(source, target) @@ -832,6 +848,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions or item_group.get("buying_cost_center") ) + def select_item(d): + filtered_items = args.get("filtered_children", []) + child_filter = d.name in filtered_items if filtered_items else True + return child_filter + fields = { "Purchase Order": { "doctype": "Purchase Invoice", @@ -854,7 +875,8 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions "wip_composite_asset": "wip_composite_asset", }, "postprocess": update_item, - "condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)), + "condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)) + and select_item(doc), }, "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True}, } diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 70084764799..d6b2fe73cac 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -2,6 +2,8 @@ # License: GNU General Public License v3. See license.txt +import json + import frappe from frappe import _ from frappe.model.mapper import get_mapped_doc @@ -347,7 +349,7 @@ def get_list_context(context=None): @frappe.whitelist() -def make_sales_order(source_name: str, target_doc=None): +def make_sales_order(source_name: str, target_doc=None, args=None): if not frappe.db.get_singles_value( "Selling Settings", "allow_sales_order_creation_for_expired_quotation" ): @@ -359,10 +361,15 @@ def make_sales_order(source_name: str, target_doc=None): ): frappe.throw(_("Validity period of this quotation has ended.")) - return _make_sales_order(source_name, target_doc) + return _make_sales_order(source_name, target_doc, args=args) -def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): +def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, args=None): + if args is None: + args = {} + if isinstance(args, str): + args = json.loads(args) + customer = _make_customer(source_name, ignore_permissions) ordered_items = get_ordered_items(source_name) @@ -430,6 +437,11 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): # Simple row return True + def select_item(d): + filtered_items = args.get("filtered_children", []) + child_filter = d.name in filtered_items if filtered_items else True + return child_filter + doclist = get_mapped_doc( "Quotation", source_name, @@ -439,7 +451,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): "doctype": "Sales Order Item", "field_map": {"parent": "prevdoc_docname", "name": "quotation_item"}, "postprocess": update_item, - "condition": can_map_row, + "condition": lambda d: can_map_row(d) and select_item(d), }, "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True}, "Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, @@ -476,11 +488,16 @@ def set_expired_status(): @frappe.whitelist() -def make_sales_invoice(source_name, target_doc=None): - return _make_sales_invoice(source_name, target_doc) +def make_sales_invoice(source_name, target_doc=None, args=None): + return _make_sales_invoice(source_name, target_doc, args=args) -def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): +def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, args=None): + if args is None: + args = {} + if isinstance(args, str): + args = json.loads(args) + customer = _make_customer(source_name, ignore_permissions) def set_missing_values(source, target): @@ -496,6 +513,11 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): target.cost_center = None target.stock_qty = flt(obj.qty) * flt(obj.conversion_factor) + def select_item(d): + filtered_items = args.get("filtered_children", []) + child_filter = d.name in filtered_items if filtered_items else True + return child_filter + doclist = get_mapped_doc( "Quotation", source_name, @@ -504,7 +526,7 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): "Quotation Item": { "doctype": "Sales Invoice Item", "postprocess": update_item, - "condition": lambda row: not row.is_alternative, + "condition": lambda row: not row.is_alternative and select_item(row), }, "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True}, "Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 78b819dc46a..e36f34b012f 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -793,6 +793,9 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex docstatus: 1, status: ["!=", "Lost"], }, + allow_child_item_selection: true, + child_fieldname: "items", + child_columns: ["item_code", "item_name", "qty", "rate", "amount"], }); setTimeout(() => { diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 9b14616373b..75260a5a3fa 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -977,6 +977,11 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None): def is_unit_price_row(source): return has_unit_price_items and source.qty == 0 + def select_item(d): + filtered_items = kwargs.get("filtered_children", []) + child_filter = d.name in filtered_items if filtered_items else True + return child_filter + def set_missing_values(source, target): if kwargs.get("ignore_pricing_rule"): # Skip pricing rule when the dn is creating from the pick list @@ -1042,7 +1047,7 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None): "name": "so_detail", "parent": "against_sales_order", }, - "condition": condition, + "condition": lambda d: condition(d) and select_item(d), "postprocess": update_item, } @@ -1098,7 +1103,12 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None): @frappe.whitelist() -def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): +def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, args=None): + if args is None: + args = {} + if isinstance(args, str): + args = json.loads(args) + # 0 qty is accepted, as the qty is uncertain for some items has_unit_price_items = frappe.db.get_value("Sales Order", source_name, "has_unit_price_items") @@ -1158,6 +1168,11 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): if cost_center: target.cost_center = cost_center + def select_item(d): + filtered_items = args.get("filtered_children", []) + child_filter = d.name in filtered_items if filtered_items else True + return child_filter + doclist = get_mapped_doc( "Sales Order", source_name, @@ -1182,7 +1197,8 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): True if is_unit_price_row(doc) else (doc.qty and (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount))) - ), + ) + and select_item(doc), }, "Sales Taxes and Charges": { "doctype": "Sales Taxes and Charges", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 440e104abb6..0cd4e24ff93 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -182,6 +182,9 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends ( company: me.frm.doc.company, project: me.frm.doc.project || undefined, }, + allow_child_item_selection: true, + child_fieldname: "items", + child_columns: ["item_code", "item_name", "qty", "delivered_qty"], }); }, __("Get Items From") @@ -231,6 +234,9 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends ( }, get_query_method: "erpnext.stock.doctype.pick_list.pick_list.get_pick_list_query", size: "extra-large", + allow_child_item_selection: true, + child_fieldname: "locations", + child_columns: ["item_code", "item_name", "stock_qty", "delivered_qty"], }); }, __("Get Items From") diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 762664bd566..6cadbdc5e47 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -2,6 +2,8 @@ # License: GNU General Public License v3. See license.txt +import json + import frappe from frappe import _ from frappe.contacts.doctype.address.address import get_company_address @@ -824,6 +826,11 @@ def get_returned_qty_map(delivery_note): @frappe.whitelist() def make_sales_invoice(source_name, target_doc=None, args=None): + if args is None: + args = {} + if isinstance(args, str): + args = json.loads(args) + doc = frappe.get_doc("Delivery Note", source_name) to_make_invoice_qty_map = {} @@ -875,6 +882,11 @@ def make_sales_invoice(source_name, target_doc=None, args=None): return pending_qty + def select_item(d): + filtered_items = args.get("filtered_children", []) + child_filter = d.name in filtered_items if filtered_items else True + return child_filter + doc = get_mapped_doc( "Delivery Note", source_name, @@ -897,6 +909,7 @@ def make_sales_invoice(source_name, target_doc=None, args=None): "filter": lambda d: get_pending_qty(d) <= 0 if not doc.get("is_return") else get_pending_qty(d) > 0, + "condition": select_item, }, "Sales Taxes and Charges": { "doctype": "Sales Taxes and Charges", diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 2420c166161..9417751f682 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -1252,11 +1252,16 @@ def create_dn_wo_so(pick_list, delivery_note=None): @frappe.whitelist() def create_dn_for_pick_lists(source_name, target_doc=None, kwargs=None): """Get Items from Multiple Pick Lists and create a Delivery Note for filtered customer""" + if kwargs is None: + kwargs = {} + if isinstance(kwargs, str): + kwargs = json.loads(kwargs) + pick_list = frappe.get_doc("Pick List", source_name) validate_item_locations(pick_list) - sales_order_arg = kwargs.get("sales_order") if kwargs else None - customer_arg = kwargs.get("customer") if kwargs else None + sales_order_arg = kwargs.get("sales_order") + customer_arg = kwargs.get("customer") if sales_order_arg: sales_orders = {sales_order_arg} @@ -1270,7 +1275,7 @@ def create_dn_for_pick_lists(source_name, target_doc=None, kwargs=None): pluck="name", ) - delivery_note = create_dn_from_so(pick_list, sales_orders, delivery_note=target_doc) + delivery_note = create_dn_from_so(pick_list, sales_orders, delivery_note=target_doc, kwargs=kwargs) if not sales_order_arg and not all(item.sales_order for item in pick_list.locations): if isinstance(delivery_note, str): @@ -1296,10 +1301,15 @@ def create_dn_with_so(sales_dict, pick_list): return delivery_note -def create_dn_from_so(pick_list, sales_order_list, delivery_note=None): +def create_dn_from_so(pick_list, sales_order_list, delivery_note=None, kwargs=None): if not sales_order_list: return delivery_note + def select_item(d): + filtered_items = kwargs.get("filtered_children", []) + child_filter = d.name in filtered_items if filtered_items else True + return child_filter + item_table_mapper = { "doctype": "Delivery Note Item", "field_map": { @@ -1307,7 +1317,9 @@ def create_dn_from_so(pick_list, sales_order_list, delivery_note=None): "name": "so_detail", "parent": "against_sales_order", }, - "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier != 1, + "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) + and doc.delivered_by_supplier != 1 + and select_item(doc), } kwargs = {"skip_item_mapping": True, "ignore_pricing_rule": pick_list.ignore_pricing_rule} diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index bcecf8be14d..db065a80c92 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -150,7 +150,11 @@ frappe.ui.form.on("Purchase Receipt", { docstatus: 1, per_received: ["<", 100], company: frm.doc.company, + update_stock: 0, }, + allow_child_item_selection: true, + child_fieldname: "items", + child_columns: ["item_code", "item_name", "qty", "received_qty"], }); }, __("Get Items From") @@ -255,6 +259,9 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend per_received: ["<", 99.99], company: me.frm.doc.company, }, + allow_child_item_selection: true, + child_fieldname: "items", + child_columns: ["item_code", "item_name", "qty", "received_qty"], }); }, __("Get Items From") diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 9e7dea631a9..1c4d28c495e 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -2,6 +2,8 @@ # License: GNU General Public License v3. See license.txt +import json + import frappe from frappe import _, throw from frappe.desk.notifications import clear_doctype_notifications @@ -1225,6 +1227,11 @@ def get_item_wise_returned_qty(pr_doc): @frappe.whitelist() def make_purchase_invoice(source_name, target_doc=None, args=None): + if args is None: + args = {} + if isinstance(args, str): + args = json.loads(args) + from erpnext.accounts.party import get_payment_terms_template doc = frappe.get_doc("Purchase Receipt", source_name) @@ -1279,6 +1286,11 @@ def make_purchase_invoice(source_name, target_doc=None, args=None): return pending_qty, returned_qty + def select_item(d): + filtered_items = args.get("filtered_children", []) + child_filter = d.name in filtered_items if filtered_items else True + return child_filter + doclist = get_mapped_doc( "Purchase Receipt", source_name, @@ -1308,9 +1320,10 @@ def make_purchase_invoice(source_name, target_doc=None, args=None): "wip_composite_asset": "wip_composite_asset", }, "postprocess": update_item, - "filter": lambda d: get_pending_qty(d)[0] <= 0 - if not doc.get("is_return") - else get_pending_qty(d)[0] > 0, + "filter": lambda d: ( + get_pending_qty(d)[0] <= 0 if not doc.get("is_return") else get_pending_qty(d)[0] > 0 + ) + and select_item(d), }, "Purchase Taxes and Charges": { "doctype": "Purchase Taxes and Charges", From 15a104b0f8ba08256ac2b5d74386cd09ce357710 Mon Sep 17 00:00:00 2001 From: Karm Soni Date: Wed, 13 Aug 2025 12:55:14 +0530 Subject: [PATCH 02/45] chore: add default value for posting_time field in subcontracting receipt (cherry picked from commit b7470617e0d2349813d11e37a3514c158a9a6500) --- .../doctype/subcontracting_receipt/subcontracting_receipt.json | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json index b8bd95bcbca..e975f384c25 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json @@ -152,6 +152,7 @@ "width": "100px" }, { + "default": "Now", "description": "Time at which materials were received", "fieldname": "posting_time", "fieldtype": "Time", From 4187e60b07d5eb06d7cc7990e70ab88044c0c070 Mon Sep 17 00:00:00 2001 From: Rehan Ansari Date: Sun, 10 Aug 2025 00:40:52 +0530 Subject: [PATCH 03/45] fix: add validation for draft PR/PI in Asset (cherry picked from commit 4cf481cca8ca00aa25eee1dff99ab0ec20e266b8) --- erpnext/assets/doctype/asset/asset.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 79e266d14c2..6726748a008 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -128,6 +128,7 @@ class Asset(AccountsController): self.validate_item() self.validate_cost_center() self.set_missing_values() + self.validate_linked_purchase_docs() self.validate_gross_and_purchase_amount() self.validate_finance_books() @@ -409,6 +410,21 @@ class Asset(AccountsController): if self.available_for_use_date and getdate(self.available_for_use_date) < getdate(self.purchase_date): frappe.throw(_("Available-for-use Date should be after purchase date")) + def validate_linked_purchase_docs(self): + for doctype_field, doctype_name in [ + ("purchase_receipt", "Purchase Receipt"), + ("purchase_invoice", "Purchase Invoice"), + ]: + linked_doc = getattr(self, doctype_field, None) + if linked_doc: + docstatus = frappe.db.get_value(doctype_name, linked_doc, "docstatus") + if docstatus == 0: + frappe.throw( + _("{0} is still in Draft. Please submit it before saving the Asset.").format( + get_link_to_form(doctype_name, linked_doc) + ) + ) + def validate_gross_and_purchase_amount(self): if self.is_existing_asset: return From 9c5dacd97793f47d5b8a7f9362ba0d38baff3111 Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Wed, 13 Aug 2025 16:38:30 +0530 Subject: [PATCH 04/45] refactor: validate linked purchase docs before field usage (cherry picked from commit 4a48b1371546d531b8180d2cb2bf8378456475e5) --- erpnext/assets/doctype/asset/asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 6726748a008..2741a881f8d 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -122,13 +122,13 @@ class Asset(AccountsController): def validate(self): self.validate_category() self.validate_precision() + self.validate_linked_purchase_docs() self.set_purchase_doc_row_item() self.validate_asset_values() self.validate_asset_and_reference() self.validate_item() self.validate_cost_center() self.set_missing_values() - self.validate_linked_purchase_docs() self.validate_gross_and_purchase_amount() self.validate_finance_books() From 3ecb09ae523c76fccf2a69ae924fc28d3ab57b56 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 13 Aug 2025 23:38:51 +0530 Subject: [PATCH 05/45] fix: current qty for batch in stock reco (cherry picked from commit 817e719cc2674ad98ad53167f2ae6aeb2432f672) --- .../stock/doctype/stock_reconciliation/stock_reconciliation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index cf789d5bca2..ec54c9dc92e 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -1478,6 +1478,7 @@ def get_stock_balance_for( posting_time=posting_time, for_stock_levels=True, consider_negative_batches=True, + do_not_check_future_batches=True, ) or 0 ) From 7a04bf85bcc4a8d0a83f1c424f022ea3293a9697 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 13 Aug 2025 23:48:30 +0530 Subject: [PATCH 06/45] chore: convert message to toast notification (cherry picked from commit fc710011107f92e409665a768a4af6cc2633347b) --- erpnext/manufacturing/doctype/bom_creator/bom_creator.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py index 6e778266b03..4761e82a048 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py @@ -473,7 +473,12 @@ def get_parent_row_no(doc, name): if row.name == name: return row.idx - frappe.msgprint(_("Parent Row No not found for {0}").format(name)) + if name == doc.name: + return None + + frappe.msgprint(_("Parent Row No not found for {0}").format(name), alert=True) + + return None @frappe.whitelist() From ad052d72d73b027ee160c26f6f4a47e6bd4a607e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 13:20:01 +0530 Subject: [PATCH 07/45] feat: enhance barcode scanner to support warehouse scanning (backport #48865) (#49162) Co-authored-by: Sagar Vora <16315650+sagarvora@users.noreply.github.com> Co-authored-by: Soni Karm <93865733+karm1000@users.noreply.github.com> --- .../doctype/pos_invoice/pos_invoice.json | 8 + .../purchase_invoice/purchase_invoice.json | 8 + .../doctype/sales_invoice/sales_invoice.json | 8 + .../purchase_order/purchase_order.json | 12 +- erpnext/public/js/controllers/transaction.js | 4 +- erpnext/public/js/utils/barcode_scanner.js | 139 ++++++++++++++++-- erpnext/public/scss/erpnext.scss | 8 + .../selling/doctype/quotation/quotation.json | 12 +- .../doctype/sales_order/sales_order.json | 8 + .../doctype/delivery_note/delivery_note.json | 8 + .../material_request/material_request.json | 17 ++- .../purchase_receipt/purchase_receipt.json | 8 + .../stock/doctype/stock_entry/stock_entry.js | 10 +- .../doctype/stock_entry/stock_entry.json | 8 + .../stock_reconciliation.js | 4 +- .../stock_reconciliation.json | 8 + erpnext/stock/tests/test_utils.py | 41 ++++++ erpnext/stock/utils.py | 53 +++++-- 18 files changed, 318 insertions(+), 46 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index b96e29979a8..5870cd9c9da 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -62,6 +62,7 @@ "items_section", "update_stock", "scan_barcode", + "last_scanned_warehouse", "items", "pricing_rule_details", "pricing_rules", @@ -1569,6 +1570,13 @@ "label": "Company Contact Person", "options": "Contact", "print_hide": 1 + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "icon": "fa fa-file-text", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 83e704734ce..ca31f1de4ec 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -47,6 +47,7 @@ "ignore_pricing_rule", "sec_warehouse", "scan_barcode", + "last_scanned_warehouse", "col_break_warehouse", "update_stock", "set_warehouse", @@ -1644,6 +1645,13 @@ "label": "Select Dispatch Address ", "options": "Address", "print_hide": 1 + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "grid_page_length": 50, diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 07b97920ae2..816a6bfeded 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -45,6 +45,7 @@ "items_section", "scan_barcode", "update_stock", + "last_scanned_warehouse", "column_break_39", "set_warehouse", "set_target_warehouse", @@ -2177,6 +2178,13 @@ "label": "Company Contact Person", "options": "Contact", "print_hide": 1 + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "grid_page_length": 50, diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 59b44a22e61..cfea482d217 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -41,8 +41,9 @@ "ignore_pricing_rule", "before_items_section", "scan_barcode", - "set_from_warehouse", + "last_scanned_warehouse", "items_col_break", + "set_from_warehouse", "set_warehouse", "items_section", "items", @@ -1294,6 +1295,13 @@ "hidden": 1, "label": "Has Unit Price Items", "no_copy": 1 + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "grid_page_length": 50, @@ -1301,7 +1309,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2025-04-09 16:54:08.836106", + "modified": "2025-07-31 17:19:40.816883", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index c9c589b6599..cab3d48c7b7 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -6,6 +6,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe setup() { super.setup(); let me = this; + this.barcode_scanner = new erpnext.utils.BarcodeScanner({ frm: this.frm }); this.set_fields_onload_for_line_item(); this.frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"]; @@ -473,8 +474,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe scan_barcode() { frappe.flags.dialog_set = false; - const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm}); - barcode_scanner.process_scan(); + this.barcode_scanner.process_scan(); } barcode(doc, cdt, cdn) { diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js index d6ef7944cee..0719e5ed99f 100644 --- a/erpnext/public/js/utils/barcode_scanner.js +++ b/erpnext/public/js/utils/barcode_scanner.js @@ -12,6 +12,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { this.batch_no_field = opts.batch_no_field || "batch_no"; this.uom_field = opts.uom_field || "uom"; this.qty_field = opts.qty_field || "qty"; + this.warehouse_field = opts.warehouse_field || "warehouse"; // field name on row which defines max quantity to be scanned e.g. picklist this.max_qty_field = opts.max_qty_field; // scanner won't add a new row if this flag is set. @@ -20,7 +21,6 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { this.prompt_qty = opts.prompt_qty; this.items_table_name = opts.items_table_name || "items"; - this.items_table = this.frm.doc[this.items_table_name]; // optional sound name to play when scan either fails or passes. // see https://frappeframework.com/docs/v14/user/en/python-api/hooks#sounds @@ -34,8 +34,10 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { // batch_no: "LOT12", // present if batch was scanned // serial_no: "987XYZ", // present if serial no was scanned // uom: "Kg", // present if barcode UOM is different from default + // warehouse: "Store-001", // present if warehouse was found (location-first scanning) // } this.scan_api = opts.scan_api || "erpnext.stock.utils.scan_barcode"; + this.has_last_scanned_warehouse = frappe.meta.has_field(this.frm.doctype, "last_scanned_warehouse"); } process_scan() { @@ -50,14 +52,31 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { this.scan_api_call(input, (r) => { const data = r && r.message; - if (!data || Object.keys(data).length === 0) { - this.show_alert(__("Cannot find Item with this Barcode"), "red"); + if ( + !data || + Object.keys(data).length === 0 || + (data.warehouse && !this.has_last_scanned_warehouse) + ) { + this.show_alert( + this.has_last_scanned_warehouse + ? __("Cannot find Item or Warehouse with this Barcode") + : __("Cannot find Item with this Barcode"), + "red" + ); this.clean_up(); this.play_fail_sound(); reject(); return; } + // Handle warehouse scanning + if (data.warehouse) { + this.handle_warehouse_scan(data); + this.play_success_sound(); + resolve(); + return; + } + me.update_table(data) .then((row) => { this.play_success_sound(); @@ -77,6 +96,10 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { method: this.scan_api, args: { search_value: input, + ctx: { + set_warehouse: this.frm.doc.set_warehouse, + company: this.frm.doc.company, + }, }, }) .then((r) => { @@ -89,11 +112,14 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { let cur_grid = this.frm.fields_dict[this.items_table_name].grid; frappe.flags.trigger_from_barcode_scanner = true; - const { item_code, barcode, batch_no, serial_no, uom } = data; + const { item_code, barcode, batch_no, serial_no, uom, default_warehouse } = data; - let row = this.get_row_to_modify_on_scan(item_code, batch_no, uom, barcode); + const warehouse = this.has_last_scanned_warehouse + ? this.frm.doc.last_scanned_warehouse || default_warehouse + : null; - this.is_new_row = false; + let row = this.get_row_to_modify_on_scan(item_code, batch_no, uom, barcode, warehouse); + const is_new_row = !row?.item_code; if (!row) { if (this.dont_allow_new_row) { this.show_alert(__("Maximum quantity scanned for item {0}.", [item_code]), "red"); @@ -101,7 +127,6 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { reject(); return; } - this.is_new_row = true; // add new row if new item/batch is scanned row = frappe.model.add_child(this.frm.doc, cur_grid.doctype, this.items_table_name); @@ -120,12 +145,13 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { () => this.set_selector_trigger_flag(data), () => this.set_item(row, item_code, barcode, batch_no, serial_no).then((qty) => { - this.show_scan_message(row.idx, row.item_code, qty); + this.show_scan_message(row.idx, !is_new_row, qty); }), () => this.set_barcode_uom(row, uom), () => this.set_serial_no(row, serial_no), () => this.set_batch_no(row, batch_no), () => this.set_barcode(row, barcode), + () => this.set_warehouse(row, warehouse), () => this.clean_up(), () => this.revert_selector_flag(), () => resolve(row), @@ -386,9 +412,17 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { } } - show_scan_message(idx, exist = null, qty = 1) { + async set_warehouse(row, warehouse) { + const warehouse_field = this.get_warehouse_field(); + + if (warehouse && frappe.meta.has_field(row.doctype, warehouse_field)) { + await frappe.model.set_value(row.doctype, row.name, warehouse_field, warehouse); + } + } + + show_scan_message(idx, is_existing_row = false, qty = 1) { // show new row or qty increase toast - if (exist) { + if (is_existing_row) { this.show_alert(__("Row #{0}: Qty increased by {1}", [idx, qty]), "green"); } else { this.show_alert(__("Row #{0}: Item added", [idx]), "green"); @@ -404,13 +438,16 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { return is_duplicate; } - get_row_to_modify_on_scan(item_code, batch_no, uom, barcode) { + get_row_to_modify_on_scan(item_code, batch_no, uom, barcode, warehouse) { let cur_grid = this.frm.fields_dict[this.items_table_name].grid; // Check if batch is scanned and table has batch no field let is_batch_no_scan = batch_no && frappe.meta.has_field(cur_grid.doctype, this.batch_no_field); let check_max_qty = this.max_qty_field && frappe.meta.has_field(cur_grid.doctype, this.max_qty_field); + const warehouse_field = this.get_warehouse_field(); + let has_warehouse_field = frappe.meta.has_field(cur_grid.doctype, warehouse_field); + const matching_row = (row) => { const item_match = row.item_code == item_code; const batch_match = !row[this.batch_no_field] || row[this.batch_no_field] == batch_no; @@ -418,20 +455,94 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { const qty_in_limit = flt(row[this.qty_field]) < flt(row[this.max_qty_field]); const item_scanned = row.has_item_scanned; + let warehouse_match = true; + if (has_warehouse_field) { + if (warehouse) { + warehouse_match = row[warehouse_field] === warehouse; + } else { + warehouse_match = !row[warehouse_field]; + } + } + return ( item_match && uom_match && + warehouse_match && !item_scanned && (!is_batch_no_scan || batch_match) && (!check_max_qty || qty_in_limit) ); }; - return this.items_table.find(matching_row) || this.get_existing_blank_row(); + const items_table = this.frm.doc[this.items_table_name] || []; + + return items_table.find(matching_row) || items_table.find((d) => !d.item_code); } - get_existing_blank_row() { - return this.items_table.find((d) => !d.item_code); + setup_last_scanned_warehouse() { + this.frm.set_df_property("last_scanned_warehouse", "options", "Warehouse"); + this.frm.set_df_property("last_scanned_warehouse", "fieldtype", "Link"); + this.frm.set_df_property("last_scanned_warehouse", "formatter", function (value, df, options, doc) { + const link_formatter = frappe.form.get_formatter(df.fieldtype); + const link_value = link_formatter(value, df, options, doc); + + if (!value) { + return link_value; + } + + const clear_btn = ` + + ${frappe.utils.icon("close", "xs", "es-icon")} + + `; + + return link_value + clear_btn; + }); + + this.frm.$wrapper.on("click", ".btn-clear-last-scanned-warehouse", (e) => { + e.preventDefault(); + e.stopPropagation(); + this.clear_warehouse_context(); + }); + } + + handle_warehouse_scan(data) { + const warehouse = data.warehouse; + const warehouse_field = this.get_warehouse_field(); + const warehouse_field_label = frappe.meta.get_label(this.items_table_name, warehouse_field); + + if (!this.last_scanned_warehouse_initialized) { + this.setup_last_scanned_warehouse(); + this.last_scanned_warehouse_initialized = true; + } + + this.frm.set_value("last_scanned_warehouse", warehouse); + this.show_alert( + __("{0} will be set as the {1} in subsequently scanned items", [ + __(warehouse).bold(), + __(warehouse_field_label).bold(), + ]), + "green", + 6 + ); + } + + clear_warehouse_context() { + this.frm.set_value("last_scanned_warehouse", null); + this.show_alert( + __( + "The last scanned warehouse has been cleared and won't be set in the subsequently scanned items" + ), + "blue", + 6 + ); + } + + get_warehouse_field() { + if (typeof this.warehouse_field === "function") { + return this.warehouse_field(this.frm.doc); + } + return this.warehouse_field; } play_success_sound() { diff --git a/erpnext/public/scss/erpnext.scss b/erpnext/public/scss/erpnext.scss index 29a2696470f..d17e4f416a4 100644 --- a/erpnext/public/scss/erpnext.scss +++ b/erpnext/public/scss/erpnext.scss @@ -593,3 +593,11 @@ body[data-route="pos"] { .frappe-control[data-fieldname="other_charges_calculation"] .ql-editor { white-space: normal; } + +.btn-clear-last-scanned-warehouse { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + z-index: 1; +} diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index b3aaae99c4d..ae5b980bb9b 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -33,6 +33,7 @@ "ignore_pricing_rule", "items_section", "scan_barcode", + "last_scanned_warehouse", "items", "sec_break23", "total_qty", @@ -1094,13 +1095,20 @@ "hidden": 1, "label": "Has Unit Price Items", "no_copy": 1 + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "icon": "fa fa-shopping-cart", "idx": 82, "is_submittable": 1, "links": [], - "modified": "2025-05-27 16:04:39.208077", + "modified": "2025-07-31 17:23:48.875382", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", @@ -1199,4 +1207,4 @@ "states": [], "timeline_field": "party_name", "title_field": "title" -} \ No newline at end of file +} diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index a3219cac8f0..1542721d117 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -41,6 +41,7 @@ "ignore_pricing_rule", "sec_warehouse", "scan_barcode", + "last_scanned_warehouse", "column_break_28", "set_warehouse", "reserve_stock", @@ -1657,6 +1658,13 @@ "hidden": 1, "label": "Has Unit Price Items", "no_copy": 1 + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "icon": "fa fa-file-text", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 3a9cca1c418..bdfbbb93916 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -38,6 +38,7 @@ "ignore_pricing_rule", "items_section", "scan_barcode", + "last_scanned_warehouse", "col_break_warehouse", "set_warehouse", "set_target_warehouse", @@ -1390,6 +1391,13 @@ "label": "Company Contact Person", "options": "Contact", "print_hide": 1 + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "icon": "fa fa-truck", diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index 60725b9ce9c..0c467df381c 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -20,9 +20,9 @@ "amended_from", "warehouse_section", "scan_barcode", - "column_break_13", - "set_from_warehouse", + "last_scanned_warehouse", "column_break5", + "set_from_warehouse", "set_warehouse", "items_section", "items", @@ -350,22 +350,25 @@ "fieldname": "column_break_35", "fieldtype": "Column Break" }, - { - "fieldname": "column_break_13", - "fieldtype": "Column Break" - }, { "fieldname": "buying_price_list", "fieldtype": "Link", "label": "Price List", "options": "Price List" + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "icon": "fa fa-ticket", "idx": 70, "is_submittable": 1, "links": [], - "modified": "2025-07-28 15:13:49.000037", + "modified": "2025-07-31 17:19:01.166208", "modified_by": "Administrator", "module": "Stock", "name": "Material Request", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index cef346a6d85..603f4a121d1 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -40,6 +40,7 @@ "ignore_pricing_rule", "sec_warehouse", "scan_barcode", + "last_scanned_warehouse", "column_break_31", "set_warehouse", "set_from_warehouse", @@ -1285,6 +1286,13 @@ "label": "Dispatch Address", "print_hide": 1, "read_only": 1 + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "grid_page_length": 50, diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 51455ef0d24..67918ee1dfd 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -1003,6 +1003,13 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle setup() { var me = this; + this.barcode_scanner = new erpnext.utils.BarcodeScanner({ + frm: this.frm, + warehouse_field: (doc) => { + return doc.purpose === "Material Transfer" ? "t_warehouse" : "s_warehouse"; + }, + }); + this.setup_posting_date_time_check(); this.frm.fields_dict.bom_no.get_query = function () { @@ -1130,8 +1137,7 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle scan_barcode() { frappe.flags.dialog_set = false; - const barcode_scanner = new erpnext.utils.BarcodeScanner({ frm: this.frm }); - barcode_scanner.process_scan(); + this.barcode_scanner.process_scan(); } on_submit() { diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index adec80dcab2..023dca5bdf2 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -50,6 +50,7 @@ "target_address_display", "sb0", "scan_barcode", + "last_scanned_warehouse", "items_section", "items", "get_stock_and_rate", @@ -691,6 +692,13 @@ "fieldtype": "Tab Break", "label": "Connections", "show_dashboard": 1 + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "icon": "fa fa-file-text", diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 44dd2952409..d8dd2a7560a 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -7,6 +7,7 @@ frappe.provide("erpnext.accounts.dimensions"); frappe.ui.form.on("Stock Reconciliation", { setup(frm) { frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"]; + frm.barcode_scanner = new erpnext.utils.BarcodeScanner({ frm }); }, onload: function (frm) { @@ -96,8 +97,7 @@ frappe.ui.form.on("Stock Reconciliation", { }, scan_barcode: function (frm) { - const barcode_scanner = new erpnext.utils.BarcodeScanner({ frm: frm }); - barcode_scanner.process_scan(); + frm.barcode_scanner.process_scan(); }, scan_mode: function (frm) { diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json index 4712b8aeb16..76f2691be72 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json @@ -18,6 +18,7 @@ "set_warehouse", "section_break_22", "scan_barcode", + "last_scanned_warehouse", "column_break_12", "scan_mode", "sb9", @@ -178,6 +179,13 @@ { "fieldname": "column_break_12", "fieldtype": "Column Break" + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "icon": "fa fa-upload-alt", diff --git a/erpnext/stock/tests/test_utils.py b/erpnext/stock/tests/test_utils.py index bc646fae45c..6e66c100466 100644 --- a/erpnext/stock/tests/test_utils.py +++ b/erpnext/stock/tests/test_utils.py @@ -81,3 +81,44 @@ class TestStockUtilities(FrappeTestCase, StockTestMixin): self.assertEqual(serial_scan["serial_no"], serial.name) self.assertEqual(serial_scan["has_batch_no"], 0) self.assertEqual(serial_scan["has_serial_no"], 1) + + def test_barcode_scanning_of_warehouse(self): + warehouse = frappe.get_doc( + { + "doctype": "Warehouse", + "warehouse_name": "Test Warehouse for Barcode", + "company": "_Test Company", + } + ).insert() + + warehouse_2 = frappe.get_doc( + { + "doctype": "Warehouse", + "warehouse_name": "Test Warehouse for Barcode 2", + "company": "_Test Company", + } + ).insert() + + warehouse_scan = scan_barcode(warehouse.name) + self.assertEqual(warehouse_scan["warehouse"], warehouse.name) + + item_with_warehouse = self.make_item( + properties={ + "item_defaults": [{"company": "_Test Company", "default_warehouse": warehouse.name}], + "barcodes": [{"barcode": "w12345"}], + } + ) + + item_scan = scan_barcode("w12345") + self.assertEqual(item_scan["item_code"], item_with_warehouse.name) + self.assertEqual(item_scan.get("default_warehouse"), None) + + ctx = {"company": "_Test Company"} + item_scan_with_ctx = scan_barcode("w12345", ctx=ctx) + self.assertEqual(item_scan_with_ctx["item_code"], item_with_warehouse.name) + self.assertEqual(item_scan_with_ctx["default_warehouse"], warehouse.name) + + ctx = {"company": "_Test Company", "set_warehouse": warehouse_2.name} + item_scan_with_ctx = scan_barcode("w12345", ctx=ctx) + self.assertEqual(item_scan_with_ctx["item_code"], item_with_warehouse.name) + self.assertEqual(item_scan_with_ctx["default_warehouse"], warehouse_2.name) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 637d56f093a..c863960d7a0 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -584,13 +584,24 @@ def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool @frappe.whitelist() -def scan_barcode(search_value: str) -> BarcodeScanResult: +def scan_barcode(search_value: str, ctx: dict | str | None = None) -> BarcodeScanResult: def set_cache(data: BarcodeScanResult): frappe.cache().set_value(f"erpnext:barcode_scan:{search_value}", data, expires_in_sec=120) + _update_item_info(data, ctx) def get_cache() -> BarcodeScanResult | None: - if data := frappe.cache().get_value(f"erpnext:barcode_scan:{search_value}"): - return data + data = frappe.cache().get_value(f"erpnext:barcode_scan:{search_value}") + if not data: + return + + _update_item_info(data, ctx) + return data + + if ctx is None: + ctx = frappe._dict() + + else: + ctx = frappe.parse_json(ctx) if scan_data := get_cache(): return scan_data @@ -603,7 +614,6 @@ def scan_barcode(search_value: str) -> BarcodeScanResult: as_dict=True, ) if barcode_data: - _update_item_info(barcode_data) set_cache(barcode_data) return barcode_data @@ -615,7 +625,6 @@ def scan_barcode(search_value: str) -> BarcodeScanResult: as_dict=True, ) if serial_no_data: - _update_item_info(serial_no_data) set_cache(serial_no_data) return serial_no_data @@ -634,22 +643,36 @@ def scan_barcode(search_value: str) -> BarcodeScanResult: ).format(search_value, batch_no_data.item_code) ) - _update_item_info(batch_no_data) set_cache(batch_no_data) return batch_no_data + warehouse = frappe.get_cached_value("Warehouse", search_value, ("name", "disabled"), as_dict=True) + if warehouse and not warehouse.disabled: + warehouse_data = {"warehouse": warehouse.name} + set_cache(warehouse_data) + return warehouse_data + return {} -def _update_item_info(scan_result: dict[str, str | None]) -> dict[str, str | None]: - if item_code := scan_result.get("item_code"): - if item_info := frappe.get_cached_value( - "Item", - item_code, - ["has_batch_no", "has_serial_no"], - as_dict=True, - ): - scan_result.update(item_info) +def _update_item_info(scan_result: dict[str, str | None], ctx: dict | None = None) -> dict[str, str | None]: + from erpnext.stock.get_item_details import get_item_warehouse + + item_code = scan_result.get("item_code") + if not item_code: + return scan_result + + if item_info := frappe.get_cached_value( + "Item", + item_code, + ("has_batch_no", "has_serial_no"), + as_dict=True, + ): + scan_result.update(item_info) + + if ctx and (warehouse := get_item_warehouse(frappe._dict(name=item_code), ctx, overwrite_warehouse=True)): + scan_result["default_warehouse"] = warehouse + return scan_result From 5ce0dc2a7a881fd7577c7363a22fabf4b58cf83a Mon Sep 17 00:00:00 2001 From: Lewis Date: Mon, 11 Aug 2025 15:27:18 -0400 Subject: [PATCH 08/45] fix(pos): include Product Bundle components in reserved qty to prevent overselling - Add `get_bundle_pos_reserved_qty` to account for component items in submitted POS Invoices - Update `get_pos_reserved_qty` to sum direct and bundle reservations - Remove double subtraction in `get_bundle_availability` to avoid underestimating bundle availability - Prevents overselling when multiple POS sessions sell bundles with shared components - Fixes #49021 (cherry picked from commit 984d744ac26ef5f5af776a296a3fb8310fec2716) --- .../doctype/pos_invoice/pos_invoice.py | 49 +++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index b0c69be4a1c..beccc7c6172 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -775,9 +775,8 @@ def get_bundle_availability(bundle_item_code, warehouse): for item in product_bundle.items: item_bin_qty = get_bin_qty(item.item_code, warehouse) item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse) - available_qty = item_bin_qty - item_pos_reserved_qty - max_available_bundles = available_qty / item.qty + max_available_bundles = item_bin_qty / item.qty if bundle_bin_qty > max_available_bundles and frappe.get_value( "Item", item.item_code, "is_stock_item" ): @@ -800,9 +799,27 @@ def get_bin_qty(item_code, warehouse): def get_pos_reserved_qty(item_code, warehouse): + """ + Calculate total quantity reserved for the given item and warehouse. + + Includes: + - Direct sales of the item in submitted POS Invoices + - Sales of the item as a component of a Product Bundle + + Excludes consolidated invoices (already merged into Sales Invoices via + POS Closing Entry). Used to reflect near real-time availability in the + POS UI and to prevent overselling while multiple sessions may be active. + """ + direct_reserved = get_direct_pos_reserved_qty(item_code, warehouse) + bundle_reserved = get_bundle_pos_reserved_qty(item_code, warehouse) + + return direct_reserved + bundle_reserved + +def get_direct_pos_reserved_qty(item_code, warehouse): + """Reserved qty for the item from direct lines in submitted POS Invoices (matching warehouse).""" + p_inv = frappe.qb.DocType("POS Invoice") p_item = frappe.qb.DocType("POS Invoice Item") - reserved_qty = ( frappe.qb.from_(p_inv) .from_(p_item) @@ -815,9 +832,33 @@ def get_pos_reserved_qty(item_code, warehouse): & (p_item.warehouse == warehouse) ) ).run(as_dict=True) - return flt(reserved_qty[0].stock_qty) if reserved_qty else 0 +def get_bundle_pos_reserved_qty(item_code, warehouse): + """Reserved qty for the item as a component of Product Bundles in submitted POS Invoices (matching warehouse).""" + + p_inv = frappe.qb.DocType("POS Invoice") + p_item = frappe.qb.DocType("POS Invoice Item") + pb = frappe.qb.DocType("Product Bundle") + pb_item = frappe.qb.DocType("Product Bundle Item") + + bundle_reserved = ( + frappe.qb.from_(p_inv) + .from_(p_item) + .from_(pb) + .from_(pb_item) + .select(Sum(p_item.stock_qty * pb_item.qty).as_("stock_qty")) + .where( + (p_inv.name == p_item.parent) + & (IfNull(p_inv.consolidated_invoice, "") == "") + & (p_item.docstatus == 1) + & (p_item.warehouse == warehouse) + & (pb.name == p_item.item_code) # POS item is a bundle + & (pb_item.parent == pb.name) # Bundle items + & (pb_item.item_code == item_code) # This specific item + ) + ).run(as_dict=True) + return flt(bundle_reserved[0].stock_qty) if bundle_reserved else 0 @frappe.whitelist() def make_sales_return(source_name, target_doc=None): From 0d793c11a121cf5e7fe08eb19d62f5f0aea2ee1a Mon Sep 17 00:00:00 2001 From: Lewis Mojica Date: Mon, 11 Aug 2025 16:06:08 -0400 Subject: [PATCH 09/45] chore: remove unused variable (cherry picked from commit 5a5804ca87cf14fb271798295df4eb6fc955656e) --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index beccc7c6172..d0ab67dcf03 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -774,7 +774,6 @@ def get_bundle_availability(bundle_item_code, warehouse): bundle_bin_qty = 1000000 for item in product_bundle.items: item_bin_qty = get_bin_qty(item.item_code, warehouse) - item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse) max_available_bundles = item_bin_qty / item.qty if bundle_bin_qty > max_available_bundles and frappe.get_value( From c9902eed72dfecc3965dff106df23fde29344904 Mon Sep 17 00:00:00 2001 From: Lewis Date: Tue, 12 Aug 2025 16:36:00 -0400 Subject: [PATCH 10/45] chore: apply pre-commit formatting (cherry picked from commit 0fc187adc3790dd1cc3c91cb7b1bce524cbce16c) --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index d0ab67dcf03..1c1a74a9d2d 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -814,6 +814,7 @@ def get_pos_reserved_qty(item_code, warehouse): return direct_reserved + bundle_reserved + def get_direct_pos_reserved_qty(item_code, warehouse): """Reserved qty for the item from direct lines in submitted POS Invoices (matching warehouse).""" @@ -833,6 +834,7 @@ def get_direct_pos_reserved_qty(item_code, warehouse): ).run(as_dict=True) return flt(reserved_qty[0].stock_qty) if reserved_qty else 0 + def get_bundle_pos_reserved_qty(item_code, warehouse): """Reserved qty for the item as a component of Product Bundles in submitted POS Invoices (matching warehouse).""" @@ -852,13 +854,14 @@ def get_bundle_pos_reserved_qty(item_code, warehouse): & (IfNull(p_inv.consolidated_invoice, "") == "") & (p_item.docstatus == 1) & (p_item.warehouse == warehouse) - & (pb.name == p_item.item_code) # POS item is a bundle - & (pb_item.parent == pb.name) # Bundle items - & (pb_item.item_code == item_code) # This specific item + & (pb.name == p_item.item_code) # POS item is a bundle + & (pb_item.parent == pb.name) # Bundle items + & (pb_item.item_code == item_code) # This specific item ) ).run(as_dict=True) return flt(bundle_reserved[0].stock_qty) if bundle_reserved else 0 + @frappe.whitelist() def make_sales_return(source_name, target_doc=None): from erpnext.controllers.sales_and_purchase_return import make_return_doc From 1f3d8e8d64b8e2919b18ac42804a454af4abe9c2 Mon Sep 17 00:00:00 2001 From: Lewis Date: Tue, 12 Aug 2025 17:17:08 -0400 Subject: [PATCH 11/45] fix(pos): populate packed_items table in POS Invoice (cherry picked from commit a65b200eb7856a5907f9a2af8b6cf1192ec753a5) --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 1c1a74a9d2d..96600114e0b 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -216,6 +216,7 @@ class POSInvoice(SalesInvoice): self.validate_loyalty_transaction() self.validate_company_with_pos_company() self.validate_full_payment() + self.update_packing_list() if self.coupon_code: from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code From 383744b8e40a1e3808bf37cc7df171082cc92dcc Mon Sep 17 00:00:00 2001 From: Lewis Date: Tue, 12 Aug 2025 18:53:09 -0400 Subject: [PATCH 12/45] fix: remove unclear message related to availability of product bundle (cherry picked from commit f5e5f7b588ce1f7bdb88575de6c565a40973132d) --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 96600114e0b..332ab14e41d 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -368,9 +368,9 @@ class POSInvoice(SalesInvoice): ) elif is_stock_item and flt(available_stock) < flt(d.stock_qty): frappe.throw( - _( - "Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}." - ).format(d.idx, item_code, warehouse, available_stock), + _("Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}.").format( + d.idx, item_code, warehouse + ), title=_("Item Unavailable"), ) From cc8283610943781fc8d64758be99c43903eca549 Mon Sep 17 00:00:00 2001 From: Lewis Date: Tue, 12 Aug 2025 20:10:39 -0400 Subject: [PATCH 13/45] fix(pos): use packed_items snapshot for bundle reservations Replaced live Product Bundle queries with `Packed Item` table lookups to ensure historical reservation accuracy. Addresses bundle qty underestimation and avoids errors when bundle definitions change after sale. Inspired by approach from @diptanilsaha in #49106. (cherry picked from commit d77d79e01173d1831b67e21e3850fc931e834f4d) --- .../doctype/pos_invoice/pos_invoice.py | 62 ++++++++----------- 1 file changed, 27 insertions(+), 35 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 332ab14e41d..bbeb88cc066 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -810,21 +810,39 @@ def get_pos_reserved_qty(item_code, warehouse): POS Closing Entry). Used to reflect near real-time availability in the POS UI and to prevent overselling while multiple sessions may be active. """ - direct_reserved = get_direct_pos_reserved_qty(item_code, warehouse) - bundle_reserved = get_bundle_pos_reserved_qty(item_code, warehouse) + pinv_item_reserved_qty = get_pos_reserved_qty_from_table("POS Invoice Item", item_code, warehouse) + packed_item_reserved_qty = get_pos_reserved_qty_from_table("Packed Item", item_code, warehouse) - return direct_reserved + bundle_reserved + reserved_qty = flt(pinv_item_reserved_qty[0].stock_qty) if pinv_item_reserved_qty else 0 + reserved_qty += flt(packed_item_reserved_qty[0].stock_qty) if packed_item_reserved_qty else 0 + + return reserved_qty -def get_direct_pos_reserved_qty(item_code, warehouse): - """Reserved qty for the item from direct lines in submitted POS Invoices (matching warehouse).""" +def get_pos_reserved_qty_from_table(child_table, item_code, warehouse): + """ + Get the total reserved quantity for a given item in POS Invoices + from a specific child table. + Args: + child_table (str): Name of the child table to query + (e.g., "POS Invoice Item", "Packed Item"). + item_code (str): The Item Code to filter by. + warehouse (str): The Warehouse to filter by. + + Returns: + float: The total reserved quantity for the item in the given + warehouse from submitted, unconsolidated POS Invoices. + """ p_inv = frappe.qb.DocType("POS Invoice") - p_item = frappe.qb.DocType("POS Invoice Item") - reserved_qty = ( + p_item = frappe.qb.DocType(child_table) + + qty_column = "qty" if child_table == "Packed Item" else "stock_qty" + + stock_qty = ( frappe.qb.from_(p_inv) .from_(p_item) - .select(Sum(p_item.stock_qty).as_("stock_qty")) + .select(Sum(p_item[qty_column]).as_("stock_qty")) .where( (p_inv.name == p_item.parent) & (IfNull(p_inv.consolidated_invoice, "") == "") @@ -833,34 +851,8 @@ def get_direct_pos_reserved_qty(item_code, warehouse): & (p_item.warehouse == warehouse) ) ).run(as_dict=True) - return flt(reserved_qty[0].stock_qty) if reserved_qty else 0 - -def get_bundle_pos_reserved_qty(item_code, warehouse): - """Reserved qty for the item as a component of Product Bundles in submitted POS Invoices (matching warehouse).""" - - p_inv = frappe.qb.DocType("POS Invoice") - p_item = frappe.qb.DocType("POS Invoice Item") - pb = frappe.qb.DocType("Product Bundle") - pb_item = frappe.qb.DocType("Product Bundle Item") - - bundle_reserved = ( - frappe.qb.from_(p_inv) - .from_(p_item) - .from_(pb) - .from_(pb_item) - .select(Sum(p_item.stock_qty * pb_item.qty).as_("stock_qty")) - .where( - (p_inv.name == p_item.parent) - & (IfNull(p_inv.consolidated_invoice, "") == "") - & (p_item.docstatus == 1) - & (p_item.warehouse == warehouse) - & (pb.name == p_item.item_code) # POS item is a bundle - & (pb_item.parent == pb.name) # Bundle items - & (pb_item.item_code == item_code) # This specific item - ) - ).run(as_dict=True) - return flt(bundle_reserved[0].stock_qty) if bundle_reserved else 0 + return stock_qty @frappe.whitelist() From f831d45cc3972162a8f118b9d2f88f0be8871454 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Thu, 14 Aug 2025 11:49:09 +0530 Subject: [PATCH 14/45] fix: product bundle child item quantity should be a positive number (cherry picked from commit 711076d02d7630517b714e8afe1e638d6710453d) --- .../selling/doctype/product_bundle/product_bundle.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py index 609f45a608f..5c981244d10 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/product_bundle.py @@ -32,6 +32,7 @@ class ProductBundle(Document): def validate(self): self.validate_main_item() self.validate_child_items() + self.validate_child_items_qty_non_zero() from erpnext.utilities.transaction_base import validate_uom_is_integer validate_uom_is_integer(self, "uom", "qty") @@ -88,6 +89,15 @@ class ProductBundle(Document): ).format(item.idx, frappe.bold(item.item_code)) ) + def validate_child_items_qty_non_zero(self): + for item in self.items: + if item.qty <= 0: + frappe.throw( + _( + "Row #{0}: Quantity cannot be a non-positive number. Please increase the quantity or remove the Item {1}" + ).format(item.idx, frappe.bold(item.item_code)) + ) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs From 93ad17ac7b69b516dd86466e7e5d677e3797cbbe Mon Sep 17 00:00:00 2001 From: Lewis Date: Wed, 13 Aug 2025 16:25:58 -0400 Subject: [PATCH 15/45] chore: improve code clarity per reviewer feedback - Rename stock_qty variable to reserved_qty for clarity - Update get_pos_reserved_qty_from_table to return float - Simplify aggregation logic in get_pos_reserved_qty - Ensure return values match docstring specifications (cherry picked from commit 54d3e5675f9d219729f38b0af8693066a734c088) --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index bbeb88cc066..4f5d599b006 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -813,8 +813,7 @@ def get_pos_reserved_qty(item_code, warehouse): pinv_item_reserved_qty = get_pos_reserved_qty_from_table("POS Invoice Item", item_code, warehouse) packed_item_reserved_qty = get_pos_reserved_qty_from_table("Packed Item", item_code, warehouse) - reserved_qty = flt(pinv_item_reserved_qty[0].stock_qty) if pinv_item_reserved_qty else 0 - reserved_qty += flt(packed_item_reserved_qty[0].stock_qty) if packed_item_reserved_qty else 0 + reserved_qty = pinv_item_reserved_qty + packed_item_reserved_qty return reserved_qty @@ -839,7 +838,7 @@ def get_pos_reserved_qty_from_table(child_table, item_code, warehouse): qty_column = "qty" if child_table == "Packed Item" else "stock_qty" - stock_qty = ( + reserved_qty = ( frappe.qb.from_(p_inv) .from_(p_item) .select(Sum(p_item[qty_column]).as_("stock_qty")) @@ -852,7 +851,7 @@ def get_pos_reserved_qty_from_table(child_table, item_code, warehouse): ) ).run(as_dict=True) - return stock_qty + return flt(reserved_qty[0].stock_qty) if reserved_qty else 0 @frappe.whitelist() From db486356db1579f7c60c77b43996f3dd7e6c9101 Mon Sep 17 00:00:00 2001 From: divyalekha99 <32547248+divyalekha99@users.noreply.github.com> Date: Thu, 14 Aug 2025 16:58:51 +0200 Subject: [PATCH 16/45] fix: wrap inter-company order button labels in __() for translation (#49178) * Updated purchase_order.js * Update sales_order.js (cherry picked from commit 078b8439d9db04db9af1ec62dda2ad156caf4897) --- erpnext/buying/doctype/purchase_order/purchase_order.js | 4 ++-- erpnext/selling/doctype/sales_order/sales_order.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 5bdc9900abf..dca3285f538 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -463,8 +463,8 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends ( if (internal) { let button_label = me.frm.doc.company === me.frm.doc.represents_company - ? "Internal Sales Order" - : "Inter Company Sales Order"; + ? __("Internal Sales Order") + : __("Inter Company Sales Order"); me.frm.add_custom_button( button_label, diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index e36f34b012f..f29cd253818 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -738,8 +738,8 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex if (internal) { let button_label = me.frm.doc.company === me.frm.doc.represents_company - ? "Internal Purchase Order" - : "Inter Company Purchase Order"; + ? __("Internal Purchase Order") + : __("Inter Company Purchase Order"); me.frm.add_custom_button( button_label, From e4398d3761668b78a1c8ed1ed4f7ca8cee22ca49 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sat, 16 Aug 2025 23:35:27 +0530 Subject: [PATCH 17/45] fix: additional cost not consider in valuation rate for Stock Entry transfer (cherry picked from commit bbc772abe7ad6b892e86da6b74d4c72345f21506) # Conflicts: # erpnext/stock/doctype/stock_entry/test_stock_entry.py --- erpnext/controllers/stock_controller.py | 1 + .../doctype/stock_entry/test_stock_entry.py | 134 ++++++++++++++++++ erpnext/stock/serial_batch_bundle.py | 18 ++- 3 files changed, 152 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 2369c39f508..8c2b4db3fc9 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -1854,6 +1854,7 @@ def make_bundle_for_material_transfer(**kwargs): row.warehouse = kwargs.warehouse + bundle_doc.set_incoming_rate() bundle_doc.calculate_qty_and_amount() bundle_doc.flags.ignore_permissions = True bundle_doc.flags.ignore_validate = True diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 68e57a3d971..042ec6abdf1 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -2021,7 +2021,141 @@ class TestStockEntry(FrappeTestCase): self.assertEqual(se.items[0].basic_rate, 300) +<<<<<<< HEAD def make_serialized_item(**args): +======= + company = "_Test Periodic Accounting Company" + + frappe.get_doc( + { + "doctype": "Company", + "company_name": company, + "abbr": "_TPC", + "default_currency": "INR", + "enable_perpetual_inventory": 0, + } + ).insert(ignore_permissions=True) + + warehouse = frappe.db.get_value("Warehouse", {"company": company, "is_group": 0}, "name") + + make_stock_entry( + item_code=item_code, + qty=10, + to_warehouse=warehouse, + basic_rate=100, + posting_date=add_days(nowdate(), -2), + ) + + jv = frappe.new_doc("Journal Entry") + jv.voucher_type = "Periodic Accounting Entry" + jv.posting_date = add_days(nowdate(), -1) + jv.posting_time = nowtime() + jv.company = company + jv.for_all_stock_asset_accounts = 1 + jv.periodic_entry_difference_account = "Stock Adjustment - _TPC" + jv.get_balance_for_periodic_accounting() + jv.save() + jv.submit() + + self.assertEqual(len(jv.accounts), 2) + self.assertEqual(jv.accounts[0].debit_in_account_currency, 1000) + self.assertEqual(jv.accounts[1].credit_in_account_currency, 1000) + self.assertEqual(jv.accounts[0].account, "Stock In Hand - _TPC") + self.assertEqual(jv.accounts[1].account, "Stock Adjustment - _TPC") + + make_stock_entry( + item_code=item_code, + qty=5, + from_warehouse=warehouse, + company=company, + posting_date=nowdate(), + posting_time=nowtime(), + ) + + jv = frappe.new_doc("Journal Entry") + jv.voucher_type = "Periodic Accounting Entry" + jv.posting_date = nowdate() + jv.posting_time = nowtime() + jv.company = company + jv.for_all_stock_asset_accounts = 1 + jv.periodic_entry_difference_account = "Stock Adjustment - _TPC" + jv.get_balance_for_periodic_accounting() + jv.save() + jv.submit() + + self.assertEqual(len(jv.accounts), 2) + self.assertEqual(jv.accounts[0].credit_in_account_currency, 500) + self.assertEqual(jv.accounts[1].debit_in_account_currency, 500) + self.assertEqual(jv.accounts[0].account, "Stock In Hand - _TPC") + self.assertEqual(jv.accounts[1].account, "Stock Adjustment - _TPC") + + def test_batch_item_additional_cost_for_material_transfer_entry(self): + item_code = "_Test Batch Item Additional Cost MTE" + make_item( + item_code, + { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_naming_series": "BT-MTE.#####", + }, + ) + + se = make_stock_entry( + item_code=item_code, + target="_Test Warehouse - _TC", + qty=2, + basic_rate=100, + use_serial_batch_fields=1, + ) + + batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + + se = make_stock_entry( + item_code=item_code, + source="_Test Warehouse - _TC", + target="_Test Warehouse 1 - _TC", + batch_no=batch_no, + use_serial_batch_fields=1, + qty=2, + purpose="Material Transfer", + do_not_save=True, + ) + + se.append( + "additional_costs", + { + "cost_center": "Main - _TC", + "amount": 50, + "expense_account": "Stock Adjustment - _TC", + "description": "Test Additional Cost", + }, + ) + se.save() + self.assertEqual(se.additional_costs[0].amount, 50) + self.assertEqual(se.items[0].basic_rate, 100) + self.assertEqual(se.items[0].valuation_rate, 125) + + se.submit() + self.assertEqual(se.items[0].basic_rate, 100) + self.assertEqual(se.items[0].valuation_rate, 125) + + incoming_rate = frappe.db.get_value( + "Stock Ledger Entry", + { + "item_code": item_code, + "warehouse": "_Test Warehouse 1 - _TC", + "voucher_type": "Stock Entry", + "voucher_no": se.name, + }, + "incoming_rate", + ) + + self.assertEqual(incoming_rate, 125.0) + + +def make_serialized_item(self, **args): +>>>>>>> bbc772abe7 (fix: additional cost not consider in valuation rate for Stock Entry transfer) args = frappe._dict(args) se = frappe.copy_doc(test_records[0]) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 203340a9ff5..172ee4edcf2 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -185,7 +185,23 @@ class SerialBatchBundle: } if self.sle.actual_qty < 0 and self.is_material_transfer(): - values_to_update["valuation_rate"] = flt(sn_doc.avg_rate) + basic_rate = flt(sn_doc.avg_rate) + ste_detail = frappe.db.get_value( + "Stock Entry Detail", + self.sle.voucher_detail_no, + ["additional_cost", "landed_cost_voucher_amount", "transfer_qty"], + as_dict=True, + ) + + additional_cost = 0.0 + + if ste_detail: + additional_cost = ( + flt(ste_detail.additional_cost) + flt(ste_detail.landed_cost_voucher_amount) + ) / flt(ste_detail.transfer_qty) + + values_to_update["basic_rate"] = basic_rate + values_to_update["valuation_rate"] = basic_rate + additional_cost if not frappe.db.get_single_value( "Stock Settings", "do_not_update_serial_batch_on_creation_of_auto_bundle" From 41e74634125acd92b50db497d0d8bfd3945533f7 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Sun, 17 Aug 2025 10:54:46 +0530 Subject: [PATCH 18/45] chore: fix conflicts --- .../doctype/stock_entry/test_stock_entry.py | 72 +------------------ 1 file changed, 1 insertion(+), 71 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 042ec6abdf1..cde5be4d6ee 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -2020,75 +2020,6 @@ class TestStockEntry(FrappeTestCase): self.assertEqual(se.items[0].basic_rate, 300) - -<<<<<<< HEAD -def make_serialized_item(**args): -======= - company = "_Test Periodic Accounting Company" - - frappe.get_doc( - { - "doctype": "Company", - "company_name": company, - "abbr": "_TPC", - "default_currency": "INR", - "enable_perpetual_inventory": 0, - } - ).insert(ignore_permissions=True) - - warehouse = frappe.db.get_value("Warehouse", {"company": company, "is_group": 0}, "name") - - make_stock_entry( - item_code=item_code, - qty=10, - to_warehouse=warehouse, - basic_rate=100, - posting_date=add_days(nowdate(), -2), - ) - - jv = frappe.new_doc("Journal Entry") - jv.voucher_type = "Periodic Accounting Entry" - jv.posting_date = add_days(nowdate(), -1) - jv.posting_time = nowtime() - jv.company = company - jv.for_all_stock_asset_accounts = 1 - jv.periodic_entry_difference_account = "Stock Adjustment - _TPC" - jv.get_balance_for_periodic_accounting() - jv.save() - jv.submit() - - self.assertEqual(len(jv.accounts), 2) - self.assertEqual(jv.accounts[0].debit_in_account_currency, 1000) - self.assertEqual(jv.accounts[1].credit_in_account_currency, 1000) - self.assertEqual(jv.accounts[0].account, "Stock In Hand - _TPC") - self.assertEqual(jv.accounts[1].account, "Stock Adjustment - _TPC") - - make_stock_entry( - item_code=item_code, - qty=5, - from_warehouse=warehouse, - company=company, - posting_date=nowdate(), - posting_time=nowtime(), - ) - - jv = frappe.new_doc("Journal Entry") - jv.voucher_type = "Periodic Accounting Entry" - jv.posting_date = nowdate() - jv.posting_time = nowtime() - jv.company = company - jv.for_all_stock_asset_accounts = 1 - jv.periodic_entry_difference_account = "Stock Adjustment - _TPC" - jv.get_balance_for_periodic_accounting() - jv.save() - jv.submit() - - self.assertEqual(len(jv.accounts), 2) - self.assertEqual(jv.accounts[0].credit_in_account_currency, 500) - self.assertEqual(jv.accounts[1].debit_in_account_currency, 500) - self.assertEqual(jv.accounts[0].account, "Stock In Hand - _TPC") - self.assertEqual(jv.accounts[1].account, "Stock Adjustment - _TPC") - def test_batch_item_additional_cost_for_material_transfer_entry(self): item_code = "_Test Batch Item Additional Cost MTE" make_item( @@ -2154,8 +2085,7 @@ def make_serialized_item(**args): self.assertEqual(incoming_rate, 125.0) -def make_serialized_item(self, **args): ->>>>>>> bbc772abe7 (fix: additional cost not consider in valuation rate for Stock Entry transfer) +def make_serialized_item(**args): args = frappe._dict(args) se = frappe.copy_doc(test_records[0]) From 016948804dd7e4f581e94c93d09f22d4cb1f7300 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Sun, 17 Aug 2025 10:55:54 +0530 Subject: [PATCH 19/45] chore: fix test case --- erpnext/stock/serial_batch_bundle.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 172ee4edcf2..21180accf83 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -189,16 +189,14 @@ class SerialBatchBundle: ste_detail = frappe.db.get_value( "Stock Entry Detail", self.sle.voucher_detail_no, - ["additional_cost", "landed_cost_voucher_amount", "transfer_qty"], + ["additional_cost", "transfer_qty"], as_dict=True, ) additional_cost = 0.0 if ste_detail: - additional_cost = ( - flt(ste_detail.additional_cost) + flt(ste_detail.landed_cost_voucher_amount) - ) / flt(ste_detail.transfer_qty) + additional_cost = flt(ste_detail.additional_cost) / flt(ste_detail.transfer_qty) values_to_update["basic_rate"] = basic_rate values_to_update["valuation_rate"] = basic_rate + additional_cost From e8490196ba0da51edc19ca718099064cbf515208 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 17 Aug 2025 13:01:58 +0530 Subject: [PATCH 20/45] Revert "fix: use checkout@v2 instead of v4" This reverts commit c9d69d9629c67cfae33a3570bc915939cc664e50. --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 66efc178b1c..5f0abc70c5b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Entire Repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 persist-credentials: false From 07ff97f64705e854e377fc7b0bc94ece91b9a188 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 17 Aug 2025 18:52:55 +0000 Subject: [PATCH 21/45] fix: ignore links in Dunning patch (backport #49201) (#49204) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> fix: ignore links in Dunning patch (#49201) --- erpnext/patches/v14_0/single_to_multi_dunning.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/patches/v14_0/single_to_multi_dunning.py b/erpnext/patches/v14_0/single_to_multi_dunning.py index 98be0204518..3def5922c07 100644 --- a/erpnext/patches/v14_0/single_to_multi_dunning.py +++ b/erpnext/patches/v14_0/single_to_multi_dunning.py @@ -48,6 +48,7 @@ def execute(): dunning.validate() dunning.flags.ignore_validate_update_after_submit = True + dunning.flags.ignore_links = True dunning.save() # Reverse entries only if dunning is submitted and not resolved From 1af8ab2a3b30ec7691659a4975cc0cb84a01a40d Mon Sep 17 00:00:00 2001 From: l0gesh29 Date: Thu, 14 Aug 2025 16:25:53 +0530 Subject: [PATCH 22/45] fix(asset): prevent translation function shadowing in make_journal_entry (cherry picked from commit 5e82de1b71ed5eb319a2a174e22045b215029815) --- erpnext/assets/doctype/asset/asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 2741a881f8d..04994acc6ab 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -1099,7 +1099,7 @@ def get_asset_account(account_name, asset=None, asset_category=None, company=Non def make_journal_entry(asset_name): asset = frappe.get_doc("Asset", asset_name) ( - _, + fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account, ) = get_depreciation_accounts(asset.asset_category, asset.company) From 605f513ce3536a1c33c1c70e2681b8995a89f4eb Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Mon, 18 Aug 2025 13:23:23 +0530 Subject: [PATCH 23/45] fix: apply grand total to default payment mode in Sales and POS invoices --- erpnext/accounts/doctype/pos_invoice/pos_invoice.js | 11 +++++++++++ erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 1 + .../accounts/doctype/sales_invoice/sales_invoice.js | 10 +++++++++- .../accounts/doctype/sales_invoice/sales_invoice.py | 1 + erpnext/public/js/controllers/taxes_and_totals.js | 7 +++++-- erpnext/selling/page/point_of_sale/pos_payment.js | 1 - 6 files changed, 27 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js index 2f170631226..b392e8045b5 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js @@ -55,6 +55,16 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex }); erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype); + + if (this.frm.doc.pos_profile) { + frappe.db + .get_value("POS Profile", this.frm.doc.pos_profile, "disable_grand_total_to_default_mop") + .then((r) => { + if (!r.exc) { + this.frm.skip_default_payment = r.message.disable_grand_total_to_default_mop; + } + }); + } } onload_post_render(frm) { @@ -113,6 +123,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex this.frm.meta.default_print_format = r.message.print_format || ""; this.frm.doc.campaign = r.message.campaign; this.frm.allow_print_before_pay = r.message.allow_print_before_pay; + this.frm.skip_default_payment = r.message.skip_default_payment; } this.frm.script_manager.trigger("update_stock"); this.calculate_taxes_and_totals(); diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 4f5d599b006..87360543efd 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -681,6 +681,7 @@ class POSInvoice(SalesInvoice): "print_format": print_format, "campaign": profile.get("campaign"), "allow_print_before_pay": profile.get("allow_print_before_pay"), + "skip_default_payment": profile.get("disable_grand_total_to_default_mop"), } @frappe.whitelist() diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 4978f95a132..a19850af8f1 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -58,6 +58,13 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( me.frm.script_manager.trigger("is_pos"); me.frm.refresh_fields(); + frappe.db + .get_value("POS Profile", this.frm.doc.pos_profile, "disable_grand_total_to_default_mop") + .then((r) => { + if (!r.exc) { + me.frm.skip_default_payment = r.message.disable_grand_total_to_default_mop; + } + }); } erpnext.queries.setup_warehouse_query(this.frm); } @@ -506,8 +513,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( }, callback: function (r) { if (!r.exc) { - if (r.message && r.message.print_format) { + if (r.message) { me.frm.pos_print_format = r.message.print_format; + me.frm.skip_default_payment = r.message.skip_default_payment; } me.frm.trigger("update_stock"); if (me.frm.doc.taxes_and_charges) { diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 386d28fd804..2de3feb9a35 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -700,6 +700,7 @@ class SalesInvoice(SellingController): "allow_edit_discount": pos.get("allow_user_to_edit_discount"), "campaign": pos.get("campaign"), "allow_print_before_pay": pos.get("allow_print_before_pay"), + "skip_default_payment": pos.get("disable_grand_total_to_default_mop"), } def update_time_sheet(self, sales_invoice): diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index be503a45d56..fe0110ce2cb 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -931,8 +931,11 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { set_default_payment(total_amount_to_pay, update_paid_amount) { var me = this; var payment_status = true; - if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)) { - + if ( + this.frm.doc.is_pos + && !cint(this.frm.skip_default_payment) + && (update_paid_amount===undefined || update_paid_amount) + ) { $.each(this.frm.doc['payments'] || [], function(index, data) { if(data.default && payment_status && total_amount_to_pay > 0) { let base_amount, amount; diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 59f293a96b0..92e9ad060df 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -342,7 +342,6 @@ erpnext.PointOfSale.Payment = class { } render_payment_section() { - this.remove_grand_total_from_default_mop(); this.render_payment_mode_dom(); this.make_invoice_fields_control(); this.update_totals_section(); From 9a12c73e22e9ecd440974d41eb896591e043100d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 11:51:24 +0000 Subject: [PATCH 24/45] fix: update dunning status based on credit notes (backport #49066) (#49208) Co-authored-by: Karm Soni Co-authored-by: Sagar Vora <16315650+sagarvora@users.noreply.github.com> --- erpnext/accounts/doctype/dunning/dunning.py | 89 ++++++++++++------- .../accounts/doctype/dunning/test_dunning.py | 58 ++++++++++++ .../doctype/payment_entry/payment_entry.py | 4 +- erpnext/accounts/utils.py | 4 + erpnext/hooks.py | 6 +- .../test_inventory_dimension.py | 4 +- 6 files changed, 127 insertions(+), 38 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 00ed85a4e0b..0f5831a967d 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -11,6 +11,7 @@ -> Resolves dunning automatically """ + import json import frappe @@ -156,40 +157,66 @@ class Dunning(AccountsController): ] -def resolve_dunning(doc, state): - """ - Check if all payments have been made and resolve dunning, if yes. Called - when a Payment Entry is submitted. - """ - for reference in doc.references: - # Consider partial and full payments: - # Submitting full payment: outstanding_amount will be 0 - # Submitting 1st partial payment: outstanding_amount will be the pending installment - # Cancelling full payment: outstanding_amount will revert to total amount - # Cancelling last partial payment: outstanding_amount will revert to pending amount - submit_condition = reference.outstanding_amount < reference.total_amount - cancel_condition = reference.outstanding_amount <= reference.total_amount +def update_linked_dunnings(doc, previous_outstanding_amount): + if ( + doc.doctype != "Sales Invoice" + or doc.is_return + or previous_outstanding_amount == doc.outstanding_amount + ): + return - if reference.reference_doctype == "Sales Invoice" and ( - submit_condition if doc.docstatus == 1 else cancel_condition - ): - state = "Resolved" if doc.docstatus == 2 else "Unresolved" - dunnings = get_linked_dunnings_as_per_state(reference.reference_name, state) + to_resolve = doc.outstanding_amount < previous_outstanding_amount + state = "Unresolved" if to_resolve else "Resolved" + dunnings = get_linked_dunnings_as_per_state(doc.name, state) + if not dunnings: + return - for dunning in dunnings: - resolve = True - dunning = frappe.get_doc("Dunning", dunning.get("name")) - for overdue_payment in dunning.overdue_payments: - outstanding_inv = frappe.get_value( - "Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount" - ) - outstanding_ps = frappe.get_value( - "Payment Schedule", overdue_payment.payment_schedule, "outstanding" - ) - resolve = False if (outstanding_ps > 0 and outstanding_inv > 0) else True + dunnings = [frappe.get_doc("Dunning", dunning.name) for dunning in dunnings] + invoices = set() + payment_schedule_ids = set() - dunning.status = "Resolved" if resolve else "Unresolved" - dunning.save() + for dunning in dunnings: + for overdue_payment in dunning.overdue_payments: + invoices.add(overdue_payment.sales_invoice) + if overdue_payment.payment_schedule: + payment_schedule_ids.add(overdue_payment.payment_schedule) + + invoice_outstanding_amounts = dict( + frappe.get_all( + "Sales Invoice", + filters={"name": ["in", list(invoices)]}, + fields=["name", "outstanding_amount"], + as_list=True, + ) + ) + + ps_outstanding_amounts = ( + dict( + frappe.get_all( + "Payment Schedule", + filters={"name": ["in", list(payment_schedule_ids)]}, + fields=["name", "outstanding"], + as_list=True, + ) + ) + if payment_schedule_ids + else {} + ) + + for dunning in dunnings: + has_outstanding = False + for overdue_payment in dunning.overdue_payments: + invoice_outstanding = invoice_outstanding_amounts[overdue_payment.sales_invoice] + ps_outstanding = ps_outstanding_amounts.get(overdue_payment.payment_schedule, 0) + has_outstanding = invoice_outstanding > 0 and ps_outstanding > 0 + if has_outstanding: + break + + new_status = "Resolved" if not has_outstanding else "Unresolved" + + if dunning.status != new_status: + dunning.status = new_status + dunning.save() def get_linked_dunnings_as_per_state(sales_invoice, state): diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index ad45b882035..4fe8e7bf9f8 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -139,6 +139,64 @@ class TestDunning(FrappeTestCase): self.assertEqual(sales_invoice.status, "Overdue") self.assertEqual(dunning.status, "Unresolved") + def test_dunning_resolution_from_credit_note(self): + """ + Test that dunning is resolved when a credit note is issued against the original invoice. + """ + sales_invoice = create_sales_invoice_against_cost_center( + posting_date=add_days(today(), -10), qty=1, rate=100 + ) + dunning = create_dunning_from_sales_invoice(sales_invoice.name) + dunning.submit() + + self.assertEqual(dunning.status, "Unresolved") + + credit_note = frappe.copy_doc(sales_invoice) + credit_note.is_return = 1 + credit_note.return_against = sales_invoice.name + credit_note.update_outstanding_for_self = 0 + + for item in credit_note.items: + item.qty = -item.qty + + credit_note.save() + credit_note.submit() + + dunning.reload() + self.assertEqual(dunning.status, "Resolved") + + credit_note.cancel() + dunning.reload() + self.assertEqual(dunning.status, "Unresolved") + + def test_dunning_not_affected_by_standalone_credit_note(self): + """ + Test that dunning is NOT resolved when a credit note has update_outstanding_for_self checked. + """ + sales_invoice = create_sales_invoice_against_cost_center( + posting_date=add_days(today(), -10), qty=1, rate=100 + ) + dunning = create_dunning_from_sales_invoice(sales_invoice.name) + dunning.submit() + + self.assertEqual(dunning.status, "Unresolved") + + credit_note = frappe.copy_doc(sales_invoice) + credit_note.is_return = 1 + credit_note.return_against = sales_invoice.name + credit_note.update_outstanding_for_self = 1 + + for item in credit_note.items: + item.qty = -item.qty + + credit_note.save() + + credit_note = frappe.get_doc("Sales Invoice", credit_note.name) + credit_note.submit() + + dunning.reload() + self.assertEqual(dunning.status, "Unresolved") + def create_dunning(overdue_days, dunning_type_name=None): posting_date = add_days(today(), -1 * overdue_days) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index b034e48e3ec..cc0da8e7b09 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -118,9 +118,9 @@ class PaymentEntry(AccountsController): if self.difference_amount: frappe.throw(_("Difference Amount must be zero")) self.update_payment_requests() + self.update_payment_schedule() self.make_gl_entries() self.update_outstanding_amounts() - self.update_payment_schedule() self.set_status() def validate_for_repost(self): @@ -221,10 +221,10 @@ class PaymentEntry(AccountsController): ) super().on_cancel() self.update_payment_requests(cancel=True) + self.update_payment_schedule(cancel=1) self.make_gl_entries(cancel=1) self.update_outstanding_amounts() self.delink_advance_entry_references() - self.update_payment_schedule(cancel=1) self.set_status() def update_payment_requests(self, cancel=False): diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 25c31b9deaa..baeaca2f354 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1906,6 +1906,8 @@ def create_payment_ledger_entry( def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, party): + from erpnext.accounts.doctype.dunning.dunning import update_linked_dunnings + if not voucher_type or not voucher_no: return @@ -1938,6 +1940,7 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa ): outstanding = voucher_outstanding[0] ref_doc = frappe.get_doc(voucher_type, voucher_no) + previous_outstanding_amount = ref_doc.outstanding_amount outstanding_amount = flt( outstanding["outstanding_in_account_currency"], ref_doc.precision("outstanding_amount") ) @@ -1951,6 +1954,7 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa outstanding_amount, ) + update_linked_dunnings(ref_doc, previous_outstanding_amount) ref_doc.set_status(update=True) ref_doc.notify_update() diff --git a/erpnext/hooks.py b/erpnext/hooks.py index db9408b0f7e..a4df5628497 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -363,7 +363,9 @@ doc_events = { "erpnext.regional.create_transaction_log", "erpnext.regional.italy.utils.sales_invoice_on_submit", ], - "on_cancel": ["erpnext.regional.italy.utils.sales_invoice_on_cancel"], + "on_cancel": [ + "erpnext.regional.italy.utils.sales_invoice_on_cancel", + ], "on_trash": "erpnext.regional.check_deletion_permission", }, "Purchase Invoice": { @@ -375,9 +377,7 @@ doc_events = { "Payment Entry": { "on_submit": [ "erpnext.regional.create_transaction_log", - "erpnext.accounts.doctype.dunning.dunning.resolve_dunning", ], - "on_cancel": ["erpnext.accounts.doctype.dunning.dunning.resolve_dunning"], "on_trash": "erpnext.regional.check_deletion_permission", }, "Address": { diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index 40b87b79f97..0136bcfcb9d 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -674,13 +674,13 @@ def prepare_data_for_internal_transfer(): company = "_Test Company with perpetual inventory" customer = create_internal_customer( - "_Test Internal Customer 3", + "_Test Internal Customer 2", company, company, ) supplier = create_internal_supplier( - "_Test Internal Supplier 3", + "_Test Internal Supplier 2", company, company, ) From 89ad9f1bb4dd967671482c2d56e5dae68dcd45f5 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Wed, 13 Aug 2025 16:17:24 +0530 Subject: [PATCH 25/45] fix: add value adjustment amount in asset value (cherry picked from commit f8050f42788ed847789e754e4535907a3afc1aa1) --- .../asset_depreciations_and_balances.py | 71 ++++++++++++++++++- .../report/general_ledger/general_ledger.json | 45 ++++++------ 2 files changed, 94 insertions(+), 22 deletions(-) diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py index cdeddf3d38b..ba3099e4f88 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py @@ -234,21 +234,35 @@ def get_group_by_asset_data(filters): asset_details = get_asset_details_for_grouped_by_category(filters) assets = get_assets_for_grouped_by_asset(filters) + asset_value_adjustment_map = get_asset_value_adjustment_map(filters) for asset_detail in asset_details: row = frappe._dict() row.update(asset_detail) + row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", ""))) + adjustments = asset_value_adjustment_map.get( + asset_detail.get("name", ""), + { + "adjustment_before_from_date": 0.0, + "adjustment_till_to_date": 0.0, + }, + ) + row.adjustment_before_from_date = adjustments["adjustment_before_from_date"] + row.adjustment_till_to_date = adjustments["adjustment_till_to_date"] + row.adjustment_during_period = flt(row.adjustment_till_to_date) - flt(row.adjustment_before_from_date) + + row.value_as_on_from_date += row.adjustment_before_from_date + row.value_as_on_to_date = ( flt(row.value_as_on_from_date) + flt(row.value_of_new_purchase) - flt(row.value_of_sold_asset) - flt(row.value_of_scrapped_asset) - flt(row.value_of_capitalized_asset) + + flt(row.adjustment_during_period) ) - row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", ""))) - row.accumulated_depreciation_as_on_to_date = ( flt(row.accumulated_depreciation_as_on_from_date) + flt(row.depreciation_amount_during_the_period) @@ -432,6 +446,59 @@ def get_assets_for_grouped_by_asset(filters): ) +def get_asset_value_adjustment_map(filters): + asset_with_value_adjustments = frappe.db.sql( + """ + SELECT + a.name AS asset, + IFNULL( + SUM( + CASE + WHEN gle.posting_date < %(from_date)s + AND (a.disposal_date IS NULL OR a.disposal_date >= %(from_date)s) + THEN gle.debit - gle.credit + ELSE 0 + END + ), + 0) AS value_adjustment_before_from_date, + IFNULL( + SUM( + CASE + WHEN gle.posting_date <= %(to_date)s + AND (a.disposal_date IS NULL OR a.disposal_date >= %(to_date)s) + THEN gle.debit - gle.credit + ELSE 0 + END + ), + 0) AS value_adjustment_till_to_date + + FROM `tabGL Entry` gle + JOIN `tabAsset` a ON gle.against_voucher = a.name + JOIN `tabAsset Category Account` aca + ON aca.parent = a.asset_category + AND aca.company_name = %(company)s + WHERE gle.is_cancelled = 0 + AND a.docstatus = 1 + AND a.company = %(company)s + AND a.purchase_date <= %(to_date)s + AND gle.account = aca.fixed_asset_account + GROUP BY a.name + """, + {"from_date": filters.from_date, "to_date": filters.to_date, "company": filters.company}, + as_dict=1, + ) + + asset_value_adjustment_map = {} + + for r in asset_with_value_adjustments: + asset_value_adjustment_map[r["asset"]] = { + "adjustment_before_from_date": flt(r.get("value_adjustment_before_from_date", 0)), + "adjustment_till_to_date": flt(r.get("value_adjustment_till_to_date", 0)), + } + + return asset_value_adjustment_map + + def get_columns(filters): columns = [] diff --git a/erpnext/accounts/report/general_ledger/general_ledger.json b/erpnext/accounts/report/general_ledger/general_ledger.json index 4ff8ed2d432..172f322de22 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.json +++ b/erpnext/accounts/report/general_ledger/general_ledger.json @@ -1,29 +1,34 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2013-12-06 13:22:23", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2017-02-24 20:17:51.995451", - "modified_by": "Administrator", - "module": "Accounts", - "name": "General Ledger", - "owner": "Administrator", - "ref_doctype": "GL Entry", - "report_name": "General Ledger", - "report_type": "Script Report", + "add_total_row": 1, + "add_translate_data": 0, + "columns": [], + "creation": "2013-12-06 13:22:23", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 3, + "is_standard": "Yes", + "letterhead": null, + "modified": "2025-08-13 12:47:27.645023", + "modified_by": "Administrator", + "module": "Accounts", + "name": "General Ledger", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "GL Entry", + "report_name": "General Ledger", + "report_type": "Script Report", "roles": [ { "role": "Accounts User" - }, + }, { "role": "Accounts Manager" - }, + }, { "role": "Auditor" } - ] -} \ No newline at end of file + ], + "timeout": 0 +} From 089007f88a3d2e13f9a6660d90eb5f70bb1b0c69 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Wed, 13 Aug 2025 16:19:51 +0530 Subject: [PATCH 26/45] fix: add value adjustment amount in report for group by category filter (cherry picked from commit cd2bab7c5fda88737f6e8f2e8e5af22d56998557) --- .../asset_depreciations_and_balances.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py index ba3099e4f88..d7884b3e973 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py @@ -25,17 +25,25 @@ def get_group_by_asset_category_data(filters): asset_categories = get_asset_categories_for_grouped_by_category(filters) assets = get_assets_for_grouped_by_category(filters) + asset_value_adjustment_map = get_asset_value_adjustment_map_by_category(filters) for asset_category in asset_categories: row = frappe._dict() row.update(asset_category) + adjustments = asset_value_adjustment_map.get(asset_category.get("asset_category"), {}) + row.adjustment_before_from_date = flt(adjustments.get("adjustment_before_from_date", 0)) + row.adjustment_till_to_date = flt(adjustments.get("adjustment_till_to_date", 0)) + row.adjustment_during_period = row.adjustment_till_to_date - row.adjustment_before_from_date + + row.value_as_on_from_date += row.adjustment_before_from_date row.value_as_on_to_date = ( flt(row.value_as_on_from_date) + flt(row.value_of_new_purchase) - flt(row.value_of_sold_asset) - flt(row.value_of_scrapped_asset) - flt(row.value_of_capitalized_asset) + + flt(row.adjustment_during_period) ) row.update( @@ -229,6 +237,59 @@ def get_assets_for_grouped_by_category(filters): ) +def get_asset_value_adjustment_map_by_category(filters): + asset_value_adjustments = frappe.db.sql( + """ + SELECT + a.asset_category AS asset_category, + IFNULL( + SUM( + CASE + WHEN gle.posting_date < %(from_date)s + AND (a.disposal_date IS NULL OR a.disposal_date >= %(from_date)s) + THEN gle.debit - gle.credit + ELSE 0 + END + ), + 0) AS value_adjustment_before_from_date, + IFNULL( + SUM( + CASE + WHEN gle.posting_date <= %(to_date)s + AND (a.disposal_date IS NULL OR a.disposal_date >= %(to_date)s) + THEN gle.debit - gle.credit + ELSE 0 + END + ), + 0) AS value_adjustment_till_to_date + + FROM `tabGL Entry` gle + JOIN `tabAsset` a ON gle.against_voucher = a.name + JOIN `tabAsset Category Account` aca + ON aca.parent = a.asset_category + AND aca.company_name = %(company)s + WHERE gle.is_cancelled = 0 + AND a.docstatus = 1 + AND a.company = %(company)s + AND a.purchase_date <= %(to_date)s + AND gle.account = aca.fixed_asset_account + GROUP BY a.asset_category + """, + {"from_date": filters.from_date, "to_date": filters.to_date, "company": filters.company}, + as_dict=1, + ) + + category_value_adjustment_map = {} + + for r in asset_value_adjustments: + category_value_adjustment_map[r["asset_category"]] = { + "adjustment_before_from_date": flt(r.get("value_adjustment_before_from_date", 0)), + "adjustment_till_to_date": flt(r.get("value_adjustment_till_to_date", 0)), + } + + return category_value_adjustment_map + + def get_group_by_asset_data(filters): data = [] From ac4a5bfe6d5d08735dbf2745ca5f2ed7b138ea9d Mon Sep 17 00:00:00 2001 From: l0gesh29 Date: Fri, 8 Aug 2025 14:20:25 +0530 Subject: [PATCH 27/45] fix: add fieldname in accounting dimension filter (cherry picked from commit ac2acc535d9742622b1733dbe7f93db2a4450032) # Conflicts: # erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json --- .../accounting_dimension_filter.json | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json index 2bd6c12a0a3..bee30296eed 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json @@ -7,6 +7,7 @@ "engine": "InnoDB", "field_order": [ "accounting_dimension", + "fieldname", "disabled", "column_break_2", "company", @@ -90,11 +91,21 @@ "fieldname": "apply_restriction_on_values", "fieldtype": "Check", "label": "Apply restriction on dimension values" + }, + { + "fieldname": "fieldname", + "fieldtype": "Data", + "hidden": 1, + "label": "Fieldname" } ], "index_web_pages_for_search": 1, "links": [], +<<<<<<< HEAD "modified": "2023-06-07 14:59:41.869117", +======= + "modified": "2025-08-08 14:13:22.203011", +>>>>>>> ac2acc535d (fix: add fieldname in accounting dimension filter) "modified_by": "Administrator", "module": "Accounts", "name": "Accounting Dimension Filter", @@ -139,8 +150,13 @@ } ], "quick_entry": 1, +<<<<<<< HEAD "sort_field": "modified", +======= + "row_format": "Dynamic", + "sort_field": "creation", +>>>>>>> ac2acc535d (fix: add fieldname in accounting dimension filter) "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} From d494d8c29988cd24de87bd248fce642a33215a3e Mon Sep 17 00:00:00 2001 From: l0gesh29 Date: Fri, 8 Aug 2025 14:21:00 +0530 Subject: [PATCH 28/45] fix: fetch fieldname in accounting dimension filter (cherry picked from commit 42f9d27d79735fad57f59c97f93bacadb4424aa3) --- .../accounting_dimension_filter.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py index 7c843cf552e..841eab6bced 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py @@ -17,17 +17,16 @@ class AccountingDimensionFilter(Document): from frappe.types import DF from erpnext.accounts.doctype.allowed_dimension.allowed_dimension import AllowedDimension - from erpnext.accounts.doctype.applicable_on_account.applicable_on_account import ( - ApplicableOnAccount, - ) + from erpnext.accounts.doctype.applicable_on_account.applicable_on_account import ApplicableOnAccount - accounting_dimension: DF.Literal + accounting_dimension: DF.Literal[None] accounts: DF.Table[ApplicableOnAccount] allow_or_restrict: DF.Literal["Allow", "Restrict"] apply_restriction_on_values: DF.Check company: DF.Link dimensions: DF.Table[AllowedDimension] disabled: DF.Check + fieldname: DF.Data | None # end: auto-generated types def before_save(self): @@ -37,6 +36,9 @@ class AccountingDimensionFilter(Document): self.set("dimensions", []) def validate(self): + self.fieldname = frappe.db.get_value( + "Accounting Dimension", {"document_type": self.accounting_dimension}, "fieldname" + ) self.validate_applicable_accounts() def validate_applicable_accounts(self): @@ -72,7 +74,7 @@ def get_dimension_filter_map(): """ SELECT a.applicable_on_account, d.dimension_value, p.accounting_dimension, - p.allow_or_restrict, a.is_mandatory + p.allow_or_restrict, p.fieldname, a.is_mandatory FROM `tabApplicable On Account` a, `tabAccounting Dimension Filter` p @@ -87,8 +89,6 @@ def get_dimension_filter_map(): dimension_filter_map = {} for f in filters: - f.fieldname = scrub(f.accounting_dimension) - build_map( dimension_filter_map, f.fieldname, From a8530105370a177b87b801bb5bc1e94959df981c Mon Sep 17 00:00:00 2001 From: l0gesh29 Date: Tue, 12 Aug 2025 14:28:18 +0530 Subject: [PATCH 29/45] fix: add patch (cherry picked from commit 3cf765d985532292b11ce03d415bcd17f1dea94b) # Conflicts: # erpnext/patches.txt --- erpnext/patches.txt | 6 +++++ ...ieldname_in_accounting_dimension_filter.py | 23 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 erpnext/patches/v15_0/update_fieldname_in_accounting_dimension_filter.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 1087f64276d..aa753e2ba5b 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -419,4 +419,10 @@ erpnext.patches.v15_0.remove_sales_partner_from_consolidated_sales_invoice 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 +<<<<<<< HEAD erpnext.patches.v15_0.update_uae_zero_rated_fetch +======= +erpnext.patches.v16_0.update_serial_no_reference_name +erpnext.patches.v16_0.set_invoice_type_in_pos_settings +erpnext.patches.v15_0.update_fieldname_in_accounting_dimension_filter +>>>>>>> 3cf765d985 (fix: add patch) diff --git a/erpnext/patches/v15_0/update_fieldname_in_accounting_dimension_filter.py b/erpnext/patches/v15_0/update_fieldname_in_accounting_dimension_filter.py new file mode 100644 index 00000000000..50d3fbe6f9c --- /dev/null +++ b/erpnext/patches/v15_0/update_fieldname_in_accounting_dimension_filter.py @@ -0,0 +1,23 @@ +import frappe +from frappe.query_builder import DocType + + +def execute(): + ADF = DocType("Accounting Dimension Filter") + AD = DocType("Accounting Dimension") + + accounting_dimension_filter = ( + frappe.qb.from_(ADF) + .join(AD) + .on(AD.document_type == ADF.accounting_dimension) + .select(ADF.name, AD.fieldname) + ).run(as_dict=True) + + for doc in accounting_dimension_filter: + frappe.db.set_value( + "Accounting Dimension Filter", + doc.name, + "fieldname", + doc.fieldname, + update_modified=False, + ) From e50d6c6b62758941e00fe12f009b666c6deb6020 Mon Sep 17 00:00:00 2001 From: l0gesh29 Date: Tue, 12 Aug 2025 12:00:53 +0530 Subject: [PATCH 30/45] fix: handle default accounting dimension (cherry picked from commit 16e440f9a7b20402c8d6e63dc00ac6150ec69485) --- .../accounting_dimension_filter.py | 3 ++- ...pdate_fieldname_in_accounting_dimension_filter.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py index 841eab6bced..040583a2847 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py @@ -38,7 +38,8 @@ class AccountingDimensionFilter(Document): def validate(self): self.fieldname = frappe.db.get_value( "Accounting Dimension", {"document_type": self.accounting_dimension}, "fieldname" - ) + ) or frappe.scrub(self.accounting_dimension) # scrub to handle default accounting dimension + self.validate_applicable_accounts() def validate_applicable_accounts(self): diff --git a/erpnext/patches/v15_0/update_fieldname_in_accounting_dimension_filter.py b/erpnext/patches/v15_0/update_fieldname_in_accounting_dimension_filter.py index 50d3fbe6f9c..11d2cc91446 100644 --- a/erpnext/patches/v15_0/update_fieldname_in_accounting_dimension_filter.py +++ b/erpnext/patches/v15_0/update_fieldname_in_accounting_dimension_filter.py @@ -3,6 +3,7 @@ from frappe.query_builder import DocType def execute(): + default_accounting_dimension() ADF = DocType("Accounting Dimension Filter") AD = DocType("Accounting Dimension") @@ -21,3 +22,14 @@ def execute(): doc.fieldname, update_modified=False, ) + + +def default_accounting_dimension(): + for accounting_dimension in ["Cost Center", "Project"]: + frappe.db.set_value( + "Accounting Dimension Filter", + {"accounting_dimension": accounting_dimension}, + "fieldname", + frappe.scrub(accounting_dimension), + update_modified=False, + ) From 0cd45a0022440d404b7a6f9a94de804569782f13 Mon Sep 17 00:00:00 2001 From: l0gesh29 Date: Tue, 12 Aug 2025 14:26:18 +0530 Subject: [PATCH 31/45] fix: handle default dimension for all company (cherry picked from commit 77021fff74a0e67b94289501b9d494bd2b9b3a51) --- ...ieldname_in_accounting_dimension_filter.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/erpnext/patches/v15_0/update_fieldname_in_accounting_dimension_filter.py b/erpnext/patches/v15_0/update_fieldname_in_accounting_dimension_filter.py index 11d2cc91446..25a35bc87fc 100644 --- a/erpnext/patches/v15_0/update_fieldname_in_accounting_dimension_filter.py +++ b/erpnext/patches/v15_0/update_fieldname_in_accounting_dimension_filter.py @@ -11,25 +11,26 @@ def execute(): frappe.qb.from_(ADF) .join(AD) .on(AD.document_type == ADF.accounting_dimension) - .select(ADF.name, AD.fieldname) + .select(ADF.name, AD.fieldname, ADF.accounting_dimension) ).run(as_dict=True) for doc in accounting_dimension_filter: + value = doc.fieldname or frappe.scrub(doc.accounting_dimension) frappe.db.set_value( "Accounting Dimension Filter", doc.name, "fieldname", - doc.fieldname, + value, update_modified=False, ) def default_accounting_dimension(): - for accounting_dimension in ["Cost Center", "Project"]: - frappe.db.set_value( - "Accounting Dimension Filter", - {"accounting_dimension": accounting_dimension}, - "fieldname", - frappe.scrub(accounting_dimension), - update_modified=False, + ADF = DocType("Accounting Dimension Filter") + for dim in ("Cost Center", "Project"): + ( + frappe.qb.update(ADF) + .set(ADF.fieldname, frappe.scrub(dim)) + .where(ADF.accounting_dimension == dim) + .run() ) From a3c5b0a510c33130ac7b9512a72224b46c88cb9e Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Fri, 15 Aug 2025 00:11:18 +0530 Subject: [PATCH 32/45] fix: use query builder instead of raw SQL in get_loyalty_details (cherry picked from commit 8696ba2f5d9e99c799d4aef577f72f2fae5678e7) --- .../loyalty_program/loyalty_program.py | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program.py b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py index 1e844afc4b0..ee37ccebd47 100644 --- a/erpnext/accounts/doctype/loyalty_program/loyalty_program.py +++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py @@ -5,6 +5,7 @@ import frappe from frappe import _ from frappe.model.document import Document +from frappe.query_builder.functions import Sum from frappe.utils import flt, today @@ -45,26 +46,34 @@ def get_loyalty_details( if not expiry_date: expiry_date = today() - condition = "" - if company: - condition = " and company=%s " % frappe.db.escape(company) - if not include_expired_entry: - condition += " and expiry_date>='%s' " % expiry_date + LoyaltyPointEntry = frappe.qb.DocType("Loyalty Point Entry") - loyalty_point_details = frappe.db.sql( - f"""select sum(loyalty_points) as loyalty_points, - sum(purchase_amount) as total_spent from `tabLoyalty Point Entry` - where customer=%s and loyalty_program=%s and posting_date <= %s - {condition} - group by customer""", - (customer, loyalty_program, expiry_date), - as_dict=1, + query = ( + frappe.qb.from_(LoyaltyPointEntry) + .select( + Sum(LoyaltyPointEntry.loyalty_points).as_("loyalty_points"), + Sum(LoyaltyPointEntry.purchase_amount).as_("total_spent"), + ) + .where( + (LoyaltyPointEntry.customer == customer) + & (LoyaltyPointEntry.loyalty_program == loyalty_program) + & (LoyaltyPointEntry.posting_date <= expiry_date) + ) + .groupby(LoyaltyPointEntry.customer) ) - if loyalty_point_details: - return loyalty_point_details[0] - else: - return {"loyalty_points": 0, "total_spent": 0} + if company: + query = query.where(LoyaltyPointEntry.company == company) + + if not include_expired_entry: + query = query.where(LoyaltyPointEntry.expiry_date >= expiry_date) + + loyalty_point_details = query.run(as_dict=True) + + return { + "loyalty_points": flt(loyalty_point_details[0].loyalty_points), + "total_spent": flt(loyalty_point_details[0].total_spent), + } @frappe.whitelist() From 9d0fe060c852d38406e53916ada2b493690d78fd Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Fri, 15 Aug 2025 19:04:31 +0530 Subject: [PATCH 33/45] fix: use query builder instead of raw SQL in get_material_requests_based_on_supplier (cherry picked from commit de919568b4f7a86c8d418c0c3fd88e1f3101696c) --- .../material_request/material_request.py | 56 ++++++++++--------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 409623a348f..6cd5a035c99 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -11,6 +11,7 @@ import frappe import frappe.defaults from frappe import _, msgprint from frappe.model.mapper import get_mapped_doc +from frappe.query_builder import Order from frappe.query_builder.functions import Sum from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, new_line_sep, nowdate @@ -555,39 +556,44 @@ def get_items_based_on_default_supplier(supplier): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, page_len, filters): - conditions = "" - if txt: - conditions += "and mr.name like '%%" + txt + "%%' " - - if filters.get("transaction_date"): - date = filters.get("transaction_date")[1] - conditions += f"and mr.transaction_date between '{date[0]}' and '{date[1]}' " - supplier = filters.get("supplier") supplier_items = get_items_based_on_default_supplier(supplier) if not supplier_items: frappe.throw(_("{0} is not the default supplier for any items.").format(supplier)) - material_requests = frappe.db.sql( - """select distinct mr.name, transaction_date,company - from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item - where mr.name = mr_item.parent - and mr_item.item_code in ({}) - and mr.material_request_type = 'Purchase' - and mr.per_ordered < 99.99 - and mr.docstatus = 1 - and mr.status != 'Stopped' - and mr.company = %s - {} - order by mr_item.item_code ASC - limit {} offset {} """.format( - ", ".join(["%s"] * len(supplier_items)), conditions, cint(page_len), cint(start) - ), - (*tuple(supplier_items), filters.get("company")), - as_dict=1, + mr = frappe.qb.DocType("Material Request") + mr_item = frappe.qb.DocType("Material Request Item") + + query = ( + frappe.qb.from_(mr) + .from_(mr_item) + .select(mr.name) + .distinct() + .select(mr.transaction_date, mr.company) + .where( + (mr.name == mr_item.parent) + & (mr_item.item_code.isin(supplier_items)) + & (mr.material_request_type == "Purchase") + & (mr.per_ordered < 99.99) + & (mr.docstatus == 1) + & (mr.status != "Stopped") + & (mr.company == filters.get("company")) + ) + .orderby(mr_item.item_code, order=Order.asc) + .limit(cint(page_len)) + .offset(cint(start)) ) + if txt: + query = query.where(mr.name.like(f"%%{txt}%%")) + + if filters.get("transaction_date"): + date = filters.get("transaction_date")[1] + query = query.where(mr.transaction_date[date[0] : date[1]]) + + material_requests = query.run(as_dict=True) + return material_requests From c9f79b3ba97c57ecbc119b42e4b07c98aacdf5ad Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Fri, 15 Aug 2025 22:22:55 +0530 Subject: [PATCH 34/45] fix: formatted string for disabled filter in get_income_account (cherry picked from commit 6320f7290f93a5278ffdfaa790af70427c20a1c8) --- erpnext/controllers/queries.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 17141751ca1..548109394de 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -615,7 +615,7 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters): if filters.get("company"): condition += "and tabAccount.company = %(company)s" - condition += f"and tabAccount.disabled = {filters.get('disabled', 0)}" + condition += " and tabAccount.disabled = %(disabled)s" return frappe.db.sql( f"""select tabAccount.name from `tabAccount` @@ -625,7 +625,11 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters): and tabAccount.`{searchfield}` LIKE %(txt)s {condition} {get_match_cond(doctype)} order by idx desc, name""", - {"txt": "%" + txt + "%", "company": filters.get("company", "")}, + { + "txt": "%" + txt + "%", + "company": filters.get("company", ""), + "disabled": cint(filters.get("disabled", 0)), + }, ) From 4a0d7fd2055cdd094f614ba7f2e4acbaf4eef307 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Fri, 15 Aug 2025 22:56:45 +0530 Subject: [PATCH 35/45] fix: use query builder instead of raw SQL in get_blanket_orders (cherry picked from commit 1db135262d9474411ef54e3367d24bb169d2503e) --- erpnext/controllers/queries.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 548109394de..55e04a2e262 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -583,21 +583,27 @@ def get_account_list(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.sql( - """select distinct bo.name, bo.blanket_order_type, bo.to_date - from `tabBlanket Order` bo, `tabBlanket Order Item` boi - where - boi.parent = bo.name - and boi.item_code = {item_code} - and bo.blanket_order_type = '{blanket_order_type}' - and bo.company = {company} - and bo.docstatus = 1""".format( - item_code=frappe.db.escape(filters.get("item")), - blanket_order_type=filters.get("blanket_order_type"), - company=frappe.db.escape(filters.get("company")), + bo = frappe.qb.DocType("Blanket Order") + bo_item = frappe.qb.DocType("Blanket Order Item") + + blanket_orders = ( + frappe.qb.from_(bo) + .from_(bo_item) + .select(bo.name) + .distinct() + .select(bo.blanket_order_type, bo.to_date) + .where( + (bo_item.parent == bo.name) + & (bo_item.item_code == filters.get("item")) + & (bo.blanket_order_type == filters.get("blanket_order_type")) + & (bo.company == filters.get("company")) + & (bo.docstatus == 1) ) + .run() ) + return blanket_orders + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs From 69389bb355793c884aa040af376c0712e52f35dc Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Sat, 16 Aug 2025 20:45:28 +0530 Subject: [PATCH 36/45] fix: sanitize column name for inventory_dimensions in get_stock_balance (cherry picked from commit eb22794f14351c2ff5731548c48bef0b91765c86) --- erpnext/stock/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index c863960d7a0..781fc81445c 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -126,8 +126,9 @@ def get_stock_balance( extra_cond = "" if inventory_dimensions_dict: for field, value in inventory_dimensions_dict.items(): + column = frappe.utils.sanitize_column(field) args[field] = value - extra_cond += f" and {field} = %({field})s" + extra_cond += f" and {column} = %({field})s" last_entry = get_previous_sle(args, extra_cond=extra_cond) From 92391a69cfbaeceae6443d09454bfe7256983bca Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Sat, 16 Aug 2025 22:15:55 +0530 Subject: [PATCH 37/45] fix: use query builder instead of raw SQL in unset_existing_data (cherry picked from commit 7fa4ed6139dfb737995fe297e40f4f5440c748c3) --- .../chart_of_accounts_importer/chart_of_accounts_importer.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index e7dab34d04a..9c4f2f8fd49 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -462,9 +462,8 @@ def unset_existing_data(company): "Sales Taxes and Charges Template", "Purchase Taxes and Charges Template", ]: - frappe.db.sql( - f'''delete from `tab{doctype}` where `company`="%s"''' % (company) # nosec - ) + dt = frappe.qb.DocType(doctype) + frappe.qb.from_(dt).where(dt.company == company).delete().run() def set_default_accounts(company): From 4ac386a84e3a7e646a2277be48ccb53dd60a5d38 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Sat, 16 Aug 2025 22:37:42 +0530 Subject: [PATCH 38/45] fix: use query builder instead of raw SQL in get_rfq_containing_supplier (cherry picked from commit 7f2a52ff71a1fd5d4a9034cf217094c0be9f341a) --- .../request_for_quotation.py | 54 +++++++++---------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 27793236dc3..d11424b555f 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -9,6 +9,7 @@ from frappe import _ from frappe.core.doctype.communication.email import make from frappe.desk.form.load import get_attachments from frappe.model.mapper import get_mapped_doc +from frappe.query_builder import Order from frappe.utils import get_url from frappe.utils.print_format import download_pdf from frappe.utils.user import get_user_fullname @@ -582,35 +583,32 @@ def get_supplier_tag(): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_rfq_containing_supplier(doctype, txt, searchfield, start, page_len, filters): - conditions = "" - if txt: - conditions += "and rfq.name like '%%" + txt + "%%' " + rfq = frappe.qb.DocType("Request for Quotation") + rfq_supplier = frappe.qb.DocType("Request for Quotation Supplier") - if filters.get("transaction_date"): - conditions += "and rfq.transaction_date = '{}'".format(filters.get("transaction_date")) - - rfq_data = frappe.db.sql( - f""" - select - distinct rfq.name, rfq.transaction_date, - rfq.company - from - `tabRequest for Quotation` rfq, `tabRequest for Quotation Supplier` rfq_supplier - where - rfq.name = rfq_supplier.parent - and rfq_supplier.supplier = %(supplier)s - and rfq.docstatus = 1 - and rfq.company = %(company)s - {conditions} - order by rfq.transaction_date ASC - limit %(page_len)s offset %(start)s """, - { - "page_len": page_len, - "start": start, - "company": filters.get("company"), - "supplier": filters.get("supplier"), - }, - as_dict=1, + query = ( + frappe.qb.from_(rfq) + .from_(rfq_supplier) + .select(rfq.name) + .distinct() + .select(rfq.transaction_date, rfq.company) + .where( + (rfq.name == rfq_supplier.parent) + & (rfq_supplier.supplier == filters.get("supplier")) + & (rfq.docstatus == 1) + & (rfq.company == filters.get("company")) + ) + .orderby(rfq.transaction_date, order=Order.asc) + .limit(page_len) + .offset(start) ) + if txt: + query = query.where(rfq.name.like(f"%%{txt}%%")) + + if filters.get("transaction_date"): + query = query.where(rfq.transaction_date == filters.get("transaction_date")) + + rfq_data = query.run(as_dict=1) + return rfq_data From 0a2a7fa6aa0219c99e404b47a54b6133840a7884 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Sat, 16 Aug 2025 22:48:45 +0530 Subject: [PATCH 39/45] fix: use query builder instead of raw SQL in get_timesheet_detail_rate (cherry picked from commit e563ed0c75fd20135a6ad288e957e75eac7d3b8d) --- erpnext/projects/doctype/timesheet/timesheet.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index 1dcee64e54f..c4ddbcaa8b1 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -342,12 +342,16 @@ def get_projectwise_timesheet_data(project=None, parent=None, from_time=None, to @frappe.whitelist() def get_timesheet_detail_rate(timelog, currency): - timelog_detail = frappe.db.sql( - f"""SELECT tsd.billing_amount as billing_amount, - ts.currency as currency FROM `tabTimesheet Detail` tsd - INNER JOIN `tabTimesheet` ts ON ts.name=tsd.parent - WHERE tsd.name = '{timelog}'""", - as_dict=1, + ts = frappe.qb.DocType("Timesheet") + ts_detail = frappe.qb.DocType("Timesheet Detail") + + timelog_detail = ( + frappe.qb.from_(ts_detail) + .inner_join(ts) + .on(ts.name == ts_detail.parent) + .select(ts_detail.billing_amount.as_("billing_amount"), ts.currency.as_("currency")) + .where(ts_detail.name == timelog) + .run(as_dict=1) )[0] if timelog_detail.currency: From dc953f70d1883c300c39e1e56765f299f9861b71 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Sat, 16 Aug 2025 23:48:10 +0530 Subject: [PATCH 40/45] fix: handle empty loyalty point details (cherry picked from commit 1231ca17c91f6c42c07e016b11a51bea090e91b4) --- .../accounts/doctype/loyalty_program/loyalty_program.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program.py b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py index ee37ccebd47..f46d94baa35 100644 --- a/erpnext/accounts/doctype/loyalty_program/loyalty_program.py +++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py @@ -70,10 +70,10 @@ def get_loyalty_details( loyalty_point_details = query.run(as_dict=True) - return { - "loyalty_points": flt(loyalty_point_details[0].loyalty_points), - "total_spent": flt(loyalty_point_details[0].total_spent), - } + if loyalty_point_details: + return loyalty_point_details[0] + else: + return {"loyalty_points": 0, "total_spent": 0} @frappe.whitelist() From 4af814dc3bb31d82f7cbe9f18a122d4be71b8402 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 19 Aug 2025 11:06:23 +0530 Subject: [PATCH 41/45] chore: resolve conflicts --- .../accounting_dimension_filter.json | 9 --------- erpnext/patches.txt | 5 ----- 2 files changed, 14 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json index bee30296eed..260bcd11db5 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json @@ -101,11 +101,7 @@ ], "index_web_pages_for_search": 1, "links": [], -<<<<<<< HEAD - "modified": "2023-06-07 14:59:41.869117", -======= "modified": "2025-08-08 14:13:22.203011", ->>>>>>> ac2acc535d (fix: add fieldname in accounting dimension filter) "modified_by": "Administrator", "module": "Accounts", "name": "Accounting Dimension Filter", @@ -150,12 +146,7 @@ } ], "quick_entry": 1, -<<<<<<< HEAD - "sort_field": "modified", -======= - "row_format": "Dynamic", "sort_field": "creation", ->>>>>>> ac2acc535d (fix: add fieldname in accounting dimension filter) "sort_order": "DESC", "states": [], "track_changes": 1 diff --git a/erpnext/patches.txt b/erpnext/patches.txt index aa753e2ba5b..ae52e1e3a09 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -419,10 +419,5 @@ erpnext.patches.v15_0.remove_sales_partner_from_consolidated_sales_invoice 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 -<<<<<<< HEAD erpnext.patches.v15_0.update_uae_zero_rated_fetch -======= -erpnext.patches.v16_0.update_serial_no_reference_name -erpnext.patches.v16_0.set_invoice_type_in_pos_settings erpnext.patches.v15_0.update_fieldname_in_accounting_dimension_filter ->>>>>>> 3cf765d985 (fix: add patch) From cfb75584656fddd519ca0ebd69e7dc78eadbff89 Mon Sep 17 00:00:00 2001 From: Logesh Periyasamy Date: Tue, 19 Aug 2025 12:02:31 +0530 Subject: [PATCH 42/45] Merge pull request #49185 from aerele/mop-sales-register fix: handle mode of payment filter (cherry picked from commit d656e02441fed8708067cef0dd2e2b3b48c2fc83) --- .../item_wise_sales_register.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index 073efdadb6a..d6ffd3db5ac 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -355,7 +355,13 @@ def apply_conditions(query, si, sii, sip, filters, additional_conditions=None): query = query.where(si.posting_date <= filters.get("to_date")) if filters.get("mode_of_payment"): - query = query.where(sip.mode_of_payment == filters.get("mode_of_payment")) + subquery = ( + frappe.qb.from_(sip) + .select(sip.parent) + .where(sip.mode_of_payment == filters.get("mode_of_payment")) + .groupby(sip.parent) + ) + query = query.where(si.name.isin(subquery)) if filters.get("warehouse"): if frappe.db.get_value("Warehouse", filters.get("warehouse"), "is_group"): @@ -424,8 +430,6 @@ def get_items(filters, additional_query_columns, additional_conditions=None): frappe.qb.from_(si) .join(sii) .on(si.name == sii.parent) - .left_join(sip) - .on(sip.parent == si.name) .left_join(item) .on(sii.item_code == item.name) .select( @@ -465,7 +469,6 @@ def get_items(filters, additional_query_columns, additional_conditions=None): si.update_stock, sii.uom, sii.qty, - sip.mode_of_payment, ) .where(si.docstatus == 1) .where(sii.parenttype == doctype) From 419f7175427d5466f4072ea97479a5cadb7d8394 Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Mon, 11 Aug 2025 17:46:48 +0530 Subject: [PATCH 43/45] fix(quotation): update currency on duplicate (cherry picked from commit 430a06d0562278664a253dc1b016b8ce2c772739) --- erpnext/public/js/controllers/transaction.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index cab3d48c7b7..79d85c20a0b 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -923,8 +923,15 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe return; } - var party_type = frappe.meta.has_field(me.frm.doc.doctype, "customer") ? "Customer" : "Supplier"; - var party_name = me.frm.doc[party_type.toLowerCase()]; + var party_type, party_name; + if( me.frm.doc.doctype == "Quotation" && me.frm.doc.quotation_to == "Customer"){ + party_type = "Customer", + party_name = me.frm.doc.party_name + } + else{ + party_type = frappe.meta.has_field(me.frm.doc.doctype, "customer") ? "Customer" : "Supplier"; + party_name = me.frm.doc[party_type.toLowerCase()]; + } if (party_name) { frappe.call({ method: "frappe.client.get_value", From da3d8fbbc52ae5e92e386e971d55efa0201f5424 Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Tue, 19 Aug 2025 13:46:41 +0530 Subject: [PATCH 44/45] fix(stock): don't override t_warehouse if no rules found (cherry picked from commit 66f217c8e6577574297357372743fc70e0a3d4f6) --- erpnext/stock/doctype/putaway_rule/putaway_rule.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 7b5fb517095..53af3e12896 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -131,7 +131,12 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): at_capacity, rules = get_ordered_putaway_rules(item_code, company, source_warehouse=source_warehouse) if not rules: - warehouse = source_warehouse or item.get("warehouse") + warehouse = ( + (source_warehouse or item.get("warehouse")) + if not item.get("t_warehouse") + else item.get("t_warehouse") + ) + if at_capacity: # rules available, but no free space items_not_accomodated.append([item_code, pending_qty]) From cea4b50bbc29f0b5e42d4e0bd1fcb4e3529ab4d6 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 19 Aug 2025 15:45:35 +0530 Subject: [PATCH 45/45] fix: company issue in setup wizard (cherry picked from commit 1fb0d1460a443bcfafe94514cc2ad5e3b79ea8b9) --- .../setup_wizard/operations/install_fixtures.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 9cdf24cf451..c78e9b5b60d 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -480,14 +480,19 @@ def install_defaults(args=None): # nosemgrep create_bank_account(args) -def set_global_defaults(args): +def set_global_defaults(kwargs): global_defaults = frappe.get_doc("Global Defaults", "Global Defaults") + company = frappe.db.get_value( + "Company", + {"company_name": kwargs.get("company_name")}, + "name", + ) global_defaults.update( { - "default_currency": args.get("currency"), - "default_company": args.get("company_name"), - "country": args.get("country"), + "default_currency": kwargs.get("currency"), + "default_company": company, + "country": kwargs.get("country"), } )