From 376da8df0a20971caf1800d5d6bbdeece88af2cd Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 21 Mar 2024 11:52:10 +0100 Subject: [PATCH 01/30] feat(Item Price): validate UOM (cherry picked from commit 69824eff80eb70b7e3139c6db84eedad84ceb8b4) # Conflicts: # erpnext/stock/doctype/item_price/item_price.py --- erpnext/stock/doctype/item_price/item_price.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index 5445e1b88b0..63cb604a28d 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -55,6 +55,19 @@ class ItemPrice(Document): if not frappe.db.exists("Item", self.item_code): frappe.throw(_("Item {0} not found.").format(self.item_code)) +<<<<<<< HEAD +======= + if self.uom and not frappe.db.exists( + "UOM Conversion Detail", {"parenttype": "Item", "parent": self.item_code, "uom": self.uom} + ): + frappe.throw(_("UOM {0} not found in Item {1}").format(self.uom, self.item_code)) + + def validate_dates(self): + if self.valid_from and self.valid_upto: + if getdate(self.valid_from) > getdate(self.valid_upto): + frappe.throw(_("Valid From Date must be lesser than Valid Up To Date.")) + +>>>>>>> 69824eff80 (feat(Item Price): validate UOM) def update_price_list_details(self): if self.price_list: price_list_details = frappe.db.get_value( From 7d607b82f1eecb4f778d5277c95267b0abd40415 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Wed, 29 Oct 2025 15:37:04 +0100 Subject: [PATCH 02/30] chore: resolve conflicts --- erpnext/stock/doctype/item_price/item_price.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index 63cb604a28d..dc693890cd7 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -55,19 +55,11 @@ class ItemPrice(Document): if not frappe.db.exists("Item", self.item_code): frappe.throw(_("Item {0} not found.").format(self.item_code)) -<<<<<<< HEAD -======= if self.uom and not frappe.db.exists( "UOM Conversion Detail", {"parenttype": "Item", "parent": self.item_code, "uom": self.uom} ): frappe.throw(_("UOM {0} not found in Item {1}").format(self.uom, self.item_code)) - def validate_dates(self): - if self.valid_from and self.valid_upto: - if getdate(self.valid_from) > getdate(self.valid_upto): - frappe.throw(_("Valid From Date must be lesser than Valid Up To Date.")) - ->>>>>>> 69824eff80 (feat(Item Price): validate UOM) def update_price_list_details(self): if self.price_list: price_list_details = frappe.db.get_value( From 3d8a344173131e5265b482a7df51115d4cab377e Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Tue, 4 Nov 2025 12:38:17 +0530 Subject: [PATCH 03/30] fix: on changes of paid from/to account fetch company bank account (cherry picked from commit 4901dc2531c559246428e32dd6c9e067731cc296) --- .../doctype/payment_entry/payment_entry.js | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index d36313dc6da..7ca374e6575 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -592,6 +592,8 @@ frappe.ui.form.on("Payment Entry", { paid_from: function (frm) { if (frm.set_party_account_based_on_party) return; + frm.events.set_company_bank_account(frm); + frm.events.set_account_currency_and_balance( frm, frm.doc.paid_from, @@ -609,6 +611,8 @@ frappe.ui.form.on("Payment Entry", { paid_to: function (frm) { if (frm.set_party_account_based_on_party) return; + frm.events.set_company_bank_account(frm); + frm.events.set_account_currency_and_balance( frm, frm.doc.paid_to, @@ -1350,6 +1354,8 @@ frappe.ui.form.on("Payment Entry", { }, bank_account: function (frm) { + if (frm.set_company_bank_account_based_on_coa) return; + const field = frm.doc.payment_type == "Pay" ? "paid_from" : "paid_to"; if (frm.doc.bank_account && ["Pay", "Receive"].includes(frm.doc.payment_type)) { frappe.call({ @@ -1388,6 +1394,34 @@ frappe.ui.form.on("Payment Entry", { } }, + set_company_bank_account: function (frm) { + if (!["Pay", "Receive"].includes(frm.doc.payment_type)) return; + + const field = frm.doc.payment_type == "Pay" ? "paid_from" : "paid_to"; + + if (!frm.doc.company || !frm.doc[field]) return; + + frm.set_company_bank_account_based_on_coa = true; + + frappe.call({ + method: "frappe.client.get_value", + args: { + doctype: "Bank Account", + filters: { + company: frm.doc.company, + account: frm.doc[field], + disabled: 0, + }, + fieldname: ["name"], + }, + callback: async function (r) { + if (r.message) await frm.set_value("bank_account", r.message.name); + + frm.set_company_bank_account_based_on_coa = false; + }, + }); + }, + sales_taxes_and_charges_template: function (frm) { frm.trigger("fetch_taxes_from_template"); }, From 87e8305753d5c26371dfa6b0ad73891c3973e22d Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Fri, 7 Nov 2025 16:03:04 +0530 Subject: [PATCH 04/30] feat(pos): prevent disabling POS Profile when open POS sessions exist (cherry picked from commit c5219278fb379de6f635a487de7f0db3bb8b6e55) --- .../accounts/doctype/pos_profile/pos_profile.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py index 143407eb750..2928782a647 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py @@ -70,6 +70,7 @@ class POSProfile(Document): # end: auto-generated types def validate(self): + self.validate_disabled() self.validate_default_profile() self.validate_all_link_fields() self.validate_duplicate_groups() @@ -94,6 +95,21 @@ class POSProfile(Document): title=_("Mandatory Accounting Dimension"), ) + def validate_disabled(self): + old_doc = self.get_doc_before_save() + + if ( + old_doc + and self.disabled + and old_doc.disabled != self.disabled + and frappe.db.exists("POS Opening Entry", {"pos_profile": self.name, "status": "Open"}) + ): + frappe.throw( + _("POS Profile {0} cannot be disabled as there are ongoing POS sessions.").format( + frappe.bold(self.name) + ) + ) + def validate_default_profile(self): for row in self.applicable_for_users: res = frappe.db.sql( From 38848ff43b4ce792313dc651077cc1d91c968aa0 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Sat, 8 Nov 2025 12:31:09 +0530 Subject: [PATCH 05/30] test: added test to validate disabled pos profile (cherry picked from commit 69016a284f5f4f644e75723a4c47908a003ba81c) # Conflicts: # erpnext/accounts/doctype/pos_profile/test_pos_profile.py --- .../doctype/pos_profile/test_pos_profile.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py index f2e3c8fcf59..427748627ed 100644 --- a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py @@ -4,6 +4,11 @@ import unittest import frappe +<<<<<<< HEAD +======= +from frappe.tests import IntegrationTestCase +from frappe.utils import cint +>>>>>>> 69016a284f (test: added test to validate disabled pos profile) from erpnext.accounts.doctype.pos_profile.pos_profile import ( get_child_nodes, @@ -38,6 +43,50 @@ class TestPOSProfile(unittest.TestCase): frappe.db.sql("delete from `tabPOS Profile`") + def test_disabled_pos_profile_creation(self): + make_pos_profile(name="_Test POS Profile 001", disabled=1) + + pos_profile = frappe.get_doc("POS Profile", "_Test POS Profile 001") + + if pos_profile: + self.assertEqual(pos_profile.disabled, 1) + + def test_disabled_pos_profile_after_opening(self): + from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile + from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry + + test_user, pos_profile = init_user_and_profile() + + if pos_profile: + create_opening_entry(pos_profile, test_user.name) + self.assertEqual(pos_profile.disabled, 0) + + pos_profile.disabled = 1 + self.assertRaises(frappe.ValidationError, pos_profile.save) + + def test_disabled_pos_profile_after_completing_session(self): + from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import ( + make_closing_entry_from_opening, + ) + from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile + from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import ( + create_opening_entry, + ) + + test_user, pos_profile = init_user_and_profile() + + if pos_profile: + opening_entry = create_opening_entry(pos_profile, test_user.name) + + closing_entry = make_closing_entry_from_opening(opening_entry) + closing_entry.submit() + + pos_profile.disabled = 1 + pos_profile.save() + pos_profile.reload() + + self.assertEqual(pos_profile.disabled, 1) + def get_customers_list(pos_profile=None): if pos_profile is None: @@ -117,6 +166,7 @@ def make_pos_profile(**args): "write_off_account": args.write_off_account or "_Test Write Off - _TC", "write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC", "location": "Block 1" if not args.do_not_set_accounting_dimension else None, + "disabled": cint(args.disabled) or 0, } ) From 68747b5818e10cbe2a92df2b1d6f0d9e236e76e7 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Sat, 8 Nov 2025 12:31:40 +0530 Subject: [PATCH 06/30] fix: prevent pos opening entry creation for disabled pos profile (cherry picked from commit e35e8968f0825256e09b7beba8615270b9559e4d) # Conflicts: # erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py --- .../pos_opening_entry/pos_opening_entry.py | 14 ++- .../test_pos_opening_entry.py | 99 +++++++++++++++++++ 2 files changed, 111 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py index 7f1890ceabf..d7965c58831 100644 --- a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py +++ b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py @@ -41,9 +41,19 @@ class POSOpeningEntry(StatusUpdater): self.set_status() def validate_pos_profile_and_cashier(self): - if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"): + if not frappe.db.exists("POS Profile", self.pos_profile): + frappe.throw(_("POS Profile {} does not exist.").format(self.pos_profile)) + + pos_profile_company, pos_profile_disabled = frappe.db.get_value( + "POS Profile", self.pos_profile, ["company", "disabled"] + ) + + if pos_profile_disabled: + frappe.throw(_("POS Profile {} is disabled.").format(frappe.bold(self.pos_profile))) + + if self.company != pos_profile_company: frappe.throw( - _("POS Profile {} does not belongs to company {}").format(self.pos_profile, self.company) + _("POS Profile {} does not belong to company {}").format(self.pos_profile, self.company) ) if not cint(frappe.db.get_value("User", self.user, "enabled")): diff --git a/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py b/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py index 64c658ab151..e0276f8bb62 100644 --- a/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py +++ b/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py @@ -6,8 +6,107 @@ import unittest import frappe +<<<<<<< HEAD class TestPOSOpeningEntry(unittest.TestCase): pass +======= +class TestPOSOpeningEntry(IntegrationTestCase): + @classmethod + def setUpClass(cls): + frappe.db.sql("delete from `tabPOS Opening Entry`") + cls.enterClassContext(cls.change_settings("POS Settings", {"invoice_type": "POS Invoice"})) + + @classmethod + def tearDownClass(cls): + frappe.db.sql("delete from `tabPOS Opening Entry`") + + def setUp(self): + # Make stock available for POS Sales + frappe.db.sql("delete from `tabPOS Opening Entry`") + make_stock_entry(target="_Test Warehouse - _TC", qty=2, basic_rate=100) + from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile + + self.init_user_and_profile = init_user_and_profile + + def tearDown(self): + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + + def test_pos_opening_entry(self): + test_user, pos_profile = self.init_user_and_profile() + opening_entry = create_opening_entry(pos_profile, test_user.name) + + self.assertEqual(opening_entry.status, "Open") + self.assertNotEqual(opening_entry.docstatus, 0) + + def test_pos_opening_entry_on_disabled_pos(self): + test_user, pos_profile = self.init_user_and_profile(disabled=1) + + with self.assertRaises(frappe.ValidationError): + create_opening_entry(pos_profile, test_user.name) + + def test_multiple_pos_opening_entries_for_same_pos_profile(self): + test_user, pos_profile = self.init_user_and_profile() + opening_entry = create_opening_entry(pos_profile, test_user.name) + + self.assertEqual(opening_entry.status, "Open") + with self.assertRaises(frappe.ValidationError): + create_opening_entry(pos_profile, test_user.name) + + def test_multiple_pos_opening_entry_for_multiple_pos_profiles(self): + test_user, pos_profile = self.init_user_and_profile() + opening_entry_1 = create_opening_entry(pos_profile, test_user.name) + + self.assertEqual(opening_entry_1.status, "Open") + self.assertEqual(opening_entry_1.user, test_user.name) + + cashier_user = create_user("test_cashier@example.com", "Accounts Manager", "Sales Manager") + frappe.set_user(cashier_user.name) + + pos_profile2 = make_pos_profile(name="_Test POS Profile 2") + opening_entry_2 = create_opening_entry(pos_profile2, cashier_user.name) + + self.assertEqual(opening_entry_2.status, "Open") + self.assertEqual(opening_entry_2.user, cashier_user.name) + + def test_multiple_pos_opening_entry_for_same_pos_profile_by_multiple_user(self): + test_user, pos_profile = self.init_user_and_profile() + cashier_user = create_user("test_cashier@example.com", "Accounts Manager", "Sales Manager") + + opening_entry = create_opening_entry(pos_profile, test_user.name) + self.assertEqual(opening_entry.status, "Open") + + with self.assertRaises(frappe.ValidationError): + create_opening_entry(pos_profile, cashier_user.name) + + def test_user_assignment_to_multiple_pos_profile(self): + test_user, pos_profile = self.init_user_and_profile() + opening_entry_1 = create_opening_entry(pos_profile, test_user.name) + self.assertEqual(opening_entry_1.user, test_user.name) + + pos_profile2 = make_pos_profile(name="_Test POS Profile 2") + with self.assertRaises(frappe.ValidationError): + create_opening_entry(pos_profile2, test_user.name) + + def test_cancel_pos_opening_entry_without_invoices(self): + test_user, pos_profile = self.init_user_and_profile() + opening_entry = create_opening_entry(pos_profile, test_user.name, get_obj=True) + + opening_entry.cancel() + self.assertEqual(opening_entry.status, "Cancelled") + self.assertNotEqual(opening_entry.docstatus, 1) + + def test_cancel_pos_opening_entry_with_invoice(self): + test_user, pos_profile = self.init_user_and_profile() + opening_entry = create_opening_entry(pos_profile, test_user.name, get_obj=True) + + pos_inv1 = create_pos_invoice(pos_profile=pos_profile.name, rate=100, do_not_save=1) + pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) + pos_inv1.save() + pos_inv1.submit() + + self.assertRaises(frappe.ValidationError, opening_entry.cancel) +>>>>>>> e35e8968f0 (fix: prevent pos opening entry creation for disabled pos profile) def create_opening_entry(pos_profile, user): From 650d2f74ba2246b77ff17a6abb059c7efb2c5fd5 Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Wed, 12 Nov 2025 11:33:56 +0530 Subject: [PATCH 07/30] chore: resolve conflict --- .../test_pos_opening_entry.py | 99 ------------------- 1 file changed, 99 deletions(-) diff --git a/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py b/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py index e0276f8bb62..64c658ab151 100644 --- a/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py +++ b/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py @@ -6,107 +6,8 @@ import unittest import frappe -<<<<<<< HEAD class TestPOSOpeningEntry(unittest.TestCase): pass -======= -class TestPOSOpeningEntry(IntegrationTestCase): - @classmethod - def setUpClass(cls): - frappe.db.sql("delete from `tabPOS Opening Entry`") - cls.enterClassContext(cls.change_settings("POS Settings", {"invoice_type": "POS Invoice"})) - - @classmethod - def tearDownClass(cls): - frappe.db.sql("delete from `tabPOS Opening Entry`") - - def setUp(self): - # Make stock available for POS Sales - frappe.db.sql("delete from `tabPOS Opening Entry`") - make_stock_entry(target="_Test Warehouse - _TC", qty=2, basic_rate=100) - from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile - - self.init_user_and_profile = init_user_and_profile - - def tearDown(self): - frappe.set_user("Administrator") - frappe.db.sql("delete from `tabPOS Profile`") - - def test_pos_opening_entry(self): - test_user, pos_profile = self.init_user_and_profile() - opening_entry = create_opening_entry(pos_profile, test_user.name) - - self.assertEqual(opening_entry.status, "Open") - self.assertNotEqual(opening_entry.docstatus, 0) - - def test_pos_opening_entry_on_disabled_pos(self): - test_user, pos_profile = self.init_user_and_profile(disabled=1) - - with self.assertRaises(frappe.ValidationError): - create_opening_entry(pos_profile, test_user.name) - - def test_multiple_pos_opening_entries_for_same_pos_profile(self): - test_user, pos_profile = self.init_user_and_profile() - opening_entry = create_opening_entry(pos_profile, test_user.name) - - self.assertEqual(opening_entry.status, "Open") - with self.assertRaises(frappe.ValidationError): - create_opening_entry(pos_profile, test_user.name) - - def test_multiple_pos_opening_entry_for_multiple_pos_profiles(self): - test_user, pos_profile = self.init_user_and_profile() - opening_entry_1 = create_opening_entry(pos_profile, test_user.name) - - self.assertEqual(opening_entry_1.status, "Open") - self.assertEqual(opening_entry_1.user, test_user.name) - - cashier_user = create_user("test_cashier@example.com", "Accounts Manager", "Sales Manager") - frappe.set_user(cashier_user.name) - - pos_profile2 = make_pos_profile(name="_Test POS Profile 2") - opening_entry_2 = create_opening_entry(pos_profile2, cashier_user.name) - - self.assertEqual(opening_entry_2.status, "Open") - self.assertEqual(opening_entry_2.user, cashier_user.name) - - def test_multiple_pos_opening_entry_for_same_pos_profile_by_multiple_user(self): - test_user, pos_profile = self.init_user_and_profile() - cashier_user = create_user("test_cashier@example.com", "Accounts Manager", "Sales Manager") - - opening_entry = create_opening_entry(pos_profile, test_user.name) - self.assertEqual(opening_entry.status, "Open") - - with self.assertRaises(frappe.ValidationError): - create_opening_entry(pos_profile, cashier_user.name) - - def test_user_assignment_to_multiple_pos_profile(self): - test_user, pos_profile = self.init_user_and_profile() - opening_entry_1 = create_opening_entry(pos_profile, test_user.name) - self.assertEqual(opening_entry_1.user, test_user.name) - - pos_profile2 = make_pos_profile(name="_Test POS Profile 2") - with self.assertRaises(frappe.ValidationError): - create_opening_entry(pos_profile2, test_user.name) - - def test_cancel_pos_opening_entry_without_invoices(self): - test_user, pos_profile = self.init_user_and_profile() - opening_entry = create_opening_entry(pos_profile, test_user.name, get_obj=True) - - opening_entry.cancel() - self.assertEqual(opening_entry.status, "Cancelled") - self.assertNotEqual(opening_entry.docstatus, 1) - - def test_cancel_pos_opening_entry_with_invoice(self): - test_user, pos_profile = self.init_user_and_profile() - opening_entry = create_opening_entry(pos_profile, test_user.name, get_obj=True) - - pos_inv1 = create_pos_invoice(pos_profile=pos_profile.name, rate=100, do_not_save=1) - pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) - pos_inv1.save() - pos_inv1.submit() - - self.assertRaises(frappe.ValidationError, opening_entry.cancel) ->>>>>>> e35e8968f0 (fix: prevent pos opening entry creation for disabled pos profile) def create_opening_entry(pos_profile, user): From af19b81343bfbeea87bb6215a711577a71399cfe Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Wed, 12 Nov 2025 11:35:02 +0530 Subject: [PATCH 08/30] chore: resolve conflict --- erpnext/accounts/doctype/pos_profile/test_pos_profile.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py index 427748627ed..5efe3bd5380 100644 --- a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py @@ -4,11 +4,7 @@ import unittest import frappe -<<<<<<< HEAD -======= -from frappe.tests import IntegrationTestCase from frappe.utils import cint ->>>>>>> 69016a284f (test: added test to validate disabled pos profile) from erpnext.accounts.doctype.pos_profile.pos_profile import ( get_child_nodes, From d5160c4c86749a7e37775953f97c29ef11412b88 Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Wed, 12 Nov 2025 12:26:26 +0530 Subject: [PATCH 09/30] test: delete outdated pos opening entry --- erpnext/accounts/doctype/pos_profile/test_pos_profile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py index 5efe3bd5380..20ee2faadcf 100644 --- a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py @@ -70,6 +70,7 @@ class TestPOSProfile(unittest.TestCase): ) test_user, pos_profile = init_user_and_profile() + frappe.db.delete("POS Opening Entry", {"pos_profile": pos_profile.name}) if pos_profile: opening_entry = create_opening_entry(pos_profile, test_user.name) From b4b8459f2c8942ae3a1cfdaccfeb46c3fbbb3c6e Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 12 Nov 2025 12:19:49 +0530 Subject: [PATCH 10/30] fix: current qty in stock reco (cherry picked from commit 58315bc963d7fea5ac0a2aba417cef5696472a92) --- .../stock_reconciliation.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 9140599e7ba..937b130cf7b 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -16,7 +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 -from erpnext.stock.utils import get_incoming_rate, get_stock_balance +from erpnext.stock.utils import get_combine_datetime, get_incoming_rate, get_stock_balance class OpeningEntryAccountError(frappe.ValidationError): @@ -1061,6 +1061,7 @@ class StockReconciliation(StockController): self.posting_date, self.posting_time, self.name, + sle_creation, ) precesion = row.precision("current_qty") @@ -1222,8 +1223,11 @@ class StockReconciliation(StockController): return current_qty -def get_batch_qty_for_stock_reco(item_code, warehouse, batch_no, posting_date, posting_time, voucher_no): +def get_batch_qty_for_stock_reco( + item_code, warehouse, batch_no, posting_date, posting_time, voucher_no, sle_creation +): ledger = frappe.qb.DocType("Stock Ledger Entry") + posting_datetime = get_combine_datetime(posting_date, posting_time) query = ( frappe.qb.from_(ledger) @@ -1236,12 +1240,11 @@ def get_batch_qty_for_stock_reco(item_code, warehouse, batch_no, posting_date, p & (ledger.docstatus == 1) & (ledger.is_cancelled == 0) & (ledger.batch_no == batch_no) - & (ledger.posting_date <= posting_date) - & ( - CombineDatetime(ledger.posting_date, ledger.posting_time) - <= CombineDatetime(posting_date, posting_time) - ) & (ledger.voucher_no != voucher_no) + & ( + (ledger.posting_datetime < posting_datetime) + | ((ledger.posting_datetime == posting_datetime) & (ledger.creation < sle_creation)) + ) ) .groupby(ledger.batch_no) ) From 2b7abfb34b6a3446d9efb990bc88fc59b59e323f Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Wed, 12 Nov 2025 16:37:52 +0530 Subject: [PATCH 11/30] fix: handle NoneType object error for product bundle --- .../serial_and_batch_bundle/serial_and_batch_bundle.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 8d216171641..d9071c406a2 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 @@ -1390,13 +1390,15 @@ class SerialandBatchBundle(Document): so_name, so_detail_no = frappe.db.get_value( "Delivery Note Item", self.voucher_detail_no, ["against_sales_order", "so_detail"] - ) + ) or [None, None] if so_name and so_detail_no: sre_names = get_sre_against_so_for_dn(so_name, so_detail_no) return sre_names + return None + @frappe.whitelist() def download_blank_csv_template(content): From 3b636d5db78feada2af765cbb97065dc16824e86 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:37:50 +0530 Subject: [PATCH 12/30] fix: first and last name in supplier quick entry (backport #50510) (#50514) Co-authored-by: ljain112 Co-authored-by: Diptanil Saha --- .../js/utils/contact_address_quick_entry.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/erpnext/public/js/utils/contact_address_quick_entry.js b/erpnext/public/js/utils/contact_address_quick_entry.js index 129b713c6f3..262d1c689a9 100644 --- a/erpnext/public/js/utils/contact_address_quick_entry.js +++ b/erpnext/public/js/utils/contact_address_quick_entry.js @@ -38,15 +38,27 @@ frappe.ui.form.ContactAddressQuickEntryForm = class ContactAddressQuickEntryForm label: __("Primary Contact Details"), collapsible: 1, }, + { + label: __("First Name"), + fieldname: "map_to_first_name", + fieldtype: "Data", + depends_on: "eval:doc.customer_type=='Company' || doc.supplier_type=='Company'", + }, + { + label: __("Last Name"), + fieldname: "map_to_last_name", + fieldtype: "Data", + depends_on: "eval:doc.customer_type=='Company' || doc.supplier_type=='Company'", + }, + { + fieldtype: "Column Break", + }, { label: __("Email Id"), fieldname: "email_address", fieldtype: "Data", options: "Email", }, - { - fieldtype: "Column Break", - }, { label: __("Mobile Number"), fieldname: "mobile_number", From 33962ac995381cf9da2b3b62946133489820d0b5 Mon Sep 17 00:00:00 2001 From: PUGAZHENDHI V <126157273+PugazhendhiVelu@users.noreply.github.com> Date: Thu, 13 Nov 2025 21:25:44 +0530 Subject: [PATCH 13/30] fix(period closing voucher): add title to error log (#50498) (cherry picked from commit 4f720b3969ac22efe24af70a99b8449814b89e4c) --- .../period_closing_voucher.py | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index 210dbc5bbf5..b416e5b8394 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -475,8 +475,15 @@ def process_gl_and_closing_entries(doc): frappe.db.set_value(doc.doctype, doc.name, "gle_processing_status", "Completed") except Exception as e: frappe.db.rollback() - frappe.log_error(e) - frappe.db.set_value(doc.doctype, doc.name, "gle_processing_status", "Failed") + frappe.log_error(title=_("Period Closing Voucher {0} GL Entry Processing Failed").format(doc.name)) + frappe.db.set_value( + doc.doctype, + doc.name, + { + "error_message": str(e), + "gle_processing_status": "Failed", + }, + ) def process_cancellation(voucher_type, voucher_no): @@ -488,8 +495,17 @@ def process_cancellation(voucher_type, voucher_no): frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Completed") except Exception as e: frappe.db.rollback() - frappe.log_error(e) - frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Failed") + frappe.log_error( + title=_("Period Closing Voucher {0} GL Entry Cancellation Failed").format(voucher_no) + ) + frappe.db.set_value( + voucher_type, + voucher_no, + { + "error_message": str(e), + "gle_processing_status": "Failed", + }, + ) def delete_closing_entries(voucher_no): From 8c98f1692a4c36a99df90cc375ed96736716e7f4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 13 Nov 2025 21:58:34 +0530 Subject: [PATCH 14/30] feat: Add first and last name fields to quick entry customer creation (backport #46281) (#50522) Co-authored-by: Nabin Hait Co-authored-by: maasanto <73234812+maasanto@users.noreply.github.com> Co-authored-by: Diptanil Saha --- .../js/utils/contact_address_quick_entry.js | 4 +++- .../selling/doctype/customer/customer.json | 21 +++++++++++++++++-- erpnext/selling/doctype/customer/customer.py | 8 ++++++- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/erpnext/public/js/utils/contact_address_quick_entry.js b/erpnext/public/js/utils/contact_address_quick_entry.js index 262d1c689a9..a13a6d38d5b 100644 --- a/erpnext/public/js/utils/contact_address_quick_entry.js +++ b/erpnext/public/js/utils/contact_address_quick_entry.js @@ -16,11 +16,13 @@ frappe.ui.form.ContactAddressQuickEntryForm = class ContactAddressQuickEntryForm insert() { /** * Using alias fieldnames because the doctype definition define "email_id" and "mobile_no" as readonly fields. - * Therefor, resulting in the fields being "hidden". + * This results in the fields being "hidden". */ const map_field_names = { email_address: "email_id", mobile_number: "mobile_no", + map_to_first_name: "first_name", + map_to_last_name: "last_name", }; Object.entries(map_field_names).forEach(([fieldname, new_fieldname]) => { diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index dec09e512fe..a04b9c414cf 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -56,6 +56,8 @@ "customer_primary_contact", "mobile_no", "email_id", + "first_name", + "last_name", "tax_tab", "taxation_section", "tax_id", @@ -581,6 +583,20 @@ "no_copy": 1, "options": "Prospect", "print_hide": 1 + }, + { + "fetch_from": "customer_primary_contact.first_name", + "fieldname": "first_name", + "fieldtype": "Read Only", + "hidden": 1, + "label": "First Name" + }, + { + "fetch_from": "customer_primary_contact.last_name", + "fieldname": "last_name", + "fieldtype": "Read Only", + "hidden": 1, + "label": "Last Name" } ], "icon": "fa fa-user", @@ -594,7 +610,7 @@ "link_fieldname": "party" } ], - "modified": "2024-06-17 03:24:59.612974", + "modified": "2025-03-05 10:01:47.885574", "modified_by": "Administrator", "module": "Selling", "name": "Customer", @@ -672,6 +688,7 @@ } ], "quick_entry": 1, + "row_format": "Dynamic", "search_fields": "customer_group,territory, mobile_no,primary_address", "show_name_in_global_search": 1, "sort_field": "modified", @@ -679,4 +696,4 @@ "states": [], "title_field": "customer_name", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 0e9c1f3e790..9bebfa1e086 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -61,12 +61,14 @@ class Customer(TransactionBase): disabled: DF.Check dn_required: DF.Check email_id: DF.ReadOnly | None + first_name: DF.ReadOnly | None gender: DF.Link | None image: DF.AttachImage | None industry: DF.Link | None is_frozen: DF.Check is_internal_customer: DF.Check language: DF.Link | None + last_name: DF.ReadOnly | None lead_name: DF.Link | None loyalty_program: DF.Link | None loyalty_program_tier: DF.Data | None @@ -248,7 +250,7 @@ class Customer(TransactionBase): def create_primary_contact(self): if not self.customer_primary_contact and not self.lead_name: - if self.mobile_no or self.email_id: + if self.mobile_no or self.email_id or self.first_name or self.last_name: contact = make_contact(self) self.db_set("customer_primary_contact", contact.name) self.db_set("mobile_no", self.mobile_no) @@ -736,6 +738,10 @@ def make_contact(args, is_primary_contact=1): contact.add_email(args.get("email_id"), is_primary=True) if args.get("mobile_no"): contact.add_phone(args.get("mobile_no"), is_primary_mobile_no=True) + if args.get("first_name"): + contact.first_name = args.get("first_name") + if args.get("last_name"): + contact.last_name = args.get("last_name") if flags := args.get("flags"): contact.insert(ignore_permissions=flags.get("ignore_permissions")) From a5ec0e4f5079a2a9919ee245124f1f53ec20bcaf Mon Sep 17 00:00:00 2001 From: Pugazhendhi Velu Date: Sun, 16 Nov 2025 06:07:55 +0000 Subject: [PATCH 15/30] fix(stock-entry): prevent default warehouse from overriding parent warehouse (cherry picked from commit 8b38578914d6bb2cd5de9912063a6cb5d6488dc4) --- erpnext/stock/doctype/stock_entry/stock_entry.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index a41a529ff2c..61c270bd170 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -1338,8 +1338,8 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle this.frm.script_manager.copy_from_first_row("items", row, ["expense_account", "cost_center"]); } - if (!row.s_warehouse) row.s_warehouse = this.frm.doc.from_warehouse; - if (!row.t_warehouse) row.t_warehouse = this.frm.doc.to_warehouse; + if (this.frm.doc.from_warehouse) row.s_warehouse = this.frm.doc.from_warehouse; + if (this.frm.doc.to_warehouse) row.t_warehouse = this.frm.doc.to_warehouse; if (cint(frappe.user_defaults?.use_serial_batch_fields)) { frappe.model.set_value(row.doctype, row.name, "use_serial_batch_fields", 1); From 0a0177cb9e1af611c0d596eca97de38afa8d595c Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Sat, 15 Nov 2025 16:04:17 +0530 Subject: [PATCH 16/30] fix: construct batch_nos and serial_nos to avoid NoneType error --- erpnext/stock/stock_ledger.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 47cb41852c2..f4956e1b600 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -2240,9 +2240,11 @@ def validate_reserved_stock(kwargs): kwargs.ignore_voucher_nos = [kwargs.voucher_no] if kwargs.serial_no: + kwargs.serial_nos = kwargs.serial_no.split("\n") validate_reserved_serial_nos(kwargs) elif kwargs.batch_no: + kwargs.batch_nos = [kwargs.batch_no] validate_reserved_batch_nos(kwargs) elif kwargs.serial_and_batch_bundle: From 2a5c9b469c61995a6850dee32f5572541dc87223 Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Mon, 17 Nov 2025 13:53:55 +0530 Subject: [PATCH 17/30] fix: enable allow_negative_stock settings --- .../doctype/stock_reconciliation/test_stock_reconciliation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index afbfcdd6062..61c32d09467 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -45,6 +45,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): def test_reco_for_moving_average(self): self._test_reco_sle_gle("Moving Average") + @change_settings("Stock Settings", {"allow_negative_stock": 1}) def _test_reco_sle_gle(self, valuation_method): item_code = self.make_item(properties={"valuation_method": valuation_method}).name From 623a0a932ec88d613256024b5fdade1a261df86d Mon Sep 17 00:00:00 2001 From: Pugazhendhi Velu Date: Mon, 17 Nov 2025 13:09:38 +0000 Subject: [PATCH 18/30] fix: add cancelled option in status field --- erpnext/assets/doctype/asset/asset.json | 5 ++--- erpnext/assets/doctype/asset/asset.py | 1 + erpnext/assets/doctype/asset_repair/asset_repair.json | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 487f67669ff..2e15dd77b62 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -371,7 +371,6 @@ "label": "Other Details" }, { - "allow_on_submit": 1, "default": "Draft", "fieldname": "status", "fieldtype": "Select", @@ -379,7 +378,7 @@ "in_standard_filter": 1, "label": "Status", "no_copy": 1, - "options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nWork In Progress", + "options": "Draft\nSubmitted\nCancelled\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nWork In Progress", "read_only": 1 }, { @@ -597,7 +596,7 @@ "link_fieldname": "target_asset" } ], - "modified": "2025-10-23 22:43:33.634452", + "modified": "2025-11-17 18:01:51.417942", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 9f8895b6e7e..6ef5a8643aa 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -103,6 +103,7 @@ class Asset(AccountsController): status: DF.Literal[ "Draft", "Submitted", + "Cancelled", "Partially Depreciated", "Fully Depreciated", "Sold", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 16f3da9b988..9b6d878378b 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -139,7 +139,7 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Asset", - "link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",null]]]", + "link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",\"Cancelled\",null]]]", "options": "Asset", "reqd": 1 }, @@ -250,7 +250,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-07-29 15:14:34.044564", + "modified": "2025-11-17 18:35:54.575265", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From 57282999ad70b543aefb0e372b85675eb4b91bfb Mon Sep 17 00:00:00 2001 From: ljain112 Date: Wed, 12 Nov 2025 18:41:20 +0530 Subject: [PATCH 19/30] fix: back calcalute total amount from rate and tax_amount in tax withholding details report (cherry picked from commit d3751d9bb4eaddbdf4282f2f4adc45ee382a1ad0) --- .../tax_withholding_details.py | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py index 81dba55d609..c96510aa497 100644 --- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py @@ -72,17 +72,28 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_ tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category") rate = get_tax_withholding_rates(tax_rate_map.get(tax_withholding_category, []), posting_date) - if net_total_map.get((voucher_type, name)): + + values = net_total_map.get((voucher_type, name)) + + if values: if voucher_type == "Journal Entry" and tax_amount and rate: # back calcalute total amount from rate and tax_amount - base_total = min(tax_amount / (rate / 100), net_total_map.get((voucher_type, name))[0]) + base_total = min(tax_amount / (rate / 100), values[0]) total_amount = grand_total = base_total - elif voucher_type == "Purchase Invoice": - total_amount, grand_total, base_total, bill_no, bill_date = net_total_map.get( - (voucher_type, name) - ) + else: - total_amount, grand_total, base_total = net_total_map.get((voucher_type, name)) + if tax_amount and rate: + # back calcalute total amount from rate and tax_amount + total_amount = (tax_amount * 100) / rate + else: + total_amount = values[0] + + grand_total = values[1] + base_total = values[2] + + if voucher_type == "Purchase Invoice": + bill_no = values[3] + bill_date = values[4] else: total_amount += entry.credit From c150e5795e9fbcf4132ac1c64f9d11c9c7a0a0c2 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Wed, 12 Nov 2025 18:59:45 +0530 Subject: [PATCH 20/30] fix: improve precision in tax amount calculations in tax withholding details report (cherry picked from commit 7c5f5405cc17ff1a56e55777af29507dbc512168) --- .../tax_withholding_details/tax_withholding_details.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py index c96510aa497..169de9cd801 100644 --- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py @@ -4,7 +4,9 @@ import frappe from frappe import _ -from frappe.utils import getdate +from frappe.utils import flt, getdate + +from erpnext.accounts.utils import get_currency_precision def execute(filters=None): @@ -43,6 +45,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_ party_map = get_party_pan_map(filters.get("party_type")) tax_rate_map = get_tax_rate_map(filters) gle_map = get_gle_map(tds_docs) + precision = get_currency_precision() out = [] entries = {} @@ -78,13 +81,13 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_ if values: if voucher_type == "Journal Entry" and tax_amount and rate: # back calcalute total amount from rate and tax_amount - base_total = min(tax_amount / (rate / 100), values[0]) + base_total = min(flt(tax_amount / (rate / 100), precision=precision), values[0]) total_amount = grand_total = base_total else: if tax_amount and rate: # back calcalute total amount from rate and tax_amount - total_amount = (tax_amount * 100) / rate + total_amount = flt((tax_amount * 100) / rate, precision=precision) else: total_amount = values[0] From 4df80c5b53c678a2740eaca74fe14cd89ae7fbbb Mon Sep 17 00:00:00 2001 From: ljain112 Date: Wed, 12 Nov 2025 19:28:38 +0530 Subject: [PATCH 21/30] chore: typo in comment (cherry picked from commit e056c0327dc2043bb3baf43c6e73706f29bc31d4) --- .../report/tax_withholding_details/tax_withholding_details.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py index 169de9cd801..78fc08614f2 100644 --- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py @@ -80,13 +80,13 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_ if values: if voucher_type == "Journal Entry" and tax_amount and rate: - # back calcalute total amount from rate and tax_amount + # back calculate total amount from rate and tax_amount base_total = min(flt(tax_amount / (rate / 100), precision=precision), values[0]) total_amount = grand_total = base_total else: if tax_amount and rate: - # back calcalute total amount from rate and tax_amount + # back calculate total amount from rate and tax_amount total_amount = flt((tax_amount * 100) / rate, precision=precision) else: total_amount = values[0] From f0eac4703726e00f151231b9694f40a4b8048520 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 10 Nov 2025 17:34:19 +0530 Subject: [PATCH 22/30] fix: add doctype parameter to lead details for correct company details (cherry picked from commit 0b91338771ad8201c95b3766babd070a9162c01f) --- erpnext/controllers/selling_controller.py | 1 + erpnext/crm/doctype/lead/lead.py | 4 ++-- erpnext/selling/doctype/quotation/quotation.js | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index e4e2ee29d9b..ff8dec4db70 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -137,6 +137,7 @@ class SellingController(StockController): lead, posting_date=self.get("transaction_date") or self.get("posting_date"), company=self.company, + doctype=self.doctype, ) ) diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index cf9a7f02f9d..f0f492191fb 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -432,7 +432,7 @@ def _set_missing_values(source, target): @frappe.whitelist() -def get_lead_details(lead, posting_date=None, company=None): +def get_lead_details(lead, posting_date=None, company=None, doctype=None): if not lead: return {} @@ -454,7 +454,7 @@ def get_lead_details(lead, posting_date=None, company=None): } ) - set_address_details(out, lead, "Lead", company=company) + set_address_details(out, lead, "Lead", doctype=doctype, company=company) taxes_and_charges = set_taxes( None, diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index da8476d6b1f..f0061c016bd 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -252,6 +252,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. lead: this.frm.doc.party_name, posting_date: this.frm.doc.transaction_date, company: this.frm.doc.company, + doctype: this.frm.doc.doctype, }, callback: function (r) { if (r.message) { From 2db91ee67efe95f97528277636245d124eb275cc Mon Sep 17 00:00:00 2001 From: Pugazhendhi Velu Date: Mon, 17 Nov 2025 14:06:26 +0000 Subject: [PATCH 23/30] fix(asset repair): validate pi status --- erpnext/assets/doctype/asset_repair/asset_repair.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 358945edf87..77e191873a5 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -60,6 +60,17 @@ class AssetRepair(AccountsController): if self.get("stock_items"): self.set_stock_items_cost() self.calculate_total_repair_cost() + self.validate_purchase_invoice_status() + + def validate_purchase_invoice_status(self): + if self.purchase_invoice: + docstatus = frappe.db.get_value("Purchase Invoice", self.purchase_invoice, "docstatus") + if docstatus == 0: + frappe.throw( + _("{0} is still in Draft. Please submit it before saving the Asset Repair.").format( + get_link_to_form("Purchase Invoice", self.purchase_invoice) + ) + ) def validate_asset(self): if self.asset_doc.status in ("Sold", "Fully Depreciated", "Scrapped"): From ac40b596651fa1e206a7f2a481d7af7dbdc1add8 Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Fri, 14 Nov 2025 16:46:10 +0530 Subject: [PATCH 24/30] fix(financial reports): set fiscal year associated with the default company --- erpnext/public/js/utils.js | 41 ++++++++++++++++++++++++-------------- erpnext/startup/boot.py | 8 ++++++++ 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 2b6d41c2d54..5942b34158d 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -420,25 +420,36 @@ $.extend(erpnext.utils, { if (!frappe.boot.setup_complete) { return; } + const today = frappe.datetime.get_today(); if (!date) { - date = frappe.datetime.get_today(); + date = today; } let fiscal_year = ""; - frappe.call({ - method: "erpnext.accounts.utils.get_fiscal_year", - args: { - date: date, - boolean: boolean, - }, - async: false, - callback: function (r) { - if (r.message) { - if (with_dates) fiscal_year = r.message; - else fiscal_year = r.message[0]; - } - }, - }); + if ( + frappe.boot.current_fiscal_year && + date >= frappe.boot.current_fiscal_year[1] && + date <= frappe.boot.current_fiscal_year[2] + ) { + if (with_dates) fiscal_year = frappe.boot.current_fiscal_year; + else fiscal_year = frappe.boot.current_fiscal_year[0]; + } else if (today != date) { + frappe.call({ + method: "erpnext.accounts.utils.get_fiscal_year", + type: "GET", // make it cacheable + args: { + date: date, + boolean: boolean, + }, + async: false, + callback: function (r) { + if (r.message) { + if (with_dates) fiscal_year = r.message; + else fiscal_year = r.message[0]; + } + }, + }); + } return fiscal_year; }, diff --git a/erpnext/startup/boot.py b/erpnext/startup/boot.py index 03cbd99f5c4..b17b6b49b6b 100644 --- a/erpnext/startup/boot.py +++ b/erpnext/startup/boot.py @@ -3,8 +3,11 @@ import frappe +from frappe.defaults import get_user_default from frappe.utils import cint +from erpnext.accounts.utils import get_fiscal_years + def boot_session(bootinfo): """boot session - send website info if guest""" @@ -53,6 +56,11 @@ def boot_session(bootinfo): ) party_account_types = frappe.db.sql(""" select name, ifnull(account_type, '') from `tabParty Type`""") + fiscal_year = get_fiscal_years( + frappe.utils.nowdate(), company=get_user_default("company"), boolean=True + ) + if fiscal_year: + bootinfo.current_fiscal_year = fiscal_year[0] bootinfo.party_account_types = frappe._dict(party_account_types) bootinfo.sysdefaults.demo_company = frappe.db.get_single_value("Global Defaults", "demo_company") From 81a16286a144bd45d2f1c1fcb78d57c4c9c07869 Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin-114@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:37:26 +0530 Subject: [PATCH 25/30] fix: unintended backported depends_on expression (#50529) Co-authored-by: Kavin <78342682+kavin0411@users.noreply.github.com> --- erpnext/manufacturing/doctype/bom/bom.json | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index 175c5818c43..04774d4f2a8 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -591,7 +591,6 @@ }, { "default": "0", - "depends_on": "eval:doc.track_semi_finished_goods === 0", "fieldname": "fg_based_operating_cost", "fieldtype": "Check", "label": "Finished Goods based Operating Cost" From 799119ad3ee843e911cd1aa0366b252855984f2a Mon Sep 17 00:00:00 2001 From: Logesh Periyasamy Date: Tue, 18 Nov 2025 16:45:15 +0530 Subject: [PATCH 26/30] fix(general_ledger): add translation for accounting dimension (cherry picked from commit 113ff17c718770185db29c229ad3598f77624399) --- erpnext/accounts/report/general_ledger/general_ledger.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 7c6c809b939..100dcd46c85 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -566,6 +566,13 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot else: update_value_in_dict(consolidated_gle, key, gle) + if filters.get("include_dimensions"): + dimensions = [*accounting_dimensions, "cost_center", "project"] + + for dimension in dimensions: + if val := gle.get(dimension): + gle[dimension] = _(val) + for value in consolidated_gle.values(): update_value_in_dict(totals, "total", value) update_value_in_dict(totals, "closing", value) From a2c82b4dc3c09da9923e92c7d62e462b262b24d7 Mon Sep 17 00:00:00 2001 From: Navin-S-R Date: Fri, 14 Nov 2025 14:05:47 +0530 Subject: [PATCH 27/30] fix: use dynamic account type to get average ratio balance (cherry picked from commit 9118f08e7b571edb7bc81780806018fdfc85182e) --- erpnext/accounts/report/financial_ratios/financial_ratios.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/financial_ratios/financial_ratios.py b/erpnext/accounts/report/financial_ratios/financial_ratios.py index 5084b6c1651..a726043d1d8 100644 --- a/erpnext/accounts/report/financial_ratios/financial_ratios.py +++ b/erpnext/accounts/report/financial_ratios/financial_ratios.py @@ -199,7 +199,7 @@ def add_turnover_ratios(data, years, period_list, filters, total_asset, net_sale avg_data = {} for d in ["Receivable", "Payable", "Stock"]: - avg_data[frappe.scrub(d)] = avg_ratio_balance("Receivable", period_list, precision, filters) + avg_data[frappe.scrub(d)] = avg_ratio_balance(d, period_list, precision, filters) avg_debtors, avg_creditors, avg_stock = ( avg_data.get("receivable"), From 627b34a120f8086d33967b5062bfa1b9dd85dbe5 Mon Sep 17 00:00:00 2001 From: Navin-S-R Date: Fri, 14 Nov 2025 14:12:29 +0530 Subject: [PATCH 28/30] fix: correct profit after tax calculation by reducing expenses from income (cherry picked from commit f420371a7e77f383878afd1fffae15646dab60b0) --- erpnext/accounts/report/financial_ratios/financial_ratios.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/financial_ratios/financial_ratios.py b/erpnext/accounts/report/financial_ratios/financial_ratios.py index a726043d1d8..48047c81944 100644 --- a/erpnext/accounts/report/financial_ratios/financial_ratios.py +++ b/erpnext/accounts/report/financial_ratios/financial_ratios.py @@ -174,7 +174,7 @@ def add_solvency_ratios( return_on_equity_ratio = {"ratio": _("Return on Equity Ratio")} for year in years: - profit_after_tax = flt(total_income.get(year)) + flt(total_expense.get(year)) + profit_after_tax = flt(total_income.get(year)) - flt(total_expense.get(year)) share_holder_fund = flt(total_asset.get(year)) - flt(total_liability.get(year)) debt_equity_ratio[year] = calculate_ratio(total_liability.get(year), share_holder_fund, precision) From 2d6640ac61fc8fdf51649b54f9b55dbce879b2eb Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 18 Nov 2025 17:05:47 +0530 Subject: [PATCH 29/30] fix: add condition for allow negative stock in pos (backport #50369) (#50600) Co-authored-by: Logesh Periyasamy fix: add condition for allow negative stock in pos (#50369) --- .../doctype/pos_invoice/pos_invoice.py | 21 ++++++++++++------- .../page/point_of_sale/point_of_sale.py | 4 ++-- .../page/point_of_sale/pos_controller.js | 4 ++++ 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 0196b2b6189..80d75bfddc3 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -189,6 +189,9 @@ class POSInvoice(SalesInvoice): super().__init__(*args, **kwargs) def validate(self): + if not self.customer: + frappe.throw(_("Please select Customer first")) + if not cint(self.is_pos): frappe.throw( _("POS Invoice should have the field {0} checked.").format(frappe.bold(_("Include Payment"))) @@ -345,14 +348,14 @@ class POSInvoice(SalesInvoice): ): return - from erpnext.stock.stock_ledger import is_negative_stock_allowed - for d in self.get("items"): if not d.serial_and_batch_bundle: - if is_negative_stock_allowed(item_code=d.item_code): - return + available_stock, is_stock_item, is_negative_stock_allowed = get_stock_availability( + d.item_code, d.warehouse + ) - available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse) + if is_negative_stock_allowed: + continue item_code, warehouse, _qty = ( frappe.bold(d.item_code), @@ -760,20 +763,22 @@ class POSInvoice(SalesInvoice): @frappe.whitelist() def get_stock_availability(item_code, warehouse): + from erpnext.stock.stock_ledger import is_negative_stock_allowed + if frappe.db.get_value("Item", item_code, "is_stock_item"): is_stock_item = True bin_qty = get_bin_qty(item_code, warehouse) pos_sales_qty = get_pos_reserved_qty(item_code, warehouse) - return bin_qty - pos_sales_qty, is_stock_item + return bin_qty - pos_sales_qty, is_stock_item, is_negative_stock_allowed(item_code=item_code) else: is_stock_item = True if frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0}): - return get_bundle_availability(item_code, warehouse), is_stock_item + return get_bundle_availability(item_code, warehouse), is_stock_item, False else: is_stock_item = False # Is a service item or non_stock item - return 0, is_stock_item + return 0, is_stock_item, False def get_bundle_availability(bundle_item_code, warehouse): diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index d8de762dcd3..df488dafeb0 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -55,7 +55,7 @@ def search_by_term(search_term, warehouse, price_list): } ) - item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse) + item_stock_qty, is_stock_item, is_negative_stock_allowed = get_stock_availability(item_code, warehouse) item_stock_qty = item_stock_qty // item.get("conversion_factor", 1) item.update({"actual_qty": item_stock_qty}) @@ -198,7 +198,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te current_date = frappe.utils.today() for item in items_data: - item.actual_qty, _ = get_stock_availability(item.item_code, warehouse) + item.actual_qty, _, is_negative_stock_allowed = get_stock_availability(item.item_code, warehouse) item_prices = frappe.get_all( "Item Price", diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 5e3218a67a2..6506ba047d0 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -759,12 +759,16 @@ erpnext.PointOfSale.Controller = class { const resp = (await this.get_available_stock(item_row.item_code, warehouse)).message; const available_qty = resp[0]; const is_stock_item = resp[1]; + const is_negative_stock_allowed = resp[2]; frappe.dom.unfreeze(); const bold_uom = item_row.stock_uom.bold(); const bold_item_code = item_row.item_code.bold(); const bold_warehouse = warehouse.bold(); const bold_available_qty = available_qty.toString().bold(); + + if (is_negative_stock_allowed) return; + if (!(available_qty > 0)) { if (is_stock_item) { frappe.model.clear_doc(item_row.doctype, item_row.name); From f8294f17543283faf248ba878732d607a60f3f34 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 18 Nov 2025 17:05:58 +0530 Subject: [PATCH 30/30] feat(Company): allow setting default sales contact, fetch into sales transaction (backport #50159) (#50599) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Co-authored-by: Diptanil Saha --- erpnext/controllers/selling_controller.py | 8 ++++++++ erpnext/public/js/utils/sales_common.js | 22 ++++++++++++++++++++++ erpnext/setup/doctype/company/company.js | 7 +++++++ erpnext/setup/doctype/company/company.json | 9 ++++++++- erpnext/setup/doctype/company/company.py | 1 + 5 files changed, 46 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index ff8dec4db70..d74ea55450a 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -95,6 +95,7 @@ class SellingController(StockController): # set contact and address details for customer, if they are not mentioned self.set_missing_lead_customer_details(for_validate=for_validate) self.set_price_list_and_item_details(for_validate=for_validate) + self.set_company_contact_person() def set_missing_lead_customer_details(self, for_validate=False): customer, lead = None, None @@ -150,6 +151,13 @@ class SellingController(StockController): self.set_price_list_currency("Selling") self.set_missing_item_details(for_validate=for_validate) + def set_company_contact_person(self): + """Set the Company's Default Sales Contact as Company Contact Person.""" + if self.company and self.meta.has_field("company_contact_person") and not self.company_contact_person: + self.company_contact_person = frappe.get_cached_value( + "Company", self.company, "default_sales_contact" + ) + def remove_shipping_charge(self): if self.shipping_rule: shipping_rule = frappe.get_doc("Shipping Rule", self.shipping_rule) diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js index bf4ef8666cd..7e2271dc38f 100644 --- a/erpnext/public/js/utils/sales_common.js +++ b/erpnext/public/js/utils/sales_common.js @@ -115,6 +115,10 @@ erpnext.sales_common = { company() { super.company(); this.set_default_company_address(); + if (!this.is_onload) { + // we don't want to override the mapped contact from prevdoc + this.set_default_company_contact_person(); + } } set_default_company_address() { @@ -139,6 +143,24 @@ erpnext.sales_common = { } } + set_default_company_contact_person() { + if (!frappe.meta.has_field(this.frm.doc.doctype, "company_contact_person")) { + return; + } + + if (this.frm.doc.company) { + frappe.db + .get_value("Company", this.frm.doc.company, "default_sales_contact") + .then((r) => { + if (r.message?.default_sales_contact) { + this.frm.set_value("company_contact_person", r.message.default_sales_contact); + } else { + this.frm.set_value("company_contact_person", ""); + } + }); + } + } + customer() { var me = this; erpnext.utils.get_party_details(this.frm, null, null, function () { diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index f736769b915..032ec707330 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -37,6 +37,13 @@ frappe.ui.form.on("Company", { return { filters: { selling: 1 } }; }); + frm.set_query("default_sales_contact", function (doc) { + return { + query: "frappe.contacts.doctype.contact.contact.contact_query", + filters: { link_doctype: "Company", link_name: doc.name }, + }; + }); + frm.set_query("default_buying_terms", function () { return { filters: { buying: 1 } }; }); diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 8c4f85ff19f..fc6533a1e89 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -103,6 +103,7 @@ "total_monthly_sales", "column_break_goals", "default_selling_terms", + "default_sales_contact", "default_warehouse_for_sales_return", "credit_limit", "transactions_annual_history", @@ -851,6 +852,12 @@ "fieldtype": "Select", "label": "Reconciliation Takes Effect On", "options": "Advance Payment Date\nOldest Of Invoice Or Advance\nReconciliation Date" + }, + { + "fieldname": "default_sales_contact", + "fieldtype": "Link", + "label": "Default Sales Contact", + "options": "Contact" } ], "icon": "fa fa-building", @@ -858,7 +865,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2025-08-25 18:34:03.602046", + "modified": "2025-11-16 16:51:27.624096", "modified_by": "Administrator", "module": "Setup", "name": "Company", diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index f9978099ed5..50fe87ef654 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -66,6 +66,7 @@ class Company(NestedSet): default_payable_account: DF.Link | None default_provisional_account: DF.Link | None default_receivable_account: DF.Link | None + default_sales_contact: DF.Link | None default_selling_terms: DF.Link | None default_warehouse_for_sales_return: DF.Link | None depreciation_cost_center: DF.Link | None