From 22869b6f9dc5b98e86b9e6442212946ab1e5257e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:10:31 +0530 Subject: [PATCH 01/51] fix(mode of payment): use valid syntax (backport #51542) (#52134) Co-authored-by: ervishnucs --- erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js index 25d437f154f..ec53f1fa78e 100644 --- a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js +++ b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js @@ -7,7 +7,7 @@ frappe.ui.form.on("Mode of Payment", { let d = locals[cdt][cdn]; return { filters: [ - ["Account", "account_type", "in", "Bank, Cash, Receivable"], + ["Account", "account_type", "in", ["Bank", "Cash", "Receivable"]], ["Account", "is_group", "=", 0], ["Account", "company", "=", d.company], ], From afc4c856f80506d528a7db182f54b46a0b879f39 Mon Sep 17 00:00:00 2001 From: aymenit2008 Date: Wed, 28 Jan 2026 13:13:33 +0300 Subject: [PATCH 02/51] fix: add docstatus condition to get_sales_invoice_item function (#51517) --- .../doctype/pos_invoice_merge_log/pos_invoice_merge_log.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index 913ab7e6c47..43091e9bda9 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -697,6 +697,7 @@ def get_sales_invoice_item(return_against_pos_invoice, pos_invoice_item): & (SalesInvoice.is_return == 0) & (SalesInvoiceItem.pos_invoice == return_against_pos_invoice) & (SalesInvoiceItem.pos_invoice_item == pos_invoice_item) + & (SalesInvoice.docstatus == 1) ) ) From 7226066772642e572cf08c05469fe98b08e46f26 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 10:22:09 +0000 Subject: [PATCH 03/51] fix(bank_account): `is_company_account` related validations (backport #51887) (#51921) Co-authored-by: diptanilsaha --- .../doctype/bank_account/bank_account.js | 4 --- .../doctype/bank_account/bank_account.json | 4 ++- .../doctype/bank_account/bank_account.py | 36 ++++++++++--------- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/erpnext/accounts/doctype/bank_account/bank_account.js b/erpnext/accounts/doctype/bank_account/bank_account.js index 202f750fb50..5173e0539f4 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.js +++ b/erpnext/accounts/doctype/bank_account/bank_account.js @@ -42,8 +42,4 @@ frappe.ui.form.on("Bank Account", { }); } }, - - is_company_account: function (frm) { - frm.set_df_property("account", "reqd", frm.doc.is_company_account); - }, }); diff --git a/erpnext/accounts/doctype/bank_account/bank_account.json b/erpnext/accounts/doctype/bank_account/bank_account.json index 4946dc168ba..ce239365f30 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.json +++ b/erpnext/accounts/doctype/bank_account/bank_account.json @@ -52,6 +52,7 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Company Account", + "mandatory_depends_on": "is_company_account", "options": "Account" }, { @@ -98,6 +99,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Company", + "mandatory_depends_on": "is_company_account", "options": "Company" }, { @@ -252,7 +254,7 @@ "link_fieldname": "default_bank_account" } ], - "modified": "2025-08-29 12:32:01.081687", + "modified": "2026-01-20 00:46:16.633364", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Account", diff --git a/erpnext/accounts/doctype/bank_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py index 5a874701a39..1aa11df0c9a 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.py +++ b/erpnext/accounts/doctype/bank_account/bank_account.py @@ -52,31 +52,35 @@ class BankAccount(Document): delete_contact_and_address("Bank Account", self.name) def validate(self): - self.validate_company() - self.validate_account() + self.validate_is_company_account() self.update_default_bank_account() - def validate_account(self): - if self.account: - if accounts := frappe.db.get_all( - "Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1 - ): - frappe.throw( - _("'{0}' account is already used by {1}. Use another account.").format( - frappe.bold(self.account), - frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])), - ) - ) + def validate_is_company_account(self): + if self.is_company_account: + if not self.company: + frappe.throw(_("Company is mandatory for company account")) - def validate_company(self): - if self.is_company_account and not self.company: - frappe.throw(_("Company is manadatory for company account")) + if not self.account: + frappe.throw(_("Company Account is mandatory")) + + self.validate_account() @deprecated def validate_iban(self): """Kept for backward compatibility, will be removed in v16.""" validate_iban(self.iban, throw=True) + def validate_account(self): + if accounts := frappe.db.get_all( + "Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1 + ): + frappe.throw( + _("'{0}' account is already used by {1}. Use another account.").format( + frappe.bold(self.account), + frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])), + ) + ) + def update_default_bank_account(self): if self.is_default and not self.disabled: frappe.db.set_value( From f48b4cda50b29b65dfe661de263105bf8a87eb22 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 11:40:18 +0000 Subject: [PATCH 04/51] feat: filter to display trial balance report without group account (backport #48486) (#52146) Co-authored-by: Diptanil Saha --- .../report/trial_balance/trial_balance.js | 6 ++++++ .../report/trial_balance/trial_balance.py | 16 +++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/trial_balance/trial_balance.js b/erpnext/accounts/report/trial_balance/trial_balance.js index 8c08c35b3e1..d1047f3276f 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.js +++ b/erpnext/accounts/report/trial_balance/trial_balance.js @@ -112,6 +112,12 @@ frappe.query_reports["Trial Balance"] = { fieldtype: "Check", default: 1, }, + { + fieldname: "show_group_accounts", + label: __("Show Group Accounts"), + fieldtype: "Check", + default: 1, + }, ], formatter: erpnext.financial_statements.formatter, tree: true, diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 13bf6f531d1..5f78aef582b 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -83,7 +83,7 @@ def validate_filters(filters): def get_data(filters): accounts = frappe.db.sql( - """select name, account_number, parent_account, account_name, root_type, report_type, lft, rgt + """select name, account_number, parent_account, account_name, root_type, report_type, is_group, lft, rgt from `tabAccount` where company=%s order by lft""", filters.company, @@ -393,6 +393,7 @@ def prepare_data(accounts, filters, parent_children_map, company_currency): "from_date": filters.from_date, "to_date": filters.to_date, "currency": company_currency, + "is_group_account": d.is_group, "account_name": ( f"{d.account_number} - {d.account_name}" if d.account_number else d.account_name ), @@ -409,6 +410,10 @@ def prepare_data(accounts, filters, parent_children_map, company_currency): data.append(row) total_row = calculate_total_row(accounts, company_currency) + + if not filters.get("show_group_accounts"): + data = hide_group_accounts(data) + data.extend([{}, total_row]) return data @@ -488,3 +493,12 @@ def prepare_opening_closing(row): row[valid_col] = 0.0 else: row[reverse_col] = 0.0 + + +def hide_group_accounts(data): + non_group_accounts_data = [] + for d in data: + if not d.get("is_group_account"): + d.update(indent=0) + non_group_accounts_data.append(d) + return non_group_accounts_data From 07c56221a5d40a1c092d778cb163a1d227903495 Mon Sep 17 00:00:00 2001 From: AarDG10 Date: Tue, 27 Jan 2026 11:35:50 +0530 Subject: [PATCH 05/51] fix(RFQ): render email templates for preview and sending (cherry picked from commit 525b3960e15734e00147b2845b2a96479a849787) --- .../request_for_quotation.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 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 b66cb09e082..cd22aec6960 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -304,12 +304,17 @@ class RequestforQuotation(BuyingController): else: sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None + rendered_message = frappe.render_template(self.message_for_supplier, doc_args) + subject_source = ( + self.subject + or frappe.get_value("Email Template", self.email_template, "subject") + or _("Request for Quotation") + ) + rendered_subject = frappe.render_template(subject_source, doc_args) if preview: return { - "message": self.message_for_supplier, - "subject": self.subject - or frappe.get_value("Email Template", self.email_template, "subject") - or _("Request for Quotation"), + "message": rendered_message, + "subject": rendered_subject, } attachments = [] @@ -333,10 +338,8 @@ class RequestforQuotation(BuyingController): self.send_email( data, sender, - self.subject - or frappe.get_value("Email Template", self.email_template, "subject") - or _("Request for Quotation"), - self.message_for_supplier, + rendered_subject, + rendered_message, attachments, ) From 43fc1ae4bf27209d8ca5756d476246861c969128 Mon Sep 17 00:00:00 2001 From: AarDG10 Date: Tue, 27 Jan 2026 11:59:18 +0530 Subject: [PATCH 06/51] ci: minor text correction (cherry picked from commit 37cdae2f3428a2afaa9632491c23dd2b29035075) --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 8b13b00c042..5db9c30135a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -60,7 +60,7 @@ body: description: Share exact version number of Frappe and ERPNext you are using. placeholder: | Frappe version - - ERPNext Verion - + ERPNext version - validations: required: true From 7b6c7c3e2740013d2b60f10469ed1b1175783290 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 29 Jan 2026 09:49:04 +0530 Subject: [PATCH 07/51] fix: production plan not considering planning datetime when creating WO (cherry picked from commit 4e19c7e8bd95b07c48cee54d752369395b0c2da4) --- .../doctype/production_plan/production_plan.py | 10 ++++++---- .../doctype/production_plan/test_production_plan.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 74ccdfd02aa..6173f40fb10 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -701,19 +701,21 @@ class ProductionPlan(Document): "project": self.project, } - key = (d.item_code, d.sales_order, d.sales_order_item, d.warehouse) + key = (d.item_code, d.sales_order, d.sales_order_item, d.warehouse, d.planned_start_date) if self.combine_items: - key = (d.item_code, d.sales_order, d.warehouse) + key = (d.item_code, d.sales_order, d.warehouse, d.planned_start_date) if not d.sales_order: - key = (d.name, d.item_code, d.warehouse) + key = (d.name, d.item_code, d.warehouse, d.planned_start_date) if not item_details["project"] and d.sales_order: item_details["project"] = frappe.get_cached_value("Sales Order", d.sales_order, "project") if self.get_items_from == "Material Request": item_details.update({"qty": d.planned_qty}) - item_dict[(d.item_code, d.material_request_item, d.warehouse)] = item_details + item_dict[ + (d.item_code, d.material_request_item, d.warehouse, d.planned_start_date) + ] = item_details else: item_details.update( { diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index b4b0468a0dd..25fc4f26247 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -867,7 +867,7 @@ class TestProductionPlan(FrappeTestCase): items_data = pln.get_production_items() # Update qty - items_data[(pln.po_items[0].name, item, None)]["qty"] = qty + items_data[(pln.po_items[0].name, item, None, pln.po_items[0].planned_start_date)]["qty"] = qty # Create and Submit Work Order for each item in items_data for _key, item in items_data.items(): From ad8c8cb0e81b322fbbeb837d5765444629cd70ee Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 14:30:22 +0530 Subject: [PATCH 08/51] Merge pull request #52140 from frappe/mergify/bp/version-15-hotfix/pr-52007 Fix: Set Zero Rate for Standalone Credit Note with Expired Batch (backport #52007) --- .../sales_invoice/test_sales_invoice.py | 60 +++++++++++++++++++ .../controllers/sales_and_purchase_return.py | 39 +++++++++++- erpnext/controllers/selling_controller.py | 21 ++++++- .../selling_settings/selling_settings.json | 12 +++- .../selling_settings/selling_settings.py | 1 + 5 files changed, 127 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index bd8af98b16f..f9b6ab4f07c 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -4775,6 +4775,66 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(q[0][0], 1) + @change_settings("Selling Settings", {"set_zero_rate_for_expired_batch": True}) + def test_zero_valuation_for_standalone_credit_note_with_expired_batch(self): + item_code = "_Test Item for Expiry Batch Zero Valuation" + make_item_for_si( + item_code, + { + "is_stock_item": 1, + "has_batch_no": 1, + "has_expiry_date": 1, + "shelf_life_in_days": 2, + "create_new_batch": 1, + "batch_number_series": "TBATCH-EBZV.####", + }, + ) + + se = make_stock_entry( + item_code=item_code, + qty=10, + target="_Test Warehouse - _TC", + rate=100, + ) + + # fetch batch no from bundle + batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + + si = create_sales_invoice( + posting_date=add_days(nowdate(), 3), + item=item_code, + qty=-10, + rate=100, + is_return=1, + update_stock=1, + use_serial_batch_fields=1, + do_not_save=1, + do_not_submit=1, + ) + + si.items[0].batch_no = batch_no + si.save() + si.submit() + + si.reload() + # check zero incoming rate in voucher + self.assertEqual(si.items[0].incoming_rate, 0.0) + + # chekc zero incoming rate in stock ledger + stock_ledger_entry = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Sales Invoice", + "voucher_no": si.name, + "item_code": item_code, + "warehouse": "_Test Warehouse - _TC", + }, + ["incoming_rate", "valuation_rate"], + as_dict=True, + ) + + self.assertEqual(stock_ledger_entry.incoming_rate, 0.0) + def make_item_for_si(item_code, properties=None): from erpnext.stock.doctype.item.test_item import make_item diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index c774c3de341..d3d5cb808f3 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -11,7 +11,7 @@ from frappe.utils import cint, flt, format_datetime, get_datetime import erpnext from erpnext.stock.serial_batch_bundle import get_batches_from_bundle from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle -from erpnext.stock.utils import get_incoming_rate, get_valuation_method +from erpnext.stock.utils import get_incoming_rate, get_valuation_method, getdate class StockOverReturnError(frappe.ValidationError): @@ -683,6 +683,29 @@ def get_rate_for_return( else: select_field = "abs(stock_value_difference / actual_qty)" + item_details = frappe.get_cached_value("Item", item_code, ["has_batch_no", "has_expiry_date"], as_dict=1) + set_zero_rate_for_expired_batch = frappe.db.get_single_value( + "Selling Settings", "set_zero_rate_for_expired_batch" + ) + + if ( + set_zero_rate_for_expired_batch + and item_details.has_batch_no + and item_details.has_expiry_date + and not return_against + and voucher_type in ["Sales Invoice", "Delivery Note"] + ): + # set incoming_rate zero explicitly for standalone credit note with expired batch + batch_no = frappe.db.get_value(f"{voucher_type} Item", voucher_detail_no, "batch_no") + if batch_no and is_batch_expired(batch_no, sle.get("posting_date")): + frappe.db.set_value( + voucher_type + " Item", + voucher_detail_no, + "incoming_rate", + 0, + ) + return 0 + rate = flt(frappe.db.get_value("Stock Ledger Entry", filters, select_field)) if not (rate and return_against) and voucher_type in ["Sales Invoice", "Delivery Note"]: rate = frappe.db.get_value(f"{voucher_type} Item", voucher_detail_no, "incoming_rate") @@ -1152,3 +1175,17 @@ def get_available_serial_nos(serial_nos, warehouse): def get_payment_data(invoice): payment = frappe.db.get_all("Sales Invoice Payment", {"parent": invoice}, ["mode_of_payment", "amount"]) return payment + + +def is_batch_expired(batch_no, posting_date): + """ + To check whether the batch is expired or not based on the posting date. + """ + expiry_date = frappe.db.get_value("Batch", batch_no, "expiry_date") + if not expiry_date: + return + + if getdate(posting_date) > getdate(expiry_date): + return True + + return False diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 3667a7a7e76..5c2dc7491c2 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -8,7 +8,7 @@ from frappe.utils import cint, flt, get_link_to_form, nowtime from erpnext.accounts.party import render_address from erpnext.controllers.accounts_controller import get_taxes_and_charges -from erpnext.controllers.sales_and_purchase_return import get_rate_for_return +from erpnext.controllers.sales_and_purchase_return import get_rate_for_return, is_batch_expired from erpnext.controllers.stock_controller import StockController from erpnext.stock.doctype.item.item import set_item_default from erpnext.stock.get_item_details import get_bin_details, get_conversion_factor @@ -521,16 +521,31 @@ class SellingController(StockController): allow_at_arms_length_price = frappe.get_cached_value( "Stock Settings", None, "allow_internal_transfer_at_arms_length_price" ) + set_zero_rate_for_expired_batch = frappe.db.get_single_value( + "Selling Settings", "set_zero_rate_for_expired_batch" + ) + items = self.get("items") + (self.get("packed_items") or []) for d in items: if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"): continue item_details = frappe.get_cached_value( - "Item", d.item_code, ["has_serial_no", "has_batch_no"], as_dict=1 + "Item", d.item_code, ["has_serial_no", "has_batch_no", "has_expiry_date"], as_dict=1 ) - if not self.get("return_against") or ( + if ( + set_zero_rate_for_expired_batch + and item_details.has_batch_no + and item_details.has_expiry_date + and self.get("is_return") + and not self.get("return_against") + and is_batch_expired(d.batch_no, self.get("posting_date")) + ): + # set incoming rate as zero for stand-lone credit note with expired batch + d.incoming_rate = 0 + + elif not self.get("return_against") or ( get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return") and not item_details.has_serial_no diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index d2f8945eb29..5bef5bc55a2 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -35,7 +35,8 @@ "hide_tax_id", "enable_discount_accounting", "allow_zero_qty_in_quotation", - "allow_zero_qty_in_sales_order" + "allow_zero_qty_in_sales_order", + "set_zero_rate_for_expired_batch" ], "fields": [ { @@ -224,6 +225,13 @@ "fieldname": "fallback_to_default_price_list", "fieldtype": "Check", "label": "Use Prices from Default Price List as Fallback" + }, + { + "default": "0", + "description": "If enabled, system will set incoming rate as zero for stand-alone credit notes with expired batch item.", + "fieldname": "set_zero_rate_for_expired_batch", + "fieldtype": "Check", + "label": "Set Incoming Rate as Zero for Expired Batch" } ], "grid_page_length": 50, @@ -232,7 +240,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2026-01-21 17:28:37.027837", + "modified": "2026-01-24 00:04:33.105916", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py index f15fdc7041d..cad8385ab73 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.py +++ b/erpnext/selling/doctype/selling_settings/selling_settings.py @@ -41,6 +41,7 @@ class SellingSettings(Document): role_to_override_stop_action: DF.Link | None sales_update_frequency: DF.Literal["Monthly", "Each Transaction", "Daily"] selling_price_list: DF.Link | None + set_zero_rate_for_expired_batch: DF.Check so_required: DF.Literal["No", "Yes"] territory: DF.Link | None validate_selling_price: DF.Check From 41c592a1a8668ddf8ab7847857e7d981561679c6 Mon Sep 17 00:00:00 2001 From: kavin-114 Date: Thu, 29 Jan 2026 00:20:23 +0530 Subject: [PATCH 09/51] fix(stock): set incoming_rate with lcv rate for internal purchase (cherry picked from commit f0dccc3cd7c797a62bbbba4894857dbbe9a60981) --- erpnext/stock/stock_ledger.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 55e30258044..be351b195e2 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -2377,6 +2377,7 @@ def get_incoming_rate_for_inter_company_transfer(sle) -> float: For inter company transfer, incoming rate is the average of the outgoing rate """ rate = 0.0 + lcv_rate = 0.0 field = "delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item" @@ -2391,7 +2392,15 @@ def get_incoming_rate_for_inter_company_transfer(sle) -> float: "incoming_rate", ) - return rate + # add lcv amount in incoming_rate + lcv_amount = frappe.db.get_value( + f"{sle.voucher_type} Item", sle.voucher_detail_no, "landed_cost_voucher_amount" + ) + + if lcv_amount: + lcv_rate = flt(lcv_amount / abs(sle.actual_qty)) + + return rate + lcv_rate def is_internal_transfer(sle): From 3ccd1b4a6cd9370f621d13d5dd80df3022d1cb98 Mon Sep 17 00:00:00 2001 From: kavin-114 Date: Thu, 29 Jan 2026 00:21:02 +0530 Subject: [PATCH 10/51] test: add unit test to check internal purchase with lcv (cherry picked from commit dd4fd89ef84126332552a202a69abe4fa5aad1e3) --- .../purchase_receipt/test_purchase_receipt.py | 78 +++++++++++++++++++ erpnext/stock/stock_ledger.py | 1 - 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index ee7b327adca..341c38bdcf1 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -4716,6 +4716,84 @@ class TestPurchaseReceipt(FrappeTestCase): return_pr = make_return_doc("Purchase Receipt", pr.name) self.assertRaises(frappe.ValidationError, return_pr.submit) + def test_internal_purchase_receipt_incoming_rate_with_lcv(self): + """ + To test inter branch transaction incoming rate calculation with lcv after item reposting + """ + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + prepare_data_for_internal_transfer() + customer = "_Test Internal Customer 2" + company = "_Test Company with perpetual inventory" + item_doc = create_item("_Test Internal PR LCV Item") + lcv_expense_account = "Expenses Included In Valuation - TCP1" + + from_warehouse = create_warehouse("_Test Internal From Warehouse LCV", company=company) + to_warehouse = create_warehouse("_Test Internal To Warehouse LCV", company=company) + + # inward qty for internal transactions + make_purchase_receipt( + item_code=item_doc.item_code, + qty=5, + rate=100, + company="_Test Company with perpetual inventory", + warehouse=from_warehouse, + ) + + idn = create_delivery_note( + item_code=item_doc.name, + company=company, + customer=customer, + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + qty=5, + rate=100, + warehouse=from_warehouse, + target_warehouse=to_warehouse, + ) + self.assertEqual(idn.items[0].rate, 100) + + ipr = make_inter_company_purchase_receipt(idn.name) + ipr.items[0].warehouse = from_warehouse + self.assertEqual(ipr.items[0].rate, 100) + ipr.submit() + + self.create_lcv(ipr.doctype, ipr.name, company, lcv_expense_account, charges=100) + ipr.reload() + + self.assertEqual(ipr.items[0].landed_cost_voucher_amount, 100) + self.assertEqual(ipr.items[0].valuation_rate, 120) + + # repost the receipt and check the stock ledger values + repost_doc = frappe.new_doc("Repost Item Valuation") + repost_doc.update( + { + "based_on": "Transaction", + "voucher_type": ipr.doctype, + "voucher_no": ipr.name, + "posting_date": ipr.posting_date, + "posting_time": ipr.posting_time, + "company": ipr.company, + "allow_negative_stock": 1, + "via_landed_cost_voucher": 0, + "allow_zero_rate": 0, + } + ) + repost_doc.save() + repost_doc.submit() + + stk_ledger = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": ipr.name, "warehouse": from_warehouse}, + ["incoming_rate", "stock_value_difference"], + as_dict=True, + ) + + # check the incoming rate and stock value change + self.assertEqual(stk_ledger.incoming_rate, 120) + self.assertEqual(stk_ledger.stock_value_difference, 600) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index be351b195e2..ae3730d4897 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -821,7 +821,6 @@ class update_entries_after: if not self.validate_negative_stock(sle): self.wh_data.qty_after_transaction += flt(sle.actual_qty) return - # Get dynamic incoming/outgoing rate if not self.args.get("sle_id"): self.get_dynamic_incoming_outgoing_rate(sle) From 19e0d75c225e92becbe55bc4b09fea6d9b8c9219 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 30 Jan 2026 11:37:21 +0530 Subject: [PATCH 11/51] fix(barcode): failing request when item has both batch and serial (cherry picked from commit 89f6f0f46f97d8847254f387744eb6723e50a1ff) --- erpnext/public/js/utils/barcode_scanner.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js index 0d93fd21382..77246d82bc6 100644 --- a/erpnext/public/js/utils/barcode_scanner.js +++ b/erpnext/public/js/utils/barcode_scanner.js @@ -138,14 +138,14 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { frappe.run_serially([ () => this.set_selector_trigger_flag(data), - () => this.set_serial_no(row, serial_no), - () => this.set_batch_no(row, batch_no), () => this.set_barcode(row, barcode), () => this.set_warehouse(row), () => this.set_item(row, item_code, barcode, batch_no, serial_no).then((qty) => { this.show_scan_message(row.idx, !is_new_row, qty); }), + () => this.set_serial_no(row, serial_no), + () => this.set_batch_no(row, batch_no), () => this.clean_up(), () => this.set_barcode_uom(row, uom), () => this.revert_selector_flag(), From bd96868736a4cb4f7b4d7b17b0c70f3832cb420d Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 30 Jan 2026 11:56:38 +0530 Subject: [PATCH 12/51] fix: hide close button on WO if WO is completed (cherry picked from commit 6e17ccf49919ef46171bed8fa5dca2f73db17e24) --- erpnext/manufacturing/doctype/work_order/work_order.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 9e0679a7cd1..971b811fbcc 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -664,7 +664,7 @@ erpnext.work_order = { set_custom_buttons: function (frm) { var doc = frm.doc; - if (doc.docstatus === 1 && doc.status !== "Closed") { + if (doc.docstatus === 1 && !["Closed", "Completed"].includes(doc.status)) { frm.add_custom_button( __("Close"), function () { @@ -674,9 +674,6 @@ erpnext.work_order = { }, __("Status") ); - } - - if (doc.docstatus === 1 && !["Closed", "Completed"].includes(doc.status)) { if (doc.status != "Stopped" && doc.status != "Completed") { frm.add_custom_button( __("Stop"), From d5570f83d28028d3b04d2bbd8420c3813b3bbac2 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 30 Jan 2026 10:19:35 +0530 Subject: [PATCH 13/51] fix: add precision to rejected batch no qty calculation (cherry picked from commit 838d2452153d6f331520d3c3e14f2aa4b6fe07a0) --- erpnext/controllers/stock_controller.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index bf91f9490a4..a20d59333b6 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -465,7 +465,10 @@ class StockController(AccountsController): if is_rejected: serial_nos = row.get("rejected_serial_no") type_of_transaction = "Inward" if not self.is_return else "Outward" - qty = row.get("rejected_qty") * row.get("conversion_factor", 1.0) + qty = flt( + row.get("rejected_qty") * row.get("conversion_factor", 1.0), + frappe.get_precision("Serial and Batch Entry", "qty"), + ) warehouse = row.get("rejected_warehouse") if ( From 0e60750bd848aa7c5550900c2055ea04d7799611 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 30 Jan 2026 16:37:08 +0530 Subject: [PATCH 14/51] fix: validate over ordering of quotation (cherry picked from commit 4cc306d2d88e39646b93209774a5a7e0cf9c29d6) # Conflicts: # erpnext/controllers/status_updater.py # erpnext/patches.txt # erpnext/selling/doctype/quotation/quotation.py # erpnext/selling/doctype/quotation_item/quotation_item.json --- erpnext/controllers/status_updater.py | 12 ++++++++++ erpnext/patches.txt | 6 +++++ .../set_ordered_qty_in_quotation_item.py | 16 +++++++++++++ .../selling/doctype/quotation/quotation.py | 24 ++++++++----------- .../quotation_item/quotation_item.json | 19 ++++++++++++++- .../doctype/quotation_item/quotation_item.py | 1 + .../doctype/sales_order/sales_order.py | 11 +++++++++ 7 files changed, 74 insertions(+), 15 deletions(-) create mode 100644 erpnext/patches/v16_0/set_ordered_qty_in_quotation_item.py diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index a7924288058..1115381b10f 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -341,10 +341,22 @@ class StatusUpdater(Document): ): return +<<<<<<< HEAD if qty_or_amount == "qty": action_msg = _( 'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.' ) +======= + if args["source_dt"] != "Pick List Item" and args["target_dt"] != "Quotation Item": + if qty_or_amount == "qty": + action_msg = _( + 'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.' + ) + else: + action_msg = _( + 'To allow over billing, update "Over Billing Allowance" in Accounts Settings or the Item.' + ) +>>>>>>> 4cc306d2d8 (fix: validate over ordering of quotation) else: action_msg = _( 'To allow over billing, update "Over Billing Allowance" in Accounts Settings or the Item.' diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 0fdfbef279b..7af2e39eb98 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -428,3 +428,9 @@ execute:frappe.db.set_single_value("Accounts Settings", "show_party_balance", 1) execute:frappe.db.set_single_value("Accounts Settings", "show_account_balance", 1) erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter #2025-12-11 erpnext.patches.v15_0.create_accounting_dimensions_in_advance_taxes_and_charges +<<<<<<< HEAD +======= +execute:frappe.delete_doc_if_exists("Workspace Sidebar", "Opening & Closing") +erpnext.patches.v16_0.migrate_transaction_deletion_task_flags_to_status # 2 +erpnext.patches.v16_0.set_ordered_qty_in_quotation_item +>>>>>>> 4cc306d2d8 (fix: validate over ordering of quotation) diff --git a/erpnext/patches/v16_0/set_ordered_qty_in_quotation_item.py b/erpnext/patches/v16_0/set_ordered_qty_in_quotation_item.py new file mode 100644 index 00000000000..93a6323eb6f --- /dev/null +++ b/erpnext/patches/v16_0/set_ordered_qty_in_quotation_item.py @@ -0,0 +1,16 @@ +import frappe + + +def execute(): + data = frappe.get_all( + "Sales Order Item", + filters={"quotation_item": ["is", "set"], "docstatus": 1}, + fields=["quotation_item", {"SUM": "stock_qty", "as": "ordered_qty"}], + group_by="quotation_item", + ) + if data: + frappe.db.auto_commit_on_many_writes = 1 + frappe.db.bulk_update( + "Quotation Item", {d.quotation_item: {"ordered_qty": d.ordered_qty} for d in data} + ) + frappe.db.auto_commit_on_many_writes = 0 diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index d6b2fe73cac..7d67807aee1 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -446,7 +446,10 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, ar "Quotation", source_name, { - "Quotation": {"doctype": "Sales Order", "validation": {"docstatus": ["=", 1]}}, + "Quotation": { + "doctype": "Sales Order", + "validation": {"docstatus": ["=", 1]}, + }, "Quotation Item": { "doctype": "Sales Order Item", "field_map": {"parent": "prevdoc_docname", "name": "quotation_item"}, @@ -549,6 +552,8 @@ def _make_customer(source_name, ignore_permissions=False): if quotation.quotation_to == "Customer": return frappe.get_doc("Customer", quotation.party_name) + elif quotation.quotation_to == "CRM Deal": + return frappe.get_doc("Customer", {"crm_deal": quotation.party_name}) # Check if a Customer already exists for the Lead or Prospect. existing_customer = None @@ -610,25 +615,16 @@ def handle_mandatory_error(e, customer, lead_name): def get_ordered_items(quotation: str): - """ - Returns a dict of ordered items with their total qty based on quotation row name. - - In `Sales Order Item`, `quotation_item` is the row name of `Quotation Item`. - - Example: - ``` - { - "refsdjhd2": 10, - "ygdhdshrt": 5, - } - ``` - """ return frappe._dict( frappe.get_all( +<<<<<<< HEAD "Sales Order Item", filters={"prevdoc_docname": quotation, "docstatus": 1}, fields=["quotation_item", "sum(qty)"], group_by="quotation_item", as_list=1, +======= + "Quotation Item", {"docstatus": 1, "parent": quotation}, ["name", "ordered_qty"], as_list=True +>>>>>>> 4cc306d2d8 (fix: validate over ordering of quotation) ) ) diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json index 74c4670063e..2f723e08120 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.json +++ b/erpnext/selling/doctype/quotation_item/quotation_item.json @@ -24,6 +24,7 @@ "uom", "conversion_factor", "stock_qty", + "ordered_qty", "available_quantity_section", "actual_qty", "column_break_ylrv", @@ -694,18 +695,34 @@ "print_hide": 1, "read_only": 1, "report_hide": 1 + }, + { + "default": "0", + "fieldname": "ordered_qty", + "fieldtype": "Float", + "hidden": 1, + "label": "Ordered Qty", + "no_copy": 1, + "non_negative": 1, + "read_only": 1, + "reqd": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2025-08-26 20:31:47.775890", + "modified": "2026-01-30 12:56:08.320190", "modified_by": "Administrator", "module": "Selling", "name": "Quotation Item", "owner": "Administrator", "permissions": [], +<<<<<<< HEAD "sort_field": "modified", +======= + "row_format": "Dynamic", + "sort_field": "creation", +>>>>>>> 4cc306d2d8 (fix: validate over ordering of quotation) "sort_order": "DESC", "states": [], "track_changes": 1 diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.py b/erpnext/selling/doctype/quotation_item/quotation_item.py index bbdd8643593..9ab265c885c 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.py +++ b/erpnext/selling/doctype/quotation_item/quotation_item.py @@ -48,6 +48,7 @@ class QuotationItem(Document): margin_type: DF.Literal["", "Percentage", "Amount"] net_amount: DF.Currency net_rate: DF.Currency + ordered_qty: DF.Float page_break: DF.Check parent: DF.Data parentfield: DF.Data diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index a757efffa4e..a1047c11a96 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -185,6 +185,16 @@ class SalesOrder(SellingController): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.status_updater = [ + { + "source_dt": "Sales Order Item", + "target_dt": "Quotation Item", + "join_field": "quotation_item", + "target_field": "ordered_qty", + "target_ref_field": "stock_qty", + "source_field": "stock_qty", + } + ] def onload(self) -> None: super().onload() @@ -419,6 +429,7 @@ class SalesOrder(SellingController): frappe.throw(_("Row #{0}: Set Supplier for item {1}").format(d.idx, d.item_code)) def on_submit(self): + super().update_prevdoc_status() self.check_credit_limit() self.update_reserved_qty() From 2c74491eb61133d401a2ec8566b527d1fd933dde Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 30 Jan 2026 19:16:39 +0530 Subject: [PATCH 15/51] fix: test cases (cherry picked from commit 36f1e3572c488475185d3f7307c6f898b85c7ad5) --- erpnext/selling/doctype/quotation/test_quotation.py | 4 ++-- erpnext/selling/doctype/sales_order/test_sales_order.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 210f4715815..e72b595f824 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -828,7 +828,7 @@ class TestQuotation(FrappeTestCase): # item code same but description different make_item("_Test Item 2", {"is_stock_item": 1}) - quotation = make_quotation(qty=1, rate=100, do_not_submit=1) + quotation = make_quotation(qty=10, rate=100, do_not_submit=1) # duplicate items for qty in [1, 1, 2, 3]: @@ -842,7 +842,7 @@ class TestQuotation(FrappeTestCase): sales_order.delivery_date = nowdate() self.assertEqual(len(sales_order.items), 6) - self.assertEqual(sales_order.items[0].qty, 1) + self.assertEqual(sales_order.items[0].qty, 10) self.assertEqual(sales_order.items[-1].qty, 5) # Row 1: 10, Row 4: 1, Row 5: 1 diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index db4d60b56f6..96ef110b965 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -57,6 +57,7 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): def tearDown(self): frappe.set_user("Administrator") + @IntegrationTestCase.change_settings("Selling Settings", {"allow_negative_rates_for_items": 1}) def test_sales_order_with_negative_rate(self): """ Test if negative rate is allowed in Sales Order via doc submission and update items From fec3a8b5115af95142bc25bf666ec4c30e462b61 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 30 Jan 2026 20:05:52 +0530 Subject: [PATCH 16/51] fix: validation when more than one FG items in repack stock entry (cherry picked from commit 6423ce2fa7febc4834c31af6d482dd719255fbb0) # Conflicts: # erpnext/stock/doctype/stock_entry/stock_entry.py --- .../stock/doctype/stock_entry/stock_entry.py | 20 +++++++++++++++++++ .../doctype/stock_entry/test_stock_entry.py | 4 ++++ 2 files changed, 24 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index d313c037e02..80571a1d813 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -226,6 +226,7 @@ class StockEntry(StockController): self.validate_job_card_item() self.set_purpose_for_stock_entry() self.clean_serial_nos() + self.validate_repack_entry() if not self.from_bom: self.fg_completed_qty = 0.0 @@ -245,6 +246,25 @@ class StockEntry(StockController): self.validate_same_source_target_warehouse_during_material_transfer() self.validate_raw_materials_exists() +<<<<<<< HEAD +======= + super().validate_subcontracting_inward() + + def validate_repack_entry(self): + if self.purpose != "Repack": + return + + fg_items = {row.item_code: row for row in self.items if row.is_finished_item} + + if len(fg_items) > 1 and not all(row.set_basic_rate_manually for row in fg_items.values()): + frappe.throw( + _( + "When there are multiple finished goods ({0}) in a Repack stock entry, the basic rate for all finished goods must be set manually. To set rate manually, enable the checkbox 'Set Basic Rate Manually' in the respective finished good row." + ).format(", ".join(fg_items)), + title=_("Set Basic Rate Manually"), + ) + +>>>>>>> 6423ce2fa7 (fix: validation when more than one FG items in repack stock entry) def validate_raw_materials_exists(self): if self.purpose not in ["Manufacture", "Repack", "Disassemble"]: return diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 2383fabaf89..f11854f52a9 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -413,6 +413,10 @@ class TestStockEntry(FrappeTestCase): }, ) repack.set_stock_entry_type() + for row in repack.items: + if row.t_warehouse: + row.set_basic_rate_manually = 1 + repack.insert() self.assertEqual(repack.items[1].is_finished_item, 1) From 6cecae288cf27321ea51479542df96750ef413cc Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 31 Jan 2026 20:18:22 +0530 Subject: [PATCH 17/51] chore: resolve conflicts --- erpnext/selling/doctype/quotation/quotation.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 7d67807aee1..3f30664e39b 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -617,14 +617,6 @@ def handle_mandatory_error(e, customer, lead_name): def get_ordered_items(quotation: str): return frappe._dict( frappe.get_all( -<<<<<<< HEAD - "Sales Order Item", - filters={"prevdoc_docname": quotation, "docstatus": 1}, - fields=["quotation_item", "sum(qty)"], - group_by="quotation_item", - as_list=1, -======= "Quotation Item", {"docstatus": 1, "parent": quotation}, ["name", "ordered_qty"], as_list=True ->>>>>>> 4cc306d2d8 (fix: validate over ordering of quotation) ) ) From 4017936befcca81fb4d0edfa6b12d430b028ec54 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 31 Jan 2026 20:19:07 +0530 Subject: [PATCH 18/51] chore: resolve conflicts --- erpnext/selling/doctype/quotation_item/quotation_item.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json index 2f723e08120..27d318de4b0 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.json +++ b/erpnext/selling/doctype/quotation_item/quotation_item.json @@ -717,12 +717,7 @@ "name": "Quotation Item", "owner": "Administrator", "permissions": [], -<<<<<<< HEAD "sort_field": "modified", -======= - "row_format": "Dynamic", - "sort_field": "creation", ->>>>>>> 4cc306d2d8 (fix: validate over ordering of quotation) "sort_order": "DESC", "states": [], "track_changes": 1 From c3b92075f00a7bfb550db48639bc8cb7933cdade Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 31 Jan 2026 20:20:36 +0530 Subject: [PATCH 19/51] chore: resolve conflicts Removed old patch entries and updated the list. --- erpnext/patches.txt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 7af2e39eb98..7ded266c62a 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -428,9 +428,4 @@ execute:frappe.db.set_single_value("Accounts Settings", "show_party_balance", 1) execute:frappe.db.set_single_value("Accounts Settings", "show_account_balance", 1) erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter #2025-12-11 erpnext.patches.v15_0.create_accounting_dimensions_in_advance_taxes_and_charges -<<<<<<< HEAD -======= -execute:frappe.delete_doc_if_exists("Workspace Sidebar", "Opening & Closing") -erpnext.patches.v16_0.migrate_transaction_deletion_task_flags_to_status # 2 erpnext.patches.v16_0.set_ordered_qty_in_quotation_item ->>>>>>> 4cc306d2d8 (fix: validate over ordering of quotation) From 7ab59aa09422cfc327228e9c83678c6118007730 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 31 Jan 2026 20:25:00 +0530 Subject: [PATCH 20/51] chore: resolve conflicts --- erpnext/controllers/status_updater.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 1115381b10f..048ef72b46c 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -341,12 +341,6 @@ class StatusUpdater(Document): ): return -<<<<<<< HEAD - if qty_or_amount == "qty": - action_msg = _( - 'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.' - ) -======= if args["source_dt"] != "Pick List Item" and args["target_dt"] != "Quotation Item": if qty_or_amount == "qty": action_msg = _( @@ -356,11 +350,8 @@ class StatusUpdater(Document): action_msg = _( 'To allow over billing, update "Over Billing Allowance" in Accounts Settings or the Item.' ) ->>>>>>> 4cc306d2d8 (fix: validate over ordering of quotation) else: - action_msg = _( - 'To allow over billing, update "Over Billing Allowance" in Accounts Settings or the Item.' - ) + action_msg = None frappe.throw( _( From 6a681557a9558d8c1ba8e323db9dea17c4c2b2f6 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 31 Jan 2026 20:27:02 +0530 Subject: [PATCH 21/51] fix: remove unneccessary check --- erpnext/controllers/status_updater.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 048ef72b46c..ab27ddb50aa 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -341,7 +341,7 @@ class StatusUpdater(Document): ): return - if args["source_dt"] != "Pick List Item" and args["target_dt"] != "Quotation Item": + if args["target_dt"] != "Quotation Item": if qty_or_amount == "qty": action_msg = _( 'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.' From 42f94f1abaa68ea9dfc358cf5e3aeb5403525da2 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 31 Jan 2026 20:36:46 +0530 Subject: [PATCH 22/51] chore: remove incorrect import --- erpnext/selling/doctype/sales_order/test_sales_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 96ef110b965..13759d0f7f7 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -57,7 +57,7 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): def tearDown(self): frappe.set_user("Administrator") - @IntegrationTestCase.change_settings("Selling Settings", {"allow_negative_rates_for_items": 1}) + @change_settings("Selling Settings", {"allow_negative_rates_for_items": 1}) def test_sales_order_with_negative_rate(self): """ Test if negative rate is allowed in Sales Order via doc submission and update items From 7e01ae9e4aef20c2d87490a884b66f1301199192 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 1 Feb 2026 10:26:14 +0530 Subject: [PATCH 23/51] fix: revert to old orm --- erpnext/patches/v16_0/set_ordered_qty_in_quotation_item.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/patches/v16_0/set_ordered_qty_in_quotation_item.py b/erpnext/patches/v16_0/set_ordered_qty_in_quotation_item.py index 93a6323eb6f..faa99fcd2ca 100644 --- a/erpnext/patches/v16_0/set_ordered_qty_in_quotation_item.py +++ b/erpnext/patches/v16_0/set_ordered_qty_in_quotation_item.py @@ -5,7 +5,7 @@ def execute(): data = frappe.get_all( "Sales Order Item", filters={"quotation_item": ["is", "set"], "docstatus": 1}, - fields=["quotation_item", {"SUM": "stock_qty", "as": "ordered_qty"}], + fields=["quotation_item", "sum(stock_qty) as ordered_qty"], group_by="quotation_item", ) if data: From 6d14cb0c6bcab121be4e560d8f99dc66da088f85 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 1 Feb 2026 16:30:59 +0530 Subject: [PATCH 24/51] chore: fix conflicts --- erpnext/stock/doctype/stock_entry/stock_entry.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 80571a1d813..d7158bf1680 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -246,10 +246,6 @@ class StockEntry(StockController): self.validate_same_source_target_warehouse_during_material_transfer() self.validate_raw_materials_exists() -<<<<<<< HEAD -======= - super().validate_subcontracting_inward() - def validate_repack_entry(self): if self.purpose != "Repack": return @@ -264,7 +260,6 @@ class StockEntry(StockController): title=_("Set Basic Rate Manually"), ) ->>>>>>> 6423ce2fa7 (fix: validation when more than one FG items in repack stock entry) def validate_raw_materials_exists(self): if self.purpose not in ["Manufacture", "Repack", "Disassemble"]: return From d516110572b5d22558a0314bd11faebccc7da008 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 2 Feb 2026 10:34:23 +0530 Subject: [PATCH 25/51] chore: fix py error on v15 --- erpnext/controllers/status_updater.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index ab27ddb50aa..cc821b22272 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -363,8 +363,7 @@ class StatusUpdater(Document): frappe.bold(_(self.doctype)), frappe.bold(item.get("item_code")), ) - + "

" - + action_msg, + + ("

" + action_msg if action_msg else ""), OverAllowanceError, title=_("Limit Crossed"), ) From d9d4b9b1173d301afc0e7a8a59bed9d4119bbea0 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 2 Feb 2026 09:51:09 +0530 Subject: [PATCH 26/51] test: over ordering of quotation items (cherry picked from commit 53e58f66785920e8194ed3e0c221828e4e808293) --- erpnext/selling/doctype/quotation/test_quotation.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index e72b595f824..1086ce09ef5 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -885,6 +885,16 @@ class TestQuotation(FrappeTestCase): f"Expected conversion rate {expected_rate}, got {quotation.conversion_rate}", ) + def test_over_order_limit(self): + quotation = make_quotation(qty=5) + so1 = make_sales_order(quotation.name) + so2 = make_sales_order(quotation.name) + so1.delivery_date = nowdate() + so2.delivery_date = nowdate() + + so1.submit() + self.assertRaises(frappe.ValidationError, so2.submit) + test_records = frappe.get_test_records("Quotation") From 528a4822405989574797b6721ee06ddb06270c1b Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 2 Feb 2026 10:12:55 +0530 Subject: [PATCH 27/51] fix: imports --- erpnext/selling/doctype/quotation/test_quotation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 1086ce09ef5..ae756f34288 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -886,6 +886,8 @@ class TestQuotation(FrappeTestCase): ) def test_over_order_limit(self): + from erpnext.selling.doctype.quotation.quotation import make_sales_order + quotation = make_quotation(qty=5) so1 = make_sales_order(quotation.name) so2 = make_sales_order(quotation.name) From 6413ce467f1708428f8179352589565e345b76b2 Mon Sep 17 00:00:00 2001 From: Tamal Majumdar Date: Thu, 29 Jan 2026 19:28:35 +0530 Subject: [PATCH 28/51] fix: journal auditing voucher print date to use posting_date (cherry picked from commit 43e2495df84afa4a41fb20025898499374901f54) --- .../journal_auditing_voucher/journal_auditing_voucher.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html b/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html index 8a6968ee373..ad8c908dbcf 100644 --- a/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html +++ b/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html @@ -17,7 +17,7 @@
- +
Date: {{ frappe.utils.format_date(doc.creation) }}
Date: {{ frappe.utils.format_date(doc.posting_date) }}
From c6937c8375bbb1d417d492884b4a5e840157212a Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 2 Feb 2026 14:08:27 +0530 Subject: [PATCH 29/51] fix: reset incoming rate in selling controller if there are changes in item (cherry picked from commit 2d6b43fd540d93138ce247b4d702d61d38e84e17) --- erpnext/controllers/selling_controller.py | 26 +++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 5c2dc7491c2..e661d8afbf4 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -518,6 +518,8 @@ class SellingController(StockController): if self.doctype not in ("Delivery Note", "Sales Invoice"): return + from erpnext.stock.serial_batch_bundle import get_batch_nos + allow_at_arms_length_price = frappe.get_cached_value( "Stock Settings", None, "allow_internal_transfer_at_arms_length_price" ) @@ -525,6 +527,7 @@ class SellingController(StockController): "Selling Settings", "set_zero_rate_for_expired_batch" ) + old_doc = self.get_doc_before_save() items = self.get("items") + (self.get("packed_items") or []) for d in items: if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"): @@ -554,6 +557,29 @@ class SellingController(StockController): # Get incoming rate based on original item cost based on valuation method qty = flt(d.get("stock_qty") or d.get("actual_qty") or d.get("qty")) + if old_doc: + old_item = next( + ( + item + for item in (old_doc.get("items") + (old_doc.get("packed_items") or [])) + if item.name == d.name + ), + None, + ) + if old_item: + old_qty = flt( + old_item.get("stock_qty") or old_item.get("actual_qty") or old_item.get("qty") + ) + if ( + old_item.item_code != d.item_code + or old_item.warehouse != d.warehouse + or old_qty != qty + or old_item.batch_no != d.batch_no + or get_batch_nos(old_item.serial_and_batch_bundle) + != get_batch_nos(d.serial_and_batch_bundle) + ): + d.incoming_rate = 0 + if ( not d.incoming_rate or self.is_internal_transfer() From 78f8922a9c7d4f9f82eb7976088c5785d38d8a6f Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 2 Feb 2026 19:59:53 +0530 Subject: [PATCH 30/51] fix: populate contact fields when creating quotation from customer (cherry picked from commit 75b2c2c83dc29e2d4d76c704238e9dd088c1a668) --- erpnext/selling/doctype/customer/customer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 7fb15943c9f..1ce480fbd2e 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -497,6 +497,9 @@ def _set_missing_values(source, target): if contact: target.contact_person = contact[0].parent + target.contact_display, target.contact_email, target.contact_mobile = frappe.get_value( + "Contact", contact[0].parent, ["full_name", "email_id", "mobile_no"] + ) @frappe.whitelist() From 5f60c0e85ec2dbff6c36e1bf5e2fd305bb4806c8 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 2 Feb 2026 20:44:22 +0530 Subject: [PATCH 31/51] Merge pull request #52246 from mihir-kandoi/st58765 (cherry picked from commit 135a433018457345a56b834b20279d5240a00b70) # Conflicts: # erpnext/stock/doctype/delivery_note/test_delivery_note.py --- erpnext/controllers/selling_controller.py | 58 ++++---------- .../delivery_note/test_delivery_note.py | 78 +++++++++++++++++++ 2 files changed, 91 insertions(+), 45 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index e661d8afbf4..7f387eea690 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -279,7 +279,7 @@ class SellingController(StockController): _( """Row #{0}: Selling rate for item {1} is lower than its {2}. Selling {3} should be atleast {4}.

