From b33bec4dadb69cc910180311e59f5f0661a4afb8 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 16 Jun 2025 17:03:17 +0530 Subject: [PATCH 01/45] fix: use set_query on sales_order link field in work order (cherry picked from commit 6def182e1a813dda519e7a432c1ac67af08a04c9) --- .../doctype/work_order/work_order.js | 29 +++++++------------ .../doctype/work_order/work_order.py | 10 ++++--- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 67233dc206c..04a87b00260 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -101,6 +101,17 @@ frappe.ui.form.on("Work Order", { }; }); + frm.set_query("sales_order", function () { + if (frm.doc.production_item) { + return { + query: "erpnext.manufacturing.doctype.work_order.work_order.query_sales_order", + filters: { + production_item: frm.doc.production_item, + }, + }; + } + }); + // formatter for work order operation frm.set_indicator_formatter("operation", function (doc) { return frm.doc.qty == doc.completed_qty ? "green" : "orange"; @@ -481,7 +492,6 @@ frappe.ui.form.on("Work Order", { callback: function (r) { if (r.message) { frm.set_value("sales_order", ""); - frm.trigger("set_sales_order"); erpnext.in_production_item_onchange = true; $.each( @@ -543,23 +553,6 @@ frappe.ui.form.on("Work Order", { frm.toggle_reqd("transfer_material_against", frm.doc.operations && frm.doc.operations.length > 0); }, - set_sales_order: function (frm) { - if (frm.doc.production_item) { - frappe.call({ - method: "erpnext.manufacturing.doctype.work_order.work_order.query_sales_order", - args: { production_item: frm.doc.production_item }, - callback: function (r) { - frm.set_query("sales_order", function () { - erpnext.in_production_item_onchange = true; - return { - filters: [["Sales Order", "name", "in", r.message]], - }; - }); - }, - }); - } - }, - additional_operating_cost: function (frm) { erpnext.work_order.calculate_cost(frm.doc); erpnext.work_order.calculate_total_cost(frm); diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 16d551c2cf9..796e9461bee 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1515,17 +1515,19 @@ def stop_unstop(work_order, status): @frappe.whitelist() -def query_sales_order(production_item: str) -> list[str]: +@frappe.validate_and_sanitize_search_inputs +def query_sales_order(doctype, txt, searchfield, start, page_len, filters) -> list[str]: return frappe.get_list( "Sales Order", + fields=["name"], filters=[ ["Sales Order", "docstatus", "=", 1], ], or_filters=[ - ["Sales Order Item", "item_code", "=", production_item], - ["Packed Item", "item_code", "=", production_item], + ["Sales Order Item", "item_code", "=", filters.get("production_item")], + ["Packed Item", "item_code", "=", filters.get("production_item")], ], - pluck="name", + as_list=True, distinct=True, ) From 89376ddf8d8bb49225b3b7513623430450481307 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 10 Jun 2025 12:26:51 +0530 Subject: [PATCH 02/45] fix: stock reconciliation validation for serial and batch (cherry picked from commit 69d54d2e0f831fb07074e43e9893d0049c45144e) --- .../serial_and_batch_bundle.py | 95 +++++++++++++++---- erpnext/stock/serial_batch_bundle.py | 61 ++++++++++-- erpnext/stock/stock_ledger.py | 3 + 3 files changed, 131 insertions(+), 28 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 b83f3c58df9..11697409820 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 @@ -225,7 +225,14 @@ class SerialandBatchBundle(Document): if not (self.has_serial_no and self.type_of_transaction == "Outward"): return - serial_nos = [d.serial_no for d in self.entries if d.serial_no] + if self.voucher_type == "Stock Reconciliation": + serial_nos = self.get_serial_nos_for_validate() + else: + serial_nos = [d.serial_no for d in self.entries if d.serial_no] + + if not serial_nos: + return + kwargs = { "item_code": self.item_code, "warehouse": self.warehouse, @@ -235,6 +242,15 @@ class SerialandBatchBundle(Document): if self.voucher_type == "POS Invoice": kwargs["ignore_voucher_nos"] = [self.voucher_no] + if self.voucher_type == "Stock Reconciliation": + kwargs.update( + { + "voucher_no": self.voucher_no, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + } + ) + available_serial_nos = get_available_serial_nos(frappe._dict(kwargs)) serial_no_warehouse = {} @@ -665,14 +681,17 @@ class SerialandBatchBundle(Document): ): self.throw_error_message(f"The {self.voucher_type} # {self.voucher_no} should be submit first.") - def check_future_entries_exists(self): + def check_future_entries_exists(self, is_cancelled=False): if self.flags and self.flags.via_landed_cost_voucher: return if not self.has_serial_no: return - serial_nos = [d.serial_no for d in self.entries if d.serial_no] + if self.voucher_type == "Stock Reconciliation": + serial_nos = self.get_serial_nos_for_validate(is_cancelled=is_cancelled) + else: + serial_nos = [d.serial_no for d in self.entries if d.serial_no] if not serial_nos: return @@ -720,6 +739,36 @@ class SerialandBatchBundle(Document): frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransactionError) + def get_serial_nos_for_validate(self, is_cancelled=False): + serial_nos = [d.serial_no for d in self.entries if d.serial_no] + skip_serial_nos = self.get_skip_serial_nos_for_stock_reconciliation(is_cancelled=is_cancelled) + serial_nos = list(set(sorted(serial_nos)) - set(sorted(skip_serial_nos))) + + return serial_nos + + def get_skip_serial_nos_for_stock_reconciliation(self, is_cancelled=False): + data = get_stock_reco_details(self.voucher_detail_no) + if not data: + return [] + + if data.current_serial_no: + current_serial_nos = set(parse_serial_nos(data.current_serial_no)) + serial_nos = set(parse_serial_nos(data.serial_no)) if data.serial_no else set([]) + return list(serial_nos.intersection(current_serial_nos)) + elif data.current_serial_and_batch_bundle: + current_serial_nos = set(get_serial_nos_from_bundle(data.current_serial_and_batch_bundle)) + if is_cancelled: + return current_serial_nos + + serial_nos = ( + set(get_serial_nos_from_bundle(data.serial_and_batch_bundle)) + if data.serial_and_batch_bundle + else set([]) + ) + return list(serial_nos.intersection(current_serial_nos)) + + return [] + def reset_qty(self, row, qty_field=None): qty_field = self.get_qty_field(row, qty_field=qty_field) qty = abs(flt(row.get(qty_field), self.precision("total_qty"))) @@ -923,8 +972,6 @@ class SerialandBatchBundle(Document): ) def validate_serial_and_batch_no_for_returned(self): - from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos - if not self.returned_against: return @@ -949,7 +996,7 @@ class SerialandBatchBundle(Document): if d.serial_and_batch_bundle: serial_nos = get_serial_nos_from_bundle(d.serial_and_batch_bundle) else: - serial_nos = get_serial_nos(d.serial_no) + serial_nos = parse_serial_nos(d.serial_no) elif self.has_batch_no: if d.serial_and_batch_bundle: @@ -1111,7 +1158,7 @@ class SerialandBatchBundle(Document): ).run() def validate_serial_and_batch_inventory(self): - self.check_future_entries_exists() + self.check_future_entries_exists(is_cancelled=True) self.validate_batch_inventory() def validate_batch_inventory(self): @@ -1418,13 +1465,6 @@ def make_batch_nos(item_code, batch_nos): frappe.msgprint(_("Batch Nos are created successfully"), alert=True) -def parse_serial_nos(data): - if isinstance(data, list): - return data - - return [s.strip() for s in cstr(data).strip().replace(",", "\n").split("\n") if s.strip()] - - @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False): @@ -1811,8 +1851,6 @@ def get_non_expired_batches(batches): def get_serial_nos_based_on_posting_date(kwargs, ignore_serial_nos): - from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos - serial_nos = set() data = get_stock_ledgers_for_serial_nos(kwargs) @@ -1826,13 +1864,14 @@ def get_serial_nos_based_on_posting_date(kwargs, ignore_serial_nos): serial_nos.difference_update(sns) elif d.serial_no: - sns = get_serial_nos(d.serial_no) + sns = parse_serial_nos(d.serial_no) if d.actual_qty > 0: serial_nos.update(sns) else: serial_nos.difference_update(sns) serial_nos = list(serial_nos) + for serial_no in ignore_serial_nos: if serial_no in serial_nos: serial_nos.remove(serial_no) @@ -1880,7 +1919,6 @@ def get_reserved_serial_nos(kwargs) -> list: def get_reserved_serial_nos_for_pos(kwargs): from erpnext.controllers.sales_and_purchase_return import get_returned_serial_nos - from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos ignore_serial_nos = [] pos_invoices = frappe.get_all( @@ -1916,7 +1954,7 @@ def get_reserved_serial_nos_for_pos(kwargs): returned_serial_nos = [] for pos_invoice in pos_invoices: if pos_invoice.serial_no: - ignore_serial_nos.extend(get_serial_nos(pos_invoice.serial_no)) + ignore_serial_nos.extend(parse_serial_nos(pos_invoice.serial_no)) if pos_invoice.is_return: continue @@ -2449,12 +2487,14 @@ def get_stock_ledgers_for_serial_nos(kwargs): query = ( frappe.qb.from_(stock_ledger_entry) .select( + stock_ledger_entry.posting_datetime, stock_ledger_entry.actual_qty, stock_ledger_entry.serial_no, stock_ledger_entry.serial_and_batch_bundle, ) .where(stock_ledger_entry.is_cancelled == 0) .orderby(stock_ledger_entry.posting_datetime) + .orderby(stock_ledger_entry.creation) ) if kwargs.get("posting_date"): @@ -2583,3 +2623,20 @@ def make_batch_no(batch_no, item_code): @frappe.whitelist() def is_duplicate_serial_no(bundle_id, serial_no): return frappe.db.exists("Serial and Batch Entry", {"parent": bundle_id, "serial_no": serial_no}) + + +def parse_serial_nos(serial_no): + if isinstance(serial_no, list): + return serial_no + + return [s.strip() for s in cstr(serial_no).strip().replace(",", "\n").split("\n") if s.strip()] + + +@frappe.request_cache +def get_stock_reco_details(voucher_detail_no): + return frappe.db.get_value( + "Stock Reconciliation Item", + voucher_detail_no, + ["current_serial_no", "serial_no", "serial_and_batch_bundle", "current_serial_and_batch_bundle"], + as_dict=True, + ) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index bff764228f5..99ad11326ea 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -341,16 +341,26 @@ class SerialBatchBundle: if not self.sle.serial_and_batch_bundle and self.sle.serial_no: serial_nos = get_parsed_serial_nos(self.sle.serial_no) - warehouse = self.warehouse if self.sle.actual_qty > 0 else None - if not serial_nos: return + if self.sle.voucher_type == "Stock Reconciliation" and self.sle.actual_qty > 0: + self.update_serial_no_status_for_stock_reco(serial_nos) + return + + self.update_serial_no_status_warehouse(self.sle, serial_nos) + + def update_serial_no_status_warehouse(self, sle, serial_nos): + warehouse = self.warehouse if sle.actual_qty > 0 else None + + if isinstance(serial_nos, str): + serial_nos = [serial_nos] + status = "Inactive" - if self.sle.actual_qty < 0: + if sle.actual_qty < 0: status = "Delivered" - if self.sle.voucher_type == "Stock Entry": - purpose = frappe.get_cached_value("Stock Entry", self.sle.voucher_no, "purpose") + if sle.voucher_type == "Stock Entry": + purpose = frappe.get_cached_value("Stock Entry", sle.voucher_no, "purpose") if purpose in [ "Manufacture", "Material Issue", @@ -369,17 +379,17 @@ class SerialBatchBundle: "Active" if warehouse else status - if (sn_table.purchase_document_no != self.sle.voucher_no and self.sle.is_cancelled != 1) + if (sn_table.purchase_document_no != sle.voucher_no and sle.is_cancelled != 1) else "Inactive", ) - .set(sn_table.company, self.sle.company) + .set(sn_table.company, sle.company) .where(sn_table.name.isin(serial_nos)) ) if status == "Delivered": - warranty_period = frappe.get_cached_value("Item", self.sle.item_code, "warranty_period") + warranty_period = frappe.get_cached_value("Item", sle.item_code, "warranty_period") if warranty_period: - warranty_expiry_date = add_days(self.sle.posting_date, cint(warranty_period)) + warranty_expiry_date = add_days(sle.posting_date, cint(warranty_period)) query = query.set(sn_table.warranty_expiry_date, warranty_expiry_date) query = query.set(sn_table.warranty_period, warranty_period) else: @@ -388,6 +398,39 @@ class SerialBatchBundle: query.run() + def update_serial_no_status_for_stock_reco(self, serial_nos): + for serial_no in serial_nos: + sle_doctype = frappe.qb.DocType("Stock Ledger Entry") + sn_table = frappe.qb.DocType("Serial and Batch Entry") + + query = ( + frappe.qb.from_(sle_doctype) + .inner_join(sn_table) + .on(sle_doctype.serial_and_batch_bundle == sn_table.parent) + .select( + sle_doctype.warehouse, + sle_doctype.actual_qty, + sle_doctype.voucher_type, + sle_doctype.voucher_no, + sle_doctype.is_cancelled, + sle_doctype.item_code, + sle_doctype.posting_date, + sle_doctype.company, + ) + .where( + (sn_table.serial_no == serial_no) + & (sle_doctype.is_cancelled == 0) + & (sn_table.docstatus == 1) + ) + .orderby(sle_doctype.posting_datetime, order=Order.desc) + .orderby(sle_doctype.creation, order=Order.desc) + .limit(1) + ) + + sle = query.run(as_dict=1) + if sle: + self.update_serial_no_status_warehouse(sle[0], serial_no) + def set_batch_no_in_serial_nos(self): entries = frappe.get_all( "Serial and Batch Entry", diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 2485f704b76..7f5984fb0b7 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1926,6 +1926,9 @@ def get_stock_reco_qty_shift(args): stock_reco_qty_shift = 0 if args.get("is_cancelled"): if args.get("previous_qty_after_transaction"): + if args.get("serial_and_batch_bundle"): + return args.get("previous_qty_after_transaction") + # get qty (balance) that was set at submission last_balance = args.get("previous_qty_after_transaction") stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance) From d3daeaf475d95f567dc6ed0a434f678b32260654 Mon Sep 17 00:00:00 2001 From: Bhavan23 Date: Wed, 18 Jun 2025 10:54:24 +0530 Subject: [PATCH 03/45] fix(asset-invoice): handle asset invoice cancellation --- .../doctype/sales_invoice/sales_invoice.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 5c6ed7a1320..b12f51e8a4b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1356,7 +1356,9 @@ class SalesInvoice(SellingController): if item.is_fixed_asset: asset = self.get_asset(item) - if self.is_return: + if (self.docstatus == 2 and not self.is_return) or ( + self.docstatus == 1 and self.is_return + ): fixed_asset_gl_entries = get_gl_entries_on_asset_regain( asset, item.base_net_amount, @@ -1369,8 +1371,10 @@ class SalesInvoice(SellingController): add_asset_activity(asset.name, _("Asset returned")) if asset.calculate_depreciation: - posting_date = frappe.db.get_value( - "Sales Invoice", self.return_against, "posting_date" + posting_date = ( + frappe.db.get_value("Sales Invoice", self.return_against, "posting_date") + if self.is_return + else self.posting_date ) reverse_depreciation_entry_made_after_disposal(asset, posting_date) notes = _( @@ -1467,8 +1471,10 @@ class SalesInvoice(SellingController): return self._enable_discount_accounting def set_asset_status(self, asset): - if self.is_return: + if self.is_return and not self.docstatus == 2: asset.set_status() + elif self.is_return and self.docstatus == 2: + asset.set_status("Sold") else: asset.set_status("Sold" if self.docstatus == 1 else None) From f85b08d2f549a735f230b4b8c58289a14e6889c4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 17:26:55 +0530 Subject: [PATCH 04/45] fix: setup wizard load chart of accounts and fiscal year on change of country (backport #48125) (#48128) fix: setup wizard load chart of accounts and fiscal year on change of country (#48125) (cherry picked from commit 14f0569a391fce6a7f89b8981e0f51cd9a8b5538) Co-authored-by: Diptanil Saha --- erpnext/public/js/setup_wizard.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js index 2ced08ed5d2..20fdbcd9b13 100644 --- a/erpnext/public/js/setup_wizard.js +++ b/erpnext/public/js/setup_wizard.js @@ -55,9 +55,13 @@ erpnext.setup.slides_settings = [ onload: function (slide) { this.bind_events(slide); - this.load_chart_of_accounts(slide); - this.set_fy_dates(slide); }, + + before_show: function () { + this.load_chart_of_accounts(this); + this.set_fy_dates(this); + }, + validate: function () { if (!this.validate_fy_dates()) { return false; From 764c71d3e16cf7c1b22e81351e63b439d5ef8385 Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Wed, 18 Jun 2025 18:13:54 +0530 Subject: [PATCH 05/45] fix: modify query to fetch valid return qty --- .../stock/doctype/delivery_note/delivery_note.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 2d44293d66f..7b9ddb7a129 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -793,13 +793,15 @@ def get_returned_qty_map(delivery_note): """returns a map: {so_detail: returned_qty}""" returned_qty_map = frappe._dict( frappe.db.sql( - """select dn_item.dn_detail, abs(dn_item.qty) as qty - from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn - where dn.name = dn_item.parent - and dn.docstatus = 1 - and dn.is_return = 1 - and dn.return_against = %s - """, + """select dn_item.dn_detail, sum(abs(dn_item.qty)) as qty + from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn + where dn.name = dn_item.parent + and dn.docstatus = 1 + and dn.is_return = 1 + and dn.return_against = %s + and dn_item.qty <= 0 + group by dn_item.item_code + """, delivery_note, ) ) From 4576fcd96f48c61f1c5f02f89a1e1376ca039c0a Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Wed, 18 Jun 2025 18:14:43 +0530 Subject: [PATCH 06/45] test: add test for validating sales invoice qty after return --- .../delivery_note/test_delivery_note.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index f34ebe4cd87..c39c5427ff0 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -6,6 +6,7 @@ import json from collections import defaultdict import frappe +from frappe.tests import change_settings from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, cstr, flt, getdate, nowdate, nowtime, today @@ -1022,6 +1023,30 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(dn2.per_billed, 100) self.assertEqual(dn2.status, "Completed") + @change_settings("Accounts Settings", {"delete_linked_ledger_entries": True}) + def test_sales_invoice_qty_after_return(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + dn = create_delivery_note(qty=10) + + dnr1 = make_sales_return(dn.name) + dnr1.get("items")[0].qty = -3 + dnr1.save().submit() + + dnr2 = make_sales_return(dn.name) + dnr2.get("items")[0].qty = -2 + dnr2.save().submit() + + si = make_sales_invoice(dn.name) + si.save().submit() + + self.assertEqual(si.get("items")[0].qty, 5) + + si.reload().cancel().delete() + dnr1.reload().cancel().delete() + dnr2.reload().cancel().delete() + dn.reload().cancel().delete() + def test_dn_billing_status_case3(self): # SO -> DN1 -> SI and SO -> SI and SO -> DN2 from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note From 9146bd95a455e5b9ee63090c4406c3cac1bfd8c0 Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Wed, 18 Jun 2025 18:41:01 +0530 Subject: [PATCH 07/45] test: update import for change_settings --- erpnext/stock/doctype/delivery_note/test_delivery_note.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index c39c5427ff0..00ce64b614c 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -6,8 +6,7 @@ import json from collections import defaultdict import frappe -from frappe.tests import change_settings -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, cstr, flt, getdate, nowdate, nowtime, today from erpnext.accounts.doctype.account.test_account import get_inventory_account From ea3015a4509e22e522c9555b8364ec93fb2a7c06 Mon Sep 17 00:00:00 2001 From: Bhavan23 Date: Wed, 18 Jun 2025 10:54:42 +0530 Subject: [PATCH 08/45] test(sales-invoice): add test case for asset invoice cancellation --- .../sales_invoice/test_sales_invoice.py | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 3ad08885b94..a8b6a7adeaa 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3135,6 +3135,65 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) self.assertEqual(schedule.journal_entry, schedule.journal_entry) + def test_depreciation_on_cancel_invoice(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + create_asset_data() + + asset = create_asset( + item_code="Macbook Pro", + purchase_date="2020-01-01", + available_for_use_date="2023-01-01", + depreciation_start_date="2023-04-01", + calculate_depreciation=1, + submit=1, + ) + post_depreciation_entries() + + si = create_sales_invoice( + item_code="Macbook Pro", asset=asset.name, qty=1, rate=10000, posting_date=getdate("2025-05-01") + ) + return_si = make_return_doc("Sales Invoice", si.name) + return_si.posting_date = getdate("2025-05-01") + return_si.submit() + return_si.reload() + return_si.cancel() + + asset.load_from_db() + + # Check if the asset schedule is updated while cancel the return invoice + expected_values = [ + ["2023-04-01", 4986.30, 4986.30, True], + ["2024-04-01", 20000.0, 24986.30, True], + ["2025-04-01", 20000.0, 44986.30, True], + ["2025-05-01", 1643.84, 46630.14, True], + ] + for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")): + self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) + self.assertEqual(expected_values[i][1], schedule.depreciation_amount) + self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) + self.assertEqual(schedule.journal_entry, schedule.journal_entry) + + si.reload() + si.cancel() + asset.load_from_db() + + # Check if the asset schedule is updated while cancel the sales invoice + expected_values = [ + ["2023-04-01", 4986.30, 4986.30, True], + ["2024-04-01", 20000.0, 24986.30, True], + ["2025-04-01", 20000.0, 44986.30, True], + ["2026-04-01", 20000.0, 64986.30, False], + ["2027-04-01", 20000.0, 84986.30, False], + ["2028-01-01", 15013.70, 100000.0, False], + ] + + for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")): + self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) + self.assertEqual(expected_values[i][1], schedule.depreciation_amount) + self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) + self.assertEqual(schedule.journal_entry, schedule.journal_entry) + def test_sales_invoice_against_supplier(self): from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import ( make_customer, From bf61014fe58a52d462f09fc3f4f69d7344cfedf4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 19 Jun 2025 13:01:51 +0530 Subject: [PATCH 09/45] test: purchase invoice provisional accounting entry (backport #48112) (#48134) test: purchase invoice provisional accounting entry (#48112) * test: fixed purchase invoice provisional accounting entry * test: added tests for multi currency (cherry picked from commit 80f992c87f5d9dd9fbff3e680beb2b776da85670) Co-authored-by: Diptanil Saha --- .../purchase_invoice/purchase_invoice.py | 8 +- .../purchase_invoice/test_purchase_invoice.py | 129 ++++++++++++++---- 2 files changed, 110 insertions(+), 27 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 805e76ad64c..fdfb50a6f42 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1226,7 +1226,7 @@ class PurchaseInvoice(BuyingController): pr_items = frappe.get_all( "Purchase Receipt Item", filters={"parent": ("in", linked_purchase_receipts)}, - fields=["name", "provisional_expense_account", "qty", "base_rate"], + fields=["name", "provisional_expense_account", "qty", "base_rate", "rate"], ) default_provisional_account = self.get_company_default("default_provisional_account") provisional_accounts = set( @@ -1254,6 +1254,7 @@ class PurchaseInvoice(BuyingController): "provisional_account": item.provisional_expense_account or default_provisional_account, "qty": item.qty, "base_rate": item.base_rate, + "rate": item.rate, "has_provisional_entry": item.name in rows_with_provisional_entries, } @@ -1270,7 +1271,10 @@ class PurchaseInvoice(BuyingController): self.posting_date, pr_item.get("provisional_account"), reverse=1, - item_amount=(min(item.qty, pr_item.get("qty")) * pr_item.get("base_rate")), + item_amount=( + (min(item.qty, pr_item.get("qty")) * pr_item.get("rate")) + * purchase_receipt_doc.get("conversion_rate") + ), ) def update_gross_purchase_amount_for_linked_assets(self, item): diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 99eb1de2cf8..f0446b35e5d 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1663,7 +1663,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): pi = create_purchase_invoice_from_receipt(pr.name) pi.set_posting_time = 1 - pi.posting_date = add_days(pr.posting_date, -1) + pi.posting_date = add_days(pr.posting_date, 1) pi.items[0].expense_account = "Cost of Goods Sold - _TC" pi.save() pi.submit() @@ -1672,30 +1672,38 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): # Check GLE for Purchase Invoice expected_gle = [ - ["Cost of Goods Sold - _TC", 250, 0, add_days(pr.posting_date, -1)], - ["Creditors - _TC", 0, 250, add_days(pr.posting_date, -1)], + ["Cost of Goods Sold - _TC", 250, 0, add_days(pr.posting_date, 1)], + ["Creditors - _TC", 0, 250, add_days(pr.posting_date, 1)], ] check_gl_entries(self, pi.name, expected_gle, pi.posting_date) expected_gle_for_purchase_receipt = [ - ["Provision Account - _TC", 250, 0, pr.posting_date], - ["_Test Account Cost for Goods Sold - _TC", 0, 250, pr.posting_date], - ["Provision Account - _TC", 0, 250, pi.posting_date], - ["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date], + ["_Test Account Cost for Goods Sold - _TC", 250, 0, pr.posting_date], + ["Provision Account - _TC", 0, 250, pr.posting_date], + ["_Test Account Cost for Goods Sold - _TC", 0, 250, pi.posting_date], + ["Provision Account - _TC", 250, 0, pi.posting_date], ] - check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date) + check_gl_entries( + self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date, voucher_type="Purchase Receipt" + ) # Cancel purchase invoice to check reverse provisional entry cancellation pi.cancel() expected_gle_for_purchase_receipt_post_pi_cancel = [ - ["Provision Account - _TC", 0, 250, pi.posting_date], ["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date], + ["Provision Account - _TC", 0, 250, pi.posting_date], ] - check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date) + check_gl_entries( + self, + pr.name, + expected_gle_for_purchase_receipt_post_pi_cancel, + pi.posting_date, + voucher_type="Purchase Receipt", + ) toggle_provisional_accounting_setting() @@ -1716,7 +1724,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): # Overbill PR: rate = 2000, qty = 10 pi = create_purchase_invoice_from_receipt(pr.name) pi.set_posting_time = 1 - pi.posting_date = add_days(pr.posting_date, -1) + pi.posting_date = add_days(pr.posting_date, 1) pi.items[0].qty = 10 pi.items[0].rate = 2000 pi.items[0].expense_account = "Cost of Goods Sold - _TC" @@ -1724,30 +1732,38 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): pi.submit() expected_gle = [ - ["Cost of Goods Sold - _TC", 20000, 0, add_days(pr.posting_date, -1)], - ["Creditors - _TC", 0, 20000, add_days(pr.posting_date, -1)], + ["Cost of Goods Sold - _TC", 20000, 0, add_days(pr.posting_date, 1)], + ["Creditors - _TC", 0, 20000, add_days(pr.posting_date, 1)], ] check_gl_entries(self, pi.name, expected_gle, pi.posting_date) expected_gle_for_purchase_receipt = [ - ["Provision Account - _TC", 5000, 0, pr.posting_date], - ["_Test Account Cost for Goods Sold - _TC", 0, 5000, pr.posting_date], - ["Provision Account - _TC", 0, 5000, pi.posting_date], - ["_Test Account Cost for Goods Sold - _TC", 5000, 0, pi.posting_date], + ["_Test Account Cost for Goods Sold - _TC", 5000, 0, pr.posting_date], + ["Provision Account - _TC", 0, 5000, pr.posting_date], + ["_Test Account Cost for Goods Sold - _TC", 0, 5000, pi.posting_date], + ["Provision Account - _TC", 5000, 0, pi.posting_date], ] - check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date) + check_gl_entries( + self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date, voucher_type="Purchase Receipt" + ) # Cancel purchase invoice to check reverse provisional entry cancellation pi.cancel() expected_gle_for_purchase_receipt_post_pi_cancel = [ - ["Provision Account - _TC", 0, 5000, pi.posting_date], ["_Test Account Cost for Goods Sold - _TC", 5000, 0, pi.posting_date], + ["Provision Account - _TC", 0, 5000, pi.posting_date], ] - check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date) + check_gl_entries( + self, + pr.name, + expected_gle_for_purchase_receipt_post_pi_cancel, + pi.posting_date, + voucher_type="Purchase Receipt", + ) toggle_provisional_accounting_setting() @@ -1780,13 +1796,76 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): check_gl_entries(self, pi.name, expected_gle, pi.posting_date) expected_gle_for_purchase_receipt = [ - ["Provision Account - _TC", 5000, 0, pr.posting_date], - ["_Test Account Cost for Goods Sold - _TC", 0, 5000, pr.posting_date], - ["Provision Account - _TC", 0, 1000, pi.posting_date], - ["_Test Account Cost for Goods Sold - _TC", 1000, 0, pi.posting_date], + ["_Test Account Cost for Goods Sold - _TC", 5000, 0, pr.posting_date], + ["Provision Account - _TC", 0, 5000, pr.posting_date], + ["_Test Account Cost for Goods Sold - _TC", 0, 1000, pi.posting_date], + ["Provision Account - _TC", 1000, 0, pi.posting_date], ] - check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date) + check_gl_entries( + self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date, voucher_type="Purchase Receipt" + ) + + toggle_provisional_accounting_setting() + + def test_provisional_accounting_entry_multi_currency(self): + setup_provisional_accounting() + + pr = make_purchase_receipt( + item_code="_Test Non Stock Item", + posting_date=add_days(nowdate(), -2), + qty=1000, + rate=111.11, + currency="USD", + do_not_save=1, + supplier="_Test Supplier USD", + ) + pr.conversion_rate = 0.014783000 + pr.save() + pr.submit() + + pi = create_purchase_invoice_from_receipt(pr.name) + pi.set_posting_time = 1 + pi.posting_date = add_days(pr.posting_date, 1) + pi.items[0].expense_account = "Cost of Goods Sold - _TC" + pi.save() + pi.submit() + + self.assertEqual(pr.items[0].provisional_expense_account, "Provision Account - _TC") + + # Check GLE for Purchase Invoice + expected_gle = [ + ["_Test Payable USD - _TC", 0, 1642.54, add_days(pr.posting_date, 1)], + ["Cost of Goods Sold - _TC", 1642.54, 0, add_days(pr.posting_date, 1)], + ] + + check_gl_entries(self, pi.name, expected_gle, pi.posting_date) + + expected_gle_for_purchase_receipt = [ + ["_Test Account Cost for Goods Sold - _TC", 1642.54, 0, pr.posting_date], + ["Provision Account - _TC", 0, 1642.54, pr.posting_date], + ["_Test Account Cost for Goods Sold - _TC", 0, 1642.54, pi.posting_date], + ["Provision Account - _TC", 1642.54, 0, pi.posting_date], + ] + check_gl_entries( + self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date, voucher_type="Purchase Receipt" + ) + + # Cancel purchase invoice to check reverse provisional entry cancellation + pi.cancel() + + expected_gle_for_purchase_receipt_post_pi_cancel = [ + ["_Test Account Cost for Goods Sold - _TC", 1642.54, 0, pi.posting_date], + ["Provision Account - _TC", 0, 1642.54, pi.posting_date], + ] + + check_gl_entries( + self, + pr.name, + expected_gle_for_purchase_receipt_post_pi_cancel, + pi.posting_date, + voucher_type="Purchase Receipt", + ) toggle_provisional_accounting_setting() From 6896216276043ba088e3f5146cd09ecae962e326 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 18 Jun 2025 18:03:04 +0530 Subject: [PATCH 10/45] fix: permission issue during reposting (cherry picked from commit dcc9fc2fece755e79dc8d3a162c296ca9e397041) --- .../stock/doctype/stock_reconciliation/stock_reconciliation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index d3b17cd3ad8..d025de845e1 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -1105,6 +1105,7 @@ class StockReconciliation(StockController): 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) From 7c2bf026ef443ddcf58a7bf2d6c66d8fb4e9a545 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 19 Jun 2025 17:54:08 +0530 Subject: [PATCH 11/45] fix: coa reset root_type on unchecking is_group on new_node (backport #48156) (#48160) fix: coa reset root_type on unchecking is_group on new_node (#48156) (cherry picked from commit 2f8893439f1e11b987cdfd648db84662a56fb558) Co-authored-by: Diptanil Saha --- erpnext/accounts/doctype/account/account_tree.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js index a61fcb4f530..0641612c615 100644 --- a/erpnext/accounts/doctype/account/account_tree.js +++ b/erpnext/accounts/doctype/account/account_tree.js @@ -138,6 +138,11 @@ frappe.treeview_settings["Account"] = { description: __( "Further accounts can be made under Groups, but entries can be made against non-Groups" ), + onchange: function () { + if (!this.value) { + this.layout.set_value("root_type", ""); + } + }, }, { fieldtype: "Select", From 4e700059379f4c9444faaf3133b3d9978e0c40f7 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 19 Jun 2025 19:33:26 +0530 Subject: [PATCH 12/45] fix: target inventory dimension for stock entry (cherry picked from commit d65cb56d662b998198bfc26df60253c528439362) --- erpnext/controllers/stock_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 3654ad5e6c5..d6930312b77 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -909,7 +909,7 @@ class StockController(AccountsController): fieldname = f"{dimension.source_fieldname}" sl_dict[dimension.target_fieldname] = row.get(fieldname) - return + continue sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname) else: From f0ddf1b223d0a7c8aafe77996764caf5e3690db7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 20 Jun 2025 11:52:53 +0530 Subject: [PATCH 13/45] fix: naming series field in bank transaction (backport #48121) (#48149) fix: naming series field in bank transaction (#48121) * fix: naming series field in bank transaction * fix: default naming_series (cherry picked from commit c94764ab527804e87fa8e4a79728d746f75c0d57) Co-authored-by: Diptanil Saha --- .../doctype/bank_transaction/bank_transaction.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json index 0328d51b892..62033126060 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json @@ -45,7 +45,6 @@ "default": "ACC-BTN-.YYYY.-", "fieldname": "naming_series", "fieldtype": "Select", - "hidden": 1, "label": "Series", "no_copy": 1, "options": "ACC-BTN-.YYYY.-", @@ -236,9 +235,10 @@ "fieldtype": "Column Break" } ], + "grid_page_length": 50, "is_submittable": 1, "links": [], - "modified": "2023-11-18 18:32:47.203694", + "modified": "2025-06-18 17:24:57.044666", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Transaction", @@ -287,9 +287,10 @@ "write": 1 } ], + "row_format": "Dynamic", "sort_field": "date", "sort_order": "DESC", "states": [], "title_field": "bank_account", "track_changes": 1 -} \ No newline at end of file +} From 1223f5551f83b66e438060874abaf2e237071a23 Mon Sep 17 00:00:00 2001 From: khushi Date: Thu, 19 Jun 2025 14:03:42 +0530 Subject: [PATCH 14/45] fix: contract autoname (cherry picked from commit e13e2bffe264180c2effc3c2cf36c4f0b15c384f) --- erpnext/crm/doctype/contract/contract.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/erpnext/crm/doctype/contract/contract.py b/erpnext/crm/doctype/contract/contract.py index 64f89552062..559c89a9629 100644 --- a/erpnext/crm/doctype/contract/contract.py +++ b/erpnext/crm/doctype/contract/contract.py @@ -6,6 +6,7 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.utils import getdate, nowdate +from frappe.model.naming import append_number_if_name_exists class Contract(Document): @@ -52,12 +53,7 @@ class Contract(Document): if self.contract_template: name += f" - {self.contract_template} Agreement" - # If identical, append contract name with the next number in the iteration - if frappe.db.exists("Contract", name): - count = len(frappe.get_all("Contract", filters={"name": ["like", f"%{name}%"]})) - name = f"{name} - {count}" - - self.name = _(name) + self.name = append_number_if_name_exists("Contract", name) def validate(self): self.set_missing_values() From 194e15fe6e48a529cf863bf234ba28c3daac5c64 Mon Sep 17 00:00:00 2001 From: khushi Date: Thu, 19 Jun 2025 14:06:04 +0530 Subject: [PATCH 15/45] chore: test contract autoname (cherry picked from commit b55d1e61c7b9149fa180d46f0f4db536056feaf6) --- erpnext/crm/doctype/contract/test_contract.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/erpnext/crm/doctype/contract/test_contract.py b/erpnext/crm/doctype/contract/test_contract.py index 13901683de8..81ccf3300fe 100644 --- a/erpnext/crm/doctype/contract/test_contract.py +++ b/erpnext/crm/doctype/contract/test_contract.py @@ -12,6 +12,19 @@ class TestContract(unittest.TestCase): frappe.db.sql("delete from `tabContract`") self.contract_doc = get_contract() + def test_autoname_appends_suffix_for_duplicates(self): + contract_1 = self.contract_doc + contract_1.insert() + self.assertEqual(contract_1.name, "_Test Customer") + + contract_2 = get_contract() + contract_2.insert() + self.assertEqual(contract_2.name, "_Test Customer-1") + + contract_3 = get_contract() + contract_3.insert() + self.assertEqual(contract_3.name, "_Test Customer-2") + def test_validate_start_date_before_end_date(self): self.contract_doc.start_date = nowdate() self.contract_doc.end_date = add_days(nowdate(), -1) From 8150638519da38b69272095652741467c8bb26e0 Mon Sep 17 00:00:00 2001 From: khushi Date: Thu, 19 Jun 2025 15:07:44 +0530 Subject: [PATCH 16/45] chore: linters check (cherry picked from commit f7e63936a9a0119de44296390b8b632399daedd1) --- erpnext/crm/doctype/contract/contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/contract/contract.py b/erpnext/crm/doctype/contract/contract.py index 559c89a9629..ea4398d7bfa 100644 --- a/erpnext/crm/doctype/contract/contract.py +++ b/erpnext/crm/doctype/contract/contract.py @@ -5,8 +5,8 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import getdate, nowdate from frappe.model.naming import append_number_if_name_exists +from frappe.utils import getdate, nowdate class Contract(Document): From 1005ee64cd670c7de8fb73e059039cb983a1b669 Mon Sep 17 00:00:00 2001 From: khushi Date: Thu, 19 Jun 2025 17:13:01 +0530 Subject: [PATCH 17/45] refactor: remove autoname (cherry picked from commit a4bb7c4e95c853fb0319a183c541e049242befae) --- erpnext/crm/doctype/contract/contract.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/erpnext/crm/doctype/contract/contract.py b/erpnext/crm/doctype/contract/contract.py index ea4398d7bfa..49988e546df 100644 --- a/erpnext/crm/doctype/contract/contract.py +++ b/erpnext/crm/doctype/contract/contract.py @@ -47,14 +47,6 @@ class Contract(Document): status: DF.Literal["Unsigned", "Active", "Inactive"] # end: auto-generated types - def autoname(self): - name = self.party_name - - if self.contract_template: - name += f" - {self.contract_template} Agreement" - - self.name = append_number_if_name_exists("Contract", name) - def validate(self): self.set_missing_values() self.validate_dates() From b3c43e85278e2d3da5ce44d8f53f9c40be2af504 Mon Sep 17 00:00:00 2001 From: khushi Date: Thu, 19 Jun 2025 17:45:22 +0530 Subject: [PATCH 18/45] feat: add naming series for Contract Doctype (cherry picked from commit bf56c73c6c7d84ac5dcebbbd693abc15ecf508b8) # Conflicts: # erpnext/crm/doctype/contract/contract.json --- erpnext/crm/doctype/contract/contract.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/contract/contract.json b/erpnext/crm/doctype/contract/contract.json index 948243402fe..118fb94cd6c 100755 --- a/erpnext/crm/doctype/contract/contract.json +++ b/erpnext/crm/doctype/contract/contract.json @@ -2,6 +2,7 @@ "actions": [], "allow_import": 1, "allow_rename": 1, + "autoname": "CON-.YYYY.-.#####", "creation": "2018-04-12 06:32:04.582486", "doctype": "DocType", "editable_grid": 1, @@ -256,10 +257,11 @@ "grid_page_length": 50, "is_submittable": 1, "links": [], - "modified": "2025-05-23 13:54:03.346537", + "modified": "2025-06-19 17:27:19.908421", "modified_by": "Administrator", "module": "CRM", "name": "Contract", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -327,6 +329,11 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", +<<<<<<< HEAD +======= + "states": [], + "title_field": "party_name", +>>>>>>> bf56c73c6c (feat: add naming series for Contract Doctype) "track_changes": 1, "track_seen": 1 } From 27b5d9493a5eb38920e911687f5a8d21a8308263 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Thu, 19 Jun 2025 18:09:10 +0530 Subject: [PATCH 19/45] feat: add search field for contract doctype (cherry picked from commit 0665691b881ddf2cb2e5a5af43a7d1dd4f24dcfd) --- erpnext/crm/doctype/contract/contract.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/contract/contract.json b/erpnext/crm/doctype/contract/contract.json index 118fb94cd6c..5938701031e 100755 --- a/erpnext/crm/doctype/contract/contract.json +++ b/erpnext/crm/doctype/contract/contract.json @@ -257,7 +257,7 @@ "grid_page_length": 50, "is_submittable": 1, "links": [], - "modified": "2025-06-19 17:27:19.908421", + "modified": "2025-06-19 17:48:45.049007", "modified_by": "Administrator", "module": "CRM", "name": "Contract", @@ -326,6 +326,7 @@ } ], "row_format": "Dynamic", + "search_fields": "party_type, party_name, contract_template", "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", From 4c2f555379dd2fdbb41433993e8627aa64011c56 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Thu, 19 Jun 2025 18:09:54 +0530 Subject: [PATCH 20/45] refactor: remove test case (cherry picked from commit 4a027125bce7bb53e982eec0e960ba8027031b5c) --- erpnext/crm/doctype/contract/test_contract.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/erpnext/crm/doctype/contract/test_contract.py b/erpnext/crm/doctype/contract/test_contract.py index 81ccf3300fe..13901683de8 100644 --- a/erpnext/crm/doctype/contract/test_contract.py +++ b/erpnext/crm/doctype/contract/test_contract.py @@ -12,19 +12,6 @@ class TestContract(unittest.TestCase): frappe.db.sql("delete from `tabContract`") self.contract_doc = get_contract() - def test_autoname_appends_suffix_for_duplicates(self): - contract_1 = self.contract_doc - contract_1.insert() - self.assertEqual(contract_1.name, "_Test Customer") - - contract_2 = get_contract() - contract_2.insert() - self.assertEqual(contract_2.name, "_Test Customer-1") - - contract_3 = get_contract() - contract_3.insert() - self.assertEqual(contract_3.name, "_Test Customer-2") - def test_validate_start_date_before_end_date(self): self.contract_doc.start_date = nowdate() self.contract_doc.end_date = add_days(nowdate(), -1) From c07d5b6decbb80acce06b8d5f6f47691fdd7dc6d Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Thu, 19 Jun 2025 18:40:37 +0530 Subject: [PATCH 21/45] chore: remove unused import (cherry picked from commit a1c0727d7b859737cc2265b98456577fb1b190c3) --- erpnext/crm/doctype/contract/contract.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/crm/doctype/contract/contract.py b/erpnext/crm/doctype/contract/contract.py index 49988e546df..223e0549ede 100644 --- a/erpnext/crm/doctype/contract/contract.py +++ b/erpnext/crm/doctype/contract/contract.py @@ -5,7 +5,6 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.model.naming import append_number_if_name_exists from frappe.utils import getdate, nowdate From 881dcf817f7528dacf5c9649bf24c5dd9df9ca9a Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Fri, 20 Jun 2025 14:23:02 +0530 Subject: [PATCH 22/45] fix: resolved conflicts --- erpnext/crm/doctype/contract/contract.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/crm/doctype/contract/contract.json b/erpnext/crm/doctype/contract/contract.json index 5938701031e..7fa7002b82d 100755 --- a/erpnext/crm/doctype/contract/contract.json +++ b/erpnext/crm/doctype/contract/contract.json @@ -330,11 +330,8 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", -<<<<<<< HEAD -======= "states": [], "title_field": "party_name", ->>>>>>> bf56c73c6c (feat: add naming series for Contract Doctype) "track_changes": 1, "track_seen": 1 } From 991ddfe1876699f2e968e1f7dbe6f22b54e03b18 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 20 Jun 2025 16:23:42 +0530 Subject: [PATCH 23/45] fix: pos item details fetch uoms on stock settings allow_uom_with_conversion_rate_defined_in_item configuration (backport #48178) (#48179) fix: pos item details fetch uoms on stock settings allow_uom_with_conversion_rate_defined_in_item configuration (#48178) (cherry picked from commit 4aa4942a1738952f315ef343ac80119632f1ff20) Co-authored-by: Diptanil Saha --- erpnext/selling/page/point_of_sale/pos_item_details.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index a0476ee6bda..7dc780dad7e 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -321,6 +321,15 @@ erpnext.PointOfSale.ItemDetails = class { me.conversion_factor_control.df.read_only = item_row.stock_uom == this.value; me.conversion_factor_control.refresh(); }; + this.uom_control.df.get_query = () => { + return { + query: "erpnext.controllers.queries.get_item_uom_query", + filters: { + item_code: me.current_item.item_code, + }, + }; + }; + this.uom_control.refresh(); } frappe.model.on("POS Invoice Item", "*", (fieldname, value, item_row) => { From b3d337a45b64586d46cae4fa6a91d9a5c8ca4e4c Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 20 Jun 2025 21:06:43 +0530 Subject: [PATCH 24/45] fix: SABB validation during the LCV (cherry picked from commit e958f886d310a4129beb0c849ae3dad0f3ab31c6) --- erpnext/controllers/stock_controller.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index d6930312b77..667e07ab9ff 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -221,7 +221,11 @@ class StockController(AccountsController): parent_details = self.get_parent_details_for_packed_items() for row in self.get(table_name): - if row.serial_and_batch_bundle and (row.serial_no or row.batch_no): + if ( + not via_landed_cost_voucher + and row.serial_and_batch_bundle + and (row.serial_no or row.batch_no) + ): self.validate_serial_nos_and_batches_with_bundle(row) if not row.serial_no and not row.batch_no and not row.get("rejected_serial_no"): From ad0819feee4b39174ac2c745eb2a55cebe4c1297 Mon Sep 17 00:00:00 2001 From: Karuppasamy923 Date: Sat, 21 Jun 2025 14:06:59 +0530 Subject: [PATCH 25/45] fix: add is_group filter for warehouse (cherry picked from commit a29ae9cf90b4105f0642b9817a417da387b87731) --- erpnext/selling/doctype/sales_order/sales_order.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 4edf378a938..28756fb1eb6 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -181,14 +181,20 @@ frappe.ui.form.on("Sales Order", { } erpnext.queries.setup_queries(frm, "Warehouse", function () { return { - filters: [["Warehouse", "company", "in", ["", cstr(frm.doc.company)]]], + filters: [ + ["Warehouse", "company", "in", ["", cstr(frm.doc.company)]], + ["Warehouse", "is_group", "=", 0], + ], }; }); frm.set_query("warehouse", "items", function (doc, cdt, cdn) { let row = locals[cdt][cdn]; let query = { - filters: [["Warehouse", "company", "in", ["", cstr(frm.doc.company)]]], + filters: [ + ["Warehouse", "company", "in", ["", cstr(frm.doc.company)]], + ["Warehouse", "is_group", "=", 0], + ], }; if (row.item_code) { query.query = "erpnext.controllers.queries.warehouse_query"; From ceab26d5f14a170e6c962aaaf23ee41df5be40f7 Mon Sep 17 00:00:00 2001 From: Karuppasamy923 Date: Tue, 17 Jun 2025 13:29:08 +0530 Subject: [PATCH 26/45] fix: add party and party_name columns to trend reports (cherry picked from commit d05204a960e1bbe0c3530971537c04facd9097f6) --- .../purchase_order_trends.py | 2 +- erpnext/controllers/trends.py | 25 +++++++++++++------ .../quotation_trends/quotation_trends.py | 2 +- .../sales_order_trends/sales_order_trends.py | 2 +- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py index d089473a16a..7398bb28736 100644 --- a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py +++ b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py @@ -24,7 +24,7 @@ def get_chart_data(data, conditions, filters): datapoints = [] - start = 2 if filters.get("based_on") in ["Item", "Supplier"] else 1 + start = 3 if filters.get("based_on") in ["Item", "Supplier"] else 1 if filters.get("group_by"): start += 1 diff --git a/erpnext/controllers/trends.py b/erpnext/controllers/trends.py index 7e6062d34b2..57edae7053a 100644 --- a/erpnext/controllers/trends.py +++ b/erpnext/controllers/trends.py @@ -98,9 +98,10 @@ def get_data(filters, conditions): sel_col = "t1.supplier" if filters.get("based_on") in ["Item", "Customer", "Supplier"]: - inc = 2 + inc = 3 else: inc = 1 + data1 = frappe.db.sql( """ select {} from `tab{}` t1, `tab{} Item` t2 {} where t2.parent = t1.name and t1.company = {} and {} between {} and {} and @@ -330,11 +331,20 @@ def based_wise_columns_query(based_on, trans): based_on_details["addl_tables"] = "" elif based_on == "Customer": - based_on_details["based_on_cols"] = [ - "Customer:Link/Customer:120", - "Territory:Link/Territory:120", - ] - based_on_details["based_on_select"] = "t1.customer_name, t1.territory, " + if trans == "Quotation": + based_on_details["based_on_cols"] = [ + "Party:Link/Customer:120", + "Party Name:Data:120", + "Territory:Link/Territory:120", + ] + based_on_details["based_on_select"] = "t1.party_name, t1.customer_name, t1.territory," + else: + based_on_details["based_on_cols"] = [ + "Customer:Link/Customer:120", + "Customer Name:Data:120", + "Territory:Link/Territory:120", + ] + based_on_details["based_on_select"] = "t1.customer, t1.customer_name, t1.territory," based_on_details["based_on_group_by"] = "t1.party_name" if trans == "Quotation" else "t1.customer" based_on_details["addl_tables"] = "" @@ -347,9 +357,10 @@ def based_wise_columns_query(based_on, trans): elif based_on == "Supplier": based_on_details["based_on_cols"] = [ "Supplier:Link/Supplier:120", + "Supplier Name:Data:120", "Supplier Group:Link/Supplier Group:140", ] - based_on_details["based_on_select"] = "t1.supplier, t3.supplier_group," + based_on_details["based_on_select"] = "t1.supplier, t1.supplier_name, t3.supplier_group," based_on_details["based_on_group_by"] = "t1.supplier" based_on_details["addl_tables"] = ",`tabSupplier` t3" based_on_details["addl_tables_relational_cond"] = " and t1.supplier = t3.name" diff --git a/erpnext/selling/report/quotation_trends/quotation_trends.py b/erpnext/selling/report/quotation_trends/quotation_trends.py index bcb8fe9297e..5f96e07f541 100644 --- a/erpnext/selling/report/quotation_trends/quotation_trends.py +++ b/erpnext/selling/report/quotation_trends/quotation_trends.py @@ -25,7 +25,7 @@ def get_chart_data(data, conditions, filters): datapoints = [] - start = 2 if filters.get("based_on") in ["Item", "Customer"] else 1 + start = 3 if filters.get("based_on") in ["Item", "Customer"] else 1 if filters.get("group_by"): start += 1 diff --git a/erpnext/selling/report/sales_order_trends/sales_order_trends.py b/erpnext/selling/report/sales_order_trends/sales_order_trends.py index 18f448c7cd2..fdd63cd5a68 100644 --- a/erpnext/selling/report/sales_order_trends/sales_order_trends.py +++ b/erpnext/selling/report/sales_order_trends/sales_order_trends.py @@ -24,7 +24,7 @@ def get_chart_data(data, conditions, filters): datapoints = [] - start = 2 if filters.get("based_on") in ["Item", "Customer"] else 1 + start = 3 if filters.get("based_on") in ["Item", "Customer"] else 1 if filters.get("group_by"): start += 1 From b6dda08290dea323cc0bc95a5dc810596146cfc8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 23 Jun 2025 15:56:02 +0530 Subject: [PATCH 27/45] chore: better label and desciption for pegged currency flag (cherry picked from commit c5cd7d91c4b619ec733398122af4eca082b8a222) --- .../doctype/accounts_settings/accounts_settings.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 0ca9309fa71..47aa8a1834e 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -596,10 +596,11 @@ }, { "default": "0", - "description": "Enable this field to fetch the exchange rates for Pegged Currencies.\n\n", + "description": "System will do an implicit conversion using the pegged currency.
\nEx: Instead of AED -> INR, system will do AED -> USD -> INR using the pegged exchange rate of AED against USD.", + "documentation_url": "/app/pegged-currencies/Pegged Currencies", "fieldname": "allow_pegged_currencies_exchange_rates", "fieldtype": "Check", - "label": "Allow Pegged Currencies Exchange Rates" + "label": "Allow Implicit Pegged Currency Conversion" } ], "icon": "icon-cog", @@ -607,7 +608,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-06-16 16:40:54.871486", + "modified": "2025-06-23 15:55:33.346398", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", From 153ed04161f86a1043c7f0d0688be8d12f31cb4f Mon Sep 17 00:00:00 2001 From: i-am-vimal Date: Thu, 19 Jun 2025 18:14:32 +0530 Subject: [PATCH 28/45] fix: add validation for exchange gain/loss entries (cherry picked from commit 5c9eddd31e16bf6f8afa5e3800e04c2ea1044c53) --- erpnext/accounts/report/general_ledger/general_ledger.py | 3 +-- erpnext/accounts/report/utils.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 7757bb37f6b..0bb14604991 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -194,8 +194,7 @@ def get_gl_entries(filters, accounting_dimensions): voucher_type, voucher_subtype, voucher_no, {dimension_fields} cost_center, project, {transaction_currency_fields} against_voucher_type, against_voucher, account_currency, - against, is_opening, creation {select_fields}, - transaction_currency + against, is_opening, creation {select_fields} from `tabGL Entry` where company=%(company)s {get_conditions(filters)} {order_by_statement} diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py index 5056b986187..02ba54604c4 100644 --- a/erpnext/accounts/report/utils.py +++ b/erpnext/accounts/report/utils.py @@ -101,7 +101,6 @@ def convert_to_presentation_currency(gl_entries, currency_info): account_currencies = list(set(entry["account_currency"] for entry in gl_entries)) for entry in gl_entries: - transaction_currency = entry.get("transaction_currency") debit = flt(entry["debit"]) credit = flt(entry["credit"]) debit_in_account_currency = flt(entry["debit_in_account_currency"]) @@ -111,7 +110,7 @@ def convert_to_presentation_currency(gl_entries, currency_info): if ( len(account_currencies) == 1 and account_currency == presentation_currency - and (transaction_currency is None or account_currency == transaction_currency) + and (debit_in_account_currency or credit_in_account_currency) ): entry["debit"] = debit_in_account_currency entry["credit"] = credit_in_account_currency From 5cabdbfe069d5c79de3121fea36e48e627a072d6 Mon Sep 17 00:00:00 2001 From: pugazhendhivelu Date: Fri, 13 Jun 2025 18:16:25 +0530 Subject: [PATCH 29/45] fix: add descendants item groups to fetch the barcode items (cherry picked from commit 4b82fe26114681fb2a6f225c8a25f8f1736df409) --- erpnext/selling/page/point_of_sale/point_of_sale.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 459a33ea1ba..842c2d3332f 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -8,7 +8,7 @@ import frappe from frappe.utils import cint from frappe.utils.nestedset import get_root_of -from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability +from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_item_group, get_stock_availability from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes, get_item_groups from erpnext.stock.utils import scan_barcode @@ -109,7 +109,8 @@ def search_by_term(search_term, warehouse, price_list): def filter_result_items(result, pos_profile): if result and result.get("items"): - pos_item_groups = frappe.db.get_all("POS Item Group", {"parent": pos_profile}, pluck="item_group") + pos_profile_doc = frappe.get_cached_doc("POS Profile", pos_profile) + pos_item_groups = get_item_group(pos_profile_doc) if not pos_item_groups: return result["items"] = [item for item in result.get("items") if item.get("item_group") in pos_item_groups] From ad40bfe4ea7c36383525f1e3a204fdcabb84a38a Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 23 Jun 2025 14:28:28 +0530 Subject: [PATCH 30/45] fix: incoming rate for the stand-alone credit note (cherry picked from commit b06eca8dcb3d553fd1e04020dbaf72b0f4ce0969) # Conflicts: # erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py --- .../sales_invoice/test_sales_invoice.py | 113 ++++++++++++++++++ .../controllers/sales_and_purchase_return.py | 8 ++ erpnext/controllers/selling_controller.py | 9 ++ 3 files changed, 130 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 3ad08885b94..8bea8e29dd5 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -4409,6 +4409,118 @@ class TestSalesInvoice(FrappeTestCase): self.assertRaises(StockOverReturnError, return_doc.save) +<<<<<<< HEAD +======= + def test_pos_sales_invoice_creation_during_pos_invoice_mode(self): + # Deleting all opening entry + frappe.db.sql("delete from `tabPOS Opening Entry`") + + with self.change_settings("POS Settings", {"invoice_type": "POS Invoice"}): + pos_profile = make_pos_profile() + + pos_profile.payments = [] + pos_profile.append("payments", {"default": 1, "mode_of_payment": "Cash"}) + + pos_profile.save() + + pos = create_sales_invoice(qty=10, do_not_save=True) + + pos.is_pos = 1 + pos.pos_profile = pos_profile.name + pos.is_created_using_pos = 1 + + pos.append("payments", {"mode_of_payment": "Cash", "amount": 1000}) + self.assertRaises(frappe.ValidationError, pos.insert) + + def test_stand_alone_credit_note_valuation(self): + from erpnext.stock.doctype.item.test_item import make_item + + item_code = "_Test Item for Credit Note Valuation" + make_item_for_si( + item_code, + { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BATCH-TCNV.####", + }, + ) + + si = create_sales_invoice( + item=item_code, + qty=-2, + rate=1200, + is_return=1, + update_stock=1, + ) + + stock_ledger_entry = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Sales Invoice", + "voucher_no": si.name, + "item_code": item_code, + "warehouse": "_Test Warehouse - _TC", + }, + ["incoming_rate", "valuation_rate", "actual_qty as qty", "stock_value_difference"], + as_dict=True, + ) + + self.assertEqual(stock_ledger_entry.incoming_rate, 1200.0) + self.assertEqual(stock_ledger_entry.valuation_rate, 1200.0) + self.assertEqual(stock_ledger_entry.qty, 2.0) + self.assertEqual(stock_ledger_entry.stock_value_difference, 2400.0) + + def test_stand_alone_credit_note_zero_valuation(self): + from erpnext.stock.doctype.item.test_item import make_item + + item_code = "_Test Item for Credit Note Zero Valuation" + make_item_for_si( + item_code, + { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BATCH-TCNZV.####", + }, + ) + + si = create_sales_invoice( + item=item_code, + qty=-2, + rate=1200, + is_return=1, + update_stock=1, + allow_zero_valuation_rate=1, + ) + + stock_ledger_entry = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Sales Invoice", + "voucher_no": si.name, + "item_code": item_code, + "warehouse": "_Test Warehouse - _TC", + }, + ["incoming_rate", "valuation_rate", "actual_qty as qty", "stock_value_difference"], + as_dict=True, + ) + + self.assertEqual(stock_ledger_entry.incoming_rate, 0.0) + self.assertEqual(stock_ledger_entry.valuation_rate, 0.0) + self.assertEqual(stock_ledger_entry.qty, 2.0) + self.assertEqual(stock_ledger_entry.stock_value_difference, 0.0) + + +def make_item_for_si(item_code, properties=None): + from erpnext.stock.doctype.item.test_item import make_item + + item = make_item(item_code, properties=properties) + item.is_stock_item = 1 + item.save() + return item + +>>>>>>> b06eca8dcb (fix: incoming rate for the stand-alone credit note) def set_advance_flag(company, flag, default_account): frappe.db.set_value( @@ -4512,6 +4624,7 @@ def create_sales_invoice(**args): "conversion_factor": args.get("conversion_factor", 1), "incoming_rate": args.incoming_rate or 0, "serial_and_batch_bundle": bundle_id, + "allow_zero_valuation_rate": args.allow_zero_valuation_rate or 0, }, ) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 475cd3f3844..cb359dc5a71 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -680,6 +680,14 @@ def get_rate_for_return( raise_error_if_no_rate=False, ) + if not rate and voucher_type in ["Sales Invoice", "Delivery Note"]: + details = frappe.db.get_value( + voucher_type + " Item", voucher_detail_no, ["rate", "allow_zero_valuation_rate"], as_dict=1 + ) + + if details and not details.allow_zero_valuation_rate: + rate = flt(details.rate) + return rate diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 73ce4c83e4c..9089b5a2829 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -525,6 +525,15 @@ class SellingController(StockController): self.doctype, self.name, d.item_code, self.return_against, item_row=d ) + if ( + self.get("is_return") + and not d.incoming_rate + and not self.get("return_against") + and not self.is_internal_transfer() + and not d.get("allow_zero_valuation_rate") + ): + d.incoming_rate = d.rate + # For internal transfers use incoming rate as the valuation rate if self.is_internal_transfer(): if self.doctype == "Delivery Note" or self.get("update_stock"): From f8cfbda4e02588c5778df91ca420738d19e6c6df Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 22:42:47 +0530 Subject: [PATCH 31/45] fix: pos item price in get_item and item search (backport #47925) (#48217) fix: pos item price in get_item and item search (#47925) * fix: pos get item and item search * refactor: resolved linter issue and renamed variables * fix: uom on get_item * fix: incorrect item quantity on pos selector * refactor: remove unused import (cherry picked from commit 919684a787bf4e524dd35fc07b235d28cc75c07d) Co-authored-by: Diptanil Saha --- .../page/point_of_sale/point_of_sale.py | 57 ++++++++++++------- 1 file changed, 36 insertions(+), 21 deletions(-) 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 459a33ea1ba..af7a010d291 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -10,6 +10,7 @@ from frappe.utils.nestedset import get_root_of from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes, get_item_groups +from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.utils import scan_barcode @@ -66,6 +67,9 @@ def search_by_term(search_term, warehouse, price_list): if batch_no: price_filters["batch_no"] = ["in", [batch_no, ""]] + if serial_no: + price_filters["uom"] = item_doc.stock_uom + price = frappe.get_list( doctype="Item Price", filters=price_filters, @@ -158,7 +162,8 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te item.description, item.stock_uom, item.image AS item_image, - item.is_stock_item + item.is_stock_item, + item.sales_uom FROM `tabItem` item {bin_join_selection} WHERE @@ -192,12 +197,9 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te current_date = frappe.utils.today() for item in items_data: - uoms = frappe.get_doc("Item", item.item_code).get("uoms", []) - item.actual_qty, _ = get_stock_availability(item.item_code, warehouse) - item.uom = item.stock_uom - item_price = frappe.get_all( + item_prices = frappe.get_all( "Item Price", fields=["price_list_rate", "currency", "uom", "batch_no", "valid_from", "valid_upto"], filters={ @@ -208,27 +210,40 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te "valid_upto": ["in", [None, "", current_date]], }, order_by="valid_from desc", - limit=1, ) - if not item_price: - result.append(item) + stock_uom_price = next((d for d in item_prices if d.get("uom") == item.stock_uom), {}) + item_uom = item.stock_uom + item_uom_price = stock_uom_price - for price in item_price: - uom = next(filter(lambda x: x.uom == price.uom, uoms), {}) + if item.sales_uom and item.sales_uom != item.stock_uom: + item_uom = item.sales_uom + sales_uom_price = next((d for d in item_prices if d.get("uom") == item.sales_uom), {}) + if sales_uom_price: + item_uom_price = sales_uom_price - if price.uom != item.stock_uom and uom and uom.conversion_factor: - item.actual_qty = item.actual_qty // uom.conversion_factor + if item_prices and not item_uom_price: + item_uom = item_prices[0].get("uom") + item_uom_price = item_prices[0] + + item_conversion_factor = get_conversion_factor(item.item_code, item_uom).get("conversion_factor") + + if item.stock_uom != item_uom: + item.actual_qty = item.actual_qty // item_conversion_factor + + if item_uom_price and item_uom != item_uom_price.get("uom"): + item_uom_price.price_list_rate = item_uom_price.price_list_rate * item_conversion_factor + + result.append( + { + **item, + "price_list_rate": item_uom_price.get("price_list_rate"), + "currency": item_uom_price.get("currency"), + "uom": item_uom, + "batch_no": item_uom_price.get("batch_no"), + } + ) - result.append( - { - **item, - "price_list_rate": price.get("price_list_rate"), - "currency": price.get("currency"), - "uom": price.uom or item.uom, - "batch_no": price.batch_no, - } - ) return {"items": result} From 4341ac7e7a5ccd09f07e12105cd5c98d22885b8d Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Fri, 20 Jun 2025 17:47:09 +0530 Subject: [PATCH 32/45] fix: update journal entry title on amend (cherry picked from commit 4a3ee4df29b02247de7c31251e4ad3ebd2b4726d) --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index a29eae8d5aa..58c0f23e05e 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -147,8 +147,7 @@ class JournalEntry(AccountsController): if self.docstatus == 0: self.apply_tax_withholding() - if not self.title: - self.title = self.get_title() + self.title = self.get_title() def validate_advance_accounts(self): journal_accounts = set([x.account for x in self.accounts]) From f56028661024ddd9b9479adf2ad4d35e1c32baac Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 24 Jun 2025 11:34:04 +0530 Subject: [PATCH 33/45] chore: fix conflicts --- .../sales_invoice/test_sales_invoice.py | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 8bea8e29dd5..f183db99842 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -4409,29 +4409,6 @@ class TestSalesInvoice(FrappeTestCase): self.assertRaises(StockOverReturnError, return_doc.save) -<<<<<<< HEAD -======= - def test_pos_sales_invoice_creation_during_pos_invoice_mode(self): - # Deleting all opening entry - frappe.db.sql("delete from `tabPOS Opening Entry`") - - with self.change_settings("POS Settings", {"invoice_type": "POS Invoice"}): - pos_profile = make_pos_profile() - - pos_profile.payments = [] - pos_profile.append("payments", {"default": 1, "mode_of_payment": "Cash"}) - - pos_profile.save() - - pos = create_sales_invoice(qty=10, do_not_save=True) - - pos.is_pos = 1 - pos.pos_profile = pos_profile.name - pos.is_created_using_pos = 1 - - pos.append("payments", {"mode_of_payment": "Cash", "amount": 1000}) - self.assertRaises(frappe.ValidationError, pos.insert) - def test_stand_alone_credit_note_valuation(self): from erpnext.stock.doctype.item.test_item import make_item @@ -4520,7 +4497,6 @@ def make_item_for_si(item_code, properties=None): item.save() return item ->>>>>>> b06eca8dcb (fix: incoming rate for the stand-alone credit note) def set_advance_flag(company, flag, default_account): frappe.db.set_value( From ac22c422c8e1a9c33d26b0f36b9bf1df9f00fabd Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 16 Jun 2025 17:11:17 +0530 Subject: [PATCH 34/45] fix: fallback expense account and cost center in subcontracting receipt (cherry picked from commit cf1d4362e57a99dc266a4f95b8f105e69cebae0c) --- .../subcontracting_receipt/subcontracting_receipt.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 73474f2afe5..01f5df3b684 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -619,11 +619,11 @@ class SubcontractingReceipt(SubcontractingController): self.add_gl_entry( gl_entries=gl_entries, account=supplier_warehouse_account, - cost_center=rm_item.cost_center, + cost_center=rm_item.cost_center or item.cost_center, debit=0.0, credit=flt(rm_item.amount), remarks=remarks, - against_account=rm_item.expense_account, + against_account=rm_item.expense_account or item.expense_account, account_currency=get_account_currency(supplier_warehouse_account), project=item.project, item=item, @@ -631,8 +631,8 @@ class SubcontractingReceipt(SubcontractingController): # Expense Account (Debit) self.add_gl_entry( gl_entries=gl_entries, - account=rm_item.expense_account, - cost_center=rm_item.cost_center, + account=rm_item.expense_account or item.expense_account, + cost_center=rm_item.cost_center or item.cost_center, debit=flt(rm_item.amount), credit=0.0, remarks=remarks, From 2d7a7d99888d5b2981d642405ba42e7283fd9ebb Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 24 Jun 2025 11:56:08 +0530 Subject: [PATCH 35/45] fix(open_opportunity): remove company=null filter (backport #48222) (#48224) * fix(open_opportunity): remove company=null filter (#48222) Signed-off-by: Akhil Narang (cherry picked from commit c8c1c962980e876e4219cd5df024906d13bc8ed3) # Conflicts: # erpnext/crm/number_card/open_opportunity/open_opportunity.json * chore: resolve conflicts --------- Co-authored-by: Akhil Narang --- .../crm/number_card/open_opportunity/open_opportunity.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/crm/number_card/open_opportunity/open_opportunity.json b/erpnext/crm/number_card/open_opportunity/open_opportunity.json index 33a757feb14..959e7d58598 100644 --- a/erpnext/crm/number_card/open_opportunity/open_opportunity.json +++ b/erpnext/crm/number_card/open_opportunity/open_opportunity.json @@ -4,18 +4,19 @@ "doctype": "Number Card", "document_type": "Opportunity", "dynamic_filters_json": "[[\"Opportunity\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", - "filters_json": "[[\"Opportunity\",\"company\",\"=\",null,false]]", + "filters_json": "[]", "function": "Count", "idx": 0, "is_public": 1, "is_standard": 1, "label": "Open Opportunity", - "modified": "2020-07-22 16:16:16.420446", + "modified": "2025-06-24 11:10:17.468713", "modified_by": "Administrator", "module": "CRM", "name": "Open Opportunity", "owner": "Administrator", + "show_full_number": 0, "show_percentage_stats": 1, "stats_time_interval": "Daily", "type": "Document Type" -} \ No newline at end of file +} From 6511eb4c7c4fbda1c4b4b274e47b5940d3572a45 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:51:15 +0530 Subject: [PATCH 36/45] refactor: track completed app setup wizards and re-run the setup wizard upon new app installation. (backport #47691) (#48223) * refactor: track completed app setup wizards and re-run the setup wizard upon new app installation. (#47691) (cherry picked from commit 75b5ba6e67d61f8ec1554a6fb922d086d1cc299d) # Conflicts: # erpnext/hooks.py # erpnext/setup/install.py * chore: fix conflicts * chore: fix conflicts * chore: fix conflicts * fix: permission issue * fix: space --------- Co-authored-by: rohitwaghchaure --- erpnext/hooks.py | 1 - erpnext/public/js/setup_wizard.js | 11 +++++++++-- erpnext/setup/install.py | 7 ------- .../setup/setup_wizard/operations/install_fixtures.py | 1 + erpnext/utilities/activation.py | 3 ++- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 21f301f2450..db9408b0f7e 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -57,7 +57,6 @@ setup_wizard_complete = "erpnext.setup.setup_wizard.setup_wizard.setup_demo" setup_wizard_test = "erpnext.setup.setup_wizard.test_setup_wizard.run_setup_wizard_test" before_install = [ - "erpnext.setup.install.check_setup_wizard_not_completed", "erpnext.setup.install.check_frappe_version", ] after_install = "erpnext.setup.install.after_install" diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js index 20fdbcd9b13..934e58f189e 100644 --- a/erpnext/public/js/setup_wizard.js +++ b/erpnext/public/js/setup_wizard.js @@ -8,6 +8,13 @@ frappe.pages["setup-wizard"].on_page_load = function (wrapper) { }; frappe.setup.on("before_load", function () { + if ( + frappe.boot.setup_wizard_completed_apps?.length && + frappe.boot.setup_wizard_completed_apps.includes("erpnext") + ) { + return; + } + erpnext.setup.slides_settings.map(frappe.setup.add_slide); }); @@ -96,7 +103,7 @@ erpnext.setup.slides_settings = [ }, set_fy_dates: function (slide) { - var country = frappe.wizard.values.country; + var country = frappe.wizard.values.country || frappe.defaults.get_default("country"); if (country) { let fy = erpnext.setup.fiscal_years[country]; @@ -118,7 +125,7 @@ erpnext.setup.slides_settings = [ }, load_chart_of_accounts: function (slide) { - let country = frappe.wizard.values.country; + let country = frappe.wizard.values.country || frappe.defaults.get_default("country"); if (country) { frappe.call({ diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index b15925ea232..d5f2c5422bd 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -39,13 +39,6 @@ def after_install(): frappe.db.commit() -def check_setup_wizard_not_completed(): - if cint(frappe.db.get_single_value("System Settings", "setup_complete") or 0): - message = """ERPNext can only be installed on a fresh site where the setup wizard is not completed. -You can reinstall this site (after saving your data) using: bench --site [sitename] reinstall""" - frappe.throw(message) # nosemgrep - - def check_frappe_version(): def major_version(v: str) -> str: return v.split(".")[0] diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 0077a214e67..9cdf24cf451 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -504,6 +504,7 @@ def update_stock_settings(): stock_settings.auto_insert_price_list_rate_if_missing = 1 stock_settings.update_price_list_based_on = "Rate" stock_settings.set_qty_in_transactions_based_on_serial_no_input = 1 + stock_settings.flags.ignore_permissions = True stock_settings.save() diff --git a/erpnext/utilities/activation.py b/erpnext/utilities/activation.py index 5a72f13b4a5..e09d87dd769 100644 --- a/erpnext/utilities/activation.py +++ b/erpnext/utilities/activation.py @@ -4,6 +4,7 @@ import frappe from frappe import _ +from frappe.core.doctype.installed_applications.installed_applications import get_setup_wizard_completed_apps import erpnext @@ -45,7 +46,7 @@ def get_level(): activation_level += 1 sales_data.append({doctype: count}) - if frappe.db.get_single_value("System Settings", "setup_complete"): + if "erpnext" in get_setup_wizard_completed_apps(): activation_level += 1 communication_number = frappe.db.count("Communication", dict(communication_medium="Email")) From 2bf8dffb604c6c9356a9fa708d49ed3c6ca78dd7 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Thu, 12 Jun 2025 11:57:30 +0530 Subject: [PATCH 37/45] fix: auto append_taxes_from_item_tax_template in backend (cherry picked from commit 4cb1fa2b6ba0a2990296dcdcec6064b5c33a6286) # Conflicts: # erpnext/controllers/accounts_controller.py # erpnext/controllers/tests/test_accounts_controller.py --- .../accounts_settings/accounts_settings.js | 17 ++++++++++ .../accounts_settings/accounts_settings.json | 8 +++++ .../accounts_settings/accounts_settings.py | 12 +++++++ .../sales_invoice/test_sales_invoice.py | 4 +++ erpnext/controllers/accounts_controller.py | 20 ++++++++++++ .../tests/test_accounts_controller.py | 31 +++++++++++++++++++ .../doctype/quotation/test_quotation.py | 11 +++++-- 7 files changed, 100 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.js b/erpnext/accounts/doctype/accounts_settings/accounts_settings.js index 95332acdc28..ba577f2b8c9 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.js +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.js @@ -22,4 +22,21 @@ frappe.ui.form.on("Accounts Settings", { } ); }, + + add_taxes_from_taxes_and_charges_template(frm) { + toggle_tax_settings(frm, "add_taxes_from_taxes_and_charges_template"); + }, + add_taxes_from_item_tax_template(frm) { + toggle_tax_settings(frm, "add_taxes_from_item_tax_template"); + }, }); + +function toggle_tax_settings(frm, field_name) { + if (frm.doc[field_name]) { + const other_field = + field_name === "add_taxes_from_item_tax_template" + ? "add_taxes_from_taxes_and_charges_template" + : "add_taxes_from_item_tax_template"; + frm.set_value(other_field, 0); + } +} diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 47aa8a1834e..80b7d996101 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -31,6 +31,7 @@ "determine_address_tax_category_from", "column_break_19", "add_taxes_from_item_tax_template", + "add_taxes_from_taxes_and_charges_template", "book_tax_discount_loss", "round_row_wise_tax", "print_settings", @@ -601,6 +602,13 @@ "fieldname": "allow_pegged_currencies_exchange_rates", "fieldtype": "Check", "label": "Allow Implicit Pegged Currency Conversion" + }, + { + "default": "0", + "description": "If no taxes are set, and Taxes and Charges Template is selected, the system will automatically apply the taxes from the chosen template.", + "fieldname": "add_taxes_from_taxes_and_charges_template", + "fieldtype": "Check", + "label": "Automatically Add Taxes from Taxes and Charges Template" } ], "icon": "icon-cog", diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 2a3cada2398..d03ebed353e 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -25,6 +25,7 @@ class AccountsSettings(Document): acc_frozen_upto: DF.Date | None add_taxes_from_item_tax_template: DF.Check + add_taxes_from_taxes_and_charges_template: DF.Check allow_multi_currency_invoices_against_single_party_account: DF.Check allow_pegged_currencies_exchange_rates: DF.Check allow_stale: DF.Check @@ -74,6 +75,7 @@ class AccountsSettings(Document): # end: auto-generated types def validate(self): + self.validate_auto_tax_settings() old_doc = self.get_doc_before_save() clear_cache = False @@ -140,3 +142,13 @@ class AccountsSettings(Document): if self.has_value_changed("reconciliation_queue_size"): if cint(self.reconciliation_queue_size) < 5 or cint(self.reconciliation_queue_size) > 100: frappe.throw(_("Queue Size should be between 5 and 100")) + + def validate_auto_tax_settings(self): + if self.add_taxes_from_item_tax_template and self.add_taxes_from_taxes_and_charges_template: + frappe.throw( + _("You cannot enable both the settings '{0}' and '{1}'.").format( + frappe.bold(self.meta.get_label("add_taxes_from_item_tax_template")), + frappe.bold(self.meta.get_label("add_taxes_from_taxes_and_charges_template")), + ), + title=_("Auto Tax Settings Error"), + ) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index f183db99842..1030a32f161 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -836,6 +836,10 @@ class TestSalesInvoice(FrappeTestCase): w = self.make() self.assertEqual(w.outstanding_amount, w.base_rounded_total) + @IntegrationTestCase.change_settings( + "Accounts Settings", + {"add_taxes_from_item_tax_template": 0, "add_taxes_from_taxes_and_charges_template": 0}, + ) def test_rounded_total_with_cash_discount(self): si = frappe.copy_doc(test_records[2]) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index e33f8f8b64c..2885490b31f 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1134,10 +1134,24 @@ class AccountsController(TransactionBase): return True def set_taxes_and_charges(self): +<<<<<<< HEAD if frappe.db.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template"): if hasattr(self, "taxes_and_charges") and not self.get("taxes") and not self.get("is_pos"): if tax_master_doctype := self.meta.get_field("taxes_and_charges").options: self.append_taxes_from_master(tax_master_doctype) +======= + if self.get("taxes") or self.get("is_pos"): + return + + if frappe.get_single_value( + "Accounts Settings", "add_taxes_from_taxes_and_charges_template" + ) and hasattr(self, "taxes_and_charges"): + if tax_master_doctype := self.meta.get_field("taxes_and_charges").options: + self.append_taxes_from_master(tax_master_doctype) + + if frappe.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template"): + self.append_taxes_from_item_tax_template() +>>>>>>> 4cb1fa2b6b (fix: auto append_taxes_from_item_tax_template in backend) def append_taxes_from_master(self, tax_master_doctype=None): if self.get("taxes_and_charges"): @@ -1169,6 +1183,12 @@ class AccountsController(TransactionBase): "account_head": account_head, "rate": 0, "description": account_head, +<<<<<<< HEAD +======= + "set_by_item_tax_template": 1, + "category": "Total", + "add_deduct_tax": "Add", +>>>>>>> 4cb1fa2b6b (fix: auto append_taxes_from_item_tax_template in backend) }, ) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 28536893ab5..b3b7427d32f 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -931,7 +931,14 @@ class TestAccountsController(FrappeTestCase): self.assertEqual(exc_je_for_si, []) self.assertEqual(exc_je_for_pe, []) +<<<<<<< HEAD @change_settings("Accounts Settings", {"add_taxes_from_item_tax_template": 1}) +======= + @IntegrationTestCase.change_settings( + "Accounts Settings", + {"add_taxes_from_item_tax_template": 0, "add_taxes_from_taxes_and_charges_template": 1}, + ) +>>>>>>> 4cb1fa2b6b (fix: auto append_taxes_from_item_tax_template in backend) def test_18_fetch_taxes_based_on_taxes_and_charges_template(self): # Create a Sales Taxes and Charges Template if not frappe.db.exists("Sales Taxes and Charges Template", "_Test Tax - _TC"): @@ -960,6 +967,30 @@ class TestAccountsController(FrappeTestCase): self.assertEqual(sinv.total_taxes_and_charges, 4.5) + @IntegrationTestCase.change_settings( + "Accounts Settings", + {"add_taxes_from_item_tax_template": 1, "add_taxes_from_taxes_and_charges_template": 0}, + ) + def test_19_fetch_taxes_based_on_item_tax_template_template(self): + # Create a Sales Invoice + sinv = frappe.new_doc("Sales Invoice") + sinv.customer = self.customer + sinv.company = self.company + sinv.currency = "INR" + sinv.append( + "items", + { + "item_code": "_Test Item", + "qty": 1, + "rate": 50, + "item_tax_template": "_Test Account Excise Duty @ 10 - _TC", + }, + ) + sinv.insert() + + self.assertEqual(sinv.taxes[0].account_head, "_Test Account Excise Duty - _TC") + self.assertEqual(sinv.total_taxes_and_charges, 5) + def test_20_journal_against_sales_invoice(self): # Invoice in Foreign Currency si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 0e58b950e99..98a2ae04c67 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -179,6 +179,10 @@ class TestQuotation(FrappeTestCase): sales_order.delivery_date = nowdate() sales_order.insert() + @IntegrationTestCase.change_settings( + "Accounts Settings", + {"add_taxes_from_item_tax_template": 0, "add_taxes_from_taxes_and_charges_template": 0}, + ) def test_make_sales_order_with_terms(self): from erpnext.selling.doctype.quotation.quotation import make_sales_order @@ -716,6 +720,10 @@ class TestQuotation(FrappeTestCase): quotation.items[0].conversion_factor = 2.23 self.assertRaises(frappe.ValidationError, quotation.save) + @IntegrationTestCase.change_settings( + "Accounts Settings", + {"add_taxes_from_item_tax_template": 1, "add_taxes_from_taxes_and_charges_template": 0}, + ) def test_item_tax_template_for_quotation(self): from erpnext.stock.doctype.item.test_item import make_item @@ -757,10 +765,7 @@ class TestQuotation(FrappeTestCase): item_doc.save() quotation = make_quotation(item_code="_Test Item Tax Template QTN", qty=1, rate=100, do_not_submit=1) - self.assertFalse(quotation.taxes) - quotation.append_taxes_from_item_tax_template() - quotation.save() self.assertTrue(quotation.taxes) for row in quotation.taxes: self.assertEqual(row.account_head, "_Test Vat - _TC") From 1ccdb67c55d1338ad02b869b6a06f6b44151b455 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 24 Jun 2025 15:31:34 +0530 Subject: [PATCH 38/45] chore: resolve conflicts --- .../doctype/sales_invoice/test_sales_invoice.py | 2 +- erpnext/controllers/accounts_controller.py | 10 ---------- erpnext/controllers/tests/test_accounts_controller.py | 8 ++------ erpnext/selling/doctype/quotation/test_quotation.py | 4 ++-- 4 files changed, 5 insertions(+), 19 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 1030a32f161..16fc4b0d7d2 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -836,7 +836,7 @@ class TestSalesInvoice(FrappeTestCase): w = self.make() self.assertEqual(w.outstanding_amount, w.base_rounded_total) - @IntegrationTestCase.change_settings( + @change_settings( "Accounts Settings", {"add_taxes_from_item_tax_template": 0, "add_taxes_from_taxes_and_charges_template": 0}, ) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 2885490b31f..b6b7ff890cf 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1134,12 +1134,6 @@ class AccountsController(TransactionBase): return True def set_taxes_and_charges(self): -<<<<<<< HEAD - if frappe.db.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template"): - if hasattr(self, "taxes_and_charges") and not self.get("taxes") and not self.get("is_pos"): - if tax_master_doctype := self.meta.get_field("taxes_and_charges").options: - self.append_taxes_from_master(tax_master_doctype) -======= if self.get("taxes") or self.get("is_pos"): return @@ -1151,7 +1145,6 @@ class AccountsController(TransactionBase): if frappe.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template"): self.append_taxes_from_item_tax_template() ->>>>>>> 4cb1fa2b6b (fix: auto append_taxes_from_item_tax_template in backend) def append_taxes_from_master(self, tax_master_doctype=None): if self.get("taxes_and_charges"): @@ -1183,12 +1176,9 @@ class AccountsController(TransactionBase): "account_head": account_head, "rate": 0, "description": account_head, -<<<<<<< HEAD -======= "set_by_item_tax_template": 1, "category": "Total", "add_deduct_tax": "Add", ->>>>>>> 4cb1fa2b6b (fix: auto append_taxes_from_item_tax_template in backend) }, ) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index b3b7427d32f..f2558572dc9 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -931,14 +931,10 @@ class TestAccountsController(FrappeTestCase): self.assertEqual(exc_je_for_si, []) self.assertEqual(exc_je_for_pe, []) -<<<<<<< HEAD - @change_settings("Accounts Settings", {"add_taxes_from_item_tax_template": 1}) -======= - @IntegrationTestCase.change_settings( + @change_settings( "Accounts Settings", {"add_taxes_from_item_tax_template": 0, "add_taxes_from_taxes_and_charges_template": 1}, ) ->>>>>>> 4cb1fa2b6b (fix: auto append_taxes_from_item_tax_template in backend) def test_18_fetch_taxes_based_on_taxes_and_charges_template(self): # Create a Sales Taxes and Charges Template if not frappe.db.exists("Sales Taxes and Charges Template", "_Test Tax - _TC"): @@ -967,7 +963,7 @@ class TestAccountsController(FrappeTestCase): self.assertEqual(sinv.total_taxes_and_charges, 4.5) - @IntegrationTestCase.change_settings( + @change_settings( "Accounts Settings", {"add_taxes_from_item_tax_template": 1, "add_taxes_from_taxes_and_charges_template": 0}, ) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 98a2ae04c67..210f4715815 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -179,7 +179,7 @@ class TestQuotation(FrappeTestCase): sales_order.delivery_date = nowdate() sales_order.insert() - @IntegrationTestCase.change_settings( + @change_settings( "Accounts Settings", {"add_taxes_from_item_tax_template": 0, "add_taxes_from_taxes_and_charges_template": 0}, ) @@ -720,7 +720,7 @@ class TestQuotation(FrappeTestCase): quotation.items[0].conversion_factor = 2.23 self.assertRaises(frappe.ValidationError, quotation.save) - @IntegrationTestCase.change_settings( + @change_settings( "Accounts Settings", {"add_taxes_from_item_tax_template": 1, "add_taxes_from_taxes_and_charges_template": 0}, ) From c2c5e45bc68691ae6fc5d3643f8ce6f6dd01bef5 Mon Sep 17 00:00:00 2001 From: Lakshit Jain <108322669+ljain112@users.noreply.github.com> Date: Tue, 24 Jun 2025 15:40:00 +0530 Subject: [PATCH 39/45] fix: get already billed amount from current doc instead of database (#48079) * fix: get already billed amount from current doc instead of database * fix: throw overbilling validation for all items in single call * refactor: minor fixes --------- Co-authored-by: Sagar Vora <16315650+sagarvora@users.noreply.github.com> (cherry picked from commit 47c3c4808e78ec29678c4c1972fc8cbd387cb1c3) --- .../sales_invoice/test_sales_invoice.py | 14 +- erpnext/controllers/accounts_controller.py | 170 ++++++++++-------- 2 files changed, 98 insertions(+), 86 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index f183db99842..5045ff0d7e4 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3429,6 +3429,7 @@ class TestSalesInvoice(FrappeTestCase): si.posting_date = getdate() si.submit() + @IntegrationTestCase.change_settings("Accounts Settings", {"over_billing_allowance": 0}) def test_over_billing_case_against_delivery_note(self): """ Test a case where duplicating the item with qty = 1 in the invoice @@ -3436,24 +3437,23 @@ class TestSalesInvoice(FrappeTestCase): """ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note - over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance") - frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", 0) - dn = create_delivery_note() dn.submit() si = make_sales_invoice(dn.name) - # make a copy of first item and add it to invoice item_copy = frappe.copy_doc(si.items[0]) + si.save() + + si.items = [] # Clear existing items si.append("items", item_copy) si.save() + si.append("items", item_copy) with self.assertRaises(frappe.ValidationError) as err: - si.submit() + si.save() self.assertTrue("cannot overbill" in str(err.exception).lower()) - - frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", over_billing_allowance) + dn.cancel() @change_settings( "Accounts Settings", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index e33f8f8b64c..b92963542b6 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2053,69 +2053,48 @@ class AccountsController(TransactionBase): def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on): from erpnext.controllers.status_updater import get_allowance_for - item_allowance = {} - global_qty_allowance, global_amount_allowance = None, None + ref_wise_billed_amount = self.get_reference_wise_billed_amt(ref_dt, item_ref_dn, based_on) - role_allowed_to_over_bill = frappe.get_cached_value( - "Accounts Settings", None, "role_allowed_to_over_bill" - ) - user_roles = frappe.get_roles() + if not ref_wise_billed_amount: + return total_overbilled_amt = 0.0 + overbilled_items = [] + precision = self.precision(based_on, "items") + precision_allowance = 1 / (10**precision) - reference_names = [d.get(item_ref_dn) for d in self.get("items") if d.get(item_ref_dn)] - reference_details = self.get_billing_reference_details(reference_names, ref_dt + " Item", based_on) + role_allowed_to_overbill = frappe.get_single_value("Accounts Settings", "role_allowed_to_over_bill") + is_overbilling_allowed = role_allowed_to_overbill in frappe.get_roles() - for item in self.get("items"): - if not item.get(item_ref_dn): - continue + for row in ref_wise_billed_amount.values(): + total_billed_amt = row.billed_amt + allowance = get_allowance_for(row.item_code, {}, None, None, "amount")[0] - ref_amt = flt(reference_details.get(item.get(item_ref_dn)), self.precision(based_on, item)) - based_on_amt = flt(item.get(based_on)) - - if not ref_amt: - if based_on_amt: # Skip warning for free items - frappe.msgprint( - _( - "System will not check over billing since amount for Item {0} in {1} is zero" - ).format(item.item_code, ref_dt), - title=_("Warning"), - indicator="orange", - ) - continue - - already_billed = self.get_billed_amount_for_item(item, item_ref_dn, based_on) - - total_billed_amt = flt(flt(already_billed) + based_on_amt, self.precision(based_on, item)) - - allowance, item_allowance, global_qty_allowance, global_amount_allowance = get_allowance_for( - item.item_code, item_allowance, global_qty_allowance, global_amount_allowance, "amount" - ) - - max_allowed_amt = flt(ref_amt * (100 + allowance) / 100) + max_allowed_amt = flt(row.ref_amt * (100 + allowance) / 100) if total_billed_amt < 0 and max_allowed_amt < 0: # while making debit note against purchase return entry(purchase receipt) getting overbill error - total_billed_amt = abs(total_billed_amt) - max_allowed_amt = abs(max_allowed_amt) + total_billed_amt, max_allowed_amt = abs(total_billed_amt), abs(max_allowed_amt) overbill_amt = total_billed_amt - max_allowed_amt + row["max_allowed_amt"] = max_allowed_amt total_overbilled_amt += overbill_amt - if overbill_amt > 0.01 and role_allowed_to_over_bill not in user_roles: - if self.doctype != "Purchase Invoice": - self.throw_overbill_exception(item, max_allowed_amt) - elif not cint( + if overbill_amt > precision_allowance and not is_overbilling_allowed: + if self.doctype != "Purchase Invoice" or not cint( frappe.db.get_single_value( "Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice" ) ): - self.throw_overbill_exception(item, max_allowed_amt) + overbilled_items.append(row) - if role_allowed_to_over_bill in user_roles and total_overbilled_amt > 0.1: + if overbilled_items: + self.throw_overbill_exception(overbilled_items, precision) + + if is_overbilling_allowed and total_overbilled_amt > 0.1: frappe.msgprint( _("Overbilling of {} ignored because you have {} role.").format( - total_overbilled_amt, role_allowed_to_over_bill + total_overbilled_amt, role_allowed_to_overbill ), indicator="orange", alert=True, @@ -2131,55 +2110,88 @@ class AccountsController(TransactionBase): ) ) - def get_billed_amount_for_item(self, item, item_ref_dn, based_on): + def get_reference_wise_billed_amt(self, ref_dt, item_ref_dn, based_on): """ Returns Sum of Amount of Sales/Purchase Invoice Items that are linked to `item_ref_dn` (`dn_detail` / `pr_detail`) that are submitted OR not submitted but are under current invoice """ + reference_names = [d.get(item_ref_dn) for d in self.items if d.get(item_ref_dn)] - from frappe.query_builder import Criterion - from frappe.query_builder.functions import Sum + if not reference_names: + return - item_doctype = frappe.qb.DocType(item.doctype) + ref_wise_billed_amount = {} + precision = self.precision(based_on, "items") + reference_details = self.get_billing_reference_details(reference_names, ref_dt + " Item", based_on) + already_billed = self.get_already_billed_amount(reference_names, item_ref_dn, based_on) + + for item in self.items: + key = item.get(item_ref_dn) + if not key: + continue + + ref_amt = flt(reference_details.get(key), precision) + current_amount = flt(item.get(based_on), precision) + + if not ref_amt: + if current_amount: # Skip warning for free items + frappe.msgprint( + _( + "System will not check over billing since amount for Item {0} in {1} is zero" + ).format(item.item_code, ref_dt), + title=_("Warning"), + indicator="orange", + ) + continue + + ref_wise_billed_amount.setdefault( + key, + frappe._dict(item_code=item.item_code, billed_amt=0.0, ref_amt=ref_amt, rows=[]), + ) + + ref_wise_billed_amount[key]["rows"].append(item.idx) + ref_wise_billed_amount[key]["ref_amt"] = ref_amt + ref_wise_billed_amount[key]["billed_amt"] += current_amount + if key in already_billed: + ref_wise_billed_amount[key]["billed_amt"] += flt(already_billed.pop(key, 0), precision) + + return ref_wise_billed_amount + + def get_already_billed_amount(self, reference_names, item_ref_dn, based_on): + item_doctype = frappe.qb.DocType(self.items[0].doctype) based_on_field = frappe.qb.Field(based_on) join_field = frappe.qb.Field(item_ref_dn) - result = ( - frappe.qb.from_(item_doctype) - .select(Sum(based_on_field)) - .where(join_field == item.get(item_ref_dn)) - .where( - Criterion.any( - [ # select all items from other invoices OR current invoices - Criterion.all( - [ # for selecting items from other invoices - item_doctype.docstatus == 1, - item_doctype.parent != self.name, - ] - ), - Criterion.all( - [ # for selecting items from current invoice, that are linked to same reference - item_doctype.docstatus == 0, - item_doctype.parent == self.name, - item_doctype.name != item.name, - ] - ), - ] - ) - ) - ).run() - - return result[0][0] if result else 0 - - def throw_overbill_exception(self, item, max_allowed_amt): - frappe.throw( - _( - "Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings" - ).format(item.item_code, item.idx, max_allowed_amt) + return frappe._dict( + ( + frappe.qb.from_(item_doctype) + .select(join_field, Sum(based_on_field)) + .where(join_field.isin(reference_names)) + .where((item_doctype.docstatus == 1) & (item_doctype.parent != self.name)) + .groupby(join_field) + ).run() ) + def throw_overbill_exception(self, overbilled_items, precision): + message = ( + _("

Cannot overbill for the following Items:

") + + "
    " + + "".join( + _("
  • Item {0} in row(s) {1} billed more than {2}
  • ").format( + frappe.bold(item.item_code), + ", ".join(str(x) for x in item.rows), + frappe.bold(fmt_money(item.max_allowed_amt, precision=precision, currency=self.currency)), + ) + for item in overbilled_items + ) + + "
" + ) + message += _("

To allow over-billing, please set allowance in Accounts Settings.

") + + frappe.throw(_(message)) + def get_company_default(self, fieldname, ignore_validation=False): from erpnext.accounts.utils import get_company_default From 651b9521b973e19e819d542ba51ce6f98451f4a5 Mon Sep 17 00:00:00 2001 From: Karuppasamy923 Date: Tue, 24 Jun 2025 13:27:52 +0530 Subject: [PATCH 40/45] fix: Update transaction currency to company currency to show correct currency symbol (cherry picked from commit b0e201a332821c8d277dea4fc12b30c572e6935f) --- erpnext/controllers/trends.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/trends.py b/erpnext/controllers/trends.py index 57edae7053a..5fe47543023 100644 --- a/erpnext/controllers/trends.py +++ b/erpnext/controllers/trends.py @@ -158,7 +158,7 @@ def get_data(filters, conditions): # get data for group_by filter row1 = frappe.db.sql( - """ select t1.currency , {} , {} from `tab{}` t1, `tab{} Item` t2 {} + """ select t4.default_currency AS currency , {} , {} from `tab{}` t1, `tab{} Item` t2 {} where t2.parent = t1.name and t1.company = {} and {} between {} and {} and t1.docstatus = 1 and {} = {} and {} = {} {} {} """.format( @@ -392,8 +392,12 @@ def based_wise_columns_query(based_on, trans): else: frappe.throw(_("Project-wise data is not available for Quotation")) - based_on_details["based_on_select"] += "t1.currency," + based_on_details["based_on_select"] += "t4.default_currency as currency," based_on_details["based_on_cols"].append("Currency:Link/Currency:120") + based_on_details["addl_tables"] += ", `tabCompany` t4" + based_on_details["addl_tables_relational_cond"] = ( + based_on_details.get("addl_tables_relational_cond", "") + " and t1.company = t4.name" + ) return based_on_details From 24f892d582144264329226e412a6c02643dec443 Mon Sep 17 00:00:00 2001 From: Karuppasamy923 Date: Tue, 24 Jun 2025 13:30:06 +0530 Subject: [PATCH 41/45] fix: Update indexing to populate correct values in trends report chart (cherry picked from commit b08d66113c2ec2143d6e2d0662ece19c0b4a75b9) --- .../purchase_order_trends/purchase_order_trends.py | 14 ++++++++++---- erpnext/controllers/trends.py | 4 +++- .../report/quotation_trends/quotation_trends.py | 14 ++++++++++---- .../sales_order_trends/sales_order_trends.py | 14 ++++++++++---- 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py index 7398bb28736..a7b4a7207c6 100644 --- a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py +++ b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py @@ -24,22 +24,28 @@ def get_chart_data(data, conditions, filters): datapoints = [] - start = 3 if filters.get("based_on") in ["Item", "Supplier"] else 1 + if filters.get("based_on") in ["Supplier"]: + start = 3 + elif filters.get("based_on") in ["Item"]: + start = 2 + else: + start = 1 + if filters.get("group_by"): start += 1 # fetch only periodic columns as labels - columns = conditions.get("columns")[start:-2][1::2] + columns = conditions.get("columns")[start:-2][2::2] labels = [column.split(":")[0] for column in columns] datapoints = [0] * len(labels) for row in data: # If group by filter, don't add first row of group (it's already summed) - if not row[start - 1]: + if not row[start]: continue # Remove None values and compute only periodic data row = [x if x else 0 for x in row[start:-2]] - row = row[1::2] + row = row[2::2] for i in range(len(row)): datapoints[i] += row[i] diff --git a/erpnext/controllers/trends.py b/erpnext/controllers/trends.py index 5fe47543023..f5046bb4c67 100644 --- a/erpnext/controllers/trends.py +++ b/erpnext/controllers/trends.py @@ -97,8 +97,10 @@ def get_data(filters, conditions): elif filters.get("group_by") == "Supplier": sel_col = "t1.supplier" - if filters.get("based_on") in ["Item", "Customer", "Supplier"]: + if filters.get("based_on") in ["Customer", "Supplier"]: inc = 3 + elif filters.get("based_on") in ["Item"]: + inc = 2 else: inc = 1 diff --git a/erpnext/selling/report/quotation_trends/quotation_trends.py b/erpnext/selling/report/quotation_trends/quotation_trends.py index 5f96e07f541..92f9d17a9c7 100644 --- a/erpnext/selling/report/quotation_trends/quotation_trends.py +++ b/erpnext/selling/report/quotation_trends/quotation_trends.py @@ -25,22 +25,28 @@ def get_chart_data(data, conditions, filters): datapoints = [] - start = 3 if filters.get("based_on") in ["Item", "Customer"] else 1 + if filters.get("based_on") in ["Customer"]: + start = 3 + elif filters.get("based_on") in ["Item"]: + start = 2 + else: + start = 1 + if filters.get("group_by"): start += 1 # fetch only periodic columns as labels - columns = conditions.get("columns")[start:-2][1::2] + columns = conditions.get("columns")[start:-2][2::2] labels = [column.split(":")[0] for column in columns] datapoints = [0] * len(labels) for row in data: # If group by filter, don't add first row of group (it's already summed) - if not row[start - 1]: + if not row[start]: continue # Remove None values and compute only periodic data row = [x if x else 0 for x in row[start:-2]] - row = row[1::2] + row = row[2::2] for i in range(len(row)): datapoints[i] += row[i] diff --git a/erpnext/selling/report/sales_order_trends/sales_order_trends.py b/erpnext/selling/report/sales_order_trends/sales_order_trends.py index fdd63cd5a68..0827110ae5d 100644 --- a/erpnext/selling/report/sales_order_trends/sales_order_trends.py +++ b/erpnext/selling/report/sales_order_trends/sales_order_trends.py @@ -24,22 +24,28 @@ def get_chart_data(data, conditions, filters): datapoints = [] - start = 3 if filters.get("based_on") in ["Item", "Customer"] else 1 + if filters.get("based_on") in ["Customer"]: + start = 3 + elif filters.get("based_on") in ["Item"]: + start = 2 + else: + start = 1 + if filters.get("group_by"): start += 1 # fetch only periodic columns as labels - columns = conditions.get("columns")[start:-2][1::2] + columns = conditions.get("columns")[start:-2][2::2] labels = [column.split(":")[0] for column in columns] datapoints = [0] * len(labels) for row in data: # If group by filter, don't add first row of group (it's already summed) - if not row[start - 1]: + if not row[start]: continue # Remove None values and compute only periodic data row = [x if x else 0 for x in row[start:-2]] - row = row[1::2] + row = row[2::2] for i in range(len(row)): datapoints[i] += row[i] From 0b96e1e3ef3132a4a07505b09dcaf45cd0a76d32 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 24 Jun 2025 15:52:24 +0530 Subject: [PATCH 42/45] chore: resolve conflicts --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 5045ff0d7e4..8fffd33903b 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3429,7 +3429,7 @@ class TestSalesInvoice(FrappeTestCase): si.posting_date = getdate() si.submit() - @IntegrationTestCase.change_settings("Accounts Settings", {"over_billing_allowance": 0}) + @change_settings("Accounts Settings", {"over_billing_allowance": 0}) def test_over_billing_case_against_delivery_note(self): """ Test a case where duplicating the item with qty = 1 in the invoice From f6bb86574e4cd3b113b5e4b976f319f68847ad72 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 24 Jun 2025 16:45:41 +0530 Subject: [PATCH 43/45] chore: fix test case for auto tax appending --- .../selling/doctype/sales_order/test_sales_order.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index edac4f70f19..ae807e5b1c0 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -177,6 +177,10 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): so.load_from_db() self.assertEqual(so.per_billed, 0) + @change_settings( + "Accounts Settings", + {"add_taxes_from_item_tax_template": 0, "add_taxes_from_taxes_and_charges_template": 1}, + ) def test_make_sales_invoice_with_terms(self): so = make_sales_order(do_not_submit=True) @@ -1828,6 +1832,10 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): self.assertEqual(so.items[0].work_order_qty, wo.produced_qty) self.assertEqual(mr.status, "Manufactured") + @change_settings( + "Accounts Settings", + {"add_taxes_from_item_tax_template": 0, "add_taxes_from_taxes_and_charges_template": 0}, + ) def test_sales_order_with_shipping_rule(self): from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule @@ -1853,6 +1861,10 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): sales_order.save() self.assertEqual(sales_order.taxes[0].tax_amount, 0) + @change_settings( + "Accounts Settings", + {"add_taxes_from_item_tax_template": 0, "add_taxes_from_taxes_and_charges_template": 1}, + ) def test_sales_order_partial_advance_payment(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( create_payment_entry, From a6c5738f4bf08f35a3639a4831f02c583b56bc9d Mon Sep 17 00:00:00 2001 From: Sugesh G <73237300+Sugesh393@users.noreply.github.com> Date: Wed, 29 Jan 2025 12:35:45 +0530 Subject: [PATCH 44/45] fix: use currency from opportunity while creating quotation (#45540) (cherry picked from commit d748b491ee2bbb2967175f58e1b04f5c2eda4722) --- erpnext/public/js/controllers/transaction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index b6f008f9664..bb0c69b3a94 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -914,7 +914,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } var get_party_currency = function() { - if (me.is_a_mapped_document()) { + if (me.is_a_mapped_document() || me.frm.doc.__onload?.load_after_mapping) { return; } From 41d22d0255c34b731a28205ce4d4b112e478c8e4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 24 Jun 2025 22:16:24 +0530 Subject: [PATCH 45/45] fix: stock adjustment entry to make stock balance zero (backport #48245) (#48247) fix: stock adjustment entry to make stock balance zero (#48245) (cherry picked from commit 66eeda641069f1b286800c750954ac829de2fac1) Co-authored-by: rohitwaghchaure --- .../doctype/stock_ledger_entry/stock_ledger_entry.py | 3 +++ .../stock_reconciliation/stock_reconciliation.py | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index a81c6c65517..11426aea51a 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -175,6 +175,9 @@ class StockLedgerEntry(Document): if frappe.flags.in_test and frappe.flags.ignore_serial_batch_bundle_validation: return + if self.is_adjustment_entry: + return + if not self.get("via_landed_cost_voucher"): SerialBatchBundle( sle=self, diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index d025de845e1..a7fb532f913 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -478,6 +478,8 @@ class StockReconciliation(StockController): frappe.db.set_value("Serial and Batch Entry", batch.name, update_values) def remove_items_with_no_change(self): + from erpnext.stock.stock_ledger import get_stock_value_difference + """Remove items if qty or rate is not changed""" self.difference_amount = 0.0 @@ -513,6 +515,14 @@ class StockReconciliation(StockController): company=self.company, ) + if not item_dict.get("qty") and not item.qty and not item.valuation_rate and not item.current_qty: + difference_amount = get_stock_value_difference( + item.item_code, item.warehouse, self.posting_date, self.posting_time, self.name + ) + + if abs(difference_amount) > 0: + return True + if ( (item.qty is None or item.qty == item_dict.get("qty")) and (item.valuation_rate is None or item.valuation_rate == item_dict.get("rate"))