From 945bdabebb599992d13d177b6f4e035888bc0bf5 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 27 Jun 2025 11:33:03 +0530 Subject: [PATCH 1/4] fix: do not allow backdated transactions against serial numbers. (#48281) --- .../purchase_receipt/test_purchase_receipt.py | 57 +++++++++++++++++++ erpnext/stock/doctype/serial_no/serial_no.py | 34 +++++++++-- 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index ef690cda67d..199a44b3734 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2761,6 +2761,63 @@ class TestPurchaseReceipt(FrappeTestCase): pr.reload() self.assertEqual(pr.status, "To Bill") + def test_serial_no_exists_in_future(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item_doc = make_item( + "Test Serial No Item Exists in Future", + { + "is_purchase_item": 1, + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "SN-SBNS-.#####", + }, + ) + + source_warehouse = "_Test Warehouse - _TC" + target_warehouse = "_Test Warehouse 1 - _TC" + if not frappe.db.exists("Warehouse", target_warehouse): + create_warehouse("_Test Warehouse 1") + + make_purchase_receipt( + item_code=item_doc.name, + qty=1, + rate=100, + serial_no="SN-SBNS-00001", + posting_date=add_days(today(), -2), + ) + + make_stock_entry( + item_code=item_doc.name, + qty=1, + rate=100, + to_warehouse=target_warehouse, + serial_no="SN-SBNS-00002", + posting_date=add_days(today(), -1), + ) + + make_stock_entry( + item_code=item_doc.name, + qty=1, + rate=100, + from_warehouse=source_warehouse, + to_warehouse=target_warehouse, + serial_no="SN-SBNS-00001", + posting_date=today(), + ) + + se = make_stock_entry( + item_code=item_doc.name, + qty=1, + rate=100, + from_warehouse=target_warehouse, + serial_no="SN-SBNS-00001", + posting_date=add_days(today(), -1), + do_not_submit=True, + ) + + self.assertRaises(frappe.ValidationError, se.submit) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index bc4d4998d0b..2c87bb56035 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -8,7 +8,18 @@ import frappe from frappe import ValidationError, _ from frappe.model.naming import make_autoname from frappe.query_builder.functions import Coalesce -from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, getdate, now, nowdate, safe_json_loads +from frappe.utils import ( + add_days, + cint, + cstr, + flt, + get_datetime, + get_link_to_form, + getdate, + now, + nowdate, + safe_json_loads, +) from erpnext.controllers.stock_controller import StockController from erpnext.stock.get_item_details import get_reserved_qty_for_so @@ -197,7 +208,7 @@ class SerialNo(StockController): for sle in frappe.db.sql( """ SELECT voucher_type, voucher_no, - posting_date, posting_time, incoming_rate, actual_qty, serial_no + posting_date, posting_time, incoming_rate, actual_qty, serial_no, posting_datetime FROM `tabStock Ledger Entry` WHERE @@ -251,8 +262,23 @@ class SerialNo(StockController): _("Cannot delete Serial No {0}, as it is used in stock transactions").format(self.name) ) - def update_serial_no_reference(self, serial_no=None): + def update_serial_no_reference(self, serial_no=None, sle=None): last_sle = self.get_last_sle(serial_no) + + _last_sle_dict = last_sle.get("last_sle") + if ( + _last_sle_dict + and sle.get("voucher_type") != "Stock Reconciliation" + and sle.get("voucher_no") != _last_sle_dict.get("voucher_no") + and get_datetime(sle.get("posting_datetime")) + < get_datetime(_last_sle_dict.get("posting_datetime")) + ): + frappe.throw( + _( + "You can not complete this transaction because a future transaction exists for the serial number {0}" + ).format(serial_no) + ) + self.set_purchase_details(last_sle.get("purchase_sle")) self.set_sales_details(last_sle.get("delivery_sle")) self.set_maintenance_status() @@ -770,7 +796,7 @@ def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False): serial_no_doc.sales_order = None serial_no_doc.validate_item() - serial_no_doc.update_serial_no_reference(serial_no) + serial_no_doc.update_serial_no_reference(serial_no, sle=args) if is_new: serial_no_doc.db_insert() From 15ae0196c610722872920e8f3b63e28f68e3a57b Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 29 Jun 2025 21:45:57 +0530 Subject: [PATCH 2/4] fix: accounting entries for standalone credit notes (cherry picked from commit 52177cffcde045fc7f65ac110b61c21540a4400c) # Conflicts: # erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py --- .../purchase_invoice/purchase_invoice.py | 30 +++++++++++++++++++ erpnext/stock/stock_ledger.py | 17 +++++++---- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 045e754734d..751b6ed3bc0 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1124,6 +1124,36 @@ class PurchaseInvoice(BuyingController): warehouse_debit_amount = stock_amount +<<<<<<< HEAD +======= + elif self.is_return and self.update_stock and (self.is_internal_supplier or not self.return_against): + net_rate = item.base_net_amount + if item.sales_incoming_rate: # for internal transfer + net_rate = item.qty * item.sales_incoming_rate + + stock_amount = net_rate + item.item_tax_amount + flt(item.landed_cost_voucher_amount) + + if flt(stock_amount, net_amt_precision) != flt(warehouse_debit_amount, net_amt_precision): + cost_of_goods_sold_account = self.get_company_default("default_expense_account") + stock_adjustment_amt = stock_amount - warehouse_debit_amount + + gl_entries.append( + self.get_gl_dict( + { + "account": cost_of_goods_sold_account, + "against": item.expense_account, + "debit": stock_adjustment_amt, + "debit_in_transaction_currency": stock_adjustment_amt / self.conversion_rate, + "remarks": self.get("remarks") or _("Stock Adjustment"), + "cost_center": item.cost_center, + "project": item.project or self.project, + }, + account_currency, + item=item, + ) + ) + +>>>>>>> 52177cffcd (fix: accounting entries for standalone credit notes) return warehouse_debit_amount def make_tax_gl_entries(self, gl_entries): diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index cc0a9e0f04e..a429802a884 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -893,12 +893,19 @@ class update_entries_after: def update_rate_on_purchase_receipt(self, sle, outgoing_rate): if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no): - if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and frappe.get_cached_value( - sle.voucher_type, sle.voucher_no, "is_internal_supplier" - ): - frappe.db.set_value( - f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", sle.outgoing_rate + if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]: + details = frappe.get_cached_value( + sle.voucher_type, + sle.voucher_no, + ["is_internal_supplier", "is_return", "return_against"], + as_dict=True, ) + if details.is_internal_supplier or (details.is_return and not details.return_against): + rate = outgoing_rate if details.is_return else sle.outgoing_rate + + frappe.db.set_value( + f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", rate + ) else: frappe.db.set_value( "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate From 6b31e54891c58328dd1ad1a3a472ad0b42f15043 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 30 Jun 2025 09:42:13 +0530 Subject: [PATCH 3/4] chore: fix conflicts --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 751b6ed3bc0..31d575358ae 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1124,8 +1124,6 @@ class PurchaseInvoice(BuyingController): warehouse_debit_amount = stock_amount -<<<<<<< HEAD -======= elif self.is_return and self.update_stock and (self.is_internal_supplier or not self.return_against): net_rate = item.base_net_amount if item.sales_incoming_rate: # for internal transfer @@ -1153,7 +1151,6 @@ class PurchaseInvoice(BuyingController): ) ) ->>>>>>> 52177cffcd (fix: accounting entries for standalone credit notes) return warehouse_debit_amount def make_tax_gl_entries(self, gl_entries): From b98cce8b9acebadc569abeb8240bd47ae4ee522d Mon Sep 17 00:00:00 2001 From: i-am-vimal Date: Thu, 19 Jun 2025 18:14:32 +0530 Subject: [PATCH 4/4] fix: add validation for exchange gain/loss entries (cherry picked from commit 5c9eddd31e16bf6f8afa5e3800e04c2ea1044c53) # Conflicts: # erpnext/accounts/report/utils.py --- erpnext/accounts/report/utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py index 5d606f648fa..f78d95f4160 100644 --- a/erpnext/accounts/report/utils.py +++ b/erpnext/accounts/report/utils.py @@ -107,7 +107,11 @@ def convert_to_presentation_currency(gl_entries, currency_info): credit_in_account_currency = flt(entry["credit_in_account_currency"]) account_currency = entry["account_currency"] - if len(account_currencies) == 1 and account_currency == presentation_currency: + if ( + len(account_currencies) == 1 + and account_currency == presentation_currency + and (debit_in_account_currency or credit_in_account_currency) + ): entry["debit"] = debit_in_account_currency entry["credit"] = credit_in_account_currency else: