Compare commits

..

2 Commits

Author SHA1 Message Date
Nabin Hait
9aef148a44 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:27 +05:30
Nabin Hait
993578dc2f test: add coverage for Itemwise Recommended Reorder Level report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:25:48 +05:30
136 changed files with 2245 additions and 130492 deletions

View File

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

View File

@@ -48,6 +48,7 @@ repos:
cypress/.*|
.*node_modules.*|
.*boilerplate.*|
erpnext/public/js/controllers/.*|
erpnext/templates/pages/order.js|
erpnext/templates/includes/.*
)$

View File

@@ -5,7 +5,6 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import getdate, nowdate
class OverlapError(frappe.ValidationError):
@@ -37,20 +36,8 @@ class AccountingPeriod(Document):
# end: auto-generated types
def validate(self):
self.validate_dates()
self.validate_overlap()
def validate_dates(self):
if getdate(self.start_date) > getdate(self.end_date):
frappe.throw(_("Start Date cannot be after End Date"))
if getdate(self.end_date) > getdate(nowdate()):
frappe.throw(
_(
"Accounting Period cannot be created for a future date. End Date {0} is after today."
).format(frappe.bold(frappe.format(self.end_date, "Date")))
)
def before_insert(self):
self.bootstrap_doctypes_for_closing()

View File

@@ -2,7 +2,7 @@
# See license.txt
import frappe
from frappe.utils import nowdate
from frappe.utils import add_months, nowdate
from erpnext.accounts.doctype.accounting_period.accounting_period import (
ClosedAccountingPeriod,
@@ -93,7 +93,7 @@ def create_accounting_period(**args):
accounting_period = frappe.new_doc("Accounting Period")
accounting_period.start_date = args.start_date or nowdate()
accounting_period.end_date = args.end_date or nowdate()
accounting_period.end_date = args.end_date or add_months(nowdate(), 1)
accounting_period.company = args.company or "_Test Company"
accounting_period.period_name = args.period_name or "_Test_Period_Name_1"
accounting_period.append("closed_documents", {"document_type": "Sales Invoice", "closed": 1})

View File

@@ -73,7 +73,7 @@ class ExchangeRateRevaluation(Document):
def validate_mandatory(self):
if not (self.company and self.posting_date):
frappe.throw(_("Please select Company and Posting Date to get entries"))
frappe.throw(_("Please select Company and Posting Date to getting entries"))
def before_submit(self):
self.remove_accounts_without_gain_loss()

View File

@@ -2,7 +2,6 @@
# See license.txt
import frappe
from frappe.query_builder.functions import Sum
from frappe.utils import today
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
@@ -64,9 +63,13 @@ class TestLoyaltyPointEntry(ERPNextTestSuite):
self.assertEqual(doc.loyalty_points, -7)
# Check balance
lpe = frappe.qb.DocType("Loyalty Point Entry")
balance = (
frappe.qb.from_(lpe).select(Sum(lpe.loyalty_points)).where(lpe.customer == self.customer_name)
).run()[0][0]
balance = frappe.db.sql(
"""
SELECT SUM(loyalty_points)
FROM `tabLoyalty Point Entry`
WHERE customer = %s
""",
(self.customer_name,),
)[0][0]
self.assertEqual(balance, 3) # 10 added, 7 redeemed

View File

@@ -3,7 +3,6 @@
import unittest
import frappe
from frappe.query_builder.functions import Sum
from frappe.utils import cint, flt, getdate, today
from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
@@ -263,12 +262,14 @@ class TestLoyaltyProgram(ERPNextTestSuite):
def get_points_earned(self):
def get_returned_amount():
si = frappe.qb.DocType("Sales Invoice")
returned_amount = (
frappe.qb.from_(si)
.select(Sum(si.grand_total))
.where((si.docstatus == 1) & (si.is_return == 1) & (si.return_against == self.name))
).run()
returned_amount = frappe.db.sql(
"""
select sum(grand_total)
from `tabSales Invoice`
where docstatus=1 and is_return=1 and ifnull(return_against, '')=%s
""",
self.name,
)
return abs(flt(returned_amount[0][0])) if returned_amount else 0
lp_details = get_loyalty_program_details_with_points(

View File

@@ -10,9 +10,9 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_pu
class TestPOSInvoiceMerging(POSInvoiceTestMixin):
def clear_pos_data(self):
frappe.db.delete("POS Opening Entry")
frappe.db.delete("POS Closing Entry")
frappe.db.delete("POS Invoice")
frappe.db.sql("delete from `tabPOS Opening Entry`;")
frappe.db.sql("delete from `tabPOS Closing Entry`;")
frappe.db.sql("delete from `tabPOS Invoice`;")
def setUp(self):
self.clear_pos_data()

View File

@@ -25,11 +25,15 @@ class TestPOSProfile(ERPNextTestSuite):
items = get_items_list(doc, doc.company)
customers = get_customers_list(doc)
products_count = frappe.db.count("Item", {"item_group": "_Test Item Group"})
customers_count = frappe.db.count("Customer", {"customer_group": "_Test Customer Group"})
products_count = frappe.db.sql(
""" select count(name) from tabItem where item_group = '_Test Item Group'""", as_list=1
)
customers_count = frappe.db.sql(
""" select count(name) from tabCustomer where customer_group = '_Test Customer Group'"""
)
self.assertEqual(len(items), products_count)
self.assertEqual(len(customers), customers_count)
self.assertEqual(len(items), products_count[0][0])
self.assertEqual(len(customers), customers_count[0][0])
def test_disabled_pos_profile_creation(self):
make_pos_profile(name="_Test POS Profile 001", disabled=1)
@@ -79,6 +83,7 @@ class TestPOSProfile(ERPNextTestSuite):
def get_customers_list(pos_profile=None):
if pos_profile is None:
pos_profile = {}
cond = "1=1"
customer_groups = []
if pos_profile.get("customer_groups"):
# Get customers based on the customer groups defined in the POS profile
@@ -86,16 +91,14 @@ def get_customers_list(pos_profile=None):
customer_groups.extend(
[d.get("name") for d in get_child_nodes("Customer Group", d.get("customer_group"))]
)
filters = {"disabled": 0}
if customer_groups:
filters["customer_group"] = ["in", customer_groups]
cond = "customer_group in ({})".format(", ".join(["%s"] * len(customer_groups)))
return (
frappe.get_all(
"Customer",
filters=filters,
fields=["name", "customer_name", "customer_group", "territory"],
frappe.db.sql(
f""" select name, customer_name, customer_group, territory from tabCustomer where disabled = 0
and {cond}""",
tuple(customer_groups),
as_dict=1,
)
or {}
)
@@ -132,8 +135,8 @@ def get_items_list(pos_profile, company):
def make_pos_profile(**args):
frappe.db.delete("POS Payment Method")
frappe.db.delete("POS Profile")
frappe.db.sql("delete from `tabPOS Payment Method`")
frappe.db.sql("delete from `tabPOS Profile`")
args = frappe._dict(args)

View File

@@ -91,9 +91,7 @@ class TestPricingRule(ERPNextTestSuite):
details = get_item_details(args)
self.assertEqual(details.get("discount_percentage"), 5)
frappe.db.set_value(
"Pricing Rule", {"campaign": "_Test Campaign"}, "priority", None, update_modified=False
)
frappe.db.sql("update `tabPricing Rule` set priority=NULL where campaign='_Test Campaign'")
from erpnext.accounts.doctype.pricing_rule.utils import MultiplePricingRuleConflict
self.assertRaises(MultiplePricingRuleConflict, get_item_details, args)

View File

@@ -3,7 +3,6 @@
import frappe
from frappe import _
from frappe.query_builder.functions import Sum
from frappe.utils import cint, flt, get_link_to_form
import erpnext
@@ -131,7 +130,6 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import (
get_purchase_document_details,
)
from erpnext.stock.utils import get_valuation_method
doc = self.doc
tax_service = TaxService(doc)
@@ -331,33 +329,20 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
self.make_provisional_gl_entry(gl_entries, item)
if not doc.is_internal_transfer():
handled = False
if (
item.item_code
and item.item_code in stock_items
and item.get("purchase_receipt")
and not doc.is_return
and get_valuation_method(item.item_code, doc.company) == "Standard Cost"
):
handled = self.make_standard_cost_srbnb_split(
gl_entries, item, expense_account, account_currency, base_amount
)
if not handled:
gl_entries.append(
self.get_gl_dict(
{
"account": expense_account,
"against": doc.supplier,
"debit": base_amount,
"debit_in_transaction_currency": amount,
"cost_center": item.cost_center,
"project": item.project or doc.project,
},
account_currency,
item=item,
)
gl_entries.append(
self.get_gl_dict(
{
"account": expense_account,
"against": doc.supplier,
"debit": base_amount,
"debit_in_transaction_currency": amount,
"cost_center": item.cost_center,
"project": item.project or doc.project,
},
account_currency,
item=item,
)
)
# check if the exchange rate has changed
if (
@@ -530,107 +515,6 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
},
)
def make_standard_cost_srbnb_split(
self, gl_entries, item, expense_account, account_currency, base_amount
):
"""For a Standard Cost item billed against a Purchase Receipt, clear SRBNB at the standard
value the receipt actually booked and post the (Net Amount - standard) difference to the
Purchase Price Variance account. Returns False (caller falls back) if the receipt value
can't be resolved."""
from erpnext.stock.doctype.item_standard_cost.item_standard_cost import (
get_purchase_price_variance_account,
)
doc = self.doc
precision = item.precision("base_net_amount")
standard_value = flt(self.get_pr_stock_value(item), precision)
if not standard_value:
return False
gl_entries.append(
self.get_gl_dict(
{
"account": expense_account,
"against": doc.supplier,
"debit": standard_value,
"debit_in_transaction_currency": flt(standard_value / doc.conversion_rate, precision),
"remarks": doc.get("remarks") or _("Accounting Entry for Stock"),
"cost_center": item.cost_center,
"project": item.project or doc.project,
},
account_currency,
item=item,
)
)
variance = flt(base_amount - standard_value, precision)
if variance:
gl_entries.append(
self.get_gl_dict(
{
"account": get_purchase_price_variance_account(item.item_code, doc.company),
"against": doc.supplier,
"debit": variance,
"debit_in_transaction_currency": flt(variance / doc.conversion_rate, precision),
"remarks": doc.get("remarks") or _("Purchase Price Variance"),
"cost_center": item.cost_center,
"project": item.project or doc.project,
},
item=item,
)
)
return True
def get_pr_stock_value(self, item):
"""Stock value (at standard) the linked Purchase Receipt booked for the quantity this invoice
row is billing.
Accepted and rejected stock for the same receipt row share `voucher_detail_no`, so the
warehouse filter is required: without it the accepted warehouse's SRBNB would be cleared at
accepted + rejected value and post the wrong Purchase Price Variance amount. The accepted
warehouse is read from the receipt row itself (not the invoice row, which may be unset on a
non-stock invoice).
The receipt's full accepted value is pro-rated to the invoiced quantity, so a partial bill
clears SRBNB (and posts PPV) for only the units it covers, not the whole receipt row."""
pr_detail = frappe.db.get_value(
"Purchase Receipt Item", item.pr_detail, ["warehouse", "stock_qty"], as_dict=True
)
if not pr_detail or not pr_detail.warehouse:
return 0.0
sle = frappe.qb.DocType("Stock Ledger Entry")
result = (
frappe.qb.from_(sle)
.select(Sum(sle.stock_value_difference))
.where(
(sle.voucher_type == "Purchase Receipt")
& (sle.voucher_no == item.purchase_receipt)
& (sle.voucher_detail_no == item.pr_detail)
& (sle.warehouse == pr_detail.warehouse)
& (sle.is_cancelled == 0)
)
).run()
accepted_value = flt(result[0][0]) if result and result[0][0] else 0.0
if not accepted_value or not flt(pr_detail.stock_qty):
return accepted_value
# Pro-rate to the quantity being billed by this invoice row (handles partial billing).
return accepted_value * flt(item.stock_qty) / flt(pr_detail.stock_qty)
def get_stock_variance_account(self, item):
"""For Standard Cost items the purchase-price-vs-standard difference is a Purchase Price
Variance; for all other items it keeps the existing behaviour (default expense account)."""
from erpnext.stock.doctype.item_standard_cost.item_standard_cost import (
get_purchase_price_variance_account,
)
from erpnext.stock.utils import get_valuation_method
if item.item_code and get_valuation_method(item.item_code, self.doc.company) == "Standard Cost":
return get_purchase_price_variance_account(item.item_code, self.doc.company)
return self.doc.get_company_default("default_expense_account")
def make_stock_adjustment_entry(self, gl_entries, item, voucher_wise_stock_value, account_currency):
doc = self.doc
net_amt_precision = item.precision("base_net_amount")
@@ -652,7 +536,7 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
)
if flt(stock_amount, net_amt_precision) != flt(warehouse_debit_amount, net_amt_precision):
cost_of_goods_sold_account = self.get_stock_variance_account(item)
cost_of_goods_sold_account = doc.get_company_default("default_expense_account")
stock_adjustment_amt = stock_amount - warehouse_debit_amount
gl_entries.append(
@@ -677,7 +561,7 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
and warehouse_debit_amount
!= flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)
):
cost_of_goods_sold_account = self.get_stock_variance_account(item)
cost_of_goods_sold_account = doc.get_company_default("default_expense_account")
stock_amount = flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)
stock_adjustment_amt = warehouse_debit_amount - stock_amount

View File

@@ -135,9 +135,38 @@ class TestAccountsPayable(ERPNextTestSuite, AccountsTestMixin):
def test_payment_terms_template_filters(self):
from erpnext.controllers.accounts_controller import get_payment_terms
template = frappe.get_doc("Payment Terms Template", "_Test Payment Term Template")
first_term = frappe.get_doc("Payment Term", template.terms[0].payment_term)
expected_payment_term = first_term.description or first_term.name
payment_term1 = frappe.get_doc(
{"doctype": "Payment Term", "payment_term_name": "_Test 50% on 15 Days"}
).insert()
payment_term2 = frappe.get_doc(
{"doctype": "Payment Term", "payment_term_name": "_Test 50% on 30 Days"}
).insert()
template = frappe.get_doc(
{
"doctype": "Payment Terms Template",
"template_name": "_Test 50-50",
"terms": [
{
"doctype": "Payment Terms Template Detail",
"due_date_based_on": "Day(s) after invoice date",
"payment_term": payment_term1.name,
"description": "_Test 50-50",
"invoice_portion": 50,
"credit_days": 15,
},
{
"doctype": "Payment Terms Template Detail",
"due_date_based_on": "Day(s) after invoice date",
"payment_term": payment_term2.name,
"description": "_Test 50-50",
"invoice_portion": 50,
"credit_days": 30,
},
],
}
)
template.insert()
filters = {
"company": self.company,
@@ -164,10 +193,12 @@ class TestAccountsPayable(ERPNextTestSuite, AccountsTestMixin):
row = report[1][0]
self.assertEqual(len(report[1]), 2)
self.assertEqual([pi.name, expected_payment_term], [row.voucher_no, row.payment_term])
self.assertEqual([pi.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])
def test_project_filter(self):
project = frappe.get_doc("Project", {"project_name": "_Test Project"})
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AP Project", "company": self.company}
).insert()
pi = self.create_purchase_invoice(do_not_submit=True)
pi.project = project.name
@@ -196,7 +227,9 @@ class TestAccountsPayable(ERPNextTestSuite, AccountsTestMixin):
"range": "30, 60, 90, 120",
}
project = frappe.get_doc("Project", {"project_name": "_Test Project"})
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AP Project Output", "company": self.company}
).insert()
pi = self.create_purchase_invoice(do_not_submit=True)
pi.project = project.name

View File

@@ -1422,10 +1422,10 @@ class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
# Party is a dynamic link on Payment Ledger Entry, so user permissions on Customer
# must be applied explicitly. The report should only show permitted customers.
original_customer = self.customer
second_customer = "_Test Customer 1"
second_customer = "_Test AR Perm Customer"
# create_customer overrides self.customer, so build the restricted invoice first
self.customer = second_customer
self.create_customer(customer_name=second_customer)
self.create_sales_invoice(no_payment_schedule=True)
self.customer = original_customer

View File

@@ -59,9 +59,16 @@ class TestCashFlow(ERPNextTestSuite):
def test_cash_purchase_of_asset_is_investing_outflow(self):
"""Buying a fixed asset for cash is an investing outflow that reduces net change in cash."""
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
asset_account = "Office Equipment - _TC"
create_account(
account_name="_Test Cash Flow Asset",
company=self.company,
parent_account="Fixed Assets - _TC",
account_type="Fixed Asset",
)
asset_account = "_Test Cash Flow Asset - _TC"
before = self.net_change_in_cash()
# debit the fixed asset, credit cash -> cash goes out

View File

@@ -64,12 +64,16 @@ class TestGeneralLedger(ERPNextTestSuite):
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
def test_opening_total_and_closing_balances(self):
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
self.clear_old_entries()
# reuse bootstrap non-party accounts; clear_old_entries() leaves them clean of GL
account = "_Test Account Cost for Goods Sold - _TC"
offset = "_Test Bank - _TC"
account = create_account(
account_name="_Test GL Account", company=self.company, parent_account="Current Assets - _TC"
)
offset = create_account(
account_name="_Test GL Offset", company=self.company, parent_account="Current Assets - _TC"
)
make_journal_entry(account, offset, 1000, posting_date=add_days(today(), -60), submit=True) # opening
make_journal_entry(account, offset, 200, posting_date=today(), submit=True) # in period
@@ -83,13 +87,19 @@ class TestGeneralLedger(ERPNextTestSuite):
self.assertEqual(labelled["'Closing (Opening + Total)'"]["debit"], 1200)
def test_categorize_by_account_subtotals(self):
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
self.clear_old_entries()
# reuse bootstrap non-party accounts; clear_old_entries() leaves them clean of GL
account_a = "_Test Account Cost for Goods Sold - _TC"
account_b = "_Test Bank - _TC"
offset = "_Test Cash - _TC"
account_a = create_account(
account_name="_Test GL Account A", company=self.company, parent_account="Current Assets - _TC"
)
account_b = create_account(
account_name="_Test GL Account B", company=self.company, parent_account="Current Assets - _TC"
)
offset = create_account(
account_name="_Test GL Offset", company=self.company, parent_account="Current Assets - _TC"
)
make_journal_entry(account_a, offset, 300, posting_date=today(), submit=True)
make_journal_entry(account_b, offset, 400, posting_date=today(), submit=True)

