From 91929138322bb584b868bbf5b7c5f39684e7b5e9 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 27 May 2025 19:21:41 +0200 Subject: [PATCH 01/21] feat: add column "Item Name" to "BOM Stock Report" (backport #47116) (#47485) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Patrick Eißler <77415730+PatrickDEissler@users.noreply.github.com> Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- .../manufacturing/report/bom_stock_report/bom_stock_report.py | 2 ++ .../report/bom_stock_report/test_bom_stock_report.py | 1 + 2 files changed, 3 insertions(+) diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py index 48ffbac5820..d233643c244 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py @@ -23,6 +23,7 @@ def get_columns(): """return columns""" columns = [ _("Item") + ":Link/Item:150", + _("Item Name") + "::240", _("Description") + "::300", _("BOM Qty") + ":Float:160", _("BOM UoM") + "::160", @@ -73,6 +74,7 @@ def get_bom_stock(filters): .on((BOM_ITEM.item_code == BIN.item_code) & (CONDITIONS)) .select( BOM_ITEM.item_code, + BOM_ITEM.item_name, BOM_ITEM.description, BOM_ITEM.stock_qty, BOM_ITEM.stock_uom, diff --git a/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py index 1c56ebe24d4..8a6915d7f5f 100644 --- a/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py +++ b/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py @@ -94,6 +94,7 @@ def get_expected_data(bom, warehouse, qty_to_produce, show_exploded_view=False): expected_data.append( [ item.item_code, + item.item_name, item.description, item.stock_qty, item.stock_uom, From 73bc57f53e80a8c2c20ec2f891109709331eb591 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 27 May 2025 21:11:49 +0200 Subject: [PATCH 02/21] Revert "fix: translate_pos_buttons" (backport #47773) (#47774) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> fix: translate_pos_buttons" (#47773) --- .../page/point_of_sale/pos_past_order_summary.js | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js index 0ad25b0a473..cf775176c07 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js @@ -321,25 +321,17 @@ erpnext.PointOfSale.PastOrderSummary = class { get_condition_btn_map(after_submission) { if (after_submission) - return [ - { - condition: true, - visible_btns: [__("Print Receipt"), __("Email Receipt"), __("New Order")], - }, - ]; + return [{ condition: true, visible_btns: ["Print Receipt", "Email Receipt", "New Order"] }]; return [ - { - condition: this.doc.docstatus === 0, - visible_btns: [__("Edit Order"), __("Delete Order")], - }, + { condition: this.doc.docstatus === 0, visible_btns: ["Edit Order", "Delete Order"] }, { condition: !this.doc.is_return && this.doc.docstatus === 1, - visible_btns: [__("Print Receipt"), __("Email Receipt"), __("Return")], + visible_btns: ["Print Receipt", "Email Receipt", "Return"], }, { condition: this.doc.is_return && this.doc.docstatus === 1, - visible_btns: [__("Print Receipt"), __("Email Receipt")], + visible_btns: ["Print Receipt", "Email Receipt"], }, ]; } From 8623a5650bbfc9161a64ee63ec8132d7690a5d74 Mon Sep 17 00:00:00 2001 From: Sanket322 Date: Sun, 13 Apr 2025 13:17:15 +0530 Subject: [PATCH 03/21] fix: check return_against exists before api call (cherry picked from commit 00b6b971978745f2a4e31e1ab22c983df9b561ff) --- .../public/js/controllers/taxes_and_totals.js | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 66b348e9eeb..541fbf1fb8a 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -896,23 +896,25 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { it should set the return to that mode of payment only. */ - let return_against_mop = await frappe.call({ - method: 'erpnext.controllers.sales_and_purchase_return.get_payment_data', - args: { - invoice: this.frm.doc.return_against - } - }); - - if (return_against_mop.message.length === 1) { - this.frm.doc.payments.forEach(payment => { - if (payment.mode_of_payment == return_against_mop.message[0].mode_of_payment) { - payment.amount = total_amount_to_pay; - } else { - payment.amount = 0; + if(this.frm.doc.return_against){ + let {message : return_against_mop } = await frappe.call({ + method: 'erpnext.controllers.sales_and_purchase_return.get_payment_data', + args: { + invoice: this.frm.doc.return_against } }); - this.frm.refresh_fields(); - return; + + if (return_against_mop.length === 1) { + this.frm.doc.payments.forEach(payment => { + if (payment.mode_of_payment == return_against_mop[0].mode_of_payment) { + payment.amount = total_amount_to_pay; + } else { + payment.amount = 0; + } + }); + this.frm.refresh_fields(); + return; + } } this.frm.doc.payments.find(payment => { From 5a23d7cdca32431e3563a966acb5eaae8f9b4610 Mon Sep 17 00:00:00 2001 From: Sanket322 Date: Sun, 13 Apr 2025 13:24:33 +0530 Subject: [PATCH 04/21] fix: ensure backend response is awaited before saving (cherry picked from commit c48db0b7c0391a6c87d552461756ad69f5866326) --- erpnext/public/js/controllers/taxes_and_totals.js | 2 +- erpnext/public/js/controllers/transaction.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 541fbf1fb8a..c50e6e53928 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -64,7 +64,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { && this.frm.doc.is_pos && this.frm.doc.is_return ) { - this.set_total_amount_to_default_mop(); + await this.set_total_amount_to_default_mop(); this.calculate_paid_amount(); } diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index db4783582ec..0f0d68b3d8b 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -870,8 +870,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe frappe.model.set_value(item.doctype, item.name, "stock_qty", valid_serial_nos.length); } - validate() { - this.calculate_taxes_and_totals(false); + async validate() { + await this.calculate_taxes_and_totals(false); } update_stock() { From 0c612be6fe345ef6b9011c6bdd85efc900d19c38 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 28 May 2025 18:13:42 +0530 Subject: [PATCH 05/21] feat: show item name for raw materials in BOM creator (cherry picked from commit 90ba4ad1e1ae5b44d7fecaa2278a5530b93c8aaa) --- erpnext/manufacturing/doctype/bom_creator/bom_creator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py index c4fb6345c93..6e778266b03 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py @@ -343,6 +343,7 @@ def get_children(doctype=None, parent=None, **kwargs): fields = [ "item_code as value", + "item_name as title", "is_expandable as expandable", "parent as parent_id", "qty", From ef9ccd7a5718e7e32d8894108dcfa3600561777c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 29 May 2025 12:25:08 +0530 Subject: [PATCH 06/21] chore: removed orphaned function (backport #47796) (#47804) chore: removed orphaned function (#47796) (cherry picked from commit cb9e6f66557caf277def082a32f7823b47c6a2c3) Co-authored-by: rohitwaghchaure --- .../controllers/sales_and_purchase_return.py | 95 ------------------- 1 file changed, 95 deletions(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index d1904fed7b6..475cd3f3844 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -431,99 +431,6 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai else: doc.run_method("calculate_taxes_and_totals") - def update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field): - from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos - from erpnext.stock.serial_batch_bundle import SerialBatchCreation - - returned_serial_nos = [] - returned_batches = frappe._dict() - serial_and_batch_field = ( - "serial_and_batch_bundle" if qty_field == "stock_qty" else "rejected_serial_and_batch_bundle" - ) - old_serial_no_field = "serial_no" if qty_field == "stock_qty" else "rejected_serial_no" - old_batch_no_field = "batch_no" - - if ( - source_doc.get(serial_and_batch_field) - or source_doc.get(old_serial_no_field) - or source_doc.get(old_batch_no_field) - ): - if item_details.has_serial_no: - returned_serial_nos = get_returned_serial_nos( - source_doc, source_parent, serial_no_field=serial_and_batch_field - ) - else: - returned_batches = get_returned_batches( - source_doc, source_parent, batch_no_field=serial_and_batch_field - ) - - type_of_transaction = "Inward" - if source_doc.get(serial_and_batch_field) and ( - frappe.db.get_value( - "Serial and Batch Bundle", source_doc.get(serial_and_batch_field), "type_of_transaction" - ) - == "Inward" - ): - type_of_transaction = "Outward" - elif source_parent.doctype in [ - "Purchase Invoice", - "Purchase Receipt", - "Subcontracting Receipt", - ]: - type_of_transaction = "Outward" - - warehouse = source_doc.warehouse if qty_field == "stock_qty" else source_doc.rejected_warehouse - if source_parent.doctype in [ - "Sales Invoice", - "POS Invoice", - "Delivery Note", - ] and source_parent.get("is_internal_customer"): - type_of_transaction = "Outward" - warehouse = source_doc.target_warehouse - - cls_obj = SerialBatchCreation( - { - "type_of_transaction": type_of_transaction, - "serial_and_batch_bundle": source_doc.get(serial_and_batch_field), - "returned_against": source_doc.name, - "item_code": source_doc.item_code, - "returned_serial_nos": returned_serial_nos, - "voucher_type": source_parent.doctype, - "do_not_submit": True, - "warehouse": warehouse, - "has_serial_no": item_details.has_serial_no, - "has_batch_no": item_details.has_batch_no, - } - ) - - serial_nos = [] - batches = frappe._dict() - if source_doc.get(old_batch_no_field): - batches = frappe._dict({source_doc.batch_no: source_doc.get(qty_field)}) - elif source_doc.get(old_serial_no_field): - serial_nos = get_serial_nos(source_doc.get(old_serial_no_field)) - elif source_doc.get(serial_and_batch_field): - if item_details.has_serial_no: - serial_nos = get_serial_nos_from_bundle(source_doc.get(serial_and_batch_field)) - else: - batches = get_batches_from_bundle(source_doc.get(serial_and_batch_field)) - - if serial_nos: - cls_obj.serial_nos = sorted(list(set(serial_nos) - set(returned_serial_nos))) - elif batches: - for batch in batches: - if batch in returned_batches: - batches[batch] -= flt(returned_batches.get(batch)) - - cls_obj.batches = batches - - if source_doc.get(serial_and_batch_field): - cls_obj.duplicate_package() - if cls_obj.serial_and_batch_bundle: - target_doc.set(serial_and_batch_field, cls_obj.serial_and_batch_bundle) - else: - target_doc.set(serial_and_batch_field, cls_obj.make_serial_and_batch_bundle().name) - def update_item(source_doc, target_doc, source_parent): target_doc.qty = -1 * source_doc.qty target_doc.pricing_rules = None @@ -866,8 +773,6 @@ def get_returned_serial_nos(child_doc, parent_doc, serial_no_field=None, ignore_ def get_returned_batches(child_doc, parent_doc, batch_no_field=None, ignore_voucher_detail_no=None): - from erpnext.stock.serial_batch_bundle import get_batches_from_bundle - batches = frappe._dict() old_field = "batch_no" From 2e3ebec53c95112b8008337d2a024a4b1882c282 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Thu, 29 May 2025 11:18:08 +0530 Subject: [PATCH 07/21] fix: use `query.walk() `for escaping special chars in receiable/payable report (cherry picked from commit a0a51b507496574d16caf5ce65c84806a08cf939) --- .../report/accounts_receivable/accounts_receivable.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index af15e365b25..263158b9ba7 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -125,7 +125,8 @@ class ReceivablePayableReport: self.build_data() def fetch_ple_in_buffered_cursor(self): - self.ple_entries = frappe.db.sql(self.ple_query.get_sql(), as_dict=True) + query, param = self.ple_query.walk() + self.ple_entries = frappe.db.sql(query, param, as_dict=True) for ple in self.ple_entries: self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding @@ -138,8 +139,9 @@ class ReceivablePayableReport: def fetch_ple_in_unbuffered_cursor(self): self.ple_entries = [] + query, param = self.ple_query.walk() with frappe.db.unbuffered_cursor(): - for ple in frappe.db.sql(self.ple_query.get_sql(), as_dict=True, as_iterator=True): + for ple in frappe.db.sql(query, param, as_dict=True, as_iterator=True): self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding self.ple_entries.append(ple) From 9df3b9b059daac6f799cc33e145035652d49b236 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 30 May 2025 14:01:32 +0530 Subject: [PATCH 08/21] fix: incorrect actual qty in product bundle balance report (backport #47791) (#47814) fix: incorrect actual qty in product bundle balance report (#47791) (cherry picked from commit c544c3e01810225c66da926963ff2758c6fd9b39) Co-authored-by: rohitwaghchaure --- .../product_bundle_balance.py | 65 ++++++++++++++----- 1 file changed, 48 insertions(+), 17 deletions(-) diff --git a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py index 10f8650b525..5a8960396b3 100644 --- a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py +++ b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py @@ -4,7 +4,7 @@ import frappe from frappe import _ -from frappe.query_builder.functions import IfNull +from frappe.query_builder.functions import IfNull, Max from frappe.utils import flt from pypika.terms import ExistsCriterion @@ -208,30 +208,21 @@ def get_stock_ledger_entries(filters, items): if not items: return [] - sle = frappe.qb.DocType("Stock Ledger Entry") - sle2 = frappe.qb.DocType("Stock Ledger Entry") + max_posting_datetime_query = get_item_wise_max_posting_datetime(filters, items) + sle = frappe.qb.DocType("Stock Ledger Entry") query = ( frappe.qb.from_(sle) - .left_join(sle2) + .join(max_posting_datetime_query) .on( - (sle.item_code == sle2.item_code) - & (sle.warehouse == sle2.warehouse) - & (sle.posting_datetime < sle2.posting_datetime) - & (sle.name < sle2.name) + (sle.item_code == max_posting_datetime_query.item_code) + & (sle.warehouse == max_posting_datetime_query.warehouse) + & (sle.posting_datetime == max_posting_datetime_query.posting_datetime) ) .select(sle.item_code, sle.warehouse, sle.qty_after_transaction, sle.company) - .where((sle2.name.isnull()) & (sle.docstatus < 2) & (sle.item_code.isin(items))) + .where(sle.is_cancelled == 0) ) - if filters.get("company"): - query = query.where(sle.company == filters.get("company")) - - if date := filters.get("date"): - query = query.where(sle.posting_date <= date) - else: - frappe.throw(_("'Date' is required")) - if filters.get("warehouse"): warehouse_details = frappe.db.get_value( "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 @@ -247,4 +238,44 @@ def get_stock_ledger_entries(filters, items): ) ) + if filters.get("company"): + query = query.where(sle.company == filters.get("company")) + + if filters.get("data"): + query = query.where(sle.posting_date <= filters.get("data")) + return query.run(as_dict=True) + + +def get_item_wise_max_posting_datetime(filters, items): + """Get the maximum Stock Ledger Entry name for the given filters and items.""" + sle = frappe.qb.DocType("Stock Ledger Entry") + query = ( + frappe.qb.from_(sle) + .select(sle.item_code, sle.warehouse, sle.name, Max(sle.posting_datetime).as_("posting_datetime")) + .where(sle.item_code.isin(items) & (sle.is_cancelled == 0)) + .groupby(sle.item_code, sle.warehouse) + ) + + if filters.get("warehouse"): + warehouse_details = frappe.db.get_value( + "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 + ) + + if warehouse_details: + wh = frappe.qb.DocType("Warehouse") + query = query.where( + sle.warehouse.isin( + frappe.qb.from_(wh) + .select(wh.name) + .where((wh.lft >= warehouse_details.lft) & (wh.rgt <= warehouse_details.rgt)) + ) + ) + + if filters.get("company"): + query = query.where(sle.company == filters.get("company")) + + if filters.get("data"): + query = query.where(sle.posting_date <= filters.get("data")) + + return query From 3879cbd86dfa75c665d122079cf366c1d372635e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 30 May 2025 15:36:51 +0530 Subject: [PATCH 09/21] fix: improved indexing for SLE queries. (backport #47194) (#47822) * fix: improved indexing for SLE queries. (#47194) (cherry picked from commit b49a835b4c1f1d4d3a39a09af609b7d7688cc992) # Conflicts: # erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json * chore: fix conflicts --------- Co-authored-by: rohitwaghchaure --- erpnext/patches.txt | 1 + erpnext/patches/v15_0/drop_sle_indexes.py | 17 +++++++++++++++++ .../stock_ledger_entry/stock_ledger_entry.json | 6 ++---- .../stock_ledger_entry/stock_ledger_entry.py | 3 +-- .../available_batch_report.py | 5 +++-- .../batch_wise_balance_history.py | 2 -- ...queue_vs_qty_after_transaction_comparison.py | 2 +- .../report/stock_analytics/stock_analytics.py | 8 ++++---- .../stock_ledger_invariant_check.py | 2 +- erpnext/stock/stock_ledger.py | 3 +-- erpnext/tests/test_perf.py | 1 - 11 files changed, 31 insertions(+), 19 deletions(-) create mode 100644 erpnext/patches/v15_0/drop_sle_indexes.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index d78e86bd1d7..57e5465f8d0 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -407,3 +407,4 @@ erpnext.patches.v14_0.set_update_price_list_based_on erpnext.patches.v15_0.set_cancelled_status_to_cancelled_pos_invoice erpnext.patches.v15_0.rename_group_by_to_categorize_by_in_custom_reports erpnext.patches.v14_0.update_full_name_in_contract +erpnext.patches.v15_0.drop_sle_indexes diff --git a/erpnext/patches/v15_0/drop_sle_indexes.py b/erpnext/patches/v15_0/drop_sle_indexes.py new file mode 100644 index 00000000000..8e18245b1a5 --- /dev/null +++ b/erpnext/patches/v15_0/drop_sle_indexes.py @@ -0,0 +1,17 @@ +import click +import frappe + + +def execute(): + table = "tabStock Ledger Entry" + index_list = ["posting_datetime_creation_index", "item_warehouse"] + + for index in index_list: + if not frappe.db.has_index(table, index): + continue + + try: + frappe.db.sql_ddl(f"ALTER TABLE `{table}` DROP INDEX `{index}`") + click.echo(f"✓ dropped {index} index from {table}") + except Exception: + frappe.log_error("Failed to drop index") diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json index 66fa6064129..ce3e3435130 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json @@ -57,7 +57,6 @@ "options": "Item", "print_width": "100px", "read_only": 1, - "search_index": 1, "width": "100px" }, { @@ -88,7 +87,6 @@ "options": "Warehouse", "print_width": "100px", "read_only": 1, - "search_index": 1, "width": "100px" }, { @@ -101,7 +99,6 @@ "oldfieldtype": "Date", "print_width": "100px", "read_only": 1, - "search_index": 1, "width": "100px" }, { @@ -357,13 +354,14 @@ "search_index": 1 } ], + "grid_page_length": 50, "hide_toolbar": 1, "icon": "fa fa-list", "idx": 1, "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2024-12-23 18:03:05.171023", + "modified": "2025-04-22 12:37:41.304109", "modified_by": "Administrator", "module": "Stock", "name": "Stock Ledger Entry", 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 6f5acd64d44..a81c6c65517 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -345,5 +345,4 @@ class StockLedgerEntry(Document): def on_doctype_update(): frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"]) frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"]) - frappe.db.add_index("Stock Ledger Entry", ["warehouse", "item_code"], "item_warehouse") - frappe.db.add_index("Stock Ledger Entry", ["posting_datetime", "creation"]) + frappe.db.add_index("Stock Ledger Entry", ["item_code", "warehouse", "posting_datetime", "creation"]) diff --git a/erpnext/stock/report/available_batch_report/available_batch_report.py b/erpnext/stock/report/available_batch_report/available_batch_report.py index f9ae94d2579..e5f14315773 100644 --- a/erpnext/stock/report/available_batch_report/available_batch_report.py +++ b/erpnext/stock/report/available_batch_report/available_batch_report.py @@ -6,7 +6,7 @@ from collections import defaultdict import frappe from frappe import _ from frappe.query_builder.functions import Sum -from frappe.utils import flt, today +from frappe.utils import flt, get_datetime, today def execute(filters=None): @@ -167,7 +167,8 @@ def get_query_based_on_filters(query, batch, table, filters): query = query.where(batch.batch_qty > 0) else: - query = query.where(table.posting_date <= filters.to_date) + to_date = get_datetime(str(filters.to_date) + " 23:59:59") + query = query.where(table.posting_datetime <= to_date) if filters.warehouse: lft, rgt = frappe.db.get_value("Warehouse", filters.warehouse, ["lft", "rgt"]) diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index dabd3b1c6e4..99e0676eca3 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -115,7 +115,6 @@ def get_stock_ledger_entries_for_batch_no(filters): & (sle.posting_datetime < posting_datetime) ) .groupby(sle.voucher_no, sle.batch_no, sle.item_code, sle.warehouse) - .orderby(sle.item_code, sle.warehouse) ) query = apply_warehouse_filter(query, sle, filters) @@ -160,7 +159,6 @@ def get_stock_ledger_entries_for_batch_bundle(filters): & (sle.posting_datetime <= to_date) ) .groupby(sle.voucher_no, batch_package.batch_no, batch_package.warehouse) - .orderby(sle.item_code, sle.warehouse) ) query = apply_warehouse_filter(query, sle, filters) diff --git a/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py index 404a58b799d..f885b78c98d 100644 --- a/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py +++ b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py @@ -66,7 +66,7 @@ def get_stock_ledger_entries(filters): "Stock Ledger Entry", fields=SLE_FIELDS, filters=sle_filters, - order_by="timestamp(posting_date, posting_time), creation", + order_by="posting_datetime, creation", ) diff --git a/erpnext/stock/report/stock_analytics/stock_analytics.py b/erpnext/stock/report/stock_analytics/stock_analytics.py index 920c315352c..16dea3c2942 100644 --- a/erpnext/stock/report/stock_analytics/stock_analytics.py +++ b/erpnext/stock/report/stock_analytics/stock_analytics.py @@ -5,8 +5,8 @@ import datetime import frappe from frappe import _, scrub from frappe.query_builder.functions import CombineDatetime +from frappe.utils import get_datetime, get_first_day_of_week, get_quarter_start, getdate from frappe.utils import get_first_day as get_first_day_of_month -from frappe.utils import get_first_day_of_week, get_quarter_start, getdate from frappe.utils.nestedset import get_descendants_of from erpnext.accounts.utils import get_fiscal_year @@ -294,9 +294,8 @@ def get_stock_ledger_entries(filters, items): sle.batch_no, ) .where((sle.docstatus < 2) & (sle.is_cancelled == 0)) - .orderby(CombineDatetime(sle.posting_date, sle.posting_time)) + .orderby(sle.posting_datetime) .orderby(sle.creation) - .orderby(sle.actual_qty) ) if items: @@ -314,7 +313,8 @@ def apply_conditions(query, filters): frappe.throw(_("'From Date' is required")) if to_date := filters.get("to_date"): - query = query.where(sle.posting_date <= to_date) + to_date = get_datetime(str(to_date) + " 23:59:59") + query = query.where(sle.posting_datetime <= to_date) else: frappe.throw(_("'To Date' is required")) diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py index 5cf653e3851..954acf998d8 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py +++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py @@ -44,7 +44,7 @@ def get_stock_ledger_entries(filters): "Stock Ledger Entry", fields=SLE_FIELDS, filters={"item_code": filters.item_code, "warehouse": filters.warehouse, "is_cancelled": 0}, - order_by="timestamp(posting_date, posting_time), creation", + order_by="posting_datetime, creation", ) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 0cda4baaadd..91641f1e35e 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1818,7 +1818,7 @@ def get_valuation_rate( # Get valuation rate from last sle for the same item and warehouse if last_valuation_rate := frappe.db.sql( # nosemgrep """select valuation_rate - from `tabStock Ledger Entry` force index (item_warehouse) + from `tabStock Ledger Entry` where item_code = %s AND warehouse = %s @@ -1962,7 +1962,6 @@ def get_next_stock_reco(kwargs): sle.actual_qty, sle.has_batch_no, ) - .force_index("item_warehouse") .where( (sle.item_code == kwargs.get("item_code")) & (sle.warehouse == kwargs.get("warehouse")) diff --git a/erpnext/tests/test_perf.py b/erpnext/tests/test_perf.py index db54ca97395..5cd98c1947f 100644 --- a/erpnext/tests/test_perf.py +++ b/erpnext/tests/test_perf.py @@ -5,7 +5,6 @@ INDEXED_FIELDS = { "Bin": ["item_code"], "GL Entry": ["voucher_no", "posting_date", "company", "party"], "Purchase Order Item": ["item_code"], - "Stock Ledger Entry": ["warehouse"], } From ba8a316b06a58aabd3f4f86191c8caaeedfeadd1 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 31 May 2025 20:52:14 +0530 Subject: [PATCH 10/21] fix: calculate discount percentage if discount amount is specified (#47806) (cherry picked from commit bb474f4f42aa6c5385a63df31c9d9af14238fad8) --- erpnext/controllers/taxes_and_totals.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 5543129d323..c47f04b71f1 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -13,6 +13,7 @@ from frappe.utils.deprecations import deprecated import erpnext from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_rate from erpnext.accounts.doctype.pricing_rule.utils import get_applied_pricing_rules +from erpnext.accounts.utils import get_currency_precision from erpnext.controllers.accounts_controller import ( validate_conversion_rate, validate_inclusive_tax, @@ -674,7 +675,16 @@ class calculate_taxes_and_totals: tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(",", ":")) def set_discount_amount(self): - if self.doc.additional_discount_percentage: + if self.doc.discount_amount: + self.doc.additional_discount_percentage = flt( + flt( + self.doc.discount_amount / flt(self.doc.get(scrub(self.doc.apply_discount_on))), + get_currency_precision(), + ) + * 100, + self.doc.precision("additional_discount_percentage"), + ) + elif self.doc.additional_discount_percentage: self.doc.discount_amount = flt( flt(self.doc.get(scrub(self.doc.apply_discount_on))) * self.doc.additional_discount_percentage From 34b62d226c5290158bc8baa37c21f4b1be817639 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 2 Jun 2025 13:24:41 +0530 Subject: [PATCH 11/21] fix: decimal issue (#47839) (cherry picked from commit 0dbd9efc9189d7efb6258adef727764c3f865f4a) --- .../serial_and_batch_bundle/serial_and_batch_bundle.py | 6 +++--- 1 file changed, 3 insertions(+), 3 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 af2915692ce..b83f3c58df9 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 @@ -722,19 +722,19 @@ class SerialandBatchBundle(Document): def reset_qty(self, row, qty_field=None): qty_field = self.get_qty_field(row, qty_field=qty_field) - qty = abs(row.get(qty_field)) + qty = abs(flt(row.get(qty_field), self.precision("total_qty"))) idx = None while qty > 0: for d in self.entries: - row_qty = abs(d.qty) + row_qty = abs(flt(d.qty, d.precision("qty"))) if row_qty >= qty: d.db_set("qty", qty if self.type_of_transaction == "Inward" else qty * -1) qty = 0 idx = d.idx break else: - qty -= row_qty + qty = flt(qty - row_qty, d.precision("qty")) idx = d.idx if idx and len(self.entries) > idx: From 5ebf1b9cc4a803698c05695747a9da836a4ec68e Mon Sep 17 00:00:00 2001 From: ljain112 Date: Wed, 28 May 2025 13:56:42 +0530 Subject: [PATCH 12/21] fix: add company filter to cost center and project in process statement of accounts (cherry picked from commit 14313b162af458b9e912fe01eb3e1104b3f4394a) --- .../process_statement_of_accounts.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js index 25274f1be81..40534059711 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js @@ -60,6 +60,20 @@ frappe.ui.form.on("Process Statement Of Accounts", { }, }; }); + frm.set_query("cost_center", function () { + return { + filters: { + company: frm.doc.company, + }, + }; + }); + frm.set_query("project", function () { + return { + filters: { + company: frm.doc.company, + }, + }; + }); if (frm.doc.__islocal) { frm.set_value("from_date", frappe.datetime.add_months(frappe.datetime.get_today(), -1)); frm.set_value("to_date", frappe.datetime.get_today()); From 9f5cfdd65bba076ed3f1e82d19eb6a9432c52242 Mon Sep 17 00:00:00 2001 From: l0gesh29 Date: Mon, 2 Jun 2025 13:35:23 +0530 Subject: [PATCH 13/21] fix: Accounts receivable shouldn't fetch DN for employees * fix: reorder function call * fix: Add condition to fetch return entries for specific party types (cherry picked from commit c8e052f3c6bcf0126b53ad9ae011f95df431c6b4) --- .../report/accounts_receivable/accounts_receivable.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 263158b9ba7..268d246ea8e 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -98,9 +98,6 @@ class ReceivablePayableReport: def get_data(self): self.get_sales_invoices_or_customers_based_on_sales_person() - # Build delivery note map against all sales invoices - self.build_delivery_note_map() - # Get invoice details like bill_no, due_date etc for all invoices self.get_invoice_details() @@ -108,7 +105,8 @@ class ReceivablePayableReport: self.get_future_payments() # Get return entries - self.get_return_entries() + if not self.filters.party_type or self.filters.party_type in ["Customer", "Supplier"]: + self.get_return_entries() # Get Exchange Rate Revaluations self.get_exchange_rate_revaluations() @@ -122,6 +120,9 @@ class ReceivablePayableReport: elif self.ple_fetch_method == "UnBuffered Cursor": self.fetch_ple_in_unbuffered_cursor() + # Build delivery note map against all sales invoices + self.build_delivery_note_map() + self.build_data() def fetch_ple_in_buffered_cursor(self): From 4c1b415b9d8733a1cb611e4f91eb03b5d7163700 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhodawala <99460106+Abdeali099@users.noreply.github.com> Date: Mon, 2 Jun 2025 14:49:15 +0530 Subject: [PATCH 14/21] fix: Handle duplicate Items qty in Quotation fix: Handle duplicate Items qty in Quotation (cherry picked from commit 39f6d8ffb68466fb4af4cfa58bebdbb35814ed42) --- .../selling/doctype/quotation/quotation.py | 71 ++++++++++--------- .../doctype/quotation/test_quotation.py | 46 ++++++++++++ 2 files changed, 85 insertions(+), 32 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 7816485a9f0..70084764799 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -174,29 +174,22 @@ class Quotation(SellingController): ) def get_ordered_status(self): - status = "Open" - ordered_items = frappe._dict( - frappe.db.get_all( - "Sales Order Item", - {"prevdoc_docname": self.name, "docstatus": 1}, - ["item_code", "sum(qty)"], - group_by="item_code", - as_list=1, - ) - ) + ordered_items = get_ordered_items(self.name) if not ordered_items: - return status + return "Open" - has_alternatives = any(row.is_alternative for row in self.get("items")) - self._items = self.get_valid_items() if has_alternatives else self.get("items") + self._items = ( + self.get_valid_items() + if any(row.is_alternative for row in self.get("items")) + else self.get("items") + ) - if any(row.qty > ordered_items.get(row.item_code, 0.0) for row in self._items): - status = "Partially Ordered" - else: - status = "Ordered" + for row in self._items: + if row.name not in ordered_items or row.qty > ordered_items[row.name]: + return "Partially Ordered" - return status + return "Ordered" def get_valid_items(self): """ @@ -371,15 +364,7 @@ def make_sales_order(source_name: str, target_doc=None): def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): customer = _make_customer(source_name, ignore_permissions) - ordered_items = frappe._dict( - frappe.db.get_all( - "Sales Order Item", - {"prevdoc_docname": source_name, "docstatus": 1}, - ["item_code", "sum(qty)"], - group_by="item_code", - as_list=1, - ) - ) + ordered_items = get_ordered_items(source_name) selected_rows = [x.get("name") for x in frappe.flags.get("args", {}).get("selected_items", [])] @@ -417,7 +402,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): target.run_method("calculate_taxes_and_totals") def update_item(obj, target, source_parent): - balance_qty = obj.qty if is_unit_price_row(obj) else obj.qty - ordered_items.get(obj.item_code, 0.0) + balance_qty = obj.qty if is_unit_price_row(obj) else obj.qty - ordered_items.get(obj.name, 0.0) target.qty = balance_qty if balance_qty > 0 else 0 target.stock_qty = flt(target.qty) * flt(obj.conversion_factor) @@ -433,10 +418,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): 2. If selections: Is Alternative Item/Has Alternative Item: Map if selected and adequate qty 3. If no selections: Simple row: Map if adequate qty """ - balance_qty = item.qty - ordered_items.get(item.item_code, 0.0) - has_valid_qty: bool = (balance_qty > 0) or is_unit_price_row(item) - - if not has_valid_qty: + if not ((item.qty > ordered_items.get(item.name, 0.0)) or is_unit_price_row(item)): return False if not selected_rows: @@ -603,3 +585,28 @@ def handle_mandatory_error(e, customer, lead_name): message += _("Please create Customer from Lead {0}.").format(get_link_to_form("Lead", lead_name)) frappe.throw(message, title=_("Mandatory Missing")) + + +def get_ordered_items(quotation: str): + """ + Returns a dict of ordered items with their total qty based on quotation row name. + + In `Sales Order Item`, `quotation_item` is the row name of `Quotation Item`. + + Example: + ``` + { + "refsdjhd2": 10, + "ygdhdshrt": 5, + } + ``` + """ + return frappe._dict( + frappe.get_all( + "Sales Order Item", + filters={"prevdoc_docname": quotation, "docstatus": 1}, + fields=["quotation_item", "sum(qty)"], + group_by="quotation_item", + as_list=1, + ) + ) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index f6f4d98d541..2dd08b91b78 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -815,6 +815,52 @@ class TestQuotation(FrappeTestCase): quotation.reload() self.assertEqual(quotation.status, "Ordered") + def test_duplicate_items_in_quotation(self): + from erpnext.selling.doctype.quotation.quotation import make_sales_order + from erpnext.stock.doctype.item.test_item import make_item + + # item code same but description different + make_item("_Test Item 2", {"is_stock_item": 1}) + + quotation = make_quotation(qty=1, rate=100, do_not_submit=1) + + # duplicate items + for qty in [1, 1, 2, 3]: + quotation.append("items", {"item_code": "_Test Item", "qty": qty, "rate": 100}) + + quotation.append("items", {"item_code": "_Test Item 2", "qty": 5, "rate": 100}) + + quotation.submit() + + sales_order = make_sales_order(quotation.name) + sales_order.delivery_date = nowdate() + + self.assertEqual(len(sales_order.items), 6) + self.assertEqual(sales_order.items[0].qty, 1) + self.assertEqual(sales_order.items[-1].qty, 5) + + # Row 1: 10, Row 4: 1, Row 5: 1 + sales_order.items[0].qty = 10 + sales_order.items[3].qty = 1 + sales_order.items[4].qty = 1 + sales_order.submit() + + quotation.reload() + self.assertEqual(quotation.status, "Partially Ordered") + + sales_order_2 = make_sales_order(quotation.name) + sales_order_2.delivery_date = nowdate() + self.assertEqual(len(sales_order_2.items), 2) + self.assertEqual(sales_order_2.items[0].qty, 1) + self.assertEqual(sales_order_2.items[1].qty, 2) + + self.assertEqual(sales_order_2.items[0].quotation_item, quotation.items[3].name) + self.assertEqual(sales_order_2.items[1].quotation_item, quotation.items[4].name) + + sales_order_2.submit() + quotation.reload() + self.assertEqual(quotation.status, "Ordered") + test_records = frappe.get_test_records("Quotation") From 3c697e90a3a04327d5b4d8b0193e48278a38e26c Mon Sep 17 00:00:00 2001 From: Karuppasamy923 Date: Wed, 28 May 2025 12:59:49 +0530 Subject: [PATCH 15/21] fix: add internal link field in Sales Order connections for internal transactions (cherry picked from commit e3e65030765c26dbbda791f291235249aa8c4707) --- erpnext/selling/doctype/sales_order/sales_order_dashboard.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py index 7c1c0deb33f..55dba73934b 100644 --- a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py +++ b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py @@ -17,6 +17,7 @@ def get_data(): "Quotation": ["items", "prevdoc_docname"], "BOM": ["items", "bom_no"], "Blanket Order": ["items", "blanket_order"], + "Purchase Order": ["items", "purchase_order"], }, "transactions": [ { From 4a1966c6807800556d78ddba5880e229c5e6ca05 Mon Sep 17 00:00:00 2001 From: Lakshit Jain <108322669+ljain112@users.noreply.github.com> Date: Tue, 3 Jun 2025 11:22:02 +0530 Subject: [PATCH 16/21] fix: cash flow report fixes (cherry picked from commit 20b87512d10530d6639d32b2c3845a87804f6ec8) --- erpnext/accounts/report/cash_flow/cash_flow.py | 6 +++++- erpnext/accounts/report/financial_statements.html | 6 +++--- erpnext/public/js/financial_statements.js | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/report/cash_flow/cash_flow.py b/erpnext/accounts/report/cash_flow/cash_flow.py index 562ac5efb81..c647c5b2e4f 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.py +++ b/erpnext/accounts/report/cash_flow/cash_flow.py @@ -75,7 +75,11 @@ def execute(filters=None): # add first net income in operations section if net_profit_loss: net_profit_loss.update( - {"indent": 1, "parent_section": cash_flow_sections[0]["section_header"]} + { + "indent": 1, + "parent_section": cash_flow_sections[0]["section_header"], + "section": net_profit_loss["account"], + } ) data.append(net_profit_loss) section_data.append(net_profit_loss) diff --git a/erpnext/accounts/report/financial_statements.html b/erpnext/accounts/report/financial_statements.html index dc1d0093bce..45d56a92037 100644 --- a/erpnext/accounts/report/financial_statements.html +++ b/erpnext/accounts/report/financial_statements.html @@ -47,12 +47,12 @@ {% for(let j=0, k=data.length; j - {%= row.account_name %} + {%= row.account_name || row.section %} {% for(let i=1, l=report_columns.length; i diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js index 78796c4feb8..f9eddcf2889 100644 --- a/erpnext/public/js/financial_statements.js +++ b/erpnext/public/js/financial_statements.js @@ -45,7 +45,7 @@ erpnext.financial_statements = { } } - if (data && column.fieldname == "account") { + if (data && column.fieldname == this.name_field) { // first column value = data.section_name || data.account_name || value; From 470534af7825402b20a72f96e46a7d25c0c2689d Mon Sep 17 00:00:00 2001 From: Marc Ramser Date: Tue, 3 Jun 2025 08:29:13 +0200 Subject: [PATCH 17/21] fix(Timesheet): Only update to_time if it's more than 1 second off (#47702) * Fix: Only update to_time if it's more than 1 second off Before, to_time was updated even when it was almost the same as the expected time (like 17:20 vs 17:19:59.998). This causes problems because of small rounding errors and caused valid times like 17:20 to be reset. Now, to_time is only updated if the difference is greater than 1 second. To reproduce the current error: * From Time 09:00:00 * To Time 17:20:00 Save To Time is 17:19:59 * Update erpnext/projects/doctype/timesheet/timesheet.py Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> * Update timesheet.py --------- Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- erpnext/projects/doctype/timesheet/timesheet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index 758b25f0de0..1dcee64e54f 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -7,7 +7,7 @@ import json import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import add_to_date, flt, get_datetime, getdate, time_diff_in_hours +from frappe.utils import add_to_date, flt, get_datetime, getdate, time_diff_in_hours, time_diff_in_seconds from erpnext.controllers.queries import get_match_cond from erpnext.setup.utils import get_exchange_rate @@ -194,7 +194,7 @@ class Timesheet(Document): return _to_time = get_datetime(add_to_date(data.from_time, hours=data.hours, as_datetime=True)) - if data.to_time != _to_time: + if abs(time_diff_in_seconds(_to_time, data.to_time)) >= 1: data.to_time = _to_time def validate_overlap(self, data): From 01dd7337a2964b6ffc762c7c25f0ee72dfa6540d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 12:28:12 +0530 Subject: [PATCH 18/21] =?UTF-8?q?feat:=20specify=20expense=20account=20and?= =?UTF-8?q?=20cost=20center=20for=20raw=20materials=20in=20Su=E2=80=A6=20(?= =?UTF-8?q?backport=20#47756)=20(#47861)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../subcontracting_receipt.py | 99 ++++++++++++++----- .../test_subcontracting_receipt.py | 50 ++++++++++ .../subcontracting_receipt_supplied_item.json | 35 ++++++- .../subcontracting_receipt_supplied_item.py | 2 + 4 files changed, 157 insertions(+), 29 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 300f7a774eb..73474f2afe5 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -10,6 +10,10 @@ import erpnext from erpnext.accounts.utils import get_account_currency from erpnext.buying.utils import check_on_hold_or_closed_status from erpnext.controllers.subcontracting_controller import SubcontractingController +from erpnext.setup.doctype.brand.brand import get_brand_defaults +from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults +from erpnext.stock.doctype.item.item import get_item_defaults +from erpnext.stock.get_item_details import get_default_cost_center, get_default_expense_account from erpnext.stock.stock_ledger import get_valuation_rate @@ -140,6 +144,9 @@ class SubcontractingReceipt(SubcontractingController): self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse") self.get_current_stock() + self.set_supplied_items_expense_account() + self.set_supplied_items_cost_center() + def on_submit(self): self.validate_closed_subcontracting_order() self.validate_available_qty_for_consumption() @@ -224,6 +231,17 @@ class SubcontractingReceipt(SubcontractingController): if not item.cost_center: item.cost_center = cost_center + def set_supplied_items_cost_center(self): + for item in self.supplied_items: + if not item.cost_center: + item.cost_center = get_default_cost_center( + {"project": self.project}, + get_item_defaults(item.rm_item_code, self.company), + get_item_group_defaults(item.rm_item_code, self.company), + get_brand_defaults(item.rm_item_code, self.company), + self.company, + ) + def set_items_expense_account(self): if self.company: expense_account = self.get_company_default("default_expense_account", ignore_validation=True) @@ -232,6 +250,22 @@ class SubcontractingReceipt(SubcontractingController): if not item.expense_account: item.expense_account = expense_account + def set_supplied_items_expense_account(self): + for item in self.supplied_items: + if not item.expense_account: + item.expense_account = get_default_expense_account( + frappe._dict( + { + "expense_account": self.get_company_default( + "default_expense_account", ignore_validation=True + ) + } + ), + get_item_defaults(item.rm_item_code, self.company), + get_item_group_defaults(item.rm_item_code, self.company), + get_brand_defaults(item.rm_item_code, self.company), + ) + def reset_supplied_items(self): if ( frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on") @@ -519,6 +553,18 @@ class SubcontractingReceipt(SubcontractingController): def make_item_gl_entries(self, gl_entries, warehouse_account=None): warehouse_with_no_account = [] + supplied_items_details = frappe._dict() + for item in self.supplied_items: + supplied_items_details.setdefault(item.reference_name, []).append( + frappe._dict( + { + "amount": item.amount, + "expense_account": item.expense_account, + "cost_center": item.cost_center, + } + ) + ) + for item in self.items: if flt(item.rate) and flt(item.qty): if warehouse_account.get(item.warehouse): @@ -568,32 +614,33 @@ class SubcontractingReceipt(SubcontractingController): ) if flt(item.rm_supp_cost) and supplier_warehouse_account: - # Supplier Warehouse Account (Credit) - self.add_gl_entry( - gl_entries=gl_entries, - account=supplier_warehouse_account, - cost_center=item.cost_center, - debit=0.0, - credit=flt(item.rm_supp_cost), - remarks=remarks, - against_account=item.expense_account, - account_currency=get_account_currency(supplier_warehouse_account), - project=item.project, - item=item, - ) - # Expense Account (Debit) - self.add_gl_entry( - gl_entries=gl_entries, - account=item.expense_account, - cost_center=item.cost_center, - debit=flt(item.rm_supp_cost), - credit=0.0, - remarks=remarks, - against_account=supplier_warehouse_account, - account_currency=get_account_currency(item.expense_account), - project=item.project, - item=item, - ) + for rm_item in supplied_items_details.get(item.name): + # Supplier Warehouse Account (Credit) + self.add_gl_entry( + gl_entries=gl_entries, + account=supplier_warehouse_account, + cost_center=rm_item.cost_center, + debit=0.0, + credit=flt(rm_item.amount), + remarks=remarks, + against_account=rm_item.expense_account, + account_currency=get_account_currency(supplier_warehouse_account), + project=item.project, + item=item, + ) + # Expense Account (Debit) + self.add_gl_entry( + gl_entries=gl_entries, + account=rm_item.expense_account, + cost_center=rm_item.cost_center, + debit=flt(rm_item.amount), + credit=0.0, + remarks=remarks, + against_account=supplier_warehouse_account, + account_currency=get_account_currency(item.expense_account), + project=item.project, + item=item, + ) # Expense Account (Debit) if item.additional_cost_per_qty: diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index b5d190f7736..55e950856bc 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -372,6 +372,56 @@ class TestSubcontractingReceipt(FrappeTestCase): self.assertTrue(get_gl_entries("Subcontracting Receipt", scr.name)) frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1) + @change_settings("Stock Settings", {"use_serial_batch_fields": 0}) + def test_subcontracting_receipt_gl_entry_with_different_rm_expense_accounts(self): + service_items = [ + { + "warehouse": "Stores - TCP1", + "item_code": "Subcontracted Service Item 7", + "qty": 10, + "rate": 100, + "fg_item": "Subcontracted Item SA4", + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work In Progress - TCP1", + service_items=service_items, + ) + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr = make_subcontracting_receipt(sco.name) + scr.save() + scr.supplied_items[1].expense_account = "_Test Write Off - TCP1" + scr.save() + scr.submit() + + for item in scr.supplied_items: + self.assertTrue(item.expense_account) + + gl_entries = get_gl_entries("Subcontracting Receipt", scr.name) + self.assertTrue(gl_entries) + + fg_warehouse_ac = get_inventory_account(scr.company, scr.items[0].warehouse) + expense_account = scr.items[0].expense_account + expected_values = { + fg_warehouse_ac: [4000, 3000], + expense_account: [2000, 4000], + "_Test Write Off - TCP1": [1000, 0], + } + + for gle in gl_entries: + self.assertEqual(expected_values[gle.account][0], gle.debit) + self.assertEqual(expected_values[gle.account][1], gle.credit) + @change_settings("Stock Settings", {"use_serial_batch_fields": 0}) def test_subcontracting_receipt_with_zero_service_cost(self): warehouse = "Stores - TCP1" diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json index e1927a5467a..ce3494e879d 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json @@ -33,7 +33,11 @@ "section_break_zwnh", "serial_no", "column_break_qibi", - "batch_no" + "batch_no", + "accounting_details_section", + "expense_account", + "accounting_dimensions_section", + "cost_center" ], "fields": [ { @@ -103,7 +107,7 @@ { "fieldname": "stock_uom", "fieldtype": "Link", - "label": "Stock Uom", + "label": "Stock UOM", "options": "UOM", "read_only": 1 }, @@ -231,18 +235,43 @@ "fieldname": "add_serial_batch_bundle", "fieldtype": "Button", "label": "Add Serial / Batch Bundle" + }, + { + "fieldname": "accounting_details_section", + "fieldtype": "Section Break", + "label": "Accounting Details" + }, + { + "fieldname": "expense_account", + "fieldtype": "Link", + "label": "Expense Account", + "options": "Account" + }, + { + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))", + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center", + "print_hide": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2024-03-30 10:26:27.237371", + "modified": "2025-05-27 12:33:58.772638", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Supplied Item", "naming_rule": "Autoincrement", "owner": "Administrator", "permissions": [], + "row_format": "Dynamic", "sort_field": "modified", "sort_order": "DESC", "states": [], diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.py b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.py index 8f09197aa83..1c6ae80969a 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.py @@ -20,8 +20,10 @@ class SubcontractingReceiptSuppliedItem(Document): bom_detail_no: DF.Data | None consumed_qty: DF.Float conversion_factor: DF.Float + cost_center: DF.Link | None current_stock: DF.Float description: DF.TextEditor | None + expense_account: DF.Link | None item_name: DF.Data | None main_item_code: DF.Link | None parent: DF.Data From 7d828dc17e68b86dde3c5d71b2beef462ac8289b Mon Sep 17 00:00:00 2001 From: Ayush Marhatta Date: Mon, 2 Jun 2025 18:21:07 +0545 Subject: [PATCH 19/21] fix: use user default for company instead of global default in purchase order analysis report (cherry picked from commit 49f23513e0247fd2c889aa83321c165f5fec5ef6) --- .../report/purchase_order_analysis/purchase_order_analysis.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js index 99b4c26ac8e..54d1cdc8b93 100644 --- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js +++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js @@ -10,7 +10,7 @@ frappe.query_reports["Purchase Order Analysis"] = { width: "80", options: "Company", reqd: 1, - default: frappe.defaults.get_default("company"), + default: frappe.defaults.get_user_default("company"), }, { fieldname: "from_date", From 3582b32f0381ca45725d59183306c67dfe38526a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 16:25:53 +0530 Subject: [PATCH 20/21] feat: allow to set valuation rate for Rejected Materials (backport #47582) (#47869) feat: allow to set valuation rate for Rejected Materials (#47582) (cherry picked from commit ca0e53dd78a34753d53c5846f31a00cdd2d6967e) Co-authored-by: rohitwaghchaure --- .../buying_settings/buying_settings.json | 44 +++++-- .../buying_settings/buying_settings.py | 4 + erpnext/controllers/buying_controller.py | 6 +- .../purchase_receipt/purchase_receipt.py | 28 +++- .../purchase_receipt/test_purchase_receipt.py | 120 ++++++++++++++++++ 5 files changed, 190 insertions(+), 12 deletions(-) diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 1b1df3fa1e0..9a2d07dbec2 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -12,21 +12,25 @@ "column_break_4", "maintain_same_rate_action", "role_to_override_stop_action", - "transaction_settings_section", + "section_break_xmlt", "po_required", - "pr_required", "blanket_order_allowance", + "column_break_sbwq", + "pr_required", "project_update_frequency", - "column_break_12", - "maintain_same_rate", + "transaction_settings_section", + "column_break_fcyl", "set_landed_cost_based_on_purchase_invoice_rate", - "allow_multiple_items", - "bill_for_rejected_quantity_in_purchase_invoice", - "disable_last_purchase_rate", - "show_pay_button", + "allow_zero_qty_in_supplier_quotation", "use_transaction_date_exchange_rate", "allow_zero_qty_in_request_for_quotation", - "allow_zero_qty_in_supplier_quotation", + "column_break_12", + "maintain_same_rate", + "allow_multiple_items", + "bill_for_rejected_quantity_in_purchase_invoice", + "set_valuation_rate_for_rejected_materials", + "disable_last_purchase_rate", + "show_pay_button", "allow_zero_qty_in_purchase_order", "subcontract", "backflush_raw_materials_of_subcontract_based_on", @@ -231,6 +235,26 @@ "fieldname": "allow_zero_qty_in_supplier_quotation", "fieldtype": "Check", "label": "Allow Supplier Quotation with Zero Quantity" + }, + { + "fieldname": "section_break_xmlt", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_sbwq", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_fcyl", + "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "bill_for_rejected_quantity_in_purchase_invoice", + "description": "If enabled, the system will generate an accounting entry for materials rejected in the Purchase Receipt.", + "fieldname": "set_valuation_rate_for_rejected_materials", + "fieldtype": "Check", + "label": "Set Valuation Rate for Rejected Materials" } ], "grid_page_length": 50, @@ -239,7 +263,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-05-06 15:21:49.639642", + "modified": "2025-05-16 15:56:38.321369", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.py b/erpnext/buying/doctype/buying_settings/buying_settings.py index 4dde7c8dabf..f3b3dbefff4 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.py +++ b/erpnext/buying/doctype/buying_settings/buying_settings.py @@ -38,6 +38,7 @@ class BuyingSettings(Document): project_update_frequency: DF.Literal["Each Transaction", "Manual"] role_to_override_stop_action: DF.Link | None set_landed_cost_based_on_purchase_invoice_rate: DF.Check + set_valuation_rate_for_rejected_materials: DF.Check show_pay_button: DF.Check supp_master_name: DF.Literal["Supplier Name", "Naming Series", "Auto Name"] supplier_group: DF.Link | None @@ -57,6 +58,9 @@ class BuyingSettings(Document): hide_name_field=False, ) + if not self.bill_for_rejected_quantity_in_purchase_invoice: + self.set_valuation_rate_for_rejected_materials = 0 + def before_save(self): self.check_maintain_same_rate() diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 385dce625fb..055b09c2d31 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -658,6 +658,10 @@ class BuyingController(SubcontractingController): sl_entries.append(from_warehouse_sle) if flt(d.rejected_qty) != 0: + valuation_rate_for_rejected_item = 0.0 + if frappe.db.get_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials"): + valuation_rate_for_rejected_item = d.valuation_rate + sl_entries.append( self.get_sl_entries( d, @@ -666,7 +670,7 @@ class BuyingController(SubcontractingController): "actual_qty": flt( flt(d.rejected_qty) * flt(d.conversion_factor), d.precision("stock_qty") ), - "incoming_rate": 0.0, + "incoming_rate": valuation_rate_for_rejected_item, "serial_and_batch_bundle": d.rejected_serial_and_batch_bundle, }, ) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 29481473d7f..903a216d9db 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -496,6 +496,14 @@ class PurchaseReceipt(BuyingController): outgoing_amount = abs(get_stock_value_difference(self.name, item.name, item.from_warehouse)) credit_amount = outgoing_amount + if item.get("rejected_qty") and frappe.db.get_single_value( + "Buying Settings", "set_valuation_rate_for_rejected_materials" + ): + outgoing_amount += abs( + get_stock_value_difference(self.name, item.name, item.rejected_warehouse) + ) + credit_amount = outgoing_amount + if credit_amount: if not account: validate_account("Stock or Asset Received But Not Billed") @@ -629,6 +637,14 @@ class PurchaseReceipt(BuyingController): valuation_amount_as_per_doc - flt(stock_value_diff), item.precision("base_net_amount") ) + if item.get("rejected_qty") and frappe.db.get_single_value( + "Buying Settings", "set_valuation_rate_for_rejected_materials" + ): + rejected_item_cost = abs( + get_stock_value_difference(self.name, item.name, item.rejected_warehouse) + ) + divisional_loss -= rejected_item_cost + if divisional_loss: loss_account = ( self.get_company_default("default_expense_account", ignore_validation=True) @@ -726,13 +742,23 @@ class PurchaseReceipt(BuyingController): make_sub_contracting_gl_entries(d) make_divisional_loss_gl_entry(d, outgoing_amount) elif (d.warehouse and d.warehouse not in warehouse_with_no_account) or ( - d.rejected_warehouse and d.rejected_warehouse not in warehouse_with_no_account + not frappe.db.get_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials") + and d.rejected_warehouse + and d.rejected_warehouse not in warehouse_with_no_account ): warehouse_with_no_account.append(d.warehouse or d.rejected_warehouse) if d.is_fixed_asset and d.landed_cost_voucher_amount: self.update_assets(d, d.valuation_rate) + if d.rejected_qty and frappe.db.get_single_value( + "Buying Settings", "set_valuation_rate_for_rejected_materials" + ): + stock_value_diff = get_stock_value_difference(self.name, d.name, d.rejected_warehouse) + stock_asset_account_name = warehouse_account[d.rejected_warehouse]["account"] + + make_item_asset_inward_gl_entry(d, stock_value_diff, stock_asset_account_name) + if warehouse_with_no_account: frappe.msgprint( _("No accounting entries for the following warehouses") diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 5dcadc6afc2..ae762093182 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -4199,6 +4199,126 @@ class TestPurchaseReceipt(FrappeTestCase): # Test 3 - OverAllowanceError should be thrown as qty is greater than qty in DN self.assertRaises(erpnext.controllers.status_updater.OverAllowanceError, pr.submit) + def test_valuation_rate_for_rejected_materials(self): + item = make_item("Test Item with Rej Material Valuation", {"is_stock_item": 1}) + company = "_Test Company with perpetual inventory" + + warehouse = create_warehouse( + "_Test In-ward Warehouse", + company="_Test Company with perpetual inventory", + ) + + rej_warehouse = create_warehouse( + "_Test Warehouse - Rejected Material", + company="_Test Company with perpetual inventory", + ) + + frappe.db.set_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", 1) + + frappe.db.set_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials", 1) + + pr = make_purchase_receipt( + item_code=item.name, + qty=10, + rate=100, + company=company, + warehouse=warehouse, + rejected_qty=5, + rejected_warehouse=rej_warehouse, + ) + + stock_received_but_not_billed_account = frappe.get_value( + "Company", + company, + "stock_received_but_not_billed", + ) + + rejected_item_cost = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Purchase Receipt", + "voucher_no": pr.name, + "warehouse": rej_warehouse, + }, + "stock_value_difference", + ) + + self.assertEqual(rejected_item_cost, 500) + + srbnb_cost = frappe.db.get_value( + "GL Entry", + { + "voucher_type": "Purchase Receipt", + "voucher_no": pr.name, + "account": stock_received_but_not_billed_account, + }, + "credit", + ) + + self.assertEqual(srbnb_cost, 1500) + + frappe.db.set_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", 0) + + frappe.db.set_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials", 0) + + def test_no_valuation_rate_for_rejected_materials(self): + item = make_item("Test Item with Rej Material No Valuation", {"is_stock_item": 1}) + company = "_Test Company with perpetual inventory" + + warehouse = create_warehouse( + "_Test In-ward Warehouse", + company="_Test Company with perpetual inventory", + ) + + rej_warehouse = create_warehouse( + "_Test Warehouse - Rejected Material", + company="_Test Company with perpetual inventory", + ) + + frappe.db.set_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", 0) + + frappe.db.set_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials", 0) + + pr = make_purchase_receipt( + item_code=item.name, + qty=10, + rate=100, + company=company, + warehouse=warehouse, + rejected_qty=5, + rejected_warehouse=rej_warehouse, + ) + + stock_received_but_not_billed_account = frappe.get_value( + "Company", + company, + "stock_received_but_not_billed", + ) + + rejected_item_cost = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Purchase Receipt", + "voucher_no": pr.name, + "warehouse": rej_warehouse, + }, + "stock_value_difference", + ) + + self.assertEqual(rejected_item_cost, 0.0) + + srbnb_cost = frappe.db.get_value( + "GL Entry", + { + "voucher_type": "Purchase Receipt", + "voucher_no": pr.name, + "account": stock_received_but_not_billed_account, + }, + "credit", + ) + + self.assertEqual(srbnb_cost, 1000) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier From 5bac652b5fc8d3196a4e238b8ca60daea34df96a Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 19 May 2025 22:53:03 +0530 Subject: [PATCH 21/21] fix: GL entries for rejected returned materials (#47612) (cherry picked from commit 3e098da01f2a4f585830fc683b2b2f2ee6f4159f) --- erpnext/controllers/buying_controller.py | 3 ++- .../stock/doctype/purchase_receipt/purchase_receipt.py | 8 ++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 055b09c2d31..4e726333a7f 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -670,7 +670,8 @@ class BuyingController(SubcontractingController): "actual_qty": flt( flt(d.rejected_qty) * flt(d.conversion_factor), d.precision("stock_qty") ), - "incoming_rate": valuation_rate_for_rejected_item, + "incoming_rate": valuation_rate_for_rejected_item if not self.is_return else 0.0, + "outgoing_rate": valuation_rate_for_rejected_item if self.is_return else 0.0, "serial_and_batch_bundle": d.rejected_serial_and_batch_bundle, }, ) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 903a216d9db..c8c81c2a124 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -499,9 +499,7 @@ class PurchaseReceipt(BuyingController): if item.get("rejected_qty") and frappe.db.get_single_value( "Buying Settings", "set_valuation_rate_for_rejected_materials" ): - outgoing_amount += abs( - get_stock_value_difference(self.name, item.name, item.rejected_warehouse) - ) + outgoing_amount += get_stock_value_difference(self.name, item.name, item.rejected_warehouse) credit_amount = outgoing_amount if credit_amount: @@ -640,9 +638,7 @@ class PurchaseReceipt(BuyingController): if item.get("rejected_qty") and frappe.db.get_single_value( "Buying Settings", "set_valuation_rate_for_rejected_materials" ): - rejected_item_cost = abs( - get_stock_value_difference(self.name, item.name, item.rejected_warehouse) - ) + rejected_item_cost = get_stock_value_difference(self.name, item.name, item.rejected_warehouse) divisional_loss -= rejected_item_cost if divisional_loss: