From f8a123e79dcecdaddb635a55f38af2caf2dfb2bf Mon Sep 17 00:00:00 2001 From: Imesha Sudasingha Date: Mon, 1 Jun 2026 11:58:40 +0530 Subject: [PATCH 01/39] feat: add New Zealand chart of accounts --- .../nz_standard_chart_of_accounts.json | 461 ++++++++++++++++++ 1 file changed, 461 insertions(+) create mode 100644 erpnext/accounts/doctype/account/chart_of_accounts/verified/nz_standard_chart_of_accounts.json diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/nz_standard_chart_of_accounts.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/nz_standard_chart_of_accounts.json new file mode 100644 index 00000000000..860c68869a9 --- /dev/null +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/nz_standard_chart_of_accounts.json @@ -0,0 +1,461 @@ +{ + "country_code": "nz", + "name": "New Zealand - Chart of Accounts with Account Numbers", + "disabled": "No", + "tree": { + "Application of Funds (Assets)": { + "Current Assets": { + "Bank Accounts": { + "Business Transaction Account": { + "account_number": "11011", + "account_type": "Bank" + }, + "Business Savings Account": { + "account_number": "11012", + "account_type": "Bank" + }, + "account_number": "11010", + "is_group": 1 + }, + "Cash on Hand": { + "account_number": "11020", + "account_type": "Cash" + }, + "Accounts Receivable": { + "Debtors": { + "account_number": "11210", + "account_type": "Receivable" + }, + "Provision for Doubtful Debts": { + "account_number": "11220" + }, + "account_number": "11200", + "is_group": 1 + }, + "Inventory": { + "Stock on Hand": { + "account_number": "11311", + "account_type": "Stock" + }, + "Work In Progress": { + "account_number": "11312", + "account_type": "Stock" + }, + "account_number": "11310", + "account_type": "Stock", + "is_group": 1 + }, + "Prepayments": { + "Prepayments": { + "account_number": "11411" + }, + "Supplier Advances": { + "account_number": "11412" + }, + "Deferred Expense": { + "account_number": "11413" + }, + "account_number": "11410", + "is_group": 1 + }, + "GST Receivable": { + "account_number": "11510", + "account_type": "Tax" + }, + "Income Tax Receivable": { + "account_number": "11520", + "account_type": "Tax" + }, + "account_number": "11000", + "is_group": 1 + }, + "Fixed Assets": { + "Plant & Equipment": { + "Plant & Equipment": { + "account_number": "16011", + "account_type": "Fixed Asset" + }, + "Accumulated Depreciation - Plant & Equipment": { + "account_number": "16012", + "account_type": "Accumulated Depreciation" + }, + "account_number": "16010", + "is_group": 1 + }, + "Motor Vehicles": { + "Motor Vehicles": { + "account_number": "16021", + "account_type": "Fixed Asset" + }, + "Accumulated Depreciation - Motor Vehicles": { + "account_number": "16022", + "account_type": "Accumulated Depreciation" + }, + "account_number": "16020", + "is_group": 1 + }, + "Office Equipment": { + "Office Equipment": { + "account_number": "16031", + "account_type": "Fixed Asset" + }, + "Accumulated Depreciation - Office Equipment": { + "account_number": "16032", + "account_type": "Accumulated Depreciation" + }, + "account_number": "16030", + "is_group": 1 + }, + "Buildings": { + "Buildings": { + "account_number": "16041", + "account_type": "Fixed Asset" + }, + "Accumulated Depreciation - Buildings": { + "account_number": "16042", + "account_type": "Accumulated Depreciation" + }, + "account_number": "16040", + "is_group": 1 + }, + "Computer Equipment": { + "Computer Equipment": { + "account_number": "16051", + "account_type": "Fixed Asset" + }, + "Accumulated Depreciation - Computer Equipment": { + "account_number": "16052", + "account_type": "Accumulated Depreciation" + }, + "account_number": "16050", + "is_group": 1 + }, + "Capital Work in Progress": { + "account_number": "16090", + "account_type": "Capital Work in Progress" + }, + "account_number": "16000", + "is_group": 1 + }, + "account_number": "10000", + "root_type": "Asset" + }, + "Source of Funds (Liabilities)": { + "Current Liabilities": { + "Accounts Payable": { + "Creditors": { + "account_number": "21010", + "account_type": "Payable" + }, + "account_number": "21000", + "is_group": 1 + }, + "Goods Received Not Invoiced": { + "account_number": "21100", + "account_type": "Stock Received But Not Billed" + }, + "Asset Received Not Invoiced": { + "account_number": "21110", + "account_type": "Asset Received But Not Billed" + }, + "Service Received Not Invoiced": { + "account_number": "21120", + "account_type": "Service Received But Not Billed" + }, + "Accrued Expenses": { + "account_number": "21200" + }, + "Wages Payable": { + "account_number": "21300" + }, + "PAYE Payable": { + "account_number": "22010" + }, + "KiwiSaver Payable": { + "account_number": "22020" + }, + "ACC Payable": { + "account_number": "22030" + }, + "Credit Cards": { + "Business Credit Card": { + "account_number": "22110" + }, + "account_number": "22100", + "is_group": 1 + }, + "Customer Advances": { + "account_number": "22200" + }, + "Deferred Revenue": { + "account_number": "22210" + }, + "Provisional Account": { + "account_number": "22220" + }, + "Tax Liabilities": { + "GST Payable": { + "account_number": "22310", + "account_type": "Tax" + }, + "GST Suspense": { + "account_number": "22320", + "account_type": "Tax" + }, + "FBT Payable": { + "account_number": "22330", + "account_type": "Tax" + }, + "Income Tax Payable": { + "account_number": "22340", + "account_type": "Tax" + }, + "account_number": "22300", + "is_group": 1 + }, + "account_number": "21500", + "is_group": 1 + }, + "Non-Current Liabilities": { + "Bank Loans": { + "Bank Loan": { + "account_number": "25011" + }, + "account_number": "25010", + "is_group": 1 + }, + "Lease Liabilities": { + "Lease Liability": { + "account_number": "25021" + }, + "account_number": "25020", + "is_group": 1 + }, + "Shareholder Loans": { + "Shareholder Loan": { + "account_number": "25031" + }, + "account_number": "25030", + "is_group": 1 + }, + "account_number": "25000", + "is_group": 1 + }, + "account_number": "20000", + "root_type": "Liability" + }, + "Equity": { + "Share Capital": { + "account_number": "31010", + "account_type": "Equity" + }, + "Drawings": { + "account_number": "31020", + "account_type": "Equity" + }, + "Current Year Earnings": { + "account_number": "35010", + "account_type": "Equity" + }, + "Retained Earnings": { + "account_number": "35020", + "account_type": "Equity" + }, + "account_number": "30000", + "root_type": "Equity" + }, + "Income": { + "Sales": { + "Sales - GST 15%": { + "account_number": "41010", + "account_type": "Income Account" + }, + "Sales - Zero Rated (Exports)": { + "account_number": "41020", + "account_type": "Income Account" + }, + "Sales - Exempt": { + "account_number": "41030", + "account_type": "Income Account" + }, + "account_number": "41000", + "is_group": 1 + }, + "Other Income": { + "Interest Income": { + "account_number": "47010", + "account_type": "Income Account" + }, + "Rounding Gain/Loss": { + "account_number": "47020", + "account_type": "Income Account" + }, + "Foreign Exchange Gain": { + "account_number": "47030", + "account_type": "Income Account" + }, + "account_number": "47000", + "is_group": 1 + }, + "account_number": "40000", + "root_type": "Income" + }, + "Expenses": { + "Cost of Goods Sold": { + "Purchases": { + "account_number": "51010", + "account_type": "Cost of Goods Sold" + }, + "Freight Inwards": { + "account_number": "51020", + "account_type": "Expenses Included In Valuation" + }, + "Duty and Landing Costs": { + "account_number": "51030", + "account_type": "Expenses Included In Valuation" + }, + "Stock Adjustment": { + "account_number": "51040", + "account_type": "Stock Adjustment" + }, + "Stock Write Off": { + "account_number": "51050", + "account_type": "Stock Adjustment" + }, + "account_number": "51000", + "account_type": "Cost of Goods Sold", + "is_group": 1 + }, + "Operating Expenses": { + "Wages & Salaries": { + "account_number": "61010", + "account_type": "Expense Account" + }, + "KiwiSaver Employer Contribution": { + "account_number": "61020", + "account_type": "Expense Account" + }, + "ACC Levies": { + "account_number": "61030", + "account_type": "Expense Account" + }, + "Rent": { + "account_number": "65010", + "account_type": "Expense Account" + }, + "Power": { + "account_number": "65020", + "account_type": "Expense Account" + }, + "Telephone": { + "account_number": "66010", + "account_type": "Expense Account" + }, + "Insurance": { + "account_number": "64010", + "account_type": "Expense Account" + }, + "Accounting Fees": { + "account_number": "64020", + "account_type": "Expense Account" + }, + "Legal Fees": { + "account_number": "64030", + "account_type": "Expense Account" + }, + "Advertising and Marketing": { + "account_number": "65030", + "account_type": "Expense Account" + }, + "Repairs and Maintenance": { + "account_number": "65040", + "account_type": "Expense Account" + }, + "Freight and Courier": { + "account_number": "65050", + "account_type": "Expense Account" + }, + "Operating Costs": { + "account_number": "65060", + "account_type": "Expense Account" + }, + "account_number": "60000", + "is_group": 1 + }, + "Depreciation and Amortisation": { + "Depreciation - Plant & Equipment": { + "account_number": "62010", + "account_type": "Depreciation" + }, + "Depreciation - Motor Vehicles": { + "account_number": "62020", + "account_type": "Depreciation" + }, + "Depreciation - Office Equipment": { + "account_number": "62030", + "account_type": "Depreciation" + }, + "Depreciation - Computer Equipment": { + "account_number": "62040", + "account_type": "Depreciation" + }, + "account_number": "62000", + "is_group": 1 + }, + "Finance Costs": { + "Bank Charges": { + "account_number": "67010", + "account_type": "Expense Account" + }, + "Interest Expense": { + "account_number": "67020", + "account_type": "Expense Account" + }, + "Rounding Off": { + "account_number": "67030", + "account_type": "Round Off" + }, + "Payment Discounts": { + "account_number": "67040", + "account_type": "Expense Account" + }, + "account_number": "67000", + "is_group": 1 + }, + "Income Tax Expense": { + "account_number": "81010", + "account_type": "Expense Account" + }, + "Foreign Exchange": { + "Exchange Gain/Loss": { + "account_number": "82010", + "account_type": "Expense Account" + }, + "Unrealized Exchange Gain/Loss": { + "account_number": "82020", + "account_type": "Expense Account" + }, + "account_number": "82000", + "is_group": 1 + }, + "Bad Debts": { + "account_number": "83010", + "account_type": "Expense Account" + }, + "Write Off": { + "account_number": "83020", + "account_type": "Expense Account" + }, + "Gain/Loss on Asset Disposal": { + "account_number": "83030", + "account_type": "Expense Account" + }, + "Expenses Included In Asset Valuation": { + "account_number": "84010", + "account_type": "Expenses Included In Asset Valuation" + }, + "account_number": "50000", + "root_type": "Expense" + } + } +} From eebb37f9fddf00b559f33de8d285627cd07cc9e3 Mon Sep 17 00:00:00 2001 From: Imesha Sudasingha Date: Mon, 1 Jun 2026 13:22:06 +0530 Subject: [PATCH 02/39] fix: simplify New Zealand sales accounts --- .../verified/nz_standard_chart_of_accounts.json | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/nz_standard_chart_of_accounts.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/nz_standard_chart_of_accounts.json index 860c68869a9..410411aa670 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/nz_standard_chart_of_accounts.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/nz_standard_chart_of_accounts.json @@ -266,20 +266,8 @@ }, "Income": { "Sales": { - "Sales - GST 15%": { - "account_number": "41010", - "account_type": "Income Account" - }, - "Sales - Zero Rated (Exports)": { - "account_number": "41020", - "account_type": "Income Account" - }, - "Sales - Exempt": { - "account_number": "41030", - "account_type": "Income Account" - }, - "account_number": "41000", - "is_group": 1 + "account_number": "41010", + "account_type": "Income Account" }, "Other Income": { "Interest Income": { From ccbca57420b23207c8c0edf7b9e92f0a79f7588c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:41:47 +0200 Subject: [PATCH 03/39] perf(transaction): exit early before backend query (backport #55556) (#55557) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- erpnext/public/js/controllers/transaction.js | 25 +++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 42163c1a6c3..e6eb9180eeb 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -418,7 +418,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } }) } - onload_post_render() { if(this.frm.doc.__islocal && !(this.frm.doc.taxes || []).length && !this.frm.doc.__onload?.load_after_mapping) { @@ -659,15 +658,23 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe }, async () => { // for internal customer instead of pricing rule directly apply valuation rate on item - const fetch_valuation_rate_for_internal_transactions = await frappe.db.get_single_value( - "Accounts Settings", "fetch_valuation_rate_for_internal_transaction" - ); - if ((me.frm.doc.is_internal_customer || me.frm.doc.is_internal_supplier) && fetch_valuation_rate_for_internal_transactions) { - me.get_incoming_rate(item, me.frm.posting_date, me.frm.posting_time, - me.frm.doc.doctype, me.frm.doc.company); - } else { - me.frm.script_manager.trigger("price_list_rate", cdt, cdn); + if ((me.frm.doc.is_internal_customer || me.frm.doc.is_internal_supplier)) { + const fetch_valuation_rate_for_internal_transactions = await frappe.db.get_single_value( + "Accounts Settings", "fetch_valuation_rate_for_internal_transaction" + ); + if (fetch_valuation_rate_for_internal_transactions) { + me.get_incoming_rate( + item, + me.frm.posting_date, + me.frm.posting_time, + me.frm.doc.doctype, + me.frm.doc.company + ); + return; + } } + + me.frm.script_manager.trigger("price_list_rate", cdt, cdn); }, () => { if (me.frm.doc.is_internal_customer || me.frm.doc.is_internal_supplier) { From 4772799db22292cf236a4a8759938f726a691b59 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 22:59:43 +0200 Subject: [PATCH 04/39] fix(item): format integer numeric variant attributes without decimals (backport #55561) (#55563) * fix(item): format integer numeric variant attributes without decimals (#55561) (cherry picked from commit 016b64df6d0085137a7e1d9fe09e090d8c8f87f7) # Conflicts: # erpnext/stock/doctype/item/item.js * chore: resolve conflicts --------- Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- erpnext/stock/doctype/item/item.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 19585b9dabc..47dd2f50656 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -740,14 +740,18 @@ $.extend(erpnext.item, { if (!row.disabled) { if (row.numeric_values) { - fieldtype = "Float"; - desc = - "Min Value: " + - row.from_range + - " , Max Value: " + - row.to_range + - ", in Increments of: " + - row.increment; + const all_are_int = + flt(row.from_range) === cint(row.from_range) && + flt(row.to_range) === cint(row.to_range) && + flt(row.increment) === cint(row.increment); + fieldtype = all_are_int ? "Int" : "Float"; + const df = { fieldtype }; + const options = all_are_int ? { inline: 1 } : { always_show_decimals: true, inline: 1 }; + desc = __("Min Value: {0}, Max Value: {1}, in Increments of: {2}", [ + frappe.format(row.from_range, df, options), + frappe.format(row.to_range, df, options), + frappe.format(row.increment, df, options), + ]); } else { fieldtype = "Data"; desc = ""; From 611849f9534068d621f35cec155f83d9eebcade4 Mon Sep 17 00:00:00 2001 From: Pandiyan P Date: Wed, 3 Jun 2026 13:11:50 +0530 Subject: [PATCH 05/39] fix(accounts): include asset items in purchase receipt validation (#55150) (cherry picked from commit d0d9411700a44027667f6a036c7d2f986fe3e343) # Conflicts: # erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py --- .../accounts/doctype/purchase_invoice/purchase_invoice.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index b80eadc7ae0..31bb09b5446 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -633,15 +633,21 @@ class PurchaseInvoice(BuyingController): throw(msg, title=_("Mandatory Purchase Order")) def pr_required(self): +<<<<<<< HEAD stock_items = self.get_stock_items() if frappe.db.get_value("Buying Settings", None, "pr_required") == "Yes": +======= + if frappe.db.get_single_value("Buying Settings", "pr_required") == "Yes": + stock_and_asset_items = self.get_stock_items() + stock_and_asset_items.extend(self.get_asset_items()) +>>>>>>> d0d9411700 (fix(accounts): include asset items in purchase receipt validation (#55150)) if frappe.get_value( "Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_receipt" ): return for d in self.get("items"): - if not d.purchase_receipt and d.item_code in stock_items: + if not d.purchase_receipt and d.item_code in stock_and_asset_items: msg = _("Purchase Receipt Required for item {}").format(frappe.bold(d.item_code)) msg += "

" msg += _( From 0a7c3581da02d1ff442934ffa2e76583b84e03a8 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:11:12 +0530 Subject: [PATCH 06/39] Avoid status updation for purchase invoice from paid to unpaid by issuing a paid debit note against it (backport #54382) (#55575) Co-authored-by: Shllokkk <140623894+Shllokkk@users.noreply.github.com> --- .../purchase_invoice/purchase_invoice.js | 19 +++++++++++ .../purchase_invoice/purchase_invoice.py | 34 ++++++++++++++++--- erpnext/controllers/accounts_controller.py | 2 +- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index a25af18e5fc..195b29e38c6 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -608,6 +608,25 @@ frappe.ui.form.on("Purchase Invoice", { }; }); + frm.set_query("write_off_account", function (doc) { + return { + filters: { + report_type: "Profit and Loss", + is_group: 0, + company: doc.company, + }, + }; + }); + + frm.set_query("write_off_cost_center", function (doc) { + return { + filters: { + is_group: 0, + company: doc.company, + }, + }; + }); + frm.fields_dict["items"].grid.get_field("deferred_expense_account").get_query = function (doc) { return { filters: { diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index b80eadc7ae0..12d90c975f1 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -287,6 +287,7 @@ class PurchaseInvoice(BuyingController): self.validate_expense_account() self.set_against_expense_account() self.validate_write_off_account() + self.validate_write_off_cost_center() self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount") self.set_status() self.validate_purchase_receipt_if_update_stock() @@ -657,6 +658,27 @@ class PurchaseInvoice(BuyingController): if self.write_off_amount and not self.write_off_account: throw(_("Please enter Write Off Account")) + if not self.write_off_account: + return + + doc = frappe.db.get_value( + "Account", self.write_off_account, ["report_type", "is_group", "company"], as_dict=True + ) + + if not doc or doc.report_type != "Profit and Loss" or doc.is_group or doc.company != self.company: + throw(_("Please enter a valid Write Off Account")) + + def validate_write_off_cost_center(self): + if not self.write_off_cost_center: + return + + doc = frappe.db.get_value( + "Cost Center", self.write_off_cost_center, ["is_group", "company"], as_dict=True + ) + + if not doc or doc.is_group or doc.company != self.company: + throw(_("Please enter a valid Write Off Cost Center")) + def check_prev_docstatus(self): for d in self.get("items"): if d.purchase_order: @@ -737,6 +759,7 @@ class PurchaseInvoice(BuyingController): def validate_for_repost(self): self.validate_write_off_account() + self.validate_write_off_cost_center() self.validate_expense_account() validate_docs_for_voucher_types(["Purchase Invoice"]) validate_docs_for_deferred_accounting([], [self.name]) @@ -850,7 +873,9 @@ class PurchaseInvoice(BuyingController): if update_outstanding == "No": update_voucher_outstanding( voucher_type=self.doctype, - voucher_no=self.return_against if cint(self.is_return) and self.return_against else self.name, + voucher_no=self.return_against + if (cint(self.is_return) and self.return_against) + else self.name, account=self.credit_to, party_type="Supplier", party=self.supplier, @@ -1533,6 +1558,9 @@ class PurchaseInvoice(BuyingController): def make_payment_gl_entries(self, gl_entries): # Make Cash GL Entries if cint(self.is_paid) and self.cash_bank_account and self.paid_amount: + against_voucher = self.name + if self.is_return and self.return_against and not self.update_outstanding_for_self: + against_voucher = self.return_against bank_account_currency = get_account_currency(self.cash_bank_account) # CASH, make payment entries gl_entries.append( @@ -1547,9 +1575,7 @@ class PurchaseInvoice(BuyingController): if self.party_account_currency == self.company_currency else self.paid_amount, "debit_in_transaction_currency": self.paid_amount, - "against_voucher": self.return_against - if cint(self.is_return) and self.return_against - else self.name, + "against_voucher": against_voucher, "against_voucher_type": self.doctype, "cost_center": self.cost_center, "project": self.project, diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index d0d7d58f30d..6bcf437e0d0 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -178,7 +178,7 @@ class AccountsController(TransactionBase): if not get_meta(self.doctype).has_field("outstanding_amount"): return - if self.get("is_return") and self.return_against and not self.get("is_pos"): + if self.get("is_return") and self.return_against and not (self.get("is_pos") or self.get("is_paid")): against_voucher_outstanding = frappe.get_value( self.doctype, self.return_against, "outstanding_amount" ) From 9095c5a3c252368377cbdacd6a2add426efd08f4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:11:51 +0530 Subject: [PATCH 07/39] regional(setup): add 0% and 6% VAT rates for Belgium (backport #54719) (#55582) Co-authored-by: Antoine Maas --- erpnext/setup/setup_wizard/data/country_wise_tax.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index 18c513b3c2b..7ee3a79b4a0 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -262,7 +262,15 @@ }, "Belgium VAT 12%": { "account_name": "VAT 12%", - "tax_rate": 12 + "tax_rate": 12.00 + }, + "Belgium VAT 6%": { + "account_name": "VAT 6%", + "tax_rate": 6.00 + }, + "Belgium VAT 0%": { + "account_name": "VAT 0%", + "tax_rate": 0.00 } }, From 271ddb6add5f062fdc33750c21e124ae275d7bc3 Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:22:59 +0530 Subject: [PATCH 08/39] fix: resolve conflict --- .../accounts/doctype/purchase_invoice/purchase_invoice.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 31bb09b5446..ff4fea0bf14 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -633,14 +633,9 @@ class PurchaseInvoice(BuyingController): throw(msg, title=_("Mandatory Purchase Order")) def pr_required(self): -<<<<<<< HEAD - stock_items = self.get_stock_items() - if frappe.db.get_value("Buying Settings", None, "pr_required") == "Yes": -======= if frappe.db.get_single_value("Buying Settings", "pr_required") == "Yes": stock_and_asset_items = self.get_stock_items() stock_and_asset_items.extend(self.get_asset_items()) ->>>>>>> d0d9411700 (fix(accounts): include asset items in purchase receipt validation (#55150)) if frappe.get_value( "Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_receipt" ): From 6d3f9d3c6f37930567deb02c20d7649499c18f2e Mon Sep 17 00:00:00 2001 From: pandiyan Date: Wed, 3 Jun 2026 18:39:22 +0530 Subject: [PATCH 09/39] fix(stock): add validation for work order seial nos and batch nos --- .../stock/doctype/stock_entry/stock_entry.py | 99 +++++++++++++++++-- 1 file changed, 91 insertions(+), 8 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index e6d6dab7426..b52945d8928 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -246,6 +246,7 @@ class StockEntry(StockController): self.calculate_rate_and_amount() self.validate_putaway_capacity() self.validate_component_and_quantities() + self.validate_finished_good_serial_batch_for_work_order() if not self.get("purpose") == "Manufacture": # ignore scrap item wh difference and empty source/target wh @@ -256,6 +257,83 @@ class StockEntry(StockController): self.validate_same_source_target_warehouse_during_material_transfer() self.validate_raw_materials_exists() + def validate_finished_good_serial_batch_for_work_order(self): + if not ( + self.work_order + and self.pro_doc + and self.pro_doc.get("track_semi_finished_goods") != 1 + and cint( + frappe.db.get_single_value( + "Manufacturing Settings", "make_serial_no_batch_from_work_order", cache=True + ) + ) + and (self.pro_doc.has_serial_no or self.pro_doc.has_batch_no) + ): + return + + for row in self.items: + if not row.is_finished_item: + continue + + if self.check_invalid_serial_batch_nos_for_finished_good_item(row): + self.reset_serial_batch_on_fg_row(row) + frappe.msgprint( + _( + "Row {0}: Serial/Batch has been reset to values linked with Work Order {1}" + " because the previously selected serial/batch does not belong to this Work Order." + ).format(row.idx, frappe.bold(self.work_order)) + ) + + def check_invalid_serial_batch_nos_for_finished_good_item(self, row) -> bool: + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + from erpnext.stock.serial_batch_bundle import get_batches_from_bundle, get_serial_nos_from_bundle + + if self.pro_doc.has_serial_no: + serial_nos = get_serial_nos(row.serial_no) if row.serial_no else [] + if not serial_nos and row.serial_and_batch_bundle: + serial_nos = get_serial_nos_from_bundle(row.serial_and_batch_bundle) + if serial_nos: + valid_serial_nos = frappe.get_all( + "Serial No", + filters={"name": ("in", serial_nos), "work_order": self.work_order}, + pluck="name", + ) + return bool(set(serial_nos) - set(valid_serial_nos)) + else: + return True + + if self.pro_doc.has_batch_no: + batch_nos = [row.batch_no] if row.batch_no else [] + if not batch_nos and row.serial_and_batch_bundle: + batch_nos = list(get_batches_from_bundle(row.serial_and_batch_bundle).keys()) + if batch_nos: + valid_batch_nos = frappe.get_all( + "Batch", + filters={"name": ("in", batch_nos), "reference_name": self.work_order}, + pluck="name", + ) + return bool(set(batch_nos) - set(valid_batch_nos)) + else: + return True + + def reset_serial_batch_on_fg_row(self, row): + item_details = frappe._dict( + { + "item_code": row.item_code, + "t_warehouse": row.t_warehouse, + "qty": row.qty, + } + ) + + row.serial_no = None + row.batch_no = None + row.serial_and_batch_bundle = None + + if self.pro_doc.has_serial_no: + self.set_serial_no_batch_for_finished_good() + elif self.pro_doc.has_batch_no: + self.set_batchwise_finished_goods(item_details, None, existing_row=row) + def validate_repack_entry(self): if self.purpose != "Repack": return @@ -2465,15 +2543,16 @@ class StockEntry(StockController): else: self.add_finished_goods(args, item) - def set_batchwise_finished_goods(self, args, item): + def set_batchwise_finished_goods(self, args, item, existing_row=None): batches = get_empty_batches_based_work_order(self.work_order, self.pro_doc.production_item) if not batches: - self.add_finished_goods(args, item) + if not existing_row: + self.add_finished_goods(args, item) else: - self.add_batchwise_finished_good(batches, args, item) + self.add_batchwise_finished_good(batches, args, item, existing_row=existing_row) - def add_batchwise_finished_good(self, batches, args, item): + def add_batchwise_finished_good(self, batches, args, item, existing_row=None): qty = flt(self.fg_completed_qty) row = frappe._dict({"batches_to_be_consume": defaultdict(float)}) @@ -2482,7 +2561,7 @@ class StockEntry(StockController): if not row.batches_to_be_consume: return - id = create_serial_and_batch_bundle( + _id = create_serial_and_batch_bundle( self, row, frappe._dict( @@ -2492,9 +2571,13 @@ class StockEntry(StockController): } ), ) - - args["serial_and_batch_bundle"] = id - self.add_finished_goods(args, item) + if existing_row: + existing_row.serial_and_batch_bundle = _id + existing_row.use_serial_batch_fields = 0 + else: + args["serial_and_batch_bundle"] = _id + args["use_serial_batch_fields"] = 0 + self.add_finished_goods(args, item) def add_finished_goods(self, args, item): self.add_to_stock_entry_detail({item.name: args}, bom_no=self.bom_no) From e8267e32377e264e44e829fdf2e27b610f5fe28b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 15:37:00 +0000 Subject: [PATCH 10/39] fix(selling): consider delivered qty (backport #55597) (#55606) Co-authored-by: Pandiyan P fix(selling): consider delivered qty (#55597) --- erpnext/selling/doctype/sales_order/sales_order.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index cb573b75452..edfcb0becfe 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -26,7 +26,7 @@ frappe.ui.form.on("Sales Order", { let color; if (!doc.qty && frm.doc.has_unit_price_items) { color = "yellow"; - } else if (doc.stock_qty <= doc.actual_qty) { + } else if (doc.stock_qty - doc.delivered_qty <= doc.actual_qty) { color = "green"; } else { color = "orange"; From 013bd1a5664a7f3a5b65f26b77a63644e06f68eb Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 16:04:25 +0000 Subject: [PATCH 11/39] fix: duplicating a Customer/Supplier shouldn't inherit the source's primary contact and address (backport #55421) (#55608) Co-authored-by: Nabin Hait Co-authored-by: Antoine Maas Co-authored-by: Mihir Kandoi fix: duplicating a Customer/Supplier shouldn't inherit the source's primary contact and address (#55421) --- erpnext/buying/doctype/supplier/supplier.json | 11 ++++++++--- erpnext/selling/doctype/customer/customer.json | 13 ++++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json index ee3c9c840f0..7f60244e916 100644 --- a/erpnext/buying/doctype/supplier/supplier.json +++ b/erpnext/buying/doctype/supplier/supplier.json @@ -362,19 +362,22 @@ "fieldname": "supplier_primary_contact", "fieldtype": "Link", "label": "Supplier Primary Contact", + "no_copy": 1, "options": "Contact" }, { "fetch_from": "supplier_primary_contact.mobile_no", "fieldname": "mobile_no", "fieldtype": "Read Only", - "label": "Mobile No" + "label": "Mobile No", + "no_copy": 1 }, { "fetch_from": "supplier_primary_contact.email_id", "fieldname": "email_id", "fieldtype": "Read Only", - "label": "Email Id" + "label": "Email Id", + "no_copy": 1 }, { "fieldname": "column_break_44", @@ -384,6 +387,7 @@ "fieldname": "primary_address", "fieldtype": "Text", "label": "Primary Address", + "no_copy": 1, "read_only": 1 }, { @@ -391,6 +395,7 @@ "fieldname": "supplier_primary_address", "fieldtype": "Link", "label": "Supplier Primary Address", + "no_copy": 1, "options": "Address" }, { @@ -486,7 +491,7 @@ "link_fieldname": "party" } ], - "modified": "2026-02-06 12:58:01.398824", + "modified": "2026-05-29 16:52:59.441272", "modified_by": "Administrator", "module": "Buying", "name": "Supplier", diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index dd1910ed15a..568aad1a36c 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -305,6 +305,7 @@ "fieldname": "customer_primary_contact", "fieldtype": "Link", "label": "Customer Primary Contact", + "no_copy": 1, "options": "Contact" }, { @@ -312,6 +313,7 @@ "fieldname": "mobile_no", "fieldtype": "Read Only", "label": "Mobile No", + "no_copy": 1, "options": "Mobile" }, { @@ -319,6 +321,7 @@ "fieldname": "email_id", "fieldtype": "Read Only", "label": "Email Id", + "no_copy": 1, "options": "Email" }, { @@ -330,12 +333,14 @@ "fieldname": "customer_primary_address", "fieldtype": "Link", "label": "Customer Primary Address", + "no_copy": 1, "options": "Address" }, { "fieldname": "primary_address", "fieldtype": "Text", "label": "Primary Address", + "no_copy": 1, "read_only": 1 }, { @@ -590,14 +595,16 @@ "fieldname": "first_name", "fieldtype": "Read Only", "hidden": 1, - "label": "First Name" + "label": "First Name", + "no_copy": 1 }, { "fetch_from": "customer_primary_contact.last_name", "fieldname": "last_name", "fieldtype": "Read Only", "hidden": 1, - "label": "Last Name" + "label": "Last Name", + "no_copy": 1 } ], "icon": "fa fa-user", @@ -611,7 +618,7 @@ "link_fieldname": "party" } ], - "modified": "2026-01-21 17:23:42.151114", + "modified": "2026-05-29 16:52:59.441272", "modified_by": "Administrator", "module": "Selling", "name": "Customer", From ddaf75a60d7c60da261a533914e22a16e3e40621 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:26:49 +0000 Subject: [PATCH 12/39] refactor: convert rfq_transaction_list to query builder (backport #55497) (#55629) * refactor: convert rfq_transaction_list to query builder (#55497) (cherry picked from commit 9cecf2e6f9a36638caa4c914d56249328e039242) # Conflicts: # erpnext/controllers/website_list_for_contact.py * chore: resolve conflicts --------- Co-authored-by: Shllokkk <140623894+Shllokkk@users.noreply.github.com> Co-authored-by: Mihir Kandoi --- erpnext/controllers/website_list_for_contact.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/erpnext/controllers/website_list_for_contact.py b/erpnext/controllers/website_list_for_contact.py index 4d141d346eb..f3f1f1b0651 100644 --- a/erpnext/controllers/website_list_for_contact.py +++ b/erpnext/controllers/website_list_for_contact.py @@ -178,13 +178,16 @@ def get_list_for_transactions( def rfq_transaction_list(parties_doctype, doctype, parties, limit_start, limit_page_length): - data = frappe.db.sql( - """select distinct parent as name, supplier from `tab{doctype}` - where supplier = '{supplier}' and docstatus=1 order by modified desc limit {start}, {len}""".format( - doctype=parties_doctype, supplier=parties[0], start=limit_start, len=limit_page_length - ), - as_dict=1, - ) + party = frappe.qb.DocType(parties_doctype) + data = ( + frappe.qb.from_(party) + .select(party.parent.as_("name"), party.supplier) + .distinct() + .where((party.supplier == party[0]) & (party.docstatus == 1)) + .orderby(party.creation, order=frappe.qb.desc) + .limit(limit_page_length) + .offset(limit_start) + ).run(as_dict=True) return post_process(doctype, data) From e8e0514a30e3c8514c25c85e74e7d36ad612dc67 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 4 Jun 2026 21:38:35 +0530 Subject: [PATCH 13/39] fix: persist main item code for MR plan items (#55623) --- .../material_request_plan_item.json | 11 ++- .../material_request_plan_item.py | 1 + .../production_plan/production_plan.py | 5 +- .../production_plan/test_production_plan.py | 6 ++ erpnext/patches.txt | 1 + ...item_code_in_material_request_plan_item.py | 97 +++++++++++++++++++ 6 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 erpnext/patches/v15_0/set_main_item_code_in_material_request_plan_item.py diff --git a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json index 06c1b497551..41b03cc7793 100644 --- a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json +++ b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json @@ -21,6 +21,7 @@ "min_order_qty", "section_break_8", "sales_order", + "main_item_code", "bin_qty_section", "actual_qty", "requested_qty", @@ -114,6 +115,14 @@ "options": "Sales Order", "read_only": 1 }, + { + "fieldname": "main_item_code", + "fieldtype": "Data", + "hidden": 1, + "label": "Main Item Code", + "no_copy": 1, + "read_only": 1 + }, { "fieldname": "requested_qty", "fieldtype": "Float", @@ -213,4 +222,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.py b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.py index aa1c72294d3..02bb50617f5 100644 --- a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.py +++ b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.py @@ -20,6 +20,7 @@ class MaterialRequestPlanItem(Document): from_warehouse: DF.Link | None item_code: DF.Link item_name: DF.Data | None + main_item_code: DF.Data | None material_request_type: DF.Literal[ "", "Purchase", "Material Transfer", "Material Issue", "Manufacture", "Customer Provided" ] diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 1d63f91caaf..67323d42d40 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1397,7 +1397,7 @@ def get_material_request_items( "sales_order": sales_order, "description": row.get("description"), "uom": row.get("purchase_uom") or row.get("stock_uom"), - "main_bom_item": row.get("main_bom_item"), + "main_item_code": row.get("main_bom_item"), } @@ -1557,6 +1557,8 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d "item_code": sa_row.production_item, "required_qty": sa_row.qty, "include_exploded_items": 0, + "sales_order": sa_row.sales_order, + "main_bom_item": sa_row.parent_item_code, } ) ) @@ -1660,6 +1662,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d "stock_uom": item_master.stock_uom, "conversion_factor": conversion_factor, "safety_stock": item_master.safety_stock, + "main_bom_item": data.get("main_bom_item"), } ) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 62aa4f6ea11..5af1fdb36b5 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1254,8 +1254,10 @@ class TestProductionPlan(FrappeTestCase): plan.get_sub_assembly_items() mr_items = [] + expected_main_item_by_mr_item = {"ChildPart1 For MR": "SubAssembly1-1 For MR"} for row in plan.sub_assembly_items: mr_items.append(row.production_item) + expected_main_item_by_mr_item[row.production_item] = row.parent_item_code row.type_of_manufacturing = "Material Request" plan.save() @@ -1265,6 +1267,10 @@ class TestProductionPlan(FrappeTestCase): for item_code in mr_items: self.assertTrue(item_code in validate_mr_items) + main_item_by_mr_item = {item.get("item_code"): item.get("main_item_code") for item in items} + for item_code, main_item_code in expected_main_item_by_mr_item.items(): + self.assertEqual(main_item_by_mr_item[item_code], main_item_code) + def test_resered_qty_for_production_plan_for_material_requests(self): from erpnext.stock.utils import get_or_make_bin diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 3e467b37b0f..239066f7c98 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -435,3 +435,4 @@ erpnext.patches.v16_0.update_order_qty_and_requested_qty_based_on_mr_and_po erpnext.patches.v16_0.depends_on_inv_dimensions erpnext.patches.v16_0.clear_procedures_from_receivable_report erpnext.patches.v16_0.migrate_address_contact_custom_fields +erpnext.patches.v15_0.set_main_item_code_in_material_request_plan_item diff --git a/erpnext/patches/v15_0/set_main_item_code_in_material_request_plan_item.py b/erpnext/patches/v15_0/set_main_item_code_in_material_request_plan_item.py new file mode 100644 index 00000000000..74d024361b3 --- /dev/null +++ b/erpnext/patches/v15_0/set_main_item_code_in_material_request_plan_item.py @@ -0,0 +1,97 @@ +import frappe + + +def execute(): + frappe.reload_doc("manufacturing", "doctype", "material_request_plan_item") + + if not frappe.db.has_column("Material Request Plan Item", "main_item_code"): + return + + for row in get_material_request_plan_items(): + if row.main_item_code: + continue + + main_item_code = get_main_item_code(row) + if main_item_code: + frappe.db.set_value( + "Material Request Plan Item", + row.name, + "main_item_code", + main_item_code, + update_modified=False, + ) + + +def get_material_request_plan_items(): + return frappe.get_all( + "Material Request Plan Item", + fields=["name", "parent", "item_code", "sales_order", "main_item_code"], + ) + + +def get_main_item_code(row): + return ( + get_main_item_code_from_sub_assembly(row) + or get_main_item_code_from_sub_assembly_bom(row) + or get_main_item_code_from_production_plan_bom(row) + ) + + +def get_main_item_code_from_sub_assembly(row): + sub_assembly = frappe.db.get_value( + "Production Plan Sub Assembly Item", + get_filters( + row, + { + "parent": row.parent, + "production_item": row.item_code, + }, + ), + "parent_item_code", + ) + + return sub_assembly + + +def get_main_item_code_from_sub_assembly_bom(row): + for sub_assembly in get_sub_assembly_items(row): + if item_exists_in_bom(row.item_code, sub_assembly.bom_no): + return frappe.db.get_value("BOM", sub_assembly.bom_no, "item") + + +def get_main_item_code_from_production_plan_bom(row): + for production_plan_item in get_production_plan_items(row): + if item_exists_in_bom(row.item_code, production_plan_item.bom_no): + return frappe.db.get_value("BOM", production_plan_item.bom_no, "item") + + +def get_sub_assembly_items(row): + return frappe.get_all( + "Production Plan Sub Assembly Item", + filters=get_filters(row, {"parent": row.parent}), + fields=["bom_no"], + ) + + +def get_production_plan_items(row): + return frappe.get_all( + "Production Plan Item", + filters=get_filters(row, {"parent": row.parent}), + fields=["bom_no"], + ) + + +def get_filters(row, filters): + if row.sales_order: + filters["sales_order"] = row.sales_order + + return filters + + +def item_exists_in_bom(item_code, bom_no): + if not bom_no: + return False + + return frappe.db.exists("BOM Item", {"parent": bom_no, "item_code": item_code}) or frappe.db.exists( + "BOM Explosion Item", {"parent": bom_no, "item_code": item_code} + ) From c15012cd51c80baff894081aa25472a6c6d53959 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:48:59 +0000 Subject: [PATCH 14/39] fix: prevent selling items from sample retention warehouse (backport #55613) (#55633) Co-authored-by: Pandiyan P Co-authored-by: Mihir Kandoi fix: prevent selling items from sample retention warehouse (#55613) --- erpnext/controllers/selling_controller.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index d5bd3501527..3383bd51fdd 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -53,6 +53,7 @@ class SellingController(StockController): self.validate_for_duplicate_items() self.validate_target_warehouse() self.validate_auto_repeat_subscription_dates() + self.validate_sample_retention_warehouse() for table_field in ["items", "packed_items"]: if self.get(table_field): self.set_serial_and_batch_bundle(table_field) @@ -874,6 +875,26 @@ class SellingController(StockController): validate_item_type(self, "is_sales_item", "sales") + def validate_sample_retention_warehouse(self): + if self.get("is_return"): + return + + sample_retention_warehouse = frappe.db.get_single_value( + "Stock Settings", "sample_retention_warehouse" + ) + if not sample_retention_warehouse: + return + + items = self.get("items") + (self.get("packed_items")) + for item in items: + if item.get("warehouse") == sample_retention_warehouse: + frappe.throw( + _("Row {0}: Cannot sell item {1} from Sample Retention Warehouse {2}").format( + item.idx, frappe.bold(item.item_code), frappe.bold(sample_retention_warehouse) + ), + title=_("Not Allowed"), + ) + def update_stock_reservation_entries(self) -> None: """Updates Delivered Qty in Stock Reservation Entries.""" From 55b07153104482f1edabe9615821adc3ca95404a Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 5 Jun 2026 11:06:10 +0530 Subject: [PATCH 15/39] fix: work order status should be in process if material transfer is skipped (backport #55641 to version-15-hotfix) (#55643) Co-authored-by: Claude Opus 4.8 --- erpnext/manufacturing/doctype/work_order/work_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 5d8382e7ebd..9d3100302e7 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -406,7 +406,7 @@ class WorkOrder(Document): elif self.docstatus == 1: if status not in ["Closed", "Stopped"]: status = "Not Started" - if flt(self.material_transferred_for_manufacturing) > 0: + if flt(self.material_transferred_for_manufacturing) > 0 or self.skip_transfer: status = "In Process" precision = frappe.get_precision("Work Order", "produced_qty") From 08f3cf98f9e0738a01ca8a5809fdbfb1b7bb6104 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Fri, 5 Jun 2026 15:31:43 +0530 Subject: [PATCH 16/39] fix: correct field order in Address and Contacts report --- .../selling/report/address_and_contacts/address_and_contacts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/report/address_and_contacts/address_and_contacts.py b/erpnext/selling/report/address_and_contacts/address_and_contacts.py index 65fef8b5ac6..ed25dd5f153 100644 --- a/erpnext/selling/report/address_and_contacts/address_and_contacts.py +++ b/erpnext/selling/report/address_and_contacts/address_and_contacts.py @@ -11,9 +11,9 @@ field_map = { "name", "address_line1", "address_line2", + "pincode", "city", "state", - "pincode", "country", "is_primary_address", ], From bde46cffd0e2321860435eceeaa6c59589ef315c Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Fri, 5 Jun 2026 15:33:39 +0530 Subject: [PATCH 17/39] fix: correct field order in Address and Contacts report --- .../selling/report/address_and_contacts/address_and_contacts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/selling/report/address_and_contacts/address_and_contacts.py b/erpnext/selling/report/address_and_contacts/address_and_contacts.py index ed25dd5f153..0b08a24484f 100644 --- a/erpnext/selling/report/address_and_contacts/address_and_contacts.py +++ b/erpnext/selling/report/address_and_contacts/address_and_contacts.py @@ -20,6 +20,7 @@ field_map = { } + def execute(filters=None): columns, data = get_columns(filters), get_data(filters) return columns, data From ffb36cff78d3002c8df2c8a5bb0dddd0df1088f2 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 5 Jun 2026 17:02:33 +0530 Subject: [PATCH 18/39] [codex] Show in-transit status for add-to-transit Stock Entries (backport #55644 to version-15-hotfix) (#55659) [codex] Show in-transit status for add-to-transit Stock Entries (#55644) (cherry picked from commit df03524b19dfdea225dafae97d122b9eb59bf1c1) --- .../doctype/stock_entry/stock_entry_list.js | 25 ++++++++++++++----- .../doctype/stock_entry/test_stock_entry.py | 10 +++++++- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_list.js b/erpnext/stock/doctype/stock_entry/stock_entry_list.js index 8a1c808cd91..3d5e066099a 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_list.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry_list.js @@ -1,11 +1,13 @@ frappe.listview_settings["Stock Entry"] = { add_fields: [ - "`tabStock Entry`.`from_warehouse`", - "`tabStock Entry`.`to_warehouse`", - "`tabStock Entry`.`purpose`", - "`tabStock Entry`.`work_order`", - "`tabStock Entry`.`bom_no`", - "`tabStock Entry`.`is_return`", + "from_warehouse", + "to_warehouse", + "purpose", + "work_order", + "bom_no", + "is_return", + "add_to_transit", + "per_transferred", ], get_indicator: function (doc) { if (doc.is_return === 1 && doc.purpose === "Material Transfer for Manufacture") { @@ -16,6 +18,17 @@ frappe.listview_settings["Stock Entry"] = { ]; } else if (doc.docstatus === 0) { return [__("Draft"), "red", "docstatus,=,0"]; + } else if ( + doc.docstatus === 1 && + doc.add_to_transit === 1 && + doc.purpose === "Material Transfer" && + doc.per_transferred < 100 + ) { + return [ + __("In Transit"), + "yellow", + "docstatus,=,1|add_to_transit,=,1|purpose,=,Material Transfer|per_transferred,<,100", + ]; } else if (doc.purpose === "Send to Warehouse" && doc.per_transferred < 100) { // not delivered & overdue return [__("Goods In Transit"), "grey", "per_transferred,<,100"]; diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 073a3e2d117..a82a1266651 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -235,7 +235,15 @@ class TestStockEntry(FrappeTestCase): self.assertEqual(transit_entry.name, end_transit_entry.items[0].against_stock_entry) self.assertEqual(transit_entry.items[0].name, end_transit_entry.items[0].ste_detail) - # create add to transit + transit_entry.reload() + self.assertEqual(transit_entry.per_transferred, 0) + + end_transit_entry.to_warehouse = "Test To Warehouse - _TC" + end_transit_entry.items[0].t_warehouse = "Test To Warehouse - _TC" + end_transit_entry.save().submit() + + transit_entry.reload() + self.assertEqual(transit_entry.per_transferred, 100) def test_material_receipt_gl_entry(self): company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") From e7eaa87a77db77b0d421b1aa3c70a5024c4e378f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 5 Jun 2026 18:45:46 +0530 Subject: [PATCH 19/39] fix: naming series issue (cherry picked from commit 3a50056968fe266325a794786b836c7da24650f8) # Conflicts: # erpnext/stock/doctype/batch/batch.py --- erpnext/stock/doctype/batch/batch.py | 8 +++++--- erpnext/stock/doctype/batch/test_batch.py | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index ba9aaa6c48a..324865fc308 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -10,8 +10,12 @@ from frappe.model.document import Document from frappe.model.naming import make_autoname, revert_series_if_last from frappe.query_builder.functions import CurDate, Sum from frappe.utils import cint, flt, get_link_to_form +<<<<<<< HEAD from frappe.utils.data import add_days from frappe.utils.jinja import render_template +======= +from frappe.utils.data import DateTimeLikeObject, add_days +>>>>>>> 3a50056968 (fix: naming series issue) class UnableToSelectBatchError(frappe.ValidationError): @@ -225,10 +229,8 @@ class Batch(Document): :return: The string that was generated. """ naming_series_prefix = _get_batch_prefix() - # validate_template(naming_series_prefix) - naming_series_prefix = render_template(str(naming_series_prefix), self.__dict__) key = _make_naming_series_key(naming_series_prefix) - name = make_autoname(key) + name = make_autoname(key, doc=self) return name diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index c3764138a08..a450b82caff 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -506,6 +506,24 @@ class TestBatch(FrappeTestCase): if not use_naming_series: frappe.set_value("Stock Settings", "Stock Settings", "use_naming_series", 0) + def test_naming_series_prefix_is_not_rendered_as_jinja(self): + from frappe.model.naming import InvalidNamingSeriesError + + stock_settings = frappe.get_single("Stock Settings") + use_naming_series = cint(stock_settings.use_naming_series) + original_prefix = stock_settings.naming_series_prefix + + frappe.set_value("Stock Settings", "Stock Settings", "use_naming_series", 1) + frappe.set_value("Stock Settings", "Stock Settings", "naming_series_prefix", "{{ 7*7 }}") + + try: + self.assertRaises( + InvalidNamingSeriesError, self.make_new_batch, "_Test Stock Item For Batch SSTI" + ) + finally: + frappe.set_value("Stock Settings", "Stock Settings", "naming_series_prefix", original_prefix) + frappe.set_value("Stock Settings", "Stock Settings", "use_naming_series", use_naming_series) + def make_new_batch(self, item_name=None, batch_id=None, do_not_insert=0): batch = frappe.new_doc("Batch") item = self.make_batch_item(item_name) From 8c9168595f23463ebf0635983105e601f823223c Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 5 Jun 2026 20:48:19 +0530 Subject: [PATCH 20/39] chore: fix conflicts Removed unused import of DateTimeLikeObject. --- erpnext/stock/doctype/batch/batch.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 324865fc308..6b2b1360cf9 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -10,12 +10,7 @@ from frappe.model.document import Document from frappe.model.naming import make_autoname, revert_series_if_last from frappe.query_builder.functions import CurDate, Sum from frappe.utils import cint, flt, get_link_to_form -<<<<<<< HEAD from frappe.utils.data import add_days -from frappe.utils.jinja import render_template -======= -from frappe.utils.data import DateTimeLikeObject, add_days ->>>>>>> 3a50056968 (fix: naming series issue) class UnableToSelectBatchError(frappe.ValidationError): From 4c9c2911a6fc2be447dab1c2b8946d69b9f4b40a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 7 Jun 2026 23:15:42 +0530 Subject: [PATCH 21/39] chore: remove unused whitelisted method from project (backport #55648) (#55672) Co-authored-by: Diptanil Saha --- erpnext/templates/pages/projects.py | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/erpnext/templates/pages/projects.py b/erpnext/templates/pages/projects.py index 446437bbb32..22750d39aea 100644 --- a/erpnext/templates/pages/projects.py +++ b/erpnext/templates/pages/projects.py @@ -64,21 +64,6 @@ def get_tasks(project, start=0, search=None, item_status=None): return list(filter(lambda x: not x.parent_task, tasks)) -@frappe.whitelist() -def get_task_html(project, start=0, item_status=None): - return frappe.render_template( - "erpnext/templates/includes/projects/project_tasks.html", - { - "doc": { - "name": project, - "project_name": project, - "tasks": get_tasks(project, start, item_status=item_status), - } - }, - is_path=True, - ) - - def get_timesheets(project, start=0, search=None): filters = {"project": project} if search: @@ -103,16 +88,6 @@ def get_timesheets(project, start=0, search=None): timesheet.update(info[0]) return timesheets - -@frappe.whitelist() -def get_timesheet_html(project, start=0): - return frappe.render_template( - "erpnext/templates/includes/projects/project_timesheets.html", - {"doc": {"timesheets": get_timesheets(project, start)}}, - is_path=True, - ) - - def get_attachments(project): return frappe.get_all( "File", From 2d0e3fd9af521e407be35fd09e9427176609513f Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Fri, 5 Jun 2026 20:43:02 +0530 Subject: [PATCH 22/39] fix: drop ignore_permissions handling from add_ac (cherry picked from commit 37d2adc74ba91f388194b3bbe07368c40573a9b2) --- erpnext/accounts/utils.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 557afc865fd..7fff2f156c4 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -397,15 +397,13 @@ def add_ac(args=None): if not args: args = frappe.local.form_dict + args.pop("ignore_permissions", None) + frappe.has_permission("Account", "create", throw=True) + args.doctype = "Account" args = make_tree_args(**args) ac = frappe.new_doc("Account") - - if args.get("ignore_permissions"): - ac.flags.ignore_permissions = True - args.pop("ignore_permissions") - ac.update(args) if not ac.parent_account: From 7852ea65af4ec5208b90d63d60fb7f6be8e9206a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 06:59:05 +0000 Subject: [PATCH 23/39] fix: update items respects workflow "Only Allow Edit For" role (backport #55667) (#55705) * fix: update items respects workflow "Only Allow Edit For" role (#55667) (cherry picked from commit 76b9b6a34ef960085849a4d22ec8a6274d62590a) # Conflicts: # erpnext/controllers/accounts_controller.py * chore: resolve conflicts --------- Co-authored-by: kaulith <64089478+kaulith@users.noreply.github.com> Co-authored-by: Mihir Kandoi --- erpnext/controllers/accounts_controller.py | 18 ++--- .../doctype/sales_order/test_sales_order.py | 72 +++++++++++++++++++ 2 files changed, 81 insertions(+), 9 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 6bcf437e0d0..0d3f13dde8c 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -7,7 +7,7 @@ from collections import defaultdict import frappe from frappe import _, bold, qb, throw -from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied +from frappe.model.workflow import get_workflow_name from frappe.query_builder import Criterion, DocType from frappe.query_builder.custom import ConstantColumn from frappe.query_builder.functions import Abs, Sum @@ -3815,7 +3815,9 @@ def validate_and_delete_children(parent, data, ordered_item=None) -> bool: @frappe.whitelist() -def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"): +def update_child_qty_rate( + parent_doctype: str, trans_items: str, parent_doctype_name: str, child_docname: str = "items" +): from erpnext.buying.doctype.supplier_quotation.supplier_quotation import get_purchased_items from erpnext.selling.doctype.quotation.quotation import get_ordered_items @@ -3841,14 +3843,12 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil current_state = doc.get(workflow_doc.workflow_state_field) roles = frappe.get_roles() - transitions = [] - for transition in workflow_doc.transitions: - if transition.next_state == current_state and transition.allowed in roles: - if not is_transition_condition_satisfied(transition, doc): - continue - transitions.append(transition.as_dict()) + allowed = any( + state.state == current_state and (not state.allow_edit or state.allow_edit in roles) + for state in workflow_doc.states + ) - if not transitions: + if not allowed: frappe.throw( _("You are not allowed to update as per the conditions set in {} Workflow.").format( get_link_to_form("Workflow", workflow) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index b5de5fdc99c..18d416ad7df 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -692,6 +692,40 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): workflow.is_active = 0 workflow.save() + def test_update_child_qty_rate_follows_allow_edit(self): + from frappe.model.workflow import apply_workflow + + workflow = make_sales_order_edit_perm_workflow() + so = make_sales_order(item_code="_Test Item", qty=1, rate=150, do_not_submit=1) + apply_workflow(so, "Approve") + + trans_item = json.dumps( + [{"item_code": "_Test Item", "rate": 150, "qty": 2, "docname": so.items[0].name}] + ) + + mover = "test@example.com" + mover_user = frappe.get_doc("User", mover) + mover_user.add_roles("Sales User", "Test Junior Approver") + with self.set_user(mover): + # transitioned the doc into Approved but is not the configured editor + self.assertRaises( + frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name + ) + + editor = "test2@example.com" + editor_user = frappe.get_doc("User", editor) + editor_user.add_roles("Sales User", "Test Approver") + with self.set_user(editor): + # Test Approver is the "Only Allow Edit For" role on Approved + update_child_qty_rate("Sales Order", trans_item, so.name) + so.reload() + self.assertEqual(so.items[0].qty, 2) + + mover_user.remove_roles("Sales User", "Test Junior Approver", "Test Approver") + editor_user.remove_roles("Sales User", "Test Junior Approver", "Test Approver") + workflow.is_active = 0 + workflow.save() + def test_material_request_for_product_bundle(self): # Create the Material Request from the sales order for the Packing Items # Check whether the material request has the correct packing item or not. @@ -2683,3 +2717,41 @@ def make_sales_order_workflow(): workflow.insert(ignore_permissions=True) return workflow + + +def make_sales_order_edit_perm_workflow(): + if frappe.db.exists("Workflow", "SO Edit Perm Workflow"): + doc = frappe.get_doc("Workflow", "SO Edit Perm Workflow") + doc.set("is_active", 1) + doc.save() + return doc + + frappe.get_doc(doctype="Role", role_name="Test Junior Approver").insert(ignore_if_duplicate=True) + frappe.get_doc(doctype="Role", role_name="Test Approver").insert(ignore_if_duplicate=True) + frappe.cache().hdel("roles", frappe.session.user) + + workflow = frappe.get_doc( + { + "doctype": "Workflow", + "workflow_name": "SO Edit Perm Workflow", + "document_type": "Sales Order", + "workflow_state_field": "workflow_state", + "is_active": 1, + "send_email_alert": 0, + } + ) + workflow.append("states", dict(state="Pending", allow_edit="All")) + workflow.append("states", dict(state="Approved", allow_edit="Test Approver", doc_status=1)) + workflow.append( + "transitions", + dict( + state="Pending", + action="Approve", + next_state="Approved", + allowed="Test Junior Approver", + allow_self_approval=1, + ), + ) + workflow.insert(ignore_permissions=True) + + return workflow From 45b232d369eb1624383986d7047d5625c7e4f08f Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Sun, 31 May 2026 18:42:26 +0530 Subject: [PATCH 24/39] fix: use new_doc with field allowlist in CRM integration endpoints (cherry picked from commit e460e83516b48664c1be09a93d83a6d80fe90f01) --- erpnext/crm/frappe_crm_api.py | 44 +++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/erpnext/crm/frappe_crm_api.py b/erpnext/crm/frappe_crm_api.py index 86521586b28..586ebe91eed 100644 --- a/erpnext/crm/frappe_crm_api.py +++ b/erpnext/crm/frappe_crm_api.py @@ -32,20 +32,16 @@ def create_custom_fields_for_frappe_crm(): @frappe.whitelist() def create_prospect_against_crm_deal(): doc = frappe.form_dict - prospect = frappe.get_doc( - { - "doctype": "Prospect", - "company_name": doc.organization or doc.lead_name, - "no_of_employees": doc.no_of_employees, - "prospect_owner": doc.deal_owner, - "company": doc.erpnext_company, - "crm_deal": doc.crm_deal, - "territory": doc.territory, - "industry": doc.industry, - "website": doc.website, - "annual_revenue": doc.annual_revenue, - } - ) + prospect = frappe.new_doc("Prospect") + prospect.company_name = doc.organization or doc.lead_name + prospect.no_of_employees = doc.no_of_employees + prospect.prospect_owner = doc.deal_owner + prospect.company = doc.erpnext_company + prospect.crm_deal = doc.crm_deal + prospect.territory = doc.territory + prospect.industry = doc.industry + prospect.website = doc.website + prospect.annual_revenue = doc.annual_revenue try: prospect_name = frappe.db.get_value("Prospect", {"company_name": prospect.company_name}) @@ -151,6 +147,18 @@ def contact_exists(email, mobile_no): return False +CUSTOMER_ALLOWED_FIELDS = { + "customer_name", + "customer_group", + "customer_type", + "territory", + "default_currency", + "industry", + "website", + "crm_deal", +} + + @frappe.whitelist() def create_customer(customer_data=None): if not customer_data: @@ -159,9 +167,11 @@ def create_customer(customer_data=None): try: customer_name = frappe.db.exists("Customer", {"customer_name": customer_data.get("customer_name")}) if not customer_name: - customer = frappe.get_doc({"doctype": "Customer", **customer_data}).insert( - ignore_permissions=True - ) + customer = frappe.new_doc("Customer") + for field in CUSTOMER_ALLOWED_FIELDS: + if customer_data.get(field) is not None: + customer.set(field, customer_data.get(field)) + customer.insert(ignore_permissions=True) customer_name = customer.name contacts = json.loads(customer_data.get("contacts")) From 2d1c0dcb53e4ff1069ceb39f67063cb954f578dd Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Sun, 31 May 2026 04:40:02 +0530 Subject: [PATCH 25/39] fix: add permission checks in accounts whitelisted methods (cherry picked from commit 5dbf3fdde0e11fa482a42bfa36559becdc814c6a) # Conflicts: # erpnext/accounts/doctype/payment_entry/payment_entry.py --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 6 +++++- erpnext/accounts/utils.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 34120776681..5a2c90a89d5 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -2279,6 +2279,9 @@ def get_outstanding_reference_documents(args, validate=False): if args.get("party_type") == "Member": return + if args.get("party_type") and args.get("party"): + frappe.has_permission(args["party_type"], "read", args["party"], throw=True) + if not args.get("get_outstanding_invoices") and not args.get("get_orders_to_be_billed"): args["get_outstanding_invoices"] = True @@ -2788,7 +2791,8 @@ def get_reference_details( ): total_amount = outstanding_amount = exchange_rate = account = None - ref_doc = frappe.get_doc(reference_doctype, reference_name) + frappe.has_permission(reference_doctype, "read", reference_name, throw=True) + ref_doc = frappe.get_lazy_doc(reference_doctype, reference_name) company_currency = ref_doc.get("company_currency") or erpnext.get_company_currency(ref_doc.company) # Only applies for Reverse Payment Entries diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 7fff2f156c4..db820ca9154 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -278,6 +278,7 @@ def get_balance_on( ) if party_type and party: + frappe.has_permission(party_type, "read", party, throw=True) cond.append( f"""gle.party_type = {frappe.db.escape(party_type)} and gle.party = {frappe.db.escape(party)} """ ) From 07b61113af40c5a247b1cb51c6bdb4afc8e83eeb Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 8 Jun 2026 13:07:26 +0530 Subject: [PATCH 26/39] fix: do not allow to make changes in SABB after submit (cherry picked from commit e36426e235790ee0f55b7d7d88d7e1f0e7937982) # Conflicts: # erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py --- .../serial_and_batch_bundle.py | 9 ++++ .../test_serial_and_batch_bundle.py | 54 +++++++++++++++++++ .../stock_reconciliation.py | 31 ++++------- 3 files changed, 72 insertions(+), 22 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 1ac6afd15b1..39e5f1425f0 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 @@ -2169,7 +2169,16 @@ def get_type_of_transaction(parent_doc, child_row): def update_serial_batch_no_ledgers(bundle, entries, child_row, parent_doc, warehouse=None) -> object: + frappe.has_permission("Serial and Batch Bundle", "write", throw=True) doc = frappe.get_doc("Serial and Batch Bundle", bundle) + + if doc.docstatus == 1: + doc.throw_error_message( + _("Serial and Batch Bundle {0} is submitted and its entries cannot be modified.").format( + frappe.bold(bundle) + ) + ) + doc.voucher_detail_no = child_row.name doc.posting_date = parent_doc.posting_date doc.posting_time = parent_doc.posting_time diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index 5e33852badc..2c184cbb876 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -734,6 +734,60 @@ class TestSerialandBatchBundle(FrappeTestCase): docstatus = frappe.db.get_value("Serial and Batch Bundle", bundle, "docstatus") self.assertEqual(docstatus, 2) + def test_submitted_bundle_entries_cannot_be_mutated(self): + # A submitted Serial and Batch Bundle is the immutable source of truth for the stock + # ledger, live batch availability and repost/valuation replay. update_serial_batch_no_ledgers + # (which the whitelisted add_serial_batch_ledgers delegates to for an existing bundle) must + # refuse to rebuild -- and thereby inflate -- the quantities of an already submitted bundle. + from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + update_serial_batch_no_ledgers, + ) + + item_code = make_item( + properties={ + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TAMPER-SBB-.#####", + } + ).name + + se = make_stock_entry( + item_code=item_code, + target="_Test Warehouse - _TC", + qty=10, + rate=100, + ) + + bundle = se.items[0].serial_and_batch_bundle + self.assertEqual(frappe.db.get_value("Serial and Batch Bundle", bundle, "docstatus"), 1) + + original = frappe.db.get_value( + "Serial and Batch Entry", {"parent": bundle}, ["name", "batch_no", "qty"], as_dict=True + ) + self.assertEqual(original.qty, 10) + + # Attempt to forge the submitted bundle: keep the same batch but inflate qty. The guard + # fires immediately after the bundle is loaded (docstatus check), so child_row / parent_doc + # only need the minimal fields the function reads. + tampered_entries = [{"batch_no": original.batch_no, "qty": 1000}] + child_row = frappe._dict({"name": se.items[0].name}) + parent_doc = frappe._dict({"posting_date": today(), "posting_time": nowtime()}) + + self.assertRaises( + frappe.ValidationError, + update_serial_batch_no_ledgers, + bundle, + tampered_entries, + child_row, + parent_doc, + ) + + # The on-disk quantity must be untouched by the rejected mutation attempt. + self.assertEqual( + frappe.db.get_value("Serial and Batch Entry", original.name, "qty"), + 10, + ) + def test_batch_duplicate_entry(self): item_code = make_item(properties={"has_batch_no": 1}).name diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 085383f4abf..6f9f42a64e8 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -9,14 +9,19 @@ from frappe.utils import add_to_date, cint, cstr, flt, get_datetime, now import erpnext from erpnext.accounts.utils import get_company_default -from erpnext.controllers.stock_controller import StockController, create_repost_item_valuation_entry +from erpnext.controllers.stock_controller import StockController from erpnext.stock.doctype.batch.batch import get_available_batches, get_batch_qty from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( get_available_serial_nos, ) from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos +<<<<<<< HEAD from erpnext.stock.utils import get_combine_datetime, get_incoming_rate, get_stock_balance +======= +from erpnext.stock.doctype.stock_reconciliation_item.stock_reconciliation_item import StockReconciliationItem +from erpnext.stock.utils import get_incoming_rate, get_stock_balance +>>>>>>> e36426e235 (fix: do not allow to make changes in SABB after submit) class OpeningEntryAccountError(frappe.ValidationError): @@ -842,22 +847,6 @@ class StockReconciliation(StockController): sl_entries.append(args) - def update_valuation_rate_for_serial_no(self): - for d in self.items: - if not d.serial_no: - continue - - serial_nos = get_serial_nos(d.serial_no) - self.update_valuation_rate_for_serial_nos(d, serial_nos) - - def update_valuation_rate_for_serial_nos(self, row, serial_nos): - valuation_rate = row.valuation_rate if self.docstatus == 1 else row.current_valuation_rate - if valuation_rate is None: - return - - for d in serial_nos: - frappe.db.set_value("Serial No", d, "purchase_rate", valuation_rate) - def get_sle_for_items(self, row, serial_nos=None, current_bundle=True): """Insert Stock Ledger Entries""" @@ -1011,11 +1000,6 @@ class StockReconciliation(StockController): d.quantity_difference = flt(d.qty) - flt(d.current_qty) d.amount_difference = flt(d.amount) - flt(d.current_amount) - def get_items_for(self, warehouse): - self.items = [] - for item in get_items(warehouse, self.posting_date, self.posting_time, self.company): - self.append("items", item) - def submit(self): if len(self.items) > 100: msgprint( @@ -1038,6 +1022,7 @@ class StockReconciliation(StockController): else: self._cancel() +<<<<<<< HEAD def add_missing_stock_ledger_entry(self, row, voucher_detail_no, sle_creation): if row.current_qty == 0: return @@ -1177,6 +1162,8 @@ def get_batch_qty_for_stock_reco( return flt(qty) +======= +>>>>>>> e36426e235 (fix: do not allow to make changes in SABB after submit) @frappe.whitelist() def get_items(warehouse, posting_date, posting_time, company, item_code=None, ignore_empty_stock=False): From dc1fb3f80479aa4059076f64fa6c5981f2486aa1 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 8 Jun 2026 15:28:44 +0530 Subject: [PATCH 27/39] chore: fix conflicts Removed unused methods related to stock ledger entries and negative stock handling. --- .../stock_reconciliation.py | 147 ------------------ 1 file changed, 147 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 6f9f42a64e8..b3e330a0a85 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -16,12 +16,7 @@ from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle impor get_available_serial_nos, ) from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos -<<<<<<< HEAD -from erpnext.stock.utils import get_combine_datetime, get_incoming_rate, get_stock_balance -======= -from erpnext.stock.doctype.stock_reconciliation_item.stock_reconciliation_item import StockReconciliationItem from erpnext.stock.utils import get_incoming_rate, get_stock_balance ->>>>>>> e36426e235 (fix: do not allow to make changes in SABB after submit) class OpeningEntryAccountError(frappe.ValidationError): @@ -1022,148 +1017,6 @@ class StockReconciliation(StockController): else: self._cancel() -<<<<<<< HEAD - def add_missing_stock_ledger_entry(self, row, voucher_detail_no, sle_creation): - if row.current_qty == 0: - return - - new_sle = frappe.get_doc(self.get_sle_for_items(row)) - new_sle.actual_qty = row.current_qty * -1 - new_sle.valuation_rate = row.current_valuation_rate - new_sle.serial_and_batch_bundle = row.current_serial_and_batch_bundle - new_sle.flags.ignore_permissions = 1 - new_sle.submit() - - creation = add_to_date(sle_creation, seconds=-1) - new_sle.db_set("creation", creation) - - if not frappe.db.exists( - "Repost Item Valuation", - {"item": row.item_code, "warehouse": row.warehouse, "docstatus": 1, "status": "Queued"}, - ): - create_repost_item_valuation_entry( - { - "based_on": "Item and Warehouse", - "item_code": row.item_code, - "warehouse": row.warehouse, - "company": self.company, - "allow_negative_stock": 1, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - } - ) - - def has_negative_stock_allowed(self): - allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) - if allow_negative_stock: - return True - - if any( - ((d.serial_and_batch_bundle or d.batch_no) and flt(d.qty) == flt(d.current_qty)) - for d in self.items - ): - allow_negative_stock = True - - return allow_negative_stock - - def get_current_qty_for_serial_or_batch(self, row, sle_creation): - doc = frappe.get_doc("Serial and Batch Bundle", row.current_serial_and_batch_bundle) - current_qty = 0.0 - if doc.has_serial_no: - current_qty = self.get_current_qty_for_serial_nos(doc, sle_creation) - elif doc.has_batch_no: - current_qty = self.get_current_qty_for_batch_nos(doc, sle_creation) - - return abs(current_qty) - - def get_current_qty_for_serial_nos(self, doc, sle_creation): - serial_nos_details = get_available_serial_nos( - frappe._dict( - { - "item_code": doc.item_code, - "warehouse": doc.warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - "creation": sle_creation, - "voucher_no": self.name, - "ignore_warehouse": 1, - } - ) - ) - - if not serial_nos_details: - return 0.0 - - doc.delete_serial_batch_entries() - current_qty = 0.0 - for serial_no_row in serial_nos_details: - current_qty += 1 - doc.append( - "entries", - { - "serial_no": serial_no_row.serial_no, - "qty": -1, - "warehouse": doc.warehouse, - "batch_no": serial_no_row.batch_no, - }, - ) - - doc.set_incoming_rate(save=True) - doc.calculate_qty_and_amount(save=True) - doc.db_update_all() - - return current_qty - - def get_current_qty_for_batch_nos(self, doc, sle_creation): - current_qty = 0.0 - precision = doc.entries[0].precision("qty") - for d in doc.entries: - qty = ( - get_batch_qty( - d.batch_no, - doc.warehouse, - creation=sle_creation, - posting_date=doc.posting_date, - posting_time=doc.posting_time, - ignore_voucher_nos=[doc.voucher_no], - for_stock_levels=True, - consider_negative_batches=True, - do_not_check_future_batches=True, - ) - or 0 - ) * -1 - - if flt(d.qty, precision) != flt(qty, precision): - d.db_set("qty", qty) - - current_qty += qty - - return current_qty - - -def get_batch_qty_for_stock_reco( - item_code, warehouse, batch_no, posting_date, posting_time, voucher_no, sle_creation -): - qty = ( - get_batch_qty( - batch_no, - warehouse, - item_code, - creation=sle_creation, - posting_date=posting_date, - posting_time=posting_time, - ignore_voucher_nos=[voucher_no], - for_stock_levels=True, - consider_negative_batches=True, - do_not_check_future_batches=True, - ) - or 0 - ) - - return flt(qty) - -======= ->>>>>>> e36426e235 (fix: do not allow to make changes in SABB after submit) @frappe.whitelist() def get_items(warehouse, posting_date, posting_time, company, item_code=None, ignore_empty_stock=False): From 09453f883ba8ea1a355d044d3f416f8925d663df Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 15:42:09 +0530 Subject: [PATCH 28/39] fix: remove item name from update items dialog item code column (backport #55718) (#55722) Co-authored-by: Mihir Kandoi Co-authored-by: Abdullah fix: remove item name from update items dialog item code column (#55718) --- erpnext/public/js/utils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 293f8e1324e..3ae0b3d1835 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -651,6 +651,7 @@ erpnext.utils.update_child_items = function (opts) { read_only: 0, disabled: 0, label: __("Item Code"), + formatter: (value) => value, get_query: function () { let filters; if (frm.doc.doctype == "Sales Order") { From ba19a245266f280dceb7e9d550d4b7d76e27f26c Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 8 Jun 2026 17:42:04 +0530 Subject: [PATCH 29/39] fix: validate fg and materials qty in the disassemble entry (cherry picked from commit 4453c1072a8a98c67ed425b5e5ae5d27d90d1c21) # Conflicts: # erpnext/stock/doctype/stock_entry/stock_entry.py # erpnext/stock/doctype/stock_entry/test_stock_entry.py --- .../stock/doctype/stock_entry/stock_entry.py | 104 ++++++++++++++++++ .../doctype/stock_entry/test_stock_entry.py | 85 ++++++++++++++ 2 files changed, 189 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index b52945d8928..cc0813ffd9e 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -80,7 +80,17 @@ from erpnext.controllers.stock_controller import StockController form_grid_templates = {"items": "templates/form_grid/stock_entry_grid.html"} +<<<<<<< HEAD class StockEntry(StockController): +======= +def _qty_tolerance(precision: int) -> float: + """One unit at the column's precision -- absorbs float rounding without letting a real + (whole-unit) quantity divergence slip through.""" + return 1.0 / (10**precision) + + +class StockEntry(StockController, SubcontractingInwardController): +>>>>>>> 4453c1072a (fix: validate fg and materials qty in the disassemble entry) # begin: auto-generated types # This code is auto-generated. Do not modify anything in this block. @@ -228,6 +238,13 @@ class StockEntry(StockController): self.validate_warehouse() self.validate_with_material_request() + + # Disassembly rows are fully derived from the source manufacture entry / work order; + # verify the posted stock quantities have not been tampered with (raw-material minting). + # Must run after set_transfer_qty() so row.transfer_qty reflects qty * conversion_factor. + if self.purpose == "Disassemble": + self.validate_disassembly_quantities() + self.validate_batch() self.validate_inspection() self.validate_fg_completed_qty() @@ -931,6 +948,93 @@ class StockEntry(StockController): title=_("Excess Disassembly"), ) + def validate_disassembly_quantities(self): + self.validate_finished_good_consumption() + self.validate_materials_against_source() + + def validate_finished_good_consumption(self): + """The finished good consumed (in stock UOM) must equal the quantity to disassemble.""" + precision = frappe.get_precision("Stock Entry Detail", "transfer_qty") + tolerance = _qty_tolerance(precision) + + fg_stock_qty = sum(flt(row.transfer_qty) for row in self.items if row.is_finished_item) + fg_completed_qty = flt(self.fg_completed_qty) + + if abs(flt(fg_stock_qty, precision) - flt(fg_completed_qty, precision)) > tolerance: + frappe.throw( + _( + "Finished good quantity being consumed ({0} in stock UOM) must equal the quantity " + "to disassemble ({1}). Do not change the UOM, conversion factor or quantity of the " + "finished good row." + ).format(flt(fg_stock_qty, precision), flt(fg_completed_qty, precision)), + title=_("Invalid Disassembly Quantity"), + ) + + def validate_materials_against_source(self): + """Every non-finished-good row's posted stock qty must equal the source qty x scale.""" + scale_factor = self._get_disassembly_scale_factor() + if not scale_factor: + # Standalone BOM disassembly: no source entry to scale against. The finished-good + # invariant above still applies; raw-material amounts come from the BOM. + return + + source_rows = self.get_items_from_manufacture_stock_entry() + source_by_name = {row.name: row for row in source_rows if row.get("name")} + source_by_item = defaultdict(float) + for row in source_rows: + source_by_item[row.item_code] += flt(row.transfer_qty) + + precision = frappe.get_precision("Stock Entry Detail", "transfer_qty") + tolerance = _qty_tolerance(precision) + + for row in self.items: + if row.is_finished_item: + continue # covered by validate_finished_good_consumption + + if row.ste_detail and row.ste_detail in source_by_name: + expected = flt(source_by_name[row.ste_detail].transfer_qty) * scale_factor + elif row.item_code in source_by_item: + expected = source_by_item[row.item_code] * scale_factor + else: + frappe.throw( + _( + "Row #{0}: Item {1} is not part of the source manufacture entry and cannot be " + "added to this disassembly." + ).format(row.idx, frappe.bold(row.item_code)), + title=_("Invalid Disassembly Item"), + ) + + if abs(flt(row.transfer_qty, precision) - flt(expected, precision)) > tolerance: + frappe.throw( + _( + "Row #{0}: Item {1} quantity ({2} in stock UOM) does not match the quantity " + "derived from the source ({3}). Do not change the UOM, conversion factor or " + "quantity of disassembly rows." + ).format( + row.idx, + frappe.bold(row.item_code), + flt(row.transfer_qty, precision), + flt(expected, precision), + ), + title=_("Invalid Disassembly Quantity"), + ) + + def _get_disassembly_scale_factor(self) -> float: + disassemble_qty = flt(self.fg_completed_qty) + if self.source_stock_entry: + source_fg_qty = flt( + frappe.db.get_value("Stock Entry", self.source_stock_entry, "fg_completed_qty") + ) + elif self.work_order: + source_fg_qty = flt(frappe.db.get_value("Work Order", self.work_order, "produced_qty")) + else: + return 0.0 + + if not source_fg_qty: + return 0.0 + + return disassemble_qty / source_fg_qty + def check_if_operations_completed(self): """Check if Time Sheets are completed against before manufacturing to capture operating costs.""" prod_order = frappe.get_doc("Work Order", self.work_order) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index a82a1266651..19b435f8481 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -2244,6 +2244,91 @@ class TestStockEntry(FrappeTestCase): se.save() se.submit() +<<<<<<< HEAD +======= + def test_disassemble_blocks_finished_good_qty_tampering(self): + # A disassembly consuming N finished goods must consume exactly N (in stock UOM). + # Switching the finished-good row to a larger UOM with a tiny conversion_factor previously + # let a user consume ~0 finished goods while still producing the full raw materials -- + # minting inventory. The quantity invariant must reject this. + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + + fg_item = make_item("_Disassemble Mint FG", properties={"is_stock_item": 1}).name + rm_item1 = make_item("_Disassemble Mint RM1", properties={"is_stock_item": 1}).name + rm_item2 = make_item("_Disassemble Mint RM2", properties={"is_stock_item": 1}).name + warehouse = "_Test Warehouse - _TC" + + # Give the finished good a non-stock UOM. When uom == stock_uom the system resets the + # conversion_factor to 1, so the tamper is only possible (and worth guarding) on a + # non-stock UOM, where the user-supplied conversion_factor is preserved. + if not frappe.db.get_value("UOM Conversion Detail", {"parent": fg_item, "uom": "Box"}): + item_doc = frappe.get_doc("Item", fg_item) + item_doc.append("uoms", {"uom": "Box", "conversion_factor": 0.01}) + item_doc.save(ignore_permissions=True) + + make_stock_entry(item_code=fg_item, target=warehouse, qty=100, purpose="Material Receipt") + bom_no = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2]).name + + se = make_stock_entry(item_code=fg_item, qty=100, purpose="Disassemble", do_not_save=True) + se.from_bom = 1 + se.use_multi_level_bom = 1 + se.bom_no = bom_no + se.fg_completed_qty = 100 + se.from_warehouse = warehouse + se.to_warehouse = warehouse + se.get_items() + + # Tamper the finished-good row: a tiny conversion factor on the larger UOM means only + # 100 * 0.01 = 1 unit is actually consumed, while raw materials are still produced at full + # quantity. The finished-good consumption invariant must reject the save. + fg_row = next(d for d in se.items if d.is_finished_item) + fg_row.uom = "Box" + fg_row.conversion_factor = 0.01 + + self.assertRaises(frappe.ValidationError, se.save) + + @ERPNextTestSuite.change_settings( + "Stock Settings", {"sample_retention_warehouse": "_Test Warehouse 1 - _TC"} + ) + def test_sample_retention_stock_entry(self): + from erpnext.stock.doctype.stock_entry.stock_entry import move_sample_to_retention_warehouse + + warehouse = "_Test Warehouse - _TC" + retain_sample_item = make_item( + "Retain Sample Item", + properties={ + "is_stock_item": 1, + "retain_sample": 1, + "sample_quantity": 2, + "has_batch_no": 1, + "has_serial_no": 1, + "create_new_batch": 1, + "batch_number_series": "SAMPLE-RET-.#####", + "serial_no_series": "SAMPLE-RET-SN-.#####", + }, + ) + material_receipt = make_stock_entry( + item_code=retain_sample_item.item_code, target=warehouse, qty=10, purpose="Material Receipt" + ) + + source_sabb = frappe.get_doc( + "Serial and Batch Bundle", material_receipt.items[0].serial_and_batch_bundle + ) + batch = source_sabb.entries[0].batch_no + serial_nos = [entry.serial_no for entry in source_sabb.entries] + + sample_entry = frappe.get_doc( + move_sample_to_retention_warehouse(material_receipt.company, material_receipt.items) + ) + sample_entry.submit() + target_sabb = frappe.get_doc("Serial and Batch Bundle", sample_entry.items[0].serial_and_batch_bundle) + + self.assertEqual(sample_entry.items[0].transfer_qty, 2) + self.assertEqual(target_sabb.entries[0].batch_no, batch) + self.assertEqual([entry.serial_no for entry in target_sabb.entries], serial_nos[:2]) + + @ERPNextTestSuite.change_settings("Manufacturing Settings", {"material_consumption": 0}) +>>>>>>> 4453c1072a (fix: validate fg and materials qty in the disassemble entry) def test_raw_material_missing_validation(self): original_value = frappe.db.get_single_value("Manufacturing Settings", "material_consumption") frappe.db.set_single_value("Manufacturing Settings", "material_consumption", 0) From 335173c5ba432ee6c269c4a916dc8e9cc0b19a8c Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 8 Jun 2026 19:44:48 +0530 Subject: [PATCH 30/39] chore: fix conflicts Reintroduce the _qty_tolerance function to handle float rounding. --- erpnext/stock/doctype/stock_entry/stock_entry.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index cc0813ffd9e..ce2eb0ced74 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -80,17 +80,7 @@ from erpnext.controllers.stock_controller import StockController form_grid_templates = {"items": "templates/form_grid/stock_entry_grid.html"} -<<<<<<< HEAD class StockEntry(StockController): -======= -def _qty_tolerance(precision: int) -> float: - """One unit at the column's precision -- absorbs float rounding without letting a real - (whole-unit) quantity divergence slip through.""" - return 1.0 / (10**precision) - - -class StockEntry(StockController, SubcontractingInwardController): ->>>>>>> 4453c1072a (fix: validate fg and materials qty in the disassemble entry) # begin: auto-generated types # This code is auto-generated. Do not modify anything in this block. @@ -3491,6 +3481,12 @@ def move_sample_to_retention_warehouse(company, items): return stock_entry.as_dict() +def _qty_tolerance(precision: int) -> float: + """One unit at the column's precision -- absorbs float rounding without letting a real + (whole-unit) quantity divergence slip through.""" + return 1.0 / (10**precision) + + @frappe.whitelist() def make_stock_in_entry(source_name, target_doc=None): def set_missing_values(source, target): From 8bccc25b1ad9d6fb9dd0739bad258f3230b3d233 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 8 Jun 2026 19:45:39 +0530 Subject: [PATCH 31/39] chore: fix conflicts Removed unused test cases related to sample retention stock entry. --- .../doctype/stock_entry/test_stock_entry.py | 44 ------------------- 1 file changed, 44 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 19b435f8481..870091932b6 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -2244,8 +2244,6 @@ class TestStockEntry(FrappeTestCase): se.save() se.submit() -<<<<<<< HEAD -======= def test_disassemble_blocks_finished_good_qty_tampering(self): # A disassembly consuming N finished goods must consume exactly N (in stock UOM). # Switching the finished-good row to a larger UOM with a tiny conversion_factor previously @@ -2287,48 +2285,6 @@ class TestStockEntry(FrappeTestCase): self.assertRaises(frappe.ValidationError, se.save) - @ERPNextTestSuite.change_settings( - "Stock Settings", {"sample_retention_warehouse": "_Test Warehouse 1 - _TC"} - ) - def test_sample_retention_stock_entry(self): - from erpnext.stock.doctype.stock_entry.stock_entry import move_sample_to_retention_warehouse - - warehouse = "_Test Warehouse - _TC" - retain_sample_item = make_item( - "Retain Sample Item", - properties={ - "is_stock_item": 1, - "retain_sample": 1, - "sample_quantity": 2, - "has_batch_no": 1, - "has_serial_no": 1, - "create_new_batch": 1, - "batch_number_series": "SAMPLE-RET-.#####", - "serial_no_series": "SAMPLE-RET-SN-.#####", - }, - ) - material_receipt = make_stock_entry( - item_code=retain_sample_item.item_code, target=warehouse, qty=10, purpose="Material Receipt" - ) - - source_sabb = frappe.get_doc( - "Serial and Batch Bundle", material_receipt.items[0].serial_and_batch_bundle - ) - batch = source_sabb.entries[0].batch_no - serial_nos = [entry.serial_no for entry in source_sabb.entries] - - sample_entry = frappe.get_doc( - move_sample_to_retention_warehouse(material_receipt.company, material_receipt.items) - ) - sample_entry.submit() - target_sabb = frappe.get_doc("Serial and Batch Bundle", sample_entry.items[0].serial_and_batch_bundle) - - self.assertEqual(sample_entry.items[0].transfer_qty, 2) - self.assertEqual(target_sabb.entries[0].batch_no, batch) - self.assertEqual([entry.serial_no for entry in target_sabb.entries], serial_nos[:2]) - - @ERPNextTestSuite.change_settings("Manufacturing Settings", {"material_consumption": 0}) ->>>>>>> 4453c1072a (fix: validate fg and materials qty in the disassemble entry) def test_raw_material_missing_validation(self): original_value = frappe.db.get_single_value("Manufacturing Settings", "material_consumption") frappe.db.set_single_value("Manufacturing Settings", "material_consumption", 0) From cedd0a19035f6e37d17c907478fcb7587cf6f4fb Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:22:54 +0000 Subject: [PATCH 32/39] =?UTF-8?q?fix:=20bypass=20project=20permission=20ch?= =?UTF-8?q?eck=20when=20updating=20consumed=20material=20=E2=80=A6=20(back?= =?UTF-8?q?port=20#55645)=20(#55706)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: bypass project permission check when updating consumed material cost (cherry picked from commit 4b0b7adeee9ce4584bbe2185e1ecba7a6e578c8d) # Conflicts: # erpnext/stock/doctype/stock_entry/stock_entry.py * chore: resolve conflicts * chore: resolve conflicts --------- Co-authored-by: pandiyan Co-authored-by: Mihir Kandoi --- erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index b52945d8928..c3fff06dab2 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -680,7 +680,7 @@ class StockEntry(StockController): amount += additional_cost_amt project = frappe.get_doc("Project", self.project) project.total_consumed_material_cost = amount - project.save() + project.save(ignore_permissions=True) def validate_item(self): stock_items = self.get_stock_items() From a94e362b8ce33144b69afb1fcfc94a261374a551 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 8 Jun 2026 23:08:17 +0530 Subject: [PATCH 33/39] fix: sql injection (cherry picked from commit bd0acf4413fa99a3710e997c2c8dcb75e0286c64) # Conflicts: # erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py --- .../inactive_sales_items.py | 43 ++++++++++++++----- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py b/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py index df3fc48f9e1..f67214dfe35 100644 --- a/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py +++ b/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py @@ -4,6 +4,7 @@ import frappe from frappe import _ +from frappe.query_builder import CustomFunction from frappe.utils import cint @@ -94,19 +95,39 @@ def get_data(filters): def get_sales_details(filters): item_details_map = {} +<<<<<<< HEAD date_field = "s.transaction_date" if filters["based_on"] == "Sales Order" else "s.posting_date" +======= + if filters["based_on"] not in ("Sales Order", "Sales Invoice"): + frappe.throw(_("Invalid value {0} for 'Based On'").format(filters["based_on"])) - sales_data = frappe.db.sql( - """ - select s.territory, s.customer, si.item_group, si.item_code, si.qty, {date_field} as last_order_date, - DATEDIFF(CURRENT_DATE, {date_field}) as days_since_last_order - from `tab{doctype}` s, `tab{doctype} Item` si - where s.name = si.parent and s.docstatus = 1 - order by days_since_last_order """.format( # nosec - date_field=date_field, doctype=filters["based_on"] - ), - as_dict=1, - ) + parent = frappe.qb.DocType(filters["based_on"]) + child_doctype = "Sales Order Item" if filters["based_on"] == "Sales Order" else "Sales Invoice Item" + child = frappe.qb.DocType(child_doctype) +>>>>>>> bd0acf4413 (fix: sql injection) + + date_diff = CustomFunction("DATEDIFF", ["d1", "d2"]) + current_date = CustomFunction("CURRENT_DATE", []) + + date_col = parent.transaction_date if filters["based_on"] == "Sales Order" else parent.posting_date + days_since_last_order = date_diff(current_date(), date_col) + + sales_data = ( + frappe.qb.from_(parent) + .inner_join(child) + .on(parent.name == child.parent) + .select( + parent.territory, + parent.customer, + child.item_group, + child.item_code, + child.qty, + date_col.as_("last_order_date"), + days_since_last_order.as_("days_since_last_order"), + ) + .where(parent.docstatus == 1) + .orderby(days_since_last_order) + ).run(as_dict=True) for d in sales_data: item_details_map.setdefault((d.territory, d.item_code), d) From c0f56cd284a1197f66d5ac600c886d45d704e8ce Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 9 Jun 2026 09:31:58 +0530 Subject: [PATCH 34/39] chore: fix conflicts Removed conflicting code related to date_field assignment based on filters. --- .../report/inactive_sales_items/inactive_sales_items.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py b/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py index f67214dfe35..6f90cb13398 100644 --- a/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py +++ b/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py @@ -95,16 +95,12 @@ def get_data(filters): def get_sales_details(filters): item_details_map = {} -<<<<<<< HEAD - date_field = "s.transaction_date" if filters["based_on"] == "Sales Order" else "s.posting_date" -======= if filters["based_on"] not in ("Sales Order", "Sales Invoice"): frappe.throw(_("Invalid value {0} for 'Based On'").format(filters["based_on"])) parent = frappe.qb.DocType(filters["based_on"]) child_doctype = "Sales Order Item" if filters["based_on"] == "Sales Order" else "Sales Invoice Item" child = frappe.qb.DocType(child_doctype) ->>>>>>> bd0acf4413 (fix: sql injection) date_diff = CustomFunction("DATEDIFF", ["d1", "d2"]) current_date = CustomFunction("CURRENT_DATE", []) From 2f5b93e308446f0842c413cf9e2c2eb1d9de0d42 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 13:01:40 +0530 Subject: [PATCH 35/39] fix(cheque_print_template): print format creation from cheque print template requires system manager (backport #55708) (#55711) Co-authored-by: Diptanil Saha fix(cheque_print_template): print format creation from cheque print template requires system manager (#55708) --- .../cheque_print_template.js | 14 +- .../cheque_print_template.json | 1346 ++++------------- .../cheque_print_template.py | 2 + .../address_and_contacts.py | 1 - erpnext/templates/pages/projects.py | 1 + 5 files changed, 304 insertions(+), 1060 deletions(-) diff --git a/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.js b/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.js index 17cb8d00e48..7ce14b6c235 100644 --- a/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.js +++ b/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.js @@ -6,12 +6,14 @@ frappe.provide("erpnext.cheque_print"); frappe.ui.form.on("Cheque Print Template", { refresh: function (frm) { if (!frm.doc.__islocal) { - frm.add_custom_button( - frm.doc.has_print_format ? __("Update Print Format") : __("Create Print Format"), - function () { - erpnext.cheque_print.view_cheque_print(frm); - } - ).addClass("btn-primary"); + if (frappe.user.has_role("System Manager")) { + frm.add_custom_button( + frm.doc.has_print_format ? __("Update Print Format") : __("Create Print Format"), + function () { + erpnext.cheque_print.view_cheque_print(frm); + } + ).addClass("btn-primary"); + } $(frm.fields_dict.cheque_print_preview.wrapper).empty(); diff --git a/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.json b/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.json index e9993c34268..155fc43c5f7 100644 --- a/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.json +++ b/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.json @@ -1,1102 +1,342 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:bank_name", - "beta": 0, - "creation": "2016-05-04 14:35:00.402544", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", + "actions": [], + "allow_bulk_edit": 1, + "autoname": "field:bank_name", + "creation": "2016-05-04 14:35:00.402544", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "settings", + "has_print_format", + "primary_settings", + "bank_name", + "cheque_size", + "starting_position_from_top_edge", + "cheque_width", + "cheque_height", + "scanned_cheque", + "column_break_5", + "is_account_payable", + "acc_pay_dist_from_top_edge", + "acc_pay_dist_from_left_edge", + "message_to_show", + "date_and_payer_settings", + "date_settings", + "date_dist_from_top_edge", + "date_dist_from_left_edge", + "payer_settings", + "payer_name_from_top_edge", + "payer_name_from_left_edge", + "amount_in_words_and_figure_settings", + "html_19", + "amt_in_words_from_top_edge", + "amt_in_words_from_left_edge", + "amt_in_word_width", + "amt_in_words_line_spacing", + "amount_in_figure", + "amt_in_figures_from_top_edge", + "amt_in_figures_from_left_edge", + "account_number_and_signatory_settings", + "account_no_settings", + "acc_no_dist_from_top_edge", + "acc_no_dist_from_left_edge", + "signatory_position", + "signatory_from_top_edge", + "signatory_from_left_edge", + "preview", + "cheque_print_preview" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "description": "", - "fieldname": "settings", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "", - "length": 0, - "no_copy": 0, - "options": "
\n

All dimensions in centimeter only

\n
", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "settings", + "fieldtype": "HTML", + "options": "
\n

All dimensions in centimeter only

\n
" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "has_print_format", - "fieldtype": "Check", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Has Print Format", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "has_print_format", + "fieldtype": "Check", + "hidden": 1, + "label": "Has Print Format", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "primary_settings", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Primary Settings", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "primary_settings", + "fieldtype": "Section Break", + "label": "Primary Settings" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "bank_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Bank Name", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "bank_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Bank Name", + "no_copy": 1, + "reqd": 1, + "unique": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "Regular", - "fieldname": "cheque_size", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Cheque Size", - "length": 0, - "no_copy": 0, - "options": "\nRegular\nA4", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "Regular", + "fieldname": "cheque_size", + "fieldtype": "Select", + "label": "Cheque Size", + "options": "\nRegular\nA4" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "depends_on": "eval:doc.cheque_size==\"A4\"", - "fieldname": "starting_position_from_top_edge", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Starting position from top edge", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "2", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "depends_on": "eval:doc.cheque_size==\"A4\"", + "fieldname": "starting_position_from_top_edge", + "fieldtype": "Float", + "label": "Starting position from top edge", + "precision": "2" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "20.00", - "fieldname": "cheque_width", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Cheque Width", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "2", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "20.00", + "fieldname": "cheque_width", + "fieldtype": "Float", + "label": "Cheque Width", + "precision": "2" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "9.00", - "fieldname": "cheque_height", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Cheque Height", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "2", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "9.00", + "fieldname": "cheque_height", + "fieldtype": "Float", + "label": "Cheque Height", + "precision": "2" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "", - "fieldname": "scanned_cheque", - "fieldtype": "Attach", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Scanned Cheque", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "scanned_cheque", + "fieldtype": "Attach", + "label": "Scanned Cheque" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "column_break_5", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "1", - "fieldname": "is_account_payable", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Is Account Payable", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "1", + "fieldname": "is_account_payable", + "fieldtype": "Check", + "label": "Is Account Payable" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "1.00", - "depends_on": "eval:doc.is_account_payable", - "fieldname": "acc_pay_dist_from_top_edge", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Distance from top edge", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "2", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "1.00", + "depends_on": "eval:doc.is_account_payable", + "fieldname": "acc_pay_dist_from_top_edge", + "fieldtype": "Float", + "label": "Distance from top edge", + "precision": "2" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "9.00", - "depends_on": "eval:doc.is_account_payable", - "fieldname": "acc_pay_dist_from_left_edge", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Distance from left edge", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "2", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "9.00", + "depends_on": "eval:doc.is_account_payable", + "fieldname": "acc_pay_dist_from_left_edge", + "fieldtype": "Float", + "label": "Distance from left edge", + "precision": "2" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "Acc. Payee", - "depends_on": "eval:doc.is_account_payable", - "fieldname": "message_to_show", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Message to show", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "Acc. Payee", + "depends_on": "eval:doc.is_account_payable", + "fieldname": "message_to_show", + "fieldtype": "Data", + "label": "Message to show" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "date_and_payer_settings", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "date_and_payer_settings", + "fieldtype": "Section Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "date_settings", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Date Settings", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "date_settings", + "fieldtype": "HTML", + "label": "Date Settings", + "options": "" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "1.00", - "fieldname": "date_dist_from_top_edge", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Distance from top edge", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "2", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "1.00", + "fieldname": "date_dist_from_top_edge", + "fieldtype": "Float", + "label": "Distance from top edge", + "precision": "2" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "15.00", - "depends_on": "", - "fieldname": "date_dist_from_left_edge", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Starting location from left edge", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "2", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "15.00", + "fieldname": "date_dist_from_left_edge", + "fieldtype": "Float", + "label": "Starting location from left edge", + "precision": "2" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "payer_settings", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Payer Settings", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "payer_settings", + "fieldtype": "Column Break", + "label": "Payer Settings" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "2.00", - "fieldname": "payer_name_from_top_edge", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Distance from top edge", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "2", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "2.00", + "fieldname": "payer_name_from_top_edge", + "fieldtype": "Float", + "label": "Distance from top edge", + "precision": "2" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "3.00", - "fieldname": "payer_name_from_left_edge", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Starting location from left edge", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "2", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "3.00", + "fieldname": "payer_name_from_left_edge", + "fieldtype": "Float", + "label": "Starting location from left edge", + "precision": "2" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "amount_in_words_and_figure_settings", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "amount_in_words_and_figure_settings", + "fieldtype": "Section Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "html_19", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "html_19", + "fieldtype": "HTML", + "options": "" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "3.00", - "fieldname": "amt_in_words_from_top_edge", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Distance from top edge", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "2", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "3.00", + "fieldname": "amt_in_words_from_top_edge", + "fieldtype": "Float", + "label": "Distance from top edge", + "precision": "2" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "4.00", - "fieldname": "amt_in_words_from_left_edge", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Starting location from left edge", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "2", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "4.00", + "fieldname": "amt_in_words_from_left_edge", + "fieldtype": "Float", + "label": "Starting location from left edge", + "precision": "2" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "15.00", - "fieldname": "amt_in_word_width", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Width of amount in word", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "2", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "15.00", + "fieldname": "amt_in_word_width", + "fieldtype": "Float", + "label": "Width of amount in word", + "precision": "2" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "0.50", - "fieldname": "amt_in_words_line_spacing", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Line spacing for amount in words", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "2", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "0.50", + "fieldname": "amt_in_words_line_spacing", + "fieldtype": "Float", + "label": "Line spacing for amount in words", + "precision": "2" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "amount_in_figure", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Amount In Figure", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "amount_in_figure", + "fieldtype": "Column Break", + "label": "Amount In Figure" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "3.50", - "fieldname": "amt_in_figures_from_top_edge", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Distance from top edge", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "2", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "3.50", + "fieldname": "amt_in_figures_from_top_edge", + "fieldtype": "Float", + "label": "Distance from top edge", + "precision": "2" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "16.00", - "fieldname": "amt_in_figures_from_left_edge", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Starting location from left edge", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "2", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "16.00", + "fieldname": "amt_in_figures_from_left_edge", + "fieldtype": "Float", + "label": "Starting location from left edge", + "precision": "2" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "account_number_and_signatory_settings", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "account_number_and_signatory_settings", + "fieldtype": "Section Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "account_no_settings", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "account_no_settings", + "fieldtype": "HTML", + "options": "" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "5.00", - "fieldname": "acc_no_dist_from_top_edge", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Distance from top edge", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "2", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "5.00", + "fieldname": "acc_no_dist_from_top_edge", + "fieldtype": "Float", + "label": "Distance from top edge", + "precision": "2" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "4.00", - "fieldname": "acc_no_dist_from_left_edge", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Starting location from left edge", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "2", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "4.00", + "fieldname": "acc_no_dist_from_left_edge", + "fieldtype": "Float", + "label": "Starting location from left edge", + "precision": "2" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "signatory_position", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Signatory Position", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "signatory_position", + "fieldtype": "Column Break", + "label": "Signatory Position" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "6.00", - "fieldname": "signatory_from_top_edge", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Distance from top edge", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "2", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "6.00", + "fieldname": "signatory_from_top_edge", + "fieldtype": "Float", + "label": "Distance from top edge", + "precision": "2" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "15.00", - "fieldname": "signatory_from_left_edge", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Starting location from left edge", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "2", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "15.00", + "fieldname": "signatory_from_left_edge", + "fieldtype": "Float", + "label": "Starting location from left edge", + "precision": "2" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "preview", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Preview", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "preview", + "fieldtype": "Section Break", + "label": "Preview" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "cheque_print_preview", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "cheque_print_preview", + "fieldtype": "HTML" } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 1, - "modified": "2016-06-23 20:19:11.694932", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Cheque Print Template", - "name_case": "", - "owner": "Administrator", + ], + "links": [], + "max_attachments": 1, + "modified": "2026-06-08 12:10:35.829531", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Cheque Print Template", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_seen": 0 -} \ No newline at end of file + ], + "row_format": "Dynamic", + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} diff --git a/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.py b/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.py index 4b1394ede17..258ea7bb8d7 100644 --- a/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.py +++ b/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.py @@ -48,6 +48,8 @@ class ChequePrintTemplate(Document): @frappe.whitelist() def create_or_update_cheque_print_format(template_name): + frappe.only_for("System Manager") + if not frappe.db.exists("Print Format", template_name): cheque_print = frappe.new_doc("Print Format") cheque_print.update( diff --git a/erpnext/selling/report/address_and_contacts/address_and_contacts.py b/erpnext/selling/report/address_and_contacts/address_and_contacts.py index 0b08a24484f..ed25dd5f153 100644 --- a/erpnext/selling/report/address_and_contacts/address_and_contacts.py +++ b/erpnext/selling/report/address_and_contacts/address_and_contacts.py @@ -20,7 +20,6 @@ field_map = { } - def execute(filters=None): columns, data = get_columns(filters), get_data(filters) return columns, data diff --git a/erpnext/templates/pages/projects.py b/erpnext/templates/pages/projects.py index 22750d39aea..8f153105e3e 100644 --- a/erpnext/templates/pages/projects.py +++ b/erpnext/templates/pages/projects.py @@ -88,6 +88,7 @@ def get_timesheets(project, start=0, search=None): timesheet.update(info[0]) return timesheets + def get_attachments(project): return frappe.get_all( "File", From a5c23a3d16dedcb05bf05e924b8630e1dec44e41 Mon Sep 17 00:00:00 2001 From: Pandiyan P Date: Tue, 9 Jun 2026 13:13:23 +0530 Subject: [PATCH 36/39] fix: restrict already invoiced qty in intercompany purchase invoice (#55754) --- .../doctype/sales_invoice/sales_invoice.js | 21 ++++++- .../doctype/sales_invoice/sales_invoice.py | 28 +++++---- .../sales_invoice/test_sales_invoice.py | 61 +++++++++++++++++++ 3 files changed, 98 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 61b9cf1fc06..259cd970941 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -180,12 +180,31 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( : "Inter Company Purchase Invoice"; me.frm.add_custom_button( - button_label, + __(button_label), function () { me.make_inter_company_invoice(); }, __("Create") ); + + frappe.call({ + method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.get_received_items", + args: { + reference_name: me.frm.doc.name, + doctype: "Purchase Invoice", + reference_fieldname: "sales_invoice_item", + }, + callback: function (r) { + if (r.exc) return; + const received_items = r.message || {}; + const has_pending_qty = me.frm.doc.items.some( + (item) => flt(item.qty) - flt(received_items[item.name] || 0) > 0 + ); + if (!has_pending_qty) { + me.frm.remove_custom_button(__(button_label), __("Create")); + } + }, + }); } } diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 3e16c503e69..df76ef6831e 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2526,7 +2526,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): "rate": "rate", }, "postprocess": update_item, - "condition": lambda doc: doc.qty > 0, + "condition": lambda doc: doc.qty - received_items.get(doc.name, 0.0) > 0, } if doctype in ["Sales Invoice", "Sales Order"]: @@ -2566,11 +2566,18 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): target_doc, set_missing_values, ) - + if not doclist.get("items"): + frappe.throw( + _( + "Cannot create Intercompany {0}. All items in the source {1} have already been fully invoiced. " + "Please check the existing linked {2}s." + ).format(target_doctype, doctype, target_doctype) + ) return doclist -def get_received_items(reference_name, doctype, reference_fieldname): +@frappe.whitelist() +def get_received_items(reference_name: str, doctype: str, reference_fieldname: str): reference_field = "inter_company_invoice_reference" if doctype == "Purchase Order": reference_field = "inter_company_order_reference" @@ -2583,20 +2590,19 @@ def get_received_items(reference_name, doctype, reference_fieldname): target_doctypes = frappe.get_all( doctype, filters=filters, - as_list=True, + pluck="name", ) - + received_items_map = {} if target_doctypes: - target_doctypes = list(target_doctypes[0]) - - received_items_map = frappe._dict( - frappe.get_all( + received_items_data = frappe.get_all( doctype + " Item", filters={"parent": ("in", target_doctypes)}, fields=[reference_fieldname, "qty"], - as_list=1, ) - ) + for item in received_items_data: + key = item.get(reference_fieldname) + if key: + received_items_map[key] = received_items_map.get(key, 0.0) + flt(item.qty) return received_items_map diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 147bb76064d..ef7c25c86a9 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2690,6 +2690,67 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(target_doc.company, "_Test Company 1") self.assertEqual(target_doc.supplier, "_Test Internal Supplier") + def test_restrict_inter_company_pi_when_sales_invoice_qty_fully_consumed(self): + item_code_1 = "_Test IC Item 1" + item_code_2 = "_Test IC Item 2" + + create_item(item_code_1, is_stock_item=1) + create_item(item_code_2, is_stock_item=1) + + si = create_sales_invoice( + company="Wind Power LLC", + customer="_Test Internal Customer", + item_code=item_code_1, + debit_to="Debtors - WP", + warehouse="Stores - WP", + income_account="Sales - WP", + expense_account="Cost of Goods Sold - WP", + cost_center="Main - WP", + currency="USD", + qty=3, + do_not_save=1, + ) + si.selling_price_list = "_Test Price List Rest of the World" + si.append( + "items", + { + "item_code": item_code_2, + "item_name": item_code_2, + "description": item_code_2, + "warehouse": "Stores - WP", + "qty": 2, + "uom": "Nos", + "stock_uom": "Nos", + "rate": 100, + "price_list_rate": 100, + "income_account": "Sales - WP", + "expense_account": "Cost of Goods Sold - WP", + "cost_center": "Main - WP", + "conversion_factor": 1, + }, + ) + + si.submit() + + target_doc = make_inter_company_transaction("Sales Invoice", si.name) + + for item in target_doc.items: + item.update( + { + "expense_account": "Cost of Goods Sold - _TC1", + "cost_center": "Main - _TC1", + } + ) + + target_doc.submit() + self.assertEqual(len(target_doc.items), 2) + self.assertEqual([item.qty for item in target_doc.items], [3, 2]) + with self.assertRaisesRegex( + frappe.ValidationError, + "already been fully invoiced", + ): + make_inter_company_transaction("Sales Invoice", si.name) + def test_inter_company_transaction_without_default_warehouse(self): "Check mapping (expense account) of inter company SI to PI in absence of default warehouse." # setup From 0e64acb0fa04042268805e11fa4f7b4a082708aa Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 13:23:49 +0000 Subject: [PATCH 37/39] fix: prevent leakage of party-derived fields in cross doctype transactions (backport #55336) (#55578) Co-authored-by: Nabin Hait Co-authored-by: Lakshit Jain fix: prevent leakage of party-derived fields in cross doctype transactions (#55336) --- .../doctype/sales_invoice/sales_invoice.py | 9 ++++- .../sales_invoice/test_sales_invoice.py | 28 ++++++++++++++ erpnext/accounts/party.py | 19 ++++++++++ .../doctype/sales_order/sales_order.py | 35 +++++------------ .../doctype/sales_order/test_sales_order.py | 35 ++++++++++++++--- .../doctype/delivery_note/delivery_note.py | 5 +-- .../purchase_receipt/test_purchase_receipt.py | 38 +++++++++++++++++++ 7 files changed, 133 insertions(+), 36 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index df76ef6831e..5d6676aa537 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -23,7 +23,12 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category get_party_tax_withholding_details, ) from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center -from erpnext.accounts.party import get_due_date, get_party_account, get_party_details +from erpnext.accounts.party import ( + CROSS_PARTY_FIELD_NO_MAP, + get_due_date, + get_party_account, + get_party_details, +) from erpnext.accounts.utils import ( cancel_exchange_gain_loss_journal, get_account_currency, @@ -2559,7 +2564,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): "doctype": target_doctype, "postprocess": update_details, "set_target_warehouse": "set_from_warehouse", - "field_no_map": ["taxes_and_charges", "set_warehouse", "shipping_address", "cost_center"], + "field_no_map": [*CROSS_PARTY_FIELD_NO_MAP, "set_warehouse", "cost_center"], }, doctype + " Item": item_field_map, }, diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index ef7c25c86a9..e210903c450 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2751,6 +2751,34 @@ class TestSalesInvoice(FrappeTestCase): ): make_inter_company_transaction("Sales Invoice", si.name) + def test_inter_company_transaction_does_not_inherit_party_fields(self): + """ + Party-derived fields on SI (from Customer) must not leak into the mapped PI. + """ + si = create_sales_invoice( + company="Wind Power LLC", + customer="_Test Internal Customer", + debit_to="Debtors - WP", + warehouse="Stores - WP", + income_account="Sales - WP", + expense_account="Cost of Goods Sold - WP", + cost_center="Main - WP", + currency="USD", + do_not_save=1, + ) + si.selling_price_list = "_Test Price List Rest of the World" + si.tax_category = "_Test Tax Category 1" + si.language = "ar" + si.payment_terms_template = "_Test Payment Term Template" + si.submit() + + pi = make_inter_company_transaction("Sales Invoice", si.name) + + supplier = frappe.get_doc("Supplier", "_Test Internal Supplier") + self.assertEqual(pi.tax_category or None, supplier.tax_category or None) + self.assertEqual(pi.language or None, supplier.language or None) + self.assertEqual(pi.payment_terms_template or None, supplier.payment_terms or None) + def test_inter_company_transaction_without_default_warehouse(self): "Check mapping (expense account) of inter company SI to PI in absence of default warehouse." # setup diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index a5d940d7b7f..5c0860b4fd3 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -48,6 +48,25 @@ SALES_TRANSACTION_TYPES = { } TRANSACTION_TYPES = PURCHASE_TRANSACTION_TYPES | SALES_TRANSACTION_TYPES +# Party-derived fields that must NOT be auto-copied by `get_mapped_doc` when the +# source and target documents belong to different parties (e.g. Sales Order → +# Purchase Order or inter-company Sales Invoice → Purchase Invoice). +CROSS_PARTY_FIELD_NO_MAP = [ + "tax_category", + "tax_id", + "tax_withholding_category", + "taxes_and_charges", + "address_display", + "contact_display", + "contact_mobile", + "contact_email", + "contact_person", + "shipping_address", + "dispatch_address", + "payment_terms_template", + "language", +] + class DuplicatePartyAccountError(frappe.ValidationError): pass diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 856a75606bc..1dd62a65239 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -10,6 +10,7 @@ import frappe.utils from frappe import _, qb from frappe.contacts.doctype.address.address import get_company_address from frappe.desk.notifications import clear_doctype_notifications +from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc from frappe.model.utils import get_fetch_values from frappe.query_builder.functions import Sum @@ -20,7 +21,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( update_linked_doc, validate_inter_company_party, ) -from erpnext.accounts.party import get_party_account +from erpnext.accounts.party import CROSS_PARTY_FIELD_NO_MAP, get_party_account from erpnext.controllers.selling_controller import SellingController from erpnext.manufacturing.doctype.blanket_order.blanket_order import ( validate_against_blanket_order, @@ -1332,7 +1333,9 @@ def get_events(start, end, filters=None): @frappe.whitelist() -def make_purchase_order_for_default_supplier(source_name, selected_items=None, target_doc=None): +def make_purchase_order_for_default_supplier( + source_name: str, selected_items: str | list | None = None, target_doc: str | Document | None = None +): """Creates Purchase Order for each Supplier. Returns a list of doc objects.""" from erpnext.setup.utils import get_exchange_rate @@ -1361,7 +1364,6 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t target.shipping_rule = "" target.tc_name = "" target.terms = "" - target.payment_terms_template = "" target.payment_schedule = [] default_price_list = frappe.get_value("Supplier", supplier, "default_price_list") @@ -1418,16 +1420,7 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t { "Sales Order": { "doctype": "Purchase Order", - "field_no_map": [ - "address_display", - "contact_display", - "contact_mobile", - "contact_email", - "contact_person", - "taxes_and_charges", - "shipping_address", - "dispatch_address", - ], + "field_no_map": [*CROSS_PARTY_FIELD_NO_MAP], "validation": {"docstatus": ["=", 1]}, }, "Sales Order Item": { @@ -1492,7 +1485,9 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t @frappe.whitelist() -def make_purchase_order(source_name, selected_items=None, target_doc=None): +def make_purchase_order( + source_name: str, selected_items: str | list | None = None, target_doc: str | Document | None = None +): if not selected_items: return @@ -1520,7 +1515,6 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): target.shipping_rule = "" target.tc_name = "" target.terms = "" - target.payment_terms_template = "" target.payment_schedule = [] if is_drop_ship_order(target): @@ -1559,16 +1553,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): { "Sales Order": { "doctype": "Purchase Order", - "field_no_map": [ - "address_display", - "contact_display", - "contact_mobile", - "contact_email", - "contact_person", - "taxes_and_charges", - "shipping_address", - "dispatch_address", - ], + "field_no_map": [*CROSS_PARTY_FIELD_NO_MAP], "validation": {"docstatus": ["=", 1]}, }, "Sales Order Item": { diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 18d416ad7df..6f8befeb876 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -24,6 +24,8 @@ from erpnext.selling.doctype.sales_order.sales_order import ( create_pick_list, make_delivery_note, make_material_request, + make_purchase_order, + make_purchase_order_for_default_supplier, make_raw_material_request, make_sales_invoice, make_work_orders, @@ -1364,8 +1366,6 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): Tests if the the Product Bundles in the Items table of Sales Orders are replaced with their child items(from the Packed Items table) on creating a Purchase Order from it. """ - from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order - product_bundle = make_item("_Test Product Bundle", {"is_stock_item": 0}) make_item("_Test Bundle Item 1", {"is_stock_item": 1}) make_item("_Test Bundle Item 2", {"is_stock_item": 1}) @@ -1394,8 +1394,6 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): """ Tests if the packed item's `ordered_qty` is updated with the quantity of the Purchase Order """ - from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order - product_bundle = make_item("_Test Product Bundle", {"is_stock_item": 0}) make_item("_Test Bundle Item 1", {"is_stock_item": 1}) make_item("_Test Bundle Item 2", {"is_stock_item": 1}) @@ -2419,8 +2417,6 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): self.assertRaises(frappe.ValidationError, so1.update_status, "Draft") def test_item_tax_transfer_from_sales_to_purchase(self): - from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order - item_tax = frappe.new_doc("Item Tax Template") item_tax.title = "Test Item Tax Template" item_tax.company = "_Test Company" @@ -2521,6 +2517,33 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): self.assertFalse(so.per_billed) self.assertEqual(so.status, "To Deliver and Bill") + def test_make_purchase_order_does_not_inherit_party_fields(self): + """ + Customer-derived fields must not leak from a drop-ship SO into the PO. + """ + so_items = [ + { + "item_code": "_Test Item", + "warehouse": "", + "qty": 1, + "rate": 100, + "delivered_by_supplier": 1, + "supplier": "_Test Supplier", + } + ] + so = make_sales_order(item_list=so_items, do_not_submit=True) + so.tax_category = "_Test Tax Category 1" + so.language = "ar" + so.payment_terms_template = "_Test Payment Term Template" + so.submit() + + po = make_purchase_order_for_default_supplier(so.name, selected_items=so_items)[0] + + supplier = frappe.get_doc("Supplier", "_Test Supplier") + self.assertEqual(po.tax_category or None, supplier.tax_category or None) + self.assertEqual(po.language or None, supplier.language or None) + self.assertEqual(po.payment_terms_template or None, supplier.payment_terms or None) + def test_pending_quantity_after_update_item_during_invoice_creation(self): so = make_sales_order(qty=30, rate=100) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index f1fc54c751d..0ad8bc781a5 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -15,7 +15,7 @@ from frappe.query_builder import DocType from frappe.query_builder.functions import Abs, Sum from frappe.utils import cint, flt -from erpnext.accounts.party import get_due_date +from erpnext.accounts.party import CROSS_PARTY_FIELD_NO_MAP, get_due_date from erpnext.controllers.accounts_controller import get_taxes_and_charges, merge_taxes from erpnext.controllers.selling_controller import SellingController from erpnext.stock.stock_ledger import validate_reserved_stock @@ -1405,8 +1405,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): doctype: { "doctype": target_doctype, "postprocess": update_details, - "field_no_map": ["taxes_and_charges", "set_warehouse"], - "field_map": {"shipping_address_name": "shipping_address"}, + "field_no_map": [*CROSS_PARTY_FIELD_NO_MAP, "set_warehouse"], }, doctype + " Item": { "doctype": target_doctype + " Item", diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index ce23acf85e2..3799e773a7d 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1050,6 +1050,44 @@ class TestPurchaseReceipt(FrappeTestCase): pr.cancel() + def test_inter_company_purchase_receipt_does_not_inherit_party_fields(self): + """ + Party-derived fields on DN (from Customer) must not leak into the mapped PR. + """ + 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" + + stock = make_purchase_receipt(warehouse="Stores - TCP1", company=company) + + dn = create_delivery_note( + company=company, + customer=customer, + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + qty=1, + rate=100, + warehouse="Stores - TCP1", + target_warehouse="Work In Progress - TCP1", + do_not_submit=True, + ) + # Stamp customer-side party fields onto the DN + dn.tax_category = "_Test Tax Category 2" + dn.language = "ar" + dn.submit() + + pr = make_inter_company_purchase_receipt(dn.name) + + supplier = frappe.get_doc("Supplier", "_Test Internal Supplier 2") + self.assertEqual(pr.tax_category or None, supplier.tax_category or None) + self.assertEqual(pr.language or None, supplier.language or None) + dn.cancel() + stock.cancel() + def test_lcv_for_internal_transfer(self): 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 From 95f46dfc015517ca390b29ad27c46fa4575f7fb2 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 9 Jun 2026 19:15:52 +0530 Subject: [PATCH 38/39] fix: handle multi-select stock ageing filters (#55776) --- .../stock/report/stock_ageing/stock_ageing.py | 35 ++++++++----------- .../report/stock_ageing/test_stock_ageing.py | 12 +++++++ 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 93fac84d60a..238b72fa957 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -11,6 +11,7 @@ from frappe.query_builder.functions import Abs, Count from frappe.utils import cint, date_diff, flt, get_datetime from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos +from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter from erpnext.stock.valuation import round_off_if_near_zero Filters = frappe._dict @@ -936,9 +937,7 @@ class FIFOSlots: ) ) - for field in ["item_code"]: - if self.filters.get(field): - query = query.where(sle[field] == self.filters.get(field)) + query = self._apply_filter(query, sle, "item_code") if self.filters.get("warehouse"): query = self._get_warehouse_conditions(sle, query) @@ -985,9 +984,7 @@ class FIFOSlots: ) ) - for field in ["item_code"]: - if self.filters.get(field): - query = query.where(sle[field] == self.filters.get(field)) + query = self._apply_filter(query, sle, "item_code") if self.filters.get("warehouse"): query = self._get_warehouse_conditions(sle, query) @@ -1020,27 +1017,25 @@ class FIFOSlots: "has_batch_no", ) - if self.filters.get("item_code"): - item = item.where(item_table.item_code == self.filters.get("item_code")) + item = self._apply_filter(item, item_table, "item_code") if self.filters.get("brand"): item = item.where(item_table.brand == self.filters.get("brand")) return item + def _apply_filter(self, query, table, fieldname: str): + filter_value = self.filters.get(fieldname) + if not filter_value: + return query + + if isinstance(filter_value, list | tuple | set): + return query.where(table[fieldname].isin(filter_value)) + + return query.where(table[fieldname] == filter_value) + def _get_warehouse_conditions(self, sle, sle_query) -> str: - warehouse = frappe.qb.DocType("Warehouse") - lft, rgt = frappe.db.get_value("Warehouse", self.filters.get("warehouse"), ["lft", "rgt"]) - - warehouse_results = ( - frappe.qb.from_(warehouse) - .select("name") - .where((warehouse.lft >= lft) & (warehouse.rgt <= rgt)) - .run() - ) - warehouse_results = [x[0] for x in warehouse_results] - - return sle_query.where(sle.warehouse.isin(warehouse_results)) + return apply_warehouse_filter(sle_query, sle, self.filters) def prepare_stock_reco_voucher_wise_count(self): self.stock_reco_voucher_wise_count = frappe._dict() diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index 601524c74e9..1c61306125e 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -129,6 +129,18 @@ class TestStockAgeing(FrappeTestCase): self.assertEqual(queue[0][0], 10.0) self.assertEqual(queue[1][0], 10.0) + def test_item_filter_supports_multi_select_values(self): + bundle = frappe.qb.DocType("Serial and Batch Bundle") + query = frappe.qb.from_(bundle).select(bundle.name) + + filtered_query = FIFOSlots(frappe._dict(item_code=["Item A"]), [])._apply_filter( + query, bundle, "item_code" + ) + + sql = filtered_query.get_sql() + self.assertIn(" IN ", sql) + self.assertNotIn("=[", sql) + def test_basic_stock_reconciliation(self): """ Ledger (same wh): [+30, reco reset >> 50, -10] From 18ca96c36ba65362bf1c25abb8eab32c64c6c7dd Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 19:10:24 +0000 Subject: [PATCH 39/39] fix(process statement of accounts): validate pdf_name and validate permission before triggering send_auto_email (backport #55781) (#55782) Co-authored-by: Diptanil Saha fix(process statement of accounts): validate pdf_name and validate permission before triggering send_auto_email (#55781) --- .../process_statement_of_accounts.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 273aa6c03de..26a45bbdc55 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -99,6 +99,7 @@ class ProcessStatementOfAccounts(Document): validate_template(self.subject) validate_template(self.body) + validate_template(self.pdf_name) if not self.customers: frappe.throw(_("Customers not selected.")) @@ -548,6 +549,7 @@ def send_emails(document_name, from_scheduler=False, posting_date=None): @frappe.whitelist() def send_auto_email(): + frappe.has_permission("Process Statement Of Accounts", throw=True) selected = frappe.get_list( "Process Statement Of Accounts", filters={"enable_auto_email": 1},