diff --git a/erpnext/accounts/custom/address.py b/erpnext/accounts/custom/address.py
index 5ea8b1d1ca8..8f57489a82f 100644
--- a/erpnext/accounts/custom/address.py
+++ b/erpnext/accounts/custom/address.py
@@ -17,7 +17,7 @@ class ERPNextAddress(Address):
def link_address(self):
"""Link address based on owner"""
- if self.is_your_company_address:
+ if self.get("is_your_company_address"):
return
return super().link_address()
@@ -28,7 +28,9 @@ class ERPNextAddress(Address):
self.is_your_company_address = 1
def validate_reference(self):
- if self.is_your_company_address and not [row for row in self.links if row.link_doctype == "Company"]:
+ if self.get("is_your_company_address") and not [
+ row for row in self.links if row.link_doctype == "Company"
+ ]:
frappe.throw(
_(
"Address needs to be linked to a Company. Please add a row for Company in the Links table."
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
index 9381ad2ff18..3bc183151c0 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
@@ -21,6 +21,8 @@
"enable_common_party_accounting",
"allow_multi_currency_invoices_against_single_party_account",
"confirm_before_resetting_posting_date",
+ "stock_expense_section",
+ "book_stock_expense_gl_entries",
"analytics_section",
"enable_discounts_and_margin",
"enable_accounting_dimensions",
@@ -75,6 +77,8 @@
"over_billing_allowance",
"credit_controller",
"role_allowed_to_over_bill",
+ "enable_overdue_billing_threshold",
+ "role_allowed_to_bypass_overdue_billing",
"column_break_11",
"assets_tab",
"asset_settings_section",
@@ -271,6 +275,21 @@
"label": "Role Allowed to over bill ",
"options": "Role"
},
+ {
+ "default": "0",
+ "description": "Block a new Sales Invoice when the customer's overdue amount exceeds the Overdue Limit set on the customer.",
+ "fieldname": "enable_overdue_billing_threshold",
+ "fieldtype": "Check",
+ "label": "Restrict Customer Over Billing"
+ },
+ {
+ "depends_on": "eval:doc.enable_overdue_billing_threshold",
+ "description": "Users with this role can still submit invoices for customers who have crossed their Overdue Limit.",
+ "fieldname": "role_allowed_to_bypass_overdue_billing",
+ "fieldtype": "Link",
+ "label": "Role Allowed to Bypass Over Billing Restriction",
+ "options": "Role"
+ },
{
"fieldname": "period_closing_settings_section",
"fieldtype": "Section Break"
@@ -749,6 +768,18 @@
"description": "Changing the account in any transaction of the DocTypes listed below will trigger a repost. To prevent reposting, remove the relevant DocType from the list.",
"fieldname": "column_break_mfor",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "stock_expense_section",
+ "fieldtype": "Section Break",
+ "label": "Stock Expense Accounting"
+ },
+ {
+ "default": "0",
+ "description": "Books Purchase Expense and Expenses Added To Stock account pairs against stock value. On enabling this, the accounts become mandatory in Company or Item Defaults for Purchase Receipt, Purchase Invoice, Stock Entry, Stock Reconciliation and Landed Cost Voucher",
+ "fieldname": "book_stock_expense_gl_entries",
+ "fieldtype": "Check",
+ "label": "Book Stock Expense GL Entries"
}
],
"grid_page_length": 50,
@@ -757,7 +788,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2026-06-24 12:59:41.868865",
+ "modified": "2026-07-27 12:00:00.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
index e76281687ab..0497429e6d2 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
@@ -62,6 +62,7 @@ class AccountsSettings(Document):
book_asset_depreciation_entry_automatically: DF.Check
book_deferred_entries_based_on: DF.Literal["Days", "Months"]
book_deferred_entries_via_journal_entry: DF.Check
+ book_stock_expense_gl_entries: DF.Check
book_tax_discount_loss: DF.Check
calculate_depr_using_total_days: DF.Check
check_supplier_invoice_uniqueness: DF.Check
@@ -77,6 +78,7 @@ class AccountsSettings(Document):
enable_fuzzy_matching: DF.Check
enable_immutable_ledger: DF.Check
enable_loyalty_point_program: DF.Check
+ enable_overdue_billing_threshold: DF.Check
enable_party_matching: DF.Check
enable_subscription: DF.Check
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
@@ -95,6 +97,7 @@ class AccountsSettings(Document):
receivable_payable_remarks_length: DF.Int
reconciliation_queue_size: DF.Int
repost_allowed_types: DF.Table[RepostAllowedTypes]
+ role_allowed_to_bypass_overdue_billing: DF.Link | None
role_allowed_to_over_bill: DF.Link | None
role_to_notify_on_depreciation_failure: DF.Link | None
role_to_override_stop_action: DF.Link | None
@@ -150,6 +153,10 @@ class AccountsSettings(Document):
toggle_subscription_sections(not self.enable_subscription)
clear_cache = True
+ if old_doc.enable_overdue_billing_threshold != self.enable_overdue_billing_threshold:
+ toggle_overdue_billing_threshold_field(not self.enable_overdue_billing_threshold)
+ clear_cache = True
+
if clear_cache:
frappe.clear_cache()
@@ -241,6 +248,10 @@ def toggle_subscription_sections(hide):
create_property_setter_for_hiding_field(doctype, "subscription_section", hide)
+def toggle_overdue_billing_threshold_field(hide):
+ create_property_setter_for_hiding_field("Customer Credit Limit", "overdue_billing_threshold", hide)
+
+
def create_property_setter_for_hiding_field(doctype, field_name, hide):
make_property_setter(
doctype,
diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
index 0a291d3668e..f459481a496 100644
--- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
@@ -143,6 +143,30 @@ def preprocess_mt940_content(content: str) -> str:
return processed_content
+MT940_CUSTOMER_REFERENCE_MAX_LEN = 16
+
+
+def get_transaction_reference(txn_data: dict) -> str:
+ """Extract the per-transaction reference from an MT940 :61: tag.
+
+ The mt940 library exposes ``transaction_reference`` from the :20: tag, which is the
+ statement-level reference and identical for every transaction in a statement. The
+ real per-transaction reference is ``customer_reference`` (with any overflow captured
+ into ``extra_details`` when a bank emits a single-line :61: longer than 16 chars).
+ """
+ customer_reference = (txn_data.get("customer_reference") or "").strip()
+
+ if len(customer_reference) == MT940_CUSTOMER_REFERENCE_MAX_LEN:
+ customer_reference += (txn_data.get("extra_details") or "").strip()
+
+ if customer_reference and customer_reference.upper() != "NONREF":
+ return customer_reference
+
+ return (txn_data.get("bank_reference") or "").strip() or (
+ txn_data.get("transaction_reference") or ""
+ ).strip()
+
+
@frappe.whitelist()
def convert_mt940_to_csv(data_import, mt940_file_path):
doc = frappe.get_doc("Bank Statement Import", data_import)
@@ -190,8 +214,8 @@ def convert_mt940_to_csv(data_import, mt940_file_path):
deposit = amount_value if amount_value > 0 else ""
withdrawal = abs(amount_value) if amount_value < 0 else ""
- description = txn.data.get("extra_details") or ""
- reference = txn.data.get("transaction_reference") or ""
+ description = txn.data.get("transaction_details") or txn.data.get("extra_details") or ""
+ reference = get_transaction_reference(txn.data)
currency = txn.data.get("currency", "")
writer.writerow([date_str, deposit, withdrawal, description, reference, doc.bank_account, currency])
diff --git a/erpnext/accounts/doctype/bank_statement_import/test_bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/test_bank_statement_import.py
index 2ae00059c83..79ec1ef976a 100644
--- a/erpnext/accounts/doctype/bank_statement_import/test_bank_statement_import.py
+++ b/erpnext/accounts/doctype/bank_statement_import/test_bank_statement_import.py
@@ -1,7 +1,10 @@
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
+import mt940
+
from erpnext.accounts.doctype.bank_statement_import.bank_statement_import import (
+ get_transaction_reference,
is_mt940_format,
preprocess_mt940_content,
)
@@ -188,6 +191,135 @@ class TestBankStatementImport(ERPNextTestSuite):
self.assertIn(":20:STMTREF167619", result) # Reference should remain unchanged
self.assertIn("UPI/TEST USER/123456789/PaidViaTestApp", result)
+ def test_get_transaction_reference_uses_customer_reference(self):
+ """Per-transaction reference must come from :61: customer_reference, not :20:."""
+ self.assertEqual(
+ get_transaction_reference(
+ {"customer_reference": "UPI-100000000001", "transaction_reference": "STMTREF12345"}
+ ),
+ "UPI-100000000001",
+ )
+
+ def test_get_transaction_reference_rejoins_overflow(self):
+ """When a bank emits a single-line :61: with >16-char reference, the regex
+ splits the tail into extra_details. We must rejoin them."""
+ self.assertEqual(
+ get_transaction_reference(
+ {
+ "customer_reference": "NEFTINW-12345678",
+ "extra_details": "90",
+ "transaction_reference": "STMTREF12345",
+ }
+ ),
+ "NEFTINW-1234567890",
+ )
+
+ def test_get_transaction_reference_falls_back_to_bank_reference_on_nonref(self):
+ """NONREF is the MT940 'no customer reference' sentinel; prefer bank_reference."""
+ self.assertEqual(
+ get_transaction_reference(
+ {
+ "customer_reference": "NONREF",
+ "bank_reference": "1234567890123456",
+ "transaction_reference": "STMTREF12345",
+ }
+ ),
+ "1234567890123456",
+ )
+
+ def test_get_transaction_reference_falls_back_to_bank_reference_on_nonref_with_extra_details(self):
+ """NONREF sentinel must trigger the bank_reference fallback even when
+ extra_details is populated. Without the 16-char gate, the old naive concat
+ would produce a junk reference like 'NONREFsome info' and bypass the check."""
+ self.assertEqual(
+ get_transaction_reference(
+ {
+ "customer_reference": "NONREF",
+ "extra_details": "some info",
+ "bank_reference": "1234567890123456",
+ "transaction_reference": "STMTREF12345",
+ }
+ ),
+ "1234567890123456",
+ )
+
+ def test_get_transaction_reference_does_not_append_extra_details_below_16_chars(self):
+ """When customer_reference is below the 16-char cap, extra_details is a
+ genuine supplementary-info field from :61: — not overflow — and must not
+ be appended to the reference."""
+ self.assertEqual(
+ get_transaction_reference(
+ {
+ "customer_reference": "TBMS-123456789",
+ "extra_details": "note field",
+ "transaction_reference": "STMTREF12345",
+ }
+ ),
+ "TBMS-123456789",
+ )
+
+ def test_get_transaction_reference_keeps_noref_literal(self):
+ """Bare 'NOREF' (without bank_reference) stays as-is; still better than the
+ statement-level reference which is identical across all transactions."""
+ self.assertEqual(
+ get_transaction_reference(
+ {
+ "customer_reference": "NOREF",
+ "bank_reference": None,
+ "transaction_reference": "STMTREF12345",
+ }
+ ),
+ "NOREF",
+ )
+
+ def test_mt940_parse_per_transaction_reference_mapping(self):
+ """End-to-end: every transaction in a statement must get its own distinct
+ reference from :61: customer_reference, never the statement-level :20: reference."""
+ mt940_content = """{1:F0112345678901X0000000000}{2:I94012345678901XN}{4:
+:20:STMTREF12345
+:25:1234567890
+:28C:12345/1
+:60F:C250716INR88123,38
+:61:2509280928D5000,00NMSCUPI-100000000001
+:86:UPI/TEST PAYEE ONE/111111111111/TestApp
+:61:2509190919D2606,00NMSCUPI-100000000002
+:86:UPI/TEST PAYEE TWO/222222222222/TestApp
+:61:2509190919D900,00NMSCUPI-100000000003
+:86:UPI/TEST PAYEE THREE/333333333333/TestApp
+:61:2508140814D5000,00NMSCUPI-100000000004
+:86:UPI/TEST PAYEE FOUR/444444444444/TestApp
+:61:2508060806D2000,00NMSCUPI-100000000005
+:86:UPI/TEST PAYEE FIVE/555555555555/TestApp
+:61:2508030803D1066,00NMSC123456789012
+:86:PCD/1234/TEST MERCHANT/01234567890123/12:00
+:61:2507310731D305,62NMSCTBMS-123456789
+:86:Chrg: Debit Card Annual Fee 1234 for 2025
+:61:2507240724C1,00NMSCNEFTINW-1234567890
+:86:NEFT TEST123456789 TEST SERVICES
+:61:2507170717C100000,00NMSCNOREF
+:86:BY CLG INST 123456/01-01-25/TESTBANK/TESTCITY
+:62F:C250930INR100000,00
+-}"""
+ transactions = list(mt940.parse(preprocess_mt940_content(mt940_content)))
+ references = [get_transaction_reference(t.data) for t in transactions]
+
+ self.assertEqual(
+ references,
+ [
+ "UPI-100000000001",
+ "UPI-100000000002",
+ "UPI-100000000003",
+ "UPI-100000000004",
+ "UPI-100000000005",
+ "123456789012",
+ "TBMS-123456789",
+ "NEFTINW-1234567890",
+ "NOREF",
+ ],
+ )
+ # No transaction should carry the statement-level reference from :20:
+ self.assertNotIn("STMTREF12345", references)
+
def test_preprocess_mt940_content_whitespace_variants(self):
"""Test handling of whitespace and different line endings"""
# Test with trailing spaces
diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
index b75d7c6fc50..3d45d0445ff 100644
--- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
+++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
@@ -616,15 +616,27 @@ class ExchangeRateRevaluation(Document):
if journals:
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
- for x in journals:
- reversal = make_reverse_journal_entry(x)
- reversal.posting_date = nowdate()
- reversal.submit()
- frappe.msgprint(
- _("Revaluation journal for {0} has been created: {1}").format(
- frappe.bold(x), get_link_to_form("Journal Entry", reversal.name)
- )
+ if drafts := frappe.db.get_all(
+ "Journal Entry",
+ filters={"docstatus": 0, "reversal_of": ["in", journals]},
+ pluck="name",
+ as_list=1,
+ ):
+ part = "journals are" if len(drafts) > 1 else "journal is"
+ doc_links = ", ".join(["{}".format(get_link_to_form("Journal Entry", x)) for x in drafts])
+ frappe.throw(
+ msg=_("Reverse {0} already available in draft status: {1}").format(part, doc_links),
)
+ else:
+ for x in journals:
+ reversal = make_reverse_journal_entry(x)
+ reversal.posting_date = nowdate()
+ reversal.save()
+ frappe.msgprint(
+ _("A draft reverse journal for {0} has been created: {1}").format(
+ frappe.bold(x), get_link_to_form("Journal Entry", reversal.name)
+ )
+ )
def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py
index 78453b68c4d..be765ad2186 100644
--- a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py
+++ b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py
@@ -9,11 +9,10 @@ from frappe.utils import add_days, flt, today
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
-from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.tests.utils import ERPNextTestSuite
-class TestExchangeRateRevaluation(ERPNextTestSuite, AccountsTestMixin):
+class TestExchangeRateRevaluation(ERPNextTestSuite):
def setUp(self):
self.company = "_Test Company"
self.item = "_Test Item"
@@ -23,14 +22,6 @@ class TestExchangeRateRevaluation(ERPNextTestSuite, AccountsTestMixin):
self.set_system_and_company_settings()
def set_system_and_company_settings(self):
- # set number and currency precision
- system_settings = frappe.get_doc("System Settings")
- system_settings.float_precision = 2
- system_settings.currency_precision = 2
- system_settings.language = "en"
- system_settings.time_zone = "Asia/Kolkata"
- system_settings.save()
-
# Using Exchange Gain/Loss account for unrealized as well.
company_doc = frappe.get_doc("Company", self.company)
company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account
@@ -300,3 +291,97 @@ class TestExchangeRateRevaluation(ERPNextTestSuite, AccountsTestMixin):
for key, _val in expected_data.items():
self.assertEqual(expected_data.get(key), account_details.get(key))
+
+ @ERPNextTestSuite.change_settings(
+ "Accounts Settings",
+ {"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
+ )
+ def test_05_revaluation_journal_reversal(self):
+ """
+ Test reversing of revaluation journals
+ """
+ si = create_sales_invoice(
+ item=self.item,
+ company=self.company,
+ customer="_Test Customer 1",
+ debit_to=self.debtors_usd,
+ posting_date=today(),
+ parent_cost_center=self.cost_center,
+ cost_center=self.cost_center,
+ rate=100,
+ price_list_rate=100,
+ do_not_submit=1,
+ )
+ si.currency = "USD"
+ si.conversion_rate = 80
+ si.save().submit()
+
+ err = frappe.new_doc("Exchange Rate Revaluation")
+ err.company = self.company
+ err.posting_date = today()
+ err.fetch_and_calculate_accounts_data()
+ self.assertEqual(len(err.accounts), 1)
+ err.save().submit()
+
+ gain_loss_account = err.get_for_unrealized_gain_loss_account()
+ usd_account = err.accounts[0].account
+ old_balance = err.accounts[0].balance_in_base_currency
+ new_balance = err.accounts[0].new_balance_in_base_currency
+ total_gain_loss = err.total_gain_loss
+
+ # Create JV for ERR
+ ret = err.check_journal_and_reversal()
+ self.assertFalse(ret.get("journals_posted"))
+ err_journals = err.make_jv_entries()
+ je = frappe.get_doc("Journal Entry", err_journals.get("revaluation_jv"))
+ je = je.submit()
+
+ je.reload()
+ self.assertEqual(je.voucher_type, "Exchange Rate Revaluation")
+ self.assertEqual(len(je.accounts), 3)
+ # A gain is credited to the gain/loss account, a loss is debited. The current
+ # exchange rate (from master data) may sit either side of the booked rate, so
+ # derive the column from the sign instead of assuming a gain.
+ gain_loss_debit = abs(total_gain_loss) if total_gain_loss < 0 else 0.0
+ gain_loss_credit = total_gain_loss if total_gain_loss > 0 else 0.0
+ expected = [
+ (usd_account, new_balance, 0.0, 100.0, 0.0),
+ (usd_account, 0.0, old_balance, 0.0, 100.0),
+ (gain_loss_account, gain_loss_debit, gain_loss_credit, gain_loss_debit, gain_loss_credit),
+ ]
+ actual = []
+ for acc in je.accounts:
+ actual.append(
+ (
+ acc.account,
+ acc.debit,
+ acc.credit,
+ acc.debit_in_account_currency,
+ acc.credit_in_account_currency,
+ )
+ )
+ self.assertEqual(expected, actual)
+
+ # Assert reversals are not posted
+ ret = err.check_journal_and_reversal()
+ self.assertTrue(ret.get("journals_posted"))
+ self.assertFalse(ret.get("reversals_posted"))
+
+ err.make_reverse_journal()
+ # submit
+ draft = frappe.db.get_all(
+ "Journal Entry",
+ filters={"docstatus": 0, "reversal_of": je.name, "voucher_type": "Exchange Rate Revaluation"},
+ pluck="name",
+ as_list=1,
+ )
+ self.assertIsNotNone(draft)
+ frappe.get_doc("Journal Entry", draft[0]).submit()
+ ret = err.check_journal_and_reversal()
+ self.assertTrue(ret.get("journals_posted"))
+ self.assertTrue(ret.get("reversals_posted"))
+
+ reverse_jv = frappe.db.get_all(
+ "Journal Entry", filters={"reversal_of": err_journals.get("revaluation_jv")}, pluck="name"
+ )
+ self.assertIsNotNone(reverse_jv)
diff --git a/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py b/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py
index 47c7e2e6366..c79cbfe1448 100644
--- a/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py
+++ b/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py
@@ -1853,28 +1853,51 @@ class GrowthViewTransformer:
self.formatted_rows = context.raw_data.get("formatted_data", [])
self.period_list = context.period_list
- def transform(self) -> None:
+ def transform(self):
for row_data in self.formatted_rows:
if row_data.get("is_blank_line"):
continue
- transformed_values = {}
- for i in range(len(self.period_list)):
- current_period = self.period_list[i]["key"]
+ if row_data.get("segment_values"):
+ self._transform_segmented_row(row_data)
+ else:
+ self._transform_single_row(row_data)
- current_value = row_data[current_period]
- previous_value = row_data[self.period_list[i - 1]["key"]] if i != 0 else 0
+ def _compute_growth_values(self, source: dict) -> dict:
+ transformed = {}
- if i == 0:
- transformed_values[current_period] = current_value
- else:
- growth_percent = self._calculate_growth(previous_value, current_value)
- transformed_values[current_period] = growth_percent
+ for i, period in enumerate(self.period_list):
+ current_period = period["key"]
+ current_value = source.get(current_period)
- row_data.update(transformed_values)
+ if current_value in (None, ""):
+ continue
+
+ if i == 0:
+ transformed[current_period] = current_value
+ else:
+ previous_period = self.period_list[i - 1]["key"]
+ previous_value = source.get(previous_period) or 0
+ transformed[current_period] = self._calculate_growth(previous_value, current_value)
+
+ return transformed
+
+ def _transform_single_row(self, row_data: dict):
+ row_data.update(self._compute_growth_values(row_data))
+
+ def _transform_segmented_row(self, row_data: dict):
+ for seg_id, seg_data in row_data.get("segment_values", {}).items():
+ if seg_data.get("is_blank_line"):
+ continue
+
+ transformed = self._compute_growth_values(seg_data)
+ seg_data.update(transformed)
+
+ for period_key, value in transformed.items():
+ row_data[f"{seg_id}_{period_key}"] = value
def _calculate_growth(self, previous_value: float, current_value: float) -> float | None:
- if current_value is None:
+ if current_value in (None, ""):
return None
if previous_value == 0 and current_value > 0:
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js
index 4659f3e2b4b..950e0b99499 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.js
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js
@@ -567,6 +567,7 @@ $.extend(erpnext.journal_entry, {
lock_reversal_entry: function (frm) {
frm.fields
.filter((field) => field.has_input)
+ .filter((field) => field.df.fieldname != "posting_date")
.forEach((field) => frm.set_df_property(field.df.fieldname, "read_only", 1));
frm.set_df_property("accounts", "read_only", 1);
},
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
index ea93b23aa11..7b2b9e922bd 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
@@ -217,7 +217,8 @@ class POSClosingEntry(StatusUpdater):
self.update_sales_invoices_closing_entry()
def before_cancel(self):
- self.check_pce_is_cancellable()
+ if self.status != "Failed":
+ self.check_pce_is_cancellable()
def on_cancel(self):
unconsolidate_pos_invoices(closing_entry=self)
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 051c7d87519..d206ec0a251 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -517,6 +517,7 @@ class SalesInvoice(SellingController):
self.update_billing_status_for_zero_amount_refdoc("Delivery Note")
self.update_billing_status_for_zero_amount_refdoc("Sales Order")
self.check_credit_limit()
+ self.check_overdue_billing_threshold()
if cint(self.is_pos) != 1 and not self.is_return:
self.update_against_document_in_jv()
@@ -778,6 +779,11 @@ class SalesInvoice(SellingController):
pos_invoice_doc = frappe.get_doc("POS Invoice", pos_invoice)
pos_invoice_doc.cancel()
+ def check_overdue_billing_threshold(self):
+ from erpnext.selling.doctype.customer.customer import check_overdue_billing_threshold
+
+ check_overdue_billing_threshold(self.customer, self.company)
+
@frappe.whitelist()
def set_missing_values(self, for_validate=False):
pos = self.set_pos_fields(for_validate)
diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule.json b/erpnext/accounts/doctype/shipping_rule/shipping_rule.json
index bec4b94b82d..e8070588564 100644
--- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.json
+++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.json
@@ -82,8 +82,7 @@
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
- "options": "Cost Center",
- "reqd": 1
+ "options": "Cost Center"
},
{
"fieldname": "shipping_amount_section",
@@ -141,19 +140,20 @@
"fieldtype": "Column Break"
},
{
- "fieldname": "project",
- "fieldtype": "Link",
- "label": "Project",
- "options": "Project"
+ "fieldname": "project",
+ "fieldtype": "Link",
+ "label": "Project",
+ "options": "Project"
}
],
"icon": "fa fa-truck",
"idx": 1,
"links": [],
- "modified": "2024-03-27 13:10:41.653314",
+ "modified": "2026-07-22 14:53:27.315435",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Shipping Rule",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -197,7 +197,8 @@
"write": 1
}
],
+ "row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "ASC",
"states": []
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py
index 7b226560668..e13ab9817e3 100644
--- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py
+++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py
@@ -36,18 +36,17 @@ class ShippingRule(Document):
from erpnext.accounts.doctype.shipping_rule_condition.shipping_rule_condition import (
ShippingRuleCondition,
)
- from erpnext.accounts.doctype.shipping_rule_country.shipping_rule_country import (
- ShippingRuleCountry,
- )
+ from erpnext.accounts.doctype.shipping_rule_country.shipping_rule_country import ShippingRuleCountry
account: DF.Link
calculate_based_on: DF.Literal["Fixed", "Net Total", "Net Weight"]
company: DF.Link
conditions: DF.Table[ShippingRuleCondition]
- cost_center: DF.Link
+ cost_center: DF.Link | None
countries: DF.Table[ShippingRuleCountry]
disabled: DF.Check
label: DF.Data
+ project: DF.Link | None
shipping_amount: DF.Currency
shipping_rule_type: DF.Literal["Selling", "Buying"]
# end: auto-generated types
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index 7b726604d8e..588c544b7cd 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -858,7 +858,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None):
doctype = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice"
- companies = frappe.get_all(
+ companies = frappe.get_list(
doctype, filters={"docstatus": 1, party_type.lower(): party}, distinct=1, fields=["company"]
)
diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js
index c94f8c245ec..4edd29a1494 100644
--- a/erpnext/accounts/report/accounts_payable/accounts_payable.js
+++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js
@@ -13,7 +13,7 @@ frappe.query_reports["Accounts Payable"] = {
},
{
fieldname: "report_date",
- label: __("Posting Date"),
+ label: __("Report Date"),
fieldtype: "Date",
default: frappe.datetime.get_today(),
},
@@ -69,10 +69,10 @@ frappe.query_reports["Accounts Payable"] = {
default: "Due Date",
},
{
- fieldname: "calculate_ageing_with",
- label: __("Calculate Ageing With"),
+ fieldname: "age_as_on",
+ label: __("Age as on"),
fieldtype: "Select",
- options: "Report Date\nToday Date",
+ options: "Report Date\nToday",
default: "Report Date",
},
{
diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js
index 3f603b62833..0b3bc077698 100644
--- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js
+++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js
@@ -12,7 +12,7 @@ frappe.query_reports["Accounts Payable Summary"] = {
},
{
fieldname: "report_date",
- label: __("Posting Date"),
+ label: __("Report Date"),
fieldtype: "Date",
default: frappe.datetime.get_today(),
},
@@ -24,10 +24,10 @@ frappe.query_reports["Accounts Payable Summary"] = {
default: "Due Date",
},
{
- fieldname: "calculate_ageing_with",
- label: __("Calculate Ageing With"),
+ fieldname: "age_as_on",
+ label: __("Age as on"),
fieldtype: "Select",
- options: "Report Date\nToday Date",
+ options: "Report Date\nToday",
default: "Report Date",
},
{
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js
index e7aa1f57036..e2b3cbb3e0c 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js
@@ -15,7 +15,7 @@ frappe.query_reports["Accounts Receivable"] = {
},
{
fieldname: "report_date",
- label: __("Posting Date"),
+ label: __("Report Date"),
fieldtype: "Date",
default: frappe.datetime.get_today(),
},
@@ -98,10 +98,10 @@ frappe.query_reports["Accounts Receivable"] = {
default: "Due Date",
},
{
- fieldname: "calculate_ageing_with",
- label: __("Calculate Ageing With"),
+ fieldname: "age_as_on",
+ label: __("Age as on"),
fieldtype: "Select",
- options: "Report Date\nToday Date",
+ options: "Report Date\nToday",
default: "Report Date",
},
{
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index d1132f8594f..6e3f7848c43 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -55,8 +55,7 @@ class ReceivablePayableReport:
self.filters.report_date = getdate(self.filters.report_date or nowdate())
self.age_as_on = (
getdate(nowdate())
- if "calculate_ageing_with" not in self.filters
- or self.filters.calculate_ageing_with == "Today Date"
+ if "age_as_on" not in self.filters or self.filters.age_as_on == "Today"
else self.filters.report_date
)
diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js
index 46585071174..c15ec8b0124 100644
--- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js
+++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js
@@ -12,7 +12,7 @@ frappe.query_reports["Accounts Receivable Summary"] = {
},
{
fieldname: "report_date",
- label: __("Posting Date"),
+ label: __("Report Date"),
fieldtype: "Date",
default: frappe.datetime.get_today(),
},
@@ -24,10 +24,10 @@ frappe.query_reports["Accounts Receivable Summary"] = {
default: "Due Date",
},
{
- fieldname: "calculate_ageing_with",
- label: __("Calculate Ageing With"),
+ fieldname: "age_as_on",
+ label: __("Age as on"),
fieldtype: "Select",
- options: "Report Date\nToday Date",
+ options: "Report Date\nToday",
default: "Report Date",
},
{
diff --git a/erpnext/accounts/report/cash_flow/cash_flow.py b/erpnext/accounts/report/cash_flow/cash_flow.py
index 4ed34ab8eb9..f90345abab0 100644
--- a/erpnext/accounts/report/cash_flow/cash_flow.py
+++ b/erpnext/accounts/report/cash_flow/cash_flow.py
@@ -80,6 +80,7 @@ def execute(filters=None):
"parent_section": None,
"indent": 0.0,
"section": cash_flow_section["section_header"],
+ "currency": company_currency,
}
)
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py
index 6dcc9cd2bc6..8d03e2afe37 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.py
+++ b/erpnext/accounts/report/gross_profit/gross_profit.py
@@ -227,6 +227,7 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
)
if total_base_amount
else 0,
+ "currency": filters.currency,
}
)
)
@@ -269,6 +270,7 @@ def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_
"buying_amount": total_buying_amount,
"gross_profit": total_gross_profit,
"gross_profit_percent": flt(gross_profit_percent, currency_precision),
+ "currency": filters.currency,
}
total_row = [total_row.get(col, None) for col in [*group_columns, "currency"]]
diff --git a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py
index dd518e838ad..f220b9a5308 100644
--- a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py
+++ b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py
@@ -14,7 +14,6 @@ def execute(filters=None):
conditions = get_columns(filters, "Purchase Order")
data = get_data(filters, conditions)
chart_data = get_chart_data(data, conditions, filters)
-
return conditions["columns"], data, None, chart_data
@@ -39,9 +38,15 @@ def get_chart_data(data, conditions, filters):
labels = [column.split(":")[0].replace(" (Amt)", "") for column in columns]
datapoints = [0] * len(labels)
+ group_by_col_idx = None
+ if filters.get("group_by"):
+ group_by_col_idx = conditions["columns"].index(conditions["grbc"][0])
+
for row in data:
- # If group by filter, don't add first row of group (it's already summed)
- if not row[start]:
+ # Skip the final grand-total row
+ if row[0] == f"'{_('Total')}'":
+ continue
+ if group_by_col_idx is not None and row[group_by_col_idx] == "":
continue
# Remove None values and compute only periodic data
row = [x if x else 0 for x in row[start:-2]]
@@ -60,4 +65,6 @@ def get_chart_data(data, conditions, filters):
"type": "line",
"lineOptions": {"regionFill": 1},
"fieldtype": "Currency",
+ "options": "currency",
+ "currency": conditions.get("company_currency"),
}
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index cad62820d03..88bb4b29a84 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -331,32 +331,51 @@ class BuyingController(SubcontractingController):
address_display_field, render_address(self.get(address_field), check_permissions=False)
)
+ def get_validated_purchase_expense_details(self, item_code):
+ fields = ("purchase_expense_account", "purchase_expense_contra_account")
+ details = get_purchase_expense_account(item_code, self.company)
+
+ for field in fields:
+ if not details.get(field):
+ details[field] = frappe.get_cached_value("Company", self.company, field)
+
+ for field in fields:
+ if not details.get(field):
+ frappe.throw(
+ _("Please set {0} in Company {1} or in the Item Defaults of Item {2}").format(
+ frappe.bold(_(frappe.unscrub(field))), self.company, item_code
+ )
+ )
+
+ return details
+
def set_gl_entry_for_purchase_expense(self, gl_entries):
+ if not cint(frappe.db.get_single_value("Accounts Settings", "book_stock_expense_gl_entries")):
+ return
+
if self.doctype == "Purchase Invoice" and not self.update_stock:
return
+ stock_items = self.get_stock_items()
+
for row in self.items:
- details = get_purchase_expense_account(row.item_code, self.company)
+ # A service item holds no stock value, so there is nothing to book against it - and it
+ # must not make the expense accounts mandatory either.
+ if row.item_code not in stock_items:
+ continue
- if not details.purchase_expense_account:
- details.purchase_expense_account = frappe.get_cached_value(
- "Company", self.company, "purchase_expense_account"
- )
-
- if not details.purchase_expense_account:
- return
-
- if not details.purchase_expense_contra_account:
- details.purchase_expense_contra_account = frappe.get_cached_value(
- "Company", self.company, "purchase_expense_contra_account"
- )
-
- if not details.purchase_expense_contra_account:
- frappe.throw(
- _("Please set Purchase Expense Contra Account in Company {0}").format(self.company)
- )
+ details = self.get_validated_purchase_expense_details(row.item_code)
+ if not details:
+ continue
amount = flt(row.valuation_rate * row.stock_qty, row.precision("base_amount"))
+ if row.landed_cost_voucher_amount:
+ amount -= flt(row.landed_cost_voucher_amount, row.precision("base_amount"))
+
+ if not amount:
+ # GL Entry rejects a row with neither a debit nor a credit.
+ continue
+
self.add_gl_entry(
gl_entries=gl_entries,
account=details.purchase_expense_account,
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 62384f3a85a..7a292b078b6 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -84,6 +84,11 @@ def stock_entry_row_requires_inspection(purpose, row):
class StockController(AccountsController):
+ #: Vouchers whose stock value change should also be booked to the Expenses Added To Stock
+ #: account pair (Stock Entry, Stock Reconciliation). Purchase Receipt books its own, against
+ #: the landed cost amount rather than the stock value difference.
+ book_expenses_added_to_stock = False
+
def validate(self):
super().validate()
@@ -858,10 +863,90 @@ class StockController(AccountsController):
).format(wh, self.company)
)
+ if self.book_expenses_added_to_stock:
+ self.append_expenses_added_to_stock_entries(gl_list, voucher_details, sle_map)
+
return process_gl_map(
gl_list, precision=precision, from_repost=frappe.flags.through_repost_item_valuation
)
+ def book_stock_expense_enabled(self):
+ if not hasattr(self, "_book_stock_expense_enabled"):
+ self._book_stock_expense_enabled = cint(
+ frappe.db.get_single_value("Accounts Settings", "book_stock_expense_gl_entries")
+ )
+
+ return self._book_stock_expense_enabled
+
+ def append_expenses_added_to_stock_entries(self, gl_list, voucher_details, sle_map):
+ if not self.book_stock_expense_enabled():
+ return
+
+ precision = self.get_debit_field_precision()
+
+ for item_row in voucher_details:
+ sle_list = sle_map.get(item_row.name)
+ if not sle_list:
+ continue
+
+ amount = flt(sum(flt(sle.stock_value_difference) for sle in sle_list), precision)
+ if not amount:
+ continue
+
+ item_code = item_row.get("item_code") or sle_list[0].item_code
+ self.append_expenses_added_to_stock_pair(gl_list, item_code, amount, item_row)
+
+ def append_expenses_added_to_stock_pair(self, gl_list, item_code, amount, item_row):
+ # A service item holds no stock value, so there is nothing to book against it - and it must
+ # not make the expense accounts mandatory either. A zero pair would be rejected by GL Entry
+ # anyway, which needs a debit or a credit on every row.
+ if not amount or not frappe.get_cached_value("Item", item_code, "is_stock_item"):
+ return
+
+ fields = ("expenses_added_to_stock_account", "expenses_added_to_stock_contra_account")
+ details = get_expenses_added_to_stock_accounts(item_code, self.company)
+
+ for field in fields:
+ if not details.get(field):
+ frappe.throw(
+ _("Please set {0} in Company {1} or in the Item Defaults of Item {2}").format(
+ frappe.bold(_(frappe.unscrub(field))), self.company, item_code
+ )
+ )
+
+ cost_center = item_row.get("cost_center") or frappe.get_cached_value(
+ "Company", self.company, "cost_center"
+ )
+ remarks = _("Expenses Added To Stock for Item {0}").format(item_code)
+ common_args = {
+ "cost_center": cost_center,
+ "project": item_row.get("project") or self.get("project"),
+ "remarks": remarks,
+ }
+
+ gl_list.append(
+ self.get_gl_dict(
+ {
+ "account": details.expenses_added_to_stock_account,
+ "against": details.expenses_added_to_stock_contra_account,
+ "debit": amount,
+ **common_args,
+ },
+ item=item_row,
+ )
+ )
+ gl_list.append(
+ self.get_gl_dict(
+ {
+ "account": details.expenses_added_to_stock_contra_account,
+ "against": details.expenses_added_to_stock_account,
+ "debit": -1 * amount,
+ **common_args,
+ },
+ item=item_row,
+ )
+ )
+
def get_debit_field_precision(self):
if not frappe.flags.debit_field_precision:
frappe.flags.debit_field_precision = frappe.get_precision("GL Entry", "debit_in_account_currency")
@@ -1371,8 +1456,9 @@ class StockController(AccountsController):
if outstanding > 0:
reservations[key].append(row)
+ precision = frappe.get_precision("Serial and Batch Entry", "qty")
for (batch_no, warehouse), reserved_qty in outstanding_qty.items():
- if flt(reserved_qty, 6) <= 0:
+ if flt(reserved_qty, precision) <= 0:
continue
batch_qty = get_batch_qty(
@@ -1383,7 +1469,7 @@ class StockController(AccountsController):
consider_negative_batches=True,
)
- if flt(batch_qty, 6) >= flt(reserved_qty, 6):
+ if flt(batch_qty, precision) >= flt(reserved_qty, precision):
continue
vouchers = ", ".join(
@@ -2567,3 +2653,31 @@ def get_item_wise_inventory_account_map(rows, company):
)
return inventory_map
+
+
+@frappe.request_cache
+def get_expenses_added_to_stock_accounts(item_code, company):
+ """Resolves the Expenses Added To Stock account pair for an item, falling back through
+ Item Defaults -> Item Group -> Brand -> Company."""
+ from erpnext.stock.doctype.item.item import get_item_defaults
+
+ fields = ["expenses_added_to_stock_account", "expenses_added_to_stock_contra_account"]
+ defaults = get_item_defaults(item_code, company)
+
+ details = frappe._dict({field: defaults.get(field) for field in fields})
+
+ if not details.expenses_added_to_stock_account:
+ details = frappe.db.get_value(
+ "Item Default", {"parent": defaults.item_group, "company": company}, fields, as_dict=1
+ ) or frappe._dict({})
+
+ if not details.expenses_added_to_stock_account and defaults.get("brand"):
+ details = frappe.db.get_value(
+ "Item Default", {"parent": defaults.brand, "company": company}, fields, as_dict=1
+ ) or frappe._dict({})
+
+ for field in fields:
+ if not details.get(field):
+ details[field] = frappe.get_cached_value("Company", company, field)
+
+ return details
diff --git a/erpnext/controllers/trends.py b/erpnext/controllers/trends.py
index 28ff84c83fd..76a809c1b8a 100644
--- a/erpnext/controllers/trends.py
+++ b/erpnext/controllers/trends.py
@@ -6,6 +6,7 @@ import frappe
from frappe import _
from frappe.utils import DateTimeLikeObject, getdate, today
+import erpnext
from erpnext.accounts.utils import get_fiscal_year
@@ -42,6 +43,9 @@ def get_columns(filters, trans):
"addl_tables": based_on_details["addl_tables"],
"addl_tables_relational_cond": based_on_details.get("addl_tables_relational_cond", ""),
}
+ conditions["company_currency"] = (
+ erpnext.get_company_currency(filters.get("company")) if filters.get("company") else None
+ )
return conditions
@@ -206,7 +210,7 @@ def get_data(filters, conditions):
data.append(des)
- total_row = calculate_total_row(data1, conditions["columns"])
+ total_row = calculate_total_row(data1, conditions["columns"], conditions.get("company_currency"))
data.append(total_row)
else:
data = frappe.db.sql(
@@ -231,19 +235,24 @@ def get_data(filters, conditions):
as_list=1,
)
- total_row = calculate_total_row(data, conditions["columns"])
+ total_row = calculate_total_row(data, conditions["columns"], conditions.get("company_currency"))
data.append(total_row)
return data
-def calculate_total_row(data, columns):
+def calculate_total_row(data, columns, company_currency=None):
def wrap_in_quotes(label):
return f"'{label}'"
total_values = {}
+ currency_col_idx = None
for i, col in enumerate(columns):
- if "Float" in col or "Currency/currency" in col:
+ # based-on and group-by columns are dicts, periodic and total columns are strings
+ if isinstance(col, dict):
+ if col.get("fieldtype") == "Link" and col.get("options") == "Currency":
+ currency_col_idx = i
+ elif "Float" in col or "Currency/currency" in col:
total_values[i] = 0
for row in data:
@@ -254,6 +263,9 @@ def calculate_total_row(data, columns):
for i in range(1, len(columns)):
total_row.append(total_values.get(i, None))
+ if currency_col_idx is not None:
+ total_row[currency_col_idx] = company_currency
+
return total_row
diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py
index 7368a800003..c76b76b856d 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.py
+++ b/erpnext/crm/doctype/opportunity/opportunity.py
@@ -279,13 +279,17 @@ class Opportunity(TransactionBase, CRMNote):
self.save()
else:
- frappe.throw(_("Cannot declare as lost, because Quotation has been made."))
+ frappe.throw(_("Cannot declare as Lost because an active Quotation exists."))
def has_active_quotation(self):
if not self.get("items", []):
return frappe.get_all(
"Quotation",
- {"opportunity": self.name, "status": ("not in", ["Lost", "Closed"]), "docstatus": 1},
+ {
+ "opportunity": self.name,
+ "status": ("not in", ["Lost", "Cancelled", "Expired"]),
+ "docstatus": 1,
+ },
"name",
)
else:
@@ -294,14 +298,20 @@ class Opportunity(TransactionBase, CRMNote):
select q.name
from `tabQuotation` q, `tabQuotation Item` qi
where q.name = qi.parent and q.docstatus=1 and qi.prevdoc_docname =%s
- and q.status not in ('Lost', 'Closed')""",
+ and q.status not in ('Lost', 'Cancelled', 'Expired')""",
self.name,
)
def has_ordered_quotation(self):
if not self.get("items", []):
return frappe.get_all(
- "Quotation", {"opportunity": self.name, "status": "Ordered", "docstatus": 1}, "name"
+ "Quotation",
+ {
+ "opportunity": self.name,
+ "status": ("in", ["Ordered", "Partially Ordered"]),
+ "docstatus": 1,
+ },
+ "name",
)
else:
return frappe.db.sql(
@@ -309,7 +319,7 @@ class Opportunity(TransactionBase, CRMNote):
select q.name
from `tabQuotation` q, `tabQuotation Item` qi
where q.name = qi.parent and q.docstatus=1 and qi.prevdoc_docname =%s
- and q.status = 'Ordered'""",
+ and q.status in ('Ordered', 'Partially Ordered')""",
self.name,
)
diff --git a/erpnext/locale/main.pot b/erpnext/locale/main.pot
index c1d1045f1af..5d792f1b847 100644
--- a/erpnext/locale/main.pot
+++ b/erpnext/locale/main.pot
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: ERPNext VERSION\n"
"Report-Msgid-Bugs-To: hello@frappe.io\n"
-"POT-Creation-Date: 2026-07-19 10:04+0000\n"
-"PO-Revision-Date: 2026-07-19 10:04+0000\n"
+"POT-Creation-Date: 2026-07-26 10:12+0000\n"
+"PO-Revision-Date: 2026-07-26 10:12+0000\n"
"Last-Translator: hello@frappe.io\n"
"Language-Team: hello@frappe.io\n"
"MIME-Version: 1.0\n"
@@ -144,6 +144,10 @@ msgstr ""
msgid "% Complete Method"
msgstr ""
+#: erpnext/projects/doctype/project/project.py:226
+msgid "% Complete must be between 0 and 100"
+msgstr ""
+
#. Label of the percent_complete (Percent) field in DocType 'Project'
#: erpnext/projects/doctype/project/project.json
msgid "% Completed"
@@ -337,6 +341,10 @@ msgstr ""
msgid "'Update Stock' cannot be checked for fixed asset sale"
msgstr ""
+#: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py:112
+msgid "'Verification Link Expiry Duration' must be between 15 to 60 minutes."
+msgstr ""
+
#: erpnext/accounts/doctype/bank_account/bank_account.py:79
msgid "'{0}' account is already used by {1}. Use another account."
msgstr ""
@@ -620,7 +628,7 @@ msgstr ""
msgid "Cannot create asset.
You're trying to create {0} asset(s) from {2} {3}.
However, only {1} item(s) were purchased and {4} asset(s) already exist against {5}."
msgstr ""
-#: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py:59
+#: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py:69
msgid "From Time cannot be later than To Time for {0}"
msgstr ""
@@ -939,11 +947,11 @@ msgstr ""
msgid "Your Shortcuts"
msgstr ""
-#: erpnext/accounts/doctype/payment_request/payment_request.py:1137
+#: erpnext/accounts/doctype/payment_request/payment_request.py:1138
msgid "Grand Total: {0}"
msgstr ""
-#: erpnext/accounts/doctype/payment_request/payment_request.py:1138
+#: erpnext/accounts/doctype/payment_request/payment_request.py:1139
msgid "Outstanding Amount: {0}"
msgstr ""
@@ -998,7 +1006,7 @@ msgstr ""
msgid "A - C"
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:361
+#: erpnext/selling/doctype/customer/customer.py:365
msgid "A Customer Group exists with same name please change the Customer name or rename the Customer Group"
msgstr ""
@@ -1102,6 +1110,10 @@ msgstr ""
msgid "A third party distributor / dealer / commission agent / affiliate / reseller who sells the companies products for a commission."
msgstr ""
+#: erpnext/crm/doctype/appointment/appointment.py:70
+msgid "A verified appointment cannot be moved back to 'Unverified' status."
+msgstr ""
+
#. Option for the 'Blood Group' (Select) field in DocType 'Employee'
#: erpnext/setup/doctype/employee/employee.json
msgid "A+"
@@ -1245,7 +1257,7 @@ msgid "Accepted Qty in Stock UOM"
msgstr ""
#. Label of the qty (Float) field in DocType 'Purchase Receipt Item'
-#: erpnext/public/js/controllers/transaction.js:2886
+#: erpnext/public/js/controllers/transaction.js:2870
#: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
msgid "Accepted Quantity"
msgstr ""
@@ -1404,7 +1416,7 @@ msgstr ""
msgid "Account Manager"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1057
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1063
#: erpnext/controllers/accounts_controller.py:2423
msgid "Account Missing"
msgstr ""
@@ -1423,7 +1435,7 @@ msgstr ""
msgid "Account Name"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:373
+#: erpnext/accounts/doctype/account/account.py:404
msgid "Account Not Found"
msgstr ""
@@ -1436,7 +1448,7 @@ msgstr ""
msgid "Account Number"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:359
+#: erpnext/accounts/doctype/account/account.py:390
msgid "Account Number {0} already used in account {1}"
msgstr ""
@@ -1475,7 +1487,7 @@ msgstr ""
#. Label of the account_type (Select) field in DocType 'Payment Ledger Entry'
#. Label of the account_type (Select) field in DocType 'Party Type'
#: erpnext/accounts/doctype/account/account.json
-#: erpnext/accounts/doctype/account/account.py:206
+#: erpnext/accounts/doctype/account/account.py:207
#: erpnext/accounts/doctype/account/account_tree.js:154
#: erpnext/accounts/doctype/bank_account/bank_account.json
#: erpnext/accounts/doctype/bank_account_type/bank_account_type.json
@@ -1491,11 +1503,11 @@ msgstr ""
msgid "Account Value"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:328
+#: erpnext/accounts/doctype/account/account.py:359
msgid "Account balance already in Credit, you are not allowed to set 'Balance Must Be' as 'Debit'"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:322
+#: erpnext/accounts/doctype/account/account.py:353
msgid "Account balance already in Debit, you are not allowed to set 'Balance Must Be' as 'Credit'"
msgstr ""
@@ -1562,24 +1574,24 @@ msgstr ""
msgid "Account where the cost of this item will be debited on purchase"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:427
+#: erpnext/accounts/doctype/account/account.py:458
msgid "Account with child nodes cannot be converted to ledger"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:279
+#: erpnext/accounts/doctype/account/account.py:310
msgid "Account with child nodes cannot be set as ledger"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:438
+#: erpnext/accounts/doctype/account/account.py:469
msgid "Account with existing transaction can not be converted to group."
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:467
+#: erpnext/accounts/doctype/account/account.py:498
msgid "Account with existing transaction can not be deleted"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:273
-#: erpnext/accounts/doctype/account/account.py:429
+#: erpnext/accounts/doctype/account/account.py:304
+#: erpnext/accounts/doctype/account/account.py:460
msgid "Account with existing transaction cannot be converted to ledger"
msgstr ""
@@ -1587,11 +1599,11 @@ msgstr ""
msgid "Account {0} added multiple times"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:291
+#: erpnext/accounts/doctype/account/account.py:322
msgid "Account {0} cannot be converted to Group as it is already set as {1} for {2}."
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:288
+#: erpnext/accounts/doctype/account/account.py:319
msgid "Account {0} cannot be disabled as it is already set as {1} for {2}."
msgstr ""
@@ -1603,7 +1615,7 @@ msgstr ""
msgid "Account {0} does not belong to company: {1}"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:590
+#: erpnext/accounts/doctype/account/account.py:621
msgid "Account {0} does not exist"
msgstr ""
@@ -1623,11 +1635,11 @@ msgstr ""
msgid "Account {0} doesn't belong to Company {1}"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:545
+#: erpnext/accounts/doctype/account/account.py:576
msgid "Account {0} exists in parent company {1}."
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:411
+#: erpnext/accounts/doctype/account/account.py:442
msgid "Account {0} is added in the child company {1}"
msgstr ""
@@ -1647,19 +1659,19 @@ msgstr ""
msgid "Account {0} should be of type Expense"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:152
+#: erpnext/accounts/doctype/account/account.py:153
msgid "Account {0}: Parent account {1} can not be a ledger"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:158
+#: erpnext/accounts/doctype/account/account.py:159
msgid "Account {0}: Parent account {1} does not belong to company: {2}"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:146
+#: erpnext/accounts/doctype/account/account.py:147
msgid "Account {0}: Parent account {1} does not exist"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:149
+#: erpnext/accounts/doctype/account/account.py:150
msgid "Account {0}: You can not assign itself as parent account"
msgstr ""
@@ -2003,7 +2015,7 @@ msgstr ""
#: erpnext/assets/doctype/asset/asset.js:198
#: erpnext/assets/doctype/asset_repair/asset_repair.js:101
#: erpnext/buying/doctype/supplier/supplier.js:123
-#: erpnext/public/js/controllers/stock_controller.js:88
+#: erpnext/public/js/controllers/stock_controller.js:118
#: erpnext/public/js/utils/ledger_preview.js:8
#: erpnext/selling/doctype/customer/customer.js:173
#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js:51
@@ -2281,6 +2293,12 @@ msgstr ""
msgid "Action Initialised"
msgstr ""
+#. Label of the action_for_expired_unverified_appointments (Select) field in
+#. DocType 'Appointment Booking Settings'
+#: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json
+msgid "Action for Expired Unverified Appointments"
+msgstr ""
+
#. Label of the action_if_accumulated_monthly_budget_exceeded (Select) field in
#. DocType 'Budget'
#: erpnext/accounts/doctype/budget/budget.json
@@ -2544,8 +2562,9 @@ msgstr ""
#: erpnext/stock/doctype/bin/bin.json
#: erpnext/stock/doctype/material_request_item/material_request_item.json
#: erpnext/stock/doctype/packed_item/packed_item.json
+#: erpnext/stock/page/stock_balance/stock_balance.js:63
#: erpnext/stock/report/product_bundle_balance/product_bundle_balance.py:96
-#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:141
+#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:142
msgid "Actual Qty"
msgstr ""
@@ -2616,10 +2635,6 @@ msgstr ""
msgid "Actual Time in Hours (via Timesheet)"
msgstr ""
-#: erpnext/stock/page/stock_balance/stock_balance.js:55
-msgid "Actual qty in stock"
-msgstr ""
-
#: erpnext/accounts/doctype/payment_entry/payment_entry.js:1538
#: erpnext/public/js/controllers/accounts.js:194
msgid "Actual type tax cannot be included in Item rate in row {0}"
@@ -3289,7 +3304,7 @@ msgstr ""
msgid "Address and Contacts"
msgstr ""
-#: erpnext/accounts/custom/address.py:33
+#: erpnext/accounts/custom/address.py:35
msgid "Address needs to be linked to a Company. Please add a row for Company in the Links table."
msgstr ""
@@ -3336,6 +3351,10 @@ msgstr ""
msgid "Advance Amount"
msgstr ""
+#: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py:93
+msgid "Advance Booking Days is mandatory for Appointment Scheduling."
+msgstr ""
+
#. Label of the advance_paid (Currency) field in DocType 'Sales Order'
#: erpnext/selling/doctype/sales_order/sales_order.json
msgid "Advance Paid"
@@ -3500,7 +3519,7 @@ msgstr ""
msgid "Against Blanket Order"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1150
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1156
msgid "Against Customer Order {0}"
msgstr ""
@@ -3697,12 +3716,6 @@ msgstr ""
msgid "Agent Busy Message"
msgstr ""
-#. Label of the agent_detail_section (Section Break) field in DocType
-#. 'Appointment Booking Settings'
-#: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json
-msgid "Agent Details"
-msgstr ""
-
#. Label of the agent_group (Link) field in DocType 'Incoming Call Handling
#. Schedule'
#: erpnext/telephony/doctype/incoming_call_handling_schedule/incoming_call_handling_schedule.json
@@ -3938,19 +3951,19 @@ msgstr ""
msgid "All items have already been transferred for this Work Order."
msgstr ""
-#: erpnext/public/js/controllers/transaction.js:3009
+#: erpnext/public/js/controllers/transaction.js:2993
msgid "All items in this document already have a linked Quality Inspection."
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1286
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1292
msgid "All items must be linked to a Sales Order or Subcontracting Inward Order for this Sales Invoice."
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1297
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1303
msgid "All linked Sales Orders must be subcontracted."
msgstr ""
-#: erpnext/stock/doctype/pick_list/pick_list.py:1605
+#: erpnext/stock/doctype/pick_list/pick_list.py:1608
msgid "All picked items have already been transferred against this Pick List"
msgstr ""
@@ -4090,7 +4103,7 @@ msgstr ""
#. Label of the allow_account_creation_against_child_company (Check) field in
#. DocType 'Company'
-#: erpnext/accounts/doctype/account/account.py:543
+#: erpnext/accounts/doctype/account/account.py:574
#: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:68
#: erpnext/setup/doctype/company/company.json
msgid "Allow Account Creation Against Child Company"
@@ -4793,7 +4806,7 @@ msgstr ""
#: erpnext/manufacturing/doctype/bom_item/bom_item.json
#: erpnext/manufacturing/doctype/work_order_additional_item/work_order_additional_item.json
#: erpnext/manufacturing/doctype/work_order_item/work_order_item.json
-#: erpnext/public/js/controllers/transaction.js:558
+#: erpnext/public/js/controllers/transaction.js:569
#: erpnext/selling/doctype/quotation/quotation.js:315
#: erpnext/selling/doctype/quotation_item/quotation_item.json
#: erpnext/selling/doctype/sales_order_item/sales_order_item.json
@@ -5013,6 +5026,10 @@ msgstr ""
msgid "An Item Group is a way to classify items based on types."
msgstr ""
+#: erpnext/crm/doctype/appointment/appointment.py:74
+msgid "An appointment booked through the portal can only be opened via email verification."
+msgstr ""
+
#. Description of the 'Notify by email on creation of automatic Material
#. Request' (Check) field in DocType 'Stock Settings'
#: erpnext/stock/doctype/stock_settings/stock_settings.json
@@ -5085,7 +5102,7 @@ msgstr ""
msgid "Another Cost Center Allocation record {0} applicable from {1}, hence this allocation will be applicable upto {2}"
msgstr ""
-#: erpnext/accounts/doctype/payment_request/payment_request.py:886
+#: erpnext/accounts/doctype/payment_request/payment_request.py:887
msgid "Another Payment Request is already processed"
msgstr ""
@@ -5405,6 +5422,12 @@ msgstr ""
msgid "Appointment"
msgstr ""
+#. Label of the success_details (Section Break) field in DocType 'Appointment
+#. Booking Settings'
+#: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json
+msgid "Appointment Booking Portal Settings"
+msgstr ""
+
#. Name of a DocType
#. Label of a Workspace Sidebar Item
#: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json
@@ -5417,10 +5440,14 @@ msgstr ""
msgid "Appointment Booking Slots"
msgstr ""
-#: erpnext/crm/doctype/appointment/appointment.py:95
+#: erpnext/crm/doctype/appointment/appointment.py:181
msgid "Appointment Confirmation"
msgstr ""
+#: erpnext/crm/doctype/appointment/appointment.py:189
+msgid "Appointment Confirmed"
+msgstr ""
+
#: erpnext/www/book_appointment/index.js:237
msgid "Appointment Created Successfully"
msgstr ""
@@ -5437,21 +5464,55 @@ msgstr ""
msgid "Appointment Duration (In Minutes)"
msgstr ""
-#: erpnext/www/book_appointment/index.py:23
-msgid "Appointment Scheduling Disabled"
+#. Label of the agent_detail_section (Section Break) field in DocType
+#. 'Appointment Booking Settings'
+#: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json
+msgid "Appointment Scheduling"
msgstr ""
#: erpnext/www/book_appointment/index.py:24
+msgid "Appointment Scheduling Disabled"
+msgstr ""
+
+#: erpnext/www/book_appointment/index.py:25
msgid "Appointment Scheduling has been disabled for this site"
msgstr ""
+#: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py:101
+msgid "Appointment Scheduling needs to be enabled for Appointment Booking through portal."
+msgstr ""
+
#. Label of the appointment_with (Link) field in DocType 'Appointment'
#: erpnext/crm/doctype/appointment/appointment.json
msgid "Appointment With"
msgstr ""
-#: erpnext/crm/doctype/appointment/appointment.py:101
-msgid "Appointment was created. But no lead was found. Please check the email to confirm"
+#: erpnext/crm/doctype/appointment/appointment.py:86
+msgid "Appointment can only be scheduled up to {0} day(s) in advance."
+msgstr ""
+
+#: erpnext/crm/doctype/appointment/appointment.py:79
+msgid "Appointment cannot be scheduled for a past time."
+msgstr ""
+
+#: erpnext/crm/doctype/appointment/appointment.py:98
+msgid "Appointment cannot be scheduled on a holiday."
+msgstr ""
+
+#: erpnext/www/book_appointment/verify/index.py:28
+msgid "Appointment has been closed. Please book the appointment again."
+msgstr ""
+
+#: erpnext/www/book_appointment/verify/index.py:33
+msgid "Appointment is already verified."
+msgstr ""
+
+#: erpnext/crm/doctype/appointment/appointment.py:116
+msgid "Appointment must be scheduled within the available slot timings."
+msgstr ""
+
+#: erpnext/crm/doctype/appointment/appointment.py:66
+msgid "Appointments created manually cannot have 'Unverified' status."
msgstr ""
#. Label of the approving_role (Link) field in DocType 'Authorization Rule'
@@ -6040,7 +6101,7 @@ msgstr ""
msgid "Asset restored after Asset Capitalization {0} was cancelled"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1569
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1575
msgid "Asset returned"
msgstr ""
@@ -6052,8 +6113,8 @@ msgstr ""
msgid "Asset scrapped via Journal Entry {0}"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1569
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1572
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1575
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1578
msgid "Asset sold"
msgstr ""
@@ -6217,7 +6278,7 @@ msgid "At least one item should be entered with negative quantity in return docu
msgstr ""
#: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:531
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:567
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:568
msgid "At least one mode of payment is required for POS invoice."
msgstr ""
@@ -6504,7 +6565,7 @@ msgstr ""
msgid "Auto Reposting of Incorrect Valuation"
msgstr ""
-#: erpnext/accounts/doctype/accounts_settings/accounts_settings.py:201
+#: erpnext/accounts/doctype/accounts_settings/accounts_settings.py:207
msgid "Auto Tax Settings Error"
msgstr ""
@@ -6916,7 +6977,7 @@ msgstr ""
#: erpnext/selling/doctype/sales_order/sales_order.js:1458
#: erpnext/stock/doctype/material_request/material_request.js:352
#: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
-#: erpnext/stock/doctype/stock_entry/stock_entry.js:810
+#: erpnext/stock/doctype/stock_entry/stock_entry.js:786
#: erpnext/stock/report/bom_search/bom_search.py:38
#: erpnext/subcontracting/doctype/subcontracting_inward_order_item/subcontracting_inward_order_item.json
#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:525
@@ -7188,7 +7249,7 @@ msgid "BOM and Production"
msgstr ""
#: erpnext/stock/doctype/material_request/material_request.js:387
-#: erpnext/stock/doctype/stock_entry/stock_entry.js:862
+#: erpnext/stock/doctype/stock_entry/stock_entry.js:838
msgid "BOM does not contain any stock item"
msgstr ""
@@ -8069,7 +8130,7 @@ msgstr ""
#: erpnext/manufacturing/doctype/job_card/job_card.json
#: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js:89
#: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py:115
-#: erpnext/public/js/controllers/transaction.js:2912
+#: erpnext/public/js/controllers/transaction.js:2896
#: erpnext/public/js/utils/barcode_scanner.js:286
#: erpnext/public/js/utils/serial_no_batch_selector.js:449
#: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
@@ -8291,7 +8352,7 @@ msgstr ""
#: erpnext/manufacturing/doctype/bom/bom.py:1373
#: erpnext/manufacturing/workspace/manufacturing/manufacturing.json
#: erpnext/stock/doctype/material_request/material_request.js:142
-#: erpnext/stock/doctype/stock_entry/stock_entry.js:796
+#: erpnext/stock/doctype/stock_entry/stock_entry.js:772
#: erpnext/workspace_sidebar/subcontracting.json
msgid "Bill of Materials"
msgstr ""
@@ -8508,7 +8569,7 @@ msgid "Bin"
msgstr ""
#: erpnext/stock/doctype/bin/bin.js:16
-msgid "Bin Qty Recalculated"
+msgid "Bin Values Recalculated"
msgstr ""
#. Label of the bio (Text Editor) field in DocType 'Employee'
@@ -8640,6 +8701,12 @@ msgstr ""
msgid "Block Supplier"
msgstr ""
+#. Description of the 'Restrict Customer Over Billing' (Check) field in DocType
+#. 'Accounts Settings'
+#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+msgid "Block a new Sales Invoice when the customer's overdue amount exceeds the Overdue Limit set on the customer."
+msgstr ""
+
#. Description of the 'Is Frozen' (Check) field in DocType 'Customer'
#: erpnext/selling/doctype/customer/customer.json
msgid "Blocks all further accounting entries on this customer's account. Only users with the frozen-entries role can override.\n"
@@ -9479,7 +9546,7 @@ msgstr ""
msgid "Can be approved by {0}"
msgstr ""
-#: erpnext/manufacturing/doctype/work_order/work_order.py:2845
+#: erpnext/manufacturing/doctype/work_order/work_order.py:2852
msgid "Can not close Work Order. Since {0} Job Cards are in Work In Progress state."
msgstr ""
@@ -9682,15 +9749,15 @@ msgstr ""
msgid "Cannot convert Task to non-group because the following child Tasks exist: {0}."
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:440
+#: erpnext/accounts/doctype/account/account.py:471
msgid "Cannot convert to Group because Account Type is selected."
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:276
+#: erpnext/accounts/doctype/account/account.py:307
msgid "Cannot covert to Group because Account Type is selected."
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2846
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2852
msgid "Cannot create Intercompany {0}. All items in the source {1} have already been fully invoiced. Please check the existing linked {2}s."
msgstr ""
@@ -9832,7 +9899,7 @@ msgstr ""
msgid "Cannot retrieve link token. Check Error Log for more information"
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:374
+#: erpnext/selling/doctype/customer/customer.py:378
msgid "Cannot select a Group type Customer Group. Please select a non-group Customer Group."
msgstr ""
@@ -10253,7 +10320,7 @@ msgstr ""
msgid "Change in Stock Value"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1076
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1082
msgid "Change the account type to Receivable or select a different account."
msgstr ""
@@ -10486,7 +10553,7 @@ msgstr ""
#. Label of the reference_date (Date) field in DocType 'Payment Entry'
#: erpnext/accounts/doctype/payment_entry/payment_entry.json
-#: erpnext/public/js/controllers/transaction.js:2823
+#: erpnext/public/js/controllers/transaction.js:2807
msgid "Cheque/Reference Date"
msgstr ""
@@ -10544,7 +10611,7 @@ msgstr ""
#. Label of the child_row_reference (Data) field in DocType 'Quality
#. Inspection'
-#: erpnext/public/js/controllers/transaction.js:2918
+#: erpnext/public/js/controllers/transaction.js:2902
#: erpnext/stock/doctype/quality_inspection/quality_inspection.json
msgid "Child Row Reference"
msgstr ""
@@ -10747,7 +10814,7 @@ msgstr ""
msgid "Closed Documents"
msgstr ""
-#: erpnext/manufacturing/doctype/work_order/work_order.py:2768
+#: erpnext/manufacturing/doctype/work_order/work_order.py:2775
msgid "Closed Work Order can not be stopped or Re-opened"
msgstr ""
@@ -11672,7 +11739,7 @@ msgstr ""
msgid "Company Name cannot be Company"
msgstr ""
-#: erpnext/accounts/custom/address.py:36
+#: erpnext/accounts/custom/address.py:38
msgid "Company Not Linked"
msgstr ""
@@ -11693,12 +11760,12 @@ msgstr ""
msgid "Company and Posting Date is mandatory"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2637
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2643
msgid "Company currencies of both the companies should match for Inter Company Transactions."
msgstr ""
#: erpnext/stock/doctype/material_request/material_request.js:381
-#: erpnext/stock/doctype/stock_entry/stock_entry.js:856
+#: erpnext/stock/doctype/stock_entry/stock_entry.js:832
msgid "Company field is required"
msgstr ""
@@ -11763,7 +11830,7 @@ msgstr ""
msgid "Company {0} added multiple times"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:509
+#: erpnext/accounts/doctype/account/account.py:540
#: erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.py:1309
msgid "Company {0} does not exist"
msgstr ""
@@ -12149,7 +12216,7 @@ msgstr ""
#. Log'
#: erpnext/accounts/doctype/pos_invoice/pos_invoice.json
#: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:580
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:581
msgid "Consolidated Sales Invoice"
msgstr ""
@@ -13443,7 +13510,7 @@ msgstr ""
msgid "Create Payment Entry for Consolidated POS Invoices."
msgstr ""
-#: erpnext/public/js/controllers/transaction.js:565
+#: erpnext/public/js/controllers/transaction.js:577
msgid "Create Payment Request"
msgstr ""
@@ -13671,7 +13738,7 @@ msgstr ""
msgid "Create a variant with the template image."
msgstr ""
-#: erpnext/stock/stock_ledger.py:2037
+#: erpnext/stock/stock_ledger.py:2052
msgid "Create an incoming stock transaction for the Item."
msgstr ""
@@ -13705,6 +13772,11 @@ msgstr ""
msgid "Created By Migration"
msgstr ""
+#. Label of the created_through_portal (Check) field in DocType 'Appointment'
+#: erpnext/crm/doctype/appointment/appointment.json
+msgid "Created through Portal"
+msgstr ""
+
#: erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py:251
msgid "Created {0} scorecards for {1} between:"
msgstr ""
@@ -13851,6 +13923,13 @@ msgstr ""
msgid "Credit"
msgstr ""
+#. Label of the credit_limits (Table) field in DocType 'Customer'
+#. Label of the credit_limits (Table) field in DocType 'Customer Group'
+#: erpnext/selling/doctype/customer/customer.json
+#: erpnext/setup/doctype/customer_group/customer_group.json
+msgid "Credit & Overdue Limits"
+msgstr ""
+
#: erpnext/accounts/report/general_ledger/general_ledger.py:744
msgid "Credit (Transaction)"
msgstr ""
@@ -13920,23 +13999,19 @@ msgstr ""
msgid "Credit Days"
msgstr ""
-#. Label of the credit_limits (Table) field in DocType 'Customer'
#. Label of the credit_limit (Currency) field in DocType 'Customer Credit
#. Limit'
#. Label of the credit_limit (Currency) field in DocType 'Company'
-#. Label of the credit_limits (Table) field in DocType 'Customer Group'
#. Label of the section_credit_limit (Section Break) field in DocType 'Supplier
#. Group'
-#: erpnext/selling/doctype/customer/customer.json
#: erpnext/selling/doctype/customer_credit_limit/customer_credit_limit.json
#: erpnext/selling/report/customer_credit_balance/customer_credit_balance.py:65
#: erpnext/setup/doctype/company/company.json
-#: erpnext/setup/doctype/customer_group/customer_group.json
#: erpnext/setup/doctype/supplier_group/supplier_group.json
msgid "Credit Limit"
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:650
+#: erpnext/selling/doctype/customer/customer.py:657
msgid "Credit Limit Crossed"
msgstr ""
@@ -14016,16 +14091,16 @@ msgstr ""
msgid "Credit in Company Currency"
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:616
-#: erpnext/selling/doctype/customer/customer.py:671
+#: erpnext/selling/doctype/customer/customer.py:623
+#: erpnext/selling/doctype/customer/customer.py:678
msgid "Credit limit has been crossed for customer {0} ({1}/{2})"
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:401
+#: erpnext/selling/doctype/customer/customer.py:405
msgid "Credit limit is already defined for the Company {0}"
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:670
+#: erpnext/selling/doctype/customer/customer.py:677
msgid "Credit limit reached for customer {0}"
msgstr ""
@@ -14085,7 +14160,7 @@ msgstr ""
msgid "Criteria weights must add up to 100%"
msgstr ""
-#: erpnext/accounts/doctype/accounts_settings/accounts_settings.py:188
+#: erpnext/accounts/doctype/accounts_settings/accounts_settings.py:194
msgid "Cron Interval should be between 1 and 59 Min"
msgstr ""
@@ -14204,7 +14279,7 @@ msgstr ""
msgid "Currency and Price List"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:346
+#: erpnext/accounts/doctype/account/account.py:377
msgid "Currency can not be changed after making entries using some other currency"
msgstr ""
@@ -15054,7 +15129,7 @@ msgstr ""
msgid "Customer required for 'Customerwise Discount'"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1190
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1196
#: erpnext/selling/doctype/sales_order/sales_order.py:436
#: erpnext/stock/doctype/delivery_note/delivery_note.py:407
msgid "Customer {0} does not belong to project {1}"
@@ -15168,7 +15243,7 @@ msgstr ""
msgid "DFS"
msgstr ""
-#: erpnext/projects/doctype/project/project.py:710
+#: erpnext/projects/doctype/project/project.py:712
msgid "Daily Project Summary for {0}"
msgstr ""
@@ -15509,13 +15584,13 @@ msgstr ""
#. Label of the debit_to (Link) field in DocType 'Sales Invoice'
#: erpnext/accounts/doctype/pos_invoice/pos_invoice.json
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.json
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1061
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1072
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1067
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1078
#: erpnext/controllers/accounts_controller.py:2403
msgid "Debit To"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1057
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1063
msgid "Debit To is required"
msgstr ""
@@ -16272,6 +16347,12 @@ msgstr ""
msgid "Delete Leads and Addresses"
msgstr ""
+#. Option for the 'Action for Expired Unverified Appointments' (Select) field
+#. in DocType 'Appointment Booking Settings'
+#: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json
+msgid "Delete Permanently"
+msgstr ""
+
#. Label of the delete_transactions_status (Select) field in DocType
#. 'Transaction Deletion Record'
#: erpnext/setup/doctype/company/company.js:168
@@ -16333,23 +16414,6 @@ msgstr ""
msgid "Deliver secondary Items"
msgstr ""
-#. Option for the 'Status' (Select) field in DocType 'Purchase Order'
-#. Option for the 'Status' (Select) field in DocType 'Serial No'
-#. Option for the 'Tracking Status' (Select) field in DocType 'Shipment'
-#. Option for the 'Status' (Select) field in DocType 'Stock Reservation Entry'
-#. Option for the 'Status' (Select) field in DocType 'Subcontracting Inward
-#. Order'
-#: erpnext/buying/doctype/purchase_order/purchase_order.json
-#: erpnext/buying/doctype/purchase_order/purchase_order_list.js:20
-#: erpnext/controllers/website_list_for_contact.py:215
-#: erpnext/stock/doctype/serial_no/serial_no.json
-#: erpnext/stock/doctype/shipment/shipment.json
-#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json
-#: erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py:61
-#: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.json
-msgid "Delivered"
-msgstr ""
-
#: erpnext/stock/report/item_wise_consumption/item_wise_consumption.py:64
msgid "Delivered Amount"
msgstr ""
@@ -16558,7 +16622,7 @@ msgstr ""
msgid "Delivery Note Trends"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1451
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1457
msgid "Delivery Note {0} is not submitted"
msgstr ""
@@ -18583,6 +18647,11 @@ msgstr ""
msgid "Email Sent to Supplier {0}"
msgstr ""
+#. Label of the email_verified (Check) field in DocType 'Appointment'
+#: erpnext/crm/doctype/appointment/appointment.json
+msgid "Email Verified"
+msgstr ""
+
#: erpnext/setup/doctype/employee/employee.py:440
msgid "Email is required to create a user"
msgstr ""
@@ -18608,10 +18677,6 @@ msgstr ""
msgid "Email sent to {0}"
msgstr ""
-#: erpnext/crm/doctype/appointment/appointment.py:114
-msgid "Email verification failed."
-msgstr ""
-
#: erpnext/accounts/letterhead/company_letterhead.html:96
#: erpnext/accounts/letterhead/company_letterhead_grey.html:114
msgid "Email:"
@@ -18815,7 +18880,7 @@ msgstr ""
msgid "Ems(Pica)"
msgstr ""
-#: erpnext/public/js/controllers/transaction.js:2981
+#: erpnext/public/js/controllers/transaction.js:2965
msgid "Enable {0} on the Item master to proceed with {1} inspection."
msgstr ""
@@ -18829,6 +18894,12 @@ msgstr ""
msgid "Enable Allow Partial Reservation in the Stock Settings to reserve partial stock."
msgstr ""
+#. Label of the enable_appointment_portal (Check) field in DocType 'Appointment
+#. Booking Settings'
+#: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json
+msgid "Enable Appointment Booking Through Portal"
+msgstr ""
+
#. Label of the enable_scheduling (Check) field in DocType 'Appointment Booking
#. Settings'
#: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json
@@ -19532,7 +19603,7 @@ msgstr ""
msgid "Example: If the transaction amount is 200, then this will be calculated as {} = {}"
msgstr ""
-#: erpnext/stock/stock_ledger.py:2319
+#: erpnext/stock/stock_ledger.py:2334
msgid "Example: Serial No {0} reserved in {1}."
msgstr ""
@@ -19695,7 +19766,7 @@ msgstr ""
msgid "Excise Entry"
msgstr ""
-#: erpnext/stock/doctype/stock_entry/stock_entry.js:1530
+#: erpnext/stock/doctype/stock_entry/stock_entry.js:1507
msgid "Excise Invoice"
msgstr ""
@@ -20289,7 +20360,7 @@ msgstr ""
msgid "Fetch Customers"
msgstr ""
-#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js:82
+#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js:71
msgid "Fetch Items from Warehouse"
msgstr ""
@@ -20328,7 +20399,7 @@ msgid "Fetch Value From"
msgstr ""
#: erpnext/stock/doctype/material_request/material_request.js:373
-#: erpnext/stock/doctype/stock_entry/stock_entry.js:833
+#: erpnext/stock/doctype/stock_entry/stock_entry.js:809
msgid "Fetch exploded BOM (including sub-assemblies)"
msgstr ""
@@ -20356,7 +20427,7 @@ msgid "Fetching Sales Orders..."
msgstr ""
#: erpnext/accounts/doctype/dunning/dunning.js:135
-#: erpnext/public/js/controllers/transaction.js:1633
+#: erpnext/public/js/controllers/transaction.js:1617
msgid "Fetching exchange rates ..."
msgstr ""
@@ -21017,7 +21088,7 @@ msgstr ""
msgid "Following Material Requests have been raised automatically based on Item's re-order level"
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:841
+#: erpnext/selling/doctype/customer/customer.py:966
msgid "Following fields are mandatory to create address:"
msgstr ""
@@ -21074,7 +21145,7 @@ msgstr ""
msgid "For Item"
msgstr ""
-#: erpnext/controllers/stock_controller.py:1683
+#: erpnext/controllers/stock_controller.py:1684
msgid "For Item {0} cannot be received more than {1} qty against the {2} {3}"
msgstr ""
@@ -21203,7 +21274,7 @@ msgstr ""
msgid "For operation {0} at row {1}, please add raw materials or set a BOM against it."
msgstr ""
-#: erpnext/manufacturing/doctype/work_order/work_order.py:2915
+#: erpnext/manufacturing/doctype/work_order/work_order.py:2922
msgid "For operation {0}: Quantity ({1}) can not be greater than pending quantity({2})"
msgstr ""
@@ -21257,7 +21328,7 @@ msgstr ""
msgid "For the item {0}, the consumed quantity should be {1} according to the BOM {2}."
msgstr ""
-#: erpnext/public/js/controllers/transaction.js:1443
+#: erpnext/public/js/controllers/transaction.js:1427
msgctxt "Clear payment terms template and/or payment schedule when due date is changed"
msgid "For the new {0} to take effect, would you like to clear the current {1}?"
msgstr ""
@@ -21725,7 +21796,7 @@ msgstr ""
msgid "From date cannot be greater than To date"
msgstr ""
-#: erpnext/accounts/doctype/shipping_rule/shipping_rule.py:79
+#: erpnext/accounts/doctype/shipping_rule/shipping_rule.py:78
msgid "From value must be less than to value in row {0}"
msgstr ""
@@ -22217,8 +22288,8 @@ msgstr ""
#: erpnext/stock/doctype/stock_entry/stock_entry.js:461
#: erpnext/stock/doctype/stock_entry/stock_entry.js:508
#: erpnext/stock/doctype/stock_entry/stock_entry.js:541
-#: erpnext/stock/doctype/stock_entry/stock_entry.js:632
-#: erpnext/stock/doctype/stock_entry/stock_entry.js:800
+#: erpnext/stock/doctype/stock_entry/stock_entry.js:608
+#: erpnext/stock/doctype/stock_entry/stock_entry.js:776
#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js:165
msgid "Get Items From"
msgstr ""
@@ -22234,8 +22305,8 @@ msgid "Get Items for Purchase Only"
msgstr ""
#: erpnext/stock/doctype/material_request/material_request.js:347
-#: erpnext/stock/doctype/stock_entry/stock_entry.js:836
-#: erpnext/stock/doctype/stock_entry/stock_entry.js:849
+#: erpnext/stock/doctype/stock_entry/stock_entry.js:812
+#: erpnext/stock/doctype/stock_entry/stock_entry.js:825
msgid "Get Items from BOM"
msgstr ""
@@ -23061,7 +23132,7 @@ msgstr ""
msgid "Here are the error logs for the aforementioned failed depreciation entries: {0}"
msgstr ""
-#: erpnext/stock/stock_ledger.py:2022
+#: erpnext/stock/stock_ledger.py:2037
msgid "Here are the options to proceed:"
msgstr ""
@@ -23198,6 +23269,10 @@ msgstr ""
msgid "Holiday List"
msgstr ""
+#: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py:89
+msgid "Holiday List - {0} is not valid for current date."
+msgstr ""
+
#. Label of the holiday_list_name (Data) field in DocType 'Holiday List'
#: erpnext/setup/doctype/holiday_list/holiday_list.json
msgid "Holiday List Name"
@@ -23716,7 +23791,7 @@ msgstr ""
msgid "If no taxes are set, and Taxes and Charges Template is selected, the system will automatically apply the taxes from the chosen template."
msgstr ""
-#: erpnext/stock/stock_ledger.py:2032
+#: erpnext/stock/stock_ledger.py:2047
msgid "If not, you can Cancel / Submit this entry"
msgstr ""
@@ -23762,7 +23837,7 @@ msgstr ""
msgid "If the account is frozen, entries are allowed to restricted users."
msgstr ""
-#: erpnext/stock/stock_ledger.py:2025
+#: erpnext/stock/stock_ledger.py:2040
msgid "If the item is transacting as a Zero Valuation Rate item in this entry, please enable 'Allow Zero Valuation Rate' in the {0} Item table."
msgstr ""
@@ -23932,7 +24007,7 @@ msgstr ""
msgid "Ignore Employee Time Overlap"
msgstr ""
-#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js:145
+#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js:134
msgid "Ignore Empty Stock"
msgstr ""
@@ -24167,6 +24242,12 @@ msgstr ""
msgid "In Mins"
msgstr ""
+#. Description of the 'Verification Link Expiry Duration' (Int) field in
+#. DocType 'Appointment Booking Settings'
+#: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json
+msgid "In Minutes (min: 15 mins, max: 60 mins)"
+msgstr ""
+
#: erpnext/accounts/report/accounts_payable/accounts_payable.js:146
#: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:178
msgid "In Party Currency"
@@ -24894,14 +24975,14 @@ msgstr ""
msgid "Inspected By"
msgstr ""
-#: erpnext/controllers/stock_controller.py:1577
+#: erpnext/controllers/stock_controller.py:1578
#: erpnext/manufacturing/doctype/job_card/job_card.py:834
msgid "Inspection Rejected"
msgstr ""
#. Label of the inspection_required (Check) field in DocType 'Stock Entry'
-#: erpnext/controllers/stock_controller.py:1547
-#: erpnext/controllers/stock_controller.py:1549
+#: erpnext/controllers/stock_controller.py:1548
+#: erpnext/controllers/stock_controller.py:1550
#: erpnext/stock/doctype/stock_entry/stock_entry.json
msgid "Inspection Required"
msgstr ""
@@ -24918,7 +24999,7 @@ msgstr ""
msgid "Inspection Required before Purchase"
msgstr ""
-#: erpnext/controllers/stock_controller.py:1562
+#: erpnext/controllers/stock_controller.py:1563
#: erpnext/manufacturing/doctype/job_card/job_card.py:815
msgid "Inspection Submission"
msgstr ""
@@ -25001,12 +25082,12 @@ msgstr ""
#: erpnext/stock/doctype/pick_list/pick_list.py:168
#: erpnext/stock/doctype/pick_list/pick_list.py:1123
#: erpnext/stock/doctype/stock_entry/stock_entry.py:1245
-#: erpnext/stock/serial_batch_bundle.py:1225 erpnext/stock/stock_ledger.py:1713
-#: erpnext/stock/stock_ledger.py:2210
+#: erpnext/stock/serial_batch_bundle.py:1225 erpnext/stock/stock_ledger.py:1728
+#: erpnext/stock/stock_ledger.py:2225
msgid "Insufficient Stock"
msgstr ""
-#: erpnext/stock/stock_ledger.py:2225
+#: erpnext/stock/stock_ledger.py:2240
msgid "Insufficient Stock for Batch"
msgstr ""
@@ -25161,7 +25242,7 @@ msgstr ""
msgid "Internal Customer Accounting"
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:260
+#: erpnext/selling/doctype/customer/customer.py:264
msgid "Internal Customer for company {0} already exists"
msgstr ""
@@ -25232,7 +25313,7 @@ msgstr ""
msgid "Internal notes about this customer. Not visible on transactions or the portal."
msgstr ""
-#: erpnext/controllers/stock_controller.py:1644
+#: erpnext/controllers/stock_controller.py:1645
msgid "Internal transfers can only be done in company's default currency"
msgstr ""
@@ -25248,8 +25329,8 @@ msgstr ""
#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:384
#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:392
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1067
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1077
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1073
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1083
#: erpnext/assets/doctype/asset_category/asset_category.py:69
#: erpnext/assets/doctype/asset_category/asset_category.py:97
#: erpnext/controllers/accounts_controller.py:3245
@@ -25262,7 +25343,7 @@ msgid "Invalid Accounting Dimension"
msgstr ""
#: erpnext/accounts/doctype/payment_entry/payment_entry.py:402
-#: erpnext/accounts/doctype/payment_request/payment_request.py:1008
+#: erpnext/accounts/doctype/payment_request/payment_request.py:1009
msgid "Invalid Allocated Amount"
msgstr ""
@@ -25291,7 +25372,7 @@ msgstr ""
msgid "Invalid Barcode. There is no Item attached to this barcode."
msgstr ""
-#: erpnext/public/js/controllers/transaction.js:3202
+#: erpnext/public/js/controllers/transaction.js:3186
msgid "Invalid Blanket Order for the selected Customer and Item"
msgstr ""
@@ -25307,7 +25388,7 @@ msgstr ""
msgid "Invalid Company Field"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2412
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2418
msgid "Invalid Company for Inter Company Transaction."
msgstr ""
@@ -25317,7 +25398,7 @@ msgstr ""
msgid "Invalid Cost Center"
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:375
+#: erpnext/selling/doctype/customer/customer.py:379
msgid "Invalid Customer Group"
msgstr ""
@@ -25394,7 +25475,7 @@ msgstr ""
msgid "Invalid POS Invoices"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:387
+#: erpnext/accounts/doctype/account/account.py:418
msgid "Invalid Parent Account"
msgstr ""
@@ -25554,7 +25635,7 @@ msgstr ""
msgid "Invalid {0}"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2410
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2416
msgid "Invalid {0} for Inter Company Transaction."
msgstr ""
@@ -25790,7 +25871,7 @@ msgstr ""
#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json
#: erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
#: erpnext/accounts/doctype/pos_profile/pos_profile.json
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2461
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2467
#: erpnext/buying/doctype/supplier/supplier.json
#: erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py:62
msgid "Invoices"
@@ -26477,7 +26558,7 @@ msgstr ""
msgid "It can take upto few hours for accurate stock values to be visible after merging items."
msgstr ""
-#: erpnext/public/js/controllers/transaction.js:2580
+#: erpnext/public/js/controllers/transaction.js:2564
msgid "It is needed to fetch Item Details."
msgstr ""
@@ -26857,7 +26938,7 @@ msgstr ""
#: erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py:86
#: erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py:119
#: erpnext/projects/doctype/timesheet/timesheet.js:214
-#: erpnext/public/js/controllers/transaction.js:2874
+#: erpnext/public/js/controllers/transaction.js:2858
#: erpnext/public/js/stock_reservation.js:112
#: erpnext/public/js/stock_reservation.js:318 erpnext/public/js/utils.js:596
#: erpnext/public/js/utils.js:753
@@ -26920,7 +27001,7 @@ msgstr ""
#: erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.py:433
#: erpnext/stock/report/serial_no_ledger/serial_no_ledger.js:7
#: erpnext/stock/report/stock_ageing/stock_ageing.py:177
-#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:104
+#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:105
#: erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py:25
#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:26
#: erpnext/subcontracting/doctype/subcontracting_inward_order_item/subcontracting_inward_order_item.json
@@ -27127,7 +27208,7 @@ msgstr ""
#: erpnext/stock/report/stock_ledger/stock_ledger.js:71
#: erpnext/stock/report/stock_ledger/stock_ledger.py:346
#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.js:39
-#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:113
+#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:114
#: erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.js:33
#: erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py:99
#: erpnext/stock/workspace/stock/stock.json
@@ -27343,7 +27424,7 @@ msgstr ""
#: erpnext/manufacturing/report/production_planning_report/production_planning_report.py:371
#: erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py:92
#: erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py:138
-#: erpnext/public/js/controllers/transaction.js:2880
+#: erpnext/public/js/controllers/transaction.js:2864
#: erpnext/public/js/utils.js:849
#: erpnext/selling/doctype/quotation_item/quotation_item.json
#: erpnext/selling/doctype/sales_order/sales_order.js:1286
@@ -27387,7 +27468,7 @@ msgstr ""
#: erpnext/stock/report/stock_analytics/stock_analytics.py:45
#: erpnext/stock/report/stock_balance/stock_balance.py:476
#: erpnext/stock/report/stock_ledger/stock_ledger.py:294
-#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:110
+#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:111
#: erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py:31
#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:32
#: erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py:98
@@ -28266,7 +28347,7 @@ msgstr ""
msgid "Job Worker Warehouse"
msgstr ""
-#: erpnext/manufacturing/doctype/work_order/work_order.py:2970
+#: erpnext/manufacturing/doctype/work_order/work_order.py:2977
msgid "Job card {0} created"
msgstr ""
@@ -28615,7 +28696,7 @@ msgstr ""
msgid "Last Fiscal Year"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:661
+#: erpnext/accounts/doctype/account/account.py:692
msgid "Last GL Entry update was done {}. This operation is not allowed while system is actively being used. Please wait for 5 minutes before retrying."
msgstr ""
@@ -29848,7 +29929,7 @@ msgstr ""
msgid "Mandatory Depends On (Backend)"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1929
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1935
msgid "Mandatory Field"
msgstr ""
@@ -30261,6 +30342,12 @@ msgstr ""
msgid "Mark As Closed"
msgstr ""
+#. Option for the 'Action for Expired Unverified Appointments' (Select) field
+#. in DocType 'Appointment Booking Settings'
+#: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json
+msgid "Mark as Closed"
+msgstr ""
+
#. Description of the 'Is Internal Customer' (Check) field in DocType
#. 'Customer'
#: erpnext/selling/doctype/customer/customer.json
@@ -30377,7 +30464,7 @@ msgstr ""
msgid "Material Consumption for Manufacture"
msgstr ""
-#: erpnext/stock/doctype/stock_entry/stock_entry.js:688
+#: erpnext/stock/doctype/stock_entry/stock_entry.js:664
msgid "Material Consumption is not set in Manufacturing Settings."
msgstr ""
@@ -30866,7 +30953,7 @@ msgstr ""
msgid "Megawatt"
msgstr ""
-#: erpnext/stock/stock_ledger.py:2038
+#: erpnext/stock/stock_ledger.py:2053
msgid "Mention Valuation Rate in the Item master."
msgstr ""
@@ -30914,7 +31001,7 @@ msgstr ""
msgid "Merged"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:604
+#: erpnext/accounts/doctype/account/account.py:635
msgid "Merging is only possible if following properties are same in both records. Is Group, Root Type, Company and Account Currency"
msgstr ""
@@ -31256,8 +31343,8 @@ msgstr ""
#: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:97
#: erpnext/accounts/doctype/pos_profile/pos_profile.py:200
#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:593
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2478
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3094
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2484
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3100
#: erpnext/assets/doctype/asset_category/asset_category.py:116
msgid "Missing Account"
msgstr ""
@@ -31303,7 +31390,7 @@ msgstr ""
msgid "Missing Parameter"
msgstr ""
-#: erpnext/utilities/__init__.py:53
+#: erpnext/utilities/__init__.py:52 erpnext/utilities/__init__.py:57
msgid "Missing Payments App"
msgstr ""
@@ -31577,11 +31664,11 @@ msgstr ""
msgid "Multiple Accounts (Journal Template)"
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:446
+#: erpnext/selling/doctype/customer/customer.py:453
msgid "Multiple Loyalty Programs found for Customer {}. Please select manually."
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1241
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1247
msgid "Multiple POS Opening Entry"
msgstr ""
@@ -32211,6 +32298,12 @@ msgstr ""
msgid "New Sales Invoice"
msgstr ""
+#. Description of the 'Overdue Limit' (Currency) field in DocType 'Customer
+#. Credit Limit'
+#: erpnext/selling/doctype/customer_credit_limit/customer_credit_limit.json
+msgid "New Sales Invoices are blocked when the customer's overdue amount exceeds this. Requires 'Restrict Customer Over Billing' in Accounts Settings."
+msgstr ""
+
#. Label of the sales_order (Check) field in DocType 'Email Digest'
#: erpnext/setup/doctype/email_digest/email_digest.json
msgid "New Sales Orders"
@@ -32242,7 +32335,7 @@ msgstr ""
msgid "New Workplace"
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:411
+#: erpnext/selling/doctype/customer/customer.py:418
msgid "New credit limit is less than current outstanding amount for the customer. Credit limit has to be atleast {0}"
msgstr ""
@@ -32309,7 +32402,7 @@ msgstr ""
msgid "No Answer"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2583
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2589
msgid "No Customer found for Inter Company Transactions which represents company {0}"
msgstr ""
@@ -32402,7 +32495,7 @@ msgstr ""
msgid "No Summary"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2567
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2573
msgid "No Supplier found for Inter Company Transactions which represents company {0}"
msgstr ""
@@ -32464,6 +32557,10 @@ msgstr ""
msgid "No additional fields available"
msgstr ""
+#: erpnext/crm/doctype/appointment/appointment.py:103
+msgid "No availability of slots are found. Please add on Appointment Booking Settings."
+msgstr ""
+
#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1362
msgid "No available quantity to reserve for item {0} in warehouse {1}"
msgstr ""
@@ -32646,7 +32743,7 @@ msgstr ""
msgid "No open Material Requests found for the given criteria."
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1235
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1241
msgid "No open POS Opening Entry found for POS Profile {0}."
msgstr ""
@@ -32778,7 +32875,7 @@ msgstr ""
msgid "No vouchers found for this transaction"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2631
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2637
msgid "No {0} found for Inter Company Transactions."
msgstr ""
@@ -33436,7 +33533,7 @@ msgstr ""
msgid "Only Include Allocated Payments"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:136
+#: erpnext/accounts/doctype/account/account.py:137
msgid "Only Parent can be of type {0}"
msgstr ""
@@ -33756,7 +33853,7 @@ msgid "Opening Invoice Tool"
msgstr ""
#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1686
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2038
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2044
msgid "Opening Invoice has rounding adjustment of {0}.
'{1}' account is required to post these values. Please set it in Company: {2}.
Or, '{3}' can be enabled to not post any rounding adjustment."
msgstr ""
@@ -34274,7 +34371,8 @@ msgstr ""
#: erpnext/selling/doctype/sales_order_item/sales_order_item.json
#: erpnext/stock/doctype/bin/bin.json
#: erpnext/stock/doctype/packed_item/packed_item.json
-#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:162
+#: erpnext/stock/page/stock_balance/stock_balance.js:60
+#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:163
msgid "Ordered Qty"
msgstr ""
@@ -34444,7 +34542,7 @@ msgstr ""
msgid "Out of stock"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1248
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1254
#: erpnext/selling/page/point_of_sale/pos_controller.js:199
msgid "Outdated POS Opening Entry"
msgstr ""
@@ -34580,7 +34678,7 @@ msgstr ""
msgid "Over Picking Allowance (%)"
msgstr ""
-#: erpnext/controllers/stock_controller.py:1814
+#: erpnext/controllers/stock_controller.py:1815
msgid "Over Receipt"
msgstr ""
@@ -34636,6 +34734,20 @@ msgstr ""
msgid "Overdue Days"
msgstr ""
+#. Label of the overdue_billing_threshold (Currency) field in DocType 'Customer
+#. Credit Limit'
+#: erpnext/selling/doctype/customer_credit_limit/customer_credit_limit.json
+msgid "Overdue Limit"
+msgstr ""
+
+#: erpnext/selling/doctype/customer/customer.py:707
+msgid "Overdue Limit Crossed"
+msgstr ""
+
+#: erpnext/selling/doctype/customer/customer.py:702
+msgid "Overdue Limit crossed for customer {0}. Overdue amount {1} exceeds the allowed limit {2}."
+msgstr ""
+
#. Name of a DocType
#: erpnext/accounts/doctype/overdue_payment/overdue_payment.json
msgid "Overdue Payment"
@@ -34661,7 +34773,7 @@ msgstr ""
msgid "Overlap in scoring between {0} and {1}"
msgstr ""
-#: erpnext/accounts/doctype/shipping_rule/shipping_rule.py:206
+#: erpnext/accounts/doctype/shipping_rule/shipping_rule.py:205
msgid "Overlapping conditions found between:"
msgstr ""
@@ -34695,15 +34807,6 @@ msgstr ""
msgid "Owned"
msgstr ""
-#: erpnext/accounts/report/sales_payment_summary/sales_payment_summary.js:29
-#: erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py:23
-#: erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py:39
-#: erpnext/accounts/report/sales_register/sales_register.js:46
-#: erpnext/accounts/report/sales_register/sales_register.py:250
-#: erpnext/crm/report/lead_details/lead_details.py:45
-msgid "Owner"
-msgstr ""
-
#. Label of the asset_owner_section (Section Break) field in DocType 'Asset'
#: erpnext/assets/doctype/asset/asset.json
msgid "Ownership"
@@ -34928,7 +35031,7 @@ msgstr ""
msgid "POS Opening Entry"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1249
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1255
msgid "POS Opening Entry - {0} is outdated. Please close the POS and create a new POS Opening Entry."
msgstr ""
@@ -34949,7 +35052,7 @@ msgstr ""
msgid "POS Opening Entry Exists"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1234
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1240
msgid "POS Opening Entry Missing"
msgstr ""
@@ -34985,7 +35088,7 @@ msgstr ""
msgid "POS Profile"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1242
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1248
msgid "POS Profile - {0} has multiple open POS Opening Entries. Please close or cancel the existing entries before proceeding."
msgstr ""
@@ -35003,11 +35106,11 @@ msgstr ""
msgid "POS Profile doesn't match {}"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1202
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1208
msgid "POS Profile is mandatory to mark this invoice as POS Transaction."
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1431
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1437
msgid "POS Profile required to make POS Entry"
msgstr ""
@@ -35113,7 +35216,7 @@ msgstr ""
msgid "Packed Items"
msgstr ""
-#: erpnext/controllers/stock_controller.py:1648
+#: erpnext/controllers/stock_controller.py:1649
msgid "Packed Items cannot be transferred internally"
msgstr ""
@@ -35261,7 +35364,7 @@ msgid "Paid To Account Type"
msgstr ""
#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:334
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1198
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1204
msgid "Paid amount + Write Off Amount can not be greater than Grand Total"
msgstr ""
@@ -35482,7 +35585,7 @@ msgstr ""
msgid "Partial Material Transferred"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1221
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1227
msgid "Partial Payment in POS Transactions are not allowed."
msgstr ""
@@ -36558,7 +36661,7 @@ msgstr ""
msgid "Payment Schedule based Payment Requests cannot be created because a Payment Entry already exists for this document."
msgstr ""
-#: erpnext/public/js/controllers/transaction.js:529
+#: erpnext/public/js/controllers/transaction.js:532
msgid "Payment Schedules"
msgstr ""
@@ -36580,7 +36683,7 @@ msgstr ""
#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1212
#: erpnext/accounts/report/gross_profit/gross_profit.py:449
#: erpnext/accounts/workspace/invoicing/invoicing.json
-#: erpnext/public/js/controllers/transaction.js:544
+#: erpnext/public/js/controllers/transaction.js:547
#: erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py:30
#: erpnext/workspace_sidebar/accounts_setup.json
msgid "Payment Term"
@@ -36704,7 +36807,7 @@ msgstr ""
msgid "Payment methods are mandatory. Please add at least one payment method."
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3098
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3104
msgid "Payment methods refreshed. Please review before proceeding."
msgstr ""
@@ -37517,7 +37620,8 @@ msgstr ""
#: erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json
#: erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.py:1032
#: erpnext/stock/doctype/bin/bin.json
-#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:148
+#: erpnext/stock/page/stock_balance/stock_balance.js:62
+#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:149
msgid "Planned Qty"
msgstr ""
@@ -37648,6 +37752,10 @@ msgstr ""
msgid "Please add a Temporary Opening account in Chart of Accounts"
msgstr ""
+#: erpnext/crm/doctype/appointment/appointment.py:95
+msgid "Please add a valid Holiday List on Appointment Booking Settings."
+msgstr ""
+
#: erpnext/accounts/doctype/bank_transaction_rule/bank_transaction_rule.py:119
msgid "Please add an account for the Bank Entry rule."
msgstr ""
@@ -37668,7 +37776,7 @@ msgstr ""
msgid "Please add the account to root level Company - {0}"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:233
+#: erpnext/accounts/doctype/account/account.py:264
msgid "Please add the account to root level Company - {}"
msgstr ""
@@ -37676,7 +37784,7 @@ msgstr ""
msgid "Please add {1} role to user {0}."
msgstr ""
-#: erpnext/controllers/stock_controller.py:1825
+#: erpnext/controllers/stock_controller.py:1826
msgid "Please adjust the qty or edit {0} to proceed."
msgstr ""
@@ -37684,7 +37792,7 @@ msgstr ""
msgid "Please attach CSV file"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3236
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3242
msgid "Please cancel and amend the Payment Entry"
msgstr ""
@@ -37726,11 +37834,14 @@ msgstr ""
msgid "Please check your Plaid client ID and secret values"
msgstr ""
-#: erpnext/crm/doctype/appointment/appointment.py:98
#: erpnext/www/book_appointment/index.js:235
msgid "Please check your email to confirm the appointment"
msgstr ""
+#: erpnext/crm/doctype/appointment/appointment.py:184
+msgid "Please check your email to confirm the appointment."
+msgstr ""
+
#: erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py:374
msgid "Please click on 'Generate Schedule'"
msgstr ""
@@ -37751,7 +37862,7 @@ msgstr ""
msgid "Please configure accounts for the Bank Entry rule."
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:642
+#: erpnext/selling/doctype/customer/customer.py:649
msgid "Please contact any of the following users to extend the credit limits for {0}: {1}"
msgstr ""
@@ -37759,11 +37870,11 @@ msgstr ""
msgid "Please contact any of the following users to {} this transaction."
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:635
+#: erpnext/selling/doctype/customer/customer.py:642
msgid "Please contact your administrator to extend the credit limits for {0}."
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:384
+#: erpnext/accounts/doctype/account/account.py:415
msgid "Please convert the parent account in corresponding child company to a group account."
msgstr ""
@@ -37835,11 +37946,11 @@ msgstr ""
msgid "Please ensure that the {0} account {1} is a Payable account. You can change the account type to Payable or select a different account."
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1061
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1067
msgid "Please ensure {} account is a Balance Sheet account."
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1071
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1077
msgid "Please ensure {} account {} is a Receivable account."
msgstr ""
@@ -37848,7 +37959,7 @@ msgid "Please enter Difference Account or set default Stock Adjustment
msgstr ""
#: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:555
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1333
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1339
msgid "Please enter Account for Change Amount"
msgstr ""
@@ -37881,7 +37992,7 @@ msgstr ""
msgid "Please enter Item Code to get Batch Number"
msgstr ""
-#: erpnext/public/js/controllers/transaction.js:3059
+#: erpnext/public/js/controllers/transaction.js:3043
msgid "Please enter Item Code to get batch no"
msgstr ""
@@ -37934,7 +38045,7 @@ msgid "Please enter Warehouse and Date"
msgstr ""
#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:660
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1329
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1335
msgid "Please enter Write Off Account"
msgstr ""
@@ -38026,6 +38137,10 @@ msgstr ""
msgid "Please fill the Sales Orders table"
msgstr ""
+#: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py:57
+msgid "Please fill up the Availability of Slots table to enable Appointment Scheduling."
+msgstr ""
+
#: erpnext/stock/doctype/shipment/shipment.js:277
msgid "Please first set Full Name, Email and Phone for the user"
msgstr ""
@@ -38246,7 +38361,7 @@ msgid "Please select a BOM"
msgstr ""
#: erpnext/accounts/party.py:445
-#: erpnext/stock/doctype/pick_list/pick_list.py:1853
+#: erpnext/stock/doctype/pick_list/pick_list.py:1856
msgid "Please select a Company"
msgstr ""
@@ -38254,7 +38369,7 @@ msgstr ""
#: erpnext/manufacturing/doctype/bom/bom.js:734
#: erpnext/manufacturing/doctype/bom/bom.py:279
#: erpnext/public/js/controllers/accounts.js:274
-#: erpnext/public/js/controllers/transaction.js:3358
+#: erpnext/public/js/controllers/transaction.js:3342
msgid "Please select a Company first."
msgstr ""
@@ -38266,6 +38381,10 @@ msgstr ""
msgid "Please select a Delivery Note"
msgstr ""
+#: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py:81
+msgid "Please select a Holiday List to enable Appointment Scheduling."
+msgstr ""
+
#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:153
msgid "Please select a Subcontracting Purchase Order."
msgstr ""
@@ -38371,7 +38490,7 @@ msgstr ""
msgid "Please select at least one row with difference value"
msgstr ""
-#: erpnext/public/js/controllers/transaction.js:572
+#: erpnext/public/js/controllers/transaction.js:584
msgid "Please select at least one schedule."
msgstr ""
@@ -38491,7 +38610,7 @@ msgstr ""
msgid "Please set Account"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1929
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1935
msgid "Please set Account for Change Amount"
msgstr ""
@@ -38585,7 +38704,7 @@ msgstr ""
msgid "Please set a Cost Center for the Asset or set an Asset Depreciation Cost Center for the Company {}"
msgstr ""
-#: erpnext/projects/doctype/project/project.py:766
+#: erpnext/projects/doctype/project/project.py:768
msgid "Please set a default Holiday List for Company {0}"
msgstr ""
@@ -38622,19 +38741,19 @@ msgstr ""
msgid "Please set both the Tax ID and Fiscal Code on Company {0}"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2475
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2481
msgid "Please set default Cash or Bank account in Mode of Payment {0}"
msgstr ""
#: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:94
#: erpnext/accounts/doctype/pos_profile/pos_profile.py:197
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3091
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3097
msgid "Please set default Cash or Bank account in Mode of Payment {}"
msgstr ""
#: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:96
#: erpnext/accounts/doctype/pos_profile/pos_profile.py:199
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3093
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3099
msgid "Please set default Cash or Bank account in Mode of Payments {}"
msgstr ""
@@ -38675,7 +38794,7 @@ msgstr ""
msgid "Please set opening number of booked depreciations"
msgstr ""
-#: erpnext/public/js/controllers/transaction.js:2723
+#: erpnext/public/js/controllers/transaction.js:2707
msgid "Please set recurring after saving"
msgstr ""
@@ -39017,7 +39136,7 @@ msgstr ""
msgid "Posting Date inheritance for exchange gain / loss"
msgstr ""
-#: erpnext/public/js/controllers/transaction.js:1153
+#: erpnext/public/js/controllers/transaction.js:1137
msgid "Posting Date will change to today's date as Edit Posting Date and Time is unchecked. Are you sure want to proceed?"
msgstr ""
@@ -40420,7 +40539,7 @@ msgstr ""
msgid "Progress (%)"
msgstr ""
-#: erpnext/projects/doctype/project/project.py:377
+#: erpnext/projects/doctype/project/project.py:379
msgid "Project Collaboration Invitation"
msgstr ""
@@ -40468,7 +40587,7 @@ msgstr ""
msgid "Project Summary"
msgstr ""
-#: erpnext/projects/doctype/project/project.py:704
+#: erpnext/projects/doctype/project/project.py:706
msgid "Project Summary for {0}"
msgstr ""
@@ -40576,8 +40695,9 @@ msgstr ""
#: erpnext/stock/doctype/bin/bin.json
#: erpnext/stock/doctype/material_request_item/material_request_item.json
#: erpnext/stock/doctype/packed_item/packed_item.json
+#: erpnext/stock/page/stock_balance/stock_balance.js:51
#: erpnext/stock/report/item_shortage_report/item_shortage_report.py:73
-#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:204
+#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:212
#: erpnext/templates/emails/reorder_item.html:12
msgid "Projected Qty"
msgstr ""
@@ -40590,16 +40710,12 @@ msgstr ""
msgid "Projected Quantity Formula"
msgstr ""
-#: erpnext/stock/page/stock_balance/stock_balance.js:51
-msgid "Projected qty"
-msgstr ""
-
#. Label of a Desktop Icon
#. Name of a Workspace
#. Label of a Card Break in the Projects Workspace
#. Title of a Workspace Sidebar
#: erpnext/config/projects.py:7 erpnext/desktop_icon/projects.json
-#: erpnext/projects/doctype/project/project.py:482
+#: erpnext/projects/doctype/project/project.py:484
#: erpnext/projects/workspace/projects/projects.json
#: erpnext/selling/doctype/customer/customer_dashboard.py:26
#: erpnext/selling/doctype/sales_order/sales_order_dashboard.py:28
@@ -41945,7 +42061,7 @@ msgstr ""
msgid "Quality Inspection Analysis"
msgstr ""
-#: erpnext/public/js/controllers/transaction.js:2980
+#: erpnext/public/js/controllers/transaction.js:2964
msgid "Quality Inspection Not Configured"
msgstr ""
@@ -42181,7 +42297,7 @@ msgstr ""
#: erpnext/stock/doctype/material_request_item/material_request_item.json
#: erpnext/stock/doctype/packing_slip_item/packing_slip_item.json
#: erpnext/stock/doctype/pick_list_item/pick_list_item.json
-#: erpnext/stock/doctype/stock_entry/stock_entry.js:829
+#: erpnext/stock/doctype/stock_entry/stock_entry.js:805
#: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
#: erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
#: erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py:36
@@ -42331,7 +42447,7 @@ msgstr ""
msgid "Quantity to Manufacture"
msgstr ""
-#: erpnext/manufacturing/doctype/work_order/work_order.py:2908
+#: erpnext/manufacturing/doctype/work_order/work_order.py:2915
msgid "Quantity to Manufacture can not be zero for the operation {0}"
msgstr ""
@@ -42368,7 +42484,7 @@ msgstr ""
msgid "Query Route String"
msgstr ""
-#: erpnext/accounts/doctype/accounts_settings/accounts_settings.py:192
+#: erpnext/accounts/doctype/accounts_settings/accounts_settings.py:198
msgid "Queue Size should be between 5 and 100"
msgstr ""
@@ -43141,10 +43257,6 @@ msgstr ""
msgid "Recalculate Batch Qty"
msgstr ""
-#: erpnext/stock/doctype/bin/bin.js:10
-msgid "Recalculate Bin Qty"
-msgstr ""
-
#. Label of the recalculate_rate (Check) field in DocType 'Stock Ledger Entry'
#: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
msgid "Recalculate Incoming/Outgoing Rate"
@@ -43156,6 +43268,10 @@ msgstr ""
msgid "Recalculate Valuation Rate"
msgstr ""
+#: erpnext/stock/doctype/bin/bin.js:10
+msgid "Recalculate Values"
+msgstr ""
+
#. Option for the 'Status' (Select) field in DocType 'Asset'
#. Option for the 'Purpose' (Select) field in DocType 'Asset Movement'
#. Option for the 'Asset Status' (Select) field in DocType 'Serial No'
@@ -43668,7 +43784,7 @@ msgstr ""
msgid "Reference #{0} dated {1}"
msgstr ""
-#: erpnext/public/js/controllers/transaction.js:2836
+#: erpnext/public/js/controllers/transaction.js:2820
msgid "Reference Date for Early Payment Discount"
msgstr ""
@@ -44096,7 +44212,7 @@ msgstr ""
msgid "Rename Log"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:557
+#: erpnext/accounts/doctype/account/account.py:588
msgid "Rename Not Allowed"
msgstr ""
@@ -44113,7 +44229,7 @@ msgstr ""
msgid "Rename jobs for doctype {0} have not been enqueued."
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:549
+#: erpnext/accounts/doctype/account/account.py:580
msgid "Renaming it is only allowed via parent company {0}, to avoid mismatch."
msgstr ""
@@ -44134,13 +44250,13 @@ msgstr ""
#. Label of the reorder_level (Float) field in DocType 'Material Request Item'
#: erpnext/stock/doctype/material_request_item/material_request_item.json
#: erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py:64
-#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:211
+#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:219
msgid "Reorder Level"
msgstr ""
#. Label of the reorder_qty (Float) field in DocType 'Material Request Item'
#: erpnext/stock/doctype/material_request_item/material_request_item.json
-#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:218
+#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:226
msgid "Reorder Qty"
msgstr ""
@@ -44233,7 +44349,7 @@ msgstr ""
msgid "Report Template"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:462
+#: erpnext/accounts/doctype/account/account.py:493
msgid "Report Type is mandatory"
msgstr ""
@@ -44546,7 +44662,8 @@ msgstr ""
#: erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py:44
#: erpnext/stock/doctype/bin/bin.json
#: erpnext/stock/doctype/packed_item/packed_item.json
-#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:155
+#: erpnext/stock/page/stock_balance/stock_balance.js:61
+#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:156
msgid "Requested Qty"
msgstr ""
@@ -44754,7 +44871,7 @@ msgstr ""
msgid "Reserved"
msgstr ""
-#: erpnext/controllers/stock_controller.py:1405
+#: erpnext/controllers/stock_controller.py:1406
msgid "Reserved Batch Conflict"
msgstr ""
@@ -44772,8 +44889,9 @@ msgstr ""
#: erpnext/stock/dashboard/item_dashboard_list.html:20
#: erpnext/stock/doctype/bin/bin.json
#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json
+#: erpnext/stock/page/stock_balance/stock_balance.js:52
#: erpnext/stock/report/reserved_stock/reserved_stock.py:124
-#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:169
+#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:170
#: erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.json
msgid "Reserved Qty"
msgstr ""
@@ -44787,11 +44905,13 @@ msgstr ""
#. Label of the reserved_qty_for_production (Float) field in DocType 'Bin'
#: erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json
#: erpnext/stock/doctype/bin/bin.json
+#: erpnext/stock/page/stock_balance/stock_balance.js:53
msgid "Reserved Qty for Production"
msgstr ""
#. Label of the reserved_qty_for_production_plan (Float) field in DocType 'Bin'
#: erpnext/stock/doctype/bin/bin.json
+#: erpnext/stock/page/stock_balance/stock_balance.js:57
msgid "Reserved Qty for Production Plan"
msgstr ""
@@ -44801,6 +44921,7 @@ msgstr ""
#. Label of the reserved_qty_for_sub_contract (Float) field in DocType 'Bin'
#: erpnext/stock/doctype/bin/bin.json
+#: erpnext/stock/page/stock_balance/stock_balance.js:54
msgid "Reserved Qty for Subcontract"
msgstr ""
@@ -44824,7 +44945,7 @@ msgstr ""
msgid "Reserved Quantity for Production"
msgstr ""
-#: erpnext/stock/stock_ledger.py:2325
+#: erpnext/stock/stock_ledger.py:2340
msgid "Reserved Serial No."
msgstr ""
@@ -44838,15 +44959,17 @@ msgstr ""
#: erpnext/stock/dashboard/item_dashboard_list.html:15
#: erpnext/stock/doctype/bin/bin.json
#: erpnext/stock/doctype/pick_list/pick_list.js:178
+#: erpnext/stock/page/stock_balance/stock_balance.js:59
#: erpnext/stock/report/reserved_stock/reserved_stock.json
#: erpnext/stock/report/stock_balance/stock_balance.py:569
-#: erpnext/stock/stock_ledger.py:2309
+#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:205
+#: erpnext/stock/stock_ledger.py:2324
#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:205
#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:333
msgid "Reserved Stock"
msgstr ""
-#: erpnext/stock/stock_ledger.py:2354
+#: erpnext/stock/stock_ledger.py:2369
msgid "Reserved Stock for Batch"
msgstr ""
@@ -44862,34 +44985,22 @@ msgstr ""
msgid "Reserved Warehouse is mandatory for the Item {item_code} in Raw Materials supplied."
msgstr ""
-#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:197
+#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:198
msgid "Reserved for POS Transactions"
msgstr ""
-#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:176
+#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:177
msgid "Reserved for Production"
msgstr ""
-#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:183
+#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:184
msgid "Reserved for Production Plan"
msgstr ""
-#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:190
+#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:191
msgid "Reserved for Sub Contracting"
msgstr ""
-#: erpnext/stock/page/stock_balance/stock_balance.js:53
-msgid "Reserved for manufacturing"
-msgstr ""
-
-#: erpnext/stock/page/stock_balance/stock_balance.js:52
-msgid "Reserved for sale"
-msgstr ""
-
-#: erpnext/stock/page/stock_balance/stock_balance.js:54
-msgid "Reserved for sub contracting"
-msgstr ""
-
#: erpnext/public/js/stock_reservation.js:203
#: erpnext/selling/doctype/sales_order/sales_order.js:418
#: erpnext/stock/doctype/pick_list/pick_list.js:307
@@ -45073,6 +45184,12 @@ msgstr ""
msgid "Restrict"
msgstr ""
+#. Label of the enable_overdue_billing_threshold (Check) field in DocType
+#. 'Accounts Settings'
+#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+msgid "Restrict Customer Over Billing"
+msgstr ""
+
#. Label of the restrict_based_on (Select) field in DocType 'Party Specific
#. Item'
#: erpnext/selling/doctype/party_specific_item/party_specific_item.json
@@ -45253,7 +45370,7 @@ msgstr ""
msgid "Return Raw Material to Customer"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1572
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1578
msgid "Return invoice of asset cancelled"
msgstr ""
@@ -45518,6 +45635,12 @@ msgstr ""
msgid "Rod"
msgstr ""
+#. Label of the role_allowed_to_bypass_overdue_billing (Link) field in DocType
+#. 'Accounts Settings'
+#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+msgid "Role Allowed to Bypass Over Billing Restriction"
+msgstr ""
+
#. Label of the role_allowed_to_over_deliver_receive (Link) field in DocType
#. 'Stock Settings'
#: erpnext/stock/doctype/stock_settings/stock_settings.json
@@ -45600,11 +45723,11 @@ msgstr ""
msgid "Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:459
+#: erpnext/accounts/doctype/account/account.py:490
msgid "Root Type is mandatory"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:215
+#: erpnext/accounts/doctype/account/account.py:246
msgid "Root cannot be edited."
msgstr ""
@@ -45811,12 +45934,12 @@ msgid "Row #1: Sequence ID must be 1 for Operation {0}."
msgstr ""
#: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:564
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2130
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2136
msgid "Row #{0} (Payment Table): Amount must be negative"
msgstr ""
#: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:562
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2125
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2131
msgid "Row #{0} (Payment Table): Amount must be positive"
msgstr ""
@@ -46229,15 +46352,15 @@ msgstr ""
msgid "Row #{0}: Qty should be less than or equal to Available Qty to Reserve (Actual Qty - Reserved Qty) {1} for Iem {2} against Batch {3} in Warehouse {4}."
msgstr ""
-#: erpnext/controllers/stock_controller.py:1543
+#: erpnext/controllers/stock_controller.py:1544
msgid "Row #{0}: Quality Inspection is required for Item {1}"
msgstr ""
-#: erpnext/controllers/stock_controller.py:1558
+#: erpnext/controllers/stock_controller.py:1559
msgid "Row #{0}: Quality Inspection {1} is not submitted for the item: {2}"
msgstr ""
-#: erpnext/controllers/stock_controller.py:1573
+#: erpnext/controllers/stock_controller.py:1574
msgid "Row #{0}: Quality Inspection {1} was rejected for item {2}"
msgstr ""
@@ -46409,7 +46532,7 @@ msgstr ""
msgid "Row #{0}: Stock not available to reserve for the Item {1} in Warehouse {2}."
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1315
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1321
msgid "Row #{0}: Stock quantity {1} ({2}) for item {3} cannot exceed {4}"
msgstr ""
@@ -46728,7 +46851,7 @@ msgstr ""
msgid "Row {0}: From Time and To Time of {1} is overlapping with {2}"
msgstr ""
-#: erpnext/controllers/stock_controller.py:1639
+#: erpnext/controllers/stock_controller.py:1640
msgid "Row {0}: From Warehouse is mandatory for internal transfers"
msgstr ""
@@ -46856,7 +46979,7 @@ msgstr ""
msgid "Row {0}: Quantity not available for {4} in warehouse {1} at posting time of the entry ({2} {3})"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:933
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:939
msgid "Row {0}: Sales Invoice {1} is already created for {2}"
msgstr ""
@@ -46872,7 +46995,7 @@ msgstr ""
msgid "Row {0}: Subcontracted Item is mandatory for the raw material {1}"
msgstr ""
-#: erpnext/controllers/stock_controller.py:1630
+#: erpnext/controllers/stock_controller.py:1631
msgid "Row {0}: Target Warehouse is mandatory for internal transfers"
msgstr ""
@@ -47629,7 +47752,7 @@ msgstr ""
msgid "Sales Order {0} is not available for production"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1445
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1451
msgid "Sales Order {0} is not submitted"
msgstr ""
@@ -48064,7 +48187,7 @@ msgstr ""
#. Label of the sample_size (Float) field in DocType 'Quality Inspection'
#: erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py:93
-#: erpnext/public/js/controllers/transaction.js:2893
+#: erpnext/public/js/controllers/transaction.js:2877
#: erpnext/stock/doctype/quality_inspection/quality_inspection.json
msgid "Sample Size"
msgstr ""
@@ -48151,7 +48274,7 @@ msgstr ""
msgid "Scan barcode for item {0}"
msgstr ""
-#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js:111
+#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js:100
msgid "Scan mode enabled, existing quantity will not be fetched."
msgstr ""
@@ -48174,7 +48297,7 @@ msgstr ""
msgid "Schedule Date"
msgstr ""
-#: erpnext/public/js/controllers/transaction.js:538
+#: erpnext/public/js/controllers/transaction.js:541
msgid "Schedule Name"
msgstr ""
@@ -48583,7 +48706,7 @@ msgstr ""
msgid "Select Items based on Delivery Date"
msgstr ""
-#: erpnext/public/js/controllers/transaction.js:2928
+#: erpnext/public/js/controllers/transaction.js:2912
msgid "Select Items for Quality Inspection"
msgstr ""
@@ -48613,7 +48736,7 @@ msgstr ""
msgid "Select Loyalty Program"
msgstr ""
-#: erpnext/public/js/controllers/transaction.js:524
+#: erpnext/public/js/controllers/transaction.js:527
msgid "Select Payment Schedule"
msgstr ""
@@ -48849,7 +48972,7 @@ msgstr ""
msgid "Selected POS Opening Entry should be open."
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2626
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2632
msgid "Selected Price List should have buying and selling fields checked."
msgstr ""
@@ -48899,7 +49022,7 @@ msgstr ""
msgid "Sell quantity cannot exceed the asset quantity"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1458
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1464
msgid "Sell quantity cannot exceed the asset quantity. Asset {0} has only {1} item(s)."
msgstr ""
@@ -49015,7 +49138,7 @@ msgid "Send Emails to Suppliers"
msgstr ""
#. Label of the send_sms (Button) field in DocType 'SMS Center'
-#: erpnext/public/js/controllers/transaction.js:743
+#: erpnext/public/js/controllers/transaction.js:727
#: erpnext/selling/doctype/sms_center/sms_center.json
msgid "Send SMS"
msgstr ""
@@ -49157,7 +49280,7 @@ msgstr ""
#: erpnext/manufacturing/doctype/job_card/job_card.json
#: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js:74
#: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py:114
-#: erpnext/public/js/controllers/transaction.js:2906
+#: erpnext/public/js/controllers/transaction.js:2890
#: erpnext/public/js/utils/serial_no_batch_selector.js:432
#: erpnext/selling/doctype/installation_note_item/installation_note_item.json
#: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
@@ -49362,7 +49485,7 @@ msgstr ""
msgid "Serial Nos are created successfully"
msgstr ""
-#: erpnext/stock/stock_ledger.py:2315
+#: erpnext/stock/stock_ledger.py:2330
msgid "Serial Nos are reserved in Stock Reservation Entries, you need to unreserve them before proceeding."
msgstr ""
@@ -49719,12 +49842,12 @@ msgid "Service Stop Date"
msgstr ""
#: erpnext/accounts/deferred_revenue.py:45
-#: erpnext/public/js/controllers/transaction.js:1815
+#: erpnext/public/js/controllers/transaction.js:1799
msgid "Service Stop Date cannot be after Service End Date"
msgstr ""
#: erpnext/accounts/deferred_revenue.py:42
-#: erpnext/public/js/controllers/transaction.js:1812
+#: erpnext/public/js/controllers/transaction.js:1796
msgid "Service Stop Date cannot be before Service Start Date"
msgstr ""
@@ -50336,7 +50459,7 @@ msgstr ""
msgid "Shipping Address does not belong to the {0}"
msgstr ""
-#: erpnext/accounts/doctype/shipping_rule/shipping_rule.py:134
+#: erpnext/accounts/doctype/shipping_rule/shipping_rule.py:133
msgid "Shipping Address does not have country, which is required for this Shipping Rule"
msgstr ""
@@ -50429,15 +50552,15 @@ msgstr ""
msgid "Shipping Zipcode"
msgstr ""
-#: erpnext/accounts/doctype/shipping_rule/shipping_rule.py:138
+#: erpnext/accounts/doctype/shipping_rule/shipping_rule.py:137
msgid "Shipping rule not applicable for country {0} in Shipping Address"
msgstr ""
-#: erpnext/accounts/doctype/shipping_rule/shipping_rule.py:157
+#: erpnext/accounts/doctype/shipping_rule/shipping_rule.py:156
msgid "Shipping rule only applicable for Buying"
msgstr ""
-#: erpnext/accounts/doctype/shipping_rule/shipping_rule.py:152
+#: erpnext/accounts/doctype/shipping_rule/shipping_rule.py:151
msgid "Shipping rule only applicable for Selling"
msgstr ""
@@ -50480,7 +50603,7 @@ msgstr ""
msgid "Short-term Provisions"
msgstr ""
-#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:225
+#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:233
msgid "Shortage Qty"
msgstr ""
@@ -51036,7 +51159,7 @@ msgstr ""
#: erpnext/selling/doctype/sales_order_item/sales_order_item.json
#: erpnext/stock/dashboard/item_dashboard.js:227
#: erpnext/stock/doctype/material_request_item/material_request_item.json
-#: erpnext/stock/doctype/stock_entry/stock_entry.js:820
+#: erpnext/stock/doctype/stock_entry/stock_entry.js:796
#: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
msgid "Source Warehouse"
msgstr ""
@@ -51228,7 +51351,7 @@ msgstr ""
msgid "Stale Days"
msgstr ""
-#: erpnext/accounts/doctype/accounts_settings/accounts_settings.py:162
+#: erpnext/accounts/doctype/accounts_settings/accounts_settings.py:168
msgid "Stale Days should start from 1."
msgstr ""
@@ -51443,7 +51566,7 @@ msgstr ""
msgid "Status and Reference"
msgstr ""
-#: erpnext/projects/doctype/project/project.py:747
+#: erpnext/projects/doctype/project/project.py:749
msgid "Status must be Cancelled or Completed"
msgstr ""
@@ -51462,6 +51585,7 @@ msgstr ""
#. Name of a Workspace
#. Title of a Workspace Sidebar
#: erpnext/accounts/doctype/account/account.json
+#: erpnext/accounts/doctype/account/account.py:224
#: erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py:11
#: erpnext/accounts/report/account_balance/account_balance.js:57
#: erpnext/desktop_icon/stock.json
@@ -51680,7 +51804,7 @@ msgstr ""
#. Name of a report
#. Label of a Link in the Stock Workspace
#. Label of a Workspace Sidebar Item
-#: erpnext/public/js/controllers/stock_controller.js:67
+#: erpnext/public/js/controllers/stock_controller.js:97
#: erpnext/public/js/utils/ledger_preview.js:37
#: erpnext/stock/doctype/item/item.js:158
#: erpnext/stock/doctype/item/item_dashboard.py:8
@@ -52242,11 +52366,11 @@ msgstr ""
msgid "Stock cannot be reserved in the group warehouse {0}."
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1273
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1279
msgid "Stock cannot be updated against the following Delivery Notes: {0}"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1342
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1348
msgid "Stock cannot be updated because the invoice contains a drop shipping item. Please disable 'Update Stock' or remove the drop shipping item."
msgstr ""
@@ -52895,12 +53019,6 @@ msgstr ""
msgid "Success Redirect URL"
msgstr ""
-#. Label of the success_details (Section Break) field in DocType 'Appointment
-#. Booking Settings'
-#: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json
-msgid "Success Settings"
-msgstr ""
-
#. Option for the 'Depreciation Entry Posting Status' (Select) field in DocType
#. 'Asset'
#: erpnext/assets/doctype/asset/asset.json
@@ -53636,7 +53754,7 @@ msgstr ""
msgid "Synchronize all accounts every hour"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:664
+#: erpnext/accounts/doctype/account/account.py:695
msgid "System In Use"
msgstr ""
@@ -53841,7 +53959,7 @@ msgstr ""
#: erpnext/stock/dashboard/item_dashboard.js:234
#: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
#: erpnext/stock/doctype/material_request_item/material_request_item.json
-#: erpnext/stock/doctype/stock_entry/stock_entry.js:826
+#: erpnext/stock/doctype/stock_entry/stock_entry.js:802
#: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
msgid "Target Warehouse"
msgstr ""
@@ -54907,7 +55025,7 @@ msgstr ""
msgid "The Loyalty Program isn't valid for the selected company"
msgstr ""
-#: erpnext/accounts/doctype/payment_request/payment_request.py:1110
+#: erpnext/accounts/doctype/payment_request/payment_request.py:1111
msgid "The Payment Request {0} is already paid, cannot process payment twice"
msgstr ""
@@ -54949,7 +55067,11 @@ msgstr ""
msgid "The account head under Liability or Equity, in which Profit/Loss will be booked"
msgstr ""
-#: erpnext/accounts/doctype/payment_request/payment_request.py:1005
+#: erpnext/accounts/doctype/account/account.py:222
+msgid "The account type of {0} cannot be changed from {1} because stock ledger entries exist against it."
+msgstr ""
+
+#: erpnext/accounts/doctype/payment_request/payment_request.py:1006
msgid "The allocated amount is greater than the outstanding amount of Payment Request {0}"
msgstr ""
@@ -54971,7 +55093,7 @@ msgstr ""
msgid "The bank account is not a company account. Please select a company account"
msgstr ""
-#: erpnext/controllers/stock_controller.py:1396
+#: erpnext/controllers/stock_controller.py:1397
msgid "The batch {0} is reserved for {1} in the warehouse {2} and the remaining quantity is not enough to cover the reservations. So, cannot proceed with the {3} {4}."
msgstr ""
@@ -55011,7 +55133,7 @@ msgstr ""
msgid "The description of the transaction"
msgstr ""
-#: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py:67
+#: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py:77
msgid "The difference between from time and To Time must be a multiple of Appointment"
msgstr ""
@@ -55247,7 +55369,7 @@ msgstr ""
msgid "The reserved stock will be released. Are you certain you wish to proceed?"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:218
+#: erpnext/accounts/doctype/account/account.py:249
msgid "The root account {0} must be a group"
msgstr ""
@@ -55292,7 +55414,7 @@ msgstr ""
msgid "The shares don't exist with the {0}"
msgstr ""
-#: erpnext/stock/stock_ledger.py:824
+#: erpnext/stock/stock_ledger.py:839
msgid "The stock for the item {0} in the {1} warehouse was negative on the {2}. You should create a positive entry {3} before the date {4} and time {5} to post the correct valuation rate. For more details, please read the documentation."
msgstr ""
@@ -55394,7 +55516,7 @@ msgstr ""
msgid "The {0} ({1}) must be equal to {2} ({3})"
msgstr ""
-#: erpnext/public/js/controllers/transaction.js:3398
+#: erpnext/public/js/controllers/transaction.js:3382
msgid "The {0} contains Unit Price Items."
msgstr ""
@@ -55426,7 +55548,7 @@ msgstr ""
msgid "There are inconsistencies between the rate, no of shares and the amount calculated"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:203
+#: erpnext/accounts/doctype/account/account.py:204
msgid "There are ledger entries against this account. Changing {0} to non-{1} in live system will cause incorrect output in 'Accounts {2}' report"
msgstr ""
@@ -55475,7 +55597,7 @@ msgstr ""
msgid "There can only be 1 Account per Company in {0} {1}"
msgstr ""
-#: erpnext/accounts/doctype/shipping_rule/shipping_rule.py:86
+#: erpnext/accounts/doctype/shipping_rule/shipping_rule.py:85
msgid "There can only be one Shipping Rule Condition with 0 or blank value for \"To Value\""
msgstr ""
@@ -55611,6 +55733,10 @@ msgstr ""
msgid "This document is over limit by {0} {1} for item {4}. Are you making another {3} against the same {2}?"
msgstr ""
+#: erpnext/templates/emails/appointment_confirmed.html:6
+msgid "This email was sent from {0}"
+msgstr ""
+
#: erpnext/stock/doctype/delivery_note/delivery_note.js:496
msgid "This field is used to set the 'Customer'."
msgstr ""
@@ -55753,6 +55879,10 @@ msgstr ""
msgid "This item filter has already been applied for the {0}"
msgstr ""
+#: erpnext/templates/emails/confirm_appointment.html:4
+msgid "This link is valid for {0} minutes"
+msgstr ""
+
#: erpnext/www/banking.py:35
msgid "This method is only meant for developer mode"
msgstr ""
@@ -55793,7 +55923,7 @@ msgstr ""
msgid "This schedule was created when Asset {0} was repaired through Asset Repair {1}."
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1549
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1555
msgid "This schedule was created when Asset {0} was restored due to Sales Invoice {1} cancellation."
msgstr ""
@@ -55805,7 +55935,7 @@ msgstr ""
msgid "This schedule was created when Asset {0} was restored."
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1545
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1551
msgid "This schedule was created when Asset {0} was returned through Sales Invoice {1}."
msgstr ""
@@ -55817,7 +55947,7 @@ msgstr ""
msgid "This schedule was created when Asset {0} was {1} into new Asset {2}."
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1521
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1527
msgid "This schedule was created when Asset {0} was {1} through Sales Invoice {2}."
msgstr ""
@@ -55870,6 +56000,10 @@ msgstr ""
msgid "This value shall be used when no matching Common Code for a record is found."
msgstr ""
+#: erpnext/www/book_appointment/verify/index.py:18
+msgid "This verification link is invalid. Please book the appointment again."
+msgstr ""
+
#: banking/src/components/features/Settings/Preferences.tsx:86
msgid "This will automatically run transaction matching rules on unreconciled transactions every hour."
msgstr ""
@@ -56015,7 +56149,7 @@ msgstr ""
msgid "Time logs are required for {0} {1}"
msgstr ""
-#: erpnext/crm/doctype/appointment/appointment.py:60
+#: erpnext/crm/doctype/appointment/appointment.py:133
msgid "Time slot is not available"
msgstr ""
@@ -56079,7 +56213,7 @@ msgstr ""
msgid "Timesheet for tasks."
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:942
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:948
msgid "Timesheet {0} cannot be invoiced in its current state"
msgstr ""
@@ -56371,11 +56505,11 @@ msgstr ""
msgid "To be Delivered to Customer"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:579
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:580
msgid "To cancel a {} you need to cancel the POS Closing Entry {}."
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:592
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:593
msgid "To cancel this Sales Invoice you need to cancel the POS Closing Entry {}."
msgstr ""
@@ -56410,7 +56544,7 @@ msgstr ""
msgid "To not apply Pricing Rule in a particular transaction, all applicable Pricing Rules should be disabled."
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:553
+#: erpnext/accounts/doctype/account/account.py:584
msgid "To overrule this, enable '{0}' in company {1}"
msgstr ""
@@ -57352,7 +57486,7 @@ msgid "Total hours: {0}"
msgstr ""
#: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:570
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:563
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:564
msgid "Total payments amount can't be greater than {}"
msgstr ""
@@ -57671,16 +57805,17 @@ msgstr ""
msgid "Transactions against the Company already exist! Chart of Accounts can only be imported for a Company with no transactions."
msgstr ""
-#. Description of the 'Credit Limit' (Table) field in DocType 'Customer'
+#. Description of the 'Credit & Overdue Limits' (Table) field in DocType
+#. 'Customer'
#: erpnext/selling/doctype/customer/customer.json
-msgid "Transactions are blocked or warned when outstanding balance exceeds this amount."
+msgid "Transactions are blocked when the outstanding balance exceeds the credit limit. When Restrict Customer Over Billing is enabled, new invoices are also blocked when the customer's overdue amount exceeds the Overdue Limit."
msgstr ""
#: banking/src/components/features/BankStatementImporter/CSV/StatementDetails.tsx:239
msgid "Transactions to be imported into the system"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1206
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1212
msgid "Transactions using Sales Invoice in POS are disabled."
msgstr ""
@@ -57817,7 +57952,7 @@ msgstr ""
msgid "Transit"
msgstr ""
-#: erpnext/stock/doctype/stock_entry/stock_entry.js:611
+#: erpnext/stock/doctype/stock_entry/stock_entry.js:587
msgid "Transit Entry"
msgstr ""
@@ -58159,7 +58294,7 @@ msgstr ""
#: erpnext/stock/report/product_bundle_balance/product_bundle_balance.py:94
#: erpnext/stock/report/stock_ageing/stock_ageing.py:223
#: erpnext/stock/report/stock_analytics/stock_analytics.py:59
-#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:134
+#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:135
#: erpnext/subcontracting/doctype/subcontracting_inward_order_service_item/subcontracting_inward_order_service_item.json
#: erpnext/templates/emails/reorder_item.html:11
#: erpnext/templates/includes/rfq/rfq_items.html:17
@@ -59073,7 +59208,7 @@ msgstr ""
msgid "Use Transaction Date Exchange Rate"
msgstr ""
-#: erpnext/projects/doctype/project/project.py:598
+#: erpnext/projects/doctype/project/project.py:600
msgid "Use a name that is different from previous project name"
msgstr ""
@@ -59223,6 +59358,12 @@ msgstr ""
msgid "Users with this role are allowed to over deliver/receive against orders above the allowance percentage"
msgstr ""
+#. Description of the 'Role Allowed to Bypass Over Billing Restriction' (Link)
+#. field in DocType 'Accounts Settings'
+#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+msgid "Users with this role can still submit invoices for customers who have crossed their Overdue Limit."
+msgstr ""
+
#. Description of the 'Role to Notify on Depreciation Failure' (Link) field in
#. DocType 'Accounts Settings'
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
@@ -59485,11 +59626,11 @@ msgstr ""
msgid "Valuation Rate (In / Out)"
msgstr ""
-#: erpnext/stock/stock_ledger.py:2041
+#: erpnext/stock/stock_ledger.py:2056
msgid "Valuation Rate Missing"
msgstr ""
-#: erpnext/stock/stock_ledger.py:2019
+#: erpnext/stock/stock_ledger.py:2034
msgid "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
msgstr ""
@@ -59713,11 +59854,6 @@ msgstr ""
msgid "Variant {0} and its template {1} cannot both be added to the same Pricing Rule"
msgstr ""
-#. Label of the variants_section (Tab Break) field in DocType 'Item'
-#: erpnext/stock/doctype/item/item.json
-msgid "Variants"
-msgstr ""
-
#. Name of a DocType
#. Label of the vehicle (Link) field in DocType 'Delivery Trip'
#: erpnext/setup/doctype/vehicle/vehicle.json
@@ -59769,16 +59905,31 @@ msgstr ""
msgid "Venture Capital"
msgstr ""
+#. Label of the verification_link_expiry_duration (Int) field in DocType
+#. 'Appointment Booking Settings'
+#: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json
+msgid "Verification Link Expiry Duration"
+msgstr ""
+
+#. Label of the verification_token (Data) field in DocType 'Appointment'
+#: erpnext/crm/doctype/appointment/appointment.json
+msgid "Verification Token"
+msgstr ""
+
#: erpnext/www/book_appointment/verify/index.html:15
msgid "Verification failed please check the link"
msgstr ""
+#: erpnext/www/book_appointment/verify/index.py:38
+msgid "Verification link has expired."
+msgstr ""
+
#. Label of the verified_by (Data) field in DocType 'Quality Inspection'
#: erpnext/stock/doctype/quality_inspection/quality_inspection.json
msgid "Verified By"
msgstr ""
-#: erpnext/templates/emails/confirm_appointment.html:6
+#: erpnext/templates/emails/confirm_appointment.html:7
#: erpnext/www/book_appointment/verify/index.html:4
msgid "Verify Email"
msgstr ""
@@ -60366,7 +60517,7 @@ msgstr ""
msgid "Warehouse not found against the account {0}"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1263
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1269
#: erpnext/stock/doctype/delivery_note/delivery_note.py:415
msgid "Warehouse required for stock Item {0}"
msgstr ""
@@ -60509,7 +60660,7 @@ msgstr ""
msgid "Warning - Row {0}: Billing Hours are more than Actual Hours"
msgstr ""
-#: erpnext/stock/stock_ledger.py:834
+#: erpnext/stock/stock_ledger.py:849
msgid "Warning on Negative Stock"
msgstr ""
@@ -60627,6 +60778,10 @@ msgstr ""
msgid "We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, uncheck the '{2}' checkbox."
msgstr ""
+#: erpnext/templates/emails/appointment_confirmed.html:3
+msgid "We look forward to meeting you"
+msgstr ""
+
#: banking/src/pages/BankStatementImporter.tsx:169
msgid "We support uploading CSV, XLSX, XLS and PDF files. Please make sure the file contains the correct columns."
msgstr ""
@@ -60851,11 +61006,11 @@ msgstr ""
msgid "When you pay for something upfront (like annual insurance), the cost is held here and recognized gradually over time"
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:380
+#: erpnext/accounts/doctype/account/account.py:411
msgid "While creating account for Child Company {0}, parent account {1} found as a ledger account."
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:370
+#: erpnext/accounts/doctype/account/account.py:401
msgid "While creating account for Child Company {0}, parent account {1} not found. Please create the parent account in corresponding COA"
msgstr ""
@@ -61129,8 +61284,8 @@ msgstr ""
msgid "Work Order cannot be raised against a Item Template"
msgstr ""
-#: erpnext/manufacturing/doctype/work_order/work_order.py:2772
-#: erpnext/manufacturing/doctype/work_order/work_order.py:2852
+#: erpnext/manufacturing/doctype/work_order/work_order.py:2779
+#: erpnext/manufacturing/doctype/work_order/work_order.py:2859
msgid "Work Order has been {0}"
msgstr ""
@@ -61486,7 +61641,7 @@ msgstr ""
msgid "You are not authorized to make/edit Stock Transactions for Item {0} under warehouse {1} before this time."
msgstr ""
-#: erpnext/accounts/doctype/account/account.py:312
+#: erpnext/accounts/doctype/account/account.py:343
msgid "You are not authorized to set Frozen value"
msgstr ""
@@ -61502,7 +61657,7 @@ msgstr ""
msgid "You can also add credit or debit values to pre-fill - these support both static values (like 200) or formulas (like transaction_amount * 0.25)."
msgstr ""
-#: erpnext/templates/emails/confirm_appointment.html:10
+#: erpnext/templates/emails/confirm_appointment.html:11
msgid "You can also copy-paste this link in your browser"
msgstr ""
@@ -61510,7 +61665,7 @@ msgstr ""
msgid "You can also set default CWIP account in Company {}"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1064
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1070
msgid "You can change the parent account to a Balance Sheet account or select a different account."
msgstr ""
@@ -61591,7 +61746,7 @@ msgstr ""
msgid "You cannot edit root node."
msgstr ""
-#: erpnext/accounts/doctype/accounts_settings/accounts_settings.py:197
+#: erpnext/accounts/doctype/accounts_settings/accounts_settings.py:203
msgid "You cannot enable both the settings '{0}' and '{1}'."
msgstr ""
@@ -61672,7 +61827,7 @@ msgstr ""
msgid "You have already selected items from {0} {1}"
msgstr ""
-#: erpnext/projects/doctype/project/project.py:365
+#: erpnext/projects/doctype/project/project.py:367
msgid "You have been invited to collaborate on the project {0}."
msgstr ""
@@ -61730,6 +61885,10 @@ msgstr ""
msgid "Your Name (required)"
msgstr ""
+#: erpnext/templates/emails/appointment_confirmed.html:2
+msgid "Your email has been verified and your appointment has been confirmed for {0}"
+msgstr ""
+
#: erpnext/www/book_appointment/verify/index.html:11
msgid "Your email has been verified and your appointment has been scheduled"
msgstr ""
@@ -61794,7 +61953,7 @@ msgstr ""
msgid "`Allow Negative rates for Items`"
msgstr ""
-#: erpnext/stock/stock_ledger.py:2033
+#: erpnext/stock/stock_ledger.py:2048
msgid "after"
msgstr ""
@@ -61835,7 +61994,7 @@ msgid "cannot be greater than 100"
msgstr ""
#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:341
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1152
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1158
msgid "dated {0}"
msgstr ""
@@ -61986,7 +62145,7 @@ msgstr ""
msgid "per hour"
msgstr ""
-#: erpnext/stock/stock_ledger.py:2034
+#: erpnext/stock/stock_ledger.py:2049
msgid "performing either one below:"
msgstr ""
@@ -62019,7 +62178,7 @@ msgstr ""
msgid "reconciled"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1523
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1529
msgid "returned"
msgstr ""
@@ -62054,7 +62213,7 @@ msgstr ""
msgid "sandbox"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1523
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1529
msgid "sold"
msgstr ""
@@ -62081,7 +62240,7 @@ msgstr ""
msgid "to"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3238
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3244
msgid "to unallocate the amount of this Return Invoice before cancelling it."
msgstr ""
@@ -62370,7 +62529,7 @@ msgstr ""
msgid "{0} is in Draft. Submit it before creating the Asset."
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1178
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1184
msgid "{0} is mandatory for Item {1}"
msgstr ""
@@ -62391,7 +62550,7 @@ msgstr ""
msgid "{0} is not a CSV file."
msgstr ""
-#: erpnext/selling/doctype/customer/customer.py:240
+#: erpnext/selling/doctype/customer/customer.py:244
msgid "{0} is not a company bank account"
msgstr ""
@@ -62475,7 +62634,7 @@ msgstr ""
msgid "{0} must be negative in return document"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2423
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2429
msgid "{0} not allowed to transact with {1}. Please change the Company or add the Company in the 'Allowed To Transact With'-Section in the Customer record."
msgstr ""
@@ -62491,7 +62650,7 @@ msgstr ""
msgid "{0} payment entries can not be filtered by {1}"
msgstr ""
-#: erpnext/controllers/stock_controller.py:1817
+#: erpnext/controllers/stock_controller.py:1818
msgid "{0} qty of Item {1} is being received into Warehouse {2} with capacity {3}."
msgstr ""
@@ -62520,16 +62679,16 @@ msgstr ""
msgid "{0} units of {1} are required in {2} with the inventory dimension: {3} on {4} {5} for {6} to complete the transaction."
msgstr ""
-#: erpnext/stock/stock_ledger.py:1686 erpnext/stock/stock_ledger.py:2201
-#: erpnext/stock/stock_ledger.py:2215
+#: erpnext/stock/stock_ledger.py:1701 erpnext/stock/stock_ledger.py:2216
+#: erpnext/stock/stock_ledger.py:2230
msgid "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
msgstr ""
-#: erpnext/stock/stock_ledger.py:2302 erpnext/stock/stock_ledger.py:2347
+#: erpnext/stock/stock_ledger.py:2317 erpnext/stock/stock_ledger.py:2362
msgid "{0} units of {1} needed in {2} on {3} {4} to complete this transaction."
msgstr ""
-#: erpnext/stock/stock_ledger.py:1680
+#: erpnext/stock/stock_ledger.py:1695
msgid "{0} units of {1} needed in {2} to complete this transaction."
msgstr ""
@@ -62816,11 +62975,11 @@ msgstr ""
msgid "{field_label} is mandatory for sub-contracted {doctype}."
msgstr ""
-#: erpnext/controllers/stock_controller.py:2283
+#: erpnext/controllers/stock_controller.py:2284
msgid "{item_name}'s Sample Size ({sample_size}) cannot be greater than the Accepted Quantity ({accepted_quantity})"
msgstr ""
-#: erpnext/controllers/stock_controller.py:2046
+#: erpnext/controllers/stock_controller.py:2047
msgid "{ref_doctype} {ref_name} status is {status}."
msgstr ""
@@ -62828,7 +62987,7 @@ msgstr ""
msgid "{}"
msgstr ""
-#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2189
+#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2195
msgid "{} can't be cancelled since the Loyalty Points earned has been redeemed. First cancel the {} No {}"
msgstr ""
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 6029a073961..f744a38d9f0 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -1003,7 +1003,7 @@ class BOM(WebsiteGenerator):
for d in self.get("items"):
old_rate = d.rate
- if not self.bom_creator and (d.is_stock_item or d.is_phantom_item):
+ if d.is_stock_item or d.is_phantom_item:
d.rate = self.get_rm_rate(
{
"company": self.company,
diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
index 16198e653c6..f589ec1acc4 100644
--- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
+++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
@@ -537,15 +537,8 @@ class BOMCreator(Document):
row.delete()
updated = True
- items = get_children(parent=kwargs.fg_item, parent_id=self.name)
- if items:
- for item in items:
- updated = True
- child_row = next((row for row in self.items if row.name == item.name), None)
- if child_row:
- child_row.delete()
- if item.expandable:
- self.delete_node(fg_item=item.value)
+ if self.delete_child_nodes(kwargs.docname or self.name):
+ updated = True
if updated:
self.set_rate_for_items()
@@ -555,6 +548,19 @@ class BOMCreator(Document):
return frappe._dict()
+ def delete_child_nodes(self, fg_reference_id: str):
+ deleted = False
+ for item in get_children(parent=fg_reference_id, parent_id=self.name):
+ child_row = next((row for row in self.items if row.name == item.name), None)
+ if child_row:
+ child_row.delete()
+
+ deleted = True
+ if item.expandable:
+ self.delete_child_nodes(item.name)
+
+ return deleted
+
@frappe.whitelist()
def get_children(doctype: str | None = None, parent: str | None = None, **kwargs):
@@ -567,7 +573,7 @@ def get_children(doctype: str | None = None, parent: str | None = None, **kwargs
kwargs = frappe._dict(kwargs)
fields = [
- "item_code as value",
+ "name as value",
"item_name as title",
"is_expandable as expandable",
"parent as parent_id",
@@ -575,6 +581,7 @@ def get_children(doctype: str | None = None, parent: str | None = None, **kwargs
"idx",
ValueWrapper("BOM Creator Item").as_("doctype"),
"name",
+ "item_code",
"uom",
"rate",
"amount",
@@ -583,7 +590,7 @@ def get_children(doctype: str | None = None, parent: str | None = None, **kwargs
]
query_filters = {
- "fg_item": parent,
+ "fg_reference_id": parent,
"parent": kwargs.parent_id,
}
diff --git a/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py
index 94a8b0b607b..e11f43e97e6 100644
--- a/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py
+++ b/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py
@@ -241,6 +241,26 @@ class TestBOMCreator(ERPNextTestSuite):
data = frappe.get_all("BOM", filters={"bom_creator": doc.name, "docstatus": 1})
self.assertEqual(len(data), 2)
+ def test_repeated_sub_assembly_keeps_own_raw_materials(self):
+ doc, first_wheel, second_wheel = make_repeated_sub_assembly_bom(
+ "Bicycle BOM with Repeated Sub Assembly"
+ )
+
+ self.assertEqual(child_items(doc, doc.name), ["Frame Assembly", "Seat Assembly"])
+ self.assertEqual(child_items(doc, first_wheel), ["Rim", "Spokes"])
+ self.assertEqual(child_items(doc, second_wheel), ["Hub"])
+
+ def test_delete_repeated_sub_assembly_keeps_sibling_raw_materials(self):
+ doc, first_wheel, second_wheel = make_repeated_sub_assembly_bom(
+ "Bicycle BOM with Deleted Sub Assembly"
+ )
+
+ doc.delete_node(doctype="BOM Creator Item", docname=second_wheel)
+ doc.reload()
+
+ self.assertEqual(child_items(doc, first_wheel), ["Rim", "Spokes"])
+ self.assertFalse([row for row in doc.items if row.item_code == "Hub"])
+
def test_edit_and_delete_reject_unknown_item(self):
final_product = "Bicycle"
make_item(
@@ -327,6 +347,55 @@ def create_items():
)
+def make_repeated_sub_assembly_bom(name):
+ """Bicycle > (Frame Assembly > Wheel Assembly > Rim, Spokes), (Seat Assembly > Wheel Assembly > Hub)"""
+ final_product = "Bicycle"
+ make_item(final_product, {"item_group": "Raw Material", "stock_uom": "Nos"})
+
+ doc = make_bom_creator(
+ name=name,
+ company="_Test Company",
+ item_code=final_product,
+ qty=1,
+ rm_cosy_as_per="Valuation Rate",
+ currency="INR",
+ plc_conversion_rate=1,
+ conversion_rate=1,
+ )
+
+ def add_sub_assembly(fg_item, fg_reference_id, item_code, raw_materials):
+ doc.add_sub_assembly(
+ fg_item=fg_item,
+ fg_reference_id=fg_reference_id,
+ bom_item={
+ "item_code": item_code,
+ "qty": 1,
+ "items": [{"item_code": item, "qty": 1} for item in raw_materials],
+ },
+ )
+ doc.reload()
+
+ return next(
+ row.name
+ for row in doc.items
+ if row.item_code == item_code and row.fg_reference_id == fg_reference_id
+ )
+
+ frame = add_sub_assembly(final_product, doc.name, "Frame Assembly", ["Frame"])
+ first_wheel = add_sub_assembly("Frame Assembly", frame, "Wheel Assembly", ["Rim", "Spokes"])
+
+ seat = add_sub_assembly(final_product, doc.name, "Seat Assembly", ["Seat"])
+ second_wheel = add_sub_assembly("Seat Assembly", seat, "Wheel Assembly", ["Hub"])
+
+ return doc, first_wheel, second_wheel
+
+
+def child_items(doc, parent):
+ from erpnext.manufacturing.doctype.bom_creator.bom_creator import get_children
+
+ return sorted(row.item_code for row in get_children(parent=parent, parent_id=doc.name))
+
+
def make_bom_creator(**kwargs):
if isinstance(kwargs, str) or isinstance(kwargs, dict):
kwargs = frappe.parse_json(kwargs)
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 27d4212cbd1..3b4f8008f08 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -981,18 +981,31 @@ class JobCard(Document):
self.update_work_order_data(for_quantity, process_loss_qty, pending_qty, time_in_mins, wo)
def update_semi_finished_good_details(self):
- if self.operation_id:
- qty = max(flt(self.manufactured_qty), flt(self.total_completed_qty))
+ if not self.operation_id:
+ return
- frappe.db.set_value("Work Order Operation", self.operation_id, "completed_qty", qty)
- if (
- self.finished_good
- and frappe.get_cached_value("Work Order", self.work_order, "production_item")
- == self.finished_good
- ):
- _wo_doc = frappe.get_doc("Work Order", self.work_order)
- _wo_doc.db_set("produced_qty", self.manufactured_qty)
- _wo_doc.db_set("status", _wo_doc.get_status())
+ job_cards = frappe.get_all(
+ "Job Card",
+ filters={
+ "work_order": self.work_order,
+ "operation_id": self.operation_id,
+ "docstatus": 1,
+ "is_corrective_job_card": 0,
+ },
+ fields=["manufactured_qty", "total_completed_qty"],
+ )
+
+ completed_qty = sum(max(flt(row.manufactured_qty), flt(row.total_completed_qty)) for row in job_cards)
+
+ frappe.db.set_value("Work Order Operation", self.operation_id, "completed_qty", completed_qty)
+ if (
+ self.finished_good
+ and frappe.get_cached_value("Work Order", self.work_order, "production_item")
+ == self.finished_good
+ ):
+ _wo_doc = frappe.get_doc("Work Order", self.work_order)
+ _wo_doc.db_set("produced_qty", sum(flt(row.manufactured_qty) for row in job_cards))
+ _wo_doc.db_set("status", _wo_doc.get_status())
def update_corrective_in_work_order(self, wo):
wo.corrective_operation_cost = 0.0
diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py
index f4b3622c9ca..826d558e830 100644
--- a/erpnext/manufacturing/doctype/job_card/test_job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py
@@ -1162,6 +1162,109 @@ class TestJobCard(ERPNextTestSuite):
self.assertEqual(manufacturing_entry.items[2].qty, 9)
self.assertEqual(flt(manufacturing_entry.items[2].basic_rate, 3), 5.278)
+ def test_semi_fg_produced_qty_across_split_job_cards(self):
+ from erpnext.manufacturing.doctype.operation.test_operation import make_operation
+ from erpnext.manufacturing.doctype.work_order.work_order import make_job_card
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ warehouse = "Stores - _TC"
+ rm = make_item("Split JC RM 1", {"is_stock_item": 1}).name
+ fg = make_item("Split JC FG 1", {"is_stock_item": 1}).name
+
+ fg_bom = frappe.new_doc(
+ "BOM",
+ company="_Test Company",
+ item=fg,
+ quantity=1,
+ with_operations=1,
+ track_semi_finished_goods=1,
+ )
+ fg_bom.append("items", {"item_code": rm, "qty": 1, "operation_row_id": 1})
+
+ operation = {
+ "operation": "Split JC Op A",
+ "workstation": "_Test Workstation A",
+ "finished_good": fg,
+ "finished_good_qty": 1,
+ "is_final_finished_good": 1,
+ "sequence_id": 1,
+ "time_in_mins": 60,
+ "source_warehouse": warehouse,
+ "fg_warehouse": warehouse,
+ "skip_material_transfer": 1,
+ }
+ make_workstation(operation)
+ make_operation(operation)
+ fg_bom.append("operations", operation)
+ fg_bom.insert()
+ fg_bom.submit()
+
+ work_order = make_wo_order_test_record(
+ item=fg,
+ qty=8,
+ source_warehouse=warehouse,
+ fg_warehouse=warehouse,
+ bom_no=fg_bom.name,
+ skip_transfer=1,
+ do_not_save=True,
+ )
+ work_order.operations[0].time_in_mins = 60
+ work_order.save()
+ work_order.submit()
+
+ make_stock_entry(item_code=rm, target=warehouse, qty=100, basic_rate=100)
+
+ job_card = frappe.get_doc(
+ "Job Card", frappe.db.get_value("Job Card", {"work_order": work_order.name}, "name")
+ )
+ job_card.for_quantity = 5
+ job_card.append(
+ "time_logs",
+ {"from_time": "2024-02-01 08:00:00", "to_time": "2024-02-01 09:00:00", "completed_qty": 5},
+ )
+ job_card.save()
+ job_card.submit()
+ frappe.get_doc(job_card.make_stock_entry_for_semi_fg_item()).submit()
+
+ work_order.reload()
+ self.assertEqual(flt(work_order.produced_qty), 5)
+
+ make_job_card(
+ work_order.name,
+ [
+ {
+ "name": work_order.operations[0].name,
+ "operation": "Split JC Op A",
+ "qty": 3,
+ "pending_qty": 3,
+ "skip_material_transfer": 1,
+ }
+ ],
+ )
+
+ job_card = frappe.get_doc(
+ "Job Card", frappe.db.get_value("Job Card", {"work_order": work_order.name, "docstatus": 0})
+ )
+ job_card.append(
+ "time_logs",
+ {
+ "from_time": "2024-02-02 08:00:00",
+ "to_time": "2024-02-02 09:00:00",
+ "completed_qty": job_card.for_quantity,
+ },
+ )
+ job_card.save()
+ job_card.submit()
+ frappe.get_doc(job_card.make_stock_entry_for_semi_fg_item()).submit()
+
+ work_order.reload()
+ self.assertEqual(flt(work_order.produced_qty), 8)
+ self.assertEqual(work_order.status, "Completed")
+ self.assertEqual(
+ flt(frappe.db.get_value("Work Order Operation", work_order.operations[0].name, "completed_qty")),
+ 8,
+ )
+
def test_semi_fg_batch_auto_pull_on_manufacture(self):
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
from erpnext.stock.doctype.item.test_item import make_item
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 645ad5d4cc5..97072a8642f 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -32,6 +32,7 @@ from erpnext.manufacturing.doctype.bom.bom import get_children as get_bom_childr
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
+from erpnext.stock.doctype.item.item import get_uom_conv_factor
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import StockReservation
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.stock.utils import get_or_make_bin
@@ -1333,9 +1334,16 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p
def get_uom_conversion_factor(item_code, uom):
- return frappe.db.get_value(
+ item = frappe.get_cached_value("Item", item_code, ["variant_of", "stock_uom"], as_dict=True)
+ conversion_factor = frappe.db.get_value(
"UOM Conversion Detail", {"parent": item_code, "uom": uom}, "conversion_factor"
)
+ if not conversion_factor and item.variant_of:
+ conversion_factor = frappe.db.get_value(
+ "UOM Conversion Detail", {"parent": item.variant_of, "uom": uom}, "conversion_factor"
+ )
+
+ return conversion_factor or get_uom_conv_factor(uom, item.stock_uom)
def get_subitems(
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index c3aaeb28526..1aeb36fe536 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -1730,6 +1730,118 @@ class TestProductionPlan(ERPNextTestSuite):
self.assertTrue(row.warehouse == mrp_warhouse)
self.assertEqual(row.quantity, 12.0)
+ def test_purchase_uom_falls_back_to_uom_conversion_factor(self):
+ from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
+
+ if not frappe.db.exists("UOM Conversion Factor", {"from_uom": "Kg", "to_uom": "Gram"}):
+ frappe.get_doc(
+ doctype="UOM Conversion Factor",
+ category="Mass",
+ from_uom="Kg",
+ to_uom="Gram",
+ value=1000,
+ ).insert()
+
+ rm = make_item("Test RM Item Global CF", {"is_stock_item": 1, "stock_uom": "Gram"})
+ rm.purchase_uom = "Kg"
+ rm.save()
+ self.assertFalse([row for row in rm.uoms if row.uom == "Kg"])
+
+ bom_tree = {"Test FG Item Global CF": {rm.name: {}}}
+ parent_bom = create_nested_bom(bom_tree, prefix="")
+
+ plan = create_production_plan(
+ item_code=parent_bom.item,
+ planned_qty=2000,
+ ignore_existing_ordered_qty=1,
+ skip_getting_mr_items=1,
+ do_not_submit=1,
+ warehouse="_Test Warehouse - _TC",
+ )
+ plan.for_warehouse = "_Test Warehouse - _TC"
+
+ items = get_items_for_material_requests(
+ plan.as_dict(), warehouses=[{"warehouse": "_Test Warehouse - _TC"}]
+ )
+
+ row = frappe._dict(next(item for item in items if item["item_code"] == rm.name))
+ self.assertEqual(row.uom, "Kg")
+ self.assertEqual(row.conversion_factor, 1000)
+ self.assertEqual(row.quantity, 2)
+
+ def test_variant_inherits_purchase_uom_conversion_factor_of_template(self):
+ from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
+
+ template = make_item(
+ "TRMVCF",
+ {
+ "is_stock_item": 1,
+ "stock_uom": "Nos",
+ "has_variants": 1,
+ "attributes": [{"attribute": "Colour"}],
+ },
+ )
+ if not [row for row in template.uoms if row.uom == "Box"]:
+ template.purchase_uom = "Box"
+ template.append("uoms", {"uom": "Box", "conversion_factor": 12})
+ template.save()
+
+ if not frappe.db.exists("Item", "TRMVCF-RED"):
+ create_variant("TRMVCF", {"Colour": "Red"}).insert()
+
+ variant = frappe.get_doc("Item", "TRMVCF-RED")
+ variant.uoms = [row for row in variant.uoms if row.uom != "Box"]
+ variant.purchase_uom = "Box"
+ variant.save()
+
+ bom_tree = {"Test FG Item Variant CF": {variant.name: {}}}
+ parent_bom = create_nested_bom(bom_tree, prefix="")
+
+ plan = create_production_plan(
+ item_code=parent_bom.item,
+ planned_qty=24,
+ ignore_existing_ordered_qty=1,
+ skip_getting_mr_items=1,
+ do_not_submit=1,
+ warehouse="_Test Warehouse - _TC",
+ )
+ plan.for_warehouse = "_Test Warehouse - _TC"
+
+ items = get_items_for_material_requests(
+ plan.as_dict(), warehouses=[{"warehouse": "_Test Warehouse - _TC"}]
+ )
+
+ row = frappe._dict(next(item for item in items if item["item_code"] == variant.name))
+ self.assertEqual(row.conversion_factor, 12)
+ self.assertEqual(row.quantity, 2)
+
+ def test_missing_purchase_uom_conversion_factor_throws(self):
+ from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
+
+ rm = make_item("Test RM Item Missing CF", {"is_stock_item": 1, "stock_uom": "Nos"})
+ rm.purchase_uom = "Box"
+ rm.save()
+
+ bom_tree = {"Test FG Item Missing CF": {rm.name: {}}}
+ parent_bom = create_nested_bom(bom_tree, prefix="")
+
+ plan = create_production_plan(
+ item_code=parent_bom.item,
+ planned_qty=10,
+ ignore_existing_ordered_qty=1,
+ skip_getting_mr_items=1,
+ do_not_submit=1,
+ warehouse="_Test Warehouse - _TC",
+ )
+ plan.for_warehouse = "_Test Warehouse - _TC"
+
+ with self.assertRaises(frappe.ValidationError) as error:
+ get_items_for_material_requests(
+ plan.as_dict(), warehouses=[{"warehouse": "_Test Warehouse - _TC"}]
+ )
+
+ self.assertIn("UOM Conversion factor", str(error.exception))
+
def test_mr_qty_for_complex_bom(self):
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
diff --git a/erpnext/manufacturing/doctype/routing/routing.js b/erpnext/manufacturing/doctype/routing/routing.js
index 83b81690ec3..44103f210c2 100644
--- a/erpnext/manufacturing/doctype/routing/routing.js
+++ b/erpnext/manufacturing/doctype/routing/routing.js
@@ -79,6 +79,11 @@ frappe.ui.form.on("BOM Operation", {
const d = locals[cdt][cdn];
frm.events.calculate_operating_cost(frm, d);
},
+
+ hour_rate: function (frm, cdt, cdn) {
+ const d = locals[cdt][cdn];
+ frm.events.calculate_operating_cost(frm, d);
+ },
});
frappe.tour["Routing"] = [
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index e17d94d4239..eecf06b15c4 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -2554,7 +2554,13 @@ def get_item_details(item, project=None, skip_bom_info=False, throw=True):
@frappe.whitelist()
def make_work_order(
- bom_no, item, qty=0, company=None, project=None, variant_items=None, use_multi_level_bom=None
+ bom_no: str,
+ item: str,
+ qty: float = 0,
+ company: str | None = None,
+ project: str | None = None,
+ variant_items: str | list | None = None,
+ use_multi_level_bom: bool | None = None,
):
from erpnext import get_default_company
@@ -2563,7 +2569,8 @@ def make_work_order(
item_details = get_item_details(item, project)
- if frappe.db.get_value("Item", item, "variant_of"):
+ # selected BOM already belongs to this variant — keep it
+ if frappe.db.get_value("Item", item, "variant_of") and frappe.db.get_value("BOM", bom_no, "item") != item:
if variant_bom := frappe.db.get_value(
"BOM",
{"item": item, "is_default": 1, "docstatus": 1},
diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py
index 21dcc771213..dec3e8f5817 100644
--- a/erpnext/manufacturing/doctype/workstation/test_workstation.py
+++ b/erpnext/manufacturing/doctype/workstation/test_workstation.py
@@ -80,7 +80,7 @@ class TestWorkstation(ERPNextTestSuite):
test_routing_operations = [
{"operation": "Test Operation A", "workstation": "_Test Workstation A", "time_in_mins": 60},
- {"operation": "Test Operation B", "workstation": "_Test Workstation A", "time_in_mins": 60},
+ {"operation": "Test Operation B", "workstation": "_Test Workstation A", "time_in_mins": 30},
]
routing_doc = create_routing(routing_name="Routing Test", operations=test_routing_operations)
bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency="INR")
@@ -110,6 +110,17 @@ class TestWorkstation(ERPNextTestSuite):
self.assertEqual(bom_doc.operations[0].hour_rate, 250)
self.assertEqual(bom_doc.operations[1].hour_rate, 250)
+ # hour_rate propagation must also refresh operating_cost (hour_rate * time_in_mins / 60)
+ # on the Routing's BOM Operation rows; the 30-min op exercises the arithmetic.
+ for operation, expected_operating_cost in (("Test Operation A", 250), ("Test Operation B", 125)):
+ hour_rate, operating_cost = frappe.db.get_value(
+ "BOM Operation",
+ {"parent": routing_doc.name, "parenttype": "Routing", "operation": operation},
+ ["hour_rate", "operating_cost"],
+ )
+ self.assertEqual(hour_rate, 250)
+ self.assertEqual(operating_cost, expected_operating_cost)
+
def make_workstation(*args, **kwargs):
args = args if args else kwargs
diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py
index 98076e18074..3a0fc175142 100644
--- a/erpnext/manufacturing/doctype/workstation/workstation.py
+++ b/erpnext/manufacturing/doctype/workstation/workstation.py
@@ -195,9 +195,10 @@ class Workstation(Document):
for bom_no in bom_list:
frappe.db.sql(
- """update `tabBOM Operation` set hour_rate = %s
+ """update `tabBOM Operation`
+ set hour_rate = %s, operating_cost = %s * time_in_mins / 60
where parent = %s and workstation = %s""",
- (self.hour_rate, bom_no[0], self.name),
+ (self.hour_rate, self.hour_rate, bom_no[0], self.name),
)
def validate_workstation_holiday(self, schedule_date, skip_holiday_list_check=False):
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index d39fb36c2f6..7be752b8694 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -491,3 +491,6 @@ erpnext.patches.v15_0.backfill_sla_link_filters_on_docfield
erpnext.patches.v16_0.crm_settings_handle_allowed_users_for_frappe_crm
erpnext.patches.v16_0.backfill_pick_list_transferred_qty
erpnext.patches.v16_0.access_control_for_project_users
+erpnext.patches.v16_0.enable_book_stock_expense_gl_entries
+erpnext.patches.v16_0.rename_ar_ap_ageing_filter
+erpnext.patches.v16_0.fix_subcontracting_titles
diff --git a/erpnext/patches/v16_0/enable_book_stock_expense_gl_entries.py b/erpnext/patches/v16_0/enable_book_stock_expense_gl_entries.py
new file mode 100644
index 00000000000..c21add4e073
--- /dev/null
+++ b/erpnext/patches/v16_0/enable_book_stock_expense_gl_entries.py
@@ -0,0 +1,10 @@
+import frappe
+
+
+def execute():
+ has_expense_accounts = frappe.db.exists(
+ "Company", {"purchase_expense_account": ("is", "set")}
+ ) or frappe.db.exists("Item Default", {"purchase_expense_account": ("is", "set")})
+
+ if has_expense_accounts:
+ frappe.db.set_single_value("Accounts Settings", "book_stock_expense_gl_entries", 1)
diff --git a/erpnext/patches/v16_0/fix_subcontracting_titles.py b/erpnext/patches/v16_0/fix_subcontracting_titles.py
new file mode 100644
index 00000000000..71c55bfc80e
--- /dev/null
+++ b/erpnext/patches/v16_0/fix_subcontracting_titles.py
@@ -0,0 +1,28 @@
+import frappe
+
+
+def execute():
+ """
+ This patch corrects the titles of the subcontracting order doctypes set to
+ the text strings "{customer_name}" or "{supplier_name}" instead of the
+ actual customer or supplier name.
+
+ Their `title_field` never pointed at `title`, so the template default was
+ stored verbatim instead of being substituted.
+ """
+
+ party_fields = {
+ "Subcontracting Order": "supplier_name",
+ "Subcontracting Inward Order": "customer_name",
+ }
+
+ for doctype, party_field in party_fields.items():
+ if not frappe.db.has_column(doctype, "title"):
+ continue
+
+ table = frappe.qb.DocType(doctype)
+ (
+ frappe.qb.update(table)
+ .set(table.title, table[party_field])
+ .where(table.title == f"{{{party_field}}}")
+ ).run()
diff --git a/erpnext/patches/v16_0/rename_ar_ap_ageing_filter.py b/erpnext/patches/v16_0/rename_ar_ap_ageing_filter.py
new file mode 100644
index 00000000000..8252c3b0aac
--- /dev/null
+++ b/erpnext/patches/v16_0/rename_ar_ap_ageing_filter.py
@@ -0,0 +1,45 @@
+import frappe
+
+REPORTS = (
+ "Accounts Receivable",
+ "Accounts Payable",
+ "Accounts Receivable Summary",
+ "Accounts Payable Summary",
+)
+
+
+def execute():
+ # filter `calculate_ageing_with` -> `age_as_on`, option "Today Date" -> "Today"
+ _migrate("Auto Email Report", "filters", "report")
+ _migrate("Dashboard Chart", "filters_json", "report_name", type_field="chart_type")
+ _migrate("Number Card", "filters_json", "report_name", type_field="type")
+
+
+def _migrate(doctype, filter_field, report_field, type_field=None):
+ conditions = {report_field: ("in", REPORTS)}
+ if type_field:
+ conditions[type_field] = "Report"
+
+ for row in frappe.get_all(doctype, filters=conditions, fields=["name", filter_field]):
+ updated = _rewrite(row.get(filter_field))
+ if updated is not None:
+ frappe.db.set_value(doctype, row.name, filter_field, updated, update_modified=False)
+
+
+def _rewrite(raw):
+ if not raw:
+ return None
+
+ try:
+ filters = frappe.parse_json(raw)
+ except ValueError:
+ return None
+
+ if not isinstance(filters, dict) or "calculate_ageing_with" not in filters:
+ return None
+
+ filters["age_as_on"] = filters.pop("calculate_ageing_with")
+ if filters["age_as_on"] == "Today Date":
+ filters["age_as_on"] = "Today"
+
+ return frappe.as_json(filters, indent=None)
diff --git a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js
index 07871687006..297f6be4ba3 100644
--- a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js
+++ b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js
@@ -57,6 +57,7 @@ class BOMConfigurator {
breadcrumb: "Manufacturing",
get_tree_nodes: "erpnext.manufacturing.doctype.bom_creator.bom_creator.get_children",
root_label: this.frm.doc.item_code,
+ get_label: (node) => this.get_node_label(node),
disable_add_node: true,
get_tree_root: false,
show_expand_all: false,
@@ -66,6 +67,23 @@ class BOMConfigurator {
};
}
+ get_node_label(node) {
+ const item_code = this.get_item_code(node);
+ const item_name = node.data?.title || item_code;
+
+ if (item_name === item_code) {
+ return frappe.utils.escape_html(item_code);
+ }
+
+ return `${frappe.utils.escape_html(item_name)} (${frappe.utils.escape_html(
+ item_code
+ )})`;
+ }
+
+ get_item_code(node) {
+ return node.data?.item_code || this.frm.doc.item_code;
+ }
+
tree_methods() {
let frm_obj = this;
let view = frappe.views.trees["BOM Configurator"];
@@ -73,7 +91,8 @@ class BOMConfigurator {
return {
onload: function (me) {
me.args["parent_id"] = frm_obj.frm.doc.name;
- me.args["parent"] = frm_obj.frm.doc.item_code;
+ me.args["parent"] = frm_obj.frm.doc.name;
+ me.root_value = frm_obj.frm.doc.name;
me.parent = frm_obj.$wrapper.get(0);
me.body = frm_obj.$wrapper.get(0);
me.make_tree();
@@ -83,7 +102,7 @@ class BOMConfigurator {
const uom = node.data.uom || frm_obj.frm.doc.uom;
const docname = node.data.name || frm_obj.frm.doc.name;
let amount = node.data.amount;
- if (node.data.value === frm_obj.frm.doc.item_code) {
+ if (node.is_root) {
amount = frm_obj.frm.doc.raw_material_cost;
}
@@ -243,7 +262,7 @@ class BOMConfigurator {
method: "add_item",
doc: this.frm.doc,
args: {
- fg_item: node.data.value,
+ fg_item: this.get_item_code(node),
item_code: data.item_code,
fg_reference_id: node.data.name || this.frm.doc.name,
qty: data.qty,
@@ -298,7 +317,7 @@ class BOMConfigurator {
method: "add_sub_assembly",
doc: this.frm.doc,
args: {
- fg_item: node.data.value,
+ fg_item: this.get_item_code(node),
fg_reference_id: node.data.name || this.frm.doc.name,
bom_item: bom_item,
operation: node.data.operation,
@@ -417,7 +436,7 @@ class BOMConfigurator {
});
dialog.set_values({
- item_code: node.data.value,
+ item_code: this.get_item_code(node),
qty: node.data.qty,
});
@@ -445,7 +464,7 @@ class BOMConfigurator {
method: "add_sub_assembly",
doc: this.frm.doc,
args: {
- fg_item: node.data.value,
+ fg_item: this.get_item_code(node),
bom_item: bom_item,
fg_reference_id: node.data.name || this.frm.doc.name,
convert_to_sub_assembly: true,
@@ -482,7 +501,6 @@ class BOMConfigurator {
method: "delete_node",
doc: this.frm.doc,
args: {
- fg_item: node.data.value,
doctype: node.data.doctype,
docname: node.data.name,
},
diff --git a/erpnext/public/js/controllers/stock_controller.js b/erpnext/public/js/controllers/stock_controller.js
index a205412e75d..eef7f2f0a37 100644
--- a/erpnext/public/js/controllers/stock_controller.js
+++ b/erpnext/public/js/controllers/stock_controller.js
@@ -11,6 +11,36 @@ erpnext.stock.StockController = class StockController extends frappe.ui.form.Con
}
}
+ onload_post_render() {
+ this.set_route_options_for_new_doc();
+ }
+
+ set_route_options_for_new_doc() {
+ // While creating a Batch or Serial and Batch Bundle from the link
+ // field, copy details from the line item to the new form
+ if (!this.frm.fields_dict.items) return;
+
+ let batch_no_field = this.frm.get_docfield("items", "batch_no");
+ if (batch_no_field) {
+ batch_no_field.get_route_options_for_new_doc = (row) => {
+ return {
+ item: row.doc.item_code,
+ };
+ };
+ }
+
+ let sbb_field = this.frm.get_docfield("items", "serial_and_batch_bundle");
+ if (sbb_field) {
+ sbb_field.get_route_options_for_new_doc = (row) => {
+ return {
+ item_code: row.doc.item_code,
+ warehouse: row.doc.warehouse || row.doc.s_warehouse || row.doc.t_warehouse,
+ voucher_type: this.frm.doc.doctype,
+ };
+ };
+ }
+ }
+
barcode(doc, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.barcode) {
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 63d450de221..ad110712d71 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -649,34 +649,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
erpnext.toggle_serial_batch_fields(this.frm);
}
- set_route_options_for_new_doc() {
- // While creating the batch from the link field, copy item from line item to batch form
-
- if (this.frm.fields_dict["items"].grid.get_field("batch_no")) {
- let batch_no_field = this.frm.get_docfield("items", "batch_no");
- if (batch_no_field) {
- batch_no_field.get_route_options_for_new_doc = function (row) {
- return {
- item: row.doc.item_code,
- };
- };
- }
- }
-
- // While creating the SABB from the link field, copy item, doctype from line item to SABB form
- if (this.frm.fields_dict["items"].grid.get_field("serial_and_batch_bundle")) {
- let sbb_field = this.frm.get_docfield("items", "serial_and_batch_bundle");
- if (sbb_field) {
- sbb_field.get_route_options_for_new_doc = (row) => {
- return {
- item_code: row.doc.item_code,
- voucher_type: this.frm.doc.doctype,
- };
- };
- }
- }
- }
-
scan_barcode() {
frappe.flags.dialog_set = false;
this.barcode_scanner.process_scan();
diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js
index 87a1dd5c766..b4ee5c89e71 100644
--- a/erpnext/public/js/financial_statements.js
+++ b/erpnext/public/js/financial_statements.js
@@ -41,6 +41,26 @@ erpnext.financial_statements = {
_is_special_view: function (column, data) {
if (!data) return false;
const view = get_filter_value("selected_view");
+
+ if (!["Growth", "Margin"].includes(view)) return false;
+
+ if (get_filter_value("report_template")) {
+ const columnInfo = erpnext.financial_statements._parse_column_info(column.fieldname, data);
+ // Account column
+ if (columnInfo.isAccount) return false;
+
+ const periodKeys = data._segment_info?.period_keys || [];
+
+ if (!periodKeys.includes(columnInfo.fieldname)) return false;
+
+ if (view === "Growth") {
+ // First period of new segment
+ if (periodKeys[0] === columnInfo.fieldname) return false;
+ }
+
+ return true;
+ }
+
return (view === "Growth" && column.colIndex >= 3) || (view === "Margin" && column.colIndex >= 2);
},
diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json
index 5556ab927b4..f5803a28276 100644
--- a/erpnext/selling/doctype/customer/customer.json
+++ b/erpnext/selling/doctype/customer/customer.json
@@ -475,10 +475,10 @@
"report_hide": 1
},
{
- "description": "Transactions are blocked or warned when outstanding balance exceeds this amount.",
+ "description": "Transactions are blocked when the outstanding balance exceeds the credit limit. When Restrict Customer Over Billing is enabled, new invoices are also blocked when the customer's overdue amount exceeds the Overdue Limit.",
"fieldname": "credit_limits",
"fieldtype": "Table",
- "label": "Credit Limit",
+ "label": "Credit & Overdue Limits",
"options": "Customer Credit Limit",
"show_description_on_click": 1
},
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index 971170d8335..126145792dd 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -16,7 +16,7 @@ from frappe.model.mapper import get_mapped_doc
from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_options
from frappe.model.utils.rename_doc import update_linked_doctypes
from frappe.query_builder import Field, functions
-from frappe.utils import cint, cstr, flt, get_formatted_email, today
+from frappe.utils import cint, cstr, flt, fmt_money, get_formatted_email, getdate, today
from frappe.utils.user import get_users_with_role
from erpnext.accounts.party import (
@@ -204,17 +204,21 @@ class Customer(TransactionBase):
self.credit_limits = []
self.payment_terms = self.default_price_list = ""
- tables = [["accounts", "account"], ["credit_limits", "credit_limit"]]
+ tables = [
+ ["accounts", ["account"]],
+ ["credit_limits", ["credit_limit", "overdue_billing_threshold"]],
+ ]
fields = ["payment_terms", "default_price_list"]
for row in tables:
- table, field = row[0], row[1]
+ table, table_fields = row[0], row[1]
if not doc.get(table):
continue
for entry in doc.get(table):
child = self.append(table)
- child.update({"company": entry.company, field: entry.get(field)})
+ child.update({"company": entry.company})
+ child.update({field: entry.get(field) for field in table_fields})
for field in fields:
if not doc.get(field):
@@ -403,6 +407,9 @@ class Customer(TransactionBase):
else:
company_record.append(limit.company)
+ if not flt(limit.credit_limit):
+ continue
+
outstanding_amt = get_customer_outstanding(
self.name, limit.company, ignore_outstanding_sales_order=limit.bypass_credit_limit_check
)
@@ -674,6 +681,124 @@ def send_emails(customer, customer_outstanding, credit_limit, credit_controller_
frappe.sendmail(recipients=credit_controller_users_list, subject=subject, message=message)
+def check_overdue_billing_threshold(customer: str, company: str) -> None:
+ if not frappe.get_single_value("Accounts Settings", "enable_overdue_billing_threshold"):
+ return
+
+ threshold = get_overdue_billing_threshold(customer, company)
+ if not threshold:
+ return
+
+ overdue_amount = get_customer_overdue_amount(customer, company)
+ if overdue_amount <= threshold:
+ return
+
+ bypass_role = frappe.get_single_value("Accounts Settings", "role_allowed_to_bypass_overdue_billing")
+ if bypass_role and bypass_role in frappe.get_roles():
+ return
+
+ company_currency = frappe.get_cached_value("Company", company, "default_currency")
+ frappe.throw(
+ _("Overdue Limit crossed for customer {0}. Overdue amount {1} exceeds the allowed limit {2}.").format(
+ customer,
+ fmt_money(overdue_amount, currency=company_currency),
+ fmt_money(threshold, currency=company_currency),
+ ),
+ title=_("Overdue Limit Crossed"),
+ )
+
+
+def get_overdue_billing_threshold(customer: str, company: str) -> float:
+ """Overdue limit set on the customer, falling back to its customer group."""
+ threshold = frappe.db.get_value(
+ "Customer Credit Limit",
+ {"parent": customer, "parenttype": "Customer", "company": company},
+ "overdue_billing_threshold",
+ )
+
+ if not threshold:
+ customer_group = frappe.get_cached_value("Customer", customer, "customer_group")
+ threshold = frappe.db.get_value(
+ "Customer Credit Limit",
+ {"parent": customer_group, "parenttype": "Customer Group", "company": company},
+ "overdue_billing_threshold",
+ )
+
+ return flt(threshold)
+
+
+def get_customer_overdue_amount(customer: str, company: str) -> float:
+ """Amount the customer owes past its due date, in company currency.
+
+ Follows the same rule as the Overdue invoice status, so a customer is only
+ blocked for what the invoice list already shows as overdue.
+ """
+ invoices = get_outstanding_invoices_for_customer(customer, company)
+ if not invoices:
+ return 0.0
+
+ payable_amounts = get_past_due_payable_amounts([d.name for d in invoices])
+ return flt(sum(get_overdue_portion(d, payable_amounts.get(d.name)) for d in invoices))
+
+
+def get_outstanding_invoices_for_customer(customer: str, company: str) -> list[frappe._dict]:
+ from frappe.query_builder.functions import Sum
+
+ gl_entry = frappe.qb.DocType("GL Entry")
+ sales_invoice = frappe.qb.DocType("Sales Invoice")
+
+ # debit - credit is always booked in company currency, so this is comparable to the overdue limit
+ outstanding = Sum(gl_entry.debit) - Sum(gl_entry.credit)
+
+ return (
+ frappe.qb.from_(gl_entry)
+ .inner_join(sales_invoice)
+ .on(sales_invoice.name == gl_entry.against_voucher)
+ .select(
+ sales_invoice.name,
+ sales_invoice.due_date,
+ sales_invoice.base_grand_total,
+ outstanding.as_("outstanding"),
+ )
+ .where(gl_entry.party_type == "Customer")
+ .where(gl_entry.party == customer)
+ .where(gl_entry.company == company)
+ .where(gl_entry.is_cancelled == 0)
+ .where(gl_entry.against_voucher_type == "Sales Invoice")
+ .groupby(sales_invoice.name, sales_invoice.due_date, sales_invoice.base_grand_total)
+ .having(outstanding > 0)
+ ).run(as_dict=True)
+
+
+def get_past_due_payable_amounts(invoices: list[str]) -> dict[str, float]:
+ from frappe.query_builder.functions import Sum
+
+ payment_schedule = frappe.qb.DocType("Payment Schedule")
+
+ rows = (
+ frappe.qb.from_(payment_schedule)
+ .select(payment_schedule.parent, Sum(payment_schedule.base_payment_amount).as_("payable"))
+ .where(payment_schedule.parenttype == "Sales Invoice")
+ .where(payment_schedule.parent.isin(invoices))
+ .where(payment_schedule.due_date < getdate())
+ .groupby(payment_schedule.parent)
+ ).run(as_dict=True)
+
+ return {d.parent: flt(d.payable) for d in rows}
+
+
+def get_overdue_portion(invoice: frappe._dict, payable_amount: float | None) -> float:
+ outstanding = flt(invoice.outstanding)
+
+ # No payable amount means either a schedule-less invoice (POS, opening) or one whose terms are
+ # all still in the future. Both are answered by the invoice due date, which is the last term.
+ if payable_amount is None:
+ return outstanding if invoice.due_date and getdate(invoice.due_date) < getdate() else 0.0
+
+ paid = flt(invoice.base_grand_total) - outstanding
+ return min(max(payable_amount - paid, 0.0), outstanding)
+
+
def get_customer_outstanding(customer, company, ignore_outstanding_sales_order=False, cost_center=None):
# Outstanding based on GL Entries
cond = ""
diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py
index 015c4467645..e7fcf2519fd 100644
--- a/erpnext/selling/doctype/customer/test_customer.py
+++ b/erpnext/selling/doctype/customer/test_customer.py
@@ -5,13 +5,15 @@
import json
import frappe
-from frappe.utils import flt, nowdate
+from frappe.utils import add_days, flt, getdate, nowdate
from erpnext.accounts.party import get_due_date
from erpnext.exceptions import PartyDisabled, PartyFrozen
from erpnext.selling.doctype.customer.customer import (
get_credit_limit,
get_customer_outstanding,
+ get_customer_overdue_amount,
+ get_overdue_billing_threshold,
make_quotation,
parse_full_name,
)
@@ -77,7 +79,11 @@ class TestCustomer(ERPNextTestSuite):
"company": "_Test Company",
"account": "Creditors - _TC",
}
- test_credit_limits = {"company": "_Test Company", "credit_limit": 350000}
+ test_credit_limits = {
+ "company": "_Test Company",
+ "credit_limit": 350000,
+ "overdue_billing_threshold": 5000,
+ }
doc.append("accounts", test_account_details)
doc.append("credit_limits", test_credit_limits)
doc.insert()
@@ -97,6 +103,7 @@ class TestCustomer(ERPNextTestSuite):
self.assertEqual(c_doc.credit_limits[0].company, "_Test Company")
self.assertEqual(c_doc.credit_limits[0].credit_limit, 350000)
+ self.assertEqual(c_doc.credit_limits[0].overdue_billing_threshold, 5000)
c_doc.delete()
doc.delete()
@@ -352,6 +359,139 @@ class TestCustomer(ERPNextTestSuite):
)
self.assertRaises(frappe.ValidationError, customer.save)
+ def test_get_customer_overdue_amount(self):
+ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+
+ baseline = get_customer_overdue_amount("_Test Customer", "_Test Company")
+
+ # a past-due, unpaid invoice adds its outstanding to the overdue amount
+ create_sales_invoice(qty=1, rate=500, posting_date=add_days(nowdate(), -30))
+ self.assertEqual(get_customer_overdue_amount("_Test Customer", "_Test Company"), baseline + 500)
+
+ # an invoice due today (not yet past due) does not
+ create_sales_invoice(qty=1, rate=700, posting_date=nowdate())
+ self.assertEqual(get_customer_overdue_amount("_Test Customer", "_Test Company"), baseline + 500)
+
+ def test_get_customer_overdue_amount_is_in_company_currency(self):
+ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+
+ baseline = get_customer_overdue_amount("_Test Customer USD", "_Test Company")
+
+ # 100 USD at a conversion rate of 50 must be counted as 5000 in company currency
+ create_sales_invoice(
+ customer="_Test Customer USD",
+ debit_to="_Test Receivable USD - _TC",
+ currency="USD",
+ conversion_rate=50,
+ qty=1,
+ rate=100,
+ posting_date=add_days(nowdate(), -30),
+ )
+
+ self.assertEqual(get_customer_overdue_amount("_Test Customer USD", "_Test Company"), baseline + 5000)
+
+ def test_get_customer_overdue_amount_follows_payment_terms(self):
+ from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
+ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+
+ def make_invoice_with_terms():
+ si = create_sales_invoice(
+ qty=1, rate=1200, posting_date=add_days(nowdate(), -60), do_not_save=True
+ )
+ si.append("payment_schedule", {"due_date": add_days(nowdate(), -60), "invoice_portion": 50})
+ si.append("payment_schedule", {"due_date": add_days(nowdate(), 30), "invoice_portion": 50})
+ si.insert()
+ si.submit()
+ return si
+
+ baseline = get_customer_overdue_amount("_Test Customer", "_Test Company")
+
+ # only the term that has fallen due counts, not the whole 1200 balance. The invoice due_date
+ # is the last term (in 30 days), so this is only caught by reading the payment schedule.
+ si = make_invoice_with_terms()
+ self.assertEqual(getdate(si.due_date), getdate(add_days(nowdate(), 30)))
+ self.assertEqual(get_customer_overdue_amount("_Test Customer", "_Test Company"), baseline + 600)
+
+ # paying off the past-due term clears the overdue amount
+ pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
+ pe.reference_no = "_Test Overdue Payment"
+ pe.reference_date = nowdate()
+ pe.paid_amount = pe.received_amount = 600
+ pe.references[0].allocated_amount = 600
+ pe.insert()
+ pe.submit()
+ self.assertEqual(get_customer_overdue_amount("_Test Customer", "_Test Company"), baseline)
+
+ def test_overdue_billing_threshold_on_submit(self):
+ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+
+ create_sales_invoice(qty=1, rate=1000, posting_date=add_days(nowdate(), -30))
+ overdue = get_customer_overdue_amount("_Test Customer", "_Test Company")
+
+ settings = frappe.get_single("Accounts Settings")
+ original_enable = settings.enable_overdue_billing_threshold
+ original_bypass_role = settings.role_allowed_to_bypass_overdue_billing
+ try:
+ settings.enable_overdue_billing_threshold = 1
+ settings.role_allowed_to_bypass_overdue_billing = None
+ settings.save()
+ set_overdue_billing_threshold("_Test Customer", "_Test Company", overdue - 100)
+
+ # overdue is over the threshold and the user has no bypass role -> blocked
+ si = create_sales_invoice(do_not_submit=True)
+ self.assertRaises(frappe.ValidationError, si.submit)
+
+ # a user holding the bypass role can still submit
+ settings.role_allowed_to_bypass_overdue_billing = "Accounts Manager"
+ settings.save()
+ si = create_sales_invoice(do_not_submit=True)
+ si.submit()
+ self.assertEqual(si.docstatus, 1)
+
+ # threshold still crossed, but the feature is off -> never blocked
+ settings.enable_overdue_billing_threshold = 0
+ settings.role_allowed_to_bypass_overdue_billing = None
+ settings.save()
+ si = create_sales_invoice(do_not_submit=True)
+ si.submit()
+ self.assertEqual(si.docstatus, 1)
+ finally:
+ settings.enable_overdue_billing_threshold = original_enable
+ settings.role_allowed_to_bypass_overdue_billing = original_bypass_role
+ settings.save()
+
+ def test_overdue_billing_threshold_falls_back_to_customer_group(self):
+ customer_group = frappe.get_cached_value("Customer", "_Test Customer", "customer_group")
+ group = frappe.get_doc("Customer Group", customer_group)
+ group.credit_limits = []
+ group.append("credit_limits", {"company": "_Test Company", "overdue_billing_threshold": 5000})
+ group.save()
+
+ # the customer has no threshold of its own, so the group's applies
+ self.assertEqual(get_overdue_billing_threshold("_Test Customer", "_Test Company"), 5000)
+
+ # a threshold on the customer wins over the group
+ set_overdue_billing_threshold("_Test Customer", "_Test Company", 2000)
+ self.assertEqual(get_overdue_billing_threshold("_Test Customer", "_Test Company"), 2000)
+
+ # a 0 on the customer inherits the group's limit
+ set_overdue_billing_threshold("_Test Customer", "_Test Company", 0)
+ self.assertEqual(get_overdue_billing_threshold("_Test Customer", "_Test Company"), 5000)
+
+ def test_overdue_threshold_row_without_credit_limit(self):
+ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+
+ # outstanding must be > 0 so a 0 credit_limit would previously trip the check
+ create_sales_invoice(qty=1, rate=500)
+
+ customer = frappe.get_doc("Customer", "_Test Customer")
+ customer.credit_limits = []
+ customer.append("credit_limits", {"company": "_Test Company", "overdue_billing_threshold": 1000})
+ customer.save()
+
+ self.assertEqual(customer.credit_limits[0].overdue_billing_threshold, 1000)
+ self.assertEqual(flt(customer.credit_limits[0].credit_limit), 0.0)
+
def test_customer_payment_terms(self):
frappe.db.set_value(
"Customer", "_Test Customer With Template", "payment_terms", "_Test Payment Term Template 3"
@@ -451,6 +591,18 @@ def set_credit_limit(customer, company, credit_limit):
customer.credit_limits[-1].db_insert()
+def set_overdue_billing_threshold(customer, company, threshold):
+ customer = frappe.get_doc("Customer", customer)
+ for d in customer.credit_limits:
+ if d.company == company:
+ d.overdue_billing_threshold = threshold
+ d.db_update()
+ return
+
+ customer.append("credit_limits", {"company": company, "overdue_billing_threshold": threshold})
+ customer.credit_limits[-1].db_insert()
+
+
def create_internal_customer(customer_name=None, represents_company=None, allowed_to_interact_with=None):
if not customer_name:
customer_name = represents_company
diff --git a/erpnext/selling/doctype/customer_credit_limit/customer_credit_limit.json b/erpnext/selling/doctype/customer_credit_limit/customer_credit_limit.json
index f738b3629fa..e208148ae08 100644
--- a/erpnext/selling/doctype/customer_credit_limit/customer_credit_limit.json
+++ b/erpnext/selling/doctype/customer_credit_limit/customer_credit_limit.json
@@ -8,6 +8,7 @@
"company",
"column_break_2",
"credit_limit",
+ "overdue_billing_threshold",
"bypass_credit_limit_check"
],
"fields": [
@@ -18,6 +19,15 @@
"in_list_view": 1,
"label": "Credit Limit"
},
+ {
+ "columns": 3,
+ "description": "New Sales Invoices are blocked when the customer's overdue amount exceeds this. Requires 'Restrict Customer Over Billing' in Accounts Settings.",
+ "fieldname": "overdue_billing_threshold",
+ "fieldtype": "Currency",
+ "hidden": 1,
+ "in_list_view": 1,
+ "label": "Overdue Limit"
+ },
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
diff --git a/erpnext/selling/doctype/customer_credit_limit/customer_credit_limit.py b/erpnext/selling/doctype/customer_credit_limit/customer_credit_limit.py
index fcc6c6e6db6..e0e21d71c91 100644
--- a/erpnext/selling/doctype/customer_credit_limit/customer_credit_limit.py
+++ b/erpnext/selling/doctype/customer_credit_limit/customer_credit_limit.py
@@ -18,6 +18,7 @@ class CustomerCreditLimit(Document):
bypass_credit_limit_check: DF.Check
company: DF.Link | None
credit_limit: DF.Currency
+ overdue_billing_threshold: DF.Currency
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py
index fbaec574b7f..cd9c668d027 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -296,6 +296,7 @@ class Quotation(SellingController):
# update enquiry status
self.update_opportunity("Quotation")
self.update_lead()
+ self.carry_forward_communication()
def on_cancel(self):
if self.lost_reasons:
@@ -307,6 +308,18 @@ class Quotation(SellingController):
self.update_opportunity("Open")
self.update_lead()
+ def carry_forward_communication(self):
+ from erpnext.crm.utils import copy_comments, link_communications
+
+ if not (
+ self.opportunity
+ and frappe.get_single_value("CRM Settings", "carry_forward_communication_and_comments")
+ ):
+ return
+
+ copy_comments("Opportunity", self.opportunity, self)
+ link_communications("Opportunity", self.opportunity, self)
+
def print_other_charges(self, docname):
print_lst = []
for d in self.get("taxes"):
diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py
index c327f4db9ab..68d08d0d982 100644
--- a/erpnext/selling/page/point_of_sale/point_of_sale.py
+++ b/erpnext/selling/page/point_of_sale/point_of_sale.py
@@ -5,7 +5,7 @@
import json
import frappe
-from frappe.query_builder import DocType, Order
+from frappe.query_builder import Criterion, DocType, Order
from frappe.utils import cint, get_datetime
from frappe.utils.nestedset import get_root_of
@@ -148,50 +148,55 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te
if not frappe.db.exists("Item Group", item_group):
item_group = get_root_of("Item Group")
- condition = get_conditions(search_term)
- condition += get_item_group_condition(pos_profile)
-
lft, rgt = frappe.db.get_value("Item Group", item_group, ["lft", "rgt"])
- bin_join_selection, bin_join_condition = "", ""
- if hide_unavailable_items:
- bin_join_selection = "LEFT JOIN `tabBin` bin ON bin.item_code = item.name"
- bin_join_condition = "AND (item.is_stock_item = 0 OR (item.is_stock_item = 1 AND bin.warehouse = %(warehouse)s AND bin.actual_qty > 0))"
+ item = frappe.qb.DocType("Item")
+ item_group_dt = frappe.qb.DocType("Item Group")
- items_data = frappe.db.sql(
- """
- SELECT
- item.name AS item_code,
+ item_group_subquery = (
+ frappe.qb.from_(item_group_dt)
+ .select(item_group_dt.name)
+ .where((item_group_dt.lft >= lft) & (item_group_dt.rgt <= rgt))
+ )
+
+ query = (
+ frappe.qb.from_(item)
+ .select(
+ item.name.as_("item_code"),
item.item_name,
item.description,
item.stock_uom,
- item.image AS item_image,
+ item.image.as_("item_image"),
item.is_stock_item,
- item.sales_uom
- FROM
- `tabItem` item {bin_join_selection}
- WHERE
- item.disabled = 0
- AND item.has_variants = 0
- AND item.is_sales_item = 1
- AND item.is_fixed_asset = 0
- AND item.item_group in (SELECT name FROM `tabItem Group` WHERE lft >= {lft} AND rgt <= {rgt})
- AND {condition}
- {bin_join_condition}
- ORDER BY
- item.name asc
- LIMIT
- {page_length} offset {start}""".format(
- start=cint(start),
- page_length=cint(page_length),
- lft=cint(lft),
- rgt=cint(rgt),
- condition=condition,
- bin_join_selection=bin_join_selection,
- bin_join_condition=bin_join_condition,
- ),
- {"warehouse": warehouse},
- as_dict=1,
+ item.sales_uom,
+ )
+ .where(
+ (item.disabled == 0)
+ & (item.has_variants == 0)
+ & (item.is_sales_item == 1)
+ & (item.is_fixed_asset == 0)
+ & (item.item_group.isin(item_group_subquery))
+ & get_conditions(search_term, item)
+ )
+ )
+
+ item_group_condition = get_item_group_condition(pos_profile, item)
+ if item_group_condition is not None:
+ query = query.where(item_group_condition)
+
+ if hide_unavailable_items:
+ bin_dt = frappe.qb.DocType("Bin")
+ query = (
+ query.left_join(bin_dt)
+ .on(bin_dt.item_code == item.name)
+ .where(
+ (item.is_stock_item == 0)
+ | ((item.is_stock_item == 1) & (bin_dt.warehouse == warehouse) & (bin_dt.actual_qty > 0))
+ )
+ )
+
+ items_data = (
+ query.orderby(item.name, order=Order.asc).limit(cint(page_length)).offset(cint(start)).run(as_dict=1)
)
# return (empty) list if there are no results
@@ -262,56 +267,63 @@ def search_for_serial_or_batch_or_barcode_number(search_value: str) -> dict[str,
return scan_barcode(search_value)
-def get_conditions(search_term):
- condition = "("
- condition += """item.name like {search_term}
- or item.item_name like {search_term}""".format(search_term=frappe.db.escape("%" + search_term + "%"))
- condition += add_search_fields_condition(search_term)
- condition += ")"
+def get_conditions(search_term, item=None):
+ if item is None:
+ item = frappe.qb.DocType("Item")
- return condition
+ pattern = f"%{search_term}%"
+ conditions = [item.name.like(pattern), item.item_name.like(pattern)]
+ conditions += add_search_fields_condition(search_term, item)
+
+ return Criterion.any(conditions)
-def add_search_fields_condition(search_term):
- condition = ""
+def add_search_fields_condition(search_term, item=None):
+ if item is None:
+ item = frappe.qb.DocType("Item")
+
+ pattern = f"%{search_term}%"
+ conditions = []
search_fields = frappe.get_all("POS Search Fields", fields=["fieldname"])
- if search_fields:
- for field in search_fields:
- if not field.get("fieldname"):
- continue
- condition += " or item.`{}` like {}".format(
- field["fieldname"], frappe.db.escape("%" + search_term + "%")
- )
- return condition
+ for field in search_fields:
+ if not field.get("fieldname"):
+ continue
+ conditions.append(item[field["fieldname"]].like(pattern))
+
+ return conditions
-def get_item_group_condition(pos_profile):
- cond = "and 1=1"
+def get_item_group_condition(pos_profile, item=None):
+ if item is None:
+ item = frappe.qb.DocType("Item")
+
item_groups = get_item_groups(pos_profile)
if item_groups:
- cond = "and item.item_group in (%s)" % (", ".join(["%s"] * len(item_groups)))
+ return item.item_group.isin(item_groups)
- return cond % tuple(item_groups)
+ return None
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def item_group_query(doctype, txt, searchfield, start, page_len, filters):
- item_groups = []
- cond = "1=1"
pos_profile = filters.get("pos_profile")
+ item_filters = [["name", "like", f"%{txt}%"]]
if pos_profile:
item_groups = get_item_groups(pos_profile)
-
if item_groups:
- cond = "name in (%s)" % (", ".join(["%s"] * len(item_groups)))
- cond = cond % tuple(item_groups)
+ item_filters.append(["name", "in", item_groups])
- return frappe.db.sql(
- f""" select distinct name from `tabItem Group`
- where {cond} and (name like %(txt)s) limit {page_len} offset {start}""",
- {"txt": "%%%s%%" % txt},
+ return frappe.get_all(
+ "Item Group",
+ filters=item_filters,
+ fields=["name"],
+ distinct=True,
+ order_by="", # original raw SQL had no ORDER BY; suppress the injected default (creation desc on MariaDB)
+ limit_start=start,
+ limit_page_length=page_len,
+ as_list=True,
)
diff --git a/erpnext/selling/page/point_of_sale/test_point_of_sale.py b/erpnext/selling/page/point_of_sale/test_point_of_sale.py
new file mode 100644
index 00000000000..dcfe6e7edb3
--- /dev/null
+++ b/erpnext/selling/page/point_of_sale/test_point_of_sale.py
@@ -0,0 +1,137 @@
+# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+import frappe
+from frappe.utils import random_string
+
+from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
+from erpnext.selling.page.point_of_sale.point_of_sale import get_items
+from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+from erpnext.tests.utils import ERPNextTestSuite
+
+
+class TestPointOfSaleGetItems(ERPNextTestSuite):
+ """Covers the raw-SQL -> frappe.qb conversion of point_of_sale.get_items."""
+
+ def setUp(self):
+ super().setUp()
+ # Reuse the bootstrap leaf item group; an item assigned directly to it
+ # falls inside its own (lft, rgt) subtree, which is what get_items filters on.
+ self.item_group = "_Test Item Group"
+
+ # A non-stock sales item keeps get_stock_availability cheap (no Bin needed)
+ # and keeps the item out of the hide_unavailable_items branch.
+ self.item_code = "_Test POS Item " + random_string(10)
+ item = frappe.get_doc(
+ {
+ "doctype": "Item",
+ "item_code": self.item_code,
+ "item_name": self.item_code,
+ "item_group": self.item_group,
+ "stock_uom": "_Test UOM",
+ "is_stock_item": 0,
+ "is_sales_item": 1,
+ "is_fixed_asset": 0,
+ "has_variants": 0,
+ "disabled": 0,
+ }
+ )
+ item.insert()
+ self.item = item
+
+ # make_pos_profile builds "_Test POS Profile" (hide_unavailable_items unset,
+ # no item_groups restriction). Rolled back by tearDown.
+ self.pos_profile = make_pos_profile().name
+
+ def _get_item_codes(self, search_term):
+ result = get_items(
+ start=0,
+ page_length=100,
+ price_list="Standard Selling",
+ item_group=self.item_group,
+ pos_profile=self.pos_profile,
+ search_term=search_term,
+ )
+ # get_items returns {"items": [...]} when the qb query yields rows,
+ # and a bare (empty) list when nothing matches.
+ items = result["items"] if isinstance(result, dict) else result
+ return [row.get("item_code") for row in items]
+
+ def _make_stock_item(self):
+ # Fresh stock item in the filtered item group so it passes the
+ # item_group.isin(subquery) clause and reaches the Bin left-join.
+ item_code = "_Test POS Stock Item " + random_string(10)
+ frappe.get_doc(
+ {
+ "doctype": "Item",
+ "item_code": item_code,
+ "item_name": item_code,
+ "item_group": self.item_group,
+ "stock_uom": "_Test UOM",
+ "is_stock_item": 1,
+ "is_sales_item": 1,
+ "is_fixed_asset": 0,
+ "has_variants": 0,
+ "disabled": 0,
+ }
+ ).insert()
+ return item_code
+
+ def test_matching_search_term_returns_item(self):
+ # search_term matches Item.name / Item.item_name via the LIKE OR-condition;
+ # scan_barcode finds nothing for this value, so the converted qb query runs.
+ item_codes = self._get_item_codes(self.item_code)
+ self.assertIn(self.item_code, item_codes)
+
+ def test_non_matching_search_term_excludes_item(self):
+ non_matching = "zzz_no_such_item_" + random_string(10)
+ item_codes = self._get_item_codes(non_matching)
+ self.assertNotIn(self.item_code, item_codes)
+
+ def test_partial_search_term_matches_on_item_name(self):
+ # A substring of the item code must still match (LIKE %term%),
+ # proving the OR/LIKE clause survived the SQL->qb conversion.
+ partial = self.item_code.split(" ")[-1]
+ item_codes = self._get_item_codes(partial)
+ self.assertIn(self.item_code, item_codes)
+
+ def test_disabled_item_is_excluded(self):
+ # disabled == 0 is part of the converted WHERE clause; flipping it
+ # must drop the item even when the search term matches.
+ frappe.db.set_value("Item", self.item_code, "disabled", 1)
+ item_codes = self._get_item_codes(self.item_code)
+ self.assertNotIn(self.item_code, item_codes)
+
+ def test_non_sales_item_is_excluded(self):
+ # is_sales_item == 1 is part of the converted WHERE clause.
+ frappe.db.set_value("Item", self.item_code, "is_sales_item", 0)
+ item_codes = self._get_item_codes(self.item_code)
+ self.assertNotIn(self.item_code, item_codes)
+
+ def test_hide_unavailable_items_filters_on_bin_actual_qty(self):
+ # Covers the hide_unavailable_items branch: the Bin left-join only keeps a
+ # stock item when bin.warehouse == profile warehouse AND bin.actual_qty > 0.
+ # A second stock item with no Bin row (no stock) must be hidden.
+ warehouse = frappe.db.get_value("POS Profile", self.pos_profile, "warehouse")
+ frappe.db.set_value("POS Profile", self.pos_profile, "hide_unavailable_items", 1)
+
+ in_stock_item = self._make_stock_item()
+ out_of_stock_item = self._make_stock_item()
+
+ # Material Receipt gives in_stock_item actual_qty > 0 in the profile warehouse;
+ # out_of_stock_item gets no Bin row at all.
+ make_stock_entry(item_code=in_stock_item, target=warehouse, qty=5, basic_rate=100)
+
+ # Sanity-check the precondition the branch keys off of.
+ self.assertGreater(
+ frappe.db.get_value("Bin", {"item_code": in_stock_item, "warehouse": warehouse}, "actual_qty")
+ or 0,
+ 0,
+ )
+ self.assertFalse(frappe.db.exists("Bin", {"item_code": out_of_stock_item}))
+
+ in_stock_codes = self._get_item_codes(in_stock_item)
+ self.assertIn(in_stock_item, in_stock_codes)
+
+ out_of_stock_codes = self._get_item_codes(out_of_stock_item)
+ self.assertNotIn(out_of_stock_item, out_of_stock_codes)
diff --git a/erpnext/selling/report/quotation_trends/quotation_trends.py b/erpnext/selling/report/quotation_trends/quotation_trends.py
index 92f9d17a9c7..e5b62569394 100644
--- a/erpnext/selling/report/quotation_trends/quotation_trends.py
+++ b/erpnext/selling/report/quotation_trends/quotation_trends.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-
from frappe import _
from erpnext.controllers.trends import get_columns, get_data
@@ -40,9 +39,15 @@ def get_chart_data(data, conditions, filters):
labels = [column.split(":")[0] for column in columns]
datapoints = [0] * len(labels)
+ group_by_col_idx = None
+ if filters.get("group_by"):
+ group_by_col_idx = conditions["columns"].index(conditions["grbc"][0])
+
for row in data:
- # If group by filter, don't add first row of group (it's already summed)
- if not row[start]:
+ # Skip the final grand-total row
+ if row[0] == f"'{_('Total')}'":
+ continue
+ if group_by_col_idx is not None and row[group_by_col_idx] == "":
continue
# Remove None values and compute only periodic data
row = [x if x else 0 for x in row[start:-2]]
@@ -59,4 +64,6 @@ def get_chart_data(data, conditions, filters):
"type": "line",
"lineOptions": {"regionFill": 1},
"fieldtype": "Currency",
+ "options": "currency",
+ "currency": conditions.get("company_currency"),
}
diff --git a/erpnext/selling/report/sales_order_trends/sales_order_trends.py b/erpnext/selling/report/sales_order_trends/sales_order_trends.py
index ca11b8302de..e0de678f22d 100644
--- a/erpnext/selling/report/sales_order_trends/sales_order_trends.py
+++ b/erpnext/selling/report/sales_order_trends/sales_order_trends.py
@@ -39,9 +39,15 @@ def get_chart_data(data, conditions, filters):
labels = [column.split(":")[0].replace(" (Amt)", "") for column in columns]
datapoints = [0] * len(labels)
+ group_by_col_idx = None
+ if filters.get("group_by"):
+ group_by_col_idx = conditions["columns"].index(conditions["grbc"][0])
+
for row in data:
- # If group by filter, don't add first row of group (it's already summed)
- if not row[start]:
+ # Skip the final grand-total row
+ if row[0] == f"'{_('Total')}'":
+ continue
+ if group_by_col_idx is not None and row[group_by_col_idx] == "":
continue
# Remove None values and compute only periodic data
row = [x if x else 0 for x in row[start:-2]]
@@ -58,4 +64,6 @@ def get_chart_data(data, conditions, filters):
"type": "line",
"lineOptions": {"regionFill": 1},
"fieldtype": "Currency",
+ "options": "currency",
+ "currency": conditions.get("company_currency"),
}
diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js
index 417d83acb1b..29f1ea3a8fe 100644
--- a/erpnext/setup/doctype/company/company.js
+++ b/erpnext/setup/doctype/company/company.js
@@ -69,6 +69,17 @@ frappe.ui.form.on("Company", {
},
};
});
+
+ ["default_wip_warehouse", "default_fg_warehouse", "default_scrap_warehouse"].forEach((fieldname) => {
+ frm.set_query(fieldname, function (doc) {
+ return {
+ filters: {
+ company: doc.name,
+ is_group: 0,
+ },
+ };
+ });
+ });
},
company_name: function (frm) {
@@ -307,6 +318,8 @@ erpnext.company.setup_queries = function (frm) {
["default_advance_received_account", { root_type: "Liability", account_type: "Receivable" }],
["default_advance_paid_account", { root_type: "Asset", account_type: "Payable" }],
["service_expense_account", { root_type: "Expense" }],
+ ["expenses_added_to_stock_account", { root_type: "Expense" }],
+ ["expenses_added_to_stock_contra_account", { root_type: "Expense" }],
],
function (i, v) {
erpnext.company.set_custom_query(frm, v);
diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json
index 353cd799415..74e0c78ecc2 100644
--- a/erpnext/setup/doctype/company/company.json
+++ b/erpnext/setup/doctype/company/company.json
@@ -117,6 +117,10 @@
"service_expense_account",
"column_break_ereg",
"purchase_expense_contra_account",
+ "stock_expense_section",
+ "expenses_added_to_stock_account",
+ "column_break_gthb",
+ "expenses_added_to_stock_contra_account",
"stock_tab",
"auto_accounting_for_stock_settings",
"enable_perpetual_inventory",
@@ -824,6 +828,27 @@
"fieldtype": "Tab Break",
"label": "Buying and Selling"
},
+ {
+ "fieldname": "stock_expense_section",
+ "fieldtype": "Section Break",
+ "label": "Stock Expense"
+ },
+ {
+ "fieldname": "expenses_added_to_stock_account",
+ "fieldtype": "Link",
+ "label": "Expenses Added To Stock Account",
+ "options": "Account"
+ },
+ {
+ "fieldname": "column_break_gthb",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "expenses_added_to_stock_contra_account",
+ "fieldtype": "Link",
+ "label": "Expenses Added To Stock Contra Account",
+ "options": "Account"
+ },
{
"fieldname": "stock_tab",
"fieldtype": "Tab Break",
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 7ac095a6fdd..20c7142b9a9 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -98,6 +98,8 @@ class Company(NestedSet):
exception_budget_approver_role: DF.Link | None
exchange_gain_loss_account: DF.Link | None
existing_company: DF.Link | None
+ expenses_added_to_stock_account: DF.Link | None
+ expenses_added_to_stock_contra_account: DF.Link | None
fax: DF.Data | None
is_group: DF.Check
lft: DF.Int
diff --git a/erpnext/setup/doctype/customer_group/customer_group.json b/erpnext/setup/doctype/customer_group/customer_group.json
index 40317c2f8f7..5461155e409 100644
--- a/erpnext/setup/doctype/customer_group/customer_group.json
+++ b/erpnext/setup/doctype/customer_group/customer_group.json
@@ -132,7 +132,7 @@
{
"fieldname": "credit_limits",
"fieldtype": "Table",
- "label": "Credit Limit",
+ "label": "Credit & Overdue Limits",
"options": "Customer Credit Limit"
}
],
diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py
index ba0ccbaf88e..9432e8c59ae 100644
--- a/erpnext/stock/deprecated_serial_batch.py
+++ b/erpnext/stock/deprecated_serial_batch.py
@@ -75,6 +75,7 @@ class DeprecatedSerialNoValuation:
| (table.serial_no.like("%\n" + serial_no))
| (table.serial_no.like("%\n" + serial_no + "\n%"))
)
+ & (table.item_code == self.sle.item_code)
& (table.company == self.sle.company)
& (table.warehouse == self.sle.warehouse)
& (table.serial_and_batch_bundle.isnull())
diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py
index 5b8f9eade10..dc73e9ffddf 100644
--- a/erpnext/stock/doctype/batch/batch.py
+++ b/erpnext/stock/doctype/batch/batch.py
@@ -296,7 +296,7 @@ def get_batches_by_oldest(item_code, warehouse):
"""Returns the oldest batch and qty for the given item_code and warehouse"""
batches = get_batch_qty(item_code=item_code, warehouse=warehouse)
batches_dates = [[batch, frappe.get_value("Batch", batch.batch_no, "expiry_date")] for batch in batches]
- batches_dates.sort(key=lambda tup: tup[1])
+ batches_dates.sort(key=lambda tup: (tup[1] is None, tup[1]))
return batches_dates
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index 64004b13d19..3fc12c0f447 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -175,7 +175,7 @@ class Bin(Document):
& (subcontract_order.docstatus == 1)
)
if subcontract_doctype == "Purchase Order"
- else (subcontract_order.docstatus == 1)
+ else ((subcontract_order.status != "Closed") & (subcontract_order.docstatus == 1))
)
)
@@ -212,6 +212,7 @@ class Bin(Document):
else (
(Coalesce(se.subcontracting_order, "") != "")
& (subcontract_order.name == se.subcontracting_order)
+ & (subcontract_order.status != "Closed")
)
)
)
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index c37e35d2114..d7fde5f6961 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -620,7 +620,13 @@ $.extend(erpnext.item, {
};
});
- let fields = ["purchase_expense_account", "purchase_expense_contra_account", "default_cogs_account"];
+ let fields = [
+ "purchase_expense_account",
+ "purchase_expense_contra_account",
+ "expenses_added_to_stock_account",
+ "expenses_added_to_stock_contra_account",
+ "default_cogs_account",
+ ];
fields.forEach((field) => {
frm.set_query(field, "item_defaults", (doc, cdt, cdn) => {
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index c075dd43310..6855b73165b 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -721,7 +721,7 @@
},
{
"default": "0",
- "description": "Expense for this item will be recognized over a period of months. Eg: prepaid insurance or annual software license",
+ "description": "Income from this item will be recognized over a period of months instead of all at once. Eg: annual subscription paid upfront.",
"fieldname": "enable_deferred_revenue",
"fieldtype": "Check",
"label": "Enable Deferred Revenue"
@@ -734,7 +734,7 @@
},
{
"default": "0",
- "description": "Income from this item will be recognized over a period of months instead of all at once. Eg: annual subscription paid upfront.",
+ "description": "Expense for this item will be recognized over a period of months. Eg: prepaid insurance or annual software license",
"fieldname": "enable_deferred_expense",
"fieldtype": "Check",
"label": "Enable Deferred Expense"
@@ -1093,7 +1093,7 @@
"image_field": "image",
"links": [],
"make_attachments_public": 1,
- "modified": "2026-07-05 23:24:45.734144",
+ "modified": "2026-07-28 18:58:43.328497",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",
diff --git a/erpnext/stock/doctype/item_default/item_default.json b/erpnext/stock/doctype/item_default/item_default.json
index b00644d51f2..a4d30ddd41c 100644
--- a/erpnext/stock/doctype/item_default/item_default.json
+++ b/erpnext/stock/doctype/item_default/item_default.json
@@ -21,6 +21,8 @@
"column_break_cpif",
"purchase_expense_account",
"purchase_expense_contra_account",
+ "expenses_added_to_stock_account",
+ "expenses_added_to_stock_contra_account",
"selling_defaults",
"selling_cost_center",
"column_break_12",
@@ -45,6 +47,7 @@
{
"fieldname": "default_warehouse",
"fieldtype": "Link",
+ "ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Default Warehouse",
"options": "Warehouse",
@@ -73,6 +76,7 @@
"fieldname": "buying_cost_center",
"fieldtype": "Link",
"label": "Default Buying Cost Center",
+ "ignore_user_permissions": 1,
"options": "Cost Center",
"show_description_on_click": 1
},
@@ -92,6 +96,7 @@
"fieldname": "expense_account",
"fieldtype": "Link",
"label": "Default Expense Account",
+ "ignore_user_permissions": 1,
"options": "Account",
"show_description_on_click": 1
},
@@ -144,6 +149,7 @@
"fieldname": "deferred_expense_account",
"fieldtype": "Link",
"label": "Deferred Expense Account",
+ "ignore_user_permissions": 1,
"options": "Account",
"show_description_on_click": 1
},
@@ -181,6 +187,7 @@
"description": "Account to record additional purchase expenses like freight or customs for this item",
"fieldname": "purchase_expense_account",
"fieldtype": "Link",
+ "ignore_user_permissions": 1,
"label": "Purchase Expense Account",
"options": "Account",
"show_description_on_click": 1
@@ -189,14 +196,34 @@
"description": "Used to balance the books when recording extra purchase costs like freight or customs",
"fieldname": "purchase_expense_contra_account",
"fieldtype": "Link",
+ "ignore_user_permissions": 1,
"label": "Purchase Expense Contra Account",
"options": "Account"
},
+ {
+ "description": "Account to track value added to stock via Stock Entry, Stock Reconciliation or Landed Cost Voucher",
+ "fieldname": "expenses_added_to_stock_account",
+ "fieldtype": "Link",
+ "ignore_user_permissions": 1,
+ "label": "Expenses Added To Stock Account",
+ "options": "Account",
+ "show_description_on_click": 1
+ },
+ {
+ "description": "Used to balance the books when recording expenses added to stock",
+ "fieldname": "expenses_added_to_stock_contra_account",
+ "fieldtype": "Link",
+ "ignore_user_permissions": 1,
+ "label": "Expenses Added To Stock Contra Account",
+ "options": "Account",
+ "show_description_on_click": 1
+ },
{
"description": "Stock account where inventory value for this item will be tracked",
"fieldname": "default_inventory_account",
"fieldtype": "Link",
"label": "Default Inventory Account",
+ "ignore_user_permissions": 1,
"options": "Account",
"show_description_on_click": 1
},
@@ -211,7 +238,7 @@
],
"istable": 1,
"links": [],
- "modified": "2026-04-27 01:49:01.396845",
+ "modified": "2026-07-28 15:39:44.848087",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Default",
diff --git a/erpnext/stock/doctype/item_default/item_default.py b/erpnext/stock/doctype/item_default/item_default.py
index 10651d260c6..7774b0ebfeb 100644
--- a/erpnext/stock/doctype/item_default/item_default.py
+++ b/erpnext/stock/doctype/item_default/item_default.py
@@ -26,6 +26,8 @@ class ItemDefault(Document):
deferred_expense_account: DF.Link | None
deferred_revenue_account: DF.Link | None
expense_account: DF.Link | None
+ expenses_added_to_stock_account: DF.Link | None
+ expenses_added_to_stock_contra_account: DF.Link | None
income_account: DF.Link | None
inventory_account_currency: DF.Link | None
parent: DF.Data
@@ -33,6 +35,7 @@ class ItemDefault(Document):
parenttype: DF.Data
purchase_expense_account: DF.Link | None
purchase_expense_contra_account: DF.Link | None
+ purchase_price_variance_account: DF.Link | None
selling_cost_center: DF.Link | None
# end: auto-generated types
diff --git a/erpnext/stock/doctype/item_reorder/item_reorder.json b/erpnext/stock/doctype/item_reorder/item_reorder.json
index a0b365cf601..847df354562 100644
--- a/erpnext/stock/doctype/item_reorder/item_reorder.json
+++ b/erpnext/stock/doctype/item_reorder/item_reorder.json
@@ -1,5 +1,6 @@
{
"actions": [],
+ "allow_bulk_edit": 1,
"autoname": "hash",
"creation": "2013-03-07 11:42:59",
"doctype": "DocType",
@@ -18,6 +19,7 @@
"columns": 3,
"fieldname": "warehouse_group",
"fieldtype": "Link",
+ "ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Check Availability in Warehouse",
"options": "Warehouse"
@@ -26,6 +28,7 @@
"columns": 2,
"fieldname": "warehouse",
"fieldtype": "Link",
+ "ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Request for",
"options": "Warehouse",
@@ -59,7 +62,7 @@
"in_create": 1,
"istable": 1,
"links": [],
- "modified": "2025-12-02 16:02:23.254963",
+ "modified": "2026-07-28 15:54:36.089238",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Reorder",
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 3e0c414e216..5fd8f333de7 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -1381,6 +1381,9 @@ def create_delivery_wo_so(pick_list, target, target_doc=None):
target_doc.company = pick_list.company
+ if not target_doc.customer:
+ target_doc.customer = pick_list.customer
+
item_table_mapper_without_so = {
"doctype": f"{target} Item",
"field_map": {
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 90d4dfab63c..c40631a6d82 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -652,6 +652,14 @@ class PurchaseReceipt(BuyingController):
item=item,
)
+ def make_expenses_added_to_stock_entries(item):
+ if not self.book_stock_expense_enabled():
+ return
+
+ amount = flt(item.landed_cost_voucher_amount, item.precision("base_net_amount"))
+ if amount and not item.is_fixed_asset:
+ self.append_expenses_added_to_stock_pair(gl_entries, item.item_code, amount, item)
+
def make_amount_difference_entry(item):
if item.amount_difference_with_purchase_invoice and stock_asset_rbnb:
account_currency = get_account_currency(stock_asset_rbnb)
@@ -796,6 +804,7 @@ class PurchaseReceipt(BuyingController):
make_item_asset_inward_gl_entry(d, stock_value_diff, stock_asset_account_name)
outgoing_amount = make_stock_received_but_not_billed_entry(d)
make_landed_cost_gl_entries(d)
+ make_expenses_added_to_stock_entries(d)
make_amount_difference_entry(d)
make_sub_contracting_gl_entries(d)
make_divisional_loss_gl_entry(d, outgoing_amount)
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index f5aedd15de2..eb867ff1f96 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -5125,6 +5125,14 @@ class TestPurchaseReceipt(ERPNextTestSuite):
self.assertEqual(srbnb_cost, 1000)
def test_purchase_expense_account(self):
+ # Single, so it outlives this test - every later Purchase Receipt / Invoice would otherwise
+ # be forced to resolve the expense account pair and throw for unconfigured companies.
+ previous = frappe.db.get_single_value("Accounts Settings", "book_stock_expense_gl_entries")
+ self.addCleanup(
+ frappe.db.set_single_value, "Accounts Settings", "book_stock_expense_gl_entries", previous
+ )
+ frappe.db.set_single_value("Accounts Settings", "book_stock_expense_gl_entries", 1)
+
item = "Test Item with Purchase Expense Account"
make_item(item, {"is_stock_item": 1})
company = "_Test Company with perpetual inventory"
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
index 160a887d851..d0e1cac2b23 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -242,7 +242,7 @@ class RepostItemValuation(Document):
def clear_attachment(self):
if attachments := get_attachments(self.doctype, self.name):
attachment = attachments[0]
- frappe.delete_doc("File", attachment.name, ignore_permissions=True)
+ frappe.delete_doc("File", attachment.name, ignore_permissions=True, force=True)
if self.reposting_data_file:
self.db_set("reposting_data_file", None)
diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
index a27c8d49ea2..a941b64856a 100644
--- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
@@ -515,6 +515,34 @@ class TestRepostItemValuation(ERPNextTestSuite, StockTestMixin):
)
)
+ def test_clear_attachment_skips_referenced_data_file(self):
+ riv = frappe.get_doc(
+ {
+ "doctype": "Repost Item Valuation",
+ "based_on": "Item and Warehouse",
+ "company": "_Test Company",
+ "item_code": "_Test Item",
+ "warehouse": "_Test Warehouse - _TC",
+ "posting_date": today(),
+ }
+ ).insert(ignore_permissions=True)
+
+ attached = frappe.get_doc(
+ {
+ "doctype": "File",
+ "file_name": "repost_data.json.gz",
+ "content": "test",
+ "attached_to_doctype": riv.doctype,
+ "attached_to_name": riv.name,
+ }
+ ).insert(ignore_permissions=True)
+ riv.db_set("reposting_data_file", attached.file_url)
+
+ riv.clear_attachment()
+
+ self.assertFalse(frappe.db.exists("File", attached.name))
+ self.assertIsNone(frappe.db.get_value("Repost Item Valuation", riv.name, "reposting_data_file"))
+
@ERPNextTestSuite.change_settings(
"Stock Reposting Settings",
{"item_based_reposting": 1, "enable_parallel_reposting": 1, "no_of_parallel_reposting": 2},
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index c627c6bbdb1..94ce0652931 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -569,8 +569,6 @@ frappe.ui.form.on("Stock Entry", {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
}
- frm.events.set_route_options_for_new_doc(frm);
-
frm.set_df_property(
"items",
"cannot_add_rows",
@@ -583,28 +581,6 @@ frappe.ui.form.on("Stock Entry", {
);
},
- set_route_options_for_new_doc(frm) {
- let batch_no_field = frm.get_docfield("items", "batch_no");
- if (batch_no_field) {
- batch_no_field.get_route_options_for_new_doc = function (row) {
- return {
- item: row.doc.item_code,
- };
- };
- }
-
- let sbb_field = frm.get_docfield("items", "serial_and_batch_bundle");
- if (sbb_field) {
- sbb_field.get_route_options_for_new_doc = (row) => {
- return {
- item_code: row.doc.item_code,
- voucher_type: frm.doc.doctype,
- warehouse: row.doc.s_warehouse || row.doc.t_warehouse,
- };
- };
- }
- },
-
get_items_from_transit_entry: function (frm) {
if (frm.doc.docstatus === 0 && !frm.doc.subcontracting_inward_order) {
frm.add_custom_button(
@@ -1312,6 +1288,7 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
}
onload_post_render() {
+ super.onload_post_render();
var me = this;
if (me.frm.doc.__islocal && me.frm.doc.company && !me.frm.doc.amended_from) {
me.company();
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index e050429eb98..50ce29b29f4 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -176,6 +176,8 @@ class StockEntry(StockController, SubcontractingInwardController):
work_order: DF.Link | None
# end: auto-generated types
+ book_expenses_added_to_stock = True
+
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.status_updater = [
diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
index 0ff773cd6e7..0491498b1b2 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
@@ -1344,6 +1344,47 @@ class TestStockLedgerEntry(ERPNextTestSuite, StockTestMixin):
# receipt2 now sits on a zero base -> 10 (not 0 from a double shift, nor a negative-stock error).
self.assertEqual(qty_after(receipt2), 10)
+ def test_cancel_shifts_same_timestamp_delivery_notes(self):
+ item = make_item().name
+ warehouse = "_Test Warehouse - _TC"
+ posting_date = today()
+ posting_time = "10:00:00"
+
+ make_stock_entry(
+ item_code=item,
+ to_warehouse=warehouse,
+ qty=100,
+ rate=10,
+ posting_date=posting_date,
+ posting_time="09:00:00",
+ )
+
+ dns = []
+ for i in range(5):
+ dns.append(
+ create_delivery_note(
+ item_code=item,
+ warehouse=warehouse,
+ qty=20,
+ rate=10 * i,
+ posting_date=posting_date,
+ posting_time=posting_time,
+ )
+ )
+ time.sleep(1)
+
+ dn = dns[2]
+ dn.cancel()
+
+ expected_qty_after_transaction_of_dns3 = 40
+ qty_after_transaction_of_dns3 = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_no": dns[3].name, "is_cancelled": 0},
+ "qty_after_transaction",
+ )
+
+ self.assertEqual(expected_qty_after_transaction_of_dns3, qty_after_transaction_of_dns3)
+
def test_get_next_stock_reco_respects_creation_order(self):
# A stock reco sharing the exact posting timestamp of the current entry must only count as the
# "next" reco when it was created after that entry. A reco created before it actually precedes
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
index ef4672899cc..e711d7248f7 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
@@ -46,17 +46,6 @@ frappe.ui.form.on("Stock Reconciliation", {
};
});
- let sbb_field = frm.get_docfield("items", "serial_and_batch_bundle");
- if (sbb_field) {
- sbb_field.get_route_options_for_new_doc = (row) => {
- return {
- item_code: row.doc.item_code,
- warehouse: row.doc.warehouse,
- voucher_type: frm.doc.doctype,
- };
- };
- }
-
if (frm.doc.company) {
erpnext.queries.setup_queries(frm, "Warehouse", function () {
return erpnext.queries.warehouse(frm.doc);
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index aa94b808988..bd789e9785e 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -57,6 +57,8 @@ class StockReconciliation(StockController):
set_warehouse: DF.Link | None
# end: auto-generated types
+ book_expenses_added_to_stock = True
+
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.head_row = ["Item Code", "Warehouse", "Quantity", "Valuation Rate"]
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 7cbe369c720..53401106f32 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -84,6 +84,7 @@ def get_item_details(ctx, doc=None, for_validate=False, overwrite_warehouse=True
for_validate = parse_json(for_validate)
overwrite_warehouse = parse_json(overwrite_warehouse)
item = frappe.get_cached_doc("Item", ctx.item_code)
+ item.check_permission()
validate_item_details(ctx, item)
if isinstance(doc, str):
diff --git a/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py b/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py
index a456bad72d7..1365a02ba25 100644
--- a/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py
+++ b/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py
@@ -14,12 +14,12 @@ def execute(filters=None):
conditions = get_columns(filters, "Delivery Note")
data = get_data(filters, conditions)
- chart_data = get_chart_data(data, filters)
+ chart_data = get_chart_data(data, conditions, filters)
return conditions["columns"], data, None, chart_data
-def get_chart_data(data, filters):
+def get_chart_data(data, conditions, filters):
def wrap_in_quotes(label):
return f"'{label}'"
@@ -52,4 +52,6 @@ def get_chart_data(data, filters):
},
"type": "bar",
"fieldtype": "Currency",
+ "options": "currency",
+ "currency": conditions.get("company_currency"),
}
diff --git a/erpnext/stock/report/landed_cost_report/landed_cost_report.py b/erpnext/stock/report/landed_cost_report/landed_cost_report.py
index b5738c2e20f..18473c51b24 100644
--- a/erpnext/stock/report/landed_cost_report/landed_cost_report.py
+++ b/erpnext/stock/report/landed_cost_report/landed_cost_report.py
@@ -4,6 +4,8 @@
import frappe
from frappe import _
+import erpnext
+
def execute(filters: dict | None = None):
columns = get_columns()
@@ -24,6 +26,14 @@ def get_columns() -> list[dict]:
"label": _("Total Landed Cost"),
"fieldname": "landed_cost",
"fieldtype": "Currency",
+ "options": "currency",
+ },
+ {
+ "label": _("Currency"),
+ "fieldname": "currency",
+ "fieldtype": "Link",
+ "options": "Currency",
+ "hidden": 1,
},
{
"label": _("Purchase Voucher Type"),
@@ -49,6 +59,7 @@ def get_columns() -> list[dict]:
def get_data(filters) -> list[list]:
+ company_currency = erpnext.get_company_currency(filters.get("company"))
landed_cost_vouchers = get_landed_cost_vouchers(filters) or {}
landed_vouchers = list(landed_cost_vouchers.keys())
vendor_invoices = {}
@@ -57,7 +68,6 @@ def get_data(filters) -> list[list]:
data = []
- print(vendor_invoices)
for name, vouchers in landed_cost_vouchers.items():
res = {
"name": name,
@@ -72,6 +82,7 @@ def get_data(filters) -> list[list]:
"landed_cost": d.landed_cost,
"voucher_type": d.voucher_type,
"voucher_no": d.voucher_no,
+ "currency": company_currency,
}
)
else:
@@ -88,7 +99,6 @@ def get_data(filters) -> list[list]:
if vendor_invoice_list and len(vendor_invoice_list) > len(vouchers):
for row in vendor_invoice_list[last_index + 1 :]:
- print(row)
data.append({"vendor_invoice": row})
return data
diff --git a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py
index 9d313b477a3..4210d1a3604 100644
--- a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py
+++ b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py
@@ -14,12 +14,12 @@ def execute(filters=None):
conditions = get_columns(filters, "Purchase Receipt")
data = get_data(filters, conditions)
- chart_data = get_chart_data(data, filters)
+ chart_data = get_chart_data(data, conditions, filters)
return conditions["columns"], data, None, chart_data
-def get_chart_data(data, filters):
+def get_chart_data(data, conditions, filters):
def wrap_in_quotes(label):
return f"'{label}'"
@@ -53,4 +53,6 @@ def get_chart_data(data, filters):
"type": "bar",
"colors": ["#5e64ff"],
"fieldtype": "Currency",
+ "options": "currency",
+ "currency": conditions.get("company_currency"),
}
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py
index e880e8db9b9..83ecd4b1fee 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/stock_ageing.py
@@ -325,6 +325,7 @@ class FIFOSlots:
del stock_ledger_entries
self._recompute_moving_average_slots()
+ self._rebalance_batch_slots()
if not self.filters.get("show_warehouse_wise_stock"):
# (Item 1, WH 1), (Item 1, WH 2) => (Item 1)
@@ -346,6 +347,29 @@ class FIFOSlots:
if is_qty_slot(slot):
slot[FIFO_VALUE_INDEX] = flt(slot[FIFO_QTY_INDEX] * rate)
+ def _rebalance_batch_slots(self) -> None:
+ for item_dict in self.item_details.values():
+ if item_dict.get("has_batch_no"):
+ self._rebalance_batch_slot_values(item_dict["fifo_queue"])
+
+ def _rebalance_batch_slot_values(self, fifo_queue: list) -> None:
+ """A batch is one valuation pool, so per-slot value differences are stale
+ detail: spread the pool value over its slots in proportion to qty."""
+ groups = {}
+ for slot in fifo_queue:
+ if is_batch_slot(slot):
+ key = slot[BATCH_SLOT_BATCH_INDEX] if slot[BATCH_SLOT_VALUATION_INDEX] else None
+ groups.setdefault(key, []).append(slot)
+
+ for slots in groups.values():
+ total_qty = sum(flt(slot[BATCH_SLOT_QTY_INDEX]) for slot in slots)
+ if total_qty <= 0:
+ continue
+
+ rate = sum(flt(slot[BATCH_SLOT_VALUE_INDEX]) for slot in slots) / total_qty
+ for slot in slots:
+ slot[BATCH_SLOT_VALUE_INDEX] = flt(slot[BATCH_SLOT_QTY_INDEX] * rate)
+
def _get_bundle_wise_details(self, stock_ledger_entries: list | None) -> tuple[dict, dict]:
if stock_ledger_entries is not None:
return frappe._dict({}), frappe._dict({})
diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py
index f072dfeba4d..39c046fb689 100644
--- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py
@@ -5,7 +5,13 @@ from unittest.mock import patch
import frappe
-from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, format_report_data, get_average_age
+from erpnext.stock.report.stock_ageing.stock_ageing import (
+ BATCH_SLOT_QTY_INDEX,
+ BATCH_SLOT_VALUE_INDEX,
+ FIFOSlots,
+ format_report_data,
+ get_average_age,
+)
from erpnext.tests.utils import ERPNextTestSuite
@@ -565,10 +571,11 @@ class TestStockAgeing(ERPNextTestSuite):
],
)
- def test_partial_batch_reco_keeps_existing_slot_values(self):
+ def test_partial_batch_reco_pools_slot_values(self):
"""Ledger (same wh, batch B): [+10 @ 100, single-SLE reco >> 12]
The reco entry qty (delta 2) does not cover the whole batch, so
- stock_value_difference / qty is not the batch rate: skip the rescale."""
+ stock_value_difference / qty is not the batch rate: skip the rescale.
+ The batch total (1400) is untouched, then pooled across both slots."""
from erpnext.stock.doctype.item.test_item import make_item
item_code = make_item(
@@ -608,11 +615,163 @@ class TestStockAgeing(ERPNextTestSuite):
slots = FIFOSlots(self.filters, sle).generate()
queue = slots[item_code]["fifo_queue"]
+ self.assertEqual(
+ [slot[:4] for slot in queue],
+ [
+ [batch_no, 1, 10.0, "2021-12-01"],
+ [batch_no, 1, 2.0, "2021-12-01"],
+ ],
+ )
+ self.assertAlmostEqual(queue[0][4], 1166.67, places=2)
+ self.assertAlmostEqual(queue[1][4], 233.33, places=2)
+
+ def test_batch_receipts_at_differing_rates_pool_slot_values(self):
+ """Ledger (same wh, batch B): [+10 @ 0, +10 @ 10] and no issue.
+ Nothing goes negative, but the batch is one valuation pool, so both
+ age slots carry the pooled rate instead of their receipt value."""
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ item_code = make_item(
+ "Test Stock Ageing Batch Pool Split",
+ {"is_stock_item": 1, "has_batch_no": 1, "valuation_method": "FIFO"},
+ ).name
+
+ batch_no = "SA-POOL-SPLIT-BATCH"
+ if not frappe.db.exists("Batch", batch_no):
+ frappe.get_doc({"doctype": "Batch", "batch_id": batch_no, "item": item_code}).insert(
+ ignore_permissions=True
+ )
+ frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
+
+ def make_sle(posting_date, voucher_no, actual_qty, qty_after, stock_value_difference):
+ return frappe._dict(
+ name=item_code,
+ actual_qty=actual_qty,
+ qty_after_transaction=qty_after,
+ stock_value_difference=stock_value_difference,
+ valuation_rate=abs(stock_value_difference / actual_qty) if actual_qty else 0,
+ warehouse="WH 1",
+ posting_date=posting_date,
+ voucher_type="Stock Entry",
+ voucher_no=voucher_no,
+ has_serial_no=False,
+ has_batch_no=True,
+ serial_no=None,
+ batch_no=batch_no,
+ )
+
+ sle = [
+ make_sle("2021-12-01", "001", 10, 10, 0),
+ make_sle("2021-12-02", "002", 10, 20, 100),
+ ]
+
+ slots = FIFOSlots(self.filters, sle).generate()
+ queue = slots[item_code]["fifo_queue"]
+
self.assertEqual(
queue,
[
- [batch_no, 1, 10.0, "2021-12-01", 1000.0],
- [batch_no, 1, 2.0, "2021-12-01", 400.0],
+ [batch_no, 1, 10.0, "2021-12-01", 50.0],
+ [batch_no, 1, 10.0, "2021-12-01", 50.0],
+ ],
+ )
+
+ def test_batch_pooling_preserves_total_on_repeating_rate(self):
+ """Ledger (same wh, batch B): [+3 @ 100/3, +6 @ 0, +2 @ 0]
+ The pooled rate does not terminate, so assert the redistributed
+ slot values still add back to the batch total."""
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ item_code = make_item(
+ "Test Stock Ageing Batch Pool Residual",
+ {"is_stock_item": 1, "has_batch_no": 1, "valuation_method": "FIFO"},
+ ).name
+
+ batch_no = "SA-POOL-RESIDUAL-BATCH"
+ if not frappe.db.exists("Batch", batch_no):
+ frappe.get_doc({"doctype": "Batch", "batch_id": batch_no, "item": item_code}).insert(
+ ignore_permissions=True
+ )
+ frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
+
+ def make_sle(posting_date, voucher_no, actual_qty, qty_after, stock_value_difference):
+ return frappe._dict(
+ name=item_code,
+ actual_qty=actual_qty,
+ qty_after_transaction=qty_after,
+ stock_value_difference=stock_value_difference,
+ valuation_rate=abs(stock_value_difference / actual_qty) if actual_qty else 0,
+ warehouse="WH 1",
+ posting_date=posting_date,
+ voucher_type="Stock Entry",
+ voucher_no=voucher_no,
+ has_serial_no=False,
+ has_batch_no=True,
+ serial_no=None,
+ batch_no=batch_no,
+ )
+
+ sle = [
+ make_sle("2021-12-01", "001", 3, 3, 100),
+ make_sle("2021-12-02", "002", 6, 9, 0),
+ make_sle("2021-12-03", "003", 2, 11, 0),
+ ]
+
+ slots = FIFOSlots(self.filters, sle).generate()
+ queue = slots[item_code]["fifo_queue"]
+
+ self.assertEqual([slot[BATCH_SLOT_QTY_INDEX] for slot in queue], [3.0, 6.0, 2.0])
+ self.assertEqual(sum(slot[BATCH_SLOT_VALUE_INDEX] for slot in queue), 100.0)
+
+ def test_batch_issue_at_pooled_rate_keeps_slot_values_positive(self):
+ """Ledger (same wh, batch B): [+10 @ 0, +10 @ 10, -4 @ pooled 5]
+ Consuming the zero-valued head slot at the pooled rate drives it
+ negative; slot values are then rebalanced to the batch pool rate."""
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ item_code = make_item(
+ "Test Stock Ageing Batch Pool Rebalance",
+ {"is_stock_item": 1, "has_batch_no": 1, "valuation_method": "FIFO"},
+ ).name
+
+ batch_no = "SA-POOL-REBALANCE-BATCH"
+ if not frappe.db.exists("Batch", batch_no):
+ frappe.get_doc({"doctype": "Batch", "batch_id": batch_no, "item": item_code}).insert(
+ ignore_permissions=True
+ )
+ frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
+
+ def make_sle(posting_date, voucher_no, actual_qty, qty_after, stock_value_difference):
+ return frappe._dict(
+ name=item_code,
+ actual_qty=actual_qty,
+ qty_after_transaction=qty_after,
+ stock_value_difference=stock_value_difference,
+ valuation_rate=abs(stock_value_difference / actual_qty) if actual_qty else 0,
+ warehouse="WH 1",
+ posting_date=posting_date,
+ voucher_type="Stock Entry",
+ voucher_no=voucher_no,
+ has_serial_no=False,
+ has_batch_no=True,
+ serial_no=None,
+ batch_no=batch_no,
+ )
+
+ sle = [
+ make_sle("2021-12-01", "001", 10, 10, 0),
+ make_sle("2021-12-02", "002", 10, 20, 100),
+ make_sle("2021-12-03", "003", -4, 16, -20),
+ ]
+
+ slots = FIFOSlots(self.filters, sle).generate()
+ queue = slots[item_code]["fifo_queue"]
+
+ self.assertEqual(
+ queue,
+ [
+ [batch_no, 1, 6.0, "2021-12-01", 30.0],
+ [batch_no, 1, 10.0, "2021-12-01", 50.0],
],
)
diff --git a/erpnext/stock/report/stock_balance/stock_balance.js b/erpnext/stock/report/stock_balance/stock_balance.js
index eef79ce6a27..3060042034b 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.js
+++ b/erpnext/stock/report/stock_balance/stock_balance.js
@@ -136,7 +136,7 @@ frappe.query_reports["Stock Balance"] = {
fieldname: "include_zero_stock_items",
label: __("Include Zero Stock Items"),
fieldtype: "Check",
- default: 0,
+ default: 1,
},
{
fieldname: "show_dimension_wise_stock",
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 7f6cc176373..23e99899068 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -497,7 +497,9 @@ class update_entries_after:
self.data = frappe._dict()
- if not self.repost_doc or not self.args.get("item_wh_wise_last_posted_sle"):
+ if (not self.repost_doc or not self.args.get("item_wh_wise_last_posted_sle")) and not self.args.get(
+ "cancelled"
+ ):
self.initialize_previous_data(self.args)
self.build()
@@ -805,10 +807,23 @@ class update_entries_after:
def process_sle_against_current_timestamp(self):
sl_entries = get_sle_against_current_voucher(self.args)
+ if self.args.get("cancelled") and sl_entries:
+ self.seed_previous_sle_for_cancellation(sl_entries[0])
for sle in sl_entries:
sle["timestamp"] = sle.posting_datetime
self.process_sle(sle)
+ def seed_previous_sle_for_cancellation(self, anchor_sle):
+ key = (anchor_sle.item_code, anchor_sle.warehouse)
+ if key in self.prev_sle_dict:
+ return
+
+ args = frappe._dict(anchor_sle)
+ args["sle_id"] = args.name
+ prev_sle = get_previous_sle_of_current_voucher(args)
+ if prev_sle:
+ self.prev_sle_dict[key] = prev_sle
+
def get_future_entries_to_fix(self):
# includes current entry!
args = self.data[self.args.warehouse].previous_sle or frappe._dict(
diff --git a/erpnext/stock/tests/test_expenses_added_to_stock.py b/erpnext/stock/tests/test_expenses_added_to_stock.py
new file mode 100644
index 00000000000..75170494875
--- /dev/null
+++ b/erpnext/stock/tests/test_expenses_added_to_stock.py
@@ -0,0 +1,211 @@
+# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import frappe
+
+from erpnext.accounts.doctype.account.test_account import create_account
+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
+
+COMPANY = "_Test Company with perpetual inventory"
+WAREHOUSE = "Stores - TCP1"
+
+
+class TestExpensesAddedToStock(ERPNextTestSuite):
+ def setUp(self):
+ self.restore_stock_expense_settings()
+ self.eats_account = create_account(
+ account_name="Expenses Added To Stock",
+ parent_account="Expenses - TCP1",
+ company=COMPANY,
+ )
+ self.eats_contra_account = create_account(
+ account_name="Expenses Added To Stock Contra",
+ parent_account="Expenses - TCP1",
+ company=COMPANY,
+ )
+ self.purchase_expense_account = create_account(
+ account_name="Test Purchase Expense EATS",
+ parent_account="Expenses - TCP1",
+ company=COMPANY,
+ )
+ self.purchase_expense_contra_account = create_account(
+ account_name="Test Purchase Expense Contra EATS",
+ parent_account="Expenses - TCP1",
+ company=COMPANY,
+ )
+ frappe.db.set_value(
+ "Company",
+ COMPANY,
+ {
+ "expenses_added_to_stock_account": self.eats_account,
+ "expenses_added_to_stock_contra_account": self.eats_contra_account,
+ "purchase_expense_account": self.purchase_expense_account,
+ "purchase_expense_contra_account": self.purchase_expense_contra_account,
+ },
+ )
+ frappe.db.set_single_value("Accounts Settings", "book_stock_expense_gl_entries", 1)
+ self.item = make_item(properties={"is_stock_item": 1, "valuation_method": "FIFO"}).name
+
+ def restore_stock_expense_settings(self):
+ """These are a Single and Company fields, so they outlive the test. Left set, every later
+ Purchase Receipt / Invoice in the run has to resolve the expense account pair and throws
+ for any company that has none configured."""
+ account_fields = [
+ "expenses_added_to_stock_account",
+ "expenses_added_to_stock_contra_account",
+ "purchase_expense_account",
+ "purchase_expense_contra_account",
+ ]
+ previous_accounts = frappe.db.get_value("Company", COMPANY, account_fields, as_dict=True)
+ previous_flag = frappe.db.get_single_value("Accounts Settings", "book_stock_expense_gl_entries")
+
+ self.addCleanup(frappe.db.set_value, "Company", COMPANY, dict(previous_accounts))
+ self.addCleanup(
+ frappe.db.set_single_value,
+ "Accounts Settings",
+ "book_stock_expense_gl_entries",
+ previous_flag,
+ )
+
+ def get_gl_balances(self, voucher_type, voucher_no):
+ entries = frappe.get_all(
+ "GL Entry",
+ filters={
+ "voucher_type": voucher_type,
+ "voucher_no": voucher_no,
+ "is_cancelled": 0,
+ "account": ("in", [self.eats_account, self.eats_contra_account]),
+ },
+ fields=["account", "debit", "credit"],
+ )
+
+ balances = frappe._dict({self.eats_account: 0.0, self.eats_contra_account: 0.0})
+ debits = frappe._dict({self.eats_account: 0.0, self.eats_contra_account: 0.0})
+ credits = frappe._dict({self.eats_account: 0.0, self.eats_contra_account: 0.0})
+ for entry in entries:
+ balances[entry.account] += entry.debit - entry.credit
+ debits[entry.account] += entry.debit
+ credits[entry.account] += entry.credit
+
+ return balances, debits, credits
+
+ def test_material_receipt_books_expenses_added_to_stock(self):
+ se = make_stock_entry(item_code=self.item, to_warehouse=WAREHOUSE, qty=10, rate=100, company=COMPANY)
+
+ _balances, debits, credits = self.get_gl_balances("Stock Entry", se.name)
+ self.assertEqual(debits[self.eats_account], 1000)
+ self.assertEqual(credits[self.eats_contra_account], 1000)
+
+ def test_material_issue_books_reverse_pair(self):
+ make_stock_entry(item_code=self.item, to_warehouse=WAREHOUSE, qty=10, rate=100, company=COMPANY)
+ se = make_stock_entry(item_code=self.item, from_warehouse=WAREHOUSE, qty=5, company=COMPANY)
+
+ _balances, debits, credits = self.get_gl_balances("Stock Entry", se.name)
+ self.assertEqual(credits[self.eats_account], 500)
+ self.assertEqual(debits[self.eats_contra_account], 500)
+
+ def test_material_transfer_books_nothing(self):
+ make_stock_entry(item_code=self.item, to_warehouse=WAREHOUSE, qty=10, rate=100, company=COMPANY)
+ se = make_stock_entry(
+ item_code=self.item,
+ from_warehouse=WAREHOUSE,
+ to_warehouse="Finished Goods - TCP1",
+ qty=5,
+ company=COMPANY,
+ )
+
+ _balances, debits, credits = self.get_gl_balances("Stock Entry", se.name)
+ self.assertEqual(debits[self.eats_account], 0)
+ self.assertEqual(credits[self.eats_account], 0)
+
+ def test_stock_reconciliation_books_pair(self):
+ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
+ create_stock_reconciliation,
+ )
+
+ make_stock_entry(item_code=self.item, to_warehouse=WAREHOUSE, qty=10, rate=100, company=COMPANY)
+ sr = create_stock_reconciliation(
+ item_code=self.item, warehouse=WAREHOUSE, qty=15, rate=100, company=COMPANY
+ )
+
+ _balances, debits, credits = self.get_gl_balances("Stock Reconciliation", sr.name)
+ self.assertEqual(debits[self.eats_account], 500)
+ self.assertEqual(credits[self.eats_contra_account], 500)
+
+ def test_landed_cost_voucher_books_pair(self):
+ from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
+ create_landed_cost_voucher,
+ )
+ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
+
+ pr = make_purchase_receipt(
+ company=COMPANY, warehouse=WAREHOUSE, item_code=self.item, qty=10, rate=100
+ )
+
+ _balances, debits, credits = self.get_gl_balances("Purchase Receipt", pr.name)
+ self.assertEqual(debits[self.eats_account], 0)
+
+ create_landed_cost_voucher("Purchase Receipt", pr.name, COMPANY, charges=200)
+
+ _balances, debits, credits = self.get_gl_balances("Purchase Receipt", pr.name)
+ self.assertEqual(debits[self.eats_account], 200)
+ self.assertEqual(credits[self.eats_contra_account], 200)
+
+ def test_no_entries_when_feature_disabled(self):
+ frappe.db.set_single_value("Accounts Settings", "book_stock_expense_gl_entries", 0)
+
+ se = make_stock_entry(item_code=self.item, to_warehouse=WAREHOUSE, qty=10, rate=100, company=COMPANY)
+
+ _balances, debits, credits = self.get_gl_balances("Stock Entry", se.name)
+ self.assertEqual(debits[self.eats_account], 0)
+ self.assertEqual(credits[self.eats_contra_account], 0)
+
+ def test_missing_contra_account_raises_when_feature_enabled(self):
+ frappe.db.set_value("Company", COMPANY, "expenses_added_to_stock_contra_account", None)
+
+ self.assertRaises(
+ frappe.ValidationError,
+ make_stock_entry,
+ item_code=self.item,
+ to_warehouse=WAREHOUSE,
+ qty=10,
+ rate=100,
+ company=COMPANY,
+ )
+
+ def test_service_item_books_nothing_on_purchase_invoice_with_update_stock(self):
+ """A service item carries no stock value, so booking it produced a GL row with neither a
+ debit nor a credit, which GL Entry rejects outright."""
+ from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
+
+ service_item = make_item(properties={"is_stock_item": 0}).name
+
+ pi = make_purchase_invoice(
+ company=COMPANY,
+ warehouse=WAREHOUSE,
+ item_code=service_item,
+ qty=1,
+ rate=500,
+ update_stock=1,
+ expense_account="Cost of Goods Sold - TCP1",
+ cost_center="Main - TCP1",
+ )
+
+ self.assertEqual(pi.docstatus, 1)
+
+ _balances, debits, credits = self.get_gl_balances("Purchase Invoice", pi.name)
+ self.assertEqual(debits[self.eats_account], 0)
+ self.assertEqual(credits[self.eats_contra_account], 0)
+
+ booked = frappe.get_all(
+ "GL Entry",
+ filters={
+ "voucher_type": "Purchase Invoice",
+ "voucher_no": pi.name,
+ "is_cancelled": 0,
+ "account": self.purchase_expense_account,
+ },
+ )
+ self.assertFalse(booked)
diff --git a/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.json b/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.json
index a0b163f4271..034e98f37b6 100644
--- a/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.json
+++ b/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.json
@@ -8,7 +8,6 @@
"document_type": "Document",
"engine": "InnoDB",
"field_order": [
- "title",
"naming_series",
"sales_order",
"customer",
@@ -29,6 +28,7 @@
"service_items_section",
"service_items",
"tab_other_info",
+ "title",
"order_status_section",
"status",
"per_raw_material_received",
@@ -43,10 +43,8 @@
"fields": [
{
"allow_on_submit": 1,
- "default": "{customer_name}",
"fieldname": "title",
"fieldtype": "Data",
- "hidden": 1,
"label": "Title",
"no_copy": 1,
"print_hide": 1
@@ -306,7 +304,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2026-02-26 17:16:21.697846",
+ "modified": "2026-07-27 11:20:14.512336",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Inward Order",
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json
index d0223a3acd2..317dd3fd71d 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json
@@ -8,7 +8,6 @@
"document_type": "Document",
"engine": "InnoDB",
"field_order": [
- "title",
"naming_series",
"purchase_order",
"supplier",
@@ -55,6 +54,7 @@
"additional_costs",
"total_additional_costs",
"tab_other_info",
+ "title",
"order_status_section",
"status",
"column_break_39",
@@ -69,10 +69,8 @@
"fields": [
{
"allow_on_submit": 1,
- "default": "{supplier_name}",
"fieldname": "title",
"fieldtype": "Data",
- "hidden": 1,
"label": "Title",
"no_copy": 1,
"print_hide": 1
@@ -494,7 +492,7 @@
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
- "modified": "2025-11-14 10:31:40.682892",
+ "modified": "2026-07-27 11:20:14.512336",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Order",
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
index bf803fc3d9a..7992bfdd546 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
+++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
@@ -336,6 +336,85 @@ class TestSubcontractingOrder(ERPNextTestSuite):
bin_after_cancel_sco.reserved_qty_for_sub_contract, bin_before_sco.reserved_qty_for_sub_contract
)
+ def test_close_subcontracting_order_releases_reserved_qty(self):
+ # RM in stock at the reserve warehouse for transfer
+ make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item", qty=10, basic_rate=100)
+ make_stock_entry(
+ target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100
+ )
+
+ bin_before_sco = frappe.db.get_value(
+ "Bin",
+ filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
+ fieldname="reserved_qty_for_sub_contract",
+ as_dict=1,
+ )
+
+ # Create SCO with a reserve warehouse on the supplied items
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 1",
+ "qty": 10,
+ "rate": 100,
+ "fg_item": "_Test FG Item",
+ "fg_item_qty": 10,
+ },
+ ]
+ sco = get_subcontracting_order(service_items=service_items)
+
+ # Transfer only 90% of the raw materials to the supplier warehouse
+ ste = frappe.get_doc(make_rm_stock_entry(sco.name))
+ for item in ste.items:
+ item.qty *= 0.9
+ ste.save()
+ ste.submit()
+ sco.load_from_db()
+ self.assertEqual(sco.status, "Partial Material Transferred")
+
+ # Receive only a partial qty so the order stays open (per_received < 100)
+ scr = make_subcontracting_receipt(sco.name)
+ scr.items[0].qty -= 1
+ scr.save()
+ scr.submit()
+ sco.load_from_db()
+ self.assertEqual(sco.status, "Partially Received")
+
+ # Keep another SCO open so transfers from the closed SCO must not reduce its reservation
+ open_sco = get_subcontracting_order(service_items=service_items)
+ self.assertEqual(open_sco.status, "Open")
+
+ bin_before_close = frappe.db.get_value(
+ "Bin",
+ filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
+ fieldname=["reserved_qty_for_sub_contract", "projected_qty"],
+ as_dict=1,
+ )
+
+ # One unit remains reserved for the partially transferred SCO, plus ten for the open SCO
+ self.assertEqual(
+ bin_before_close.reserved_qty_for_sub_contract,
+ bin_before_sco.reserved_qty_for_sub_contract + 11,
+ )
+
+ # Close the partially-received order
+ sco.update_status("Closed")
+ self.assertEqual(sco.status, "Closed")
+
+ bin_after_close = frappe.db.get_value(
+ "Bin",
+ filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
+ fieldname=["reserved_qty_for_sub_contract", "projected_qty"],
+ as_dict=1,
+ )
+
+ # Closing releases the remaining unit without applying its transfer against the open SCO
+ self.assertEqual(
+ bin_after_close.reserved_qty_for_sub_contract,
+ bin_before_sco.reserved_qty_for_sub_contract + 10,
+ )
+ self.assertEqual(bin_after_close.projected_qty, bin_before_close.projected_qty + 1)
+
def test_send_to_subcontractor_ste_submit_without_sco_write_permission(self):
"""A Stock-only user (can submit Stock Entries but has no Subcontracting Order write) must be
able to submit and cancel a 'Send to Subcontractor' Stock Entry. The SCO status update on the