Compare commits

..

34 Commits

Author SHA1 Message Date
Diptanil Saha
1e2adc0706 ci: bump pre-commit actions to v3.0.1 (#56562) 2026-06-26 11:14:57 +00:00
Mihir Kandoi
05e7317375 Merge pull request #56559 from Shllokkk/serial-batch-bundle-print-none-fix
fix: remove dead bundle helper call from purchase receipt print format
2026-06-26 16:37:09 +05:30
Mihir Kandoi
c5700f5df4 Merge pull request #56467 from mihir-kandoi/messages/stock
chore: rewrite user-facing messages in stock module
2026-06-26 15:29:32 +05:30
Shllokkk
b9f5a77fa7 fix: remove dead bundle helper call from purchase receipt print format 2026-06-26 15:00:54 +05:30
Nabin Hait
1ffdfbe86e Merge pull request #56539 from frappe/chore/test-delayed-order-report
test: Delayed Order Report report coverage
2026-06-26 14:58:10 +05:30
Nabin Hait
c3ae7a0b95 Merge pull request #56538 from frappe/chore/test-delayed-item-report
test: Delayed Item Report report coverage
2026-06-26 14:57:51 +05:30
Nabin Hait
99e85bfd82 Merge pull request #56537 from frappe/chore/test-batch-item-expiry-status
test: Batch Item Expiry Status report coverage
2026-06-26 14:56:31 +05:30
Nabin Hait
dca5d2ca3a Merge pull request #56523 from frappe/chore/test-stock-ledger-invariant-check
test: Stock Ledger Invariant Check report coverage
2026-06-26 14:38:21 +05:30
Nabin Hait
dc59ee8034 Merge pull request #56533 from frappe/chore/test-item-variant-details
test: Item Variant Details report coverage
2026-06-26 14:36:58 +05:30
Nabin Hait
76da65ab4c Merge pull request #56531 from frappe/chore/test-item-price-stock
test: Item Price Stock report coverage
2026-06-26 14:36:25 +05:30
Nabin Hait
bde23492fb Merge pull request #56528 from frappe/chore/test-serial-no-ledger
test: Serial No Ledger report coverage
2026-06-26 14:34:51 +05:30
Nabin Hait
d47bc64576 Merge pull request #56527 from frappe/chore/test-serial-and-batch-summary
test: Serial and Batch Summary report coverage
2026-06-26 14:33:55 +05:30
Nabin Hait
4ac863d653 Merge pull request #56515 from frappe/chore/test-batch-wise-balance-history
test: Batch-Wise Balance History report coverage
2026-06-26 14:29:30 +05:30
Nabin Hait
5fe84305fc Merge pull request #56503 from frappe/chore/trial-balance-for-party-test-coverage
test: Trial Balance for Party report coverage
2026-06-26 14:28:04 +05:30
Nabin Hait
245925815e test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:36:26 +05:30
Nabin Hait
7f5f2ccfa3 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:28:05 +05:30
Nabin Hait
dc4f5ce0ab test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:27:56 +05:30
Nabin Hait
182ef8a8e8 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:27:48 +05:30
Nabin Hait
67ac8f64e8 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:27:14 +05:30
Nabin Hait
9158d5f893 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:27:04 +05:30
Nabin Hait
0e6f50ca24 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:26:28 +05:30
Nabin Hait
6ceddd7a83 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:26:24 +05:30
Nabin Hait
293ca4e96f test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:26:08 +05:30
Nabin Hait
b537e8b183 test: add coverage for Delayed Order Report report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:33:20 +05:30
Nabin Hait
ce5239132c test: add coverage for Delayed Item Report report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:33:12 +05:30
Nabin Hait
8bef2b13a1 test: add coverage for Batch Item Expiry Status report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:33:05 +05:30
Nabin Hait
4a6b189221 test: add coverage for Item Variant Details report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:25:55 +05:30
Nabin Hait
08ce18fe58 test: add coverage for Item Price Stock report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:25:40 +05:30
Nabin Hait
7661e5ed96 test: add coverage for Serial No Ledger report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:25:18 +05:30
Nabin Hait
dddaa80f99 test: add coverage for Serial and Batch Summary report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:25:11 +05:30
Nabin Hait
09be6fed9a test: add coverage for Stock Ledger Invariant Check report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:19:59 +05:30
Nabin Hait
5790bcf99d test: add coverage for Batch-Wise Balance History report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:19:04 +05:30
Nabin Hait
d18177665b test: add coverage for Trial Balance for Party report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 09:11:45 +05:30
Mihir Kandoi
11da80c9c5 chore: rewrite user-facing messages in Stock module
Conservative cleanup of frappe.throw/msgprint messages per the message style
guide; meaning, severity, and .format() arguments are unchanged:

- index bare {} placeholders as {0}/{1}/... so translators can reorder
- move f-strings / .format() / concatenation out of _() (they break gettext
  extraction and never translate)
- wrap translatable dynamic values (DocType/Select labels) in _()
- fix grammar and colloquialisms
- drop no-op _() wrapping runtime-built strings

Part of #53976.
2026-06-25 17:36:40 +05:30
46 changed files with 759 additions and 127 deletions

View File

@@ -21,7 +21,7 @@ jobs:
cache: pip
- name: Install and Run Pre-commit
uses: pre-commit/action@v3.0.0
uses: pre-commit/action@v3.0.1
semgrep:
name: semgrep

View File

@@ -0,0 +1,117 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.trial_balance_for_party.trial_balance_for_party import execute
from erpnext.tests.utils import ERPNextTestSuite
class TestTrialBalanceForParty(ERPNextTestSuite):
def run_report(self, **extra):
filters = frappe._dict(
{
"company": "_Test Company",
"party_type": "Customer",
"fiscal_year": "_Test Fiscal Year 2026",
"from_date": "2026-01-01",
"to_date": "2026-12-31",
**extra,
}
)
return execute(filters)[1]
def party_row(self, party, **extra):
return next(row for row in self.run_report(party=party, **extra) if row.get("party") == party)
def test_sales_invoice_shown_as_period_debit(self):
customer = "_Test Customer"
create_sales_invoice(customer=customer, qty=1, rate=10000, posting_date="2026-06-01")
row = self.party_row(customer)
self.assertEqual(row["opening_debit"], 0)
self.assertEqual(row["debit"], 10000)
self.assertEqual(row["credit"], 0)
self.assertEqual(row["closing_debit"], 10000)
self.assertEqual(row["closing_credit"], 0)
def test_receipt_nets_invoice_in_closing(self):
customer = "_Test Customer"
create_sales_invoice(customer=customer, qty=1, rate=10000, posting_date="2026-06-01")
create_payment_entry(
payment_type="Receive",
party_type="Customer",
party=customer,
paid_from="Debtors - _TC",
paid_to="_Test Bank - _TC",
paid_amount=4000,
save=True,
submit=True,
)
row = self.party_row(customer)
self.assertEqual(row["debit"], 10000)
self.assertEqual(row["credit"], 4000)
# closing nets debit against credit: 10000 - 4000
self.assertEqual(row["closing_debit"], 6000)
self.assertEqual(row["closing_credit"], 0)
def test_prior_period_invoice_shown_as_opening(self):
customer = "_Test Customer"
# invoice dated before from_date should land in the opening balance, not within-period
create_sales_invoice(customer=customer, qty=1, rate=10000, posting_date="2025-12-01")
row = self.party_row(customer)
self.assertEqual(row["opening_debit"], 10000)
self.assertEqual(row["debit"], 0)
self.assertEqual(row["closing_debit"], 10000)
def test_exclude_zero_balance_parties(self):
customer = "_Test Customer"
create_sales_invoice(customer=customer, qty=1, rate=10000, posting_date="2026-06-01")
create_payment_entry(
payment_type="Receive",
party_type="Customer",
party=customer,
paid_from="Debtors - _TC",
paid_to="_Test Bank - _TC",
paid_amount=10000,
save=True,
submit=True,
)
# fully settled party still shows by default ...
self.assertEqual(self.party_row(customer)["closing_debit"], 0)
# ... but is hidden when zero-balance parties are excluded
parties = {row.get("party") for row in self.run_report(exclude_zero_balance_parties=1)}
self.assertNotIn(customer, parties)
def test_purchase_invoice_shown_as_supplier_credit(self):
supplier = "_Test Supplier"
make_purchase_invoice(supplier=supplier, qty=1, rate=8000, posting_date="2026-06-01")
row = self.party_row(supplier, party_type="Supplier")
self.assertEqual(row["credit"], 8000)
self.assertEqual(row["debit"], 0)
self.assertEqual(row["closing_credit"], 8000)
self.assertEqual(row["closing_debit"], 0)
def test_totals_row_sums_party_rows(self):
create_sales_invoice(customer="_Test Customer 1", qty=1, rate=10000, posting_date="2026-06-01")
create_sales_invoice(customer="_Test Customer 2", qty=1, rate=6000, posting_date="2026-06-01")
data = self.run_report()
totals = data[-1] # totals row is appended last
party_rows = data[:-1]
for column in (
"opening_debit",
"opening_credit",
"debit",
"credit",
"closing_debit",
"closing_credit",
):
self.assertEqual(totals[column], sum(row[column] for row in party_rows))

View File

@@ -390,7 +390,7 @@ def validate_serial_no_with_batch(serial_nos, item_code):
serial_no_link = ",".join(get_link_to_form("Serial No", sn) for sn in serial_nos)
message = "Serial Nos" if len(serial_nos) > 1 else "Serial No"
message = _("Serial Nos") if len(serial_nos) > 1 else _("Serial No")
frappe.throw(_("There is no batch found against the {0}: {1}").format(message, serial_no_link))

View File

@@ -79,7 +79,7 @@ def make_sales_invoice(
target.run_method("set_po_nos")
if len(target.get("items")) == 0:
frappe.throw(_("All these items have already been Invoiced/Returned"))
frappe.throw(_("All these items have already been invoiced/returned"))
if args and args.get("merge_taxes"):
merge_taxes(source, target)

View File

@@ -216,7 +216,7 @@ class DeliveryTrip(Document):
(list of list of str): List of address routes split at locks, if optimize is `True`
"""
if not self.driver_address:
frappe.throw(_("Cannot Calculate Arrival Time as Driver Address is Missing."))
frappe.throw(_("Cannot calculate arrival time as the driver address is missing."))
home_address = get_address_display(frappe.get_doc("Address", self.driver_address).as_dict())

View File

@@ -463,7 +463,7 @@ class Item(Document):
def validate_item_type(self):
if self.has_serial_no == 1 and self.is_stock_item == 0 and not self.is_fixed_asset:
frappe.throw(_("'Has Serial No' can not be 'Yes' for non-stock item"))
frappe.throw(_("'Has Serial No' cannot be 'Yes' for non-stock item"))
if self.has_serial_no == 0 and self.serial_no_series:
self.serial_no_series = None
@@ -1508,7 +1508,9 @@ def validate_item_default_company_links(item_defaults: list[ItemDefault]) -> Non
company = frappe.db.get_value(doctype, item_default.get(field), "company", cache=True)
if company and company != item_default.company:
frappe.throw(
_("Row #{}: {} {} doesn't belong to Company {}. Please select valid {}.").format(
_(
"Row #{0}: {1} {2} does not belong to Company {3}. Please select valid {4}."
).format(
item_default.idx,
doctype,
frappe.bold(item_default.get(field)),

View File

@@ -33,7 +33,7 @@ class ItemAlternative(Document):
def has_alternative_item(self):
if self.item_code and not frappe.db.get_value("Item", self.item_code, "allow_alternative_item"):
frappe.throw(_("Not allow to set alternative item for the item {0}").format(self.item_code))
frappe.throw(_("Cannot set alternative item for the item {0}").format(self.item_code))
def validate_alternative_item(self):
if self.item_code == self.alternative_item_code:
@@ -65,7 +65,7 @@ class ItemAlternative(Document):
indicator="Orange",
)
alternate_item_check_msg = _("Allow Alternative Item must be checked on Item {}")
alternate_item_check_msg = _("Allow Alternative Item must be checked on Item {0}")
if not item_data.allow_alternative_item:
frappe.throw(alternate_item_check_msg.format(self.item_code))
@@ -81,7 +81,7 @@ class ItemAlternative(Document):
"name": ("!=", self.name),
},
):
frappe.throw(_("Already record exists for the item {0}").format(self.item_code))
frappe.throw(_("Record already exists for the item {0}").format(self.item_code))
@frappe.whitelist()

View File

@@ -68,7 +68,7 @@ class ItemPrice(Document):
if not price_list_details:
link = frappe.utils.get_link_to_form("Price List", self.price_list)
frappe.throw(f"The price list {link} does not exist or is disabled")
frappe.throw(_("The price list {0} does not exist or is disabled").format(link))
self.buying, self.selling, self.currency = price_list_details

View File

@@ -129,8 +129,10 @@ class LandedCostVoucher(Document):
d.receipt_document_type, d.receipt_document, ["docstatus", "company"]
)
if docstatus != 1:
msg = f"Row {d.idx}: {d.receipt_document_type} {frappe.bold(d.receipt_document)} must be submitted"
frappe.throw(_(msg), title=_("Invalid Document"))
msg = _("Row {0}: {1} {2} must be submitted").format(
d.idx, d.receipt_document_type, frappe.bold(d.receipt_document)
)
frappe.throw(msg, title=_("Invalid Document"))
if company != self.company:
frappe.throw(
@@ -244,7 +246,7 @@ class LandedCostVoucher(Document):
if not total:
frappe.throw(
_(
"Total {0} for all items is zero, may be you should change 'Distribute Charges Based On'"
"Total {0} for all items is zero, maybe you should change 'Distribute Charges Based On'"
).format(based_on)
)
@@ -375,8 +377,8 @@ class LandedCostVoucher(Document):
if not docs or total_asset_qty < item.qty:
frappe.throw(
_(
"For item <b>{0}</b>, only <b>{1}</b> asset have been created or linked to <b>{2}</b>. "
"Please create or link <b>{3}</b> more asset with the respective document."
"For item <b>{0}</b>, only <b>{1}</b> assets have been created or linked to <b>{2}</b>. "
"Please create or link <b>{3}</b> more assets with the respective document."
).format(
item.item_code, total_asset_qty, item.receipt_document, item.qty - total_asset_qty
)

View File

@@ -350,7 +350,7 @@ class MaterialRequest(BuyingController):
if d.ordered_qty and flt(d.ordered_qty, precision) > flt(allowed_qty, precision):
frappe.throw(
_(
"The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than allowed requested quantity {2} for Item {3}"
"The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than allowed requested quantity {2} for Item {3}"
).format(d.ordered_qty, d.parent, allowed_qty, d.item_code)
)
@@ -576,7 +576,7 @@ def raise_work_orders(material_request: str, company: str):
if errors:
frappe.throw(
_("Work Order cannot be created for following reason: <br> {0}").format(new_line_sep(errors))
_("Work Order cannot be created for the following reason: <br> {0}").format(new_line_sep(errors))
)
return work_orders

View File

@@ -80,15 +80,13 @@ class PackingSlip(StatusUpdater):
"""Raises an exception if the `Delivery Note` status is not Draft"""
if cint(frappe.db.get_value("Delivery Note", self.delivery_note, "docstatus")) != 0:
frappe.throw(
_("A Packing Slip can only be created for Draft Delivery Note.").format(self.delivery_note)
)
frappe.throw(_("A Packing Slip can only be created for a Draft Delivery Note."))
def validate_case_nos(self):
"""Validate if case nos overlap. If they do, recommend next case no."""
if cint(self.from_case_no) <= 0:
frappe.throw(_("The 'From Package No.' field must neither be empty nor it's value less than 1."))
frappe.throw(_("The 'From Package No.' field must not be empty or have a value less than 1."))
elif not self.to_case_no:
self.to_case_no = self.from_case_no
elif cint(self.to_case_no) < cint(self.from_case_no):

View File

@@ -286,7 +286,7 @@ def create_stock_entry(pick_list: str | dict):
validate_item_locations(pick_list)
if stock_entry_exists(pick_list.get("name")):
return frappe.msgprint(_("Stock Entry has been already created against this Pick List"))
return frappe.msgprint(_("Stock Entry has already been created against this Pick List"))
stock_entry = frappe.new_doc("Stock Entry")
stock_entry.pick_list = pick_list.get("name")

View File

@@ -232,7 +232,7 @@ class PickList(TransactionBase):
and frappe.db.get_value("Sales Order", location.sales_order, "per_picked", cache=True) == 100
):
frappe.throw(
_("Row #{}: item {} has been picked already.").format(location.idx, location.item_code)
_("Row #{0}: item {1} has been picked already.").format(location.idx, location.item_code)
)
def before_submit(self):
@@ -647,7 +647,7 @@ class PickList(TransactionBase):
continue
if not item.item_code:
frappe.throw(f"Row #{item.idx}: Item Code is Mandatory")
frappe.throw(_("Row #{0}: Item Code is Mandatory").format(item.idx))
if not cint(
frappe.get_cached_value("Item", item.item_code, "is_stock_item")
) and not get_active_product_bundle(item.item_code):

View File

@@ -260,7 +260,7 @@ class PurchaseReceipt(BuyingController):
self.check_for_on_hold_or_closed_status("Purchase Order", "purchase_order")
if getdate(self.posting_date) > getdate(nowdate()):
throw(_("Posting Date cannot be future date"))
throw(_("Posting Date cannot be a future date"))
self.get_current_stock()
self.reset_default_field_value("set_warehouse", "items", "warehouse")
@@ -329,14 +329,18 @@ class PurchaseReceipt(BuyingController):
)
if qi.reference_type != self.doctype or qi.reference_name != self.name:
msg = f"""Row #{item.idx}: Please select a valid Quality Inspection with Reference Type
{frappe.bold(self.doctype)} and Reference Name {frappe.bold(self.name)}."""
frappe.throw(_(msg))
frappe.throw(
_(
"Row #{0}: Please select a valid Quality Inspection with Reference Type {1} and Reference Name {2}."
).format(item.idx, frappe.bold(self.doctype), frappe.bold(self.name))
)
if qi.item_code != item.item_code:
msg = f"""Row #{item.idx}: Please select a valid Quality Inspection with Item Code
{frappe.bold(item.item_code)}."""
frappe.throw(_(msg))
frappe.throw(
_("Row #{0}: Please select a valid Quality Inspection with Item Code {1}.").format(
item.idx, frappe.bold(item.item_code)
)
)
def get_already_received_qty(self, po, po_detail):
qty = frappe.get_all(

View File

@@ -58,7 +58,7 @@ class PutawayRule(Document):
def validate_priority(self):
if self.priority < 1:
frappe.throw(_("Priority cannot be lesser than 1."), title=_("Invalid Priority"))
frappe.throw(_("Priority cannot be less than 1."), title=_("Invalid Priority"))
def validate_warehouse_and_company(self):
company = frappe.db.get_value("Warehouse", self.warehouse, "company")
@@ -303,7 +303,7 @@ def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=N
def show_unassigned_items_message(items_not_accomodated):
msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "<br><br>"
msg = _("The following Items, having Putaway Rules, could not be accommodated:") + "<br><br>"
formatted_item_rows = ""
for entry in items_not_accomodated:

View File

@@ -134,7 +134,7 @@ class QualityInspection(Document):
):
frappe.throw(
_(
"'Inspection Required before Purchase' has disabled for the item {0}, no need to create the QI"
"'Inspection Required before Purchase' is disabled for the item {0}, no need to create the QI"
).format(get_link_to_form("Item", self.item_code))
)
@@ -143,7 +143,7 @@ class QualityInspection(Document):
):
frappe.throw(
_(
"'Inspection Required before Delivery' has disabled for the item {0}, no need to create the QI"
"'Inspection Required before Delivery' is disabled for the item {0}, no need to create the QI"
).format(get_link_to_form("Item", self.item_code))
)

View File

@@ -209,7 +209,7 @@ class RepostItemValuation(Document):
):
frappe.msgprint(_("Caution: This might alter frozen accounts."))
return
frappe.throw(_("You cannot repost item valuation before {}").format(acc_frozen_till_date))
frappe.throw(_("You cannot repost item valuation before {0}").format(acc_frozen_till_date))
def reset_field_values(self):
if self.based_on == "Transaction":

View File

@@ -165,7 +165,7 @@ class SerialandBatchBundle(Document):
if invalid_serial_nos:
msg = _(
"You cannot outward following {0} as either they are Delivered, Inactive or located in a different warehouse."
"You cannot outward the following {0} as they are either Delivered, Inactive or located in a different warehouse."
).format(_("Serial Nos") if len(invalid_serial_nos) > 1 else _("Serial No"))
msg += "<hr>"
msg += ", ".join(sn for sn in invalid_serial_nos)
@@ -183,7 +183,7 @@ class SerialandBatchBundle(Document):
if self.voucher_type == "POS Invoice":
if not frappe.db.exists("POS Invoice Item", self.voucher_detail_no):
frappe.throw(
_("The serial and batch bundle {0} not linked to {1} {2}").format(
_("The serial and batch bundle {0} is not linked to {1} {2}").format(
bold(self.name), self.voucher_type, bold(self.voucher_no)
)
)
@@ -195,7 +195,7 @@ class SerialandBatchBundle(Document):
return
frappe.throw(
_("The serial and batch bundle {0} not linked to {1} {2}").format(
_("The serial and batch bundle {0} is not linked to {1} {2}").format(
bold(self.name), self.voucher_type, bold(self.voucher_no)
)
)
@@ -227,7 +227,7 @@ class SerialandBatchBundle(Document):
for row in data:
frappe.throw(
_(
"You can't process the serial number {0} as it has already been used in the SABB {1}. {2} if you want to inward same serial number multiple times then enabled 'Allow existing Serial No to be Manufactured/Received again' in the {3}"
"You cannot process the serial number {0} as it has already been used in the SABB {1}. {2} If you want to inward the same serial number multiple times, then enable 'Allow existing Serial No to be Manufactured/Received again' in the {3}"
).format(
row.serial_no,
get_link_to_form("Serial and Batch Bundle", row.parent),
@@ -376,7 +376,7 @@ class SerialandBatchBundle(Document):
if len(serial_nos) == 1:
frappe.throw(
_(
"Serial No {0} is already Delivered. You cannot use them again in Manufacture / Repack entry."
"Serial No {0} is already Delivered. You cannot use it again in Manufacture / Repack entry."
).format(bold(serial_nos[0]))
)
else:
@@ -654,12 +654,12 @@ class SerialandBatchBundle(Document):
def validate_negative_batch(self, batch_no, available_qty):
if available_qty < 0 and not self.is_stock_reco_for_valuation_adjustment(available_qty):
msg = f"""Batch No {bold(batch_no)} of an Item {bold(self.item_code)}
has negative stock
of quantity {bold(available_qty)} in the
warehouse {self.warehouse}"""
frappe.throw(_(msg), BatchNegativeStockError)
frappe.throw(
_("Batch No {0} of Item {1} has negative stock of quantity {2} in the warehouse {3}").format(
bold(batch_no), bold(self.item_code), bold(available_qty), self.warehouse
),
BatchNegativeStockError,
)
def is_stock_reco_for_valuation_adjustment(self, available_qty):
if (
@@ -1153,8 +1153,7 @@ class SerialandBatchBundle(Document):
def validate_serial_and_batch_no(self):
if self.item_code and not self.has_serial_no and not self.has_batch_no:
msg = f"The Item {self.item_code} does not have Serial No or Batch No"
frappe.throw(_(msg))
frappe.throw(_("The Item {0} does not have Serial No or Batch No").format(self.item_code))
serial_nos = []
batch_nos = []
@@ -1589,12 +1588,11 @@ class SerialandBatchBundle(Document):
date_msg = " " + _("as of {0}").format(format_datetime(posting_datetime))
msg = _(
"""
The Batch {0} of an item {1} has negative stock in the warehouse {2}{3}.
Please add a stock quantity of {4} to proceed with this entry.
If it is not possible to make an adjustment entry, please enable 'Allow Negative Stock for Batch' in the batch {0} or in the Stock Settings to proceed.
However, enabling this setting may lead to negative stock in the system.
So please ensure the stock levels are adjusted as soon as possible to maintain the correct valuation rate."""
"The Batch {0} of item {1} has negative stock in the warehouse {2}{3}. "
"Please add a stock quantity of {4} to proceed with this entry. "
"If it is not possible to make an adjustment entry, please enable 'Allow Negative Stock for Batch' in the batch {0} or in the Stock Settings to proceed. "
"However, enabling this setting may lead to negative stock in the system. "
"So please ensure the stock levels are adjusted as soon as possible to maintain the correct valuation rate."
).format(
bold(batch_no),
bold(self.item_code),
@@ -1728,9 +1726,11 @@ class SerialandBatchBundle(Document):
and self.voucher_detail_no
and frappe.db.exists(child_doctype, self.voucher_detail_no)
):
msg = f"""The {self.voucher_type} {bold(self.voucher_no)}
is in submitted state, please cancel it first"""
frappe.throw(_(msg))
frappe.throw(
_("The {0} {1} is in submitted state, please cancel it first").format(
self.voucher_type, bold(self.voucher_no)
)
)
def on_trash(self):
self.validate_voucher_no_docstatus()
@@ -3486,13 +3486,13 @@ def is_serial_batch_no_exists(
):
if serial_no and not frappe.db.exists("Serial No", serial_no):
if type_of_transaction != "Inward":
frappe.throw(_("Serial No {0} does not exists").format(serial_no))
frappe.throw(_("Serial No {0} does not exist").format(serial_no))
make_serial_no(serial_no, item_code)
if batch_no and not frappe.db.exists("Batch", batch_no):
if type_of_transaction != "Inward":
frappe.throw(_("Batch No {0} does not exists").format(batch_no))
frappe.throw(_("Batch No {0} does not exist").format(batch_no))
make_batch_no(batch_no, item_code)

View File

@@ -98,7 +98,7 @@ class StockClosingEntry(Document):
enqueue(prepare_closing_stock_balance, name=self.name, queue="long", timeout=1500)
frappe.msgprint(
_(
"Stock Closing Entry {0} has been queued for processing, system will take sometime to complete it."
"Stock Closing Entry {0} has been queued for processing, the system will take some time to complete it."
).format(self.name)
)

View File

@@ -124,7 +124,8 @@ class BaseManufactureStockEntry(BaseStockEntry):
self.doc.process_loss_qty = flt(process_loss_qty, precision)
frappe.msgprint(
_("The Process Loss Qty has reset as per job cards Process Loss Qty"), alert=True
_("The Process Loss Qty has been reset as per the Job Card's Process Loss Qty"),
alert=True,
)
if not self.doc.process_loss_percentage and not self.doc.process_loss_qty:

View File

@@ -91,7 +91,7 @@ class SendToSubcontractorStockEntry(BaseStockEntry):
child_row.db_set(self.doc.subcontract_data.rm_detail_field, order_rm_detail)
elif not child_row.allow_alternative_item:
frappe.throw(
_("Row {0}# Item {1} not found in 'Raw Materials Supplied' table in {2} {3}").format(
_("Row #{0}: Item {1} not found in 'Raw Materials Supplied' table in {2} {3}").format(
child_row.idx,
item_code,
self.doc.subcontract_data.order_doctype,

View File

@@ -404,12 +404,11 @@ class StockEntry(StockController, SubcontractingInwardController):
if row.job_card_item or not row.s_warehouse:
continue
msg = f"""Row #{row.idx}: The job card item reference
is missing. Kindly create the stock entry
from the job card. If you have added the row manually
then you won't be able to add job card item reference."""
frappe.throw(_(msg))
frappe.throw(
_(
"Row #{0}: The job card item reference is missing. Kindly create the stock entry from the job card. If you have added the row manually then you won't be able to add job card item reference."
).format(row.idx)
)
def validate_work_order_status(self):
pro_doc = frappe.get_doc("Work Order", self.work_order)
@@ -885,7 +884,7 @@ class StockEntry(StockController, SubcontractingInwardController):
if not finished_items:
frappe.throw(
msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name),
msg=_("There must be at least 1 Finished Good in this Stock Entry").format(self.name),
title=_("Missing Finished Good"),
exc=FinishedGoodError,
)
@@ -908,7 +907,7 @@ class StockEntry(StockController, SubcontractingInwardController):
# No work order could mean independent Manufacture entry, if so skip validation
if self.work_order and self.fg_completed_qty > allowed_qty:
frappe.throw(
_("For quantity {0} should not be greater than allowed quantity {1}").format(
_("Quantity {0} should not be greater than allowed quantity {1}").format(
flt(self.fg_completed_qty), allowed_qty
)
)
@@ -1373,7 +1372,8 @@ class StockEntry(StockController, SubcontractingInwardController):
self.process_loss_qty = flt(process_loss_qty, precision)
frappe.msgprint(
_("The Process Loss Qty has reset as per job cards Process Loss Qty"), alert=True
_("The Process Loss Qty has been reset as per the job card's Process Loss Qty"),
alert=True,
)
if not self.process_loss_percentage and not self.process_loss_qty:

View File

@@ -100,7 +100,7 @@ class StockEntryDetail(Document):
def validate_and_update_item_details(self, item_details, company, purpose):
if flt(self.qty) and flt(self.qty) < 0:
frappe.throw(
_("Row {0}: The item {1}, quantity must be positive number").format(
_("Row {0}: The item {1}, quantity must be a positive number").format(
self.idx, bold(self.item_code)
)
)
@@ -153,7 +153,7 @@ class StockEntryDetail(Document):
if is_opening == "Yes" and acc_details.report_type == "Profit and Loss":
frappe.throw(
_(
"Difference Account must be a Asset/Liability type account "
"Difference Account must be an Asset/Liability type account "
"(Temporary Opening), since this Stock Entry is an Opening Entry"
),
OpeningEntryAccountError,

View File

@@ -62,7 +62,7 @@ class StockEntryType(Document):
"Subcontracting Delivery",
"Subcontracting Return",
]:
frappe.throw(f"Stock Entry Type {self.name} cannot be set as standard")
frappe.throw(_("Stock Entry Type {0} cannot be set as standard").format(self.name))
class ManufactureEntry:

View File

@@ -342,7 +342,7 @@ class StockLedgerEntry(Document):
"You are not authorized to make/edit Stock Transactions for Item {0} under warehouse {1} before this time."
).format(frappe.bold(self.item_code), frappe.bold(self.warehouse))
msg += "<br><br>" + _("Please contact any of the following users to {} this transaction.")
msg += "<br><br>" + _("Please contact any of the following users for this transaction.")
msg += "<br>" + "<br>".join(authorized_users)
frappe.throw(msg, BackDatedStockTransaction, title=_("Backdated Stock Entry"))

View File

@@ -982,7 +982,7 @@ class StockReconciliation(StockController):
if frappe.db.get_value("Account", self.expense_account, "report_type") == "Profit and Loss":
frappe.throw(
_(
"Difference Account must be a Asset/Liability type account, since this Stock Reconciliation is an Opening Entry"
"Difference Account must be an Asset/Liability type account, since this Stock Reconciliation is an Opening Entry"
),
OpeningEntryAccountError,
)
@@ -1246,7 +1246,7 @@ def get_stock_balance_for(
if not item_dict:
# In cases of data upload to Items table
msg = _("Item {} does not exist.").format(item_code)
msg = _("Item {0} does not exist.").format(item_code)
frappe.throw(msg, title=_("Missing"))
serial_nos = None

View File

@@ -138,7 +138,7 @@ class StockReservationEntry(Document):
frappe.throw(
_(
"Cannot cancel Stock Reservation Entry {0}, as it has used in the work order {1}. Please cancel the work order first or unreserved the stock"
"Cannot cancel Stock Reservation Entry {0}, as it has been used in the work order {1}. Please cancel the work order first or unreserve the stock"
).format(
", ".join([frappe.bold(entry.name) for entry in entries]),
", ".join([frappe.bold(wo.name) for wo in work_orders]),
@@ -261,7 +261,7 @@ class StockReservationEntry(Document):
if cint(frappe.db.get_value("UOM", self.stock_uom, "must_be_whole_number", cache=True)):
if cint(self.reserved_qty) != flt(self.reserved_qty, self.precision("reserved_qty")):
msg = _(
"Reserved Qty ({0}) cannot be a fraction. To allow this, disable '{1}' in UOM {3}."
"Reserved Qty ({0}) cannot be a fraction. To allow this, disable '{1}' in UOM {2}."
).format(
flt(self.reserved_qty, self.precision("reserved_qty")),
frappe.bold(_("Must be Whole Number")),
@@ -427,7 +427,7 @@ class StockReservationEntry(Document):
entry.db_update()
else:
msg = _(
"Row #{0}: Qty should be less than or equal to Available Qty to Reserve (Actual Qty - Reserved Qty) {1} for Iem {2} against Batch {3} in Warehouse {4}."
"Row #{0}: Qty should be less than or equal to Available Qty to Reserve (Actual Qty - Reserved Qty) {1} for Item {2} against Batch {3} in Warehouse {4}."
).format(
entry.idx,
frappe.bold(available_qty_to_reserve),
@@ -623,19 +623,19 @@ class StockReservationEntry(Document):
if qty_to_be_reserved > allowed_qty:
actual_qty = get_stock_balance(self.item_code, self.warehouse)
msg = """
Cannot reserve more than Allowed Qty {} {} for Item {} against {} {}.<br /><br />
The <b>Allowed Qty</b> is calculated as follows:<br />
<ul>
<li>Actual Qty [Available Qty at Warehouse] = {}</li>
<li>Reserved Stock [Ignore current SRE] = {}</li>
<li>Available Qty To Reserve [Actual Qty - Reserved Stock] = {}</li>
<li>Voucher Qty [Voucher Item Qty] = {}</li>
<li>Delivered Qty [Qty delivered against the Voucher Item] = {}</li>
<li>Total Reserved Qty [Qty reserved against the Voucher Item] = {}</li>
<li>Allowed Qty [Minimum of (Available Qty To Reserve, (Voucher Qty - Delivered Qty - Total Reserved Qty))] = {}</li>
</ul>
""".format(
msg = _(
"Cannot reserve more than Allowed Qty {0} {1} for Item {2} against {3} {4}.<br /><br />"
"The <b>Allowed Qty</b> is calculated as follows:<br />"
"<ul>"
"<li>Actual Qty [Available Qty at Warehouse] = {5}</li>"
"<li>Reserved Stock [Ignore current SRE] = {6}</li>"
"<li>Available Qty To Reserve [Actual Qty - Reserved Stock] = {7}</li>"
"<li>Voucher Qty [Voucher Item Qty] = {8}</li>"
"<li>Delivered Qty [Qty delivered against the Voucher Item] = {9}</li>"
"<li>Total Reserved Qty [Qty reserved against the Voucher Item] = {10}</li>"
"<li>Allowed Qty [Minimum of (Available Qty To Reserve, (Voucher Qty - Delivered Qty - Total Reserved Qty))] = {11}</li>"
"</ul>"
).format(
frappe.bold(allowed_qty),
self.stock_uom,
frappe.bold(self.item_code),

View File

@@ -189,7 +189,7 @@ class StockSettings(Document):
if sle:
frappe.throw(
_(
"Can't change the valuation method, as there are transactions against some items which do not have its own valuation method"
"Can't change the valuation method, as there are transactions against some items which do not have their own valuation method"
)
)
@@ -247,7 +247,7 @@ class StockSettings(Document):
if has_reserved_stock:
frappe.throw(
_("As there are reserved stock, you cannot disable {0}.").format(
_("As there is reserved stock, you cannot disable {0}.").format(
frappe.bold(_("Stock Reservation"))
)
)

View File

@@ -355,9 +355,10 @@ def validate_item_details(ctx: ItemDetailsCtx, item):
validate_end_of_life(item.name, item.end_of_life, item.disabled)
if cint(item.has_variants):
msg = f"Item {item.name} is a template, please select one of its variants"
throw(_(msg), title=_("Template Item Selected"))
throw(
_("Item {0} is a template, please select one of its variants").format(item.name),
title=_("Template Item Selected"),
)
elif ctx.doctype != "Material Request":
if ctx.is_subcontracted and item.is_stock_item:

View File

@@ -0,0 +1,62 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.report.batch_item_expiry_status.batch_item_expiry_status import execute
from erpnext.tests.utils import ERPNextTestSuite
class TestBatchItemExpiryStatus(ERPNextTestSuite):
def run_report(self, **extra):
filters = frappe._dict(
{
"from_date": "2026-01-01",
"to_date": "2026-12-31",
"company": "_Test Company",
}
)
filters.update(extra)
return execute(filters)[1]
def test_batch_listed_with_balance(self):
item = make_item(
properties={
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "BIE-.#####",
"has_expiry_date": 1,
"shelf_life_in_days": 30,
}
).name
make_stock_entry(
item_code=item,
to_warehouse="_Test Warehouse - _TC",
qty=10,
rate=100,
posting_date="2026-06-01",
)
batch_no = frappe.db.get_value("Batch", {"item": item}, "name")
self.assertTrue(batch_no, "Stock entry did not auto-create a batch")
data = self.run_report(item=item)
# Columns: [item, item_name, batch, stock_uom, quantity, expires_on, expiry_in_days]
row = next((r for r in data if r[2] == batch_no), None)
self.assertIsNotNone(row, f"Batch {batch_no} not found in report for item {item}")
self.assertEqual(row[0], item)
self.assertEqual(row[2], batch_no)
self.assertEqual(row[4], 10)
# expiry = batch manufacturing_date + 30 day shelf life; matches the Batch record
batch_expiry = frappe.db.get_value("Batch", batch_no, "expiry_date")
self.assertIsNotNone(row[5], "Expiry date should be set for a batch with shelf life")
self.assertEqual(frappe.utils.getdate(row[5]), frappe.utils.getdate(batch_expiry))
# Expiry (In Days) column = days until expiry
expected_days = max((frappe.utils.getdate(batch_expiry) - frappe.utils.datetime.date.today()).days, 0)
self.assertEqual(row[6], expected_days)

View File

@@ -0,0 +1,52 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.report.batch_wise_balance_history.batch_wise_balance_history import execute
from erpnext.tests.utils import ERPNextTestSuite
WH = "Stores - _TC"
# row indexes: 0 item, 1 name, 2 desc, 3 wh, 4 batch, 5 opening, 6 in, 7 out, 8 bal, 9 rate, 10 value, 11 uom
class TestBatchWiseBalanceHistory(ERPNextTestSuite):
def make_batch_item(self):
return make_item(
properties={
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "BWB-.#####",
}
).name
def run_report(self, item, from_date="2026-01-01", to_date="2026-12-31"):
filters = frappe._dict(
{"company": "_Test Company", "item_code": item, "from_date": from_date, "to_date": to_date}
)
return execute(filters)[1]
def test_in_out_balance_and_valuation(self):
item = self.make_batch_item()
make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=100, posting_date="2026-06-01")
make_stock_entry(item_code=item, from_warehouse=WH, qty=4, posting_date="2026-06-02")
(row,) = self.run_report(item)
self.assertEqual(row[5], 0) # opening
self.assertEqual(row[6], 10) # in
self.assertEqual(row[7], 4) # out
self.assertEqual(row[8], 6) # balance
self.assertEqual(row[9], 100) # valuation rate
self.assertEqual(row[10], 600) # balance value
def test_opening_qty_from_prior_period(self):
item = self.make_batch_item()
make_stock_entry(item_code=item, to_warehouse=WH, qty=8, rate=50, posting_date="2025-12-01")
(row,) = self.run_report(item)
self.assertEqual(row[5], 8) # opening carried from 2025
self.assertEqual(row[6], 0)
self.assertEqual(row[8], 8) # balance

View File

@@ -0,0 +1,68 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from erpnext.selling.doctype.sales_order.mapper import make_delivery_note
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.report.delayed_item_report.delayed_item_report import execute
from erpnext.tests.utils import ERPNextTestSuite
class TestDelayedItemReport(ERPNextTestSuite):
def run_report(self, **extra):
filters = frappe._dict(
{
"company": "_Test Company",
"based_on": "Delivery Note",
"from_date": "2026-06-01",
"to_date": "2026-06-30",
}
)
filters.update(extra)
return execute(filters)[1]
def test_late_delivery_shows_delay(self):
item = "_Test Item"
make_stock_entry(
item_code=item,
qty=10,
to_warehouse="Stores - _TC",
rate=100,
posting_date="2026-06-01",
company="_Test Company",
)
so = make_sales_order(
item_code=item,
qty=10,
rate=100,
warehouse="Stores - _TC",
transaction_date="2026-06-01",
company="_Test Company",
do_not_submit=True,
)
so.delivery_date = "2026-06-05"
for row in so.items:
row.delivery_date = "2026-06-05"
so.submit()
dn = make_delivery_note(so.name)
dn.posting_date = "2026-06-10"
dn.set_posting_time = 1
dn.insert()
dn.submit()
rows = self.run_report(sales_order=so.name)
matching = [r for r in rows if r.get("name") == dn.name and r.get("item_code") == item]
self.assertTrue(matching, f"No report row found for DN {dn.name} / item {item}")
row = matching[0]
self.assertEqual(row.get("sales_order"), so.name)
self.assertEqual(str(row.get("delivery_date")), "2026-06-05")
self.assertEqual(str(row.get("posting_date")), "2026-06-10")
# delayed_days = date_diff(actual posting_date, expected delivery_date)
self.assertEqual(row.get("delayed_days"), 5)

View File

@@ -0,0 +1,63 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from erpnext.selling.doctype.sales_order.mapper import make_delivery_note
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.report.delayed_order_report.delayed_order_report import execute
from erpnext.tests.utils import ERPNextTestSuite
class TestDelayedOrderReport(ERPNextTestSuite):
def run_report(self, **extra):
filters = frappe._dict(
{
"company": "_Test Company",
"from_date": "2026-06-01",
"to_date": "2026-06-30",
"based_on": "Delivery Note",
}
)
filters.update(extra)
return execute(filters)[1]
def test_late_order_shows_delay(self):
item_code = "_Test Item"
make_stock_entry(
item_code=item_code,
target="Stores - _TC",
qty=10,
basic_rate=100,
posting_date="2026-06-01",
)
sales_order = make_sales_order(
item_code=item_code,
qty=5,
warehouse="Stores - _TC",
transaction_date="2026-06-01",
do_not_submit=True,
)
sales_order.delivery_date = "2026-06-05"
for item in sales_order.items:
item.delivery_date = "2026-06-05"
sales_order.submit()
delivery_note = make_delivery_note(sales_order.name)
delivery_note.set_posting_time = 1
delivery_note.posting_date = "2026-06-10"
delivery_note.insert()
delivery_note.submit()
data = self.run_report(sales_order=sales_order.name)
matching = [row for row in data if row.get("sales_order") == sales_order.name]
self.assertEqual(len(matching), 1)
row = matching[0]
self.assertEqual(frappe.utils.getdate(row.get("delivery_date")), frappe.utils.getdate("2026-06-05"))
self.assertEqual(frappe.utils.getdate(row.get("posting_date")), frappe.utils.getdate("2026-06-10"))
self.assertEqual(row.get("delayed_days"), 5)

View File

@@ -0,0 +1,47 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.report.item_price_stock.item_price_stock import execute
from erpnext.tests.utils import ERPNextTestSuite
class TestItemPriceStock(ERPNextTestSuite):
def run_report(self, **extra):
return execute(frappe._dict(extra))[1]
def test_price_and_stock_shown(self):
item = "_Test Item"
frappe.get_doc(
{
"doctype": "Item Price",
"item_code": item,
"price_list": "Standard Selling",
"price_list_rate": 300,
}
).insert()
make_stock_entry(
item_code=item,
to_warehouse="Stores - _TC",
qty=7,
rate=100,
posting_date="2026-06-01",
)
rows = self.run_report(item_code=item)
warehouse_rows = [
row
for row in rows
if row["warehouse"] == "Stores - _TC" and row["selling_price_list"] == "Standard Selling"
]
self.assertEqual(len(warehouse_rows), 1)
row = warehouse_rows[0]
self.assertEqual(row["item_code"], item)
self.assertEqual(row["selling_price_list"], "Standard Selling")
self.assertEqual(row["selling_rate"], 300)
self.assertEqual(row["stock_available"], 7)

View File

@@ -22,7 +22,7 @@ def get_data(item):
)
if not variant_results:
frappe.msgprint(_("There aren't any item variants for the selected item"))
frappe.msgprint(_("There are no item variants for the selected item"))
return []
else:
variant_list = [variant["name"] for variant in variant_results]

View File

@@ -0,0 +1,37 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from erpnext.controllers.item_variant import create_variant
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.report.item_variant_details.item_variant_details import execute
from erpnext.tests.utils import ERPNextTestSuite
class TestItemVariantDetails(ERPNextTestSuite):
def run_report(self, **extra):
return execute(frappe._dict(extra))[1]
def test_variants_listed_for_template(self):
template = "_Test Variant Item"
variant = create_variant(template, {"Test Size": "Small"})
variant.insert(ignore_if_duplicate=True)
make_stock_entry(
item_code=variant.name,
to_warehouse="Stores - _TC",
qty=5,
rate=100,
)
rows = self.run_report(item=template)
variant_rows = [row for row in rows if row.get("variant_name") == variant.name]
self.assertEqual(len(variant_rows), 1)
row = variant_rows[0]
self.assertEqual(row.get("test_size"), "Small")
self.assertEqual(row.get("current_stock"), 5)
self.assertEqual(row.get("open_orders"), 0)

View File

@@ -0,0 +1,68 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from erpnext.tests.utils import ERPNextTestSuite
class TestSerialAndBatchSummary(ERPNextTestSuite):
def run_report(self, **extra):
from erpnext.stock.report.serial_and_batch_summary.serial_and_batch_summary import execute
return execute(frappe._dict(extra))[1]
@staticmethod
def _cancel_and_delete_stock_entry(name):
if not frappe.db.exists("Stock Entry", name):
return
doc = frappe.get_doc("Stock Entry", name)
if doc.docstatus == 1:
doc.cancel()
frappe.delete_doc("Stock Entry", name, force=1)
def test_serial_receipt_listed(self):
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
item = "_Test Serialized Item With Series"
se = make_stock_entry(item_code=item, to_warehouse="Stores - _TC", qty=3, basic_rate=100)
self.addCleanup(self._cancel_and_delete_stock_entry, se.name)
data = self.run_report(voucher_no=[se.name], voucher_type="Stock Entry")
self.assertEqual(len(data), 3)
self.assertEqual(len({row.serial_no for row in data}), 3)
for row in data:
self.assertTrue(row.serial_no)
self.assertEqual(row.qty, 1)
self.assertEqual(row.incoming_rate, 100)
self.assertEqual(row.warehouse, "Stores - _TC")
self.assertEqual(row.voucher_no, se.name)
def test_batch_receipt_listed(self):
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_batch_from_bundle,
)
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
item = make_item(
properties={
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "SBB-.#####",
}
).name
se = make_stock_entry(item_code=item, to_warehouse="_Test Warehouse - _TC", qty=10, basic_rate=50)
self.addCleanup(self._cancel_and_delete_stock_entry, se.name)
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
data = self.run_report(voucher_no=[se.name], voucher_type="Stock Entry")
row = next((d for d in data if d.batch_no == batch_no), None)
self.assertIsNotNone(row)
self.assertEqual(row.qty, 10)
self.assertEqual(row.incoming_rate, 50)
self.assertEqual(row.warehouse, "_Test Warehouse - _TC")
self.assertEqual(row.voucher_no, se.name)

View File

@@ -0,0 +1,67 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.report.serial_no_ledger.serial_no_ledger import execute
from erpnext.tests.utils import ERPNextTestSuite
class TestSerialNoLedger(ERPNextTestSuite):
def run_report(self, **extra):
filters = {
"company": "_Test Company",
"warehouse": "Stores - _TC",
"posting_date": "2026-06-30",
}
filters.update(extra)
return execute(frappe._dict(filters))[1]
def make_serial_item(self) -> str:
return "_Test Serialized Item With Series"
def test_receipt_appears_in_serial_ledger(self):
item = self.make_serial_item()
stock_entry = make_stock_entry(
item_code=item,
to_warehouse="Stores - _TC",
qty=2,
rate=100,
posting_date="2026-06-01",
)
serial_nos = frappe.get_all("Serial No", {"item_code": item}, pluck="name")
self.assertEqual(len(serial_nos), 2)
serial_no = serial_nos[0]
data = self.run_report(item_code=item, serial_no=serial_no)
self.assertEqual(len(data), 1)
row = data[0]
self.assertEqual(row["serial_no"], serial_no)
self.assertEqual(row["voucher_type"], "Stock Entry")
self.assertEqual(row["voucher_no"], stock_entry.name)
self.assertEqual(row["warehouse"], "Stores - _TC")
self.assertEqual(row["qty"], 1)
self.assertEqual(row["valuation_rate"], 100)
def test_filter_by_item_lists_all_received_serials(self):
item = self.make_serial_item()
make_stock_entry(
item_code=item,
to_warehouse="Stores - _TC",
qty=2,
rate=150,
posting_date="2026-06-01",
)
serial_nos = frappe.get_all("Serial No", {"item_code": item}, pluck="name")
data = self.run_report(item_code=item)
ledger_serials = sorted(row["serial_no"] for row in data)
self.assertEqual(ledger_serials, sorted(serial_nos))
for row in data:
self.assertEqual(row["qty"], 1)
self.assertEqual(row["valuation_rate"], 150)

View File

@@ -4,11 +4,12 @@
import frappe
from frappe.utils import add_days, today
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.report.stock_ledger.stock_ledger import execute
from erpnext.tests.utils import ERPNextTestSuite
WAREHOUSE = "Stores - _TC"
WAREHOUSE = "_Test Warehouse - _TC"
class TestStockLedgerReport(ERPNextTestSuite):
@@ -16,8 +17,7 @@ class TestStockLedgerReport(ERPNextTestSuite):
A shared `make_movements`/`run` pair keeps each test small without persisting
any data: movements are created per test and rolled back, while the report runs
read-only. Tests reuse bootstrap items and transact in `Stores - _TC`, which
starts clean (zero balance) for these items.
read-only.
"""
def make_movements(self, item_code, movements):
@@ -35,7 +35,7 @@ class TestStockLedgerReport(ERPNextTestSuite):
return list(execute(filters)[1])
def test_in_out_quantities_and_running_balance(self):
item = "_Test Item"
item = make_item().name
self.make_movements(
item,
[
@@ -54,7 +54,7 @@ class TestStockLedgerReport(ERPNextTestSuite):
self.assertEqual(issue["qty_after_transaction"], 6)
def test_opening_balance_reflects_movements_before_from_date(self):
item = "_Test Item"
item = make_item().name
self.make_movements(
item,
[
@@ -79,8 +79,8 @@ class TestStockLedgerReport(ERPNextTestSuite):
self.assertEqual(issue["qty_after_transaction"], 6)
def test_filters_to_requested_item_only(self):
item_a = "_Test Item"
item_b = "_Test Item 2"
item_a = make_item().name
item_b = make_item().name
self.make_movements(item_a, [{"qty": 5, "to_warehouse": WAREHOUSE, "basic_rate": 100}])
self.make_movements(item_b, [{"qty": 7, "to_warehouse": WAREHOUSE, "basic_rate": 100}])

View File

@@ -0,0 +1,44 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.report.stock_ledger_invariant_check.stock_ledger_invariant_check import execute
from erpnext.tests.utils import ERPNextTestSuite
WAREHOUSE = "Stores - _TC"
COMPANY = "_Test Company"
ITEM = "_Test Item"
class TestStockLedgerInvariantCheck(ERPNextTestSuite):
def run_report(self, **extra):
filters = frappe._dict({"company": COMPANY, "warehouse": WAREHOUSE})
filters.update(extra)
return execute(filters)[1]
def make_movements(self) -> str:
frappe.db.set_value("Item", ITEM, "valuation_method", "FIFO")
make_stock_entry(item_code=ITEM, to_warehouse=WAREHOUSE, qty=10, rate=100, posting_date="2026-06-01")
make_stock_entry(item_code=ITEM, to_warehouse=WAREHOUSE, qty=5, rate=120, posting_date="2026-06-02")
make_stock_entry(item_code=ITEM, from_warehouse=WAREHOUSE, qty=4, rate=0, posting_date="2026-06-03")
return ITEM
def test_diagnostic_rows_have_no_discrepancy(self):
item = self.make_movements()
data = self.run_report(item_code=item)
self.assertEqual(len(data), 3)
for row in data:
self.assertLess(abs(row.difference_in_qty), 0.01)
self.assertLess(abs(row.fifo_qty_diff), 0.01)
self.assertLess(abs(row.diff_value_diff), 0.01)
def test_running_balance_matches(self):
item = self.make_movements()
data = self.run_report(item_code=item)
self.assertEqual(data[-1].qty_after_transaction, 11)

View File

@@ -4,31 +4,29 @@
import frappe
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.report.stock_projected_qty.stock_projected_qty import execute
from erpnext.tests.utils import ERPNextTestSuite
# Use a clean warehouse (zero baseline) so projected-qty assertions are exact.
WAREHOUSE = "Stores - _TC"
WAREHOUSE = "_Test Warehouse - _TC"
class TestStockProjectedQty(ERPNextTestSuite):
"""Correctness tests for the Stock Projected Qty report (a current-Bin snapshot)."""
def run_report(self, item_code, warehouse=None):
def run_report(self, item_code):
filters = frappe._dict(company="_Test Company", item_code=item_code)
if warehouse:
filters.warehouse = warehouse
columns, data = execute(filters)
fields = [column["fieldname"] for column in columns]
return [dict(zip(fields, row, strict=False)) for row in data]
def test_projected_qty_includes_actual_and_ordered(self):
item = "_Test Item"
item = make_item().name
make_stock_entry(item_code=item, qty=10, to_warehouse=WAREHOUSE, basic_rate=100)
create_purchase_order(item_code=item, qty=5, rate=100, warehouse=WAREHOUSE)
row = self.run_report(item, warehouse=WAREHOUSE)[0]
row = self.run_report(item)[0]
self.assertEqual(row["actual_qty"], 10)
self.assertEqual(row["ordered_qty"], 5)
self.assertEqual(row["projected_qty"], 15)
@@ -37,7 +35,7 @@ class TestStockProjectedQty(ERPNextTestSuite):
"""projected_qty = actual + ordered + requested + planned
- reserved - reserved_for_production - reserved_for_subcontract - reserved_for_production_plan
and every component is surfaced as its own column."""
item = "_Test Item"
item = make_item().name
make_stock_entry(item_code=item, qty=100, to_warehouse=WAREHOUSE, basic_rate=100)
bin_doc = frappe.get_doc("Bin", {"item_code": item, "warehouse": WAREHOUSE})
@@ -59,7 +57,7 @@ class TestStockProjectedQty(ERPNextTestSuite):
# 100 + 50 + 30 + 20 - 10 - 8 - 6 - 4
self.assertEqual(bin_doc.projected_qty, 172)
row = self.run_report(item, warehouse=WAREHOUSE)[0]
row = self.run_report(item)[0]
self.assertEqual(row["actual_qty"], 100)
self.assertEqual(row["ordered_qty"], 50)
self.assertEqual(row["indented_qty"], 30)
@@ -71,7 +69,7 @@ class TestStockProjectedQty(ERPNextTestSuite):
self.assertEqual(row["projected_qty"], 172)
def test_shortage_qty_from_reorder_level(self):
item = "_Test Item"
item = make_item().name
doc = frappe.get_doc("Item", item)
doc.append(
"reorder_levels",
@@ -85,14 +83,14 @@ class TestStockProjectedQty(ERPNextTestSuite):
doc.save()
make_stock_entry(item_code=item, qty=10, to_warehouse=WAREHOUSE, basic_rate=100)
row = self.run_report(item, warehouse=WAREHOUSE)[0]
row = self.run_report(item)[0]
self.assertEqual(row["re_order_level"], 20)
self.assertEqual(row["projected_qty"], 10)
self.assertEqual(row["shortage_qty"], 10) # reorder level 20 - projected 10
def test_item_filter_returns_only_requested_item(self):
item_a = "_Test Item"
item_b = "_Test Item 2"
item_a = make_item().name
item_b = make_item().name
make_stock_entry(item_code=item_a, qty=5, to_warehouse=WAREHOUSE, basic_rate=100)
make_stock_entry(item_code=item_b, qty=7, to_warehouse=WAREHOUSE, basic_rate=100)

View File

@@ -1231,7 +1231,9 @@ class SerialBatchCreation:
required_qty = flt(abs(self.actual_qty), precision)
if required_qty - total_qty > 0:
msg = f"For the item {bold(doc.item_code)}, the Available qty {bold(total_qty)} is less than the Required Qty {bold(required_qty)} in the warehouse {bold(doc.warehouse)}. Please add sufficient qty in the warehouse."
msg = _(
"For the item {0}, the Available qty {1} is less than the Required Qty {2} in the warehouse {3}. Please add sufficient qty in the warehouse."
).format(bold(doc.item_code), bold(total_qty), bold(required_qty), bold(doc.warehouse))
frappe.throw(msg, title=_("Insufficient Stock"))
def set_auto_serial_batch_entries_for_outward(self):

View File

@@ -101,7 +101,7 @@ class StockInternalTransferService:
if recevied_qty > flt(transferred_qty, precision):
frappe.throw(
_("For Item {0} cannot be received more than {1} qty against the {2} {3}").format(
_("Item {0} cannot be received in more than {1} qty against the {2} {3}").format(
bold(key[1]),
bold(flt(transferred_qty, precision)),
bold(parent_doctype),

View File

@@ -496,7 +496,7 @@ class SerialBatchBundleService:
if throw_error:
frappe.throw(
_(
"At row {0}: Serial and Batch Bundle {1} has already created. Please remove the values from the serial no or batch no fields."
"At row {0}: Serial and Batch Bundle {1} has already been created. Please remove the values from the serial no or batch no fields."
).format(row.idx, row.serial_and_batch_bundle)
)

View File

@@ -366,8 +366,7 @@ def create_file(doc, compressed_content):
def validate_item_warehouse(args):
for field in ["item_code", "warehouse", "posting_date", "posting_time"]:
if args.get(field) in [None, ""]:
validation_msg = f"The field {frappe.unscrub(field)} is required for the reposting"
frappe.throw(_(validation_msg))
frappe.throw(_("The field {0} is required for reposting").format(frappe.unscrub(field)))
def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None, reposting_data=None):
@@ -831,7 +830,7 @@ class update_entries_after:
if previous_sle and previous_sle.get("qty_after_transaction") < 0 and sle.get("actual_qty") > 0:
frappe.msgprint(
_(
"The stock for the item {0} in the {1} warehouse was negative on the {2}. You should create a positive entry {3} before the date {4} and time {5} to post the correct valuation rate. For more details, please read the <a href='https://docs.erpnext.com/docs/user/manual/en/stock-adjustment-cogs-with-negative-stock'>documentation<a>."
"The stock for the item {0} in the {1} warehouse was negative on the {2}. You should create a positive entry {3} before the date {4} and time {5} to post the correct valuation rate. For more details, please read the <a href='https://docs.erpnext.com/docs/user/manual/en/stock-adjustment-cogs-with-negative-stock'>documentation</a>."
).format(
bold(sle.item_code),
bold(sle.warehouse),