diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 6701673cc7f..37d8363beaa 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -43,3 +43,6 @@ jobs: - name: Run Semgrep rules run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness + + - name: Semgrep for Test Correctness + run: semgrep ci --include=**/test_*.py --config ./semgrep/test-correctness.yml diff --git a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py index 81e639dc6b2..250442a3cd4 100644 --- a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py @@ -10,9 +10,6 @@ from erpnext.tests.utils import ERPNextTestSuite class TestAccountingDimension(ERPNextTestSuite): - def setUp(self): - create_dimension() - def test_dimension_against_sales_invoice(self): si = create_sales_invoice(do_not_save=1) @@ -77,63 +74,3 @@ class TestAccountingDimension(ERPNextTestSuite): si.save() self.assertRaises(frappe.ValidationError, si.submit) - - -def create_dimension(): - frappe.set_user("Administrator") - - if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}): - dimension = frappe.get_doc( - { - "doctype": "Accounting Dimension", - "document_type": "Department", - } - ) - dimension.append( - "dimension_defaults", - { - "company": "_Test Company", - "reference_document": "Department", - "default_dimension": "_Test Department - _TC", - }, - ) - dimension.insert() - dimension.save() - else: - dimension = frappe.get_doc("Accounting Dimension", "Department") - dimension.disabled = 0 - dimension.save() - - if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}): - dimension1 = frappe.get_doc( - { - "doctype": "Accounting Dimension", - "document_type": "Location", - } - ) - - dimension1.append( - "dimension_defaults", - { - "company": "_Test Company", - "reference_document": "Location", - "default_dimension": "Block 1", - }, - ) - - dimension1.insert() - dimension1.save() - else: - dimension1 = frappe.get_doc("Accounting Dimension", "Location") - dimension1.disabled = 0 - dimension1.save() - - -def disable_dimension(): - dimension1 = frappe.get_doc("Accounting Dimension", "Department") - dimension1.disabled = 1 - dimension1.save() - - dimension2 = frappe.get_doc("Accounting Dimension", "Location") - dimension2.disabled = 1 - dimension2.save() diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py index 9bed10824bb..fe7d4706967 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py @@ -5,10 +5,6 @@ import unittest import frappe -from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( - create_dimension, - disable_dimension, -) from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError from erpnext.tests.utils import ERPNextTestSuite @@ -16,7 +12,6 @@ from erpnext.tests.utils import ERPNextTestSuite class TestAccountingDimensionFilter(ERPNextTestSuite): def setUp(self): - create_dimension() create_accounting_dimension_filter() self.invoice_list = [] diff --git a/erpnext/accounts/doctype/bank_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py index c0dc6467f8f..9fe5b4ba3fb 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.py +++ b/erpnext/accounts/doctype/bank_account/bank_account.py @@ -116,6 +116,7 @@ def get_default_company_bank_account(company, party_type, party): @frappe.whitelist() def get_bank_account_details(bank_account): + frappe.has_permission("Bank Account", doc=bank_account, ptype="read", throw=True) return frappe.get_cached_value( "Bank Account", bank_account, ["account", "bank", "bank_account_no"], as_dict=1 ) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 3abfa176622..452164c728c 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -489,4 +489,5 @@ def rename_temporarily_named_docs(doctype): for hook in frappe.get_hooks(hook_type): frappe.call(hook, newname=newname, oldname=oldname) - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() diff --git a/erpnext/accounts/doctype/ledger_merge/ledger_merge.py b/erpnext/accounts/doctype/ledger_merge/ledger_merge.py index 008b4115f5f..dc3fd5a9d04 100644 --- a/erpnext/accounts/doctype/ledger_merge/ledger_merge.py +++ b/erpnext/accounts/doctype/ledger_merge/ledger_merge.py @@ -71,14 +71,16 @@ def start_merge(docname): ledger_merge.account, ) row.db_set("merged", 1) - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() successful_merges += 1 frappe.publish_realtime( "ledger_merge_progress", {"ledger_merge": ledger_merge.name, "current": successful_merges, "total": total}, ) except Exception: - frappe.db.rollback() + if not frappe.in_test: + frappe.db.rollback() ledger_merge.log_error("Ledger merge failed") finally: if successful_merges == total: diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js index 4938e6690e5..466b38126d7 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js @@ -50,6 +50,7 @@ frappe.ui.form.on("Opening Invoice Creation Tool", { refresh: function (frm) { frm.disable_save(); + frm.trigger("create_missing_party"); !frm.doc.import_in_progress && frm.trigger("make_dashboard"); frm.page.set_primary_action(__("Create Invoices"), () => { let btn_primary = frm.page.btn_primary.get(0); @@ -123,7 +124,8 @@ frappe.ui.form.on("Opening Invoice Creation Tool", { invoice_type: function (frm) { $.each(frm.doc.invoices, (idx, row) => { row.party_type = frm.doc.invoice_type == "Sales" ? "Customer" : "Supplier"; - row.party = ""; + frappe.model.set_value(row.doctype, row.name, "party", ""); + frappe.model.set_value(row.doctype, row.name, "party_name", ""); }); frm.refresh_fields(); }, @@ -162,9 +164,35 @@ frappe.ui.form.on("Opening Invoice Creation Tool", { row.party_type = frm.doc.invoice_type == "Sales" ? "Customer" : "Supplier"; }); }, + + create_missing_party: function (frm) { + if (frm.doc.create_missing_party) { + frm.fields_dict["invoices"].grid.update_docfield_property("party", "reqd", 0); + frm.fields_dict["invoices"].grid.update_docfield_property("party_name", "read_only", 0); + } else { + frm.fields_dict["invoices"].grid.update_docfield_property("party", "reqd", 1); + frm.fields_dict["invoices"].grid.update_docfield_property("party_name", "read_only", 1); + } + frm.refresh_field("invoices"); + }, }); frappe.ui.form.on("Opening Invoice Creation Tool Item", { + party: function (frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (!row.party) { + frappe.model.set_value(cdt, cdn, "party_name", ""); + return; + } + + let party_type = frm.doc.invoice_type == "Sales" ? "Customer" : "Supplier"; + let name_field = party_type === "Customer" ? "customer_name" : "supplier_name"; + + frappe.db.get_value(party_type, row.party, name_field, (r) => { + frappe.model.set_value(cdt, cdn, "party_name", r?.[name_field] || ""); + }); + }, + invoices_add: (frm) => { frm.trigger("update_invoice_table"); }, diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json index deeee72c18a..8d1c3e87ba1 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json @@ -8,9 +8,9 @@ "engine": "InnoDB", "field_order": [ "company", - "create_missing_party", "column_break_3", "invoice_type", + "create_missing_party", "accounting_dimensions_section", "cost_center", "dimension_col_break", @@ -29,7 +29,7 @@ }, { "default": "0", - "description": "Create missing customer or supplier.", + "description": "If party does not exist, create it using the Party Name field.", "fieldname": "create_missing_party", "fieldtype": "Check", "label": "Create Missing Party" @@ -65,10 +65,10 @@ "options": "Cost Center" }, { - "fieldname": "project", - "fieldtype": "Link", - "label": "Project", - "options": "Project" + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" }, { "collapsible": 1, @@ -84,7 +84,7 @@ "hide_toolbar": 1, "issingle": 1, "links": [], - "modified": "2024-03-27 13:10:06.564397", + "modified": "2026-03-23 00:32:15.600086", "modified_by": "Administrator", "module": "Accounts", "name": "Opening Invoice Creation Tool", @@ -101,8 +101,9 @@ } ], "quick_entry": 1, + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py index 2f3a893a73f..3949e242567 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py @@ -5,7 +5,7 @@ import frappe from frappe import _, scrub from frappe.model.document import Document -from frappe.utils import flt, nowdate +from frappe.utils import escape_html, flt, nowdate from frappe.utils.background_jobs import enqueue, is_job_enqueued from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( @@ -32,6 +32,7 @@ class OpeningInvoiceCreationTool(Document): create_missing_party: DF.Check invoice_type: DF.Literal["Sales", "Purchase"] invoices: DF.Table[OpeningInvoiceCreationToolItem] + project: DF.Link | None # end: auto-generated types def onload(self): @@ -85,6 +86,11 @@ class OpeningInvoiceCreationTool(Document): ) prepare_invoice_summary(doctype, invoices) + invoices_summary_companies = list(invoices_summary.keys()) + + for company in invoices_summary_companies: + invoices_summary[escape_html(company)] = invoices_summary.pop(company) + return invoices_summary, max_count def validate_company(self): @@ -102,10 +108,20 @@ class OpeningInvoiceCreationTool(Document): row.due_date = row.due_date or nowdate() def validate_mandatory_invoice_fields(self, row): - if not frappe.db.exists(row.party_type, row.party): - if self.create_missing_party: - self.add_party(row.party_type, row.party) - else: + if self.create_missing_party: + if not row.party and not row.party_name: + frappe.throw(_("Row #{}: Either Party ID or Party Name is required").format(row.idx)) + + if not row.party and row.party_name: + row.party = self.add_party(row.party_type, row.party_name) + + if row.party and not frappe.db.exists(row.party_type, row.party): + row.party = self.add_party(row.party_type, row.party) + + else: + if not row.party: + frappe.throw(_("Row #{}: Party ID is required").format(row.idx)) + if not frappe.db.exists(row.party_type, row.party): frappe.throw( _("Row #{}: {} {} does not exist.").format( row.idx, frappe.bold(row.party_type), frappe.bold(row.party) @@ -113,7 +129,7 @@ class OpeningInvoiceCreationTool(Document): ) mandatory_error_msg = _("Row #{0}: {1} is required to create the Opening {2} Invoices") - for d in ("Party", "Outstanding Amount", "Temporary Opening Account"): + for d in ("Outstanding Amount", "Temporary Opening Account"): if not row.get(scrub(d)): frappe.throw(mandatory_error_msg.format(row.idx, d, self.invoice_type)) @@ -159,6 +175,7 @@ class OpeningInvoiceCreationTool(Document): party_doc.flags.ignore_mandatory = True party_doc.save(ignore_permissions=True) + return party_doc.name def get_invoice_dict(self, row=None): def get_item_dict(): @@ -262,7 +279,8 @@ def start_import(invoices): doc.flags.ignore_mandatory = True doc.insert(set_name=invoice_number) doc.submit() - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() names.append(doc.name) except Exception: errors += 1 diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool_dashboard.html b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool_dashboard.html index afbcfa5602a..43be52717c3 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool_dashboard.html +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool_dashboard.html @@ -1,5 +1,5 @@ {% $.each(data, (company, summary) => { %} -
{{ company }}
+
{{ company }}
@@ -23,7 +23,7 @@ - + {% endif %} {% }); %} diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py index 3d57c781983..c01ada6d317 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py @@ -3,10 +3,6 @@ import frappe -from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( - create_dimension, - disable_dimension, -) from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import ( get_temporary_opening_account, ) @@ -14,11 +10,6 @@ from erpnext.tests.utils import ERPNextTestSuite class TestOpeningInvoiceCreationTool(ERPNextTestSuite): - def setUp(self): - if not frappe.db.exists("Company", "_Test Opening Invoice Company"): - make_company() - create_dimension() - def make_invoices( self, invoice_type="Sales", @@ -183,19 +174,6 @@ def get_opening_invoice_creation_dict(**args): return invoice_dict -def make_company(): - if frappe.db.exists("Company", "_Test Opening Invoice Company"): - return frappe.get_doc("Company", "_Test Opening Invoice Company") - - company = frappe.new_doc("Company") - company.company_name = "_Test Opening Invoice Company" - company.abbr = "_TOIC" - company.default_currency = "INR" - company.country = "Pakistan" - company.insert() - return company - - def make_customer(customer=None): customer_name = customer or "Opening Customer" customer = frappe.get_doc( diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json b/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json index 29daab42439..74ce2a6fb67 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json @@ -8,6 +8,7 @@ "invoice_number", "party_type", "party", + "party_name", "temporary_opening_account", "column_break_3", "posting_date", @@ -35,9 +36,9 @@ "fieldname": "party", "fieldtype": "Dynamic Link", "in_list_view": 1, - "label": "Party", - "options": "party_type", - "reqd": 1 + "label": "Party ID", + "mandatory_depends_on": "eval: !parent.create_missing_party", + "options": "party_type" }, { "fieldname": "temporary_opening_account", @@ -118,11 +119,17 @@ "fieldname": "supplier_invoice_date", "fieldtype": "Date", "label": "Supplier Invoice Date" + }, + { + "fieldname": "party_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Party Name" } ], "istable": 1, "links": [], - "modified": "2025-12-01 16:18:07.997594", + "modified": "2026-03-20 02:11:42.023575", "modified_by": "Administrator", "module": "Accounts", "name": "Opening Invoice Creation Tool Item", diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.py b/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.py index 1ea025322b4..38e97672c96 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.py @@ -22,7 +22,8 @@ class OpeningInvoiceCreationToolItem(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data - party: DF.DynamicLink + party: DF.DynamicLink | None + party_name: DF.Data | None party_type: DF.Link | None posting_date: DF.Date | None qty: DF.Data | None diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 289c19a5885..d60f77120b2 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -2376,9 +2376,7 @@ def get_outstanding_reference_documents(args, validate=False): vouchers=args.get("vouchers") or None, ) - outstanding_invoices = split_invoices_based_on_payment_terms( - outstanding_invoices, args.get("company") - ) + outstanding_invoices = split_refdocs_based_on_payment_terms(outstanding_invoices, args.get("company")) for d in outstanding_invoices: d["exchange_rate"] = 1 @@ -2416,6 +2414,8 @@ def get_outstanding_reference_documents(args, validate=False): filters=args, ) + orders_to_be_billed = split_refdocs_based_on_payment_terms(orders_to_be_billed, args.get("company")) + data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed if not data: @@ -2438,13 +2438,13 @@ def get_outstanding_reference_documents(args, validate=False): return data -def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list: +def split_refdocs_based_on_payment_terms(refdocs, company) -> list: """Split a list of invoices based on their payment terms.""" - exc_rates = get_currency_data(outstanding_invoices, company) + exc_rates = get_currency_data(refdocs, company) - outstanding_invoices_after_split = [] - for entry in outstanding_invoices: - if entry.voucher_type in ["Sales Invoice", "Purchase Invoice"]: + outstanding_refdoc_after_split = [] + for entry in refdocs: + if entry.voucher_type in ["Sales Invoice", "Purchase Invoice", "Sales Order", "Purchase Order"]: if payment_term_template := frappe.db.get_value( entry.voucher_type, entry.voucher_no, "payment_terms_template" ): @@ -2459,25 +2459,25 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list ), alert=True, ) - outstanding_invoices_after_split += split_rows + outstanding_refdoc_after_split += split_rows continue # If not an invoice or no payment terms template, add as it is - outstanding_invoices_after_split.append(entry) + outstanding_refdoc_after_split.append(entry) - return outstanding_invoices_after_split + return outstanding_refdoc_after_split -def get_currency_data(outstanding_invoices: list, company: str | None = None) -> dict: +def get_currency_data(outstanding_refdocs: list, company: str | None = None) -> dict: """Get currency and conversion data for a list of invoices.""" exc_rates = frappe._dict() company_currency = frappe.db.get_value("Company", company, "default_currency") if company else None - for doctype in ["Sales Invoice", "Purchase Invoice"]: - invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype] + for doctype in ["Sales Invoice", "Purchase Invoice", "Sales Order", "Purchase Order"]: + refdoc = [x.voucher_no for x in outstanding_refdocs if x.voucher_type == doctype] for x in frappe.db.get_all( doctype, - filters={"name": ["in", invoices]}, + filters={"name": ["in", refdoc]}, fields=["name", "currency", "conversion_rate", "party_account_currency"], ): exc_rates[x.name] = frappe._dict( diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 12f5276fbfb..9d2890f5e79 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -2019,6 +2019,92 @@ class TestPaymentEntry(ERPNextTestSuite): self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, pe.doctype, pe.name) self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, "Journal Entry", jv[0]) + def test_outstanding_orders_split_by_payment_terms(self): + create_payment_terms_template() + + so = make_sales_order(do_not_save=1, qty=1, rate=200) + so.payment_terms_template = "Test Receivable Template" + so.save().submit() + + args = { + "posting_date": nowdate(), + "company": so.company, + "party_type": "Customer", + "payment_type": "Receive", + "party": so.customer, + "party_account": "Debtors - _TC", + "get_orders_to_be_billed": True, + } + + references = get_outstanding_reference_documents(args) + + self.assertEqual(len(references), 2) + self.assertEqual(references[0].voucher_no, so.name) + self.assertEqual(references[1].voucher_no, so.name) + self.assertEqual(references[0].payment_term, "Basic Amount Receivable") + self.assertEqual(references[1].payment_term, "Tax Receivable") + + def test_outstanding_orders_no_split_when_allocate_disabled(self): + create_payment_terms_template() + + template = frappe.get_doc("Payment Terms Template", "Test Receivable Template") + template.allocate_payment_based_on_payment_terms = 0 + template.save() + + so = make_sales_order(do_not_save=1, qty=1, rate=200) + so.payment_terms_template = "Test Receivable Template" + so.save().submit() + + args = { + "posting_date": nowdate(), + "company": so.company, + "party_type": "Customer", + "payment_type": "Receive", + "party": so.customer, + "party_account": "Debtors - _TC", + "get_orders_to_be_billed": True, + } + + references = get_outstanding_reference_documents(args) + + self.assertEqual(len(references), 1) + self.assertIsNone(references[0].payment_term) + + template.allocate_payment_based_on_payment_terms = 1 + template.save() + + def test_outstanding_multicurrency_sales_order_split(self): + create_payment_terms_template() + + so = make_sales_order( + customer="_Test Customer USD", + currency="USD", + qty=1, + rate=100, + do_not_submit=True, + ) + so.payment_terms_template = "Test Receivable Template" + so.conversion_rate = 50 + so.save().submit() + + args = { + "posting_date": nowdate(), + "company": so.company, + "party_type": "Customer", + "payment_type": "Receive", + "party": so.customer, + "party_account": "Debtors - _TC", + "get_orders_to_be_billed": True, + } + + references = get_outstanding_reference_documents(args) + + # Should split without throwing currency errors + self.assertEqual(len(references), 2) + for ref in references: + self.assertEqual(ref.voucher_no, so.name) + self.assertIsNotNone(ref.payment_term) + def create_payment_entry(**args): payment_entry = frappe.new_doc("Payment Entry") diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index c95945bf6e2..e16e132957f 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -750,7 +750,8 @@ def make_payment_request(**args): pr.submit() if args.order_type == "Shopping Cart": - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() frappe.local.response["type"] = "redirect" frappe.local.response["location"] = pr.get_payment_url() diff --git a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py index af5f73a39ec..05e24d16a3a 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py +++ b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py @@ -4,10 +4,6 @@ import unittest import frappe -from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( - create_dimension, - disable_dimension, -) from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import ( make_closing_entry_from_opening, ) @@ -162,7 +158,6 @@ class TestPOSClosingEntry(ERPNextTestSuite): test case to check whether we can create POS Closing Entry without mandatory accounting dimension """ - create_dimension() location = frappe.get_doc("Accounting Dimension", "Location") location.dimension_defaults[0].mandatory_for_bs = True location.save() @@ -198,7 +193,6 @@ class TestPOSClosingEntry(ERPNextTestSuite): ) accounting_dimension_department.mandatory_for_bs = 0 accounting_dimension_department.save() - disable_dimension() def test_merging_into_sales_invoice_for_batched_item(self): frappe.flags.print_message = False diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 9cf27216b1e..a40cd03240e 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -563,10 +563,10 @@ def send_emails(document_name, from_scheduler=False, posting_date=None): new_from_date = add_months(new_to_date, -1 * doc.filter_duration) doc.add_comment("Comment", "Emails sent on: " + frappe.utils.format_datetime(frappe.utils.now())) if doc.report == "General Ledger": - doc.db_set("to_date", new_to_date, commit=True) - doc.db_set("from_date", new_from_date, commit=True) + frappe.db.set_value(doc.doctype, doc.name, "to_date", new_to_date) + frappe.db.set_value(doc.doctype, doc.name, "from_date", new_from_date) else: - doc.db_set("posting_date", new_to_date, commit=True) + frappe.db.set_value(doc.doctype, doc.name, "posting_date", new_to_date) return True else: return False diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index d2021e0f9a4..adb7dad6726 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -8,7 +8,6 @@ "email_append_to": 1, "engine": "InnoDB", "field_order": [ - "title", "naming_series", "supplier", "supplier_name", @@ -209,16 +208,6 @@ "connections_tab" ], "fields": [ - { - "allow_on_submit": 1, - "default": "{supplier_name}", - "fieldname": "title", - "fieldtype": "Data", - "hidden": 1, - "label": "Title", - "no_copy": 1, - "print_hide": 1 - }, { "fieldname": "naming_series", "fieldtype": "Select", @@ -1693,7 +1682,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2026-03-17 20:44:00.221219", + "modified": "2026-03-25 11:45:38.696888", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", @@ -1756,6 +1745,6 @@ "sort_order": "DESC", "states": [], "timeline_field": "supplier", - "title_field": "title", + "title_field": "supplier_name", "track_changes": 1 } diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 527c33225c6..7c076e197a5 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -203,7 +203,6 @@ class PurchaseInvoice(BuyingController): taxes_and_charges_deducted: DF.Currency tc_name: DF.Link | None terms: DF.TextEditor | None - title: DF.Data | None to_date: DF.Date | None total: DF.Currency total_advance: DF.Currency @@ -617,12 +616,13 @@ class PurchaseInvoice(BuyingController): frappe.db.set_value(self.doctype, self.name, "against_expense_account", self.against_expense_account) def po_required(self): - if frappe.db.get_single_value("Buying Settings", "po_required") == "Yes": - if frappe.get_value( + if ( + frappe.db.get_single_value("Buying Settings", "po_required") == "Yes" + and not self.is_internal_transfer() + and not frappe.get_value( "Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_order" - ): - return - + ) + ): for d in self.get("items"): if not d.purchase_order: msg = _("Purchase Order Required for item {}").format(frappe.bold(d.item_code)) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 33117c639dc..09febdfd915 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -2189,11 +2189,6 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin): def test_offsetting_entries_for_accounting_dimensions(self): from erpnext.accounts.doctype.account.test_account import create_account - from erpnext.accounts.report.trial_balance.test_trial_balance import ( - clear_dimension_defaults, - create_accounting_dimension, - disable_dimension, - ) create_account( account_name="Offsetting", @@ -2201,7 +2196,16 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin): parent_account="Temporary Accounts - _TC", ) - create_accounting_dimension(company="_Test Company", offsetting_account="Offsetting - _TC") + dim = frappe.get_doc("Accounting Dimension", "Branch") + dim.append( + "dimension_defaults", + { + "company": "_Test Company", + "reference_document": "Branch", + "offsetting_account": "Offsetting - _TC", + }, + ) + dim.save() branch1 = frappe.new_doc("Branch") branch1.branch = "Location 1" @@ -2238,8 +2242,6 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin): voucher_type="Purchase Invoice", additional_columns=["branch"], ) - clear_dimension_defaults("Branch") - disable_dimension() def test_repost_accounting_entries(self): # update repost settings diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 3616f196bb7..002cdc4a43c 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2246,13 +2246,6 @@ class TestSalesInvoice(ERPNextTestSuite): @ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": True}) def test_rounding_adjustment_3(self): - from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import create_dimension - - # Dimension creates custom field, which does an implicit DB commit as it is a DDL command - # Ensure dimension don't have any mandatory fields - create_dimension() - - # rollback from tearDown() happens till here si = create_sales_invoice(do_not_save=True) si.items = [] for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]: diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 0b3da559e39..642f918c3b1 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -772,7 +772,8 @@ def process_all(subscription: list, posting_date: DateTimeLikeObject | None = No try: subscription = frappe.get_doc("Subscription", subscription_name) subscription.process(posting_date) - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() except frappe.ValidationError: frappe.db.rollback() subscription.log_error("Subscription failed") diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index 66dc090f7c7..bd633c94dc9 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -18,7 +18,6 @@ class TestTaxWithholdingCategory(ERPNextTestSuite): # create relevant supplier, etc create_records() create_tax_withholding_category_records() - make_pan_no_field() def validate_tax_withholding_entries(self, doctype, docname, expected_entries): """Validate tax withholding entries for a document""" @@ -3998,18 +3997,3 @@ def create_lower_deduction_certificate( "certificate_limit": limit, } ).insert() - - -def make_pan_no_field(): - pan_field = { - "Supplier": [ - { - "fieldname": "pan", - "label": "PAN", - "fieldtype": "Data", - "translatable": 0, - } - ] - } - - create_custom_fields(pan_field, update=1) diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py index caa464c5447..22fabeabca6 100644 --- a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py +++ b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py @@ -48,6 +48,9 @@ class Deferred_Item: Generate report data for output """ ret_data = frappe._dict({"name": self.item_name}) + ret_data.service_start_date = self.service_start_date + ret_data.service_end_date = self.service_end_date + ret_data.amount = self.base_net_amount for period in self.period_total: ret_data[period.key] = period.total ret_data.indent = 1 @@ -205,6 +208,9 @@ class Deferred_Invoice: for item in self.uniq_items: self.items.append(Deferred_Item(item, self, [x for x in items if x.item == item])) + # roll-up amount from all deferred items + self.amount_total = sum(item.base_net_amount for item in self.items) + def calculate_invoice_revenue_expense_for_period(self): """ calculate deferred revenue/expense for all items in invoice @@ -232,7 +238,7 @@ class Deferred_Invoice: generate report data for invoice, includes invoice total """ ret_data = [] - inv_total = frappe._dict({"name": self.name}) + inv_total = frappe._dict({"name": self.name, "amount": self.amount_total}) for x in self.period_total: inv_total[x.key] = x.total inv_total.indent = 0 @@ -386,6 +392,24 @@ class Deferred_Revenue_and_Expense_Report: def get_columns(self): columns = [] columns.append({"label": _("Name"), "fieldname": "name", "fieldtype": "Data", "read_only": 1}) + columns.append( + { + "label": _("Service Start Date"), + "fieldname": "service_start_date", + "fieldtype": "Date", + "read_only": 1, + } + ) + columns.append( + { + "label": _("Service End Date"), + "fieldname": "service_end_date", + "fieldtype": "Date", + "read_only": 1, + } + ) + columns.append({"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "read_only": 1}) + for period in self.period_list: columns.append( { @@ -415,6 +439,8 @@ class Deferred_Revenue_and_Expense_Report: elif self.filters.type == "Expense": total_row = frappe._dict({"name": "Total Deferred Expense"}) + total_row["amount"] = sum(inv.amount_total for inv in self.deferred_invoices) + for idx, period in enumerate(self.period_list, 0): total_row[period.key] = self.period_total[idx].total ret.append(total_row) diff --git a/erpnext/accounts/report/trial_balance/test_trial_balance.py b/erpnext/accounts/report/trial_balance/test_trial_balance.py index 42cf62af0a0..c37f9d5a46a 100644 --- a/erpnext/accounts/report/trial_balance/test_trial_balance.py +++ b/erpnext/accounts/report/trial_balance/test_trial_balance.py @@ -14,7 +14,6 @@ class TestTrialBalance(ERPNextTestSuite): from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center from erpnext.accounts.utils import get_fiscal_year - self.company = create_company() create_cost_center( cost_center_name="Test Cost Center", company="Trial Balance Company", @@ -26,7 +25,16 @@ class TestTrialBalance(ERPNextTestSuite): parent_account="Temporary Accounts - TBC", ) self.fiscal_year = get_fiscal_year(today(), company="Trial Balance Company")[0] - create_accounting_dimension() + dim = frappe.get_doc("Accounting Dimension", "Branch") + dim.append( + "dimension_defaults", + { + "company": "Trial Balance Company", + "automatically_post_balancing_accounting_entry": 1, + "offsetting_account": "Offsetting - TBC", + }, + ) + dim.save() def test_offsetting_entries_for_accounting_dimensions(self): """ @@ -45,7 +53,7 @@ class TestTrialBalance(ERPNextTestSuite): branch2.insert(ignore_if_duplicate=True) si = create_sales_invoice( - company=self.company, + company="Trial Balance Company", debit_to="Debtors - TBC", cost_center="Test Cost Center - TBC", income_account="Sales - TBC", @@ -57,60 +65,7 @@ class TestTrialBalance(ERPNextTestSuite): si.submit() filters = frappe._dict( - {"company": self.company, "fiscal_year": self.fiscal_year, "branch": ["Location 1"]} + {"company": "Trial Balance Company", "fiscal_year": self.fiscal_year, "branch": ["Location 1"]} ) total_row = execute(filters)[1][-1] self.assertEqual(total_row["debit"], total_row["credit"]) - - -def create_company(**args): - args = frappe._dict(args) - company = frappe.get_doc( - { - "doctype": "Company", - "company_name": args.company_name or "Trial Balance Company", - "country": args.country or "India", - "default_currency": args.currency or "INR", - "parent_company": args.get("parent_company"), - "is_group": args.get("is_group"), - } - ) - company.insert(ignore_if_duplicate=True) - return company.name - - -def create_accounting_dimension(**args): - args = frappe._dict(args) - document_type = args.document_type or "Branch" - if frappe.db.exists("Accounting Dimension", document_type): - accounting_dimension = frappe.get_doc("Accounting Dimension", document_type) - accounting_dimension.disabled = 0 - else: - accounting_dimension = frappe.new_doc("Accounting Dimension") - accounting_dimension.document_type = document_type - accounting_dimension.insert() - - accounting_dimension.set("dimension_defaults", []) - accounting_dimension.append( - "dimension_defaults", - { - "company": args.company or "Trial Balance Company", - "automatically_post_balancing_accounting_entry": 1, - "offsetting_account": args.offsetting_account or "Offsetting - TBC", - }, - ) - accounting_dimension.save() - - -def disable_dimension(**args): - args = frappe._dict(args) - document_type = args.document_type or "Branch" - dimension = frappe.get_doc("Accounting Dimension", document_type) - dimension.disabled = 1 - dimension.save() - - -def clear_dimension_defaults(dimension_name): - accounting_dimension = frappe.get_doc("Accounting Dimension", dimension_name) - accounting_dimension.dimension_defaults = [] - accounting_dimension.save() diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 832e3736e7d..1a9788be48e 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -61,7 +61,9 @@ def book_depreciation_entries(date): accounting_dimensions, ) - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() + except Exception as e: frappe.db.rollback() failed_assets.append(asset_name) @@ -71,7 +73,8 @@ def book_depreciation_entries(date): if failed_assets: set_depr_entry_posting_status_for_failed_assets(failed_assets) notify_depr_entry_posting_error(failed_assets, error_logs) - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() def get_depreciable_assets_data(date): diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py index f245ac4f0a2..e37ac4c2bf3 100644 --- a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py @@ -380,6 +380,7 @@ class TestAssetCapitalization(ERPNextTestSuite): "asset_type": "Composite Component", "purchase_date": pr.posting_date, "available_for_use_date": pr.posting_date, + "location": "Test Location", } ) consumed_asset_doc.save() diff --git a/erpnext/assets/doctype/asset_movement/test_asset_movement.py b/erpnext/assets/doctype/asset_movement/test_asset_movement.py index 88dd93a93cb..76d37d3abb4 100644 --- a/erpnext/assets/doctype/asset_movement/test_asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/test_asset_movement.py @@ -16,7 +16,6 @@ class TestAssetMovement(ERPNextTestSuite): frappe.db.set_value( "Company", "_Test Company", "capital_work_in_progress_account", "CWIP Account - _TC" ) - make_location() def test_movement(self): pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location") @@ -40,10 +39,6 @@ class TestAssetMovement(ERPNextTestSuite): if asset.docstatus == 0: asset.submit() - # check asset movement is created - if not frappe.db.exists("Location", "Test Location 2"): - frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert() - create_asset_movement( purpose="Transfer", company=asset.company, @@ -122,9 +117,6 @@ class TestAssetMovement(ERPNextTestSuite): if asset.docstatus == 0: asset.submit() - if not frappe.db.exists("Location", "Test Location 2"): - frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert() - movement = frappe.get_doc({"doctype": "Asset Movement", "reference_name": pr.name}) self.assertRaises(frappe.ValidationError, movement.cancel) @@ -150,9 +142,6 @@ class TestAssetMovement(ERPNextTestSuite): asset = create_asset(item_code="Macbook Pro", do_not_save=1) asset.save().submit() - if not frappe.db.exists("Location", "Test Location 2"): - frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert() - asset_creation_date = frappe.db.get_value( "Asset Movement", [["Asset Movement Item", "asset", "=", asset.name], ["docstatus", "=", 1]], @@ -197,9 +186,3 @@ def create_asset_movement(**args): movement.submit() return movement - - -def make_location(): - for location in ["Pune", "Mumbai", "Nagpur"]: - if not frappe.db.exists("Location", location): - frappe.get_doc({"doctype": "Location", "location_name": location}).insert(ignore_permissions=True) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 2a1b37aae2a..260dc52ceac 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -9,7 +9,6 @@ "engine": "InnoDB", "field_order": [ "supplier_section", - "title", "naming_series", "supplier", "supplier_name", @@ -172,17 +171,6 @@ "fieldtype": "Section Break", "options": "fa fa-user" }, - { - "allow_on_submit": 1, - "default": "{supplier_name}", - "fieldname": "title", - "fieldtype": "Data", - "hidden": 1, - "label": "Title", - "no_copy": 1, - "print_hide": 1, - "reqd": 1 - }, { "fieldname": "naming_series", "fieldtype": "Select", @@ -1328,7 +1316,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2026-03-09 17:15:29.184682", + "modified": "2026-03-25 11:46:18.748951", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index a0daeca51f2..2cb285c14f3 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -159,7 +159,6 @@ class PurchaseOrder(BuyingController): taxes_and_charges_deducted: DF.Currency tc_name: DF.Link | None terms: DF.TextEditor | None - title: DF.Data to_date: DF.Date | None total: DF.Currency total_net_weight: DF.Float @@ -780,7 +779,8 @@ def make_purchase_invoice_from_portal(purchase_order_name): if frappe.session.user not in frappe.get_all("Portal User", {"parent": doc.supplier}, pluck="user"): frappe.throw(_("Not Permitted"), frappe.PermissionError) doc.save() - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() frappe.response["type"] = "redirect" frappe.response.location = "/purchase-invoices/" + doc.name @@ -802,18 +802,18 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions target.set_payment_schedule() target.credit_to = get_party_account("Supplier", source.supplier, source.company) + def get_billed_qty(po_item_name): + from frappe.query_builder.functions import Sum + + table = frappe.qb.DocType("Purchase Invoice Item") + query = ( + frappe.qb.from_(table) + .select(Sum(table.qty).as_("qty")) + .where((table.docstatus == 1) & (table.po_detail == po_item_name)) + ) + return query.run(pluck="qty")[0] or 0 + def update_item(obj, target, source_parent): - def get_billed_qty(po_item_name): - from frappe.query_builder.functions import Sum - - table = frappe.qb.DocType("Purchase Invoice Item") - query = ( - frappe.qb.from_(table) - .select(Sum(table.qty).as_("qty")) - .where((table.docstatus == 1) & (table.po_detail == po_item_name)) - ) - return query.run(pluck="qty")[0] or 0 - billed_qty = flt(get_billed_qty(obj.name)) target.qty = flt(obj.qty) - billed_qty @@ -853,7 +853,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions "wip_composite_asset": "wip_composite_asset", }, "postprocess": update_item, - "condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)) + "condition": lambda doc: ( + doc.base_amount == 0 + or abs(doc.billed_amt) < abs(doc.amount) + or doc.qty > flt(get_billed_qty(doc.name)) + ) and select_item(doc), }, "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True}, diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index fe4bb12c3db..e6956111ea0 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -1386,6 +1386,34 @@ class TestPurchaseOrder(ERPNextTestSuite): self.assertEqual(pi_2.status, "Paid") self.assertEqual(po.status, "Completed") + def test_purchase_order_over_billing_missing_item(self): + item1 = make_item( + "_Test Item for Overbilling", + ).name + + item2 = make_item( + "_Test Item for Overbilling 2", + ).name + + po = create_purchase_order(qty=10, rate=1000, item_code=item1, do_not_save=1) + po.append("items", {"item_code": item2, "qty": 5, "rate": 20, "warehouse": "_Test Warehouse - _TC"}) + po.taxes = [] + po.insert() + po.submit() + + pi1 = make_pi_from_po(po.name) + pi1.items[0].qty = 8 + pi1.items[0].rate = 1250 + pi1.remove(pi1.items[1]) + pi1.insert() + pi1.submit() + + self.assertEqual(pi1.grand_total, 10000.0) + self.assertTrue(len(pi1.items) == 1) + + pi2 = make_pi_from_po(po.name) + self.assertEqual(len(pi2.items), 2) + def create_po_for_sc_testing(): from erpnext.controllers.tests.test_subcontracting_controller import ( diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index b71d0dd3006..8baeba950b9 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -100,6 +100,7 @@ frappe.ui.form.on("Request for Quotation", { fieldname: "print_format", options: "Print Format", placeholder: "Standard", + default: frappe.get_meta("Request for Quotation").default_print_format || "", get_query: () => { return { filters: { diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json index 18e1356b263..de8b4d28547 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -9,8 +9,6 @@ "field_order": [ "naming_series", "company", - "billing_address", - "billing_address_display", "vendor", "column_break1", "transaction_date", @@ -43,7 +41,13 @@ "select_print_heading", "letter_head", "more_info", - "opportunity" + "opportunity", + "address_and_contact_tab", + "billing_address", + "billing_address_display", + "column_break_czul", + "shipping_address", + "shipping_address_display" ], "fields": [ { @@ -346,6 +350,27 @@ "fieldtype": "Check", "hidden": 1, "label": "Use HTML" + }, + { + "fieldname": "address_and_contact_tab", + "fieldtype": "Tab Break", + "label": "Address & Contact" + }, + { + "fieldname": "column_break_czul", + "fieldtype": "Column Break" + }, + { + "fieldname": "shipping_address", + "fieldtype": "Link", + "label": "Company Shipping Address", + "options": "Address" + }, + { + "fieldname": "shipping_address_display", + "fieldtype": "Text Editor", + "label": "Shipping Address Details", + "read_only": 1 } ], "grid_page_length": 50, @@ -353,7 +378,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2026-03-09 17:15:29.774614", + "modified": "2026-03-19 15:27:56.730649", "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation", diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 38ab9af4eab..1fc2cdf3386 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -56,6 +56,8 @@ class RequestforQuotation(BuyingController): select_print_heading: DF.Link | None send_attached_files: DF.Check send_document_print: DF.Check + shipping_address: DF.Link | None + shipping_address_display: DF.TextEditor | None status: DF.Literal["", "Draft", "Submitted", "Cancelled"] subject: DF.Data suppliers: DF.Table[RequestforQuotationSupplier] diff --git a/erpnext/manufacturing/report/bom_stock_calculated/__init__.py b/erpnext/buying/print_format/request_for_quotation_with_item_image/__init__.py similarity index 100% rename from erpnext/manufacturing/report/bom_stock_calculated/__init__.py rename to erpnext/buying/print_format/request_for_quotation_with_item_image/__init__.py diff --git a/erpnext/buying/print_format/request_for_quotation_with_item_image/request_for_quotation_with_item_image.json b/erpnext/buying/print_format/request_for_quotation_with_item_image/request_for_quotation_with_item_image.json new file mode 100644 index 00000000000..26f131aec5b --- /dev/null +++ b/erpnext/buying/print_format/request_for_quotation_with_item_image/request_for_quotation_with_item_image.json @@ -0,0 +1,33 @@ +{ + "absolute_value": 0, + "align_labels_right": 0, + "creation": "2026-03-19 15:17:39.094444", + "custom_format": 1, + "default_print_language": "en", + "disabled": 0, + "doc_type": "Request for Quotation", + "docstatus": 0, + "doctype": "Print Format", + "font_size": 14, + "html": "{%- macro add_header(page_num, max_pages, doc, letter_head, no_letterhead, footer, print_settings=None, print_heading_template=None) -%}\n\n{% if letter_head and not no_letterhead %}\n
{{ letter_head }}
\n{% endif %}\n{% if print_heading_template %}\n{{ frappe.render_template(print_heading_template, {\"doc\":doc}) }}\n{% endif %}\n{%- endmacro -%}\n\n{% for page in layout %}\n
\n\t
\n\t\t{{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead, footer, print_settings) }}\n\t
\n\t{%- if doc.meta.is_submittable and doc.docstatus==2-%}\n\t\t
\n\t\t\t

{{ _(\"CANCELLED\") }}

\n\t\t
\n\t{%- endif -%}\n\t{%- if doc.meta.is_submittable and doc.docstatus==0 and (print_settings==None or print_settings.add_draft_heading) -%}\n\t\t
\n\t\t\t

{{ _(\"DRAFT\") }}

\n\t\t
\n\t{%- endif -%}\n\n\t\n\n\t
\n\t\t
{{ format_currency(summary[doctype].outstanding_amount, summary.currency, 2) }}
\n\t\t\t\n\t\t\t\t\n\n\t\t\t\t\n\t\t\t\n\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
{{ _(\"Supplier Name:\") }}
\n\t\t\t\t\t\t
{{ _(\"Shipping Address:\") }}
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
{{ doc.vendor }}
\n\t\t\t\t\t\t
\n \t\t\t\t\t{% if doc.shipping_address %}\n \t\t\t\t\t\t{% set shipping_address = frappe.db.get_value(\"Address\", doc.shipping_address, [\"address_line1\", \"address_line2\", \"city\", \"state\", \"pincode\", \"country\"], as_dict=True) %}\n {{ doc.shipping_address }}
\n \t\t\t\t\t\t{{ shipping_address.address_line1 or \"\" }}
\n \t\t\t\t\t\t{% if shipping_address.address_line2 %}{{ shipping_address.address_line2 }}
{% endif %}\n \t\t\t\t\t\t{{ shipping_address.city or \"\" }} {{ shipping_address.state or \"\" }} {{ shipping_address.pincode or \"\" }} {{ shipping_address.country or \"\" }}
\n \t\t\t\t\t{% endif %}\n\t\t\t\t\t\t
\n\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
{{ _(\"Order Date:\") }}
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
{{ frappe.utils.format_date(doc.transaction_date) }}
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
{{ _(\"Required By:\") }}
\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
{{ frappe.utils.format_date(doc.schedule_date) }}
\n\t\t\t\t\t
\n\t\t\t\t
\n\n\t\t\n\t\t{% set item_naming_by = frappe.db.get_single_value(\"Stock Settings\", \"item_naming_by\") %}\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t{% if item_naming_by != \"Item Code\" %}\n\t\t\t\t\t\t\n\t\t\t\t\t{% endif %}\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t{% for item in doc.items %}\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t{% if item_naming_by != \"Item Code\" %}\n\t\t\t\t\t\t\n\t\t\t\t\t{% endif %}\n\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t{% endfor %}\n\t\t\t\n\t\t
{{ _(\"No\") }}{{ _(\"Item\") }}{{ _(\"Item Code\") }}{{ _(\"Quantity\") }}
{{ loop.index }}\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{% if item.image %}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{% endif %}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{{ item.item_name }}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t
{{ item.item_code }}{{ item.get_formatted(\"qty\", 0) }} {{ item.uom }}
\n\n\n\t\t\n\t\t{% if doc.terms %}\n\t\t
\n\t\t\t
{{ _(\"Terms and Conditions\") }}
\n\t\t\t{{ doc.terms}}\n\t\t
\n\t\t{% endif %}\n\t\n\n{% endfor %}\n", + "idx": 0, + "line_breaks": 0, + "margin_bottom": 15.0, + "margin_left": 15.0, + "margin_right": 15.0, + "margin_top": 15.0, + "modified": "2026-03-23 14:29:41.591636", + "modified_by": "Administrator", + "module": "Buying", + "name": "Request for Quotation with Item Image", + "owner": "Administrator", + "page_number": "Hide", + "pdf_generator": "wkhtmltopdf", + "print_format_builder": 0, + "print_format_builder_beta": 0, + "print_format_for": "DocType", + "print_format_type": "Jinja", + "raw_printing": 0, + "show_section_headings": 0, + "standard": "Yes" +} diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 24819c3ee19..17d5d929bb5 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -4324,6 +4324,8 @@ def get_missing_company_details(doctype, docname): company = frappe.db.get_value(doctype, docname, "company") if doctype in ["Purchase Order", "Purchase Invoice"]: company_address = frappe.db.get_value(doctype, docname, "billing_address") + elif doctype in ["Request for Quotation"]: + company_address = frappe.db.get_value(doctype, docname, "shipping_address") else: company_address = frappe.db.get_value(doctype, docname, "company_address") @@ -4423,6 +4425,7 @@ def update_doc_company_address(current_doctype, docname, company_address, detail "Sales Invoice": ("company_address", "company_address_display"), "Delivery Note": ("company_address", "company_address_display"), "POS Invoice": ("company_address", "company_address_display"), + "Request for Quotation": ("shipping_address", "shipping_address_display"), } address_field, display_field = address_field_map.get( diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 07349a3363f..6383049be9c 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -503,11 +503,15 @@ class BuyingController(SubcontractingController): if d.category not in ["Valuation", "Valuation and Total"]: continue + amount = flt(d.base_tax_amount_after_discount_amount) * ( + -1 if d.get("add_deduct_tax") == "Deduct" else 1 + ) + if d.charge_type == "On Net Total": - total_valuation_amount += flt(d.base_tax_amount_after_discount_amount) + total_valuation_amount += amount tax_accounts.append(d.account_head) else: - total_actual_tax_amount += flt(d.base_tax_amount_after_discount_amount) + total_actual_tax_amount += amount return tax_accounts, total_valuation_amount, total_actual_tax_amount @@ -1094,7 +1098,8 @@ class BuyingController(SubcontractingController): for dimension in accounting_dimensions[0]: fieldname = dimension["fieldname"] default_dimension = accounting_dimensions[1].get(self.company, {}).get(fieldname) - asset.update({fieldname: row.get(fieldname) or self.get(fieldname) or default_dimension}) + if not asset.get(fieldname): + asset.update({fieldname: row.get(fieldname) or self.get(fieldname) or default_dimension}) asset.flags.ignore_validate = True asset.flags.ignore_mandatory = True diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index b7845bc5698..9b663c0d2b9 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -1002,3 +1002,26 @@ def get_item_uom_query(doctype, txt, searchfield, start, page_len, filters): limit_page_length=page_len, as_list=1, ) + + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_warehouse_address(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict): + table = frappe.qb.DocType(doctype) + child_table = frappe.qb.DocType("Dynamic Link") + + query = ( + frappe.qb.from_(table) + .inner_join(child_table) + .on((table.name == child_table.parent) & (child_table.parenttype == doctype)) + .select(table.name) + .where( + (child_table.link_name == filters.get("warehouse")) + & (table.disabled == 0) + & (child_table.link_doctype == "Warehouse") + & (table.name.like(f"%{txt}%")) + ) + .offset(start) + .limit(page_len) + ) + return query.run(as_list=1) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 6bb9b2f91fc..021e034a70d 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -1567,25 +1567,10 @@ class TestAccountsController(ERPNextTestSuite): frappe.db.set_value("Company", self.company, "cost_center", cc) - def setup_dimensions(self): - # create dimension - from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( - create_dimension, - ) - - create_dimension() - # make it non-mandatory - loc = frappe.get_doc("Accounting Dimension", "Location") - for x in loc.dimension_defaults: - x.mandatory_for_bs = False - x.mandatory_for_pl = False - loc.save() - def test_90_dimensions_filter(self): """ Test workings of dimension filters """ - self.setup_dimensions() rate_in_account_currency = 1 # Invoices @@ -1653,7 +1638,6 @@ class TestAccountsController(ERPNextTestSuite): self.assertEqual(len(pr.payments), 1) def test_91_cr_note_should_inherit_dimension(self): - self.setup_dimensions() rate_in_account_currency = 1 # Invoice @@ -1698,7 +1682,6 @@ class TestAccountsController(ERPNextTestSuite): def test_92_dimension_inhertiance_exc_gain_loss(self): # Sales Invoice in Foreign Currency - self.setup_dimensions() rate_in_account_currency = 1 dpt = "Research & Development - _TC" @@ -1734,7 +1717,6 @@ class TestAccountsController(ERPNextTestSuite): ) def test_93_dimension_inheritance_on_advance(self): - self.setup_dimensions() dpt = "Research & Development - _TC" adv = self.create_payment_entry(amount=1, source_exc_rate=85) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index b41064ce9e3..9a213aea5fc 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -120,7 +120,8 @@ class Appointment(Document): self.auto_assign() self.create_calendar_event() self.save(ignore_permissions=True) - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() def create_lead_and_link(self): # Return if already linked diff --git a/erpnext/crm/doctype/contract_template/contract_template.json b/erpnext/crm/doctype/contract_template/contract_template.json index 223464d3eb8..baa6b289005 100644 --- a/erpnext/crm/doctype/contract_template/contract_template.json +++ b/erpnext/crm/doctype/contract_template/contract_template.json @@ -56,7 +56,7 @@ } ], "links": [], - "modified": "2024-03-27 13:06:46.495091", + "modified": "2026-03-25 19:27:19.162421", "modified_by": "Administrator", "module": "CRM", "name": "Contract Template", @@ -75,44 +75,36 @@ "write": 1 }, { - "create": 1, - "delete": 1, "email": 1, "export": 1, "print": 1, "read": 1, "report": 1, "role": "Sales Manager", - "share": 1, - "write": 1 + "share": 1 }, { - "create": 1, - "delete": 1, "email": 1, "export": 1, "print": 1, "read": 1, "report": 1, "role": "Purchase Manager", - "share": 1, - "write": 1 + "share": 1 }, { - "create": 1, - "delete": 1, "email": 1, "export": 1, "print": 1, "read": 1, "report": 1, "role": "HR Manager", - "share": 1, - "write": 1 + "share": 1 } ], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py index 9e24a26caa8..4454ede5310 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -204,8 +204,22 @@ def send_mail(entry, email_campaign): # called from hooks on doc_event Email Unsubscribe def unsubscribe_recipient(unsubscribe, method): - if unsubscribe.reference_doctype == "Email Campaign": - frappe.db.set_value("Email Campaign", unsubscribe.reference_name, "status", "Unsubscribed") + if unsubscribe.reference_doctype != "Email Campaign": + return + + email_campaign = frappe.get_doc("Email Campaign", unsubscribe.reference_name) + + if email_campaign.email_campaign_for == "Email Group": + if unsubscribe.email: + frappe.db.set_value( + "Email Group Member", + {"email_group": email_campaign.recipient, "email": unsubscribe.email}, + "unsubscribed", + 1, + ) + else: + # For Lead or Contact + frappe.db.set_value("Email Campaign", email_campaign.name, "status", "Unsubscribed") # called through hooks to update email campaign status daily diff --git a/erpnext/edi/doctype/code_list/code_list.py b/erpnext/edi/doctype/code_list/code_list.py index 8957c6565b9..e723157e7a0 100644 --- a/erpnext/edi/doctype/code_list/code_list.py +++ b/erpnext/edi/doctype/code_list/code_list.py @@ -5,6 +5,7 @@ from typing import TYPE_CHECKING import frappe from frappe.model.document import Document +from frappe.utils import escape_html if TYPE_CHECKING: from lxml.etree import Element @@ -63,14 +64,16 @@ class CodeList(Document): def from_genericode(self, root: "Element"): """Extract Code List details from genericode XML""" - self.title = root.find(".//Identification/ShortName").text + self.title = escape_html(root.find(".//Identification/ShortName").text) self.version = root.find(".//Identification/Version").text self.canonical_uri = root.find(".//CanonicalUri").text # optionals - self.description = getattr(root.find(".//Identification/LongName"), "text", None) - self.publisher = getattr(root.find(".//Identification/Agency/ShortName"), "text", None) + self.description = escape_html(getattr(root.find(".//Identification/LongName"), "text", None)) + self.publisher = escape_html(getattr(root.find(".//Identification/Agency/ShortName"), "text", None)) if not self.publisher: - self.publisher = getattr(root.find(".//Identification/Agency/LongName"), "text", None) + self.publisher = escape_html( + getattr(root.find(".//Identification/Agency/LongName"), "text", None) + ) self.publisher_id = getattr(root.find(".//Identification/Agency/Identifier"), "text", None) self.url = getattr(root.find(".//Identification/LocationUri"), "text", None) diff --git a/erpnext/edi/doctype/code_list/code_list_import.py b/erpnext/edi/doctype/code_list/code_list_import.py index 3909eb22766..71cb7d0f82d 100644 --- a/erpnext/edi/doctype/code_list/code_list_import.py +++ b/erpnext/edi/doctype/code_list/code_list_import.py @@ -3,6 +3,7 @@ import json import frappe import requests from frappe import _ +from frappe.utils import escape_html from lxml import etree URL_PREFIXES = ("http://", "https://") @@ -32,7 +33,12 @@ def import_genericode(): content = f.read() # Parse the xml content - parser = etree.XMLParser(remove_blank_text=True) + parser = etree.XMLParser( + remove_blank_text=True, + resolve_entities=False, + load_dtd=False, + no_network=True, + ) try: root = etree.fromstring(content, parser=parser) except Exception as e: @@ -104,7 +110,7 @@ def get_genericode_columns_and_examples(root): # Get column names for column in root.findall(".//Column"): - column_id = column.get("Id") + column_id = escape_html(column.get("Id")) columns.append(column_id) example_values[column_id] = [] filterable_columns[column_id] = set() @@ -112,7 +118,7 @@ def get_genericode_columns_and_examples(root): # Get all values and count unique occurrences for row in root.findall(".//SimpleCodeList/Row"): for value in row.findall("Value"): - column_id = value.get("ColumnRef") + column_id = escape_html(value.get("ColumnRef")) if column_id not in columns: # Handle undeclared column columns.append(column_id) @@ -123,7 +129,7 @@ def get_genericode_columns_and_examples(root): if simple_value is None: continue - filterable_columns[column_id].add(simple_value.text) + filterable_columns[column_id].add(escape_html(simple_value.text)) # Get example values (up to 3) and filter columns with cardinality <= 5 for row in root.findall(".//SimpleCodeList/Row")[:3]: @@ -133,7 +139,7 @@ def get_genericode_columns_and_examples(root): if simple_value is None: continue - example_values[column_id].append(simple_value.text) + example_values[column_id].append(escape_html(simple_value.text)) filterable_columns = {k: list(v) for k, v in filterable_columns.items() if len(v) <= 5} diff --git a/erpnext/hooks.py b/erpnext/hooks.py index c997443b41d..cc4a08d8b67 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -62,7 +62,6 @@ welcome_email = "erpnext.setup.utils.welcome_email" # setup wizard setup_wizard_requires = "assets/erpnext/js/setup_wizard.js" setup_wizard_stages = "erpnext.setup.setup_wizard.setup_wizard.get_setup_stages" -setup_wizard_complete = "erpnext.setup.setup_wizard.setup_wizard.setup_demo" after_install = "erpnext.setup.install.after_install" diff --git a/erpnext/locale/main.pot b/erpnext/locale/main.pot index f627ccf1b41..21fb2c6830e 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-03-22 09:44+0000\n" -"PO-Revision-Date: 2026-03-22 09:44+0000\n" +"POT-Creation-Date: 2026-03-29 09:46+0000\n" +"PO-Revision-Date: 2026-03-29 09:46+0000\n" "Last-Translator: hello@frappe.io\n" "Language-Team: hello@frappe.io\n" "MIME-Version: 1.0\n" @@ -262,7 +262,7 @@ msgstr "" msgid "% of materials delivered against this Sales Order" msgstr "" -#: erpnext/controllers/accounts_controller.py:2371 +#: erpnext/controllers/accounts_controller.py:2381 msgid "'Account' in the Accounting section of Customer {0}" msgstr "" @@ -278,7 +278,7 @@ msgstr "" msgid "'Days Since Last Order' must be greater than or equal to zero" msgstr "" -#: erpnext/controllers/accounts_controller.py:2376 +#: erpnext/controllers/accounts_controller.py:2386 msgid "'Default {0} Account' in Company {1}" msgstr "" @@ -865,11 +865,6 @@ msgstr "" msgid "Masters & Reports" msgstr "" -#. Header text in the Selling Workspace -#: erpnext/selling/workspace/selling/selling.json -msgid "Quick Access" -msgstr "" - #. Header text in the Invoicing Workspace #. Header text in the Assets Workspace #. Header text in the Buying Workspace @@ -915,11 +910,11 @@ msgstr "" msgid "Your Shortcuts" msgstr "" -#: erpnext/accounts/doctype/payment_request/payment_request.py:1133 +#: erpnext/accounts/doctype/payment_request/payment_request.py:1134 msgid "Grand Total: {0}" msgstr "" -#: erpnext/accounts/doctype/payment_request/payment_request.py:1134 +#: erpnext/accounts/doctype/payment_request/payment_request.py:1135 msgid "Outstanding Amount: {0}" msgstr "" @@ -1335,7 +1330,7 @@ msgid "Account Manager" msgstr "" #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1007 -#: erpnext/controllers/accounts_controller.py:2380 +#: erpnext/controllers/accounts_controller.py:2390 msgid "Account Missing" msgstr "" @@ -1568,7 +1563,7 @@ msgstr "" msgid "Account: {0} is not permitted under Payment Entry" msgstr "" -#: erpnext/controllers/accounts_controller.py:3269 +#: erpnext/controllers/accounts_controller.py:3279 msgid "Account: {0} with currency: {1} can not be selected" msgstr "" @@ -1862,7 +1857,7 @@ msgstr "" msgid "Accounting Entry for Landed Cost Voucher for SCR {0}" msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:840 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:844 msgid "Accounting Entry for Service" msgstr "" @@ -1877,18 +1872,18 @@ msgstr "" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1490 #: erpnext/controllers/stock_controller.py:727 #: erpnext/controllers/stock_controller.py:744 -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:933 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:937 #: erpnext/stock/doctype/stock_entry/stock_entry.py:1904 #: erpnext/stock/doctype/stock_entry/stock_entry.py:1918 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:708 msgid "Accounting Entry for Stock" msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:737 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:741 msgid "Accounting Entry for {0}" msgstr "" -#: erpnext/controllers/accounts_controller.py:2421 +#: erpnext/controllers/accounts_controller.py:2431 msgid "Accounting Entry for {0}: {1} can only be made in currency: {2}" msgstr "" @@ -1961,7 +1956,7 @@ msgstr "" #: erpnext/setup/doctype/email_digest/email_digest.json #: erpnext/setup/doctype/incoterm/incoterm.json #: erpnext/setup/doctype/supplier_group/supplier_group.json -#: erpnext/setup/install.py:368 +#: erpnext/setup/install.py:369 msgid "Accounts" msgstr "" @@ -2414,7 +2409,7 @@ msgstr "" msgid "Actual Operation Time" msgstr "" -#: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py:430 +#: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py:456 msgid "Actual Posting" msgstr "" @@ -2670,7 +2665,7 @@ msgstr "" msgid "Add Sub Assembly" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:516 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:517 #: erpnext/public/js/event.js:32 msgid "Add Suppliers" msgstr "" @@ -3015,6 +3010,8 @@ msgstr "" #. Invoice' #. Label of the address_and_contact_tab (Tab Break) field in DocType 'Purchase #. Order' +#. Label of the address_and_contact_tab (Tab Break) field in DocType 'Request +#. for Quotation' #. Label of the contact_and_address_tab (Tab Break) field in DocType 'Supplier' #. Label of the address_and_contact_tab (Tab Break) field in DocType 'Supplier #. Quotation' @@ -3035,6 +3032,7 @@ msgstr "" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/buying/doctype/purchase_order/purchase_order.json +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.json #: erpnext/buying/doctype/supplier/supplier.json #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.json #: erpnext/crm/doctype/opportunity/opportunity.json @@ -3153,7 +3151,7 @@ msgstr "" msgid "Adjustment Against" msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:665 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:669 msgid "Adjustment based on Purchase Invoice rate" msgstr "" @@ -3310,12 +3308,6 @@ msgstr "" msgid "Aerospace" msgstr "" -#. Label of the affected_transactions (Code) field in DocType 'Repost Item -#. Valuation' -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json -msgid "Affected Transactions" -msgstr "" - #. Label of the against (Text) field in DocType 'GL Entry' #: erpnext/accounts/doctype/gl_entry/gl_entry.json #: erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.html:20 @@ -3440,7 +3432,7 @@ msgstr "" msgid "Against Stock Entry" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:332 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:331 msgid "Against Supplier Invoice {0}" msgstr "" @@ -3741,11 +3733,11 @@ msgstr "" msgid "All communications including and above this shall be moved into the new Issue" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:967 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:972 msgid "All items are already requested" msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:1426 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:1430 msgid "All items have already been Invoiced/Returned" msgstr "" @@ -4510,6 +4502,7 @@ msgstr "" #: erpnext/accounts/print_format/sales_invoice_print/sales_invoice_print.html:93 #: erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py:48 #: erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py:79 +#: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py:411 #: erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py:44 #: erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py:273 #: erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py:327 @@ -4727,8 +4720,8 @@ msgstr "" msgid "Ampere-Second" msgstr "" -#: erpnext/controllers/trends.py:269 erpnext/controllers/trends.py:281 -#: erpnext/controllers/trends.py:290 +#: erpnext/controllers/trends.py:277 erpnext/controllers/trends.py:289 +#: erpnext/controllers/trends.py:298 msgid "Amt" msgstr "" @@ -4737,7 +4730,7 @@ msgstr "" msgid "An Item Group is a way to classify items based on types." msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:535 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:537 msgid "An error has been appeared while reposting item valuation via {0}" msgstr "" @@ -4803,7 +4796,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:883 +#: erpnext/accounts/doctype/payment_request/payment_request.py:884 msgid "Another Payment Request is already processed" msgstr "" @@ -5242,11 +5235,11 @@ msgstr "" msgid "As there are reserved stock, you cannot disable {0}." msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1087 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1092 msgid "As there are sufficient Sub Assembly Items, Work Order is not required for Warehouse {0}." msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1827 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1832 msgid "As there are sufficient raw materials, Material Request is not required for Warehouse {0}." msgstr "" @@ -5664,7 +5657,7 @@ msgstr "" msgid "Asset cannot be cancelled, as it is already {0}" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:393 +#: erpnext/assets/doctype/asset/depreciation.py:396 msgid "Asset cannot be scrapped before the last depreciation entry." msgstr "" @@ -5696,7 +5689,7 @@ msgstr "" msgid "Asset received at Location {0} and issued to Employee {1}" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:454 +#: erpnext/assets/doctype/asset/depreciation.py:457 msgid "Asset restored" msgstr "" @@ -5708,11 +5701,11 @@ msgstr "" msgid "Asset returned" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:441 +#: erpnext/assets/doctype/asset/depreciation.py:444 msgid "Asset scrapped" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:443 +#: erpnext/assets/doctype/asset/depreciation.py:446 msgid "Asset scrapped via Journal Entry {0}" msgstr "" @@ -5737,7 +5730,7 @@ msgstr "" msgid "Asset updated due to Asset Repair {0} {1}." msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:375 +#: erpnext/assets/doctype/asset/depreciation.py:378 msgid "Asset {0} cannot be scrapped, as it is already {1}" msgstr "" @@ -5778,7 +5771,7 @@ msgstr "" msgid "Asset {0} is not submitted. Please submit the asset before proceeding." msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:373 +#: erpnext/assets/doctype/asset/depreciation.py:376 msgid "Asset {0} must be submitted" msgstr "" @@ -5828,7 +5821,7 @@ msgstr "" msgid "Assets {assets_link} created for {item_code}" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:232 +#: erpnext/manufacturing/doctype/job_card/job_card.js:240 msgid "Assign Job to Employee" msgstr "" @@ -6195,6 +6188,10 @@ msgstr "" msgid "Auto Tax Settings Error" msgstr "" +#: erpnext/setup/doctype/employee/employee.py:170 +msgid "Auto User Creation Error" +msgstr "" + #. Description of the 'Close Replied Opportunity After Days' (Int) field in #. DocType 'CRM Settings' #: erpnext/crm/doctype/crm_settings/crm_settings.json @@ -6312,7 +6309,8 @@ msgstr "" #. Label of the available_quantity_section (Section Break) field in DocType #. 'Pick List Item' #: erpnext/manufacturing/doctype/workstation/workstation.js:505 -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py:88 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:118 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:175 #: erpnext/public/js/utils.js:627 #: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json #: erpnext/stock/doctype/pick_list_item/pick_list_item.json @@ -6530,8 +6528,7 @@ msgstr "" #: erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json #: erpnext/manufacturing/report/bom_explorer/bom_explorer.js:8 #: erpnext/manufacturing/report/bom_explorer/bom_explorer.py:67 -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js:8 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js:5 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js:8 #: erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py:109 #: erpnext/manufacturing/workspace/manufacturing/manufacturing.json #: erpnext/selling/doctype/sales_order/sales_order.js:1415 @@ -6551,7 +6548,7 @@ msgstr "" msgid "BOM 1" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1757 +#: erpnext/manufacturing/doctype/bom/bom.py:1760 msgid "BOM 1 {0} and BOM 2 {1} should not be same" msgstr "" @@ -6692,10 +6689,6 @@ msgstr "" msgid "BOM Operations Time" msgstr "" -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py:26 -msgid "BOM Qty" -msgstr "" - #: erpnext/stock/report/item_prices/item_prices.py:60 msgid "BOM Rate" msgstr "" @@ -6715,15 +6708,12 @@ msgid "BOM Search" msgstr "" #. Name of a report -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.json -msgid "BOM Stock Calculated" +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.json +msgid "BOM Stock Analysis" msgstr "" -#. Name of a report #. Label of a Link in the Manufacturing Workspace #. Label of a Workspace Sidebar Item -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html:1 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.json #: erpnext/manufacturing/workspace/manufacturing/manufacturing.json #: erpnext/workspace_sidebar/manufacturing.json msgid "BOM Stock Report" @@ -6734,10 +6724,6 @@ msgstr "" msgid "BOM Tree" msgstr "" -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py:27 -msgid "BOM UOM" -msgstr "" - #. Name of a DocType #: erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json msgid "BOM Update Batch" @@ -6816,15 +6802,15 @@ msgstr "" msgid "BOM recursion: {1} cannot be parent or child of {0}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1492 +#: erpnext/manufacturing/doctype/bom/bom.py:1494 msgid "BOM {0} does not belong to Item {1}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1474 +#: erpnext/manufacturing/doctype/bom/bom.py:1476 msgid "BOM {0} must be active" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1477 +#: erpnext/manufacturing/doctype/bom/bom.py:1479 msgid "BOM {0} must be submitted" msgstr "" @@ -7754,7 +7740,7 @@ msgstr "" #. Label of a Card Break in the Manufacturing Workspace #. Label of a Link in the Manufacturing Workspace #. Label of a Workspace Sidebar Item -#: erpnext/manufacturing/doctype/bom/bom.py:1324 +#: erpnext/manufacturing/doctype/bom/bom.py:1326 #: erpnext/manufacturing/workspace/manufacturing/manufacturing.json #: erpnext/stock/doctype/material_request/material_request.js:139 #: erpnext/stock/doctype/stock_entry/stock_entry.js:695 @@ -8934,7 +8920,7 @@ msgid "Can only make payment against unbilled {0}" msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1517 -#: erpnext/controllers/accounts_controller.py:3178 +#: erpnext/controllers/accounts_controller.py:3188 #: erpnext/public/js/controllers/accounts.js:103 msgid "Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'" msgstr "" @@ -8999,7 +8985,7 @@ msgstr "" msgid "Cannot Optimize Route as Driver Address is Missing." msgstr "" -#: erpnext/setup/doctype/employee/employee.py:181 +#: erpnext/setup/doctype/employee/employee.py:295 msgid "Cannot Relieve Employee" msgstr "" @@ -9043,7 +9029,7 @@ msgstr "" msgid "Cannot cancel because submitted Stock Entry {0} exists" msgstr "" -#: erpnext/stock/stock_ledger.py:209 +#: erpnext/stock/stock_ledger.py:179 msgid "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet." msgstr "" @@ -9055,7 +9041,7 @@ msgstr "" msgid "Cannot cancel this document as it is linked with the submitted Asset Value Adjustment {0}. Please cancel the Asset Value Adjustment to continue." msgstr "" -#: erpnext/controllers/buying_controller.py:1136 +#: erpnext/controllers/buying_controller.py:1137 msgid "Cannot cancel this document as it is linked with the submitted asset {asset_link}. Please cancel the asset to continue." msgstr "" @@ -9103,7 +9089,7 @@ msgstr "" msgid "Cannot covert to Group because Account Type is selected." msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:1014 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:1018 msgid "Cannot create Stock Reservation Entries for future dated Purchase Receipts." msgstr "" @@ -9120,7 +9106,7 @@ msgstr "" msgid "Cannot create return for consolidated invoice {0}." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1175 +#: erpnext/manufacturing/doctype/bom/bom.py:1177 msgid "Cannot deactivate or cancel BOM as it is linked with other BOMs" msgstr "" @@ -9141,7 +9127,7 @@ msgstr "" msgid "Cannot delete Serial No {0}, as it is used in stock transactions" msgstr "" -#: erpnext/controllers/accounts_controller.py:3774 +#: erpnext/controllers/accounts_controller.py:3784 msgid "Cannot delete an item which has been ordered" msgstr "" @@ -9191,7 +9177,7 @@ msgstr "" msgid "Cannot find Item with this Barcode" msgstr "" -#: erpnext/controllers/accounts_controller.py:3726 +#: erpnext/controllers/accounts_controller.py:3736 msgid "Cannot find a default warehouse for item {0}. Please set one in the Item Master or in Stock Settings." msgstr "" @@ -9215,12 +9201,12 @@ msgstr "" msgid "Cannot receive from customer against negative outstanding" msgstr "" -#: erpnext/controllers/accounts_controller.py:3922 +#: erpnext/controllers/accounts_controller.py:3932 msgid "Cannot reduce quantity than ordered or purchased quantity" msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1530 -#: erpnext/controllers/accounts_controller.py:3193 +#: erpnext/controllers/accounts_controller.py:3203 #: erpnext/public/js/controllers/accounts.js:120 msgid "Cannot refer row number greater than or equal to current row number for this Charge type" msgstr "" @@ -9236,7 +9222,7 @@ msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1523 #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1701 #: erpnext/accounts/doctype/payment_entry/payment_entry.py:1827 -#: erpnext/controllers/accounts_controller.py:3183 +#: erpnext/controllers/accounts_controller.py:3193 #: erpnext/public/js/controllers/accounts.js:112 #: erpnext/public/js/controllers/taxes_and_totals.js:531 msgid "Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row" @@ -9254,11 +9240,11 @@ msgstr "" msgid "Cannot set multiple Item Defaults for a company." msgstr "" -#: erpnext/controllers/accounts_controller.py:3888 +#: erpnext/controllers/accounts_controller.py:3898 msgid "Cannot set quantity less than delivered quantity" msgstr "" -#: erpnext/controllers/accounts_controller.py:3889 +#: erpnext/controllers/accounts_controller.py:3899 msgid "Cannot set quantity less than received quantity" msgstr "" @@ -9270,7 +9256,7 @@ msgstr "" msgid "Cannot start deletion. Another deletion {0} is already queued/running. Please wait for it to complete." msgstr "" -#: erpnext/controllers/accounts_controller.py:3916 +#: erpnext/controllers/accounts_controller.py:3926 msgid "Cannot update rate as item {0} is already ordered or purchased against this quotation" msgstr "" @@ -9451,7 +9437,7 @@ msgstr "" msgid "Cash In Hand" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:322 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:321 msgid "Cash or Bank Account is mandatory for making payment entry" msgstr "" @@ -9545,8 +9531,8 @@ msgstr "" msgid "Category-wise Asset Value" msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:298 -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:140 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:297 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:142 msgid "Caution" msgstr "" @@ -9683,7 +9669,7 @@ msgid "Channel Partner" msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2256 -#: erpnext/controllers/accounts_controller.py:3246 +#: erpnext/controllers/accounts_controller.py:3256 msgid "Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount" msgstr "" @@ -10857,7 +10843,7 @@ msgstr "" msgid "Company Address Name" msgstr "" -#: erpnext/controllers/accounts_controller.py:4340 +#: erpnext/controllers/accounts_controller.py:4352 msgid "Company Address is missing. You don't have permission to update it. Please contact your System Manager." msgstr "" @@ -10927,7 +10913,7 @@ msgid "Company Field" msgstr "" #. Label of the company_logo (Attach Image) field in DocType 'Company' -#: erpnext/public/js/print.js:75 erpnext/setup/doctype/company/company.json +#: erpnext/public/js/print.js:77 erpnext/setup/doctype/company/company.json msgid "Company Logo" msgstr "" @@ -10939,7 +10925,10 @@ msgstr "" msgid "Company Not Linked" msgstr "" +#. Label of the shipping_address (Link) field in DocType 'Request for +#. Quotation' #. Label of the shipping_address (Link) field in DocType 'Subcontracting Order' +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.json #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json msgid "Company Shipping Address" msgstr "" @@ -10988,6 +10977,10 @@ msgstr "" msgid "Company of asset {0} and purchase document {1} doesn't matches." msgstr "" +#: erpnext/setup/doctype/employee/employee.py:168 +msgid "Company or Personal Email is mandatory when 'Create User Automatically' is enabled" +msgstr "" + #. Description of the 'Registration Details' (Code) field in DocType 'Company' #: erpnext/setup/doctype/company/company.json msgid "Company registration numbers for your reference. Tax numbers etc." @@ -11058,7 +11051,7 @@ msgstr "" msgid "Competitors" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:269 +#: erpnext/manufacturing/doctype/job_card/job_card.js:277 #: erpnext/manufacturing/doctype/workstation/workstation.js:151 msgid "Complete Job" msgstr "" @@ -11105,8 +11098,8 @@ msgstr "" msgid "Completed Qty cannot be greater than 'Qty to Manufacture'" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:317 -#: erpnext/manufacturing/doctype/job_card/job_card.js:438 +#: erpnext/manufacturing/doctype/job_card/job_card.js:325 +#: erpnext/manufacturing/doctype/job_card/job_card.js:446 #: erpnext/manufacturing/doctype/workstation/workstation.js:296 msgid "Completed Quantity" msgstr "" @@ -11787,15 +11780,15 @@ msgstr "" msgid "Conversion factor for item {0} has been reset to 1.0 as the uom {1} is same as stock uom {2}." msgstr "" -#: erpnext/controllers/accounts_controller.py:2961 +#: erpnext/controllers/accounts_controller.py:2971 msgid "Conversion rate cannot be 0" msgstr "" -#: erpnext/controllers/accounts_controller.py:2968 +#: erpnext/controllers/accounts_controller.py:2978 msgid "Conversion rate is 1.00, but document currency is different from company currency" msgstr "" -#: erpnext/controllers/accounts_controller.py:2964 +#: erpnext/controllers/accounts_controller.py:2974 msgid "Conversion rate must be 1.00 if document currency is same as company currency" msgstr "" @@ -11861,13 +11854,13 @@ msgstr "" msgid "Corrective Action" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:495 +#: erpnext/manufacturing/doctype/job_card/job_card.js:503 msgid "Corrective Job Card" msgstr "" #. Label of the corrective_operation_section (Tab Break) field in DocType 'Job #. Card' -#: erpnext/manufacturing/doctype/job_card/job_card.js:502 +#: erpnext/manufacturing/doctype/job_card/job_card.js:510 #: erpnext/manufacturing/doctype/job_card/job_card.json msgid "Corrective Operation" msgstr "" @@ -12106,7 +12099,7 @@ msgid "Cost Center is a part of Cost Center Allocation, hence cannot be converte msgstr "" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1433 -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:899 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:903 msgid "Cost Center is required in row {0} in Taxes table for type {1}" msgstr "" @@ -12428,7 +12421,7 @@ msgstr "" msgid "Create Inter Company Journal Entry" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js:54 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js:55 msgid "Create Invoices" msgstr "" @@ -12666,7 +12659,7 @@ msgstr "" msgid "Create Supplier" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:180 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:181 msgid "Create Supplier Quotation" msgstr "" @@ -12697,13 +12690,19 @@ msgstr "" msgid "Create Transfer Entry" msgstr "" -#. Label of the create_user (Button) field in DocType 'Employee' -#: erpnext/setup/doctype/employee/employee.json +#: erpnext/setup/doctype/employee/employee.js:50 +#: erpnext/setup/doctype/employee/employee.js:52 #: erpnext/utilities/activation.py:117 msgid "Create User" msgstr "" +#. Label of the create_user_automatically (Check) field in DocType 'Employee' +#: erpnext/setup/doctype/employee/employee.json +msgid "Create User Automatically" +msgstr "" + #. Label of the create_user_permission (Check) field in DocType 'Employee' +#: erpnext/setup/doctype/employee/employee.js:65 #: erpnext/setup/doctype/employee/employee.json msgid "Create User Permission" msgstr "" @@ -12741,7 +12740,7 @@ msgstr "" msgid "Create a variant with the template image." msgstr "" -#: erpnext/stock/stock_ledger.py:2018 +#: erpnext/stock/stock_ledger.py:2065 msgid "Create an incoming stock transaction for the Item." msgstr "" @@ -12760,12 +12759,6 @@ msgstr "" msgid "Create in Draft Status" msgstr "" -#. Description of the 'Create Missing Party' (Check) field in DocType 'Opening -#. Invoice Creation Tool' -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json -msgid "Create missing customer or supplier." -msgstr "" - #. Label of an action in the Onboarding Step 'Create Supplier' #: erpnext/buying/onboarding_step/create_supplier/create_supplier.json msgid "Create supplier" @@ -12785,6 +12778,12 @@ msgstr "" msgid "Created {0} scorecards for {1} between:" msgstr "" +#. Description of the 'Create User Automatically' (Check) field in DocType +#. 'Employee' +#: erpnext/setup/doctype/employee/employee.json +msgid "Creates a User account for this employee using the Preferred, Company, or Personal email." +msgstr "" + #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js:140 msgid "Creating Accounts..." msgstr "" @@ -12809,7 +12808,7 @@ msgstr "" msgid "Creating Packing Slip ..." msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js:60 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js:61 msgid "Creating Purchase Invoices ..." msgstr "" @@ -12823,7 +12822,7 @@ msgstr "" msgid "Creating Purchase Receipt ..." msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js:58 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js:59 msgid "Creating Sales Invoices ..." msgstr "" @@ -12844,11 +12843,11 @@ msgstr "" msgid "Creating Subcontracting Receipt ..." msgstr "" -#: erpnext/setup/doctype/employee/employee.js:92 +#: erpnext/setup/doctype/employee/employee.js:85 msgid "Creating User..." msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:287 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:300 msgid "Creating {} out of {} {}" msgstr "" @@ -13061,9 +13060,9 @@ msgstr "" #. Label of the credit_to (Link) field in DocType 'Purchase Invoice' #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:379 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:387 -#: erpnext/controllers/accounts_controller.py:2360 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:378 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:386 +#: erpnext/controllers/accounts_controller.py:2370 msgid "Credit To" msgstr "" @@ -14283,7 +14282,7 @@ msgstr "" msgid "Date of Birth" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:146 +#: erpnext/setup/doctype/employee/employee.py:260 msgid "Date of Birth cannot be greater than today." msgstr "" @@ -14535,7 +14534,7 @@ msgstr "" #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1011 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1022 -#: erpnext/controllers/accounts_controller.py:2360 +#: erpnext/controllers/accounts_controller.py:2370 msgid "Debit To" msgstr "" @@ -14713,7 +14712,7 @@ msgstr "" msgid "Default BOM for {0} not found" msgstr "" -#: erpnext/controllers/accounts_controller.py:3960 +#: erpnext/controllers/accounts_controller.py:3970 msgid "Default BOM not found for FG Item {0}" msgstr "" @@ -15202,6 +15201,11 @@ msgstr "" msgid "Define Project type." msgstr "" +#. Description of the 'End of Life' (Date) field in DocType 'Item' +#: erpnext/stock/doctype/item/item.json +msgid "Defines the date after which the item can no longer be used in transactions or manufacturing" +msgstr "" + #. Name of a UOM #: erpnext/setup/setup_wizard/data/uom_data.json msgid "Dekagram/Litre" @@ -15788,7 +15792,7 @@ msgstr "" msgid "Depreciation Entry against asset {0}" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:255 +#: erpnext/assets/doctype/asset/depreciation.py:258 msgid "Depreciation Entry against {0} worth {1}" msgstr "" @@ -15800,7 +15804,7 @@ msgstr "" msgid "Depreciation Expense Account" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:302 +#: erpnext/assets/doctype/asset/depreciation.py:305 msgid "Depreciation Expense Account should be an Income or Expense Account." msgstr "" @@ -16003,7 +16007,7 @@ msgstr "" msgid "Difference Posting Date" msgstr "" -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py:100 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:120 msgid "Difference Qty" msgstr "" @@ -16572,7 +16576,7 @@ msgstr "" msgid "Disposal Date" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:832 +#: erpnext/assets/doctype/asset/depreciation.py:835 msgid "Disposal date {0} cannot be before {1} date {2} of the asset." msgstr "" @@ -16610,12 +16614,6 @@ msgstr "" msgid "Distance from top edge" msgstr "" -#. Label of the distinct_item_and_warehouse (Code) field in DocType 'Repost -#. Item Valuation' -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json -msgid "Distinct Item and Warehouse" -msgstr "" - #. Description of a DocType #: erpnext/stock/doctype/serial_no/serial_no.json msgid "Distinct unit of an Item" @@ -16767,7 +16765,7 @@ msgstr "" msgid "Do you want to submit the material request" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:103 +#: erpnext/manufacturing/doctype/job_card/job_card.js:111 msgid "Do you want to submit the stock entry?" msgstr "" @@ -16823,10 +16821,6 @@ msgstr "" msgid "Document Type already used as a dimension" msgstr "" -#: erpnext/setup/install.py:189 -msgid "Documentation" -msgstr "" - #. Description of the 'Reconciliation Queue Size' (Int) field in DocType #. 'Accounts Settings' #: erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -16884,7 +16878,7 @@ msgstr "" msgid "Download CSV Template" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:144 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:145 msgid "Download PDF for Supplier" msgstr "" @@ -17450,10 +17444,18 @@ msgstr "" msgid "Email Receipt" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:371 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:373 msgid "Email Sent to Supplier {0}" msgstr "" +#: erpnext/setup/doctype/employee/employee.py:433 +msgid "Email is required to create a user" +msgstr "" + +#: erpnext/setup/doctype/employee/employee.js:72 +msgid "Email is required to create a user." +msgstr "" + #: erpnext/stock/doctype/shipment/shipment.js:174 msgid "Email or Phone/Mobile of the Contact are mandatory to continue." msgstr "" @@ -17584,11 +17586,6 @@ msgstr "" msgid "Employee Education" msgstr "" -#. Label of the exit (Tab Break) field in DocType 'Employee' -#: erpnext/setup/doctype/employee/employee.json -msgid "Employee Exit" -msgstr "" - #. Name of a DocType #: erpnext/setup/doctype/employee_external_work_history/employee_external_work_history.json msgid "Employee External Work History" @@ -17637,11 +17634,11 @@ msgstr "" msgid "Employee User Id" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:211 +#: erpnext/setup/doctype/employee/employee.py:325 msgid "Employee cannot report to himself." msgstr "" -#: erpnext/setup/doctype/employee/employee.py:443 +#: erpnext/setup/doctype/employee/employee.py:567 msgid "Employee is required" msgstr "" @@ -17649,6 +17646,10 @@ msgstr "" msgid "Employee is required while issuing Asset {0}" msgstr "" +#: erpnext/setup/doctype/employee/employee.py:430 +msgid "Employee {0} already has a linked user" +msgstr "" + #: erpnext/assets/doctype/asset_movement/asset_movement.py:92 #: erpnext/assets/doctype/asset_movement/asset_movement.py:113 msgid "Employee {0} does not belong to the company {1}" @@ -17658,7 +17659,7 @@ msgstr "" msgid "Employee {0} is currently working on another workstation. Please assign another employee." msgstr "" -#: erpnext/setup/doctype/employee/employee.py:468 +#: erpnext/setup/doctype/employee/employee.py:592 msgid "Employee {0} not found" msgstr "" @@ -17874,6 +17875,11 @@ msgstr "" msgid "Enable to apply SLA on every {0}" msgstr "" +#. Description of the 'Retain Sample' (Check) field in DocType 'Item' +#: erpnext/stock/doctype/item/item.json +msgid "Enable to reserve a small sample from each batch for any analysis arising ahead" +msgstr "" + #. Label of the enable_tracking_sales_commissions (Check) field in DocType #. 'Selling Settings' #: erpnext/selling/doctype/selling_settings/selling_settings.json @@ -17927,8 +17933,8 @@ msgstr "" #. Label of the end_time (Time) field in DocType 'Stock Reposting Settings' #. Label of the end_time (Time) field in DocType 'Service Day' #. Label of the end_time (Datetime) field in DocType 'Call Log' -#: erpnext/manufacturing/doctype/job_card/job_card.js:375 -#: erpnext/manufacturing/doctype/job_card/job_card.js:445 +#: erpnext/manufacturing/doctype/job_card/job_card.js:383 +#: erpnext/manufacturing/doctype/job_card/job_card.js:453 #: erpnext/manufacturing/doctype/workstation_working_hour/workstation_working_hour.json #: erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json #: erpnext/support/doctype/service_day/service_day.json @@ -17987,12 +17993,6 @@ msgstr "" msgid "Engineer" msgstr "" -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html:13 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html:23 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py:30 -msgid "Enough Parts to Build" -msgstr "" - #. Label of the ensure_delivery_based_on_produced_serial_no (Check) field in #. DocType 'Sales Order Item' #: erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -18003,11 +18003,11 @@ msgstr "" msgid "Enter API key in Google Settings." msgstr "" -#: erpnext/public/js/print.js:62 +#: erpnext/public/js/print.js:64 msgid "Enter Company Details" msgstr "" -#: erpnext/setup/doctype/employee/employee.js:108 +#: erpnext/setup/doctype/employee/employee.js:148 msgid "Enter First and Last name of Employee, based on Which Full Name will be updated. IN transactions, it will be Full Name which will be fetched." msgstr "" @@ -18019,8 +18019,8 @@ msgstr "" msgid "Enter Serial Nos" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:402 -#: erpnext/manufacturing/doctype/job_card/job_card.js:471 +#: erpnext/manufacturing/doctype/job_card/job_card.js:410 +#: erpnext/manufacturing/doctype/job_card/job_card.js:479 #: erpnext/manufacturing/doctype/workstation/workstation.js:312 msgid "Enter Value" msgstr "" @@ -18161,11 +18161,11 @@ msgstr "" msgid "Error Description" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:277 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:290 msgid "Error Occurred" msgstr "" -#: erpnext/telephony/doctype/call_log/call_log.py:195 +#: erpnext/telephony/doctype/call_log/call_log.py:197 msgid "Error during caller information update" msgstr "" @@ -18181,7 +18181,7 @@ msgstr "" msgid "Error in party matching for Bank Transaction {0}" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:319 +#: erpnext/assets/doctype/asset/depreciation.py:322 msgid "Error while posting depreciation entries" msgstr "" @@ -18189,7 +18189,7 @@ msgstr "" msgid "Error while processing deferred accounting for {0}" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:531 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:533 msgid "Error while reposting item valuation" msgstr "" @@ -18262,7 +18262,7 @@ msgstr "" msgid "Example: ABCD.#####. If series is set and Batch No is not mentioned in transactions, then automatic batch number will be created based on this series. If you always want to explicitly mention Batch No for this item, leave this blank. Note: this setting will take priority over the Naming Series Prefix in Stock Settings." msgstr "" -#: erpnext/stock/stock_ledger.py:2281 +#: erpnext/stock/stock_ledger.py:2328 msgid "Example: Serial No {0} reserved in {1}." msgstr "" @@ -18486,12 +18486,17 @@ msgstr "" msgid "Existing Customer" msgstr "" +#. Label of the exit (Tab Break) field in DocType 'Employee' +#: erpnext/setup/doctype/employee/employee.json +msgid "Exit" +msgstr "" + #. Label of the held_on (Date) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Exit Interview Held On" msgstr "" -#: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py:444 +#: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py:470 msgid "Expected" msgstr "" @@ -18605,7 +18610,7 @@ msgstr "" #: erpnext/accounts/doctype/cashier_closing/cashier_closing.json #: erpnext/accounts/doctype/ledger_merge/ledger_merge.json #: erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.json -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:605 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:604 #: erpnext/accounts/report/account_balance/account_balance.js:28 #: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js:89 #: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:183 @@ -18676,13 +18681,13 @@ msgstr "" msgid "Expense Head" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:499 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:523 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:543 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:498 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:522 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:542 msgid "Expense Head Changed" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:601 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:600 msgid "Expense account is mandatory for item {0}" msgstr "" @@ -18804,6 +18809,10 @@ msgstr "" msgid "FG / Semi FG Item" msgstr "" +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js:21 +msgid "FG Items to Make" +msgstr "" + #. Option for the 'Default Stock Valuation Method' (Select) field in DocType #. 'Company' #. Option for the 'Valuation Method' (Select) field in DocType 'Item' @@ -19004,7 +19013,7 @@ msgstr "" msgid "Fetched only {0} available serial numbers." msgstr "" -#: erpnext/edi/doctype/code_list/code_list_import.py:27 +#: erpnext/edi/doctype/code_list/code_list_import.py:28 msgid "Fetching Error" msgstr "" @@ -19316,15 +19325,15 @@ msgstr "" msgid "Finished Good Item Quantity" msgstr "" -#: erpnext/controllers/accounts_controller.py:3946 +#: erpnext/controllers/accounts_controller.py:3956 msgid "Finished Good Item is not specified for service item {0}" msgstr "" -#: erpnext/controllers/accounts_controller.py:3963 +#: erpnext/controllers/accounts_controller.py:3973 msgid "Finished Good Item {0} Qty can not be zero" msgstr "" -#: erpnext/controllers/accounts_controller.py:3957 +#: erpnext/controllers/accounts_controller.py:3967 msgid "Finished Good Item {0} must be a sub-contracted item" msgstr "" @@ -19725,7 +19734,7 @@ msgid "For Job Card" msgstr "" #. Label of the for_operation (Link) field in DocType 'Job Card' -#: erpnext/manufacturing/doctype/job_card/job_card.js:515 +#: erpnext/manufacturing/doctype/job_card/job_card.js:523 #: erpnext/manufacturing/doctype/job_card/job_card.json msgid "For Operation" msgstr "" @@ -19857,7 +19866,7 @@ msgstr "" msgid "For row {0} in {1}. To include {2} in Item rate, rows {3} must also be included" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1716 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1721 msgid "For row {0}: Enter Planned Qty" msgstr "" @@ -20034,8 +20043,8 @@ msgstr "" msgid "From BOM" msgstr "" -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py:63 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py:25 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:105 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:169 msgid "From BOM No" msgstr "" @@ -20468,7 +20477,7 @@ msgstr "" msgid "Future Payments" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:382 +#: erpnext/assets/doctype/asset/depreciation.py:385 msgid "Future date is not allowed" msgstr "" @@ -20771,9 +20780,9 @@ msgstr "" #: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1125 #: erpnext/buying/doctype/purchase_order/purchase_order.js:540 #: erpnext/buying/doctype/purchase_order/purchase_order.js:563 -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:379 -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:401 -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:446 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:380 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:402 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:447 #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.js:75 #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.js:108 #: erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js:80 @@ -20816,7 +20825,7 @@ msgstr "" msgid "Get Items from BOM" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:418 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:419 msgid "Get Items from Material Requests against this Supplier" msgstr "" @@ -20906,12 +20915,12 @@ msgstr "" msgid "Get Sub Assembly Items" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:460 -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:480 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:461 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:481 msgid "Get Suppliers" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:484 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:485 msgid "Get Suppliers By" msgstr "" @@ -21558,8 +21567,8 @@ msgstr "" msgid "Hectopascal" msgstr "" -#. Label of the height (Int) field in DocType 'Shipment Parcel' -#. Label of the height (Int) field in DocType 'Shipment Parcel Template' +#. Label of the height (Float) field in DocType 'Shipment Parcel' +#. Label of the height (Float) field in DocType 'Shipment Parcel Template' #: erpnext/stock/doctype/shipment_parcel/shipment_parcel.json #: erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json msgid "Height (cm)" @@ -21585,11 +21594,11 @@ msgstr "" msgid "Helps you distribute the Budget/Target across months if you have seasonality in your business." msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:349 +#: erpnext/assets/doctype/asset/depreciation.py:352 msgid "Here are the error logs for the aforementioned failed depreciation entries: {0}" msgstr "" -#: erpnext/stock/stock_ledger.py:2003 +#: erpnext/stock/stock_ledger.py:2050 msgid "Here are the options to proceed:" msgstr "" @@ -21604,7 +21613,7 @@ msgstr "" msgid "Here you can maintain height, weight, allergies, medical concerns etc" msgstr "" -#: erpnext/setup/doctype/employee/employee.js:134 +#: erpnext/setup/doctype/employee/employee.js:174 msgid "Here, you can select a senior of this Employee. Based on this, Organization Chart will be populated." msgstr "" @@ -21617,7 +21626,7 @@ msgstr "" msgid "Hertz" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:533 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:535 msgid "Hi," msgstr "" @@ -22174,10 +22183,16 @@ 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:2013 +#: erpnext/stock/stock_ledger.py:2060 msgid "If not, you can Cancel / Submit this entry" msgstr "" +#. Description of the 'Create Missing Party' (Check) field in DocType 'Opening +#. Invoice Creation Tool' +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json +msgid "If party does not exist, create it using the Party Name field." +msgstr "" + #. Description of the 'Free Item Rate' (Currency) field in DocType 'Pricing #. Rule' #: erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -22203,7 +22218,7 @@ msgstr "" msgid "If the account is frozen, entries are allowed to restricted users." msgstr "" -#: erpnext/stock/stock_ledger.py:2006 +#: erpnext/stock/stock_ledger.py:2053 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 "" @@ -22300,11 +22315,11 @@ msgstr "" msgid "If you need to reconcile particular transactions against each other, then please select accordingly. If not, all the transactions will be allocated in FIFO order." msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1092 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1097 msgid "If you still want to proceed, please disable 'Skip Available Sub Assembly Items' checkbox." msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1832 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1837 msgid "If you still want to proceed, please enable {0}." msgstr "" @@ -22384,7 +22399,7 @@ msgstr "" msgid "Ignore Existing Ordered Qty" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1824 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1829 msgid "Ignore Existing Projected Quantity" msgstr "" @@ -22496,6 +22511,10 @@ msgstr "" msgid "Import Data" msgstr "" +#: erpnext/setup/doctype/employee/employee_list.js:16 +msgid "Import Employees" +msgstr "" + #: erpnext/edi/doctype/code_list/code_list.js:7 #: erpnext/edi/doctype/code_list/code_list_list.js:3 #: erpnext/edi/doctype/common_code/common_code_list.js:3 @@ -22607,12 +22626,6 @@ msgstr "" msgid "In Stock" msgstr "" -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html:12 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html:22 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py:29 -msgid "In Stock Qty" -msgstr "" - #. Option for the 'Status' (Select) field in DocType 'Delivery Trip' #. Option for the 'Transfer Status' (Select) field in DocType 'Material #. Request' @@ -23354,8 +23367,8 @@ msgstr "" msgid "Insufficient Capacity" msgstr "" -#: erpnext/controllers/accounts_controller.py:3840 -#: erpnext/controllers/accounts_controller.py:3864 +#: erpnext/controllers/accounts_controller.py:3850 +#: erpnext/controllers/accounts_controller.py:3874 msgid "Insufficient Permissions" msgstr "" @@ -23364,12 +23377,12 @@ msgstr "" #: erpnext/stock/doctype/pick_list/pick_list.py:162 #: erpnext/stock/doctype/pick_list/pick_list.py:1055 #: erpnext/stock/doctype/stock_entry/stock_entry.py:956 -#: erpnext/stock/serial_batch_bundle.py:1205 erpnext/stock/stock_ledger.py:1713 -#: erpnext/stock/stock_ledger.py:2172 +#: erpnext/stock/serial_batch_bundle.py:1205 erpnext/stock/stock_ledger.py:1741 +#: erpnext/stock/stock_ledger.py:2219 msgid "Insufficient Stock" msgstr "" -#: erpnext/stock/stock_ledger.py:2187 +#: erpnext/stock/stock_ledger.py:2234 msgid "Insufficient Stock for Batch" msgstr "" @@ -23604,14 +23617,14 @@ msgstr "" msgid "Interval should be between 1 to 59 MInutes" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:380 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:388 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:379 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:387 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1017 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1027 #: erpnext/assets/doctype/asset_category/asset_category.py:69 #: erpnext/assets/doctype/asset_category/asset_category.py:97 -#: erpnext/controllers/accounts_controller.py:3207 -#: erpnext/controllers/accounts_controller.py:3215 +#: erpnext/controllers/accounts_controller.py:3217 +#: erpnext/controllers/accounts_controller.py:3225 msgid "Invalid Account" msgstr "" @@ -23620,7 +23633,7 @@ msgid "Invalid Accounting Dimension" msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:400 -#: erpnext/accounts/doctype/payment_request/payment_request.py:1004 +#: erpnext/accounts/doctype/payment_request/payment_request.py:1005 msgid "Invalid Allocated Amount" msgstr "" @@ -23662,7 +23675,7 @@ msgstr "" #: erpnext/assets/doctype/asset/asset.py:361 #: erpnext/assets/doctype/asset/asset.py:368 -#: erpnext/controllers/accounts_controller.py:3230 +#: erpnext/controllers/accounts_controller.py:3240 msgid "Invalid Cost Center" msgstr "" @@ -23696,7 +23709,7 @@ msgid "Invalid Group By" msgstr "" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:499 -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:955 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:960 msgid "Invalid Item" msgstr "" @@ -23748,7 +23761,7 @@ msgstr "" msgid "Invalid Priority" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1230 +#: erpnext/manufacturing/doctype/bom/bom.py:1232 msgid "Invalid Process Loss Configuration" msgstr "" @@ -23756,8 +23769,8 @@ msgstr "" msgid "Invalid Purchase Invoice" msgstr "" -#: erpnext/controllers/accounts_controller.py:3884 -#: erpnext/controllers/accounts_controller.py:3898 +#: erpnext/controllers/accounts_controller.py:3894 +#: erpnext/controllers/accounts_controller.py:3908 msgid "Invalid Qty" msgstr "" @@ -23865,7 +23878,7 @@ msgid "Invalid {0}: {1}" msgstr "" #. Label of the inventory_section (Tab Break) field in DocType 'Item' -#: erpnext/setup/install.py:358 erpnext/stock/doctype/item/item.json +#: erpnext/setup/install.py:359 erpnext/stock/doctype/item/item.json msgid "Inventory" msgstr "" @@ -24824,10 +24837,8 @@ msgstr "" #: erpnext/manufacturing/doctype/bom/bom.json #: erpnext/manufacturing/doctype/plant_floor/plant_floor.js:109 #: erpnext/manufacturing/doctype/workstation/workstation_job_card.html:25 -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py:50 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html:9 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html:19 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py:22 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:101 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:165 #: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js:68 #: erpnext/manufacturing/report/process_loss_report/process_loss_report.js:15 #: erpnext/manufacturing/report/process_loss_report/process_loss_report.py:74 @@ -25217,7 +25228,7 @@ msgstr "" msgid "Item Code cannot be changed for Serial No." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:455 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:454 msgid "Item Code required at Row No {0}" msgstr "" @@ -25596,7 +25607,6 @@ msgstr "" #: erpnext/manufacturing/notification/material_request_receipt_notification/material_request_receipt_notification.html:8 #: erpnext/manufacturing/report/bom_explorer/bom_explorer.py:66 #: erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py:109 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py:23 #: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py:106 #: erpnext/manufacturing/report/job_card_summary/job_card_summary.py:158 #: erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.py:958 @@ -26033,7 +26043,7 @@ msgstr "" msgid "Item operation" msgstr "" -#: erpnext/controllers/accounts_controller.py:3938 +#: erpnext/controllers/accounts_controller.py:3948 msgid "Item qty can not be updated as raw materials are already processed." msgstr "" @@ -26108,7 +26118,7 @@ msgstr "" msgid "Item {0} has reached its end of life on {1}" msgstr "" -#: erpnext/stock/stock_ledger.py:118 +#: erpnext/stock/stock_ledger.py:117 msgid "Item {0} ignored since it is not a stock item" msgstr "" @@ -26132,7 +26142,7 @@ msgstr "" msgid "Item {0} is not a stock Item" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:954 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:959 msgid "Item {0} is not a subcontracted item" msgstr "" @@ -26164,7 +26174,7 @@ msgstr "" msgid "Item {0} not found." msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:325 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:324 msgid "Item {0}: Ordered qty {1} cannot be less than minimum order qty {2} (defined in Item)." msgstr "" @@ -26242,7 +26252,7 @@ msgstr "" msgid "Items Filter" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1678 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1683 #: erpnext/selling/doctype/sales_order/sales_order.js:1676 msgid "Items Required" msgstr "" @@ -26266,11 +26276,11 @@ msgstr "" msgid "Items and Pricing" msgstr "" -#: erpnext/controllers/accounts_controller.py:4198 +#: erpnext/controllers/accounts_controller.py:4208 msgid "Items cannot be updated as Subcontracting Inward Order(s) exist against this Subcontracted Sales Order." msgstr "" -#: erpnext/controllers/accounts_controller.py:4191 +#: erpnext/controllers/accounts_controller.py:4201 msgid "Items cannot be updated as Subcontracting Order is created against the Purchase Order {0}." msgstr "" @@ -26292,7 +26302,7 @@ msgstr "" msgid "Items to Be Repost" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1677 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1682 msgid "Items to Manufacture are required to pull the Raw Materials associated with it." msgstr "" @@ -26868,7 +26878,7 @@ msgstr "" #: erpnext/buying/doctype/purchase_order_item/purchase_order_item.json #: erpnext/manufacturing/doctype/bom/bom.json #: erpnext/manufacturing/doctype/bom_creator/bom_creator.json -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py:106 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:123 #: erpnext/stock/doctype/item/item.json #: erpnext/stock/report/item_prices/item_prices.py:56 msgid "Last Purchase Rate" @@ -27140,6 +27150,11 @@ msgstr "" msgid "Ledgers" msgstr "" +#. Label of the vouchers_posted (Int) field in DocType 'Repost Item Valuation' +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +msgid "Ledgers Posted" +msgstr "" + #. Label of the left_child (Link) field in DocType 'Bisect Nodes' #: erpnext/accounts/doctype/bisect_nodes/bisect_nodes.json msgid "Left Child" @@ -27170,8 +27185,8 @@ msgstr "" msgid "Legend" msgstr "" -#. Label of the length (Int) field in DocType 'Shipment Parcel' -#. Label of the length (Int) field in DocType 'Shipment Parcel Template' +#. Label of the length (Float) field in DocType 'Shipment Parcel' +#. Label of the length (Float) field in DocType 'Shipment Parcel Template' #: erpnext/stock/doctype/shipment_parcel/shipment_parcel.json #: erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json msgid "Length (cm)" @@ -27286,7 +27301,7 @@ msgstr "" msgid "Link to Material Request" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:451 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:452 #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.js:80 msgid "Link to Material Requests" msgstr "" @@ -27924,7 +27939,7 @@ msgstr "" #. Label of the make (Data) field in DocType 'Vehicle' #: erpnext/accounts/doctype/journal_entry/journal_entry.js:123 -#: erpnext/manufacturing/doctype/job_card/job_card.js:536 +#: erpnext/manufacturing/doctype/job_card/job_card.js:544 #: erpnext/manufacturing/doctype/work_order/work_order.js:832 #: erpnext/manufacturing/doctype/work_order/work_order.js:866 #: erpnext/setup/doctype/vehicle/vehicle.json @@ -27984,12 +27999,12 @@ msgstr "" msgid "Make Serial No / Batch from Work Order" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:101 +#: erpnext/manufacturing/doctype/job_card/job_card.js:109 #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:256 msgid "Make Stock Entry" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:410 +#: erpnext/manufacturing/doctype/job_card/job_card.js:418 msgid "Make Subcontracting PO" msgstr "" @@ -28193,7 +28208,7 @@ msgstr "" #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json #: erpnext/buying/doctype/purchase_order_item/purchase_order_item.json #: erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py:70 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:110 #: erpnext/stock/doctype/item/item.json #: erpnext/stock/doctype/item_manufacturer/item_manufacturer.json #: erpnext/stock/doctype/manufacturer/manufacturer.json @@ -28223,7 +28238,7 @@ msgstr "" #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json #: erpnext/buying/doctype/purchase_order_item/purchase_order_item.json #: erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py:76 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:113 #: erpnext/stock/doctype/item_manufacturer/item_manufacturer.json #: erpnext/stock/doctype/material_request_item/material_request_item.json #: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -28257,7 +28272,7 @@ msgstr "" #: erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json #: erpnext/manufacturing/workspace/manufacturing/manufacturing.json #: erpnext/selling/doctype/sales_order/sales_order_dashboard.py:29 -#: erpnext/setup/doctype/company/company.json erpnext/setup/install.py:363 +#: erpnext/setup/doctype/company/company.json erpnext/setup/install.py:364 #: erpnext/setup/setup_wizard/data/industry_type.txt:31 #: erpnext/stock/doctype/batch/batch.json erpnext/stock/doctype/item/item.json #: erpnext/stock/doctype/item_lead_time/item_lead_time.json @@ -28606,14 +28621,14 @@ msgstr "" #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json #: erpnext/buying/doctype/purchase_order/purchase_order.js:519 #: erpnext/buying/doctype/purchase_order_item/purchase_order_item.json -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:360 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:361 #: erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.js:56 #: erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json #: erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.js:33 #: erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py:184 #: erpnext/buying/workspace/buying/buying.json -#: erpnext/manufacturing/doctype/job_card/job_card.js:160 +#: erpnext/manufacturing/doctype/job_card/job_card.js:168 #: erpnext/manufacturing/doctype/production_plan/production_plan.js:159 #: erpnext/manufacturing/doctype/production_plan/production_plan.json #: erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json @@ -28728,7 +28743,7 @@ msgstr "" msgid "Material Request used to make this Stock Entry" msgstr "" -#: erpnext/controllers/subcontracting_controller.py:1337 +#: erpnext/controllers/subcontracting_controller.py:1343 msgid "Material Request {0} is cancelled or stopped" msgstr "" @@ -28782,7 +28797,7 @@ msgstr "" #. Option for the 'Purpose' (Select) field in DocType 'Pick List' #. Option for the 'Purpose' (Select) field in DocType 'Stock Entry' #. Option for the 'Purpose' (Select) field in DocType 'Stock Entry Type' -#: erpnext/manufacturing/doctype/job_card/job_card.js:174 +#: erpnext/manufacturing/doctype/job_card/job_card.js:182 #: erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json #: erpnext/setup/setup_wizard/operations/install_fixtures.py:83 #: erpnext/stock/doctype/item/item.json @@ -28847,7 +28862,7 @@ msgstr "" msgid "Materials To Be Transferred" msgstr "" -#: erpnext/controllers/subcontracting_controller.py:1570 +#: erpnext/controllers/subcontracting_controller.py:1576 msgid "Materials are already received against the {0} {1}" msgstr "" @@ -28943,6 +28958,11 @@ msgstr "" msgid "Maximum Payment Amount" msgstr "" +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:82 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:151 +msgid "Maximum Producible Items" +msgstr "" + #: erpnext/stock/doctype/stock_entry/stock_entry.py:3870 msgid "Maximum Samples - {0} can be retained for Batch {1} and Item {2}." msgstr "" @@ -29002,7 +29022,7 @@ msgstr "" msgid "Megawatt" msgstr "" -#: erpnext/stock/stock_ledger.py:2019 +#: erpnext/stock/stock_ledger.py:2066 msgid "Mention Valuation Rate in the Item master." msgstr "" @@ -29375,7 +29395,7 @@ 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:597 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:596 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2422 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3030 #: erpnext/assets/doctype/asset_category/asset_category.py:116 @@ -29415,7 +29435,7 @@ msgstr "" msgid "Missing Item" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:443 +#: erpnext/setup/doctype/employee/employee.py:567 msgid "Missing Parameter" msgstr "" @@ -29439,7 +29459,7 @@ msgstr "" msgid "Missing required filter: {0}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1183 +#: erpnext/manufacturing/doctype/bom/bom.py:1185 #: erpnext/manufacturing/doctype/work_order/work_order.py:1476 msgid "Missing value" msgstr "" @@ -29721,7 +29741,7 @@ msgstr "" #: erpnext/manufacturing/doctype/work_order/work_order.py:1423 #: erpnext/setup/doctype/uom/uom.json #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:267 -#: erpnext/utilities/transaction_base.py:567 +#: erpnext/utilities/transaction_base.py:568 msgid "Must be Whole Number" msgstr "" @@ -30426,7 +30446,7 @@ msgstr "" msgid "No Item with Serial No {0}" msgstr "" -#: erpnext/controllers/subcontracting_controller.py:1488 +#: erpnext/controllers/subcontracting_controller.py:1494 msgid "No Items selected for transfer." msgstr "" @@ -30461,7 +30481,7 @@ msgstr "" msgid "No Permission" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:787 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:792 msgid "No Purchase Orders were created" msgstr "" @@ -30470,7 +30490,7 @@ msgstr "" msgid "No Records for these settings." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:337 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:336 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1105 msgid "No Remarks" msgstr "" @@ -30515,12 +30535,12 @@ msgstr "" msgid "No Unreconciled Payments found for this party" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:784 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:789 #: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py:249 msgid "No Work Orders were created" msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:829 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:833 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:860 msgid "No accounting entries for the following warehouses" msgstr "" @@ -30569,7 +30589,7 @@ msgstr "" msgid "No employee was scheduled for call popup" msgstr "" -#: erpnext/controllers/subcontracting_controller.py:1379 +#: erpnext/controllers/subcontracting_controller.py:1385 msgid "No item available for transfer." msgstr "" @@ -30594,7 +30614,7 @@ msgstr "" msgid "No matches occurred via auto reconciliation" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1036 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1041 msgid "No material request created" msgstr "" @@ -30627,6 +30647,12 @@ msgstr "" msgid "No of Interactions" msgstr "" +#. Label of the total_reposting_count (Int) field in DocType 'Repost Item +#. Valuation' +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +msgid "No of Items to Repost" +msgstr "" + #. Label of the no_of_months_exp (Int) field in DocType 'Item' #: erpnext/stock/doctype/item/item.json msgid "No of Months (Expense)" @@ -30806,7 +30832,7 @@ msgstr "" msgid "Non Profit" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1590 +#: erpnext/manufacturing/doctype/bom/bom.py:1593 msgid "Non stock items" msgstr "" @@ -31650,7 +31676,7 @@ msgstr "" msgid "Opening Entry can not be created after Period Closing Voucher is created." msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:286 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:299 msgid "Opening Invoice Creation In Progress" msgstr "" @@ -31669,7 +31695,7 @@ msgstr "" msgid "Opening Invoice Creation Tool Item" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:100 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:101 msgid "Opening Invoice Item" msgstr "" @@ -31687,7 +31713,7 @@ msgstr "" msgid "Opening Invoices" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js:140 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js:142 msgid "Opening Invoices Summary" msgstr "" @@ -31767,7 +31793,7 @@ msgstr "" msgid "Operating Cost Per BOM Quantity" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1677 +#: erpnext/manufacturing/doctype/bom/bom.py:1680 msgid "Operating Cost as per Work Order / BOM" msgstr "" @@ -31858,7 +31884,7 @@ msgstr "" msgid "Operation time does not depend on quantity to produce" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:578 +#: erpnext/manufacturing/doctype/job_card/job_card.js:586 msgid "Operation {0} added multiple times in the work order {1}" msgstr "" @@ -31892,7 +31918,7 @@ msgstr "" msgid "Operations Routing" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1192 +#: erpnext/manufacturing/doctype/bom/bom.py:1194 msgid "Operations cannot be left blank" msgstr "" @@ -31941,7 +31967,7 @@ msgstr "" #. Label of the opportunity_name (Link) field in DocType 'Customer' #. Label of the opportunity (Link) field in DocType 'Quotation' #. Label of a Workspace Sidebar Item -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:384 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:385 #: erpnext/buying/doctype/request_for_quotation/request_for_quotation.json #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.json #: erpnext/crm/doctype/crm_settings/crm_settings.json @@ -32457,7 +32483,7 @@ msgstr "" msgid "Over Billing Allowance (%)" msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:1317 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:1321 msgid "Over Billing Allowance exceeded for Purchase Receipt Item {0} ({1}) by {2}%" msgstr "" @@ -32560,7 +32586,7 @@ msgstr "" msgid "Overlap in scoring between {0} and {1}" msgstr "" -#: erpnext/accounts/doctype/shipping_rule/shipping_rule.py:199 +#: erpnext/accounts/doctype/shipping_rule/shipping_rule.py:201 msgid "Overlapping conditions found between:" msgstr "" @@ -33098,7 +33124,7 @@ msgstr "" msgid "Paid To Account Type" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:327 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:326 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1151 msgid "Paid amount + Write Off Amount can not be greater than Grand Total" msgstr "" @@ -33302,7 +33328,7 @@ msgstr "" msgid "Parsed file is not in valid MT940 format or contains no transactions." msgstr "" -#: erpnext/edi/doctype/code_list/code_list_import.py:39 +#: erpnext/edi/doctype/code_list/code_list_import.py:45 msgid "Parsing Error" msgstr "" @@ -33475,8 +33501,6 @@ msgstr "" #. Label of the party (Dynamic Link) field in DocType 'Journal Entry Account' #. Label of the party (Dynamic Link) field in DocType 'Journal Entry Template #. Account' -#. Label of the party (Dynamic Link) field in DocType 'Opening Invoice Creation -#. Tool Item' #. Label of the party (Dynamic Link) field in DocType 'Payment Entry' #. Label of the party (Dynamic Link) field in DocType 'Payment Ledger Entry' #. Label of the party (Dynamic Link) field in DocType 'Payment Reconciliation' @@ -33495,7 +33519,6 @@ msgstr "" #: erpnext/accounts/doctype/gl_entry/gl_entry.json #: erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json #: erpnext/accounts/doctype/journal_entry_template_account/journal_entry_template_account.json -#: erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json #: erpnext/accounts/doctype/payment_entry/payment_entry.json #: erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json #: erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json @@ -33567,7 +33590,7 @@ msgstr "" msgid "Party Account No. (Bank Statement)" msgstr "" -#: erpnext/controllers/accounts_controller.py:2452 +#: erpnext/controllers/accounts_controller.py:2462 msgid "Party Account {0} currency ({1}) and document currency ({2}) should be same" msgstr "" @@ -33595,6 +33618,12 @@ msgstr "" msgid "Party IBAN (Bank Statement)" msgstr "" +#. Label of the party (Dynamic Link) field in DocType 'Opening Invoice Creation +#. Tool Item' +#: erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json +msgid "Party ID" +msgstr "" + #. Label of the section_break_7 (Section Break) field in DocType 'Pricing Rule' #. Label of the section_break_8 (Section Break) field in DocType 'Promotional #. Scheme' @@ -33617,10 +33646,13 @@ msgstr "" msgid "Party Mismatch" msgstr "" +#. Label of the party_name (Data) field in DocType 'Opening Invoice Creation +#. Tool Item' #. Label of the party_name (Data) field in DocType 'Payment Entry' #. Label of the party_name (Data) field in DocType 'Payment Request' #. Label of the party_name (Dynamic Link) field in DocType 'Contract' #. Label of the party (Dynamic Link) field in DocType 'Party Specific Item' +#: erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json #: erpnext/accounts/doctype/payment_entry/payment_entry.json #: erpnext/accounts/doctype/payment_request/payment_request.json #: erpnext/accounts/report/general_ledger/general_ledger.js:110 @@ -33780,7 +33812,7 @@ msgstr "" msgid "Pause" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:255 +#: erpnext/manufacturing/doctype/job_card/job_card.js:263 msgid "Pause Job" msgstr "" @@ -34274,7 +34306,7 @@ msgstr "" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/buying/doctype/purchase_order/purchase_order.json -#: erpnext/controllers/accounts_controller.py:2732 +#: erpnext/controllers/accounts_controller.py:2742 #: erpnext/selling/doctype/quotation/quotation.json #: erpnext/selling/doctype/sales_order/sales_order.json msgid "Payment Schedule" @@ -34902,7 +34934,7 @@ msgstr "" #. Label of the phone_no (Data) field in DocType 'Company' #. Label of the phone_no (Data) field in DocType 'Warehouse' -#: erpnext/public/js/print.js:77 erpnext/setup/doctype/company/company.json +#: erpnext/public/js/print.js:79 erpnext/setup/doctype/company/company.json #: erpnext/stock/doctype/warehouse/warehouse.json msgid "Phone No" msgstr "" @@ -35278,7 +35310,7 @@ msgstr "" msgid "Please Set Priority" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:155 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:166 msgid "Please Set Supplier Group in Buying Settings." msgstr "" @@ -35298,7 +35330,7 @@ msgstr "" msgid "Please add Operations first." msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:210 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:212 msgid "Please add Request for Quotation to the sidebar in Portal Settings." msgstr "" @@ -35306,7 +35338,7 @@ msgstr "" msgid "Please add Root Account for - {0}" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:302 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:315 msgid "Please add a Temporary Opening account in Chart of Accounts" msgstr "" @@ -35372,7 +35404,7 @@ msgstr "" msgid "Please check the 'Enable Serial and Batch No for Item' checkbox in the {0} to make Serial and Batch Bundle for the item." msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:539 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:541 msgid "Please check the error message and take necessary actions to fix the error and then restart the reposting again." msgstr "" @@ -35437,7 +35469,7 @@ msgstr "" msgid "Please delete Product Bundle {0}, before merging {1} into {2}" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:556 +#: erpnext/assets/doctype/asset/depreciation.py:559 msgid "Please disable workflow temporarily for Journal Entry {0}" msgstr "" @@ -35473,11 +35505,11 @@ msgstr "" msgid "Please enable {} in {} to allow same item in multiple rows" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:377 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:376 msgid "Please ensure that the {0} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:385 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:384 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 "" @@ -35543,10 +35575,6 @@ msgstr "" msgid "Please enter Planned Qty for Item {0} at row {1}" msgstr "" -#: erpnext/setup/doctype/employee/employee.js:83 -msgid "Please enter Preferred Contact Email" -msgstr "" - #: erpnext/manufacturing/doctype/work_order/work_order.js:73 msgid "Please enter Production Item first" msgstr "" @@ -35604,7 +35632,7 @@ msgstr "" msgid "Please enter company name first" msgstr "" -#: erpnext/controllers/accounts_controller.py:2958 +#: erpnext/controllers/accounts_controller.py:2968 msgid "Please enter default currency in Company Master" msgstr "" @@ -35624,7 +35652,7 @@ msgstr "" msgid "Please enter quantity for item {0}" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:183 +#: erpnext/setup/doctype/employee/employee.py:297 msgid "Please enter relieving date." msgstr "" @@ -35644,7 +35672,7 @@ msgstr "" msgid "Please enter the phone number first" msgstr "" -#: erpnext/controllers/buying_controller.py:1184 +#: erpnext/controllers/buying_controller.py:1185 msgid "Please enter the {schedule_date}." msgstr "" @@ -35652,7 +35680,7 @@ msgstr "" msgid "Please enter valid Financial Year Start and End Dates" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:219 +#: erpnext/setup/doctype/employee/employee.py:333 msgid "Please enter {0}" msgstr "" @@ -35692,7 +35720,7 @@ msgstr "" msgid "Please import accounts against parent company or enable {} in company master." msgstr "" -#: erpnext/setup/doctype/employee/employee.py:180 +#: erpnext/setup/doctype/employee/employee.py:294 msgid "Please make sure the employees above report to another Active employee." msgstr "" @@ -35847,7 +35875,7 @@ msgstr "" msgid "Please select Posting Date first" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1243 +#: erpnext/manufacturing/doctype/bom/bom.py:1245 msgid "Please select Price List" msgstr "" @@ -35875,11 +35903,11 @@ msgstr "" msgid "Please select Subcontracting Order instead of Purchase Order {0}" msgstr "" -#: erpnext/controllers/accounts_controller.py:2807 +#: erpnext/controllers/accounts_controller.py:2817 msgid "Please select Unrealized Profit / Loss account or add default Unrealized Profit / Loss account account for company {0}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1498 +#: erpnext/manufacturing/doctype/bom/bom.py:1500 msgid "Please select a BOM" msgstr "" @@ -36035,7 +36063,7 @@ msgstr "" msgid "Please select rows to create Reposting Entries" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:92 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:93 msgid "Please select the Company" msgstr "" @@ -36079,11 +36107,11 @@ msgstr "" msgid "Please set 'Apply Additional Discount On'" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:783 +#: erpnext/assets/doctype/asset/depreciation.py:786 msgid "Please set 'Asset Depreciation Cost Center' in Company {0}" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:781 +#: erpnext/assets/doctype/asset/depreciation.py:784 msgid "Please set 'Gain/Loss Account on Asset Disposal' in Company {0}" msgstr "" @@ -36125,7 +36153,7 @@ msgstr "" msgid "Please set Customer Address to determine if the transaction is an export." msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:745 +#: erpnext/assets/doctype/asset/depreciation.py:748 msgid "Please set Depreciation related Accounts in Asset Category {0} or Company {1}" msgstr "" @@ -36143,11 +36171,11 @@ msgstr "" msgid "Please set Fiscal Code for the public administration '%s'" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:731 +#: erpnext/assets/doctype/asset/depreciation.py:734 msgid "Please set Fixed Asset Account in Asset Category {0}" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:594 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:593 msgid "Please set Fixed Asset Account in {} against {}." msgstr "" @@ -36193,7 +36221,7 @@ msgstr "" msgid "Please set a default Holiday List for Company {0}" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:270 +#: erpnext/setup/doctype/employee/employee.py:384 msgid "Please set a default Holiday List for Employee {0} or Company {1}" msgstr "" @@ -36271,7 +36299,7 @@ msgstr "" msgid "Please set filter based on Item or Warehouse" msgstr "" -#: erpnext/controllers/accounts_controller.py:2368 +#: erpnext/controllers/accounts_controller.py:2378 msgid "Please set one of the following:" msgstr "" @@ -36287,7 +36315,7 @@ msgstr "" msgid "Please set the Customer Address" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:170 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:182 msgid "Please set the Default Cost Center in {0} company." msgstr "" @@ -36346,7 +36374,7 @@ msgstr "" msgid "Please setup and enable a group account with the Account Type - {0} for the company {1}" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:354 +#: erpnext/assets/doctype/asset/depreciation.py:357 msgid "Please share this email with your support team so that they can find and fix the issue." msgstr "" @@ -36360,7 +36388,7 @@ msgstr "" msgid "Please specify Company to proceed" msgstr "" -#: erpnext/controllers/accounts_controller.py:3189 +#: erpnext/controllers/accounts_controller.py:3199 #: erpnext/public/js/controllers/accounts.js:117 msgid "Please specify a valid Row ID for row {0} in table {1}" msgstr "" @@ -36436,7 +36464,7 @@ msgstr "" msgid "Portal Users" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:406 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:407 msgid "Possible Supplier" msgstr "" @@ -36605,7 +36633,7 @@ msgstr "" msgid "Posting Date Inheritance for Exchange Gain / Loss" msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:269 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:270 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:142 msgid "Posting Date cannot be future date" msgstr "" @@ -36814,7 +36842,7 @@ msgid "Preventive Maintenance" msgstr "" #. Label of the preview (Button) field in DocType 'Request for Quotation' -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:266 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:267 #: erpnext/buying/doctype/request_for_quotation/request_for_quotation.json msgid "Preview Email" msgstr "" @@ -37449,7 +37477,7 @@ msgstr "" msgid "Process Loss" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1226 +#: erpnext/manufacturing/doctype/bom/bom.py:1228 msgid "Process Loss Percentage cannot be greater than 100" msgstr "" @@ -37471,7 +37499,7 @@ msgstr "" msgid "Process Loss Qty" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:332 +#: erpnext/manufacturing/doctype/job_card/job_card.js:340 msgid "Process Loss Quantity" msgstr "" @@ -38050,7 +38078,7 @@ msgstr "" msgid "Project wise Stock Tracking " msgstr "" -#: erpnext/controllers/trends.py:421 +#: erpnext/controllers/trends.py:429 msgid "Project-wise data is not available for Quotation" msgstr "" @@ -38328,7 +38356,7 @@ msgstr "" #: erpnext/accounts/doctype/tax_rule/tax_rule.json #: erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json #: erpnext/projects/doctype/project/project_dashboard.py:16 -#: erpnext/setup/doctype/company/company.py:463 erpnext/setup/install.py:377 +#: erpnext/setup/doctype/company/company.py:463 erpnext/setup/install.py:378 #: erpnext/stock/doctype/item/item.json #: erpnext/stock/doctype/item_lead_time/item_lead_time.json #: erpnext/stock/doctype/item_reorder/item_reorder.json @@ -38483,8 +38511,8 @@ msgstr "" msgid "Purchase Invoice cannot be made against an existing asset {0}" msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:449 -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:463 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:453 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:467 msgid "Purchase Invoice {0} is already submitted" msgstr "" @@ -38637,7 +38665,7 @@ msgstr "" msgid "Purchase Order already created for all Sales Order items" msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:335 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:339 msgid "Purchase Order number required for Item {0}" msgstr "" @@ -38664,7 +38692,7 @@ msgstr "" msgid "Purchase Orders Items Overdue" msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:286 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:285 msgid "Purchase Orders are not allowed for {0} due to a scorecard standing of {1}." msgstr "" @@ -38981,8 +39009,8 @@ msgstr "" #: erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.json #: erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py:240 #: erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py:224 -#: erpnext/controllers/trends.py:268 erpnext/controllers/trends.py:280 -#: erpnext/controllers/trends.py:285 +#: erpnext/controllers/trends.py:276 erpnext/controllers/trends.py:288 +#: erpnext/controllers/trends.py:293 #: erpnext/crm/doctype/opportunity_item/opportunity_item.json #: erpnext/manufacturing/doctype/bom/bom.js:1086 #: erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json @@ -39098,7 +39126,8 @@ msgstr "" msgid "Qty In Stock" msgstr "" -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py:82 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:117 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:174 msgid "Qty Per Unit" msgstr "" @@ -39217,7 +39246,7 @@ msgstr "" msgid "Qty to Fetch" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:304 +#: erpnext/manufacturing/doctype/job_card/job_card.js:312 #: erpnext/manufacturing/doctype/job_card/job_card.py:871 msgid "Qty to Manufacture" msgstr "" @@ -39728,7 +39757,11 @@ msgid "Quantity is required" msgstr "" #: erpnext/stock/dashboard/item_dashboard.js:285 -msgid "Quantity must be greater than zero, and less or equal to {0}" +msgid "Quantity must be greater than zero" +msgstr "" + +#: erpnext/stock/dashboard/item_dashboard.js:290 +msgid "Quantity must be less than or equal to {0}" msgstr "" #: erpnext/manufacturing/doctype/work_order/work_order.js:1037 @@ -39746,16 +39779,12 @@ msgid "Quantity required for Item {0} in row {1}" msgstr "" #: erpnext/manufacturing/doctype/bom/bom.py:678 -#: erpnext/manufacturing/doctype/job_card/job_card.js:385 -#: erpnext/manufacturing/doctype/job_card/job_card.js:455 +#: erpnext/manufacturing/doctype/job_card/job_card.js:393 +#: erpnext/manufacturing/doctype/job_card/job_card.js:463 #: erpnext/manufacturing/doctype/workstation/workstation.js:303 msgid "Quantity should be greater than 0" msgstr "" -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js:21 -msgid "Quantity to Make" -msgstr "" - #: erpnext/manufacturing/doctype/work_order/work_order.js:343 msgid "Quantity to Manufacture" msgstr "" @@ -39768,14 +39797,6 @@ msgstr "" msgid "Quantity to Manufacture must be greater than 0." msgstr "" -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js:24 -msgid "Quantity to Produce" -msgstr "" - -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py:37 -msgid "Quantity to Produce should be greater than zero." -msgstr "" - #: erpnext/public/js/utils/barcode_scanner.js:257 msgid "Quantity to Scan" msgstr "" @@ -39950,7 +39971,7 @@ msgstr "" msgid "RFQ and Purchase Order Settings" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:129 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:131 msgid "RFQs are not allowed for {0} due to a scorecard standing of {1}" msgstr "" @@ -40227,7 +40248,7 @@ msgstr "" msgid "Rate at which this tax is applied" msgstr "" -#: erpnext/controllers/accounts_controller.py:4064 +#: erpnext/controllers/accounts_controller.py:4074 msgid "Rate of '{}' items cannot be changed" msgstr "" @@ -41281,7 +41302,7 @@ msgstr "" msgid "Release Date" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:318 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:317 msgid "Release date must be in the future" msgstr "" @@ -41623,7 +41644,7 @@ msgstr "" msgid "Repost Item Valuation" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:344 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:346 msgid "Repost Item Valuation restarted for selected failed records." msgstr "" @@ -41663,10 +41684,6 @@ msgstr "" msgid "Repost started in the background" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:119 -msgid "Reposting Completed {0}%" -msgstr "" - #. Label of the reposting_data_file (Attach) field in DocType 'Repost Item #. Valuation' #: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json @@ -41676,10 +41693,10 @@ msgstr "" #. Label of the reposting_info_section (Section Break) field in DocType 'Repost #. Item Valuation' #: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json -msgid "Reposting Info" +msgid "Reposting Item and Warehouse" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:127 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:131 msgid "Reposting Progress" msgstr "" @@ -41689,12 +41706,30 @@ msgstr "" msgid "Reposting Reference" msgstr "" +#. Label of the vouchers_based_on_item_and_warehouse_section (Section Break) +#. field in DocType 'Repost Item Valuation' +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +msgid "Reposting Vouchers" +msgstr "" + +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:149 +msgid "Reposting Vouchers Progress" +msgstr "" + #: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:216 #: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:327 msgid "Reposting entries created: {0}" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:103 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:123 +msgid "Reposting for Item-Wh Completed {0}%" +msgstr "" + +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:141 +msgid "Reposting for Vouchers Completed {0}%" +msgstr "" + +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:109 msgid "Reposting has been started in the background." msgstr "" @@ -41781,8 +41816,8 @@ msgstr "" #. Label of a Workspace Sidebar Item #: erpnext/buying/doctype/buying_settings/buying_settings.json #: erpnext/buying/doctype/request_for_quotation/request_for_quotation.json -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:324 -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:426 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:326 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:428 #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.js:88 #: erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json #: erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js:70 @@ -41929,10 +41964,7 @@ msgstr "" #: erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json #: erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json #: erpnext/manufacturing/doctype/work_order_item/work_order_item.json -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py:94 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html:11 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html:21 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py:28 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:119 #: erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py:58 #: erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.py:1057 #: erpnext/manufacturing/report/production_planning_report/production_planning_report.py:426 @@ -42129,7 +42161,7 @@ msgstr "" msgid "Reserved Quantity for Production" msgstr "" -#: erpnext/stock/stock_ledger.py:2287 +#: erpnext/stock/stock_ledger.py:2334 msgid "Reserved Serial No." msgstr "" @@ -42145,13 +42177,13 @@ msgstr "" #: erpnext/stock/doctype/pick_list/pick_list.js:170 #: erpnext/stock/report/reserved_stock/reserved_stock.json #: erpnext/stock/report/stock_balance/stock_balance.py:572 -#: erpnext/stock/stock_ledger.py:2271 +#: erpnext/stock/stock_ledger.py:2318 #: 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:2316 +#: erpnext/stock/stock_ledger.py:2363 msgid "Reserved Stock for Batch" msgstr "" @@ -42352,7 +42384,7 @@ msgstr "" msgid "Rest Of The World" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:84 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:90 msgid "Restart" msgstr "" @@ -42417,7 +42449,7 @@ msgstr "" msgid "Resume" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:239 +#: erpnext/manufacturing/doctype/job_card/job_card.js:247 msgid "Resume Job" msgstr "" @@ -43143,7 +43175,7 @@ msgstr "" msgid "Row #{0}: Asset {1} is already sold" msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:334 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:333 msgid "Row #{0}: BOM is not specified for subcontracting item {0}" msgstr "" @@ -43179,27 +43211,27 @@ msgstr "" msgid "Row #{0}: Cannot create entry with different taxable AND withholding document links." msgstr "" -#: erpnext/controllers/accounts_controller.py:3767 +#: erpnext/controllers/accounts_controller.py:3777 msgid "Row #{0}: Cannot delete item {1} which has already been billed." msgstr "" -#: erpnext/controllers/accounts_controller.py:3741 +#: erpnext/controllers/accounts_controller.py:3751 msgid "Row #{0}: Cannot delete item {1} which has already been delivered" msgstr "" -#: erpnext/controllers/accounts_controller.py:3760 +#: erpnext/controllers/accounts_controller.py:3770 msgid "Row #{0}: Cannot delete item {1} which has already been received" msgstr "" -#: erpnext/controllers/accounts_controller.py:3747 +#: erpnext/controllers/accounts_controller.py:3757 msgid "Row #{0}: Cannot delete item {1} which has work order assigned to it." msgstr "" -#: erpnext/controllers/accounts_controller.py:3753 +#: erpnext/controllers/accounts_controller.py:3763 msgid "Row #{0}: Cannot delete item {1} which is already ordered against this Sales Order." msgstr "" -#: erpnext/controllers/accounts_controller.py:4074 +#: erpnext/controllers/accounts_controller.py:4084 msgid "Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}." msgstr "" @@ -43282,7 +43314,7 @@ msgstr "" msgid "Row #{0}: Dates overlapping with other row in group {1}" msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:358 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:357 msgid "Row #{0}: Default BOM not found for FG Item {1}" msgstr "" @@ -43306,17 +43338,17 @@ msgstr "" msgid "Row #{0}: Expense account {1} is not valid for Purchase Invoice {2}. Only expense accounts from non-stock items are allowed." msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:363 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:362 #: erpnext/selling/doctype/sales_order/sales_order.py:303 msgid "Row #{0}: Finished Good Item Qty can not be zero" msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:345 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:344 #: erpnext/selling/doctype/sales_order/sales_order.py:283 msgid "Row #{0}: Finished Good Item is not specified for service item {1}" msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:352 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:351 #: erpnext/selling/doctype/sales_order/sales_order.py:290 msgid "Row #{0}: Finished Good Item {1} must be a sub-contracted item" msgstr "" @@ -43444,11 +43476,11 @@ msgstr "" msgid "Row #{0}: Overconsumption of Customer Provided Item {1} against Work Order {2} is not allowed in the Subcontracting Inward process." msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1051 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1056 msgid "Row #{0}: Please select Item Code in Assembly Items" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1054 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1059 msgid "Row #{0}: Please select the BOM No in Assembly Items" msgstr "" @@ -43456,7 +43488,7 @@ msgstr "" msgid "Row #{0}: Please select the Finished Good Item against which this Customer Provided Item will be used." msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1048 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1053 msgid "Row #{0}: Please select the Sub Assembly Warehouse" msgstr "" @@ -43596,7 +43628,7 @@ msgstr "" msgid "Row #{0}: Set Supplier for item {1}" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1058 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1063 msgid "Row #{0}: Since 'Track Semi Finished Goods' is enabled, the BOM {1} cannot be used for Sub Assembly Items" msgstr "" @@ -43717,7 +43749,7 @@ msgstr "" msgid "Row #{0}: {1} is not a valid reading field. Please refer to the field description." msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:115 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:126 msgid "Row #{0}: {1} is required to create the Opening {2} Invoices" msgstr "" @@ -43725,7 +43757,7 @@ msgstr "" msgid "Row #{0}: {1} of {2} should be {3}. Please update the {1} or select a different account." msgstr "" -#: erpnext/controllers/accounts_controller.py:3881 +#: erpnext/controllers/accounts_controller.py:3891 msgid "Row #{0}:Quantity for Item {1} cannot be zero." msgstr "" @@ -43761,7 +43793,7 @@ msgstr "" msgid "Row #{idx}: {from_warehouse_field} and {to_warehouse_field} cannot be same." msgstr "" -#: erpnext/controllers/buying_controller.py:1176 +#: erpnext/controllers/buying_controller.py:1177 msgid "Row #{idx}: {schedule_date} cannot be before {transaction_date}." msgstr "" @@ -43769,6 +43801,10 @@ msgstr "" msgid "Row #{}: Currency of {} - {} doesn't matches company currency." msgstr "" +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:108 +msgid "Row #{}: Either Party ID or Party Name is required" +msgstr "" + #: erpnext/assets/doctype/asset/asset.py:421 msgid "Row #{}: Finance Book should not be empty since you're using multiple." msgstr "" @@ -43785,6 +43821,10 @@ msgstr "" msgid "Row #{}: POS Invoice {} is not submitted yet" msgstr "" +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:118 +msgid "Row #{}: Party ID is required" +msgstr "" + #: erpnext/assets/doctype/asset_maintenance/asset_maintenance.py:41 msgid "Row #{}: Please assign task to a member." msgstr "" @@ -43814,7 +43854,7 @@ msgstr "" msgid "Row #{}: {}" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:110 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:121 msgid "Row #{}: {} {} does not exist." msgstr "" @@ -43822,7 +43862,7 @@ msgstr "" msgid "Row #{}: {} {} doesn't belong to Company {}. Please select valid {}." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:444 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:443 msgid "Row No {0}: Warehouse is required. Please set a Default Warehouse for Item {1} and Company {2}" msgstr "" @@ -43888,7 +43928,7 @@ msgstr "" msgid "Row {0}: Conversion Factor is mandatory" msgstr "" -#: erpnext/controllers/accounts_controller.py:3227 +#: erpnext/controllers/accounts_controller.py:3237 msgid "Row {0}: Cost Center {1} does not belong to Company {2}" msgstr "" @@ -43916,7 +43956,7 @@ msgstr "" msgid "Row {0}: Delivery Warehouse cannot be same as Customer Warehouse for Item {1}." msgstr "" -#: erpnext/controllers/accounts_controller.py:2720 +#: erpnext/controllers/accounts_controller.py:2730 msgid "Row {0}: Due Date in the Payment Terms table cannot be before Posting Date" msgstr "" @@ -43941,19 +43981,19 @@ msgstr "" msgid "Row {0}: Expense Account {1} is linked to company {2}. Please select an account belonging to company {3}." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:534 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:533 msgid "Row {0}: Expense Head changed to {1} as no Purchase Receipt is created against Item {2}." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:491 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:490 msgid "Row {0}: Expense Head changed to {1} because account {2} is not linked to warehouse {3} or it is not the default inventory account" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:516 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:515 msgid "Row {0}: Expense Head changed to {1} because expense is booked against this account in Purchase Receipt {2}" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:152 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:154 msgid "Row {0}: For Supplier {1}, Email Address is Required to send an email" msgstr "" @@ -44006,7 +44046,7 @@ msgstr "" msgid "Row {0}: Item {1}'s quantity cannot be higher than the available quantity." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1209 +#: erpnext/manufacturing/doctype/bom/bom.py:1211 msgid "Row {0}: Operation time should be greater than 0 for operation {1}" msgstr "" @@ -44122,7 +44162,7 @@ msgstr "" msgid "Row {0}: The item {1}, quantity must be positive number" msgstr "" -#: erpnext/controllers/accounts_controller.py:3204 +#: erpnext/controllers/accounts_controller.py:3214 msgid "Row {0}: The {3} Account {1} does not belong to the company {2}" msgstr "" @@ -44146,7 +44186,7 @@ msgstr "" msgid "Row {0}: Warehouse {1} is linked to company {2}. Please select a warehouse belonging to company {3}." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1203 +#: erpnext/manufacturing/doctype/bom/bom.py:1205 #: erpnext/manufacturing/doctype/work_order/work_order.py:415 msgid "Row {0}: Workstation or Workstation Type is mandatory for an operation {1}" msgstr "" @@ -44179,7 +44219,7 @@ msgstr "" msgid "Row {0}: {2} Item {1} does not exist in {2} {3}" msgstr "" -#: erpnext/utilities/transaction_base.py:562 +#: erpnext/utilities/transaction_base.py:563 msgid "Row {1}: Quantity ({0}) cannot be a fraction. To allow this, disable '{2}' in UOM {3}." msgstr "" @@ -44209,7 +44249,7 @@ msgstr "" msgid "Rows with Same Account heads will be merged on Ledger" msgstr "" -#: erpnext/controllers/accounts_controller.py:2731 +#: erpnext/controllers/accounts_controller.py:2741 msgid "Rows with duplicate due dates in other rows were found: {0}" msgstr "" @@ -44391,7 +44431,7 @@ msgstr "" #: erpnext/setup/doctype/company/company.py:649 #: erpnext/setup/doctype/company/company_dashboard.py:9 #: erpnext/setup/doctype/sales_person/sales_person_dashboard.py:12 -#: erpnext/setup/install.py:372 +#: erpnext/setup/install.py:373 #: erpnext/setup/setup_wizard/operations/install_fixtures.py:297 #: erpnext/stock/doctype/item/item.json #: erpnext/stock/doctype/pick_list/pick_list_dashboard.py:16 @@ -45215,7 +45255,7 @@ msgstr "" msgid "Same item cannot be entered multiple times." msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:121 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:123 msgid "Same supplier has been entered multiple times" msgstr "" @@ -45505,7 +45545,7 @@ msgstr "" msgid "Scrap Warehouse" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:384 +#: erpnext/assets/doctype/asset/depreciation.py:387 msgid "Scrap date cannot be before purchase date" msgstr "" @@ -45653,11 +45693,11 @@ msgstr "" msgid "Select Company" msgstr "" -#: erpnext/public/js/print.js:113 +#: erpnext/public/js/print.js:115 msgid "Select Company Address" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:533 +#: erpnext/manufacturing/doctype/job_card/job_card.js:541 msgid "Select Corrective Operation" msgstr "" @@ -45667,11 +45707,11 @@ msgstr "" msgid "Select Customers By" msgstr "" -#: erpnext/setup/doctype/employee/employee.js:120 +#: erpnext/setup/doctype/employee/employee.js:160 msgid "Select Date of Birth. This will validate Employees age and prevent hiring of under-age staff." msgstr "" -#: erpnext/setup/doctype/employee/employee.js:127 +#: erpnext/setup/doctype/employee/employee.js:167 msgid "Select Date of joining. It will have impact on the first salary calculation, Leave allocation on pro-rata bases." msgstr "" @@ -45693,7 +45733,7 @@ msgstr "" msgid "Select Dispatch Address " msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:221 +#: erpnext/manufacturing/doctype/job_card/job_card.js:229 msgid "Select Employees" msgstr "" @@ -45752,7 +45792,7 @@ msgstr "" msgid "Select Payment Schedule" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:410 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:411 msgid "Select Possible Supplier" msgstr "" @@ -45815,7 +45855,7 @@ msgstr "" msgid "Select a Company" msgstr "" -#: erpnext/setup/doctype/employee/employee.js:115 +#: erpnext/setup/doctype/employee/employee.js:155 msgid "Select a Company this Employee belongs to." msgstr "" @@ -45869,7 +45909,7 @@ msgstr "" msgid "Select company name first." msgstr "" -#: erpnext/controllers/accounts_controller.py:2979 +#: erpnext/controllers/accounts_controller.py:2989 msgid "Select finance book for the item {0} at row {1}" msgstr "" @@ -46449,7 +46489,7 @@ msgstr "" msgid "Serial Nos are created successfully" msgstr "" -#: erpnext/stock/stock_ledger.py:2277 +#: erpnext/stock/stock_ledger.py:2324 msgid "Serial Nos are reserved in Stock Reservation Entries, you need to unreserve them before proceeding." msgstr "" @@ -46744,6 +46784,7 @@ msgstr "" #: erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.json #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json #: erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +#: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py:405 msgid "Service End Date" msgstr "" @@ -46887,6 +46928,7 @@ msgstr "" #: erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.json #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json #: erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +#: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py:397 msgid "Service Start Date" msgstr "" @@ -46945,8 +46987,8 @@ msgstr "" msgid "Set Delivery Warehouse" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:404 -#: erpnext/manufacturing/doctype/job_card/job_card.js:473 +#: erpnext/manufacturing/doctype/job_card/job_card.js:412 +#: erpnext/manufacturing/doctype/job_card/job_card.js:481 msgid "Set Finished Good Quantity" msgstr "" @@ -47246,7 +47288,7 @@ msgstr "" msgid "Setting up company" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1182 +#: erpnext/manufacturing/doctype/bom/bom.py:1184 #: erpnext/manufacturing/doctype/work_order/work_order.py:1475 msgid "Setting {0} is required" msgstr "" @@ -47471,10 +47513,13 @@ msgstr "" #. Label of the shipping_address_display (Text Editor) field in DocType #. 'Purchase Order' #. Label of the shipping_address_display (Text Editor) field in DocType +#. 'Request for Quotation' +#. Label of the shipping_address_display (Text Editor) field in DocType #. 'Supplier Quotation' #. Label of the shipping_address_display (Text Editor) field in DocType #. 'Subcontracting Order' #: erpnext/buying/doctype/purchase_order/purchase_order.json +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.json #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.json #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json msgid "Shipping Address Details" @@ -47829,9 +47874,8 @@ msgstr "" msgid "Show Warehouse-wise Stock" msgstr "" -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js:28 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js:19 -msgid "Show exploded view" +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js:26 +msgid "Show availability of exploded items" msgstr "" #: erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.js:88 @@ -48061,7 +48105,7 @@ msgstr "" msgid "Solvency Ratios" msgstr "" -#: erpnext/controllers/accounts_controller.py:4332 +#: erpnext/controllers/accounts_controller.py:4344 msgid "Some required Company details are missing. You don't have permission to update them. Please contact your System Manager." msgstr "" @@ -48191,7 +48235,7 @@ msgstr "" msgid "Source and target warehouse cannot be same for row {0}" msgstr "" -#: erpnext/stock/dashboard/item_dashboard.js:290 +#: erpnext/stock/dashboard/item_dashboard.js:295 msgid "Source and target warehouse must be different" msgstr "" @@ -48344,7 +48388,7 @@ msgstr "" #: erpnext/setup/setup_wizard/operations/defaults_setup.py:70 #: erpnext/setup/setup_wizard/operations/install_fixtures.py:485 -#: erpnext/tests/utils.py:297 +#: erpnext/tests/utils.py:316 msgid "Standard Buying" msgstr "" @@ -48358,8 +48402,8 @@ msgstr "" #: erpnext/setup/setup_wizard/operations/defaults_setup.py:70 #: erpnext/setup/setup_wizard/operations/install_fixtures.py:493 -#: erpnext/stock/doctype/item/item.py:267 erpnext/tests/utils.py:305 -#: erpnext/tests/utils.py:2494 +#: erpnext/stock/doctype/item/item.py:267 erpnext/tests/utils.py:324 +#: erpnext/tests/utils.py:2514 msgid "Standard Selling" msgstr "" @@ -48415,7 +48459,7 @@ msgstr "" msgid "Start Date should be lower than End Date" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:215 +#: erpnext/manufacturing/doctype/job_card/job_card.js:223 #: erpnext/manufacturing/doctype/workstation/workstation.js:124 msgid "Start Job" msgstr "" @@ -48424,7 +48468,7 @@ msgstr "" msgid "Start Merge" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:99 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:105 msgid "Start Reposting" msgstr "" @@ -48991,7 +49035,7 @@ msgid "Stock Reservation Entries Cancelled" msgstr "" #: erpnext/controllers/subcontracting_inward_controller.py:1003 -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:2238 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:2243 #: erpnext/manufacturing/doctype/work_order/work_order.py:2124 #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1777 msgid "Stock Reservation Entries Created" @@ -49365,7 +49409,7 @@ msgstr "" #: erpnext/setup/doctype/company/company.py:384 #: erpnext/setup/setup_wizard/operations/defaults_setup.py:33 #: erpnext/setup/setup_wizard/operations/install_fixtures.py:537 -#: erpnext/stock/doctype/item/item.py:304 erpnext/tests/utils.py:270 +#: erpnext/stock/doctype/item/item.py:304 erpnext/tests/utils.py:289 msgid "Stores" msgstr "" @@ -49423,7 +49467,7 @@ msgstr "" #. Label of the operation (Link) field in DocType 'Job Card Time Log' #. Name of a DocType -#: erpnext/manufacturing/doctype/job_card/job_card.js:349 +#: erpnext/manufacturing/doctype/job_card/job_card.js:357 #: erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json #: erpnext/manufacturing/doctype/sub_operation/sub_operation.json msgid "Sub Operation" @@ -49447,7 +49491,7 @@ msgstr "" msgid "Sub Total" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:621 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:626 msgid "Sub assembly item references are missing. Please fetch the sub assemblies and raw materials again." msgstr "" @@ -49661,7 +49705,7 @@ msgstr "" #. Receipt Supplied Item' #. Label of a Workspace Sidebar Item #: erpnext/buying/doctype/purchase_order/purchase_order.js:399 -#: erpnext/controllers/subcontracting_controller.py:1167 +#: erpnext/controllers/subcontracting_controller.py:1173 #: erpnext/manufacturing/workspace/manufacturing/manufacturing.json #: erpnext/stock/doctype/stock_entry/stock_entry.json #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json @@ -49813,7 +49857,7 @@ msgstr "" msgid "Submit this Work Order for further processing." msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:306 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:308 msgid "Submit your Quotation" msgstr "" @@ -50122,8 +50166,8 @@ msgstr "" #: erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js:37 #: erpnext/assets/doctype/asset/asset.json #: erpnext/buying/doctype/purchase_order/purchase_order.json -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:184 -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:269 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:185 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:270 #: erpnext/buying/doctype/request_for_quotation/request_for_quotation.json #: erpnext/buying/doctype/request_for_quotation_supplier/request_for_quotation_supplier.json #: erpnext/buying/doctype/supplier/supplier.json @@ -50256,7 +50300,7 @@ msgstr "" #: erpnext/accounts/report/purchase_register/purchase_register.py:186 #: erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js:55 #: erpnext/buying/doctype/purchase_order/purchase_order.json -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:502 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:503 #: erpnext/buying/doctype/supplier/supplier.json #: erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.py:105 #: erpnext/buying/workspace/buying/buying.json @@ -50455,7 +50499,7 @@ msgstr "" #. Name of a report #. Label of a Link in the Buying Workspace #. Label of a Workspace Sidebar Item -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:154 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:155 #: erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.json #: erpnext/buying/workspace/buying/buying.json #: erpnext/workspace_sidebar/buying.json @@ -50470,7 +50514,7 @@ msgstr "" msgid "Supplier Quotation Item" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:495 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:497 msgid "Supplier Quotation {0} Created" msgstr "" @@ -50559,7 +50603,7 @@ msgstr "" #. Label of the supplier_warehouse (Link) field in DocType 'Purchase Receipt' #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/buying/doctype/purchase_order/purchase_order.json -#: erpnext/manufacturing/doctype/job_card/job_card.js:89 +#: erpnext/manufacturing/doctype/job_card/job_card.js:97 #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.json msgid "Supplier Warehouse" msgstr "" @@ -51920,7 +51964,7 @@ msgstr "" msgid "The 'From Package No.' field must neither be empty nor it's value less than 1." msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:411 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:413 msgid "The Access to Request for Quotation From Portal is Disabled. To Allow Access, Enable it in Portal Settings." msgstr "" @@ -51961,7 +52005,7 @@ msgstr "" msgid "The Loyalty Program isn't valid for the selected company" msgstr "" -#: erpnext/accounts/doctype/payment_request/payment_request.py:1106 +#: erpnext/accounts/doctype/payment_request/payment_request.py:1107 msgid "The Payment Request {0} is already paid, cannot process payment twice" msgstr "" @@ -52003,7 +52047,7 @@ 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:1001 +#: erpnext/accounts/doctype/payment_request/payment_request.py:1002 msgid "The allocated amount is greater than the outstanding amount of Payment Request {0}" msgstr "" @@ -52076,7 +52120,7 @@ msgstr "" msgid "The following Purchase Invoices are not submitted:" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:344 +#: erpnext/assets/doctype/asset/depreciation.py:347 msgid "The following assets have failed to automatically post depreciation entries: {0}" msgstr "" @@ -52092,7 +52136,7 @@ msgstr "" msgid "The following deleted attributes exist in Variants but not in the Template. You can either delete the Variants or keep the attribute(s) in template." msgstr "" -#: erpnext/setup/doctype/employee/employee.py:175 +#: erpnext/setup/doctype/employee/employee.py:289 msgid "The following employees are currently still reporting to {0}:" msgstr "" @@ -52123,7 +52167,7 @@ msgstr "" msgid "The holiday on {0} is not between From Date and To Date" msgstr "" -#: erpnext/controllers/buying_controller.py:1243 +#: erpnext/controllers/buying_controller.py:1244 msgid "The item {item} is not marked as {type_of} item. You can enable it as {type_of} item from its Item master." msgstr "" @@ -52131,7 +52175,7 @@ msgstr "" msgid "The items {0} and {1} are present in the following {2} :" msgstr "" -#: erpnext/controllers/buying_controller.py:1236 +#: erpnext/controllers/buying_controller.py:1237 msgid "The items {items} are not marked as {type_of} item. You can enable them as {type_of} item from their Item masters." msgstr "" @@ -52266,7 +52310,7 @@ msgstr "" msgid "The shares don't exist with the {0}" msgstr "" -#: erpnext/stock/stock_ledger.py:803 +#: erpnext/stock/stock_ledger.py:806 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 "" @@ -52304,7 +52348,7 @@ msgstr "" msgid "The uploaded file does not appear to be in valid MT940 format." msgstr "" -#: erpnext/edi/doctype/code_list/code_list_import.py:48 +#: erpnext/edi/doctype/code_list/code_list_import.py:54 msgid "The uploaded file does not match the selected Code List." msgstr "" @@ -52542,7 +52586,7 @@ msgstr "" msgid "This is a location where scraped materials are stored." msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:318 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:319 msgid "This is a preview of the email to be sent. A PDF of the document will automatically be attached with the email." msgstr "" @@ -52590,7 +52634,7 @@ msgstr "" msgid "This is considered dangerous from accounting point of view." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:540 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:539 msgid "This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice" msgstr "" @@ -52640,7 +52684,7 @@ msgstr "" msgid "This schedule was created when Asset {0} was restored on Asset Capitalization {1}'s cancellation." msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:458 +#: erpnext/assets/doctype/asset/depreciation.py:461 msgid "This schedule was created when Asset {0} was restored." msgstr "" @@ -52648,7 +52692,7 @@ msgstr "" msgid "This schedule was created when Asset {0} was returned through Sales Invoice {1}." msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:417 +#: erpnext/assets/doctype/asset/depreciation.py:420 msgid "This schedule was created when Asset {0} was scrapped." msgstr "" @@ -53198,7 +53242,7 @@ msgid "To include sub-assembly costs and scrap items in Finished Goods on a work msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2247 -#: erpnext/controllers/accounts_controller.py:3237 +#: erpnext/controllers/accounts_controller.py:3247 msgid "To include tax in row {0} in Item rate, taxes in rows {1} must also be included" msgstr "" @@ -53648,6 +53692,11 @@ msgstr "" msgid "Total Landed Cost (Company Currency)" msgstr "" +#. Label of the total_vouchers (Int) field in DocType 'Repost Item Valuation' +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +msgid "Total Ledgers" +msgstr "" + #: erpnext/accounts/report/balance_sheet/balance_sheet.py:219 msgid "Total Liability" msgstr "" @@ -53753,7 +53802,7 @@ msgstr "" msgid "Total Paid Amount" msgstr "" -#: erpnext/controllers/accounts_controller.py:2785 +#: erpnext/controllers/accounts_controller.py:2795 msgid "Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total" msgstr "" @@ -53842,12 +53891,6 @@ msgstr "" msgid "Total Repair Cost" msgstr "" -#. Label of the total_reposting_count (Int) field in DocType 'Repost Item -#. Valuation' -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json -msgid "Total Reposting Count" -msgstr "" - #: erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py:44 msgid "Total Revenue" msgstr "" @@ -54211,7 +54254,7 @@ msgstr "" msgid "Transaction Date" msgstr "" -#: erpnext/setup/doctype/company/company.py:1104 +#: erpnext/setup/doctype/company/company.py:1106 msgid "Transaction Deletion Document {0} has been triggered for company {1}" msgstr "" @@ -54821,7 +54864,7 @@ msgstr "" msgid "UOM Conversion Factor" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1463 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1468 msgid "UOM Conversion factor ({0} -> {1}) not found for item: {2}" msgstr "" @@ -54997,7 +55040,7 @@ msgstr "" msgid "Unit" msgstr "" -#: erpnext/controllers/accounts_controller.py:4064 +#: erpnext/controllers/accounts_controller.py:4074 msgid "Unit Price" msgstr "" @@ -55462,7 +55505,7 @@ msgstr "" msgid "Updating Work Order status" msgstr "" -#: erpnext/public/js/print.js:151 +#: erpnext/public/js/print.js:153 msgid "Updating details." msgstr "" @@ -55701,7 +55744,7 @@ msgstr "" msgid "User has not applied rule on the invoice {0}" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:187 +#: erpnext/setup/doctype/employee/employee.py:301 msgid "User {0} does not exist" msgstr "" @@ -55709,15 +55752,15 @@ msgstr "" msgid "User {0} doesn't have any default POS Profile. Check Default at Row {1} for this User." msgstr "" -#: erpnext/setup/doctype/employee/employee.py:205 +#: erpnext/setup/doctype/employee/employee.py:319 msgid "User {0} is already assigned to Employee {1}" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:243 +#: erpnext/setup/doctype/employee/employee.py:357 msgid "User {0}: Removed Employee Self Service role as there is no mapped employee." msgstr "" -#: erpnext/setup/doctype/employee/employee.py:238 +#: erpnext/setup/doctype/employee/employee.py:352 msgid "User {0}: Removed Employee role as there is no mapped employee." msgstr "" @@ -56010,11 +56053,11 @@ msgstr "" msgid "Valuation Rate (In / Out)" msgstr "" -#: erpnext/stock/stock_ledger.py:2022 +#: erpnext/stock/stock_ledger.py:2069 msgid "Valuation Rate Missing" msgstr "" -#: erpnext/stock/stock_ledger.py:2000 +#: erpnext/stock/stock_ledger.py:2047 msgid "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}." msgstr "" @@ -56046,7 +56089,7 @@ msgid "Valuation rate for the item as per Sales Invoice (Only for Internal Trans msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2271 -#: erpnext/controllers/accounts_controller.py:3261 +#: erpnext/controllers/accounts_controller.py:3271 msgid "Valuation type charges can not be marked as Inclusive" msgstr "" @@ -56820,6 +56863,10 @@ msgstr "" msgid "Warehouse is mandatory" msgstr "" +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:286 +msgid "Warehouse is required to get producible FG Items" +msgstr "" + #: erpnext/stock/doctype/warehouse/warehouse.py:259 msgid "Warehouse not found against the account {0}" msgstr "" @@ -56959,7 +57006,7 @@ msgstr "" msgid "Warning - Row {0}: Billing Hours are more than Actual Hours" msgstr "" -#: erpnext/stock/stock_ledger.py:813 +#: erpnext/stock/stock_ledger.py:816 msgid "Warning on Negative Stock" msgstr "" @@ -57273,8 +57320,8 @@ msgstr "" msgid "Widowed" msgstr "" -#. Label of the width (Int) field in DocType 'Shipment Parcel' -#. Label of the width (Int) field in DocType 'Shipment Parcel Template' +#. Label of the width (Float) field in DocType 'Shipment Parcel' +#. Label of the width (Float) field in DocType 'Shipment Parcel Template' #: erpnext/stock/doctype/shipment_parcel/shipment_parcel.json #: erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json msgid "Width (cm)" @@ -57819,7 +57866,7 @@ msgstr "" msgid "You are importing data for the code list:" msgstr "" -#: erpnext/controllers/accounts_controller.py:3861 +#: erpnext/controllers/accounts_controller.py:3871 msgid "You are not allowed to update as per the conditions set in {} Workflow." msgstr "" @@ -57960,7 +58007,7 @@ msgstr "" msgid "You do not have permission to edit this document" msgstr "" -#: erpnext/controllers/accounts_controller.py:3837 +#: erpnext/controllers/accounts_controller.py:3847 msgid "You do not have permissions to {} items in a {}." msgstr "" @@ -57972,7 +58019,7 @@ msgstr "" msgid "You don't have enough points to redeem." msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:273 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:286 msgid "You had {} errors while creating opening invoices. Check {} for more details" msgstr "" @@ -58012,7 +58059,7 @@ msgstr "" msgid "You need to cancel POS Closing Entry {} to be able to cancel this document." msgstr "" -#: erpnext/controllers/accounts_controller.py:3212 +#: erpnext/controllers/accounts_controller.py:3222 msgid "You selected the account group {1} as {2} Account in row {0}. Please select a single account." msgstr "" @@ -58080,7 +58127,7 @@ msgstr "" msgid "`Allow Negative rates for Items`" msgstr "" -#: erpnext/stock/stock_ledger.py:2014 +#: erpnext/stock/stock_ledger.py:2061 msgid "after" msgstr "" @@ -58120,7 +58167,7 @@ msgstr "" msgid "cannot be greater than 100" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:334 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:333 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1102 msgid "dated {0}" msgstr "" @@ -58270,7 +58317,7 @@ msgstr "" msgid "per hour" msgstr "" -#: erpnext/stock/stock_ledger.py:2015 +#: erpnext/stock/stock_ledger.py:2062 msgid "performing either one below:" msgstr "" @@ -58404,7 +58451,7 @@ msgstr "" msgid "{0} {1} has submitted Assets. Remove Item {2} from table to continue." msgstr "" -#: erpnext/controllers/accounts_controller.py:2367 +#: erpnext/controllers/accounts_controller.py:2377 msgid "{0} Account not found against Customer {1}." msgstr "" @@ -58432,7 +58479,7 @@ msgstr "" msgid "{0} Number {1} is already used in {2} {3}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1635 +#: erpnext/manufacturing/doctype/bom/bom.py:1638 msgid "{0} Operating Cost for operation {1}" msgstr "" @@ -58460,7 +58507,7 @@ msgstr "" msgid "{0} account is not of type {1}" msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:515 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:519 msgid "{0} account not found while submitting purchase receipt" msgstr "" @@ -58493,6 +58540,10 @@ msgstr "" msgid "{0} asset cannot be transferred" msgstr "" +#: erpnext/controllers/trends.py:60 +msgid "{0} can be either {1} or {2}." +msgstr "" + #: erpnext/accounts/doctype/pricing_rule/pricing_rule.py:279 msgid "{0} can not be negative" msgstr "" @@ -58509,8 +58560,8 @@ msgstr "" msgid "{0} cannot be zero" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:918 -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1034 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:923 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1039 #: erpnext/stock/doctype/pick_list/pick_list.py:1297 #: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py:322 msgid "{0} created" @@ -58524,11 +58575,11 @@ msgstr "" msgid "{0} currency must be same as company's default currency. Please select another account." msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:295 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:294 msgid "{0} currently has a {1} Supplier Scorecard standing, and Purchase Orders to this supplier should be issued with caution." msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:137 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:139 msgid "{0} currently has a {1} Supplier Scorecard standing, and RFQs to this supplier should be issued with caution." msgstr "" @@ -58570,7 +58621,7 @@ msgstr "" msgid "{0} hours" msgstr "" -#: erpnext/controllers/accounts_controller.py:2725 +#: erpnext/controllers/accounts_controller.py:2735 msgid "{0} in row {1}" msgstr "" @@ -58613,7 +58664,7 @@ msgstr "" msgid "{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}" msgstr "" -#: erpnext/controllers/accounts_controller.py:3169 +#: erpnext/controllers/accounts_controller.py:3179 msgid "{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}." msgstr "" @@ -58729,16 +58780,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:2163 -#: erpnext/stock/stock_ledger.py:2177 +#: erpnext/stock/stock_ledger.py:1714 erpnext/stock/stock_ledger.py:2210 +#: erpnext/stock/stock_ledger.py:2224 msgid "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction." msgstr "" -#: erpnext/stock/stock_ledger.py:2264 erpnext/stock/stock_ledger.py:2309 +#: erpnext/stock/stock_ledger.py:2311 erpnext/stock/stock_ledger.py:2356 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:1708 msgid "{0} units of {1} needed in {2} to complete this transaction." msgstr "" @@ -58804,7 +58855,7 @@ msgstr "" msgid "{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts." msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:435 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:434 #: erpnext/selling/doctype/sales_order/sales_order.py:598 #: erpnext/stock/doctype/material_request/material_request.py:255 msgid "{0} {1} has been modified. Please refresh." @@ -58827,7 +58878,7 @@ msgid "{0} {1} is associated with {2}, but Party Account is {3}" msgstr "" #: erpnext/controllers/selling_controller.py:495 -#: erpnext/controllers/subcontracting_controller.py:1167 +#: erpnext/controllers/subcontracting_controller.py:1173 msgid "{0} {1} is cancelled or closed" msgstr "" diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 58fce82c208..2ee62b06ad5 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -944,12 +944,14 @@ class BOM(WebsiteGenerator): hour_rate / flt(self.conversion_rate) if self.conversion_rate and hour_rate else hour_rate ) - if row.hour_rate and row.time_in_mins: + if row.hour_rate: row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate) - row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0 - row.base_operating_cost = flt(row.operating_cost) * flt(self.conversion_rate) - row.cost_per_unit = row.operating_cost / (row.batch_size or 1.0) - row.base_cost_per_unit = row.base_operating_cost / (row.batch_size or 1.0) + + if row.time_in_mins: + row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0 + row.base_operating_cost = flt(row.operating_cost) * flt(self.conversion_rate) + row.cost_per_unit = row.operating_cost / (row.batch_size or 1.0) + row.base_cost_per_unit = row.base_operating_cost / (row.batch_size or 1.0) if update_hour_rate: row.db_update() diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index b392a2aa02b..9fb7dcb51b2 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -40,6 +40,14 @@ frappe.ui.form.on("Job Card", { }; }); + frm.set_query("work_order", function () { + return { + filters: { + status: ["not in", ["Cancelled", "Closed", "Stopped"]], + }, + }; + }); + frm.events.set_company_filters(frm, "target_warehouse"); frm.events.set_company_filters(frm, "source_warehouse"); frm.events.set_company_filters(frm, "wip_warehouse"); @@ -780,26 +788,12 @@ frappe.ui.form.on("Job Card Time Log", { frm.events.set_total_completed_qty(frm); }, - - time_in_mins(frm, cdt, cdn) { - let d = locals[cdt][cdn]; - if (d.time_in_mins) { - d.to_time = add_mins_to_time(d.from_time, d.time_in_mins); - frappe.model.set_value(cdt, cdn, "to_time", d.to_time); - } - }, }); function get_seconds_diff(d1, d2) { return moment(d1).diff(d2, "seconds"); } -function add_mins_to_time(datetime, mins) { - let new_date = moment(datetime).add(mins, "minutes"); - - return new_date.format("YYYY-MM-DD HH:mm:ss"); -} - function get_last_completed_row(time_logs) { let completed_rows = time_logs.filter((d) => d.to_time); diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 30b3968fc80..36364f6740b 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -616,7 +616,12 @@ class ProductionPlan(Document): None, ): item.db_set("sub_assembly_item_reference", reference) - elif self.reserve_stock and item.main_item_code and item.from_bom: + elif ( + self.reserve_stock + and item.main_item_code + and item.from_bom + and item.main_item_code != frappe.get_cached_value("BOM", item.from_bom, "item") + ): frappe.throw( _( "Sub assembly item references are missing. Please fetch the sub assemblies and raw materials again." @@ -1778,8 +1783,10 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d ) sales_order = data.get("sales_order") + qty_precision = frappe.get_precision("Material Request Plan Item", "quantity") for key, details in item_details.items(): + details.qty = flt(details.qty, qty_precision) so_item_details.setdefault(sales_order, frappe._dict()) if key in so_item_details.get(sales_order, {}): so_item_details[sales_order][key]["qty"] = so_item_details[sales_order][key].get( diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 3b39c58fbac..bea542b7bfa 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -508,10 +508,28 @@ class TestWorkOrder(ERPNextTestSuite): def test_work_order_material_transferred_qty_with_process_loss(self): stock_entries = [] - bom = frappe.get_doc( - "BOM", {"docstatus": 1, "with_operations": 1, "company": "_Test Company", "has_variants": 0} + item_code = make_item("_Test Item For Process Loss", {"is_stock_item": 1}).name + rm_item_code = make_item("Test Item For Process Loss RM", {"is_stock_item": 1}).name + + bom = make_bom( + item=item_code, + raw_materials=[rm_item_code], + with_operations=1, + do_not_save=True, ) + operation = { + "operation": "_Test Operation 1", + "workstation": "_Test Workstation 1", + "description": "Test Data", + "operating_cost": 100, + "time_in_mins": 40, + } + + bom.append("operations", operation) + bom.save() + bom.submit() + work_order = make_wo_order_test_record( item=bom.item, qty=2, diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 4c317203295..f9d380964bc 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -587,7 +587,7 @@ class WorkOrder(Document): if self.docstatus == 0: status = "Draft" elif self.docstatus == 1: - if status != "Stopped": + if status not in ["Closed", "Stopped"]: status = "Not Started" if flt(self.material_transferred_for_manufacturing) > 0: status = "In Process" diff --git a/erpnext/manufacturing/report/bom_stock_report/__init__.py b/erpnext/manufacturing/report/bom_stock_analysis/__init__.py similarity index 100% rename from erpnext/manufacturing/report/bom_stock_report/__init__.py rename to erpnext/manufacturing/report/bom_stock_analysis/__init__.py diff --git a/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js b/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js new file mode 100644 index 00000000000..7629c102d7c --- /dev/null +++ b/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js @@ -0,0 +1,59 @@ +// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.query_reports["BOM Stock Analysis"] = { + filters: [ + { + fieldname: "bom", + label: __("BOM"), + fieldtype: "Link", + options: "BOM", + reqd: 1, + }, + { + fieldname: "warehouse", + label: __("Warehouse"), + fieldtype: "Link", + options: "Warehouse", + }, + { + fieldname: "qty_to_make", + label: __("FG Items to Make"), + fieldtype: "Float", + }, + { + fieldname: "show_exploded_view", + label: __("Show availability of exploded items"), + fieldtype: "Check", + default: false, + }, + ], + formatter(value, row, column, data, default_formatter) { + if (data && data.bold && column.fieldname === "item") { + return value ? `${value}` : ""; + } + + value = default_formatter(value, row, column, data); + + if (column.fieldname === "difference_qty" && value !== "" && value !== undefined) { + const numeric = parseFloat(value.replace(/,/g, "")) || 0; + if (numeric < 0) { + value = `${value}`; + } else if (numeric > 0) { + value = `${value}`; + } + } + + if (data && data.bold) { + if (column.fieldname === "description") { + const qty_to_make = Number(frappe.query_report.get_filter_value("qty_to_make")) || 0; + const producible = Number(String(data.description ?? "").replace(/,/g, "")) || 0; + const colour = qty_to_make && producible < qty_to_make ? "red" : "green"; + return `${value}`; + } + return `${value}`; + } + + return value; + }, +}; diff --git a/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.json b/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.json new file mode 100644 index 00000000000..b0e68f77ba7 --- /dev/null +++ b/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.json @@ -0,0 +1,31 @@ +{ + "add_total_row": 0, + "add_translate_data": 0, + "columns": [], + "creation": "2026-03-23 15:42:06.064606", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letter_head": null, + "modified": "2026-03-23 15:48:56.933892", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "BOM Stock Analysis", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "BOM", + "report_name": "BOM Stock Analysis", + "report_type": "Script Report", + "roles": [ + { + "role": "Manufacturing Manager" + }, + { + "role": "Manufacturing User" + } + ], + "timeout": 0 +} diff --git a/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py b/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py new file mode 100644 index 00000000000..59578127f9f --- /dev/null +++ b/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py @@ -0,0 +1,326 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.query_builder.functions import Floor, IfNull, Sum +from frappe.utils import flt, fmt_money +from frappe.utils.data import comma_and +from pypika.terms import ExistsCriterion + + +def execute(filters=None): + filters = filters or {} + if filters.get("qty_to_make"): + columns = get_columns_with_qty_to_make() + data = get_data_with_qty_to_make(filters) + else: + columns = get_columns_without_qty_to_make() + data = get_data_without_qty_to_make(filters) + + return columns, data + + +def fmt_qty(value): + """Format a float quantity for display as a string, so blank rows stay blank.""" + return frappe.utils.fmt_money(value, precision=2, currency=None) + + +def fmt_rate(value): + """Format a currency rate for display as a string.""" + currency = frappe.defaults.get_global_default("currency") + return frappe.utils.fmt_money(value, precision=2, currency=currency) + + +def get_data_with_qty_to_make(filters): + bom_data = get_bom_data(filters) + manufacture_details = get_manufacturer_records() + purchase_rates = batch_fetch_purchase_rates(bom_data) + qty_to_make = flt(filters.get("qty_to_make")) + + data = [] + for row in bom_data: + qty_per_unit = flt(row.qty_per_unit) if row.qty_per_unit > 0 else 0 + required_qty = qty_to_make * qty_per_unit + difference_qty = flt(row.actual_qty) - required_qty + rate = purchase_rates.get(row.item_code, 0) + + data.append( + { + "item": row.item_code, + "description": row.description, + "from_bom_no": row.from_bom_no, + "manufacturer": comma_and( + manufacture_details.get(row.item_code, {}).get("manufacturer", []), add_quotes=False + ), + "manufacturer_part_number": comma_and( + manufacture_details.get(row.item_code, {}).get("manufacturer_part", []), add_quotes=False + ), + "qty_per_unit": fmt_qty(qty_per_unit), + "available_qty": fmt_qty(row.actual_qty), + "required_qty": fmt_qty(required_qty), + "difference_qty": fmt_qty(difference_qty), + "last_purchase_rate": fmt_rate(rate), + "_available_qty": flt(row.actual_qty), + "_qty_per_unit": qty_per_unit, + } + ) + + min_producible = ( + min(int(r["_available_qty"] // r["_qty_per_unit"]) for r in data if r["_qty_per_unit"]) if data else 0 + ) + + for row in data: + row.pop("_available_qty", None) + row.pop("_qty_per_unit", None) + + # blank spacer row + data.append({}) + + data.append( + { + "item": _("Maximum Producible Items"), + "description": min_producible, + "from_bom_no": "", + "manufacturer": "", + "manufacturer_part_number": "", + "qty_per_unit": "", + "available_qty": "", + "required_qty": "", + "difference_qty": "", + "last_purchase_rate": "", + "bold": 1, + } + ) + + return data + + +def get_columns_with_qty_to_make(): + return [ + {"fieldname": "item", "label": _("Item"), "fieldtype": "Link", "options": "Item", "width": 180}, + {"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 160}, + { + "fieldname": "from_bom_no", + "label": _("From BOM No"), + "fieldtype": "Link", + "options": "BOM", + "width": 150, + }, + {"fieldname": "manufacturer", "label": _("Manufacturer"), "fieldtype": "Data", "width": 130}, + { + "fieldname": "manufacturer_part_number", + "label": _("Manufacturer Part Number"), + "fieldtype": "Data", + "width": 170, + }, + {"fieldname": "qty_per_unit", "label": _("Qty Per Unit"), "fieldtype": "Data", "width": 110}, + {"fieldname": "available_qty", "label": _("Available Qty"), "fieldtype": "Data", "width": 120}, + {"fieldname": "required_qty", "label": _("Required Qty"), "fieldtype": "Data", "width": 120}, + {"fieldname": "difference_qty", "label": _("Difference Qty"), "fieldtype": "Data", "width": 130}, + { + "fieldname": "last_purchase_rate", + "label": _("Last Purchase Rate"), + "fieldtype": "Data", + "width": 160, + }, + ] + + +def get_data_without_qty_to_make(filters): + raw_rows = get_producible_fg_items(filters) + + data = [] + for row in raw_rows: + data.append( + { + "item": row[0], + "description": row[1], + "from_bom_no": row[2], + "qty_per_unit": fmt_qty(row[3]), + "available_qty": fmt_qty(row[4]), + } + ) + + min_producible = min((row[5] or 0) for row in raw_rows) if raw_rows else 0 + # blank spacer row + data.append({}) + + data.append( + { + "item": _("Maximum Producible Items"), + "description": min_producible, + "from_bom_no": "", + "qty_per_unit": "", + "available_qty": "", + "bold": 1, + } + ) + + return data + + +def get_columns_without_qty_to_make(): + return [ + {"fieldname": "item", "label": _("Item"), "fieldtype": "Link", "options": "Item", "width": 180}, + {"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 200}, + { + "fieldname": "from_bom_no", + "label": _("From BOM No"), + "fieldtype": "Link", + "options": "BOM", + "width": 160, + }, + {"fieldname": "qty_per_unit", "label": _("Qty Per Unit"), "fieldtype": "Data", "width": 120}, + {"fieldname": "available_qty", "label": _("Available Qty"), "fieldtype": "Data", "width": 120}, + ] + + +def batch_fetch_purchase_rates(bom_data): + if not bom_data: + return {} + item_codes = [row.item_code for row in bom_data] + return { + r.name: r.last_purchase_rate + for r in frappe.get_all( + "Item", + filters={"name": ["in", item_codes]}, + fields=["name", "last_purchase_rate"], + ) + } + + +def get_bom_data(filters): + bom_item_table = "BOM Explosion Item" if filters.get("show_exploded_view") else "BOM Item" + + bom_item = frappe.qb.DocType(bom_item_table) + bin = frappe.qb.DocType("Bin") + + query = ( + frappe.qb.from_(bom_item) + .left_join(bin) + .on(bom_item.item_code == bin.item_code) + .select( + bom_item.item_code, + bom_item.description, + bom_item.parent.as_("from_bom_no"), + Sum(bom_item.qty_consumed_per_unit).as_("qty_per_unit"), + IfNull(Sum(bin.actual_qty), 0).as_("actual_qty"), + ) + .where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM")) + .groupby(bom_item.item_code) + .orderby(bom_item.idx) + ) + + if filters.get("warehouse"): + warehouse_details = frappe.db.get_value( + "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 + ) + if warehouse_details: + wh = frappe.qb.DocType("Warehouse") + query = query.where( + ExistsCriterion( + frappe.qb.from_(wh) + .select(wh.name) + .where( + (wh.lft >= warehouse_details.lft) + & (wh.rgt <= warehouse_details.rgt) + & (bin.warehouse == wh.name) + ) + ) + ) + else: + query = query.where(bin.warehouse == filters.get("warehouse")) + + if bom_item_table == "BOM Item": + query = query.select(bom_item.bom_no, bom_item.is_phantom_item) + + data = query.run(as_dict=True) + return explode_phantom_boms(data, filters) if bom_item_table == "BOM Item" else data + + +def explode_phantom_boms(data, filters): + original_bom = filters.get("bom") + replacements = [] + + for idx, item in enumerate(data): + if not item.is_phantom_item: + continue + + filters["bom"] = item.bom_no + children = get_bom_data(filters) + filters["bom"] = original_bom + + for child in children: + child.qty_per_unit = (child.qty_per_unit or 0) * (item.qty_per_unit or 0) + + replacements.append((idx, children)) + + for idx, children in reversed(replacements): + data.pop(idx) + data[idx:idx] = children + + return data + + +def get_manufacturer_records(): + details = frappe.get_all( + "Item Manufacturer", fields=["manufacturer", "manufacturer_part_no", "item_code"] + ) + manufacture_details = frappe._dict() + for detail in details: + dic = manufacture_details.setdefault(detail.get("item_code"), {}) + dic.setdefault("manufacturer", []).append(detail.get("manufacturer")) + dic.setdefault("manufacturer_part", []).append(detail.get("manufacturer_part_no")) + return manufacture_details + + +def get_producible_fg_items(filters): + BOM_ITEM = frappe.qb.DocType("BOM Item") + BOM = frappe.qb.DocType("BOM") + BIN = frappe.qb.DocType("Bin") + WH = frappe.qb.DocType("Warehouse") + + warehouse = filters.get("warehouse") + if not warehouse: + frappe.throw(_("Warehouse is required to get producible FG Items")) + + warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1) + + if warehouse_details: + bin_subquery = ( + frappe.qb.from_(BIN) + .join(WH) + .on(BIN.warehouse == WH.name) + .select(BIN.item_code, Sum(BIN.actual_qty).as_("actual_qty")) + .where((WH.lft >= warehouse_details.lft) & (WH.rgt <= warehouse_details.rgt)) + .groupby(BIN.item_code) + ) + else: + bin_subquery = ( + frappe.qb.from_(BIN) + .select(BIN.item_code, Sum(BIN.actual_qty).as_("actual_qty")) + .where(BIN.warehouse == warehouse) + .groupby(BIN.item_code) + ) + + query = ( + frappe.qb.from_(BOM_ITEM) + .join(BOM) + .on(BOM_ITEM.parent == BOM.name) + .left_join(bin_subquery) + .on(BOM_ITEM.item_code == bin_subquery.item_code) + .select( + BOM_ITEM.item_code, + BOM_ITEM.description, + BOM_ITEM.parent.as_("from_bom_no"), + (BOM_ITEM.stock_qty / BOM.quantity).as_("qty_per_unit"), + IfNull(bin_subquery.actual_qty, 0).as_("available_qty"), + Floor(bin_subquery.actual_qty / ((Sum(BOM_ITEM.stock_qty)) / BOM.quantity)), + ) + .where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM")) + .groupby(BOM_ITEM.item_code) + .orderby(BOM_ITEM.idx) + ) + + return query.run(as_list=True) diff --git a/erpnext/manufacturing/report/bom_stock_analysis/test_bom_stock_analysis.py b/erpnext/manufacturing/report/bom_stock_analysis/test_bom_stock_analysis.py new file mode 100644 index 00000000000..fd8a52afde0 --- /dev/null +++ b/erpnext/manufacturing/report/bom_stock_analysis/test_bom_stock_analysis.py @@ -0,0 +1,171 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt +import frappe +from frappe.utils import fmt_money + +from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom +from erpnext.manufacturing.report.bom_stock_analysis.bom_stock_analysis import ( + execute as bom_stock_analysis_report, +) +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.tests.utils import ERPNextTestSuite + + +def fmt_qty(value): + return fmt_money(value, precision=2, currency=None) + + +def fmt_rate(value): + currency = frappe.defaults.get_global_default("currency") + return fmt_money(value, precision=2, currency=currency) + + +class TestBOMStockAnalysis(ERPNextTestSuite): + def setUp(self): + self.fg_item, self.rm_items = create_items() + self.boms = create_boms(self.fg_item, self.rm_items) + + def test_bom_stock_analysis(self): + qty_to_make = 10 + + # Case 1: When Item(s) Qty and Stock Qty are equal. + raw_data = bom_stock_analysis_report( + filters={ + "qty_to_make": qty_to_make, + "bom": self.boms[0].name, + } + )[1] + + data, footer = split_data_and_footer(raw_data) + expected_data, expected_min = get_expected_data(self.boms[0], qty_to_make) + + self.assertSetEqual( + set(tuple(sorted(r.items())) for r in data), + set(tuple(sorted(r.items())) for r in expected_data), + ) + self.assertEqual(footer.get("description"), expected_min) + + # Case 2: When Item(s) Qty and Stock Qty are different and BOM Qty is 1. + raw_data = bom_stock_analysis_report( + filters={ + "qty_to_make": qty_to_make, + "bom": self.boms[1].name, + } + )[1] + + data, footer = split_data_and_footer(raw_data) + expected_data, expected_min = get_expected_data(self.boms[1], qty_to_make) + + self.assertSetEqual( + set(tuple(sorted(r.items())) for r in data), + set(tuple(sorted(r.items())) for r in expected_data), + ) + self.assertEqual(footer.get("description"), expected_min) + + # Case 3: When Item(s) Qty and Stock Qty are different and BOM Qty is greater than 1. + raw_data = bom_stock_analysis_report( + filters={ + "qty_to_make": qty_to_make, + "bom": self.boms[2].name, + } + )[1] + + data, footer = split_data_and_footer(raw_data) + expected_data, expected_min = get_expected_data(self.boms[2], qty_to_make) + + self.assertSetEqual( + set(tuple(sorted(r.items())) for r in data), + set(tuple(sorted(r.items())) for r in expected_data), + ) + self.assertEqual(footer.get("description"), expected_min) + + +def split_data_and_footer(raw_data): + """Separate component rows from the footer row. Skips blank spacer rows.""" + data = [row for row in raw_data if row and not row.get("bold")] + footer = next((row for row in raw_data if row and row.get("bold")), {}) + return data, footer + + +def create_items(): + fg_item = make_item(properties={"is_stock_item": 1}).name + rm_item1 = make_item( + properties={ + "is_stock_item": 1, + "standard_rate": 100, + "opening_stock": 100, + "last_purchase_rate": 100, + "item_defaults": [{"company": "_Test Company", "default_warehouse": "Stores - _TC"}], + } + ).name + rm_item2 = make_item( + properties={ + "is_stock_item": 1, + "standard_rate": 200, + "opening_stock": 200, + "last_purchase_rate": 200, + "item_defaults": [{"company": "_Test Company", "default_warehouse": "Stores - _TC"}], + } + ).name + + return fg_item, [rm_item1, rm_item2] + + +def create_boms(fg_item, rm_items): + def update_bom_items(bom, uom, conversion_factor): + for item in bom.items: + item.uom = uom + item.conversion_factor = conversion_factor + return bom + + bom1 = make_bom(item=fg_item, quantity=1, raw_materials=rm_items, rm_qty=10) + + bom2 = make_bom(item=fg_item, quantity=1, raw_materials=rm_items, rm_qty=10, do_not_submit=True) + bom2 = update_bom_items(bom2, "Box", 10) + bom2.save() + bom2.submit() + + bom3 = make_bom(item=fg_item, quantity=2, raw_materials=rm_items, rm_qty=10, do_not_submit=True) + bom3 = update_bom_items(bom3, "Box", 10) + bom3.save() + bom3.submit() + + return [bom1, bom2, bom3] + + +def get_expected_data(bom, qty_to_make): + """ + Returns (component_rows, min_producible). + Component rows are dicts matching what the report produces. + min_producible is the expected footer value. + """ + expected_data = [] + producible_per_item = [] + + for idx, bom_item in enumerate(bom.items): + qty_per_unit = float(bom_item.stock_qty / bom.quantity) + available_qty = float(100 * (idx + 1)) + required_qty = float(qty_to_make * qty_per_unit) + difference_qty = available_qty - required_qty + last_purchase_rate = float(100 * (idx + 1)) + + expected_data.append( + { + "item": bom_item.item_code, + "description": bom_item.item_code, # description falls back to item_code in test items + "from_bom_no": bom.name, + "manufacturer": "", + "manufacturer_part_number": "", + "qty_per_unit": fmt_qty(qty_per_unit), + "available_qty": fmt_qty(available_qty), + "required_qty": fmt_qty(required_qty), + "difference_qty": fmt_qty(difference_qty), + "last_purchase_rate": fmt_rate(last_purchase_rate), + } + ) + + producible_per_item.append(int(available_qty // qty_per_unit) if qty_per_unit else 0) + + min_producible = min(producible_per_item) if producible_per_item else 0 + + return expected_data, min_producible diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js deleted file mode 100644 index 76a95127853..00000000000 --- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2016, Epoch Consulting and contributors -// For license information, please see license.txt - -frappe.query_reports["BOM Stock Calculated"] = { - filters: [ - { - fieldname: "bom", - label: __("BOM"), - fieldtype: "Link", - options: "BOM", - reqd: 1, - }, - { - fieldname: "warehouse", - label: __("Warehouse"), - fieldtype: "Link", - options: "Warehouse", - }, - { - fieldname: "qty_to_make", - label: __("Quantity to Make"), - fieldtype: "Float", - default: "1.0", - reqd: 1, - }, - { - fieldname: "show_exploded_view", - label: __("Show exploded view"), - fieldtype: "Check", - default: false, - }, - ], -}; diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.json b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.json deleted file mode 100644 index 73421cebf0e..00000000000 --- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "add_total_row": 0, - "creation": "2018-05-17 12:40:31.355049", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 0, - "is_standard": "Yes", - "letter_head": "", - "modified": "2018-06-18 13:33:18.103220", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "BOM Stock Calculated", - "owner": "Administrator", - "ref_doctype": "BOM", - "report_name": "BOM Stock Calculated", - "report_type": "Script Report", - "roles": [ - { - "role": "Manufacturing Manager" - }, - { - "role": "Manufacturing User" - } - ] -} diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py deleted file mode 100644 index 4b5df4df4b2..00000000000 --- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py +++ /dev/null @@ -1,199 +0,0 @@ -# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import frappe -from frappe import _ -from frappe.query_builder.functions import IfNull, Sum -from frappe.utils.data import comma_and -from pypika.terms import ExistsCriterion - - -def execute(filters=None): - columns = get_columns() - data = [] - - bom_data = get_bom_data(filters) - qty_to_make = filters.get("qty_to_make") - manufacture_details = get_manufacturer_records() - - for row in bom_data: - required_qty = qty_to_make * row.qty_per_unit - last_purchase_rate = frappe.db.get_value("Item", row.item_code, "last_purchase_rate") - - data.append(get_report_data(last_purchase_rate, required_qty, row, manufacture_details)) - - return columns, data - - -def get_report_data(last_purchase_rate, required_qty, row, manufacture_details): - qty_per_unit = row.qty_per_unit if row.qty_per_unit > 0 else 0 - difference_qty = row.actual_qty - required_qty - return [ - row.item_code, - row.description, - row.from_bom_no, - comma_and(manufacture_details.get(row.item_code, {}).get("manufacturer", []), add_quotes=False), - comma_and(manufacture_details.get(row.item_code, {}).get("manufacturer_part", []), add_quotes=False), - qty_per_unit, - row.actual_qty, - required_qty, - difference_qty, - last_purchase_rate, - ] - - -def get_columns(): - return [ - { - "fieldname": "item", - "label": _("Item"), - "fieldtype": "Link", - "options": "Item", - "width": 120, - }, - { - "fieldname": "description", - "label": _("Description"), - "fieldtype": "Data", - "width": 150, - }, - { - "fieldname": "from_bom_no", - "label": _("From BOM No"), - "fieldtype": "Link", - "options": "BOM", - "width": 150, - }, - { - "fieldname": "manufacturer", - "label": _("Manufacturer"), - "fieldtype": "Data", - "width": 120, - }, - { - "fieldname": "manufacturer_part_number", - "label": _("Manufacturer Part Number"), - "fieldtype": "Data", - "width": 150, - }, - { - "fieldname": "qty_per_unit", - "label": _("Qty Per Unit"), - "fieldtype": "Float", - "width": 110, - }, - { - "fieldname": "available_qty", - "label": _("Available Qty"), - "fieldtype": "Float", - "width": 120, - }, - { - "fieldname": "required_qty", - "label": _("Required Qty"), - "fieldtype": "Float", - "width": 120, - }, - { - "fieldname": "difference_qty", - "label": _("Difference Qty"), - "fieldtype": "Float", - "width": 130, - }, - { - "fieldname": "last_purchase_rate", - "label": _("Last Purchase Rate"), - "fieldtype": "Float", - "width": 160, - }, - ] - - -def get_bom_data(filters): - bom_item_table = "BOM Explosion Item" if filters.get("show_exploded_view") else "BOM Item" - - bom_item = frappe.qb.DocType(bom_item_table) - bin = frappe.qb.DocType("Bin") - - query = ( - frappe.qb.from_(bom_item) - .left_join(bin) - .on(bom_item.item_code == bin.item_code) - .select( - bom_item.item_code, - bom_item.description, - bom_item.parent.as_("from_bom_no"), - bom_item.qty_consumed_per_unit.as_("qty_per_unit"), - IfNull(Sum(bin.actual_qty), 0).as_("actual_qty"), - ) - .where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM")) - .groupby(bom_item.item_code) - .orderby(bom_item.idx) - ) - - if filters.get("warehouse"): - warehouse_details = frappe.db.get_value( - "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 - ) - - if warehouse_details: - wh = frappe.qb.DocType("Warehouse") - query = query.where( - ExistsCriterion( - frappe.qb.from_(wh) - .select(wh.name) - .where( - (wh.lft >= warehouse_details.lft) - & (wh.rgt <= warehouse_details.rgt) - & (bin.warehouse == wh.name) - ) - ) - ) - else: - query = query.where(bin.warehouse == filters.get("warehouse")) - - if bom_item_table == "BOM Item": - query = query.select(bom_item.bom_no, bom_item.is_phantom_item) - - data = query.run(as_dict=True) - return explode_phantom_boms(data, filters) if bom_item_table == "BOM Item" else data - - -def explode_phantom_boms(data, filters): - original_bom = filters.get("bom") - replacements = [] - - for idx, item in enumerate(data): - if not item.is_phantom_item: - continue - - filters["bom"] = item.bom_no - children = get_bom_data(filters) - filters["bom"] = original_bom - - for child in children: - child.qty_per_unit = (child.qty_per_unit or 0) * (item.qty_per_unit or 0) - - replacements.append((idx, children)) - - for idx, children in reversed(replacements): - data.pop(idx) - data[idx:idx] = children - - filters["bom"] = original_bom - return data - - -def get_manufacturer_records(): - details = frappe.get_all( - "Item Manufacturer", fields=["manufacturer", "manufacturer_part_no", "item_code"] - ) - - manufacture_details = frappe._dict() - for detail in details: - dic = manufacture_details.setdefault(detail.get("item_code"), {}) - dic.setdefault("manufacturer", []).append(detail.get("manufacturer")) - dic.setdefault("manufacturer_part", []).append(detail.get("manufacturer_part_no")) - - return manufacture_details diff --git a/erpnext/manufacturing/report/bom_stock_calculated/test_bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/test_bom_stock_calculated.py deleted file mode 100644 index e0105b114c5..00000000000 --- a/erpnext/manufacturing/report/bom_stock_calculated/test_bom_stock_calculated.py +++ /dev/null @@ -1,119 +0,0 @@ -# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import frappe - -from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom -from erpnext.manufacturing.report.bom_stock_calculated.bom_stock_calculated import ( - execute as bom_stock_calculated_report, -) -from erpnext.stock.doctype.item.test_item import make_item -from erpnext.tests.utils import ERPNextTestSuite - - -class TestBOMStockCalculated(ERPNextTestSuite): - def setUp(self): - self.fg_item, self.rm_items = create_items() - self.boms = create_boms(self.fg_item, self.rm_items) - - def test_bom_stock_calculated(self): - qty_to_make = 10 - - # Case 1: When Item(s) Qty and Stock Qty are equal. - data = bom_stock_calculated_report( - filters={ - "qty_to_make": qty_to_make, - "bom": self.boms[0].name, - } - )[1] - expected_data = get_expected_data(self.boms[0], qty_to_make) - self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) - - # Case 2: When Item(s) Qty and Stock Qty are different and BOM Qty is 1. - data = bom_stock_calculated_report( - filters={ - "qty_to_make": qty_to_make, - "bom": self.boms[1].name, - } - )[1] - expected_data = get_expected_data(self.boms[1], qty_to_make) - self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) - - # Case 3: When Item(s) Qty and Stock Qty are different and BOM Qty is greater than 1. - data = bom_stock_calculated_report( - filters={ - "qty_to_make": qty_to_make, - "bom": self.boms[2].name, - } - )[1] - expected_data = get_expected_data(self.boms[2], qty_to_make) - self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) - - -def create_items(): - fg_item = make_item(properties={"is_stock_item": 1}).name - rm_item1 = make_item( - properties={ - "is_stock_item": 1, - "standard_rate": 100, - "opening_stock": 100, - "last_purchase_rate": 100, - "item_defaults": [{"company": "_Test Company", "default_warehouse": "Stores - _TC"}], - } - ).name - rm_item2 = make_item( - properties={ - "is_stock_item": 1, - "standard_rate": 200, - "opening_stock": 200, - "last_purchase_rate": 200, - "item_defaults": [{"company": "_Test Company", "default_warehouse": "Stores - _TC"}], - } - ).name - - return fg_item, [rm_item1, rm_item2] - - -def create_boms(fg_item, rm_items): - def update_bom_items(bom, uom, conversion_factor): - for item in bom.items: - item.uom = uom - item.conversion_factor = conversion_factor - - return bom - - bom1 = make_bom(item=fg_item, quantity=1, raw_materials=rm_items, rm_qty=10) - - bom2 = make_bom(item=fg_item, quantity=1, raw_materials=rm_items, rm_qty=10, do_not_submit=True) - bom2 = update_bom_items(bom2, "Box", 10) - bom2.save() - bom2.submit() - - bom3 = make_bom(item=fg_item, quantity=2, raw_materials=rm_items, rm_qty=10, do_not_submit=True) - bom3 = update_bom_items(bom3, "Box", 10) - bom3.save() - bom3.submit() - - return [bom1, bom2, bom3] - - -def get_expected_data(bom, qty_to_make): - expected_data = [] - - for idx in range(len(bom.items)): - expected_data.append( - [ - bom.items[idx].item_code, - bom.items[idx].item_code, - bom.name, - "", - "", - float(bom.items[idx].stock_qty / bom.quantity), - float(100 * (idx + 1)), - float(qty_to_make * (bom.items[idx].stock_qty / bom.quantity)), - float((100 * (idx + 1)) - (qty_to_make * (bom.items[idx].stock_qty / bom.quantity))), - float(100 * (idx + 1)), - ] - ) - - return expected_data diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html deleted file mode 100644 index 2ae8848cc03..00000000000 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html +++ /dev/null @@ -1,27 +0,0 @@ -

{%= __("BOM Stock Report") %}

-
{%= filters.bom %}
-
{%= filters.warehouse %}
-
- - - - - - - - - - - - - {% for(var i=0, l=data.length; i - - - - - - - {% } %} - -
{%= __("Item") %}{%= __("Description") %}{%= __("Required Qty") %}{%= __("In Stock Qty") %}{%= __("Enough Parts to Build") %}
{%= data[i][ __("Item")] %}{%= data[i][ __("Description")] %} {%= data[i][ __("Required Qty")] %} {%= data[i][ __("In Stock Qty")] %} {%= data[i][ __("Enough Parts to Build")] %}
diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js deleted file mode 100644 index 91d73d0101c..00000000000 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js +++ /dev/null @@ -1,41 +0,0 @@ -frappe.query_reports["BOM Stock Report"] = { - filters: [ - { - fieldname: "bom", - label: __("BOM"), - fieldtype: "Link", - options: "BOM", - reqd: 1, - }, - { - fieldname: "warehouse", - label: __("Warehouse"), - fieldtype: "Link", - options: "Warehouse", - reqd: 1, - }, - { - fieldname: "show_exploded_view", - label: __("Show exploded view"), - fieldtype: "Check", - }, - { - fieldname: "qty_to_produce", - label: __("Quantity to Produce"), - fieldtype: "Int", - default: "1", - }, - ], - formatter: function (value, row, column, data, default_formatter) { - value = default_formatter(value, row, column, data); - - if (column.id == "item") { - if (data["in_stock_qty"] >= data["required_qty"]) { - value = `
${data["item"]}`; - } else { - value = `${data["item"]}`; - } - } - return value; - }, -}; diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.json b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.json deleted file mode 100644 index c563b87686d..00000000000 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2017-01-10 14:00:50.387244", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 0, - "is_standard": "Yes", - "letter_head": "", - "modified": "2017-06-23 04:46:43.209008", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "BOM Stock Report", - "owner": "Administrator", - "query": "SELECT \n\tbom_item.item_code as \"Item:Link/Item:200\",\n\tbom_item.description as \"Description:Data:300\",\n\tbom_item.qty as \"Required Qty:Float:100\",\n\tledger.actual_qty as \"In Stock Qty:Float:100\",\n\tFLOOR(ledger.actual_qty /bom_item.qty) as \"Enough Parts to Build:Int:100\"\nFROM\n\t`tabBOM Item` AS bom_item \n\tLEFT JOIN `tabBin` AS ledger\t\n\t\tON bom_item.item_code = ledger.item_code \n\t\tAND ledger.warehouse = %(warehouse)s\nWHERE\n\tbom_item.parent=%(bom)s\n\nGROUP BY bom_item.item_code", - "ref_doctype": "BOM", - "report_name": "BOM Stock Report", - "report_type": "Script Report", - "roles": [ - { - "role": "Manufacturing Manager" - }, - { - "role": "Manufacturing User" - } - ] -} \ No newline at end of file diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py deleted file mode 100644 index eeda32c64c7..00000000000 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import frappe -from frappe import _ -from frappe.query_builder.functions import Floor, Sum -from frappe.utils import cint - - -def execute(filters=None): - if not filters: - filters = {} - - columns = get_columns() - data = get_bom_stock(filters) - - return columns, data - - -def get_columns(): - return [ - _("Item") + ":Link/Item:150", - _("Item Name") + "::240", - _("Description") + "::300", - _("From BOM No") + "::200", - _("BOM Qty") + ":Float:160", - _("BOM UOM") + "::160", - _("Required Qty") + ":Float:120", - _("In Stock Qty") + ":Float:120", - _("Enough Parts to Build") + ":Float:200", - ] - - -def get_bom_stock(filters): - qty_to_produce = filters.get("qty_to_produce") - if cint(qty_to_produce) <= 0: - frappe.throw(_("Quantity to Produce should be greater than zero.")) - - bom_item_table = "BOM Explosion Item" if filters.get("show_exploded_view") else "BOM Item" - - warehouse = filters.get("warehouse") - warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1) - - BOM = frappe.qb.DocType("BOM") - BOM_ITEM = frappe.qb.DocType(bom_item_table) - BIN = frappe.qb.DocType("Bin") - WH = frappe.qb.DocType("Warehouse") - - if warehouse_details: - bin_subquery = ( - frappe.qb.from_(BIN) - .join(WH) - .on(BIN.warehouse == WH.name) - .select(BIN.item_code, Sum(BIN.actual_qty).as_("actual_qty")) - .where((WH.lft >= warehouse_details.lft) & (WH.rgt <= warehouse_details.rgt)) - .groupby(BIN.item_code) - ) - else: - bin_subquery = ( - frappe.qb.from_(BIN) - .select(BIN.item_code, Sum(BIN.actual_qty).as_("actual_qty")) - .where(BIN.warehouse == warehouse) - .groupby(BIN.item_code) - ) - - QUERY = ( - frappe.qb.from_(BOM) - .join(BOM_ITEM) - .on(BOM.name == BOM_ITEM.parent) - .left_join(bin_subquery) - .on(BOM_ITEM.item_code == bin_subquery.item_code) - .select( - BOM_ITEM.item_code, - BOM_ITEM.item_name, - BOM_ITEM.description, - BOM.name, - Sum(BOM_ITEM.stock_qty), - BOM_ITEM.stock_uom, - (Sum(BOM_ITEM.stock_qty) * qty_to_produce) / BOM.quantity, - bin_subquery.actual_qty, - Floor(bin_subquery.actual_qty / ((Sum(BOM_ITEM.stock_qty) * qty_to_produce) / BOM.quantity)), - ) - .where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM")) - .groupby(BOM_ITEM.item_code) - .orderby(BOM_ITEM.idx) - ) - - if bom_item_table == "BOM Item": - QUERY = QUERY.select(BOM_ITEM.bom_no, BOM_ITEM.is_phantom_item) - - data = QUERY.run(as_list=True) - return explode_phantom_boms(data, filters) if bom_item_table == "BOM Item" else data - - -def explode_phantom_boms(data, filters): - expanded = [] - for row in data: - if row[-1]: # last element is `is_phantom_item` - phantom_filters = filters.copy() - phantom_filters["qty_to_produce"] = row[-5] - phantom_filters["bom"] = row[-2] - expanded.extend(get_bom_stock(phantom_filters)) - else: - expanded.append(row) - - return expanded diff --git a/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py deleted file mode 100644 index 43706fcb4de..00000000000 --- a/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py +++ /dev/null @@ -1,112 +0,0 @@ -# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import frappe -from frappe.exceptions import ValidationError -from frappe.utils import floor - -from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom -from erpnext.manufacturing.report.bom_stock_report.bom_stock_report import ( - get_bom_stock as bom_stock_report, -) -from erpnext.stock.doctype.item.test_item import make_item -from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry -from erpnext.tests.utils import ERPNextTestSuite - - -class TestBomStockReport(ERPNextTestSuite): - def setUp(self): - self.warehouse = "_Test Warehouse - _TC" - self.fg_item, self.rm_items = create_items() - make_stock_entry(target=self.warehouse, item_code=self.rm_items[0], qty=20, basic_rate=100) - make_stock_entry(target=self.warehouse, item_code=self.rm_items[1], qty=40, basic_rate=200) - self.bom = make_bom(item=self.fg_item, quantity=1, raw_materials=self.rm_items, rm_qty=10) - - def test_bom_stock_report(self): - # Test 1: When `qty_to_produce` is 0. - filters = frappe._dict( - { - "bom": self.bom.name, - "warehouse": "Stores - _TC", - "qty_to_produce": 0, - } - ) - self.assertRaises(ValidationError, bom_stock_report, filters) - - # Test 2: When stock is not available. - data = bom_stock_report( - frappe._dict( - { - "bom": self.bom.name, - "warehouse": "Stores - _TC", - "qty_to_produce": 1, - } - ) - ) - expected_data = get_expected_data(self.bom, "Stores - _TC", 1) - self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) - - # Test 3: When stock is available. - data = bom_stock_report( - frappe._dict( - { - "bom": self.bom.name, - "warehouse": self.warehouse, - "qty_to_produce": 1, - } - ) - ) - expected_data = get_expected_data(self.bom, self.warehouse, 1) - self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) - - -def create_items(): - fg_item = make_item(properties={"is_stock_item": 1}).name - rm_item1 = make_item( - properties={ - "is_stock_item": 1, - "standard_rate": 100, - "opening_stock": 100, - "last_purchase_rate": 100, - } - ).name - rm_item2 = make_item( - properties={ - "is_stock_item": 1, - "standard_rate": 200, - "opening_stock": 200, - "last_purchase_rate": 200, - } - ).name - - return fg_item, [rm_item1, rm_item2] - - -def get_expected_data(bom, warehouse, qty_to_produce, show_exploded_view=False): - expected_data = [] - - for item in bom.get("exploded_items") if show_exploded_view else bom.get("items"): - in_stock_qty = frappe.get_cached_value( - "Bin", {"item_code": item.item_code, "warehouse": warehouse}, "actual_qty" - ) - - expected_data.append( - [ - item.item_code, - item.item_name, - item.description, - bom.name, - item.stock_qty, - item.stock_uom, - item.stock_qty * qty_to_produce / bom.quantity, - in_stock_qty, - floor(in_stock_qty / (item.stock_qty * qty_to_produce / bom.quantity)) - if in_stock_qty - else None, - item.bom_no, - item.is_phantom_item, - ] - ) - - return expected_data diff --git a/erpnext/manufacturing/report/test_reports.py b/erpnext/manufacturing/report/test_reports.py index adeddd996a5..505a82c3501 100644 --- a/erpnext/manufacturing/report/test_reports.py +++ b/erpnext/manufacturing/report/test_reports.py @@ -21,8 +21,7 @@ class TestManufacturingReports(ERPNextTestSuite): self.REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [ ("BOM Explorer", {"bom": self.last_bom}), ("BOM Operations Time", {}), - ("BOM Stock Calculated", {"bom": self.last_bom, "qty_to_make": 2}), - ("BOM Stock Report", {"bom": self.last_bom, "qty_to_produce": 2}), + ("BOM Stock Analysis", {"bom": self.last_bom, "_optional": ["warehouse"]}), ("Cost of Poor Quality Report", {"item": "_Test Item", "serial_no": "00"}), ("Downtime Analysis", {}), ( diff --git a/erpnext/public/js/print.js b/erpnext/public/js/print.js index 4f397ef2047..43ba9afc148 100644 --- a/erpnext/public/js/print.js +++ b/erpnext/public/js/print.js @@ -5,6 +5,7 @@ const doctype_list = [ "Purchase Order", "Purchase Invoice", "POS Invoice", + "Request for Quotation", ]; const allowed_print_formats = [ "Sales Order Standard", @@ -19,6 +20,7 @@ const allowed_print_formats = [ "Purchase Invoice with Item Image", "POS Invoice Standard", "POS Invoice with Item Image", + "Request for Quotation with Item Image", ]; const allowed_letterheads = ["Company Letterhead", "Company Letterhead - Grey"]; diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index b841f456bc9..9c02879284c 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -1029,11 +1029,11 @@ class TestQuotation(ERPNextTestSuite): def test_make_quotation_qar_to_inr(self): quotation = make_quotation( currency="QAR", - transaction_date="2026-06-04", + transaction_date="2026-01-01", ) cache = frappe.cache() - key = "currency_exchange_rate_{}:{}:{}".format("2026-06-04", "QAR", "INR") + key = "currency_exchange_rate_{}:{}:{}".format("2026-01-01", "QAR", "INR") value = cache.get(key) expected_rate = flt(value) / 3.64 diff --git a/erpnext/selling/workspace/selling/selling.json b/erpnext/selling/workspace/selling/selling.json index 5cfe409d128..141b3634708 100644 --- a/erpnext/selling/workspace/selling/selling.json +++ b/erpnext/selling/workspace/selling/selling.json @@ -6,7 +6,7 @@ "label": "Sales Order Trends" } ], - "content": "[{\"id\":\"vBSf8Vi9U8\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Sales Order Trends\",\"col\":12}},{\"id\":\"aW2i5R5GRP\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"43fzlS1qZg\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Sales Orders\",\"col\":4}},{\"id\":\"jhtxl-XOGi\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Sales Amount\",\"col\":4}},{\"id\":\"0Ioq-P11FP\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Average Order Value\",\"col\":4}},{\"id\":\"1it3dCOnm6\",\"type\":\"header\",\"data\":{\"text\":\"Quick Access\",\"col\":12}},{\"id\":\"0BcePLg0g1\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"uze5dJ1ipL\",\"type\":\"card\",\"data\":{\"card_name\":\"Selling\",\"col\":4}},{\"id\":\"3j2fYwMAkq\",\"type\":\"card\",\"data\":{\"card_name\":\"Point of Sale\",\"col\":4}},{\"id\":\"xImm8NepFt\",\"type\":\"card\",\"data\":{\"card_name\":\"Items and Pricing\",\"col\":4}},{\"id\":\"6MjIe7KCQo\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"id\":\"lBu2EKgmJF\",\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"id\":\"1ARHrjg4kI\",\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]", + "content": "[{\"id\":\"vBSf8Vi9U8\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Sales Order Trends\",\"col\":12}},{\"id\":\"aW2i5R5GRP\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"43fzlS1qZg\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Sales Orders\",\"col\":4}},{\"id\":\"jhtxl-XOGi\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Sales Amount\",\"col\":4}},{\"id\":\"0Ioq-P11FP\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Average Order Value\",\"col\":4}},{\"id\":\"0BcePLg0g1\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"uze5dJ1ipL\",\"type\":\"card\",\"data\":{\"card_name\":\"Selling\",\"col\":4}},{\"id\":\"3j2fYwMAkq\",\"type\":\"card\",\"data\":{\"card_name\":\"Point of Sale\",\"col\":4}},{\"id\":\"xImm8NepFt\",\"type\":\"card\",\"data\":{\"card_name\":\"Items and Pricing\",\"col\":4}},{\"id\":\"6MjIe7KCQo\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"id\":\"lBu2EKgmJF\",\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"id\":\"1ARHrjg4kI\",\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]", "creation": "2020-01-28 11:49:12.092882", "custom_blocks": [], "docstatus": 0, @@ -622,7 +622,7 @@ "type": "Link" } ], - "modified": "2026-01-02 17:42:20.131214", + "modified": "2026-02-19 13:01:26.893303", "modified_by": "Administrator", "module": "Selling", "name": "Selling", diff --git a/erpnext/setup/demo.py b/erpnext/setup/demo.py index ec48a3e1447..c460b1520c4 100644 --- a/erpnext/setup/demo.py +++ b/erpnext/setup/demo.py @@ -7,7 +7,7 @@ from random import randint import frappe from frappe import _ -from frappe.utils import add_days, getdate +from frappe.utils import add_days, get_url_to_form, getdate from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.utils import get_fiscal_year @@ -16,21 +16,44 @@ from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account -def setup_demo_data(): +def setup_demo_data(company_name): from frappe.utils.telemetry import capture capture("demo_data_creation_started", "erpnext") try: - company = create_demo_company() + frappe.db.savepoint("demo_data") + company = create_demo_company(company_name) process_masters() make_transactions(company) - frappe.cache.delete_keys("bootinfo") - frappe.publish_realtime("demo_data_complete") + capture("demo_data_creation_completed", "erpnext") + frappe.clear_messages() except Exception: - frappe.log_error("Failed to create demo data") + frappe.db.rollback(save_point="demo_data") + error_log = frappe.log_error("Failed to create demo data") + log_demo_data_failed_notification(error_log) capture("demo_data_creation_failed", "erpnext", properties={"exception": frappe.get_traceback()}) - raise - capture("demo_data_creation_completed", "erpnext") + + +def log_demo_data_failed_notification(error_log): + from frappe.core.doctype.role.role import get_users + from frappe.desk.doctype.notification_log.notification_log import make_notification_logs + + frappe.msgprint( + _("Demo data creation failed. Check notifications for more info."), + alert=True, + indicator="red", + realtime=True, + ) + + users = get_users("System Manager") + + notif_log_doc = { + "subject": _("Demo Data creation failed."), + "type": "Alert", + "link": get_url_to_form("Error Log", error_log.name), + } + + make_notification_logs(notif_log_doc, users) @frappe.whitelist() @@ -56,21 +79,8 @@ def clear_demo_data(): ) -def create_demo_company(): - if frappe.flags.in_test: - hash = frappe.generate_hash(length=3) - company_doc = frappe._dict( - { - "company_name": "Test Company" + " " + hash, - "abbr": "TC" + hash, - "default_currency": "INR", - "country": "India", - "chart_of_accounts": "Standard", - } - ) - else: - company = frappe.db.get_all("Company")[0].name - company_doc = frappe.get_doc("Company", company).as_dict() +def create_demo_company(company): + company_doc = frappe.get_doc("Company", company).as_dict() # Make a dummy company new_company = frappe.new_doc("Company") diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 5da3ca40904..8111935a339 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -927,7 +927,7 @@ def update_transactions_annual_history(company, commit=False): transactions_history = get_all_transactions_annual_history(company) frappe.db.set_value("Company", company, "transactions_annual_history", json.dumps(transactions_history)) - if commit: + if commit and not frappe.in_test: frappe.db.commit() @@ -936,7 +936,9 @@ def cache_companies_monthly_sales_history(): for company in companies: update_company_monthly_sales(company) update_transactions_annual_history(company) - frappe.db.commit() + + if not frappe.in_test: + frappe.db.commit() @frappe.whitelist() diff --git a/erpnext/setup/doctype/company/test_company.py b/erpnext/setup/doctype/company/test_company.py index c3fb5dd6ff2..36606e90755 100644 --- a/erpnext/setup/doctype/company/test_company.py +++ b/erpnext/setup/doctype/company/test_company.py @@ -199,7 +199,9 @@ class TestCompany(ERPNextTestSuite): def test_demo_data(self): from erpnext.setup.demo import clear_demo_data, setup_demo_data - setup_demo_data() + self.load_test_records("Company") + + setup_demo_data(self.globalTestRecords["Company"][0]["company_name"]) company_name = frappe.db.get_value("Company", {"name": ("like", "%(Demo)")}) self.assertTrue(company_name) diff --git a/erpnext/setup/doctype/company/test_records.json b/erpnext/setup/doctype/company/test_records.json index 1b51d98413e..74615e60162 100644 --- a/erpnext/setup/doctype/company/test_records.json +++ b/erpnext/setup/doctype/company/test_records.json @@ -180,5 +180,39 @@ "default_currency": "ZAR", "doctype": "Company", "create_chart_of_accounts_based_on": "Standard Template" + }, + { + "abbr": "_TOIC", + "company_name": "_Test Opening Invoice Company", + "country": "Pakistan", + "default_currency": "INR", + "doctype": "Company", + "create_chart_of_accounts_based_on": "Standard Template" + }, + { + "abbr": "TBC", + "company_name": "Trial Balance Company", + "country": "India", + "default_currency": "INR", + "doctype": "Company", + "create_chart_of_accounts_based_on": "Standard Template" + }, + { + "abbr": "_TSS", + "company_name": "_Test Support SLA", + "country": "India", + "default_currency": "INR", + "doctype": "Company", + "chart_of_accounts": "Standard", + "create_chart_of_accounts_based_on": "Standard Template" + }, + { + "abbr": "TQC", + "company_name": "Test Quality Company", + "country": "India", + "default_currency": "INR", + "doctype": "Company", + "chart_of_accounts": "Standard", + "create_chart_of_accounts_based_on": "Standard Template" } ] diff --git a/erpnext/setup/doctype/employee/employee.js b/erpnext/setup/doctype/employee/employee.js index 21d67d70e78..b4adc01b102 100755 --- a/erpnext/setup/doctype/employee/employee.js +++ b/erpnext/setup/doctype/employee/employee.js @@ -45,6 +45,64 @@ frappe.ui.form.on("Employee", { refresh: function (frm) { frm.fields_dict.date_of_birth.datepicker.update({ maxDate: new Date() }); + + if (!frm.is_new() && !frm.doc.user_id) { + frm.add_custom_button(__("Create User"), () => { + const dialog = new frappe.ui.Dialog({ + title: __("Create User"), + fields: [ + { + fieldtype: "Data", + fieldname: "email", + label: __("Email"), + reqd: 1, + default: + frm.doc.prefered_email || frm.doc.company_email || frm.doc.personal_email, + }, + { + fieldtype: "Check", + fieldname: "create_user_permission", + label: __("Create User Permission"), + default: 1, + }, + ], + primary_action_label: __("Create"), + primary_action: (values) => { + if (!values.email) { + frappe.msgprint(__("Email is required to create a user.")); + return; + } + + frappe + .call({ + method: "erpnext.setup.doctype.employee.employee.create_user", + args: { + employee: frm.doc.name, + email: values.email, + create_user_permission: values.create_user_permission ? 1 : 0, + }, + freeze: true, + freeze_message: __("Creating User..."), + }) + .then(() => { + dialog.hide(); + frm.reload_doc(); + }); + }, + }); + + dialog.show(); + }); + } + }, + + create_user_automatically: function (frm) { + if (frm.doc.create_user_automatically) { + frm.set_value("user_id", ""); + frm.set_df_property("user_id", "read_only", 1); + } else { + frm.set_df_property("user_id", "read_only", 0); + } }, prefered_contact_email: function (frm) { @@ -77,24 +135,6 @@ frappe.ui.form.on("Employee", { }, }); }, - - create_user: function (frm) { - if (!frm.doc.prefered_email) { - frappe.throw(__("Please enter Preferred Contact Email")); - } - frappe.call({ - method: "erpnext.setup.doctype.employee.employee.create_user", - args: { - employee: frm.doc.name, - email: frm.doc.prefered_email, - }, - freeze: true, - freeze_message: __("Creating User..."), - callback: function (r) { - frm.reload_doc(); - }, - }); - }, }); cur_frm.cscript = new erpnext.setup.EmployeeController({ diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json index e663913e8d7..03f68b91dc5 100644 --- a/erpnext/setup/doctype/employee/employee.json +++ b/erpnext/setup/doctype/employee/employee.json @@ -28,8 +28,9 @@ "status", "erpnext_user", "user_id", - "create_user", "create_user_permission", + "column_break_xwnm", + "create_user_automatically", "company_details_section", "company", "department", @@ -39,19 +40,11 @@ "reports_to", "column_break_18", "branch", - "employment_details", - "scheduled_confirmation_date", - "column_break_32", - "final_confirmation_date", - "contract_end_date", - "col_break_22", - "notice_number_of_days", - "date_of_retirement", "contact_details", "cell_number", "column_break_40", - "personal_email", "company_email", + "personal_email", "column_break4", "prefered_contact_email", "prefered_email", @@ -101,6 +94,14 @@ "external_work_history", "history_in_company", "internal_work_history", + "employment_details", + "scheduled_confirmation_date", + "column_break_32", + "final_confirmation_date", + "contract_end_date", + "col_break_22", + "notice_number_of_days", + "date_of_retirement", "exit", "resignation_letter_date", "relieving_date", @@ -273,6 +274,7 @@ }, { "collapsible": 1, + "collapsible_depends_on": "eval:doc.__islocal", "fieldname": "erpnext_user", "fieldtype": "Section Break", "label": "User Details" @@ -285,20 +287,23 @@ "label": "User ID", "options": "User" }, - { - "depends_on": "eval:(!doc.user_id)", - "fieldname": "create_user", - "fieldtype": "Button", - "label": "Create User" - }, { "default": "1", - "depends_on": "user_id", + "depends_on": "eval:doc.user_id || doc.create_user_automatically", "description": "This will restrict user access to other employee records", "fieldname": "create_user_permission", "fieldtype": "Check", "label": "Create User Permission" }, + { + "default": "0", + "depends_on": "eval:doc.__islocal && !doc.user_id", + "description": "Creates a User account for this employee using the Preferred, Company, or Personal email.", + "fieldname": "create_user_automatically", + "fieldtype": "Check", + "label": "Create User Automatically", + "set_only_once": 1 + }, { "allow_in_quick_entry": 1, "collapsible": 1, @@ -348,6 +353,7 @@ { "fieldname": "department", "fieldtype": "Link", + "in_list_view": 1, "in_standard_filter": 1, "label": "Department", "oldfieldname": "department", @@ -377,6 +383,7 @@ { "fieldname": "branch", "fieldtype": "Link", + "in_list_view": 1, "label": "Branch", "oldfieldname": "branch", "oldfieldtype": "Link", @@ -600,7 +607,7 @@ "collapsible": 1, "fieldname": "exit", "fieldtype": "Tab Break", - "label": "Employee Exit", + "label": "Exit", "oldfieldtype": "Section Break" }, { @@ -816,6 +823,10 @@ "fieldtype": "Data", "label": "IBAN", "options": "IBAN" + }, + { + "fieldname": "column_break_xwnm", + "fieldtype": "Column Break" } ], "icon": "fa fa-user", @@ -823,7 +834,7 @@ "image_field": "image", "is_tree": 1, "links": [], - "modified": "2025-08-29 11:52:12.819878", + "modified": "2026-03-23 15:26:05.149280", "modified_by": "Administrator", "module": "Setup", "name": "Employee", diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index 1ecbb4b9ac7..d66d091320b 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -8,7 +8,7 @@ from frappe.permissions import ( get_doc_permissions, remove_user_permission, ) -from frappe.utils import cstr, getdate, today, validate_email_address +from frappe.utils import cint, cstr, getdate, today, validate_email_address from frappe.utils.nestedset import NestedSet from erpnext.utilities.transaction_base import delete_events @@ -23,6 +23,94 @@ class InactiveEmployeeStatusError(frappe.ValidationError): class Employee(NestedSet): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + from erpnext.setup.doctype.employee_education.employee_education import EmployeeEducation + from erpnext.setup.doctype.employee_external_work_history.employee_external_work_history import ( + EmployeeExternalWorkHistory, + ) + from erpnext.setup.doctype.employee_internal_work_history.employee_internal_work_history import ( + EmployeeInternalWorkHistory, + ) + + attendance_device_id: DF.Data | None + bank_ac_no: DF.Data | None + bank_name: DF.Data | None + bio: DF.TextEditor | None + blood_group: DF.Literal["", "A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-"] + branch: DF.Link | None + cell_number: DF.Data | None + company: DF.Link + company_email: DF.Data | None + contract_end_date: DF.Date | None + create_user_automatically: DF.Check + create_user_permission: DF.Check + ctc: DF.Currency + current_accommodation_type: DF.Literal["", "Rented", "Owned"] + current_address: DF.SmallText | None + date_of_birth: DF.Date + date_of_issue: DF.Date | None + date_of_joining: DF.Date + date_of_retirement: DF.Date | None + department: DF.Link | None + designation: DF.Link | None + education: DF.Table[EmployeeEducation] + emergency_phone_number: DF.Data | None + employee: DF.Data | None + employee_name: DF.Data | None + employee_number: DF.Data | None + encashment_date: DF.Date | None + external_work_history: DF.Table[EmployeeExternalWorkHistory] + family_background: DF.SmallText | None + feedback: DF.SmallText | None + final_confirmation_date: DF.Date | None + first_name: DF.Data + gender: DF.Link + health_details: DF.SmallText | None + held_on: DF.Date | None + holiday_list: DF.Link | None + iban: DF.Data | None + image: DF.AttachImage | None + internal_work_history: DF.Table[EmployeeInternalWorkHistory] + last_name: DF.Data | None + leave_encashed: DF.Literal["", "Yes", "No"] + lft: DF.Int + marital_status: DF.Literal["", "Single", "Married", "Divorced", "Widowed"] + middle_name: DF.Data | None + naming_series: DF.Literal["HR-EMP-"] + new_workplace: DF.Data | None + notice_number_of_days: DF.Int + old_parent: DF.Data | None + passport_number: DF.Data | None + permanent_accommodation_type: DF.Literal["", "Rented", "Owned"] + permanent_address: DF.SmallText | None + person_to_be_contacted: DF.Data | None + personal_email: DF.Data | None + place_of_issue: DF.Data | None + prefered_contact_email: DF.Literal["", "Company Email", "Personal Email", "User ID"] + prefered_email: DF.Data | None + reason_for_leaving: DF.SmallText | None + relation: DF.Data | None + relieving_date: DF.Date | None + reports_to: DF.Link | None + resignation_letter_date: DF.Date | None + rgt: DF.Int + salary_currency: DF.Link | None + salary_mode: DF.Literal["", "Bank", "Cash", "Cheque"] + salutation: DF.Link | None + scheduled_confirmation_date: DF.Date | None + status: DF.Literal["Active", "Inactive", "Suspended", "Left"] + unsubscribed: DF.Check + user_id: DF.Link | None + valid_upto: DF.Date | None + # end: auto-generated types + nsm_parent_field = "reports_to" def autoname(self): @@ -72,6 +160,16 @@ class Employee(NestedSet): self.validate_for_enabled_user_id(data.get("enabled", 0)) self.validate_duplicate_user_id() + def validate_auto_user_creation(self): + if self.create_user_automatically and not ( + self.prefered_email or self.company_email or self.personal_email + ): + frappe.throw( + _("Company or Personal Email is mandatory when 'Create User Automatically' is enabled"), + frappe.MandatoryError, + title=_("Auto User Creation Error"), + ) + def update_nsm_model(self): frappe.utils.nestedset.update_nsm(self) @@ -83,6 +181,22 @@ class Employee(NestedSet): self.update_user_permissions() self.reset_employee_emails_cache() + def before_insert(self): + self.validate_auto_user_creation() + + def after_insert(self): + if not self.create_user_automatically: + return + + if self.user_id: + return + + create_user( + employee=self.name, + email=self.prefered_email or self.company_email or self.personal_email, + create_user_permission=self.create_user_permission, + ) + def update_user_permissions(self): if not self.has_value_changed("user_id") and not self.has_value_changed("create_user_permission"): return @@ -310,10 +424,17 @@ def deactivate_sales_person(status=None, employee=None): @frappe.whitelist() -def create_user(employee, user=None, email=None): +def create_user(employee: str, email: str | None = None, create_user_permission: int = 0) -> str: emp = frappe.get_doc("Employee", employee) + if emp.user_id: + frappe.throw(_("Employee {0} already has a linked user").format(emp.name)) + if not email: + frappe.throw(_("Email is required to create a user")) + + email = validate_email_address(email, True) employee_name = emp.employee_name.split(" ") + first_name = employee_name[0] middle_name = last_name = "" if len(employee_name) >= 3: @@ -322,16 +443,10 @@ def create_user(employee, user=None, email=None): elif len(employee_name) == 2: last_name = employee_name[1] - first_name = employee_name[0] - - if email: - emp.prefered_email = email - user = frappe.new_doc("User") user.update( { - "name": emp.employee_name, - "email": emp.prefered_email, + "email": email, "enabled": 1, "first_name": first_name, "middle_name": middle_name, @@ -342,9 +457,18 @@ def create_user(employee, user=None, email=None): "bio": emp.bio, } ) + emp.db_set("user_id", email) + user.append_roles("Employee") user.insert() + emp.user_id = user.name + emp.create_user_permission = cint(create_user_permission) emp.save() + + if cint(create_user_permission): + add_user_permission("Employee", emp.name, user.name) + add_user_permission("Company", emp.company, user.name) + return user.name diff --git a/erpnext/setup/doctype/employee/employee_list.js b/erpnext/setup/doctype/employee/employee_list.js index b50eb381c95..33cf7225626 100644 --- a/erpnext/setup/doctype/employee/employee_list.js +++ b/erpnext/setup/doctype/employee/employee_list.js @@ -1,11 +1,25 @@ frappe.listview_settings["Employee"] = { add_fields: ["status", "branch", "department", "designation", "image"], filters: [["status", "=", "Active"]], - get_indicator: function (doc) { + get_indicator(doc) { return [ __(doc.status, null, "Employee"), { Active: "green", Inactive: "red", Left: "gray", Suspended: "orange" }[doc.status], "status,=," + doc.status, ]; }, + + onload(listview) { + if (frappe.perm.has_perm("Employee", 0, "create")) { + frappe.db.count("Employee").then((count) => { + if (count === 0) { + listview.page.add_inner_button(__("Import Employees"), () => { + frappe.new_doc("Data Import", { + reference_doctype: "Employee", + }); + }); + } + }); + } + }, }; diff --git a/erpnext/setup/doctype/employee/test_employee.py b/erpnext/setup/doctype/employee/test_employee.py index c022f724a66..b553898dc49 100644 --- a/erpnext/setup/doctype/employee/test_employee.py +++ b/erpnext/setup/doctype/employee/test_employee.py @@ -64,6 +64,58 @@ class TestEmployee(ERPNextTestSuite): self.assertEqual(qb_employee_list, employee_list) frappe.set_user("Administrator") + def test_create_user_automatically(self): + def get_new_employee(email: str, create_user_permission: int): + return frappe.get_doc( + { + "doctype": "Employee", + "first_name": "Test Auto User 1", + "company": "_Test Company", + "date_of_birth": "2000-05-08", + "date_of_joining": "2013-01-01", + "gender": "Female", + "personal_email": email, + "status": "Active", + "create_user_automatically": 1, + "create_user_permission": create_user_permission, + } + ).insert() + + employee1 = get_new_employee("test_auto_user1@example.com", True) + user = frappe.db.get_value("User", "test_auto_user1@example.com") + self.assertTrue(user) + self.assertEqual(employee1.user_id, user) + + # Verify user permissions are created + self.assertTrue( + frappe.db.exists( + "User Permission", {"allow": "Employee", "for_value": employee1.name, "user": user} + ) + ) + self.assertTrue( + frappe.db.exists( + "User Permission", {"allow": "Company", "for_value": employee1.company, "user": user} + ) + ) + + # Test disabled create_user_permission + employee2 = get_new_employee("test_auto_user2@example.com", False) + user2 = frappe.db.get_value("User", "test_auto_user2@example.com") + self.assertTrue(user2) + self.assertEqual(employee2.user_id, user2) + + # Verify user permissions are not created + self.assertFalse( + frappe.db.exists( + "User Permission", {"allow": "Employee", "for_value": employee2.name, "user": user2} + ) + ) + self.assertFalse( + frappe.db.exists( + "User Permission", {"allow": "Company", "for_value": employee2.company, "user": user2} + ) + ) + def make_employee(user, company=None, **kwargs): if not frappe.db.get_value("User", user): diff --git a/erpnext/setup/doctype/party_type/party_type.py b/erpnext/setup/doctype/party_type/party_type.py index 6730d1cbdce..cf72eb3bbdc 100644 --- a/erpnext/setup/doctype/party_type/party_type.py +++ b/erpnext/setup/doctype/party_type/party_type.py @@ -4,6 +4,7 @@ import frappe from frappe.model.document import Document +from frappe.query_builder import DocType class PartyType(Document): @@ -24,29 +25,36 @@ class PartyType(Document): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs -def get_party_type(doctype, txt, searchfield, start, page_len, filters): - cond = "" - account_type = None +def get_party_type(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict): + PartyType = DocType("Party Type") + get_party_type_query = frappe.qb.from_(PartyType).select(PartyType.name).orderby(PartyType.name) + + condition_list = [] if filters and filters.get("account"): account_type = frappe.db.get_value("Account", filters.get("account"), "account_type") if account_type: if account_type in ["Receivable", "Payable"]: # Include Employee regardless of its configured account_type, but still respect the text filter - cond = "and (account_type = %(account_type)s or name = 'Employee')" + condition_list.append( + (PartyType.account_type == account_type) | (PartyType.name == "Employee") + ) else: - cond = "and account_type = %(account_type)s" + condition_list.append(PartyType.account_type == account_type) - # Build parameters dictionary - params = {"txt": "%" + txt + "%", "start": start, "page_len": page_len} - if account_type: - params["account_type"] = account_type + for condition in condition_list: + get_party_type_query = get_party_type_query.where(condition) - result = frappe.db.sql( - f"""select name from `tabParty Type` - where `{searchfield}` LIKE %(txt)s {cond} - order by name limit %(page_len)s offset %(start)s""", - params, - ) + if frappe.local.lang == "en": + get_party_type_query = get_party_type_query.where(getattr(PartyType, searchfield).like(f"%{txt}%")) + get_party_type_query = get_party_type_query.limit(page_len) + get_party_type_query = get_party_type_query.offset(start) + + result = get_party_type_query.run() + else: + result = get_party_type_query.run() + test_str = txt.lower() + result = [row for row in result if test_str in frappe._(row[0]).lower()] + result = result[start : start + page_len] return result or [] diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index 726906ac6cb..69fbe650f2a 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -310,6 +310,7 @@ def set_default_print_formats(): "Purchase Order": "Purchase Order with Item Image", "Purchase Invoice": "Purchase Invoice with Item Image", "POS Invoice": "POS Invoice with Item Image", + "Request for Quotation": "Request for Quotation with Item Image", } for doctype, print_format in default_map.items(): diff --git a/erpnext/setup/setup_wizard/setup_wizard.py b/erpnext/setup/setup_wizard/setup_wizard.py index 9a49af2b10e..20330d89631 100644 --- a/erpnext/setup/setup_wizard/setup_wizard.py +++ b/erpnext/setup/setup_wizard/setup_wizard.py @@ -10,39 +10,34 @@ from erpnext.setup.setup_wizard.operations import install_fixtures as fixtures def get_setup_stages(args=None): - if frappe.db.sql("select name from tabCompany"): - stages = [ + stages = [ + { + "status": _("Installing presets"), + "fail_msg": _("Failed to install presets"), + "tasks": [{"fn": stage_fixtures, "args": args, "fail_msg": _("Failed to install presets")}], + }, + { + "status": _("Setting up company"), + "fail_msg": _("Failed to setup company"), + "tasks": [{"fn": setup_company, "args": args, "fail_msg": _("Failed to setup company")}], + }, + { + "status": _("Setting defaults"), + "fail_msg": _("Failed to set defaults"), + "tasks": [ + {"fn": setup_defaults, "args": args, "fail_msg": _("Failed to setup defaults")}, + ], + }, + ] + + if args.get("setup_demo"): + stages.append( { - "status": _("Wrapping up"), - "fail_msg": _("Failed to login"), - "tasks": [{"fn": fin, "args": args, "fail_msg": _("Failed to login")}], + "status": _("Creating demo data"), + "fail_msg": _("Failed to create demo data"), + "tasks": [{"fn": setup_demo, "args": args, "fail_msg": _("Failed to create demo data")}], } - ] - else: - stages = [ - { - "status": _("Installing presets"), - "fail_msg": _("Failed to install presets"), - "tasks": [{"fn": stage_fixtures, "args": args, "fail_msg": _("Failed to install presets")}], - }, - { - "status": _("Setting up company"), - "fail_msg": _("Failed to setup company"), - "tasks": [{"fn": setup_company, "args": args, "fail_msg": _("Failed to setup company")}], - }, - { - "status": _("Setting defaults"), - "fail_msg": "Failed to set defaults", - "tasks": [ - {"fn": setup_defaults, "args": args, "fail_msg": _("Failed to setup defaults")}, - ], - }, - { - "status": _("Wrapping up"), - "fail_msg": _("Failed to login"), - "tasks": [{"fn": fin, "args": args, "fail_msg": _("Failed to login")}], - }, - ] + ) return stages @@ -59,19 +54,8 @@ def setup_defaults(args): fixtures.install_defaults(frappe._dict(args)) -def fin(args): - frappe.local.message_log = [] - login_as_first_user(args) - - -def setup_demo(args): - if args.get("setup_demo"): - frappe.enqueue(setup_demo_data, enqueue_after_commit=True, at_front=True) - - -def login_as_first_user(args): - if args.get("email") and hasattr(frappe.local, "login_manager"): - frappe.local.login_manager.login_as(args.get("email")) +def setup_demo(args): # nosemgrep + setup_demo_data(args.get("company_name")) # Only for programmatical use @@ -79,4 +63,3 @@ def setup_complete(args=None): stage_fixtures(args) setup_company(args) setup_defaults(args) - fin(args) diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js index 17f65ce270c..018a4f86fa8 100644 --- a/erpnext/stock/dashboard/item_dashboard.js +++ b/erpnext/stock/dashboard/item_dashboard.js @@ -281,8 +281,13 @@ erpnext.stock.move_item = function (item, source, target, actual_qty, rate, stoc } dialog.set_primary_action(__("Create Stock Entry"), function () { - if (source && (dialog.get_value("qty") == 0 || dialog.get_value("qty") > actual_qty)) { - frappe.msgprint(__("Quantity must be greater than zero, and less or equal to {0}", [actual_qty])); + if (flt(dialog.get_value("qty")) <= 0) { + frappe.msgprint(__("Quantity must be greater than zero")); + return; + } + + if (source && dialog.get_value("qty") > actual_qty) { + frappe.msgprint(__("Quantity must be less than or equal to {0}", [actual_qty])); return; } diff --git a/erpnext/stock/dashboard/item_dashboard.py b/erpnext/stock/dashboard/item_dashboard.py index d77ed7a6212..5de54c55461 100644 --- a/erpnext/stock/dashboard/item_dashboard.py +++ b/erpnext/stock/dashboard/item_dashboard.py @@ -1,6 +1,6 @@ import frappe from frappe.desk.reportview import build_match_conditions -from frappe.utils import cint, flt +from frappe.utils import cint, escape_html, flt from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( get_sre_reserved_qty_for_items_and_warehouses as get_reserved_stock_details, @@ -70,8 +70,10 @@ def get_data( for item in items: item.update( { - "item_name": frappe.get_cached_value("Item", item.item_code, "item_name"), - "stock_uom": frappe.get_cached_value("Item", item.item_code, "stock_uom"), + "item_code": escape_html(item.item_code), + "item_name": escape_html(frappe.get_cached_value("Item", item.item_code, "item_name")), + "stock_uom": escape_html(frappe.get_cached_value("Item", item.item_code, "stock_uom")), + "warehouse": escape_html(item.warehouse), "disable_quick_entry": frappe.get_cached_value("Item", item.item_code, "has_batch_no") or frappe.get_cached_value("Item", item.item_code, "has_serial_no"), "projected_qty": flt(item.projected_qty, precision), diff --git a/erpnext/stock/dashboard/item_dashboard_list.html b/erpnext/stock/dashboard/item_dashboard_list.html index ae90ff80686..34d51814b2f 100644 --- a/erpnext/stock/dashboard/item_dashboard_list.html +++ b/erpnext/stock/dashboard/item_dashboard_list.html @@ -50,15 +50,15 @@ data-warehouse="{{ d.warehouse }}" data-actual_qty="{{ d.actual_qty }}" data-stock-uom="{{ d.stock_uom }}" - data-item="{{ escape(d.item_code) }}">{{ __("Move") }} + data-item="{{ d.item_code }}">{{ __("Move") }} {% endif %} {% endif %} diff --git a/erpnext/stock/dashboard/warehouse_capacity_dashboard.py b/erpnext/stock/dashboard/warehouse_capacity_dashboard.py index 75b2951e30b..39701ed3f0d 100644 --- a/erpnext/stock/dashboard/warehouse_capacity_dashboard.py +++ b/erpnext/stock/dashboard/warehouse_capacity_dashboard.py @@ -1,6 +1,6 @@ import frappe from frappe.desk.reportview import build_match_conditions -from frappe.utils import flt, nowdate +from frappe.utils import escape_html, flt, nowdate from erpnext.stock.utils import get_stock_balance @@ -75,6 +75,9 @@ def get_warehouse_capacity_data(filters, start): balance_qty = get_stock_balance(entry.item_code, entry.warehouse, nowdate()) or 0 entry.update( { + "warehouse": escape_html(entry.warehouse), + "item_code": escape_html(entry.item_code), + "company": escape_html(entry.company), "actual_qty": balance_qty, "percent_occupied": flt((flt(balance_qty) / flt(entry.stock_capacity)) * 100, 0), } diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index cbf6059a812..fb62b0eb5c0 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -22,9 +22,6 @@ from erpnext.tests.utils import ERPNextTestSuite class TestInventoryDimension(ERPNextTestSuite): - def setUp(self): - prepare_test_data() - def test_validate_inventory_dimension(self): # Can not be child doc inv_dim1 = create_inventory_dimension( @@ -77,6 +74,7 @@ class TestInventoryDimension(ERPNextTestSuite): self.assertFalse(custom_field) def test_inventory_dimension(self): + create_warehouse("Shelf Warehouse") warehouse = "Shelf Warehouse - _TC" item_code = "_Test Item" @@ -556,28 +554,6 @@ def get_voucher_sl_entries(voucher_no, fields): ) -def prepare_test_data(): - for shelf in ["Shelf 1", "Shelf 2"]: - if not frappe.db.exists("Shelf", shelf): - frappe.get_doc({"doctype": "Shelf", "shelf_name": shelf}).insert(ignore_permissions=True) - - create_warehouse("Shelf Warehouse") - - for rack in ["Rack 1", "Rack 2"]: - if not frappe.db.exists("Rack", rack): - frappe.get_doc({"doctype": "Rack", "rack_name": rack}).insert(ignore_permissions=True) - - create_warehouse("Rack Warehouse") - - for site in ["Site 1", "Site 2"]: - if not frappe.db.exists("Inv Site", site): - frappe.get_doc({"doctype": "Inv Site", "site_name": site}).insert(ignore_permissions=True) - - for store in ["Store 1", "Store 2"]: - if not frappe.db.exists("Store", store): - frappe.get_doc({"doctype": "Store", "store_name": store}).insert(ignore_permissions=True) - - def create_inventory_dimension(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index b957b905258..e4996462278 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -47,7 +47,6 @@ "column_break_cqdk", "valuation_rate", "inventory_settings_section", - "shelf_life_in_days", "end_of_life", "default_material_request_type", "column_break1", @@ -64,6 +63,7 @@ "create_new_batch", "batch_number_series", "has_expiry_date", + "shelf_life_in_days", "retain_sample", "sample_quantity", "column_break_37", @@ -334,6 +334,7 @@ "options": "fa fa-truck" }, { + "depends_on": "has_expiry_date", "fieldname": "shelf_life_in_days", "fieldtype": "Int", "label": "Shelf Life In Days", @@ -343,11 +344,13 @@ { "default": "2099-12-31", "depends_on": "is_stock_item", + "description": "Defines the date after which the item can no longer be used in transactions or manufacturing", "fieldname": "end_of_life", "fieldtype": "Date", "label": "End of Life", "oldfieldname": "end_of_life", - "oldfieldtype": "Date" + "oldfieldtype": "Date", + "show_description_on_click": 1 }, { "default": "Purchase", @@ -467,9 +470,12 @@ { "default": "0", "depends_on": "has_batch_no", + "description": "Enable to reserve a small sample from each batch for any analysis arising ahead", + "documentation_url": "https://docs.frappe.io/erpnext/retain-sample-stock", "fieldname": "retain_sample", "fieldtype": "Check", - "label": "Retain Sample" + "label": "Retain Sample", + "show_description_on_click": 1 }, { "depends_on": "eval: (doc.retain_sample && doc.has_batch_no)", @@ -989,7 +995,7 @@ "image_field": "image", "links": [], "make_attachments_public": 1, - "modified": "2026-03-17 20:39:05.218344", + "modified": "2026-03-24 15:45:40.207531", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index fc4bc589f9d..4332b7429a6 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -10,6 +10,7 @@ from frappe.query_builder.custom import ConstantColumn from frappe.utils import cint, flt import erpnext +from erpnext import is_perpetual_inventory_enabled from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -175,6 +176,9 @@ class LandedCostVoucher(Document): ) def validate_expense_accounts(self): + if not is_perpetual_inventory_enabled(self.company): + return + for t in self.taxes: company = frappe.get_cached_value("Account", t.expense_account, "company") diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index ee2b2051e8b..26df7f59135 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -180,6 +180,8 @@ class TestLandedCostVoucher(ERPNextTestSuite): self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0) def test_lcv_validates_company(self): + from erpnext import is_perpetual_inventory_enabled + from erpnext.accounts.doctype.account.test_account import create_account from erpnext.stock.doctype.landed_cost_voucher.landed_cost_voucher import ( IncorrectCompanyValidationError, ) @@ -187,6 +189,20 @@ class TestLandedCostVoucher(ERPNextTestSuite): company_a = "_Test Company" company_b = "_Test Company with perpetual inventory" + srbnb = create_account( + account_name="Stock Received But Not Billed", + account_type="Stock Received But Not Billed", + parent_account="Stock Liabilities - _TC", + company=company_a, + account_currency="INR", + ) + + epi = is_perpetual_inventory_enabled(company_a) + company_doc = frappe.get_doc("Company", company_a) + company_doc.enable_perpetual_inventory = 1 + company_doc.stock_received_but_not_billed = srbnb + company_doc.save() + pr = make_purchase_receipt( company=company_a, warehouse="Stores - _TC", @@ -212,6 +228,9 @@ class TestLandedCostVoucher(ERPNextTestSuite): distribute_landed_cost_on_items(lcv) lcv.submit() + frappe.db.set_value("Company", company_a, "enable_perpetual_inventory", epi) + frappe.local.enable_perpetual_inventory = {} + def test_landed_cost_voucher_for_zero_purchase_rate(self): "Test impact of LCV on future stock balances." from erpnext.stock.doctype.item.test_item import make_item diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index e72637901b5..c25a6ecd62d 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -1083,7 +1083,9 @@ class TestMaterialRequest(ERPNextTestSuite): pl.locations[0].qty = 2 pl.locations[0].stock_qty = 2 - self.assertRaises(frappe.ValidationError, pl.submit) + + # System should allow picking qty for excess transfer + pl.submit() def test_mr_status_with_partial_and_excess_end_transit(self): material_request = make_material_request( diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index ef80966c2a6..7c31ec7a672 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -86,6 +86,7 @@ class PickList(TransactionBase): "join_field": "material_request_item", "target_ref_field": "stock_qty", "source_field": "stock_qty", + "validate_qty": False, } ] @@ -522,8 +523,26 @@ class PickList(TransactionBase): self.item_location_map = frappe._dict() from_warehouses = [self.parent_warehouse] if self.parent_warehouse else [] - if self.parent_warehouse: - from_warehouses.extend(get_descendants_of("Warehouse", self.parent_warehouse)) + + if self.work_order: + root_warehouse = frappe.db.get_value( + "Warehouse", {"company": self.company, "parent_warehouse": ["IS", "NOT SET"], "is_group": 1} + ) + + from_warehouses = [root_warehouse] + + if from_warehouses: + from_warehouses.extend(get_descendants_of("Warehouse", from_warehouses[0])) + + item_warehouse_dict = frappe._dict() + if self.work_order: + item_warehouse_list = frappe.get_all( + "Work Order Item", + filters={"parent": self.work_order}, + fields=["item_code", "source_warehouse"], + ) + if item_warehouse_list: + item_warehouse_dict = {item.item_code: item.source_warehouse for item in item_warehouse_list} # Create replica before resetting, to handle empty table on update after submit. locations_replica = self.get("locations") @@ -541,6 +560,13 @@ class PickList(TransactionBase): len_idx = len(self.get("locations")) or 0 for item_doc in items: item_code = item_doc.item_code + priority_warehouses = [] + + if self.work_order and item_warehouse_dict.get(item_code): + source_warehouse = item_warehouse_dict.get(item_code) + priority_warehouses = [source_warehouse] + priority_warehouses.extend(get_descendants_of("Warehouse", source_warehouse)) + from_warehouses = list(dict.fromkeys(priority_warehouses + from_warehouses)) self.item_location_map.setdefault( item_code, @@ -551,6 +577,7 @@ class PickList(TransactionBase): self.company, picked_item_details=picked_items_details.get(item_code), consider_rejected_warehouses=self.consider_rejected_warehouses, + priority_warehouses=priority_warehouses, ), ) @@ -968,6 +995,7 @@ def get_available_item_locations( ignore_validation=False, picked_item_details=None, consider_rejected_warehouses=False, + priority_warehouses=None, ): locations = [] @@ -1008,7 +1036,7 @@ def get_available_item_locations( locations = filter_locations_by_picked_materials(locations, picked_item_details) if locations: - locations = get_locations_based_on_required_qty(locations, required_qty) + locations = get_locations_based_on_required_qty(locations, required_qty, priority_warehouses) if not ignore_validation: validate_picked_materials(item_code, required_qty, locations, picked_item_details) @@ -1016,9 +1044,14 @@ def get_available_item_locations( return locations -def get_locations_based_on_required_qty(locations, required_qty): +def get_locations_based_on_required_qty(locations, required_qty, priority_warehouses): filtered_locations = [] + if priority_warehouses: + priority_locations = [loc for loc in locations if loc.warehouse in priority_warehouses] + fallback_locations = [loc for loc in locations if loc.warehouse not in priority_warehouses] + locations = priority_locations + fallback_locations + for location in locations: if location.qty >= required_qty: location.qty = required_qty diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 283e0207d60..85a45f1686b 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -1050,6 +1050,53 @@ class TestPickList(ERPNextTestSuite): pl = create_pick_list(so.name) self.assertFalse(pl.locations) + def test_pick_list_warehouse_for_work_order(self): + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + from erpnext.manufacturing.doctype.work_order.work_order import create_pick_list, make_work_order + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + # Create Warehouses for Work Order + source_warehouse = create_warehouse("_Test WO Warehouse") + wip_warehouse = create_warehouse("_Test WIP Warehouse", company="_Test Company") + fg_warehouse = create_warehouse("_Test Finished Goods Warehouse", company="_Test Company") + + # Create Finished Good Item + fg_item = make_item("Test Work Order Finished Good Item", properties={"is_stock_item": 1}).name + + # Create Raw Material Item + rm_item = make_item("Test Work Order Raw Material Item", properties={"is_stock_item": 1}).name + + # Create BOM + bom = make_bom(item=fg_item, rate=100, raw_materials=[rm_item]) + + # Create Inward entry for Raw Material + make_stock_entry(item=rm_item, to_warehouse=wip_warehouse, qty=10) + make_stock_entry(item=rm_item, to_warehouse=source_warehouse, qty=10) + + # Create Work Order + wo = make_work_order(item=fg_item, qty=5, bom_no=bom.name, company="_Test Company") + wo.required_items[0].source_warehouse = source_warehouse + wo.fg_warehouse = fg_warehouse + wo.skip_transfer = True + wo.submit() + + # Create Pick List + pl = create_pick_list(wo.name, for_qty=wo.qty) + + # System prioritises the Source Warehouse + self.assertEqual(pl.locations[0].warehouse, source_warehouse) + self.assertEqual(pl.locations[0].item_code, rm_item) + self.assertEqual(pl.locations[0].qty, 5) + + # Create Outward Entry from Source Warehouse + make_stock_entry(item=rm_item, from_warehouse=source_warehouse, qty=10) + pl.set_item_locations() + + # System should pick other available warehouses + self.assertEqual(pl.locations[0].warehouse, wip_warehouse) + self.assertEqual(pl.locations[0].item_code, rm_item) + self.assertEqual(pl.locations[0].qty, 5) + def test_pick_list_validation_for_serial_no(self): warehouse = "_Test Warehouse - _TC" item = make_item( diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 74cdfb38f78..6eba41c3883 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1240,6 +1240,65 @@ class TestPurchaseReceipt(ERPNextTestSuite): pr.cancel() + def test_item_valuation_with_deduct_valuation_and_total_tax(self): + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work In Progress - TCP1", + qty=5, + rate=100, + do_not_save=1, + ) + + pr.append( + "taxes", + { + "charge_type": "Actual", + "add_deduct_tax": "Deduct", + "account_head": "_Test Account Shipping Charges - TCP1", + "category": "Valuation and Total", + "cost_center": "Main - TCP1", + "description": "Valuation Discount", + "tax_amount": 20, + }, + ) + + pr.insert() + + self.assertAlmostEqual(pr.items[0].item_tax_amount, -20.0, places=2) + self.assertAlmostEqual(pr.items[0].valuation_rate, 96.0, places=2) + + pr.delete() + + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work In Progress - TCP1", + qty=5, + rate=100, + do_not_save=1, + ) + + pr.append( + "taxes", + { + "charge_type": "On Net Total", + "add_deduct_tax": "Deduct", + "account_head": "_Test Account Shipping Charges - TCP1", + "category": "Valuation and Total", + "cost_center": "Main - TCP1", + "description": "Valuation Discount", + "rate": 10, + }, + ) + + pr.insert() + + self.assertAlmostEqual(pr.items[0].item_tax_amount, -50.0, places=2) + self.assertAlmostEqual(pr.items[0].valuation_rate, 90.0, places=2) + + pr.delete() + def test_po_to_pi_and_po_to_pr_worflow_full(self): """Test following behaviour: - Create PO diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js index 0f64949f621..134903f2309 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js @@ -69,9 +69,15 @@ frappe.ui.form.on("Repost Item Valuation", { } if (frm.doc.status == "In Progress") { - frm.doc.current_index = data.current_index; - frm.doc.items_to_be_repost = data.items_to_be_repost; - frm.doc.total_reposting_count = data.total_reposting_count; + if (data.current_index) { + frm.doc.current_index = data.current_index; + frm.doc.items_to_be_repost = data.items_to_be_repost; + } + + if (data.vouchers_posted) { + frm.doc.total_vouchers = data.total_vouchers; + frm.doc.vouchers_posted = data.vouchers_posted; + } frm.dashboard.reset(); frm.trigger("show_reposting_progress"); @@ -108,15 +114,31 @@ frappe.ui.form.on("Repost Item Valuation", { show_reposting_progress: function (frm) { var bars = []; - + let title = ""; + let progress = 0.0; let total_count = frm.doc.items_to_be_repost ? JSON.parse(frm.doc.items_to_be_repost).length : 0; - if (frm.doc?.total_reposting_count) { - total_count = frm.doc.total_reposting_count; + if (total_count > 1) { + progress = flt((cint(frm.doc.current_index) / total_count) * 100, 2) || 0.5; + title = __("Reposting for Item-Wh Completed {0}%", [progress]); + + bars.push({ + title: title, + width: progress + "%", + progress_class: "progress-bar-success", + }); + + frm.dashboard.add_progress(__("Reposting Progress"), bars); } - let progress = flt((cint(frm.doc.current_index) / total_count) * 100, 2) || 0.5; - var title = __("Reposting Completed {0}%", [progress]); + if (!frm.doc.vouchers_posted) { + return; + } + + // Show voucher posting progress if vouchers are being reposted + bars = []; + progress = flt((cint(frm.doc.vouchers_posted) / cint(frm.doc.total_vouchers)) * 100, 2) || 0.5; + title = __("Reposting for Vouchers Completed {0}%", [progress]); bars.push({ title: title, @@ -124,7 +146,7 @@ frappe.ui.form.on("Repost Item Valuation", { progress_class: "progress-bar-success", }); - frm.dashboard.add_progress(__("Reposting Progress"), bars); + frm.dashboard.add_progress(__("Reposting Vouchers Progress"), bars); }, restart_reposting: function (frm) { diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json index e1ae6d00cd9..ce43ae3a54f 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json @@ -27,14 +27,16 @@ "error_section", "error_log", "reposting_info_section", - "reposting_data_file", "items_to_be_repost", - "distinct_item_and_warehouse", "column_break_o1sj", "total_reposting_count", "current_index", "gl_reposting_index", - "affected_transactions" + "reposting_data_file", + "vouchers_based_on_item_and_warehouse_section", + "total_vouchers", + "column_break_yqwo", + "vouchers_posted" ], "fields": [ { @@ -167,15 +169,6 @@ "print_hide": 1, "read_only": 1 }, - { - "fieldname": "distinct_item_and_warehouse", - "fieldtype": "Code", - "hidden": 1, - "label": "Distinct Item and Warehouse", - "no_copy": 1, - "print_hide": 1, - "read_only": 1 - }, { "fieldname": "current_index", "fieldtype": "Int", @@ -185,14 +178,6 @@ "print_hide": 1, "read_only": 1 }, - { - "fieldname": "affected_transactions", - "fieldtype": "Code", - "hidden": 1, - "label": "Affected Transactions", - "no_copy": 1, - "read_only": 1 - }, { "default": "0", "fieldname": "gl_reposting_index", @@ -205,7 +190,7 @@ { "fieldname": "reposting_info_section", "fieldtype": "Section Break", - "label": "Reposting Info" + "label": "Reposting Item and Warehouse" }, { "fieldname": "column_break_o1sj", @@ -214,14 +199,7 @@ { "fieldname": "total_reposting_count", "fieldtype": "Int", - "label": "Total Reposting Count", - "no_copy": 1, - "read_only": 1 - }, - { - "fieldname": "reposting_data_file", - "fieldtype": "Attach", - "label": "Reposting Data File", + "label": "No of Items to Repost", "no_copy": 1, "read_only": 1 }, @@ -247,13 +225,44 @@ "fieldname": "repost_only_accounting_ledgers", "fieldtype": "Check", "label": "Repost Only Accounting Ledgers" + }, + { + "fieldname": "vouchers_based_on_item_and_warehouse_section", + "fieldtype": "Section Break", + "hidden": 1, + "label": "Reposting Vouchers" + }, + { + "fieldname": "total_vouchers", + "fieldtype": "Int", + "label": "Total Ledgers", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_yqwo", + "fieldtype": "Column Break" + }, + { + "fieldname": "vouchers_posted", + "fieldtype": "Int", + "label": "Ledgers Posted", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "reposting_data_file", + "fieldtype": "Attach", + "label": "Reposting Data File", + "no_copy": 1, + "read_only": 1 } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2026-02-25 14:22:21.681549", + "modified": "2026-03-27 18:59:58.637964", "modified_by": "Administrator", "module": "Stock", "name": "Repost Item Valuation", 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 f5b4ef3e8f5..2b4d5c28692 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -35,14 +35,12 @@ class RepostItemValuation(Document): if TYPE_CHECKING: from frappe.types import DF - affected_transactions: DF.Code | None allow_negative_stock: DF.Check allow_zero_rate: DF.Check amended_from: DF.Link | None based_on: DF.Literal["Transaction", "Item and Warehouse"] company: DF.Link | None current_index: DF.Int - distinct_item_and_warehouse: DF.Code | None error_log: DF.LongText | None gl_reposting_index: DF.Int item_code: DF.Link | None @@ -55,9 +53,11 @@ class RepostItemValuation(Document): reposting_reference: DF.Data | None status: DF.Literal["Queued", "In Progress", "Completed", "Skipped", "Failed", "Cancelled"] total_reposting_count: DF.Int + total_vouchers: DF.Int via_landed_cost_voucher: DF.Check voucher_no: DF.DynamicLink | None voucher_type: DF.Link | None + vouchers_posted: DF.Int warehouse: DF.Link | None # end: auto-generated types @@ -261,6 +261,8 @@ class RepostItemValuation(Document): self.items_to_be_repost = None self.gl_reposting_index = 0 self.total_reposting_count = 0 + self.total_vouchers = 0 + self.vouchers_posted = 0 self.clear_attachment() self.db_update() @@ -435,7 +437,7 @@ def repost_sl_entries(doc): ) else: repost_future_sle( - args=[ + items_to_be_repost=[ frappe._dict( { "item_code": doc.item_code, diff --git a/erpnext/stock/doctype/shipment_parcel/shipment_parcel.json b/erpnext/stock/doctype/shipment_parcel/shipment_parcel.json index 321599e2b4b..32d0df2c873 100644 --- a/erpnext/stock/doctype/shipment_parcel/shipment_parcel.json +++ b/erpnext/stock/doctype/shipment_parcel/shipment_parcel.json @@ -14,19 +14,19 @@ "fields": [ { "fieldname": "length", - "fieldtype": "Int", + "fieldtype": "Float", "in_list_view": 1, "label": "Length (cm)" }, { "fieldname": "width", - "fieldtype": "Int", + "fieldtype": "Float", "in_list_view": 1, "label": "Width (cm)" }, { "fieldname": "height", - "fieldtype": "Int", + "fieldtype": "Float", "in_list_view": 1, "label": "Height (cm)" }, @@ -49,7 +49,7 @@ ], "istable": 1, "links": [], - "modified": "2024-03-27 13:10:41.396354", + "modified": "2026-03-29 00:00:00.000000", "modified_by": "Administrator", "module": "Stock", "name": "Shipment Parcel", diff --git a/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json index 9eb9ba46762..6e55b59a497 100644 --- a/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json +++ b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json @@ -15,21 +15,21 @@ "fields": [ { "fieldname": "length", - "fieldtype": "Int", + "fieldtype": "Float", "in_list_view": 1, "label": "Length (cm)", "reqd": 1 }, { "fieldname": "width", - "fieldtype": "Int", + "fieldtype": "Float", "in_list_view": 1, "label": "Width (cm)", "reqd": 1 }, { "fieldname": "height", - "fieldtype": "Int", + "fieldtype": "Float", "in_list_view": 1, "label": "Height (cm)", "reqd": 1 @@ -52,7 +52,7 @@ } ], "links": [], - "modified": "2024-03-27 13:10:41.521126", + "modified": "2026-03-29 00:00:00.000000", "modified_by": "Administrator", "module": "Stock", "name": "Shipment Parcel Template", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 4fdd9df1adf..f71b67e1127 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -38,18 +38,18 @@ frappe.ui.form.on("Stock Entry", { frm.set_query("source_warehouse_address", function () { return { + query: "erpnext.controllers.queries.get_warehouse_address", filters: { - link_doctype: "Warehouse", - link_name: frm.doc.from_warehouse, + warehouse: frm.doc.from_warehouse, }, }; }); frm.set_query("target_warehouse_address", function () { return { + query: "erpnext.controllers.queries.get_warehouse_address", filters: { - link_doctype: "Warehouse", - link_name: frm.doc.to_warehouse, + warehouse: frm.doc.to_warehouse, }, }; }); diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 5d8ebdda56f..48488a7c5b6 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -2422,12 +2422,11 @@ class TestStockEntry(ERPNextTestSuite): Unit test case to check the document naming rule with company condition For Quality Inspection, when created from Stock Entry. """ - from erpnext.accounts.report.trial_balance.test_trial_balance import create_company from erpnext.controllers.stock_controller import make_quality_inspections from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse # create a separate company to handle document naming rule with company condition - qc_company = create_company(company_name="Test Quality Company") + qc_company = "Test Quality Company" # create document naming rule based on that for Quality Inspection Doctype qc_naming_rule = frappe.new_doc( diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index c6990c9492d..2ea52e91a8d 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -7,6 +7,7 @@ from operator import itemgetter import frappe from frappe import _ +from frappe.query_builder.functions import Count from frappe.utils import cint, date_diff, flt, get_datetime from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -240,9 +241,9 @@ class FIFOSlots: Returns dict of the foll.g structure: Key = Item A / (Item A, Warehouse A) Key: { - 'details' -> Dict: ** item details **, - 'fifo_queue' -> List: ** list of lists containing entries/slots for existing stock, - consumed/updated and maintained via FIFO. ** + 'details' -> Dict: ** item details **, + 'fifo_queue' -> List: ** list of lists containing entries/slots for existing stock, + consumed/updated and maintained via FIFO. ** } """ from erpnext.stock.serial_batch_bundle import get_serial_nos_from_bundle @@ -253,16 +254,33 @@ class FIFOSlots: if stock_ledger_entries is None: bundle_wise_serial_nos = self.__get_bundle_wise_serial_nos() + # prepare single sle voucher detail lookup + self.prepare_stock_reco_voucher_wise_count() + with frappe.db.unbuffered_cursor(): if stock_ledger_entries is None: stock_ledger_entries = self.__get_stock_ledger_entries() for d in stock_ledger_entries: key, fifo_queue, transferred_item_key = self.__init_key_stores(d) + prev_balance_qty = self.item_details[key].get("qty_after_transaction", 0) - if d.voucher_type == "Stock Reconciliation": + if d.voucher_type == "Stock Reconciliation" and ( + not d.batch_no or d.serial_no or d.serial_and_batch_bundle + ): + if d.voucher_detail_no in self.stock_reco_voucher_wise_count: + # for legacy recon with single sle has qty_after_transaction and stock_value_difference without outward entry + # for exisitng handle emptying the existing queue and details. + d.stock_value_difference = flt(d.qty_after_transaction * d.valuation_rate) + d.actual_qty = d.qty_after_transaction + self.item_details[key]["qty_after_transaction"] = 0 + self.item_details[key]["total_qty"] = 0 + fifo_queue.clear() + else: + d.actual_qty = flt(d.qty_after_transaction) - flt(prev_balance_qty) + + elif d.voucher_type == "Stock Reconciliation": # get difference in qty shift as actual qty - prev_balance_qty = self.item_details[key].get("qty_after_transaction", 0) d.actual_qty = flt(d.qty_after_transaction) - flt(prev_balance_qty) serial_nos = get_serial_nos(d.serial_no) if d.serial_no else [] @@ -280,6 +298,14 @@ class FIFOSlots: self.__update_balances(d, key) + # handle serial nos misconsumption + if d.has_serial_no: + qty_after = cint(self.item_details[key]["qty_after_transaction"]) + if qty_after <= 0: + fifo_queue.clear() + elif len(fifo_queue) > qty_after: + fifo_queue[:] = fifo_queue[:qty_after] + # Note that stock_ledger_entries is an iterator, you can not reuse it like a list del stock_ledger_entries @@ -406,7 +432,6 @@ class FIFOSlots: def __update_balances(self, row: dict, key: tuple | str): self.item_details[key]["qty_after_transaction"] = row.qty_after_transaction - if "total_qty" not in self.item_details[key]: self.item_details[key]["total_qty"] = row.actual_qty else: @@ -462,6 +487,7 @@ class FIFOSlots: sle.posting_date, sle.voucher_type, sle.voucher_no, + sle.voucher_detail_no, sle.serial_no, sle.batch_no, sle.qty_after_transaction, @@ -558,3 +584,36 @@ class FIFOSlots: warehouse_results = [x[0] for x in warehouse_results] return sle_query.where(sle.warehouse.isin(warehouse_results)) + + def prepare_stock_reco_voucher_wise_count(self): + self.stock_reco_voucher_wise_count = frappe._dict() + + doctype = frappe.qb.DocType("Stock Ledger Entry") + item = frappe.qb.DocType("Item") + + query = ( + frappe.qb.from_(doctype) + .inner_join(item) + .on(doctype.item_code == item.name) + .select(doctype.voucher_detail_no, Count(doctype.name).as_("count")) + .where( + (doctype.voucher_type == "Stock Reconciliation") + & (doctype.docstatus < 2) + & (doctype.is_cancelled == 0) + ) + .groupby(doctype.voucher_detail_no) + ) + + data = query.run(as_dict=True) + if not data: + return + + for row in data: + if row.count != 1: + continue + + sr_item = frappe.db.get_value( + "Stock Reconciliation Item", row.voucher_detail_no, ["current_qty", "qty"], as_dict=True + ) + if sr_item.qty and sr_item.current_qty: + self.stock_reco_voucher_wise_count[row.voucher_detail_no] = sr_item.current_qty diff --git a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py index 97243d57001..e0d39c5dc7a 100644 --- a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py +++ b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py @@ -248,12 +248,7 @@ def get_item_warehouse_combinations(filters: dict | None = None) -> dict: bin.warehouse, item.valuation_method, ) - .where( - (item.is_stock_item == 1) - & (item.has_serial_no == 0) - & (warehouse.is_group == 0) - & (warehouse.company == filters.company) - ) + .where((item.is_stock_item == 1) & (warehouse.is_group == 0) & (warehouse.company == filters.company)) ) if filters.item_code: diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index b2401da4f8f..7f6deda9b8c 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -31,7 +31,8 @@ def repost(only_actual=False, allow_negative_stock=False, allow_zero_rate=False, for d in item_warehouses: try: repost_stock(d[0], d[1], allow_zero_rate, only_actual, only_bin, allow_negative_stock) - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() except Exception: frappe.db.rollback() diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 1e6cec59a5c..c9114355a58 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -4,6 +4,7 @@ import copy import gzip import json +from collections import deque import frappe from frappe import _, bold, scrub @@ -16,6 +17,7 @@ from frappe.utils import ( cstr, flt, format_date, + get_datetime, get_link_to_form, getdate, now, @@ -77,9 +79,6 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc future_sle_exists(args, sl_entries) for sle in sl_entries: - if sle.serial_no and not via_landed_cost_voucher: - validate_serial_no(sle) - if cancelled: sle["actual_qty"] = -flt(sle.get("actual_qty")) @@ -160,35 +159,6 @@ def get_args_for_future_sle(row): ) -def validate_serial_no(sle): - from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos - - for sn in get_serial_nos(sle.serial_no): - args = copy.deepcopy(sle) - args.serial_no = sn - args.warehouse = "" - - vouchers = [] - for row in get_stock_ledger_entries(args, ">"): - voucher_type = frappe.bold(row.voucher_type) - voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no)) - vouchers.append(f"{voucher_type} {voucher_no}") - - if vouchers: - serial_no = frappe.bold(sn) - msg = ( - f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first. - The list of the transactions are as below.""" - + "

" - - title = "Cannot Submit" if not sle.get("is_cancelled") else "Cannot Cancel" - frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction) - - def validate_cancellation(kargs): if kargs[0].get("is_cancelled"): repost_entry = frappe.db.get_value( @@ -242,146 +212,96 @@ def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False): def repost_future_sle( - args=None, + items_to_be_repost=None, voucher_type=None, voucher_no=None, allow_negative_stock=None, via_landed_cost_voucher=False, doc=None, ): - if not args: - args = [] # set args to empty list if None to avoid enumerate error - reposting_data = {} + if not items_to_be_repost: + items_to_be_repost = get_items_to_be_repost( + voucher_type=voucher_type, voucher_no=voucher_no, doc=doc, reposting_data=reposting_data + ) + if doc and doc.reposting_data_file: reposting_data = get_reposting_data(doc.reposting_data_file) - items_to_be_repost = get_items_to_be_repost( - voucher_type=voucher_type, voucher_no=voucher_no, doc=doc, reposting_data=reposting_data + repost_affected_transaction = get_affected_transactions(doc, reposting_data) or set() + resume_item_wh_wise_last_posted_sle = ( + get_item_wh_wise_last_posted_sle_from_reposting_data(doc, reposting_data) or {} ) - if items_to_be_repost: - args = items_to_be_repost - - distinct_item_warehouses = get_distinct_item_warehouse(args, doc, reposting_data=reposting_data) - affected_transactions = get_affected_transactions(doc, reposting_data=reposting_data) - - i = get_current_index(doc) or 0 - while i < len(args): - validate_item_warehouse(args[i]) + if not items_to_be_repost: + return + index = get_current_index(doc) or 0 + while index < len(items_to_be_repost): obj = update_entries_after( { - "item_code": args[i].get("item_code"), - "warehouse": args[i].get("warehouse"), - "posting_date": args[i].get("posting_date"), - "posting_time": args[i].get("posting_time"), - "creation": args[i].get("creation"), - "distinct_item_warehouses": distinct_item_warehouses, - "items_to_be_repost": args, - "current_index": i, + "item_code": items_to_be_repost[index].get("item_code"), + "warehouse": items_to_be_repost[index].get("warehouse"), + "posting_date": items_to_be_repost[index].get("posting_date"), + "posting_time": items_to_be_repost[index].get("posting_time"), + "creation": items_to_be_repost[index].get("creation"), + "current_idx": index, + "items_to_be_repost": items_to_be_repost, + "repost_doc": doc, + "repost_affected_transaction": repost_affected_transaction, + "item_wh_wise_last_posted_sle": resume_item_wh_wise_last_posted_sle, }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher, ) - affected_transactions.update(obj.affected_transactions) - key = (args[i].get("item_code"), args[i].get("warehouse")) - if distinct_item_warehouses.get(key): - distinct_item_warehouses[key].reposting_status = True + index += 1 - if obj.new_items_found: - for _item_wh, data in distinct_item_warehouses.items(): - if ("args_idx" not in data and not data.reposting_status) or ( - data.sle_changed and data.reposting_status - ): - data.args_idx = len(args) - args.append(data.sle) - elif data.sle_changed and not data.reposting_status: - args[data.args_idx] = data.sle - - data.sle_changed = False - i += 1 - - if doc: - update_args_in_repost_item_valuation( - doc, i, args, distinct_item_warehouses, affected_transactions - ) + resume_item_wh_wise_last_posted_sle = {} + repost_affected_transaction.update(obj.repost_affected_transaction) + update_args_in_repost_item_valuation(doc, index, items_to_be_repost, repost_affected_transaction) -def get_reposting_data(file_path) -> dict: - file_name = frappe.db.get_value( - "File", +def update_args_in_repost_item_valuation( + doc, + index, + items_to_be_repost, + repost_affected_transaction, + item_wh_wise_last_posted_sle=None, + only_affected_transaction=False, +): + file_name = "" + has_file = False + + if not item_wh_wise_last_posted_sle: + item_wh_wise_last_posted_sle = {} + + if doc.reposting_data_file: + has_file = True + + if doc.reposting_data_file: + file_name = get_reposting_file_name(doc.doctype, doc.name) + # frappe.delete_doc("File", file_name, ignore_permissions=True, delete_permanently=True) + + doc.reposting_data_file = create_json_gz_file( { - "file_url": file_path, - "attached_to_field": "reposting_data_file", + "repost_affected_transaction": repost_affected_transaction, + "item_wh_wise_last_posted_sle": {str(k): v for k, v in item_wh_wise_last_posted_sle.items()} + or {}, }, - "name", + doc, + file_name, ) - if not file_name: - return frappe._dict() - - attached_file = frappe.get_doc("File", file_name) - - content = attached_file.get_content() - if isinstance(content, str): - content = content.encode("utf-8") - - try: - data = gzip.decompress(content) - except Exception: - return frappe._dict() - - if data := json.loads(data.decode("utf-8")): - data = data - - return parse_json(data) - - -def validate_item_warehouse(args): - for field in ["item_code", "warehouse", "posting_date", "posting_time"]: - if args.get(field) in [None, ""]: - validation_msg = f"The field {frappe.unscrub(field)} is required for the reposting" - frappe.throw(_(validation_msg)) - - -def update_args_in_repost_item_valuation(doc, index, args, distinct_item_warehouses, affected_transactions): - if not doc.items_to_be_repost: - file_name = "" - if doc.reposting_data_file: - file_name = get_reposting_file_name(doc.doctype, doc.name) - # frappe.delete_doc("File", file_name, ignore_permissions=True, delete_permanently=True) - - doc.reposting_data_file = create_json_gz_file( - { - "items_to_be_repost": args, - "distinct_item_and_warehouse": {str(k): v for k, v in distinct_item_warehouses.items()}, - "affected_transactions": affected_transactions, - }, - doc, - file_name, - ) - + if not only_affected_transaction or not has_file: doc.db_set( { "current_index": index, - "total_reposting_count": len(args), + "items_to_be_repost": frappe.as_json(items_to_be_repost), + "total_reposting_count": len(items_to_be_repost), "reposting_data_file": doc.reposting_data_file, } ) - else: - doc.db_set( - { - "items_to_be_repost": json.dumps(args, default=str), - "distinct_item_and_warehouse": json.dumps( - {str(k): v for k, v in distinct_item_warehouses.items()}, default=str - ), - "current_index": index, - "affected_transactions": frappe.as_json(affected_transactions), - } - ) - if not frappe.in_test: frappe.db.commit() @@ -389,9 +309,8 @@ def update_args_in_repost_item_valuation(doc, index, args, distinct_item_warehou "item_reposting_progress", { "name": doc.name, - "items_to_be_repost": json.dumps(args, default=str), "current_index": index, - "total_reposting_count": len(args), + "total_reposting_count": len(items_to_be_repost), }, doctype=doc.doctype, docname=doc.name, @@ -448,23 +367,27 @@ def create_file(doc, compressed_content): return _file.file_url -def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None, reposting_data=None): - if not reposting_data and doc and doc.reposting_data_file: - reposting_data = get_reposting_data(doc.reposting_data_file) +def validate_item_warehouse(args): + for field in ["item_code", "warehouse", "posting_date", "posting_time"]: + if args.get(field) in [None, ""]: + validation_msg = f"The field {frappe.unscrub(field)} is required for the reposting" + frappe.throw(_(validation_msg)) + +def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None, reposting_data=None): if reposting_data and reposting_data.items_to_be_repost: return reposting_data.items_to_be_repost items_to_be_repost = [] if doc and doc.items_to_be_repost: - items_to_be_repost = json.loads(doc.items_to_be_repost) or [] + items_to_be_repost = json.loads(doc.items_to_be_repost) if not items_to_be_repost and voucher_type and voucher_no: items_to_be_repost = frappe.db.get_all( "Stock Ledger Entry", filters={"voucher_type": voucher_type, "voucher_no": voucher_no}, - fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"], + fields=["item_code", "warehouse", "posting_date", "posting_time", "creation", "posting_datetime"], order_by="creation asc", group_by="item_code, warehouse", ) @@ -472,51 +395,54 @@ def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None, reposti return items_to_be_repost or [] -def get_distinct_item_warehouse(args=None, doc=None, reposting_data=None): - if not reposting_data and doc and doc.reposting_data_file: - reposting_data = get_reposting_data(doc.reposting_data_file) - - if reposting_data and reposting_data.distinct_item_and_warehouse: - return parse_distinct_items_and_warehouses(reposting_data.distinct_item_and_warehouse) - - distinct_item_warehouses = {} - - if doc and doc.distinct_item_and_warehouse: - distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse) - distinct_item_warehouses = { - frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items() - } - else: - for i, d in enumerate(args): - distinct_item_warehouses.setdefault( - (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i}) - ) - - return distinct_item_warehouses - - -def parse_distinct_items_and_warehouses(distinct_items_and_warehouses): - new_dict = frappe._dict({}) - - # convert string keys to tuple - for k, v in distinct_items_and_warehouses.items(): - new_dict[frappe.safe_eval(k)] = frappe._dict(v) - - return new_dict - - def get_affected_transactions(doc, reposting_data=None) -> set[tuple[str, str]]: if not reposting_data and doc and doc.reposting_data_file: reposting_data = get_reposting_data(doc.reposting_data_file) - if reposting_data and reposting_data.affected_transactions: - return {tuple(transaction) for transaction in reposting_data.affected_transactions} + if reposting_data and reposting_data.repost_affected_transaction: + return {tuple(transaction) for transaction in reposting_data.repost_affected_transaction} - if not doc.affected_transactions: - return set() + return set() - transactions = frappe.parse_json(doc.affected_transactions) - return {tuple(transaction) for transaction in transactions} + +def get_item_wh_wise_last_posted_sle_from_reposting_data(doc, reposting_data=None): + if not reposting_data and doc and doc.reposting_data_file: + reposting_data = get_reposting_data(doc.reposting_data_file) + + if reposting_data and reposting_data.item_wh_wise_last_posted_sle: + return frappe._dict(reposting_data.item_wh_wise_last_posted_sle) + + return frappe._dict() + + +def get_reposting_data(file_path) -> dict: + file_name = frappe.db.get_value( + "File", + { + "file_url": file_path, + "attached_to_field": "reposting_data_file", + }, + "name", + ) + + if not file_name: + return frappe._dict() + + attached_file = frappe.get_doc("File", file_name) + + content = attached_file.get_content() + if isinstance(content, str): + content = content.encode("utf-8") + + try: + data = gzip.decompress(content) + except Exception: + return frappe._dict() + + if data := json.loads(data.decode("utf-8")): + data = data + + return parse_json(data) def get_current_index(doc=None): @@ -552,6 +478,10 @@ class update_entries_after: self.allow_zero_rate = allow_zero_rate self.via_landed_cost_voucher = via_landed_cost_voucher self.item_code = args.get("item_code") + self.stock_ledgers_to_repost = [] + self.current_idx = args.get("current_idx", 0) + self.repost_doc = args.get("repost_doc") or None + self.items_to_be_repost = args.get("items_to_be_repost") or None self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed( item_code=self.item_code @@ -561,17 +491,20 @@ class update_entries_after: if self.args.sle_id: self.args["name"] = self.args.sle_id + self.prev_sle_dict = frappe._dict({}) self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company") self.set_precision() self.valuation_method = get_valuation_method(self.item_code, self.company) + self.repost_affected_transaction = args.get("repost_affected_transaction") or set() self.new_items_found = False - self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict()) - self.affected_transactions: set[tuple[str, str]] = set() self.reserved_stock = self.get_reserved_stock() self.data = frappe._dict() - self.initialize_previous_data(self.args) + + if not self.repost_doc or not self.args.get("item_wh_wise_last_posted_sle"): + self.initialize_previous_data(self.args) + self.build() def get_reserved_stock(self): @@ -621,7 +554,14 @@ class update_entries_after: """ self.data.setdefault(args.warehouse, frappe._dict()) warehouse_dict = self.data[args.warehouse] + + if self.stock_ledgers_to_repost: + return + previous_sle = get_previous_sle_of_current_voucher(args) + if previous_sle: + self.prev_sle_dict[(args.get("item_code"), args.get("warehouse"))] = previous_sle + warehouse_dict.previous_sle = previous_sle for key in ("qty_after_transaction", "valuation_rate", "stock_value"): @@ -643,27 +583,185 @@ class update_entries_after: if not future_sle_exists(self.args): self.update_bin() else: - entries_to_fix = self.get_future_entries_to_fix() + self.item_wh_wise_last_posted_sle = self.get_item_wh_wise_last_posted_sle() + _item_wh_sle = self.sort_sles(self.item_wh_wise_last_posted_sle.values()) - i = 0 - while i < len(entries_to_fix): - sle = entries_to_fix[i] - i += 1 + while _item_wh_sle: + self.initialize_reposting() + sle_dict = _item_wh_sle.pop(0) + self.repost_stock_ledgers(sle_dict) - self.process_sle(sle) - self.update_bin_data(sle) - - if sle.dependant_sle_voucher_detail_no: - entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle) - if sle.voucher_type == "Stock Entry" and is_repack_entry(sle.voucher_no): - # for repack entries, we need to repost both source and target warehouses - self.update_distinct_item_warehouses_for_repack(sle) + self.update_bin() + self.reset_vouchers_and_idx() + self.update_data_in_repost() if self.exceptions: self.raise_exceptions() - def update_distinct_item_warehouses_for_repack(self, sle): - sles = ( + def initialize_reposting(self): + self._sles = [] + self.distinct_sles = set() + self.distinct_dependant_item_wh = set() + self.prev_sle_dict = frappe._dict({}) + + def get_item_wh_wise_last_posted_sle(self): + if self.args and self.args.get("item_wh_wise_last_posted_sle"): + _sles = {} + for key, sle in self.args.get("item_wh_wise_last_posted_sle").items(): + _sles[frappe.safe_eval(key)] = frappe._dict(sle) + + return _sles + + return { + (self.args.item_code, self.args.warehouse): frappe._dict( + { + "item_code": self.args.item_code, + "warehouse": self.args.warehouse, + "posting_datetime": get_combine_datetime(self.args.posting_date, self.args.posting_time), + "posting_date": self.args.posting_date, + "posting_time": self.args.posting_time, + "creation": self.args.creation, + } + ) + } + + def repost_stock_ledgers(self, sle_dict=None): + self._sles = self.get_future_entries_to_repost(sle_dict) + + if not isinstance(self._sles, deque): + self._sles = deque(self._sles) + + i = 0 + while self._sles: + sle = self._sles.popleft() + i += 1 + if sle.name in self.distinct_sles: + continue + + item_wh_key = (sle.item_code, sle.warehouse) + if item_wh_key not in self.prev_sle_dict: + self.prev_sle_dict[item_wh_key] = get_previous_sle_of_current_voucher(sle) + + self.repost_stock_ledger_entry(sle) + + # To avoid duplicate reposting of same sle in case of multiple dependant sle + self.distinct_sles.add(sle.name) + + if sle.dependant_sle_voucher_detail_no: + self.include_dependant_sle_in_reposting(sle) + self.update_item_wh_wise_last_posted_sle(sle) + + if i % 1000 == 0: + self.update_data_in_repost(len(self._sles), i) + + def sort_sles(self, sles): + return sorted( + sles, + key=lambda d: ( + get_datetime(d.posting_datetime), + get_datetime(d.creation), + ), + ) + + def include_dependant_sle_in_reposting(self, sle): + repost_dependant_sle = False + if sle.voucher_type == "Stock Entry" and is_repack_entry(sle.voucher_no): + repack_sles = self.get_sles_for_repack(sle) + for repack_sle in repack_sles: + if (repack_sle.item_code, repack_sle.warehouse) in self.distinct_dependant_item_wh: + continue + + repost_dependant_sle = True + self.distinct_dependant_item_wh.add((repack_sle.item_code, repack_sle.warehouse)) + self._sles.extend(self.get_future_entries_to_repost(repack_sle)) + else: + dependant_sles = get_sle_by_voucher_detail_no(sle.dependant_sle_voucher_detail_no) + for depend_sle in dependant_sles: + if (depend_sle.item_code, depend_sle.warehouse) in self.distinct_dependant_item_wh: + continue + + repost_dependant_sle = True + self.distinct_dependant_item_wh.add((depend_sle.item_code, depend_sle.warehouse)) + self._sles.extend(self.get_future_entries_to_repost(depend_sle)) + + if repost_dependant_sle: + self._sles = deque(self.sort_sles(self._sles)) + + def repost_stock_ledger_entry(self, sle): + if self.args.item_code != sle.item_code or self.args.warehouse != sle.warehouse: + self.repost_affected_transaction.add((sle.voucher_type, sle.voucher_no)) + + if isinstance(sle, dict): + sle = frappe._dict(sle) + + self.process_sle(sle) + self.update_item_wh_wise_last_posted_sle(sle) + + def update_item_wh_wise_last_posted_sle(self, sle): + if not self._sles: + self.item_wh_wise_last_posted_sle = frappe._dict() + return + + self.item_wh_wise_last_posted_sle[(sle.item_code, sle.warehouse)] = frappe._dict( + { + "item_code": sle.item_code, + "warehouse": sle.warehouse, + "posting_date": sle.posting_date, + "posting_time": sle.posting_time, + "posting_datetime": sle.posting_datetime + or get_combine_datetime(sle.posting_date, sle.posting_time), + "creation": sle.creation, + } + ) + + def reset_vouchers_and_idx(self): + self.stock_ledgers_to_repost = [] + self.prev_sle_dict = frappe._dict() + self.item_wh_wise_last_posted_sle = frappe._dict() + + def update_data_in_repost(self, total_sles=None, index=None): + if not self.repost_doc: + return + + values_to_update = { + "total_vouchers": cint(total_sles) + cint(index), + "vouchers_posted": index or 0, + } + + self.repost_doc.db_set(values_to_update) + + update_args_in_repost_item_valuation( + self.repost_doc, + self.current_idx, + self.items_to_be_repost, + self.repost_affected_transaction, + self.item_wh_wise_last_posted_sle, + only_affected_transaction=True, + ) + + if not frappe.in_test: + # To maintain the state of the reposting, so if timeout happens, it can be resumed from the last posted voucher + frappe.db.commit() # nosemgrep + + self.publish_real_time_progress(total_sles=total_sles, index=index) + + def publish_real_time_progress(self, total_sles=None, index=None): + frappe.publish_realtime( + "item_reposting_progress", + { + "name": self.repost_doc.name, + "total_vouchers": cint(total_sles) + cint(index), + "vouchers_posted": index or 0, + }, + doctype=self.repost_doc.doctype, + docname=self.repost_doc.name, + ) + + def get_future_entries_to_repost(self, kwargs): + return get_stock_ledger_entries(kwargs, ">=", "asc", for_update=True, check_serial_no=False) + + def get_sles_for_repack(self, sle): + return ( frappe.get_all( "Stock Ledger Entry", filters={ @@ -671,16 +769,20 @@ class update_entries_after: "voucher_no": sle.voucher_no, "actual_qty": (">", 0), "is_cancelled": 0, - "voucher_detail_no": ("!=", sle.dependant_sle_voucher_detail_no), + "dependant_sle_voucher_detail_no": ("!=", sle.dependant_sle_voucher_detail_no), }, - fields=["*"], + fields=[ + "item_code", + "warehouse", + "posting_date", + "posting_time", + "posting_datetime", + "creation", + ], ) or [] ) - for dependant_sle in sles: - self.update_distinct_item_warehouses(dependant_sle) - def has_stock_reco_with_serial_batch(self, sle): if ( sle.voucher_type == "Stock Reconciliation" @@ -691,33 +793,11 @@ class update_entries_after: return False def process_sle_against_current_timestamp(self): - sl_entries = self.get_sle_against_current_voucher() + sl_entries = get_sle_against_current_voucher(self.args) for sle in sl_entries: sle["timestamp"] = sle.posting_datetime self.process_sle(sle) - def get_sle_against_current_voucher(self): - self.args["posting_datetime"] = get_combine_datetime(self.args.posting_date, self.args.posting_time) - doctype = frappe.qb.DocType("Stock Ledger Entry") - - query = ( - frappe.qb.from_(doctype) - .select("*") - .where( - (doctype.item_code == self.args.item_code) - & (doctype.warehouse == self.args.warehouse) - & (doctype.is_cancelled == 0) - & (doctype.posting_datetime == self.args.posting_datetime) - ) - .orderby(doctype.creation, order=Order.asc) - .for_update() - ) - - if not self.args.get("cancelled"): - query = query.where(doctype.creation == self.args.creation) - - return query.run(as_dict=True) - def get_future_entries_to_fix(self): # includes current entry! args = self.data[self.args.warehouse].previous_sle or frappe._dict( @@ -726,78 +806,8 @@ class update_entries_after: return list(self.get_sle_after_datetime(args)) - def get_dependent_entries_to_fix(self, entries_to_fix, sle): - dependant_sle = get_sle_by_voucher_detail_no( - sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name - ) - - if not dependant_sle: - return entries_to_fix - elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse: - return entries_to_fix - elif dependant_sle.item_code != self.item_code: - self.update_distinct_item_warehouses(dependant_sle) - return entries_to_fix - elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data: - return entries_to_fix - else: - self.initialize_previous_data(dependant_sle) - self.update_distinct_item_warehouses(dependant_sle) - return entries_to_fix - - def update_distinct_item_warehouses(self, dependant_sle): - key = (dependant_sle.item_code, dependant_sle.warehouse) - val = frappe._dict({"sle": dependant_sle}) - - if key not in self.distinct_item_warehouses: - self.distinct_item_warehouses[key] = val - self.new_items_found = True - else: - existing_sle = self.distinct_item_warehouses[key].get("sle", {}) - if getdate(existing_sle.get("posting_date")) > getdate(dependant_sle.posting_date): - self.distinct_item_warehouses[key] = val - self.new_items_found = True - elif ( - dependant_sle.actual_qty > 0 - and dependant_sle.voucher_type == "Stock Entry" - and is_transfer_stock_entry(dependant_sle.voucher_no) - ): - if self.distinct_item_warehouses[key].get("transfer_entry_to_repost"): - return - - val["transfer_entry_to_repost"] = True - self.distinct_item_warehouses[key] = val - self.new_items_found = True - - def is_dependent_voucher_reposted(self, dependant_sle) -> bool: - # Return False if the dependent voucher is not reposted - - if self.args.items_to_be_repost and self.args.current_index: - index = self.args.current_index - while index < len(self.args.items_to_be_repost): - if ( - self.args.items_to_be_repost[index].get("item_code") == dependant_sle.item_code - and self.args.items_to_be_repost[index].get("warehouse") == dependant_sle.warehouse - ): - if getdate(self.args.items_to_be_repost[index].get("posting_date")) > getdate( - dependant_sle.posting_date - ): - self.args.items_to_be_repost[index]["posting_date"] = dependant_sle.posting_date - - return False - - index += 1 - - return True - - def get_dependent_voucher_detail_nos(self, key): - if "dependent_voucher_detail_nos" not in self.distinct_item_warehouses[key]: - self.distinct_item_warehouses[key].dependent_voucher_detail_nos = [] - - return self.distinct_item_warehouses[key].dependent_voucher_detail_nos - def validate_previous_sle_qty(self, sle): - previous_sle = self.data[sle.warehouse].previous_sle + previous_sle = self.prev_sle_dict.get((sle.item_code, sle.warehouse)) if previous_sle and previous_sle.get("qty_after_transaction") < 0 and sle.get("actual_qty") > 0: frappe.msgprint( _( @@ -816,10 +826,32 @@ class update_entries_after: def process_sle(self, sle): # previous sle data for this warehouse - self.wh_data = self.data[sle.warehouse] + key = (sle.item_code, sle.warehouse) + if key not in self.prev_sle_dict: + prev_sle = get_previous_sle_of_current_voucher(sle) + if prev_sle: + self.prev_sle_dict[key] = prev_sle + + if not self.prev_sle_dict.get(key): + self.prev_sle_dict[key] = frappe._dict( + { + "qty_after_transaction": 0.0, + "valuation_rate": 0.0, + "stock_value": 0.0, + "prev_stock_value": 0.0, + "stock_queue": [], + } + ) + + self.wh_data = self.prev_sle_dict.get(key) + + if self.wh_data.stock_queue and isinstance(self.wh_data.stock_queue, str): + self.wh_data.stock_queue = json.loads(self.wh_data.stock_queue) + + if not self.wh_data.prev_stock_value: + self.wh_data.prev_stock_value = self.wh_data.stock_value self.validate_previous_sle_qty(sle) - self.affected_transactions.add((sle.voucher_type, sle.voucher_no)) if (sle.serial_no and not self.via_landed_cost_voucher) or not cint(self.allow_negative_stock): # validate negative stock for serialized items, fifo valuation @@ -922,6 +954,7 @@ class update_entries_after: sle.stock_queue = json.dumps(self.wh_data.stock_queue) sle.stock_value_difference = stock_value_difference + if ( sle.is_adjustment_entry and flt(sle.qty_after_transaction, self.flt_precision) == 0 @@ -946,6 +979,8 @@ class update_entries_after: sle.modified = now() frappe.get_doc(sle).db_update() + self.prev_sle_dict[key] = sle + if not self.args.get("sle_id") or ( sle.serial_and_batch_bundle and sle.auto_created_serial_and_batch_bundle ): @@ -1728,15 +1763,42 @@ class update_entries_after: def update_bin(self): # update bin for each warehouse - for warehouse, data in self.data.items(): - bin_name = get_or_make_bin(self.item_code, warehouse) + for (item_code, warehouse), data in self.prev_sle_dict.items(): + bin_name = get_or_make_bin(item_code, warehouse) - updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value} + updated_values = { + "actual_qty": flt(data.qty_after_transaction), + "stock_value": flt(data.stock_value), + } if data.valuation_rate is not None: - updated_values["valuation_rate"] = data.valuation_rate + updated_values["valuation_rate"] = flt(data.valuation_rate) + frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True) +def get_sle_against_current_voucher(kwargs): + kwargs["posting_datetime"] = get_combine_datetime(kwargs.posting_date, kwargs.posting_time) + doctype = frappe.qb.DocType("Stock Ledger Entry") + + query = ( + frappe.qb.from_(doctype) + .select("*") + .where( + (doctype.item_code == kwargs.item_code) + & (doctype.warehouse == kwargs.warehouse) + & (doctype.is_cancelled == 0) + & (doctype.posting_datetime == kwargs.posting_datetime) + ) + .orderby(doctype.creation, order=Order.asc) + .for_update() + ) + + if not kwargs.get("cancelled"): + query = query.where(doctype.creation == kwargs.creation) + + return query.run(as_dict=True) + + def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False): """get stock ledger entries filtered by specific posting datetime conditions""" @@ -1889,23 +1951,15 @@ def get_stock_ledger_entries( ) -def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None): - return frappe.db.get_value( +def get_sle_by_voucher_detail_no(voucher_detail_no): + return frappe.get_all( "Stock Ledger Entry", - {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle], "is_cancelled": 0}, - [ - "item_code", - "warehouse", - "actual_qty", - "qty_after_transaction", - "posting_date", - "posting_time", - "voucher_detail_no", - "posting_datetime as timestamp", - "voucher_type", - "voucher_no", - ], - as_dict=1, + filters={ + "voucher_detail_no": voucher_detail_no, + "is_cancelled": 0, + "dependant_sle_voucher_detail_no": ("is", "not set"), + }, + fields=["item_code", "warehouse", "posting_date", "posting_time", "posting_datetime", "creation"], ) diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index d7ada387f82..0f6c1262b69 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -14,32 +14,6 @@ from erpnext.tests.utils import ERPNextTestSuite class TestServiceLevelAgreement(ERPNextTestSuite): - def setUp(self): - self.create_company() - frappe.db.set_single_value("Support Settings", "track_service_level_agreement", 1) - lead = frappe.qb.DocType("Lead") - frappe.qb.from_(lead).delete().where(lead.company == self.company).run() - - def create_company(self): - name = "_Test Support SLA" - company = None - if frappe.db.exists("Company", name): - company = frappe.get_doc("Company", name) - else: - company = frappe.get_doc( - { - "doctype": "Company", - "company_name": name, - "country": "India", - "default_currency": "INR", - "create_chart_of_accounts_based_on": "Standard Template", - "chart_of_accounts": "Standard", - } - ) - company = company.save() - - self.company = company.name - def test_service_level_agreement(self): # Default Service Level Agreement create_default_service_level_agreement = create_service_level_agreement( @@ -220,10 +194,9 @@ class TestServiceLevelAgreement(ERPNextTestSuite): doctype=doctype, sla_fulfilled_on=[{"status": "Converted"}], ) - # make lead with default SLA creation = datetime.datetime(2019, 3, 4, 12, 0) - lead = make_lead(creation=creation, index=1, company=self.company) + lead = make_lead(creation=creation, index=1, company="_Test Support SLA") self.assertEqual(lead.service_level_agreement, lead_sla.name) self.assertEqual(lead.response_by, datetime.datetime(2019, 3, 4, 16, 0)) @@ -251,7 +224,7 @@ class TestServiceLevelAgreement(ERPNextTestSuite): ) creation = datetime.datetime(2020, 3, 4, 4, 0) - lead = make_lead(creation, index=2, company=self.company) + lead = make_lead(creation, index=2, company="_Test Support SLA") frappe.flags.current_time = datetime.datetime(2020, 3, 4, 4, 15) lead.reload() @@ -285,7 +258,7 @@ class TestServiceLevelAgreement(ERPNextTestSuite): ) creation = datetime.datetime(2019, 3, 4, 12, 0) - lead = make_lead(creation=creation, index=1, company=self.company) + lead = make_lead(creation=creation, index=1, company="_Test Support SLA") self.assertEqual(lead.response_by, datetime.datetime(2019, 3, 4, 16, 0)) # failed with response time only @@ -312,7 +285,7 @@ class TestServiceLevelAgreement(ERPNextTestSuite): # fulfilled with response time only creation = datetime.datetime(2019, 3, 4, 12, 0) - lead = make_lead(creation=creation, index=2, company=self.company) + lead = make_lead(creation=creation, index=2, company="_Test Support SLA") self.assertEqual(lead.service_level_agreement, lead_sla.name) self.assertEqual(lead.response_by, datetime.datetime(2019, 3, 4, 16, 0)) @@ -339,7 +312,7 @@ class TestServiceLevelAgreement(ERPNextTestSuite): apply_sla_for_resolution=0, ) creation = datetime.datetime(2019, 3, 4, 12, 0) - lead = make_lead(creation=creation, index=4, company=self.company) + lead = make_lead(creation=creation, index=4, company="_Test Support SLA") applied_sla = frappe.db.get_value("Lead", lead.name, "service_level_agreement") self.assertFalse(applied_sla) diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index 0b5fd5dc368..b2ae785e110 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -190,7 +190,9 @@ def link_existing_conversations(doc, state): call_log = frappe.get_doc("Call Log", log) call_log.add_link(link_type=doc.doctype, link_name=doc.name) call_log.save(ignore_permissions=True) - frappe.db.commit() + + if not frappe.in_test: + frappe.db.commit() except Exception: frappe.log_error(title=_("Error during caller information update")) diff --git a/erpnext/templates/pages/order.html b/erpnext/templates/pages/order.html index 0805a32ae33..5563a58b730 100644 --- a/erpnext/templates/pages/order.html +++ b/erpnext/templates/pages/order.html @@ -140,7 +140,7 @@
{% for attachment in attachments %}

- {{ attachment.file_name }} + {{ attachment.file_name|e }}

{% endfor %}
diff --git a/erpnext/templates/pages/projects.html b/erpnext/templates/pages/projects.html index d88088c9819..e671e91db2f 100644 --- a/erpnext/templates/pages/projects.html +++ b/erpnext/templates/pages/projects.html @@ -82,11 +82,11 @@
{% for attachment in doc.attachments %}
- +
- {{ attachment.file_name }} + {{ attachment.file_name|e }}
{{ attachment.file_size }} @@ -101,8 +101,8 @@
{% endblock %} diff --git a/erpnext/tests/test_webform.py b/erpnext/tests/test_webform.py index 8b4ed9ceec9..9ba780e4805 100644 --- a/erpnext/tests/test_webform.py +++ b/erpnext/tests/test_webform.py @@ -22,7 +22,6 @@ class TestWebsite(ERPNextTestSuite): po1 = create_purchase_order(supplier="Supplier1") po2 = create_purchase_order(supplier="Supplier2") - create_custom_doctype() create_webform() create_order_assignment(supplier="Supplier1", po=po1.name) create_order_assignment(supplier="Supplier2", po=po2.name) @@ -62,42 +61,6 @@ def create_user(name, email): ).insert(ignore_if_duplicate=True) -def create_custom_doctype(): - frappe.get_doc( - { - "doctype": "DocType", - "name": "Order Assignment", - "module": "Buying", - "custom": 1, - "autoname": "field:po", - "fields": [ - {"label": "PO", "fieldname": "po", "fieldtype": "Link", "options": "Purchase Order"}, - { - "label": "Supplier", - "fieldname": "supplier", - "fieldtype": "Data", - "fetch_from": "po.supplier", - }, - ], - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1, - }, - {"read": 1, "role": "Supplier"}, - ], - } - ).insert(ignore_if_duplicate=True) - - def create_webform(): frappe.get_doc( { diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py index 9485eb9af42..6aefc4da247 100644 --- a/erpnext/tests/utils.py +++ b/erpnext/tests/utils.py @@ -8,6 +8,7 @@ from typing import Any, NewType import frappe from frappe import _ from frappe.core.doctype.report.report import get_report_module_dotted_path +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.tests.utils import load_test_records_for from frappe.utils import now_datetime, today @@ -240,16 +241,29 @@ class BootStrapTestData: self.make_sales_person() self.make_activity_type() self.make_address() + self.update_support_settings() self.update_selling_settings() self.update_stock_settings() self.update_system_settings() frappe.db.commit() # nosemgrep - # custom doctype # DDL commands have implicit commit + # Dimensions + self.make_dimensions() + + # custom doctype self.make_custom_doctype() + # data on custom doctype + self.make_shelf() + self.make_rack() + self.make_inv_site() + self.make_store() + + # custom field + self.make_custom_field() + def update_system_settings(self): system_settings = frappe.get_doc("System Settings") system_settings.time_zone = "Asia/Kolkata" @@ -258,6 +272,11 @@ class BootStrapTestData: system_settings.rounding_method = "Banker's Rounding" system_settings.save() + def update_support_settings(self): + support_settings = frappe.get_doc("Support Settings") + support_settings.track_service_level_agreement = True + support_settings.save() + def update_selling_settings(self): selling_settings = frappe.get_doc("Selling Settings") selling_settings.selling_price_list = "Standard Selling" @@ -956,6 +975,7 @@ class BootStrapTestData: def make_location(self): records = [ {"doctype": "Location", "location_name": "Test Location"}, + {"doctype": "Location", "location_name": "Test Location 2"}, {"doctype": "Location", "location_name": "Test Location Area", "is_group": 1, "is_container": 1}, { "doctype": "Location", @@ -2743,6 +2763,46 @@ class BootStrapTestData: } ).insert(ignore_permissions=True) + if not frappe.db.exists("DocType", "Order Assignment"): + frappe.get_doc( + { + "doctype": "DocType", + "name": "Order Assignment", + "module": "Buying", + "custom": 1, + "autoname": "field:po", + "fields": [ + { + "label": "PO", + "fieldname": "po", + "fieldtype": "Link", + "options": "Purchase Order", + }, + { + "label": "Supplier", + "fieldname": "supplier", + "fieldtype": "Data", + "fetch_from": "po.supplier", + }, + ], + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1, + }, + {"read": 1, "role": "Supplier"}, + ], + } + ).insert(ignore_if_duplicate=True) + def make_address(self): records = [ { @@ -2794,6 +2854,103 @@ class BootStrapTestData: ] self.make_records(["address_title", "address_type"], records) + def make_dimensions(self): + records = [ + { + "doctype": "Accounting Dimension", + "document_type": "Department", + "dimension_defaults": [ + { + "company": "_Test Company", + "reference_document": "Department", + "default_dimension": "_Test Department - _TC", + } + ], + }, + { + "doctype": "Accounting Dimension", + "document_type": "Location", + "dimension_defaults": [ + { + "company": "_Test Company", + "reference_document": "Location", + "default_dimension": "Block 1", + } + ], + }, + { + "doctype": "Accounting Dimension", + "document_type": "Branch", + }, + ] + self.make_records(["document_type"], records) + + def make_custom_field(self): + pan_field = { + "Supplier": [ + { + "fieldname": "pan", + "label": "PAN", + "fieldtype": "Data", + "translatable": 0, + } + ] + } + + create_custom_fields(pan_field, update=1) + + def make_shelf(self): + records = [ + { + "doctype": "Shelf", + "shelf_name": "Shelf 1", + }, + { + "doctype": "Shelf", + "shelf_name": "Shelf 2", + }, + ] + self.make_records(["shelf_name"], records) + + def make_rack(self): + records = [ + { + "doctype": "Rack", + "rack_name": "Rack 1", + }, + { + "doctype": "Rack", + "rack_name": "Rack 2", + }, + ] + self.make_records(["rack_name"], records) + + def make_inv_site(self): + records = [ + { + "doctype": "Inv Site", + "site_name": "Site 1", + }, + { + "doctype": "Inv Site", + "site_name": "Site 2", + }, + ] + self.make_records(["site_name"], records) + + def make_store(self): + records = [ + { + "doctype": "Store", + "store_name": "Store 1", + }, + { + "doctype": "Store", + "store_name": "Store 2", + }, + ] + self.make_records(["store_name"], records) + BootStrapTestData() diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index fb1628fe305..55d60fb389d 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -263,7 +263,7 @@ class TransactionBase(StatusUpdater): "company": self.get("company"), "order_type": self.get("order_type"), "is_pos": cint(self.get("is_pos")), - "is_return": cint(self.get("is_return)")), + "is_return": cint(self.get("is_return")), "is_subcontracted": self.get("is_subcontracted"), "ignore_pricing_rule": self.get("ignore_pricing_rule"), "doctype": self.get("doctype"), @@ -287,7 +287,8 @@ class TransactionBase(StatusUpdater): "child_docname": item.get("name"), "is_old_subcontracting_flow": self.get("is_old_subcontracting_flow"), } - ) + ), + self, ) @frappe.whitelist() diff --git a/semgrep/test-correctness.yml b/semgrep/test-correctness.yml new file mode 100644 index 00000000000..34eb82fa1d6 --- /dev/null +++ b/semgrep/test-correctness.yml @@ -0,0 +1,18 @@ +rules: +- id: Dont-commit + pattern: frappe.db.commit() + message: Commiting inside test breaks idempotency. + languages: [python] + severity: ERROR +- id: Implicit-commit + pattern: frappe.db.truncate() + message: DB truncation does implict commit which breaks test idempotency. + languages: [python] + severity: ERROR +- id: Dont-override-teardown + pattern: | + def tearDown(...): + ... + message: ERPNextTestSuite forces rollback on each tearDown, which ensures idempotency. Don't override tearDown. + languages: [python] + severity: ERROR