View File

@@ -597,21 +597,11 @@ def execute_synced_report(filters):
def get_data_duckdb(filters, conn):
# accounts and all metadata via frappe.db — only GL Entry comes from DuckDB
accounts = frappe.get_all(
"Account",
filters={"company": filters.company},
fields=[
"name",
"account_number",
"parent_account",
"account_name",
"root_type",
"report_type",
"is_group",
"lft",
"rgt",
],
order_by="lft",
accounts = frappe.db.sql(
"""select name, account_number, parent_account, account_name, root_type, report_type, is_group, lft, rgt
from `tabAccount` where company=%s order by lft""",
filters.company,
as_dict=True,
)
if not accounts:
return None

View File

@@ -1,117 +0,0 @@
# 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

@@ -238,13 +238,22 @@ class TestAssetRepair(ERPNextTestSuite):
submit=1,
)
gle = frappe.qb.DocType("GL Entry")
gl_entries = (
frappe.qb.from_(gle)
.select(gle.account, Sum(gle.debit).as_("debit"), Sum(gle.credit).as_("credit"))
.where((gle.voucher_type == "Asset Repair") & (gle.voucher_no == asset_repair.name))
.groupby(gle.account)
).run(as_dict=True)
gl_entries = frappe.db.sql(
"""
select
account,
sum(debit) as debit,
sum(credit) as credit
from `tabGL Entry`
where
voucher_type='Asset Repair'
and voucher_no=%s
group by
account
""",
asset_repair.name,
as_dict=1,
)
self.assertTrue(gl_entries)
@@ -278,13 +287,22 @@ class TestAssetRepair(ERPNextTestSuite):
submit=1,
)
gle = frappe.qb.DocType("GL Entry")
gl_entries = (
frappe.qb.from_(gle)
.select(gle.account, Sum(gle.debit).as_("debit"), Sum(gle.credit).as_("credit"))
.where((gle.voucher_type == "Asset Repair") & (gle.voucher_no == asset_repair.name))
.groupby(gle.account)
).run(as_dict=True)
gl_entries = frappe.db.sql(
"""
select
account,
sum(debit) as debit,
sum(credit) as credit
from `tabGL Entry`
where
voucher_type='Asset Repair'
and voucher_no=%s
group by
account
""",
asset_repair.name,
as_dict=1,
)
self.assertTrue(gl_entries)

View File

@@ -94,12 +94,11 @@ class TestAssetValueAdjustment(ERPNextTestSuite):
("_Test Fixed Asset - _TC", 0.0, 4625.29),
)
gle = frappe.get_all(
"GL Entry",
filters={"voucher_type": "Journal Entry", "voucher_no": adj_doc.journal_entry},
fields=["account", "debit", "credit"],
order_by="account",
as_list=True,
gle = frappe.db.sql(
"""select account, debit, credit from `tabGL Entry`
where voucher_type='Journal Entry' and voucher_no = %s
order by account""",
adj_doc.journal_entry,
)
self.assertSequenceEqual(gle, expected_gle)
@@ -185,12 +184,11 @@ class TestAssetValueAdjustment(ERPNextTestSuite):
("_Test Fixed Asset - _TC", 0.0, 5175.29),
)
gle = frappe.get_all(
"GL Entry",
filters={"voucher_type": "Journal Entry", "voucher_no": adj_doc.journal_entry},
fields=["account", "debit", "credit"],
order_by="account",
as_list=True,
gle = frappe.db.sql(
"""select account, debit, credit from `tabGL Entry`
where voucher_type='Journal Entry' and voucher_no = %s
order by account""",
adj_doc.journal_entry,
)
self.assertSequenceEqual(gle, expected_gle)

View File

@@ -3,26 +3,11 @@
import frappe
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries
from erpnext.assets.doctype.asset.test_asset import AssetSetup, create_asset
from erpnext.assets.doctype.asset_capitalization.test_asset_capitalization import (
create_asset_capitalization,
)
from erpnext.assets.doctype.asset_value_adjustment.test_asset_value_adjustment import (
make_asset_value_adjustment,
)
from erpnext.assets.report.fixed_asset_register.fixed_asset_register import execute
class TestFixedAssetRegister(AssetSetup):
def run_report(self, **extra):
filters = frappe._dict(company="_Test Company", **extra)
return execute(filters)[1]
def report_row(self, asset_name, **extra):
return next(row for row in self.run_report(**extra) if row["asset_id"] == asset_name)
def test_report_lists_submitted_asset(self):
"""Exercises the report's converted queries -- including the depreciation aggregate that groups
by asset.name (must be valid on Postgres) -- by asserting a submitted asset is listed."""
@@ -33,170 +18,16 @@ class TestFixedAssetRegister(AssetSetup):
location="Test Location",
submit=1,
)
ids = {
row["asset_id"]
for row in self.run_report(
status="In Location",
filter_based_on="Date Range",
from_date="2020-01-01",
to_date="2030-12-31",
date_based_on="Purchase Date",
)
}
self.assertIn(asset.name, ids)
def test_asset_appears_with_purchase_value(self):
asset = create_asset(
item_code="Macbook Pro", net_purchase_amount=100000, purchase_amount=100000, submit=True
filters = frappe._dict(
{
"company": "_Test Company",
"status": "In Location",
"filter_based_on": "Date Range",
"from_date": "2020-01-01",
"to_date": "2030-12-31",
"date_based_on": "Purchase Date",
}
)
row = self.report_row(asset.name)
self.assertEqual(row["net_purchase_amount"], 100000)
self.assertEqual(row["asset_value"], 100000) # no depreciation yet
self.assertEqual(row["asset_category"], "Computers")
def test_asset_value_reduced_by_opening_depreciation(self):
asset = create_asset(
item_code="Macbook Pro",
net_purchase_amount=100000,
purchase_amount=100000,
opening_accumulated_depreciation=20000,
opening_number_of_booked_depreciations=2,
submit=True,
)
row = self.report_row(asset.name)
self.assertEqual(row["opening_accumulated_depreciation"], 20000)
self.assertEqual(row["asset_value"], 80000) # 100000 - 20000
def test_status_in_location_filter_shows_active_asset(self):
asset = create_asset(
item_code="Macbook Pro", net_purchase_amount=100000, purchase_amount=100000, submit=True
)
ids = {row["asset_id"] for row in self.run_report(status="In Location")}
self.assertIn(asset.name, ids)
def test_asset_category_filter(self):
asset = create_asset(
item_code="Macbook Pro", net_purchase_amount=100000, purchase_amount=100000, submit=True
)
ids = {row["asset_id"] for row in self.run_report(asset_category="Computers")}
self.assertIn(asset.name, ids)
def test_group_by_asset_category_sums_values(self):
before_net, before_value = self.computers_group_totals()
create_asset(item_code="Macbook Pro", net_purchase_amount=100000, purchase_amount=100000, submit=True)
create_asset(
item_code="Macbook Pro",
asset_name="Macbook Pro 2",
net_purchase_amount=50000,
purchase_amount=50000,
submit=True,
)
after_net, after_value = self.computers_group_totals()
# assert on the delta so pre-existing Computers assets don't skew the totals
self.assertEqual(after_net - before_net, 150000)
self.assertEqual(after_value - before_value, 150000)
def computers_group_totals(self):
row = next(
(r for r in self.run_report(group_by="Asset Category") if r["asset_category"] == "Computers"),
None,
)
return (row["net_purchase_amount"], row["asset_value"]) if row else (0, 0)
def test_booked_depreciation_reduces_asset_value(self):
asset = create_asset(
item_code="Macbook Pro",
calculate_depreciation=1,
available_for_use_date="2019-12-31",
depreciation_start_date="2020-12-31",
frequency_of_depreciation=12,
total_number_of_depreciations=3,
expected_value_after_useful_life=10000,
net_purchase_amount=100000,
purchase_amount=100000,
submit=True,
)
# books one depreciation entry of (100000 - 10000) / 3 = 30000
post_depreciation_entries(date="2021-01-01")
row = self.report_row(asset.name)
self.assertEqual(row["depreciated_amount"], 30000)
self.assertEqual(row["asset_value"], 70000) # 100000 - 30000
def test_revaluation_adjusts_asset_value(self):
asset = create_asset(
item_code="Macbook Pro", net_purchase_amount=100000, purchase_amount=100000, submit=True
)
# revalue the asset upwards by 20000
make_asset_value_adjustment(
asset=asset.name, current_asset_value=100000, new_asset_value=120000
).submit()
row = self.report_row(asset.name)
self.assertEqual(row["asset_value"], 120000) # 100000 + 20000 revaluation
def test_depreciation_and_revaluation_together(self):
asset = create_asset(
item_code="Macbook Pro",
calculate_depreciation=1,
available_for_use_date="2019-12-31",
depreciation_start_date="2020-12-31",
frequency_of_depreciation=12,
total_number_of_depreciations=3,
expected_value_after_useful_life=10000,
net_purchase_amount=100000,
purchase_amount=100000,
submit=True,
)
# books one depreciation entry of (100000 - 10000) / 3 = 30000, leaving 70000
post_depreciation_entries(date="2021-01-01")
# revalue the depreciated asset down from 70000 to 60000
make_asset_value_adjustment(
asset=asset.name, current_asset_value=70000, new_asset_value=60000
).submit()
row = self.report_row(asset.name)
self.assertEqual(row["depreciated_amount"], 30000)
self.assertEqual(row["asset_value"], 60000) # 100000 - 30000 depreciation - 10000 revaluation
def test_sold_asset_hidden_from_in_location_and_shown_in_disposed(self):
asset = create_asset(
item_code="Macbook Pro", net_purchase_amount=100000, purchase_amount=100000, submit=True
)
create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=80000)
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
self.assertNotIn(asset.name, {row["asset_id"] for row in self.run_report(status="In Location")})
self.assertIn(asset.name, {row["asset_id"] for row in self.run_report(status="Disposed")})
def test_capitalized_asset_hidden_from_in_location_and_shown_in_disposed(self):
consumed_asset = create_asset(
asset_name="Consumed Asset",
net_purchase_amount=100000,
purchase_amount=100000,
submit=True,
)
composite_asset = create_asset(
asset_name="Composite Asset", asset_type="Composite Asset", submit=False
)
create_asset_capitalization(
target_asset=composite_asset.name, consumed_asset=consumed_asset.name, submit=1
)
self.assertEqual(frappe.db.get_value("Asset", consumed_asset.name, "status"), "Capitalized")
self.assertNotIn(
consumed_asset.name, {row["asset_id"] for row in self.run_report(status="In Location")}
)
self.assertIn(consumed_asset.name, {row["asset_id"] for row in self.run_report(status="Disposed")})
data = execute(filters)[1]
asset_ids = {row.get("asset_id") for row in data}
self.assertIn(asset.name, asset_ids)

View File

@@ -547,7 +547,6 @@
"fieldtype": "Data",
"in_global_search": 1,
"label": "Alias",
"no_copy": 1,
"unique": 1
}
],
@@ -562,7 +561,7 @@
"link_fieldname": "party"
}
],
"modified": "2026-06-27 16:12:33.190257",
"modified": "2026-06-22 12:23:09.241125",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",

View File