Alternatively, - you can disable selling price validation in {5} to bypass + you can disable '{5}' in {6} to bypass this validation.""" ).format( idx, @@ -287,7 +287,8 @@ class SellingController(StockController): bold(ref_rate_field), bold("net rate"), bold(rate), - get_link_to_form("Selling Settings", "Selling Settings"), + bold(frappe.get_meta("Selling Settings").get_label("validate_selling_price")), + get_link_to_form("Selling Settings"), ), title=_("Invalid Selling Price"), ) @@ -298,7 +299,6 @@ class SellingController(StockController): return is_internal_customer = self.get("is_internal_customer") - valuation_rate_map = {} for item in self.items: if not item.item_code or item.is_free_item: @@ -308,7 +308,9 @@ class SellingController(StockController): "Item", item.item_code, ("last_purchase_rate", "is_stock_item") ) - last_purchase_rate_in_sales_uom = last_purchase_rate * (item.conversion_factor or 1) + last_purchase_rate_in_sales_uom = flt( + last_purchase_rate * (item.conversion_factor or 1), item.precision("base_net_rate") + ) if flt(item.base_net_rate) < flt(last_purchase_rate_in_sales_uom): throw_message(item.idx, item.item_name, last_purchase_rate_in_sales_uom, "last purchase rate") @@ -316,50 +318,16 @@ class SellingController(StockController): if is_internal_customer or not is_stock_item: continue - valuation_rate_map[(item.item_code, item.warehouse)] = None - - if not valuation_rate_map: - return - - or_conditions = ( - f"""(item_code = {frappe.db.escape(valuation_rate[0])} - and warehouse = {frappe.db.escape(valuation_rate[1])})""" - for valuation_rate in valuation_rate_map - ) - - valuation_rates = frappe.db.sql( - f""" - select - item_code, warehouse, valuation_rate - from - `tabBin` - where - ({" or ".join(or_conditions)}) - and valuation_rate > 0 - """, - as_dict=True, - ) - - for rate in valuation_rates: - valuation_rate_map[(rate.item_code, rate.warehouse)] = rate.valuation_rate - - for item in self.items: - if not item.item_code or item.is_free_item: - continue - - last_valuation_rate = valuation_rate_map.get((item.item_code, item.warehouse)) - - if not last_valuation_rate: - continue - - last_valuation_rate_in_sales_uom = last_valuation_rate * (item.conversion_factor or 1) - - if flt(item.base_net_rate) < flt(last_valuation_rate_in_sales_uom): + if item.get("incoming_rate") and item.base_net_rate < ( + valuation_rate := flt( + item.incoming_rate * (item.conversion_factor or 1), item.precision("base_net_rate") + ) + ): throw_message( item.idx, item.item_name, - last_valuation_rate_in_sales_uom, - "valuation rate (Moving Average)", + valuation_rate, + "valuation rate", ) def get_item_list(self): diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 29eabe2670e..65cc37cff88 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -2807,6 +2807,84 @@ class TestDeliveryNote(FrappeTestCase): frappe.db.set_single_value("System Settings", "float_precision", original_flt_precision) +<<<<<<< HEAD +======= + def test_different_rate_for_same_serial_nos(self): + item_code = make_item( + "Test Different Rate Serial No Item", + properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "DRSN-.#####"}, + ).name + + se = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100) + serial_nos = get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle) + + dn = create_delivery_note( + item_code=item_code, + qty=1, + rate=300, + use_serial_batch_fields=1, + serial_no="\n".join(serial_nos), + ) + + dn.reload() + + sabb = frappe.get_doc("Serial and Batch Bundle", dn.items[0].serial_and_batch_bundle) + for entry in sabb.entries: + self.assertEqual(entry.incoming_rate, 100) + + make_stock_entry( + item_code=item_code, + target="_Test Warehouse - _TC", + qty=1, + basic_rate=200, + use_serial_batch_fields=1, + serial_no="\n".join(serial_nos), + ) + dn1 = create_delivery_note( + item_code=item_code, + qty=1, + rate=300, + use_serial_batch_fields=1, + serial_no="\n".join(serial_nos), + ) + + dn1.reload() + + sabb = frappe.get_doc("Serial and Batch Bundle", dn1.items[0].serial_and_batch_bundle) + for entry in sabb.entries: + self.assertEqual(entry.incoming_rate, 200) + + doc = frappe.new_doc("Repost Item Valuation") + doc.voucher_type = "Stock Entry" + doc.voucher_no = se.name + doc.submit() + + sabb = frappe.get_doc("Serial and Batch Bundle", dn.items[0].serial_and_batch_bundle) + for entry in sabb.entries: + self.assertEqual(entry.incoming_rate, 100) + + sabb = frappe.get_doc("Serial and Batch Bundle", dn1.items[0].serial_and_batch_bundle) + for entry in sabb.entries: + self.assertEqual(entry.incoming_rate, 200) + + @IntegrationTestCase.change_settings("Selling Settings", {"validate_selling_price": 1}) + def test_validate_selling_price(self): + item_code = make_item("VSP Item", properties={"is_stock_item": 1}).name + make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=10) + make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=1) + + dn = create_delivery_note( + item_code=item_code, + qty=1, + rate=9, + do_not_save=True, + ) + self.assertRaises(frappe.ValidationError, dn.save) + dn.items[0].incoming_rate = 0 + dn.items[0].stock_qty = 2 + dn.save() + +>>>>>>> 135a433018 (Merge pull request #52246 from mihir-kandoi/st58765) def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") From f5481dc7d563e6300014c4e5924158a9a7bde3a8 Mon Sep 17 00:00:00 2001 From: Solede Date: Mon, 2 Feb 2026 20:28:42 +0100 Subject: [PATCH 32/51] fix: backport Switzerland VAT rates update to version-15 (#52244) Co-authored-by: Claude Opus 4.5 --- .../setup/setup_wizard/data/country_wise_tax.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index 3616425771c..3259a2323b7 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -4844,17 +4844,17 @@ "Switzerland": { "Switzerland normal VAT": { - "account_name": "VAT 7.7%", - "tax_rate": 7.70, + "account_name": "VAT 8.1%", + "tax_rate": 8.10, "default": 1 }, "Switzerland reduced VAT": { - "account_name": "VAT 2.5%", - "tax_rate": 2.50 + "account_name": "VAT 2.6%", + "tax_rate": 2.60 }, "Switzerland lodging VAT": { - "account_name": "VAT 3.7%", - "tax_rate": 3.70 + "account_name": "VAT 3.8%", + "tax_rate": 3.80 } }, From ac1f29d5cb71c279e955cc4e31680c193767fdf8 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 19:37:52 +0000 Subject: [PATCH 33/51] fix: duplicate account number (Indonesia COA) (backport #52080) (#52316) Co-authored-by: Apriliansyah Idris Co-authored-by: Diptanil Saha fix: duplicate account number (Indonesia COA) (#52080) --- .../verified/id_chart_of_accounts.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json index fb974765db0..939e8219421 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json @@ -33,6 +33,17 @@ }, "account_number": "1151.000" }, + "Pajak Dibayar di Muka": { + "PPN Masukan": { + "account_number": "1152.001", + "account_type": "Tax" + }, + "PPh 23 Dibayar di Muka": { + "account_number": "1152.002", + "account_type": "Tax" + }, + "account_number": "1152.000" + }, "account_number": "1150.000" }, "Kas": { From 3b12d60877e5a26fa75b78c421dba6b3773f3856 Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Thu, 29 Jan 2026 15:42:18 +0530 Subject: [PATCH 34/51] fix(subcontracting): include item bom in supplied items grouping key (cherry picked from commit 0d372a62a1b9c2f0233f3c55a02725cfca970433) # Conflicts: # erpnext/controllers/subcontracting_controller.py --- .../controllers/subcontracting_controller.py | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 07d4fd31387..6d7d5d5d579 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -254,10 +254,14 @@ class SubcontractingController(StockController): ): for row in frappe.get_all( f"{self.subcontract_data.order_doctype} Item", +<<<<<<< HEAD fields=["item_code", "(qty - received_qty) as qty", "parent", "name"], +======= + fields=["item_code", {"SUB": ["qty", "received_qty"], "as": "qty"}, "parent", "bom"], +>>>>>>> 0d372a62a1 (fix(subcontracting): include item bom in supplied items grouping key) filters={"docstatus": 1, "parent": ("in", self.subcontract_orders)}, ): - self.qty_to_be_received[(row.item_code, row.parent)] += row.qty + self.qty_to_be_received[(row.item_code, row.parent, row.bom)] += row.qty def __get_transferred_items(self): se = frappe.qb.DocType("Stock Entry") @@ -829,13 +833,17 @@ class SubcontractingController(StockController): self.__set_serial_nos(item_row, rm_obj) def __get_qty_based_on_material_transfer(self, item_row, transfer_item): - key = (item_row.item_code, item_row.get(self.subcontract_data.order_field)) + key = ( + item_row.item_code, + item_row.get(self.subcontract_data.order_field), + item_row.get("bom"), + ) if self.qty_to_be_received == item_row.qty: return transfer_item.qty - if self.qty_to_be_received: - qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0)) + if self.qty_to_be_received.get(key): + qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key)) transfer_item.item_details.required_qty = transfer_item.qty if transfer_item.serial_no or frappe.get_cached_value( @@ -880,7 +888,11 @@ class SubcontractingController(StockController): if self.qty_to_be_received: self.qty_to_be_received[ - (row.item_code, row.get(self.subcontract_data.order_field)) + ( + row.item_code, + row.get(self.subcontract_data.order_field), + row.get("bom"), + ) ] -= row.qty def __set_rate_for_serial_and_batch_bundle(self): From 1d7ba16caffa91a11f4c9990c999c10e71840c54 Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Thu, 29 Jan 2026 15:46:41 +0530 Subject: [PATCH 35/51] test(subcontracting): add test for consumed_qty calculation with similar finished goods (cherry picked from commit 4d9412181c6538493c4420867236d14f11b03fde) --- .../test_subcontracting_receipt.py | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index 443ca8cb569..26025169979 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -618,6 +618,117 @@ class TestSubcontractingReceipt(FrappeTestCase): for item in scr.supplied_items: self.assertFalse(item.available_qty_for_consumption) + def test_supplied_items_consumed_qty_for_similar_finished_goods(self): + """ + Test that supplied raw material consumption is calculated correctly + when multiple subcontracted service items use the same finished good + but different BOMs. + """ + + from erpnext.controllers.subcontracting_controller import ( + make_rm_stock_entry as make_subcontract_transfer_entry, + ) + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + + # Configuration: Backflush based on subcontract material transfer + set_backflush_based_on("Material Transferred for Subcontract") + + # Create Raw Materials + raw_material_1 = make_item("_RM Item 1", properties={"is_stock_item": 1}).name + + raw_material_2 = make_item("_RM Item 2", properties={"is_stock_item": 1}).name + + # Create Subcontracted Finished Good + finished_good = make_item("_Finished Good Item", properties={"is_stock_item": 1}) + finished_good.is_sub_contracted_item = 1 + finished_good.save() + + # Receive Raw Materials into Warehouse + for raw_material in (raw_material_1, raw_material_2): + make_stock_entry( + item_code=raw_material, + qty=10, + target="_Test Warehouse - _TC", + basic_rate=100, + ) + + # Create BOMs for the same Finished Good with different RMs + bom_rm_1 = make_bom( + item=finished_good.name, + quantity=1, + raw_materials=[raw_material_1], + ).name + + _bom_rm_2 = make_bom( + item=finished_good.name, + quantity=1, + raw_materials=[raw_material_2], + ).name + + # Define Subcontracted Service Items + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 1, + "rate": 100, + "fg_item": finished_good.name, + "fg_item_qty": 10, + }, + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 1, + "rate": 150, + "fg_item": finished_good.name, + "fg_item_qty": 10, + }, + ] + + # Create Subcontracting Order + subcontracting_order = get_subcontracting_order( + service_items=service_items, + do_not_save=True, + ) + + # Assign BOM only to the first service item + subcontracting_order.items[0].bom = bom_rm_1 + subcontracting_order.save() + subcontracting_order.submit() + + # Prepare Raw Material Transfer Items + raw_material_transfer_items = [] + for supplied_item in subcontracting_order.supplied_items: + raw_material_transfer_items.append( + { + "item_code": supplied_item.main_item_code, + "rm_item_code": supplied_item.rm_item_code, + "qty": supplied_item.required_qty, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", + } + ) + + # Transfer Raw Materials to Subcontractor Warehouse + stock_entry = frappe.get_doc( + make_subcontract_transfer_entry( + subcontracting_order.name, + raw_material_transfer_items, + ) + ) + stock_entry.to_warehouse = "_Test Warehouse 1 - _TC" + stock_entry.save() + stock_entry.submit() + + # Create Subcontracting Receipt + subcontracting_receipt = make_subcontracting_receipt(subcontracting_order.name) + subcontracting_receipt.save() + + # Check consumed_qty for each supplied item + self.assertEqual(len(subcontracting_receipt.supplied_items), 2) + self.assertEqual(subcontracting_receipt.supplied_items[0].consumed_qty, 10) + self.assertEqual(subcontracting_receipt.supplied_items[1].consumed_qty, 10) + def test_supplied_items_cost_after_reposting(self): # Set Backflush Based On as "BOM" set_backflush_based_on("BOM") From 12a2e9875128643256c9795481efcf7b73b6b488 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 3 Feb 2026 09:10:41 +0530 Subject: [PATCH 36/51] chore: resolve conflicts --- .../delivery_note/test_delivery_note.py | 63 +------------------ 1 file changed, 1 insertion(+), 62 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 65cc37cff88..4c6c4be2a52 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -2807,67 +2807,7 @@ class TestDeliveryNote(FrappeTestCase): frappe.db.set_single_value("System Settings", "float_precision", original_flt_precision) -<<<<<<< HEAD -======= - def test_different_rate_for_same_serial_nos(self): - item_code = make_item( - "Test Different Rate Serial No Item", - properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "DRSN-.#####"}, - ).name - - se = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100) - serial_nos = get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle) - - dn = create_delivery_note( - item_code=item_code, - qty=1, - rate=300, - use_serial_batch_fields=1, - serial_no="\n".join(serial_nos), - ) - - dn.reload() - - sabb = frappe.get_doc("Serial and Batch Bundle", dn.items[0].serial_and_batch_bundle) - for entry in sabb.entries: - self.assertEqual(entry.incoming_rate, 100) - - make_stock_entry( - item_code=item_code, - target="_Test Warehouse - _TC", - qty=1, - basic_rate=200, - use_serial_batch_fields=1, - serial_no="\n".join(serial_nos), - ) - dn1 = create_delivery_note( - item_code=item_code, - qty=1, - rate=300, - use_serial_batch_fields=1, - serial_no="\n".join(serial_nos), - ) - - dn1.reload() - - sabb = frappe.get_doc("Serial and Batch Bundle", dn1.items[0].serial_and_batch_bundle) - for entry in sabb.entries: - self.assertEqual(entry.incoming_rate, 200) - - doc = frappe.new_doc("Repost Item Valuation") - doc.voucher_type = "Stock Entry" - doc.voucher_no = se.name - doc.submit() - - sabb = frappe.get_doc("Serial and Batch Bundle", dn.items[0].serial_and_batch_bundle) - for entry in sabb.entries: - self.assertEqual(entry.incoming_rate, 100) - - sabb = frappe.get_doc("Serial and Batch Bundle", dn1.items[0].serial_and_batch_bundle) - for entry in sabb.entries: - self.assertEqual(entry.incoming_rate, 200) - - @IntegrationTestCase.change_settings("Selling Settings", {"validate_selling_price": 1}) + @change_settings("Selling Settings", {"validate_selling_price": 1}) def test_validate_selling_price(self): item_code = make_item("VSP Item", properties={"is_stock_item": 1}).name make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=10) @@ -2884,7 +2824,6 @@ class TestDeliveryNote(FrappeTestCase): dn.items[0].stock_qty = 2 dn.save() ->>>>>>> 135a433018 (Merge pull request #52246 from mihir-kandoi/st58765) def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") From c6127575f5814c8b2a116c557824aaae8b401209 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 3 Feb 2026 09:12:14 +0530 Subject: [PATCH 37/51] chore: resolve conflicts --- erpnext/controllers/subcontracting_controller.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 6d7d5d5d579..246693d2d01 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -254,11 +254,7 @@ class SubcontractingController(StockController): ): for row in frappe.get_all( f"{self.subcontract_data.order_doctype} Item", -<<<<<<< HEAD - fields=["item_code", "(qty - received_qty) as qty", "parent", "name"], -======= - fields=["item_code", {"SUB": ["qty", "received_qty"], "as": "qty"}, "parent", "bom"], ->>>>>>> 0d372a62a1 (fix(subcontracting): include item bom in supplied items grouping key) + fields=["item_code", "(qty - received_qty) as qty", "parent", "bom"], filters={"docstatus": 1, "parent": ("in", self.subcontract_orders)}, ): self.qty_to_be_received[(row.item_code, row.parent, row.bom)] += row.qty From a61ad1599866cc54c723f7b8b566ddb3738c73c4 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 3 Feb 2026 09:29:46 +0530 Subject: [PATCH 38/51] fix: add missing param --- erpnext/controllers/selling_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 7f387eea690..c2a9afadcf0 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -288,7 +288,7 @@ class SellingController(StockController): bold("net rate"), bold(rate), bold(frappe.get_meta("Selling Settings").get_label("validate_selling_price")), - get_link_to_form("Selling Settings"), + get_link_to_form("Selling Settings", "Selling Settings"), ), title=_("Invalid Selling Price"), ) From d9d48da50566511ae5a67aef64208b67a841074e Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Tue, 20 Jan 2026 08:46:33 +0530 Subject: [PATCH 39/51] fix: include credit notes in project gross margin calculation (cherry picked from commit a378fee8e01b6889d10448aeb67b92ca11d95fb6) --- erpnext/projects/doctype/project/project.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 1f434a485b5..bc4e4d869c0 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -308,6 +308,8 @@ class Project(Document): self.gross_margin = flt(self.total_billed_amount) - expense_amount if self.total_billed_amount: self.per_gross_margin = (self.gross_margin / flt(self.total_billed_amount)) * 100 + else: + self.per_gross_margin = 0 def update_purchase_costing(self): total_purchase_cost = calculate_total_purchase_cost(self.name) From 0cbb7f8714f8051e5e4f9c9d4638b8b051b4670c Mon Sep 17 00:00:00 2001 From: kavin-114 Date: Tue, 3 Feb 2026 13:41:21 +0530 Subject: [PATCH 40/51] fix(stock): add stock recon opening stock condition --- erpnext/stock/report/stock_balance/stock_balance.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 2535fc4096f..913a31df1a7 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -198,7 +198,11 @@ class StockBalanceReport: for field in self.inventory_dimensions: qty_dict[field] = entry.get(field) - if entry.voucher_type == "Stock Reconciliation" and (not entry.batch_no or entry.serial_no): + if ( + entry.voucher_type == "Stock Reconciliation" + and frappe.get_cached_value(entry.voucher_type, entry.voucher_no, "purpose") != "Opening Stock" + and (not entry.batch_no or entry.serial_no) + ): qty_diff = flt(entry.qty_after_transaction) - flt(qty_dict.bal_qty) else: qty_diff = flt(entry.actual_qty) From e6083a57de2b64390e56ca4ee449f3565272cd8c Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Mon, 2 Feb 2026 11:59:42 +0530 Subject: [PATCH 41/51] fix(stock): ignore packing slip while cancelling the sales invoice (cherry picked from commit c58887b44a040f010b499e90c90fb52eb479767c) --- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 2c96286d3bb..aeb993e2399 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -43,6 +43,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( "Unreconcile Payment Entries", "Serial and Batch Bundle", "Bank Transaction", + "Packing Slip", ]; if (!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { From c30d76ae686982fa60a2f6d5ffa5b4b3e7b3ebc5 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 3 Feb 2026 14:53:30 +0530 Subject: [PATCH 42/51] fix: negative stock for purchase return --- .../purchase_receipt/test_purchase_receipt.py | 78 +++++++++++++++++++ .../serial_and_batch_bundle.py | 72 ++++++++++------- 2 files changed, 120 insertions(+), 30 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 341c38bdcf1..1685c9d98a8 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -4794,6 +4794,84 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(stk_ledger.incoming_rate, 120) self.assertEqual(stk_ledger.stock_value_difference, 600) + def test_negative_stock_error_for_purchase_return_when_stock_exists_in_future_date(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + from erpnext.stock.stock_ledger import NegativeStockError + + item_code = make_item( + "Test Negative Stock for Purchase Return with Future Stock Item", + { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TNSPFPRI.#####", + }, + ).name + + make_purchase_receipt( + item_code=item_code, + posting_date=add_days(today(), -4), + qty=100, + rate=100, + warehouse="_Test Warehouse - _TC", + ) + + pr1 = make_purchase_receipt( + item_code=item_code, + posting_date=add_days(today(), -3), + qty=100, + rate=100, + warehouse="_Test Warehouse - _TC", + ) + + batch1 = get_batch_from_bundle(pr1.items[0].serial_and_batch_bundle) + + pr2 = make_purchase_receipt( + item_code=item_code, + posting_date=add_days(today(), -2), + qty=100, + rate=100, + warehouse="_Test Warehouse - _TC", + ) + + batch2 = get_batch_from_bundle(pr2.items[0].serial_and_batch_bundle) + + make_stock_entry( + item_code=item_code, + qty=100, + posting_date=add_days(today(), -1), + source="_Test Warehouse - _TC", + target="_Test Warehouse 1 - _TC", + batch_no=batch1, + use_serial_batch_fields=1, + ) + + make_stock_entry( + item_code=item_code, + qty=100, + posting_date=add_days(today(), -1), + source="_Test Warehouse - _TC", + target="_Test Warehouse 1 - _TC", + batch_no=batch2, + use_serial_batch_fields=1, + ) + + make_stock_entry( + item_code=item_code, + qty=100, + posting_date=today(), + source="_Test Warehouse 1 - _TC", + target="_Test Warehouse - _TC", + batch_no=batch1, + use_serial_batch_fields=1, + ) + + make_purchase_entry = make_return_doc("Purchase Receipt", pr1.name) + make_purchase_entry.set_posting_time = 1 + make_purchase_entry.posting_date = pr1.posting_date + self.assertRaises(NegativeStockError, make_purchase_entry.submit) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index fe10c6aeeb9..46be3cbbf8f 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -15,6 +15,7 @@ from frappe.utils import ( cint, cstr, flt, + get_datetime, get_link_to_form, getdate, now, @@ -1419,31 +1420,44 @@ class SerialandBatchBundle(Document): for d in self.entries: available_qty = batch_wise_available_qty.get(d.batch_no, 0) if flt(available_qty, precision) < 0: - frappe.throw( - _( - """ - The Batch {0} of an item {1} has negative stock in the warehouse {2}. Please add a stock quantity of {3} to proceed with this entry.""" - ).format( - bold(d.batch_no), - bold(self.item_code), - bold(self.warehouse), - bold(abs(flt(available_qty, precision))), - ), - title=_("Negative Stock Error"), - ) + self.throw_negative_batch(d.batch_no, available_qty, precision) + + def throw_negative_batch(self, batch_no, available_qty, precision): + from erpnext.stock.stock_ledger import NegativeStockError + + frappe.throw( + _( + """ + The Batch {0} of an item {1} has negative stock in the warehouse {2}. Please add a stock quantity of {3} to proceed with this entry.""" + ).format( + bold(batch_no), + bold(self.item_code), + bold(self.warehouse), + bold(abs(flt(available_qty, precision))), + ), + title=_("Negative Stock Error"), + exc=NegativeStockError, + ) def get_batchwise_available_qty(self): - available_qty = self.get_available_qty_from_sabb() - available_qty_from_ledger = self.get_available_qty_from_stock_ledger() + batchwise_entries = self.get_available_qty_from_sabb() + batchwise_entries.extend(self.get_available_qty_from_stock_ledger()) - if not available_qty_from_ledger: - return available_qty + available_qty = frappe._dict({}) + batchwise_entries = sorted( + batchwise_entries, + key=lambda x: (get_datetime(x.get("posting_datetime")), get_datetime(x.get("creation"))), + ) - for batch_no, qty in available_qty_from_ledger.items(): - if batch_no in available_qty: - available_qty[batch_no] += qty + precision = frappe.get_precision("Serial and Batch Entry", "qty") + for row in batchwise_entries: + if row.batch_no in available_qty: + available_qty[row.batch_no] += flt(row.qty) else: - available_qty[batch_no] = qty + available_qty[row.batch_no] = flt(row.qty) + + if flt(available_qty[row.batch_no], precision) < 0: + self.throw_negative_batch(row.batch_no, available_qty[row.batch_no], precision) return available_qty @@ -1456,7 +1470,9 @@ class SerialandBatchBundle(Document): frappe.qb.from_(sle) .select( sle.batch_no, - Sum(sle.actual_qty).as_("available_qty"), + sle.actual_qty.as_("qty"), + sle.posting_datetime, + sle.creation, ) .where( (sle.item_code == self.item_code) @@ -1468,12 +1484,9 @@ class SerialandBatchBundle(Document): & (sle.batch_no.isnotnull()) ) .for_update() - .groupby(sle.batch_no) ) - res = query.run(as_list=True) - - return frappe._dict(res) if res else frappe._dict() + return query.run(as_dict=True) def get_available_qty_from_sabb(self): batches = [d.batch_no for d in self.entries if d.batch_no] @@ -1487,7 +1500,9 @@ class SerialandBatchBundle(Document): .on(parent.name == child.parent) .select( child.batch_no, - Sum(child.qty).as_("total_qty"), + child.qty, + CombineDatetime(parent.posting_date, parent.posting_time).as_("posting_datetime"), + parent.creation, ) .where( (parent.warehouse == self.warehouse) @@ -1498,14 +1513,11 @@ class SerialandBatchBundle(Document): & (parent.type_of_transaction.isin(["Inward", "Outward"])) ) .for_update() - .groupby(child.batch_no) ) query = query.where(parent.voucher_type != "Pick List") - res = query.run(as_list=True) - - return frappe._dict(res) if res else frappe._dict() + return query.run(as_dict=True) def validate_voucher_no_docstatus(self): if self.voucher_type == "POS Invoice": From e5e3b8a6ae7dbd89493a7721ca79c01d6a2f2b26 Mon Sep 17 00:00:00 2001 From: Dharanidharan2813 Date: Thu, 22 Jan 2026 15:48:28 +0530 Subject: [PATCH 43/51] feat(delivery-note): add status indicator when document is partially billed (cherry picked from commit 7767000ccf3a8e4afaf1d4b52f806e6053b086bf) # Conflicts: # erpnext/stock/doctype/delivery_note/delivery_note.json --- erpnext/controllers/status_updater.py | 3 ++- erpnext/stock/doctype/delivery_note/delivery_note.json | 4 ++-- erpnext/stock/doctype/delivery_note/delivery_note.py | 10 +++++++++- .../stock/doctype/delivery_note/delivery_note_list.js | 6 ++++-- .../stock/doctype/delivery_note/test_delivery_note.py | 3 ++- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index cc821b22272..dcecd995a48 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -83,7 +83,8 @@ status_map = { ], "Delivery Note": [ ["Draft", None], - ["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"], + ["To Bill", "eval:self.per_billed == 0 and self.docstatus == 1"], + ["Partially Billed", "eval:self.per_billed < 100 and self.per_billed > 0 and self.docstatus == 1"], ["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"], ["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"], ["Return", "eval:self.is_return == 1 and self.per_billed == 0 and self.docstatus == 1"], diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 97873234dcf..06623192797 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -1091,7 +1091,7 @@ "no_copy": 1, "oldfieldname": "status", "oldfieldtype": "Select", - "options": "\nDraft\nTo Bill\nCompleted\nReturn\nReturn Issued\nCancelled\nClosed", + "options": "\nDraft\nTo Bill\nPartially Billed\nCompleted\nReturn\nReturn Issued\nCancelled\nClosed", "print_hide": 1, "print_width": "150px", "read_only": 1, @@ -1404,7 +1404,7 @@ "idx": 146, "is_submittable": 1, "links": [], - "modified": "2025-12-02 23:55:25.415443", + "modified": "2026-02-03 12:27:19.055918", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index f2c6be169be..b2dd23f80bd 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -127,7 +127,15 @@ class DeliveryNote(SellingController): shipping_rule: DF.Link | None source: DF.Link | None status: DF.Literal[ - "", "Draft", "To Bill", "Completed", "Return", "Return Issued", "Cancelled", "Closed" + "", + "Draft", + "To Bill", + "Partially Billed", + "Completed", + "Return", + "Return Issued", + "Cancelled", + "Closed", ] tax_category: DF.Link | None tax_id: DF.Data | None diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_list.js b/erpnext/stock/doctype/delivery_note/delivery_note_list.js index 0f045bf405d..914063e1041 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_list.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note_list.js @@ -18,8 +18,10 @@ frappe.listview_settings["Delivery Note"] = { return [__("Closed"), "green", "status,=,Closed"]; } else if (doc.status === "Return Issued") { return [__("Return Issued"), "grey", "status,=,Return Issued"]; - } else if (flt(doc.per_billed, 2) < 100) { - return [__("To Bill"), "orange", "per_billed,<,100|docstatus,=,1"]; + } else if (flt(doc.per_billed) == 0) { + return [__("To Bill"), "orange", "per_billed,=,0|docstatus,=,1"]; + } else if (flt(doc.per_billed, 2) > 0 && flt(doc.per_billed, 2) < 100) { + return [__("Partially Billed"), "yellow", "per_billed,<,100|docstatus,=,1"]; } else if (flt(doc.per_billed, 2) === 100) { return [__("Completed"), "green", "per_billed,=,100|docstatus,=,1"]; } diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 4c6c4be2a52..90bae7f68f5 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1093,7 +1093,8 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(dn2.get("items")[0].billed_amt, 400) self.assertEqual(dn2.per_billed, 80) - self.assertEqual(dn2.status, "To Bill") + # Since 20% of DN2 is yet to be billed, it should be classified as partially billed. + self.assertEqual(dn2.status, "Partially Billed") def test_dn_billing_status_case4(self): # SO -> SI -> DN From a638dece6b79ef26fe8486dfa1e3baf0afcd13b7 Mon Sep 17 00:00:00 2001 From: kavin-114 Date: Thu, 29 Jan 2026 02:17:20 +0530 Subject: [PATCH 44/51] fix(stock): remove is_return condition on pos batch qty calculation (cherry picked from commit 2c19c1fd061bb3d60cf870976985d63790fdad67) --- .../serial_and_batch_bundle/serial_and_batch_bundle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 46be3cbbf8f..8ab75360c08 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -2477,11 +2477,11 @@ def get_reserved_batches_for_pos(kwargs) -> dict: key = (row.batch_no, row.warehouse) if key in pos_batches: - pos_batches[key]["qty"] -= row.qty * -1 if row.is_return else row.qty + pos_batches[key]["qty"] += row.qty * -1 else: pos_batches[key] = frappe._dict( { - "qty": (row.qty * -1 if not row.is_return else row.qty), + "qty": row.qty * -1, "warehouse": row.warehouse, } ) From 5b5d0f56de54c734eae080d9754bcec89ec3920a Mon Sep 17 00:00:00 2001 From: kavin-114 Date: Tue, 3 Feb 2026 12:43:09 +0530 Subject: [PATCH 45/51] test: add unit test case for pos reserved with return qty (cherry picked from commit 12ec997027591ea57ccf3b779d0dcd4d7474f464) --- .../doctype/pos_invoice/test_pos_invoice.py | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 97b3e87770f..c1be1d2eae0 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -838,6 +838,53 @@ class TestPOSInvoice(unittest.TestCase): if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC": self.assertEqual(batch.qty, 5) + def test_pos_batch_reservation_with_return_qty(self): + """ + Test POS Invoice reserved qty for batch without bundle with return invoices. + """ + from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + get_auto_batch_nos, + ) + from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( + create_batch_item_with_batch, + ) + + create_batch_item_with_batch("_Batch Item Reserve Return", "TestBatch-RR 01") + se = make_stock_entry( + target="_Test Warehouse - _TC", + item_code="_Batch Item Reserve Return", + qty=30, + basic_rate=100, + ) + + se.reload() + + batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + + # POS Invoice for the batch without bundle + pos_inv = create_pos_invoice(item="_Batch Item Reserve Return", rate=300, qty=15, do_not_save=1) + pos_inv.append( + "payments", + {"mode_of_payment": "Cash", "amount": 4500}, + ) + pos_inv.items[0].batch_no = batch_no + pos_inv.save() + pos_inv.submit() + + # POS Invoice return + pos_return = make_sales_return(pos_inv.name) + + pos_return.insert() + pos_return.submit() + + batches = get_auto_batch_nos( + frappe._dict({"item_code": "_Batch Item Reserve Return", "warehouse": "_Test Warehouse - _TC"}) + ) + + for batch in batches: + if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC": + self.assertEqual(batch.qty, 30) + def test_pos_batch_item_qty_validation(self): from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( BatchNegativeStockError, From 32c5861919301b297326916ef9b9a1af079564d9 Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Mon, 2 Feb 2026 12:04:44 +0530 Subject: [PATCH 46/51] fix(profit and loss statement): exclude non period columns (cherry picked from commit 6180e5eb53e6958bbf334ac8eb054577d7d1a6d1) --- .../profit_and_loss_statement/profit_and_loss_statement.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py index ccb4d26f77b..00ab15dba12 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py @@ -159,11 +159,11 @@ def get_net_profit_loss(income, expense, period_list, company, currency=None, co def get_chart_data(filters, columns, income, expense, net_profit_loss, currency): - labels = [d.get("label") for d in columns[2:]] + labels = [d.get("label") for d in columns[4:]] income_data, expense_data, net_profit = [], [], [] - for p in columns[2:]: + for p in columns[4:]: if income: income_data.append(income[-2].get(p.get("fieldname"))) if expense: From 28929df0e8f85e12f4e896b377a373cbada53357 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 3 Feb 2026 17:57:16 +0530 Subject: [PATCH 47/51] fix: zero valuation rate if returning from different warehouse --- .../controllers/sales_and_purchase_return.py | 26 ++++++++++- .../purchase_receipt/test_purchase_receipt.py | 44 +++++++++++++++++++ .../serial_and_batch_bundle.py | 8 +++- 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index d3d5cb808f3..a53eb2130a8 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -770,12 +770,34 @@ def get_filters( if reference_voucher_detail_no: filters["voucher_detail_no"] = reference_voucher_detail_no - if voucher_type in ["Purchase Receipt", "Purchase Invoice"] and item_row and item_row.get("warehouse"): - filters["warehouse"] = item_row.get("warehouse") + warehouses = [] + if voucher_type in ["Purchase Receipt", "Purchase Invoice"] and item_row: + if reference_voucher_detail_no: + warehouses = get_warehouses_for_return(voucher_type, reference_voucher_detail_no) + + if item_row.get("warehouse") and item_row.get("warehouse") in warehouses: + filters["warehouse"] = item_row.get("warehouse") return filters +def get_warehouses_for_return(voucher_type, name): + warehouses = [] + warehouse_details = frappe.get_all( + voucher_type + " Item", + filters={"name": name, "docstatus": 1}, + fields=["warehouse", "rejected_warehouse"], + ) + + for d in warehouse_details: + if d.warehouse: + warehouses.append(d.warehouse) + if d.rejected_warehouse: + warehouses.append(d.rejected_warehouse) + + return warehouses + + def get_returned_serial_nos(child_doc, parent_doc, serial_no_field=None, ignore_voucher_detail_no=None): from erpnext.stock.doctype.serial_no.serial_no import ( get_serial_nos as get_serial_nos_from_serial_no, diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 1685c9d98a8..c91b9e22632 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -4872,6 +4872,50 @@ class TestPurchaseReceipt(FrappeTestCase): make_purchase_entry.posting_date = pr1.posting_date self.assertRaises(NegativeStockError, make_purchase_entry.submit) + def test_purchase_return_from_different_warehouse(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item_code = make_item( + "Test Purchase Return From Different Warehouse Item", + { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TPRFDWU.#####", + }, + ).name + + pr1 = make_purchase_receipt( + item_code=item_code, + posting_date=add_days(today(), -4), + qty=100, + rate=100, + warehouse="_Test Warehouse - _TC", + ) + + batch1 = get_batch_from_bundle(pr1.items[0].serial_and_batch_bundle) + + make_stock_entry( + item_code=item_code, + qty=100, + posting_date=add_days(today(), -1), + source="_Test Warehouse - _TC", + target="_Test Warehouse 1 - _TC", + batch_no=batch1, + use_serial_batch_fields=1, + ) + + make_purchase_entry = make_return_doc("Purchase Receipt", pr1.name) + make_purchase_entry.items[0].warehouse = "_Test Warehouse 1 - _TC" + make_purchase_entry.submit() + make_purchase_entry.reload() + + sabb = frappe.get_doc("Serial and Batch Bundle", make_purchase_entry.items[0].serial_and_batch_bundle) + for row in sabb.entries: + self.assertEqual(row.warehouse, "_Test Warehouse 1 - _TC") + self.assertEqual(row.incoming_rate, 100) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 8ab75360c08..f51254784d9 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -440,6 +440,8 @@ class SerialandBatchBundle(Document): ) def get_valuation_rate_for_return_entry(self, return_against): + from erpnext.controllers.sales_and_purchase_return import get_warehouses_for_return + if not self.voucher_detail_no: return {} @@ -469,9 +471,11 @@ class SerialandBatchBundle(Document): ["Serial and Batch Bundle", "voucher_detail_no", "=", return_against_voucher_detail_no], ] + # Added to handle rejected warehouse case if self.voucher_type in ["Purchase Receipt", "Purchase Invoice"]: - # Added to handle rejected warehouse case - filters.append(["Serial and Batch Entry", "warehouse", "=", self.warehouse]) + warehouses = get_warehouses_for_return(self.voucher_type, return_against_voucher_detail_no) + if self.warehouse in warehouses: + filters.append(["Serial and Batch Entry", "warehouse", "=", self.warehouse]) bundle_data = frappe.get_all( "Serial and Batch Bundle", From e42f8ffd5d4e6cfd214af30c5c964d25ba497817 Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Sat, 10 Jan 2026 17:25:42 +0530 Subject: [PATCH 48/51] fix: correct exchange gain loss in ppr (cherry picked from commit 02e96039ac829ddaf751667e701feb6011cfb946) --- .../process_payment_reconciliation.py | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py index c4c75913d65..f650806966f 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py +++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py @@ -412,8 +412,9 @@ def reconcile(doc: None | str = None) -> None: for x in allocations: pr.append("allocation", x) + skip_ref_details_update_for_pe = check_multi_currency(pr) # reconcile - pr.reconcile_allocations(skip_ref_details_update_for_pe=True) + pr.reconcile_allocations(skip_ref_details_update_for_pe=skip_ref_details_update_for_pe) # If Payment Entry, update details only for newly linked references # This is for performance @@ -503,6 +504,37 @@ def reconcile(doc: None | str = None) -> None: frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed") +def check_multi_currency(pr_doc): + GL = frappe.qb.DocType("GL Entry") + Account = frappe.qb.DocType("Account") + + def get_account_currency(voucher_type, voucher_no): + currency = ( + frappe.qb.from_(GL) + .join(Account) + .on(GL.account == Account.name) + .select(Account.account_currency) + .where( + (GL.voucher_type == voucher_type) + & (GL.voucher_no == voucher_no) + & (Account.account_type.isin(["Payable", "Receivable"])) + ) + .limit(1) + ).run(as_dict=True) + + return currency[0].account_currency if currency else None + + for allocation in pr_doc.allocation: + reference_currency = get_account_currency(allocation.reference_type, allocation.reference_name) + + invoice_currency = get_account_currency(allocation.invoice_type, allocation.invoice_number) + + if reference_currency != invoice_currency: + return True + + return False + + @frappe.whitelist() def is_any_doc_running(for_filter: str | dict | None = None) -> str | None: running_doc = None From f1ba8258185709142e43adf65f9d60225b94a291 Mon Sep 17 00:00:00 2001 From: kavin-114 Date: Tue, 3 Feb 2026 18:36:18 +0530 Subject: [PATCH 49/51] fix(stock): fetch batch wise valuation rate in get_items (cherry picked from commit c5df570262a5efded9292029e59694f86b5bfc7d) --- .../doctype/stock_reconciliation/stock_reconciliation.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 5359995ea9c..8ab9cf210ce 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -1266,15 +1266,11 @@ def get_items(warehouse, posting_date, posting_time, company, item_code=None, ig for d in items: if (d.item_code, d.warehouse) in itemwise_batch_data: - valuation_rate = get_stock_balance( - d.item_code, d.warehouse, posting_date, posting_time, with_valuation_rate=True - )[1] - for row in itemwise_batch_data.get((d.item_code, d.warehouse)): if ignore_empty_stock and not row.qty: continue - args = get_item_data(row, row.qty, valuation_rate) + args = get_item_data(row, row.qty, row.valuation_rate) res.append(args) else: stock_bal = get_stock_balance( @@ -1408,6 +1404,7 @@ def get_itemwise_batch(warehouse, posting_date, company, item_code=None): "item_code": row[0], "warehouse": row[3], "qty": row[8], + "valuation_rate": row[9], "item_name": row[1], "batch_no": row[4], } From ba17fdd07221177c9f94023304a0c070377b26b6 Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Sun, 1 Feb 2026 10:42:33 +0530 Subject: [PATCH 50/51] fix(stock): include subcontracting order qty while calculating the bin qty (cherry picked from commit de8f8ef9f4d882f75bfb35aff4331a8e14e8746c) # Conflicts: # erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py --- erpnext/stock/stock_balance.py | 70 ++++++++++++++++--- .../subcontracting_order.py | 33 +++------ 2 files changed, 69 insertions(+), 34 deletions(-) diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index 6a2f818f7ae..c3f5086fbc5 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -3,6 +3,7 @@ import frappe +from frappe.query_builder.functions import Coalesce, Sum from frappe.utils import cstr, flt, now, nowdate, nowtime from erpnext.controllers.stock_controller import create_repost_item_valuation_entry @@ -182,18 +183,67 @@ def get_indented_qty(item_code, warehouse): def get_ordered_qty(item_code, warehouse): - ordered_qty = frappe.db.sql( - """ - select sum((po_item.qty - po_item.received_qty)*po_item.conversion_factor) - from `tabPurchase Order Item` po_item, `tabPurchase Order` po - where po_item.item_code=%s and po_item.warehouse=%s - and po_item.qty > po_item.received_qty and po_item.parent=po.name - and po.status not in ('Closed', 'Delivered') and po.docstatus=1 - and po_item.delivered_by_supplier = 0""", - (item_code, warehouse), + """Return total pending ordered quantity for an item in a warehouse. + Includes outstanding quantities from Purchase Orders and Subcontracting Orders""" + + purchase_order_qty = get_purchase_order_qty(item_code, warehouse) + subcontracting_order_qty = get_subcontracting_order_qty(item_code, warehouse) + + return flt(purchase_order_qty) + flt(subcontracting_order_qty) + + +def get_purchase_order_qty(item_code, warehouse): + PurchaseOrder = frappe.qb.DocType("Purchase Order") + PurchaseOrderItem = frappe.qb.DocType("Purchase Order Item") + + purchase_order_qty = ( + frappe.qb.from_(PurchaseOrderItem) + .join(PurchaseOrder) + .on(PurchaseOrderItem.parent == PurchaseOrder.name) + .select( + Sum( + (PurchaseOrderItem.qty - PurchaseOrderItem.received_qty) * PurchaseOrderItem.conversion_factor + ) + ) + .where( + (PurchaseOrderItem.item_code == item_code) + & (PurchaseOrderItem.warehouse == warehouse) + & (PurchaseOrderItem.qty > PurchaseOrderItem.received_qty) + & (PurchaseOrder.status.notin(["Closed", "Delivered"])) + & (PurchaseOrder.docstatus == 1) + & (Coalesce(PurchaseOrderItem.delivered_by_supplier, 0) == 0) + ) + .run() ) - return flt(ordered_qty[0][0]) if ordered_qty else 0 + return purchase_order_qty[0][0] if purchase_order_qty else 0 + + +def get_subcontracting_order_qty(item_code, warehouse): + SubcontractingOrder = frappe.qb.DocType("Subcontracting Order") + SubcontractingOrderItem = frappe.qb.DocType("Subcontracting Order Item") + + subcontracting_order_qty = ( + frappe.qb.from_(SubcontractingOrderItem) + .join(SubcontractingOrder) + .on(SubcontractingOrderItem.parent == SubcontractingOrder.name) + .select( + Sum( + (SubcontractingOrderItem.qty - SubcontractingOrderItem.received_qty) + * SubcontractingOrderItem.conversion_factor + ) + ) + .where( + (SubcontractingOrderItem.item_code == item_code) + & (SubcontractingOrderItem.warehouse == warehouse) + & (SubcontractingOrderItem.qty > SubcontractingOrderItem.received_qty) + & (SubcontractingOrder.status.notin(["Closed", "Completed"])) + & (SubcontractingOrder.docstatus == 1) + ) + .run() + ) + + return subcontracting_order_qty[0][0] if subcontracting_order_qty else 0 def get_planned_qty(item_code, warehouse): diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index 4f87e695dfc..4a4462ce764 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -8,7 +8,15 @@ from frappe.utils import flt from erpnext.buying.utils import check_on_hold_or_closed_status from erpnext.controllers.subcontracting_controller import SubcontractingController +<<<<<<< HEAD from erpnext.stock.stock_balance import update_bin_qty +======= +from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( + StockReservation, + has_reserved_stock, +) +from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty +>>>>>>> de8f8ef9f4 (fix(stock): include subcontracting order qty while calculating the bin qty) from erpnext.stock.utils import get_bin @@ -211,30 +219,7 @@ class SubcontractingOrder(SubcontractingController): ): item_wh_list.append([item.item_code, item.warehouse]) for item_code, warehouse in item_wh_list: - update_bin_qty(item_code, warehouse, {"ordered_qty": self.get_ordered_qty(item_code, warehouse)}) - - @staticmethod - def get_ordered_qty(item_code, warehouse): - table = frappe.qb.DocType("Subcontracting Order") - child = frappe.qb.DocType("Subcontracting Order Item") - - query = ( - frappe.qb.from_(table) - .inner_join(child) - .on(table.name == child.parent) - .select((child.qty - child.received_qty) * child.conversion_factor) - .where( - (table.docstatus == 1) - & (child.item_code == item_code) - & (child.warehouse == warehouse) - & (child.qty > child.received_qty) - & (table.status != "Completed") - ) - ) - - query = query.run() - - return flt(query[0][0]) if query else 0 + update_bin_qty(item_code, warehouse, {"ordered_qty": get_ordered_qty(item_code, warehouse)}) def update_reserved_qty_for_subcontracting(self, sco_item_rows=None): for item in self.supplied_items: From c0116bcde5e070eade22b50b4f55b4dae6f80779 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 3 Feb 2026 20:30:40 +0530 Subject: [PATCH 51/51] chore: resolve conflicts --- .../doctype/subcontracting_order/subcontracting_order.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index 4a4462ce764..f764329e338 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -8,15 +8,7 @@ from frappe.utils import flt from erpnext.buying.utils import check_on_hold_or_closed_status from erpnext.controllers.subcontracting_controller import SubcontractingController -<<<<<<< HEAD -from erpnext.stock.stock_balance import update_bin_qty -======= -from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( - StockReservation, - has_reserved_stock, -) from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty ->>>>>>> de8f8ef9f4 (fix(stock): include subcontracting order qty while calculating the bin qty) from erpnext.stock.utils import get_bin