@@ -274,7 +274,7 @@ class AccountsController(TransactionBase):
if invalid_advances := [x for x in self.advances if not x.reference_type or not x.reference_name]:
frappe.throw(
_(
"Rows: {0} in {1} section are invalid. Reference Name should point to a valid Payment Entry or Journal Entry."
"Rows: {0} in {1} section are Invalid. Reference Name should point to a valid Payment Entry or Journal Entry."
).format(
frappe.bold(comma_and([x.idx for x in invalid_advances])),
frappe.bold(_("Advance Payments")),
@@ -1233,7 +1233,7 @@ class AccountsController(TransactionBase):
{"sales_order": None, "sales_order_item": None},
)
frappe.msgprint(_("Purchase Orders {0} are unlinked").format("\n".join(linked_po)))
frappe.msgprint(_("Purchase Orders {0} are un-linked").format("\n".join(linked_po)))
def get_company_default(self, fieldname, ignore_validation=False):
from erpnext.accounts.utils import get_company_default

View File

@@ -129,7 +129,7 @@ class BuyingController(SubcontractingController):
msg += f"<li>{po} ({date})</li>"
msg += "</ul>"
frappe.throw(msg)
frappe.throw(_(msg))
def create_package_for_transfer(self) -> None:
"""Create serial and batch package for Sourece Warehouse in case of inter transfer."""
@@ -287,7 +287,7 @@ class BuyingController(SubcontractingController):
if self.is_return and len(not_cancelled_asset):
frappe.throw(
_(
"{0} has submitted assets linked to it. You need to cancel the assets to create purchase return."
"{} has submitted assets linked to it. You need to cancel the assets to create purchase return."
).format(self.return_against),
title=_("Not Allowed"),
)
@@ -738,7 +738,7 @@ class BuyingController(SubcontractingController):
frappe.throw(
_("Row #{idx}: {field_label} can not be negative for item {item_code}.").format(
idx=item_row["idx"],
field_label=_(frappe.get_meta(item_row.doctype).get_label(fieldname)),
field_label=frappe.get_meta(item_row.doctype).get_label(fieldname),
item_code=frappe.bold(item_row["item_code"]),
)
)

View File

@@ -77,7 +77,7 @@ def validate_return_against(doc):
# validate update stock
if doc.doctype == "Sales Invoice" and doc.update_stock and not ref_doc.update_stock:
frappe.throw(
_("'Update Stock' cannot be checked because items are not delivered via {0}").format(
_("'Update Stock' can not be checked because items are not delivered via {0}").format(
doc.return_against
)
)

View File

@@ -297,7 +297,7 @@ class SellingController(StockController):
throw(
_(
"""Row #{0}: Selling rate for item {1} is lower than its {2}.
Selling {3} should be at least {4}.<br><br>Alternatively,
Selling {3} should be atleast {4}.<br><br>Alternatively,
you can disable '{5}' in {6} to bypass
this validation."""
).format(
@@ -869,7 +869,7 @@ class SellingController(StockController):
duplicate_items_msg = _("Item {0} entered multiple times.").format(frappe.bold(d.item_code))
duplicate_items_msg += "<br><br>"
duplicate_items_msg += _("Please enable {0} in {1} to allow same item in multiple rows").format(
duplicate_items_msg += _("Please enable {} in {} to allow same item in multiple rows").format(
frappe.bold(_("Allow Item to Be Added Multiple Times in a Transaction")),
get_link_to_form("Selling Settings", "Selling Settings"),
)
@@ -898,7 +898,7 @@ class SellingController(StockController):
if not self.get("is_internal_customer") and any(d.get("target_warehouse") for d in items):
msg = _("Target Warehouse is set for some items but the customer is not an internal customer.")
msg += " " + _("This {0} will be treated as material transfer.").format(_(self.doctype))
msg += " " + _("This {} will be treated as material transfer.").format(_(self.doctype))
frappe.msgprint(msg, title="Internal Transfer", alert=True)
def validate_items(self):

View File

@@ -286,10 +286,10 @@ class StatusUpdater(Document):
# get unique transactions to update
for d in self.get_all_children():
if hasattr(d, "qty") and flt(d.qty) < 0 and not self.get("is_return"):
frappe.throw(_("For an item {0}, quantity must be a positive number").format(d.item_code))
frappe.throw(_("For an item {0}, quantity must be positive number").format(d.item_code))
if hasattr(d, "qty") and flt(d.qty) > 0 and self.get("is_return"):
frappe.throw(_("For an item {0}, quantity must be a negative number").format(d.item_code))
frappe.throw(_("For an item {0}, quantity must be negative number").format(d.item_code))
if (
not selling_negative_rate_allowed and self.doctype in ["Sales Invoice", "Delivery Note"]
@@ -300,7 +300,7 @@ class StatusUpdater(Document):
if hasattr(d, "item_code") and hasattr(d, "rate") and flt(d.rate) < 0:
frappe.throw(
_(
"For item {0}, rate must be a positive number. To allow negative rates, enable {1} in {2}"
"For item {0}, rate must be a positive number. To Allow negative rates, enable {1} in {2}"
).format(
frappe.bold(d.item_code),
frappe.bold(_("`Allow Negative rates for Items`")),

View File

@@ -820,8 +820,6 @@ def create_item_wise_repost_entries(
):
"""Using a voucher create repost item valuation records for all item-warehouse pairs."""
from erpnext.stock.utils import get_valuation_method
stock_ledger_entries = get_items_to_be_repost(voucher_type, voucher_no)
distinct_item_warehouses = set()
@@ -833,11 +831,6 @@ def create_item_wise_repost_entries(
continue
distinct_item_warehouses.add(item_wh)
# Standard Cost items don't need a full repost: a backdated entry only shifts future balances
# (qty and value at the standard rate), which is done in place by update_qty_in_future_sle.
if get_valuation_method(sle.item_code) == "Standard Cost":
continue
repost_entry = frappe.new_doc("Repost Item Valuation")
repost_entry.based_on = "Item and Warehouse"

View File

@@ -211,7 +211,7 @@ class SubcontractingController(StockController):
)
if bom_item != item.item_code:
frappe.throw(
_("Row {0}: Please select a valid BOM for Item {1}.").format(
_("Row {0}: Please select an valid BOM for Item {1}.").format(
item.idx, item.item_name
)
)
@@ -1053,10 +1053,8 @@ class SubcontractingController(StockController):
link = get_link_to_form(
self.subcontract_data.order_doctype, row.get(self.subcontract_data.order_field)
)
msg = _("The Batch No {0} has not been supplied against the {1} {2}").format(
frappe.bold(row.get("batch_no")), self.subcontract_data.order_doctype, link
)
frappe.throw(msg, title=_("Incorrect Batch Consumed"))
msg = f'The Batch No {frappe.bold(row.get("batch_no"))} has not supplied against the {self.subcontract_data.order_doctype} {link}'
frappe.throw(_(msg), title=_("Incorrect Batch Consumed"))
def __validate_serial_no(self, row, key):
if row.get("serial_and_batch_bundle") and self.__transferred_items.get(key).get("serial_no"):
@@ -1068,10 +1066,8 @@ class SubcontractingController(StockController):
link = get_link_to_form(
self.subcontract_data.order_doctype, row.get(self.subcontract_data.order_field)
)
msg = _("The Serial Nos {0} have not been supplied against the {1} {2}").format(
incorrect_sn, self.subcontract_data.order_doctype, link
)
frappe.throw(msg, title=_("Incorrect Serial Number Consumed"))
msg = f"The Serial Nos {incorrect_sn} has not supplied against the {self.subcontract_data.order_doctype} {link}"
frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed"))
def __validate_supplied_or_received_items(self):
if self.doctype not in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:

View File

@@ -78,7 +78,7 @@ class SubcontractingInwardController:
):
frappe.throw(
_(
"Row #{0}: Item {1} mismatch. Changing the item code is not permitted, add another row instead."
"Row #{0}: Item {1} mismatch. Changing of item code is not permitted, add another row instead."
).format(item.idx, get_link_to_form("Item", item.item_code))
)
@@ -126,7 +126,7 @@ class SubcontractingInwardController:
or frappe.get_cached_value("Subcontracting Inward Order Item", item.scio_detail, "item_code")
):
frappe.throw(
_("Row #{0}: Item {1} mismatch. Changing the item code is not permitted.").format(
_("Row #{0}: Item {1} mismatch. Changing of item code is not permitted.").format(
item.idx, get_link_to_form("Item", item.item_code)
)
)
@@ -441,7 +441,7 @@ class SubcontractingInwardController:
):
frappe.throw(
_(
"Row #{0}: Batch No(s) {1} are not a part of the linked Subcontracting Inward Order. Please select valid Batch No(s)."
"Row #{0}: Batch No(s) {1} is not a part of the linked Subcontracting Inward Order. Please select valid Batch No(s)."
).format(
item.idx,
", ".join([get_link_to_form("Batch No", bn) for bn in incorrect_batch_nos]),

View File

@@ -131,9 +131,9 @@ class calculate_taxes_and_totals:
if item.item_tax_template not in taxes:
item.item_tax_template = taxes[0]
frappe.msgprint(
_(
"Row {0}: Item Tax template for {1} updated as per validity and rate applied"
).format(item.idx, frappe.bold(item.item_code))
_("Row {0}: Item Tax template updated as per validity and rate applied").format(
item.idx, frappe.bold(item.item_code)
)
)
# For correct tax_amount calculation re-computation is required
@@ -564,7 +564,7 @@ class calculate_taxes_and_totals:
+ "<br>".join(invalid_rows)
)
frappe.throw(message)
frappe.throw(_(message))
def get_tax_amount_if_for_valuation_or_deduction(self, tax_amount, tax):
# if just for valuation, do not add the tax amount in total

View File

@@ -56,10 +56,10 @@ def validate_filters(filters):
frappe.throw(_("{0} is mandatory").format(_(f)))
if not frappe.db.exists("Fiscal Year", filters.get("fiscal_year")):
frappe.throw(_("Fiscal Year {0} does not exist").format(filters.get("fiscal_year")))
frappe.throw(_("Fiscal Year {0} Does Not Exist").format(filters.get("fiscal_year")))
if filters.get("based_on") == filters.get("group_by"):
frappe.throw(_("'Based On' and 'Group By' can not be the same"))
frappe.throw(_("'Based On' and 'Group By' can not be same"))
if filters.get("period_based_on") and filters.period_based_on not in ["bill_date", "posting_date"]:
frappe.throw(

View File

@@ -308,4 +308,4 @@ def add_role_for_portal_user(portal_user, role):
return
user_doc.add_roles(role)
frappe.msgprint(_("Added {1} role to user {0}.").format(frappe.bold(user_doc.name), role), alert=True)
frappe.msgprint(_("Added {1} Role to User {0}.").format(frappe.bold(user_doc.name), role), alert=True)

View File

@@ -59,7 +59,7 @@ class AppointmentBookingSettings(Document):
err_msg = _("<b>From Time</b> cannot be later than <b>To Time</b> for {0}").format(
record.day_of_week
)
frappe.throw(err_msg)
frappe.throw(_(err_msg))
def duration_is_divisible(self, from_time, to_time):
timedelta = to_time - from_time

View File

@@ -3,7 +3,7 @@
import frappe
from frappe import _
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields, delete_custom_fields
from frappe.model.document import Document
@@ -49,7 +49,7 @@ class CRMSettings(Document):
if self.enable_frappe_crm_data_synchronization and not self.allowed_users:
frappe.throw(
_(
"Please add at least one user on Allowed Users to allow Data Synchronization from Frappe CRM site."
"Please add atleast one user on Allowed Users to allow Data Synchronization from Frappe CRM site."
)
)

View File

@@ -171,7 +171,7 @@ def add_bank_accounts(response: str | dict, bank: str | dict, company: str):
except Exception:
frappe.log_error("Plaid Link Error")
frappe.throw(
_("There was an error updating Bank Account {0} while linking with Plaid.").format(
_("There was an error updating Bank Account {} while linking with Plaid.").format(
existing_bank_account
),
title=_("Plaid Link Failed"),

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: hello@frappe.io\n"
"POT-Creation-Date: 2026-06-21 10:42+0000\n"
"PO-Revision-Date: 2026-06-23 19:26\n"
"PO-Revision-Date: 2026-06-21 19:01\n"
"Last-Translator: hello@frappe.io\n"
"Language-Team: Arabic\n"
"MIME-Version: 1.0\n"
@@ -21009,7 +21009,7 @@ msgstr "للتشغيل"
#: banking/src/pages/BankStatementImporter.tsx:172
msgid "For PDF statements, we auto-detect the tables on each page. You can then confirm each detected table, map its columns, and exclude anything that is not transactions (e.g. ads or summaries). Password-protected PDFs are supported - the password is saved on the bank account and reused."
msgstr "بالنسبة لبيانات PDF، نقوم بالكشف التلقائي عن الجداول في كل صفحة. يمكنك بعد ذلك تأكيد كل جدول تم اكتشافه، وتعيين أعمدته، واستبعاد أي شيء لا يمثل معاملات (مثل الإعلانات أو الملخصات). يتم دعم ملفات PDF المحمية بكلمة مرور - يتم حفظ كلمة المرور في الحساب البنكي وإعادة استخدامها."
msgstr ""
#. Label of the for_price_list (Link) field in DocType 'Pricing Rule'
#. Label of the for_price_list (Link) field in DocType 'Promotional Scheme

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: hello@frappe.io\n"
"POT-Creation-Date: 2026-06-21 10:42+0000\n"
"PO-Revision-Date: 2026-06-24 19:23\n"
"PO-Revision-Date: 2026-06-21 19:03\n"
"Last-Translator: hello@frappe.io\n"
"Language-Team: Bosnian\n"
"MIME-Version: 1.0\n"
@@ -27,8 +27,8 @@ msgid "\n"
"\t\t\tSo please ensure the stock levels are adjusted as soon as possible to maintain the correct valuation rate."
msgstr "\n"
"\t\t\tŠarža {0} artikla {1} ima negativne zalihe u skladištu {2}{3}.\n"
"\t\t\tDodaj količinu zaliha od {4} da biste nastavili s ovim unosom.\n"
"\t\t\tAko nije moguće izvršiti unos prilagođavanja, omogućite 'Dozvoli Negativne Zalihe za Šaržu' za Šaržu {0} ili u Postavkama Zaliha da biste nastavili.\n"
"\t\t\tMolimo dodajte količinu zaliha od {4} da biste nastavili s ovim unosom.\n"
"\t\t\tAko nije moguće izvršiti unos prilagođavanja, omogućite 'Dozvoli Negativne Zalihe za Šaržu' ya Šaržu {0} ili u Postavkama Zaliha da biste nastavili.\n"
"\t\t\tMeđutim, omogućavanje ove postavke može dovesti do negativnih zaliha u sistemu.\n"
"\t\t\tStoga, molimo vas da osigurate da se nivoi zaliha što prije prilagode kako bi se održala ispravna stopa vrednovanja."
@@ -12197,7 +12197,7 @@ msgstr "Konsolidirana Prodajna Faktura"
#. Name of a report
#: erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.json
msgid "Consolidated Trial Balance"
msgstr "Konsolidovani Probni Bilans"
msgstr "Konsolidovani Bruto Bilans"
#: erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.py:71
msgid "Consolidated Trial Balance can be generated for Companies having same root Company."
@@ -12205,7 +12205,7 @@ msgstr "Konsolidovani Bruto Bilans može se generirati za poduzeća koje imaju i
#: erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.py:167
msgid "Consolidated Trial balance could not be generated as Exchange Rate from {0} to {1} is not available for {2}."
msgstr "Konsolidovani Probni Bilans nije mogao biti generisan jer kurs valute od {0} do {1} nije dostupan za {2}."
msgstr "Konsolidovani Bruto Bilans nije mogao biti generisan jer kurs od {0} do {1} nije dostupan za {2}."
#. Option for the 'Lead Type' (Select) field in DocType 'Lead'
#: erpnext/crm/doctype/lead/lead.json
@@ -58110,7 +58110,7 @@ msgstr "Stablo Procedura"
#: erpnext/workspace_sidebar/invoicing.json
#: erpnext/workspace_sidebar/payments.json
msgid "Trial Balance"
msgstr "Probni Bilans"
msgstr "Bruto Stanje"
#. Name of a report
#: erpnext/accounts/report/trial_balance_simple/trial_balance_simple.json
@@ -58124,7 +58124,7 @@ msgstr "Bruto Stanje (Jednostavno)"
#: erpnext/accounts/workspace/financial_reports/financial_reports.json
#: erpnext/workspace_sidebar/financial_reports.json
msgid "Trial Balance for Party"
msgstr "Probni Bilans Stranke"
msgstr "Bruto Stanje Stranke"
#. Label of the trial_period_end (Date) field in DocType 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: hello@frappe.io\n"
"POT-Creation-Date: 2026-06-21 10:42+0000\n"
"PO-Revision-Date: 2026-06-23 19:26\n"
"PO-Revision-Date: 2026-06-21 19:02\n"
"Last-Translator: hello@frappe.io\n"
"Language-Team: German\n"
"MIME-Version: 1.0\n"
@@ -4293,7 +4293,7 @@ msgstr "Verkauf erlauben"
#. in DocType 'Selling Settings'
#: erpnext/selling/doctype/selling_settings/selling_settings.json
msgid "Allow Sales Order creation for expired Quotation"
msgstr "Auftragserstellung für abgelaufene Angebote zulassen"
msgstr ""
#. Label of the allow_zero_qty_in_sales_order (Check) field in DocType 'Selling
#. Settings'
@@ -4409,7 +4409,7 @@ msgstr "Rechnungswährung darf sich von Kontowährung unterscheiden"
#. 'Selling Settings'
#: erpnext/selling/doctype/selling_settings/selling_settings.json
msgid "Allow multiple Sales Orders against a customer's Purchase Order"
msgstr "Mehrere Aufträge (je Kunde) mit derselben Bestellnummer erlauben"
msgstr ""
#. Label of the allow_negative_rates_for_items (Check) field in DocType 'Buying
#. Settings'
@@ -48219,7 +48219,7 @@ msgstr "Einsparungen"
#. Name of a UOM
#: erpnext/setup/setup_wizard/data/uom_data.json
msgid "Sazhen"
msgstr "Saschen"
msgstr ""
#. Label of the scan_barcode (Data) field in DocType 'POS Invoice'
#. Label of the scan_barcode (Data) field in DocType 'Purchase Invoice'
@@ -51463,7 +51463,7 @@ msgstr "Quadratmeile"
#. Name of a UOM
#: erpnext/setup/setup_wizard/data/uom_data.json
msgid "Square Yard"
msgstr "Quadratyard"
msgstr ""
#. Label of the stage_name (Data) field in DocType 'Sales Stage'
#: erpnext/crm/doctype/sales_stage/sales_stage.json
@@ -52544,7 +52544,7 @@ msgstr "Lagerbestände/Konten können nicht eingefroren werden, da die Verarbeit
#. Name of a UOM
#: erpnext/setup/setup_wizard/data/uom_data.json
msgid "Stone"
msgstr "Stone"
msgstr ""
#. Label of the stop_reason (Select) field in DocType 'Downtime Entry'
#: erpnext/manufacturing/doctype/downtime_entry/downtime_entry.json
@@ -59818,7 +59818,7 @@ msgstr "Wert oder Menge"
#. Name of a UOM
#: erpnext/setup/setup_wizard/data/uom_data.json
msgid "Vara"
msgstr "Vara"
msgstr ""
#. Label of the variable (Data) field in DocType 'Bank Statement Import Log
#. Column Map'
@@ -61136,7 +61136,7 @@ msgstr "Einbehalt-Dokumenttyp"
#: banking/src/components/features/Settings/Preferences.tsx:70
msgid "Within 1 day"
msgstr "Innerhalb eines Tages"
msgstr ""
#: banking/src/components/features/Settings/Preferences.tsx:71
msgid "Within 2 days"
@@ -62090,7 +62090,7 @@ msgstr ""
#. Exchange Settings'
#: erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json
msgid "frankfurter.dev"
msgstr "frankfurter.dev"
msgstr ""
#. Option for the 'Service Provider' (Select) field in DocType 'Currency
#. Exchange Settings'

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: hello@frappe.io\n"
"POT-Creation-Date: 2026-06-21 10:42+0000\n"
"PO-Revision-Date: 2026-06-23 19:26\n"
"PO-Revision-Date: 2026-06-21 19:03\n"
"Last-Translator: hello@frappe.io\n"
"Language-Team: Persian\n"
"MIME-Version: 1.0\n"
@@ -61985,7 +61985,7 @@ msgstr "frankfurter.dev"
#. Exchange Settings'
#: erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json
msgid "frankfurter.dev - v2"
msgstr "frankfurter.dev - v2"
msgstr ""
#: erpnext/templates/form_grid/item_grid.html:66
#: erpnext/templates/form_grid/item_grid.html:80

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: hello@frappe.io\n"
"POT-Creation-Date: 2026-06-21 10:42+0000\n"
"PO-Revision-Date: 2026-06-24 19:23\n"
"PO-Revision-Date: 2026-06-21 19:03\n"
"Last-Translator: hello@frappe.io\n"
"Language-Team: Croatian\n"
"MIME-Version: 1.0\n"
@@ -25,12 +25,7 @@ msgid "\n"
"\t\t\tIf 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.\n"
"\t\t\tHowever, enabling this setting may lead to negative stock in the system.\n"
"\t\t\tSo please ensure the stock levels are adjusted as soon as possible to maintain the correct valuation rate."
msgstr "\n"
"\t\t\tŠarža {0} artikla {1} ima negativne zalihe u skladištu {2}{3}.\n"
"\t\t\tDodaj količinu zaliha od {4} da biste nastavili s ovim unosom.\n"
"\t\t\tAko nije moguće izvršiti unos prilagođavanja, omogućite 'Dozvoli Negativne Zalihe za Šaržu' za Šaržu {0} ili u Postavkama Zaliha da biste nastavili.\n"
"\t\t\tMeđutim, omogućavanje ove postavke može dovesti do negativnih zaliha u ssustavu.\n"
"\t\t\tStoga, molimo vas da osigurate da se razina zaliha što prije prilagode kako bi se održala ispravna stopa vrednovanja."
msgstr ""
#. Label of the column_break_32 (Column Break) field in DocType 'Email Digest'
#: erpnext/setup/doctype/email_digest/email_digest.json
@@ -1072,7 +1067,7 @@ msgstr "Otpremnica se može kreirati samo za nacrt Dostavnice."
#: erpnext/accounts/services/gl_validator.py:123
msgid "A Period Closing Voucher is already submitted and an Opening Entry can no longer be created. {0} to learn more."
msgstr "Verifikat Zatvaranje Razdoblja je već podnesen i početni unos se više ne može kreirati. {0} za više informacija."
msgstr ""
#. Description of a DocType
#: erpnext/stock/doctype/price_list/price_list.json
@@ -2737,7 +2732,7 @@ msgstr "Dodaj više zadataka"
#: erpnext/stock/doctype/item/item.js:974
msgid "Add Opening Stock"
msgstr "Dodaj Početne Zalihe"
msgstr ""
#. Label of the add_deduct_tax (Select) field in DocType 'Advance Taxes and
#. Charges'
@@ -4029,7 +4024,7 @@ msgstr "Automatski Dodjeli Predujam (FIFO)"
#. 'Purchase Taxes and Charges'
#: erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json
msgid "Allocate Full Amount to Stock Items"
msgstr "Dodijeli Puni Iznos Artiklima Zaliha"
msgstr ""
#: erpnext/accounts/doctype/payment_entry/payment_entry.js:919
msgid "Allocate Payment Amount"
@@ -4608,7 +4603,7 @@ msgstr "Također se ne možete vratiti na FIFO nakon što ste za ovu stavku post
#: erpnext/stock/report/stock_balance/stock_balance.py:644
msgid "Alt UOM"
msgstr "Alternativna Jedinica"
msgstr ""
#: erpnext/manufacturing/doctype/bom/bom.js:291
#: erpnext/manufacturing/doctype/work_order/work_order.js:158
@@ -7300,7 +7295,7 @@ msgstr "Količinsko Stanje"
#: erpnext/stock/report/stock_balance/stock_balance.py:635
msgid "Balance Qty (Alt UOM)"
msgstr "Količinsko Stanja (Alternativna Jedinica)"
msgstr ""
#: erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js:71
msgid "Balance Qty (Stock)"
@@ -12197,15 +12192,15 @@ msgstr "Konsolidirana Prodajna Faktura"
#. Name of a report
#: erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.json
msgid "Consolidated Trial Balance"
msgstr "Konsolidirana Probna Bilanca"
msgstr "Konsolidirana Bruto Bilanca"
#: erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.py:71
msgid "Consolidated Trial Balance can be generated for Companies having same root Company."
msgstr "Konsolidirana Probna Bilanca može se generirati za tvrtke koje imaju istu matičnu tvrtku."
msgstr "Konsolidirana Bruto Bilanca može se generirati za tvrtke koje imaju istu matičnu tvrtku."
#: erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.py:167
msgid "Consolidated Trial balance could not be generated as Exchange Rate from {0} to {1} is not available for {2}."
msgstr "Konsolidirana Probna Bilanca nije mongla biti generirana jer devizni tečaj od {0} do {1} nije dostupan za {2}."
msgstr "Konsolidirana Bruto Bilanca nije mogla biti generirana jer tečaj od {0} do {1} nije dostupan za {2}."
#. Option for the 'Lead Type' (Select) field in DocType 'Lead'
#: erpnext/crm/doctype/lead/lead.json
@@ -13777,7 +13772,7 @@ msgstr "Kreiranje Naloga Knjiženja u toku..."
#: erpnext/stock/doctype/item/item.js:988
msgid "Creating Opening Stock Entry..."
msgstr "Kreiranje Početnog Unosa Zaliha..."
msgstr ""
#: erpnext/stock/doctype/packing_slip/packing_slip.js:42
msgid "Creating Packing Slip ..."
@@ -16104,7 +16099,7 @@ msgstr "Standard Predlošci PDV-a za prodaju, nabavu i artikle su kreirani."
#: erpnext/stock/doctype/item/item.js:942
#: erpnext/stock/doctype/item/item.js:954
msgid "Default warehouse from Item Defaults."
msgstr "Standard Skladište iz Standard Postavki Artikala."
msgstr ""
#. Description of the 'Time Between Operations (Mins)' (Int) field in DocType
#. 'Manufacturing Settings'
@@ -16290,7 +16285,7 @@ msgstr "Izbriši Transakcije"
#: erpnext/setup/doctype/company/company.js:254
msgid "Delete all the Transactions for {0}"
msgstr "Izbriši sve transakcije za {0}"
msgstr ""
#. Label of a Link in the ERPNext Settings Workspace
#: erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
@@ -19574,7 +19569,7 @@ msgstr "Prekomjerna Demontaža"
#: erpnext/stock/doctype/stock_entry/services/material_transfer.py:243
msgid "Excess Material Transfer"
msgstr "Prijenos Dodatnog Materijala"
msgstr ""
#: erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js:55
msgid "Excess Materials Consumed"
@@ -23445,7 +23440,7 @@ msgstr "Ako je označeno, odabrana količina neće biti automatski ispunjena pri
#. DocType 'Purchase Taxes and Charges'
#: erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json
msgid "If checked, the entire amount (e.g. Freight) is allocated to the valuation of stock & asset items only. If unchecked, the amount is distributed across all items and the portion belonging to non-stock items is not added to valuation."
msgstr "Ako je odabrano, cijeli iznos (npr. Vozarina) se dodjeljuje samo za stopu vrijednovanja zaliha i imovine. Ako nije odabrano, iznos se raspoređuje na sve artikle, a dio koji pripada artiklima koje nisu na zalihama se ne dodaje stopi vrijednovanja."
msgstr ""
#. Description of the 'Considered In Paid Amount' (Check) field in DocType
#. 'Purchase Taxes and Charges'
@@ -23620,7 +23615,7 @@ msgstr "Ako je omogućeno, sustav će dopustiti negativne unose zaliha za šarž
#. 'Batch'
#: erpnext/stock/doctype/batch/batch.json
msgid "If enabled, the system will allow negative stock entries for this batch, overriding the 'Allow negative stock for Batch' setting in Stock Settings. This may lead to incorrect valuation rates, so it is recommended to avoid using this option."
msgstr "Ako je omogućeno, sustav će dopustiti unos negativnih zaliha za ovu šaržu, poništavajući postavku 'Dopusti negativne zalihe za Šaržu' u Postavkama Zaliha. To može dovesti do netočnih stopa vrednovanja, stoga se preporučuje izbjegavanje korištenja ove opcije."
msgstr ""
#. Description of the 'Allow UOM with conversion rate defined in Item' (Check)
#. field in DocType 'Stock Settings'
@@ -25521,7 +25516,7 @@ msgstr "Nevažeći upit pretraživanja"
#: erpnext/stock/doctype/stock_entry/stock_entry.py:1649
msgid "Invalid subcontract order field: {0}"
msgstr "Nevažeći nalog podizvođača: {0}"
msgstr ""
#: erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py:99
msgid "Invalid value {0} for 'Based On'"
@@ -28871,7 +28866,7 @@ msgstr "Odsustvo Isplaćeno?"
#: erpnext/stock/doctype/item/item.js:969
msgid "Leave as 0 to allow zero valuation rate."
msgstr "Ostavite kao 0 kako biste omogućili nultu stopu vrednovanja."
msgstr ""
#. Description of the 'Success Redirect URL' (Data) field in DocType
#. 'Appointment Booking Settings'
@@ -32348,7 +32343,7 @@ msgstr "Bez Odgovora"
#: erpnext/stock/doctype/item/item.js:913
msgid "No Company Found"
msgstr "Nije pronađenaTvrtka"
msgstr ""
#: erpnext/accounts/doctype/sales_invoice/mapper.py:115
msgid "No Customer found for Inter Company Transactions which represents company {0}"
@@ -32539,7 +32534,7 @@ msgstr "Nema podataka. Čini se da ste otpremili praznu datoteku"
#: erpnext/stock/doctype/item/item.js:943
msgid "No default warehouse set for this company. Entry will use Stock Settings default."
msgstr "Za ovu tvrtku nije postavljeno standard skladište. Unos će koristiti standard postavke zaliha."
msgstr ""
#: erpnext/templates/generators/bom.html:85
msgid "No description given"
@@ -32825,7 +32820,7 @@ msgstr "Nisu pronađeni vaučeri za ovu transakciju"
#: erpnext/stock/doctype/item/item.py:1734
msgid "No warehouse found for company {0}. Please set a Default Warehouse in Item Defaults or Stock Settings."
msgstr "Nije pronađeno skladište za {0}. Postavi Standard Skladište u Postavkama Artikala ili Postavkama Zaliha."
msgstr ""
#: erpnext/accounts/doctype/sales_invoice/mapper.py:163
msgid "No {0} found for Inter Company Transactions."
@@ -33569,7 +33564,7 @@ msgstr "Dozvoljene su samo vrijednosti između [0,1). Kao {0,00, 0,04, 0,09, ...
#. 'Repost Item Valuation'
#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json
msgid "Only works for Purchase Receipt, Purchase Invoice and Stock Entry"
msgstr "Radi samo za Račun Nabave, Fakturu Nabave i Unos Zaliha"
msgstr ""
#: erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py:43
msgid "Only {0} are supported"
@@ -33862,24 +33857,24 @@ msgstr "Početna Zaliha"
#: erpnext/stock/doctype/item/item.py:1588
msgid "Opening Stock can only be set for stock items."
msgstr "Početne zalihe mogu se postaviti samo za artikle na zalihi."
msgstr ""
#: erpnext/stock/doctype/item/item.py:1595
msgid "Opening Stock cannot be created as stock transactions already exist for item {0}."
msgstr "Početne zalihe se ne mogu kreirati jer već postoje transakcije zaliha za artikal {0}."
msgstr ""
#: erpnext/stock/doctype/item/item.py:1591
msgid "Opening Stock for serialised or batch items must be set via the Stock Reconciliation form."
msgstr "Početne zalihe za serijske ili šaržne artikle mora se postaviti putem Usklađivanje Zaliha."
msgstr ""
#: erpnext/stock/doctype/item/item.py:356
msgid "Opening Stock reconciliation created with zero valuation rate: {0}"
msgstr "Početno Usklađivanje Zaliha kreirano sa nultom stopom vrednovanja: {0}"
msgstr ""
#: erpnext/stock/doctype/item/item.py:364
#: erpnext/stock/doctype/item/item.py:1637
msgid "Opening Stock reconciliation created: {0}"
msgstr "Početno Usklađivanje Zaliha kreirano: {0}"
msgstr ""
#. Label of the opening_time (Time) field in DocType 'Issue'
#: erpnext/support/doctype/issue/issue.json
@@ -33897,7 +33892,7 @@ msgstr "Otvaranje & Zatvaranje"
#: erpnext/stock/doctype/item/item.py:199
msgid "Opening stock creation has been queued and will be created in the background. Please check the Stock Reconciliation after some time."
msgstr "Kreiranje početnih zaliha je stavljeno u red čekanja i bit će kreirano u pozadini. Molimo provjerite usklađivanje zaliha nakon nekog vremena."
msgstr ""
#. Label of the operating_component (Link) field in DocType 'Workstation Cost'
#. Label of the operating_component (Data) field in DocType 'Landed Cost Taxes
@@ -35651,7 +35646,7 @@ msgstr "Djelomično Rezervisano"
#. Option for the 'Status' (Select) field in DocType 'Job Card'
#: erpnext/manufacturing/doctype/job_card/job_card.json
msgid "Partially Transferred"
msgstr "Djelomično Preneseno"
msgstr ""
#. Option for the 'Status' (Select) field in DocType 'Stock Reservation Entry'
#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json
@@ -37727,7 +37722,7 @@ msgstr "Dodaj barem jednu seriju imenovanja."
#: erpnext/stock/doctype/item/item.js:914
msgid "Please add at least one row in Item Defaults with a Company before setting opening stock."
msgstr "Dodaj barem jedan red u Postavke Artikala sa tvrtkom prije postavljanja početnih zaliha."
msgstr ""
#: erpnext/public/js/utils/serial_no_batch_selector.js:663
msgid "Please add atleast one Serial No / Batch No"
@@ -38133,7 +38128,7 @@ msgstr "Potvrdi da datoteka koju koristite ima kolonu 'Nadređeni Račun' u zagl
#: erpnext/setup/doctype/company/company.js:234
msgid "Please make sure you really want to delete all the transactions for {0}. Your master data will remain as it is. This action cannot be undone."
msgstr "Da li zaista želiš izbrisati sve transakcije za {0}. Vaši glavni podaci će ostati onakvi kakvi jesu. Ova radnja se ne može poništiti."
msgstr ""
#: erpnext/stock/doctype/item/item.js:1025
msgid "Please mention 'Weight UOM' along with Weight."
@@ -38653,7 +38648,7 @@ msgstr "Postavi Centar Troškova za Imovinu ili postavite Centar Troškova Amort
#: erpnext/stock/doctype/item/item.py:339
#: erpnext/stock/doctype/item/item.py:1621
msgid "Please set a Temporary Opening account for company {0} to create an Opening Stock reconciliation."
msgstr "Postavi Privremeni Početni Račun za {0} kako biste kreirali početno usklađivanje zaliha."
msgstr ""
#: erpnext/projects/doctype/project/project.py:806
msgid "Please set a default Holiday List for Company {0}"
@@ -43162,7 +43157,7 @@ msgstr "Dostignut je Najviši Nivo"
#: erpnext/accounts/services/gl_validator.py:127
msgid "Read the docs"
msgstr "Pročitaj dokumentaciju"
msgstr ""
#. Label of the reading_1 (Data) field in DocType 'Quality Inspection Reading'
#: erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json
@@ -43275,7 +43270,7 @@ msgstr "Preračunaj Nabavnu/Prodajnu Cijenu"
#. Item Valuation'
#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json
msgid "Recalculate Valuation Rate"
msgstr "Ponovo izračunaj Stopu Vrednovanja"
msgstr ""
#. Option for the 'Status' (Select) field in DocType 'Asset'
#. Option for the 'Purpose' (Select) field in DocType 'Asset Movement'
@@ -46056,7 +46051,7 @@ msgstr "Red #{0}: Ne može se prenijeti više od potrebne količine {1} za artik
#: erpnext/stock/doctype/stock_entry/services/material_transfer.py:233
msgid "Row #{0}: Cannot transfer {1} {2} of Item {3}. Maximum transferable quantity is {4} {2}."
msgstr "Red #{0}: Ne može se prenijeti {1} {2} artikal {3}. Najveća prenosiva količina je {4} {2}."
msgstr ""
#: erpnext/selling/doctype/product_bundle/product_bundle.py:138
msgid "Row #{0}: Child Item should not be a Product Bundle. Please remove Item {1} and Save"
@@ -47747,7 +47742,7 @@ msgstr "Prodajni Nalog {0} već postoji naspram Nabavnog Naloga Klijenta {1}. Da
#: erpnext/projects/doctype/project/project.py:256
msgid "Sales Order {0} is already linked to Project {1}, skipping the link."
msgstr "Prodajni Nalog {0} je već povezan s projektom {1}, preskoči poveznicu."
msgstr ""
#: erpnext/selling/doctype/sales_order/mapper.py:883
#: erpnext/selling/doctype/sales_order/mapper.py:896
@@ -50043,7 +50038,7 @@ msgstr "Postavi Novi Datum Izdavanja"
#: erpnext/stock/doctype/item/item.js:203
msgid "Set Opening Stock"
msgstr "Postavi Početne Zalihe"
msgstr ""
#. Label of the set_op_cost_and_secondary_items_from_sub_assemblies (Check)
#. field in DocType 'Manufacturing Settings'
@@ -50737,7 +50732,7 @@ msgstr "Prikažite ukupnu vrijednost iz Podružnica"
#: erpnext/stock/report/stock_balance/stock_balance.js:115
msgid "Show Alternate UOM Balance"
msgstr "Prikaži Saldo Alternativne Jedinice"
msgstr ""
#: erpnext/accounts/report/general_ledger/general_ledger.js:199
msgid "Show Cancelled Entries"
@@ -58110,12 +58105,12 @@ msgstr "Stablo Procedura"
#: erpnext/workspace_sidebar/invoicing.json
#: erpnext/workspace_sidebar/payments.json
msgid "Trial Balance"
msgstr "Probna Bilanca"
msgstr "Bruto Stanje"
#. Name of a report
#: erpnext/accounts/report/trial_balance_simple/trial_balance_simple.json
msgid "Trial Balance (Simple)"
msgstr "Probna Bilanca (Jednostavno)"
msgstr "Bruto Stanje (Jednostavno)"
#. Name of a report
#. Label of a Link in the Financial Reports Workspace
@@ -58124,7 +58119,7 @@ msgstr "Probna Bilanca (Jednostavno)"
#: erpnext/accounts/workspace/financial_reports/financial_reports.json
#: erpnext/workspace_sidebar/financial_reports.json
msgid "Trial Balance for Party"
msgstr "Probna Bilanca Stranke"
msgstr "Bruto Stanje Stranke"
#. Label of the trial_period_end (Date) field in DocType 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
@@ -59690,7 +59685,7 @@ msgstr "Nedostaje Stopa Vrednovanja"
#: erpnext/stock/doctype/item/item.py:1604
msgid "Valuation Rate cannot be negative."
msgstr "Stopa Vrednovanja ne može biti negativna."
msgstr ""
#: erpnext/stock/stock_ledger.py:2037
msgid "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
@@ -62099,7 +62094,7 @@ msgstr "frankfurter.dev"
#. Exchange Settings'
#: erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json
msgid "frankfurter.dev - v2"
msgstr "frankfurter.dev - v2"
msgstr ""
#: erpnext/templates/form_grid/item_grid.html:66
#: erpnext/templates/form_grid/item_grid.html:80

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: hello@frappe.io\n"
"POT-Creation-Date: 2026-06-21 10:42+0000\n"
"PO-Revision-Date: 2026-06-24 19:22\n"
"PO-Revision-Date: 2026-06-21 19:02\n"
"Last-Translator: hello@frappe.io\n"
"Language-Team: Hungarian\n"
"MIME-Version: 1.0\n"
@@ -11865,7 +11865,7 @@ msgstr ""
#. Label of the items (Table) field in DocType 'BOM'
#: erpnext/manufacturing/doctype/bom/bom.json
msgid "Components"
msgstr "Komponensek"
msgstr ""
#. Option for the 'Asset Type' (Select) field in DocType 'Asset'
#: erpnext/assets/doctype/asset/asset.json
@@ -36116,7 +36116,7 @@ msgstr ""
#: banking/src/components/features/ActionLog/ActionLogDialogBody.tsx:408
msgid "Payment Details"
msgstr "Fizetési részletek"
msgstr ""
#. Label of the payment_document (Link) field in DocType 'Bank Clearance
#. Detail'
@@ -59394,7 +59394,7 @@ msgstr ""
#: erpnext/stock/doctype/item/item_prices.html:86
msgid "Valid Upto"
msgstr "Valid Upto"
msgstr ""
#. Label of the countries (Table) field in DocType 'Shipping Rule'
#: erpnext/accounts/doctype/shipping_rule/shipping_rule.json

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: hello@frappe.io\n"
"POT-Creation-Date: 2026-06-21 10:42+0000\n"
"PO-Revision-Date: 2026-06-24 19:23\n"
"PO-Revision-Date: 2026-06-21 19:02\n"
"Last-Translator: hello@frappe.io\n"
"Language-Team: Slovenian\n"
"MIME-Version: 1.0\n"
@@ -12751,13 +12751,13 @@ msgstr ""
#. Label of the cost_allocation (Currency) field in DocType 'BOM'
#: erpnext/manufacturing/doctype/bom/bom.json
msgid "Cost Allocation"
msgstr "porazdelitve stroškov"
msgstr ""
#. Label of the cost_allocation_per (Percent) field in DocType 'BOM Secondary
#. Item'
#: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json
msgid "Cost Allocation %"
msgstr "porazdelitve stroškov %"
msgstr ""
#. Label of the cost_allocation__process_loss_section (Section Break) field in
#. DocType 'BOM'

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: hello@frappe.io\n"
"POT-Creation-Date: 2026-06-21 10:42+0000\n"
"PO-Revision-Date: 2026-06-24 19:23\n"
"PO-Revision-Date: 2026-06-21 19:02\n"
"Last-Translator: hello@frappe.io\n"
"Language-Team: Swedish\n"
"MIME-Version: 1.0\n"
@@ -12204,15 +12204,15 @@ msgstr "Konsoliderad Försäljning Faktura"
#. Name of a report
#: erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.json
msgid "Consolidated Trial Balance"
msgstr "Konsoliderad Prov Saldo"
msgstr "Konsoliderat Brutto Saldo"
#: erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.py:71
msgid "Consolidated Trial Balance can be generated for Companies having same root Company."
msgstr "Konsoliderad Prov Saldo kan skapas för bolag som har samma moderbolag."
msgstr "Konsoliderad Brutto Saldo kan skapas för bolag som har samma moderbolag."
#: erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.py:167
msgid "Consolidated Trial balance could not be generated as Exchange Rate from {0} to {1} is not available for {2}."
msgstr "Konsoliderad Prov Saldo kunde inte skapas eftersom växelkurs från {0} till {1} inte är tillgänglig för {2}."
msgstr "Konsoliderad Brutto Saldo kunde inte skapas eftersom växelkurs från {0} till {1} inte är tillgänglig för {2}."
#. Option for the 'Lead Type' (Select) field in DocType 'Lead'
#: erpnext/crm/doctype/lead/lead.json
@@ -58117,12 +58117,12 @@ msgstr "Kvalitet Procedur Träd"
#: erpnext/workspace_sidebar/invoicing.json
#: erpnext/workspace_sidebar/payments.json
msgid "Trial Balance"
msgstr "Prov Saldo"
msgstr "Brutto Saldo"
#. Name of a report
#: erpnext/accounts/report/trial_balance_simple/trial_balance_simple.json
msgid "Trial Balance (Simple)"
msgstr "Prov Saldo (Enkel)"
msgstr "Brutto Saldo (Enkel)"
#. Name of a report
#. Label of a Link in the Financial Reports Workspace
@@ -58131,7 +58131,7 @@ msgstr "Prov Saldo (Enkel)"
#: erpnext/accounts/workspace/financial_reports/financial_reports.json
#: erpnext/workspace_sidebar/financial_reports.json
msgid "Trial Balance for Party"
msgstr "Prov Saldo för Parti"
msgstr "Brutto Saldo för Parti"
#. Label of the trial_period_end (Date) field in DocType 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json

File diff suppressed because it is too large Load Diff

View File

@@ -97,10 +97,10 @@ class TestBOM(ERPNextTestSuite):
update_cost_in_all_boms_in_test()
# check if new valuation rate updated in all BOMs
for d in frappe.get_all(
"BOM Item",
filters={"item_code": "_Test Item 2", "docstatus": 1, "parenttype": "BOM"},
fields=["base_rate"],
for d in frappe.db.sql(
"""select base_rate from `tabBOM Item`
where item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'""",
as_dict=1,
):
self.assertEqual(d.base_rate, rm_base_rate + 10)
@@ -881,8 +881,12 @@ def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=Non
warehouse_list = [warehouse_list]
if not warehouse_list:
warehouse_list = frappe.get_all(
"Bin", filters={"item_code": item_code, "actual_qty": [">", 0]}, pluck="warehouse"
warehouse_list = frappe.db.sql_list(
"""
select warehouse from `tabBin`
where item_code=%s and actual_qty > 0
""",
item_code,
)
if not warehouse_list:

View File

@@ -5249,8 +5249,11 @@ def update_job_card(job_card, jc_qty=None, days=None):
def get_secondary_item_details(bom_no):
secondary_items = {}
for item in frappe.get_all(
"BOM Secondary Item", filters={"parent": bom_no}, fields=["item_code", "stock_qty"]
for item in frappe.db.sql(
"""select item_code, stock_qty from `tabBOM Secondary Item`
where parent = %s""",
bom_no,
as_dict=1,
):
secondary_items[item.item_code] = item.stock_qty

View File

@@ -49,7 +49,7 @@ class TestProject(ERPNextTestSuite):
def test_project_with_template_having_no_parent_and_depend_tasks(self):
project_name = "Test Project with Template - No Parent and Dependend Tasks"
frappe.db.delete("Task", {"project": project_name})
frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
frappe.delete_doc("Project", project_name)
task1 = task_exists("Test Template Task with No Parent and Dependency")
@@ -82,7 +82,7 @@ class TestProject(ERPNextTestSuite):
if frappe.db.get_value("Project", {"project_name": project_name}, "name"):
project_name = frappe.db.get_value("Project", {"project_name": project_name}, "name")
frappe.db.delete("Task", {"project": project_name})
frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
frappe.delete_doc("Project", project_name)
task1 = task_exists("Test Template Task Parent")
@@ -137,7 +137,7 @@ class TestProject(ERPNextTestSuite):
def test_project_template_having_dependent_tasks(self):
project_name = "Test Project with Template - Dependent Tasks"
frappe.db.delete("Task", {"project": project_name})
frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
frappe.delete_doc("Project", project_name)
task1 = task_exists("Test Template Task for Dependency")
@@ -252,7 +252,7 @@ class TestProject(ERPNextTestSuite):
def test_project_having_no_tasks_complete(self):
project_name = "Test Project - No Tasks Completion"
frappe.db.delete("Task", {"project": project_name})
frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
frappe.delete_doc("Project", project_name)
project = frappe.get_doc(

View File

@@ -144,7 +144,7 @@ class Task(NestedSet):
if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"):
frappe.throw(
_(
"Cannot complete task {0} as its dependent task {1} is not completed / cancelled."
"Cannot complete task {0} as its dependant task {1} are not completed / cancelled."
).format(frappe.bold(self.name), frappe.bold(d.task))
)
@@ -316,7 +316,7 @@ class Task(NestedSet):
def on_trash(self):
if check_if_child_exists(self.name):
throw(_("Child Task exists for this Task. You cannot delete this Task."))
throw(_("Child Task exists for this Task. You can not delete this Task."))
self.update_nsm_model()

View File

@@ -105,7 +105,7 @@ class TimesheetDetail(Document):
def validate_dates(self):
"""Validate that to_time is not before from_time."""
if self.from_time and self.to_time and time_diff_in_hours(self.to_time, self.from_time) < 0:
frappe.throw(_("To Time cannot be before From Time"))
frappe.throw(_("To Time cannot be before from date"))
def validate_parent_project(self, parent_project: str):
"""Validate that project is same as Timesheet's parent project."""

View File

@@ -23,12 +23,15 @@ erpnext.accounts.taxes = {
onload: function (frm) {
if (frm.get_field("taxes")) {
frm.set_query("account_head", "taxes", function (doc) {
let account_type = ["Tax", "Chargeable"];
if (frm.cscript.tax_table == "Sales Taxes and Charges") {
account_type.push("Expense Account");
var account_type = ["Tax", "Chargeable", "Expense Account"];
} else {
account_type.push("Income Account", "Expenses Included In Valuation");
var account_type = [
"Tax",
"Chargeable",
"Income Account",
"Expenses Included In Valuation",
];
}
return {

View File

@@ -952,15 +952,14 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
if (["Sales Invoice", "POS Invoice", "Purchase Invoice"].includes(this.frm.doc.doctype)) {
let grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
let base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total;
let total_amount_to_pay;
if (this.frm.doc.party_account_currency == this.frm.doc.currency) {
total_amount_to_pay = flt(
var total_amount_to_pay = flt(
grand_total - this.frm.doc.total_advance - this.frm.doc.write_off_amount,
precision("grand_total")
);
} else {
total_amount_to_pay = flt(
var total_amount_to_pay = flt(
flt(base_grand_total, precision("base_grand_total")) -
this.frm.doc.total_advance -
this.frm.doc.base_write_off_amount,
@@ -1005,15 +1004,14 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
async set_total_amount_to_default_mop() {
let grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
let base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total;
let total_amount_to_pay;
if (this.frm.doc.party_account_currency == this.frm.doc.currency) {
total_amount_to_pay = flt(
var total_amount_to_pay = flt(
grand_total - this.frm.doc.total_advance - this.frm.doc.write_off_amount,
precision("grand_total")
);
} else {
total_amount_to_pay = flt(
var total_amount_to_pay = flt(
flt(base_grand_total, precision("base_grand_total")) -
this.frm.doc.total_advance -
this.frm.doc.base_write_off_amount,

View File

@@ -1291,8 +1291,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
var set_party_account = function (set_pricing) {
if (["Sales Invoice", "Purchase Invoice"].includes(me.frm.doc.doctype)) {
let party_type = me.frm.doc.doctype == "Sales Invoice" ? "Customer" : "Supplier";
let party_account_field = me.frm.doc.doctype == "Sales Invoice" ? "debit_to" : "credit_to";
if (me.frm.doc.doctype == "Sales Invoice") {
var party_type = "Customer";
var party_account_field = "debit_to";
} else {
var party_type = "Supplier";
var party_account_field = "credit_to";
}
var party = me.frm.doc[frappe.model.scrub(party_type)];
if (
@@ -2066,7 +2071,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
if (this.frm.doc.operations && this.frm.doc.operations.length > 0) {
let item_grid = this.frm.fields_dict["operations"].grid;
var item_grid = this.frm.fields_dict["operations"].grid;
$.each(["base_operating_cost", "base_hour_rate"], function (i, fname) {
if (frappe.meta.get_docfield(item_grid.doctype, fname))
item_grid.set_column_disp(fname, me.frm.doc.currency != company_currency);
@@ -2074,7 +2079,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
if (this.frm.doc.secondary_items && this.frm.doc.secondary_items.length > 0) {
let item_grid = this.frm.fields_dict["secondary_items"].grid;
var item_grid = this.frm.fields_dict["secondary_items"].grid;
$.each(["base_rate", "base_amount"], function (i, fname) {
if (frappe.meta.get_docfield(item_grid.doctype, fname))
item_grid.set_column_disp(fname, me.frm.doc.currency != company_currency);
@@ -2465,7 +2470,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
row_to_modify[key] = pr_row[key];
}
if (Object.prototype.hasOwnProperty.call(this.frm.doc, "is_pos") && this.frm.doc.is_pos) {
if (this.frm.doc.hasOwnProperty("is_pos") && this.frm.doc.is_pos) {
let r = await frappe.db.get_value("POS Profile", this.frm.doc.pos_profile, "cost_center");
if (r.message.cost_center) {
row_to_modify["cost_center"] = r.message.cost_center;
@@ -2730,7 +2735,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
$.each(me.frm.doc.items || [], function (i, item) {
if (
item.name &&
Object.prototype.hasOwnProperty.call(r.message, item.name) &&
r.message.hasOwnProperty(item.name) &&
r.message[item.name].item_tax_template
) {
item.item_tax_template = r.message[item.name].item_tax_template;

View File

@@ -681,7 +681,6 @@
"fieldtype": "Data",
"in_global_search": 1,
"label": "Alias",
"no_copy": 1,
"unique": 1
}
],
@@ -696,7 +695,7 @@
"link_fieldname": "party"
}
],
"modified": "2026-06-27 16:12:10.457900",
"modified": "2026-06-22 12:23:19.196991",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",

View File

@@ -158,7 +158,7 @@ class Customer(TransactionBase):
new_customer_name = f"{self.customer_name} - {cstr(count)}"
msgprint(
_("Changed customer name to '{0}' as '{1}' already exists.").format(
_("Changed customer name to '{}' as '{}' already exists.").format(
new_customer_name, self.customer_name
),
title=_("Note"),
@@ -356,7 +356,7 @@ class Customer(TransactionBase):
if frappe.db.exists("Customer Group", self.name):
frappe.throw(
_(
"A Customer Group exists with the same name. Please change the Customer name or rename the Customer Group"
"A Customer Group exists with same name please change the Customer name or rename the Customer Group"
),
frappe.NameError,
)
@@ -406,7 +406,7 @@ class Customer(TransactionBase):
if flt(limit.credit_limit) < outstanding_amt:
frappe.throw(
_(
"""New credit limit is less than current outstanding amount for the customer. Credit limit has to be at least {0}"""
"""New credit limit is less than current outstanding amount for the customer. Credit limit has to be atleast {0}"""
).format(outstanding_amt)
)
@@ -440,7 +440,7 @@ class Customer(TransactionBase):
self.loyalty_program = loyalty_program[0]
else:
frappe.msgprint(
_("Multiple Loyalty Programs found for Customer {0}. Please select manually.").format(
_("Multiple Loyalty Programs found for Customer {}. Please select manually.").format(
frappe.bold(self.customer_name)
)
)

View File

@@ -172,7 +172,7 @@ def make_address(args, is_primary_address=1, is_shipping_address=1):
if reqd_fields:
msg = _("Following fields are mandatory to create address:")
frappe.throw(
msg + " <br><br> <ul>{}</ul>".format("\n".join(reqd_fields)),
"{} <br><br> <ul>{}</ul>".format(msg, "\n".join(reqd_fields)),
title=_("Missing Values Required"),
)

View File

@@ -32,6 +32,4 @@ class PartySpecificItem(Document):
},
)
if exists:
frappe.throw(
_("This item filter has already been applied for the {0}").format(_(self.party_type))
)
frappe.throw(_("This item filter has already been applied for the {0}").format(self.party_type))

View File

@@ -118,9 +118,9 @@ class ProductBundle(Document):
if len(invoice_links):
frappe.throw(
_(
"This Product Bundle is linked with {0}. You will have to cancel these documents in order to delete this Product Bundle"
).format(", ".join(invoice_links)),
"This Product Bundle is linked with {}. You will have to cancel these documents in order to delete this Product Bundle".format(
", ".join(invoice_links)
),
title=_("Not Allowed"),
)

View File

@@ -7,7 +7,6 @@ from unittest.mock import patch
import frappe
import frappe.permissions
from frappe.core.doctype.user_permission.test_user_permission import create_user
from frappe.query_builder.functions import Sum
from frappe.tests import change_settings
from frappe.utils import add_days, flt, getdate, nowdate, today
@@ -908,12 +907,10 @@ class TestSalesOrder(ERPNextTestSuite):
item_doc.save()
else:
# update valid from
frappe.db.set_value(
"Item Tax",
{"parent": item, "item_tax_template": tax_template},
"valid_from",
today(),
update_modified=False,
frappe.db.sql(
"""UPDATE `tabItem Tax` set valid_from = CURRENT_DATE
where parent = %(item)s and item_tax_template = %(tax)s""",
{"item": item, "tax": tax_template},
)
so = make_sales_order(item_code=item, qty=1, do_not_save=1)
@@ -963,12 +960,10 @@ class TestSalesOrder(ERPNextTestSuite):
self.assertEqual(so.taxes[1].total, 480)
# teardown
frappe.db.set_value(
"Item Tax",
{"parent": item, "item_tax_template": tax_template},
"valid_from",
None,
update_modified=False,
frappe.db.sql(
"""UPDATE `tabItem Tax` set valid_from = NULL
where parent = %(item)s and item_tax_template = %(tax)s""",
{"item": item, "tax": tax_template},
)
so.cancel()
so.delete()
@@ -1564,12 +1559,10 @@ class TestSalesOrder(ERPNextTestSuite):
# Check if Work Orders were raised
for item in so_item_name:
wo = frappe.qb.DocType("Work Order")
wo_qty = (
frappe.qb.from_(wo)
.select(Sum(wo.qty))
.where((wo.sales_order == so.name) & (wo.sales_order_item == item))
).run()
wo_qty = frappe.db.sql(
"select sum(qty) from `tabWork Order` where sales_order=%s and sales_order_item=%s",
(so.name, item),
)
self.assertEqual(wo_qty[0][0], so_item_name.get(item))
def test_advance_payment_entry_unlink_against_sales_order(self):
@@ -1749,7 +1742,9 @@ class TestSalesOrder(ERPNextTestSuite):
mr_dict["include_exploded_items"] = 0
mr_dict["ignore_existing_ordered_qty"] = 1
make_raw_material_request(mr_dict, so.company, so.name)
mr = frappe.get_all("Material Request", fields=["name"], order_by="creation desc", limit=1)[0]
mr = frappe.db.sql(
"""select name from `tabMaterial Request` ORDER BY creation DESC LIMIT 1""", as_dict=1
)[0]
mr_doc = frappe.get_doc("Material Request", mr.get("name"))
self.assertEqual(mr_doc.items[0].sales_order, so.name)

View File

@@ -16,7 +16,7 @@ def validate_filters(from_date, to_date, company):
frappe.throw(_("To Date must be greater than From Date"))
if not company:
frappe.throw(_("Please select a Company"))
frappe.throw(_("Please Select a Company"))
@frappe.whitelist()

View File

@@ -47,7 +47,7 @@ class SalesPartnerSummaryReport:
frappe.throw(_("Please select the document type first."))
if self.filters.get("doctype") not in SALES_TRANSACTION_DOCTYPES:
frappe.throw(_("DocType can be one of {0}").format(comma_or(SALES_TRANSACTION_DOCTYPES)))
frappe.throw(_("DocType can be one of them {0}").format(comma_or(SALES_TRANSACTION_DOCTYPES)))
if not self.filters.get("company"):
frappe.throw(_("Please select a company."))

View File

@@ -19,7 +19,7 @@ class SalesPartnerSummaryReportTestMixin(ERPNextTestSuite):
with self.assertRaisesRegex(
frappe.ValidationError,
_("DocType can be one of {0}").format(comma_or(SALES_TRANSACTION_DOCTYPES)),
_("DocType can be one of them {0}").format(comma_or(SALES_TRANSACTION_DOCTYPES)),
):
run(self.report_name, self.filters)

View File

@@ -129,7 +129,6 @@
"valuation_method",
"column_break_32",
"stock_adjustment_account",
"default_purchase_price_variance_account",
"stock_received_but_not_billed",
"stock_delivered_but_not_billed",
"disable_sdbnb_in_sr",
@@ -492,15 +491,6 @@
"no_copy": 1,
"options": "Account"
},
{
"description": "Used for items valued at Standard Cost: the difference between the purchase price and the standard rate is booked here.",
"fieldname": "default_purchase_price_variance_account",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Default Purchase Price Variance Account",
"no_copy": 1,
"options": "Account"
},
{
"fieldname": "column_break_32",
"fieldtype": "Column Break"
@@ -1014,7 +1004,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
"modified": "2026-06-26 10:05:00.000000",
"modified": "2026-05-14 16:50:34.132345",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",

View File

@@ -2,7 +2,6 @@
# License: GNU General Public License v3. See license.txt
import frappe
from frappe.query_builder.functions import Max
from frappe.utils.nestedset import (
NestedSetChildExistsError,
NestedSetInvalidMergeError,
@@ -21,8 +20,7 @@ class TestItemGroup(ERPNextTestSuite):
def test_basic_tree(self, records=None):
min_lft = 1
ig = frappe.qb.DocType("Item Group")
max_rgt = frappe.qb.from_(ig).select(Max(ig.rgt)).run()[0][0]
max_rgt = frappe.db.sql("select max(rgt) from `tabItem Group`")[0][0]
if not records:
records = self.globalTestRecords["Item Group"][2:]
@@ -133,7 +131,12 @@ class TestItemGroup(ERPNextTestSuite):
frappe.db.get_value("Item Group", parent_item_group, "rgt")
ancestors = get_ancestors_of("Item Group", "_Test Item Group B - 3")
ancestors = frappe.get_all("Item Group", filters={"name": ["in", ancestors]}, fields=["name", "rgt"])
ancestors = frappe.db.sql(
"""select name, rgt from `tabItem Group`
where name in ({})""".format(", ".join(["%s"] * len(ancestors))),
tuple(ancestors),
as_dict=True,
)
frappe.delete_doc("Item Group", "_Test Item Group B - 3")
records_to_test = self.globalTestRecords["Item Group"][2:]
@@ -165,8 +168,9 @@ class TestItemGroup(ERPNextTestSuite):
self.test_basic_tree()
# move its children back
for name in frappe.get_all(
"Item Group", filters={"parent_item_group": "_Test Item Group C"}, pluck="name"
for name in frappe.db.sql_list(
"""select name from `tabItem Group`
where parent_item_group='_Test Item Group C'"""
):
doc = frappe.get_doc("Item Group", name)
doc.parent_item_group = "_Test Item Group B"
@@ -214,7 +218,11 @@ class TestItemGroup(ERPNextTestSuite):
def get_no_of_children(item_groups, no_of_children):
children = []
for ig in item_groups:
children += frappe.get_all("Item Group", filters={"parent_item_group": ig}, pluck="name")
children += frappe.db.sql_list(
"""select name from `tabItem Group`
where ifnull(parent_item_group, '')=%s""",
ig or "",
)
if len(children):
return get_no_of_children(children, no_of_children + len(children))

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

@@ -277,27 +277,19 @@ def update_qty(bin_name, args):
- flt(bin_details.reserved_qty_for_production_plan)
)
bin_values = {
"actual_qty": actual_qty,
"ordered_qty": ordered_qty,
"reserved_qty": reserved_qty,
"indented_qty": indented_qty,
"planned_qty": planned_qty,
"projected_qty": projected_qty,
}
# Standard Cost items are not reposted on backdated entries, so the Bin's stock value is not
# refreshed by a repost. Keep it in step with the balance at the standard rate.
from erpnext.stock.utils import get_valuation_method
if get_valuation_method(args.get("item_code")) == "Standard Cost":
from erpnext.stock.doctype.item_standard_cost.item_standard_cost import get_item_standard_rate
bin_values["stock_value"] = flt(actual_qty) * flt(
get_item_standard_rate(args.get("item_code"), args.get("company"))
)
frappe.db.set_value("Bin", bin_name, bin_values, update_modified=True)
frappe.db.set_value(
"Bin",
bin_name,
{
"actual_qty": actual_qty,
"ordered_qty": ordered_qty,
"reserved_qty": reserved_qty,
"indented_qty": indented_qty,
"planned_qty": planned_qty,
"projected_qty": projected_qty,
},
update_modified=True,
)
def get_actual_qty(item_code, warehouse):

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 the driver address is missing."))
frappe.throw(_("Cannot Calculate Arrival Time as Driver Address is Missing."))
home_address = get_address_display(frappe.get_doc("Address", self.driver_address).as_dict())

View File

@@ -384,7 +384,7 @@
"fieldname": "valuation_method",
"fieldtype": "Select",
"label": "Valuation Method",
"options": "\nFIFO\nMoving Average\nLIFO\nStandard Cost"
"options": "\nFIFO\nMoving Average\nLIFO"
},
{
"depends_on": "is_stock_item",
@@ -1090,7 +1090,7 @@
"image_field": "image",
"links": [],
"make_attachments_public": 1,
"modified": "2026-06-26 10:05:00.000000",
"modified": "2026-05-27 10:18:46.862670",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",

View File

@@ -143,7 +143,7 @@ class Item(Document):
taxes: DF.Table[ItemTax]
total_projected_qty: DF.Float
uoms: DF.Table[UOMConversionDetail]
valuation_method: DF.Literal["", "FIFO", "Moving Average", "LIFO", "Standard Cost"]
valuation_method: DF.Literal["", "FIFO", "Moving Average", "LIFO"]
valuation_rate: DF.Currency
variant_based_on: DF.Literal["Item Attribute", "Manufacturer"]
variant_of: DF.Link | None
@@ -239,7 +239,6 @@ class Item(Document):
self.validate_item_defaults()
self.validate_auto_reorder_enabled_in_stock_settings()
self.cant_change()
self.validate_standard_cost_change()
self.validate_item_tax_net_rate_range()
if not self.is_new():
@@ -464,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' cannot be 'Yes' for non-stock item"))
frappe.throw(_("'Has Serial No' can not be 'Yes' for non-stock item"))
if self.has_serial_no == 0 and self.serial_no_series:
self.serial_no_series = None
@@ -1061,30 +1060,6 @@ class Item(Document):
for d in self.attributes:
d.variant_of = self.variant_of
def validate_standard_cost_change(self):
"""Once stock exists, an item's valuation method cannot be switched to or from Standard
Cost — either change would leave existing stock valued on a basis the ledger never
recorded."""
if not self.is_standard_cost_valuation_change():
return
if self.stock_ledger_created():
frappe.throw(
_(
"Valuation Method cannot be changed to or from 'Standard Cost' for {0} because stock transactions already exist for it."
).format(frappe.bold(self.name))
)
def is_standard_cost_valuation_change(self):
"""True if this save switches the valuation method into or out of Standard Cost."""
if self.is_new() or not self.has_value_changed("valuation_method"):
return False
previous = self.get_doc_before_save()
was_standard = previous and previous.valuation_method == "Standard Cost"
is_standard = self.valuation_method == "Standard Cost"
return bool(was_standard or is_standard)
def cant_change(self):
if self.is_new():
return
@@ -1533,9 +1508,7 @@ 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 #{0}: {1} {2} does not belong to Company {3}. Please select valid {4}."
).format(
_("Row #{}: {} {} doesn't belong to Company {}. Please select valid {}.").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(_("Cannot set alternative item for the item {0}").format(self.item_code))
frappe.throw(_("Not allow to 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 {0}")
alternate_item_check_msg = _("Allow Alternative Item must be checked on Item {}")
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(_("Record already exists for the item {0}").format(self.item_code))
frappe.throw(_("Already record exists for the item {0}").format(self.item_code))
@frappe.whitelist()

View File

@@ -34,7 +34,6 @@
"default_provisional_account",
"purchase_expense_account",
"purchase_expense_contra_account",
"purchase_price_variance_account",
"selling_defaults",
"column_break_sales",
"vf_selling_cost_center",
@@ -190,14 +189,6 @@
"options": "Account",
"show_description_on_click": 1
},
{
"description": "For Standard Cost items: the purchase price vs standard rate difference is booked here. Falls back to the Company's Default Purchase Price Variance Account.",
"fieldname": "purchase_price_variance_account",
"fieldtype": "Link",
"label": "Purchase Price Variance Account",
"options": "Account",
"show_description_on_click": 1
},
{
"fieldname": "column_break_purchase",
"fieldtype": "Column Break"
@@ -365,7 +356,7 @@
],
"istable": 1,
"links": [],
"modified": "2026-06-26 10:05:00.000000",
"modified": "2026-06-03 17:25:35.982082",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Default",

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(_("The price list {0} does not exist or is disabled").format(link))
frappe.throw(f"The price list {link} does not exist or is disabled")
self.buying, self.selling, self.currency = price_list_details

View File

@@ -1,16 +0,0 @@
// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Item Standard Cost", {
setup(frm) {
// Only allow items whose effective valuation method is "Standard Cost".
frm.set_query("item_code", () => {
return {
query: "erpnext.stock.doctype.item_standard_cost.item_standard_cost.get_standard_cost_items",
filters: {
company: frm.doc.company,
},
};
});
},
});

View File

@@ -1,138 +0,0 @@
{
"actions": [],
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2026-06-26 11:00:00.000000",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"naming_series",
"item_code",
"company",
"column_break_main",
"standard_rate",
"effective_date",
"revaluation_section",
"revaluation_entry",
"amended_from"
],
"fields": [
{
"default": "ISC-.YYYY.-",
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Series",
"options": "ISC-.YYYY.-",
"reqd": 1,
"set_only_once": 1
},
{
"fieldname": "item_code",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Item",
"options": "Item",
"reqd": 1,
"search_index": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Company",
"options": "Company",
"reqd": 1,
"search_index": 1
},
{
"fieldname": "column_break_main",
"fieldtype": "Column Break"
},
{
"fieldname": "standard_rate",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Standard Valuation Rate",
"options": "Company:company:default_currency",
"reqd": 1
},
{
"default": "Today",
"fieldname": "effective_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Effective Date",
"reqd": 1
},
{
"fieldname": "revaluation_section",
"fieldtype": "Section Break",
"label": "Revaluation"
},
{
"description": "Stock Reconciliation auto-created to revalue on-hand stock to the new standard rate.",
"fieldname": "revaluation_entry",
"fieldtype": "Link",
"label": "Revaluation Entry",
"no_copy": 1,
"options": "Stock Reconciliation",
"read_only": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Item Standard Cost",
"print_hide": 1,
"read_only": 1,
"search_index": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2026-06-26 11:00:00.000000",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Standard Cost",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Stock Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -1,299 +0,0 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.query_builder.functions import Max
from frappe.utils import flt, get_datetime, get_link_to_form, getdate, nowtime, today
from frappe.utils.caching import request_cache
from erpnext.stock.utils import get_valuation_method
class ItemStandardCost(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
amended_from: DF.Link | None
company: DF.Link
effective_date: DF.Date
item_code: DF.Link
naming_series: DF.Literal["ISC-.YYYY.-"]
revaluation_entry: DF.Link | None
standard_rate: DF.Currency
# end: auto-generated types
def validate(self):
self.validate_item()
self.validate_effective_date()
self.validate_rate()
def validate_item(self):
if not frappe.get_cached_value("Item", self.item_code, "is_stock_item"):
frappe.throw(_("{0} is not a stock item.").format(frappe.bold(self.item_code)))
if get_valuation_method(self.item_code, self.company) != "Standard Cost":
frappe.throw(
_("Valuation Method of Item {0} must be set to 'Standard Cost'.").format(
get_link_to_form("Item", self.item_code)
)
)
def validate_effective_date(self):
# Standard cost is set "as of now"; future-dating would leave a gap where new receipts
# are valued at a rate that is not yet effective.
if getdate(self.effective_date) > getdate(today()):
frappe.throw(_("Effective Date cannot be a future date."))
# Effective dates must be strictly increasing so the rate history can be read by date.
last = self.get_last_standard_cost()
if last and getdate(self.effective_date) <= getdate(last.effective_date):
frappe.throw(
_("Effective Date must be after {0} (the last Standard Cost {1}).").format(
frappe.bold(frappe.format(last.effective_date, "Date")),
get_link_to_form("Item Standard Cost", last.name),
)
)
def validate_rate(self):
if flt(self.standard_rate) <= 0:
frappe.throw(_("Standard Valuation Rate must be greater than zero."))
if self.get_last_standard_cost() is None:
# First-ever rate for this item+company: only allowed when no stock movement exists,
# so the item starts its life under Standard Cost (no historical revaluation needed).
if self.has_any_sle():
frappe.throw(
_(
"Standard Cost can only be set up for {0} in {1} before any stock transaction exists."
).format(get_link_to_form("Item", self.item_code), frappe.bold(self.company))
)
return
# R1: a rate change must be effective on/after the latest stock activity, so the
# revaluation entry it creates never sits behind existing transactions.
last_sle_date = self.get_last_sle_date()
if last_sle_date and getdate(self.effective_date) < getdate(last_sle_date):
frappe.throw(
_("Effective Date cannot be before the last stock transaction date {0}.").format(
frappe.bold(frappe.format(last_sle_date, "Date"))
)
)
def on_submit(self):
# This record is now the effective rate. Drop any request-cached lookup that may have read the
# previous (or missing) rate earlier in the request, so the revaluation below — and anything
# else in this request — reads the newly submitted rate.
clear_item_standard_rate_cache()
self.create_revaluation_entry()
def before_cancel(self):
frappe.throw(
_("Item Standard Cost cannot be cancelled. Submit a new record to change the standard rate.")
)
def create_revaluation_entry(self):
"""Revalue on-hand stock to the new standard rate via a Stock Reconciliation.
Submitted atomically: if the reconciliation cannot be submitted (closed period, frozen
accounts, etc.) the exception propagates and this submission is rolled back."""
balances = self.get_warehouse_wise_balance()
if not balances:
return
reco = frappe.new_doc("Stock Reconciliation")
reco.company = self.company
reco.purpose = "Stock Reconciliation"
reco.posting_date = self.effective_date
reco.posting_time = self.get_revaluation_posting_time()
reco.set_posting_time = 1
for row in balances:
reco.append(
"items",
{
"item_code": self.item_code,
"warehouse": row.warehouse,
"qty": row.actual_qty,
"valuation_rate": self.standard_rate,
},
)
reco.flags.via_item_standard_cost = True
reco.insert()
reco.submit()
self.db_set("revaluation_entry", reco.name)
def get_revaluation_posting_time(self):
"""Post the revaluation after the day's last stock movement.
The reconciliation asserts the current on-hand quantity (Bin.actual_qty). If it were posted
before later same-day movements, it would backdate that quantity ahead of them and corrupt the
qty/value timeline. Using the time of the last SLE on the effective date (the reconciliation
sorts after it on creation) keeps the snapshot at the correct point; if there is no movement
that day, the current time is safe since no later movement can exist."""
sle = frappe.qb.DocType("Stock Ledger Entry")
result = (
frappe.qb.from_(sle)
.select(Max(sle.posting_datetime))
.where(
(sle.item_code == self.item_code)
& (sle.company == self.company)
& (sle.is_cancelled == 0)
& (sle.posting_date == getdate(self.effective_date))
)
).run()
last_datetime = result[0][0] if result and result[0][0] else None
# Keep microsecond precision: posting_datetime is compared at microsecond granularity, so a
# truncated time would sort the reco before a same-second movement. Matching the exact time
# lets the later creation order the reco after it.
return get_datetime(last_datetime).strftime("%H:%M:%S.%f") if last_datetime else nowtime()
def get_warehouse_wise_balance(self):
bin_table = frappe.qb.DocType("Bin")
warehouse = frappe.qb.DocType("Warehouse")
return (
frappe.qb.from_(bin_table)
.inner_join(warehouse)
.on(bin_table.warehouse == warehouse.name)
.select(bin_table.warehouse, bin_table.actual_qty)
.where(
(bin_table.item_code == self.item_code)
& (warehouse.company == self.company)
& (bin_table.actual_qty != 0)
)
).run(as_dict=True)
def get_last_standard_cost(self):
records = frappe.get_all(
"Item Standard Cost",
filters={
"item_code": self.item_code,
"company": self.company,
"docstatus": 1,
"name": ("!=", self.name),
},
fields=["name", "effective_date"],
order_by="effective_date desc, creation desc",
limit=1,
)
return records[0] if records else None
def get_last_sle_date(self):
sle = frappe.qb.DocType("Stock Ledger Entry")
result = (
frappe.qb.from_(sle)
.select(Max(sle.posting_date))
.where(
(sle.item_code == self.item_code) & (sle.company == self.company) & (sle.is_cancelled == 0)
)
).run()
return result[0][0] if result and result[0][0] else None
def has_any_sle(self):
return bool(
frappe.db.exists(
"Stock Ledger Entry",
{"item_code": self.item_code, "company": self.company, "is_cancelled": 0},
)
)
@request_cache
def get_item_standard_rate(item_code, company, posting_date=None):
"""Return the standard valuation rate effective for `item_code` in `company` as of
`posting_date` (defaults to today) — i.e. the latest submitted Item Standard Cost whose
effective date is on or before the posting date."""
posting_date = posting_date or today()
rate = frappe.get_all(
"Item Standard Cost",
filters={
"item_code": item_code,
"company": company,
"docstatus": 1,
"effective_date": ("<=", getdate(posting_date)),
},
fields=["standard_rate"],
order_by="effective_date desc, creation desc",
limit=1,
pluck="standard_rate",
)
return flt(rate[0]) if rate else None
def clear_item_standard_rate_cache():
"""Drop the request-cached results of `get_item_standard_rate` so reads after a new Item Standard
Cost is submitted see the fresh rate instead of a value cached earlier in the same request."""
cache = getattr(frappe.local, "request_cache", None)
if cache:
cache.pop(get_item_standard_rate.__wrapped__, None)
def get_purchase_price_variance_account(item_code, company):
"""Resolve the Purchase Price Variance account for a Standard Cost item: the per-company
Item Default override if set, otherwise the Company default."""
account = frappe.db.get_value(
"Item Default",
{"parent": item_code, "company": company},
"purchase_price_variance_account",
)
if not account:
account = frappe.get_cached_value("Company", company, "default_purchase_price_variance_account")
if not account:
frappe.throw(
_(
"Please set a Purchase Price Variance Account for Item {0} or a Default Purchase Price Variance Account in Company {1}."
).format(get_link_to_form("Item", item_code), frappe.bold(company))
)
return account
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_standard_cost_items(
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict | None
):
"""Link-field query for Item Standard Cost: only items whose effective valuation method is
'Standard Cost' — i.e. the item is explicitly Standard Cost, or it has no valuation method of its
own and the applicable default (Company, else Stock Settings) is Standard Cost. This mirrors
get_valuation_method, so every shown item also passes validate_item."""
company = (filters or {}).get("company")
if company:
default_method = frappe.get_cached_value("Company", company, "valuation_method")
else:
default_method = frappe.db.get_single_value("Stock Settings", "valuation_method")
if default_method == "Standard Cost":
# Items with no method of their own inherit the Standard Cost default.
valuation_condition = "and ifnull(item.valuation_method, '') in ('', 'Standard Cost')"
else:
valuation_condition = "and item.valuation_method = 'Standard Cost'"
return frappe.db.sql( # nosemgrep
f"""
select item.name, item.item_name
from `tabItem` item
where item.is_stock_item = 1
and item.disabled = 0
and item.has_variants = 0
{valuation_condition}
and ({searchfield} like %(txt)s or item.item_name like %(txt)s)
order by
(case when item.name like %(txt)s then 0 else 1 end),
item.name
limit %(page_len)s offset %(start)s
""",
{"txt": f"%{txt}%", "start": start, "page_len": page_len},
)

View File

@@ -1,487 +0,0 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from frappe.utils import add_days, flt, 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.tests.utils import ERPNextTestSuite
TEST_COMPANY = "_Test Company"
TEST_WAREHOUSE = "_Test Warehouse - _TC"
# Perpetual-inventory company, needed to assert stock GL entries.
PI_COMPANY = "_Test Company with perpetual inventory"
PI_STORES = "Stores - TCP1"
PI_FG = "Finished Goods - TCP1"
def create_standard_cost_item(**properties):
props = {"valuation_method": "Standard Cost", "is_stock_item": 1, "is_purchase_item": 1}
props.update(properties)
return make_item(properties=props)
def create_item_standard_cost(item_code, rate, company=TEST_COMPANY, effective_date=None, submit=True):
doc = frappe.new_doc("Item Standard Cost")
doc.item_code = item_code
doc.company = company
doc.standard_rate = rate
doc.effective_date = effective_date or today()
doc.insert()
if submit:
doc.submit()
return doc
def ensure_ppv_account(company):
"""Ensure `company` has a Default Purchase Price Variance Account so receipts/invoices of
Standard Cost items can book the receipt-rate-vs-standard difference."""
account = frappe.get_cached_value("Company", company, "default_purchase_price_variance_account")
if account:
return account
from erpnext.accounts.doctype.account.test_account import create_account
# Place it under the same group as the company's default expense account.
expense_account = frappe.get_cached_value("Company", company, "default_expense_account")
parent_account = frappe.db.get_value("Account", expense_account, "parent_account")
account = create_account(
account_name="Purchase Price Variance",
account_type="Expense Account",
parent_account=parent_account,
company=company,
account_currency=frappe.get_cached_value("Company", company, "default_currency"),
)
frappe.db.set_value("Company", company, "default_purchase_price_variance_account", account)
return account
class TestItemStandardCost(ERPNextTestSuite):
def setUp(self):
ensure_ppv_account(TEST_COMPANY)
ensure_ppv_account(PI_COMPANY)
def test_only_for_standard_cost_items(self):
item = make_item(properties={"valuation_method": "FIFO", "is_stock_item": 1})
isc = frappe.new_doc("Item Standard Cost")
isc.item_code = item.name
isc.company = TEST_COMPANY
isc.standard_rate = 100
self.assertRaises(frappe.ValidationError, isc.insert)
def test_item_link_query_lists_only_standard_cost_items(self):
from erpnext.stock.doctype.item_standard_cost.item_standard_cost import get_standard_cost_items
sc_item = create_standard_cost_item().name
fifo_item = make_item(properties={"valuation_method": "FIFO", "is_stock_item": 1}).name
def listed(item_code):
rows = get_standard_cost_items("Item", item_code, "name", 0, 20, {"company": TEST_COMPANY})
return item_code in [row[0] for row in rows]
self.assertTrue(listed(sc_item))
self.assertFalse(listed(fifo_item))
def test_rate_must_be_positive(self):
item = create_standard_cost_item()
isc = frappe.new_doc("Item Standard Cost")
isc.item_code = item.name
isc.company = TEST_COMPANY
isc.standard_rate = 0
self.assertRaises(frappe.ValidationError, isc.insert)
def test_future_effective_date_blocked(self):
item = create_standard_cost_item()
isc = frappe.new_doc("Item Standard Cost")
isc.item_code = item.name
isc.company = TEST_COMPANY
isc.standard_rate = 100
isc.effective_date = add_days(today(), 5)
self.assertRaises(frappe.ValidationError, isc.insert)
def test_first_record_requires_no_stock_ledger_entry(self):
# An item that already has stock movement cannot be moved onto Standard Cost retroactively.
item = make_item(properties={"valuation_method": "FIFO", "is_stock_item": 1})
make_stock_entry(item_code=item.name, target=TEST_WAREHOUSE, qty=5, basic_rate=100)
# Force the method at the db level (the Item-level guard would otherwise block enabling
# Standard Cost while stock exists) and drop the cached valuation method.
frappe.db.set_value("Item", item.name, "valuation_method", "Standard Cost")
frappe.local.request_cache.clear()
isc = frappe.new_doc("Item Standard Cost")
isc.item_code = item.name
isc.company = TEST_COMPANY
isc.standard_rate = 100
self.assertRaises(frappe.ValidationError, isc.insert)
def test_receipt_valued_at_standard(self):
item = create_standard_cost_item()
create_item_standard_cost(item.name, rate=100)
# Receive at a different (billed) rate; the ledger must still value at the standard 100.
se = make_stock_entry(item_code=item.name, target=TEST_WAREHOUSE, qty=10, basic_rate=150)
sle = frappe.get_all(
"Stock Ledger Entry",
filters={"voucher_no": se.name, "is_cancelled": 0},
fields=["valuation_rate", "stock_value", "incoming_rate"],
)[0]
self.assertEqual(flt(sle.valuation_rate), 100)
self.assertEqual(flt(sle.stock_value), 1000)
self.assertEqual(flt(sle.incoming_rate), 100)
def test_rate_change_revalues_on_hand_stock(self):
# Effective dates must strictly increase, so stage the rate change on a later date.
item = create_standard_cost_item()
create_item_standard_cost(item.name, rate=100, effective_date=add_days(today(), -10))
make_stock_entry(
item_code=item.name,
target=TEST_WAREHOUSE,
qty=10,
basic_rate=100,
posting_date=add_days(today(), -5),
)
isc = create_item_standard_cost(item.name, rate=130, effective_date=today())
# Submitting the new rate must auto-create and submit a revaluation Stock Reconciliation.
self.assertTrue(isc.revaluation_entry)
reco_status = frappe.db.get_value("Stock Reconciliation", isc.revaluation_entry, "docstatus")
self.assertEqual(reco_status, 1)
stock_value = frappe.db.get_value(
"Bin", {"item_code": item.name, "warehouse": TEST_WAREHOUSE}, "stock_value"
)
self.assertEqual(flt(stock_value), 1300)
def test_backdated_entry_fast_qty_repost(self):
item = create_standard_cost_item()
create_item_standard_cost(item.name, rate=100, effective_date=add_days(today(), -10))
se1 = make_stock_entry(
item_code=item.name,
target=TEST_WAREHOUSE,
qty=10,
basic_rate=100,
posting_date=add_days(today(), -5),
)
se2 = make_stock_entry(
item_code=item.name,
target=TEST_WAREHOUSE,
qty=5,
basic_rate=100,
posting_date=add_days(today(), -2),
)
se0 = make_stock_entry(
item_code=item.name,
target=TEST_WAREHOUSE,
qty=20,
basic_rate=100,
posting_date=add_days(today(), -7),
)
def sle(se):
return frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_no": se.name, "is_cancelled": 0},
["qty_after_transaction", "stock_value"],
as_dict=True,
)
self.assertEqual(flt(sle(se0).qty_after_transaction), 20)
self.assertEqual(flt(sle(se1).qty_after_transaction), 30)
self.assertEqual(flt(sle(se2).qty_after_transaction), 35)
self.assertEqual(flt(sle(se1).stock_value), 3000)
self.assertEqual(flt(sle(se2).stock_value), 3500)
bin_data = frappe.db.get_value(
"Bin",
{"item_code": item.name, "warehouse": TEST_WAREHOUSE},
["actual_qty", "stock_value"],
as_dict=True,
)
self.assertEqual(flt(bin_data.actual_qty), 35)
self.assertEqual(flt(bin_data.stock_value), 3500)
self.assertFalse(frappe.db.exists("Repost Item Valuation", {"voucher_no": se0.name}))
def test_cannot_cancel(self):
item = create_standard_cost_item()
isc = create_item_standard_cost(item.name, rate=100)
self.assertRaises(frappe.ValidationError, isc.cancel)
def test_direct_stock_reconciliation_blocked(self):
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
item = create_standard_cost_item()
create_item_standard_cost(item.name, rate=100)
make_stock_entry(item_code=item.name, target=TEST_WAREHOUSE, qty=10, basic_rate=100)
self.assertRaises(
frappe.ValidationError,
create_stock_reconciliation,
item_code=item.name,
warehouse=TEST_WAREHOUSE,
qty=8,
rate=120,
)
def test_backdated_transaction_blocked(self):
item = create_standard_cost_item()
create_item_standard_cost(item.name, rate=100, effective_date=today())
# R2 is enforced when the stock ledger entries are written, i.e. at submit time.
se = make_stock_entry(
item_code=item.name,
target=TEST_WAREHOUSE,
qty=10,
basic_rate=100,
posting_date=add_days(today(), -3),
do_not_submit=True,
)
self.assertRaises(frappe.ValidationError, se.submit)
def test_manufacturing_variance_books_to_stock_adjustment(self):
# RM standard 50, FG standard 200. Consuming 5 RM (250) to produce 1 FG (200) leaves a
# 50 manufacturing variance, which must land in the company's Stock Adjustment account.
rm = create_standard_cost_item()
fg = create_standard_cost_item()
create_item_standard_cost(rm.name, rate=50, company=PI_COMPANY)
create_item_standard_cost(fg.name, rate=200, company=PI_COMPANY)
make_stock_entry(item_code=rm.name, to_warehouse=PI_STORES, company=PI_COMPANY, qty=10, basic_rate=50)
se = frappe.new_doc("Stock Entry")
se.purpose = "Repack"
se.stock_entry_type = "Repack"
se.company = PI_COMPANY
se.append("items", {"item_code": rm.name, "s_warehouse": PI_STORES, "qty": 5})
se.append("items", {"item_code": fg.name, "t_warehouse": PI_FG, "qty": 1, "is_finished_item": 1})
se.insert()
se.submit()
# FG is valued at its own standard, not the rolled-up RM cost.
fg_sle = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_no": se.name, "item_code": fg.name, "is_cancelled": 0},
["valuation_rate", "stock_value_difference"],
as_dict=True,
)
self.assertEqual(flt(fg_sle.valuation_rate), 200)
self.assertEqual(flt(fg_sle.stock_value_difference), 200)
stock_adj = frappe.get_cached_value("Company", PI_COMPANY, "stock_adjustment_account")
net = frappe.db.sql(
"select sum(debit - credit) from `tabGL Entry` where voucher_no=%s and account=%s",
(se.name, stock_adj),
)[0][0]
self.assertEqual(flt(net), 50)
def test_valuation_method_change_blocked_with_stock(self):
item = create_standard_cost_item()
create_item_standard_cost(item.name, rate=100)
make_stock_entry(item_code=item.name, target=TEST_WAREHOUSE, qty=10, basic_rate=100)
item.reload()
item.valuation_method = "FIFO"
self.assertRaises(frappe.ValidationError, item.save)
def test_batched_item_revalued_across_warehouses(self):
# A rate change must revalue a batched Standard Cost item in every warehouse, posted as a
# pure value change without a serial/batch bundle.
item = create_standard_cost_item(
has_batch_no=1, create_new_batch=1, batch_number_series="SC-BATCH-.####"
)
create_item_standard_cost(
item.name, rate=100, company=PI_COMPANY, effective_date=add_days(today(), -5)
)
make_stock_entry(
item_code=item.name,
to_warehouse=PI_STORES,
company=PI_COMPANY,
qty=3,
basic_rate=100,
use_serial_batch_fields=1,
posting_date=add_days(today(), -3),
)
make_stock_entry(
item_code=item.name,
to_warehouse=PI_FG,
company=PI_COMPANY,
qty=2,
basic_rate=100,
use_serial_batch_fields=1,
posting_date=add_days(today(), -3),
)
isc = create_item_standard_cost(item.name, rate=150, company=PI_COMPANY, effective_date=today())
self.assertTrue(isc.revaluation_entry)
for warehouse, qty in ((PI_STORES, 3), (PI_FG, 2)):
stock_value = frappe.db.get_value(
"Bin", {"item_code": item.name, "warehouse": warehouse}, "stock_value"
)
self.assertEqual(flt(stock_value), qty * 150)
def test_serialized_item_revalued_across_warehouses(self):
item = create_standard_cost_item(has_serial_no=1, serial_no_series="SC-SER-.####")
create_item_standard_cost(
item.name, rate=100, company=PI_COMPANY, effective_date=add_days(today(), -5)
)
make_stock_entry(
item_code=item.name,
to_warehouse=PI_STORES,
company=PI_COMPANY,
qty=3,
basic_rate=100,
use_serial_batch_fields=1,
posting_date=add_days(today(), -3),
)
make_stock_entry(
item_code=item.name,
to_warehouse=PI_FG,
company=PI_COMPANY,
qty=2,
basic_rate=100,
use_serial_batch_fields=1,
posting_date=add_days(today(), -3),
)
isc = create_item_standard_cost(item.name, rate=150, company=PI_COMPANY, effective_date=today())
self.assertTrue(isc.revaluation_entry)
for warehouse, qty in ((PI_STORES, 3), (PI_FG, 2)):
stock_value = frappe.db.get_value(
"Bin", {"item_code": item.name, "warehouse": warehouse}, "stock_value"
)
self.assertEqual(flt(stock_value), qty * 150)
def test_standard_rate_cache_invalidated_after_submit(self):
from erpnext.stock.doctype.item_standard_cost.item_standard_cost import get_item_standard_rate
item = create_standard_cost_item()
# Read (and request-cache) the rate before any Item Standard Cost exists.
self.assertIsNone(get_item_standard_rate(item.name, TEST_COMPANY))
create_item_standard_cost(item.name, rate=100)
# The submit must have invalidated the cache, so this reads the freshly submitted rate.
self.assertEqual(flt(get_item_standard_rate(item.name, TEST_COMPANY)), 100)
def test_pr_stock_value_excludes_rejected_warehouse(self):
# Accepted and rejected stock for one receipt row share voucher_detail_no. The standard-cost
# SRBNB split must clear only the accepted warehouse's value, not accepted + rejected.
from erpnext.accounts.doctype.purchase_invoice.services.gl_composer import (
PurchaseInvoiceGLComposer,
)
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
item = create_standard_cost_item()
create_item_standard_cost(item.name, rate=100, company=PI_COMPANY)
rejected_warehouse = create_warehouse("_Test SC Rejected Warehouse", company=PI_COMPANY)
# Receive 10 accepted + 2 rejected at a billed rate of 150; both SLEs value at the standard 100.
pr = make_purchase_receipt(
item_code=item.name,
company=PI_COMPANY,
warehouse=PI_STORES,
qty=10,
rejected_qty=2,
rejected_warehouse=rejected_warehouse,
rate=150,
)
# Method body uses only `item`, so it can be called unbound.
def pr_value(stock_qty):
mock_item = frappe._dict(
purchase_receipt=pr.name, pr_detail=pr.items[0].name, stock_qty=stock_qty
)
return flt(PurchaseInvoiceGLComposer.get_pr_stock_value(None, mock_item))
# Billing all 10: accepted only (10 * 100), not accepted + rejected (12 * 100).
self.assertEqual(pr_value(10), 1000)
# Billing only 4 of the 10 accepted units: pro-rated to the invoiced qty (4 * 100).
self.assertEqual(pr_value(4), 400)
def test_pr_books_variance_to_ppv_account(self):
# Receiving a Standard Cost item at a rate above the standard must book the difference to the
# Purchase Price Variance account, not the default expense (COGS) account.
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
ppv_account = ensure_ppv_account(PI_COMPANY)
cogs_account = frappe.get_cached_value("Company", PI_COMPANY, "default_expense_account")
item = create_standard_cost_item()
create_item_standard_cost(item.name, rate=130, company=PI_COMPANY)
# Receive 1 @ 200: stock booked at standard 130, the 70 difference is the purchase price variance.
pr = make_purchase_receipt(
item_code=item.name, company=PI_COMPANY, warehouse=PI_STORES, qty=1, rate=200
)
def booked(account):
return flt(
frappe.db.sql(
"select sum(debit - credit) from `tabGL Entry` where voucher_no=%s and account=%s and is_cancelled=0",
(pr.name, account),
)[0][0]
)
self.assertEqual(booked(ppv_account), 70)
self.assertEqual(booked(cogs_account), 0)
def test_pr_throws_without_ppv_account(self):
# Receiving a Standard Cost item with a variance but no PPV account configured must error.
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
previous = frappe.get_cached_value("Company", PI_COMPANY, "default_purchase_price_variance_account")
frappe.db.set_value("Company", PI_COMPANY, "default_purchase_price_variance_account", None)
frappe.clear_cache(doctype="Company")
try:
item = create_standard_cost_item()
create_item_standard_cost(item.name, rate=130, company=PI_COMPANY)
self.assertRaises(
frappe.ValidationError,
make_purchase_receipt,
item_code=item.name,
company=PI_COMPANY,
warehouse=PI_STORES,
qty=1,
rate=200,
)
finally:
frappe.db.set_value("Company", PI_COMPANY, "default_purchase_price_variance_account", previous)
frappe.clear_cache(doctype="Company")
def test_revaluation_posted_after_same_day_movement(self):
# A movement earlier on the effective date must not end up after the revaluation, otherwise the
# reco would backdate the current quantity ahead of it.
item = create_standard_cost_item()
create_item_standard_cost(item.name, rate=100, effective_date=add_days(today(), -2))
se = make_stock_entry(
item_code=item.name, target=TEST_WAREHOUSE, qty=10, basic_rate=100, posting_date=today()
)
isc = create_item_standard_cost(item.name, rate=150, effective_date=today())
reco_time = frappe.db.get_value("Stock Reconciliation", isc.revaluation_entry, "posting_time")
se_time = frappe.db.get_value(
"Stock Ledger Entry", {"voucher_no": se.name, "is_cancelled": 0}, "posting_time"
)
self.assertGreaterEqual(str(reco_time), str(se_time))
stock_value = frappe.db.get_value(
"Bin", {"item_code": item.name, "warehouse": TEST_WAREHOUSE}, "stock_value"
)
self.assertEqual(flt(stock_value), 1500)

View File

@@ -129,10 +129,8 @@ class LandedCostVoucher(Document):
d.receipt_document_type, d.receipt_document, ["docstatus", "company"]
)
if docstatus != 1:
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"))
msg = f"Row {d.idx}: {d.receipt_document_type} {frappe.bold(d.receipt_document)} must be submitted"
frappe.throw(_(msg), title=_("Invalid Document"))
if company != self.company:
frappe.throw(
@@ -246,7 +244,7 @@ class LandedCostVoucher(Document):
if not total:
frappe.throw(
_(
"Total {0} for all items is zero, maybe you should change 'Distribute Charges Based On'"
"Total {0} for all items is zero, may be you should change 'Distribute Charges Based On'"
).format(based_on)
)
@@ -377,8 +375,8 @@ class LandedCostVoucher(Document):
if not docs or total_asset_qty < item.qty:
frappe.throw(
_(
"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."
"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."
).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 the following reason: <br> {0}").format(new_line_sep(errors))
_("Work Order cannot be created for following reason: <br> {0}").format(new_line_sep(errors))
)
return work_orders

View File

@@ -746,11 +746,11 @@ class TestMaterialRequest(ERPNextTestSuite):
mr = frappe.get_doc("Material Request", mr.name)
mr.submit()
completed_qty = mr.items[0].ordered_qty
requested_qty = frappe.db.get_value(
"Bin",
{"item_code": mr.items[0].item_code, "warehouse": mr.items[0].warehouse},
"indented_qty",
)
requested_qty = frappe.db.sql(
"""select indented_qty from `tabBin` where \
item_code= %s and warehouse= %s """,
(mr.items[0].item_code, mr.items[0].warehouse),
)[0][0]
prod_order = raise_work_orders(mr.name, mr.company)
po = frappe.get_doc("Work Order", prod_order[0])
@@ -760,11 +760,11 @@ class TestMaterialRequest(ERPNextTestSuite):
mr = frappe.get_doc("Material Request", mr.name)
self.assertEqual(completed_qty + po.qty, mr.items[0].ordered_qty)
new_requested_qty = frappe.db.get_value(
"Bin",
{"item_code": mr.items[0].item_code, "warehouse": mr.items[0].warehouse},
"indented_qty",
)
new_requested_qty = frappe.db.sql(
"""select indented_qty from `tabBin` where \
item_code= %s and warehouse= %s """,
(mr.items[0].item_code, mr.items[0].warehouse),
)[0][0]
self.assertEqual(requested_qty - po.qty, new_requested_qty)
@@ -773,11 +773,11 @@ class TestMaterialRequest(ERPNextTestSuite):
mr = frappe.get_doc("Material Request", mr.name)
self.assertEqual(completed_qty, mr.items[0].ordered_qty)
new_requested_qty = frappe.db.get_value(
"Bin",
{"item_code": mr.items[0].item_code, "warehouse": mr.items[0].warehouse},
"indented_qty",
)
new_requested_qty = frappe.db.sql(
"""select indented_qty from `tabBin` where \
item_code= %s and warehouse= %s """,
(mr.items[0].item_code, mr.items[0].warehouse),
)[0][0]
self.assertEqual(requested_qty, new_requested_qty)
def test_requested_qty_multi_uom(self):

View File

@@ -80,13 +80,15 @@ 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 a Draft Delivery Note."))
frappe.throw(
_("A Packing Slip can only be created for Draft Delivery Note.").format(self.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 not be empty or have a value less than 1."))
frappe.throw(_("The 'From Package No.' field must neither be empty nor it's 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 already been created against this Pick List"))
return frappe.msgprint(_("Stock Entry has been already 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 #{0}: item {1} has been picked already.").format(location.idx, location.item_code)
_("Row #{}: item {} 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(_("Row #{0}: Item Code is Mandatory").format(item.idx))
frappe.throw(f"Row #{item.idx}: Item Code is Mandatory")
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 a future date"))
throw(_("Posting Date cannot be future date"))
self.get_current_stock()
self.reset_default_field_value("set_warehouse", "items", "warehouse")
@@ -329,18 +329,14 @@ class PurchaseReceipt(BuyingController):
)
if qi.reference_type != self.doctype or qi.reference_name != self.name:
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))
)
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))
if qi.item_code != item.item_code:
frappe.throw(
_("Row #{0}: Please select a valid Quality Inspection with Item Code {1}.").format(
item.idx, frappe.bold(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))
def get_already_received_qty(self, po, po_detail):
qty = frappe.get_all(

View File

@@ -240,7 +240,13 @@ class PurchaseReceiptGLComposer(BaseStockGLComposer):
divisional_loss -= rejected_item_cost
if divisional_loss:
loss_account = self.get_divisional_loss_account(item, stock_asset_rbnb)
loss_account = (
doc.get_company_default("default_expense_account", ignore_validation=True)
or stock_asset_rbnb
)
if doc.is_return and item.expense_account:
loss_account = item.expense_account
cost_center = item.cost_center or frappe.get_cached_value(
"Company", doc.company, "cost_center"
@@ -353,30 +359,6 @@ class PurchaseReceiptGLComposer(BaseStockGLComposer):
+ "\n".join(warehouse_with_no_account)
)
def get_divisional_loss_account(self, item, stock_asset_rbnb):
"""Account that absorbs the difference between the document value and the value actually
booked into stock. For a Standard Cost item this difference is a purchase price variance
(receipt rate vs standard rate), so it goes to the Purchase Price Variance account; for all
other items it keeps the existing behaviour (default expense account, or the item's expense
account on a return)."""
from erpnext.stock.utils import get_valuation_method
doc = self.doc
if item.item_code and get_valuation_method(item.item_code, doc.company) == "Standard Cost":
from erpnext.stock.doctype.item_standard_cost.item_standard_cost import (
get_purchase_price_variance_account,
)
return get_purchase_price_variance_account(item.item_code, doc.company)
loss_account = (
doc.get_company_default("default_expense_account", ignore_validation=True) or stock_asset_rbnb
)
if doc.is_return and item.expense_account:
loss_account = item.expense_account
return loss_account
def _make_tax_gl_entries(self, gl_entries: list, via_landed_cost_voucher: bool = False) -> None:
doc = self.doc
negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in doc.get("items")])

View File

@@ -58,7 +58,7 @@ class PutawayRule(Document):
def validate_priority(self):
if self.priority < 1:
frappe.throw(_("Priority cannot be less than 1."), title=_("Invalid Priority"))
frappe.throw(_("Priority cannot be lesser 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 accommodated:") + "<br><br>"
msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "<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' is disabled for the item {0}, no need to create the QI"
"'Inspection Required before Purchase' has 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' is disabled for the item {0}, no need to create the QI"
"'Inspection Required before Delivery' has disabled for the item {0}, no need to create the QI"
).format(get_link_to_form("Item", self.item_code))
)

View File

@@ -1,6 +1,5 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:parameter",
"creation": "2020-12-28 17:06:00.254129",
"doctype": "DocType",
@@ -35,7 +34,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-06-19 10:55:00.000000",
"modified": "2024-03-27 13:10:28.861722",
"modified_by": "Administrator",
"module": "Stock",
"name": "Quality Inspection Parameter",

View File

@@ -94,7 +94,7 @@ class RepostItemValuation(Document):
self.validate_recreate_stock_ledgers()
def set_default_posting_time(self):
if self.posting_time is None:
if not self.posting_time:
self.posting_time = nowtime()
if not self.posting_date:
@@ -209,7 +209,7 @@ class RepostItemValuation(Document):
):
frappe.msgprint(_("Caution: This might alter frozen accounts."))
return
frappe.throw(_("You cannot repost item valuation before {0}").format(acc_frozen_till_date))
frappe.throw(_("You cannot repost item valuation before {}").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 the following {0} as they are either Delivered, Inactive or located in a different warehouse."
"You cannot outward following {0} as either they are 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} is not linked to {1} {2}").format(
_("The serial and batch bundle {0} 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} is not linked to {1} {2}").format(
_("The serial and batch bundle {0} 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 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}"
"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}"
).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 it again in Manufacture / Repack entry."
"Serial No {0} is already Delivered. You cannot use them 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):
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,
)
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)
def is_stock_reco_for_valuation_adjustment(self, available_qty):
if (
@@ -1153,7 +1153,8 @@ 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:
frappe.throw(_("The Item {0} does not have Serial No or Batch No").format(self.item_code))
msg = f"The Item {self.item_code} does not have Serial No or Batch No"
frappe.throw(_(msg))
serial_nos = []
batch_nos = []
@@ -1588,11 +1589,12 @@ class SerialandBatchBundle(Document):
date_msg = " " + _("as of {0}").format(format_datetime(posting_datetime))
msg = _(
"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."
"""
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."""
).format(
bold(batch_no),
bold(self.item_code),
@@ -1726,11 +1728,9 @@ class SerialandBatchBundle(Document):
and self.voucher_detail_no
and frappe.db.exists(child_doctype, self.voucher_detail_no)
):
frappe.throw(
_("The {0} {1} is in submitted state, please cancel it first").format(
self.voucher_type, bold(self.voucher_no)
)
)
msg = f"""The {self.voucher_type} {bold(self.voucher_no)}
is in submitted state, please cancel it first"""
frappe.throw(_(msg))
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 exist").format(serial_no))
frappe.throw(_("Serial No {0} does not exists").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 exist").format(batch_no))
frappe.throw(_("Batch No {0} does not exists").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, the system will take some time to complete it."
"Stock Closing Entry {0} has been queued for processing, system will take sometime to complete it."
).format(self.name)
)

View File

@@ -124,8 +124,7 @@ class BaseManufactureStockEntry(BaseStockEntry):
self.doc.process_loss_qty = flt(process_loss_qty, precision)
frappe.msgprint(
_("The Process Loss Qty has been reset as per the Job Card's Process Loss Qty"),
alert=True,
_("The Process Loss Qty has reset as per job cards 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,11 +404,12 @@ class StockEntry(StockController, SubcontractingInwardController):
if row.job_card_item or not row.s_warehouse:
continue
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)
)
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))
def validate_work_order_status(self):
pro_doc = frappe.get_doc("Work Order", self.work_order)
@@ -884,7 +885,7 @@ class StockEntry(StockController, SubcontractingInwardController):
if not finished_items:
frappe.throw(
msg=_("There must be at least 1 Finished Good in this Stock Entry").format(self.name),
msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name),
title=_("Missing Finished Good"),
exc=FinishedGoodError,
)
@@ -907,7 +908,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(
_("Quantity {0} should not be greater than allowed quantity {1}").format(
_("For quantity {0} should not be greater than allowed quantity {1}").format(
flt(self.fg_completed_qty), allowed_qty
)
)
@@ -1372,8 +1373,7 @@ class StockEntry(StockController, SubcontractingInwardController):
self.process_loss_qty = flt(process_loss_qty, precision)
frappe.msgprint(
_("The Process Loss Qty has been reset as per the job card's Process Loss Qty"),
alert=True,
_("The Process Loss Qty has reset as per job cards Process Loss Qty"), alert=True
)
if not self.process_loss_percentage and not self.process_loss_qty:

View File

@@ -40,12 +40,19 @@ from erpnext.tests.utils import ERPNextTestSuite
def get_sle(**args):
return frappe.get_all(
"Stock Ledger Entry",
filters=args,
fields=["*"],
order_by="posting_datetime desc, creation desc",
limit=1,
condition, values = "", []
for key, value in args.items():
condition += " and " if condition else " where "
condition += f"`{key}`=%s"
values.append(value)
return frappe.db.sql(
# posting_datetime is the precomputed date+time column; MySQL-only timestamp(date,time) errors on Postgres
"""select * from `tabStock Ledger Entry` %s
order by posting_datetime desc, creation desc limit 1"""
% condition,
values,
as_dict=1,
)
@@ -262,10 +269,20 @@ class TestStockEntry(ERPNextTestSuite):
mr.cancel()
self.assertTrue(
frappe.db.exists("Stock Ledger Entry", {"voucher_type": "Stock Entry", "voucher_no": mr.name})
frappe.db.sql(
"""select * from `tabStock Ledger Entry`
where voucher_type='Stock Entry' and voucher_no=%s""",
mr.name,
)
)
self.assertTrue(frappe.db.exists("GL Entry", {"voucher_type": "Stock Entry", "voucher_no": mr.name}))
self.assertTrue(
frappe.db.sql(
"""select * from `tabGL Entry`
where voucher_type='Stock Entry' and voucher_no=%s""",
mr.name,
)
)
def test_material_issue_gl_entry(self):
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
@@ -344,7 +361,12 @@ class TestStockEntry(ERPNextTestSuite):
if source_warehouse_account == target_warehouse_account:
# no gl entry as both source and target warehouse has linked to same account.
self.assertFalse(
frappe.db.exists("GL Entry", {"voucher_type": "Stock Entry", "voucher_no": mtn.name})
frappe.db.sql(
"""select * from `tabGL Entry`
where voucher_type='Stock Entry' and voucher_no=%s""",
mtn.name,
as_dict=1,
)
)
else:
@@ -438,9 +460,14 @@ class TestStockEntry(ERPNextTestSuite):
],
)
self.assertFalse(
frappe.db.exists("GL Entry", {"voucher_type": "Stock Entry", "voucher_no": repack.name})
gl_entries = frappe.db.sql(
"""select account, debit, credit
from `tabGL Entry` where voucher_type='Stock Entry' and voucher_no=%s
order by account desc""",
repack.name,
as_dict=1,
)
self.assertFalse(gl_entries)
def test_repack_with_additional_costs(self):
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
@@ -574,15 +601,15 @@ class TestStockEntry(ERPNextTestSuite):
expected_sle.sort(key=lambda x: x[1])
# check stock ledger entries
sle = frappe.get_all(
"Stock Ledger Entry",
filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
fields=["item_code", "warehouse", "actual_qty"],
order_by="item_code, warehouse, actual_qty",
as_list=True,
sle = frappe.db.sql(
"""select item_code, warehouse, actual_qty
from `tabStock Ledger Entry` where voucher_type = %s
and voucher_no = %s order by item_code, warehouse, actual_qty""",
(voucher_type, voucher_no),
as_list=1,
)
self.assertTrue(sle)
sle = sorted(sle, key=lambda x: x[1])
sle.sort(key=lambda x: x[1])
for i, sle_value in enumerate(sle):
self.assertEqual(expected_sle[i][0], sle_value[0])
@@ -592,16 +619,16 @@ class TestStockEntry(ERPNextTestSuite):
def check_gl_entries(self, voucher_type, voucher_no, expected_gl_entries):
expected_gl_entries.sort(key=lambda x: x[0])
gl_entries = frappe.get_all(
"GL Entry",
filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
fields=["account", "debit", "credit"],
order_by="account asc, debit asc",
as_list=True,
gl_entries = frappe.db.sql(
"""select account, debit, credit
from `tabGL Entry` where voucher_type=%s and voucher_no=%s
order by account asc, debit asc""",
(voucher_type, voucher_no),
as_list=1,
)
self.assertTrue(gl_entries)
gl_entries = sorted(gl_entries, key=lambda x: x[0])
gl_entries.sort(key=lambda x: x[0])
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gl_entries[i][0], gle[0])
self.assertEqual(expected_gl_entries[i][1], gle[1])

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 a positive number").format(
_("Row {0}: The item {1}, quantity must be 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 an Asset/Liability type account "
"Difference Account must be a 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(_("Stock Entry Type {0} cannot be set as standard").format(self.name))
frappe.throw(f"Stock Entry Type {self.name} cannot be set as standard")
class ManufactureEntry:

View File

@@ -234,21 +234,12 @@ class StockLedgerEntry(Document):
self.throw_error_message(f"Item {self.item_code} must be a stock Item")
if item_detail.has_serial_no or item_detail.has_batch_no:
if not self.serial_and_batch_bundle and not self.is_standard_cost_revaluation():
if not self.serial_and_batch_bundle:
self.throw_error_message(f"Serial No / Batch No are mandatory for Item {self.item_code}")
if self.serial_and_batch_bundle and not item_detail.has_serial_no and not item_detail.has_batch_no:
self.throw_error_message(f"Serial No and Batch No are not allowed for Item {self.item_code}")
def is_standard_cost_revaluation(self):
"""A Standard Cost item is revalued through a Stock Reconciliation that changes the rate only
(qty unchanged); it carries no serial/batch bundle, so the bundle requirement is bypassed."""
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import is_standard_cost_item
return self.voucher_type == "Stock Reconciliation" and is_standard_cost_item(
self.item_code, self.company
)
def throw_error_message(self, message, exception=frappe.ValidationError):
frappe.throw(_(message), exception)
@@ -351,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 for this transaction.")
msg += "<br><br>" + _("Please contact any of the following users to {} this transaction.")
msg += "<br>" + "<br>".join(authorized_users)
frappe.throw(msg, BackDatedStockTransaction, title=_("Backdated Stock Entry"))

Some files were not shown because too many files have changed in this diff Show More