From 3592637b5c88249ff9c0a721eb7d8d3b7df646c6 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 30 Mar 2026 17:58:47 +0530 Subject: [PATCH 01/79] fix(taxes): increase rounding threshold for tax breakup calculations (cherry picked from commit 7f87a5e5c6fb7c283b59484da8e4d9cd798aea95) --- erpnext/controllers/taxes_and_totals.py | 2 +- .../tests/test_item_wise_tax_details.py | 104 ++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 9d98eed668d..fa55aa1daa5 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -522,7 +522,7 @@ class calculate_taxes_and_totals: diff = flt(expected_amount - actual_breakup, 5) # TODO: fix rounding difference issues - if abs(diff) <= 0.5: + if abs(diff) <= 1: detail_row = self.doc._item_wise_tax_details[last_idx] detail_row["amount"] = flt(detail_row["amount"] + diff, 5) diff --git a/erpnext/controllers/tests/test_item_wise_tax_details.py b/erpnext/controllers/tests/test_item_wise_tax_details.py index f6d94c61eca..d4a560284ef 100644 --- a/erpnext/controllers/tests/test_item_wise_tax_details.py +++ b/erpnext/controllers/tests/test_item_wise_tax_details.py @@ -124,3 +124,107 @@ class TestTaxesAndTotals(ERPNextTestSuite): ] self.assertEqual(actual_values, expected_values) + + def test_item_wise_tax_detail_with_multi_currency(self): + """ + For multi-item, multi-currency invoices, item-wise tax breakup should + still reconcile with base tax totals. + """ + doc = frappe.get_doc( + { + "doctype": "Sales Invoice", + "customer": "_Test Customer", + "company": "_Test Company", + "currency": "USD", + "debit_to": "_Test Receivable USD - _TC", + "conversion_rate": 129.99, + "items": [ + { + "item_code": "_Test Item", + "qty": 1, + "rate": 47.41, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + { + "item_code": "_Test Item", + "qty": 2, + "rate": 33.33, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + ], + "taxes": [ + { + "charge_type": "On Net Total", + "account_head": "_Test Account VAT - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "rate": 16, + }, + { + "charge_type": "On Previous Row Amount", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 10, + "row_id": 1, + }, + ], + } + ) + doc.save() + + details_by_tax = {} + for detail in doc.item_wise_tax_details: + bucket = details_by_tax.setdefault(detail.tax_row, {"amount": 0.0, "taxable_amount": 0.0}) + bucket["amount"] += detail.amount + + for tax in doc.taxes: + detail_totals = details_by_tax[tax.name] + self.assertAlmostEqual( + detail_totals["amount"], tax.base_tax_amount_after_discount_amount, places=2 + ) + + def test_item_wise_tax_detail_with_multi_currency_with_single_item(self): + """ + When the tax amount (in transaction currency) has more decimals than + the field precision, rounding must happen *before* multiplying by + conversion_rate — the same order used by _set_in_company_currency. + """ + doc = frappe.get_doc( + { + "doctype": "Sales Invoice", + "customer": "_Test Customer", + "company": "_Test Company", + "currency": "USD", + "debit_to": "_Test Receivable USD - _TC", + "conversion_rate": 129.99, + "items": [ + { + "item_code": "_Test Item", + "qty": 1, + "rate": 47.41, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + } + ], + "taxes": [ + { + "charge_type": "On Net Total", + "account_head": "_Test Account VAT - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "rate": 16, + }, + ], + } + ) + doc.save() + + tax = doc.taxes[0] + detail = doc.item_wise_tax_details[0] + self.assertEqual(detail.amount, tax.base_tax_amount_after_discount_amount) From 6689b17b882f2bd8e5ea3a7ac10aba9e90d90345 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 30 Mar 2026 18:37:07 +0530 Subject: [PATCH 02/79] fix(tests): update item code and quantity in tax detail test case (cherry picked from commit 3449ab063aac954f38bf3dbf7048f13457f60dcb) --- erpnext/controllers/tests/test_item_wise_tax_details.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/tests/test_item_wise_tax_details.py b/erpnext/controllers/tests/test_item_wise_tax_details.py index d4a560284ef..6b259429cc5 100644 --- a/erpnext/controllers/tests/test_item_wise_tax_details.py +++ b/erpnext/controllers/tests/test_item_wise_tax_details.py @@ -148,7 +148,7 @@ class TestTaxesAndTotals(ERPNextTestSuite): "cost_center": "_Test Cost Center - _TC", }, { - "item_code": "_Test Item", + "item_code": "_Test Item 2", "qty": 2, "rate": 33.33, "income_account": "Sales - _TC", From 6ad5e8960782f761ce4dcca305c2c94ea88b11fc Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Mon, 30 Mar 2026 19:37:59 +0530 Subject: [PATCH 03/79] fix(taxes): improve tax calculation accuracy and update test assertions (cherry picked from commit a18196f584d4d234e669e23842223f6042360c1d) --- erpnext/controllers/taxes_and_totals.py | 29 +++++++++++++++---- .../tests/test_item_wise_tax_details.py | 7 ++--- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index fa55aa1daa5..f0da61ad900 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -285,6 +285,13 @@ class calculate_taxes_and_totals: self.doc._item_wise_tax_details = item_wise_tax_details self.doc.item_wise_tax_details = [] + for tax in self.doc.get("taxes"): + if not tax.get("dont_recompute_tax"): + tax._running_txn_tax_total = 0.0 + tax._running_base_tax_total = 0.0 + tax._running_txn_taxable_total = 0.0 + tax._running_base_taxable_total = 0.0 + def determine_exclusive_rate(self): if not any(cint(tax.included_in_print_rate) for tax in self.doc.get("taxes")): return @@ -521,8 +528,7 @@ class calculate_taxes_and_totals: actual_breakup = tax._total_tax_breakup diff = flt(expected_amount - actual_breakup, 5) - # TODO: fix rounding difference issues - if abs(diff) <= 1: + if abs(diff) <= 0.5: detail_row = self.doc._item_wise_tax_details[last_idx] detail_row["amount"] = flt(detail_row["amount"] + diff, 5) @@ -597,14 +603,25 @@ class calculate_taxes_and_totals: def set_item_wise_tax(self, item, tax, tax_rate, current_tax_amount, current_net_amount): # store tax breakup for each item multiplier = -1 if tax.get("add_deduct_tax") == "Deduct" else 1 - item_wise_tax_amount = flt( - current_tax_amount * self.doc.conversion_rate * multiplier, tax.precision("tax_amount") + + # Error diffusion: derive each item's base amount as a delta of the running cumulative total + # so the sum always equals base_tax_amount_after_discount_amount. + tax._running_txn_tax_total += current_tax_amount * multiplier + new_base_tax_total = flt( + flt(tax._running_txn_tax_total, tax.precision("tax_amount")) * self.doc.conversion_rate, + tax.precision("base_tax_amount"), ) + item_wise_tax_amount = new_base_tax_total - tax._running_base_tax_total + tax._running_base_tax_total = new_base_tax_total if tax.charge_type != "On Item Quantity": - item_wise_taxable_amount = flt( - current_net_amount * self.doc.conversion_rate * multiplier, tax.precision("tax_amount") + tax._running_txn_taxable_total += current_net_amount * multiplier + new_base_taxable_total = flt( + flt(tax._running_txn_taxable_total, tax.precision("net_amount")) * self.doc.conversion_rate, + tax.precision("base_net_amount"), ) + item_wise_taxable_amount = new_base_taxable_total - tax._running_base_taxable_total + tax._running_base_taxable_total = new_base_taxable_total else: item_wise_taxable_amount = 0.0 diff --git a/erpnext/controllers/tests/test_item_wise_tax_details.py b/erpnext/controllers/tests/test_item_wise_tax_details.py index 6b259429cc5..cce4f2ce024 100644 --- a/erpnext/controllers/tests/test_item_wise_tax_details.py +++ b/erpnext/controllers/tests/test_item_wise_tax_details.py @@ -179,14 +179,11 @@ class TestTaxesAndTotals(ERPNextTestSuite): details_by_tax = {} for detail in doc.item_wise_tax_details: - bucket = details_by_tax.setdefault(detail.tax_row, {"amount": 0.0, "taxable_amount": 0.0}) + bucket = details_by_tax.setdefault(detail.tax_row, {"amount": 0.0}) bucket["amount"] += detail.amount for tax in doc.taxes: - detail_totals = details_by_tax[tax.name] - self.assertAlmostEqual( - detail_totals["amount"], tax.base_tax_amount_after_discount_amount, places=2 - ) + self.assertEqual(details_by_tax[tax.name]["amount"], tax.base_tax_amount_after_discount_amount) def test_item_wise_tax_detail_with_multi_currency_with_single_item(self): """ From 5922d25210d434478a0947a0b6a68fda23164e0f Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 30 Mar 2026 20:04:53 +0530 Subject: [PATCH 04/79] test: update item-wise tax detail test for high conversion rates (cherry picked from commit fc8437c499e3746c82cc64c26751f8b546fd8d94) # Conflicts: # erpnext/controllers/tests/test_item_wise_tax_details.py --- .../tests/test_item_wise_tax_details.py | 46 ++++++++++++++----- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/erpnext/controllers/tests/test_item_wise_tax_details.py b/erpnext/controllers/tests/test_item_wise_tax_details.py index cce4f2ce024..c7a61fcb47d 100644 --- a/erpnext/controllers/tests/test_item_wise_tax_details.py +++ b/erpnext/controllers/tests/test_item_wise_tax_details.py @@ -2,8 +2,12 @@ import json import frappe +<<<<<<< HEAD from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals from erpnext.tests.utils import ERPNextTestSuite +======= +from erpnext.tests.utils import ERPNextTestSuite, change_settings +>>>>>>> fc8437c499 (test: update item-wise tax detail test for high conversion rates) class TestTaxesAndTotals(ERPNextTestSuite): @@ -125,10 +129,22 @@ class TestTaxesAndTotals(ERPNextTestSuite): self.assertEqual(actual_values, expected_values) - def test_item_wise_tax_detail_with_multi_currency(self): + @change_settings("Selling Settings", {"allow_multiple_items": 1}) + def test_item_wise_tax_detail_high_conversion_rate(self): """ - For multi-item, multi-currency invoices, item-wise tax breakup should - still reconcile with base tax totals. + With a high conversion rate (e.g. USD -> KRW ~1300), independently rounding + each item's base tax amount causes per-item errors that accumulate and exceed + the 0.5-unit safety threshold, raising a validation error. + + Error diffusion fixes this: the cumulative base total after the last item + equals base_tax_amount_after_discount_amount exactly, so the sum of all + per-item amounts is always exact regardless of item count or rate magnitude. + + Analytically with conversion_rate=1300, rate=7.77 x3 items, VAT 16%: + per-item txn tax = 1.2432 + OLD independent: flt(1.2432 * 1300, 2) = 1616.16 -> sum 4848.48 + expected base: flt(flt(3.7296, 2) * 1300, 0) = flt(3.73 * 1300, 0) = 4849 + diff = 0.52 -> exceeds 0.5 threshold -> would throw with old code """ doc = frappe.get_doc( { @@ -137,20 +153,28 @@ class TestTaxesAndTotals(ERPNextTestSuite): "company": "_Test Company", "currency": "USD", "debit_to": "_Test Receivable USD - _TC", - "conversion_rate": 129.99, + "conversion_rate": 1300, "items": [ { "item_code": "_Test Item", "qty": 1, - "rate": 47.41, + "rate": 7.77, "income_account": "Sales - _TC", "expense_account": "Cost of Goods Sold - _TC", "cost_center": "_Test Cost Center - _TC", }, { - "item_code": "_Test Item 2", - "qty": 2, - "rate": 33.33, + "item_code": "_Test Item", + "qty": 1, + "rate": 7.77, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + { + "item_code": "_Test Item", + "qty": 1, + "rate": 7.77, "income_account": "Sales - _TC", "expense_account": "Cost of Goods Sold - _TC", "cost_center": "_Test Cost Center - _TC", @@ -179,11 +203,11 @@ class TestTaxesAndTotals(ERPNextTestSuite): details_by_tax = {} for detail in doc.item_wise_tax_details: - bucket = details_by_tax.setdefault(detail.tax_row, {"amount": 0.0}) - bucket["amount"] += detail.amount + bucket = details_by_tax.setdefault(detail.tax_row, 0.0) + details_by_tax[detail.tax_row] = bucket + detail.amount for tax in doc.taxes: - self.assertEqual(details_by_tax[tax.name]["amount"], tax.base_tax_amount_after_discount_amount) + self.assertEqual(details_by_tax[tax.name], tax.base_tax_amount_after_discount_amount) def test_item_wise_tax_detail_with_multi_currency_with_single_item(self): """ From a478fb713139c0359c5b605581cabcc8a5352027 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 23:33:22 +0530 Subject: [PATCH 05/79] fix(warehouse_capacity_dashboard): removed `escape` from template (backport #53907) (#53909) Co-authored-by: diptanilsaha fix(warehouse_capacity_dashboard): removed `escape` from template (#53907) --- .../warehouse_capacity_summary.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html index adab4786403..5a69c405364 100644 --- a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html @@ -32,8 +32,8 @@ class="btn btn-default btn-xs btn-edit" style="margin: 4px 0; float: left;" data-warehouse="{{ d.warehouse }}" - data-item="{{ escape(d.item_code) }}" - data-company="{{ escape(d.company) }}"> + data-item="{{ d.item_code }}" + data-company="{{ d.company }}"> {{ __("Edit Capacity") }} From 0dcacad79321878c7d660804d2249b85f3f16f7a Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 31 Mar 2026 11:05:46 +0530 Subject: [PATCH 06/79] chore: resolve conflicts --- erpnext/controllers/tests/test_item_wise_tax_details.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/controllers/tests/test_item_wise_tax_details.py b/erpnext/controllers/tests/test_item_wise_tax_details.py index c7a61fcb47d..30f1e51d7f4 100644 --- a/erpnext/controllers/tests/test_item_wise_tax_details.py +++ b/erpnext/controllers/tests/test_item_wise_tax_details.py @@ -2,12 +2,7 @@ import json import frappe -<<<<<<< HEAD -from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals -from erpnext.tests.utils import ERPNextTestSuite -======= from erpnext.tests.utils import ERPNextTestSuite, change_settings ->>>>>>> fc8437c499 (test: update item-wise tax detail test for high conversion rates) class TestTaxesAndTotals(ERPNextTestSuite): From 8be7793f89fe64f837ed15b321e3da8441d2b656 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 06:09:44 +0000 Subject: [PATCH 07/79] chore: remove inter warehouse transfer settings (backport #53860) (#53941) Co-authored-by: Nishka Gosalia <58264710+nishkagosalia@users.noreply.github.com> --- .../stock_settings/stock_settings.json | 27 +------------ .../doctype/stock_settings/stock_settings.py | 39 ------------------- 2 files changed, 1 insertion(+), 65 deletions(-) diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index cd9ced97baf..8e42e4d3177 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -74,10 +74,6 @@ "auto_indent", "column_break_27", "reorder_email_notify", - "inter_warehouse_transfer_settings_section", - "allow_from_dn", - "column_break_31", - "allow_from_pr", "stock_closing_tab", "control_historical_stock_transactions_section", "stock_frozen_upto", @@ -225,23 +221,6 @@ "fieldtype": "Data", "label": "Naming Series Prefix" }, - { - "fieldname": "inter_warehouse_transfer_settings_section", - "fieldtype": "Section Break", - "label": "Inter Warehouse Transfer Settings" - }, - { - "default": "0", - "fieldname": "allow_from_dn", - "fieldtype": "Check", - "label": "Allow Material Transfer from Delivery Note to Sales Invoice" - }, - { - "default": "0", - "fieldname": "allow_from_pr", - "fieldtype": "Check", - "label": "Allow Material Transfer from Purchase Receipt to Purchase Invoice" - }, { "description": "If mentioned, the system will allow only the users with this Role to create or modify any stock transaction earlier than the latest stock transaction for a specific item and warehouse. If set as blank, it allows all users to create/edit back-dated transactions.", "fieldname": "role_allowed_to_create_edit_back_dated_transactions", @@ -289,10 +268,6 @@ "fieldname": "column_break_27", "fieldtype": "Column Break" }, - { - "fieldname": "column_break_31", - "fieldtype": "Column Break" - }, { "fieldname": "quality_inspection_settings_section", "fieldtype": "Section Break", @@ -564,7 +539,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2026-03-16 13:28:19.254641", + "modified": "2026-03-27 22:39:16.812184", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index f2d54794094..2d85675f2ea 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -26,8 +26,6 @@ class StockSettings(Document): action_if_quality_inspection_is_not_submitted: DF.Literal["Stop", "Warn"] action_if_quality_inspection_is_rejected: DF.Literal["Stop", "Warn"] allow_existing_serial_no: DF.Check - allow_from_dn: DF.Check - allow_from_pr: DF.Check allow_internal_transfer_at_arms_length_price: DF.Check allow_negative_stock: DF.Check allow_negative_stock_for_batch: DF.Check @@ -261,9 +259,6 @@ class StockSettings(Document): ) ) - def on_update(self): - self.toggle_warehouse_field_for_inter_warehouse_transfer() - def change_precision_for_for_sales(self): doc_before_save = self.get_doc_before_save() if doc_before_save and ( @@ -314,40 +309,6 @@ class StockSettings(Document): validate_fields_for_doctype=False, ) - def toggle_warehouse_field_for_inter_warehouse_transfer(self): - make_property_setter( - "Sales Invoice Item", - "target_warehouse", - "hidden", - 1 - cint(self.allow_from_dn), - "Check", - validate_fields_for_doctype=False, - ) - make_property_setter( - "Delivery Note Item", - "target_warehouse", - "hidden", - 1 - cint(self.allow_from_dn), - "Check", - validate_fields_for_doctype=False, - ) - make_property_setter( - "Purchase Invoice Item", - "from_warehouse", - "hidden", - 1 - cint(self.allow_from_pr), - "Check", - validate_fields_for_doctype=False, - ) - make_property_setter( - "Purchase Receipt Item", - "from_warehouse", - "hidden", - 1 - cint(self.allow_from_pr), - "Check", - validate_fields_for_doctype=False, - ) - def clean_all_descriptions(): for item in frappe.get_all("Item", ["name", "description"]): From 4705f53d2c53b597df3cd6e5a4bf5fd321cfcfbe Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Tue, 31 Mar 2026 01:52:01 +0530 Subject: [PATCH 08/79] fix: dynamic labels on invoice type change (cherry picked from commit 820bd15e1e1ddb63442fb2052621e2bf60b15045) --- .../opening_invoice_creation_tool.js | 32 +++++++++++++++++-- .../opening_invoice_creation_tool.json | 12 +++++-- 2 files changed, 38 insertions(+), 6 deletions(-) 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 466b38126d7..22d977dd60f 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 @@ -70,9 +70,7 @@ frappe.ui.form.on("Opening Invoice Creation Tool", { }); }); - if (frm.doc.create_missing_party) { - frm.set_df_property("party", "fieldtype", "Data", frm.doc.name, "invoices"); - } + frm.trigger("update_party_labels"); }, setup_company_filters: function (frm) { @@ -127,7 +125,9 @@ frappe.ui.form.on("Opening Invoice Creation Tool", { frappe.model.set_value(row.doctype, row.name, "party", ""); frappe.model.set_value(row.doctype, row.name, "party_name", ""); }); + frm.clear_table("invoices"); frm.refresh_fields(); + frm.trigger("update_party_labels"); }, make_dashboard: function (frm) { @@ -175,6 +175,32 @@ frappe.ui.form.on("Opening Invoice Creation Tool", { } frm.refresh_field("invoices"); }, + + update_party_labels: function (frm) { + let is_sales = frm.doc.invoice_type == "Sales"; + + frm.fields_dict["invoices"].grid.update_docfield_property( + "party", + "label", + is_sales ? "Customer ID" : "Supplier ID" + ); + frm.fields_dict["invoices"].grid.update_docfield_property( + "party_name", + "label", + is_sales ? "Customer Name" : "Supplier Name" + ); + + frm.set_df_property( + "create_missing_party", + "description", + is_sales + ? __("If party does not exist, create it using the Customer Name field.") + : __("If party does not exist, create it using the Supplier Name field.") + ); + + frm.refresh_field("invoices"); + frm.refresh_field("create_missing_party"); + }, }); frappe.ui.form.on("Opening Invoice Creation Tool Item", { 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 8d1c3e87ba1..535b7384b4d 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 @@ -7,10 +7,11 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "section_break_ynel", "company", + "create_missing_party", "column_break_3", "invoice_type", - "create_missing_party", "accounting_dimensions_section", "cost_center", "dimension_col_break", @@ -25,11 +26,11 @@ "in_list_view": 1, "label": "Company", "options": "Company", + "remember_last_selected_value": 1, "reqd": 1 }, { "default": "0", - "description": "If party does not exist, create it using the Party Name field.", "fieldname": "create_missing_party", "fieldtype": "Check", "label": "Create Missing Party" @@ -79,12 +80,17 @@ { "fieldname": "dimension_col_break", "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_ynel", + "fieldtype": "Section Break", + "hide_border": 1 } ], "hide_toolbar": 1, "issingle": 1, "links": [], - "modified": "2026-03-23 00:32:15.600086", + "modified": "2026-03-31 01:47:20.360352", "modified_by": "Administrator", "module": "Accounts", "name": "Opening Invoice Creation Tool", From 5ade905ee85b92d724e1d7ba515cb9c70e24812c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 12:26:29 +0530 Subject: [PATCH 09/79] feat(Payment Request): Added a toggle for using the payment schedule (backport #53922) (#53928) * feat(Payment Request): Added a toggle for using the payment schedule (cherry picked from commit 8ec15b537ec1da26d097378bc67b63629dbc758f) --------- Co-authored-by: Jatin3128 Co-authored-by: Jatin3128 <140256508+Jatin3128@users.noreply.github.com> --- .../accounts_settings/accounts_settings.json | 16 +++++++++++++++- .../accounts_settings/accounts_settings.py | 1 + erpnext/public/js/controllers/transaction.js | 6 +++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index ee734184452..29673e89b6c 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -65,6 +65,7 @@ "payment_options_section", "enable_loyalty_point_program", "column_break_ctam", + "fetch_payment_schedule_in_payment_request", "invoicing_settings_tab", "accounts_transactions_settings_section", "over_billing_allowance", @@ -688,6 +689,19 @@ "fieldname": "enable_accounting_dimensions", "fieldtype": "Check", "label": "Enable Accounting Dimensions" + }, + { + "default": "1", + "description": "Enable Subscription tracking in invoice", + "fieldname": "enable_subscription", + "fieldtype": "Check", + "label": "Enable Subscription" + }, + { + "default": "1", + "fieldname": "fetch_payment_schedule_in_payment_request", + "fieldtype": "Check", + "label": "Fetch Payment Schedule In Payment Request" } ], "grid_page_length": 50, @@ -697,7 +711,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2026-02-27 01:04:09.415288", + "modified": "2026-03-30 07:32:58.182018", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index e75b8ad1710..94b35eba00a 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -73,6 +73,7 @@ class AccountsSettings(Document): enable_loyalty_point_program: DF.Check enable_party_matching: DF.Check exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"] + fetch_payment_schedule_in_payment_request: DF.Check fetch_valuation_rate_for_internal_transaction: DF.Check general_ledger_remarks_length: DF.Int ignore_account_closing_balance: DF.Check diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 1100ab7a581..f0be33b6a87 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -459,8 +459,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe reference_name: frm.doc.name, }, }); + const value = await frappe.db.get_single_value( + "Accounts Settings", + "fetch_payment_schedule_in_payment_request" + ); - if (!schedules.length) { + if (!value || !schedules.length) { this.make_payment_request(); return; } From a8d0fb5ac92c50f52e9a07fb88ccec42a412973b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 26 Mar 2026 11:32:00 +0530 Subject: [PATCH 10/79] refactor(test): erpnext testsuite should be primary superclass (cherry picked from commit f3148e052c8312c5a1592da74de53a20562d2afb) --- .../test_advance_payment_ledger_entry.py | 2 +- .../test_bank_reconciliation_tool.py | 2 +- .../doctype/bank_transaction/test_bank_transaction_fees.py | 5 +++-- .../test_exchange_rate_revaluation.py | 2 +- erpnext/accounts/doctype/ledger_health/test_ledger_health.py | 2 +- .../test_process_statement_of_accounts.py | 2 +- .../test_repost_accounting_ledger.py | 2 +- .../doctype/unreconcile_payment/test_unreconcile_payment.py | 2 +- .../report/accounts_payable/test_accounts_payable.py | 2 +- .../report/accounts_receivable/test_accounts_receivable.py | 2 +- .../test_accounts_receivable_summary.py | 2 +- .../customer_ledger_summary/test_customer_ledger_summary.py | 2 +- .../test_item_wise_purchase_register.py | 2 +- .../test_item_wise_sales_register.py | 2 +- .../test_profit_and_loss_statement.py | 2 +- .../accounts/report/sales_register/test_sales_register.py | 2 +- .../supplier_ledger_summary/test_supplier_ledger_summary.py | 2 +- .../tax_withholding_details/test_tax_withholding_details.py | 2 +- erpnext/controllers/tests/test_distributed_discount.py | 2 +- erpnext/controllers/tests/test_reactivity.py | 2 +- erpnext/edi/doctype/code_list/test_code_list.py | 4 ++-- erpnext/edi/doctype/common_code/test_common_code.py | 4 ++-- .../address_template/test_regional_address_template.py | 5 ++--- erpnext/selling/doctype/sales_order/test_sales_order.py | 2 +- 24 files changed, 29 insertions(+), 29 deletions(-) diff --git a/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py b/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py index 53047b61718..d910bef3d94 100644 --- a/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py +++ b/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py @@ -15,7 +15,7 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde from erpnext.tests.utils import ERPNextTestSuite -class TestAdvancePaymentLedgerEntry(AccountsTestMixin, ERPNextTestSuite): +class TestAdvancePaymentLedgerEntry(ERPNextTestSuite, AccountsTestMixin): """ Integration tests for AdvancePaymentLedgerEntry. Use this class for testing interactions between multiple components. diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py index 5354aa0c4dd..3a55b3fc1d8 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py @@ -15,7 +15,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.tests.utils import ERPNextTestSuite -class TestBankReconciliationTool(AccountsTestMixin, ERPNextTestSuite): +class TestBankReconciliationTool(ERPNextTestSuite, AccountsTestMixin): def setUp(self): self.create_company() self.create_customer() diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction_fees.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction_fees.py index 95fc615d91d..e0ea8cd441a 100644 --- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction_fees.py +++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction_fees.py @@ -2,10 +2,11 @@ # See license.txt import frappe -from frappe.tests import UnitTestCase + +from erpnext.tests.utils import ERPNextTestSuite -class TestBankTransactionFees(UnitTestCase): +class TestBankTransactionFees(ERPNextTestSuite): def test_included_fee_throws(self): """A fee that's part of a withdrawal cannot be bigger than the withdrawal itself.""" diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py index 1310a8b482b..a6adba537e2 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py @@ -13,7 +13,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.tests.utils import ERPNextTestSuite -class TestExchangeRateRevaluation(AccountsTestMixin, ERPNextTestSuite): +class TestExchangeRateRevaluation(ERPNextTestSuite, AccountsTestMixin): def setUp(self): self.create_company() self.create_usd_receivable_account() diff --git a/erpnext/accounts/doctype/ledger_health/test_ledger_health.py b/erpnext/accounts/doctype/ledger_health/test_ledger_health.py index 84fd3925ded..d9d4249ca69 100644 --- a/erpnext/accounts/doctype/ledger_health/test_ledger_health.py +++ b/erpnext/accounts/doctype/ledger_health/test_ledger_health.py @@ -10,7 +10,7 @@ from erpnext.accounts.utils import run_ledger_health_checks from erpnext.tests.utils import ERPNextTestSuite -class TestLedgerHealth(AccountsTestMixin, ERPNextTestSuite): +class TestLedgerHealth(ERPNextTestSuite, AccountsTestMixin): def setUp(self): self.create_company() self.create_customer() diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py index c16933c7836..6e2f2300054 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py @@ -14,7 +14,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.tests.utils import ERPNextTestSuite -class TestProcessStatementOfAccounts(AccountsTestMixin, ERPNextTestSuite): +class TestProcessStatementOfAccounts(ERPNextTestSuite, AccountsTestMixin): def setUp(self): frappe.db.set_single_value("Selling Settings", "validate_selling_price", 0) letterhead = frappe.get_doc("Letter Head", "Company Letterhead - Grey") diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py index b60c13fc8b8..08075b2b0be 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py @@ -16,7 +16,7 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_ from erpnext.tests.utils import ERPNextTestSuite -class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite): +class TestRepostAccountingLedger(ERPNextTestSuite, AccountsTestMixin): def setUp(self): self.create_company() self.create_customer() diff --git a/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py b/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py index 7f931b7556e..e3bfed7de55 100644 --- a/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py +++ b/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py @@ -14,7 +14,7 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde from erpnext.tests.utils import ERPNextTestSuite -class TestUnreconcilePayment(AccountsTestMixin, ERPNextTestSuite): +class TestUnreconcilePayment(ERPNextTestSuite, AccountsTestMixin): def setUp(self): self.create_company() self.create_customer() diff --git a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py index 87a4b989661..a8074468f55 100644 --- a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py +++ b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py @@ -7,7 +7,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.tests.utils import ERPNextTestSuite -class TestAccountsPayable(AccountsTestMixin, ERPNextTestSuite): +class TestAccountsPayable(ERPNextTestSuite, AccountsTestMixin): def setUp(self): self.create_company() self.create_customer() diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index d38ce924cf0..a739502074e 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -10,7 +10,7 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde from erpnext.tests.utils import ERPNextTestSuite -class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite): +class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin): def setUp(self): self.create_company() self.create_customer() diff --git a/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py index 96fa4ae8b64..5b8065eef0c 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py @@ -8,7 +8,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.tests.utils import ERPNextTestSuite -class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite): +class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin): def setUp(self): self.maxDiff = None self.create_company() diff --git a/erpnext/accounts/report/customer_ledger_summary/test_customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/test_customer_ledger_summary.py index 0b114dd96d2..624e007c1b0 100644 --- a/erpnext/accounts/report/customer_ledger_summary/test_customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/test_customer_ledger_summary.py @@ -10,7 +10,7 @@ from erpnext.controllers.sales_and_purchase_return import make_return_doc from erpnext.tests.utils import ERPNextTestSuite -class TestCustomerLedgerSummary(AccountsTestMixin, ERPNextTestSuite): +class TestCustomerLedgerSummary(ERPNextTestSuite, AccountsTestMixin): def setUp(self): self.create_company() self.create_customer() diff --git a/erpnext/accounts/report/item_wise_purchase_register/test_item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/test_item_wise_purchase_register.py index eed45ea60bb..64b0dfc739d 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/test_item_wise_purchase_register.py +++ b/erpnext/accounts/report/item_wise_purchase_register/test_item_wise_purchase_register.py @@ -7,7 +7,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.tests.utils import ERPNextTestSuite -class TestItemWisePurchaseRegister(AccountsTestMixin, ERPNextTestSuite): +class TestItemWisePurchaseRegister(ERPNextTestSuite, AccountsTestMixin): def setUp(self): self.create_company() self.create_supplier() diff --git a/erpnext/accounts/report/item_wise_sales_register/test_item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/test_item_wise_sales_register.py index 689edeac1c4..708bf1ffe89 100644 --- a/erpnext/accounts/report/item_wise_sales_register/test_item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/test_item_wise_sales_register.py @@ -7,7 +7,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.tests.utils import ERPNextTestSuite -class TestItemWiseSalesRegister(AccountsTestMixin, ERPNextTestSuite): +class TestItemWiseSalesRegister(ERPNextTestSuite, AccountsTestMixin): def setUp(self): self.create_company() self.create_customer() diff --git a/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py index 90d28033f19..4a509f63843 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py +++ b/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py @@ -12,7 +12,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.tests.utils import ERPNextTestSuite -class TestProfitAndLossStatement(AccountsTestMixin, ERPNextTestSuite): +class TestProfitAndLossStatement(ERPNextTestSuite, AccountsTestMixin): def setUp(self): self.create_company() self.create_customer() diff --git a/erpnext/accounts/report/sales_register/test_sales_register.py b/erpnext/accounts/report/sales_register/test_sales_register.py index ca284cc636c..c6926d57dea 100644 --- a/erpnext/accounts/report/sales_register/test_sales_register.py +++ b/erpnext/accounts/report/sales_register/test_sales_register.py @@ -7,7 +7,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.tests.utils import ERPNextTestSuite -class TestItemWiseSalesRegister(AccountsTestMixin, ERPNextTestSuite): +class TestItemWiseSalesRegister(ERPNextTestSuite, AccountsTestMixin): def setUp(self): self.create_company() self.create_customer() diff --git a/erpnext/accounts/report/supplier_ledger_summary/test_supplier_ledger_summary.py b/erpnext/accounts/report/supplier_ledger_summary/test_supplier_ledger_summary.py index 418d86abb66..4ee0ea04677 100644 --- a/erpnext/accounts/report/supplier_ledger_summary/test_supplier_ledger_summary.py +++ b/erpnext/accounts/report/supplier_ledger_summary/test_supplier_ledger_summary.py @@ -7,7 +7,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.tests.utils import ERPNextTestSuite -class TestSupplierLedgerSummary(AccountsTestMixin, ERPNextTestSuite): +class TestSupplierLedgerSummary(ERPNextTestSuite, AccountsTestMixin): def setUp(self): self.create_company() self.create_supplier() diff --git a/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py index a2a732e8de6..49e50b7ff32 100644 --- a/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py @@ -16,7 +16,7 @@ from erpnext.accounts.utils import get_fiscal_year from erpnext.tests.utils import ERPNextTestSuite -class TestTaxWithholdingDetails(AccountsTestMixin, ERPNextTestSuite): +class TestTaxWithholdingDetails(ERPNextTestSuite, AccountsTestMixin): def setUp(self): self.create_company() self.clear_old_entries() diff --git a/erpnext/controllers/tests/test_distributed_discount.py b/erpnext/controllers/tests/test_distributed_discount.py index 4f4911c8537..94b5f43d29f 100644 --- a/erpnext/controllers/tests/test_distributed_discount.py +++ b/erpnext/controllers/tests/test_distributed_discount.py @@ -4,7 +4,7 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde from erpnext.tests.utils import ERPNextTestSuite -class TestTaxesAndTotals(AccountsTestMixin, ERPNextTestSuite): +class TestTaxesAndTotals(ERPNextTestSuite, AccountsTestMixin): @ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": 1}) def test_distributed_discount_amount(self): so = make_sales_order(do_not_save=1) diff --git a/erpnext/controllers/tests/test_reactivity.py b/erpnext/controllers/tests/test_reactivity.py index fa3007087e1..1fdc9c8be73 100644 --- a/erpnext/controllers/tests/test_reactivity.py +++ b/erpnext/controllers/tests/test_reactivity.py @@ -7,7 +7,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.tests.utils import ERPNextTestSuite -class TestReactivity(AccountsTestMixin, ERPNextTestSuite): +class TestReactivity(ERPNextTestSuite, AccountsTestMixin): def setUp(self): self.create_company() self.create_customer() diff --git a/erpnext/edi/doctype/code_list/test_code_list.py b/erpnext/edi/doctype/code_list/test_code_list.py index d37b1ee8f5a..7c9ec54a627 100644 --- a/erpnext/edi/doctype/code_list/test_code_list.py +++ b/erpnext/edi/doctype/code_list/test_code_list.py @@ -2,8 +2,8 @@ # See license.txt # import frappe -from frappe.tests.utils import FrappeTestCase +from erpnext.tests.utils import ERPNextTestSuite -class TestCodeList(FrappeTestCase): +class TestCodeList(ERPNextTestSuite): pass diff --git a/erpnext/edi/doctype/common_code/test_common_code.py b/erpnext/edi/doctype/common_code/test_common_code.py index e9c67b2cc82..19e0f02d99f 100644 --- a/erpnext/edi/doctype/common_code/test_common_code.py +++ b/erpnext/edi/doctype/common_code/test_common_code.py @@ -2,8 +2,8 @@ # See license.txt # import frappe -from frappe.tests.utils import FrappeTestCase +from erpnext.tests.utils import ERPNextTestSuite -class TestCommonCode(FrappeTestCase): +class TestCommonCode(ERPNextTestSuite): pass diff --git a/erpnext/regional/address_template/test_regional_address_template.py b/erpnext/regional/address_template/test_regional_address_template.py index 952748b3338..76e63d40f81 100644 --- a/erpnext/regional/address_template/test_regional_address_template.py +++ b/erpnext/regional/address_template/test_regional_address_template.py @@ -1,8 +1,7 @@ -from unittest import TestCase - import frappe from erpnext.regional.address_template.setup import get_address_templates, update_address_template +from erpnext.tests.utils import ERPNextTestSuite def ensure_country(country): @@ -14,7 +13,7 @@ def ensure_country(country): return c -class TestRegionalAddressTemplate(TestCase): +class TestRegionalAddressTemplate(ERPNextTestSuite): def test_get_address_templates(self): """Get the countries and paths from the templates directory.""" templates = get_address_templates() diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index f6f4e8bea4f..a27674b9191 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -35,7 +35,7 @@ from erpnext.stock.get_item_details import get_bin_details from erpnext.tests.utils import ERPNextTestSuite -class TestSalesOrder(AccountsTestMixin, ERPNextTestSuite): +class TestSalesOrder(ERPNextTestSuite, AccountsTestMixin): def setUp(self): self.create_customer("_Test Customer Credit") From 013aea6b7e8663832dd28ca3097105d1c53f5bca Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 26 Mar 2026 12:44:52 +0530 Subject: [PATCH 11/79] refactor(test): move logic from AccountsTestMixin to ERPNextTestSuite (cherry picked from commit 2b37d7514d799ca705222fc5afe46dfd703734ea) --- .../test_repost_accounting_ledger.py | 100 +++++++++--------- erpnext/tests/utils.py | 8 ++ 2 files changed, 56 insertions(+), 52 deletions(-) diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py index 08075b2b0be..793bde5c99f 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py @@ -9,29 +9,25 @@ from frappe.utils import add_days, nowdate, today from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice -from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.accounts.utils import get_fiscal_year from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries, make_purchase_receipt from erpnext.tests.utils import ERPNextTestSuite -class TestRepostAccountingLedger(ERPNextTestSuite, AccountsTestMixin): +class TestRepostAccountingLedger(ERPNextTestSuite): def setUp(self): - self.create_company() - self.create_customer() - self.create_item() frappe.db.set_single_value("Selling Settings", "validate_selling_price", 0) update_repost_settings() def test_01_basic_functions(self): si = create_sales_invoice( - item=self.item, - company=self.company, - customer=self.customer, - debit_to=self.debit_to, - parent_cost_center=self.cost_center, - cost_center=self.cost_center, + item="_Test Item", + company="_Test Company", + customer="_Test Customer", + debit_to="Debtors - _TC", + parent_cost_center="Main - _TC", + cost_center="Main - _TC", rate=100, ) @@ -48,7 +44,7 @@ class TestRepostAccountingLedger(ERPNextTestSuite, AccountsTestMixin): # Test Validation Error ral = frappe.new_doc("Repost Accounting Ledger") - ral.company = self.company + ral.company = "_Test Company" ral.delete_cancelled_entries = True ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name}) ral.append( @@ -65,7 +61,7 @@ class TestRepostAccountingLedger(ERPNextTestSuite, AccountsTestMixin): ral.save() # manually set an incorrect debit amount in DB - gle = frappe.db.get_all("GL Entry", filters={"voucher_no": si.name, "account": self.debit_to}) + gle = frappe.db.get_all("GL Entry", filters={"voucher_no": si.name, "account": "Debtors - _TC"}) frappe.db.set_value("GL Entry", gle[0], "debit", 90) gl = qb.DocType("GL Entry") @@ -94,23 +90,23 @@ class TestRepostAccountingLedger(ERPNextTestSuite, AccountsTestMixin): def test_02_deferred_accounting_valiations(self): si = create_sales_invoice( - item=self.item, - company=self.company, - customer=self.customer, - debit_to=self.debit_to, - parent_cost_center=self.cost_center, - cost_center=self.cost_center, + item="_Test Item", + company="_Test Company", + customer="_Test Customer", + debit_to="Debtors - _TC", + parent_cost_center="Main - _TC", + cost_center="Main - _TC", rate=100, do_not_submit=True, ) si.items[0].enable_deferred_revenue = True - si.items[0].deferred_revenue_account = self.deferred_revenue + si.items[0].deferred_revenue_account = "Deferred Revenue - _TC" si.items[0].service_start_date = nowdate() si.items[0].service_end_date = add_days(nowdate(), 90) si.save().submit() ral = frappe.new_doc("Repost Accounting Ledger") - ral.company = self.company + ral.company = "_Test Company" ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name}) self.assertRaises(frappe.ValidationError, ral.save) @@ -118,35 +114,35 @@ class TestRepostAccountingLedger(ERPNextTestSuite, AccountsTestMixin): def test_04_pcv_validation(self): # Clear old GL entries so PCV can be submitted. gl = frappe.qb.DocType("GL Entry") - qb.from_(gl).delete().where(gl.company == self.company).run() + qb.from_(gl).delete().where(gl.company == "_Test Company").run() si = create_sales_invoice( - item=self.item, - company=self.company, - customer=self.customer, - debit_to=self.debit_to, - parent_cost_center=self.cost_center, - cost_center=self.cost_center, + item="_Test Item", + company="_Test Company", + customer="_Test Customer", + debit_to="Debtors - _TC", + parent_cost_center="Main - _TC", + cost_center="Main - _TC", rate=100, ) - fy = get_fiscal_year(today(), company=self.company) + fy = get_fiscal_year(today(), company="_Test Company") pcv = frappe.get_doc( { "doctype": "Period Closing Voucher", "transaction_date": today(), "period_start_date": fy[1], "period_end_date": today(), - "company": self.company, + "company": "_Test Company", "fiscal_year": fy[0], - "cost_center": self.cost_center, - "closing_account_head": self.retained_earnings, + "cost_center": "Main - _TC", + "closing_account_head": "Retained Earnings - _TC", "remarks": "test", } ) pcv.save().submit() ral = frappe.new_doc("Repost Accounting Ledger") - ral.company = self.company + ral.company = "_Test Company" ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name}) self.assertRaises(frappe.ValidationError, ral.save) @@ -156,12 +152,12 @@ class TestRepostAccountingLedger(ERPNextTestSuite, AccountsTestMixin): def test_03_deletion_flag_and_preview_function(self): si = create_sales_invoice( - item=self.item, - company=self.company, - customer=self.customer, - debit_to=self.debit_to, - parent_cost_center=self.cost_center, - cost_center=self.cost_center, + item="_Test Item", + company="_Test Company", + customer="_Test Customer", + debit_to="Debtors - _TC", + parent_cost_center="Main - _TC", + cost_center="Main - _TC", rate=100, ) @@ -170,7 +166,7 @@ class TestRepostAccountingLedger(ERPNextTestSuite, AccountsTestMixin): # with deletion flag set ral = frappe.new_doc("Repost Accounting Ledger") - ral.company = self.company + ral.company = "_Test Company" ral.delete_cancelled_entries = True ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name}) ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name}) @@ -181,12 +177,12 @@ class TestRepostAccountingLedger(ERPNextTestSuite, AccountsTestMixin): def test_05_without_deletion_flag(self): si = create_sales_invoice( - item=self.item, - company=self.company, - customer=self.customer, - debit_to=self.debit_to, - parent_cost_center=self.cost_center, - cost_center=self.cost_center, + item="_Test Item", + company="_Test Company", + customer="_Test Customer", + debit_to="Debtors - _TC", + parent_cost_center="Main - _TC", + cost_center="Main - _TC", rate=100, ) @@ -195,7 +191,7 @@ class TestRepostAccountingLedger(ERPNextTestSuite, AccountsTestMixin): # without deletion flag set ral = frappe.new_doc("Repost Accounting Ledger") - ral.company = self.company + ral.company = "_Test Company" ral.delete_cancelled_entries = False ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name}) ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name}) @@ -210,16 +206,16 @@ class TestRepostAccountingLedger(ERPNextTestSuite, AccountsTestMixin): provisional_account = create_account( account_name="Provision Account", parent_account="Current Liabilities - _TC", - company=self.company, + company="_Test Company", ) another_provisional_account = create_account( account_name="Another Provision Account", parent_account="Current Liabilities - _TC", - company=self.company, + company="_Test Company", ) - company = frappe.get_doc("Company", self.company) + company = frappe.get_doc("Company", "_Test Company") company.enable_provisional_accounting_for_non_stock_items = 1 company.default_provisional_account = provisional_account company.save() @@ -229,7 +225,7 @@ class TestRepostAccountingLedger(ERPNextTestSuite, AccountsTestMixin): item = make_item(properties={"is_stock_item": 0}) - pr = make_purchase_receipt(company=self.company, item_code=item.name, rate=1000.0, qty=1.0) + pr = make_purchase_receipt(company="_Test Company", item_code=item.name, rate=1000.0, qty=1.0) pr_gl_entries = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True) expected_pr_gles = [ {"account": provisional_account, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc}, @@ -246,7 +242,7 @@ class TestRepostAccountingLedger(ERPNextTestSuite, AccountsTestMixin): ) repost_doc = frappe.new_doc("Repost Accounting Ledger") - repost_doc.company = self.company + repost_doc.company = "_Test Company" repost_doc.delete_cancelled_entries = True repost_doc.append("vouchers", {"voucher_type": pr.doctype, "voucher_no": pr.name}) repost_doc.save().submit() diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py index 6aefc4da247..e2a2a7ab195 100644 --- a/erpnext/tests/utils.py +++ b/erpnext/tests/utils.py @@ -1980,6 +1980,14 @@ class BootStrapTestData: ["_Test Payable", "Current Liabilities", 0, "Payable", None], ["_Test Receivable USD", "Current Assets", 0, "Receivable", "USD"], ["_Test Payable USD", "Current Liabilities", 0, "Payable", "USD"], + # Deferred Account + ["Deferred Revenue", "Current Liabilities", 0, None, None], + ["Deferred Expense", "Current Assets", 0, None, None], + # Bank + ["HDFC", "Bank Accounts", 0, "Bank", None], + # Advance Account + ["Advance Received", "Current Liabilities", 0, "Receivable", None], + ["Advance Paid", "Current Assets", 0, "Payable", None], # Loyalty Account ["Loyalty", "Direct Expenses", 0, "Expense Account", None], ] From 85b08e4706bb28b3899a532ea4d1dd80d191c829 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 27 Mar 2026 11:56:30 +0530 Subject: [PATCH 12/79] refactor(test): remove AccountsTestMixin from distributed discount (cherry picked from commit 0b6546ea0689aed862b862df9c85c48a679dc1f9) --- erpnext/controllers/tests/test_distributed_discount.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/controllers/tests/test_distributed_discount.py b/erpnext/controllers/tests/test_distributed_discount.py index 94b5f43d29f..e5efe9518b5 100644 --- a/erpnext/controllers/tests/test_distributed_discount.py +++ b/erpnext/controllers/tests/test_distributed_discount.py @@ -1,10 +1,9 @@ -from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.tests.utils import ERPNextTestSuite -class TestTaxesAndTotals(ERPNextTestSuite, AccountsTestMixin): +class TestTaxesAndTotals(ERPNextTestSuite): @ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": 1}) def test_distributed_discount_amount(self): so = make_sales_order(do_not_save=1) From ee61d796313d55c1e2b28f3dfa60373074b409af Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 27 Mar 2026 11:56:52 +0530 Subject: [PATCH 13/79] refactor(test): remove AccountsTestMixin from reactivity (cherry picked from commit d2ee967383aee47b6b6ca0e5066a700b59aa051c) --- erpnext/accounts/test/accounts_mixin.py | 20 ----------- erpnext/controllers/tests/test_reactivity.py | 38 ++++++-------------- 2 files changed, 10 insertions(+), 48 deletions(-) diff --git a/erpnext/accounts/test/accounts_mixin.py b/erpnext/accounts/test/accounts_mixin.py index 5b1c9e6aa57..c7619e8afd9 100644 --- a/erpnext/accounts/test/accounts_mixin.py +++ b/erpnext/accounts/test/accounts_mixin.py @@ -229,23 +229,3 @@ class AccountsTestMixin: ] for doctype in doctype_list: qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run() - - def create_price_list(self): - pl_name = "Mixin Price List" - if not frappe.db.exists("Price List", pl_name): - self.price_list = ( - frappe.get_doc( - { - "doctype": "Price List", - "currency": "INR", - "enabled": True, - "selling": True, - "buying": True, - "price_list_name": pl_name, - } - ) - .insert() - .name - ) - else: - self.price_list = frappe.get_doc("Price List", pl_name).name diff --git a/erpnext/controllers/tests/test_reactivity.py b/erpnext/controllers/tests/test_reactivity.py index 1fdc9c8be73..17f6f480589 100644 --- a/erpnext/controllers/tests/test_reactivity.py +++ b/erpnext/controllers/tests/test_reactivity.py @@ -2,36 +2,17 @@ import frappe from frappe import qb from frappe.utils import today -from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import disable_dimension -from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.tests.utils import ERPNextTestSuite -class TestReactivity(ERPNextTestSuite, AccountsTestMixin): - def setUp(self): - self.create_company() - self.create_customer() - self.create_item() - self.create_usd_receivable_account() - self.create_price_list() - self.clear_old_entries() - - def disable_dimensions(self): - res = frappe.db.get_all("Accounting Dimension", filters={"disabled": False}) - for x in res: - dim = frappe.get_doc("Accounting Dimension", x.name) - dim.disabled = True - dim.save() - +class TestReactivity(ERPNextTestSuite): def test_01_basic_item_details(self): - self.disable_dimensions() - # set Item Price frappe.get_doc( { "doctype": "Item Price", - "item_code": self.item, - "price_list": self.price_list, + "item_code": "_Test Item", + "price_list": "Standard Selling", "price_list_rate": 90, "selling": True, "rate": 90, @@ -42,17 +23,18 @@ class TestReactivity(ERPNextTestSuite, AccountsTestMixin): si = frappe.get_doc( { "doctype": "Sales Invoice", - "company": self.company, - "customer": self.customer, - "debit_to": self.debit_to, + "company": "_Test Company", + "customer": "_Test Customer", + "debit_to": "Debtors - _TC", "posting_date": today(), - "cost_center": self.cost_center, + "cost_center": "Main - _TC", + "currency": "INR", "conversion_rate": 1, - "selling_price_list": self.price_list, + "selling_price_list": "Standard Selling", } ) itm = si.append("items") - itm.item_code = self.item + itm.item_code = "_Test Item" si.process_item_selection(itm.idx) self.assertEqual(itm.rate, 90) From 05f47bbf6e5df7f85ff3a27458121cb7d39ca460 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 27 Mar 2026 12:12:48 +0530 Subject: [PATCH 14/79] refactor(test): remove AccountsTestMixin from Sales Order (cherry picked from commit 2aecf0103a6b0d79f743e5214c3bc52ffc26537a) --- .../doctype/sales_order/test_sales_order.py | 52 ++++++++----------- 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index a27674b9191..6616c52b720 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -10,7 +10,6 @@ from frappe.core.doctype.user_permission.test_user_permission import create_user from frappe.tests import change_settings from frappe.utils import add_days, flt, nowdate, today -from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.controllers.accounts_controller import InvalidQtyError, get_due_date, update_child_qty_rate from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import ( make_maintenance_schedule, @@ -35,10 +34,7 @@ from erpnext.stock.get_item_details import get_bin_details from erpnext.tests.utils import ERPNextTestSuite -class TestSalesOrder(ERPNextTestSuite, AccountsTestMixin): - def setUp(self): - self.create_customer("_Test Customer Credit") - +class TestSalesOrder(ERPNextTestSuite): @ERPNextTestSuite.change_settings( "Stock Settings", { @@ -2439,7 +2435,7 @@ class TestSalesOrder(ERPNextTestSuite, AccountsTestMixin): def test_credit_limit_on_so_reopning(self): # set credit limit company = "_Test Company" - customer = frappe.get_doc("Customer", self.customer) + customer = frappe.get_doc("Customer", "_Test Customer") customer.credit_limits = [] customer.append( "credit_limits", {"company": company, "credit_limit": 1000, "bypass_credit_limit_check": False} @@ -2447,35 +2443,33 @@ class TestSalesOrder(ERPNextTestSuite, AccountsTestMixin): customer.save() so1 = make_sales_order(qty=9, rate=100, do_not_submit=True) - so1.customer = self.customer + so1.customer = customer.name so1.save().submit() so1.update_status("Closed") so2 = make_sales_order(qty=9, rate=100, do_not_submit=True) - so2.customer = self.customer + so2.customer = customer.name so2.save().submit() self.assertRaises(frappe.ValidationError, so1.update_status, "Draft") @ERPNextTestSuite.change_settings("Stock Settings", {"enable_stock_reservation": True}) def test_warehouse_mapping_based_on_stock_reservation(self): - self.create_company(company_name="Glass Ceiling", abbr="GC") - self.create_item("Lamy Safari 2", True, self.warehouse_stores, self.company, 2000) - self.create_customer() - self.clear_old_entries() + warehouse = "Stores - _TC" + warehouse_finished = "Finished Goods - _TC" so = frappe.new_doc("Sales Order") - so.company = self.company - so.customer = self.customer + so.company = "_Test Company" + so.customer = "_Test Customer" so.transaction_date = today() so.append( "items", { - "item_code": self.item, + "item_code": "_Test Item", "qty": 10, "rate": 2000, - "warehouse": self.warehouse_stores, + "warehouse": "Stores - _TC", "delivery_date": today(), }, ) @@ -2485,12 +2479,12 @@ class TestSalesOrder(ERPNextTestSuite, AccountsTestMixin): se = frappe.get_doc( { "doctype": "Stock Entry", - "company": self.company, + "company": "_Test Company", "stock_entry_type": "Material Receipt", "posting_date": today(), "items": [ - {"item_code": self.item, "t_warehouse": self.warehouse_stores, "qty": 5}, - {"item_code": self.item, "t_warehouse": self.warehouse_finished_goods, "qty": 5}, + {"item_code": "_Test Item", "t_warehouse": warehouse, "qty": 5}, + {"item_code": "_Test Item", "t_warehouse": warehouse_finished, "qty": 5}, ], } ) @@ -2503,7 +2497,7 @@ class TestSalesOrder(ERPNextTestSuite, AccountsTestMixin): { "sales_order_item": itm.name, "item_code": itm.item_code, - "warehouse": self.warehouse_stores, + "warehouse": warehouse, "qty_to_reserve": 2, } ] @@ -2513,7 +2507,7 @@ class TestSalesOrder(ERPNextTestSuite, AccountsTestMixin): { "sales_order_item": itm.name, "item_code": itm.item_code, - "warehouse": self.warehouse_finished_goods, + "warehouse": warehouse_finished, "qty_to_reserve": 3, } ] @@ -2523,31 +2517,31 @@ class TestSalesOrder(ERPNextTestSuite, AccountsTestMixin): dn = make_delivery_note(so.name, kwargs={"for_reserved_stock": True}) self.assertEqual(2, len(dn.items)) self.assertEqual(dn.items[0].qty, 2) - self.assertEqual(dn.items[0].warehouse, self.warehouse_stores) + self.assertEqual(dn.items[0].warehouse, warehouse) self.assertEqual(dn.items[1].qty, 3) - self.assertEqual(dn.items[1].warehouse, self.warehouse_finished_goods) + self.assertEqual(dn.items[1].warehouse, warehouse_finished) from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse - warehouse = create_warehouse("Test Warehouse 1", company=self.company) + warehouse = create_warehouse("Test Warehouse 1", company="_Test Company") make_stock_entry( - item_code=self.item, + item_code="_Test Item", target=warehouse, qty=5, - company=self.company, + company="_Test Company", ) so = frappe.new_doc("Sales Order") so.reserve_stock = 1 - so.company = self.company - so.customer = self.customer + so.company = "_Test Company" + so.customer = "_Test Customer" so.transaction_date = today() so.currency = "INR" so.append( "items", { - "item_code": self.item, + "item_code": "_Test Item", "qty": 5, "rate": 2000, "warehouse": warehouse, From 2c81f79df7d3a9d5ce88449a3533605d4dabc0e1 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 31 Mar 2026 16:09:00 +0530 Subject: [PATCH 15/79] fix: rejected serial no field showing even if serial / batch feature not enabled (cherry picked from commit c2f419ac3d004bff2464fdd1d690c78d8f0ff549) --- erpnext/public/js/utils.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 6fb1b88060c..06ccc25a94e 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -44,7 +44,11 @@ $.extend(erpnext, { } if (["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"].includes(frm.doc.doctype)) { - fields.push("add_serial_batch_for_rejected_qty", "rejected_serial_and_batch_bundle"); + fields.push( + "add_serial_batch_for_rejected_qty", + "rejected_serial_and_batch_bundle", + "rejected_serial_no" + ); } let child_name = "items"; From 573a1a0dcb7f11c35cfa75c18d23f71a6a2a41d2 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 16:29:29 +0530 Subject: [PATCH 16/79] fix: do not show inv dimension unnecessarily in stock entry (backport #53946) (#53951) Co-authored-by: Mihir Kandoi fix: do not show inv dimension unnecessarily in stock entry (#53946) --- .../stock/doctype/inventory_dimension/inventory_dimension.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py index 469e4d5e53a..53a2e45f1df 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py @@ -182,6 +182,7 @@ class InventoryDimension(Document): insert_after="inventory_dimension", options=self.reference_document, label=_(label), + depends_on="eval:doc.s_warehouse" if doctype == "Stock Entry Detail" else "", search_index=1, reqd=self.reqd, mandatory_depends_on=self.mandatory_depends_on, @@ -273,7 +274,7 @@ class InventoryDimension(Document): elif doctype != "Stock Entry Detail": display_depends_on = "eval:parent.is_internal_customer == 1" elif doctype == "Stock Entry Detail": - display_depends_on = "eval:parent.purpose != 'Material Issue'" + display_depends_on = "eval:doc.t_warehouse" fieldname = f"{fieldname_start_with}_{self.source_fieldname}" label = f"{label_start_with} {self.dimension_name}" From 04cced2fb5f8e34d67b23500c7eb0a5a69fef721 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Thu, 26 Mar 2026 16:23:14 +0530 Subject: [PATCH 17/79] fix: prevent selection of group type customer group in customer master (cherry picked from commit 6068dc959f5c0594749f8f6bb3406c77c08e131d) --- erpnext/selling/doctype/customer/customer.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index d5e44e41a7f..08bea658d9d 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -173,6 +173,7 @@ class Customer(TransactionBase): def validate(self): self.flags.is_new_doc = self.is_new() self.flags.old_lead = self.lead_name + self.validate_customer_group() validate_party_accounts(self) self.validate_credit_limit_on_change() self.set_loyalty_program() @@ -356,6 +357,17 @@ class Customer(TransactionBase): frappe.NameError, ) + def validate_customer_group(self): + if not self.customer_group: + return + + is_group = frappe.db.get_value("Customer Group", self.customer_group, "is_group") + if is_group: + frappe.throw( + _("Cannot select a Group type Customer Group. Please select a non-group Customer Group."), + title=_("Invalid Customer Group"), + ) + def validate_credit_limit_on_change(self): if self.get("__islocal") or not self.credit_limits: return From 8674aafc86897bd2cd4ede47bc2a0d0287e548cf Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Tue, 31 Mar 2026 15:29:22 +0530 Subject: [PATCH 18/79] fix(test): do not use is_group enabled customer group in test (cherry picked from commit 75fa2b227711eb4e90d5c868db80cc42b25fe2f7) --- .../doctype/bank_transaction/test_bank_transaction.py | 4 ++-- .../test_opening_invoice_creation_tool.py | 2 +- erpnext/controllers/tests/test_qty_based_taxes.py | 2 +- erpnext/stock/doctype/shipment/test_shipment.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py index c69d255c51a..c7668a5a592 100644 --- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py @@ -382,7 +382,7 @@ def add_vouchers(gl_account="_Test Bank - _TC"): frappe.get_doc( { "doctype": "Customer", - "customer_group": "All Customer Groups", + "customer_group": "Individual", "customer_type": "Company", "customer_name": "Poore Simon's", } @@ -413,7 +413,7 @@ def add_vouchers(gl_account="_Test Bank - _TC"): frappe.get_doc( { "doctype": "Customer", - "customer_group": "All Customer Groups", + "customer_group": "Individual", "customer_type": "Company", "customer_name": "Fayva", } 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 c01ada6d317..4b64dc57306 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 @@ -180,7 +180,7 @@ def make_customer(customer=None): { "doctype": "Customer", "customer_name": customer_name, - "customer_group": "All Customer Groups", + "customer_group": "Individual", "customer_type": "Company", "territory": "All Territories", } diff --git a/erpnext/controllers/tests/test_qty_based_taxes.py b/erpnext/controllers/tests/test_qty_based_taxes.py index e3ddb0d1e1b..d934066a2b4 100644 --- a/erpnext/controllers/tests/test_qty_based_taxes.py +++ b/erpnext/controllers/tests/test_qty_based_taxes.py @@ -68,7 +68,7 @@ class TestTaxes(ERPNextTestSuite): { "doctype": "Customer", "customer_name": uuid4(), - "customer_group": "All Customer Groups", + "customer_group": "Individual", } ).insert() self.supplier = frappe.get_doc( diff --git a/erpnext/stock/doctype/shipment/test_shipment.py b/erpnext/stock/doctype/shipment/test_shipment.py index 7e58f942faf..0ee89f62ee8 100644 --- a/erpnext/stock/doctype/shipment/test_shipment.py +++ b/erpnext/stock/doctype/shipment/test_shipment.py @@ -177,7 +177,7 @@ def create_shipment_customer(customer_name): customer = frappe.new_doc("Customer") customer.customer_name = customer_name customer.customer_type = "Company" - customer.customer_group = "All Customer Groups" + customer.customer_group = "Individual" customer.territory = "All Territories" customer.insert() return customer From f42a1e8a147b793cefb13eee45aeee0ed0085b80 Mon Sep 17 00:00:00 2001 From: nishkagosalia Date: Tue, 31 Mar 2026 17:52:41 +0530 Subject: [PATCH 19/79] fix: Party Field only visibile when party type selected (cherry picked from commit e9e510a76e09da7ffed1e6f1e7be996080f585c6) --- erpnext/accounts/report/general_ledger/general_ledger.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index 46ce1933834..1f66b2768a6 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -74,6 +74,7 @@ frappe.query_reports["General Ledger"] = { label: __("Party"), fieldtype: "MultiSelectList", options: "party_type", + depends_on: "party_type", get_data: function (txt) { if (!frappe.query_report.filters) return; From c4c76cc1b2e65f510d57fa65a3ef966454ef6266 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 31 Mar 2026 17:23:01 +0530 Subject: [PATCH 20/79] fix: ensure accurate rounding for item-wise tax and taxable amounts (cherry picked from commit 9b37f2d95c343b878fb3872575bf231a96e93416) --- erpnext/controllers/taxes_and_totals.py | 8 ++- .../tests/test_item_wise_tax_details.py | 57 +++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index f0da61ad900..6f34e6bbb6e 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -611,7 +611,9 @@ class calculate_taxes_and_totals: flt(tax._running_txn_tax_total, tax.precision("tax_amount")) * self.doc.conversion_rate, tax.precision("base_tax_amount"), ) - item_wise_tax_amount = new_base_tax_total - tax._running_base_tax_total + item_wise_tax_amount = flt( + new_base_tax_total - tax._running_base_tax_total, tax.precision("base_tax_amount") + ) tax._running_base_tax_total = new_base_tax_total if tax.charge_type != "On Item Quantity": @@ -620,7 +622,9 @@ class calculate_taxes_and_totals: flt(tax._running_txn_taxable_total, tax.precision("net_amount")) * self.doc.conversion_rate, tax.precision("base_net_amount"), ) - item_wise_taxable_amount = new_base_taxable_total - tax._running_base_taxable_total + item_wise_taxable_amount = flt( + new_base_taxable_total - tax._running_base_taxable_total, tax.precision("base_net_amount") + ) tax._running_base_taxable_total = new_base_taxable_total else: item_wise_taxable_amount = 0.0 diff --git a/erpnext/controllers/tests/test_item_wise_tax_details.py b/erpnext/controllers/tests/test_item_wise_tax_details.py index 30f1e51d7f4..a921442472e 100644 --- a/erpnext/controllers/tests/test_item_wise_tax_details.py +++ b/erpnext/controllers/tests/test_item_wise_tax_details.py @@ -204,6 +204,63 @@ class TestTaxesAndTotals(ERPNextTestSuite): for tax in doc.taxes: self.assertEqual(details_by_tax[tax.name], tax.base_tax_amount_after_discount_amount) + @change_settings("Selling Settings", {"allow_multiple_items": 1}) + def test_rounding_in_item_wise_tax_details(self): + """ + This test verifies the amounts are properly rounded. + """ + doc = frappe.get_doc( + { + "doctype": "Sales Invoice", + "customer": "_Test Customer", + "company": "_Test Company", + "currency": "INR", + "conversion_rate": 1, + "items": [ + { + "item_code": "_Test Item", + "qty": 5, + "rate": 20, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + { + "item_code": "_Test Item", + "qty": 3, + "rate": 19, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + { + "item_code": "_Test Item", + "qty": 1, + "rate": 1000, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + ], + "taxes": [ + { + "charge_type": "On Net Total", + "account_head": "_Test Account VAT - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "rate": 9, + }, + ], + } + ) + doc.save() + + # item 1: taxable=100, tax=9.0; item 2: taxable=57, tax=5.13; item 3: taxable=1000, tax=90.0 + # error diffusion: 14.13 - 9.0 = 5.130000000000001 without rounding + # 3rd item ensures the artifact is on a middle row (not corrected by last-row adjustment) + for detail in doc.item_wise_tax_details: + self.assertEqual(detail.amount, round(detail.amount, 2)) + def test_item_wise_tax_detail_with_multi_currency_with_single_item(self): """ When the tax amount (in transaction currency) has more decimals than From 9386c1328ad933186778677ec1c7e0a5a76de879 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 31 Mar 2026 17:46:46 +0530 Subject: [PATCH 21/79] test: improve test case (cherry picked from commit b73b161cbed791a8eb5e2f2939ed85a9c6534b50) --- erpnext/controllers/tests/test_item_wise_tax_details.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/tests/test_item_wise_tax_details.py b/erpnext/controllers/tests/test_item_wise_tax_details.py index a921442472e..7e19c1dc057 100644 --- a/erpnext/controllers/tests/test_item_wise_tax_details.py +++ b/erpnext/controllers/tests/test_item_wise_tax_details.py @@ -1,6 +1,7 @@ import json import frappe +from frappe.utils import flt from erpnext.tests.utils import ERPNextTestSuite, change_settings @@ -257,9 +258,8 @@ class TestTaxesAndTotals(ERPNextTestSuite): # item 1: taxable=100, tax=9.0; item 2: taxable=57, tax=5.13; item 3: taxable=1000, tax=90.0 # error diffusion: 14.13 - 9.0 = 5.130000000000001 without rounding - # 3rd item ensures the artifact is on a middle row (not corrected by last-row adjustment) for detail in doc.item_wise_tax_details: - self.assertEqual(detail.amount, round(detail.amount, 2)) + self.assertEqual(detail.amount, flt(detail.amount, detail.precision("amount"))) def test_item_wise_tax_detail_with_multi_currency_with_single_item(self): """ From e230f72e0ba93e49c8564c6f2ffb84b1659eb596 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 15:51:58 +0000 Subject: [PATCH 22/79] fix: include rejected qty in tax (purchase receipt) (backport #53624) (#53972) Co-authored-by: Mihir Kandoi fix: include rejected qty in tax (purchase receipt) (#53624) --- erpnext/controllers/buying_controller.py | 2 +- erpnext/controllers/taxes_and_totals.py | 20 +++++++++++-- .../purchase_receipt/purchase_receipt.py | 2 +- .../purchase_receipt/test_purchase_receipt.py | 29 ++++++++++++++++++- 4 files changed, 48 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 6383049be9c..fd86291027e 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -457,7 +457,7 @@ class BuyingController(SubcontractingController): get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0 ) - net_rate = item.base_net_amount + net_rate = item.qty * item.base_net_rate if item.sales_incoming_rate: # for internal transfer net_rate = item.qty * item.sales_incoming_rate diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index f0da61ad900..c9e8ad9be82 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -164,6 +164,9 @@ class calculate_taxes_and_totals: return if not self.discount_amount_applied: + bill_for_rejected_quantity_in_purchase_invoice = frappe.get_single_value( + "Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice" + ) for item in self.doc.items: self.doc.round_floats_in(item) @@ -225,7 +228,13 @@ class calculate_taxes_and_totals: elif not item.qty and self.doc.get("is_debit_note"): item.amount = flt(item.rate, item.precision("amount")) else: - item.amount = flt(item.rate * item.qty, item.precision("amount")) + qty = ( + (item.qty + item.rejected_qty) + if bill_for_rejected_quantity_in_purchase_invoice + and self.doc.doctype == "Purchase Receipt" + else item.qty + ) + item.amount = flt(item.rate * qty, item.precision("amount")) item.net_amount = item.amount @@ -379,9 +388,16 @@ class calculate_taxes_and_totals: self.doc.total ) = self.doc.base_total = self.doc.net_total = self.doc.base_net_total = 0.0 + bill_for_rejected_quantity_in_purchase_invoice = frappe.get_single_value( + "Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice" + ) for item in self._items: self.doc.total += item.amount - self.doc.total_qty += item.qty + self.doc.total_qty += ( + (item.qty + item.rejected_qty) + if bill_for_rejected_quantity_in_purchase_invoice and self.doc.doctype == "Purchase Receipt" + else item.qty + ) self.doc.base_total += item.base_amount self.doc.net_total += item.net_amount self.doc.base_net_total += item.base_net_amount diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index e78faa9511a..dc8885b1ca4 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -561,7 +561,7 @@ class PurchaseReceipt(BuyingController): else flt(item.net_amount, item.precision("net_amount")) ) - outgoing_amount = item.base_net_amount + outgoing_amount = item.qty * item.base_net_rate if self.is_internal_transfer() and item.valuation_rate: outgoing_amount = abs(get_stock_value_difference(self.name, item.name, item.from_warehouse)) credit_amount = outgoing_amount diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 6eba41c3883..50f28a75b18 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -4610,7 +4610,7 @@ class TestPurchaseReceipt(ERPNextTestSuite): self.assertEqual(srbnb_cost, 1500) - def test_valuation_rate_for_rejected_materials_withoout_accepted_materials(self): + def test_valuation_rate_for_rejected_materials_without_accepted_materials(self): item = make_item("Test Item with Rej Material Valuation WO Accepted", {"is_stock_item": 1}) company = "_Test Company with perpetual inventory" @@ -5423,6 +5423,33 @@ class TestPurchaseReceipt(ERPNextTestSuite): self.assertEqual(row.warehouse, "_Test Warehouse 1 - _TC") self.assertEqual(row.incoming_rate, 100) + def test_bill_for_rejected_quantity_in_purchase_invoice(self): + item_code = make_item("Test Rejected Qty", {"is_stock_item": 1}).name + + with self.change_settings("Buying Settings", {"bill_for_rejected_quantity_in_purchase_invoice": 0}): + pr = make_purchase_receipt( + item_code=item_code, + qty=10, + rejected_qty=2, + rate=10, + warehouse="_Test Warehouse - _TC", + ) + + self.assertEqual(pr.total_qty, 10) + self.assertEqual(pr.total, 100) + + with self.change_settings("Buying Settings", {"bill_for_rejected_quantity_in_purchase_invoice": 1}): + pr = make_purchase_receipt( + item_code=item_code, + qty=10, + rejected_qty=2, + rate=10, + warehouse="_Test Warehouse - _TC", + ) + + self.assertEqual(pr.total_qty, 12) + self.assertEqual(pr.total, 120) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier From 8db397bdae1f273d2f336abadafc4627469381d5 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 1 Apr 2026 13:11:39 +0530 Subject: [PATCH 23/79] feat: co product by product support (#52979) (#53975) --- erpnext/controllers/stock_controller.py | 2 +- .../controllers/subcontracting_controller.py | 26 +- .../subcontracting_inward_controller.py | 109 ++++---- .../tests/test_subcontracting_controller.py | 54 +++- erpnext/manufacturing/doctype/bom/bom.js | 53 +--- erpnext/manufacturing/doctype/bom/bom.json | 113 +++++---- erpnext/manufacturing/doctype/bom/bom.py | 166 ++++++++----- erpnext/manufacturing/doctype/bom/test_bom.py | 2 +- .../doctype/bom/test_records.json | 10 +- .../doctype/bom_creator/bom_creator.py | 1 - .../bom_scrap_item/bom_scrap_item.json | 109 -------- .../__init__.py | 0 .../bom_secondary_item.json | 232 ++++++++++++++++++ .../bom_secondary_item.py} | 21 +- .../doctype/job_card/job_card.js | 6 +- .../doctype/job_card/job_card.json | 34 +-- .../doctype/job_card/job_card.py | 71 ++++-- .../doctype/job_card/test_job_card.py | 187 ++++++++++++++ .../__init__.py | 0 .../job_card_secondary_item.json} | 32 ++- .../job_card_secondary_item.py} | 4 +- .../manufacturing_settings.json | 18 +- .../manufacturing_settings.py | 2 +- .../production_plan/test_production_plan.py | 18 ++ .../doctype/work_order/test_work_order.py | 50 ++-- .../doctype/work_order/work_order.js | 1 + .../doctype/work_order/work_order.py | 16 +- erpnext/patches.txt | 1 + erpnext/patches/v16_0/co_by_product_patch.py | 104 ++++++++ erpnext/public/js/controllers/transaction.js | 14 +- .../selling_settings/selling_settings.json | 16 +- .../selling_settings/selling_settings.py | 2 +- erpnext/setup/doctype/company/company.py | 2 +- .../stock/doctype/stock_entry/stock_entry.js | 6 +- .../stock/doctype/stock_entry/stock_entry.py | 210 +++++++++------- .../doctype/stock_entry/test_stock_entry.py | 55 ++++- .../stock_entry_detail.json | 35 ++- .../stock_entry_detail/stock_entry_detail.py | 4 +- .../stock_entry_type/stock_entry_type.py | 13 +- .../subcontracting_inward_order.json | 22 +- .../subcontracting_inward_order.py | 32 +-- .../test_subcontracting_inward_order.py | 10 +- .../__init__.py | 0 ...tracting_inward_order_secondary_item.json} | 23 +- ...ontracting_inward_order_secondary_item.py} | 3 +- .../subcontracting_order.py | 7 + .../subcontracting_order_item.json | 2 +- .../subcontracting_receipt.js | 17 +- .../subcontracting_receipt.json | 18 +- .../subcontracting_receipt.py | 135 ++++++---- .../test_subcontracting_receipt.py | 23 +- .../subcontracting_receipt_item.json | 94 ++++--- .../subcontracting_receipt_item.py | 6 +- 53 files changed, 1492 insertions(+), 699 deletions(-) delete mode 100644 erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json rename erpnext/manufacturing/doctype/{bom_scrap_item => bom_secondary_item}/__init__.py (100%) create mode 100644 erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json rename erpnext/manufacturing/doctype/{bom_scrap_item/bom_scrap_item.py => bom_secondary_item/bom_secondary_item.py} (50%) rename erpnext/manufacturing/doctype/{job_card_scrap_item => job_card_secondary_item}/__init__.py (100%) rename erpnext/manufacturing/doctype/{job_card_scrap_item/job_card_scrap_item.json => job_card_secondary_item/job_card_secondary_item.json} (73%) rename erpnext/manufacturing/doctype/{job_card_scrap_item/job_card_scrap_item.py => job_card_secondary_item/job_card_secondary_item.py} (78%) create mode 100644 erpnext/patches/v16_0/co_by_product_patch.py rename erpnext/subcontracting/doctype/{subcontracting_inward_order_scrap_item => subcontracting_inward_order_secondary_item}/__init__.py (100%) rename erpnext/subcontracting/doctype/{subcontracting_inward_order_scrap_item/subcontracting_inward_order_scrap_item.json => subcontracting_inward_order_secondary_item/subcontracting_inward_order_secondary_item.json} (83%) rename erpnext/subcontracting/doctype/{subcontracting_inward_order_scrap_item/subcontracting_inward_order_scrap_item.py => subcontracting_inward_order_secondary_item/subcontracting_inward_order_secondary_item.py} (81%) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 1fed02a23ed..05f2e18c878 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -1435,7 +1435,7 @@ class StockController(AccountsController): elif self.doctype == "Stock Entry" and row.t_warehouse: qi_required = True # inward stock needs inspection - if row.get("is_scrap_item"): + if row.get("type") or row.get("is_legacy_scrap_item"): continue if qi_required: # validate row only if inspection is required on item level diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 6d886bd9ecf..e922a0ea9fc 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -160,7 +160,7 @@ class SubcontractingController(StockController): ).format(item.idx, get_link_to_form("Item", item.item_code)) ) - if not item.get("is_scrap_item"): + if not item.get("type") and not item.get("is_legacy_scrap_item"): if not is_sub_contracted_item: frappe.throw( _("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name) @@ -206,7 +206,7 @@ class SubcontractingController(StockController): ).format(item.idx, item.item_name) ) - if self.doctype != "Subcontracting Inward Order": + if self.doctype not in ["Subcontracting Inward Order", "Subcontracting Receipt"]: item.amount = item.qty * item.rate if item.bom: @@ -238,7 +238,7 @@ class SubcontractingController(StockController): and self._doc_before_save ): for row in self._doc_before_save.get("items"): - item_dict[row.name] = (row.item_code, row.qty + (row.get("rejected_qty") or 0)) + item_dict[row.name] = (row.item_code, row.received_qty) return item_dict @@ -264,7 +264,7 @@ class SubcontractingController(StockController): self.__reference_name.append(row.name) if (row.name not in item_dict) or ( row.item_code, - row.qty + (row.get("rejected_qty") or 0), + row.received_qty, ) != item_dict[row.name]: self.__changed_name.append(row.name) @@ -962,7 +962,7 @@ class SubcontractingController(StockController): ): qty = ( flt(bom_item.qty_consumed_per_unit) - * flt(row.qty + (row.get("rejected_qty") or 0)) + * flt(row.get("received_qty") or (row.qty + (row.get("rejected_qty") or 0))) * row.conversion_factor ) bom_item.main_item_code = row.item_code @@ -1285,22 +1285,28 @@ class SubcontractingController(StockController): if self.total_additional_costs: if self.distribute_additional_costs_based_on == "Amount": total_amt = sum( - flt(item.amount) for item in self.get("items") if not item.get("is_scrap_item") + flt(item.amount) + for item in self.get("items") + if not item.get("type") and not item.get("is_legacy_scrap_item") ) for item in self.items: - if not item.get("is_scrap_item"): + if not item.get("type") and not item.get("is_legacy_scrap_item"): item.additional_cost_per_qty = ( (item.amount * self.total_additional_costs) / total_amt ) / item.qty else: - total_qty = sum(flt(item.qty) for item in self.get("items") if not item.get("is_scrap_item")) + total_qty = sum( + flt(item.qty) + for item in self.get("items") + if not item.get("type") and not item.get("is_legacy_scrap_item") + ) additional_cost_per_qty = self.total_additional_costs / total_qty for item in self.items: - if not item.get("is_scrap_item"): + if not item.get("type") and not item.get("is_legacy_scrap_item"): item.additional_cost_per_qty = additional_cost_per_qty else: for item in self.items: - if not item.get("is_scrap_item"): + if not item.get("type") and not item.get("is_legacy_scrap_item"): item.additional_cost_per_qty = 0 @frappe.whitelist() diff --git a/erpnext/controllers/subcontracting_inward_controller.py b/erpnext/controllers/subcontracting_inward_controller.py index 1a3ff66b825..6428ca10822 100644 --- a/erpnext/controllers/subcontracting_inward_controller.py +++ b/erpnext/controllers/subcontracting_inward_controller.py @@ -1,3 +1,5 @@ +from collections import defaultdict + import frappe from frappe import _, bold from frappe.query_builder import Case @@ -18,7 +20,7 @@ class SubcontractingInwardController: def on_submit_subcontracting_inward(self): self.update_inward_order_item() self.update_inward_order_received_items() - self.update_inward_order_scrap_items() + self.update_inward_order_secondary_items() self.create_stock_reservation_entries_for_inward() self.update_inward_order_status() @@ -28,7 +30,7 @@ class SubcontractingInwardController: self.validate_delivery() self.validate_receive_from_customer_cancel() self.update_inward_order_received_items() - self.update_inward_order_scrap_items() + self.update_inward_order_secondary_items() self.remove_reference_for_additional_items() self.update_inward_order_status() @@ -239,7 +241,8 @@ class SubcontractingInwardController: item for item in self.get("items") if not item.is_finished_item - and not item.is_scrap_item + and not item.type + and not item.is_legacy_scrap_item and frappe.get_cached_value("Item", item.item_code, "is_customer_provided_item") ] @@ -368,7 +371,9 @@ class SubcontractingInwardController: if self.subcontracting_inward_order: if self.purpose in ["Subcontracting Delivery", "Subcontracting Return", "Manufacture"]: for item in self.items: - if (item.is_finished_item or item.is_scrap_item) and item.valuation_rate == 0: + if ( + item.is_finished_item or item.type or item.is_legacy_scrap_item + ) and item.valuation_rate == 0: item.allow_zero_valuation_rate = 1 def validate_warehouse_(self): @@ -467,7 +472,7 @@ class SubcontractingInwardController: self.validate_delivery_on_save() else: for item in self.items: - if not item.is_scrap_item: + if not item.type and not item.is_legacy_scrap_item: delivered_qty, returned_qty = frappe.get_value( "Subcontracting Inward Order Item", item.scio_detail, @@ -519,7 +524,7 @@ class SubcontractingInwardController: if max_allowed_qty: max_allowed_qty = max_allowed_qty[0] else: - table = frappe.qb.DocType("Subcontracting Inward Order Scrap Item") + table = frappe.qb.DocType("Subcontracting Inward Order Secondary Item") query = ( frappe.qb.from_(table) .select((table.produced_qty - table.delivered_qty).as_("max_allowed_qty")) @@ -538,8 +543,8 @@ class SubcontractingInwardController: bold( frappe.get_cached_value( "Subcontracting Inward Order Item" - if not item.is_scrap_item - else "Subcontracting Inward Order Scrap Item", + if not item.type and not item.is_legacy_scrap_item + else "Subcontracting Inward Order Secondary Item", item.scio_detail, "stock_uom", ) @@ -590,9 +595,9 @@ class SubcontractingInwardController: ) for item in [item for item in self.items if not item.is_finished_item]: - if item.is_scrap_item: - scio_scrap_item = frappe.get_value( - "Subcontracting Inward Order Scrap Item", + if item.type or item.is_legacy_scrap_item: + scio_secondary_item = frappe.get_value( + "Subcontracting Inward Order Secondary Item", { "docstatus": 1, "item_code": item.item_code, @@ -603,12 +608,13 @@ class SubcontractingInwardController: as_dict=True, ) if ( - scio_scrap_item - and scio_scrap_item.delivered_qty > scio_scrap_item.produced_qty - item.transfer_qty + scio_secondary_item + and scio_secondary_item.delivered_qty + > scio_secondary_item.produced_qty - item.transfer_qty ): frappe.throw( _( - "Row #{0}: Cannot cancel this Manufacturing Stock Entry as quantity of Scrap Item {1} produced cannot be less than quantity delivered." + "Row #{0}: Cannot cancel this Manufacturing Stock Entry as quantity of Secondary Item {1} produced cannot be less than quantity delivered." ).format(item.idx, get_link_to_form("Item", item.item_code)) ) else: @@ -648,8 +654,8 @@ class SubcontractingInwardController: for item in self.items: doctype = ( "Subcontracting Inward Order Item" - if not item.is_scrap_item - else "Subcontracting Inward Order Scrap Item" + if not item.type and not item.is_legacy_scrap_item + else "Subcontracting Inward Order Secondary Item" ) frappe.db.set_value( doctype, @@ -763,7 +769,11 @@ class SubcontractingInwardController: customer_warehouse = frappe.get_cached_value( "Subcontracting Inward Order", self.subcontracting_inward_order, "customer_warehouse" ) - items = [item for item in self.items if not item.is_finished_item and not item.is_scrap_item] + items = [ + item + for item in self.items + if not item.is_finished_item and not item.type and not item.is_legacy_scrap_item + ] item_code_wh = frappe._dict( { ( @@ -860,24 +870,24 @@ class SubcontractingInwardController: doc.insert() doc.submit() - def update_inward_order_scrap_items(self): + def update_inward_order_secondary_items(self): if (scio := self.subcontracting_inward_order) and self.purpose == "Manufacture": - scrap_items_list = [item for item in self.items if item.is_scrap_item] - scrap_items = frappe._dict( - { - (item.item_code, item.t_warehouse): item.transfer_qty - if self._action == "submit" - else -item.transfer_qty - for item in scrap_items_list - } - ) - if scrap_items: - item_codes, warehouses = zip(*list(scrap_items.keys()), strict=True) + secondary_items_list = [item for item in self.items if item.type or item.is_legacy_scrap_item] + + secondary_items = defaultdict(float) + for item in secondary_items_list: + secondary_items[(item.item_code, item.t_warehouse)] += ( + item.transfer_qty if self._action == "submit" else -item.transfer_qty + ) + secondary_items = frappe._dict(secondary_items) + + if secondary_items: + item_codes, warehouses = zip(*list(secondary_items.keys()), strict=True) item_codes = list(item_codes) warehouses = list(warehouses) result = frappe.get_all( - "Subcontracting Inward Order Scrap Item", + "Subcontracting Inward Order Secondary Item", filters={ "item_code": ["in", item_codes], "warehouse": ["in", warehouses], @@ -890,7 +900,7 @@ class SubcontractingInwardController: ) if result: - scrap_item_dict = frappe._dict( + secondary_items_dict = frappe._dict( { (d.item_code, d.warehouse): frappe._dict( {"name": d.name, "produced_qty": d.produced_qty} @@ -900,40 +910,45 @@ class SubcontractingInwardController: ) deleted_docs = [] case_expr = Case() - table = frappe.qb.DocType("Subcontracting Inward Order Scrap Item") - for key, value in scrap_item_dict.items(): - if self._action == "cancel" and value.produced_qty - abs(scrap_items.get(key)) == 0: + table = frappe.qb.DocType("Subcontracting Inward Order Secondary Item") + for key, value in secondary_items_dict.items(): + if ( + self._action == "cancel" + and value.produced_qty - abs(secondary_items.get(key)) == 0 + ): deleted_docs.append(value.name) - frappe.delete_doc("Subcontracting Inward Order Scrap Item", value.name) + frappe.delete_doc("Subcontracting Inward Order Secondary Item", value.name) else: case_expr = case_expr.when( - table.name == value.name, value.produced_qty + scrap_items.get(key) + table.name == value.name, value.produced_qty + secondary_items.get(key) ) if final_list := list( - set([v.name for v in scrap_item_dict.values()]) - set(deleted_docs) + set([v.name for v in secondary_items_dict.values()]) - set(deleted_docs) ): frappe.qb.update(table).set(table.produced_qty, case_expr).where( (table.name.isin(final_list)) & (table.docstatus == 1) ).run() fg_item_code = next(fg for fg in self.items if fg.is_finished_item).item_code - for scrap_item in [ + for secondary_item in [ item - for item in scrap_items_list + for item in secondary_items_list if (item.item_code, item.t_warehouse) not in [(d.item_code, d.warehouse) for d in result] ]: doc = frappe.new_doc( - "Subcontracting Inward Order Scrap Item", + "Subcontracting Inward Order Secondary Item", parent=scio, parenttype="Subcontracting Inward Order", - parentfield="scrap_items", - idx=frappe.db.count("Subcontracting Inward Order Scrap Item", {"parent": scio}) + 1, - item_code=scrap_item.item_code, + parentfield="secondary_items", + idx=frappe.db.count("Subcontracting Inward Order Secondary Item", {"parent": scio}) + + 1, + item_code=secondary_item.item_code, fg_item_code=fg_item_code, - stock_uom=scrap_item.stock_uom, - warehouse=scrap_item.t_warehouse, - produced_qty=scrap_item.transfer_qty, + stock_uom=secondary_item.stock_uom, + warehouse=secondary_item.t_warehouse, + produced_qty=secondary_item.transfer_qty, + type=secondary_item.type, delivered_qty=0, reference_name=frappe.get_value( "Work Order", self.work_order, "subcontracting_inward_order_item" @@ -965,7 +980,7 @@ class SubcontractingInwardController: and ( not frappe.db.exists("Subcontracting Inward Order Received Item", item.scio_detail) and not frappe.db.exists("Subcontracting Inward Order Item", item.scio_detail) - and not frappe.db.exists("Subcontracting Inward Order Scrap Item", item.scio_detail) + and not frappe.db.exists("Subcontracting Inward Order Secondary Item", item.scio_detail) ) ] for item in items: diff --git a/erpnext/controllers/tests/test_subcontracting_controller.py b/erpnext/controllers/tests/test_subcontracting_controller.py index 465b318d09b..0dbacb3c22d 100644 --- a/erpnext/controllers/tests/test_subcontracting_controller.py +++ b/erpnext/controllers/tests/test_subcontracting_controller.py @@ -501,8 +501,8 @@ class TestSubcontractingController(ERPNextTestSuite): scr1.items[0].qty = 2 add_second_row_in_scr(scr1) scr1.flags.ignore_mandatory = True - scr1.save() scr1.set_missing_values() + scr1.save() scr1.submit() for _key, value in get_supplied_items(scr1).items(): @@ -513,8 +513,8 @@ class TestSubcontractingController(ERPNextTestSuite): scr2.items[0].qty = 2 add_second_row_in_scr(scr2) scr2.flags.ignore_mandatory = True - scr2.save() scr2.set_missing_values() + scr2.save() scr2.submit() for _key, value in get_supplied_items(scr2).items(): @@ -523,8 +523,8 @@ class TestSubcontractingController(ERPNextTestSuite): scr3 = make_subcontracting_receipt(sco.name) scr3.items[0].qty = 2 scr3.flags.ignore_mandatory = True - scr3.save() scr3.set_missing_values() + scr3.save() scr3.submit() for _key, value in get_supplied_items(scr3).items(): @@ -1164,6 +1164,54 @@ class TestSubcontractingController(ERPNextTestSuite): self.assertEqual([item.rm_item_code for item in sco.supplied_items], expected) + def test_co_by_product(self): + frappe.set_value("UOM", "Nos", "must_be_whole_number", 0) + + fg_item = make_item("FG Item", properties={"is_stock_item": 1, "is_sub_contracted_item": 1}).name + rm_item = make_item("RM Item", properties={"is_stock_item": 1}).name + scrap_item = make_item("Scrap Item", properties={"is_stock_item": 1}).name + make_bom( + item=fg_item, raw_materials=[rm_item], scrap_items=[scrap_item], process_loss_percentage=10 + ).name + + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 11", + "qty": 5, + "rate": 100, + "fg_item": fg_item, + "fg_item_qty": 5, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr1 = make_subcontracting_receipt(sco.name) + scr1.get_secondary_items() + scr1.save() + + self.assertEqual(scr1.items[0].received_qty, 5) + self.assertEqual(scr1.items[0].process_loss_qty, 0.5) + self.assertEqual(scr1.items[0].qty, 4.5) + self.assertEqual(scr1.items[0].rate, 200) + self.assertEqual(scr1.items[0].amount, 900) + + self.assertEqual(scr1.items[1].item_code, scrap_item) + self.assertEqual(scr1.items[1].received_qty, 5) + self.assertEqual(scr1.items[1].process_loss_qty, 0.5) + self.assertEqual(scr1.items[1].qty, 4.5) + self.assertEqual(flt(scr1.items[1].rate, 3), 11.111) + self.assertEqual(scr1.items[1].amount, 50) + + frappe.set_value("UOM", "Nos", "must_be_whole_number", 1) + def add_second_row_in_scr(scr): item_dict = {} diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 454f1934e13..1dc64997198 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -620,10 +620,10 @@ erpnext.bom.BomController = class BomController extends erpnext.TransactionContr } item_code(doc, cdt, cdn) { - var scrap_items = false; + let secondary_items = false; var child = locals[cdt][cdn]; - if (child.doctype == "BOM Scrap Item") { - scrap_items = true; + if (child.doctype == "BOM Secondary Item") { + secondary_items = true; } if (child.bom_no) { @@ -634,7 +634,7 @@ erpnext.bom.BomController = class BomController extends erpnext.TransactionContr child.do_not_explode = 1; } - get_bom_material_detail(doc, cdt, cdn, scrap_items); + get_bom_material_detail(doc, cdt, cdn, secondary_items); } buying_price_list(doc) { @@ -683,7 +683,7 @@ cur_frm.cscript.is_default = function (doc) { if (doc.is_default) cur_frm.set_value("is_active", 1); }; -var get_bom_material_detail = function (doc, cdt, cdn, scrap_items) { +var get_bom_material_detail = function (doc, cdt, cdn, secondary_items) { if (!doc.company) { frappe.throw({ message: __("Please select a Company first."), title: __("Mandatory") }); } @@ -697,7 +697,6 @@ var get_bom_material_detail = function (doc, cdt, cdn, scrap_items) { company: doc.company, item_code: d.item_code, bom_no: d.bom_no != null ? d.bom_no : "", - scrap_items: scrap_items, qty: d.qty, stock_qty: d.stock_qty, include_item_in_manufacturing: d.include_item_in_manufacturing, @@ -706,15 +705,15 @@ var get_bom_material_detail = function (doc, cdt, cdn, scrap_items) { conversion_factor: d.conversion_factor, sourced_by_supplier: d.sourced_by_supplier, do_not_explode: d.do_not_explode, + fetch_rate: !secondary_items, }, callback: function (r) { $.extend(d, r.message); refresh_field("items"); - refresh_field("scrap_items"); + refresh_field("secondary_items"); doc = locals[doc.doctype][doc.name]; erpnext.bom.calculate_rm_cost(doc); - erpnext.bom.calculate_scrap_materials_cost(doc); erpnext.bom.calculate_total(doc); }, freeze: true, @@ -724,20 +723,18 @@ var get_bom_material_detail = function (doc, cdt, cdn, scrap_items) { cur_frm.cscript.qty = function (doc) { erpnext.bom.calculate_rm_cost(doc); - erpnext.bom.calculate_scrap_materials_cost(doc); erpnext.bom.calculate_total(doc); }; cur_frm.cscript.rate = function (doc, cdt, cdn) { var d = locals[cdt][cdn]; - const is_scrap_item = cdt == "BOM Scrap Item"; + const is_secondary_item = cdt == "BOM Secondary Item"; if (d.bom_no) { frappe.msgprint(__("You cannot change the rate if BOM is mentioned against any Item.")); - get_bom_material_detail(doc, cdt, cdn, is_scrap_item); + get_bom_material_detail(doc, cdt, cdn, is_secondary_item); } else { erpnext.bom.calculate_rm_cost(doc); - erpnext.bom.calculate_scrap_materials_cost(doc); erpnext.bom.calculate_total(doc); } }; @@ -745,7 +742,6 @@ cur_frm.cscript.rate = function (doc, cdt, cdn) { erpnext.bom.update_cost = function (doc) { erpnext.bom.calculate_op_cost(doc); erpnext.bom.calculate_rm_cost(doc); - erpnext.bom.calculate_scrap_materials_cost(doc); erpnext.bom.calculate_total(doc); }; @@ -804,34 +800,11 @@ erpnext.bom.calculate_rm_cost = function (doc) { cur_frm.set_value("base_raw_material_cost", base_total_rm_cost); }; -// sm : scrap material -erpnext.bom.calculate_scrap_materials_cost = function (doc) { - var sm = doc.scrap_items || []; - var total_sm_cost = 0; - var base_total_sm_cost = 0; - - for (var i = 0; i < sm.length; i++) { - var base_rate = flt(sm[i].rate) * flt(doc.conversion_rate); - var amount = flt(sm[i].rate) * flt(sm[i].stock_qty); - var base_amount = amount * flt(doc.conversion_rate); - - frappe.model.set_value("BOM Scrap Item", sm[i].name, "base_rate", base_rate); - frappe.model.set_value("BOM Scrap Item", sm[i].name, "amount", amount); - frappe.model.set_value("BOM Scrap Item", sm[i].name, "base_amount", base_amount); - - total_sm_cost += amount; - base_total_sm_cost += base_amount; - } - - cur_frm.set_value("scrap_material_cost", total_sm_cost); - cur_frm.set_value("base_scrap_material_cost", base_total_sm_cost); -}; - // Calculate Total Cost erpnext.bom.calculate_total = function (doc) { - var total_cost = flt(doc.operating_cost) + flt(doc.raw_material_cost) - flt(doc.scrap_material_cost); + var total_cost = flt(doc.operating_cost) + flt(doc.raw_material_cost) - flt(doc.secondary_items_cost); var base_total_cost = - flt(doc.base_operating_cost) + flt(doc.base_raw_material_cost) - flt(doc.base_scrap_material_cost); + flt(doc.base_operating_cost) + flt(doc.base_raw_material_cost) - flt(doc.base_secondary_items_cost); cur_frm.set_value("total_cost", total_cost); cur_frm.set_value("base_total_cost", base_total_cost); @@ -986,7 +959,7 @@ frappe.tour["BOM"] = [ }, ]; -frappe.ui.form.on("BOM Scrap Item", { +frappe.ui.form.on("BOM Secondary Item", { item_code(frm, cdt, cdn) { const { item_code } = locals[cdt][cdn]; }, @@ -1007,7 +980,7 @@ function trigger_process_loss_qty_prompt(frm, cdt, cdn, item_code) { const row = locals[cdt][cdn]; row.stock_qty = (frm.doc.quantity * data.percent) / 100; row.qty = row.stock_qty / (row.conversion_factor || 1); - refresh_field("scrap_items"); + refresh_field("secondary_items"); }, __("Set Process Loss Item Quantity"), __("Set Quantity") diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index 491920a0f29..8574e58a498 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -16,6 +16,14 @@ "allow_alternative_item", "set_rate_of_sub_assembly_item_based_on_bom", "is_phantom_bom", + "cost_allocation_section", + "cost_allocation_per", + "column_break_srby", + "cost_allocation", + "process_loss_section", + "process_loss_percentage", + "column_break_ssj2", + "process_loss_qty", "currency_detail", "rm_cost_as_per", "buying_price_list", @@ -38,21 +46,16 @@ "operations", "materials_section", "items", - "scrap_section", - "scrap_items_section", - "scrap_items", - "process_loss_section", - "process_loss_percentage", - "column_break_ssj2", - "process_loss_qty", + "secondary_items_tab", + "secondary_items", "costing", "operating_cost", "raw_material_cost", - "scrap_material_cost", + "secondary_items_cost", "cb1", "base_operating_cost", "base_raw_material_cost", - "base_scrap_material_cost", + "base_secondary_items_cost", "column_break_26", "total_cost", "base_total_cost", @@ -298,19 +301,6 @@ "options": "BOM Item", "reqd": 1 }, - { - "collapsible": 1, - "depends_on": "eval:!doc.is_phantom_bom", - "fieldname": "scrap_section", - "fieldtype": "Tab Break", - "label": "Scrap & Process Loss" - }, - { - "fieldname": "scrap_items", - "fieldtype": "Table", - "label": "Scrap Items", - "options": "BOM Scrap Item" - }, { "fieldname": "costing", "fieldtype": "Tab Break", @@ -332,15 +322,6 @@ "options": "currency", "read_only": 1 }, - { - "depends_on": "eval:!doc.is_phantom_bom", - "fieldname": "scrap_material_cost", - "fieldtype": "Currency", - "label": "Scrap Material Cost", - "options": "currency", - "print_hide": 1, - "read_only": 1 - }, { "fieldname": "cb1", "fieldtype": "Column Break" @@ -362,15 +343,6 @@ "print_hide": 1, "read_only": 1 }, - { - "depends_on": "eval:!doc.is_phantom_bom", - "fieldname": "base_scrap_material_cost", - "fieldtype": "Currency", - "label": "Scrap Material Cost(Company Currency)", - "no_copy": 1, - "options": "Company:company:default_currency", - "read_only": 1 - }, { "fieldname": "total_cost", "fieldtype": "Currency", @@ -602,12 +574,6 @@ "fieldname": "column_break_ivyw", "fieldtype": "Column Break" }, - { - "fieldname": "scrap_items_section", - "fieldtype": "Section Break", - "hide_border": 1, - "label": "Scrap Items" - }, { "default": "0", "fieldname": "fg_based_operating_cost", @@ -706,6 +672,59 @@ "fieldname": "quality_inspection_tab", "fieldtype": "Tab Break", "label": "Quality Inspection" + }, + { + "fieldname": "secondary_items", + "fieldtype": "Table", + "label": "Secondary Items", + "options": "BOM Secondary Item" + }, + { + "depends_on": "eval:!doc.is_phantom_bom", + "fieldname": "secondary_items_cost", + "fieldtype": "Currency", + "label": "Secondary Items Cost", + "options": "currency", + "print_hide": 1, + "read_only": 1 + }, + { + "depends_on": "eval:!doc.is_phantom_bom", + "fieldname": "base_secondary_items_cost", + "fieldtype": "Currency", + "label": "Secondary Items Cost (Company Currency)", + "no_copy": 1, + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "secondary_items_tab", + "fieldtype": "Tab Break", + "label": "Secondary Items" + }, + { + "fieldname": "cost_allocation_section", + "fieldtype": "Section Break", + "label": "Cost Allocation" + }, + { + "fieldname": "column_break_srby", + "fieldtype": "Column Break" + }, + { + "fieldname": "cost_allocation", + "fieldtype": "Currency", + "label": "Cost Allocation", + "non_negative": 1, + "options": "currency", + "read_only": 1 + }, + { + "default": "100", + "fieldname": "cost_allocation_per", + "fieldtype": "Percent", + "label": "% Cost Allocation", + "non_negative": 1 } ], "icon": "fa fa-sitemap", @@ -713,7 +732,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2026-02-06 17:23:15.255301", + "modified": "2026-02-26 14:13:34.040181", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 2ee62b06ad5..a231eee9d84 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -113,19 +113,21 @@ class BOM(WebsiteGenerator): from erpnext.manufacturing.doctype.bom_explosion_item.bom_explosion_item import BOMExplosionItem from erpnext.manufacturing.doctype.bom_item.bom_item import BOMItem from erpnext.manufacturing.doctype.bom_operation.bom_operation import BOMOperation - from erpnext.manufacturing.doctype.bom_scrap_item.bom_scrap_item import BOMScrapItem + from erpnext.manufacturing.doctype.bom_secondary_item.bom_secondary_item import BOMSecondaryItem allow_alternative_item: DF.Check amended_from: DF.Link | None base_operating_cost: DF.Currency base_raw_material_cost: DF.Currency - base_scrap_material_cost: DF.Currency + base_secondary_items_cost: DF.Currency base_total_cost: DF.Currency bom_creator: DF.Link | None bom_creator_item: DF.Data | None buying_price_list: DF.Link | None company: DF.Link conversion_rate: DF.Float + cost_allocation: DF.Currency + cost_allocation_per: DF.Percent currency: DF.Link default_source_warehouse: DF.Link | None default_target_warehouse: DF.Link | None @@ -155,8 +157,8 @@ class BOM(WebsiteGenerator): rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List"] route: DF.SmallText | None routing: DF.Link | None - scrap_items: DF.Table[BOMScrapItem] - scrap_material_cost: DF.Currency + secondary_items: DF.Table[BOMSecondaryItem] + secondary_items_cost: DF.Currency set_rate_of_sub_assembly_item_based_on_bom: DF.Check show_in_website: DF.Check show_items: DF.Check @@ -284,7 +286,7 @@ class BOM(WebsiteGenerator): self.set_plc_conversion_rate() self.validate_uom_is_interger() self.set_bom_material_details() - self.set_bom_scrap_items_detail() + self.set_secondary_items_details() self.validate_materials() self.validate_transfer_against() self.set_routing_operations() @@ -294,9 +296,12 @@ class BOM(WebsiteGenerator): self.update_stock_qty() self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate=False, save=False) self.set_process_loss_qty() - self.validate_scrap_items() + self.validate_uoms() self.set_default_uom() self.validate_semi_finished_goods() + self.validate_secondary_items() + self.set_fg_cost_allocation() + self.validate_total_cost_allocation() if self.docstatus == 1: self.validate_raw_materials_of_operation() @@ -326,6 +331,22 @@ class BOM(WebsiteGenerator): ), ) + def validate_secondary_items(self): + for item in self.secondary_items: + if not item.qty: + frappe.throw( + _("Row #{0}: Quantity should be greater than 0 for {1} Item {2}").format( + item.idx, item.type, get_link_to_form("Item", item.item_code) + ) + ) + + if item.process_loss_per >= 100: + frappe.throw( + _("Row #{0}: Process Loss Percentage should be less than 100% for {1} Item {2}").format( + item.idx, item.type, get_link_to_form("Item", item.item_code) + ) + ) + def validate_raw_materials_of_operation(self): if not self.track_semi_finished_goods or not self.operations: return @@ -401,6 +422,24 @@ class BOM(WebsiteGenerator): doc = frappe.get_doc("BOM Creator", self.bom_creator) doc.set_status(save=True) + def set_fg_cost_allocation(self): + total_secondary_items_per = 0 + for item in self.secondary_items: + total_secondary_items_per += item.cost_allocation_per + + if self.cost_allocation_per == 100 and total_secondary_items_per: + self.cost_allocation_per -= total_secondary_items_per + + self.cost_allocation = self.raw_material_cost * (self.cost_allocation_per / 100) + + def validate_total_cost_allocation(self): + total_cost_allocation_per = self.cost_allocation_per + for item in self.secondary_items: + total_cost_allocation_per += item.cost_allocation_per + + if total_cost_allocation_per != 100: + frappe.throw(_("Cost allocation between finished goods and secondary items should equal 100%")) + def on_update_after_submit(self): self.validate_bom_links() self.manage_default_bom() @@ -462,6 +501,7 @@ class BOM(WebsiteGenerator): "conversion_factor": item.conversion_factor, "sourced_by_supplier": item.sourced_by_supplier, "do_not_explode": item.do_not_explode, + "fetch_rate": True, } ) @@ -469,13 +509,13 @@ class BOM(WebsiteGenerator): if not item.get(r): item.set(r, ret[r]) - def set_bom_scrap_items_detail(self): - for item in self.get("scrap_items"): + def set_secondary_items_details(self): + for item in self.get("secondary_items"): args = { "item_code": item.item_code, "company": self.company, - "scrap_items": True, - "bom_no": "", + "uom": item.uom, + "fetch_rate": False, } ret = self.get_bom_material_detail(args) for key, value in ret.items(): @@ -495,7 +535,7 @@ class BOM(WebsiteGenerator): item = self.get_item_det(args["item_code"]) - args["bom_no"] = args["bom_no"] or item and cstr(item["default_bom"]) or "" + args["bom_no"] = args.get("bom_no") or item and cstr(item["default_bom"]) or "" args["transfer_for_manufacture"] = ( cstr(args.get("include_item_in_manufacturing", "")) or item @@ -504,7 +544,7 @@ class BOM(WebsiteGenerator): ) args.update(item) - rate = self.get_rm_rate(args) + rate = self.get_rm_rate(args) if args.get("fetch_rate") else 0 ret_item = { "item_name": item and args["item_name"] or "", "description": item and args["description"] or "", @@ -546,9 +586,7 @@ class BOM(WebsiteGenerator): if not self.rm_cost_as_per: self.rm_cost_as_per = "Valuation Rate" - if arg.get("scrap_items"): - rate = get_valuation_rate(arg) - elif arg: + if arg: # Customer Provided parts and Supplier sourced parts will have zero rate if not frappe.db.get_value("Item", arg["item_code"], "is_customer_provided_item") and not arg.get( "sourced_by_supplier" @@ -688,7 +726,7 @@ class BOM(WebsiteGenerator): ) def update_stock_qty(self): - for m in self.get("items"): + for m in self.get("items") + self.get("secondary_items"): if not m.conversion_factor: m.conversion_factor = flt(get_conversion_factor(m.item_code, m.uom)["conversion_factor"]) if m.uom and m.qty: @@ -889,16 +927,16 @@ class BOM(WebsiteGenerator): """Calculate bom totals""" self.calculate_op_cost(update_hour_rate) self.calculate_rm_cost(save=save_updates) - self.calculate_sm_cost(save=save_updates) + self.calculate_secondary_items_costs(save=save_updates) if save_updates: # not via doc event, table is not regenerated and needs updation self.calculate_exploded_cost() old_cost = self.total_cost - self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost + self.total_cost = self.operating_cost + self.raw_material_cost - self.secondary_items_cost self.base_total_cost = ( - self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost + self.base_operating_cost + self.base_raw_material_cost - self.base_secondary_items_cost ) if self.total_cost != old_cost: @@ -997,29 +1035,24 @@ class BOM(WebsiteGenerator): self.raw_material_cost = total_rm_cost self.base_raw_material_cost = base_total_rm_cost - def calculate_sm_cost(self, save=False): + def calculate_secondary_items_costs(self, save=False): """Fetch RM rate as per today's valuation rate and calculate totals""" total_sm_cost = 0 base_total_sm_cost = 0 + precision = self.precision("raw_material_cost") - for d in self.get("scrap_items"): - d.base_rate = flt(d.rate, d.precision("rate")) * flt( - self.conversion_rate, self.precision("conversion_rate") - ) - d.amount = flt( - flt(d.rate, d.precision("rate")) * flt(d.stock_qty, d.precision("stock_qty")), - d.precision("amount"), - ) - d.base_amount = flt(d.amount, d.precision("amount")) * flt( - self.conversion_rate, self.precision("conversion_rate") - ) - total_sm_cost += d.amount - base_total_sm_cost += d.base_amount - if save: - d.db_update() + for d in self.get("secondary_items"): + if not d.is_legacy: + d.cost = flt(self.raw_material_cost * (d.cost_allocation_per / 100), precision) + d.base_cost = flt(d.cost * self.conversion_rate, precision) - self.scrap_material_cost = total_sm_cost - self.base_scrap_material_cost = base_total_sm_cost + total_sm_cost += d.cost + base_total_sm_cost += d.base_cost + if save: + d.db_update() + + self.secondary_items_cost = total_sm_cost + self.base_secondary_items_cost = base_total_sm_cost def calculate_exploded_cost(self): "Set exploded row cost from it's parent BOM." @@ -1221,16 +1254,29 @@ class BOM(WebsiteGenerator): if self.process_loss_percentage: self.process_loss_qty = flt(self.quantity) * flt(self.process_loss_percentage) / 100 - def validate_scrap_items(self): - must_be_whole_number = frappe.get_value("UOM", self.uom, "must_be_whole_number") + for item in self.secondary_items: + item.process_loss_qty = flt( + item.stock_qty * (item.process_loss_per / 100), self.precision("quantity") + ) - if self.process_loss_percentage and self.process_loss_percentage > 100: + def validate_uoms(self): + self.validate_uom(self.item, self.uom, self.process_loss_percentage, self.process_loss_qty) + for item in self.secondary_items: + self.validate_uom(item.item_code, item.stock_uom, item.process_loss_per, item.process_loss_qty) + + def validate_uom(self, item_code, uom, process_loss_per, process_loss_qty): + must_be_whole_number = frappe.get_value("UOM", uom, "must_be_whole_number") + + if process_loss_per and process_loss_per > 100: frappe.throw(_("Process Loss Percentage cannot be greater than 100")) - if self.process_loss_qty and must_be_whole_number and self.process_loss_qty % 1 != 0: - msg = f"Item: {frappe.bold(self.item)} with Stock UOM: {frappe.bold(self.uom)} can't have fractional process loss qty as UOM {frappe.bold(self.uom)} is a whole Number." + if process_loss_qty and must_be_whole_number and process_loss_qty % 1 != 0: + msg = f"Item: {frappe.bold(item_code)} with Stock UOM: {frappe.bold(uom)} can't have fractional process loss qty as UOM {frappe.bold(uom)} is a whole Number." frappe.throw(msg, title=_("Invalid Process Loss Configuration")) + def has_scrap_items(self): + return any(d.get("type") == "Scrap" or d.get("is_legacy") for d in self.get("secondary_items")) + def get_bom_item_rate(args, bom_doc): if bom_doc.rm_cost_as_per == "Valuation Rate": @@ -1332,7 +1378,7 @@ def get_bom_items_as_dict( company, qty=1, fetch_exploded=1, - fetch_scrap_items=0, + fetch_secondary_items=0, include_non_stock_items=False, fetch_qty_in_stock_uom=True, ): @@ -1343,7 +1389,7 @@ def get_bom_items_as_dict( fetch_exploded = 0 group_by_cond = "group by item_code, operation_row_id, stock_uom" - if fetch_scrap_items: + if fetch_secondary_items: fetch_exploded = 0 group_by_cond = "group by item_code" @@ -1355,8 +1401,6 @@ def get_bom_items_as_dict( sum(bom_item.{qty_field}/ifnull(bom.quantity, 1)) * %(qty)s as qty, item.image, bom.project, - bom_item.rate, - sum(bom_item.{qty_field}/ifnull(bom.quantity, 1)) * bom_item.rate * %(qty)s as amount, item.stock_uom, item.item_group, item.allow_alternative_item, @@ -1388,17 +1432,18 @@ def get_bom_items_as_dict( group_by_cond=group_by_cond, select_columns=""", bom_item.source_warehouse, bom_item.operation, bom_item.include_item_in_manufacturing, bom_item.description, bom_item.rate, bom_item.sourced_by_supplier, + sum(bom_item.stock_qty/ifnull(bom.quantity, 1)) * bom_item.rate * %(qty)s as amount, (Select idx from `tabBOM Item` where item_code = bom_item.item_code and parent = %(parent)s limit 1) as idx""", ) items = frappe.db.sql( query, {"parent": bom, "qty": qty, "bom": bom, "company": company}, as_dict=True ) - elif fetch_scrap_items: + elif fetch_secondary_items: query = query.format( - table="BOM Scrap Item", + table="BOM Secondary Item", where_conditions=")", - select_columns=", item.description", + select_columns=", item.description, bom_item.cost_allocation_per, bom_item.process_loss_per, bom_item.type, bom_item.name, bom_item.is_legacy", is_stock_item=is_stock_item, qty_field="stock_qty", group_by_cond=group_by_cond, @@ -1411,8 +1456,9 @@ def get_bom_items_as_dict( where_conditions="or bom_item.is_phantom_item)", is_stock_item=is_stock_item, qty_field="stock_qty" if fetch_qty_in_stock_uom else "qty", - select_columns=""", bom_item.uom, bom_item.conversion_factor, bom_item.source_warehouse, + select_columns=""", bom_item.rate, bom_item.uom, bom_item.conversion_factor, bom_item.source_warehouse, bom_item.operation, bom_item.include_item_in_manufacturing, bom_item.sourced_by_supplier, + sum(bom_item.stock_qty/ifnull(bom.quantity, 1)) * bom_item.rate * %(qty)s as amount, bom_item.description, bom_item.base_rate as rate, bom_item.operation_row_id, bom_item.is_phantom_item , bom_item.bom_no """, group_by_cond=group_by_cond, ) @@ -1432,7 +1478,7 @@ def get_bom_items_as_dict( company, qty=item.get("qty"), fetch_exploded=fetch_exploded, - fetch_scrap_items=fetch_scrap_items, + fetch_secondary_items=fetch_secondary_items, include_non_stock_items=include_non_stock_items, fetch_qty_in_stock_uom=fetch_qty_in_stock_uom, ) @@ -1482,7 +1528,7 @@ def validate_bom_no(item, bom_no): for d in bom.items: if d.item_code.lower() == item.lower(): rm_item_exists = True - for d in bom.scrap_items: + for d in bom.secondary_items: if d.item_code.lower() == item.lower(): rm_item_exists = True if ( @@ -1773,7 +1819,7 @@ def get_bom_diff(bom1, bom2): identifiers = { "operations": "operation", "items": "item_code", - "scrap_items": "item_code", + "secondary_items": "item_code", "exploded_items": "item_code", } @@ -1919,9 +1965,9 @@ def get_op_cost_from_sub_assemblies(bom_no, op_cost=0): return op_cost -def get_scrap_items_from_sub_assemblies(bom_no, company, qty, scrap_items=None): - if not scrap_items: - scrap_items = {} +def get_secondary_items_from_sub_assemblies(bom_no, company, qty, secondary_items=None): + if not secondary_items: + secondary_items = {} bom_items = frappe.get_all( "BOM Item", @@ -1935,9 +1981,9 @@ def get_scrap_items_from_sub_assemblies(bom_no, company, qty, scrap_items=None): continue qty = flt(row.qty) * flt(qty) - items = get_bom_items_as_dict(row.bom_no, company, qty=qty, fetch_exploded=0, fetch_scrap_items=1) - scrap_items.update(items) + items = get_bom_items_as_dict(row.bom_no, company, qty=qty, fetch_exploded=0, fetch_secondary_items=1) + secondary_items.update(items) - get_scrap_items_from_sub_assemblies(row.bom_no, company, qty, scrap_items) + get_secondary_items_from_sub_assemblies(row.bom_no, company, qty, secondary_items) - return scrap_items + return secondary_items diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 68a29d7da4e..3296559afc5 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -895,7 +895,7 @@ def create_bom_with_process_loss_item( if scrap_qty: bom_doc.append( - "scrap_items", + "secondary_items", { "item_code": fg_item.item_code, "qty": scrap_qty, diff --git a/erpnext/manufacturing/doctype/bom/test_records.json b/erpnext/manufacturing/doctype/bom/test_records.json index 27752d85119..7c5c41fec19 100644 --- a/erpnext/manufacturing/doctype/bom/test_records.json +++ b/erpnext/manufacturing/doctype/bom/test_records.json @@ -36,15 +36,17 @@ "quantity": 1.0 }, { - "scrap_items":[ + "secondary_items":[ { "amount": 2000.0, - "doctype": "BOM Scrap Item", + "doctype": "BOM Secondary Item", "item_code": "_Test Item Home Desktop 100", - "parentfield": "scrap_items", + "parentfield": "secondary_items", "stock_qty": 1.0, "rate": 2000.0, - "stock_uom": "_Test UOM" + "stock_uom": "_Test UOM", + "type": "Scrap", + "is_legacy": 1 } ], "items": [ diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py index e071dadb998..e3feac1061a 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py @@ -356,7 +356,6 @@ class BOMCreator(Document): { "bom_no": bom_no, "allow_alternative_item": 1, - "allow_scrap_items": not item.get("is_phantom_item"), "include_item_in_manufacturing": 1, } ) diff --git a/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json b/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json deleted file mode 100644 index e782a882e8b..00000000000 --- a/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json +++ /dev/null @@ -1,109 +0,0 @@ -{ - "actions": [], - "creation": "2016-09-26 02:19:21.642081", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "item_code", - "column_break_2", - "item_name", - "quantity_and_rate", - "stock_qty", - "rate", - "amount", - "column_break_6", - "stock_uom", - "base_rate", - "base_amount" - ], - "fields": [ - { - "fieldname": "item_code", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Item Code", - "options": "Item", - "reqd": 1 - }, - { - "fieldname": "item_name", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Item Name" - }, - { - "fieldname": "quantity_and_rate", - "fieldtype": "Section Break", - "label": "Quantity and Rate" - }, - { - "fieldname": "stock_qty", - "fieldtype": "Float", - "in_list_view": 1, - "label": "Qty", - "non_negative": 1, - "reqd": 1 - }, - { - "fieldname": "rate", - "fieldtype": "Currency", - "in_list_view": 1, - "label": "Rate", - "non_negative": 1, - "options": "currency" - }, - { - "fieldname": "amount", - "fieldtype": "Currency", - "label": "Amount", - "options": "currency", - "read_only": 1 - }, - { - "fieldname": "column_break_6", - "fieldtype": "Column Break" - }, - { - "fieldname": "stock_uom", - "fieldtype": "Link", - "label": "Stock UOM", - "options": "UOM", - "read_only": 1 - }, - { - "fieldname": "base_rate", - "fieldtype": "Currency", - "label": "Basic Rate (Company Currency)", - "options": "Company:company:default_currency", - "print_hide": 1, - "read_only": 1 - }, - { - "fieldname": "base_amount", - "fieldtype": "Currency", - "label": "Basic Amount (Company Currency)", - "options": "Company:company:default_currency", - "print_hide": 1, - "read_only": 1 - }, - { - "fieldname": "column_break_2", - "fieldtype": "Column Break" - } - ], - "istable": 1, - "links": [], - "modified": "2025-07-31 16:21:44.047007", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "BOM Scrap Item", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "row_format": "Dynamic", - "sort_field": "creation", - "sort_order": "DESC", - "states": [], - "track_changes": 1 -} diff --git a/erpnext/manufacturing/doctype/bom_scrap_item/__init__.py b/erpnext/manufacturing/doctype/bom_secondary_item/__init__.py similarity index 100% rename from erpnext/manufacturing/doctype/bom_scrap_item/__init__.py rename to erpnext/manufacturing/doctype/bom_secondary_item/__init__.py diff --git a/erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json b/erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json new file mode 100644 index 00000000000..39fa55123f4 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json @@ -0,0 +1,232 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2026-02-25 12:44:21.760154", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "type", + "rate", + "column_break_gres", + "is_legacy", + "section_break_sbnk", + "item_code", + "item_name", + "uom", + "column_break_atlf", + "qty", + "stock_uom", + "conversion_factor", + "stock_qty", + "section_break_yith", + "image", + "description", + "column_break_wsra", + "image_nygv", + "section_break_ielf", + "cost_allocation_per", + "process_loss_per", + "column_break_gtbl", + "cost", + "base_cost", + "process_loss_qty" + ], + "fields": [ + { + "depends_on": "eval:!doc.is_legacy", + "fieldname": "type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Type", + "mandatory_depends_on": "eval:!doc.is_legacy", + "options": "\nCo-Product\nBy-Product\nScrap\nAdditional Finished Good" + }, + { + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "reqd": 1 + }, + { + "fetch_from": "item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Item Name", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "cost", + "fieldtype": "Currency", + "label": "Cost", + "no_copy": 1, + "non_negative": 1, + "options": "currency", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "section_break_sbnk", + "fieldtype": "Section Break" + }, + { + "fetch_from": "item_code.stock_uom", + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM", + "read_only": 1 + }, + { + "fieldname": "column_break_atlf", + "fieldtype": "Column Break" + }, + { + "fieldname": "uom", + "fieldtype": "Link", + "in_list_view": 1, + "label": "UOM", + "options": "UOM", + "reqd": 1 + }, + { + "default": "1", + "fieldname": "conversion_factor", + "fieldtype": "Float", + "label": "Conversion Factor", + "non_negative": 1, + "reqd": 1 + }, + { + "depends_on": "eval:!doc.is_legacy", + "fieldname": "section_break_ielf", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_gtbl", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_yith", + "fieldtype": "Section Break" + }, + { + "fetch_from": "item_code.image", + "fieldname": "image", + "fieldtype": "Attach Image", + "hidden": 1, + "label": "Image", + "read_only": 1 + }, + { + "fieldname": "column_break_wsra", + "fieldtype": "Column Break" + }, + { + "fieldname": "stock_qty", + "fieldtype": "Float", + "label": "Stock Qty", + "non_negative": 1, + "read_only": 1 + }, + { + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Qty", + "non_negative": 1, + "reqd": 1 + }, + { + "default": "0", + "fieldname": "cost_allocation_per", + "fieldtype": "Percent", + "label": "Cost Allocation %", + "non_negative": 1, + "reqd": 1 + }, + { + "default": "0", + "fieldname": "process_loss_per", + "fieldtype": "Percent", + "label": "Process Loss %", + "non_negative": 1, + "reqd": 1 + }, + { + "fieldname": "description", + "fieldtype": "Text Editor", + "label": "Description" + }, + { + "depends_on": "image", + "fieldname": "image_nygv", + "fieldtype": "Image", + "options": "image", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "base_cost", + "fieldtype": "Currency", + "hidden": 1, + "label": "Base Cost (Company Currency)", + "no_copy": 1, + "non_negative": 1, + "options": "Company:company:default_currency", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "column_break_gres", + "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "is_legacy", + "fieldname": "is_legacy", + "fieldtype": "Check", + "label": "Is Legacy", + "no_copy": 1, + "read_only": 1 + }, + { + "depends_on": "eval:doc.is_legacy", + "fieldname": "rate", + "fieldtype": "Currency", + "label": "Rate", + "no_copy": 1, + "non_negative": 1, + "read_only": 1 + }, + { + "default": "0", + "fieldname": "process_loss_qty", + "fieldtype": "Float", + "label": "Process Loss Qty", + "no_copy": 1, + "non_negative": 1, + "read_only": 1, + "reqd": 1 + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2026-03-11 12:12:29.208031", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "BOM Secondary Item", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} diff --git a/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.py b/erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.py similarity index 50% rename from erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.py rename to erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.py index 043bbc63b50..87748fe2269 100644 --- a/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.py +++ b/erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.py @@ -1,11 +1,11 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt - +# import frappe from frappe.model.document import Document -class BOMScrapItem(Document): +class BOMSecondaryItem(Document): # begin: auto-generated types # This code is auto-generated. Do not modify anything in this block. @@ -14,17 +14,26 @@ class BOMScrapItem(Document): if TYPE_CHECKING: from frappe.types import DF - amount: DF.Currency - base_amount: DF.Currency - base_rate: DF.Currency + base_cost: DF.Currency + conversion_factor: DF.Float + cost: DF.Currency + cost_allocation_per: DF.Percent + description: DF.TextEditor | None + image: DF.AttachImage | None + is_legacy: DF.Check item_code: DF.Link item_name: DF.Data | None parent: DF.Data parentfield: DF.Data parenttype: DF.Data + process_loss_per: DF.Percent + process_loss_qty: DF.Float + qty: DF.Float rate: DF.Currency stock_qty: DF.Float stock_uom: DF.Link | None + type: DF.Literal["", "Co-Product", "By-Product", "Scrap", "Additional Finished Good"] + uom: DF.Link # end: auto-generated types pass diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 9fb7dcb51b2..68d1e3e6214 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -23,7 +23,7 @@ frappe.ui.form.on("Job Card", { }; }); - frm.set_query("item_code", "scrap_items", () => { + frm.set_query("item_code", "secondary_items", () => { return { filters: { disabled: 0, @@ -104,7 +104,7 @@ frappe.ui.form.on("Job Card", { frm.doc.docstatus === 1 && !frm.doc.is_subcontracted && (frm.doc.skip_material_transfer || frm.doc.transferred_qty > 0) && - flt(frm.doc.for_quantity) + flt(frm.doc.process_loss_qty) > flt(frm.doc.manufactured_qty) + flt(frm.doc.manufactured_qty) + flt(frm.doc.process_loss_qty) < flt(frm.doc.for_quantity) ) { frm.add_custom_button(__("Make Stock Entry"), () => { frappe.confirm( @@ -278,8 +278,6 @@ frappe.ui.form.on("Job Card", { frm.trigger("complete_job_card"); }); } - - frm.trigger("make_dashboard"); } } diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 6b34eb7711a..728e8fc27ec 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -59,8 +59,8 @@ "time_logs", "section_break_21", "sub_operations", - "scrap_items_section", - "scrap_items", + "secondary_items_section", + "secondary_items", "corrective_operation_section", "for_job_card", "is_corrective_job_card", @@ -406,20 +406,6 @@ "options": "Batch", "read_only": 1 }, - { - "collapsible": 1, - "fieldname": "scrap_items_section", - "fieldtype": "Tab Break", - "label": "Scrap Items" - }, - { - "fieldname": "scrap_items", - "fieldtype": "Table", - "label": "Scrap Items", - "no_copy": 1, - "options": "Job Card Scrap Item", - "print_hide": 1 - }, { "fetch_from": "operation.quality_inspection_template", "fieldname": "quality_inspection_template", @@ -623,12 +609,26 @@ { "fieldname": "column_break_xhzg", "fieldtype": "Column Break" + }, + { + "fieldname": "secondary_items", + "fieldtype": "Table", + "label": "Secondary Items", + "no_copy": 1, + "options": "Job Card Secondary Item", + "print_hide": 1 + }, + { + "collapsible": 1, + "fieldname": "secondary_items_section", + "fieldtype": "Tab Break", + "label": "Secondary Items" } ], "grid_page_length": 50, "is_submittable": 1, "links": [], - "modified": "2026-02-06 18:27:03.178783", + "modified": "2026-02-26 15:13:56.767070", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card", diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 0f4c9d569fa..a4eaec8e73f 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -71,7 +71,9 @@ class JobCard(Document): from erpnext.manufacturing.doctype.job_card_scheduled_time.job_card_scheduled_time import ( JobCardScheduledTime, ) - from erpnext.manufacturing.doctype.job_card_scrap_item.job_card_scrap_item import JobCardScrapItem + from erpnext.manufacturing.doctype.job_card_secondary_item.job_card_secondary_item import ( + JobCardSecondaryItem, + ) from erpnext.manufacturing.doctype.job_card_time_log.job_card_time_log import JobCardTimeLog actual_end_date: DF.Datetime | None @@ -110,7 +112,7 @@ class JobCard(Document): remarks: DF.SmallText | None requested_qty: DF.Float scheduled_time_logs: DF.Table[JobCardScheduledTime] - scrap_items: DF.Table[JobCardScrapItem] + secondary_items: DF.Table[JobCardSecondaryItem] semi_fg_bom: DF.Link | None sequence_id: DF.Int serial_and_batch_bundle: DF.Link | None @@ -199,6 +201,7 @@ class JobCard(Document): def set_manufactured_qty(self): table_name = "Stock Entry" + child_name = "Stock Entry Detail" if self.is_subcontracted: table_name = "Subcontracting Receipt Item" @@ -208,8 +211,13 @@ class JobCard(Document): if self.is_subcontracted: query = query.select(Sum(table.qty)) else: - query = query.select(Sum(table.fg_completed_qty)) - query = query.where(table.purpose == "Manufacture") + child = frappe.qb.DocType(child_name) + query = ( + query.join(child) + .on(table.name == child.parent) + .select(Sum(child.transfer_qty)) + .where((table.purpose == "Manufacture") & (child.is_finished_item == 1)) + ) qty = query.run()[0][0] or 0.0 self.manufactured_qty = flt(qty) @@ -267,25 +275,35 @@ class JobCard(Document): row.sub_operation = row.operation self.append("sub_operations", row) - def set_scrap_items(self): - if not self.semi_fg_bom: + def set_secondary_items(self): + if not self.semi_fg_bom and not self.bom_no: return items_dict = get_bom_items_as_dict( - self.semi_fg_bom, self.company, qty=self.for_quantity, fetch_exploded=0, fetch_scrap_items=1 + self.semi_fg_bom or self.bom_no, + self.company, + qty=self.for_quantity, + fetch_exploded=0, + fetch_secondary_items=1, ) for item_code, values in items_dict.items(): values = frappe._dict(values) + secondary_item = { + "item_code": item_code, + "stock_qty": values.qty, + "item_name": values.item_name, + "stock_uom": values.stock_uom, + "type": values.type, + "bom_secondary_item": values.name, + } - self.append( - "scrap_items", - { - "item_code": item_code, - "stock_qty": values.qty, - "item_name": values.item_name, - "stock_uom": values.stock_uom, - }, - ) + if not values.is_legacy: + secondary_item["stock_qty"] -= flt( + secondary_item["stock_qty"] * (values.process_loss_per / 100), + self.precision("for_quantity"), + ) + + self.append("secondary_items", secondary_item) def validate_time_logs(self, save=False): self.total_time_in_mins = 0.0 @@ -1181,7 +1199,7 @@ class JobCard(Document): def set_status(self, update_status=False): self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0] if self.finished_good and self.docstatus == 1: - if self.manufactured_qty >= self.for_quantity: + if (self.manufactured_qty + self.process_loss_qty) >= self.for_quantity: self.status = "Completed" elif self.transferred_qty > 0 or self.skip_material_transfer: self.status = "Work In Progress" @@ -1456,12 +1474,24 @@ class JobCard(Document): ) @frappe.whitelist() - def make_stock_entry_for_semi_fg_item(self, auto_submit=False): + def make_stock_entry_for_semi_fg_item(self, auto_submit: bool = False): + def get_consumed_process_loss(): + table = frappe.qb.DocType("Stock Entry") + query = ( + frappe.qb.from_(table) + .select(Sum(table.process_loss_qty)) + .where( + (table.purpose == "Manufacture") & (table.job_card == self.name) & (table.docstatus == 1) + ) + ) + return query.run()[0][0] or 0 + from erpnext.stock.doctype.stock_entry_type.stock_entry_type import ManufactureEntry ste = ManufactureEntry( { "for_quantity": self.for_quantity - self.manufactured_qty, + "process_loss_qty": max(self.process_loss_qty - get_consumed_process_loss(), 0), "job_card": self.name, "skip_material_transfer": self.skip_material_transfer, "backflush_from_wip_warehouse": self.backflush_from_wip_warehouse, @@ -1481,9 +1511,10 @@ class JobCard(Document): wo_doc = frappe.get_doc("Work Order", self.work_order) add_additional_cost(ste.stock_entry, wo_doc, self) - ste.stock_entry.set_scrap_items() + ste.stock_entry.pro_doc = frappe.get_doc("Work Order", self.work_order) + ste.stock_entry.set_secondary_items_from_job_card() for row in ste.stock_entry.items: - if row.is_scrap_item and not row.t_warehouse: + if (row.type or row.is_legacy_scrap_item) and not row.t_warehouse: row.t_warehouse = self.target_warehouse if auto_submit: diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 556d3911eb3..a25b6e1af3d 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -882,6 +882,193 @@ class TestJobCard(ERPNextTestSuite): s = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 6)) self.assertEqual(s.additional_costs[0].amount, 8) + def test_co_by_product_for_sfg_flow(self): + from erpnext.manufacturing.doctype.operation.test_operation import make_operation + + frappe.db.set_value("UOM", "Nos", "must_be_whole_number", 0) + + def create_bom(raw_material, finished_good, scrap_item, submit=True): + bom = frappe.new_doc("BOM") + bom.company = "_Test Company" + bom.item = finished_good + bom.quantity = 1 + bom.append("items", {"item_code": raw_material, "qty": 1}) + bom.append( + "secondary_items", + { + "item_code": scrap_item, + "qty": 1, + "process_loss_per": 10, + "cost_allocation_per": 5, + "type": "Scrap", + }, + ) + if submit: + bom.insert() + bom.submit() + + return bom + + rm1 = create_item("RM 1") + scrap1 = create_item("Scrap 1") + sfg = create_item("SFG 1") + sfg_bom = create_bom(rm1.name, sfg.name, scrap1.name) + + rm2 = create_item("RM 2") + fg1 = create_item("FG 1") + scrap2 = create_item("Scrap 2") + scrap_extra = create_item("Scrap Extra") + fg_bom = create_bom(rm2.name, fg1.name, scrap2.name, submit=False) + fg_bom.with_operations = 1 + fg_bom.track_semi_finished_goods = 1 + + operation1 = { + "operation": "Test Operation A", + "workstation": "_Test Workstation A", + "finished_good": sfg.name, + "bom_no": sfg_bom.name, + "finished_good_qty": 1, + "sequence_id": 1, + "time_in_mins": 30, + } + operation2 = { + "operation": "Test Operation B", + "workstation": "_Test Workstation A", + "finished_good": fg1.name, + "bom_no": fg_bom.name, + "finished_good_qty": 1, + "is_final_finished_good": 1, + "sequence_id": 2, + "time_in_mins": 30, + } + + make_workstation(operation1) + make_operation(operation1) + make_operation(operation2) + + fg_bom.append("operations", operation1) + fg_bom.append("operations", operation2) + fg_bom.append("items", {"item_code": sfg.name, "qty": 1, "uom": "Nos", "operation_row_id": 2}) + fg_bom.insert() + fg_bom.save() + fg_bom.submit() + + work_order = make_wo_order_test_record( + item=fg1.name, + qty=10, + source_warehouse="Stores - _TC", + fg_warehouse="Finished Goods - _TC", + bom_no=fg_bom.name, + skip_transfer=1, + do_not_save=True, + ) + + work_order.operations[0].time_in_mins = 60 + work_order.operations[1].time_in_mins = 60 + work_order.save() + work_order.submit() + + job_card = frappe.get_doc( + "Job Card", + frappe.db.get_value( + "Job Card", {"work_order": work_order.name, "operation": "Test Operation A"}, "name" + ), + ) + job_card.append( + "time_logs", + { + "from_time": "2009-01-01 12:06:25", + "to_time": "2009-01-01 12:37:25", + "completed_qty": job_card.for_quantity, + }, + ) + job_card.append( + "secondary_items", {"item_code": scrap_extra.name, "stock_qty": 5, "type": "Co-Product"} + ) + job_card.submit() + + for row in sfg_bom.items: + make_stock_entry( + item_code=row.item_code, + target="Stores - _TC", + qty=10, + basic_rate=100, + ) + + manufacturing_entry = frappe.get_doc(job_card.make_stock_entry_for_semi_fg_item()) + manufacturing_entry.submit() + + self.assertEqual(manufacturing_entry.items[2].item_code, scrap1.name) + self.assertEqual(manufacturing_entry.items[2].qty, 9) + self.assertEqual(flt(manufacturing_entry.items[2].basic_rate, 3), 5.556) + self.assertEqual(manufacturing_entry.items[3].item_code, scrap_extra.name) + self.assertEqual(manufacturing_entry.items[3].type, "Co-Product") + self.assertEqual(manufacturing_entry.items[3].qty, 5) + self.assertEqual(manufacturing_entry.items[3].basic_rate, 0) + + job_card = frappe.get_doc( + "Job Card", + frappe.db.get_value( + "Job Card", {"work_order": work_order.name, "operation": "Test Operation B"}, "name" + ), + ) + job_card.append( + "time_logs", + { + "from_time": "2009-02-01 12:06:25", + "to_time": "2009-02-01 12:37:25", + "completed_qty": job_card.for_quantity, + }, + ) + job_card.submit() + + for row in fg_bom.items: + make_stock_entry( + item_code=row.item_code, + target="Stores - _TC", + qty=10, + basic_rate=100, + ) + + manufacturing_entry = frappe.get_doc(job_card.make_stock_entry_for_semi_fg_item()) + manufacturing_entry.submit() + + self.assertEqual(manufacturing_entry.items[2].item_code, scrap2.name) + self.assertEqual(manufacturing_entry.items[2].qty, 9) + self.assertEqual(flt(manufacturing_entry.items[2].basic_rate, 3), 5.556) + + def test_secondary_items_without_sfg(self): + for row in frappe.get_doc("BOM", self.work_order.bom_no).items: + make_stock_entry( + item_code=row.item_code, + target="_Test Warehouse - _TC", + qty=10, + basic_rate=100, + ) + + job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name}) + job_card.append("secondary_items", {"item_code": "_Test Item", "stock_qty": 2, "type": "Scrap"}) + job_card.append( + "time_logs", + { + "from_time": "2009-01-01 12:06:25", + "to_time": "2009-01-01 12:37:25", + "completed_qty": job_card.for_quantity, + }, + ) + job_card.save() + job_card.submit() + + from erpnext.manufacturing.doctype.work_order.work_order import ( + make_stock_entry as make_stock_entry_for_wo, + ) + + s = frappe.get_doc(make_stock_entry_for_wo(self.work_order.name, "Manufacture")) + s.submit() + + self.assertEqual(s.items[3].item_code, "_Test Item") + self.assertEqual(s.items[3].transfer_qty, 2) + def create_bom_with_multiple_operations(): "Create a BOM with multiple operations and Material Transfer against Job Card" diff --git a/erpnext/manufacturing/doctype/job_card_scrap_item/__init__.py b/erpnext/manufacturing/doctype/job_card_secondary_item/__init__.py similarity index 100% rename from erpnext/manufacturing/doctype/job_card_scrap_item/__init__.py rename to erpnext/manufacturing/doctype/job_card_secondary_item/__init__.py diff --git a/erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.json b/erpnext/manufacturing/doctype/job_card_secondary_item/job_card_secondary_item.json similarity index 73% rename from erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.json rename to erpnext/manufacturing/doctype/job_card_secondary_item/job_card_secondary_item.json index fdb8ec44bdc..d9ac0e08ced 100644 --- a/erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.json +++ b/erpnext/manufacturing/doctype/job_card_secondary_item/job_card_secondary_item.json @@ -5,10 +5,12 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "type", + "description", + "column_break_3", "item_code", "item_name", - "column_break_3", - "description", + "bom_secondary_item", "quantity_and_rate", "stock_qty", "column_break_6", @@ -19,7 +21,7 @@ "fieldname": "item_code", "fieldtype": "Link", "in_list_view": 1, - "label": "Scrap Item Code", + "label": "Secondary Item Code", "options": "Item", "reqd": 1 }, @@ -28,7 +30,7 @@ "fieldname": "item_name", "fieldtype": "Data", "in_list_view": 1, - "label": "Scrap Item Name" + "label": "Secondary Item Name" }, { "fieldname": "column_break_3", @@ -65,20 +67,36 @@ "label": "Stock UOM", "options": "UOM", "read_only": 1 + }, + { + "fieldname": "type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Type", + "options": "Co-Product\nBy-Product\nScrap\nAdditional Finished Good", + "reqd": 1 + }, + { + "fieldname": "bom_secondary_item", + "fieldtype": "Data", + "hidden": 1, + "label": "BOM Secondary Item Reference", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-07-29 13:09:57.323835", + "modified": "2026-03-06 13:51:00.492621", "modified_by": "Administrator", "module": "Manufacturing", - "name": "Job Card Scrap Item", + "name": "Job Card Secondary Item", "owner": "Administrator", "permissions": [], "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/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.py b/erpnext/manufacturing/doctype/job_card_secondary_item/job_card_secondary_item.py similarity index 78% rename from erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.py rename to erpnext/manufacturing/doctype/job_card_secondary_item/job_card_secondary_item.py index e4b926efc07..3a71ab9d755 100644 --- a/erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.py +++ b/erpnext/manufacturing/doctype/job_card_secondary_item/job_card_secondary_item.py @@ -4,7 +4,7 @@ from frappe.model.document import Document -class JobCardScrapItem(Document): +class JobCardSecondaryItem(Document): # begin: auto-generated types # This code is auto-generated. Do not modify anything in this block. @@ -13,6 +13,7 @@ class JobCardScrapItem(Document): if TYPE_CHECKING: from frappe.types import DF + bom_secondary_item: DF.Data | None description: DF.SmallText | None item_code: DF.Link item_name: DF.Data | None @@ -21,6 +22,7 @@ class JobCardScrapItem(Document): parenttype: DF.Data stock_qty: DF.Float stock_uom: DF.Link | None + type: DF.Literal["Co-Product", "By-Product", "Scrap", "Additional Finished Good"] # end: auto-generated types pass diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json index 1a150dc864f..778334b96d0 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json @@ -36,7 +36,7 @@ "capacity_planning_for_days", "mins_between_operations", "other_settings_section", - "set_op_cost_and_scrap_from_sub_assemblies", + "set_op_cost_and_secondary_items_from_sub_assemblies", "column_break_23", "make_serial_no_batch_from_work_order" ], @@ -202,13 +202,6 @@ "fieldtype": "Check", "label": "Validate Components and Quantities Per BOM" }, - { - "default": "0", - "description": "To include sub-assembly costs and scrap items in Finished Goods on a work order without using a job card, when the 'Use Multi-Level BOM' option is enabled.", - "fieldname": "set_op_cost_and_scrap_from_sub_assemblies", - "fieldtype": "Check", - "label": "Set Operating Cost / Scrap Items From Sub-assemblies" - }, { "default": "0", "description": "Enabling this checkbox will force each Job Card Time Log to have From Time and To Time", @@ -237,6 +230,13 @@ "fieldname": "allow_editing_of_items_and_quantities_in_work_order", "fieldtype": "Check", "label": "Allow Editing of Items and Quantities in Work Order" + }, + { + "default": "0", + "description": "To include sub-assembly costs and secondary items in Finished Goods on a work order without using a job card, when the 'Use Multi-Level BOM' option is enabled.", + "fieldname": "set_op_cost_and_secondary_items_from_sub_assemblies", + "fieldtype": "Check", + "label": "Set Operating Cost / Secondary Items From Sub-assemblies" } ], "hide_toolbar": 0, @@ -244,7 +244,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2026-03-16 13:28:20.714576", + "modified": "2026-03-20 13:28:20.714576", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing Settings", diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py index e60a9627a21..2913d70395d 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py @@ -32,7 +32,7 @@ class ManufacturingSettings(Document): mins_between_operations: DF.Int overproduction_percentage_for_sales_order: DF.Percent overproduction_percentage_for_work_order: DF.Percent - set_op_cost_and_scrap_from_sub_assemblies: DF.Check + set_op_cost_and_secondary_items_from_sub_assemblies: DF.Check transfer_extra_materials_percentage: DF.Percent update_bom_costs_automatically: DF.Check validate_components_quantities_per_bom: DF.Check diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 4612c427714..5d7e2fa2b36 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -2875,6 +2875,7 @@ def make_bom(**args): "company": args.company or "_Test Company", "routing": args.routing, "with_operations": args.with_operations or 0, + "process_loss_percentage": args.process_loss_percentage or 0, } ) @@ -2896,6 +2897,23 @@ def make_bom(**args): }, ) + if args.scrap_items: + for item in args.scrap_items: + item_doc = frappe.get_doc("Item", item) + bom.append( + "secondary_items", + { + "type": "Scrap", + "item_code": item, + "item_name": item, + "uom": item_doc.stock_uom, + "stock_uom": item_doc.stock_uom, + "qty": args.scrap_qty or 1, + "cost_allocation_per": args.scrap_cost_allocation_per or 10, + "process_loss_per": args.scrap_process_loss_per or 10, + }, + ) + if not args.do_not_save: bom.insert(ignore_permissions=True) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index bea542b7bfa..81ee66ecb4f 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -329,7 +329,7 @@ class TestWorkOrder(ERPNextTestSuite): cint(bin1_on_stop_production.projected_qty) + 1, cint(self.bin1_at_start.projected_qty) ) - def test_scrap_material_qty(self): + def test_secondary_material_qty(self): wo_order = make_wo_order_test_record(planned_start_date=now(), qty=2) # add raw materials to stores @@ -354,15 +354,15 @@ class TestWorkOrder(ERPNextTestSuite): "Work Order", wo_order.name, ["scrap_warehouse", "qty", "produced_qty", "bom_no"], as_dict=1 ) - scrap_item_details = get_scrap_item_details(wo_order_details.bom_no) + secondary_item_details = get_secondary_item_details(wo_order_details.bom_no) self.assertEqual(wo_order_details.produced_qty, 2) for item in s.items: - if item.bom_no and item.item_code in scrap_item_details: + if item.bom_no and item.item_code in secondary_item_details: self.assertEqual(wo_order_details.scrap_warehouse, item.t_warehouse) self.assertEqual( - flt(wo_order_details.qty) * flt(scrap_item_details[item.item_code]), item.qty + flt(wo_order_details.qty) * flt(secondary_item_details[item.item_code]), item.qty ) def test_allow_overproduction(self): @@ -1015,7 +1015,7 @@ class TestWorkOrder(ERPNextTestSuite): self.assertEqual(wo.status, "Completed") @timeout(seconds=60) - def test_job_card_scrap_item(self): + def test_job_card_secondary_item(self): items = [ "Test FG Item for Scrap Item Test", "Test RM Item 1 for Scrap Item Test", @@ -1074,7 +1074,7 @@ class TestWorkOrder(ERPNextTestSuite): stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10)) for row in stock_entry.items: - if row.is_scrap_item: + if row.type or row.is_legacy_scrap_item: self.assertEqual(row.qty, 1) # Partial Job Card 1 with qty 10 @@ -1086,7 +1086,7 @@ class TestWorkOrder(ERPNextTestSuite): stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10)) for row in stock_entry.items: - if row.is_scrap_item: + if row.type or row.is_legacy_scrap_item: self.assertEqual(row.qty, 2) # Partial Job Card 2 with qty 10 @@ -2134,10 +2134,12 @@ class TestWorkOrder(ERPNextTestSuite): for row in se_doc.additional_costs: self.assertEqual(row.expense_account, operating_cost_account) - def test_op_cost_and_scrap_based_on_sub_assemblies(self): + def test_set_op_cost_and_secondary_items_from_sub_assemblies(self): # Make Sub Assembly BOM 1 - frappe.db.set_single_value("Manufacturing Settings", "set_op_cost_and_scrap_from_sub_assemblies", 1) + frappe.db.set_single_value( + "Manufacturing Settings", "set_op_cost_and_secondary_items_from_sub_assemblies", 1 + ) items = { "Test Final FG Item": 0, @@ -2169,16 +2171,20 @@ class TestWorkOrder(ERPNextTestSuite): se_doc.save() self.assertTrue(se_doc.additional_costs) - scrap_items = [] + secondary_items = [] for item in se_doc.items: - if item.is_scrap_item: - scrap_items.append(item.item_code) + if item.type or item.is_legacy_scrap_item: + secondary_items.append(item.item_code) - self.assertEqual(sorted(scrap_items), sorted(["Test Final Scrap Item 1", "Test Final Scrap Item 2"])) + self.assertEqual( + sorted(secondary_items), sorted(["Test Final Scrap Item 1", "Test Final Scrap Item 2"]) + ) for row in se_doc.additional_costs: self.assertEqual(row.amount, 3000) - frappe.db.set_single_value("Manufacturing Settings", "set_op_cost_and_scrap_from_sub_assemblies", 0) + frappe.db.set_single_value( + "Manufacturing Settings", "set_op_cost_and_secondary_items_from_sub_assemblies", 0 + ) @ERPNextTestSuite.change_settings( "Manufacturing Settings", {"material_consumption": 1, "get_rm_cost_from_consumption_entry": 1} @@ -3951,7 +3957,7 @@ def prepare_boms_for_sub_assembly_test(): do_not_submit=True, ) - bom.append("scrap_items", {"item_code": "Test Final Scrap Item 1", "qty": 1}) + bom.append("secondary_items", {"item_code": "Test Final Scrap Item 1", "qty": 1, "is_legacy": 1}) bom.submit() @@ -3964,7 +3970,7 @@ def prepare_boms_for_sub_assembly_test(): do_not_submit=True, ) - bom.append("scrap_items", {"item_code": "Test Final Scrap Item 2", "qty": 1}) + bom.append("secondary_items", {"item_code": "Test Final Scrap Item 2", "qty": 1, "is_legacy": 1}) bom.submit() @@ -4159,7 +4165,7 @@ def update_job_card(job_card, jc_qty=None, days=None): employee = frappe.db.get_value("Employee", {"status": "Active"}, "name") job_card_doc = frappe.get_doc("Job Card", job_card) job_card_doc.set( - "scrap_items", + "secondary_items", [ {"item_code": "Test RM Item 1 for Scrap Item Test", "stock_qty": 2}, {"item_code": "Test RM Item 2 for Scrap Item Test", "stock_qty": 2}, @@ -4199,17 +4205,17 @@ def update_job_card(job_card, jc_qty=None, days=None): job_card_doc.submit() -def get_scrap_item_details(bom_no): - scrap_items = {} +def get_secondary_item_details(bom_no): + secondary_items = {} for item in frappe.db.sql( - """select item_code, stock_qty from `tabBOM Scrap Item` + """select item_code, stock_qty from `tabBOM Secondary Item` where parent = %s""", bom_no, as_dict=1, ): - scrap_items[item.item_code] = item.stock_qty + secondary_items[item.item_code] = item.stock_qty - return scrap_items + return secondary_items def allow_overproduction(fieldname, percentage): diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index f382d1dcb60..18b5be64c10 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -387,6 +387,7 @@ frappe.ui.form.on("Work Order", { args: { work_order: frm.doc.name, operations: selected_rows, + parent_bom: frm.doc.bom_no, }, callback: function () { frm.reload_doc(); diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index f9d380964bc..72fafa03edd 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -2356,7 +2356,7 @@ def check_if_scrap_warehouse_mandatory(bom_no): if bom_no: bom = frappe.get_doc("BOM", bom_no) - if len(bom.scrap_items) > 0: + if bom.has_scrap_items(): res["set_scrap_wh_mandatory"] = True return res @@ -2420,6 +2420,7 @@ def make_stock_entry( stock_entry.set_stock_entry_type() stock_entry.is_additional_transfer_entry = is_additional_transfer_entry stock_entry.get_items() + stock_entry.set_secondary_items_from_job_card() if purpose != "Disassemble": stock_entry.set_serial_no_batch_for_finished_good() @@ -2478,14 +2479,14 @@ def query_sales_order(doctype, txt, searchfield, start, page_len, filters) -> li @frappe.whitelist() -def make_job_card(work_order, operations): +def make_job_card(work_order: str, operations: str | list, parent_bom: str | None = None): if isinstance(operations, str): operations = json.loads(operations) work_order = frappe.get_doc("Work Order", work_order) for row in operations: row = frappe._dict(row) - row.update(get_operation_details(row.name, work_order)) + row.update(get_operation_details(row.name, work_order, parent_bom)) validate_operation_data(row) qty = row.get("qty") @@ -2495,7 +2496,7 @@ def make_job_card(work_order, operations): create_job_card(work_order, row, auto_create=True) -def get_operation_details(name, work_order): +def get_operation_details(name, work_order, parent_bom): for row in work_order.operations: if row.name == name: return { @@ -2505,7 +2506,7 @@ def get_operation_details(name, work_order): "fg_warehouse": row.fg_warehouse, "wip_warehouse": row.wip_warehouse, "finished_good": row.finished_good, - "bom_no": row.get("bom_no"), + "bom_no": row.get("bom_no") or parent_bom, "is_subcontracted": row.get("is_subcontracted"), } @@ -2640,8 +2641,9 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create work_order.transfer_material_against == "Job Card" and not work_order.skip_transfer ): doc.get_required_items() - if work_order.track_semi_finished_goods: - doc.set_scrap_items() + + if work_order.track_semi_finished_goods: + doc.set_secondary_items() if auto_create: doc.flags.ignore_mandatory = True diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 4b1fc449473..8e36eaaed40 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -472,3 +472,4 @@ erpnext.patches.v16_0.update_order_qty_and_requested_qty_based_on_mr_and_po erpnext.patches.v16_0.enable_serial_batch_setting erpnext.patches.v16_0.update_requested_qty_packed_item erpnext.patches.v16_0.remove_payables_receivables_workspace +erpnext.patches.v16_0.co_by_product_patch diff --git a/erpnext/patches/v16_0/co_by_product_patch.py b/erpnext/patches/v16_0/co_by_product_patch.py new file mode 100644 index 00000000000..63f43e85b9e --- /dev/null +++ b/erpnext/patches/v16_0/co_by_product_patch.py @@ -0,0 +1,104 @@ +from collections import defaultdict + +import frappe +from frappe.model.utils.rename_field import rename_field + + +def execute(): + copy_doctypes() + rename_fields() + + +def copy_doctypes(): + previous = frappe.db.auto_commit_on_many_writes + frappe.db.auto_commit_on_many_writes = True + try: + insert_into_bom() + insert_into_job_card() + if frappe.db.has_table("Subcontracting Inward Order Scrap Item"): + insert_into_subcontracting_inward() + finally: + frappe.db.auto_commit_on_many_writes = previous + + +def insert_into_bom(): + fields = ["item_code", "item_name", "stock_uom", "stock_qty", "rate"] + data = frappe.get_all("BOM Scrap Item", {"docstatus": ("<", 2)}, ["parent", *fields]) + grouped_data = defaultdict(list) + for item in data: + grouped_data[item.parent].append(item) + + for parent, items in grouped_data.items(): + bom = frappe.get_doc("BOM", parent) + for item in items: + secondary_item = frappe.new_doc( + "BOM Secondary Item", parent_doc=bom, parentfield="secondary_items" + ) + secondary_item.update({field: item[field] for field in fields}) + secondary_item.update( + { + "uom": item.stock_uom, + "conversion_factor": 1, + "qty": item.stock_qty, + "is_legacy": 1, + "type": "Scrap", + } + ) + secondary_item.insert() + + +def insert_into_job_card(): + fields = ["item_code", "item_name", "description", "stock_qty", "stock_uom"] + bulk_insert("Job Card", "Job Card Scrap Item", "Job Card Secondary Item", fields, ["type"], ["Scrap"]) + + +def insert_into_subcontracting_inward(): + fields = [ + "item_code", + "fg_item_code", + "stock_uom", + "warehouse", + "reference_name", + "produced_qty", + "delivered_qty", + ] + bulk_insert( + "Subcontracting Inward Order", + "Subcontracting Inward Order Scrap Item", + "Subcontracting Inward Order Secondary Item", + fields, + ["type"], + ["Scrap"], + ) + + +def bulk_insert(parent_doctype, old_doctype, new_doctype, old_fields, new_fields, new_values): + data = frappe.get_all(old_doctype, {"docstatus": ("<", 2)}, ["parent", *old_fields]) + grouped_data = defaultdict(list) + + for item in data: + grouped_data[item.parent].append(item) + + for parent, items in grouped_data.items(): + parent_doc = frappe.get_doc(parent_doctype, parent) + for item in items: + secondary_item = frappe.new_doc(new_doctype, parent_doc=parent_doc, parentfield="secondary_items") + secondary_item.update({old_field: item[old_field] for old_field in old_fields}) + secondary_item.update( + {new_field: new_value for new_field, new_value in zip(new_fields, new_values, strict=True)} + ) + secondary_item.insert() + + +def rename_fields(): + rename_field("BOM", "scrap_material_cost", "secondary_items_cost") + rename_field("BOM", "base_scrap_material_cost", "base_secondary_items_cost") + rename_field("Stock Entry Detail", "is_scrap_item", "is_legacy_scrap_item") + rename_field( + "Manufacturing Settings", + "set_op_cost_and_scrap_from_sub_assemblies", + "set_op_cost_and_secondary_items_from_sub_assemblies", + ) + rename_field("Selling Settings", "deliver_scrap_items", "deliver_secondary_items") + rename_field("Subcontracting Receipt Item", "is_scrap_item", "is_legacy_scrap_item") + rename_field("Subcontracting Receipt Item", "scrap_cost_per_qty", "secondary_items_cost_per_qty") diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index f0be33b6a87..4971f914b1e 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1855,7 +1855,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe "base_operating_cost", "base_raw_material_cost", "base_total_cost", - "base_scrap_material_cost", + "base_secondary_items_cost", "base_totals_section", ], company_currency @@ -1873,7 +1873,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe "paid_amount", "write_off_amount", "operating_cost", - "scrap_material_cost", + "secondary_items_cost", "raw_material_cost", "total_cost", "totals_section", @@ -1919,7 +1919,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe "base_operating_cost", "base_raw_material_cost", "base_total_cost", - "base_scrap_material_cost", + "base_secondary_items_cost", "base_rounding_adjustment", ], this.frm.doc.currency != company_currency @@ -1984,11 +1984,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe }); } - if (this.frm.doc.scrap_items && this.frm.doc.scrap_items.length > 0) { - this.frm.set_currency_labels(["rate", "amount"], this.frm.doc.currency, "scrap_items"); - this.frm.set_currency_labels(["base_rate", "base_amount"], company_currency, "scrap_items"); + if (this.frm.doc.secondary_items && this.frm.doc.secondary_items.length > 0) { + this.frm.set_currency_labels(["rate", "amount"], this.frm.doc.currency, "secondary_items"); + this.frm.set_currency_labels(["base_rate", "base_amount"], company_currency, "secondary_items"); - var item_grid = this.frm.fields_dict["scrap_items"].grid; + var item_grid = this.frm.fields_dict["secondary_items"].grid; $.each(["base_rate", "base_amount"], function (i, fname) { if (frappe.meta.get_docfield(item_grid.doctype, fname)) item_grid.set_column_disp(fname, me.frm.doc.currency != company_currency); diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index b7896b58dff..d501f8abd51 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -49,7 +49,7 @@ "section_break_zwh6", "allow_delivery_of_overproduced_qty", "column_break_mla9", - "deliver_scrap_items" + "deliver_secondary_items" ], "fields": [ { @@ -260,13 +260,6 @@ "fieldname": "column_break_mla9", "fieldtype": "Column Break" }, - { - "default": "0", - "description": "If enabled, the Scrap Item generated against a Finished Good will also be added in the Stock Entry when delivering that Finished Good.", - "fieldname": "deliver_scrap_items", - "fieldtype": "Check", - "label": "Deliver Scrap Items" - }, { "fieldname": "item_price_tab", "fieldtype": "Tab Break", @@ -320,6 +313,13 @@ "fieldname": "enable_utm", "fieldtype": "Check", "label": "Enable UTM" + }, + { + "default": "0", + "description": "If enabled, the Secondary Items generated against a Finished Good will also be added in the Stock Entry when delivering that Finished Good.", + "fieldname": "deliver_secondary_items", + "fieldtype": "Check", + "label": "Deliver Secondary Items" } ], "grid_page_length": 50, diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py index 8621f5f066d..c13d4ce0a6c 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.py +++ b/erpnext/selling/doctype/selling_settings/selling_settings.py @@ -41,7 +41,7 @@ class SellingSettings(Document): blanket_order_allowance: DF.Float cust_master_name: DF.Literal["Customer Name", "Naming Series", "Auto Name"] customer_group: DF.Link | None - deliver_scrap_items: DF.Check + deliver_secondary_items: DF.Check dn_required: DF.Literal["No", "Yes"] dont_reserve_sales_order_qty_on_sales_return: DF.Check editable_bundle_item_rates: DF.Check diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 8111935a339..51eb71d6f79 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -820,7 +820,7 @@ class Company(NestedSet): boms = frappe.db.sql_list("select name from tabBOM where company=%s", self.name) if boms: frappe.db.sql("delete from tabBOM where company=%s", self.name) - for dt in ("BOM Operation", "BOM Item", "BOM Scrap Item", "BOM Explosion Item"): + for dt in ("BOM Operation", "BOM Item", "BOM Secondary Item", "BOM Explosion Item"): frappe.db.sql( "delete from `tab{}` where parent in ({})".format(dt, ", ".join(["%s"] * len(boms))), tuple(boms), diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index f71b67e1127..dbfad27be26 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -1334,13 +1334,13 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle } fg_completed_qty() { - this.get_items(); + if (!this.frm.doc.job_card) { + this.get_items(); + } } get_items() { var me = this; - if (!this.frm.doc.fg_completed_qty || !this.frm.doc.bom_no) - frappe.throw(__("BOM and Manufacturing Quantity are required")); if (this.frm.doc.work_order || this.frm.doc.bom_no) { // if work order / bom is mentioned, get items diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 4ce2bda3631..19c00ceacea 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -31,7 +31,7 @@ from erpnext.manufacturing.doctype.bom.bom import ( add_additional_cost, get_bom_items_as_dict, get_op_cost_from_sub_assemblies, - get_scrap_items_from_sub_assemblies, + get_secondary_items_from_sub_assemblies, validate_bom_no, ) from erpnext.setup.doctype.brand.brand import get_brand_defaults @@ -245,7 +245,7 @@ class StockEntry(StockController, SubcontractingInwardController): self.validate_company_in_accounting_dimension() if self.purpose in ("Manufacture", "Repack"): - self.mark_finished_and_scrap_items() + self.mark_finished_and_secondary_items() if not self.job_card: self.validate_finished_goods() else: @@ -272,7 +272,7 @@ class StockEntry(StockController, SubcontractingInwardController): self.validate_component_and_quantities() if self.get("purpose") != "Manufacture": - # ignore scrap item wh difference and empty source/target wh + # ignore other item wh difference and empty source/target wh # in Manufacture Entry self.reset_default_field_value("from_warehouse", "items", "s_warehouse") self.reset_default_field_value("to_warehouse", "items", "t_warehouse") @@ -656,7 +656,7 @@ class StockEntry(StockController, SubcontractingInwardController): item.expense_account = frappe.get_value("Company", self.company, "default_expense_account") def validate_fg_completed_qty(self): - if self.purpose != "Manufacture": + if self.purpose != "Manufacture" or not self.from_bom: return fg_qty = defaultdict(float) @@ -789,7 +789,7 @@ class StockEntry(StockController, SubcontractingInwardController): if self.purpose == "Manufacture": if has_bom: - if d.is_finished_item or d.is_scrap_item: + if d.is_finished_item or d.type or d.is_legacy_scrap_item: d.s_warehouse = None if not d.t_warehouse: frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx)) @@ -1093,11 +1093,10 @@ class StockEntry(StockController, SubcontractingInwardController): def set_basic_rate(self, reset_outgoing_rate=True, raise_error_if_no_rate=True): """ - Set rate for outgoing, scrapped and finished items + Set rate for outgoing, secondary and finished items """ # Set rate for outgoing items outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate, raise_error_if_no_rate) - finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item) items = [] # Set basic rate for incoming items @@ -1111,11 +1110,19 @@ class StockEntry(StockController, SubcontractingInwardController): elif d.is_finished_item: if self.purpose == "Manufacture": d.basic_rate = self.get_basic_rate_for_manufactured_item( - finished_item_qty, outgoing_items_cost + d.transfer_qty, outgoing_items_cost ) elif self.purpose == "Repack": d.basic_rate = self.get_basic_rate_for_repacked_items(d.transfer_qty, outgoing_items_cost) + if self.bom_no: + d.basic_rate *= frappe.get_value("BOM", self.bom_no, "cost_allocation_per") / 100 + elif d.type and d.bom_secondary_item: + cost_allocation_per = frappe.get_value( + "BOM Secondary Item", d.bom_secondary_item, "cost_allocation_per" + ) + d.basic_rate = (outgoing_items_cost * (cost_allocation_per / 100)) / d.transfer_qty + if not d.basic_rate and not d.allow_zero_valuation_rate: if self.is_new(): raise_error_if_no_rate = False @@ -1198,7 +1205,7 @@ class StockEntry(StockController, SubcontractingInwardController): def get_basic_rate_for_manufactured_item(self, finished_item_qty, outgoing_items_cost=0) -> float: settings = frappe.get_single("Manufacturing Settings") - scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item]) + scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_legacy_scrap_item]) if settings.material_consumption: if settings.get_rm_cost_from_consumption_entry and self.work_order: @@ -1212,7 +1219,7 @@ class StockEntry(StockController, SubcontractingInwardController): }, ): for item in self.items: - if not item.is_finished_item and not item.is_scrap_item: + if not item.is_finished_item and not item.type and not item.is_legacy_scrap_item: label = frappe.get_meta(settings.doctype).get_label( "get_rm_cost_from_consumption_entry" ) @@ -1614,7 +1621,7 @@ class StockEntry(StockController, SubcontractingInwardController): order, ) - def mark_finished_and_scrap_items(self): + def mark_finished_and_secondary_items(self): if self.purpose != "Repack" and any( [d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)] ): @@ -1631,11 +1638,9 @@ class StockEntry(StockController, SubcontractingInwardController): if d.t_warehouse and not d.s_warehouse: if self.purpose == "Repack" or d.item_code == finished_item: d.is_finished_item = 1 - else: - d.is_scrap_item = 1 else: d.is_finished_item = 0 - d.is_scrap_item = 0 + d.type = "" def get_finished_item(self): finished_item = None @@ -2434,7 +2439,7 @@ class StockEntry(StockController, SubcontractingInwardController): self.load_items_from_bom() self.set_serial_batch_from_reserved_entry() - self.set_scrap_items() + self.set_secondary_items() self.set_actual_qty() self.validate_customer_provided_item() self.calculate_rate_and_amount(raise_error_if_no_rate=False) @@ -2579,14 +2584,21 @@ class StockEntry(StockController, SubcontractingInwardController): return query.run(as_dict=True) - def set_scrap_items(self): - if self.purpose != "Send to Subcontractor" and self.purpose in ["Manufacture", "Repack"]: - scrap_item_dict = self.get_bom_scrap_material(self.fg_completed_qty) - for item in scrap_item_dict.values(): - if self.pro_doc and self.pro_doc.scrap_warehouse: - item["to_warehouse"] = self.pro_doc.scrap_warehouse + def set_secondary_items(self): + if self.purpose in ["Manufacture", "Repack"]: + secondary_items_dict = self.get_secondary_items(self.fg_completed_qty) + for item in secondary_items_dict.values(): + if self.pro_doc and item.type: + if self.pro_doc.scrap_warehouse and item.type == "Scrap": + item["to_warehouse"] = self.pro_doc.scrap_warehouse - self.add_to_stock_entry_detail(scrap_item_dict, bom_no=self.bom_no) + if item.process_loss_per: + item["qty"] -= flt( + item["qty"] * (item.process_loss_per / 100), + self.precision("fg_completed_qty"), + ) + + self.add_to_stock_entry_detail(secondary_items_dict, bom_no=self.bom_no) def set_process_loss_qty(self): if self.purpose not in ("Manufacture", "Repack"): @@ -2600,7 +2612,7 @@ class StockEntry(StockController, SubcontractingInwardController): fields=[{"MAX": "process_loss_qty", "as": "process_loss_qty"}], ) - if data and data[0].process_loss_qty is not None: + if data and data[0].process_loss_qty: process_loss_qty = data[0].process_loss_qty if flt(self.process_loss_qty, precision) != flt(process_loss_qty, precision): self.process_loss_qty = flt(process_loss_qty, precision) @@ -2632,7 +2644,7 @@ class StockEntry(StockController, SubcontractingInwardController): if not self.pro_doc: self.pro_doc = frappe.get_doc("Work Order", self.work_order) - if self.pro_doc: + if self.pro_doc and not self.pro_doc.track_semi_finished_goods: self.bom_no = self.pro_doc.bom_no else: # invalid work order @@ -2774,54 +2786,59 @@ class StockEntry(StockController, SubcontractingInwardController): return item_dict - def get_bom_scrap_material(self, qty): + def get_secondary_items(self, qty): from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict if ( - frappe.db.get_single_value("Manufacturing Settings", "set_op_cost_and_scrap_from_sub_assemblies") + frappe.db.get_single_value( + "Manufacturing Settings", "set_op_cost_and_secondary_items_from_sub_assemblies" + ) and self.work_order and frappe.get_cached_value("Work Order", self.work_order, "use_multi_level_bom") ): - item_dict = get_scrap_items_from_sub_assemblies(self.bom_no, self.company, qty) + item_dict = get_secondary_items_from_sub_assemblies(self.bom_no, self.company, qty) else: # item dict = { item_code: {qty, description, stock_uom} } item_dict = ( get_bom_items_as_dict( - self.bom_no, self.company, qty=qty, fetch_exploded=0, fetch_scrap_items=1 + self.bom_no, self.company, qty=qty, fetch_exploded=0, fetch_secondary_items=1 ) or {} ) for item in item_dict.values(): item.from_warehouse = "" - item.is_scrap_item = 1 - - for row in self.get_scrap_items_from_job_card(): - if row.stock_qty <= 0: - continue - - item_row = item_dict.get(row.item_code) - if not item_row: - item_row = frappe._dict({}) - - item_row.update( - { - "uom": row.stock_uom, - "from_warehouse": "", - "qty": row.stock_qty + flt(item_row.stock_qty), - "converison_factor": 1, - "is_scrap_item": 1, - "item_name": row.item_name, - "description": row.description, - "allow_zero_valuation_rate": 1, - } - ) - - item_dict[row.item_code] = item_row return item_dict - def get_scrap_items_from_job_card(self): + def set_secondary_items_from_job_card(self): + if self.purpose not in ["Manufacture", "Repack"]: + return + + item_dict = {} + for row in self.get_secondary_items_from_job_card(): + if row.stock_qty <= 0: + continue + + item_dict[row.item_code] = frappe._dict( + { + "uom": row.stock_uom, + "from_warehouse": "", + "qty": row.stock_qty, + "conversion_factor": 1, + "type": row.type, + "item_name": row.item_name, + "description": row.description, + "bom_secondary_item": row.bom_secondary_item, + } + ) + + for item in item_dict.values(): + item.from_warehouse = "" + + self.add_to_stock_entry_detail(item_dict) + + def get_secondary_items_from_job_card(self): if not hasattr(self, "pro_doc"): self.pro_doc = None @@ -2832,70 +2849,78 @@ class StockEntry(StockController, SubcontractingInwardController): return [] job_card = frappe.qb.DocType("Job Card") - job_card_scrap_item = frappe.qb.DocType("Job Card Scrap Item") + job_card_secondary_item = frappe.qb.DocType("Job Card Secondary Item") - scrap_items = ( + other = ( frappe.qb.from_(job_card) .select( - Sum(job_card_scrap_item.stock_qty).as_("stock_qty"), - job_card_scrap_item.item_code, - job_card_scrap_item.item_name, - job_card_scrap_item.description, - job_card_scrap_item.stock_uom, + Sum(job_card_secondary_item.stock_qty).as_("stock_qty"), + job_card_secondary_item.item_code, + job_card_secondary_item.item_name, + job_card_secondary_item.description, + job_card_secondary_item.stock_uom, + job_card_secondary_item.type, + job_card_secondary_item.bom_secondary_item, ) - .join(job_card_scrap_item) - .on(job_card_scrap_item.parent == job_card.name) + .join(job_card_secondary_item) + .on(job_card_secondary_item.parent == job_card.name) .where( - (job_card_scrap_item.item_code.isnotnull()) + (job_card_secondary_item.item_code.isnotnull()) & (job_card.work_order == self.work_order) & (job_card.docstatus == 1) ) - .groupby(job_card_scrap_item.item_code) + .groupby(job_card_secondary_item.item_code, job_card_secondary_item.type) + .orderby(job_card_secondary_item.idx) ) if self.job_card: - scrap_items = scrap_items.where(job_card.name == self.job_card) + other = other.where(job_card.name == self.job_card) - scrap_items = scrap_items.run(as_dict=1) + other = other.run(as_dict=1) if self.job_card: pending_qty = flt(self.fg_completed_qty) else: pending_qty = flt(self.get_completed_job_card_qty()) - flt(self.pro_doc.produced_qty) - used_scrap_items = self.get_used_scrap_items() - for row in scrap_items: - row.stock_qty -= flt(used_scrap_items.get(row.item_code)) + used_secondary_items = self.get_used_secondary_items() + for row in other: + row.stock_qty -= flt(used_secondary_items.get(row.item_code)) row.stock_qty = (row.stock_qty) * flt(self.fg_completed_qty) / flt(pending_qty) - if used_scrap_items.get(row.item_code): - used_scrap_items[row.item_code] -= row.stock_qty + if used_secondary_items.get(row.item_code): + used_secondary_items[row.item_code] -= row.stock_qty if cint(frappe.get_cached_value("UOM", row.stock_uom, "must_be_whole_number")): row.stock_qty = frappe.utils.ceil(row.stock_qty) - return scrap_items + return other def get_completed_job_card_qty(self): return flt(min([d.completed_qty for d in self.pro_doc.operations])) - def get_used_scrap_items(self): - used_scrap_items = defaultdict(float) - data = frappe.get_all( - "Stock Entry", - fields=["`tabStock Entry Detail`.`item_code`", "`tabStock Entry Detail`.`qty`"], - filters=[ - ["Stock Entry", "work_order", "=", self.work_order], - ["Stock Entry Detail", "is_scrap_item", "=", 1], - ["Stock Entry", "docstatus", "=", 1], - ["Stock Entry", "purpose", "in", ["Repack", "Manufacture"]], - ], - ) + def get_used_secondary_items(self): + used_secondary_items = defaultdict(float) + + StockEntry = frappe.qb.DocType("Stock Entry") + StockEntryDetail = frappe.qb.DocType("Stock Entry Detail") + data = ( + frappe.qb.from_(StockEntry) + .inner_join(StockEntryDetail) + .on(StockEntryDetail.parent == StockEntry.name) + .select(StockEntryDetail.item_code, StockEntryDetail.qty) + .where( + (StockEntry.work_order == self.work_order) + & ((StockEntryDetail.type.isnotnull()) | (StockEntryDetail.is_legacy_scrap_item == 1)) + & (StockEntry.docstatus == 1) + & (StockEntry.purpose.isin(["Repack", "Manufacture"])) + ) + ).run(as_dict=1) for row in data: - used_scrap_items[row.item_code] += row.qty + used_secondary_items[row.item_code] += row.qty - return used_scrap_items + return used_secondary_items def get_unconsumed_raw_materials(self): wo = frappe.get_doc("Work Order", self.work_order) @@ -3187,7 +3212,12 @@ class StockEntry(StockController, SubcontractingInwardController): item_row = item_dict[d] child_qty = flt(item_row["qty"], precision) - if not self.is_return and child_qty <= 0 and not item_row.get("is_scrap_item"): + if ( + not self.is_return + and child_qty <= 0 + and not item_row.get("type") + and not item_row.get("is_legacy_scrap_item") + ): if self.purpose not in ["Receive from Customer", "Send to Subcontractor"]: continue @@ -3205,11 +3235,13 @@ class StockEntry(StockController, SubcontractingInwardController): item_row, company=self.company ) se_child.is_finished_item = item_row.get("is_finished_item", 0) - se_child.is_scrap_item = item_row.get("is_scrap_item", 0) se_child.po_detail = item_row.get("po_detail") se_child.sco_rm_detail = item_row.get("sco_rm_detail") se_child.scio_detail = item_row.get("scio_detail") se_child.sample_quantity = item_row.get("sample_quantity", 0) + se_child.type = item_row.get("type") + se_child.is_legacy_scrap_item = item_row.get("is_legacy") + se_child.bom_secondary_item = item_row.get("name") or item_row.get("bom_secondary_item") for field in [ self.subcontract_data.rm_detail_field, @@ -3686,7 +3718,7 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None): if ( bom_no and frappe.db.get_single_value( - "Manufacturing Settings", "set_op_cost_and_scrap_from_sub_assemblies" + "Manufacturing Settings", "set_op_cost_and_secondary_items_from_sub_assemblies" ) and frappe.get_cached_value("Work Order", work_order.name, "use_multi_level_bom") ): diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 48488a7c5b6..b102e20cfc4 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -909,8 +909,8 @@ class TestStockEntry(ERPNextTestSuite): if d.s_warehouse: rm_cost += d.amount fg_cost = next(filter(lambda x: x.item_code == "_Test FG Item", s.get("items"))).amount - scrap_cost = next(filter(lambda x: x.is_scrap_item, s.get("items"))).amount - self.assertEqual(fg_cost, flt(rm_cost - scrap_cost, 2)) + secondary_item_cost = next(filter(lambda x: x.type or x.is_legacy_scrap_item, s.get("items"))).amount + self.assertEqual(fg_cost, flt(rm_cost - secondary_item_cost, 2)) # When Stock Entry has only FG + Scrap s.items.pop(0) @@ -989,15 +989,15 @@ class TestStockEntry(ERPNextTestSuite): self.assertRaises(frappe.ValidationError, ste.submit) - def test_quality_check_for_scrap_item(self): + def test_quality_check_for_secondary_item(self): from erpnext.manufacturing.doctype.work_order.work_order import ( make_stock_entry as _make_stock_entry, ) - scrap_item = "_Test Scrap Item 1" - make_item(scrap_item, {"is_stock_item": 1, "is_purchase_item": 0}) + secondary_item = "_Test Scrap Item 1" + make_item(secondary_item, {"is_stock_item": 1, "is_purchase_item": 0}) - bom_name = frappe.db.get_value("BOM Scrap Item", {"docstatus": 1}, "parent") + bom_name = frappe.db.get_value("BOM Secondary Item", {"docstatus": 1}, "parent") production_item = frappe.db.get_value("BOM", bom_name, "item") work_order = frappe.new_doc("Work Order") @@ -1027,18 +1027,18 @@ class TestStockEntry(ERPNextTestSuite): basic_rate=row.basic_rate or 100, ) - if row.is_scrap_item: - row.item_code = scrap_item - row.uom = frappe.db.get_value("Item", scrap_item, "stock_uom") - row.stock_uom = frappe.db.get_value("Item", scrap_item, "stock_uom") + if row.type or row.is_legacy_scrap_item: + row.item_code = secondary_item + row.uom = frappe.db.get_value("Item", secondary_item, "stock_uom") + row.stock_uom = frappe.db.get_value("Item", secondary_item, "stock_uom") stock_entry.inspection_required = 1 stock_entry.save() - self.assertTrue([row.item_code for row in stock_entry.items if row.is_scrap_item]) + self.assertTrue([row.item_code for row in stock_entry.items if row.type or row.is_legacy_scrap_item]) for row in stock_entry.items: - if not row.is_scrap_item: + if not row.type and not row.is_legacy_scrap_item: qc = frappe.get_doc( { "doctype": "Quality Inspection", @@ -1058,7 +1058,7 @@ class TestStockEntry(ERPNextTestSuite): stock_entry.reload() stock_entry.submit() for row in stock_entry.items: - if row.is_scrap_item: + if row.type or row.is_legacy_scrap_item: self.assertFalse(row.quality_inspection) else: self.assertTrue(row.quality_inspection) @@ -2464,6 +2464,35 @@ class TestStockEntry(ERPNextTestSuite): # delete naming rule frappe.delete_doc("Document Naming Rule", qc_naming_rule.name) + def test_co_by_product(self): + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + + frappe.set_value("UOM", "Nos", "must_be_whole_number", 0) + + fg_item = make_item("FG Item", properties={"is_stock_item": 1}).name + rm_item = make_item("RM Item", properties={"is_stock_item": 1}).name + scrap_item = make_item("Scrap Item", properties={"is_stock_item": 1}).name + warehouse = "_Test Warehouse - _TC" + make_stock_entry(item_code=rm_item, target=warehouse, qty=5, rate=10, purpose="Material Receipt") + + bom_no = make_bom( + item=fg_item, raw_materials=[rm_item], scrap_items=[scrap_item], process_loss_percentage=10 + ).name + se = make_stock_entry(item_code=fg_item, qty=5, purpose="Manufacture", do_not_save=True) + se.from_bom = 1 + se.bom_no = bom_no + se.fg_completed_qty = 5 + se.from_warehouse = warehouse + se.to_warehouse = "_Test Warehouse 1 - _TC" + se.get_items() + se.save() + se.reload() + + self.assertEqual(se.items[1].qty, 4.5) + self.assertEqual(se.items[1].amount, 45) + self.assertEqual(se.items[2].qty, 4.5) + self.assertEqual(se.items[2].amount, 5) + def make_serialized_item(self, **args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index eceba634bf3..f28f5e25a66 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -18,7 +18,8 @@ "item_name", "col_break2", "is_finished_item", - "is_scrap_item", + "is_legacy_scrap_item", + "type", "quality_inspection", "subcontracted_item", "against_fg", @@ -81,7 +82,8 @@ "putaway_rule", "column_break_51", "reference_purchase_receipt", - "job_card_item" + "job_card_item", + "bom_secondary_item" ], "fields": [ { @@ -558,12 +560,7 @@ }, { "default": "0", - "fieldname": "is_scrap_item", - "fieldtype": "Check", - "label": "Is Scrap Item" - }, - { - "default": "0", + "depends_on": "eval:!doc.is_legacy_scrap_item && !doc.type", "fieldname": "is_finished_item", "fieldtype": "Check", "label": "Is Finished Item", @@ -654,6 +651,28 @@ "no_copy": 1, "options": "Subcontracting Inward Order Item", "set_only_once": 1 + }, + { + "depends_on": "eval:parent.purpose == \"Manufacture\" && doc.t_warehouse && !doc.is_finished_item && !doc.is_legacy_scrap_item", + "fieldname": "type", + "fieldtype": "Select", + "label": "Type", + "options": "\nCo-Product\nBy-Product\nScrap\nAdditional Finished Good" + }, + { + "fieldname": "bom_secondary_item", + "fieldtype": "Data", + "hidden": 1, + "label": "BOM Secondary Item", + "read_only": 1 + }, + { + "default": "0", + "depends_on": "is_legacy_scrap_item", + "fieldname": "is_legacy_scrap_item", + "fieldtype": "Check", + "label": "Is Legacy Scrap Item", + "read_only": 1 } ], "grid_page_length": 50, diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py index 95bb7181a0f..0c1a21fefce 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py @@ -26,6 +26,7 @@ class StockEntryDetail(Document): basic_rate: DF.Currency batch_no: DF.Link | None bom_no: DF.Link | None + bom_secondary_item: DF.Data | None conversion_factor: DF.Float cost_center: DF.Link | None customer_provided_item_cost: DF.Currency @@ -34,7 +35,7 @@ class StockEntryDetail(Document): has_item_scanned: DF.Check image: DF.Attach | None is_finished_item: DF.Check - is_scrap_item: DF.Check + is_legacy_scrap_item: DF.Check item_code: DF.Link item_group: DF.Data | None item_name: DF.Data | None @@ -66,6 +67,7 @@ class StockEntryDetail(Document): t_warehouse: DF.Link | None transfer_qty: DF.Float transferred_qty: DF.Float + type: DF.Literal["", "Co-Product", "By-Product", "Scrap", "Additional Finished Good"] uom: DF.Link use_serial_batch_fields: DF.Check valuation_rate: DF.Currency diff --git a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py index 4a768ee94fd..f02c06810f0 100644 --- a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py +++ b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py @@ -75,13 +75,18 @@ class ManufactureEntry: self.stock_entry = frappe.new_doc("Stock Entry") self.stock_entry.purpose = self.purpose self.stock_entry.company = self.company - self.stock_entry.from_bom = 1 - self.stock_entry.bom_no = self.bom_no - self.stock_entry.use_multi_level_bom = 1 + + if self.bom_no: + self.stock_entry.from_bom = 1 + self.stock_entry.bom_no = self.bom_no + self.stock_entry.use_multi_level_bom = 1 + self.stock_entry.fg_completed_qty = self.for_quantity + self.stock_entry.process_loss_qty = self.process_loss_qty self.stock_entry.project = self.project self.stock_entry.job_card = self.job_card self.stock_entry.set_stock_entry_type() + self.stock_entry.work_order = self.work_order self.prepare_source_warehouse() self.add_raw_materials() @@ -303,7 +308,7 @@ class ManufactureEntry: args = { "to_warehouse": self.fg_warehouse, "from_warehouse": "", - "qty": self.for_quantity, + "qty": self.for_quantity - self.process_loss_qty, "item_name": item.item_name, "description": item.description, "stock_uom": item.stock_uom, diff --git a/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.json b/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.json index 95ac21ac71b..a0b163f4271 100644 --- a/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.json +++ b/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.json @@ -25,7 +25,7 @@ "raw_materials_received_section", "received_items", "scrap_items_generated_section", - "scrap_items", + "secondary_items", "service_items_section", "service_items", "tab_other_info", @@ -252,17 +252,10 @@ "reqd": 1 }, { - "depends_on": "scrap_items", + "depends_on": "secondary_items", "fieldname": "scrap_items_generated_section", "fieldtype": "Section Break", - "label": "Scrap Items Generated" - }, - { - "fieldname": "scrap_items", - "fieldtype": "Table", - "label": "Scrap Items", - "no_copy": 1, - "options": "Subcontracting Inward Order Scrap Item" + "label": "Secondary Items Generated" }, { "fieldname": "per_returned", @@ -300,13 +293,20 @@ "label": "Customer Currency", "options": "Currency", "read_only": 1 + }, + { + "fieldname": "secondary_items", + "fieldtype": "Table", + "label": "Secondary Items", + "no_copy": 1, + "options": "Subcontracting Inward Order Secondary Item" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-12-09 15:52:55.781346", + "modified": "2026-02-26 17:16:21.697846", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Inward Order", diff --git a/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py b/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py index b516518bfcb..aea08e18b34 100644 --- a/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py @@ -25,8 +25,8 @@ class SubcontractingInwardOrder(SubcontractingController): from erpnext.subcontracting.doctype.subcontracting_inward_order_received_item.subcontracting_inward_order_received_item import ( SubcontractingInwardOrderReceivedItem, ) - from erpnext.subcontracting.doctype.subcontracting_inward_order_scrap_item.subcontracting_inward_order_scrap_item import ( - SubcontractingInwardOrderScrapItem, + from erpnext.subcontracting.doctype.subcontracting_inward_order_secondary_item.subcontracting_inward_order_secondary_item import ( + SubcontractingInwardOrderSecondaryItem, ) from erpnext.subcontracting.doctype.subcontracting_inward_order_service_item.subcontracting_inward_order_service_item import ( SubcontractingInwardOrderServiceItem, @@ -48,7 +48,7 @@ class SubcontractingInwardOrder(SubcontractingController): per_returned: DF.Percent received_items: DF.Table[SubcontractingInwardOrderReceivedItem] sales_order: DF.Link - scrap_items: DF.Table[SubcontractingInwardOrderScrapItem] + secondary_items: DF.Table[SubcontractingInwardOrderSecondaryItem] service_items: DF.Table[SubcontractingInwardOrderServiceItem] set_delivery_warehouse: DF.Link | None status: DF.Literal[ @@ -474,23 +474,25 @@ class SubcontractingInwardOrder(SubcontractingController): stock_entry.add_to_stock_entry_detail(items_dict) if ( - frappe.get_single_value("Selling Settings", "deliver_scrap_items") - and self.scrap_items + frappe.get_single_value("Selling Settings", "deliver_secondary_items") + and self.secondary_items and scio_details ): - scrap_items = [ - scrap_item for scrap_item in self.scrap_items if scrap_item.reference_name in scio_details + secondary_items = [ + secondary_item + for secondary_item in self.secondary_items + if secondary_item.reference_name in scio_details ] - for scrap_item in scrap_items: - qty = scrap_item.produced_qty - scrap_item.delivered_qty + for secondary_item in secondary_items: + qty = secondary_item.produced_qty - secondary_item.delivered_qty if qty > 0: items_dict = { - scrap_item.item_code: { - "qty": scrap_item.produced_qty - scrap_item.delivered_qty, - "from_warehouse": scrap_item.warehouse, - "stock_uom": scrap_item.stock_uom, - "scio_detail": scrap_item.name, - "is_scrap_item": 1, + secondary_item.item_code: { + "qty": secondary_item.produced_qty - secondary_item.delivered_qty, + "from_warehouse": secondary_item.warehouse, + "stock_uom": secondary_item.stock_uom, + "scio_detail": secondary_item.name, + "type": secondary_item.type, } } diff --git a/erpnext/subcontracting/doctype/subcontracting_inward_order/test_subcontracting_inward_order.py b/erpnext/subcontracting/doctype/subcontracting_inward_order/test_subcontracting_inward_order.py index 9463b11bf4c..d035f4ddcb9 100644 --- a/erpnext/subcontracting/doctype/subcontracting_inward_order/test_subcontracting_inward_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_inward_order/test_subcontracting_inward_order.py @@ -323,10 +323,12 @@ class IntegrationTestSubcontractingInwardOrder(ERPNextTestSuite): delivery.items[0].qty = 6 self.assertRaises(frappe.ValidationError, delivery.submit) - @ERPNextTestSuite.change_settings("Selling Settings", {"deliver_scrap_items": 1}) + @ERPNextTestSuite.change_settings("Selling Settings", {"deliver_secondary_items": 1}) def test_secondary_items_delivery(self): new_bom = frappe.copy_doc(frappe.get_doc("BOM", "BOM-Basic FG Item-001")) - new_bom.scrap_items.append(frappe.new_doc("BOM Scrap Item", item_code="Basic RM 2", qty=1)) + new_bom.secondary_items.append( + frappe.new_doc("BOM Secondary Item", item_code="Basic RM 2", qty=1, type="Scrap") + ) new_bom.submit() sc_bom = frappe.get_doc("Subcontracting BOM", "SB-0001") sc_bom.finished_good_bom = new_bom.name @@ -343,12 +345,12 @@ class IntegrationTestSubcontractingInwardOrder(ERPNextTestSuite): frappe.new_doc("Stock Entry").update(make_stock_entry_from_wo(wo.name, "Manufacture")).submit() scio.reload() - self.assertEqual(scio.scrap_items[0].item_code, "Basic RM 2") + self.assertEqual(scio.secondary_items[0].item_code, "Basic RM 2") delivery = frappe.new_doc("Stock Entry").update(scio.make_subcontracting_delivery()) self.assertEqual(delivery.items[-1].item_code, "Basic RM 2") - frappe.db.set_single_value("Selling Settings", "deliver_scrap_items", 0) + frappe.db.set_single_value("Selling Settings", "deliver_secondary_items", 0) delivery = frappe.new_doc("Stock Entry").update(scio.make_subcontracting_delivery()) self.assertNotEqual(delivery.items[-1].item_code, "Basic RM 2") diff --git a/erpnext/subcontracting/doctype/subcontracting_inward_order_scrap_item/__init__.py b/erpnext/subcontracting/doctype/subcontracting_inward_order_secondary_item/__init__.py similarity index 100% rename from erpnext/subcontracting/doctype/subcontracting_inward_order_scrap_item/__init__.py rename to erpnext/subcontracting/doctype/subcontracting_inward_order_secondary_item/__init__.py diff --git a/erpnext/subcontracting/doctype/subcontracting_inward_order_scrap_item/subcontracting_inward_order_scrap_item.json b/erpnext/subcontracting/doctype/subcontracting_inward_order_secondary_item/subcontracting_inward_order_secondary_item.json similarity index 83% rename from erpnext/subcontracting/doctype/subcontracting_inward_order_scrap_item/subcontracting_inward_order_scrap_item.json rename to erpnext/subcontracting/doctype/subcontracting_inward_order_secondary_item/subcontracting_inward_order_secondary_item.json index 78902701532..94a640b41ce 100644 --- a/erpnext/subcontracting/doctype/subcontracting_inward_order_scrap_item/subcontracting_inward_order_scrap_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_inward_order_secondary_item/subcontracting_inward_order_secondary_item.json @@ -6,13 +6,15 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "column_break_rptg", + "type", + "reference_name", + "column_break_jkzt", "item_code", "fg_item_code", "column_break_hoxe", "stock_uom", "warehouse", - "column_break_rptg", - "reference_name", "section_break_gqk9", "produced_qty", "column_break_n4xc", @@ -93,16 +95,29 @@ { "fieldname": "column_break_n4xc", "fieldtype": "Column Break" + }, + { + "fieldname": "type", + "fieldtype": "Select", + "label": "Type", + "no_copy": 1, + "options": "Co-Product\nBy-Product\nScrap\nAdditional Finished Good", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "column_break_jkzt", + "fieldtype": "Column Break" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-10-14 10:28:30.192350", + "modified": "2026-02-27 15:15:40.009957", "modified_by": "Administrator", "module": "Subcontracting", - "name": "Subcontracting Inward Order Scrap Item", + "name": "Subcontracting Inward Order Secondary Item", "owner": "Administrator", "permissions": [], "row_format": "Dynamic", diff --git a/erpnext/subcontracting/doctype/subcontracting_inward_order_scrap_item/subcontracting_inward_order_scrap_item.py b/erpnext/subcontracting/doctype/subcontracting_inward_order_secondary_item/subcontracting_inward_order_secondary_item.py similarity index 81% rename from erpnext/subcontracting/doctype/subcontracting_inward_order_scrap_item/subcontracting_inward_order_scrap_item.py rename to erpnext/subcontracting/doctype/subcontracting_inward_order_secondary_item/subcontracting_inward_order_secondary_item.py index d7aaae229dd..767f216921a 100644 --- a/erpnext/subcontracting/doctype/subcontracting_inward_order_scrap_item/subcontracting_inward_order_scrap_item.py +++ b/erpnext/subcontracting/doctype/subcontracting_inward_order_secondary_item/subcontracting_inward_order_secondary_item.py @@ -5,7 +5,7 @@ from frappe.model.document import Document -class SubcontractingInwardOrderScrapItem(Document): +class SubcontractingInwardOrderSecondaryItem(Document): # begin: auto-generated types # This code is auto-generated. Do not modify anything in this block. @@ -23,6 +23,7 @@ class SubcontractingInwardOrderScrapItem(Document): produced_qty: DF.Float reference_name: DF.Data stock_uom: DF.Link + type: DF.Literal["Co-Product", "By-Product", "Scrap", "Additional Finished Good"] warehouse: DF.Link # end: auto-generated types diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index 1e05afa2fbf..40de8eb39d4 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -439,6 +439,13 @@ def get_mapped_subcontracting_receipt(source_name, target_doc=None, items=None): target.purchase_order = source_parent.purchase_order target.purchase_order_item = source.purchase_order_item target.qty = items.get(source.name) or (flt(source.qty) - flt(source.received_qty)) + target.received_qty = target.qty + if process_loss_per := frappe.get_value("BOM", source.bom, "process_loss_percentage"): + target.process_loss_qty = flt( + target.qty * (process_loss_per / 100), target.precision("process_loss_qty") + ) + target.qty -= target.process_loss_qty + target.amount = (flt(source.qty) - flt(source.received_qty)) * flt(source.rate) items = {item["name"]: item["qty"] for item in items} if items else {} diff --git a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json index 689b64492f5..44ec2185ce6 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json @@ -425,7 +425,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-11-03 12:29:45.156101", + "modified": "2026-02-27 23:03:36.436504", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Order Item", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js index 3339cff689c..5bb7c2f0cc2 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js @@ -174,6 +174,7 @@ frappe.ui.form.on("Subcontracting Receipt", { frm.trigger("setup_quality_inspection"); frm.trigger("set_route_options_for_new_doc"); + frm.set_df_property("items", "cannot_add_rows", true); }, set_warehouse: (frm) => { @@ -184,15 +185,15 @@ frappe.ui.form.on("Subcontracting Receipt", { set_warehouse_in_children(frm.doc.items, "rejected_warehouse", frm.doc.rejected_warehouse); }, - get_scrap_items: (frm) => { + get_secondary_items: (frm) => { frappe.call({ doc: frm.doc, - method: "get_scrap_items", + method: "get_secondary_items", args: { recalculate_rate: true, }, freeze: true, - freeze_message: __("Getting Scrap Items"), + freeze_message: __("Getting Secondary Items"), callback: (r) => { if (!r.exc) { frm.refresh(); @@ -422,11 +423,19 @@ frappe.ui.form.on("Subcontracting Receipt Item", { set_missing_values(frm); }, + rejected_qty(frm) { + set_missing_values(frm); + }, + + process_loss_qty(frm) { + set_missing_values(frm); + }, + rate(frm) { set_missing_values(frm); }, - items_delete: (frm) => { + items_delete(frm) { set_missing_values(frm); }, diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json index 79b46ec146a..a284f24fd50 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json @@ -29,8 +29,8 @@ "col_break_warehouse", "supplier_warehouse", "items_section", - "get_scrap_items", "items", + "get_secondary_items", "section_break0", "total_qty", "column_break_27", @@ -631,13 +631,6 @@ "label": "Edit Posting Date and Time", "print_hide": 1 }, - { - "depends_on": "eval: (!doc.__islocal && doc.docstatus == 0)", - "fieldname": "get_scrap_items", - "fieldtype": "Button", - "label": "Get Scrap Items", - "options": "get_scrap_items" - }, { "fieldname": "supplier_delivery_note", "fieldtype": "Data", @@ -674,12 +667,19 @@ "fieldtype": "Tab Break", "label": "Connections", "show_dashboard": 1 + }, + { + "depends_on": "eval: (!doc.__islocal && doc.docstatus == 0)", + "fieldname": "get_secondary_items", + "fieldtype": "Button", + "label": "Get Secondary Items", + "options": "get_secondary_items" } ], "in_create": 1, "is_submittable": 1, "links": [], - "modified": "2025-10-08 21:43:27.065640", + "modified": "2026-02-27 17:59:44.107193", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 2456e2ef90f..664adf254f8 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -144,12 +144,12 @@ class SubcontractingReceipt(SubcontractingController): super().validate() if self.is_new() and self.get("_action") == "save" and not frappe.in_test: - self.get_scrap_items() + self.get_secondary_items() self.set_missing_values() if self.get("_action") == "submit": - self.validate_scrap_items() + self.validate_secondary_items() self.validate_accepted_warehouse() self.validate_rejected_warehouse() @@ -343,39 +343,66 @@ class SubcontractingReceipt(SubcontractingController): self.update_rate_for_supplied_items() @frappe.whitelist() - def get_scrap_items(self, recalculate_rate=False): - self.remove_scrap_items() + def get_secondary_items(self, recalculate_rate: bool | None = False): + self.remove_secondary_items() for item in list(self.items): if item.bom: bom = frappe.get_doc("BOM", item.bom) - for scrap_item in bom.scrap_items: - qty = flt(item.qty) * (flt(scrap_item.stock_qty) / flt(bom.quantity)) - rate = ( - get_valuation_rate( - scrap_item.item_code, - self.set_warehouse, - self.doctype, - self.name, - currency=erpnext.get_company_currency(self.company), - company=self.company, - ) - or scrap_item.rate + for secondary_item in bom.secondary_items: + per_unit = secondary_item.stock_qty / bom.quantity + received_qty = flt(item.received_qty * per_unit, item.precision("received_qty")) + qty = flt( + item.received_qty * (per_unit - (secondary_item.process_loss_qty / bom.quantity)), + item.precision("qty"), ) + if not secondary_item.is_legacy: + lcv_cost_per_qty = ( + flt(item.landed_cost_voucher_amount) / flt(item.qty) if flt(item.qty) else 0.0 + ) + fg_item_cost = ( + flt(item.rm_cost_per_qty) + + flt(item.secondary_items_cost_per_qty) + + flt(item.additional_cost_per_qty) + + flt(lcv_cost_per_qty) + + flt(item.service_cost_per_qty) + ) * flt(item.received_qty) + rate = ( + (item.amount if self.is_new() else fg_item_cost) + * (secondary_item.cost_allocation_per / 100) + ) / qty + else: + rate = ( + get_valuation_rate( + secondary_item.item_code, + self.set_warehouse, + self.doctype, + self.name, + currency=erpnext.get_company_currency(self.company), + company=self.company, + ) + or secondary_item.rate + ) + self.append( "items", { - "is_scrap_item": 1, + "type": secondary_item.type, + "is_legacy_scrap_item": secondary_item.is_legacy, "reference_name": item.name, - "item_code": scrap_item.item_code, - "item_name": scrap_item.item_name, - "qty": qty, - "stock_uom": scrap_item.stock_uom, + "item_code": secondary_item.item_code, + "item_name": secondary_item.item_name, + "qty": received_qty + if not secondary_item.is_legacy + else flt(item.qty) * (flt(secondary_item.stock_qty) / flt(bom.quantity)), + "received_qty": received_qty, + "process_loss_qty": received_qty - qty, + "stock_uom": secondary_item.stock_uom, "rate": rate, "rm_cost_per_qty": 0, "service_cost_per_qty": 0, "additional_cost_per_qty": 0, - "scrap_cost_per_qty": 0, + "secondary_items_cost_per_qty": 0, "amount": qty * rate, "warehouse": self.set_warehouse, "rejected_warehouse": self.rejected_warehouse, @@ -386,15 +413,12 @@ class SubcontractingReceipt(SubcontractingController): self.calculate_additional_costs() self.calculate_items_qty_and_amount() - def remove_scrap_items(self, recalculate_rate=False): + def remove_secondary_items(self): for item in list(self.items): - if item.is_scrap_item: + if item.type or item.is_legacy_scrap_item: self.remove(item) else: - item.scrap_cost_per_qty = 0 - - if recalculate_rate: - self.calculate_items_qty_and_amount() + item.secondary_items_cost_per_qty = 0 @frappe.whitelist() def set_missing_values(self): @@ -449,30 +473,35 @@ class SubcontractingReceipt(SubcontractingController): else: rm_cost_map[item.reference_name] = item.amount - scrap_cost_map = {} + secondary_items_cost_map = {} for item in self.get("items") or []: - if item.is_scrap_item: - item.amount = flt(item.qty) * flt(item.rate) + if item.type or item.is_legacy_scrap_item: + qty = ( + flt(item.qty) + if item.is_legacy_scrap_item + else (flt(item.received_qty) - flt(item.process_loss_qty)) + ) + item.amount = qty * flt(item.rate) - if item.reference_name in scrap_cost_map: - scrap_cost_map[item.reference_name] += item.amount + if item.reference_name in secondary_items_cost_map: + secondary_items_cost_map[item.reference_name] += item.amount else: - scrap_cost_map[item.reference_name] = item.amount + secondary_items_cost_map[item.reference_name] = item.amount total_qty = total_amount = 0 for item in self.get("items") or []: - if not item.is_scrap_item: + if not item.type and not item.is_legacy_scrap_item: if item.qty: if item.name in rm_cost_map: item.rm_supp_cost = rm_cost_map[item.name] - item.rm_cost_per_qty = item.rm_supp_cost / item.qty + item.rm_cost_per_qty = item.rm_supp_cost / (item.received_qty or item.qty) rm_cost_map.pop(item.name) - if item.name in scrap_cost_map: - item.scrap_cost_per_qty = scrap_cost_map[item.name] / item.qty - scrap_cost_map.pop(item.name) + if item.name in secondary_items_cost_map: + item.secondary_items_cost_per_qty = secondary_items_cost_map[item.name] / item.qty + secondary_items_cost_map.pop(item.name) else: - item.scrap_cost_per_qty = 0 + item.secondary_items_cost_per_qty = 0 lcv_cost_per_qty = 0.0 if item.landed_cost_voucher_amount: @@ -483,36 +512,44 @@ class SubcontractingReceipt(SubcontractingController): + flt(item.service_cost_per_qty) + flt(item.additional_cost_per_qty) + flt(lcv_cost_per_qty) - - flt(item.scrap_cost_per_qty) ) - item.received_qty = flt(item.qty) + flt(item.rejected_qty) - item.amount = flt(item.qty) * flt(item.rate) + if item.bom: + item.received_qty = flt(item.qty) + flt(item.rejected_qty) + flt(item.process_loss_qty) + item.amount = ( + flt(item.received_qty) + * flt(item.rate) + * (frappe.get_value("BOM", item.bom, "cost_allocation_per") / 100) + ) + item.rate = item.amount / (item.qty or item.rejected_qty) + else: + item.qty = flt(item.received_qty) - flt(item.process_loss_qty) + item.amount = flt(item.qty) * flt(item.rate) - total_qty += flt(item.qty) + total_qty += flt(item.qty) + flt(item.rejected_qty) total_amount += item.amount else: self.total_qty = total_qty self.total = total_amount - def validate_scrap_items(self): + def validate_secondary_items(self): for item in self.items: - if item.is_scrap_item: + if item.type or item.is_legacy_scrap_item: if not item.qty: frappe.throw( - _("Row #{0}: Scrap Item Qty cannot be zero").format(item.idx), + _("Row #{0}: Secondary Item Qty cannot be zero").format(item.idx), ) if item.rejected_qty: frappe.throw( - _("Row #{0}: Rejected Qty cannot be set for Scrap Item {1}.").format( + _("Row #{0}: Rejected Qty cannot be set for Secondary Item {1}.").format( item.idx, frappe.bold(item.item_code) ), ) if not item.reference_name: frappe.throw( - _("Row #{0}: Finished Good reference is mandatory for Scrap Item {1}.").format( + _("Row #{0}: Finished Good reference is mandatory for Secondary Item {1}.").format( item.idx, frappe.bold(item.item_code) ), ) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index 53466f7405d..b4b0c930082 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -597,6 +597,7 @@ class TestSubcontractingReceipt(ERPNextTestSuite): scr.items[0].qty = 6 # Accepted Qty scr.items[0].rejected_qty = 4 + scr.set_missing_values() scr.save() # consumed_qty should be (accepted_qty * qty_consumed_per_unit) = (6 * 1) = 6 @@ -1154,7 +1155,7 @@ class TestSubcontractingReceipt(ERPNextTestSuite): # ValidationError should not be raised as `Inspection Required before Purchase` is disabled scr2.submit() - def test_scrap_items_for_subcontracting_receipt(self): + def test_secondary_items_for_subcontracting_receipt(self): set_backflush_based_on("BOM") fg_item = "Subcontracted Item SA1" @@ -1166,9 +1167,9 @@ class TestSubcontractingReceipt(ERPNextTestSuite): ] # Create Scrap Items - scrap_item_1 = make_item(properties={"is_stock_item": 1, "valuation_rate": 10}).name - scrap_item_2 = make_item(properties={"is_stock_item": 1, "valuation_rate": 20}).name - scrap_items = [scrap_item_1, scrap_item_2] + secondary_item_1 = make_item(properties={"is_stock_item": 1, "valuation_rate": 10}).name + secondary_item_2 = make_item(properties={"is_stock_item": 1, "valuation_rate": 20}).name + secondary_items = [secondary_item_1, secondary_item_2] service_items = [ { @@ -1187,13 +1188,14 @@ class TestSubcontractingReceipt(ERPNextTestSuite): ) for idx, item in enumerate(bom.items): item.qty = 1 * (idx + 1) - for idx, item in enumerate(scrap_items): + for idx, item in enumerate(secondary_items): bom.append( - "scrap_items", + "secondary_items", { "item_code": item, "stock_qty": 1 * (idx + 1), "rate": 10 * (idx + 1), + "is_legacy": 1, }, ) bom.save() @@ -1216,12 +1218,13 @@ class TestSubcontractingReceipt(ERPNextTestSuite): # Create Subcontracting Receipt scr = make_subcontracting_receipt(sco.name) scr.save() - scr.get_scrap_items() + scr.get_secondary_items() - # Test - 1: Scrap Items should be fetched from BOM in items table with `is_scrap_item` = 1 - scr_scrap_items = set([item.item_code for item in scr.items if item.is_scrap_item]) + scr_secondary_items = set( + [item.item_code for item in scr.items if item.type or item.is_legacy_scrap_item] + ) self.assertEqual(len(scr.items), 3) # 1 FG Item + 2 Scrap Items - self.assertEqual(scr_scrap_items, set(scrap_items)) + self.assertEqual(scr_secondary_items, set(secondary_items)) scr.submit() diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json index 9c1f8e60946..b6d07f66b98 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -8,9 +8,10 @@ "engine": "InnoDB", "field_order": [ "item_code", + "is_legacy_scrap_item", + "type", "column_break_2", "item_name", - "is_scrap_item", "section_break_4", "description", "brand", @@ -22,6 +23,7 @@ "qty", "rejected_qty", "returned_qty", + "process_loss_qty", "col_break2", "stock_uom", "conversion_factor", @@ -33,7 +35,7 @@ "rm_cost_per_qty", "service_cost_per_qty", "additional_cost_per_qty", - "scrap_cost_per_qty", + "secondary_items_cost_per_qty", "rm_supp_cost", "warehouse_and_reference", "warehouse", @@ -144,7 +146,7 @@ "default": "0", "fieldname": "received_qty", "fieldtype": "Float", - "label": "Received Quantity", + "label": "Qty (As per BOM)", "no_copy": 1, "print_hide": 1, "print_width": "100px", @@ -157,22 +159,23 @@ "fieldname": "qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Accepted Quantity", + "label": "Accepted Qty", "no_copy": 1, "print_width": "100px", + "read_only_depends_on": "eval:doc.type || doc.is_legacy_scrap_item", "width": "100px" }, { "columns": 1, - "depends_on": "eval: !parent.is_return", + "depends_on": "eval:!parent.is_return && !doc.type && !doc.is_legacy_scrap_item", "fieldname": "rejected_qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Rejected Quantity", + "label": "Rejected Qty", "no_copy": 1, "print_hide": 1, "print_width": "100px", - "read_only_depends_on": "eval: doc.is_scrap_item", + "read_only_depends_on": "eval:doc.type || doc.is_legacy_scrap_item", "width": "100px" }, { @@ -181,6 +184,7 @@ "print_hide": 1 }, { + "fetch_from": "item_code.stock_uom", "fieldname": "stock_uom", "fieldtype": "Link", "label": "Stock UOM", @@ -230,7 +234,7 @@ }, { "default": "0", - "depends_on": "eval: !doc.is_scrap_item", + "depends_on": "eval:!doc.type && !doc.is_legacy_scrap_item", "fieldname": "rm_cost_per_qty", "fieldtype": "Currency", "label": "Raw Material Cost Per Qty", @@ -240,7 +244,7 @@ }, { "default": "0", - "depends_on": "eval: !doc.is_scrap_item", + "depends_on": "eval:!doc.type && !doc.is_legacy_scrap_item", "fieldname": "service_cost_per_qty", "fieldtype": "Currency", "label": "Service Cost Per Qty", @@ -250,7 +254,7 @@ }, { "default": "0", - "depends_on": "eval: !doc.is_scrap_item", + "depends_on": "eval:!doc.type && !doc.is_legacy_scrap_item", "fieldname": "additional_cost_per_qty", "fieldtype": "Currency", "label": "Additional Cost Per Qty", @@ -274,7 +278,7 @@ "width": "100px" }, { - "depends_on": "eval: !parent.is_return", + "depends_on": "eval: !parent.is_return && !doc.type && !doc.is_legacy_scrap_item", "fieldname": "rejected_warehouse", "fieldtype": "Link", "ignore_user_permissions": 1, @@ -283,11 +287,10 @@ "options": "Warehouse", "print_hide": 1, "print_width": "100px", - "read_only_depends_on": "eval: doc.is_scrap_item", "width": "100px" }, { - "depends_on": "eval:!doc.__islocal", + "depends_on": "eval:!doc.__islocal && !doc.type && !doc.is_legacy_scrap_item", "fieldname": "quality_inspection", "fieldtype": "Link", "label": "Quality Inspection", @@ -369,7 +372,7 @@ "no_copy": 1, "options": "BOM", "print_hide": 1, - "read_only_depends_on": "eval: doc.is_scrap_item" + "read_only_depends_on": "eval:doc.type || doc.is_legacy_scrap_item" }, { "fetch_from": "item_code.brand", @@ -496,7 +499,7 @@ "print_hide": 1 }, { - "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", + "depends_on": "eval:(doc.use_serial_batch_fields === 0 || doc.docstatus === 1) && !doc.type && !doc.is_legacy_scrap_item", "fieldname": "rejected_serial_and_batch_bundle", "fieldtype": "Link", "label": "Rejected Serial and Batch Bundle", @@ -504,26 +507,6 @@ "options": "Serial and Batch Bundle", "print_hide": 1 }, - { - "default": "0", - "depends_on": "eval: !doc.bom", - "fieldname": "is_scrap_item", - "fieldtype": "Check", - "label": "Is Scrap Item", - "no_copy": 1, - "print_hide": 1, - "read_only_depends_on": "eval: doc.bom" - }, - { - "default": "0", - "depends_on": "eval: !doc.is_scrap_item", - "fieldname": "scrap_cost_per_qty", - "fieldtype": "Float", - "label": "Scrap Cost Per Qty", - "no_copy": 1, - "non_negative": 1, - "read_only": 1 - }, { "fieldname": "reference_name", "fieldtype": "Data", @@ -553,6 +536,7 @@ }, { "default": "0", + "depends_on": "eval:doc.bom", "fieldname": "include_exploded_items", "fieldtype": "Check", "label": "Include Exploded Items", @@ -580,7 +564,7 @@ "label": "Add Serial / Batch Bundle" }, { - "depends_on": "eval:doc.use_serial_batch_fields === 0", + "depends_on": "eval:doc.use_serial_batch_fields === 0 && !doc.type && !doc.is_legacy_scrap_item", "fieldname": "add_serial_batch_for_rejected_qty", "fieldtype": "Button", "label": "Add Serial / Batch No (Rejected Qty)" @@ -594,6 +578,7 @@ "search_index": 1 }, { + "depends_on": "eval:!doc.type && !doc.is_legacy_scrap_item", "fieldname": "landed_cost_voucher_amount", "fieldtype": "Currency", "label": "Landed Cost Voucher Amount", @@ -609,13 +594,48 @@ "fieldtype": "Link", "label": "Service Expense Account", "options": "Account" + }, + { + "fieldname": "type", + "fieldtype": "Select", + "label": "Type", + "no_copy": 1, + "options": "\nCo-Product\nBy-Product\nScrap\nAdditional Finished Good", + "print_hide": 1, + "read_only": 1 + }, + { + "default": "0", + "depends_on": "eval:!doc.type && !doc.is_legacy_scrap_item", + "fieldname": "secondary_items_cost_per_qty", + "fieldtype": "Currency", + "label": "Secondary Items Cost Per Qty", + "no_copy": 1, + "non_negative": 1, + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "default": "0", + "depends_on": "is_legacy_scrap_item", + "fieldname": "is_legacy_scrap_item", + "fieldtype": "Check", + "label": "Is Legacy Scrap Item", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "process_loss_qty", + "fieldtype": "Float", + "label": "Process Loss Qty", + "non_negative": 1 } ], "grid_page_length": 50, "idx": 1, "istable": 1, "links": [], - "modified": "2025-09-26 12:00:38.877638", + "modified": "2026-03-09 15:11:16.977539", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Item", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py index e916a90462f..c6233b841a2 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py @@ -25,7 +25,7 @@ class SubcontractingReceiptItem(Document): expense_account: DF.Link | None image: DF.Attach | None include_exploded_items: DF.Check - is_scrap_item: DF.Check + is_legacy_scrap_item: DF.Check item_code: DF.Link item_name: DF.Data | None job_card: DF.Link | None @@ -36,6 +36,7 @@ class SubcontractingReceiptItem(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data + process_loss_qty: DF.Float project: DF.Link | None purchase_order: DF.Link | None purchase_order_item: DF.Data | None @@ -52,7 +53,7 @@ class SubcontractingReceiptItem(Document): rm_cost_per_qty: DF.Currency rm_supp_cost: DF.Currency schedule_date: DF.Date | None - scrap_cost_per_qty: DF.Float + secondary_items_cost_per_qty: DF.Currency serial_and_batch_bundle: DF.Link | None serial_no: DF.SmallText | None service_cost_per_qty: DF.Currency @@ -61,6 +62,7 @@ class SubcontractingReceiptItem(Document): subcontracting_order: DF.Link | None subcontracting_order_item: DF.Data | None subcontracting_receipt_item: DF.Data | None + type: DF.Literal["", "Co-Product", "By-Product", "Scrap", "Additional Finished Good"] use_serial_batch_fields: DF.Check warehouse: DF.Link | None # end: auto-generated types From 5a7d0d2765c199d7ddd7b60c7d5c7f02dcee1ea8 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 31 Mar 2026 17:31:36 +0530 Subject: [PATCH 24/79] fix: hide fields related to track Semi-Finished Goods if feature has disabled (cherry picked from commit 399faf0ced853faf5ab3d087b7332da11bdea6d8) --- erpnext/manufacturing/doctype/bom/bom.js | 47 ++++++++++++++++++- .../doctype/bom_operation/bom_operation.json | 3 +- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 1dc64997198..0ff10f8383a 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -19,6 +19,21 @@ frappe.ui.form.on("BOM", { }; }); + frm.set_query("workstation", "operations", function (doc, cdt, cdn) { + let row = locals[cdt][cdn]; + let filters = { + disabled: 0, + }; + + if (row.workstation_type) { + filters.workstation_type = row.workstation_type; + } + + return { + filters: filters, + }; + }); + frm.set_query("operation", "items", function () { if (!frm.doc.operations?.length) { frappe.throw(__("Please add Operations first.")); @@ -123,7 +138,16 @@ frappe.ui.form.on("BOM", { }, toggle_fields_for_semi_finished_goods(frm) { - let fields = ["finished_good", "finished_good_qty", "bom_no"]; + let fields = [ + "finished_good", + "finished_good_qty", + "bom_no", + "skip_material_transfer", + "wip_warehouse", + "fg_warehouse", + "is_subcontracted", + "is_final_finished_good", + ]; fields.forEach((field) => { frm.fields_dict["operations"].grid.update_docfield_property( @@ -131,9 +155,21 @@ frappe.ui.form.on("BOM", { "read_only", !frm.doc.track_semi_finished_goods ); + + frm.fields_dict["operations"].grid.update_docfield_property( + field, + "in_list_view", + frm.doc.track_semi_finished_goods + ); + + frm.fields_dict["operations"].grid.update_docfield_property( + field, + "hidden", + !frm.doc.track_semi_finished_goods + ); }); - refresh_field("operations"); + frm.fields_dict["operations"].grid.reset_grid(); }, with_operations: function (frm) { @@ -173,6 +209,8 @@ frappe.ui.form.on("BOM", { refresh(frm) { frm.toggle_enable("item", frm.doc.__islocal); + frm.trigger("toggle_fields_for_semi_finished_goods"); + frm.set_indicator_formatter("item_code", function (doc) { if (doc.original_item) { return doc.item_code != doc.original_item ? "orange" : ""; @@ -864,6 +902,11 @@ frappe.ui.form.on("BOM Operation", "workstation", function (frm, cdt, cdn) { frappe.ui.form.on("BOM Operation", "workstation_type", function (frm, cdt, cdn) { var d = locals[cdt][cdn]; if (!d.workstation_type) return; + + if (d.workstation) { + frappe.model.set_value(cdt, cdn, "workstation", ""); + } + frappe.call({ method: "frappe.client.get", args: { diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json index ad33af6dfff..11c704649a3 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -55,7 +55,6 @@ }, { "columns": 2, - "depends_on": "eval:!doc.workstation_type", "fieldname": "workstation", "fieldtype": "Link", "in_list_view": 1, @@ -297,7 +296,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-02-17 15:33:28.495850", + "modified": "2026-03-31 17:09:48.771834", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Operation", From 5aaca83fe460500c73de4d3194891898580d72d3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 08:11:20 +0000 Subject: [PATCH 25/79] fix: remove reference in serial/batch when document is cancelled (backport #53979) (#53989) --- .../serial_and_batch_bundle.py | 38 +++++++++++++++++++ .../test_serial_and_batch_bundle.py | 32 ++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 45790ed89c4..5220c2b6274 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -1489,6 +1489,7 @@ class SerialandBatchBundle(Document): def on_cancel(self): self.validate_voucher_no_docstatus() self.validate_batch_quantity() + self.remove_source_document_no() def validate_batch_quantity(self): if not self.has_batch_no: @@ -1507,6 +1508,43 @@ class SerialandBatchBundle(Document): if flt(available_qty, precision) < 0: self.throw_negative_batch(d.batch_no, available_qty, precision) + def remove_source_document_no(self): + if not self.has_serial_no and not self.has_batch_no: + return + + if self.total_qty <= 0: + return + + if self.has_serial_no: + serial_nos = [d.serial_no for d in self.entries if d.serial_no] + sn_table = frappe.qb.DocType("Serial No") + ( + frappe.qb.update(sn_table) + .set(sn_table.reference_doctype, None) + .set(sn_table.reference_name, None) + .set(sn_table.posting_date, None) + .where( + (sn_table.name.isin(serial_nos)) + & (sn_table.reference_doctype == self.voucher_type) + & (sn_table.reference_name == self.voucher_no) + & (sn_table.posting_date == getdate(self.posting_datetime)) + ) + ).run() + + if self.has_batch_no: + batch_nos = [d.batch_no for d in self.entries if d.batch_no] + batch_table = frappe.qb.DocType("Batch") + ( + frappe.qb.update(batch_table) + .set(batch_table.reference_doctype, None) + .set(batch_table.reference_name, None) + .where( + (batch_table.name.isin(batch_nos)) + & (batch_table.reference_doctype == self.voucher_type) + & (batch_table.reference_name == self.voucher_no) + ) + ).run() + def throw_negative_batch(self, batch_no, available_qty, precision, posting_datetime=None): from erpnext.stock.stock_ledger import NegativeStockError diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index c6929fe4cdb..ab360d8133b 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -1077,6 +1077,38 @@ class TestSerialandBatchBundle(ERPNextTestSuite): self.assertTrue(bundle_doc.docstatus == 0) self.assertRaises(frappe.ValidationError, bundle_doc.submit) + def test_reference_voucher_on_cancel(self): + """ + When a source document is cancelled, the reference voucher field + in the respective serial or batch document should be nullified. + """ + + item_code = make_item( + "Serial Item", + properties={ + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "SERIAL.#####", + }, + ).name + + se = make_stock_entry( + item_code=item_code, + qty=1, + target="_Test Warehouse - _TC", + ) + serial_no = get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)[0] + self.assertEqual(frappe.get_value("Serial No", serial_no, "reference_name"), se.name) + + se.cancel() + self.assertIsNone(frappe.get_value("Serial No", serial_no, "reference_name")) + + se1 = frappe.copy_doc(se, ignore_no_copy=False) + se1.items[0].serial_no = serial_no + se1.submit() + + self.assertEqual(frappe.get_value("Serial No", serial_no, "reference_name"), se1.name) + def get_batch_from_bundle(bundle): from erpnext.stock.serial_batch_bundle import get_batch_nos From fc5a04db2e92e92f05965b24ac5289ab3fa93163 Mon Sep 17 00:00:00 2001 From: kavin-114 Date: Thu, 2 Apr 2026 23:27:24 +0530 Subject: [PATCH 26/79] fix(stock): update stock queue in SABE for return entries (cherry picked from commit 0af8077bcc828422593dfa51b99bcac249a8bbed) --- .../serial_and_batch_bundle.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 5220c2b6274..5daab368156 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -410,6 +410,25 @@ class SerialandBatchBundle(Document): def set_valuation_rate_for_return_entry(self, return_against, row, save=False, prev_sle=None): if valuation_details := self.get_valuation_rate_for_return_entry(return_against): + from erpnext.stock.utils import get_valuation_method + + valuation_method = get_valuation_method(self.item_code, self.company) + + stock_queue = [] + non_batchwise_batches = [] + if not self.has_serial_no and valuation_method == "FIFO": + non_batchwise_batches = frappe.get_all( + "Batch", + filters={ + "name": ("in", [d.batch_no for d in self.entries if d.batch_no]), + "use_batchwise_valuation": 0, + }, + pluck="name", + ) + + if non_batchwise_batches and prev_sle and prev_sle.stock_queue: + stock_queue = parse_json(prev_sle.stock_queue) + for row in self.entries: if valuation_details: self.validate_returned_serial_batch_no(return_against, row, valuation_details) @@ -431,11 +450,25 @@ class SerialandBatchBundle(Document): row.incoming_rate = flt(valuation_rate) row.stock_value_difference = flt(row.qty) * flt(row.incoming_rate) + if ( + non_batchwise_batches + and row.batch_no in non_batchwise_batches + and row.incoming_rate is not None + ): + if flt(row.qty) > 0: + stock_queue.append([row.qty, row.incoming_rate]) + elif flt(row.qty) < 0: + stock_queue = FIFOValuation(stock_queue) + stock_queue.remove_stock(qty=abs(row.qty)) + stock_queue = stock_queue.state + row.stock_queue = json.dumps(stock_queue) + if save: row.db_set( { "incoming_rate": row.incoming_rate, "stock_value_difference": row.stock_value_difference, + "stock_queue": row.get("stock_queue"), } ) From d3f1bfc628b1651797c22d26adcbaa07a201b388 Mon Sep 17 00:00:00 2001 From: kavin-114 Date: Fri, 3 Apr 2026 00:02:42 +0530 Subject: [PATCH 27/79] test(stock): add unit test to update stock queue for return (cherry picked from commit e537896df882f81fcabd999a9aa74f1cd1aa7462) --- .../test_serial_and_batch_bundle.py | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index ab360d8133b..37d4a45f954 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -1109,6 +1109,173 @@ class TestSerialandBatchBundle(ERPNextTestSuite): self.assertEqual(frappe.get_value("Serial No", serial_no, "reference_name"), se1.name) + def test_stock_queue_for_return_entry_with_non_batchwise_valuation(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + + batch_item_code = "Old Batch Return Queue Test" + make_item( + batch_item_code, + { + "has_batch_no": 1, + "batch_number_series": "TEST-RET-Q-.#####", + "create_new_batch": 1, + "is_stock_item": 1, + "valuation_method": "FIFO", + }, + ) + + batch_id = "Old Batch Return Queue 1" + if not frappe.db.exists("Batch", batch_id): + batch_doc = frappe.get_doc( + { + "doctype": "Batch", + "batch_id": batch_id, + "item": batch_item_code, + "use_batchwise_valuation": 0, + } + ).insert(ignore_permissions=True) + + batch_doc.db_set( + { + "use_batchwise_valuation": 0, + "batch_qty": 0, + } + ) + + # Create initial stock with FIFO queue: [[10, 100], [20, 200]] + make_stock_entry( + item_code=batch_item_code, + target="_Test Warehouse - _TC", + qty=10, + rate=100, + batch_no=batch_id, + use_serial_batch_fields=True, + ) + + make_stock_entry( + item_code=batch_item_code, + target="_Test Warehouse - _TC", + qty=20, + rate=200, + batch_no=batch_id, + use_serial_batch_fields=True, + ) + + # Purchase Receipt: inward 5 @ 300 + pr = make_purchase_receipt( + item_code=batch_item_code, + warehouse="_Test Warehouse - _TC", + qty=5, + rate=300, + batch_no=batch_id, + use_serial_batch_fields=True, + ) + + sle = frappe.db.get_value( + "Stock Ledger Entry", + {"item_code": batch_item_code, "is_cancelled": 0, "voucher_no": pr.name}, + ["stock_queue"], + as_dict=True, + ) + + # Stock queue should now be [[10, 100], [20, 200], [5, 300]] + self.assertEqual(json.loads(sle.stock_queue), [[10, 100], [20, 200], [5, 300]]) + + # Purchase Return: return 5 against the PR + return_pr = make_return_doc("Purchase Receipt", pr.name) + return_pr.submit() + + return_sle = frappe.db.get_value( + "Stock Ledger Entry", + {"item_code": batch_item_code, "is_cancelled": 0, "voucher_no": return_pr.name}, + ["stock_queue"], + as_dict=True, + ) + + # Stock queue should have 5 removed via FIFO from [[10, 100], [20, 200], [5, 300]] + # FIFO removes from front: [10, 100] -> [5, 100], rest unchanged + self.assertEqual(json.loads(return_sle.stock_queue), [[5, 100], [20, 200], [5, 300]]) + + def test_stock_queue_for_return_entry_with_empty_fifo_queue(self): + """Credit note (sales return) against empty FIFO queue should still rebuild stock_queue.""" + from erpnext.controllers.sales_and_purchase_return import make_return_doc + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + batch_item_code = "Old Batch Empty Queue Test" + make_item( + batch_item_code, + { + "has_batch_no": 1, + "batch_number_series": "TEST-EQ-.#####", + "create_new_batch": 1, + "is_stock_item": 1, + "valuation_method": "FIFO", + }, + ) + + batch_id = "Old Batch Empty Queue 1" + if not frappe.db.exists("Batch", batch_id): + batch_doc = frappe.get_doc( + { + "doctype": "Batch", + "batch_id": batch_id, + "item": batch_item_code, + "use_batchwise_valuation": 0, + } + ).insert(ignore_permissions=True) + + batch_doc.db_set( + { + "use_batchwise_valuation": 0, + "batch_qty": 0, + } + ) + + # Inward 10 @ 100, then outward all 10 to empty the queue + make_stock_entry( + item_code=batch_item_code, + target="_Test Warehouse - _TC", + qty=10, + rate=100, + batch_no=batch_id, + use_serial_batch_fields=True, + ) + + dn = create_delivery_note( + item_code=batch_item_code, + warehouse="_Test Warehouse - _TC", + qty=10, + rate=150, + batch_no=batch_id, + use_serial_batch_fields=True, + ) + + # Verify queue is empty after full outward + sle = frappe.db.get_value( + "Stock Ledger Entry", + {"item_code": batch_item_code, "is_cancelled": 0, "voucher_no": dn.name}, + ["stock_queue"], + as_dict=True, + ) + self.assertFalse(json.loads(sle.stock_queue or "[]")) + + # Sales return (credit note): 5 items come back at original rate 100 + return_dn = make_return_doc("Delivery Note", dn.name) + for row in return_dn.items: + row.qty = -5 + return_dn.save().submit() + + return_sle = frappe.db.get_value( + "Stock Ledger Entry", + {"item_code": batch_item_code, "is_cancelled": 0, "voucher_no": return_dn.name}, + ["stock_queue"], + as_dict=True, + ) + + # Stock queue should have the returned stock: [[5, 100]] + self.assertEqual(json.loads(return_sle.stock_queue), [[5, 100]]) + def get_batch_from_bundle(bundle): from erpnext.stock.serial_batch_bundle import get_batch_nos From 01610b2fa7a0a772edf8a76399ebb0161012b3b5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 5 Apr 2026 07:19:23 +0000 Subject: [PATCH 28/79] fix(manufacturing): handle null cur_dialog in BOM work order dialog (backport #54011) (#54015) --- erpnext/manufacturing/doctype/bom/bom.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 0ff10f8383a..32c543703bc 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -407,6 +407,7 @@ frappe.ui.form.on("BOM", { reqd: 1, default: 1, onchange: () => { + if (!cur_dialog) return; const { quantity, items: rm } = frm.doc; const variant_items_map = rm.reduce((acc, item) => { acc[item.item_code] = item.qty; From bd67ef8d2652ea09155f30db3015eb6dc853fcfb Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sat, 4 Apr 2026 13:22:51 +0530 Subject: [PATCH 29/79] fix: screen freezes if consumed qty set in SCR (cherry picked from commit dd7be2b370d958a6429dad1b867a151e1af97908) --- erpnext/public/js/utils.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 06ccc25a94e..935cae7f571 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -21,6 +21,10 @@ $.extend(erpnext, { toggle_serial_batch_fields(frm) { let hide_fields = cint(frappe.user_defaults?.enable_serial_and_batch_no_for_item) === 0 ? 1 : 0; + if (!hide_fields) { + return; + } + let fields = ["serial_and_batch_bundle", "use_serial_batch_fields", "serial_no", "batch_no"]; if ( @@ -60,6 +64,12 @@ $.extend(erpnext, { child_name = "stock_items"; } + let sn_field = frm.fields_dict[child_name].grid.docfields.filter((d) => d.fieldname === "serial_no"); + if (sn_field?.length && sn_field[0].hidden === 1) { + // Already field is hidden + return; + } + fields.forEach((field) => { if (frm.fields_dict[child_name].get_field(field)) { frm.fields_dict[child_name].grid.update_docfield_property(field, "hidden", hide_fields); @@ -72,7 +82,11 @@ $.extend(erpnext, { if ( frm.doc.doctype === "Subcontracting Receipt" && - !["add_serial_batch_for_rejected_qty", "rejected_serial_and_batch_bundle"].includes(field) + ![ + "add_serial_batch_for_rejected_qty", + "rejected_serial_and_batch_bundle", + "rejected_serial_no", + ].includes(field) ) { frm.fields_dict["supplied_items"].grid.update_docfield_property( field, @@ -85,12 +99,14 @@ $.extend(erpnext, { "in_list_view", hide_fields ? 0 : 1 ); - - frm.fields_dict["supplied_items"].grid.reset_grid(); } } }); + if (frm.doc.doctype === "Subcontracting Receipt") { + frm.fields_dict["supplied_items"].grid.reset_grid(); + } + frm.fields_dict[child_name].grid.reset_grid(); }, From 1c0956c6e24230cb05001702c6a43c8733214d4e Mon Sep 17 00:00:00 2001 From: MochaMind Date: Sun, 5 Apr 2026 19:32:11 +0530 Subject: [PATCH 30/79] chore: update POT file (#54017) --- erpnext/locale/main.pot | 1537 +++++++++++++++++++++------------------ 1 file changed, 837 insertions(+), 700 deletions(-) diff --git a/erpnext/locale/main.pot b/erpnext/locale/main.pot index 21fb2c6830e..2ce76e47917 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-29 09:46+0000\n" -"PO-Revision-Date: 2026-03-29 09:46+0000\n" +"POT-Creation-Date: 2026-04-05 09:48+0000\n" +"PO-Revision-Date: 2026-04-05 09:48+0000\n" "Last-Translator: hello@frappe.io\n" "Language-Team: hello@frappe.io\n" "MIME-Version: 1.0\n" @@ -16,7 +16,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.16.0\n" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1520 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1558 msgid "" "\n" "\t\t\tThe Batch {0} of an item {1} has negative stock in the warehouse {2}{3}.\n" @@ -149,6 +149,11 @@ msgstr "" msgid "% Completed" msgstr "" +#. Label of the cost_allocation_per (Percent) field in DocType 'BOM' +#: erpnext/manufacturing/doctype/bom/bom.json +msgid "% Cost Allocation" +msgstr "" + #. Label of the per_delivered (Percent) field in DocType 'Pick List' #. Label of the per_delivered (Percent) field in DocType 'Subcontracting Inward #. Order' @@ -157,7 +162,7 @@ msgstr "" msgid "% Delivered" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:1000 +#: erpnext/manufacturing/doctype/bom/bom.js:1017 #, python-format msgid "% Finished Item Quantity" msgstr "" @@ -969,7 +974,7 @@ msgstr "" msgid "A - C" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:353 +#: erpnext/selling/doctype/customer/customer.py:354 msgid "A Customer Group exists with same name please change the Customer name or rename the Customer Group" msgstr "" @@ -1165,7 +1170,9 @@ msgid "Acceptance Criteria Value" msgstr "" #. Label of the qty (Float) field in DocType 'Purchase Invoice Item' +#. Label of the qty (Float) field in DocType 'Subcontracting Receipt Item' #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +#: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json msgid "Accepted Qty" msgstr "" @@ -1177,10 +1184,8 @@ msgid "Accepted Qty in Stock UOM" msgstr "" #. Label of the qty (Float) field in DocType 'Purchase Receipt Item' -#. Label of the qty (Float) field in DocType 'Subcontracting Receipt Item' -#: erpnext/public/js/controllers/transaction.js:2919 +#: erpnext/public/js/controllers/transaction.js:2923 #: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json -#: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json msgid "Accepted Quantity" msgstr "" @@ -1848,12 +1853,12 @@ msgstr "" msgid "Accounting Entry for Asset" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1959 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1979 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1964 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1984 msgid "Accounting Entry for LCV in Stock Entry {0}" msgstr "" -#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:873 +#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:910 msgid "Accounting Entry for Landed Cost Voucher for SCR {0}" msgstr "" @@ -1873,9 +1878,9 @@ msgstr "" #: erpnext/controllers/stock_controller.py:727 #: erpnext/controllers/stock_controller.py:744 #: 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 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1909 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1923 +#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:745 msgid "Accounting Entry for Stock" msgstr "" @@ -2513,7 +2518,7 @@ msgstr "" msgid "Add / Edit Prices" msgstr "" -#: erpnext/accounts/report/general_ledger/general_ledger.js:207 +#: erpnext/accounts/report/general_ledger/general_ledger.js:208 msgid "Add Columns in Transaction Currency" msgstr "" @@ -2604,7 +2609,7 @@ msgid "Add Quote" msgstr "" #. Label of the add_raw_materials (Button) field in DocType 'BOM Operation' -#: erpnext/manufacturing/doctype/bom/bom.js:1028 +#: erpnext/manufacturing/doctype/bom/bom.js:1045 #: erpnext/manufacturing/doctype/bom_operation/bom_operation.json msgid "Add Raw Materials" msgstr "" @@ -2689,7 +2694,7 @@ msgid "Add details" msgstr "" #: erpnext/stock/doctype/pick_list/pick_list.js:86 -#: erpnext/stock/doctype/pick_list/pick_list.py:905 +#: erpnext/stock/doctype/pick_list/pick_list.py:932 msgid "Add items in the Item Locations table" msgstr "" @@ -2880,7 +2885,7 @@ msgstr "" msgid "Additional Discount Amount (Company Currency)" msgstr "" -#: erpnext/controllers/taxes_and_totals.py:796 +#: erpnext/controllers/taxes_and_totals.py:833 msgid "Additional Discount Amount ({discount_amount}) cannot exceed the total before such discount ({total_before_discount})" msgstr "" @@ -2917,6 +2922,21 @@ msgstr "" msgid "Additional Discount Percentage" msgstr "" +#. Option for the 'Type' (Select) field in DocType 'BOM Secondary Item' +#. Option for the 'Type' (Select) field in DocType 'Job Card Secondary Item' +#. Option for the 'Type' (Select) field in DocType 'Stock Entry Detail' +#. Option for the 'Type' (Select) field in DocType 'Subcontracting Inward Order +#. Secondary Item' +#. Option for the 'Type' (Select) field in DocType 'Subcontracting Receipt +#. Item' +#: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json +#: erpnext/manufacturing/doctype/job_card_secondary_item/job_card_secondary_item.json +#: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +#: erpnext/subcontracting/doctype/subcontracting_inward_order_secondary_item/subcontracting_inward_order_secondary_item.json +#: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +msgid "Additional Finished Good" +msgstr "" + #. Label of the addtional_info (Section Break) field in DocType 'Journal Entry' #. Label of the additional_info_section (Section Break) field in DocType #. 'Purchase Invoice' @@ -2960,7 +2980,7 @@ msgstr "" msgid "Additional Information updated successfully." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:811 +#: erpnext/manufacturing/doctype/work_order/work_order.js:812 msgid "Additional Material Transfer" msgstr "" @@ -2997,7 +3017,7 @@ msgstr "" msgid "Additional information regarding the customer." msgstr "" -#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:590 +#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:627 msgid "Additional {0} {1} of item {2} required as per BOM to complete this transaction" msgstr "" @@ -3264,7 +3284,7 @@ msgstr "" msgid "Advance amount" msgstr "" -#: erpnext/controllers/taxes_and_totals.py:933 +#: erpnext/controllers/taxes_and_totals.py:970 msgid "Advance amount cannot be greater than {0} {1}" msgstr "" @@ -3480,7 +3500,7 @@ msgstr "" msgid "Age (Days)" msgstr "" -#: erpnext/stock/report/stock_ageing/stock_ageing.py:220 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:221 msgid "Age ({0})" msgstr "" @@ -3600,7 +3620,7 @@ msgstr "" msgid "All Activities HTML" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:369 +#: erpnext/manufacturing/doctype/bom/bom.py:390 msgid "All BOMs" msgstr "" @@ -3745,11 +3765,11 @@ msgstr "" msgid "All items have already been received" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3111 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3136 msgid "All items have already been transferred for this Work Order." msgstr "" -#: erpnext/public/js/controllers/transaction.js:3028 +#: erpnext/public/js/controllers/transaction.js:3032 msgid "All items in this document already have a linked Quality Inspection." msgstr "" @@ -3771,7 +3791,7 @@ msgstr "" msgid "All the items have been already returned." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1195 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1196 msgid "All the required items (raw materials) will be fetched from BOM and populated in this table. Here you can also change the Source Warehouse for any item. And during the production, you can track transferred raw materials from this table." msgstr "" @@ -3978,16 +3998,6 @@ msgstr "" msgid "Allow Lead Duplication based on Emails" msgstr "" -#. Label of the allow_from_dn (Check) field in DocType 'Stock Settings' -#: erpnext/stock/doctype/stock_settings/stock_settings.json -msgid "Allow Material Transfer from Delivery Note to Sales Invoice" -msgstr "" - -#. Label of the allow_from_pr (Check) field in DocType 'Stock Settings' -#: erpnext/stock/doctype/stock_settings/stock_settings.json -msgid "Allow Material Transfer from Purchase Receipt to Purchase Invoice" -msgstr "" - #: erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.js:9 msgid "Allow Multiple Material Consumption" msgstr "" @@ -4005,8 +4015,8 @@ msgstr "" #: erpnext/stock/doctype/item/item.json #: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json #: erpnext/stock/doctype/stock_settings/stock_settings.json -#: erpnext/stock/doctype/stock_settings/stock_settings.py:217 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:229 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:215 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:227 msgid "Allow Negative Stock" msgstr "" @@ -4308,7 +4318,7 @@ msgstr "" msgid "Allows users to submit Supplier Quotations with zero quantity. Useful when rates are fixed but the quantities are not. Eg. Rate Contracts." msgstr "" -#: erpnext/stock/doctype/pick_list/pick_list.py:1048 +#: erpnext/stock/doctype/pick_list/pick_list.py:1081 msgid "Already Picked" msgstr "" @@ -4324,10 +4334,10 @@ msgstr "" msgid "Also you can't switch back to FIFO after setting the valuation method to Moving Average for this item." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:250 +#: erpnext/manufacturing/doctype/bom/bom.js:288 #: erpnext/manufacturing/doctype/work_order/work_order.js:165 #: erpnext/manufacturing/doctype/work_order/work_order.js:180 -#: erpnext/public/js/utils.js:567 +#: erpnext/public/js/utils.js:571 #: erpnext/stock/doctype/stock_entry/stock_entry.js:288 msgid "Alternate Item" msgstr "" @@ -4429,7 +4439,6 @@ msgstr "" #. Label of the amount (Currency) field in DocType 'BOM Creator Item' #. Label of the amount (Currency) field in DocType 'BOM Explosion Item' #. Label of the amount (Currency) field in DocType 'BOM Item' -#. Label of the amount (Currency) field in DocType 'BOM Scrap Item' #. Label of the amount (Currency) field in DocType 'Work Order Item' #. Option for the 'Margin Type' (Select) field in DocType 'Quotation Item' #. Label of the amount (Currency) field in DocType 'Quotation Item' @@ -4524,9 +4533,8 @@ msgstr "" #: erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json #: erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json #: erpnext/manufacturing/doctype/bom_item/bom_item.json -#: erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json #: erpnext/manufacturing/doctype/work_order_item/work_order_item.json -#: erpnext/public/js/controllers/transaction.js:506 +#: erpnext/public/js/controllers/transaction.js:510 #: erpnext/selling/doctype/quotation/quotation.js:316 #: erpnext/selling/doctype/quotation_item/quotation_item.json #: erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -4757,7 +4765,7 @@ msgstr "" msgid "Analytical Accounting" msgstr "" -#: erpnext/public/js/utils.js:164 +#: erpnext/public/js/utils.js:168 msgid "Annual Billing: {0}" msgstr "" @@ -5231,7 +5239,7 @@ msgstr "" msgid "As there are existing submitted transactions against item {0}, you can not change the value of {1}." msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:242 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:240 msgid "As there are reserved stock, you cannot disable {0}." msgstr "" @@ -5239,12 +5247,12 @@ msgstr "" 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:1832 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1834 msgid "As there are sufficient raw materials, Material Request is not required for Warehouse {0}." msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:216 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:228 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:214 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:226 msgid "As {0} is enabled, you can not enable {1}." msgstr "" @@ -5775,7 +5783,7 @@ msgstr "" msgid "Asset {0} must be submitted" msgstr "" -#: erpnext/controllers/buying_controller.py:1030 +#: erpnext/controllers/buying_controller.py:1034 msgid "Asset {assets_link} created for {item_code}" msgstr "" @@ -5813,11 +5821,11 @@ msgstr "" msgid "Assets Setup" msgstr "" -#: erpnext/controllers/buying_controller.py:1048 +#: erpnext/controllers/buying_controller.py:1052 msgid "Assets not created for {item_code}. You will have to create asset manually." msgstr "" -#: erpnext/controllers/buying_controller.py:1035 +#: erpnext/controllers/buying_controller.py:1039 msgid "Assets {assets_link} created for {item_code}" msgstr "" @@ -5841,11 +5849,11 @@ msgstr "" msgid "Associate" msgstr "" -#: erpnext/stock/doctype/pick_list/pick_list.py:134 +#: erpnext/stock/doctype/pick_list/pick_list.py:135 msgid "At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} for the batch {4} in the warehouse {5}. Please restock the item." msgstr "" -#: erpnext/stock/doctype/pick_list/pick_list.py:159 +#: erpnext/stock/doctype/pick_list/pick_list.py:160 msgid "At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} in the warehouse {4}." msgstr "" @@ -6184,7 +6192,7 @@ msgstr "" msgid "Auto Reserve Stock for Sales Order on Purchase" msgstr "" -#: erpnext/accounts/doctype/accounts_settings/accounts_settings.py:185 +#: erpnext/accounts/doctype/accounts_settings/accounts_settings.py:186 msgid "Auto Tax Settings Error" msgstr "" @@ -6311,10 +6319,10 @@ msgstr "" #: erpnext/manufacturing/doctype/workstation/workstation.js:505 #: 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/public/js/utils.js:631 #: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json #: erpnext/stock/doctype/pick_list_item/pick_list_item.json -#: erpnext/stock/report/stock_ageing/stock_ageing.py:169 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:170 msgid "Available Qty" msgstr "" @@ -6415,8 +6423,8 @@ msgstr "" msgid "Available-for-use Date should be after purchase date" msgstr "" -#: erpnext/stock/report/stock_ageing/stock_ageing.py:170 -#: erpnext/stock/report/stock_ageing/stock_ageing.py:204 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:171 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:205 #: erpnext/stock/report/stock_balance/stock_balance.py:590 msgid "Average Age" msgstr "" @@ -6548,7 +6556,7 @@ msgstr "" msgid "BOM 1" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1760 +#: erpnext/manufacturing/doctype/bom/bom.py:1806 msgid "BOM 1 {0} and BOM 2 {1} should not be same" msgstr "" @@ -6693,11 +6701,6 @@ msgstr "" msgid "BOM Rate" msgstr "" -#. Name of a DocType -#: erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json -msgid "BOM Scrap Item" -msgstr "" - #. Label of a Link in the Manufacturing Workspace #. Name of a report #. Label of a Workspace Sidebar Item @@ -6707,6 +6710,19 @@ msgstr "" msgid "BOM Search" msgstr "" +#. Name of a DocType +#. Label of the bom_secondary_item (Data) field in DocType 'Stock Entry Detail' +#: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json +#: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +msgid "BOM Secondary Item" +msgstr "" + +#. Label of the bom_secondary_item (Data) field in DocType 'Job Card Secondary +#. Item' +#: erpnext/manufacturing/doctype/job_card_secondary_item/job_card_secondary_item.json +msgid "BOM Secondary Item Reference" +msgstr "" + #. Name of a report #: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.json msgid "BOM Stock Analysis" @@ -6775,14 +6791,10 @@ msgstr "" msgid "BOM Website Operation" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2279 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2284 msgid "BOM and Finished Good Quantity is mandatory for Disassembly" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.js:1343 -msgid "BOM and Manufacturing Quantity are required" -msgstr "" - #. Label of the bom_and_work_order_tab (Tab Break) field in DocType #. 'Manufacturing Settings' #: erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json @@ -6798,23 +6810,23 @@ msgstr "" msgid "BOM recursion: {0} cannot be child of {1}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:751 +#: erpnext/manufacturing/doctype/bom/bom.py:789 msgid "BOM recursion: {1} cannot be parent or child of {0}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1494 +#: erpnext/manufacturing/doctype/bom/bom.py:1540 msgid "BOM {0} does not belong to Item {1}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1476 +#: erpnext/manufacturing/doctype/bom/bom.py:1522 msgid "BOM {0} must be active" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1479 +#: erpnext/manufacturing/doctype/bom/bom.py:1525 msgid "BOM {0} must be submitted" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:839 +#: erpnext/manufacturing/doctype/bom/bom.py:877 msgid "BOM {0} not found for the item {1}" msgstr "" @@ -7356,6 +7368,11 @@ msgstr "" msgid "Base Change Amount (Company Currency)" msgstr "" +#. Label of the base_cost (Currency) field in DocType 'BOM Secondary Item' +#: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json +msgid "Base Cost (Company Currency)" +msgstr "" + #. Label of the base_cost_per_unit (Float) field in DocType 'BOM Operation' #: erpnext/manufacturing/doctype/bom_operation/bom_operation.json msgid "Base Cost Per Unit" @@ -7444,16 +7461,9 @@ msgstr "" msgid "Basic Amount" msgstr "" -#. Label of the base_amount (Currency) field in DocType 'BOM Scrap Item' -#: erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json -msgid "Basic Amount (Company Currency)" -msgstr "" - #. Label of the base_rate (Currency) field in DocType 'BOM Item' -#. Label of the base_rate (Currency) field in DocType 'BOM Scrap Item' #. Label of the base_rate (Currency) field in DocType 'Sales Order Item' #: erpnext/manufacturing/doctype/bom_item/bom_item.json -#: erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json #: erpnext/selling/doctype/sales_order_item/sales_order_item.json msgid "Basic Rate (Company Currency)" msgstr "" @@ -7544,7 +7554,7 @@ msgstr "" #: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js:89 #: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py:115 -#: erpnext/public/js/controllers/transaction.js:2945 +#: erpnext/public/js/controllers/transaction.js:2949 #: erpnext/public/js/utils/barcode_scanner.js:281 #: erpnext/public/js/utils/serial_no_batch_selector.js:438 #: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -7579,7 +7589,7 @@ msgstr "" msgid "Batch No is mandatory" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:3397 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:3435 msgid "Batch No {0} does not exists" msgstr "" @@ -7602,7 +7612,7 @@ msgstr "" msgid "Batch Nos" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1938 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1976 msgid "Batch Nos are created successfully" msgstr "" @@ -7668,12 +7678,12 @@ msgstr "" msgid "Batch {0} is not available in warehouse {1}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3288 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3320 #: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py:290 msgid "Batch {0} of Item {1} has expired." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3294 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3326 msgid "Batch {0} of Item {1} is disabled." msgstr "" @@ -7740,7 +7750,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:1326 +#: erpnext/manufacturing/doctype/bom/bom.py:1372 #: erpnext/manufacturing/workspace/manufacturing/manufacturing.json #: erpnext/stock/doctype/material_request/material_request.js:139 #: erpnext/stock/doctype/stock_entry/stock_entry.js:695 @@ -8269,7 +8279,7 @@ msgstr "" msgid "Brokerage" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:193 +#: erpnext/manufacturing/doctype/bom/bom.js:231 msgid "Browse BOM" msgstr "" @@ -8577,6 +8587,21 @@ msgstr "" msgid "By default, the Supplier Name is set as per the Supplier Name entered. If you want Suppliers to be named by a Naming Series choose the 'Naming Series' option." msgstr "" +#. Option for the 'Type' (Select) field in DocType 'BOM Secondary Item' +#. Option for the 'Type' (Select) field in DocType 'Job Card Secondary Item' +#. Option for the 'Type' (Select) field in DocType 'Stock Entry Detail' +#. Option for the 'Type' (Select) field in DocType 'Subcontracting Inward Order +#. Secondary Item' +#. Option for the 'Type' (Select) field in DocType 'Subcontracting Receipt +#. Item' +#: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json +#: erpnext/manufacturing/doctype/job_card_secondary_item/job_card_secondary_item.json +#: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +#: erpnext/subcontracting/doctype/subcontracting_inward_order_secondary_item/subcontracting_inward_order_secondary_item.json +#: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +msgid "By-Product" +msgstr "" + #. Label of the bypass_credit_limit_check (Check) field in DocType 'Customer #. Credit Limit' #: erpnext/selling/doctype/customer_credit_limit/customer_credit_limit.json @@ -8886,7 +8911,7 @@ msgstr "" msgid "Can be approved by {0}" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:2529 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2530 msgid "Can not close Work Order. Since {0} Job Cards are in Work In Progress state." msgstr "" @@ -8926,7 +8951,7 @@ msgid "Can refer row only if the charge type is 'On Previous Row Amount' or 'Pre msgstr "" #: erpnext/setup/doctype/company/company.py:207 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:183 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:181 msgid "Can't change the valuation method, as there are transactions against some items which do not have its own valuation method" msgstr "" @@ -9033,7 +9058,7 @@ msgstr "" msgid "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet." msgstr "" -#: erpnext/controllers/subcontracting_inward_controller.py:587 +#: erpnext/controllers/subcontracting_inward_controller.py:592 msgid "Cannot cancel this Manufacturing Stock Entry as quantity of Finished Good produced cannot be less than quantity delivered in the linked Subcontracting Inward Order." msgstr "" @@ -9041,7 +9066,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:1137 +#: erpnext/controllers/buying_controller.py:1141 msgid "Cannot cancel this document as it is linked with the submitted asset {asset_link}. Please cancel the asset to continue." msgstr "" @@ -9094,7 +9119,7 @@ msgid "Cannot create Stock Reservation Entries for future dated Purchase Receipt msgstr "" #: erpnext/selling/doctype/sales_order/sales_order.py:1886 -#: erpnext/stock/doctype/pick_list/pick_list.py:254 +#: erpnext/stock/doctype/pick_list/pick_list.py:255 msgid "Cannot create a pick list for Sales Order {0} because it has reserved stock. Please unreserve the stock in order to create a pick list." msgstr "" @@ -9106,7 +9131,7 @@ msgstr "" msgid "Cannot create return for consolidated invoice {0}." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1177 +#: erpnext/manufacturing/doctype/bom/bom.py:1210 msgid "Cannot deactivate or cancel BOM as it is linked with other BOMs" msgstr "" @@ -9140,7 +9165,7 @@ msgstr "" msgid "Cannot delete virtual DocType: {0}. Virtual DocTypes do not have database tables." msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:148 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:146 msgid "Cannot disable Serial and Batch No for Item, as there are existing records for serial / batch." msgstr "" @@ -9148,7 +9173,7 @@ msgstr "" msgid "Cannot disable perpetual inventory, as there are existing Stock Ledger Entries for the company {0}. Please cancel the stock transactions first and try again." msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:129 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:127 msgid "Cannot disable {0} as it may lead to incorrect stock valuation." msgstr "" @@ -9219,6 +9244,10 @@ msgstr "" msgid "Cannot retrieve link token. Check Error Log for more information" msgstr "" +#: erpnext/selling/doctype/customer/customer.py:367 +msgid "Cannot select a Group type Customer Group. Please select a non-group Customer Group." +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 @@ -9485,12 +9514,12 @@ msgstr "" msgid "Categorize By" msgstr "" -#: erpnext/accounts/report/general_ledger/general_ledger.js:116 +#: erpnext/accounts/report/general_ledger/general_ledger.js:117 #: erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js:80 msgid "Categorize by" msgstr "" -#: erpnext/accounts/report/general_ledger/general_ledger.js:129 +#: erpnext/accounts/report/general_ledger/general_ledger.js:130 msgid "Categorize by Account" msgstr "" @@ -9498,7 +9527,7 @@ msgstr "" msgid "Categorize by Item" msgstr "" -#: erpnext/accounts/report/general_ledger/general_ledger.js:133 +#: erpnext/accounts/report/general_ledger/general_ledger.js:134 msgid "Categorize by Party" msgstr "" @@ -9510,14 +9539,14 @@ msgstr "" #. Option for the 'Categorize By' (Select) field in DocType 'Process Statement #. Of Accounts' #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json -#: erpnext/accounts/report/general_ledger/general_ledger.js:121 +#: erpnext/accounts/report/general_ledger/general_ledger.js:122 msgid "Categorize by Voucher" msgstr "" #. Option for the 'Categorize By' (Select) field in DocType 'Process Statement #. Of Accounts' #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json -#: erpnext/accounts/report/general_ledger/general_ledger.js:125 +#: erpnext/accounts/report/general_ledger/general_ledger.js:126 msgid "Categorize by Voucher (Consolidated)" msgstr "" @@ -9855,7 +9884,7 @@ msgstr "" #. Label of the reference_date (Date) field in DocType 'Payment Entry' #: erpnext/accounts/doctype/payment_entry/payment_entry.json -#: erpnext/public/js/controllers/transaction.js:2856 +#: erpnext/public/js/controllers/transaction.js:2860 msgid "Cheque/Reference Date" msgstr "" @@ -9909,7 +9938,7 @@ msgstr "" #. Label of the child_row_reference (Data) field in DocType 'Quality #. Inspection' -#: erpnext/public/js/controllers/transaction.js:2951 +#: erpnext/public/js/controllers/transaction.js:2955 #: erpnext/stock/doctype/quality_inspection/quality_inspection.json msgid "Child Row Reference" msgstr "" @@ -10081,7 +10110,7 @@ msgstr "" msgid "Closed Documents" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:2452 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2453 msgid "Closed Work Order can not be stopped or Re-opened" msgstr "" @@ -10166,6 +10195,21 @@ msgstr "" msgid "Closing [Opening + Total] " msgstr "" +#. Option for the 'Type' (Select) field in DocType 'BOM Secondary Item' +#. Option for the 'Type' (Select) field in DocType 'Job Card Secondary Item' +#. Option for the 'Type' (Select) field in DocType 'Stock Entry Detail' +#. Option for the 'Type' (Select) field in DocType 'Subcontracting Inward Order +#. Secondary Item' +#. Option for the 'Type' (Select) field in DocType 'Subcontracting Receipt +#. Item' +#: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json +#: erpnext/manufacturing/doctype/job_card_secondary_item/job_card_secondary_item.json +#: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +#: erpnext/subcontracting/doctype/subcontracting_inward_order_secondary_item/subcontracting_inward_order_secondary_item.json +#: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +msgid "Co-Product" +msgstr "" + #. Name of a DocType #. Label of the code_list (Link) field in DocType 'Common Code' #: erpnext/edi/doctype/code_list/code_list.json @@ -11098,8 +11142,8 @@ msgstr "" msgid "Completed Qty cannot be greater than 'Qty to Manufacture'" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:325 -#: erpnext/manufacturing/doctype/job_card/job_card.js:446 +#: erpnext/manufacturing/doctype/job_card/job_card.js:323 +#: erpnext/manufacturing/doctype/job_card/job_card.js:444 #: erpnext/manufacturing/doctype/workstation/workstation.js:296 msgid "Completed Quantity" msgstr "" @@ -11235,7 +11279,7 @@ msgstr "" msgid "Connection" msgstr "" -#: erpnext/accounts/report/general_ledger/general_ledger.js:175 +#: erpnext/accounts/report/general_ledger/general_ledger.js:176 msgid "Consider Accounting Dimensions" msgstr "" @@ -11245,7 +11289,7 @@ msgstr "" msgid "Consider Minimum Order Qty" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1017 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1018 msgid "Consider Process Loss" msgstr "" @@ -11465,7 +11509,7 @@ msgstr "" msgid "Consumed Stock Total Value" msgstr "" -#: erpnext/stock/doctype/stock_entry_type/stock_entry_type.py:127 +#: erpnext/stock/doctype/stock_entry_type/stock_entry_type.py:132 msgid "Consumed quantity of item {0} exceeds transferred quantity." msgstr "" @@ -11718,6 +11762,7 @@ msgstr "" #. Item Supplied' #. Label of the conversion_factor (Float) field in DocType 'BOM Creator Item' #. Label of the conversion_factor (Float) field in DocType 'BOM Item' +#. Label of the conversion_factor (Float) field in DocType 'BOM Secondary Item' #. Label of the conversion_factor (Float) field in DocType 'Material Request #. Plan Item' #. Label of the conversion_factor (Float) field in DocType 'Delivery Schedule @@ -11745,8 +11790,9 @@ msgstr "" #: erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json #: erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json #: erpnext/manufacturing/doctype/bom_item/bom_item.json +#: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json #: erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json -#: erpnext/public/js/utils.js:882 +#: erpnext/public/js/utils.js:886 #: erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json #: erpnext/stock/doctype/packed_item/packed_item.json #: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -11854,13 +11900,13 @@ msgstr "" msgid "Corrective Action" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:503 +#: erpnext/manufacturing/doctype/job_card/job_card.js:501 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:510 +#: erpnext/manufacturing/doctype/job_card/job_card.js:508 #: erpnext/manufacturing/doctype/job_card/job_card.json msgid "Corrective Operation" msgstr "" @@ -11882,10 +11928,24 @@ msgid "Cosmetics" msgstr "" #. Label of the cost (Currency) field in DocType 'Subscription Plan' +#. Label of the cost (Currency) field in DocType 'BOM Secondary Item' #: erpnext/accounts/doctype/subscription_plan/subscription_plan.json +#: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json msgid "Cost" msgstr "" +#. Label of the cost_allocation_section (Section Break) field in DocType 'BOM' +#. Label of the cost_allocation (Currency) field in DocType 'BOM' +#: erpnext/manufacturing/doctype/bom/bom.json +msgid "Cost Allocation" +msgstr "" + +#. Label of the cost_allocation_per (Percent) field in DocType 'BOM Secondary +#. Item' +#: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json +msgid "Cost Allocation %" +msgstr "" + #. Label of the cost_center (Link) field in DocType 'Account Closing Balance' #. Label of the cost_center (Link) field in DocType 'Advance Taxes and Charges' #. Option for the 'Budget Against' (Select) field in DocType 'Budget' @@ -12007,7 +12067,7 @@ msgstr "" #: erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.js:42 #: erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py:204 #: erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js:98 -#: erpnext/accounts/report/general_ledger/general_ledger.js:153 +#: erpnext/accounts/report/general_ledger/general_ledger.js:154 #: erpnext/accounts/report/general_ledger/general_ledger.py:776 #: erpnext/accounts/report/gross_profit/gross_profit.js:68 #: erpnext/accounts/report/gross_profit/gross_profit.py:395 @@ -12145,6 +12205,10 @@ msgstr "" msgid "Cost Per Unit" msgstr "" +#: erpnext/manufacturing/doctype/bom/bom.py:441 +msgid "Cost allocation between finished goods and secondary items should equal 100%" +msgstr "" + #. Title of an incoterm #: erpnext/setup/doctype/incoterm/incoterms.csv:8 msgid "Cost and Freight" @@ -12240,7 +12304,7 @@ msgstr "" msgid "Costing and Billing fields has been updated" msgstr "" -#: erpnext/setup/demo.py:55 +#: erpnext/setup/demo.py:78 msgid "Could Not Delete Demo Data" msgstr "" @@ -12524,11 +12588,11 @@ msgstr "" msgid "Create Payment Entry for Consolidated POS Invoices." msgstr "" -#: erpnext/public/js/controllers/transaction.js:513 +#: erpnext/public/js/controllers/transaction.js:517 msgid "Create Payment Request" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:793 +#: erpnext/manufacturing/doctype/work_order/work_order.js:794 msgid "Create Pick List" msgstr "" @@ -12740,7 +12804,7 @@ msgstr "" msgid "Create a variant with the template image." msgstr "" -#: erpnext/stock/stock_ledger.py:2065 +#: erpnext/stock/stock_ledger.py:2072 msgid "Create an incoming stock transaction for the Item." msgstr "" @@ -12847,7 +12911,11 @@ msgstr "" msgid "Creating User..." msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:300 +#: erpnext/setup/setup_wizard/setup_wizard.py:36 +msgid "Creating demo data" +msgstr "" + +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:305 msgid "Creating {} out of {} {}" msgstr "" @@ -12978,7 +13046,7 @@ msgstr "" msgid "Credit Limit" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:630 +#: erpnext/selling/doctype/customer/customer.py:642 msgid "Credit Limit Crossed" msgstr "" @@ -13071,16 +13139,16 @@ msgstr "" msgid "Credit in Company Currency" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:596 -#: erpnext/selling/doctype/customer/customer.py:651 +#: erpnext/selling/doctype/customer/customer.py:608 +#: erpnext/selling/doctype/customer/customer.py:663 msgid "Credit limit has been crossed for customer {0} ({1}/{2})" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:382 +#: erpnext/selling/doctype/customer/customer.py:394 msgid "Credit limit is already defined for the Company {0}" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:650 +#: erpnext/selling/doctype/customer/customer.py:662 msgid "Credit limit reached for customer {0}" msgstr "" @@ -13135,7 +13203,7 @@ msgstr "" msgid "Criteria weights must add up to 100%" msgstr "" -#: erpnext/accounts/doctype/accounts_settings/accounts_settings.py:172 +#: erpnext/accounts/doctype/accounts_settings/accounts_settings.py:173 msgid "Cron Interval should be between 1 and 59 Min" msgstr "" @@ -13272,7 +13340,7 @@ msgstr "" msgid "Currency of the Closing Account must be {0}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:685 +#: erpnext/manufacturing/doctype/bom/bom.py:723 msgid "Currency of the price list {0} must be {1} or {2}" msgstr "" @@ -15329,9 +15397,10 @@ msgstr "" msgid "Delimiter options" msgstr "" -#. Label of the deliver_scrap_items (Check) field in DocType 'Selling Settings' +#. Label of the deliver_secondary_items (Check) field in DocType 'Selling +#. Settings' #: erpnext/selling/doctype/selling_settings/selling_settings.json -msgid "Deliver Scrap Items" +msgid "Deliver Secondary Items" msgstr "" #. Option for the 'Status' (Select) field in DocType 'Purchase Order' @@ -15394,7 +15463,7 @@ msgstr "" #. Label of the delivered_qty (Float) field in DocType 'Subcontracting Inward #. Order Item' #. Label of the delivered_qty (Float) field in DocType 'Subcontracting Inward -#. Order Scrap Item' +#. Order Secondary Item' #: erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json #: erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json #: erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -15404,7 +15473,7 @@ msgstr "" #: erpnext/stock/report/reserved_stock/reserved_stock.py:131 #: erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py:63 #: erpnext/subcontracting/doctype/subcontracting_inward_order_item/subcontracting_inward_order_item.json -#: erpnext/subcontracting/doctype/subcontracting_inward_order_scrap_item/subcontracting_inward_order_scrap_item.json +#: erpnext/subcontracting/doctype/subcontracting_inward_order_secondary_item/subcontracting_inward_order_secondary_item.json msgid "Delivered Qty" msgstr "" @@ -15440,7 +15509,7 @@ msgstr "" #: erpnext/manufacturing/doctype/master_production_schedule_item/master_production_schedule_item.json #: erpnext/manufacturing/doctype/sales_forecast_item/sales_forecast_item.json #: erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.py:1067 -#: erpnext/public/js/utils.js:875 +#: erpnext/public/js/utils.js:879 #: erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json #: erpnext/selling/doctype/sales_order/sales_order.js:624 #: erpnext/selling/doctype/sales_order/sales_order.js:1490 @@ -15666,10 +15735,18 @@ msgstr "" msgid "Demo Company" msgstr "" +#: erpnext/setup/demo.py:51 +msgid "Demo Data creation failed." +msgstr "" + #: erpnext/public/js/utils/demo.js:25 msgid "Demo data cleared" msgstr "" +#: erpnext/setup/demo.py:42 +msgid "Demo data creation failed. Check notifications for more info." +msgstr "" + #: erpnext/setup/setup_wizard/data/industry_type.txt:18 msgid "Department Stores" msgstr "" @@ -16208,7 +16285,7 @@ msgstr "" msgid "Disassemble Order" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:443 +#: erpnext/manufacturing/doctype/work_order/work_order.js:444 msgid "Disassemble Qty cannot be less than or equal to 0." msgstr "" @@ -16725,7 +16802,7 @@ msgstr "" msgid "Do Not Use Batch-wise Valuation" msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:130 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:128 msgid "Do Not Use Batchwise Valuation" msgstr "" @@ -17237,7 +17314,7 @@ msgstr "" msgid "Each Transaction" msgstr "" -#: erpnext/stock/report/stock_ageing/stock_ageing.py:176 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:177 msgid "Earliest" msgstr "" @@ -17655,7 +17732,7 @@ msgstr "" msgid "Employee {0} does not belong to the company {1}" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:357 +#: erpnext/manufacturing/doctype/job_card/job_card.py:375 msgid "Employee {0} is currently working on another workstation. Please assign another employee." msgstr "" @@ -17832,6 +17909,18 @@ msgstr "" msgid "Enable Stock Reservation" msgstr "" +#. Label of the enable_subscription (Check) field in DocType 'Accounts +#. Settings' +#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json +msgid "Enable Subscription" +msgstr "" + +#. Description of the 'Enable Subscription' (Check) field in DocType 'Accounts +#. Settings' +#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json +msgid "Enable Subscription tracking in invoice" +msgstr "" + #. Label of the enable_utm (Check) field in DocType 'Selling Settings' #: erpnext/selling/doctype/selling_settings/selling_settings.json msgid "Enable UTM" @@ -17933,8 +18022,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:383 -#: erpnext/manufacturing/doctype/job_card/job_card.js:453 +#: erpnext/manufacturing/doctype/job_card/job_card.js:381 +#: erpnext/manufacturing/doctype/job_card/job_card.js:451 #: 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 @@ -18019,8 +18108,8 @@ msgstr "" msgid "Enter Serial Nos" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:410 -#: erpnext/manufacturing/doctype/job_card/job_card.js:479 +#: erpnext/manufacturing/doctype/job_card/job_card.js:408 +#: erpnext/manufacturing/doctype/job_card/job_card.js:477 #: erpnext/manufacturing/doctype/workstation/workstation.js:312 msgid "Enter Value" msgstr "" @@ -18096,11 +18185,11 @@ msgstr "" msgid "Enter the opening stock units." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:973 +#: erpnext/manufacturing/doctype/bom/bom.js:990 msgid "Enter the quantity of the Item that will be manufactured from this Bill of Materials." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1157 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1158 msgid "Enter the quantity to manufacture. Raw material Items will be fetched only when this is set." msgstr "" @@ -18161,7 +18250,7 @@ msgstr "" msgid "Error Description" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:290 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:295 msgid "Error Occurred" msgstr "" @@ -18262,7 +18351,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:2328 +#: erpnext/stock/stock_ledger.py:2335 msgid "Example: Serial No {0} reserved in {1}." msgstr "" @@ -18276,7 +18365,7 @@ msgstr "" msgid "Excess Materials Consumed" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1114 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1132 msgid "Excess Transfer" msgstr "" @@ -18712,7 +18801,7 @@ msgstr "" msgid "Expenses Included In Valuation" msgstr "" -#: erpnext/stock/doctype/pick_list/pick_list.py:306 +#: erpnext/stock/doctype/pick_list/pick_list.py:307 #: erpnext/stock/doctype/stock_entry/stock_entry.js:409 msgid "Expired Batches" msgstr "" @@ -18786,7 +18875,7 @@ msgstr "" msgid "Extra Consumed Qty" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:254 +#: erpnext/manufacturing/doctype/job_card/job_card.py:262 msgid "Extra Job Card Quantity" msgstr "" @@ -18868,20 +18957,18 @@ msgstr "" msgid "Failed to Authenticate the API key." msgstr "" -#: erpnext/setup/demo.py:54 +#: erpnext/setup/setup_wizard/setup_wizard.py:37 +#: erpnext/setup/setup_wizard/setup_wizard.py:38 +msgid "Failed to create demo data" +msgstr "" + +#: erpnext/setup/demo.py:77 msgid "Failed to erase demo data, please delete the demo company manually." msgstr "" -#: erpnext/setup/setup_wizard/setup_wizard.py:25 -#: erpnext/setup/setup_wizard/setup_wizard.py:26 -msgid "Failed to install presets" -msgstr "" - +#: erpnext/setup/setup_wizard/setup_wizard.py:16 #: erpnext/setup/setup_wizard/setup_wizard.py:17 -#: erpnext/setup/setup_wizard/setup_wizard.py:18 -#: erpnext/setup/setup_wizard/setup_wizard.py:42 -#: erpnext/setup/setup_wizard/setup_wizard.py:43 -msgid "Failed to login" +msgid "Failed to install presets" msgstr "" #: erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py:164 @@ -18896,12 +18983,16 @@ msgstr "" msgid "Failed to send email for campaign {0} to {1}" msgstr "" -#: erpnext/setup/setup_wizard/setup_wizard.py:30 -#: erpnext/setup/setup_wizard/setup_wizard.py:31 +#: erpnext/setup/setup_wizard/setup_wizard.py:26 +msgid "Failed to set defaults" +msgstr "" + +#: erpnext/setup/setup_wizard/setup_wizard.py:21 +#: erpnext/setup/setup_wizard/setup_wizard.py:22 msgid "Failed to setup company" msgstr "" -#: erpnext/setup/setup_wizard/setup_wizard.py:37 +#: erpnext/setup/setup_wizard/setup_wizard.py:28 msgid "Failed to setup defaults" msgstr "" @@ -18978,6 +19069,12 @@ msgstr "" msgid "Fetch Overdue Payments" msgstr "" +#. Label of the fetch_payment_schedule_in_payment_request (Check) field in +#. DocType 'Accounts Settings' +#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json +msgid "Fetch Payment Schedule In Payment Request" +msgstr "" + #: erpnext/accounts/doctype/subscription/subscription.js:36 msgid "Fetch Subscription Updates" msgstr "" @@ -19026,7 +19123,7 @@ msgid "Fetching Sales Orders..." msgstr "" #: erpnext/accounts/doctype/dunning/dunning.js:135 -#: erpnext/public/js/controllers/transaction.js:1593 +#: erpnext/public/js/controllers/transaction.js:1597 msgid "Fetching exchange rates ..." msgstr "" @@ -19266,9 +19363,9 @@ msgstr "" msgid "Financial reports will be generated using GL Entry doctypes (should be enabled if Period Closing Voucher is not posted for all years sequentially or missing) " msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:877 -#: erpnext/manufacturing/doctype/work_order/work_order.js:892 -#: erpnext/manufacturing/doctype/work_order/work_order.js:901 +#: erpnext/manufacturing/doctype/work_order/work_order.js:878 +#: erpnext/manufacturing/doctype/work_order/work_order.js:893 +#: erpnext/manufacturing/doctype/work_order/work_order.js:902 msgid "Finish" msgstr "" @@ -19299,20 +19396,20 @@ msgstr "" #. Service Item' #. Label of the fg_item (Link) field in DocType 'Subcontracting Order Service #. Item' -#: erpnext/public/js/utils.js:901 +#: erpnext/public/js/utils.js:905 #: erpnext/subcontracting/doctype/subcontracting_inward_order_service_item/subcontracting_inward_order_service_item.json #: erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json msgid "Finished Good Item" msgstr "" #. Label of the fg_item_code (Link) field in DocType 'Subcontracting Inward -#. Order Scrap Item' +#. Order Secondary Item' #: erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.py:37 -#: erpnext/subcontracting/doctype/subcontracting_inward_order_scrap_item/subcontracting_inward_order_scrap_item.json +#: erpnext/subcontracting/doctype/subcontracting_inward_order_secondary_item/subcontracting_inward_order_secondary_item.json msgid "Finished Good Item Code" msgstr "" -#: erpnext/public/js/utils.js:919 +#: erpnext/public/js/utils.js:923 msgid "Finished Good Item Qty" msgstr "" @@ -19419,7 +19516,7 @@ msgstr "" msgid "Finished Goods based Operating Cost" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1670 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1675 msgid "Finished Item {0} does not match with Work Order {1}" msgstr "" @@ -19589,7 +19686,7 @@ msgstr "" msgid "Fixed Asset Turnover Ratio" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:742 +#: erpnext/manufacturing/doctype/bom/bom.py:780 msgid "Fixed Asset item {0} cannot be used in BOMs." msgstr "" @@ -19667,7 +19764,7 @@ msgstr "" msgid "Following Material Requests have been raised automatically based on Item's re-order level" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:821 +#: erpnext/selling/doctype/customer/customer.py:833 msgid "Following fields are mandatory to create address:" msgstr "" @@ -19734,7 +19831,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:523 +#: erpnext/manufacturing/doctype/job_card/job_card.js:521 #: erpnext/manufacturing/doctype/job_card/job_card.json msgid "For Operation" msgstr "" @@ -19823,7 +19920,7 @@ msgstr "" msgid "For individual supplier" msgstr "" -#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:370 +#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:374 msgid "For item {0}, only {1} asset have been created or linked to {2}. Please create or link {3} more asset with the respective document." msgstr "" @@ -19831,11 +19928,11 @@ msgstr "" msgid "For item {0}, rate must be a positive number. To Allow negative rates, enable {1} in {2}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:346 +#: erpnext/manufacturing/doctype/bom/bom.py:367 msgid "For operation {0} at row {1}, please add raw materials or set a BOM against it." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:2599 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2600 msgid "For operation {0}: Quantity ({1}) can not be greater than pending quantity({2})" msgstr "" @@ -19852,7 +19949,7 @@ msgstr "" msgid "For projected and forecast quantities, the system will consider all child warehouses under the selected parent warehouse." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1702 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1707 msgid "For quantity {0} should not be greater than allowed quantity {1}" msgstr "" @@ -19889,7 +19986,7 @@ msgstr "" msgid "For the item {0}, the consumed quantity should be {1} according to the BOM {2}." msgstr "" -#: erpnext/public/js/controllers/transaction.js:1403 +#: erpnext/public/js/controllers/transaction.js:1407 msgctxt "Clear payment terms template and/or payment schedule when due date is changed" msgid "For the new {0} to take effect, would you like to clear the current {1}?" msgstr "" @@ -20894,10 +20991,10 @@ msgstr "" msgid "Get Sales Orders" msgstr "" -#. Label of the get_scrap_items (Button) field in DocType 'Subcontracting +#. Label of the get_secondary_items (Button) field in DocType 'Subcontracting #. Receipt' #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json -msgid "Get Scrap Items" +msgid "Get Secondary Items" msgstr "" #. Label of the get_started_sections (Code) field in DocType 'Support Settings' @@ -20941,8 +21038,8 @@ msgstr "" msgid "Get stops from" msgstr "" -#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js:195 -msgid "Getting Scrap Items" +#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js:196 +msgid "Getting Secondary Items" msgstr "" #. Option for the 'Coupon Type' (Select) field in DocType 'Coupon Code' @@ -20996,7 +21093,7 @@ msgstr "" msgid "Goods Transferred" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2220 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2225 msgid "Goods are already received against the outward entry {0}" msgstr "" @@ -21293,7 +21390,7 @@ msgstr "" msgid "Group Same Items" msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:158 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:156 msgid "Group Warehouses cannot be used in transactions. Please change the value of {0}" msgstr "" @@ -21598,7 +21695,7 @@ msgstr "" msgid "Here are the error logs for the aforementioned failed depreciation entries: {0}" msgstr "" -#: erpnext/stock/stock_ledger.py:2050 +#: erpnext/stock/stock_ledger.py:2057 msgid "Here are the options to proceed:" msgstr "" @@ -22073,10 +22170,10 @@ msgstr "" msgid "If enabled, system will set incoming rate as zero for stand-alone credit notes with expired batch item." msgstr "" -#. Description of the 'Deliver Scrap Items' (Check) field in DocType 'Selling -#. Settings' +#. Description of the 'Deliver Secondary Items' (Check) field in DocType +#. 'Selling Settings' #: erpnext/selling/doctype/selling_settings/selling_settings.json -msgid "If enabled, the Scrap Item generated against a Finished Good will also be added in the Stock Entry when delivering that Finished Good." +msgid "If enabled, the Secondary Items generated against a Finished Good will also be added in the Stock Entry when delivering that Finished Good." msgstr "" #. Description of the 'Disable Rounded Total' (Check) field in DocType 'POS @@ -22183,14 +22280,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:2060 +#: erpnext/stock/stock_ledger.py:2067 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." +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js:197 +msgid "If party does not exist, create it using the Customer Name field." +msgstr "" + +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js:198 +msgid "If party does not exist, create it using the Supplier Name field." msgstr "" #. Description of the 'Free Item Rate' (Currency) field in DocType 'Pricing @@ -22209,7 +22308,7 @@ msgstr "" msgid "If set, the system does not use the user's Email or the standard outgoing Email account for sending request for quotations." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1190 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1191 msgid "If the BOM results in Scrap material, the Scrap Warehouse needs to be selected." msgstr "" @@ -22218,7 +22317,7 @@ msgstr "" msgid "If the account is frozen, entries are allowed to restricted users." msgstr "" -#: erpnext/stock/stock_ledger.py:2053 +#: erpnext/stock/stock_ledger.py:2060 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 "" @@ -22228,7 +22327,7 @@ msgstr "" msgid "If the reorder check is set at the Group warehouse level, the available quantity becomes the sum of the projected quantities of all its child warehouses." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1209 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1210 msgid "If the selected BOM has Operations mentioned in it, the system will fetch all Operations from BOM, these values can be changed." msgstr "" @@ -22319,7 +22418,7 @@ msgstr "" msgid "If you still want to proceed, please disable 'Skip Available Sub Assembly Items' checkbox." msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1837 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1839 msgid "If you still want to proceed, please enable {0}." msgstr "" @@ -22391,7 +22490,7 @@ msgstr "" #. Label of the ignore_exchange_rate_revaluation_journals (Check) field in #. DocType 'Process Statement Of Accounts' #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json -#: erpnext/accounts/report/general_ledger/general_ledger.js:217 +#: erpnext/accounts/report/general_ledger/general_ledger.js:218 msgid "Ignore Exchange Rate Revaluation and Gain / Loss Journals" msgstr "" @@ -22399,7 +22498,7 @@ msgstr "" msgid "Ignore Existing Ordered Qty" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1829 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1831 msgid "Ignore Existing Projected Quantity" msgstr "" @@ -22443,7 +22542,7 @@ msgstr "" #. Of Accounts' #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json #: erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js:120 -#: erpnext/accounts/report/general_ledger/general_ledger.js:222 +#: erpnext/accounts/report/general_ledger/general_ledger.js:223 msgid "Ignore System Generated Credit / Debit Notes" msgstr "" @@ -22819,7 +22918,7 @@ msgstr "" #: erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js:131 #: erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.js:85 #: erpnext/accounts/report/custom_financial_statement/custom_financial_statement.js:29 -#: erpnext/accounts/report/general_ledger/general_ledger.js:186 +#: erpnext/accounts/report/general_ledger/general_ledger.js:187 #: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js:46 #: erpnext/accounts/report/trial_balance/trial_balance.js:105 msgid "Include Default FB Entries" @@ -23050,7 +23149,7 @@ msgstr "" msgid "Incompatible Setting Detected" msgstr "" -#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:191 +#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:195 msgid "Incorrect Account" msgstr "" @@ -23067,7 +23166,7 @@ msgstr "" msgid "Incorrect Check in (group) Warehouse for Reorder" msgstr "" -#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:142 +#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:143 msgid "Incorrect Company" msgstr "" @@ -23080,7 +23179,7 @@ msgstr "" msgid "Incorrect Date" msgstr "" -#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:157 +#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:158 msgid "Incorrect Invoice" msgstr "" @@ -23088,7 +23187,7 @@ msgstr "" msgid "Incorrect Payment Type" msgstr "" -#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:113 +#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:114 msgid "Incorrect Reference Document (Purchase Receipt Item)" msgstr "" @@ -23115,9 +23214,9 @@ msgstr "" msgid "Incorrect Type of Transaction" msgstr "" -#: erpnext/stock/doctype/pick_list/pick_list.py:186 -#: erpnext/stock/doctype/pick_list/pick_list.py:210 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:161 +#: erpnext/stock/doctype/pick_list/pick_list.py:187 +#: erpnext/stock/doctype/pick_list/pick_list.py:211 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:159 msgid "Incorrect Warehouse" msgstr "" @@ -23274,7 +23373,7 @@ msgid "Inspected By" msgstr "" #: erpnext/controllers/stock_controller.py:1494 -#: erpnext/manufacturing/doctype/job_card/job_card.py:814 +#: erpnext/manufacturing/doctype/job_card/job_card.py:832 msgid "Inspection Rejected" msgstr "" @@ -23298,7 +23397,7 @@ msgid "Inspection Required before Purchase" msgstr "" #: erpnext/controllers/stock_controller.py:1479 -#: erpnext/manufacturing/doctype/job_card/job_card.py:795 +#: erpnext/manufacturing/doctype/job_card/job_card.py:813 msgid "Inspection Submission" msgstr "" @@ -23353,7 +23452,7 @@ msgstr "" msgid "Installed Qty" msgstr "" -#: erpnext/setup/setup_wizard/setup_wizard.py:24 +#: erpnext/setup/setup_wizard/setup_wizard.py:15 msgid "Installing presets" msgstr "" @@ -23373,16 +23472,16 @@ msgid "Insufficient Permissions" msgstr "" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:462 -#: erpnext/stock/doctype/pick_list/pick_list.py:144 -#: erpnext/stock/doctype/pick_list/pick_list.py:162 -#: erpnext/stock/doctype/pick_list/pick_list.py:1055 +#: erpnext/stock/doctype/pick_list/pick_list.py:145 +#: erpnext/stock/doctype/pick_list/pick_list.py:163 +#: erpnext/stock/doctype/pick_list/pick_list.py:1088 #: erpnext/stock/doctype/stock_entry/stock_entry.py:956 -#: erpnext/stock/serial_batch_bundle.py:1205 erpnext/stock/stock_ledger.py:1741 -#: erpnext/stock/stock_ledger.py:2219 +#: erpnext/stock/serial_batch_bundle.py:1205 erpnext/stock/stock_ledger.py:1748 +#: erpnext/stock/stock_ledger.py:2226 msgid "Insufficient Stock" msgstr "" -#: erpnext/stock/stock_ledger.py:2234 +#: erpnext/stock/stock_ledger.py:2241 msgid "Insufficient Stock for Batch" msgstr "" @@ -23497,12 +23596,6 @@ msgstr "" msgid "Inter Transfer Reference" msgstr "" -#. Label of the inter_warehouse_transfer_settings_section (Section Break) field -#. in DocType 'Stock Settings' -#: erpnext/stock/doctype/stock_settings/stock_settings.json -msgid "Inter Warehouse Transfer Settings" -msgstr "" - #. Label of the interest (Currency) field in DocType 'Overdue Payment' #: erpnext/accounts/doctype/overdue_payment/overdue_payment.json msgid "Interest" @@ -23543,7 +23636,7 @@ msgstr "" msgid "Internal Customer Accounting" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:254 +#: erpnext/selling/doctype/customer/customer.py:255 msgid "Internal Customer for company {0} already exists" msgstr "" @@ -23653,7 +23746,7 @@ msgstr "" msgid "Invalid Barcode. There is no Item attached to this barcode." msgstr "" -#: erpnext/public/js/controllers/transaction.js:3212 +#: erpnext/public/js/controllers/transaction.js:3216 msgid "Invalid Blanket Order for the selected Customer and Item" msgstr "" @@ -23679,6 +23772,10 @@ msgstr "" msgid "Invalid Cost Center" msgstr "" +#: erpnext/selling/doctype/customer/customer.py:368 +msgid "Invalid Customer Group" +msgstr "" + #: erpnext/selling/doctype/sales_order/sales_order.py:417 msgid "Invalid Delivery Date" msgstr "" @@ -23687,11 +23784,11 @@ msgstr "" msgid "Invalid Discount" msgstr "" -#: erpnext/controllers/taxes_and_totals.py:803 +#: erpnext/controllers/taxes_and_totals.py:840 msgid "Invalid Discount Amount" msgstr "" -#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:129 +#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:130 msgid "Invalid Document" msgstr "" @@ -23761,7 +23858,7 @@ msgstr "" msgid "Invalid Priority" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1232 +#: erpnext/manufacturing/doctype/bom/bom.py:1275 msgid "Invalid Process Loss Configuration" msgstr "" @@ -23778,7 +23875,7 @@ msgstr "" msgid "Invalid Quantity" msgstr "" -#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:475 +#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:479 msgid "Invalid Query" msgstr "" @@ -23799,7 +23896,7 @@ msgstr "" msgid "Invalid Selling Price" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1745 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1750 msgid "Invalid Serial and Batch Bundle" msgstr "" @@ -23853,7 +23950,7 @@ msgstr "" msgid "Invalid result key. Response:" msgstr "" -#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:475 +#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:479 msgid "Invalid search query" msgstr "" @@ -24402,6 +24499,20 @@ msgstr "" msgid "Is Internal Supplier" msgstr "" +#. Label of the is_legacy (Check) field in DocType 'BOM Secondary Item' +#: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json +msgid "Is Legacy" +msgstr "" + +#. Label of the is_legacy_scrap_item (Check) field in DocType 'Stock Entry +#. Detail' +#. Label of the is_legacy_scrap_item (Check) field in DocType 'Subcontracting +#. Receipt Item' +#: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +#: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +msgid "Is Legacy Scrap Item" +msgstr "" + #. Label of the is_mandatory (Check) field in DocType 'Applicable On Account' #: erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json msgid "Is Mandatory" @@ -24552,14 +24663,6 @@ msgstr "" msgid "Is Sales Order Required for Sales Invoice & Delivery Note Creation?" msgstr "" -#. Label of the is_scrap_item (Check) field in DocType 'Stock Entry Detail' -#. Label of the is_scrap_item (Check) field in DocType 'Subcontracting Receipt -#. Item' -#: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json -#: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json -msgid "Is Scrap Item" -msgstr "" - #. Label of the is_short_year (Check) field in DocType 'Fiscal Year' #: erpnext/accounts/doctype/fiscal_year/fiscal_year.json msgid "Is Short/Long Year" @@ -24772,11 +24875,11 @@ msgstr "" msgid "It can take upto few hours for accurate stock values to be visible after merging items." msgstr "" -#: erpnext/public/js/controllers/transaction.js:2613 +#: erpnext/public/js/controllers/transaction.js:2617 msgid "It is needed to fetch Item Details." msgstr "" -#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:211 +#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:215 msgid "It's not possible to distribute charges equally when total amount is zero, please set 'Distribute Charges Based On' as 'Quantity'" msgstr "" @@ -24831,9 +24934,9 @@ msgstr "" #: erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js:33 #: erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py:204 #: erpnext/buying/workspace/buying/buying.json -#: erpnext/controllers/taxes_and_totals.py:1212 +#: erpnext/controllers/taxes_and_totals.py:1249 #: erpnext/manufacturing/doctype/blanket_order/blanket_order.json -#: erpnext/manufacturing/doctype/bom/bom.js:1066 +#: erpnext/manufacturing/doctype/bom/bom.js:1083 #: erpnext/manufacturing/doctype/bom/bom.json #: erpnext/manufacturing/doctype/plant_floor/plant_floor.js:109 #: erpnext/manufacturing/doctype/workstation/workstation_job_card.html:25 @@ -25018,7 +25121,7 @@ msgstr "" #. Label of the item_code (Link) field in DocType 'BOM Creator Item' #. Label of the item_code (Link) field in DocType 'BOM Explosion Item' #. Label of the item_code (Link) field in DocType 'BOM Item' -#. Label of the item_code (Link) field in DocType 'BOM Scrap Item' +#. Label of the item_code (Link) field in DocType 'BOM Secondary Item' #. Label of the item_code (Link) field in DocType 'BOM Website Item' #. Label of the item_code (Link) field in DocType 'Job Card Item' #. Label of the item_code (Link) field in DocType 'Master Production Schedule @@ -25062,7 +25165,7 @@ msgstr "" #. Label of the main_item_code (Link) field in DocType 'Subcontracting Inward #. Order Received Item' #. Label of the item_code (Link) field in DocType 'Subcontracting Inward Order -#. Scrap Item' +#. Secondary Item' #. Label of the item_code (Link) field in DocType 'Subcontracting Inward Order #. Service Item' #. Label of the item_code (Link) field in DocType 'Subcontracting Order Item' @@ -25109,7 +25212,7 @@ msgstr "" #: erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json #: erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json #: erpnext/manufacturing/doctype/bom_item/bom_item.json -#: erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json +#: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json #: erpnext/manufacturing/doctype/bom_website_item/bom_website_item.json #: erpnext/manufacturing/doctype/job_card_item/job_card_item.json #: erpnext/manufacturing/doctype/master_production_schedule_item/master_production_schedule_item.json @@ -25134,10 +25237,10 @@ msgstr "" #: erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py:86 #: erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py:119 #: erpnext/projects/doctype/timesheet/timesheet.js:214 -#: erpnext/public/js/controllers/transaction.js:2907 +#: erpnext/public/js/controllers/transaction.js:2911 #: erpnext/public/js/stock_reservation.js:112 -#: erpnext/public/js/stock_reservation.js:318 erpnext/public/js/utils.js:559 -#: erpnext/public/js/utils.js:716 +#: erpnext/public/js/stock_reservation.js:318 erpnext/public/js/utils.js:563 +#: erpnext/public/js/utils.js:720 #: erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.json #: erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json #: erpnext/selling/doctype/installation_note_item/installation_note_item.json @@ -25195,13 +25298,13 @@ msgstr "" #: erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.js:8 #: erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.py:433 #: erpnext/stock/report/serial_no_ledger/serial_no_ledger.js:7 -#: erpnext/stock/report/stock_ageing/stock_ageing.py:132 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:133 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:104 #: erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py:25 #: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:26 #: erpnext/subcontracting/doctype/subcontracting_inward_order_item/subcontracting_inward_order_item.json #: erpnext/subcontracting/doctype/subcontracting_inward_order_received_item/subcontracting_inward_order_received_item.json -#: erpnext/subcontracting/doctype/subcontracting_inward_order_scrap_item/subcontracting_inward_order_scrap_item.json +#: erpnext/subcontracting/doctype/subcontracting_inward_order_secondary_item/subcontracting_inward_order_secondary_item.json #: erpnext/subcontracting/doctype/subcontracting_inward_order_service_item/subcontracting_inward_order_service_item.json #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:253 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:352 @@ -25394,7 +25497,7 @@ msgstr "" #: erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py:55 #: erpnext/stock/report/product_bundle_balance/product_bundle_balance.js:37 #: erpnext/stock/report/product_bundle_balance/product_bundle_balance.py:100 -#: erpnext/stock/report/stock_ageing/stock_ageing.py:141 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:142 #: erpnext/stock/report/stock_analytics/stock_analytics.js:8 #: erpnext/stock/report/stock_analytics/stock_analytics.py:52 #: erpnext/stock/report/stock_balance/stock_balance.js:32 @@ -25515,7 +25618,7 @@ msgstr "" #. Label of the item_name (Data) field in DocType 'BOM Creator Item' #. Label of the item_name (Data) field in DocType 'BOM Explosion Item' #. Label of the item_name (Data) field in DocType 'BOM Item' -#. Label of the item_name (Data) field in DocType 'BOM Scrap Item' +#. Label of the item_name (Data) field in DocType 'BOM Secondary Item' #. Label of the item_name (Data) field in DocType 'BOM Website Item' #. Label of the item_name (Read Only) field in DocType 'Job Card' #. Label of the item_name (Data) field in DocType 'Job Card Item' @@ -25594,7 +25697,7 @@ msgstr "" #: erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json #: erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json #: erpnext/manufacturing/doctype/bom_item/bom_item.json -#: erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json +#: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json #: erpnext/manufacturing/doctype/bom_website_item/bom_website_item.json #: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/manufacturing/doctype/job_card_item/job_card_item.json @@ -25615,8 +25718,8 @@ msgstr "" #: erpnext/manufacturing/report/production_planning_report/production_planning_report.py:371 #: erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py:92 #: erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py:138 -#: erpnext/public/js/controllers/transaction.js:2913 -#: erpnext/public/js/utils.js:811 +#: erpnext/public/js/controllers/transaction.js:2917 +#: erpnext/public/js/utils.js:815 #: erpnext/selling/doctype/quotation_item/quotation_item.json #: erpnext/selling/doctype/sales_order/sales_order.js:1252 #: erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -25654,7 +25757,7 @@ msgstr "" #: erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py:54 #: erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py:131 #: erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.py:440 -#: erpnext/stock/report/stock_ageing/stock_ageing.py:138 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:139 #: erpnext/stock/report/stock_analytics/stock_analytics.py:45 #: erpnext/stock/report/stock_balance/stock_balance.py:479 #: erpnext/stock/report/stock_ledger/stock_ledger.py:277 @@ -25770,7 +25873,7 @@ msgstr "" msgid "Item Row" msgstr "" -#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:167 +#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:168 msgid "Item Row {0}: {1} {2} does not exist in above '{1}' table" msgstr "" @@ -25993,7 +26096,7 @@ msgstr "" msgid "Item Wise Tax Details" msgstr "" -#: erpnext/controllers/taxes_and_totals.py:537 +#: erpnext/controllers/taxes_and_totals.py:559 msgid "Item Wise Tax Details do not match with Taxes and Charges at the following rows:" msgstr "" @@ -26013,7 +26116,7 @@ msgstr "" msgid "Item and Warranty Details" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3267 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3299 msgid "Item for row {0} does not match Material Request" msgstr "" @@ -26029,7 +26132,7 @@ msgstr "" msgid "Item is removed since no serial / batch no selected." msgstr "" -#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:163 +#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:164 msgid "Item must be added using 'Get Items from Purchase Receipts' button" msgstr "" @@ -26047,7 +26150,7 @@ msgstr "" msgid "Item qty can not be updated as raw materials are already processed." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1148 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1155 msgid "Item rate has been updated to zero as Allow Zero Valuation Rate is checked for item {0}" msgstr "" @@ -26090,7 +26193,7 @@ msgstr "" msgid "Item {0} does not exist" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:670 +#: erpnext/manufacturing/doctype/bom/bom.py:708 msgid "Item {0} does not exist in the system or has expired" msgstr "" @@ -26146,7 +26249,7 @@ msgstr "" msgid "Item {0} is not a subcontracted item" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2132 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2137 msgid "Item {0} is not active or end of life has been reached" msgstr "" @@ -26166,7 +26269,7 @@ msgstr "" msgid "Item {0} must be a non-stock item" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1481 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1488 msgid "Item {0} not found in 'Raw Materials Supplied' table in {1} {2}" msgstr "" @@ -26232,7 +26335,7 @@ msgstr "" msgid "Item/Item Code required to get Item Tax Template." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:412 +#: erpnext/manufacturing/doctype/bom/bom.py:451 msgid "Item: {0} does not exist in the system" msgstr "" @@ -26292,7 +26395,7 @@ msgstr "" msgid "Items not found." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1144 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1151 msgid "Items rate has been updated to zero as Allow Zero Valuation Rate is checked for the following items: {0}" msgstr "" @@ -26367,9 +26470,9 @@ msgstr "" #: erpnext/buying/doctype/purchase_order_item/purchase_order_item.json #: erpnext/manufacturing/doctype/bom/bom.json #: erpnext/manufacturing/doctype/job_card/job_card.json -#: erpnext/manufacturing/doctype/job_card/job_card.py:979 +#: erpnext/manufacturing/doctype/job_card/job_card.py:997 #: erpnext/manufacturing/doctype/operation/operation.json -#: erpnext/manufacturing/doctype/work_order/work_order.js:396 +#: erpnext/manufacturing/doctype/work_order/work_order.js:397 #: erpnext/manufacturing/doctype/work_order/work_order.json #: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js:29 #: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py:86 @@ -26407,8 +26510,8 @@ msgid "Job Card Scheduled Time" msgstr "" #. Name of a DocType -#: erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.json -msgid "Job Card Scrap Item" +#: erpnext/manufacturing/doctype/job_card_secondary_item/job_card_secondary_item.json +msgid "Job Card Secondary Item" msgstr "" #. Name of a report @@ -26431,7 +26534,7 @@ msgstr "" msgid "Job Card and Capacity Planning" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1455 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1473 msgid "Job Card {0} has been completed" msgstr "" @@ -26507,7 +26610,7 @@ msgstr "" msgid "Job Worker Warehouse" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:2652 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2654 msgid "Job card {0} created" msgstr "" @@ -26723,7 +26826,7 @@ msgstr "" msgid "Kilowatt-Hour" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:981 +#: erpnext/manufacturing/doctype/job_card/job_card.py:999 msgid "Kindly cancel the Manufacturing Entries first against the work order {0}." msgstr "" @@ -26925,7 +27028,7 @@ msgstr "" msgid "Last transacted" msgstr "" -#: erpnext/stock/report/stock_ageing/stock_ageing.py:177 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:178 msgid "Latest" msgstr "" @@ -27598,7 +27701,7 @@ msgstr "" msgid "Loyalty Points will be calculated from the spent done (via the Sales Invoice), based on collection factor mentioned." msgstr "" -#: erpnext/public/js/utils.js:180 +#: erpnext/public/js/utils.js:184 msgid "Loyalty Points: {0}" msgstr "" @@ -27939,9 +28042,9 @@ 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:544 -#: erpnext/manufacturing/doctype/work_order/work_order.js:832 -#: erpnext/manufacturing/doctype/work_order/work_order.js:866 +#: erpnext/manufacturing/doctype/job_card/job_card.js:542 +#: erpnext/manufacturing/doctype/work_order/work_order.js:833 +#: erpnext/manufacturing/doctype/work_order/work_order.js:867 #: erpnext/setup/doctype/vehicle/vehicle.json msgid "Make" msgstr "" @@ -28004,7 +28107,7 @@ msgstr "" msgid "Make Stock Entry" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:418 +#: erpnext/manufacturing/doctype/job_card/job_card.js:416 msgid "Make Subcontracting PO" msgstr "" @@ -28167,8 +28270,8 @@ msgstr "" #: erpnext/stock/doctype/material_request_item/material_request_item.json #: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json #: erpnext/stock/doctype/stock_entry/stock_entry.json -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1225 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1241 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1232 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1248 #: erpnext/stock/doctype/stock_entry_type/stock_entry_type.json #: erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json #: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -28317,7 +28420,7 @@ msgstr "" msgid "Manufacturing Manager" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2385 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2390 msgid "Manufacturing Quantity is mandatory" msgstr "" @@ -28394,7 +28497,7 @@ msgstr "" msgid "Mapping Subcontracting Order ..." msgstr "" -#: erpnext/public/js/utils.js:1046 +#: erpnext/public/js/utils.js:1050 msgid "Mapping {0} ..." msgstr "" @@ -28540,7 +28643,7 @@ msgstr "" msgid "Material" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:857 +#: erpnext/manufacturing/doctype/work_order/work_order.js:858 msgid "Material Consumption" msgstr "" @@ -28548,7 +28651,7 @@ msgstr "" #. Option for the 'Purpose' (Select) field in DocType 'Stock Entry Type' #: erpnext/setup/setup_wizard/operations/install_fixtures.py:114 #: erpnext/stock/doctype/stock_entry/stock_entry.json -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1226 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1233 #: erpnext/stock/doctype/stock_entry_type/stock_entry_type.json msgid "Material Consumption for Manufacture" msgstr "" @@ -28743,7 +28846,7 @@ msgstr "" msgid "Material Request used to make this Stock Entry" msgstr "" -#: erpnext/controllers/subcontracting_controller.py:1343 +#: erpnext/controllers/subcontracting_controller.py:1349 msgid "Material Request {0} is cancelled or stopped" msgstr "" @@ -28862,12 +28965,12 @@ msgstr "" msgid "Materials To Be Transferred" msgstr "" -#: erpnext/controllers/subcontracting_controller.py:1576 +#: erpnext/controllers/subcontracting_controller.py:1582 msgid "Materials are already received against the {0} {1}" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:181 -#: erpnext/manufacturing/doctype/job_card/job_card.py:835 +#: erpnext/manufacturing/doctype/job_card/job_card.py:183 +#: erpnext/manufacturing/doctype/job_card/job_card.py:853 msgid "Materials needs to be transferred to the work in progress warehouse for the job card {0}" msgstr "" @@ -28936,7 +29039,7 @@ msgstr "" msgid "Max discount allowed for item: {0} is {1}%" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1009 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1010 #: erpnext/stock/doctype/pick_list/pick_list.js:200 msgid "Max: {0}" msgstr "" @@ -28963,11 +29066,11 @@ msgstr "" msgid "Maximum Producible Items" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3870 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3902 msgid "Maximum Samples - {0} can be retained for Batch {1} and Item {2}." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3861 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3893 msgid "Maximum Samples - {0} have already been retained for Batch {1} and Item {2} in Batch {3}." msgstr "" @@ -29022,7 +29125,7 @@ msgstr "" msgid "Megawatt" msgstr "" -#: erpnext/stock/stock_ledger.py:2066 +#: erpnext/stock/stock_ledger.py:2073 msgid "Mention Valuation Rate in the Item master." msgstr "" @@ -29067,7 +29170,7 @@ msgstr "" msgid "Merge Similar Account Heads" msgstr "" -#: erpnext/public/js/utils.js:1078 +#: erpnext/public/js/utils.js:1082 msgid "Merge taxes from multiple documents" msgstr "" @@ -29385,7 +29488,7 @@ msgstr "" msgid "Miscellaneous Expenses" msgstr "" -#: erpnext/controllers/buying_controller.py:702 +#: erpnext/controllers/buying_controller.py:706 msgid "Mismatch" msgstr "" @@ -29423,7 +29526,7 @@ msgstr "" msgid "Missing Finance Book" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1680 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1685 msgid "Missing Finished Good" msgstr "" @@ -29447,7 +29550,7 @@ msgstr "" msgid "Missing Serial No Bundle" msgstr "" -#: erpnext/stock/doctype/pick_list/pick_list.py:170 +#: erpnext/stock/doctype/pick_list/pick_list.py:171 msgid "Missing Warehouse" msgstr "" @@ -29459,7 +29562,7 @@ msgstr "" msgid "Missing required filter: {0}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1185 +#: erpnext/manufacturing/doctype/bom/bom.py:1218 #: erpnext/manufacturing/doctype/work_order/work_order.py:1476 msgid "Missing value" msgstr "" @@ -29695,7 +29798,7 @@ msgstr "" msgid "Multi-level BOM Creator" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:427 +#: erpnext/selling/doctype/customer/customer.py:439 msgid "Multiple Loyalty Programs found for Customer {}. Please select manually." msgstr "" @@ -29729,7 +29832,7 @@ msgstr "" msgid "Multiple fiscal years exist for the date {0}. Please set company in Fiscal Year" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1687 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1692 msgid "Multiple items cannot be marked as finished item" msgstr "" @@ -29866,7 +29969,7 @@ msgstr "" msgid "Negative Quantity is not allowed" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1537 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1575 #: erpnext/stock/serial_batch_bundle.py:1528 msgid "Negative Stock Error" msgstr "" @@ -30337,7 +30440,7 @@ msgstr "" msgid "New Task" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:206 +#: erpnext/manufacturing/doctype/bom/bom.js:244 msgid "New Version" msgstr "" @@ -30350,7 +30453,7 @@ msgstr "" msgid "New Workplace" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:392 +#: erpnext/selling/doctype/customer/customer.py:404 msgid "New credit limit is less than current outstanding amount for the customer. Credit limit has to be atleast {0}" msgstr "" @@ -30446,7 +30549,7 @@ msgstr "" msgid "No Item with Serial No {0}" msgstr "" -#: erpnext/controllers/subcontracting_controller.py:1494 +#: erpnext/controllers/subcontracting_controller.py:1500 msgid "No Items selected for transfer." msgstr "" @@ -30541,7 +30644,7 @@ msgid "No Work Orders were created" msgstr "" #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:833 -#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:860 +#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:897 msgid "No accounting entries for the following warehouses" msgstr "" @@ -30589,7 +30692,7 @@ msgstr "" msgid "No employee was scheduled for call popup" msgstr "" -#: erpnext/controllers/subcontracting_controller.py:1385 +#: erpnext/controllers/subcontracting_controller.py:1391 msgid "No item available for transfer." msgstr "" @@ -30730,7 +30833,7 @@ msgstr "" msgid "No pending Material Requests found to link for the given items." msgstr "" -#: erpnext/public/js/controllers/transaction.js:468 +#: erpnext/public/js/controllers/transaction.js:472 msgid "No pending payment schedules available." msgstr "" @@ -30832,7 +30935,7 @@ msgstr "" msgid "Non Profit" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1593 +#: erpnext/manufacturing/doctype/bom/bom.py:1639 msgid "Non stock items" msgstr "" @@ -30961,7 +31064,7 @@ msgstr "" msgid "Note: Email will not be sent to disabled users" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:754 +#: erpnext/manufacturing/doctype/bom/bom.py:792 msgid "Note: If you want to use the finished good {0} as a raw material, then enable the 'Do Not Explode' checkbox in the Items table against the same raw material." msgstr "" @@ -31343,7 +31446,7 @@ msgstr "" msgid "Once set, this invoice will be on hold till the set date" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:744 +#: erpnext/manufacturing/doctype/work_order/work_order.js:745 msgid "Once the Work Order is Closed. It can't be resumed." msgstr "" @@ -31435,11 +31538,11 @@ msgstr "" msgid "Only one of Deposit or Withdrawal should be non-zero when applying an Excluded Fee." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:324 +#: erpnext/manufacturing/doctype/bom/bom.py:329 msgid "Only one operation can have 'Is Final Finished Good' checked when 'Track Semi Finished Goods' is enabled." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1240 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1247 msgid "Only one {0} entry can be created against the Work Order {1}" msgstr "" @@ -31676,7 +31779,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:299 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:304 msgid "Opening Invoice Creation In Progress" msgstr "" @@ -31695,7 +31798,7 @@ msgstr "" msgid "Opening Invoice Creation Tool Item" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:101 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:106 msgid "Opening Invoice Item" msgstr "" @@ -31793,7 +31896,7 @@ msgstr "" msgid "Operating Cost Per BOM Quantity" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1680 +#: erpnext/manufacturing/doctype/bom/bom.py:1726 msgid "Operating Cost as per Work Order / BOM" msgstr "" @@ -31884,11 +31987,11 @@ msgstr "" msgid "Operation time does not depend on quantity to produce" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:586 +#: erpnext/manufacturing/doctype/job_card/job_card.js:584 msgid "Operation {0} added multiple times in the work order {1}" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1228 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1246 msgid "Operation {0} does not belong to the work order {1}" msgstr "" @@ -31918,7 +32021,7 @@ msgstr "" msgid "Operations Routing" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1194 +#: erpnext/manufacturing/doctype/bom/bom.py:1227 msgid "Operations cannot be left blank" msgstr "" @@ -32361,7 +32464,7 @@ msgstr "" msgid "Out of Order" msgstr "" -#: erpnext/stock/doctype/pick_list/pick_list.py:603 +#: erpnext/stock/doctype/pick_list/pick_list.py:630 msgid "Out of Stock" msgstr "" @@ -33281,7 +33384,7 @@ msgstr "" msgid "Parent Row No" msgstr "" -#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:534 +#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:533 msgid "Parent Row No not found for {0}" msgstr "" @@ -33655,7 +33758,7 @@ msgstr "" #: 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 +#: erpnext/accounts/report/general_ledger/general_ledger.js:111 #: erpnext/accounts/report/general_ledger/general_ledger.py:761 #: erpnext/crm/doctype/contract/contract.json #: erpnext/selling/doctype/party_specific_item/party_specific_item.json @@ -34316,7 +34419,7 @@ msgstr "" msgid "Payment Schedule based Payment Requests cannot be created because a Payment Entry already exists for this document." msgstr "" -#: erpnext/public/js/controllers/transaction.js:478 +#: erpnext/public/js/controllers/transaction.js:482 msgid "Payment Schedules" msgstr "" @@ -34342,7 +34445,7 @@ msgstr "" #: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1256 #: erpnext/accounts/report/gross_profit/gross_profit.py:449 #: erpnext/accounts/workspace/invoicing/invoicing.json -#: erpnext/public/js/controllers/transaction.js:492 +#: erpnext/public/js/controllers/transaction.js:496 #: erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py:30 #: erpnext/workspace_sidebar/accounts_setup.json msgid "Payment Term" @@ -34964,7 +35067,7 @@ msgstr "" msgid "Pick List" msgstr "" -#: erpnext/stock/doctype/pick_list/pick_list.py:266 +#: erpnext/stock/doctype/pick_list/pick_list.py:267 msgid "Pick List Incomplete" msgstr "" @@ -35283,7 +35386,7 @@ msgstr "" msgid "Plants and Machineries" msgstr "" -#: erpnext/stock/doctype/pick_list/pick_list.py:600 +#: erpnext/stock/doctype/pick_list/pick_list.py:627 msgid "Please Restock Items and Update the Pick List to continue. To discontinue, cancel the Pick List." msgstr "" @@ -35310,7 +35413,7 @@ msgstr "" msgid "Please Set Priority" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:166 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:171 msgid "Please Set Supplier Group in Buying Settings." msgstr "" @@ -35326,7 +35429,7 @@ msgstr "" msgid "Please add Mode of payments and opening balance details." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:24 +#: erpnext/manufacturing/doctype/bom/bom.js:39 msgid "Please add Operations first." msgstr "" @@ -35338,7 +35441,7 @@ msgstr "" msgid "Please add Root Account for - {0}" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:315 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:320 msgid "Please add a Temporary Opening account in Chart of Accounts" msgstr "" @@ -35396,7 +35499,7 @@ msgstr "" msgid "Please check Process Deferred Accounting {0} and submit manually after resolving errors." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:105 +#: erpnext/manufacturing/doctype/bom/bom.js:120 msgid "Please check either with operations or FG Based Operating Cost." msgstr "" @@ -35429,7 +35532,7 @@ msgstr "" msgid "Please click on 'Generate Schedule' to get schedule" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:622 +#: erpnext/selling/doctype/customer/customer.py:634 msgid "Please contact any of the following users to extend the credit limits for {0}: {1}" msgstr "" @@ -35437,7 +35540,7 @@ msgstr "" msgid "Please contact any of the following users to {} this transaction." msgstr "" -#: erpnext/selling/doctype/customer/customer.py:615 +#: erpnext/selling/doctype/customer/customer.py:627 msgid "Please contact your administrator to extend the credit limits for {0}." msgstr "" @@ -35449,7 +35552,7 @@ msgstr "" msgid "Please create Customer from Lead {0}." msgstr "" -#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:154 +#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:155 msgid "Please create Landed Cost Vouchers against Invoices that have 'Update Stock' enabled." msgstr "" @@ -35489,7 +35592,7 @@ msgstr "" msgid "Please enable Applicable on Purchase Order and Applicable on Booking Actual Expenses" msgstr "" -#: erpnext/stock/doctype/pick_list/pick_list.py:317 +#: erpnext/stock/doctype/pick_list/pick_list.py:318 msgid "Please enable Use Old Serial / Batch Fields to make_bundle" msgstr "" @@ -35559,7 +35662,7 @@ msgstr "" msgid "Please enter Item Code to get Batch Number" msgstr "" -#: erpnext/public/js/controllers/transaction.js:3069 +#: erpnext/public/js/controllers/transaction.js:3073 msgid "Please enter Item Code to get batch no" msgstr "" @@ -35583,7 +35686,7 @@ msgstr "" msgid "Please enter Purchase Receipt first" msgstr "" -#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:118 +#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:119 msgid "Please enter Receipt Document" msgstr "" @@ -35672,7 +35775,7 @@ msgstr "" msgid "Please enter the phone number first" msgstr "" -#: erpnext/controllers/buying_controller.py:1185 +#: erpnext/controllers/buying_controller.py:1189 msgid "Please enter the {schedule_date}." msgstr "" @@ -35782,7 +35885,7 @@ msgstr "" msgid "Please select Template Type to download template" msgstr "" -#: erpnext/controllers/taxes_and_totals.py:809 +#: erpnext/controllers/taxes_and_totals.py:846 #: erpnext/public/js/controllers/taxes_and_totals.js:796 msgid "Please select Apply Discount On" msgstr "" @@ -35795,7 +35898,7 @@ msgstr "" msgid "Please select BOM for Item in Row {0}" msgstr "" -#: erpnext/controllers/buying_controller.py:636 +#: erpnext/controllers/buying_controller.py:640 msgid "Please select BOM in BOM field for Item {item_code}." msgstr "" @@ -35875,7 +35978,7 @@ msgstr "" msgid "Please select Posting Date first" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1245 +#: erpnext/manufacturing/doctype/bom/bom.py:1291 msgid "Please select Price List" msgstr "" @@ -35899,7 +36002,7 @@ msgstr "" msgid "Please select Stock Asset Account" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1604 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1611 msgid "Please select Subcontracting Order instead of Purchase Order {0}" msgstr "" @@ -35907,20 +36010,20 @@ msgstr "" 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:1500 +#: erpnext/manufacturing/doctype/bom/bom.py:1546 msgid "Please select a BOM" msgstr "" #: erpnext/accounts/party.py:417 -#: erpnext/stock/doctype/pick_list/pick_list.py:1656 +#: erpnext/stock/doctype/pick_list/pick_list.py:1689 msgid "Please select a Company" msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.js:268 -#: erpnext/manufacturing/doctype/bom/bom.js:688 -#: erpnext/manufacturing/doctype/bom/bom.py:276 +#: erpnext/manufacturing/doctype/bom/bom.js:727 +#: erpnext/manufacturing/doctype/bom/bom.py:278 #: erpnext/public/js/controllers/accounts.js:277 -#: erpnext/public/js/controllers/transaction.js:3368 +#: erpnext/public/js/controllers/transaction.js:3372 msgid "Please select a Company first." msgstr "" @@ -35944,7 +36047,7 @@ msgstr "" msgid "Please select a Warehouse" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1569 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1600 msgid "Please select a Work Order first." msgstr "" @@ -36013,7 +36116,7 @@ msgstr "" msgid "Please select at least one row with difference value" msgstr "" -#: erpnext/public/js/controllers/transaction.js:520 +#: erpnext/public/js/controllers/transaction.js:524 msgid "Please select at least one schedule." msgstr "" @@ -36063,7 +36166,7 @@ msgstr "" msgid "Please select rows to create Reposting Entries" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:93 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:98 msgid "Please select the Company" msgstr "" @@ -36307,7 +36410,7 @@ msgstr "" msgid "Please set opening number of booked depreciations" msgstr "" -#: erpnext/public/js/controllers/transaction.js:2756 +#: erpnext/public/js/controllers/transaction.js:2760 msgid "Please set recurring after saving" msgstr "" @@ -36315,19 +36418,19 @@ msgstr "" msgid "Please set the Customer Address" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:182 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:187 msgid "Please set the Default Cost Center in {0} company." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:661 +#: erpnext/manufacturing/doctype/work_order/work_order.js:662 msgid "Please set the Item Code first" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1632 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1663 msgid "Please set the Target Warehouse in the Job Card" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1636 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1667 msgid "Please set the WIP Warehouse in the Job Card" msgstr "" @@ -36638,7 +36741,7 @@ msgstr "" msgid "Posting Date cannot be future date" msgstr "" -#: erpnext/public/js/controllers/transaction.js:1108 +#: erpnext/public/js/controllers/transaction.js:1112 msgid "Posting Date will change to today's date as Edit Posting Date and Time is unchecked. Are you sure want to proceed?" msgstr "" @@ -36701,7 +36804,7 @@ msgstr "" msgid "Posting Time" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2333 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2338 msgid "Posting date and posting time is mandatory" msgstr "" @@ -37102,7 +37205,7 @@ msgstr "" msgid "Price is not set for the item." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:566 +#: erpnext/manufacturing/doctype/bom/bom.py:604 msgid "Price not found for item {0} in price list {1}" msgstr "" @@ -37477,11 +37580,18 @@ msgstr "" msgid "Process Loss" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1228 +#. Label of the process_loss_per (Percent) field in DocType 'BOM Secondary +#. Item' +#: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json +msgid "Process Loss %" +msgstr "" + +#: erpnext/manufacturing/doctype/bom/bom.py:1271 msgid "Process Loss Percentage cannot be greater than 100" msgstr "" #. Label of the process_loss_qty (Float) field in DocType 'BOM' +#. Label of the process_loss_qty (Float) field in DocType 'BOM Secondary Item' #. Label of the process_loss_qty (Float) field in DocType 'Job Card' #. Label of the process_loss_qty (Float) field in DocType 'Work Order' #. Label of the process_loss_qty (Float) field in DocType 'Work Order @@ -37489,17 +37599,21 @@ msgstr "" #. Label of the process_loss_qty (Float) field in DocType 'Stock Entry' #. Label of the process_loss_qty (Float) field in DocType 'Subcontracting #. Inward Order Item' +#. Label of the process_loss_qty (Float) field in DocType 'Subcontracting +#. Receipt Item' #: erpnext/manufacturing/doctype/bom/bom.json +#: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json #: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/manufacturing/doctype/work_order/work_order.json #: erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json #: erpnext/manufacturing/report/process_loss_report/process_loss_report.py:94 #: erpnext/stock/doctype/stock_entry/stock_entry.json #: erpnext/subcontracting/doctype/subcontracting_inward_order_item/subcontracting_inward_order_item.json +#: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json msgid "Process Loss Qty" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:340 +#: erpnext/manufacturing/doctype/job_card/job_card.js:338 msgid "Process Loss Quantity" msgstr "" @@ -37638,7 +37752,7 @@ msgstr "" #. Label of the produced_qty (Float) field in DocType 'Subcontracting Inward #. Order Item' #. Label of the produced_qty (Float) field in DocType 'Subcontracting Inward -#. Order Scrap Item' +#. Order Secondary Item' #: erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json #: erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json #: erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py:50 @@ -37646,7 +37760,7 @@ msgstr "" #: erpnext/manufacturing/report/work_order_summary/work_order_summary.py:215 #: erpnext/stock/doctype/batch/batch.json #: erpnext/subcontracting/doctype/subcontracting_inward_order_item/subcontracting_inward_order_item.json -#: erpnext/subcontracting/doctype/subcontracting_inward_order_scrap_item/subcontracting_inward_order_scrap_item.json +#: erpnext/subcontracting/doctype/subcontracting_inward_order_secondary_item/subcontracting_inward_order_secondary_item.json msgid "Produced Qty" msgstr "" @@ -38555,7 +38669,7 @@ msgstr "" #: erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js:48 #: erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py:203 #: erpnext/buying/workspace/buying/buying.json -#: erpnext/controllers/buying_controller.py:918 +#: erpnext/controllers/buying_controller.py:922 #: erpnext/crm/doctype/contract/contract.json #: erpnext/manufacturing/doctype/blanket_order/blanket_order.js:54 #: erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json @@ -38630,7 +38744,7 @@ msgstr "" msgid "Purchase Order Item Supplied" msgstr "" -#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:975 +#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:1012 msgid "Purchase Order Item reference is missing in Subcontracting Receipt {0}" msgstr "" @@ -38677,7 +38791,7 @@ msgstr "" msgid "Purchase Order {0} is not submitted" msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:883 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:887 msgid "Purchase Orders" msgstr "" @@ -38818,7 +38932,7 @@ msgstr "" msgid "Purchase Receipt doesn't have any Item for which Retain Sample is enabled." msgstr "" -#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:1051 +#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:1088 msgid "Purchase Receipt {0} created." msgstr "" @@ -38981,10 +39095,10 @@ msgstr "" #. Label of the qty (Float) field in DocType 'Opportunity Item' #. Label of the qty (Float) field in DocType 'BOM Creator Item' #. Label of the qty (Float) field in DocType 'BOM Item' -#. Label of the stock_qty (Float) field in DocType 'BOM Scrap Item' +#. Label of the qty (Float) field in DocType 'BOM Secondary Item' #. Label of the qty (Float) field in DocType 'BOM Website Item' #. Label of the qty_section (Section Break) field in DocType 'Job Card Item' -#. Label of the stock_qty (Float) field in DocType 'Job Card Scrap Item' +#. Label of the stock_qty (Float) field in DocType 'Job Card Secondary Item' #. Label of the qty (Float) field in DocType 'Production Plan Item Reference' #. Label of the qty_section (Section Break) field in DocType 'Work Order Item' #. Label of the qty (Float) field in DocType 'Delivery Schedule Item' @@ -39012,13 +39126,13 @@ msgstr "" #: 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/bom.js:1103 #: erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json #: erpnext/manufacturing/doctype/bom_item/bom_item.json -#: erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json +#: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json #: erpnext/manufacturing/doctype/bom_website_item/bom_website_item.json #: erpnext/manufacturing/doctype/job_card_item/job_card_item.json -#: erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.json +#: erpnext/manufacturing/doctype/job_card_secondary_item/job_card_secondary_item.json #: erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json #: erpnext/manufacturing/doctype/work_order_item/work_order_item.json #: erpnext/manufacturing/doctype/workstation/workstation_job_card.html:28 @@ -39028,7 +39142,7 @@ msgstr "" #: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:398 #: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:499 #: erpnext/public/js/stock_reservation.js:134 -#: erpnext/public/js/stock_reservation.js:336 erpnext/public/js/utils.js:849 +#: erpnext/public/js/stock_reservation.js:336 erpnext/public/js/utils.js:853 #: erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json #: erpnext/selling/doctype/product_bundle_item/product_bundle_item.json #: erpnext/selling/doctype/sales_order/sales_order.js:390 @@ -39062,6 +39176,12 @@ msgstr "" msgid "Qty " msgstr "" +#. Label of the received_qty (Float) field in DocType 'Subcontracting Receipt +#. Item' +#: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +msgid "Qty (As per BOM)" +msgstr "" + #. Label of the company_total_stock (Float) field in DocType 'Sales Invoice #. Item' #. Label of the company_total_stock (Float) field in DocType 'Quotation Item' @@ -39133,7 +39253,7 @@ msgstr "" #. Label of the for_quantity (Float) field in DocType 'Job Card' #. Label of the qty (Float) field in DocType 'Work Order' -#: erpnext/manufacturing/doctype/bom/bom.js:367 +#: erpnext/manufacturing/doctype/bom/bom.js:405 #: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/manufacturing/doctype/work_order/work_order.json #: erpnext/manufacturing/report/process_loss_report/process_loss_report.py:82 @@ -39144,7 +39264,7 @@ msgstr "" msgid "Qty To Manufacture ({0}) cannot be a fraction for the UOM {2}. To allow this, disable '{1}' in the UOM {2}." msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:251 +#: erpnext/manufacturing/doctype/job_card/job_card.py:259 msgid "Qty To Manufacture in the job card cannot be greater than Qty To Manufacture in the work order for the operation {0}.

Solution: Either you can reduce the Qty To Manufacture in the job card or set the 'Overproduction Percentage For Work Order' in the {1}." msgstr "" @@ -39195,7 +39315,7 @@ msgstr "" msgid "Qty for which recursion isn't applicable." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1007 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1008 msgid "Qty for {0}" msgstr "" @@ -39213,7 +39333,7 @@ msgstr "" msgid "Qty of Finished Goods Item" msgstr "" -#: erpnext/stock/doctype/pick_list/pick_list.py:647 +#: erpnext/stock/doctype/pick_list/pick_list.py:674 msgid "Qty of Finished Goods Item should be greater than 0." msgstr "" @@ -39246,8 +39366,8 @@ msgstr "" msgid "Qty to Fetch" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:312 -#: erpnext/manufacturing/doctype/job_card/job_card.py:871 +#: erpnext/manufacturing/doctype/job_card/job_card.js:310 +#: erpnext/manufacturing/doctype/job_card/job_card.py:889 msgid "Qty to Manufacture" msgstr "" @@ -39398,7 +39518,7 @@ msgstr "" #: erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json #: erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json -#: erpnext/manufacturing/doctype/bom/bom.js:236 +#: erpnext/manufacturing/doctype/bom/bom.js:274 #: erpnext/manufacturing/doctype/bom/bom.json #: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/quality_management/workspace/quality/quality.json @@ -39478,17 +39598,17 @@ msgstr "" msgid "Quality Inspection Template Name" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:780 +#: erpnext/manufacturing/doctype/job_card/job_card.py:798 msgid "Quality Inspection is required for the item {0} before completing the job card {1}" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:791 -#: erpnext/manufacturing/doctype/job_card/job_card.py:800 +#: erpnext/manufacturing/doctype/job_card/job_card.py:809 +#: erpnext/manufacturing/doctype/job_card/job_card.py:818 msgid "Quality Inspection {0} is not submitted for the item: {1}" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:810 -#: erpnext/manufacturing/doctype/job_card/job_card.py:819 +#: erpnext/manufacturing/doctype/job_card/job_card.py:828 +#: erpnext/manufacturing/doctype/job_card/job_card.py:837 msgid "Quality Inspection {0} is rejected for the item: {1}" msgstr "" @@ -39624,7 +39744,7 @@ msgstr "" #: erpnext/buying/report/purchase_analytics/purchase_analytics.js:28 #: erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py:213 #: erpnext/manufacturing/doctype/blanket_order_item/blanket_order_item.json -#: erpnext/manufacturing/doctype/bom/bom.js:454 +#: erpnext/manufacturing/doctype/bom/bom.js:493 #: erpnext/manufacturing/doctype/bom/bom.json #: erpnext/manufacturing/doctype/bom_creator/bom_creator.js:69 #: erpnext/manufacturing/doctype/bom_creator/bom_creator.json @@ -39708,10 +39828,8 @@ msgstr "" #. Label of the quantity_and_rate_section (Section Break) field in DocType 'BOM #. Creator Item' #. Label of the quantity_and_rate (Section Break) field in DocType 'BOM Item' -#. Label of the quantity_and_rate (Section Break) field in DocType 'BOM Scrap -#. Item' #. Label of the quantity_and_rate (Section Break) field in DocType 'Job Card -#. Scrap Item' +#. Secondary Item' #. Label of the quantity_and_rate (Section Break) field in DocType 'Quotation #. Item' #. Label of the quantity_and_rate (Section Break) field in DocType 'Sales Order @@ -39728,8 +39846,7 @@ msgstr "" #: erpnext/crm/doctype/opportunity_item/opportunity_item.json #: erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json #: erpnext/manufacturing/doctype/bom_item/bom_item.json -#: erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json -#: erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.json +#: erpnext/manufacturing/doctype/job_card_secondary_item/job_card_secondary_item.json #: erpnext/selling/doctype/quotation_item/quotation_item.json #: erpnext/selling/doctype/sales_order_item/sales_order_item.json #: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -39764,7 +39881,7 @@ msgstr "" msgid "Quantity must be less than or equal to {0}" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1037 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1038 #: erpnext/stock/doctype/pick_list/pick_list.js:206 msgid "Quantity must not be more than {0}" msgstr "" @@ -39774,13 +39891,13 @@ msgstr "" msgid "Quantity of item obtained after manufacturing / repacking from given quantities of raw materials" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:734 +#: erpnext/manufacturing/doctype/bom/bom.py:772 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:393 -#: erpnext/manufacturing/doctype/job_card/job_card.js:463 +#: erpnext/manufacturing/doctype/bom/bom.py:716 +#: erpnext/manufacturing/doctype/job_card/job_card.js:391 +#: erpnext/manufacturing/doctype/job_card/job_card.js:461 #: erpnext/manufacturing/doctype/workstation/workstation.js:303 msgid "Quantity should be greater than 0" msgstr "" @@ -39789,7 +39906,7 @@ msgstr "" msgid "Quantity to Manufacture" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:2592 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2593 msgid "Quantity to Manufacture can not be zero for the operation {0}" msgstr "" @@ -39826,7 +39943,7 @@ msgstr "" msgid "Query Route String" msgstr "" -#: erpnext/accounts/doctype/accounts_settings/accounts_settings.py:176 +#: erpnext/accounts/doctype/accounts_settings/accounts_settings.py:177 msgid "Queue Size should be between 5 and 100" msgstr "" @@ -40015,7 +40132,7 @@ msgstr "" #. Label of the rate (Currency) field in DocType 'BOM Creator Item' #. Label of the rate (Currency) field in DocType 'BOM Explosion Item' #. Label of the rate (Currency) field in DocType 'BOM Item' -#. Label of the rate (Currency) field in DocType 'BOM Scrap Item' +#. Label of the rate (Currency) field in DocType 'BOM Secondary Item' #. Label of the rate (Currency) field in DocType 'Work Order Item' #. Label of the rate (Float) field in DocType 'Product Bundle Item' #. Label of the rate (Currency) field in DocType 'Quotation Item' @@ -40064,9 +40181,9 @@ msgstr "" #: erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json #: erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json #: erpnext/manufacturing/doctype/bom_item/bom_item.json -#: erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json +#: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json #: erpnext/manufacturing/doctype/work_order_item/work_order_item.json -#: erpnext/public/js/utils.js:859 +#: erpnext/public/js/utils.js:863 #: erpnext/selling/doctype/product_bundle_item/product_bundle_item.json #: erpnext/selling/doctype/quotation_item/quotation_item.json #: erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -40388,8 +40505,8 @@ msgstr "" #. Label of the materials_section (Section Break) field in DocType 'BOM' #. Label of the section_break_8 (Section Break) field in DocType 'Job Card' #. Label of the mr_items (Table) field in DocType 'Production Plan' -#: erpnext/manufacturing/doctype/bom/bom.js:407 -#: erpnext/manufacturing/doctype/bom/bom.js:1059 +#: erpnext/manufacturing/doctype/bom/bom.js:446 +#: erpnext/manufacturing/doctype/bom/bom.js:1076 #: erpnext/manufacturing/doctype/bom/bom.json #: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -40453,7 +40570,7 @@ msgstr "" msgid "Raw Materials Supplied Cost" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:726 +#: erpnext/manufacturing/doctype/bom/bom.py:764 msgid "Raw Materials cannot be blank." msgstr "" @@ -40475,7 +40592,7 @@ msgstr "" #: erpnext/buying/doctype/purchase_order/purchase_order.js:369 #: erpnext/manufacturing/doctype/production_plan/production_plan.js:124 -#: erpnext/manufacturing/doctype/work_order/work_order.js:760 +#: erpnext/manufacturing/doctype/work_order/work_order.js:761 #: erpnext/selling/doctype/sales_order/sales_order.js:968 #: erpnext/selling/doctype/sales_order/sales_order_list.js:70 #: erpnext/stock/doctype/material_request/material_request.js:243 @@ -40771,13 +40888,10 @@ msgid "Received Qty in Stock UOM" msgstr "" #. Label of the received_qty (Float) field in DocType 'Purchase Receipt Item' -#. Label of the received_qty (Float) field in DocType 'Subcontracting Receipt -#. Item' #: erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.py:119 #: erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.py:50 #: erpnext/manufacturing/notification/material_request_receipt_notification/material_request_receipt_notification.html:9 #: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json -#: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json msgid "Received Quantity" msgstr "" @@ -41025,7 +41139,7 @@ msgstr "" msgid "Reference #{0} dated {1}" msgstr "" -#: erpnext/public/js/controllers/transaction.js:2869 +#: erpnext/public/js/controllers/transaction.js:2873 msgid "Reference Date for Early Payment Discount" msgstr "" @@ -41216,20 +41330,20 @@ msgstr "" msgid "Regular" msgstr "" -#: erpnext/stock/doctype/inventory_dimension/inventory_dimension.py:198 +#: erpnext/stock/doctype/inventory_dimension/inventory_dimension.py:199 msgid "Rejected " msgstr "" #. Label of the rejected_qty (Float) field in DocType 'Purchase Invoice Item' +#. Label of the rejected_qty (Float) field in DocType 'Subcontracting Receipt +#. Item' #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +#: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json msgid "Rejected Qty" msgstr "" #. Label of the rejected_qty (Float) field in DocType 'Purchase Receipt Item' -#. Label of the rejected_qty (Float) field in DocType 'Subcontracting Receipt -#. Item' #: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json -#: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json msgid "Rejected Quantity" msgstr "" @@ -41774,7 +41888,7 @@ msgstr "" msgid "Reqd Qty (BOM)" msgstr "" -#: erpnext/public/js/utils.js:875 +#: erpnext/public/js/utils.js:879 msgid "Reqd by date" msgstr "" @@ -42046,7 +42160,7 @@ msgstr "" msgid "Reservation Based On" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:918 +#: erpnext/manufacturing/doctype/work_order/work_order.js:919 #: erpnext/selling/doctype/sales_order/sales_order.js:99 #: erpnext/stock/doctype/pick_list/pick_list.js:150 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:180 @@ -42161,14 +42275,14 @@ msgstr "" msgid "Reserved Quantity for Production" msgstr "" -#: erpnext/stock/stock_ledger.py:2334 +#: erpnext/stock/stock_ledger.py:2341 msgid "Reserved Serial No." msgstr "" #. Label of the reserved_stock (Float) field in DocType 'Bin' #. Name of a report #: erpnext/manufacturing/doctype/plant_floor/stock_summary_template.html:24 -#: erpnext/manufacturing/doctype/work_order/work_order.js:934 +#: erpnext/manufacturing/doctype/work_order/work_order.js:935 #: erpnext/public/js/stock_reservation.js:236 #: erpnext/selling/doctype/sales_order/sales_order.js:127 #: erpnext/selling/doctype/sales_order/sales_order.js:457 @@ -42177,13 +42291,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:2318 +#: erpnext/stock/stock_ledger.py:2325 #: 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:2363 +#: erpnext/stock/stock_ledger.py:2370 msgid "Reserved Stock for Batch" msgstr "" @@ -42195,7 +42309,7 @@ msgstr "" msgid "Reserved Stock for Sub-assembly" msgstr "" -#: erpnext/controllers/buying_controller.py:645 +#: erpnext/controllers/buying_controller.py:649 msgid "Reserved Warehouse is mandatory for the Item {item_code} in Raw Materials supplied." msgstr "" @@ -43138,11 +43252,11 @@ msgid "Row #{0}: Acceptance Criteria Formula is required." msgstr "" #: erpnext/controllers/subcontracting_controller.py:125 -#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:534 +#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:571 msgid "Row #{0}: Accepted Warehouse and Rejected Warehouse cannot be same" msgstr "" -#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:527 +#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:564 msgid "Row #{0}: Accepted Warehouse is mandatory for the accepted Item {1}" msgstr "" @@ -43187,7 +43301,7 @@ msgstr "" msgid "Row #{0}: Batch No {1} is already selected." msgstr "" -#: erpnext/controllers/subcontracting_inward_controller.py:430 +#: erpnext/controllers/subcontracting_inward_controller.py:435 msgid "Row #{0}: Batch No(s) {1} is not a part of the linked Subcontracting Inward Order. Please select valid Batch No(s)." msgstr "" @@ -43195,15 +43309,15 @@ msgstr "" msgid "Row #{0}: Cannot allocate more than {1} against payment term {2}" msgstr "" -#: erpnext/controllers/subcontracting_inward_controller.py:631 +#: erpnext/controllers/subcontracting_inward_controller.py:637 msgid "Row #{0}: Cannot cancel this Manufacturing Stock Entry as billed quantity of Item {1} cannot be greater than consumed quantity." msgstr "" -#: erpnext/controllers/subcontracting_inward_controller.py:610 -msgid "Row #{0}: Cannot cancel this Manufacturing Stock Entry as quantity of Scrap Item {1} produced cannot be less than quantity delivered." +#: erpnext/controllers/subcontracting_inward_controller.py:616 +msgid "Row #{0}: Cannot cancel this Manufacturing Stock Entry as quantity of Secondary Item {1} produced cannot be less than quantity delivered." msgstr "" -#: erpnext/controllers/subcontracting_inward_controller.py:478 +#: erpnext/controllers/subcontracting_inward_controller.py:483 msgid "Row #{0}: Cannot cancel this Stock Entry as returned quantity cannot be greater than delivered quantity for Item {1} in the linked Subcontracting Inward Order" msgstr "" @@ -43235,7 +43349,7 @@ msgstr "" msgid "Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}." msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1109 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1127 msgid "Row #{0}: Cannot transfer more than Required Qty {1} for Item {2} against Job Card {3}" msgstr "" @@ -43275,13 +43389,13 @@ msgstr "" msgid "Row #{0}: Cumulative threshold cannot be less than Single Transaction threshold" msgstr "" -#: erpnext/controllers/subcontracting_inward_controller.py:88 +#: erpnext/controllers/subcontracting_inward_controller.py:90 msgid "Row #{0}: Customer Provided Item {1} against Subcontracting Inward Order Item {2} ({3}) cannot be added multiple times." msgstr "" -#: erpnext/controllers/subcontracting_inward_controller.py:176 -#: erpnext/controllers/subcontracting_inward_controller.py:301 -#: erpnext/controllers/subcontracting_inward_controller.py:349 +#: erpnext/controllers/subcontracting_inward_controller.py:178 +#: erpnext/controllers/subcontracting_inward_controller.py:304 +#: erpnext/controllers/subcontracting_inward_controller.py:352 msgid "Row #{0}: Customer Provided Item {1} cannot be added multiple times in the Subcontracting Inward process." msgstr "" @@ -43293,7 +43407,7 @@ msgstr "" msgid "Row #{0}: Customer Provided Item {1} does not exist in the Required Items table linked to the Subcontracting Inward Order." msgstr "" -#: erpnext/controllers/subcontracting_inward_controller.py:285 +#: erpnext/controllers/subcontracting_inward_controller.py:288 msgid "Row #{0}: Customer Provided Item {1} exceeds quantity available through Subcontracting Inward Order" msgstr "" @@ -43301,12 +43415,12 @@ msgstr "" msgid "Row #{0}: Customer Provided Item {1} has insufficient quantity in the Subcontracting Inward Order. Available quantity is {2}." msgstr "" -#: erpnext/controllers/subcontracting_inward_controller.py:312 +#: erpnext/controllers/subcontracting_inward_controller.py:315 msgid "Row #{0}: Customer Provided Item {1} is not a part of Subcontracting Inward Order {2}" msgstr "" -#: erpnext/controllers/subcontracting_inward_controller.py:218 -#: erpnext/controllers/subcontracting_inward_controller.py:360 +#: erpnext/controllers/subcontracting_inward_controller.py:220 +#: erpnext/controllers/subcontracting_inward_controller.py:363 msgid "Row #{0}: Customer Provided Item {1} is not a part of Work Order {2}" msgstr "" @@ -43357,12 +43471,12 @@ msgstr "" msgid "Row #{0}: Finished Good must be {1}" msgstr "" -#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:515 -msgid "Row #{0}: Finished Good reference is mandatory for Scrap Item {1}." +#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:552 +msgid "Row #{0}: Finished Good reference is mandatory for Secondary Item {1}." msgstr "" -#: erpnext/controllers/subcontracting_inward_controller.py:168 -#: erpnext/controllers/subcontracting_inward_controller.py:291 +#: erpnext/controllers/subcontracting_inward_controller.py:170 +#: erpnext/controllers/subcontracting_inward_controller.py:294 msgid "Row #{0}: For Customer Provided Item {1}, Source Warehouse must be {2}" msgstr "" @@ -43382,7 +43496,7 @@ msgstr "" msgid "Row #{0}: From Date cannot be before To Date" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:861 +#: erpnext/manufacturing/doctype/job_card/job_card.py:879 msgid "Row #{0}: From Time and To Time fields are required" msgstr "" @@ -43390,7 +43504,7 @@ msgstr "" msgid "Row #{0}: Item added" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1535 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1542 msgid "Row #{0}: Item {1} cannot be transferred more than {2} against {3} {4}" msgstr "" @@ -43414,7 +43528,7 @@ msgstr "" msgid "Row #{0}: Item {1} in warehouse {2}: Available {3}, Needed {4}." msgstr "" -#: erpnext/controllers/subcontracting_inward_controller.py:63 +#: erpnext/controllers/subcontracting_inward_controller.py:65 msgid "Row #{0}: Item {1} is not a Customer Provided Item." msgstr "" @@ -43422,8 +43536,8 @@ msgstr "" msgid "Row #{0}: Item {1} is not a Serialized/Batched Item. It cannot have a Serial No/Batch No against it." msgstr "" -#: erpnext/controllers/subcontracting_inward_controller.py:113 -#: erpnext/controllers/subcontracting_inward_controller.py:491 +#: erpnext/controllers/subcontracting_inward_controller.py:115 +#: erpnext/controllers/subcontracting_inward_controller.py:496 msgid "Row #{0}: Item {1} is not a part of Subcontracting Inward Order {2}" msgstr "" @@ -43435,11 +43549,11 @@ msgstr "" msgid "Row #{0}: Item {1} is not a stock item" msgstr "" -#: erpnext/controllers/subcontracting_inward_controller.py:77 +#: erpnext/controllers/subcontracting_inward_controller.py:79 msgid "Row #{0}: Item {1} mismatch. Changing of item code is not permitted, add another row instead." msgstr "" -#: erpnext/controllers/subcontracting_inward_controller.py:126 +#: erpnext/controllers/subcontracting_inward_controller.py:128 msgid "Row #{0}: Item {1} mismatch. Changing of item code is not permitted." msgstr "" @@ -43471,8 +43585,8 @@ msgstr "" msgid "Row #{0}: Operation {1} is not completed for {2} qty of finished goods in Work Order {3}. Please update operation status via Job Card {4}." msgstr "" -#: erpnext/controllers/subcontracting_inward_controller.py:206 -#: erpnext/controllers/subcontracting_inward_controller.py:339 +#: erpnext/controllers/subcontracting_inward_controller.py:208 +#: erpnext/controllers/subcontracting_inward_controller.py:342 msgid "Row #{0}: Overconsumption of Customer Provided Item {1} against Work Order {2} is not allowed in the Subcontracting Inward process." msgstr "" @@ -43484,7 +43598,7 @@ msgstr "" msgid "Row #{0}: Please select the BOM No in Assembly Items" msgstr "" -#: erpnext/controllers/subcontracting_inward_controller.py:104 +#: erpnext/controllers/subcontracting_inward_controller.py:106 msgid "Row #{0}: Please select the Finished Good Item against which this Customer Provided Item will be used." msgstr "" @@ -43500,6 +43614,11 @@ msgstr "" msgid "Row #{0}: Please update deferred revenue/expense account in item row or default account in company master" msgstr "" +#: erpnext/manufacturing/doctype/bom/bom.py:345 +#, python-format +msgid "Row #{0}: Process Loss Percentage should be less than 100% for {1} Item {2}" +msgstr "" + #: erpnext/public/js/utils/barcode_scanner.js:425 msgid "Row #{0}: Qty increased by {1}" msgstr "" @@ -43533,10 +43652,14 @@ msgstr "" msgid "Row #{0}: Quantity for Item {1} cannot be zero." msgstr "" -#: erpnext/controllers/subcontracting_inward_controller.py:532 +#: erpnext/controllers/subcontracting_inward_controller.py:537 msgid "Row #{0}: Quantity of Item {1} cannot be more than {2} {3} against Subcontracting Inward Order {4}" msgstr "" +#: erpnext/manufacturing/doctype/bom/bom.py:338 +msgid "Row #{0}: Quantity should be greater than 0 for {1} Item {2}" +msgstr "" + #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1696 msgid "Row #{0}: Quantity to reserve for the Item {1} should be greater than 0." msgstr "" @@ -43556,8 +43679,8 @@ msgstr "" msgid "Row #{0}: Reference Document Type must be one of Sales Order, Sales Invoice, Journal Entry or Dunning" msgstr "" -#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:508 -msgid "Row #{0}: Rejected Qty cannot be set for Scrap Item {1}." +#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:545 +msgid "Row #{0}: Rejected Qty cannot be set for Secondary Item {1}." msgstr "" #: erpnext/controllers/subcontracting_controller.py:118 @@ -43572,16 +43695,16 @@ msgstr "" msgid "Row #{0}: Return Against is required for returning asset" msgstr "" -#: erpnext/controllers/subcontracting_inward_controller.py:140 +#: erpnext/controllers/subcontracting_inward_controller.py:142 msgid "Row #{0}: Returned quantity cannot be greater than available quantity for Item {1}" msgstr "" -#: erpnext/controllers/subcontracting_inward_controller.py:153 +#: erpnext/controllers/subcontracting_inward_controller.py:155 msgid "Row #{0}: Returned quantity cannot be greater than available quantity to return for Item {1}" msgstr "" -#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:503 -msgid "Row #{0}: Scrap Item Qty cannot be zero" +#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:540 +msgid "Row #{0}: Secondary Item Qty cannot be zero" msgstr "" #: erpnext/controllers/selling_controller.py:296 @@ -43608,7 +43731,7 @@ msgstr "" msgid "Row #{0}: Serial No {1} is already selected." msgstr "" -#: erpnext/controllers/subcontracting_inward_controller.py:419 +#: erpnext/controllers/subcontracting_inward_controller.py:424 msgid "Row #{0}: Serial No(s) {1} are not a part of the linked Subcontracting Inward Order. Please select valid Serial No(s)." msgstr "" @@ -43632,7 +43755,7 @@ msgstr "" msgid "Row #{0}: Since 'Track Semi Finished Goods' is enabled, the BOM {1} cannot be used for Sub Assembly Items" msgstr "" -#: erpnext/controllers/subcontracting_inward_controller.py:398 +#: erpnext/controllers/subcontracting_inward_controller.py:403 msgid "Row #{0}: Source Warehouse must be same as Customer Warehouse {1} from the linked Subcontracting Inward Order" msgstr "" @@ -43697,7 +43820,7 @@ msgstr "" msgid "Row #{0}: Stock quantity {1} ({2}) for item {3} cannot exceed {4}" msgstr "" -#: erpnext/controllers/subcontracting_inward_controller.py:392 +#: erpnext/controllers/subcontracting_inward_controller.py:397 msgid "Row #{0}: Target Warehouse must be same as Customer Warehouse {1} from the linked Subcontracting Inward Order" msgstr "" @@ -43729,7 +43852,7 @@ msgstr "" msgid "Row #{0}: Withholding Amount {1} does not match calculated amount {2}." msgstr "" -#: erpnext/controllers/subcontracting_inward_controller.py:572 +#: erpnext/controllers/subcontracting_inward_controller.py:577 msgid "Row #{0}: Work Order exists against full or partial quantity of Item {1}" msgstr "" @@ -43749,7 +43872,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:126 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:131 msgid "Row #{0}: {1} is required to create the Opening {2} Invoices" msgstr "" @@ -43769,23 +43892,23 @@ msgstr "" msgid "Row #{idx}: Cannot select Supplier Warehouse while suppling raw materials to subcontractor." msgstr "" -#: erpnext/controllers/buying_controller.py:576 +#: erpnext/controllers/buying_controller.py:580 msgid "Row #{idx}: Item rate has been updated as per valuation rate since its an internal stock transfer." msgstr "" -#: erpnext/controllers/buying_controller.py:1060 +#: erpnext/controllers/buying_controller.py:1064 msgid "Row #{idx}: Please enter a location for the asset item {item_code}." msgstr "" -#: erpnext/controllers/buying_controller.py:699 +#: erpnext/controllers/buying_controller.py:703 msgid "Row #{idx}: Received Qty must be equal to Accepted + Rejected Qty for Item {item_code}." msgstr "" -#: erpnext/controllers/buying_controller.py:712 +#: erpnext/controllers/buying_controller.py:716 msgid "Row #{idx}: {field_label} can not be negative for item {item_code}." msgstr "" -#: erpnext/controllers/buying_controller.py:665 +#: erpnext/controllers/buying_controller.py:669 msgid "Row #{idx}: {field_label} is mandatory." msgstr "" @@ -43793,7 +43916,7 @@ msgstr "" msgid "Row #{idx}: {from_warehouse_field} and {to_warehouse_field} cannot be same." msgstr "" -#: erpnext/controllers/buying_controller.py:1177 +#: erpnext/controllers/buying_controller.py:1181 msgid "Row #{idx}: {schedule_date} cannot be before {transaction_date}." msgstr "" @@ -43801,7 +43924,7 @@ msgstr "" msgid "Row #{}: Currency of {} - {} doesn't matches company currency." msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:108 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:113 msgid "Row #{}: Either Party ID or Party Name is required" msgstr "" @@ -43821,7 +43944,7 @@ msgstr "" msgid "Row #{}: POS Invoice {} is not submitted yet" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:118 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:123 msgid "Row #{}: Party ID is required" msgstr "" @@ -43845,7 +43968,7 @@ msgstr "" msgid "Row #{}: You cannot add positive quantities in a return invoice. Please remove item {} to complete the return." msgstr "" -#: erpnext/stock/doctype/pick_list/pick_list.py:233 +#: erpnext/stock/doctype/pick_list/pick_list.py:234 msgid "Row #{}: item {} has been picked already." msgstr "" @@ -43854,7 +43977,7 @@ msgstr "" msgid "Row #{}: {}" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:121 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:126 msgid "Row #{}: {} {} does not exist." msgstr "" @@ -43866,15 +43989,15 @@ msgstr "" msgid "Row No {0}: Warehouse is required. Please set a Default Warehouse for Item {1} and Company {2}" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:728 +#: erpnext/manufacturing/doctype/job_card/job_card.py:746 msgid "Row {0} : Operation is required against the raw material item {1}" msgstr "" -#: erpnext/stock/doctype/pick_list/pick_list.py:263 +#: erpnext/stock/doctype/pick_list/pick_list.py:264 msgid "Row {0} picked quantity is less than the required quantity, additional {1} {2} required." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1559 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1566 msgid "Row {0}# Item {1} not found in 'Raw Materials Supplied' table in {2} {3}" msgstr "" @@ -43906,7 +44029,7 @@ msgstr "" msgid "Row {0}: Allocated amount {1} must be less than or equal to remaining payment amount {2}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1220 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1227 msgid "Row {0}: As {1} is enabled, raw materials cannot be added to {2} entry. Use {3} entry to consume raw materials." msgstr "" @@ -43918,7 +44041,7 @@ msgstr "" msgid "Row {0}: Both Debit and Credit values cannot be zero" msgstr "" -#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:550 +#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:587 msgid "" "Row {0}: Consumed Qty {1} {2} must be less than or equal to Available Qty For Consumption\n" "\t\t\t\t\t{3} {4} in Consumed Items Table." @@ -43932,7 +44055,7 @@ msgstr "" msgid "Row {0}: Cost Center {1} does not belong to Company {2}" msgstr "" -#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:174 +#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:175 msgid "Row {0}: Cost center is required for an item {1}" msgstr "" @@ -43940,7 +44063,7 @@ msgstr "" msgid "Row {0}: Credit entry can not be linked with a {1}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:538 +#: erpnext/manufacturing/doctype/bom/bom.py:578 msgid "Row {0}: Currency of the BOM #{1} should be equal to the selected currency {2}" msgstr "" @@ -43965,7 +44088,7 @@ msgid "Row {0}: Either Delivery Note Item or Packed Item reference is mandatory. msgstr "" #: erpnext/accounts/doctype/journal_entry/journal_entry.py:1018 -#: erpnext/controllers/taxes_and_totals.py:1340 +#: erpnext/controllers/taxes_and_totals.py:1377 msgid "Row {0}: Exchange Rate is mandatory" msgstr "" @@ -43977,7 +44100,7 @@ msgstr "" msgid "Row {0}: Expected Value After Useful Life must be less than Net Purchase Amount" msgstr "" -#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:183 +#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:187 msgid "Row {0}: Expense Account {1} is linked to company {2}. Please select an account belonging to company {3}." msgstr "" @@ -44001,7 +44124,7 @@ msgstr "" msgid "Row {0}: From Time and To Time is mandatory." msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:306 +#: erpnext/manufacturing/doctype/job_card/job_card.py:324 #: erpnext/projects/doctype/timesheet/timesheet.py:225 msgid "Row {0}: From Time and To Time of {1} is overlapping with {2}" msgstr "" @@ -44010,7 +44133,7 @@ msgstr "" msgid "Row {0}: From Warehouse is mandatory for internal transfers" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:297 +#: erpnext/manufacturing/doctype/job_card/job_card.py:315 msgid "Row {0}: From time must be less than to time" msgstr "" @@ -44046,7 +44169,7 @@ msgstr "" msgid "Row {0}: Item {1}'s quantity cannot be higher than the available quantity." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1211 +#: erpnext/manufacturing/doctype/bom/bom.py:1244 msgid "Row {0}: Operation time should be greater than 0 for operation {1}" msgstr "" @@ -44110,7 +44233,7 @@ msgstr "" msgid "Row {0}: Project must be same as the one set in the Timesheet: {1}." msgstr "" -#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:151 +#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:152 msgid "Row {0}: Purchase Invoice {1} has no stock impact." msgstr "" @@ -44142,7 +44265,7 @@ msgstr "" msgid "Row {0}: Shift cannot be changed since the depreciation has already been processed" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1572 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1579 msgid "Row {0}: Subcontracted Item is mandatory for the raw material {1}" msgstr "" @@ -44170,7 +44293,7 @@ msgstr "" msgid "Row {0}: To set {1} periodicity, difference between from and to date must be greater than or equal to {2}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3362 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3394 msgid "Row {0}: Transferred quantity cannot be greater than the requested quantity." msgstr "" @@ -44178,15 +44301,15 @@ msgstr "" msgid "Row {0}: UOM Conversion Factor is mandatory" msgstr "" -#: erpnext/stock/doctype/pick_list/pick_list.py:169 +#: erpnext/stock/doctype/pick_list/pick_list.py:170 msgid "Row {0}: Warehouse is required" msgstr "" -#: erpnext/stock/doctype/pick_list/pick_list.py:178 +#: erpnext/stock/doctype/pick_list/pick_list.py:179 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:1205 +#: erpnext/manufacturing/doctype/bom/bom.py:1238 #: erpnext/manufacturing/doctype/work_order/work_order.py:415 msgid "Row {0}: Workstation or Workstation Type is mandatory for an operation {1}" msgstr "" @@ -44211,11 +44334,11 @@ msgstr "" msgid "Row {0}: {1} {2} does not match with {3}" msgstr "" -#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:133 +#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:134 msgid "Row {0}: {1} {2} is linked to company {3}. Please select a document belonging to company {4}." msgstr "" -#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:107 +#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:108 msgid "Row {0}: {2} Item {1} does not exist in {2} {3}" msgstr "" @@ -44223,7 +44346,7 @@ msgstr "" msgid "Row {1}: Quantity ({0}) cannot be a fraction. To allow this, disable '{2}' in UOM {3}." msgstr "" -#: erpnext/controllers/buying_controller.py:1042 +#: erpnext/controllers/buying_controller.py:1046 msgid "Row {idx}: Asset Naming Series is mandatory for the auto creation of assets for item {item_code}." msgstr "" @@ -44332,7 +44455,7 @@ msgstr "" msgid "SLA Paused On" msgstr "" -#: erpnext/public/js/utils.js:1239 +#: erpnext/public/js/utils.js:1243 msgid "SLA is on hold since {0}" msgstr "" @@ -45279,12 +45402,12 @@ msgstr "" #. Label of the sample_size (Float) field in DocType 'Quality Inspection' #: erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py:93 -#: erpnext/public/js/controllers/transaction.js:2926 +#: erpnext/public/js/controllers/transaction.js:2930 #: erpnext/stock/doctype/quality_inspection/quality_inspection.json msgid "Sample Size" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3852 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3884 msgid "Sample quantity {0} cannot be more than received quantity {1}" msgstr "" @@ -45385,7 +45508,7 @@ msgstr "" msgid "Schedule Date" msgstr "" -#: erpnext/public/js/controllers/transaction.js:486 +#: erpnext/public/js/controllers/transaction.js:490 msgid "Schedule Name" msgstr "" @@ -45487,59 +45610,25 @@ msgstr "" msgid "Scoring Standings" msgstr "" -#. Label of the scrap_section (Tab Break) field in DocType 'BOM' -#: erpnext/manufacturing/doctype/bom/bom.json -msgid "Scrap & Process Loss" +#. Option for the 'Type' (Select) field in DocType 'BOM Secondary Item' +#. Option for the 'Type' (Select) field in DocType 'Job Card Secondary Item' +#. Option for the 'Type' (Select) field in DocType 'Stock Entry Detail' +#. Option for the 'Type' (Select) field in DocType 'Subcontracting Inward Order +#. Secondary Item' +#. Option for the 'Type' (Select) field in DocType 'Subcontracting Receipt +#. Item' +#: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json +#: erpnext/manufacturing/doctype/job_card_secondary_item/job_card_secondary_item.json +#: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +#: erpnext/subcontracting/doctype/subcontracting_inward_order_secondary_item/subcontracting_inward_order_secondary_item.json +#: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +msgid "Scrap" msgstr "" #: erpnext/assets/doctype/asset/asset.js:163 msgid "Scrap Asset" msgstr "" -#. Label of the scrap_cost_per_qty (Float) field in DocType 'Subcontracting -#. Receipt Item' -#: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json -msgid "Scrap Cost Per Qty" -msgstr "" - -#. Label of the item_code (Link) field in DocType 'Job Card Scrap Item' -#: erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.json -msgid "Scrap Item Code" -msgstr "" - -#. Label of the item_name (Data) field in DocType 'Job Card Scrap Item' -#: erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.json -msgid "Scrap Item Name" -msgstr "" - -#. Label of the scrap_items (Table) field in DocType 'BOM' -#. Label of the scrap_items_section (Section Break) field in DocType 'BOM' -#. Label of the scrap_items_section (Tab Break) field in DocType 'Job Card' -#. Label of the scrap_items (Table) field in DocType 'Job Card' -#. Label of the scrap_items (Table) field in DocType 'Subcontracting Inward -#. Order' -#: erpnext/manufacturing/doctype/bom/bom.json -#: erpnext/manufacturing/doctype/job_card/job_card.json -#: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.json -msgid "Scrap Items" -msgstr "" - -#. Label of the scrap_items_generated_section (Section Break) field in DocType -#. 'Subcontracting Inward Order' -#: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.json -msgid "Scrap Items Generated" -msgstr "" - -#. Label of the scrap_material_cost (Currency) field in DocType 'BOM' -#: erpnext/manufacturing/doctype/bom/bom.json -msgid "Scrap Material Cost" -msgstr "" - -#. Label of the base_scrap_material_cost (Currency) field in DocType 'BOM' -#: erpnext/manufacturing/doctype/bom/bom.json -msgid "Scrap Material Cost(Company Currency)" -msgstr "" - #. Label of the scrap_warehouse (Link) field in DocType 'Work Order' #: erpnext/manufacturing/doctype/work_order/work_order.json msgid "Scrap Warehouse" @@ -45594,6 +45683,50 @@ msgstr "" msgid "Second Email" msgstr "" +#. Label of the item_code (Link) field in DocType 'Job Card Secondary Item' +#: erpnext/manufacturing/doctype/job_card_secondary_item/job_card_secondary_item.json +msgid "Secondary Item Code" +msgstr "" + +#. Label of the item_name (Data) field in DocType 'Job Card Secondary Item' +#: erpnext/manufacturing/doctype/job_card_secondary_item/job_card_secondary_item.json +msgid "Secondary Item Name" +msgstr "" + +#. Label of the secondary_items (Table) field in DocType 'BOM' +#. Label of the secondary_items_tab (Tab Break) field in DocType 'BOM' +#. Label of the secondary_items (Table) field in DocType 'Job Card' +#. Label of the secondary_items_section (Tab Break) field in DocType 'Job Card' +#. Label of the secondary_items (Table) field in DocType 'Subcontracting Inward +#. Order' +#: erpnext/manufacturing/doctype/bom/bom.json +#: erpnext/manufacturing/doctype/job_card/job_card.json +#: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.json +msgid "Secondary Items" +msgstr "" + +#. Label of the secondary_items_cost (Currency) field in DocType 'BOM' +#: erpnext/manufacturing/doctype/bom/bom.json +msgid "Secondary Items Cost" +msgstr "" + +#. Label of the base_secondary_items_cost (Currency) field in DocType 'BOM' +#: erpnext/manufacturing/doctype/bom/bom.json +msgid "Secondary Items Cost (Company Currency)" +msgstr "" + +#. Label of the secondary_items_cost_per_qty (Currency) field in DocType +#. 'Subcontracting Receipt Item' +#: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +msgid "Secondary Items Cost Per Qty" +msgstr "" + +#. Label of the scrap_items_generated_section (Section Break) field in DocType +#. 'Subcontracting Inward Order' +#: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.json +msgid "Secondary Items Generated" +msgstr "" + #. Label of the secondary_party (Dynamic Link) field in DocType 'Party Link' #: erpnext/accounts/doctype/party_link/party_link.json msgid "Secondary Party" @@ -45643,7 +45776,7 @@ msgstr "" msgid "Select Accounting Dimension." msgstr "" -#: erpnext/public/js/utils.js:535 +#: erpnext/public/js/utils.js:539 msgid "Select Alternate Item" msgstr "" @@ -45697,7 +45830,7 @@ msgstr "" msgid "Select Company Address" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:541 +#: erpnext/manufacturing/doctype/job_card/job_card.js:539 msgid "Select Corrective Operation" msgstr "" @@ -45758,7 +45891,7 @@ msgstr "" msgid "Select Items based on Delivery Date" msgstr "" -#: erpnext/public/js/controllers/transaction.js:2965 +#: erpnext/public/js/controllers/transaction.js:2969 msgid "Select Items for Quality Inspection" msgstr "" @@ -45788,7 +45921,7 @@ msgstr "" msgid "Select Loyalty Program" msgstr "" -#: erpnext/public/js/controllers/transaction.js:473 +#: erpnext/public/js/controllers/transaction.js:477 msgid "Select Payment Schedule" msgstr "" @@ -45796,7 +45929,7 @@ msgstr "" msgid "Select Possible Supplier" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1043 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1044 #: erpnext/stock/doctype/pick_list/pick_list.js:216 msgid "Select Quantity" msgstr "" @@ -45917,7 +46050,7 @@ msgstr "" msgid "Select item group" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:434 +#: erpnext/manufacturing/doctype/bom/bom.js:473 msgid "Select template item" msgstr "" @@ -45930,11 +46063,11 @@ msgstr "" msgid "Select the Default Workstation where the Operation will be performed. This will be fetched in BOMs and Work Orders." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1145 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1146 msgid "Select the Item to be manufactured." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:966 +#: erpnext/manufacturing/doctype/bom/bom.js:983 msgid "Select the Item to be manufactured. The Item name, UoM, Company, and Currency will be fetched automatically." msgstr "" @@ -45955,11 +46088,11 @@ msgstr "" msgid "Select the date and your timezone" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:985 +#: erpnext/manufacturing/doctype/bom/bom.js:1002 msgid "Select the raw materials (Items) required to manufacture the Item" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:489 +#: erpnext/manufacturing/doctype/bom/bom.js:528 msgid "Select variant item code for the template item {0}" msgstr "" @@ -46090,7 +46223,7 @@ msgstr "" #: erpnext/selling/doctype/selling_settings/selling_settings.json #: erpnext/selling/workspace/selling/selling.json #: erpnext/setup/workspace/erpnext_settings/erpnext_settings.json -#: erpnext/stock/doctype/stock_settings/stock_settings.py:260 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:258 #: erpnext/workspace_sidebar/erpnext_settings.json msgid "Selling Settings" msgstr "" @@ -46148,7 +46281,7 @@ msgid "Send Emails to Suppliers" msgstr "" #. Label of the send_sms (Button) field in DocType 'SMS Center' -#: erpnext/public/js/controllers/transaction.js:692 +#: erpnext/public/js/controllers/transaction.js:696 #: erpnext/selling/doctype/sms_center/sms_center.json msgid "Send SMS" msgstr "" @@ -46232,7 +46365,7 @@ msgstr "" msgid "Serial / Batch No" msgstr "" -#: erpnext/public/js/utils.js:197 +#: erpnext/public/js/utils.js:201 msgid "Serial / Batch Nos" msgstr "" @@ -46284,7 +46417,7 @@ msgstr "" #: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js:74 #: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py:114 -#: erpnext/public/js/controllers/transaction.js:2939 +#: erpnext/public/js/controllers/transaction.js:2943 #: erpnext/public/js/utils/serial_no_batch_selector.js:421 #: erpnext/selling/doctype/installation_note_item/installation_note_item.json #: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -46345,7 +46478,7 @@ msgstr "" msgid "Serial No Range" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2601 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2639 msgid "Serial No Reserved" msgstr "" @@ -46431,7 +46564,7 @@ msgstr "" msgid "Serial No {0} does not exist" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:3391 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:3429 msgid "Serial No {0} does not exists" msgstr "" @@ -46485,11 +46618,11 @@ msgstr "" msgid "Serial Nos and Batches" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1887 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1925 msgid "Serial Nos are created successfully" msgstr "" -#: erpnext/stock/stock_ledger.py:2324 +#: erpnext/stock/stock_ledger.py:2331 msgid "Serial Nos are reserved in Stock Reservation Entries, you need to unreserve them before proceeding." msgstr "" @@ -46566,11 +46699,11 @@ msgstr "" msgid "Serial and Batch Bundle" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2109 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2147 msgid "Serial and Batch Bundle created" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2181 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2219 msgid "Serial and Batch Bundle updated" msgstr "" @@ -46943,12 +47076,12 @@ msgid "Service Stop Date" msgstr "" #: erpnext/accounts/deferred_revenue.py:45 -#: erpnext/public/js/controllers/transaction.js:1775 +#: erpnext/public/js/controllers/transaction.js:1779 msgid "Service Stop Date cannot be after Service End Date" msgstr "" #: erpnext/accounts/deferred_revenue.py:42 -#: erpnext/public/js/controllers/transaction.js:1772 +#: erpnext/public/js/controllers/transaction.js:1776 msgid "Service Stop Date cannot be before Service Start Date" msgstr "" @@ -46987,8 +47120,8 @@ msgstr "" msgid "Set Delivery Warehouse" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:412 -#: erpnext/manufacturing/doctype/job_card/job_card.js:481 +#: erpnext/manufacturing/doctype/job_card/job_card.js:410 +#: erpnext/manufacturing/doctype/job_card/job_card.js:479 msgid "Set Finished Good Quantity" msgstr "" @@ -47033,10 +47166,10 @@ msgstr "" msgid "Set New Release Date" msgstr "" -#. Label of the set_op_cost_and_scrap_from_sub_assemblies (Check) field in -#. DocType 'Manufacturing Settings' +#. Label of the set_op_cost_and_secondary_items_from_sub_assemblies (Check) +#. field in DocType 'Manufacturing Settings' #: erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json -msgid "Set Operating Cost / Scrap Items From Sub-assemblies" +msgid "Set Operating Cost / Secondary Items From Sub-assemblies" msgstr "" #. Label of the set_cost_based_on_bom_qty (Check) field in DocType 'BOM @@ -47054,7 +47187,7 @@ msgstr "" msgid "Set Posting Date" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:1012 +#: erpnext/manufacturing/doctype/bom/bom.js:1029 msgid "Set Process Loss Item Quantity" msgstr "" @@ -47177,7 +47310,7 @@ msgstr "" msgid "Set fieldname from which you want to fetch the data from the parent form." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:1002 +#: erpnext/manufacturing/doctype/bom/bom.js:1019 msgid "Set quantity of process loss item:" msgstr "" @@ -47193,7 +47326,7 @@ msgstr "" msgid "Set targets Item Group-wise for this Sales Person." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1202 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1203 msgid "Set the Planned Start Date (an Estimated Date at which you want the Production to begin)" msgstr "" @@ -47274,7 +47407,7 @@ msgstr "" msgid "Setting Item Locations..." msgstr "" -#: erpnext/setup/setup_wizard/setup_wizard.py:34 +#: erpnext/setup/setup_wizard/setup_wizard.py:25 msgid "Setting defaults" msgstr "" @@ -47284,11 +47417,11 @@ msgstr "" msgid "Setting the account as a Company Account is necessary for Bank Reconciliation" msgstr "" -#: erpnext/setup/setup_wizard/setup_wizard.py:29 +#: erpnext/setup/setup_wizard/setup_wizard.py:20 msgid "Setting up company" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1184 +#: erpnext/manufacturing/doctype/bom/bom.py:1217 #: erpnext/manufacturing/doctype/work_order/work_order.py:1475 msgid "Setting {0} is required" msgstr "" @@ -47707,7 +47840,7 @@ msgstr "" msgid "Show Barcode Field in Stock Transactions" msgstr "" -#: erpnext/accounts/report/general_ledger/general_ledger.js:192 +#: erpnext/accounts/report/general_ledger/general_ledger.js:193 msgid "Show Cancelled Entries" msgstr "" @@ -47715,7 +47848,7 @@ msgstr "" msgid "Show Completed" msgstr "" -#: erpnext/accounts/report/general_ledger/general_ledger.js:202 +#: erpnext/accounts/report/general_ledger/general_ledger.js:203 msgid "Show Credit / Debit in Company Currency" msgstr "" @@ -47798,7 +47931,7 @@ msgstr "" #. Label of the show_net_values_in_party_account (Check) field in DocType #. 'Process Statement Of Accounts' #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json -#: erpnext/accounts/report/general_ledger/general_ledger.js:197 +#: erpnext/accounts/report/general_ledger/general_ledger.js:198 msgid "Show Net Values in Party Account" msgstr "" @@ -47806,7 +47939,7 @@ msgstr "" msgid "Show Open" msgstr "" -#: erpnext/accounts/report/general_ledger/general_ledger.js:181 +#: erpnext/accounts/report/general_ledger/general_ledger.js:182 msgid "Show Opening Entries" msgstr "" @@ -47839,7 +47972,7 @@ msgstr "" #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json #: erpnext/accounts/report/accounts_payable/accounts_payable.js:125 #: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:162 -#: erpnext/accounts/report/general_ledger/general_ledger.js:212 +#: erpnext/accounts/report/general_ledger/general_ledger.js:213 msgid "Show Remarks" msgstr "" @@ -48008,7 +48141,7 @@ msgstr "" msgid "Since there is a process loss of {0} units for the finished good {1}, you should reduce the quantity by {0} units for the finished good {1} in the Items Table." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:317 +#: erpnext/manufacturing/doctype/bom/bom.py:322 msgid "Since you have enabled 'Track Semi Finished Goods', at least one operation must have 'Is Final Finished Good' checked. For that set the FG / Semi FG Item as {0} against an operation." msgstr "" @@ -48189,7 +48322,7 @@ msgstr "" #. Label of the s_warehouse (Link) field in DocType 'Stock Entry Detail' #: erpnext/accounts/doctype/pos_invoice/pos_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json -#: erpnext/manufacturing/doctype/bom/bom.js:461 +#: erpnext/manufacturing/doctype/bom/bom.js:500 #: erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json #: erpnext/manufacturing/doctype/bom_item/bom_item.json #: erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -48382,7 +48515,7 @@ msgstr "" msgid "Stale Days" msgstr "" -#: erpnext/accounts/doctype/accounts_settings/accounts_settings.py:146 +#: erpnext/accounts/doctype/accounts_settings/accounts_settings.py:147 msgid "Stale Days should start from 1." msgstr "" @@ -48403,7 +48536,7 @@ 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:324 -#: erpnext/tests/utils.py:2514 +#: erpnext/tests/utils.py:2522 msgid "Standard Selling" msgstr "" @@ -48755,7 +48888,7 @@ msgstr "" msgid "Stock Entry Type" msgstr "" -#: erpnext/stock/doctype/pick_list/pick_list.py:1475 +#: erpnext/stock/doctype/pick_list/pick_list.py:1508 msgid "Stock Entry has been already created against this Pick List" msgstr "" @@ -48763,7 +48896,7 @@ msgstr "" msgid "Stock Entry {0} created" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1495 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1526 msgid "Stock Entry {0} has created" msgstr "" @@ -48919,6 +49052,7 @@ msgstr "" #. Label of the stock_qty (Float) field in DocType 'BOM Creator Item' #. Label of the stock_qty (Float) field in DocType 'BOM Explosion Item' #. Label of the stock_qty (Float) field in DocType 'BOM Item' +#. Label of the stock_qty (Float) field in DocType 'BOM Secondary Item' #. Label of the stock_qty (Float) field in DocType 'Delivery Schedule Item' #. Label of the stock_qty (Float) field in DocType 'Material Request Item' #: erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py:257 @@ -48926,6 +49060,7 @@ msgstr "" #: erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json #: erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json #: erpnext/manufacturing/doctype/bom_item/bom_item.json +#: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json #: erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json #: erpnext/stock/doctype/material_request_item/material_request_item.json #: erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py:34 @@ -48996,9 +49131,9 @@ msgstr "" #: erpnext/manufacturing/doctype/production_plan/production_plan.js:289 #: erpnext/manufacturing/doctype/production_plan/production_plan.js:297 #: erpnext/manufacturing/doctype/production_plan/production_plan.js:303 -#: erpnext/manufacturing/doctype/work_order/work_order.js:920 -#: erpnext/manufacturing/doctype/work_order/work_order.js:929 -#: erpnext/manufacturing/doctype/work_order/work_order.js:936 +#: erpnext/manufacturing/doctype/work_order/work_order.js:921 +#: erpnext/manufacturing/doctype/work_order/work_order.js:930 +#: erpnext/manufacturing/doctype/work_order/work_order.js:937 #: erpnext/manufacturing/doctype/work_order/work_order_dashboard.py:14 #: erpnext/public/js/stock_reservation.js:12 #: erpnext/selling/doctype/sales_order/sales_order.js:101 @@ -49019,9 +49154,9 @@ msgstr "" #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1699 #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1716 #: erpnext/stock/doctype/stock_settings/stock_settings.json -#: erpnext/stock/doctype/stock_settings/stock_settings.py:217 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:229 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:243 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:215 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:227 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:241 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:182 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:195 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:207 @@ -49034,8 +49169,8 @@ msgstr "" msgid "Stock Reservation Entries Cancelled" msgstr "" -#: erpnext/controllers/subcontracting_inward_controller.py:1003 -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:2243 +#: erpnext/controllers/subcontracting_inward_controller.py:1018 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:2245 #: 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" @@ -49148,9 +49283,9 @@ msgstr "" #. Label of the stock_uom (Link) field in DocType 'BOM Creator Item' #. Label of the stock_uom (Link) field in DocType 'BOM Explosion Item' #. Label of the stock_uom (Link) field in DocType 'BOM Item' -#. Label of the stock_uom (Link) field in DocType 'BOM Scrap Item' +#. Label of the stock_uom (Link) field in DocType 'BOM Secondary Item' #. Label of the stock_uom (Link) field in DocType 'Job Card Item' -#. Label of the stock_uom (Link) field in DocType 'Job Card Scrap Item' +#. Label of the stock_uom (Link) field in DocType 'Job Card Secondary Item' #. Label of the stock_uom (Link) field in DocType 'Production Plan Sub Assembly #. Item' #. Label of the stock_uom (Link) field in DocType 'Work Order' @@ -49174,7 +49309,7 @@ msgstr "" #. Label of the stock_uom (Link) field in DocType 'Subcontracting Inward Order #. Received Item' #. Label of the stock_uom (Link) field in DocType 'Subcontracting Inward Order -#. Scrap Item' +#. Secondary Item' #. Label of the stock_uom (Link) field in DocType 'Subcontracting Order Item' #. Label of the stock_uom (Link) field in DocType 'Subcontracting Order #. Supplied Item' @@ -49195,9 +49330,9 @@ msgstr "" #: erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json #: erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json #: erpnext/manufacturing/doctype/bom_item/bom_item.json -#: erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json +#: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json #: erpnext/manufacturing/doctype/job_card_item/job_card_item.json -#: erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.json +#: erpnext/manufacturing/doctype/job_card_secondary_item/job_card_secondary_item.json #: erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json #: erpnext/manufacturing/doctype/work_order/work_order.json #: erpnext/manufacturing/doctype/work_order_item/work_order_item.json @@ -49221,7 +49356,7 @@ msgstr "" #: erpnext/stock/report/stock_ledger/stock_ledger.py:279 #: erpnext/subcontracting/doctype/subcontracting_inward_order_item/subcontracting_inward_order_item.json #: erpnext/subcontracting/doctype/subcontracting_inward_order_received_item/subcontracting_inward_order_received_item.json -#: erpnext/subcontracting/doctype/subcontracting_inward_order_scrap_item/subcontracting_inward_order_scrap_item.json +#: erpnext/subcontracting/doctype/subcontracting_inward_order_secondary_item/subcontracting_inward_order_secondary_item.json #: erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json #: erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.json #: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -49467,7 +49602,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:357 +#: erpnext/manufacturing/doctype/job_card/job_card.js:355 #: erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json #: erpnext/manufacturing/doctype/sub_operation/sub_operation.json msgid "Sub Operation" @@ -49679,8 +49814,8 @@ msgid "Subcontracting Inward Order Received Item" msgstr "" #. Name of a DocType -#: erpnext/subcontracting/doctype/subcontracting_inward_order_scrap_item/subcontracting_inward_order_scrap_item.json -msgid "Subcontracting Inward Order Scrap Item" +#: erpnext/subcontracting/doctype/subcontracting_inward_order_secondary_item/subcontracting_inward_order_secondary_item.json +msgid "Subcontracting Inward Order Secondary Item" msgstr "" #. Name of a DocType @@ -49743,7 +49878,7 @@ msgstr "" msgid "Subcontracting Order Supplied Item" msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:920 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:924 msgid "Subcontracting Order {0} created." msgstr "" @@ -49832,8 +49967,8 @@ msgstr "" msgid "Subdivision" msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:916 -#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:1047 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:920 +#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:1084 msgid "Submit Action Failed" msgstr "" @@ -50943,7 +51078,7 @@ msgstr "" msgid "Target Warehouse Reservation Error" msgstr "" -#: erpnext/controllers/subcontracting_inward_controller.py:230 +#: erpnext/controllers/subcontracting_inward_controller.py:232 msgid "Target Warehouse for Finished Good must be same as Finished Good Warehouse {1} in Work Order {2} linked to the Subcontracting Inward Order." msgstr "" @@ -51189,7 +51324,7 @@ msgstr "" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js:86 -#: erpnext/accounts/report/general_ledger/general_ledger.js:141 +#: erpnext/accounts/report/general_ledger/general_ledger.js:142 #: erpnext/accounts/report/purchase_register/purchase_register.py:192 #: erpnext/accounts/report/sales_register/sales_register.py:215 #: erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js:67 @@ -51432,7 +51567,7 @@ msgstr "" #. Detail' #: erpnext/accounts/doctype/item_wise_tax_detail/item_wise_tax_detail.json #: erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py:157 -#: erpnext/controllers/taxes_and_totals.py:1212 +#: erpnext/controllers/taxes_and_totals.py:1249 msgid "Taxable Amount" msgstr "" @@ -51638,7 +51773,7 @@ msgstr "" msgid "Television" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:413 +#: erpnext/manufacturing/doctype/bom/bom.js:452 msgid "Template Item" msgstr "" @@ -52013,11 +52148,11 @@ msgstr "" msgid "The Payment Term at row {0} is possibly a duplicate." msgstr "" -#: erpnext/stock/doctype/pick_list/pick_list.py:341 +#: erpnext/stock/doctype/pick_list/pick_list.py:342 msgid "The Pick List having Stock Reservation Entries cannot be updated. If you need to make changes, we recommend canceling the existing Stock Reservation Entries before updating the Pick List." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2609 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2621 msgid "The Process Loss Qty has reset as per job cards Process Loss Qty" msgstr "" @@ -52025,15 +52160,15 @@ msgstr "" msgid "The Sales Person is linked with {0}" msgstr "" -#: erpnext/stock/doctype/pick_list/pick_list.py:207 +#: erpnext/stock/doctype/pick_list/pick_list.py:208 msgid "The Serial No at Row #{0}: {1} is not available in warehouse {2}." msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2598 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2636 msgid "The Serial No {0} is reserved against the {1} {2} and cannot be used for any other transaction." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1742 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1747 msgid "The Serial and Batch Bundle {0} is not valid for this transaction. The 'Type of Transaction' should be 'Outward' instead of 'Inward' in Serial and Batch Bundle {0}" msgstr "" @@ -52059,7 +52194,7 @@ msgstr "" msgid "The batch {0} is already reserved in {1} {2}. So, cannot proceed with the {3} {4}, which is created against the {5} {6}." msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1301 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1319 msgid "The completed quantity {0} of an operation {1} cannot be greater than the completed quantity {2} of a previous operation {3}." msgstr "" @@ -52071,7 +52206,7 @@ msgstr "" msgid "The current POS opening entry is outdated. Please close it and create a new one." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1150 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1151 msgid "The default BOM for that item will be fetched by the system. You can also change the BOM." msgstr "" @@ -52124,7 +52259,7 @@ msgstr "" msgid "The following assets have failed to automatically post depreciation entries: {0}" msgstr "" -#: erpnext/stock/doctype/pick_list/pick_list.py:305 +#: erpnext/stock/doctype/pick_list/pick_list.py:306 msgid "The following batches are expired, please restock them:
{0}" msgstr "" @@ -52167,7 +52302,7 @@ msgstr "" msgid "The holiday on {0} is not between From Date and To Date" msgstr "" -#: erpnext/controllers/buying_controller.py:1244 +#: erpnext/controllers/buying_controller.py:1248 msgid "The item {item} is not marked as {type_of} item. You can enable it as {type_of} item from its Item master." msgstr "" @@ -52175,7 +52310,7 @@ msgstr "" msgid "The items {0} and {1} are present in the following {2} :" msgstr "" -#: erpnext/controllers/buying_controller.py:1237 +#: erpnext/controllers/buying_controller.py:1241 msgid "The items {items} are not marked as {type_of} item. You can enable them as {type_of} item from their Item masters." msgstr "" @@ -52257,7 +52392,7 @@ msgstr "" msgid "The percentage you are allowed to transfer more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed transfer 110 units." msgstr "" -#: erpnext/public/js/utils.js:947 +#: erpnext/public/js/utils.js:951 msgid "The reserved stock will be released when you update items. Are you certain you wish to proceed?" msgstr "" @@ -52310,7 +52445,7 @@ msgstr "" msgid "The shares don't exist with the {0}" msgstr "" -#: erpnext/stock/stock_ledger.py:806 +#: erpnext/stock/stock_ledger.py:813 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 "" @@ -52376,23 +52511,23 @@ msgstr "" msgid "The value {0} is already assigned to an existing Item {1}." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1178 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1179 msgid "The warehouse where you store finished Items before they are shipped." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1171 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1172 msgid "The warehouse where you store your raw materials. Each required item can have a separate source warehouse. Group warehouse also can be selected as source warehouse. On submission of the Work Order, the raw materials will be reserved in these warehouses for production usage." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1183 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1184 msgid "The warehouse where your Items will be transferred when you begin production. Group Warehouse can also be selected as a Work in Progress warehouse." msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:874 +#: erpnext/manufacturing/doctype/job_card/job_card.py:892 msgid "The {0} ({1}) must be equal to {2} ({3})" msgstr "" -#: erpnext/public/js/controllers/transaction.js:3408 +#: erpnext/public/js/controllers/transaction.js:3412 msgid "The {0} contains Unit Price Items." msgstr "" @@ -52408,7 +52543,7 @@ msgstr "" msgid "The {0} {1} does not match with the {0} {2} in the {3} {4}" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:977 +#: erpnext/manufacturing/doctype/job_card/job_card.py:995 msgid "The {0} {1} is used to calculate the valuation cost for the finished good {2}." msgstr "" @@ -52432,7 +52567,7 @@ msgstr "" msgid "There are no Failed transactions" msgstr "" -#: erpnext/setup/demo.py:120 +#: erpnext/setup/demo.py:130 msgid "There are no active Fiscal Years for which Demo Data can be generated." msgstr "" @@ -52472,7 +52607,7 @@ msgstr "" msgid "There is no batch found against the {0}: {1}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1679 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1684 msgid "There must be atleast 1 Finished Good in this Stock Entry" msgstr "" @@ -52515,7 +52650,7 @@ msgstr "" msgid "This Month's Summary" msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:929 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:933 msgid "This Purchase Order has been fully subcontracted." msgstr "" @@ -52561,7 +52696,7 @@ msgstr "" msgid "This invoice has already been paid." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:269 +#: erpnext/manufacturing/doctype/bom/bom.js:307 msgid "This is a Template BOM and will be used to make the work order for {0} of the item {1}" msgstr "" @@ -52638,7 +52773,7 @@ msgstr "" msgid "This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1164 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1165 msgid "This is enabled by default. If you want to plan materials for sub-assemblies of the Item you're manufacturing leave this enabled. If you plan and manufacture the sub-assemblies separately, you can disable this checkbox." msgstr "" @@ -52855,7 +52990,7 @@ msgstr "" msgid "Time in mins." msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:853 +#: erpnext/manufacturing/doctype/job_card/job_card.py:871 msgid "Time logs are required for {0} {1}" msgstr "" @@ -53183,7 +53318,7 @@ msgstr "" msgid "To Warehouse (Optional)" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:980 +#: erpnext/manufacturing/doctype/bom/bom.js:997 msgid "To add Operations tick the 'With Operations' checkbox." msgstr "" @@ -53235,10 +53370,10 @@ msgstr "" msgid "To include non-stock items in the material request planning. i.e. Items for which 'Maintain Stock' checkbox is unticked." msgstr "" -#. Description of the 'Set Operating Cost / Scrap Items From Sub-assemblies' -#. (Check) field in DocType 'Manufacturing Settings' +#. Description of the 'Set Operating Cost / Secondary Items From +#. Sub-assemblies' (Check) field in DocType 'Manufacturing Settings' #: erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json -msgid "To include sub-assembly costs and scrap items in Finished Goods on a work order without using a job card, when the 'Use Multi-Level BOM' option is enabled." +msgid "To include sub-assembly costs and secondary items in Finished Goods on a work order without using a job card, when the 'Use Multi-Level BOM' option is enabled." msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2247 @@ -53442,7 +53577,7 @@ msgstr "" msgid "Total Amount in Words" msgstr "" -#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:258 +#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:262 msgid "Total Applicable Charges in Purchase Receipt Items table must be same as Total Taxes and Charges" msgstr "" @@ -53527,12 +53662,12 @@ msgstr "" #. Label of the total_completed_qty (Float) field in DocType 'Job Card' #: erpnext/manufacturing/doctype/job_card/job_card.json -#: erpnext/manufacturing/doctype/job_card/job_card.py:870 +#: erpnext/manufacturing/doctype/job_card/job_card.py:888 #: erpnext/manufacturing/report/job_card_summary/job_card_summary.py:174 msgid "Total Completed Qty" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:188 +#: erpnext/manufacturing/doctype/job_card/job_card.py:190 msgid "Total Completed Qty is required for Job Card {0}, please start and complete the job card before submission" msgstr "" @@ -54021,7 +54156,7 @@ msgstr "" msgid "Total Time in Mins" msgstr "" -#: erpnext/public/js/utils.js:173 +#: erpnext/public/js/utils.js:177 msgid "Total Unpaid: {0}" msgstr "" @@ -54100,7 +54235,7 @@ msgstr "" msgid "Total allocated percentage for sales team should be 100" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:192 +#: erpnext/selling/doctype/customer/customer.py:193 msgid "Total contribution percentage should be equal to 100" msgstr "" @@ -54136,7 +54271,7 @@ msgstr "" msgid "Total {0} ({1})" msgstr "" -#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:239 +#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:243 msgid "Total {0} for all items is zero, may be you should change 'Distribute Charges Based On'" msgstr "" @@ -54368,7 +54503,7 @@ msgstr "" msgid "Transaction from which tax is withheld" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:846 +#: erpnext/manufacturing/doctype/job_card/job_card.py:864 msgid "Transaction not allowed against stopped Work Order {0}" msgstr "" @@ -54724,6 +54859,7 @@ msgstr "" #. Label of the uom (Link) field in DocType 'BOM Creator' #. Label of the uom (Link) field in DocType 'BOM Creator Item' #. Label of the uom (Link) field in DocType 'BOM Item' +#. Label of the uom (Link) field in DocType 'BOM Secondary Item' #. Label of the uom (Link) field in DocType 'Job Card Item' #. Label of the uom (Link) field in DocType 'Master Production Schedule Item' #. Label of the uom (Link) field in DocType 'Material Request Plan Item' @@ -54772,6 +54908,7 @@ msgstr "" #: erpnext/manufacturing/doctype/bom_creator/bom_creator.json #: erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json #: erpnext/manufacturing/doctype/bom_item/bom_item.json +#: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json #: erpnext/manufacturing/doctype/job_card_item/job_card_item.json #: erpnext/manufacturing/doctype/master_production_schedule_item/master_production_schedule_item.json #: erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json @@ -54781,7 +54918,7 @@ msgstr "" #: erpnext/manufacturing/doctype/workstation/workstation.js:480 #: erpnext/manufacturing/report/bom_explorer/bom_explorer.py:70 #: erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py:110 -#: erpnext/public/js/stock_analytics.js:94 erpnext/public/js/utils.js:820 +#: erpnext/public/js/stock_analytics.js:94 erpnext/public/js/utils.js:824 #: erpnext/quality_management/doctype/quality_goal_objective/quality_goal_objective.json #: erpnext/quality_management/doctype/quality_review_objective/quality_review_objective.json #: erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json @@ -54808,7 +54945,7 @@ msgstr "" #: erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py:87 #: erpnext/stock/report/item_prices/item_prices.py:55 #: erpnext/stock/report/product_bundle_balance/product_bundle_balance.py:94 -#: erpnext/stock/report/stock_ageing/stock_ageing.py:178 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:179 #: erpnext/stock/report/stock_analytics/stock_analytics.py:59 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:134 #: erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py:60 @@ -54877,7 +55014,7 @@ msgstr "" msgid "UOM Name" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3774 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3806 msgid "UOM conversion factor required for UOM: {0} in Item: {1}" msgstr "" @@ -55191,7 +55328,7 @@ msgstr "" msgid "Unreconciled Entries" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:927 +#: erpnext/manufacturing/doctype/work_order/work_order.js:928 #: erpnext/selling/doctype/sales_order/sales_order.js:114 #: erpnext/stock/doctype/pick_list/pick_list.js:158 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:193 @@ -55372,7 +55509,7 @@ msgstr "" #. Option for the 'Update Type' (Select) field in DocType 'BOM Update Log' #. Label of the update_cost_section (Section Break) field in DocType 'BOM #. Update Tool' -#: erpnext/manufacturing/doctype/bom/bom.js:185 +#: erpnext/manufacturing/doctype/bom/bom.js:223 #: erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json #: erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.json msgid "Update Cost" @@ -55399,7 +55536,7 @@ msgstr "" #: erpnext/buying/doctype/purchase_order/purchase_order.js:324 #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.js:43 -#: erpnext/public/js/utils.js:926 +#: erpnext/public/js/utils.js:930 #: erpnext/selling/doctype/quotation/quotation.js:135 #: erpnext/selling/doctype/sales_order/sales_order.js:82 #: erpnext/selling/doctype/sales_order/sales_order.js:940 @@ -55501,7 +55638,7 @@ msgstr "" msgid "Updating Variants..." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1126 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1127 msgid "Updating Work Order status" msgstr "" @@ -55622,7 +55759,7 @@ msgstr "" #. Label of the use_multi_level_bom (Check) field in DocType 'Work Order' #. Label of the use_multi_level_bom (Check) field in DocType 'Stock Entry' -#: erpnext/manufacturing/doctype/bom/bom.js:395 +#: erpnext/manufacturing/doctype/bom/bom.js:434 #: erpnext/manufacturing/doctype/work_order/work_order.json #: erpnext/stock/doctype/stock_entry/stock_entry.json msgid "Use Multi-Level BOM" @@ -56053,11 +56190,11 @@ msgstr "" msgid "Valuation Rate (In / Out)" msgstr "" -#: erpnext/stock/stock_ledger.py:2069 +#: erpnext/stock/stock_ledger.py:2076 msgid "Valuation Rate Missing" msgstr "" -#: erpnext/stock/stock_ledger.py:2047 +#: erpnext/stock/stock_ledger.py:2054 msgid "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}." msgstr "" @@ -56101,7 +56238,7 @@ msgstr "" msgid "Value (G - D)" msgstr "" -#: erpnext/stock/report/stock_ageing/stock_ageing.py:221 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:222 msgid "Value ({0})" msgstr "" @@ -56229,7 +56366,7 @@ msgstr "" msgid "Variant Attributes" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:226 +#: erpnext/manufacturing/doctype/bom/bom.js:264 msgid "Variant BOM" msgstr "" @@ -56251,8 +56388,8 @@ msgstr "" msgid "Variant Field" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:349 -#: erpnext/manufacturing/doctype/bom/bom.js:428 +#: erpnext/manufacturing/doctype/bom/bom.js:387 +#: erpnext/manufacturing/doctype/bom/bom.js:467 msgid "Variant Item" msgstr "" @@ -57006,7 +57143,7 @@ msgstr "" msgid "Warning - Row {0}: Billing Hours are more than Actual Hours" msgstr "" -#: erpnext/stock/stock_ledger.py:816 +#: erpnext/stock/stock_ledger.py:823 msgid "Warning on Negative Stock" msgstr "" @@ -57431,7 +57568,7 @@ msgstr "" #. Option for the 'From Voucher Type' (Select) field in DocType 'Stock #. Reservation Entry' #. Label of a Workspace Sidebar Item -#: erpnext/manufacturing/doctype/bom/bom.js:217 +#: erpnext/manufacturing/doctype/bom/bom.js:255 #: erpnext/manufacturing/doctype/bom/bom.json #: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/manufacturing/doctype/work_order/work_order.json @@ -57527,8 +57664,8 @@ msgstr "" msgid "Work Order cannot be raised against a Item Template" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:2456 -#: erpnext/manufacturing/doctype/work_order/work_order.py:2536 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2457 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2537 msgid "Work Order has been {0}" msgstr "" @@ -57882,7 +58019,7 @@ msgstr "" msgid "You are not authorized to set Frozen value" msgstr "" -#: erpnext/stock/doctype/pick_list/pick_list.py:512 +#: erpnext/stock/doctype/pick_list/pick_list.py:513 msgid "You are picking more than required quantity for the item {0}. Check if there is any other pick list created for the sales order {1}." msgstr "" @@ -57931,7 +58068,7 @@ msgstr "" msgid "You can use {0} to reconcile against {1} later." msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1313 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1331 msgid "You can't make any changes to Job Card since Work Order is closed." msgstr "" @@ -57943,7 +58080,7 @@ msgstr "" msgid "You can't redeem Loyalty Points having more value than the Total Amount." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:736 +#: erpnext/manufacturing/doctype/bom/bom.js:773 msgid "You cannot change the rate if BOM is mentioned against any Item." msgstr "" @@ -57971,7 +58108,7 @@ msgstr "" msgid "You cannot edit root node." msgstr "" -#: erpnext/accounts/doctype/accounts_settings/accounts_settings.py:181 +#: erpnext/accounts/doctype/accounts_settings/accounts_settings.py:182 msgid "You cannot enable both the settings '{0}' and '{1}'." msgstr "" @@ -58003,7 +58140,7 @@ msgstr "" msgid "You cannot {0} this document because another Period Closing Entry {1} exists after {2}" msgstr "" -#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:566 +#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:565 msgid "You do not have permission to edit this document" msgstr "" @@ -58019,11 +58156,11 @@ msgstr "" msgid "You don't have enough points to redeem." 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:291 msgid "You had {} errors while creating opening invoices. Check {} for more details" msgstr "" -#: erpnext/public/js/utils.js:1026 +#: erpnext/public/js/utils.js:1030 msgid "You have already selected items from {0} {1}" msgstr "" @@ -58031,7 +58168,7 @@ msgstr "" msgid "You have been invited to collaborate on the project {0}." msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:255 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:253 msgid "You have enabled {0} and {1} in {2}. This can lead to prices from the default price list being inserted in the transaction price list." msgstr "" @@ -58127,7 +58264,7 @@ msgstr "" msgid "`Allow Negative rates for Items`" msgstr "" -#: erpnext/stock/stock_ledger.py:2061 +#: erpnext/stock/stock_ledger.py:2068 msgid "after" msgstr "" @@ -58143,11 +58280,11 @@ msgstr "" msgid "as Title" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:1004 +#: erpnext/manufacturing/doctype/bom/bom.js:1021 msgid "as a percentage of finished item quantity" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1518 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1556 msgid "as of {0}" msgstr "" @@ -58296,7 +58433,7 @@ msgstr "" msgid "paid to" msgstr "" -#: erpnext/public/js/utils.js:443 +#: erpnext/public/js/utils.js:447 msgid "payments app is not installed. Please install it from {0} or {1}" msgstr "" @@ -58317,7 +58454,7 @@ msgstr "" msgid "per hour" msgstr "" -#: erpnext/stock/stock_ledger.py:2062 +#: erpnext/stock/stock_ledger.py:2069 msgid "performing either one below:" msgstr "" @@ -58447,7 +58584,7 @@ msgstr "" msgid "{0} ({1}) cannot be greater than planned quantity ({2}) in Work Order {3}" msgstr "" -#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:381 +#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:385 msgid "{0} {1} has submitted Assets. Remove Item {2} from table to continue." msgstr "" @@ -58479,11 +58616,11 @@ msgstr "" msgid "{0} Number {1} is already used in {2} {3}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1638 +#: erpnext/manufacturing/doctype/bom/bom.py:1684 msgid "{0} Operating Cost for operation {1}" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:555 +#: erpnext/manufacturing/doctype/work_order/work_order.js:556 msgid "{0} Operations: {1}" msgstr "" @@ -58562,7 +58699,7 @@ msgstr "" #: 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/stock/doctype/pick_list/pick_list.py:1330 #: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py:322 msgid "{0} created" msgstr "" @@ -58668,11 +58805,11 @@ msgstr "" msgid "{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}." msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1742 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1780 msgid "{0} is not a CSV file." msgstr "" -#: erpnext/selling/doctype/customer/customer.py:234 +#: erpnext/selling/doctype/customer/customer.py:235 msgid "{0} is not a company bank account" msgstr "" @@ -58716,27 +58853,27 @@ msgstr "" msgid "{0} is open. Close the POS or cancel the existing POS Opening Entry to create a new POS Opening Entry." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:520 +#: erpnext/manufacturing/doctype/work_order/work_order.js:521 msgid "{0} items disassembled" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:484 +#: erpnext/manufacturing/doctype/work_order/work_order.js:485 msgid "{0} items in progress" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:508 +#: erpnext/manufacturing/doctype/work_order/work_order.js:509 msgid "{0} items lost during process." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:465 +#: erpnext/manufacturing/doctype/work_order/work_order.js:466 msgid "{0} items produced" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:488 +#: erpnext/manufacturing/doctype/work_order/work_order.js:489 msgid "{0} items returned" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:491 +#: erpnext/manufacturing/doctype/work_order/work_order.js:492 msgid "{0} items to return" msgstr "" @@ -58748,7 +58885,7 @@ msgstr "" msgid "{0} not allowed to transact with {1}. Please change the Company or add the Company in the 'Allowed To Transact With'-Section in the Customer record." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:573 +#: erpnext/manufacturing/doctype/bom/bom.py:611 msgid "{0} not found for item {1}" msgstr "" @@ -58768,11 +58905,11 @@ msgstr "" msgid "{0} units are reserved for Item {1} in Warehouse {2}, please un-reserve the same to {3} the Stock Reconciliation." msgstr "" -#: erpnext/stock/doctype/pick_list/pick_list.py:1052 +#: erpnext/stock/doctype/pick_list/pick_list.py:1085 msgid "{0} units of Item {1} is not available in any of the warehouses." msgstr "" -#: erpnext/stock/doctype/pick_list/pick_list.py:1045 +#: erpnext/stock/doctype/pick_list/pick_list.py:1078 msgid "{0} units of Item {1} is not available in any of the warehouses. Other Pick Lists exist for this item." msgstr "" @@ -58780,16 +58917,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:1714 erpnext/stock/stock_ledger.py:2210 -#: erpnext/stock/stock_ledger.py:2224 +#: erpnext/stock/stock_ledger.py:1721 erpnext/stock/stock_ledger.py:2217 +#: erpnext/stock/stock_ledger.py:2231 msgid "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction." msgstr "" -#: erpnext/stock/stock_ledger.py:2311 erpnext/stock/stock_ledger.py:2356 +#: erpnext/stock/stock_ledger.py:2318 erpnext/stock/stock_ledger.py:2363 msgid "{0} units of {1} needed in {2} on {3} {4} to complete this transaction." msgstr "" -#: erpnext/stock/stock_ledger.py:1708 +#: erpnext/stock/stock_ledger.py:1715 msgid "{0} units of {1} needed in {2} to complete this transaction." msgstr "" @@ -58817,7 +58954,7 @@ msgstr "" msgid "{0} will be set as the {1} in subsequently scanned items" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:986 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1004 msgid "{0} {1}" msgstr "" @@ -59015,8 +59152,8 @@ msgstr "" msgid "{0}'s {1} cannot be after {2}'s Expected End Date." msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1285 -#: erpnext/manufacturing/doctype/job_card/job_card.py:1293 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1303 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1311 msgid "{0}, complete the operation {1} before the operation {2}." msgstr "" @@ -59056,15 +59193,15 @@ msgstr "" msgid "{0}: {1} must be less than {2}" msgstr "" -#: erpnext/controllers/buying_controller.py:1019 +#: erpnext/controllers/buying_controller.py:1023 msgid "{count} Assets created for {item_code}" msgstr "" -#: erpnext/controllers/buying_controller.py:917 +#: erpnext/controllers/buying_controller.py:921 msgid "{doctype} {name} is cancelled or closed." msgstr "" -#: erpnext/controllers/buying_controller.py:628 +#: erpnext/controllers/buying_controller.py:632 msgid "{field_label} is mandatory for sub-contracted {doctype}." msgstr "" @@ -59072,7 +59209,7 @@ msgstr "" msgid "{item_name}'s Sample Size ({sample_size}) cannot be greater than the Accepted Quantity ({accepted_quantity})" msgstr "" -#: erpnext/controllers/buying_controller.py:725 +#: erpnext/controllers/buying_controller.py:729 msgid "{ref_doctype} {ref_name} is {status}." msgstr "" From bc86e2c1f2fe81a4638babb0690937c9d2e7b421 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 5 Apr 2026 21:16:31 +0530 Subject: [PATCH 31/79] fix: update min date based on transaction_date (backport #53803) (#54025) Co-authored-by: Vishnu Priya Baskaran <145791817+ervishnucs@users.noreply.github.com> fix: update min date based on transaction_date (#53803) --- erpnext/selling/doctype/sales_order/sales_order.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index ff22b0e4c2e..82926bd3855 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -63,6 +63,13 @@ frappe.ui.form.on("Sales Order", { }); } }, + transaction_date(frm) { + prevent_past_delivery_dates(frm); + frm.set_value("delivery_date", ""); + frm.doc.items.forEach((d) => { + frappe.model.set_value(d.doctype, d.name, "delivery_date", ""); + }); + }, refresh: function (frm) { frm.fields_dict["items"].grid.update_docfield_property( From 89e3e3c59e0a6e6ceaee5d3688018744dc4ec9cf Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sat, 4 Apr 2026 12:47:43 +0530 Subject: [PATCH 32/79] fix: do not repost GL if no change in valuation (cherry picked from commit bb53cce22890fdffb7708d2c844ca858ed274b83) --- erpnext/stock/stock_ledger.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index c9114355a58..f754cab7650 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -688,9 +688,6 @@ class update_entries_after: 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) @@ -953,6 +950,8 @@ class update_entries_after: sle.stock_value = self.wh_data.stock_value sle.stock_queue = json.dumps(self.wh_data.stock_queue) + old_stock_value_difference = sle.stock_value_difference + sle.stock_value_difference = stock_value_difference if ( @@ -986,6 +985,14 @@ class update_entries_after: ): self.update_outgoing_rate_on_transaction(sle) + if flt(old_stock_value_difference, self.currency_precision) == flt( + sle.stock_value_difference, self.currency_precision + ): + return + + 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)) + def get_serialized_values(self, sle): from erpnext.stock.serial_batch_bundle import SerialNoValuation From ab08162f34ce2cc1f68316757fbabd0cd59d17a1 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 05:42:53 +0000 Subject: [PATCH 33/79] fix: show current stock qty in Stock Entry PDF (backport #53761) (#54032) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 19c00ceacea..8078abb2848 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -201,6 +201,13 @@ class StockEntry(StockController, SubcontractingInwardController): ) def onload(self): + self.update_items_from_bin_details() + + def before_print(self, settings=None): + super().before_print(settings) + self.update_items_from_bin_details() + + def update_items_from_bin_details(self): for item in self.get("items"): item.update(get_bin_details(item.item_code, item.s_warehouse or item.t_warehouse)) From 8941699a34aa07103dd8e469ecc95b1aec26adb5 Mon Sep 17 00:00:00 2001 From: Sagar Vora <16315650+sagarvora@users.noreply.github.com> Date: Mon, 6 Apr 2026 12:33:49 +0530 Subject: [PATCH 34/79] fix: skip discount amount validation when not saving (cherry picked from commit 09755833881742fe6f14bddd3c215f5029ab1d28) --- erpnext/controllers/taxes_and_totals.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index ecab30b5013..7f41e8476ea 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -825,7 +825,8 @@ class calculate_taxes_and_totals: discount_amount += total_return_discount # validate that discount amount cannot exceed the total before discount - if ( + # only during save (i.e. when `_action` is set) + if self.doc.get("_action") and ( (grand_total >= 0 and discount_amount > grand_total) or (grand_total < 0 and discount_amount < grand_total) # returns ): From 9bc0e3b2ce28ffafb2079166a52394205d93bda1 Mon Sep 17 00:00:00 2001 From: Sagar Vora <16315650+sagarvora@users.noreply.github.com> Date: Mon, 6 Apr 2026 12:37:32 +0530 Subject: [PATCH 35/79] test: add test for discount amount on partial purchase receipt Co-authored-by: ravibharathi656 <131471282+ravibharathi656@users.noreply.github.com> (cherry picked from commit 135cb5fd670ddd2aa1642282fd5aad300fd006ff) --- .../purchase_order/test_purchase_order.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index e6956111ea0..da06d3fa04a 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -291,6 +291,30 @@ class TestPurchaseOrder(ERPNextTestSuite): # ordered qty should decrease (back to initial) on row deletion self.assertEqual(get_ordered_qty(), existing_ordered_qty) + def test_discount_amount_partial_purchase_receipt(self): + po = create_purchase_order(qty=4, rate=100, do_not_save=1) + po.apply_discount_on = "Grand Total" + po.discount_amount = 120 + po.save() + po.submit() + + self.assertEqual(po.grand_total, 280) + + pr1 = make_purchase_receipt(po.name) + pr1.items[0].qty = 3 + pr1.save() + pr1.submit() + + self.assertEqual(pr1.discount_amount, 120) + self.assertEqual(pr1.grand_total, 180) + + pr2 = make_purchase_receipt(po.name) + pr2.save() + pr2.submit() + + self.assertEqual(pr2.discount_amount, 0) + self.assertEqual(pr2.grand_total, 100) + def test_update_child_perm(self): po = create_purchase_order(item_code="_Test Item", qty=4) From cd983120834802b5a67c30fb0309183c14094f63 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Mon, 6 Apr 2026 14:35:01 +0530 Subject: [PATCH 36/79] fix: print hide unnecessary fields (cherry picked from commit 8f83616b60df061b75d5dc57cd35dfa0b1e54e3a) # Conflicts: # erpnext/selling/doctype/sales_order_item/sales_order_item.json --- .../purchase_invoice_item.json | 41 +++++++++++++------ .../sales_invoice_item.json | 14 +++++-- .../purchase_order_item.json | 18 +++++--- .../sales_order_item/sales_order_item.json | 22 ++++++++-- .../purchase_receipt_item.json | 26 ++++++++---- 5 files changed, 88 insertions(+), 33 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 00951fdd4cc..7a55a5eb141 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -190,6 +190,7 @@ "fieldtype": "Float", "label": "Received Qty", "no_copy": 1, + "print_hide": 1, "read_only": 1 }, { @@ -206,7 +207,8 @@ { "fieldname": "rejected_qty", "fieldtype": "Float", - "label": "Rejected Qty" + "label": "Rejected Qty", + "print_hide": 1 }, { "depends_on": "eval:doc.uom != doc.stock_uom", @@ -226,6 +228,7 @@ "fieldtype": "Link", "label": "UOM", "options": "UOM", + "print_hide": 1, "reqd": 1 }, { @@ -261,14 +264,16 @@ "depends_on": "price_list_rate", "fieldname": "discount_percentage", "fieldtype": "Percent", - "label": "Discount on Price List Rate (%)" + "label": "Discount on Price List Rate (%)", + "print_hide": 1 }, { "depends_on": "price_list_rate", "fieldname": "discount_amount", "fieldtype": "Currency", "label": "Discount Amount", - "options": "currency" + "options": "currency", + "print_hide": 1 }, { "fieldname": "col_break3", @@ -401,12 +406,14 @@ { "fieldname": "weight_per_unit", "fieldtype": "Float", - "label": "Weight Per Unit" + "label": "Weight Per Unit", + "print_hide": 1 }, { "fieldname": "total_weight", "fieldtype": "Float", "label": "Total Weight", + "print_hide": 1, "read_only": 1 }, { @@ -417,7 +424,8 @@ "fieldname": "weight_uom", "fieldtype": "Link", "label": "Weight UOM", - "options": "UOM" + "options": "UOM", + "print_hide": 1 }, { "depends_on": "eval:parent.update_stock", @@ -429,7 +437,8 @@ "fieldname": "warehouse", "fieldtype": "Link", "label": "Accepted Warehouse", - "options": "Warehouse" + "options": "Warehouse", + "print_hide": 1 }, { "fieldname": "rejected_warehouse", @@ -674,7 +683,8 @@ "fieldname": "asset_location", "fieldtype": "Link", "label": "Asset Location", - "options": "Location" + "options": "Location", + "print_hide": 1 }, { "fieldname": "po_detail", @@ -796,6 +806,7 @@ "fieldtype": "Link", "label": "Asset Category", "options": "Asset Category", + "print_hide": 1, "read_only": 1 }, { @@ -828,6 +839,7 @@ "label": "Rate of Stock UOM", "no_copy": 1, "options": "currency", + "print_hide": 1, "read_only": 1 }, { @@ -866,6 +878,7 @@ "fieldtype": "Currency", "label": "Rate With Margin", "options": "currency", + "print_hide": 1, "read_only": 1 }, { @@ -892,7 +905,8 @@ "default": "1", "fieldname": "apply_tds", "fieldtype": "Check", - "label": "Consider for Tax Withholding" + "label": "Consider for Tax Withholding", + "print_hide": 1 }, { "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", @@ -918,7 +932,8 @@ "fieldname": "wip_composite_asset", "fieldtype": "Link", "label": "WIP Composite Asset", - "options": "Asset" + "options": "Asset", + "print_hide": 1 }, { "depends_on": "eval:doc.use_serial_batch_fields === 0 && doc.docstatus === 0", @@ -930,7 +945,8 @@ "default": "0", "fieldname": "use_serial_batch_fields", "fieldtype": "Check", - "label": "Use Serial No / Batch Fields" + "label": "Use Serial No / Batch Fields", + "print_hide": 1 }, { "depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1", @@ -977,7 +993,8 @@ "fieldname": "distributed_discount_amount", "fieldtype": "Currency", "label": "Distributed Discount Amount", - "options": "currency" + "options": "currency", + "print_hide": 1 }, { "fieldname": "tax_withholding_category", @@ -991,7 +1008,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2026-02-15 21:07:49.455930", + "modified": "2026-03-25 18:03:33.522195", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index c90e1ff42d2..cd18994d0c5 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -207,6 +207,7 @@ "fieldtype": "Link", "label": "Stock UOM", "options": "UOM", + "print_hide": 1, "read_only": 1 }, { @@ -310,7 +311,8 @@ "fieldname": "discount_amount", "fieldtype": "Currency", "label": "Discount Amount", - "options": "currency" + "options": "currency", + "print_hide": 1 }, { "depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount", @@ -853,6 +855,7 @@ "fieldtype": "Currency", "label": "Rate of Stock UOM", "no_copy": 1, + "print_hide": 1, "options": "currency", "read_only": 1 }, @@ -869,6 +872,7 @@ "fieldname": "grant_commission", "fieldtype": "Check", "label": "Grant Commission", + "print_hide": 1, "read_only": 1 }, { @@ -926,7 +930,8 @@ "default": "0", "fieldname": "use_serial_batch_fields", "fieldtype": "Check", - "label": "Use Serial No / Batch Fields" + "label": "Use Serial No / Batch Fields", + "print_hide": 1 }, { "depends_on": "eval:doc.use_serial_batch_fields === 1 && parent.update_stock === 1", @@ -941,7 +946,8 @@ "fieldname": "distributed_discount_amount", "fieldtype": "Currency", "label": "Distributed Discount Amount", - "options": "currency" + "options": "currency", + "print_hide": 1 }, { "fieldname": "available_quantity_section", @@ -1010,7 +1016,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2026-02-23 14:37:14.853941", + "modified": "2026-02-24 14:37:16.853941", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 5e5eb7fd55b..2337d6a9fb6 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -280,14 +280,16 @@ "depends_on": "price_list_rate", "fieldname": "discount_percentage", "fieldtype": "Percent", - "label": "Discount on Price List Rate (%)" + "label": "Discount on Price List Rate (%)", + "print_hide": 1 }, { "depends_on": "price_list_rate", "fieldname": "discount_amount", "fieldtype": "Currency", "label": "Discount Amount", - "options": "currency" + "options": "currency", + "print_hide": 1 }, { "fieldname": "col_break3", @@ -428,6 +430,7 @@ "fieldname": "weight_per_unit", "fieldtype": "Float", "label": "Weight Per Unit", + "print_hide": 1, "read_only": 1 }, { @@ -763,6 +766,7 @@ "label": "Rate of Stock UOM", "no_copy": 1, "options": "currency", + "print_hide": 1, "read_only": 1 }, { @@ -779,6 +783,7 @@ "fieldtype": "Float", "label": "Available Qty at Company", "no_copy": 1, + "print_hide": 1, "read_only": 1 }, { @@ -878,7 +883,8 @@ "fieldname": "fg_item_qty", "fieldtype": "Float", "label": "Finished Good Qty", - "mandatory_depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow" + "mandatory_depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow", + "print_hide": 1 }, { "depends_on": "eval:parent.is_internal_supplier", @@ -923,7 +929,8 @@ "fieldname": "distributed_discount_amount", "fieldtype": "Currency", "label": "Distributed Discount Amount", - "options": "currency" + "options": "currency", + "print_hide": 1 }, { "allow_on_submit": 1, @@ -934,6 +941,7 @@ "label": "Subcontracted Quantity", "no_copy": 1, "non_negative": 1, + "print_hide": 1, "read_only": 1 } ], @@ -942,7 +950,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-10-30 16:51:56.761673", + "modified": "2025-11-30 16:51:57.761673", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index d98dc8dccc4..af82501dec1 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -343,7 +343,8 @@ "fieldname": "discount_amount", "fieldtype": "Currency", "label": "Discount Amount", - "options": "currency" + "options": "currency", + "print_hide": 1 }, { "depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount", @@ -503,12 +504,14 @@ { "fieldname": "weight_per_unit", "fieldtype": "Float", - "label": "Weight Per Unit" + "label": "Weight Per Unit", + "print_hide": 1 }, { "fieldname": "total_weight", "fieldtype": "Float", "label": "Total Weight", + "print_hide": 1, "read_only": 1 }, { @@ -822,6 +825,7 @@ "label": "Rate of Stock UOM", "no_copy": 1, "options": "currency", + "print_hide": 1, "read_only": 1 }, { @@ -830,6 +834,7 @@ "fieldname": "grant_commission", "fieldtype": "Check", "label": "Grant Commission", + "print_hide": 1, "read_only": 1 }, { @@ -837,6 +842,7 @@ "fieldtype": "Float", "label": "Picked Qty (in Stock UOM)", "no_copy": 1, + "print_hide": 1, "read_only": 1 }, { @@ -910,6 +916,7 @@ "fieldtype": "Float", "label": "Production Plan Qty", "no_copy": 1, + "print_hide": 1, "read_only": 1 }, { @@ -926,7 +933,8 @@ "fieldname": "distributed_discount_amount", "fieldtype": "Currency", "label": "Distributed Discount Amount", - "options": "currency" + "options": "currency", + "print_hide": 1 }, { "allow_on_submit": 1, @@ -995,6 +1003,7 @@ "label": "Subcontracted Quantity", "no_copy": 1, "non_negative": 1, + "print_hide": 1, "read_only": 1 }, { @@ -1010,7 +1019,8 @@ "fieldname": "fg_item_qty", "fieldtype": "Float", "label": "Finished Good Qty", - "mandatory_depends_on": "eval:parent.is_subcontracted" + "mandatory_depends_on": "eval:parent.is_subcontracted", + "print_hide": 1 }, { "fieldname": "requested_qty", @@ -1025,7 +1035,11 @@ "idx": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2026-02-21 16:39:00.200328", +======= + "modified": "2026-02-22 16:40:00.200328", +>>>>>>> 8f83616b60 (fix: print hide unnecessary fields) "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index a01a4841e49..e388199c361 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -334,7 +334,8 @@ "fieldname": "discount_amount", "fieldtype": "Currency", "label": "Discount Amount", - "options": "currency" + "options": "currency", + "print_hide": 1 }, { "fieldname": "col_break3", @@ -470,12 +471,14 @@ { "fieldname": "weight_per_unit", "fieldtype": "Float", - "label": "Weight Per Unit" + "label": "Weight Per Unit", + "print_hide": 1 }, { "fieldname": "total_weight", "fieldtype": "Float", "label": "Total Weight", + "print_hide": 1, "read_only": 1 }, { @@ -783,7 +786,8 @@ "fieldname": "expense_account", "fieldtype": "Link", "label": "Expense Account", - "options": "Account" + "options": "Account", + "print_hide": 1 }, { "fieldname": "accounting_dimensions_section", @@ -820,7 +824,8 @@ "fieldname": "asset_location", "fieldtype": "Link", "label": "Asset Location", - "options": "Location" + "options": "Location", + "print_hide": 1 }, { "depends_on": "is_fixed_asset", @@ -829,6 +834,7 @@ "fieldtype": "Link", "label": "Asset Category", "options": "Asset Category", + "print_hide": 1, "read_only": 1 }, { @@ -898,6 +904,7 @@ "label": "Rate of Stock UOM", "no_copy": 1, "options": "currency", + "print_hide": 1, "read_only": 1 }, { @@ -949,7 +956,8 @@ "fieldname": "base_rate_with_margin", "fieldtype": "Currency", "label": "Rate With Margin (Company Currency)", - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", + "print_hide": 1 }, { "fieldname": "purchase_invoice", @@ -1103,7 +1111,8 @@ "default": "0", "fieldname": "use_serial_batch_fields", "fieldtype": "Check", - "label": "Use Serial No / Batch Fields" + "label": "Use Serial No / Batch Fields", + "print_hide": 1 }, { "default": "0", @@ -1126,7 +1135,8 @@ "fieldname": "distributed_discount_amount", "fieldtype": "Currency", "label": "Distributed Discount Amount", - "options": "currency" + "options": "currency", + "print_hide": 1 }, { "fieldname": "amount_difference_with_purchase_invoice", @@ -1140,7 +1150,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2026-02-04 14:42:10.646809", + "modified": "2026-02-07 14:42:11.646809", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", From 66ee208cb2d63dd5052e8a7017dcdd8c968af248 Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Mon, 6 Apr 2026 16:19:53 +0530 Subject: [PATCH 37/79] fix: conflicts --- .../selling/doctype/sales_order_item/sales_order_item.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index af82501dec1..0f043a73fa4 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -1035,11 +1035,7 @@ "idx": 1, "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2026-02-21 16:39:00.200328", -======= "modified": "2026-02-22 16:40:00.200328", ->>>>>>> 8f83616b60 (fix: print hide unnecessary fields) "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", From 5719992cdaf836762e80ee14177f9ae9b1d2bbc9 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 6 Apr 2026 14:15:03 +0530 Subject: [PATCH 38/79] fix: GL entries for different exchange rate in the purchase invoice (cherry picked from commit a953709640259ab53c2c009afe86189884eff9b8) --- .../purchase_invoice/purchase_invoice.py | 11 +- .../purchase_invoice/test_purchase_invoice.py | 13 ++- .../buying/doctype/supplier/test_supplier.py | 9 ++ .../purchase_receipt/purchase_receipt.py | 102 ++++++++++++++---- .../purchase_receipt/test_purchase_receipt.py | 67 ++++++++++++ 5 files changed, 178 insertions(+), 24 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 7c076e197a5..8b561730de0 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -983,6 +983,10 @@ class PurchaseInvoice(BuyingController): if provisional_accounting_for_non_stock_items: self.get_provisional_accounts() + adjust_incoming_rate = frappe.db.get_single_value( + "Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate" + ) + for item in self.get("items"): if flt(item.base_net_amount) or (self.get("update_stock") and item.valuation_rate): if item.item_code: @@ -1161,7 +1165,11 @@ class PurchaseInvoice(BuyingController): ) # check if the exchange rate has changed - if item.get("purchase_receipt") and self.auto_accounting_for_stock: + if ( + not adjust_incoming_rate + and item.get("purchase_receipt") + and self.auto_accounting_for_stock + ): if ( exchange_rate_map[item.purchase_receipt] and self.conversion_rate != exchange_rate_map[item.purchase_receipt] @@ -1198,6 +1206,7 @@ class PurchaseInvoice(BuyingController): item=item, ) ) + if ( self.auto_accounting_for_stock and self.is_opening == "No" diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 09febdfd915..b42574ee206 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -350,6 +350,12 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin): make_purchase_invoice as create_purchase_invoice, ) + original_value = frappe.db.get_single_value( + "Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate" + ) + + frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0) + pr = make_purchase_receipt( company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", @@ -368,14 +374,19 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin): # fetching the latest GL Entry with exchange gain and loss account account amount = frappe.db.get_value( - "GL Entry", {"account": exchange_gain_loss_account, "voucher_no": pi.name}, "credit" + "GL Entry", {"account": exchange_gain_loss_account, "voucher_no": pi.name}, "debit" ) + discrepancy_caused_by_exchange_rate_diff = abs( pi.items[0].base_net_amount - pr.items[0].base_net_amount ) self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount) + frappe.db.set_single_value( + "Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", original_value + ) + def test_purchase_invoice_with_exchange_rate_difference_for_non_stock_item(self): from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( make_purchase_invoice as create_purchase_invoice, diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py index 663a7b48e46..6a7675ffba9 100644 --- a/erpnext/buying/doctype/supplier/test_supplier.py +++ b/erpnext/buying/doctype/supplier/test_supplier.py @@ -167,6 +167,15 @@ def create_supplier(**args): if not args.without_supplier_group: doc.supplier_group = args.supplier_group or "Services" + if args.get("party_account"): + doc.append( + "accounts", + { + "company": frappe.db.get_value("Account", args.get("party_account"), "company"), + "account": args.get("party_account"), + }, + ) + doc.insert() return doc diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index dc8885b1ca4..dfa30796996 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1259,11 +1259,11 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate total_amount, total_billed_amount, pi_landed_cost_amount = 0, 0, 0 item_wise_returned_qty = get_item_wise_returned_qty(pr_doc) + billed_qty_amt = frappe._dict() if adjust_incoming_rate: - item_wise_billed_qty = get_billed_qty_against_purchase_receipt(pr_doc) - - billed_qty_based_on_po = get_billed_qty_against_purchase_order(pr_doc) + billed_qty_amt = get_billed_qty_amount_against_purchase_receipt(pr_doc) + billed_qty_amt_based_on_po = get_billed_qty_amount_against_purchase_order(pr_doc) for item in pr_doc.items: returned_qty = flt(item_wise_returned_qty.get(item.name)) @@ -1293,22 +1293,46 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate item.billed_amt is not None and item.amount is not None and ( - item_wise_billed_qty.get(item.name) - or billed_qty_based_on_po.get(item.purchase_order_item) + billed_qty_amt.get(item.name) or billed_qty_amt_based_on_po.get(item.purchase_order_item) ) ): - qty = item_wise_billed_qty.get(item.name) - if not qty: - if item.qty < billed_qty_based_on_po.get(item.purchase_order_item): + qty = None + if billed_qty_amt.get(item.name): + qty = billed_qty_amt.get(item.name).get("qty") + + if not qty and billed_qty_amt_based_on_po.get(item.purchase_order_item): + if item.qty < billed_qty_amt_based_on_po.get(item.purchase_order_item)["qty"]: qty = item.qty else: - qty = billed_qty_based_on_po.get(item.purchase_order_item) + qty = billed_qty_amt_based_on_po.get(item.purchase_order_item)["qty"] - billed_qty_based_on_po[item.purchase_order_item] -= qty + billed_qty_amt_based_on_po[item.purchase_order_item]["qty"] -= qty - adjusted_amt = (flt(item.billed_amt / qty) - flt(item.rate)) * item.qty + billed_amt = item.billed_amt + if billed_qty_amt.get(item.name): + billed_amt = flt(billed_qty_amt.get(item.name).get("amount")) + elif billed_qty_amt_based_on_po.get(item.purchase_order_item): + total_billed_qty = ( + billed_qty_amt_based_on_po.get(item.purchase_order_item).get("qty") + qty + ) - adjusted_amt = flt(adjusted_amt * flt(pr_doc.conversion_rate), item.precision("amount")) + if total_billed_qty: + billed_amt = flt( + flt(billed_qty_amt_based_on_po.get(item.purchase_order_item).get("amount")) + * (qty / total_billed_qty) + ) + else: + billed_amt = 0.0 + + # Reduce billed amount based on PO for next iterations + billed_qty_amt_based_on_po[item.purchase_order_item]["amount"] -= billed_amt + + if qty: + adjusted_amt = ( + flt(billed_amt / qty) - (flt(item.rate) * flt(pr_doc.conversion_rate)) + ) * item.qty + + adjusted_amt = flt(adjusted_amt, item.precision("amount")) pi_landed_cost_amount += adjusted_amt item.db_set("amount_difference_with_purchase_invoice", adjusted_amt, update_modified=False) elif amount and item.billed_amt > amount: @@ -1337,23 +1361,40 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate adjust_incoming_rate_for_pr(pr_doc) -def get_billed_qty_against_purchase_receipt(pr_doc): +def get_billed_qty_amount_against_purchase_receipt(pr_doc): pr_names = [d.name for d in pr_doc.items] + parent_table = frappe.qb.DocType("Purchase Invoice") table = frappe.qb.DocType("Purchase Invoice Item") query = ( - frappe.qb.from_(table) - .select(table.pr_detail, fn.Sum(table.qty).as_("qty")) + frappe.qb.from_(parent_table) + .inner_join(table) + .on(parent_table.name == table.parent) + .select( + table.pr_detail, + fn.Sum(table.amount * parent_table.conversion_rate).as_("amount"), + fn.Sum(table.qty).as_("qty"), + ) .where((table.pr_detail.isin(pr_names)) & (table.docstatus == 1)) .groupby(table.pr_detail) ) - invoice_data = query.run(as_list=1) + invoice_data = query.run(as_dict=1) if not invoice_data: return frappe._dict() - return frappe._dict(invoice_data) + + billed_qty_amt = frappe._dict() + + for row in invoice_data: + if row.pr_detail not in billed_qty_amt: + billed_qty_amt[row.pr_detail] = {"amount": 0, "qty": 0} + + billed_qty_amt[row.pr_detail]["amount"] += flt(row.amount) + billed_qty_amt[row.pr_detail]["qty"] += flt(row.qty) + + return billed_qty_amt -def get_billed_qty_against_purchase_order(pr_doc): +def get_billed_qty_amount_against_purchase_order(pr_doc): po_names = list( set( [ @@ -1366,15 +1407,32 @@ def get_billed_qty_against_purchase_order(pr_doc): invoice_data_po_based = frappe._dict() if po_names: + parent_table = frappe.qb.DocType("Purchase Invoice") table = frappe.qb.DocType("Purchase Invoice Item") + query = ( - frappe.qb.from_(table) - .select(table.po_detail, fn.Sum(table.qty).as_("qty")) + frappe.qb.from_(parent_table) + .inner_join(table) + .on(parent_table.name == table.parent) + .select( + table.po_detail, + fn.Sum(table.qty).as_("qty"), + fn.Sum(table.amount * parent_table.conversion_rate).as_("amount"), + ) .where((table.po_detail.isin(po_names)) & (table.docstatus == 1) & (table.pr_detail.isnull())) .groupby(table.po_detail) ) - invoice_data_po_based = query.run(as_list=1) - invoice_data_po_based = frappe._dict(invoice_data_po_based) + + invoice_data = query.run(as_dict=1) + if not invoice_data: + return frappe._dict() + + for row in invoice_data: + if row.po_detail not in invoice_data_po_based: + invoice_data_po_based[row.po_detail] = {"amount": 0, "qty": 0} + + invoice_data_po_based[row.po_detail]["amount"] += flt(row.amount) + invoice_data_po_based[row.po_detail]["qty"] += flt(row.qty) return invoice_data_po_based diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 50f28a75b18..828ad603d8e 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -5450,6 +5450,70 @@ class TestPurchaseReceipt(ERPNextTestSuite): self.assertEqual(pr.total_qty, 12) self.assertEqual(pr.total, 120) + def test_different_exchange_rate_in_pr_and_pi(self): + from erpnext.accounts.doctype.account.test_account import create_account + + original_value = frappe.db.get_single_value( + "Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate" + ) + + frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1) + + party_account = create_account( + account_name="USD Party Account Creditors", + parent_account="Accounts Payable - TCP1", + account_type="Payable", + company="_Test Company with perpetual inventory", + account_currency="USD", + ) + + supplier = create_supplier( + supplier_name="_Test USD Supplier New 1", default_currency="USD", party_account=party_account + ).name + item_code = make_item("Test Item for Different Exchange Rate", {"is_stock_item": 1}).name + + pr = make_purchase_receipt( + item_code=item_code, + qty=1, + currency="USD", + conversion_rate=80, + rate=100, + company="_Test Company with perpetual inventory", + warehouse=frappe.get_value( + "Warehouse", {"company": "_Test Company with perpetual inventory"}, "name" + ), + supplier=supplier, + ) + + self.assertEqual(pr.currency, "USD") + self.assertEqual(pr.conversion_rate, 80) + + gl_entries = get_gl_entries(pr.doctype, pr.name) + self.assertTrue(len(gl_entries) == 2) + for row in gl_entries: + amount = row.credit or row.debit + self.assertEqual(amount, 8000.0) + + pi = make_purchase_invoice(pr.name) + pi.conversion_rate = 90 + pi.currency = "USD" + + pi.save() + pi.submit() + + gl_entries = get_gl_entries(pi.doctype, pi.name) + self.assertTrue(len(gl_entries) == 2) + + accounts = ["USD Party Account Creditors - TCP1", "Stock Received But Not Billed - TCP1"] + for row in gl_entries: + amount = row.credit or row.debit + self.assertEqual(amount, 9000.0) + self.assertTrue(row.account in accounts) + + frappe.db.set_single_value( + "Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", original_value + ) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier @@ -5620,6 +5684,9 @@ def make_purchase_receipt(**args): pr.return_against = args.return_against pr.apply_putaway_rule = args.apply_putaway_rule + if args.get("conversion_rate") is not None: + pr.conversion_rate = args.conversion_rate + qty = args.qty if args.qty is not None else 5 rejected_qty = args.rejected_qty or 0 received_qty = args.received_qty or flt(rejected_qty) + flt(qty) From 273caa38d99d90b9da6a3ed1b62108afc298336d Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Wed, 1 Apr 2026 08:43:56 +0530 Subject: [PATCH 39/79] fix(ux): refresh grid to correctly persist the state of fields (cherry picked from commit da778edf488a5a555d32b4cb400b0d327ff430a2) --- .../doctype/work_order/work_order.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 18b5be64c10..7a964a76231 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -244,13 +244,16 @@ frappe.ui.form.on("Work Order", { }, toggle_items_editable(frm) { - if (!frm.doc.__onload?.allow_editing_items) { - frm.set_df_property("required_items", "cannot_delete_rows", true); - frm.set_df_property("required_items", "cannot_add_rows", true); - frm.fields_dict["required_items"].grid.update_docfield_property("item_code", "read_only", 1); - frm.fields_dict["required_items"].grid.update_docfield_property("required_qty", "read_only", 1); - frm.fields_dict["required_items"].grid.refresh(); - } + let allow_edit = true; + if (!frm.doc.__onload?.allow_editing_items) allow_edit = false; + + frm.set_df_property("required_items", "cannot_delete_rows", !allow_edit); + frm.set_df_property("required_items", "cannot_add_rows", !allow_edit); + + const grid = frm.fields_dict["required_items"].grid; + grid.update_docfield_property("item_code", "read_only", !allow_edit); + grid.update_docfield_property("required_qty", "read_only", !allow_edit); + grid.refresh(); }, hide_reserve_stock_button(frm) { From 84382db5ca1911f40b8dec904477686ccc29cdff Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 12:00:04 +0000 Subject: [PATCH 40/79] fix: remove title field from purchase receipt (backport #54051) (#54065) Co-authored-by: Mihir Kandoi fix: remove title field from purchase receipt (#54051) --- .../purchase_receipt/purchase_receipt.json | 17 +++-------------- .../purchase_receipt/purchase_receipt.py | 1 - 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 805b7eef9e6..82745b34bbf 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -3,7 +3,7 @@ "allow_auto_repeat": 1, "allow_import": 1, "autoname": "naming_series:", - "creation": "2013-05-21 16:16:39", + "creation": "2026-04-06 14:10:33.384946", "doctype": "DocType", "document_type": "Document", "editable_grid": 1, @@ -11,7 +11,6 @@ "field_order": [ "supplier_section", "column_break0", - "title", "naming_series", "supplier", "supplier_name", @@ -171,16 +170,6 @@ "print_width": "50%", "width": "50%" }, - { - "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", @@ -1303,7 +1292,7 @@ "idx": 261, "is_submittable": 1, "links": [], - "modified": "2026-03-09 17:15:28.602690", + "modified": "2026-04-06 14:11:29.630333", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", @@ -1371,6 +1360,6 @@ "sort_order": "DESC", "states": [], "timeline_field": "supplier", - "title_field": "title", + "title_field": "supplier_name", "track_changes": 1 } diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index dc8885b1ca4..71fff578f06 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -148,7 +148,6 @@ class PurchaseReceipt(BuyingController): taxes_and_charges_deducted: DF.Currency tc_name: DF.Link | None terms: DF.TextEditor | None - title: DF.Data | None total: DF.Currency total_net_weight: DF.Float total_qty: DF.Float From af81ed874b13c0bda088ff8728bab7524410c035 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 15:17:11 +0000 Subject: [PATCH 41/79] fix: transactions where update stock is 0 should not create SLEs (backport #54035) (#54077) Co-authored-by: Nishka Gosalia <58264710+nishkagosalia@users.noreply.github.com> fix: transactions where update stock is 0 should not create SLEs (#54035) --- .../repost_item_valuation/repost_item_valuation.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 2b4d5c28692..84cf6234dcf 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -82,6 +82,7 @@ class RepostItemValuation(Document): def validate(self): self.reset_repost_only_accounting_ledgers() self.set_company() + self.validate_update_stock() self.validate_period_closing_voucher() self.set_status(write=False) self.reset_field_values() @@ -93,6 +94,18 @@ class RepostItemValuation(Document): if self.repost_only_accounting_ledgers and self.based_on != "Transaction": self.repost_only_accounting_ledgers = 0 + def validate_update_stock(self): + if ( + self.voucher_type in ["Sales Invoice", "Purchase Invoice"] + and not self.repost_only_accounting_ledgers + ): + update_stock = frappe.get_value(self.voucher_type, self.voucher_no, "update_stock") + if not update_stock: + msg = _( + "Since {0} has 'Update Stock' disabled, you cannot create repost item valuation against it" + ).format(get_link_to_form(self.voucher_type, self.voucher_no)) + frappe.throw(msg) + def validate_recreate_stock_ledgers(self): if not self.recreate_stock_ledgers: return From dc58754a60425e455352c148e1c17c8725a6f33e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 17:18:45 +0000 Subject: [PATCH 42/79] fix: add tax_id handling in Tax Withholding Entry (backport #53598) (#54081) Co-authored-by: Lakshit Jain fix: add tax_id handling in Tax Withholding Entry (#53598) --- .../tax_withholding_category.py | 13 +++--- .../test_tax_withholding_category.py | 42 +++++++++++++++++++ .../tax_withholding_entry.py | 10 +++-- 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index c78b026c227..dd8caa60d7d 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -128,6 +128,7 @@ class TaxWithholdingDetails: self.party_type = party_type self.party = party self.company = company + self.tax_id = get_tax_id_for_party(self.party_type, self.party) def get(self) -> list: """ @@ -161,6 +162,7 @@ class TaxWithholdingDetails: disable_cumulative_threshold=doc.disable_cumulative_threshold, disable_transaction_threshold=doc.disable_transaction_threshold, taxable_amount=0, + tax_id=self.tax_id, ) # ldc (only if valid based on posting date) @@ -181,17 +183,13 @@ class TaxWithholdingDetails: if self.party_type != "Supplier": return ldc_details - # NOTE: This can be a configurable option - # To check if filter by tax_id is needed - tax_id = get_tax_id_for_party(self.party_type, self.party) - # ldc details - ldc_records = self.get_valid_ldc_records(tax_id) + ldc_records = self.get_valid_ldc_records(self.tax_id) if not ldc_records: return ldc_details ldc_names = [ldc.name for ldc in ldc_records] - ldc_utilization_map = self.get_ldc_utilization_by_category(ldc_names, tax_id) + ldc_utilization_map = self.get_ldc_utilization_by_category(ldc_names, self.tax_id) # map for ldc in ldc_records: @@ -254,4 +252,5 @@ class TaxWithholdingDetails: @allow_regional def get_tax_id_for_party(party_type, party): - return None + # cannot use tax_id from doc because payment and journal entry do not have tax_id field.\ + return frappe.db.get_value(party_type, party, "tax_id") 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 bd633c94dc9..2d0450107bc 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 @@ -2,6 +2,7 @@ # See license.txt import datetime +from unittest.mock import patch import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields @@ -3541,6 +3542,47 @@ class TestTaxWithholdingCategory(ERPNextTestSuite): entry.withholding_amount = 5001 # Should be 5000 (10% of 50000) self.assertRaisesRegex(frappe.ValidationError, "Withholding Amount.*does not match", pi.save) + def test_tax_id_is_set_in_all_generated_entries_from_party_doctype(self): + self.setup_party_with_category("Supplier", "Test TDS Supplier3", "New TDS Category") + frappe.db.set_value("Supplier", "Test TDS Supplier3", "tax_id", "ABCTY1234D") + + pi = create_purchase_invoice(supplier="Test TDS Supplier3", rate=40000) + pi.submit() + + entries = frappe.get_all( + "Tax Withholding Entry", + filters={"parenttype": "Purchase Invoice", "parent": pi.name}, + fields=["name", "tax_id"], + ) + + self.assertTrue(entries) + self.assertTrue(all(entry.tax_id == "ABCTY1234D" for entry in entries)) + + def test_threshold_considers_two_parties_with_same_tax_id_with_overrided_hook(self): + self.setup_party_with_category("Supplier", "Test TDS Supplier1", "Cumulative Threshold TDS") + self.setup_party_with_category("Supplier", "Test TDS Supplier2", "Cumulative Threshold TDS") + + with patch( + "erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category.get_tax_id_for_party", + return_value="AAAPL1234C", + ): + pi1 = create_purchase_invoice(supplier="Test TDS Supplier1", rate=20000) + pi1.submit() + + pi2 = create_purchase_invoice(supplier="Test TDS Supplier2", rate=20000) + + pi2.submit() + + entries = frappe.get_all( + "Tax Withholding Entry", + filters={"parenttype": "Purchase Invoice", "parent": pi2.name}, + fields=["status", "withholding_amount"], + ) + + self.assertEqual(len(entries), 1) + self.assertEqual(entries[0].status, "Settled") + self.assertEqual(entries[0].withholding_amount, 2000.0) + def create_purchase_invoice(**args): # return sales invoice doc object diff --git a/erpnext/accounts/doctype/tax_withholding_entry/tax_withholding_entry.py b/erpnext/accounts/doctype/tax_withholding_entry/tax_withholding_entry.py index 6aff1116935..8f8ee7898af 100644 --- a/erpnext/accounts/doctype/tax_withholding_entry/tax_withholding_entry.py +++ b/erpnext/accounts/doctype/tax_withholding_entry/tax_withholding_entry.py @@ -344,7 +344,6 @@ class TaxWithholdingEntry(Document): from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import ( TaxWithholdingDetails, - get_tax_id_for_party, ) @@ -646,8 +645,11 @@ class TaxWithholdingController: # NOTE: This can be a configurable option # To check if filter by tax_id is needed - tax_id = get_tax_id_for_party(self.party_type, self.party) - query = query.where(entry.tax_id == tax_id) if tax_id else query.where(entry.party == self.party) + query = ( + query.where(entry.tax_id == category.tax_id) + if category.tax_id + else query.where(entry.party == self.party) + ) return query @@ -686,6 +688,7 @@ class TaxWithholdingController: "company": self.doc.company, "party_type": self.party_type, "party": self.party, + "tax_id": category.tax_id, "tax_withholding_category": category.name, "tax_withholding_group": category.tax_withholding_group, "tax_rate": category.tax_rate, @@ -1052,6 +1055,7 @@ class TaxWithholdingController: "party_type": self.party_type, "party": self.party, "company": self.doc.company, + "tax_id": category.tax_id, } ) return entry From e6722c84fa4d8ebc2f14af64f1dd16bd4b749dfb Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 04:14:45 +0000 Subject: [PATCH 43/79] fix: dif_inward_from_outward_workspace_sidebar (backport #54083) (#54088) Co-authored-by: mahsem <137205921+mahsem@users.noreply.github.com> fix: dif_inward_from_outward_workspace_sidebar (#54083) --- erpnext/workspace_sidebar/subcontracting.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/workspace_sidebar/subcontracting.json b/erpnext/workspace_sidebar/subcontracting.json index e2aa91fcfa6..60509c5c5cd 100644 --- a/erpnext/workspace_sidebar/subcontracting.json +++ b/erpnext/workspace_sidebar/subcontracting.json @@ -71,7 +71,7 @@ "icon": "", "indent": 0, "keep_closed": 0, - "label": "Subcontracting Order", + "label": "Subcontracting Inward Order", "link_to": "Subcontracting Inward Order", "link_type": "DocType", "show_arrow": 0, @@ -230,7 +230,7 @@ "type": "Link" } ], - "modified": "2026-02-23 22:40:17.130101", + "modified": "2026-04-06 20:22:17.130321", "modified_by": "Administrator", "module": "Buying", "module_onboarding": "Subcontracting Onboarding", From 62b83cacce540a671b85d565d5d277090ba80a44 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 10:25:13 +0530 Subject: [PATCH 44/79] =?UTF-8?q?fix:=20resolve=20user=20permission=20erro?= =?UTF-8?q?r=20on=20status=20change=20by=20updating=20user=20=E2=80=A6=20(?= =?UTF-8?q?backport=20#54033)=20(#54060)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Krishna Shirsath --- erpnext/setup/doctype/employee/employee.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index d66d091320b..81324fb89ba 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -301,7 +301,7 @@ class Employee(NestedSet): frappe.throw(_("User {0} does not exist").format(self.user_id)) if self.status != "Active" and enabled or self.status == "Active" and enabled == 0: - frappe.set_value("User", self.user_id, "enabled", not enabled) + frappe.db.set_value("User", self.user_id, "enabled", not enabled) def validate_duplicate_user_id(self): Employee = frappe.qb.DocType("Employee") From ff262655bb018a42bf1cc920534564e36476eeae Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 10:25:44 +0530 Subject: [PATCH 45/79] feat: croatian_address_template (backport #53888) (#54058) Co-authored-by: mahsem <137205921+mahsem@users.noreply.github.com> --- erpnext/regional/address_template/templates/croatia.html | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 erpnext/regional/address_template/templates/croatia.html diff --git a/erpnext/regional/address_template/templates/croatia.html b/erpnext/regional/address_template/templates/croatia.html new file mode 100644 index 00000000000..0c2ed73f0ae --- /dev/null +++ b/erpnext/regional/address_template/templates/croatia.html @@ -0,0 +1,4 @@ +{{ address_line1 }}
+{% if address_line2 %}{{ address_line2 }}
{% endif -%} +{{ pincode }} {{ city | upper }}
+{{ country | upper }} \ No newline at end of file From 454271ad68073ad85e65e6029266c24548d5d94c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 05:55:55 +0000 Subject: [PATCH 46/79] fix: divide sub-assembly cost by qty to get per-unit rate in BOM Creator (backport #54090) (#54091) --- erpnext/manufacturing/doctype/bom_creator/bom_creator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py index e3feac1061a..97849b6f17e 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py @@ -203,7 +203,9 @@ class BOMCreator(Document): self, ) else: - row.rate = flt(self.get_raw_material_cost(row.item_code) * row.conversion_factor) + row.rate = flt( + self.get_raw_material_cost(row.item_code) / flt(row.qty or 1) * row.conversion_factor + ) row.amount = flt(row.rate) * flt(row.qty) amount += flt(row.amount) From 995a29e3e1929a8da376cacafa5a53c4fb243e05 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 06:40:45 +0000 Subject: [PATCH 47/79] fix: task gantt popup text not visible in light theme (backport #53882) (#54094) Co-authored-by: Sakthivel Murugan S <129778327+ssakthivelmurugan@users.noreply.github.com> fix: task gantt popup text not visible in light theme (#53882) --- erpnext/projects/doctype/task/task_list.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/projects/doctype/task/task_list.js b/erpnext/projects/doctype/task/task_list.js index 17b0ed2c7fa..2516a327d44 100644 --- a/erpnext/projects/doctype/task/task_list.js +++ b/erpnext/projects/doctype/task/task_list.js @@ -35,30 +35,30 @@ frappe.listview_settings["Task"] = { }, gantt_custom_popup_html: function (ganttobj, task) { let html = ` -
+ ${ganttobj.name} `; if (task.project) { html += `

${__("Project")}: - + ${task.project}

`; } html += `

${__("Progress")}: - ${ganttobj.progress}% + ${ganttobj.progress}%

`; if (task._assign) { const assign_list = JSON.parse(task._assign); const assignment_wrapper = ` Assigned to: - + ${assign_list.map((user) => frappe.user_info(user).fullname).join(", ")} `; From 21f36f5c21dceeb7214d961fe8ff5731bd41106e Mon Sep 17 00:00:00 2001 From: ervishnucs Date: Thu, 12 Mar 2026 17:33:56 +0530 Subject: [PATCH 48/79] fix: remove null from link_filters (cherry picked from commit a518a735f35a6ac0e68f2de0a2e2048264f27f7b) --- erpnext/assets/doctype/asset_repair/asset_repair.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 71b9469cfbd..4fc9a31b875 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -130,7 +130,7 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Asset", - "link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",\"Cancelled\",null]]]", + "link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",\"Cancelled\"]]]", "options": "Asset", "reqd": 1 }, From b91af5b2b987e04da90c9376261d155b2a665a88 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Wed, 25 Mar 2026 10:43:28 +0530 Subject: [PATCH 49/79] fix: create source_stock_entry to refer to original manufacturing entry (cherry picked from commit d4baa9a74af097a47ffdb267e5a0073f4c5d6721) --- erpnext/stock/doctype/stock_entry/stock_entry.json | 10 ++++++++++ erpnext/stock/doctype/stock_entry/stock_entry.py | 1 + 2 files changed, 11 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 7c9dadb9a55..81cbad37c24 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -24,6 +24,7 @@ "work_order", "subcontracting_order", "outgoing_stock_entry", + "source_stock_entry", "bom_info_section", "from_bom", "use_multi_level_bom", @@ -125,6 +126,15 @@ "options": "Stock Entry", "read_only": 1 }, + { + "depends_on": "eval:doc.purpose == 'Disassemble'", + "fieldname": "source_stock_entry", + "fieldtype": "Link", + "label": "Source Stock Entry (Manufacture)", + "no_copy": 1, + "options": "Stock Entry", + "print_hide": 1 + }, { "bold": 1, "fetch_from": "stock_entry_type.purpose", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 8078abb2848..8d5c1fe60dc 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -151,6 +151,7 @@ class StockEntry(StockController, SubcontractingInwardController): select_print_heading: DF.Link | None set_posting_time: DF.Check source_address_display: DF.TextEditor | None + source_stock_entry: DF.Link | None source_warehouse_address: DF.Link | None stock_entry_type: DF.Link subcontracting_inward_order: DF.Link | None From c9d03d049c57fdd41af00ea28fe292f511d41ec5 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Wed, 25 Mar 2026 16:49:58 +0530 Subject: [PATCH 50/79] fix: disassembly prompt with source stock entry field (cherry picked from commit 68e97808c566cbb34716f1b9dee4820f8d9c28a9) # Conflicts: # erpnext/manufacturing/doctype/work_order/work_order.py --- .../doctype/work_order/work_order.js | 63 ++++++++++++++++++- .../doctype/work_order/work_order.py | 28 +++++++++ 2 files changed, 88 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 7a964a76231..0e8729bf4ba 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -441,7 +441,7 @@ frappe.ui.form.on("Work Order", { make_disassembly_order(frm) { erpnext.work_order - .show_prompt_for_qty_input(frm, "Disassemble") + .show_disassembly_prompt(frm) .then((data) => { if (flt(data.qty) <= 0) { frappe.msgprint(__("Disassemble Qty cannot be less than or equal to 0.")); @@ -451,11 +451,14 @@ frappe.ui.form.on("Work Order", { work_order_id: frm.doc.name, purpose: "Disassemble", qty: data.qty, + source_stock_entry: data.source_stock_entry, }); }) .then((stock_entry) => { - frappe.model.sync(stock_entry); - frappe.set_route("Form", stock_entry.doctype, stock_entry.name); + if (stock_entry) { + frappe.model.sync(stock_entry); + frappe.set_route("Form", stock_entry.doctype, stock_entry.name); + } }); }, @@ -1002,6 +1005,60 @@ erpnext.work_order = { return flt(max, precision("qty")); }, + show_disassembly_prompt: function (frm) { + let max_qty = flt(frm.doc.produced_qty - frm.doc.disassembled_qty); + + let fields = [ + { + fieldtype: "Link", + label: __("Source Manufacture Entry"), + fieldname: "source_stock_entry", + options: "Stock Entry", + description: __("Optional. Select a specific manufacture entry to reverse."), + get_query: () => { + return { + filters: { + work_order: frm.doc.name, + purpose: "Manufacture", + docstatus: 1, + }, + }; + }, + onchange: async function () { + if (!frm.disassembly_prompt) return; + + let se_name = this.value; + let qty = max_qty; + if (se_name) { + qty = await frappe.xcall( + "erpnext.manufacturing.doctype.work_order.work_order.get_disassembly_available_qty", + { stock_entry_name: se_name } + ); + } + + frm.disassembly_prompt.set_value("qty", qty); + frm.disassembly_prompt.fields_dict.qty.set_description(__("Max: {0}", [qty])); + }, + }, + { + fieldtype: "Float", + label: __("Qty for {0}", [__("Disassemble")]), + fieldname: "qty", + description: __("Max: {0}", [max_qty]), + default: max_qty, + }, + ]; + + return new Promise((resolve, reject) => { + frm.disassembly_prompt = frappe.prompt( + fields, + (data) => resolve(data), + __("Disassemble"), + __("Create") + ); + }); + }, + show_prompt_for_qty_input: function (frm, purpose, qty, additional_transfer_entry) { let max = !additional_transfer_entry ? this.get_max_transferable_qty(frm, purpose) : qty; diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 72fafa03edd..9b6f95f25df 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -2376,6 +2376,7 @@ def make_stock_entry( qty: float | None = None, target_warehouse: str | None = None, is_additional_transfer_entry: bool = False, + source_stock_entry: str | None = None, ): work_order = frappe.get_doc("Work Order", work_order_id) if not frappe.db.get_value("Warehouse", work_order.wip_warehouse, "is_group"): @@ -2416,6 +2417,8 @@ def make_stock_entry( if purpose == "Disassemble": stock_entry.from_warehouse = work_order.fg_warehouse stock_entry.to_warehouse = target_warehouse or work_order.source_warehouse + if source_stock_entry: + stock_entry.source_stock_entry = source_stock_entry stock_entry.set_stock_entry_type() stock_entry.is_additional_transfer_entry = is_additional_transfer_entry @@ -2429,7 +2432,32 @@ def make_stock_entry( @frappe.whitelist() +<<<<<<< HEAD def get_default_warehouse(company): +======= +def get_disassembly_available_qty(stock_entry_name: str) -> float: + se = frappe.db.get_value("Stock Entry", stock_entry_name, ["fg_completed_qty"], as_dict=True) + if not se: + return 0.0 + + already_disassembled = flt( + frappe.db.get_value( + "Stock Entry", + { + "source_stock_entry": stock_entry_name, + "purpose": "Disassemble", + "docstatus": 1, + }, + [{"SUM": "fg_completed_qty"}], + ) + ) + + return flt(se.fg_completed_qty) - already_disassembled + + +@frappe.whitelist() +def get_default_warehouse(company: str): +>>>>>>> 68e97808c5 (fix: disassembly prompt with source stock entry field) wip, fg, scrap = frappe.get_cached_value( "Company", company, ["default_wip_warehouse", "default_fg_warehouse", "default_scrap_warehouse"] ) From 5f67ef70bbc97c98b2ca3d9c220fa98270778371 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Wed, 25 Mar 2026 17:04:45 +0530 Subject: [PATCH 51/79] fix: set_query for source stock entry (cherry picked from commit b47dfacb3e10461b6cffff470391ce2fbe4624d0) --- erpnext/stock/doctype/stock_entry/stock_entry.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index dbfad27be26..13e38465681 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -36,6 +36,16 @@ frappe.ui.form.on("Stock Entry", { }; }); + frm.set_query("source_stock_entry", function () { + return { + filters: { + purpose: "Manufacture", + docstatus: 1, + work_order: frm.doc.work_order || undefined, + }, + }; + }); + frm.set_query("source_warehouse_address", function () { return { query: "erpnext.controllers.queries.get_warehouse_address", From 84a063a9bf5876a5b4f395262318fd6acb9f604f Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Wed, 25 Mar 2026 17:11:11 +0530 Subject: [PATCH 52/79] fix: custom button to disassemble manufactured stock entry with work order (cherry picked from commit b64f86148cc326541709e057684f4ab967a5050f) --- .../stock/doctype/stock_entry/stock_entry.js | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 13e38465681..efaaf475570 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -340,6 +340,55 @@ frappe.ui.form.on("Stock Entry", { __("View") ); } + + if (frm.doc.purpose === "Manufacture" && frm.doc.work_order) { + frm.add_custom_button( + __("Disassemble"), + async function () { + let available_qty = await frappe.xcall( + "erpnext.manufacturing.doctype.work_order.work_order.get_disassembly_available_qty", + { stock_entry_name: frm.doc.name } + ); + frappe.prompt( + // fields + { + fieldtype: "Float", + label: __("Qty to Disassemble"), + fieldname: "qty", + default: available_qty, + description: __("Max: {0}", [available_qty]), + }, + // callback + async (data) => { + if (data.qty > available_qty) { + frappe.throw( + __("Cannot disassemble more than available quantity ({0})", [ + available_qty, + ]) + ); + } + + let stock_entry = await frappe.xcall( + "erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry", + { + work_order_id: frm.doc.work_order, + purpose: "Disassemble", + qty: data.qty, + source_stock_entry: frm.doc.name, + } + ); + if (stock_entry) { + frappe.model.sync(stock_entry); + frappe.set_route("Form", stock_entry.doctype, stock_entry.name); + } + }, + __("Disassemble"), + __("Create") + ); + }, + __("Create") + ); + } } if (frm.doc.docstatus === 0 && !frm.doc.subcontracting_inward_order) { From 1c4b2a7148527d7212c96be3b5eb3680d8571010 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Wed, 25 Mar 2026 18:27:50 +0530 Subject: [PATCH 53/79] fix: support creating disassembly (without link of WO) (cherry picked from commit dba82720b6ae5849034a1fbe510f71b2e203a3a7) --- .../stock/doctype/stock_entry/stock_entry.js | 77 +++++++++++++------ 1 file changed, 54 insertions(+), 23 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index efaaf475570..e4c1ffa4d26 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -242,6 +242,30 @@ frappe.ui.form.on("Stock Entry", { }); }, + source_stock_entry: async function (frm) { + if (!frm.doc.source_stock_entry || frm.doc.purpose !== "Disassemble") return; + + if (frm._via_source_stock_entry) { + frm.call({ + doc: frm.doc, + method: "get_items", + callback: function (r) { + if (!r.exc) refresh_field("items"); + }, + }); + frm._via_source_stock_entry = false; + return; + } + + let available_qty = await frappe.xcall( + "erpnext.manufacturing.doctype.work_order.work_order.get_disassembly_available_qty", + { stock_entry_name: frm.doc.source_stock_entry } + ); + + // triggers get_items() via its onchange + await frm.set_value("fg_completed_qty", available_qty); + }, + outgoing_stock_entry: function (frm) { frappe.call({ doc: frm.doc, @@ -341,7 +365,7 @@ frappe.ui.form.on("Stock Entry", { ); } - if (frm.doc.purpose === "Manufacture" && frm.doc.work_order) { + if (frm.doc.purpose === "Manufacture") { frm.add_custom_button( __("Disassemble"), async function () { @@ -350,7 +374,6 @@ frappe.ui.form.on("Stock Entry", { { stock_entry_name: frm.doc.name } ); frappe.prompt( - // fields { fieldtype: "Float", label: __("Qty to Disassemble"), @@ -358,28 +381,33 @@ frappe.ui.form.on("Stock Entry", { default: available_qty, description: __("Max: {0}", [available_qty]), }, - // callback async (data) => { - if (data.qty > available_qty) { - frappe.throw( - __("Cannot disassemble more than available quantity ({0})", [ - available_qty, - ]) + if (frm.doc.work_order) { + let stock_entry = await frappe.xcall( + "erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry", + { + work_order_id: frm.doc.work_order, + purpose: "Disassemble", + qty: data.qty, + source_stock_entry: frm.doc.name, + } ); - } - - let stock_entry = await frappe.xcall( - "erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry", - { - work_order_id: frm.doc.work_order, - purpose: "Disassemble", - qty: data.qty, - source_stock_entry: frm.doc.name, + if (stock_entry) { + frappe.model.sync(stock_entry); + frappe.set_route("Form", stock_entry.doctype, stock_entry.name); } - ); - if (stock_entry) { - frappe.model.sync(stock_entry); - frappe.set_route("Form", stock_entry.doctype, stock_entry.name); + } else { + let se = frappe.model.get_new_doc("Stock Entry"); + se.company = frm.doc.company; + se.stock_entry_type = "Disassemble"; + se.purpose = "Disassemble"; + se.source_stock_entry = frm.doc.name; + se.from_bom = frm.doc.from_bom; + se.bom_no = frm.doc.bom_no; + se.fg_completed_qty = data.qty; + frm._via_source_stock_entry = true; + + frappe.set_route("Form", "Stock Entry", se.name); } }, __("Disassemble"), @@ -1401,8 +1429,11 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle get_items() { var me = this; - if (this.frm.doc.work_order || this.frm.doc.bom_no) { - // if work order / bom is mentioned, get items + if ( + this.frm.doc.work_order || + this.frm.doc.bom_no || + (this.frm.doc.purpose === "Disassemble" && this.frm.doc.source_stock_entry) + ) { return this.frm.call({ doc: me.frm.doc, freeze: true, From 1237f9a0b17881f2f686bb715d1b7415094a4bf0 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Mon, 30 Mar 2026 15:46:37 +0530 Subject: [PATCH 54/79] fix: validate qty that can be disassembled from source stock entry. (cherry picked from commit 6394dead724b346deae30a6f2b8088d68cac0176) # Conflicts: # erpnext/manufacturing/doctype/work_order/work_order.py --- .../doctype/work_order/work_order.py | 25 +++++++++++-------- .../stock/doctype/stock_entry/stock_entry.py | 21 ++++++++++++++++ 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 9b6f95f25df..268918eca00 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -2433,24 +2433,27 @@ def make_stock_entry( @frappe.whitelist() <<<<<<< HEAD +<<<<<<< HEAD def get_default_warehouse(company): ======= def get_disassembly_available_qty(stock_entry_name: str) -> float: +======= +def get_disassembly_available_qty(stock_entry_name: str, current_se_name: str | None = None) -> float: +>>>>>>> 6394dead72 (fix: validate qty that can be disassembled from source stock entry.) se = frappe.db.get_value("Stock Entry", stock_entry_name, ["fg_completed_qty"], as_dict=True) if not se: return 0.0 - already_disassembled = flt( - frappe.db.get_value( - "Stock Entry", - { - "source_stock_entry": stock_entry_name, - "purpose": "Disassemble", - "docstatus": 1, - }, - [{"SUM": "fg_completed_qty"}], - ) - ) + filters = { + "source_stock_entry": stock_entry_name, + "purpose": "Disassemble", + "docstatus": 1, + } + + if current_se_name: + filters["name"] = ("!=", current_se_name) + + already_disassembled = flt(frappe.db.get_value("Stock Entry", filters, [{"SUM": "fg_completed_qty"}])) return flt(se.fg_completed_qty) - already_disassembled diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 8d5c1fe60dc..c1eeec3eaf4 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -247,6 +247,7 @@ class StockEntry(StockController, SubcontractingInwardController): self.validate_warehouse() self.validate_warehouse_of_sabb() self.validate_work_order() + self.validate_source_stock_entry() self.validate_bom() self.set_process_loss_qty() self.validate_purchase_order() @@ -847,6 +848,26 @@ class StockEntry(StockController, SubcontractingInwardController): elif self.purpose != "Material Transfer": self.work_order = None + def validate_source_stock_entry(self): + if not self.get("source_stock_entry"): + return + + from erpnext.manufacturing.doctype.work_order.work_order import get_disassembly_available_qty + + available_qty = get_disassembly_available_qty(self.source_stock_entry, self.name) + + if flt(self.fg_completed_qty) > available_qty: + frappe.throw( + _( + "Cannot disassemble {0} qty against Stock Entry {1}. Only {2} qty available to disassemble." + ).format( + self.fg_completed_qty, + self.source_stock_entry, + available_qty, + ), + title=_("Excess Disassembly"), + ) + def check_if_operations_completed(self): """Check if Time Sheets are completed against before manufacturing to capture operating costs.""" prod_order = frappe.get_doc("Work Order", self.work_order) From 4232640a8b2670d04ac585f6a7eb4dc53b32ddf8 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Mon, 30 Mar 2026 18:19:19 +0530 Subject: [PATCH 55/79] fix: add support to fetch items based on manufacture stock entry; fix how it's done from work order (cherry picked from commit 1ed0124ad7668cffe2aa858edaadf9a52faab313) --- .../stock/doctype/stock_entry/stock_entry.py | 178 ++++++++++++------ 1 file changed, 121 insertions(+), 57 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index c1eeec3eaf4..7284b50a3e7 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -29,7 +29,6 @@ from erpnext.buying.utils import check_on_hold_or_closed_status from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals from erpnext.manufacturing.doctype.bom.bom import ( add_additional_cost, - get_bom_items_as_dict, get_op_cost_from_sub_assemblies, get_secondary_items_from_sub_assemblies, validate_bom_no, @@ -2269,45 +2268,108 @@ class StockEntry(StockController, SubcontractingInwardController): ) def get_items_for_disassembly(self): - """Get items for Disassembly Order""" + """Get items for Disassembly Order. + + Priority: + 1. From a specific Manufacture Stock Entry (exact reversal) + 2. From Work Order required_items (reflects WO changes) + 3. From BOM (standalone disassembly) + """ + + if self.get("source_stock_entry"): + return self._add_items_for_disassembly_from_stock_entry() if self.work_order: return self._add_items_for_disassembly_from_work_order() return self._add_items_for_disassembly_from_bom() + def _add_items_for_disassembly_from_stock_entry(self): + source_fg_qty = frappe.db.get_value("Stock Entry", self.source_stock_entry, "fg_completed_qty") + if not source_fg_qty: + frappe.throw( + _("Source Stock Entry {0} has no finished goods quantity").format(self.source_stock_entry) + ) + + scale_factor = flt(self.fg_completed_qty) / flt(source_fg_qty) + + for source_row in self.get_items_from_manufacture_stock_entry(self.source_stock_entry): + if source_row.is_finished_item: + qty = flt(self.fg_completed_qty) + s_warehouse = self.from_warehouse or source_row.t_warehouse + t_warehouse = "" + else: + qty = flt(source_row.qty * scale_factor) + s_warehouse = "" + t_warehouse = self.to_warehouse or source_row.s_warehouse + + use_serial_batch_fields = 1 if (source_row.batch_no or source_row.serial_no) else 0 + + self.append( + "items", + { + "item_code": source_row.item_code, + "item_name": source_row.item_name, + "description": source_row.description, + "stock_uom": source_row.stock_uom, + "uom": source_row.uom, + "conversion_factor": source_row.conversion_factor, + "basic_rate": source_row.basic_rate, + "qty": qty, + "s_warehouse": s_warehouse, + "t_warehouse": t_warehouse, + "is_finished_item": source_row.is_finished_item, + "against_stock_entry": self.source_stock_entry, + "ste_detail": source_row.name, + "batch_no": source_row.batch_no, + "serial_no": source_row.serial_no, + "use_serial_batch_fields": use_serial_batch_fields, + }, + ) + def _add_items_for_disassembly_from_work_order(self): - items = self.get_items_from_manufacture_entry() + wo = frappe.get_doc("Work Order", self.work_order) - s_warehouse = frappe.db.get_value("Work Order", self.work_order, "fg_warehouse") + if not wo.required_items: + return self._add_items_for_disassembly_from_bom() - items_dict = get_bom_items_as_dict( - self.bom_no, - self.company, - self.fg_completed_qty, - fetch_exploded=self.use_multi_level_bom, - fetch_qty_in_stock_uom=False, + scale_factor = flt(self.fg_completed_qty) / flt(wo.qty) if flt(wo.qty) else 0 + + # RMs + for ri in wo.required_items: + self.append( + "items", + { + "item_code": ri.item_code, + "item_name": ri.item_name, + "description": ri.description, + "qty": flt(ri.required_qty * scale_factor), + "stock_uom": ri.stock_uom, + "uom": ri.stock_uom, + "conversion_factor": 1, + "t_warehouse": ri.source_warehouse or wo.source_warehouse or self.to_warehouse, + "s_warehouse": "", + "is_finished_item": 0, + }, + ) + + # FG + self.append( + "items", + { + "item_code": wo.production_item, + "item_name": wo.item_name, + "description": wo.description, + "qty": flt(self.fg_completed_qty), + "stock_uom": wo.stock_uom, + "uom": wo.stock_uom, + "conversion_factor": 1, + "s_warehouse": self.from_warehouse or wo.fg_warehouse, + "t_warehouse": "", + "is_finished_item": 1, + }, ) - for row in items: - child_row = self.append("items", {}) - for field, value in row.items(): - if value is not None: - child_row.set(field, value) - - # update qty and amount from BOM items - bom_items = items_dict.get(row.item_code) - if bom_items: - child_row.qty = bom_items.get("qty", child_row.qty) - child_row.amount = bom_items.get("amount", child_row.amount) - - if row.is_finished_item: - child_row.qty = self.fg_completed_qty - - child_row.s_warehouse = (self.from_warehouse or s_warehouse) if row.is_finished_item else "" - child_row.t_warehouse = row.s_warehouse - child_row.is_finished_item = 0 if row.is_finished_item else 1 - def _add_items_for_disassembly_from_bom(self): if not self.bom_no or not self.fg_completed_qty: frappe.throw(_("BOM and Finished Good Quantity is mandatory for Disassembly")) @@ -2325,34 +2387,36 @@ class StockEntry(StockController, SubcontractingInwardController): # Finished goods self.load_items_from_bom() - def get_items_from_manufacture_entry(self): - return frappe.get_all( - "Stock Entry", - fields=[ - "`tabStock Entry Detail`.`item_code`", - "`tabStock Entry Detail`.`item_name`", - "`tabStock Entry Detail`.`description`", - {"SUM": "`tabStock Entry Detail`.`qty`", "as": "qty"}, - {"SUM": "`tabStock Entry Detail`.`transfer_qty`", "as": "transfer_qty"}, - "`tabStock Entry Detail`.`stock_uom`", - "`tabStock Entry Detail`.`uom`", - "`tabStock Entry Detail`.`basic_rate`", - "`tabStock Entry Detail`.`conversion_factor`", - "`tabStock Entry Detail`.`is_finished_item`", - "`tabStock Entry Detail`.`batch_no`", - "`tabStock Entry Detail`.`serial_no`", - "`tabStock Entry Detail`.`s_warehouse`", - "`tabStock Entry Detail`.`t_warehouse`", - "`tabStock Entry Detail`.`use_serial_batch_fields`", - ], - filters=[ - ["Stock Entry", "purpose", "=", "Manufacture"], - ["Stock Entry", "work_order", "=", self.work_order], - ["Stock Entry", "docstatus", "=", 1], - ["Stock Entry Detail", "docstatus", "=", 1], - ], - order_by="`tabStock Entry Detail`.`idx` desc, `tabStock Entry Detail`.`is_finished_item` desc", - group_by="`tabStock Entry Detail`.`item_code`", + def get_items_from_manufacture_stock_entry(self, stock_entry): + SE = frappe.qb.DocType("Stock Entry") + SED = frappe.qb.DocType("Stock Entry Detail") + + return ( + frappe.qb.from_(SED) + .join(SE) + .on(SED.parent == SE.name) + .select( + SED.name, + SED.item_code, + SED.item_name, + SED.description, + SED.qty, + SED.transfer_qty, + SED.stock_uom, + SED.uom, + SED.basic_rate, + SED.conversion_factor, + SED.is_finished_item, + SED.batch_no, + SED.serial_no, + SED.use_serial_batch_fields, + SED.s_warehouse, + SED.t_warehouse, + ) + .where(SE.name == stock_entry) + .where(SE.docstatus == 1) + .orderby(SED.idx) + .run(as_dict=True) ) @frappe.whitelist() From eead8d6d8c219daa21c5742efe9255fb80833a9e Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Mon, 30 Mar 2026 18:19:55 +0530 Subject: [PATCH 56/79] fix: auto-set source_stock_entry (cherry picked from commit 2e4e8bcaa7566283fe8d9db3bf9a50cfb1f1b68e) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 7284b50a3e7..b878f097e60 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2276,6 +2276,21 @@ class StockEntry(StockController, SubcontractingInwardController): 3. From BOM (standalone disassembly) """ + # Auto-set source_stock_entry if WO has exactly one manufacture entry + if not self.get("source_stock_entry") and self.work_order: + manufacture_entries = frappe.get_all( + "Stock Entry", + filters={ + "work_order": self.work_order, + "purpose": "Manufacture", + "docstatus": 1, + }, + pluck="name", + limit_page_length=2, + ) + if len(manufacture_entries) == 1: + self.source_stock_entry = manufacture_entries[0] + if self.get("source_stock_entry"): return self._add_items_for_disassembly_from_stock_entry() From 919cbd5c0229fdde896afad5dbf58fbe2da65556 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Tue, 31 Mar 2026 07:44:25 +0530 Subject: [PATCH 57/79] fix: correct warehouse preference for disassemble (cherry picked from commit d3d6b5c6608b9a21db381160987c2b5fa17f2229) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index b878f097e60..2068b0cf514 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2362,7 +2362,8 @@ class StockEntry(StockController, SubcontractingInwardController): "stock_uom": ri.stock_uom, "uom": ri.stock_uom, "conversion_factor": 1, - "t_warehouse": ri.source_warehouse or wo.source_warehouse or self.to_warehouse, + # manufacture transfers RMs from WIP (not source warehouse) + "t_warehouse": self.to_warehouse or wo.wip_warehouse, "s_warehouse": "", "is_finished_item": 0, }, From ff104edf1289dbf9876db11d8bdc95ecd9fb8d3b Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Tue, 31 Mar 2026 09:27:19 +0530 Subject: [PATCH 58/79] fix: set serial and batch from source stock entry - on disassemble (cherry picked from commit 13b019ab8efe75cff787b400cbbcb9b5c9677bfb) --- .../stock/doctype/stock_entry/stock_entry.py | 107 +++++++++++++----- 1 file changed, 81 insertions(+), 26 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 2068b0cf514..3aee2be095a 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -331,6 +331,58 @@ class StockEntry(StockController, SubcontractingInwardController): if self.purpose != "Disassemble": return + if self.get("source_stock_entry"): + self._set_serial_batch_for_disassembly_from_stock_entry() + else: + self._set_serial_batch_for_disassembly_from_available_materials() + + def _set_serial_batch_for_disassembly_from_stock_entry(self): + from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + get_voucher_wise_serial_batch_from_bundle, + ) + + source_fg_qty = flt(frappe.db.get_value("Stock Entry", self.source_stock_entry, "fg_completed_qty")) + scale_factor = flt(self.fg_completed_qty) / source_fg_qty if source_fg_qty else 0 + + bundle_data = get_voucher_wise_serial_batch_from_bundle(voucher_no=[self.source_stock_entry]) + source_rows_by_name = { + r.name: r for r in self.get_items_from_manufacture_stock_entry(self.source_stock_entry) + } + + for row in self.items: + if not row.ste_detail: + continue + + source_row = source_rows_by_name.get(row.ste_detail) + if not source_row: + continue + + source_warehouse = source_row.s_warehouse or source_row.t_warehouse + key = (source_row.item_code, source_warehouse, self.source_stock_entry) + source_bundle = bundle_data.get(key, {}) + + batches = defaultdict(float) + serial_nos = [] + + if source_bundle.get("batch_nos"): + qty_remaining = row.transfer_qty + for batch_no, batch_qty in source_bundle["batch_nos"].items(): + if qty_remaining <= 0: + break + alloc = min(flt(batch_qty) * scale_factor, qty_remaining) + batches[batch_no] = alloc + qty_remaining -= alloc + elif source_row.batch_no: + batches[source_row.batch_no] = row.transfer_qty + + if source_bundle.get("serial_nos"): + serial_nos = get_serial_nos(source_bundle["serial_nos"])[: int(row.transfer_qty)] + elif source_row.serial_no: + serial_nos = get_serial_nos(source_row.serial_no)[: int(row.transfer_qty)] + + self._set_serial_batch_bundle_for_disassembly_row(row, serial_nos, batches) + + def _set_serial_batch_for_disassembly_from_available_materials(self): available_materials = get_available_materials(self.work_order, self) for row in self.items: warehouse = row.s_warehouse or row.t_warehouse @@ -356,33 +408,37 @@ class StockEntry(StockController, SubcontractingInwardController): if materials.serial_nos: serial_nos = materials.serial_nos[: int(row.transfer_qty)] - if not serial_nos and not batches: - continue + self._set_serial_batch_bundle_for_disassembly_row(row, serial_nos, batches) - bundle_doc = SerialBatchCreation( - { - "item_code": row.item_code, - "warehouse": warehouse, - "posting_datetime": get_combine_datetime(self.posting_date, self.posting_time), - "voucher_type": self.doctype, - "voucher_no": self.name, - "voucher_detail_no": row.name, - "qty": row.transfer_qty, - "type_of_transaction": "Inward" if row.t_warehouse else "Outward", - "company": self.company, - "do_not_submit": True, - } - ).make_serial_and_batch_bundle(serial_nos=serial_nos, batch_nos=batches) + def _set_serial_batch_bundle_for_disassembly_row(self, row, serial_nos, batches): + if not serial_nos and not batches: + return - row.serial_and_batch_bundle = bundle_doc.name - row.use_serial_batch_fields = 0 + warehouse = row.s_warehouse or row.t_warehouse + bundle_doc = SerialBatchCreation( + { + "item_code": row.item_code, + "warehouse": warehouse, + "posting_datetime": get_combine_datetime(self.posting_date, self.posting_time), + "voucher_type": self.doctype, + "voucher_no": self.name, + "voucher_detail_no": row.name, + "qty": row.transfer_qty, + "type_of_transaction": "Inward" if row.t_warehouse else "Outward", + "company": self.company, + "do_not_submit": True, + } + ).make_serial_and_batch_bundle(serial_nos=serial_nos, batch_nos=batches) - row.db_set( - { - "serial_and_batch_bundle": bundle_doc.name, - "use_serial_batch_fields": 0, - } - ) + row.serial_and_batch_bundle = bundle_doc.name + row.use_serial_batch_fields = 0 + + row.db_set( + { + "serial_and_batch_bundle": bundle_doc.name, + "use_serial_batch_fields": 0, + } + ) def on_submit(self): self.set_serial_batch_for_disassembly() @@ -2336,8 +2392,7 @@ class StockEntry(StockController, SubcontractingInwardController): "is_finished_item": source_row.is_finished_item, "against_stock_entry": self.source_stock_entry, "ste_detail": source_row.name, - "batch_no": source_row.batch_no, - "serial_no": source_row.serial_no, + # batch and serial bundles built on submit "use_serial_batch_fields": use_serial_batch_fields, }, ) From 195a10efb3577a1de72daa25cd4f72d7f3d4d306 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Tue, 31 Mar 2026 15:01:46 +0530 Subject: [PATCH 59/79] test: disassembly from wo (cherry picked from commit 342a14d3403de36a09f5df3e3739489e9a1ab879) --- .../doctype/work_order/test_work_order.py | 24 +++---------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 81ee66ecb4f..7fec4314bca 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -2419,7 +2419,7 @@ class TestWorkOrder(ERPNextTestSuite): stock_entry.submit() - def test_disassembly_order_with_qty_behavior(self): + def test_disassembly_order_with_qty_from_wo_behavior(self): # Create raw material and FG item raw_item = make_item("Test Raw for Disassembly", {"is_stock_item": 1}).name fg_item = make_item("Test FG for Disassembly", {"is_stock_item": 1}).name @@ -2459,27 +2459,9 @@ class TestWorkOrder(ERPNextTestSuite): se_for_manufacture = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", wo.qty)) se_for_manufacture.submit() - # Simulate a disassembly stock entry + # Disassembly via WO required_items path (no source_stock_entry) disassemble_qty = 4 stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Disassemble", disassemble_qty)) - stock_entry.append( - "items", - { - "item_code": fg_item, - "qty": disassemble_qty, - "s_warehouse": wo.fg_warehouse, - }, - ) - - for bom_item in bom.items: - stock_entry.append( - "items", - { - "item_code": bom_item.item_code, - "qty": (bom_item.qty / bom.quantity) * disassemble_qty, - "t_warehouse": wo.source_warehouse, - }, - ) wo.reload() stock_entry.save() @@ -2494,7 +2476,7 @@ class TestWorkOrder(ERPNextTestSuite): f"Expected FG qty {disassemble_qty}, found {finished_good_entry.qty}", ) - # Assert raw materials + # Assert raw materials - qty scaled from WO required_items for item in stock_entry.items: if item.item_code == fg_item: continue From 4c0ebee15be6bc8d4ac131eb725f097017f5a2dd Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Tue, 31 Mar 2026 15:14:56 +0530 Subject: [PATCH 60/79] test: disassemble with source stock entry reference (cherry picked from commit 6988e2cbbc00221c117a07c0a45edac5d97b34ab) --- .../doctype/work_order/test_work_order.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 7fec4314bca..b62b4194f72 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -2500,6 +2500,30 @@ class TestWorkOrder(ERPNextTestSuite): f"Work Order disassembled_qty mismatch: expected {disassemble_qty}, got {wo.disassembled_qty}", ) + # Second disassembly: explicitly linked to manufacture SE — verifies SE-linked path + # (first disassembly auto-set source_stock_entry since there's only one manufacture entry) + disassemble_qty_2 = 2 + stock_entry_2 = frappe.get_doc( + make_stock_entry( + wo.name, "Disassemble", disassemble_qty_2, source_stock_entry=se_for_manufacture.name + ) + ) + stock_entry_2.save() + stock_entry_2.submit() + + # All rows must trace back to se_for_manufacture + for item in stock_entry_2.items: + self.assertEqual(item.against_stock_entry, se_for_manufacture.name) + self.assertTrue(item.ste_detail) + + # RM qty scaled from the manufacture SE rows + rm_row = next((i for i in stock_entry_2.items if i.item_code == raw_item), None) + expected_rm_qty = (bom.items[0].qty / bom.quantity) * disassemble_qty_2 + self.assertAlmostEqual(rm_row.qty, expected_rm_qty, places=3) + + wo.reload() + self.assertEqual(wo.disassembled_qty, disassemble_qty + disassemble_qty_2) + def test_disassembly_with_multiple_manufacture_entries(self): """ Test that disassembly does not create duplicate items when manufacturing From 8444778f74e38c10eedb80afb7832b56a57cab99 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Tue, 31 Mar 2026 15:18:42 +0530 Subject: [PATCH 61/79] test: additional items in stock entry considered with disassembly (cherry picked from commit d32977e3a9d0eea3813a3a9368146df8c69ba995) --- .../doctype/work_order/test_work_order.py | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index b62b4194f72..7ff5b0fee82 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -2639,17 +2639,16 @@ class TestWorkOrder(ERPNextTestSuite): def test_disassembly_with_additional_rm_not_in_bom(self): """ - Test that disassembly correctly handles additional raw materials that were - manually added during manufacturing (not part of the BOM). + Test that SE-linked disassembly includes additional raw materials + that were manually added during manufacturing (not part of the BOM). Scenario: 1. Create Work Order for 10 units with 2 raw materials in BOM 2. Transfer raw materials for manufacture 3. Manufacture in 2 parts (3 units, then 7 units) 4. In each manufacture entry, manually add an extra consumable item - (not in BOM) in proportion to the manufactured qty - 5. Create Disassembly for 4 units - 6. Verify that the additional RM is included in disassembly with proportional qty + 5. Disassemble 3 units linked to first manufacture entry + 6. Verify additional RM is included with correct proportional qty from SE1 """ from erpnext.stock.doctype.stock_entry.test_stock_entry import ( make_stock_entry as make_stock_entry_test_record, @@ -2685,9 +2684,8 @@ class TestWorkOrder(ERPNextTestSuite): se_for_material_transfer.save() se_for_material_transfer.submit() - # First Manufacture Entry - 3 units + # First Manufacture Entry - 3 units with additional RM se_manufacture1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 3)) - # Additional RM se_manufacture1.append( "items", { @@ -2700,9 +2698,8 @@ class TestWorkOrder(ERPNextTestSuite): se_manufacture1.save() se_manufacture1.submit() - # Second Manufacture Entry - 7 units + # Second Manufacture Entry - 7 units with additional RM se_manufacture2 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 7)) - # AAdditional RM se_manufacture2.append( "items", { @@ -2718,13 +2715,15 @@ class TestWorkOrder(ERPNextTestSuite): wo.reload() self.assertEqual(wo.produced_qty, 10) - # Disassembly for 4 units - disassemble_qty = 4 - stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Disassemble", disassemble_qty)) + # Disassemble 3 units linked to first manufacture entry + disassemble_qty = 3 + stock_entry = frappe.get_doc( + make_stock_entry(wo.name, "Disassemble", disassemble_qty, source_stock_entry=se_manufacture1.name) + ) stock_entry.save() stock_entry.submit() - # No duplicate + # No duplicates item_counts = {} for item in stock_entry.items: item_code = item.item_code @@ -2737,16 +2736,15 @@ class TestWorkOrder(ERPNextTestSuite): f"Found duplicate items in disassembly stock entry: {duplicates}", ) - # Additional RM qty + # Additional RM should be included — qty proportional to SE1 (3 units -> 3 additional RM) additional_rm_row = next((i for i in stock_entry.items if i.item_code == additional_rm), None) self.assertIsNotNone( additional_rm_row, f"Additional raw material {additional_rm} not found in disassembly", ) - # intentional full reversal as not part of BOM - # eg: dies or consumables used during manufacturing - expected_additional_rm_qty = 3 + 7 + # SE1 had 3 additional RM for 3 manufactured units, disassembling all 3 + expected_additional_rm_qty = 3 self.assertAlmostEqual( additional_rm_row.qty, expected_additional_rm_qty, @@ -2754,7 +2752,7 @@ class TestWorkOrder(ERPNextTestSuite): msg=f"Additional RM qty mismatch: expected {expected_additional_rm_qty}, got {additional_rm_row.qty}", ) - # RM qty + # BOM RM qty — scaled from SE1's rows for bom_item in bom.items: expected_qty = (bom_item.qty / bom.quantity) * disassemble_qty rm_row = next((i for i in stock_entry.items if i.item_code == bom_item.item_code), None) @@ -2770,6 +2768,7 @@ class TestWorkOrder(ERPNextTestSuite): fg_item_row = next((i for i in stock_entry.items if i.item_code == fg_item), None) self.assertEqual(fg_item_row.qty, disassemble_qty) + # FG + 2 BOM RM + 1 additional RM = 4 items expected_items = 4 self.assertEqual( len(stock_entry.items), @@ -2777,6 +2776,11 @@ class TestWorkOrder(ERPNextTestSuite): f"Expected {expected_items} items, found {len(stock_entry.items)}", ) + # Verify traceability + for item in stock_entry.items: + self.assertEqual(item.against_stock_entry, se_manufacture1.name) + self.assertTrue(item.ste_detail) + def test_components_alternate_item_for_bom_based_manufacture_entry(self): frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM") frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 1) From e1a4d9fab44b84a2c4a85763b10b36b7adff62a0 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Tue, 31 Mar 2026 17:52:58 +0530 Subject: [PATCH 62/79] test: disassembly of items with batch and serial numbers (cherry picked from commit 1693698fed085fdc5a20b9bdd23a0d5ae96af195) --- .../doctype/work_order/test_work_order.py | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 7ff5b0fee82..ac9ce2091a9 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -2781,6 +2781,206 @@ class TestWorkOrder(ERPNextTestSuite): self.assertEqual(item.against_stock_entry, se_manufacture1.name) self.assertTrue(item.ste_detail) + def test_disassembly_auto_sets_source_stock_entry(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import ( + make_stock_entry as make_stock_entry_test_record, + ) + + raw_item = make_item("Test Raw Auto Set Disassembly", {"is_stock_item": 1}).name + fg_item = make_item("Test FG Auto Set Disassembly", {"is_stock_item": 1}).name + bom = make_bom(item=fg_item, quantity=1, raw_materials=[raw_item], rm_qty=2) + + wo = make_wo_order_test_record(production_item=fg_item, qty=5, bom_no=bom.name, status="Not Started") + + make_stock_entry_test_record( + item_code=raw_item, purpose="Material Receipt", target=wo.wip_warehouse, qty=50, basic_rate=100 + ) + + se_transfer = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", wo.qty)) + for item in se_transfer.items: + item.s_warehouse = wo.wip_warehouse + se_transfer.save() + se_transfer.submit() + + se_manufacture = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", wo.qty)) + se_manufacture.submit() + + # Disassemble without specifying source_stock_entry + stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Disassemble", 3)) + stock_entry.save() + + # source_stock_entry should be auto-set since only one manufacture entry + self.assertEqual(stock_entry.source_stock_entry, se_manufacture.name) + + # All items should have against_stock_entry linked + for item in stock_entry.items: + self.assertEqual(item.against_stock_entry, se_manufacture.name) + self.assertTrue(item.ste_detail) + + stock_entry.submit() + + def test_disassembly_batch_tracked_items(self): + from erpnext.stock.doctype.batch.batch import make_batch + from erpnext.stock.doctype.stock_entry.test_stock_entry import ( + make_stock_entry as make_stock_entry_test_record, + ) + + wip_wh = "_Test Warehouse - _TC" + + rm_item = make_item( + "Test Batch RM for Disassembly SB", + { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TBRD-RM-.###", + }, + ).name + fg_item = make_item( + "Test Batch FG for Disassembly SB", + { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TBRD-FG-.###", + }, + ).name + + bom = make_bom(item=fg_item, quantity=1, raw_materials=[rm_item], rm_qty=2) + wo = make_wo_order_test_record( + production_item=fg_item, + qty=6, + bom_no=bom.name, + skip_transfer=1, + source_warehouse=wip_wh, + status="Not Started", + ) + + # Stock up RM — batch auto-created on receipt + rm_receipt = make_stock_entry_test_record( + item_code=rm_item, purpose="Material Receipt", target=wip_wh, qty=18, basic_rate=100 + ) + rm_bundle = frappe.db.get_value( + "Stock Entry Detail", {"parent": rm_receipt.name, "item_code": rm_item}, "serial_and_batch_bundle" + ) + rm_batch = get_batch_from_bundle(rm_bundle) + + # Pre-create FG batch so we can assign it to the manufacture row + fg_batch = make_batch(frappe._dict(item=fg_item)) + + # Manufacture 3 units: assign batches explicitly on RM and FG rows + se_manufacture = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 3)) + for row in se_manufacture.items: + if row.item_code == rm_item: + row.batch_no = rm_batch + row.use_serial_batch_fields = 1 + elif row.item_code == fg_item: + row.batch_no = fg_batch + row.use_serial_batch_fields = 1 + se_manufacture.save() + se_manufacture.submit() + + # Disassemble 2 of the 3 manufactured units linked to the manufacture SE + disassemble_qty = 2 + stock_entry = frappe.get_doc( + make_stock_entry(wo.name, "Disassemble", disassemble_qty, source_stock_entry=se_manufacture.name) + ) + stock_entry.save() + stock_entry.submit() + + # FG row: consuming batch from FG warehouse — bundle must use FG batch + fg_row = next((i for i in stock_entry.items if i.item_code == fg_item), None) + self.assertIsNotNone(fg_row) + self.assertTrue(fg_row.serial_and_batch_bundle, "FG row must have a serial_and_batch_bundle") + self.assertEqual(get_batch_from_bundle(fg_row.serial_and_batch_bundle), fg_batch) + + # RM row: returning to WIP warehouse — bundle must use RM batch + rm_row = next((i for i in stock_entry.items if i.item_code == rm_item), None) + self.assertIsNotNone(rm_row) + self.assertTrue(rm_row.serial_and_batch_bundle, "RM row must have a serial_and_batch_bundle") + self.assertEqual(get_batch_from_bundle(rm_row.serial_and_batch_bundle), rm_batch) + + # RM qty: 2 FG disassembled x 2 RM per FG = 4 + self.assertAlmostEqual(rm_row.qty, 4.0, places=3) + + def test_disassembly_serial_tracked_items(self): + from frappe.model.naming import make_autoname + + from erpnext.stock.doctype.stock_entry.test_stock_entry import ( + make_stock_entry as make_stock_entry_test_record, + ) + + wip_wh = "_Test Warehouse - _TC" + + rm_item = make_item( + "Test Serial RM for Disassembly SB", + {"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "TSRD-RM-.####"}, + ).name + fg_item = make_item( + "Test Serial FG for Disassembly SB", + {"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "TSRD-FG-.####"}, + ).name + + bom = make_bom(item=fg_item, quantity=1, raw_materials=[rm_item], rm_qty=2) + wo = make_wo_order_test_record( + production_item=fg_item, + qty=6, + bom_no=bom.name, + skip_transfer=1, + source_warehouse=wip_wh, + status="Not Started", + ) + + # Stock up 6 RM serials — series auto-generates them + rm_receipt = make_stock_entry_test_record( + item_code=rm_item, purpose="Material Receipt", target=wip_wh, qty=6, basic_rate=100 + ) + rm_bundle = frappe.db.get_value( + "Stock Entry Detail", {"parent": rm_receipt.name, "item_code": rm_item}, "serial_and_batch_bundle" + ) + all_rm_serials = get_serial_nos_from_bundle(rm_bundle) + self.assertEqual(len(all_rm_serials), 6) + + # Pre-generate 3 FG serial numbers + series = frappe.db.get_value("Item", fg_item, "serial_no_series") + fg_serials = [make_autoname(series) for _ in range(3)] + + # Manufacture 3 units: consume first 6 RM serials, produce 3 FG serials + se_manufacture = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 3)) + for row in se_manufacture.items: + if row.item_code == rm_item: + row.serial_no = "\n".join(all_rm_serials) + row.use_serial_batch_fields = 1 + elif row.item_code == fg_item: + row.serial_no = "\n".join(fg_serials) + row.use_serial_batch_fields = 1 + se_manufacture.save() + se_manufacture.submit() + + # Disassemble 2 of the 3 manufactured units + disassemble_qty = 2 + stock_entry = frappe.get_doc( + make_stock_entry(wo.name, "Disassemble", disassemble_qty, source_stock_entry=se_manufacture.name) + ) + stock_entry.save() + stock_entry.submit() + + # FG row: 2 serials consumed — must be a subset of the manufacture FG serials + fg_row = next((i for i in stock_entry.items if i.item_code == fg_item), None) + self.assertIsNotNone(fg_row) + self.assertTrue(fg_row.serial_and_batch_bundle, "FG row must have a serial_and_batch_bundle") + fg_dasm_serials = get_serial_nos_from_bundle(fg_row.serial_and_batch_bundle) + self.assertEqual(len(fg_dasm_serials), disassemble_qty) + self.assertTrue(set(fg_dasm_serials).issubset(set(fg_serials))) + + # RM row: 4 serials returned (2 FG x 2 RM each) — must be a subset of manufacture RM serials + rm_row = next((i for i in stock_entry.items if i.item_code == rm_item), None) + self.assertIsNotNone(rm_row) + self.assertTrue(rm_row.serial_and_batch_bundle, "RM row must have a serial_and_batch_bundle") + rm_dasm_serials = get_serial_nos_from_bundle(rm_row.serial_and_batch_bundle) + self.assertEqual(len(rm_dasm_serials), disassemble_qty * 2) + self.assertTrue(set(rm_dasm_serials).issubset(set(all_rm_serials))) + def test_components_alternate_item_for_bom_based_manufacture_entry(self): frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM") frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 1) From d50279b718c195ba6f2735d448bc4736f3d4bfcd Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Wed, 1 Apr 2026 15:48:25 +0530 Subject: [PATCH 63/79] fix: handle disassembly for secondary / scrap items (cherry picked from commit 2be8313819c7811afc3758a037457d46f3cae244) --- .../stock/doctype/stock_entry/stock_entry.py | 43 ++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 3aee2be095a..046e325375a 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -864,7 +864,7 @@ class StockEntry(StockController, SubcontractingInwardController): if self.purpose == "Disassemble": if has_bom: - if d.is_finished_item: + if d.is_finished_item or d.type or d.is_legacy_scrap_item: d.t_warehouse = None if not d.s_warehouse: frappe.throw(_("Source warehouse is mandatory for row {0}").format(d.idx)) @@ -2369,10 +2369,16 @@ class StockEntry(StockController, SubcontractingInwardController): qty = flt(self.fg_completed_qty) s_warehouse = self.from_warehouse or source_row.t_warehouse t_warehouse = "" - else: + elif source_row.s_warehouse: + # RM: was consumed FROM s_warehouse → return TO s_warehouse qty = flt(source_row.qty * scale_factor) s_warehouse = "" t_warehouse = self.to_warehouse or source_row.s_warehouse + else: + # Scrap/secondary: was produced TO t_warehouse → take FROM t_warehouse + qty = flt(source_row.qty * scale_factor) + s_warehouse = source_row.t_warehouse + t_warehouse = "" use_serial_batch_fields = 1 if (source_row.batch_no or source_row.serial_no) else 0 @@ -2390,6 +2396,9 @@ class StockEntry(StockController, SubcontractingInwardController): "s_warehouse": s_warehouse, "t_warehouse": t_warehouse, "is_finished_item": source_row.is_finished_item, + "type": source_row.type, + "is_legacy_scrap_item": source_row.is_legacy_scrap_item, + "bom_secondary_item": source_row.bom_secondary_item, "against_stock_entry": self.source_stock_entry, "ste_detail": source_row.name, # batch and serial bundles built on submit @@ -2424,6 +2433,16 @@ class StockEntry(StockController, SubcontractingInwardController): }, ) + # Secondary/Scrap items + secondary_items = self.get_secondary_items(self.fg_completed_qty) + if secondary_items: + scrap_warehouse = wo.scrap_warehouse or self.from_warehouse or wo.fg_warehouse + for item in secondary_items.values(): + item["from_warehouse"] = scrap_warehouse + item["to_warehouse"] = "" + item["is_finished_item"] = 0 + self.add_to_stock_entry_detail(secondary_items, bom_no=self.bom_no) + # FG self.append( "items", @@ -2455,6 +2474,23 @@ class StockEntry(StockController, SubcontractingInwardController): self.add_to_stock_entry_detail(item_dict) + # Secondary/Scrap items (reverse of what set_secondary_items does for Manufacture) + secondary_items = self.get_secondary_items(self.fg_completed_qty) + if secondary_items: + scrap_warehouse = self.from_warehouse + if self.work_order: + wo_values = frappe.db.get_value( + "Work Order", self.work_order, ["scrap_warehouse", "fg_warehouse"], as_dict=True + ) + scrap_warehouse = wo_values.scrap_warehouse or scrap_warehouse or wo_values.fg_warehouse + + for item in secondary_items.values(): + item["from_warehouse"] = scrap_warehouse + item["to_warehouse"] = "" + item["is_finished_item"] = 0 + + self.add_to_stock_entry_detail(secondary_items, bom_no=self.bom_no) + # Finished goods self.load_items_from_bom() @@ -2478,6 +2514,9 @@ class StockEntry(StockController, SubcontractingInwardController): SED.basic_rate, SED.conversion_factor, SED.is_finished_item, + SED.type, + SED.is_legacy_scrap_item, + SED.bom_secondary_item, SED.batch_no, SED.serial_no, SED.use_serial_batch_fields, From 901e6267293d3c02a0c98ea21b1047143d063a19 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Wed, 1 Apr 2026 16:17:00 +0530 Subject: [PATCH 64/79] test: disassembly for scrap / secondary item (cherry picked from commit a6d41151ff33e99d8e5c189ee90b4c3707537956) --- .../doctype/work_order/test_work_order.py | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index ac9ce2091a9..77a9acdf02e 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -2527,7 +2527,8 @@ class TestWorkOrder(ERPNextTestSuite): def test_disassembly_with_multiple_manufacture_entries(self): """ Test that disassembly does not create duplicate items when manufacturing - is done in multiple batches (multiple manufacture stock entries). + is done in multiple batches (multiple manufacture stock entries), including + secondary/scrap items. Scenario: 1. Create Work Order for 10 units @@ -2536,11 +2537,19 @@ class TestWorkOrder(ERPNextTestSuite): 4. Create Disassembly for 4 units 5. Verify no duplicate items in the disassembly stock entry """ - # Create RM and FG item + # Create RM, scrap and FG item raw_item1 = make_item("Test Raw for Multi Batch Disassembly 1", {"is_stock_item": 1}).name raw_item2 = make_item("Test Raw for Multi Batch Disassembly 2", {"is_stock_item": 1}).name + scrap_item = make_item("Test Scrap for Multi Batch Disassembly", {"is_stock_item": 1}).name fg_item = make_item("Test FG for Multi Batch Disassembly", {"is_stock_item": 1}).name - bom = make_bom(item=fg_item, quantity=1, raw_materials=[raw_item1, raw_item2], rm_qty=2) + bom = make_bom( + item=fg_item, + quantity=1, + raw_materials=[raw_item1, raw_item2], + rm_qty=2, + scrap_items=[scrap_item], + scrap_qty=10, + ) # Create WO wo = make_wo_order_test_record(production_item=fg_item, qty=10, bom_no=bom.name, status="Not Started") @@ -2615,7 +2624,7 @@ class TestWorkOrder(ERPNextTestSuite): f"Found duplicate items in disassembly stock entry: {duplicates}", ) - expected_items = 3 # FG item + 2 raw materials + expected_items = 4 # FG item + 2 raw materials + 1 scrap item self.assertEqual( len(stock_entry.items), expected_items, @@ -2626,6 +2635,15 @@ class TestWorkOrder(ERPNextTestSuite): fg_item_row = next((i for i in stock_entry.items if i.item_code == fg_item), None) self.assertEqual(fg_item_row.qty, disassemble_qty) + # Secondary/Scrap item: should be taken from scrap warehouse in disassembly + scrap_row = next((i for i in stock_entry.items if i.item_code == scrap_item), None) + self.assertIsNotNone(scrap_row) + self.assertEqual(scrap_row.type, "Scrap") + self.assertTrue(scrap_row.s_warehouse) + self.assertFalse(scrap_row.t_warehouse) + self.assertEqual(scrap_row.s_warehouse, wo.scrap_warehouse) + self.assertEqual(scrap_row.qty, 40) + # RM quantities for bom_item in bom.items: expected_qty = (bom_item.qty / bom.quantity) * disassemble_qty From 31ac46ae4c04ce379def08dc2cadb84f5e5a1261 Mon Sep 17 00:00:00 2001 From: vorasmit Date: Wed, 1 Apr 2026 23:42:36 +0530 Subject: [PATCH 65/79] fix: manufacture entry with group_by support (cherry picked from commit 3cf1ce83608b51a0e98663378670ed810bd6305c) --- .../stock/doctype/stock_entry/stock_entry.py | 60 ++++++++++--------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 046e325375a..92ff6d7da83 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2494,37 +2494,43 @@ class StockEntry(StockController, SubcontractingInwardController): # Finished goods self.load_items_from_bom() - def get_items_from_manufacture_stock_entry(self, stock_entry): + def get_items_from_manufacture_stock_entry(self, stock_entry=None): SE = frappe.qb.DocType("Stock Entry") SED = frappe.qb.DocType("Stock Entry Detail") + query = frappe.qb.from_(SED).join(SE).on(SED.parent == SE.name).where(SE.docstatus == 1) + + common_fields = [ + SED.item_code, + SED.item_name, + SED.description, + SED.stock_uom, + SED.uom, + SED.basic_rate, + SED.conversion_factor, + SED.is_finished_item, + SED.type, + SED.is_legacy_scrap_item, + SED.bom_secondary_item, + SED.batch_no, + SED.serial_no, + SED.use_serial_batch_fields, + SED.s_warehouse, + SED.t_warehouse, + ] + + if stock_entry: + return ( + query.select(SED.name, SED.qty, SED.transfer_qty, *common_fields) + .where(SE.name == stock_entry) + .orderby(SED.idx) + .run(as_dict=True) + ) return ( - frappe.qb.from_(SED) - .join(SE) - .on(SED.parent == SE.name) - .select( - SED.name, - SED.item_code, - SED.item_name, - SED.description, - SED.qty, - SED.transfer_qty, - SED.stock_uom, - SED.uom, - SED.basic_rate, - SED.conversion_factor, - SED.is_finished_item, - SED.type, - SED.is_legacy_scrap_item, - SED.bom_secondary_item, - SED.batch_no, - SED.serial_no, - SED.use_serial_batch_fields, - SED.s_warehouse, - SED.t_warehouse, - ) - .where(SE.name == stock_entry) - .where(SE.docstatus == 1) + query.select(Sum(SED.qty).as_("qty"), Sum(SED.transfer_qty).as_("transfer_qty"), *common_fields) + .where(SE.purpose == "Manufacture") + .where(SE.work_order == self.work_order) + .groupby(SED.item_code) .orderby(SED.idx) .run(as_dict=True) ) From 0ceb08410475c82157dbaff6431dcd06ae1031e2 Mon Sep 17 00:00:00 2001 From: vorasmit Date: Wed, 1 Apr 2026 23:56:44 +0530 Subject: [PATCH 66/79] fix: avg stock entries for disassembly from WO (cherry picked from commit 71fd18bdf93630410377c840342f1cd36933e3d6) --- .../doctype/work_order/test_work_order.py | 4 +- .../stock/doctype/stock_entry/stock_entry.py | 153 ++++++++---------- 2 files changed, 67 insertions(+), 90 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 77a9acdf02e..e5ae8ec39ec 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -2642,7 +2642,9 @@ class TestWorkOrder(ERPNextTestSuite): self.assertTrue(scrap_row.s_warehouse) self.assertFalse(scrap_row.t_warehouse) self.assertEqual(scrap_row.s_warehouse, wo.scrap_warehouse) - self.assertEqual(scrap_row.qty, 40) + # BOM has scrap_qty=10/FG but also process_loss_per=10%, so actual scrap per FG = 9 + # Total produced = 9*3 + 9*7 = 90, disassemble 4/10 → 36 + self.assertEqual(scrap_row.qty, 36) # RM quantities for bom_item in bom.items: diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 92ff6d7da83..9d73a3f117c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2328,7 +2328,7 @@ class StockEntry(StockController, SubcontractingInwardController): Priority: 1. From a specific Manufacture Stock Entry (exact reversal) - 2. From Work Order required_items (reflects WO changes) + 2. From Work Order Manufacture Stock Entries (averaged reversal) 3. From BOM (standalone disassembly) """ @@ -2362,104 +2362,79 @@ class StockEntry(StockController, SubcontractingInwardController): _("Source Stock Entry {0} has no finished goods quantity").format(self.source_stock_entry) ) - scale_factor = flt(self.fg_completed_qty) / flt(source_fg_qty) + disassemble_qty = flt(self.fg_completed_qty) + scale_factor = disassemble_qty / flt(source_fg_qty) - for source_row in self.get_items_from_manufacture_stock_entry(self.source_stock_entry): - if source_row.is_finished_item: - qty = flt(self.fg_completed_qty) - s_warehouse = self.from_warehouse or source_row.t_warehouse - t_warehouse = "" - elif source_row.s_warehouse: - # RM: was consumed FROM s_warehouse → return TO s_warehouse - qty = flt(source_row.qty * scale_factor) - s_warehouse = "" - t_warehouse = self.to_warehouse or source_row.s_warehouse - else: - # Scrap/secondary: was produced TO t_warehouse → take FROM t_warehouse - qty = flt(source_row.qty * scale_factor) - s_warehouse = source_row.t_warehouse - t_warehouse = "" - - use_serial_batch_fields = 1 if (source_row.batch_no or source_row.serial_no) else 0 - - self.append( - "items", - { - "item_code": source_row.item_code, - "item_name": source_row.item_name, - "description": source_row.description, - "stock_uom": source_row.stock_uom, - "uom": source_row.uom, - "conversion_factor": source_row.conversion_factor, - "basic_rate": source_row.basic_rate, - "qty": qty, - "s_warehouse": s_warehouse, - "t_warehouse": t_warehouse, - "is_finished_item": source_row.is_finished_item, - "type": source_row.type, - "is_legacy_scrap_item": source_row.is_legacy_scrap_item, - "bom_secondary_item": source_row.bom_secondary_item, - "against_stock_entry": self.source_stock_entry, - "ste_detail": source_row.name, - # batch and serial bundles built on submit - "use_serial_batch_fields": use_serial_batch_fields, - }, - ) + self._append_disassembly_row_from_source( + disassemble_qty=disassemble_qty, + scale_factor=scale_factor, + source_stock_entry=self.source_stock_entry, + ) def _add_items_for_disassembly_from_work_order(self): wo = frappe.get_doc("Work Order", self.work_order) - if not wo.required_items: - return self._add_items_for_disassembly_from_bom() + wo_produced_qty = flt(wo.produced_qty) + if wo_produced_qty <= 0: + frappe.throw(_("Work Order {0} has no produced qty").format(self.work_order)) - scale_factor = flt(self.fg_completed_qty) / flt(wo.qty) if flt(wo.qty) else 0 + disassemble_qty = flt(self.fg_completed_qty) + if disassemble_qty <= 0: + frappe.throw(_("Disassemble Qty cannot be less than or equal to 0.")) - # RMs - for ri in wo.required_items: - self.append( - "items", - { - "item_code": ri.item_code, - "item_name": ri.item_name, - "description": ri.description, - "qty": flt(ri.required_qty * scale_factor), - "stock_uom": ri.stock_uom, - "uom": ri.stock_uom, - "conversion_factor": 1, - # manufacture transfers RMs from WIP (not source warehouse) - "t_warehouse": self.to_warehouse or wo.wip_warehouse, - "s_warehouse": "", - "is_finished_item": 0, - }, - ) + scale_factor = disassemble_qty / wo_produced_qty - # Secondary/Scrap items - secondary_items = self.get_secondary_items(self.fg_completed_qty) - if secondary_items: - scrap_warehouse = wo.scrap_warehouse or self.from_warehouse or wo.fg_warehouse - for item in secondary_items.values(): - item["from_warehouse"] = scrap_warehouse - item["to_warehouse"] = "" - item["is_finished_item"] = 0 - self.add_to_stock_entry_detail(secondary_items, bom_no=self.bom_no) - - # FG - self.append( - "items", - { - "item_code": wo.production_item, - "item_name": wo.item_name, - "description": wo.description, - "qty": flt(self.fg_completed_qty), - "stock_uom": wo.stock_uom, - "uom": wo.stock_uom, - "conversion_factor": 1, - "s_warehouse": self.from_warehouse or wo.fg_warehouse, - "t_warehouse": "", - "is_finished_item": 1, - }, + self._append_disassembly_row_from_source( + disassemble_qty=disassemble_qty, + scale_factor=scale_factor, ) + def _append_disassembly_row_from_source(self, disassemble_qty, scale_factor, source_stock_entry=None): + for source_row in self.get_items_from_manufacture_stock_entry(self.source_stock_entry): + if source_row.is_finished_item: + qty = disassemble_qty + s_warehouse = self.from_warehouse or source_row.t_warehouse + t_warehouse = "" + elif source_row.s_warehouse: + # RM: was consumed FROM s_warehouse -> return TO s_warehouse + qty = flt(source_row.qty * scale_factor) + s_warehouse = "" + t_warehouse = self.to_warehouse or source_row.s_warehouse + else: + # Scrap/secondary: was produced TO t_warehouse -> take FROM t_warehouse + qty = flt(source_row.qty * scale_factor) + s_warehouse = source_row.t_warehouse + t_warehouse = "" + + item = { + "item_code": source_row.item_code, + "item_name": source_row.item_name, + "description": source_row.description, + "stock_uom": source_row.stock_uom, + "uom": source_row.uom, + "conversion_factor": source_row.conversion_factor, + "basic_rate": source_row.basic_rate, + "qty": qty, + "s_warehouse": s_warehouse, + "t_warehouse": t_warehouse, + "is_finished_item": source_row.is_finished_item, + "type": source_row.type, + "is_legacy_scrap_item": source_row.is_legacy_scrap_item, + "bom_secondary_item": source_row.bom_secondary_item, + # batch and serial bundles built on submit + "use_serial_batch_fields": 1 if (source_row.batch_no or source_row.serial_no) else 0, + } + + if source_stock_entry: + item.update( + { + "against_stock_entry": source_stock_entry, + "ste_detail": source_row.name, + } + ) + + self.append("items", item) + def _add_items_for_disassembly_from_bom(self): if not self.bom_no or not self.fg_completed_qty: frappe.throw(_("BOM and Finished Good Quantity is mandatory for Disassembly")) From e4eb88d80b3cdeb4124aa07ffba2868315904248 Mon Sep 17 00:00:00 2001 From: vorasmit Date: Fri, 3 Apr 2026 16:19:43 +0530 Subject: [PATCH 67/79] fix: use get_value (cherry picked from commit a71e8bb116a5ca8eb72d3e7b1815a38245757159) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 9d73a3f117c..be184de0e20 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2372,9 +2372,9 @@ class StockEntry(StockController, SubcontractingInwardController): ) def _add_items_for_disassembly_from_work_order(self): - wo = frappe.get_doc("Work Order", self.work_order) + wo_produced_qty = frappe.db.get_value("Work Order", self.work_order, "produced_qty") - wo_produced_qty = flt(wo.produced_qty) + wo_produced_qty = flt(wo_produced_qty) if wo_produced_qty <= 0: frappe.throw(_("Work Order {0} has no produced qty").format(self.work_order)) From b030eeafb8d920f32f84f7f4e62e84b607297c94 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Mon, 6 Apr 2026 20:08:38 +0530 Subject: [PATCH 68/79] fix: validate work order consistency in stock entry (cherry picked from commit ea392b2009a478eb06307aa3d63ca863488eecef) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index be184de0e20..3cbd6a5b4db 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -907,6 +907,16 @@ class StockEntry(StockController, SubcontractingInwardController): if not self.get("source_stock_entry"): return + if self.work_order: + source_wo = frappe.db.get_value("Stock Entry", self.source_stock_entry, "work_order") + if source_wo and source_wo != self.work_order: + frappe.throw( + _( + "Source Stock Entry {0} belongs to Work Order {1}, not {2}. Please use a manufacture entry from the same Work Order." + ).format(self.source_stock_entry, source_wo, self.work_order), + title=_("Work Order Mismatch"), + ) + from erpnext.manufacturing.doctype.work_order.work_order import get_disassembly_available_qty available_qty = get_disassembly_available_qty(self.source_stock_entry, self.name) @@ -2704,7 +2714,7 @@ class StockEntry(StockController, SubcontractingInwardController): sorted_items = sorted(self.items, key=lambda x: x.item_code) if self.purpose == "Manufacture": # ensure finished item at last - sorted_items = sorted(sorted_items, key=lambda x: (x.t_warehouse)) + sorted_items = sorted(sorted_items, key=lambda x: x.t_warehouse) idx = 0 for row in sorted_items: From 0a257ea63d6820a49b13b713f1ffa6a4185ad479 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Tue, 7 Apr 2026 09:59:54 +0530 Subject: [PATCH 69/79] fix: process loss with bom path disassembly (cherry picked from commit 93ad48bc1bf7f21e280c0fc068c8e04ab8c422ec) --- .../doctype/work_order/test_work_order.py | 32 ++++++++++++++++++- .../stock/doctype/stock_entry/stock_entry.py | 6 ++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index e5ae8ec39ec..8c135d297d9 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -6,7 +6,7 @@ from collections import defaultdict import frappe from frappe.tests import timeout -from frappe.utils import add_days, add_months, add_to_date, cint, flt, now, today +from frappe.utils import add_days, add_months, add_to_date, cint, flt, now, nowdate, nowtime, today from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError from erpnext.manufacturing.doctype.job_card.job_card import make_stock_entry as make_stock_entry_from_jc @@ -2657,6 +2657,36 @@ class TestWorkOrder(ERPNextTestSuite): msg=f"Raw material {bom_item.item_code} qty mismatch", ) + # -- BOM-path disassembly (no source_stock_entry, no work_order) -- + bom_disassemble_qty = 2 + bom_se = frappe.get_doc( + { + "doctype": "Stock Entry", + "stock_entry_type": "Disassemble", + "purpose": "Disassemble", + "from_bom": 1, + "bom_no": bom.name, + "fg_completed_qty": bom_disassemble_qty, + "from_warehouse": wo.fg_warehouse, + "to_warehouse": wo.wip_warehouse, + "company": wo.company, + "posting_date": nowdate(), + "posting_time": nowtime(), + } + ) + bom_se.get_items() + bom_se.save() + bom_se.submit() + + bom_scrap_row = next((i for i in bom_se.items if i.item_code == scrap_item), None) + self.assertIsNotNone(bom_scrap_row, "Scrap item must appear in BOM-path disassembly") + # Without fix 3: qty = 10 * 2 = 20; with fix 3 (process_loss_per=10%): qty = 9 * 2 = 18 + self.assertEqual( + bom_scrap_row.qty, + 18, + f"BOM-path disassembly must apply process_loss_per; expected 18, got {bom_scrap_row.qty}", + ) + def test_disassembly_with_additional_rm_not_in_bom(self): """ Test that SE-linked disassembly includes additional raw materials diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 3cbd6a5b4db..0f954c88493 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2474,6 +2474,12 @@ class StockEntry(StockController, SubcontractingInwardController): item["to_warehouse"] = "" item["is_finished_item"] = 0 + if item.get("process_loss_per"): + item["qty"] -= flt( + item["qty"] * (item["process_loss_per"] / 100), + self.precision("fg_completed_qty"), + ) + self.add_to_stock_entry_detail(secondary_items, bom_no=self.bom_no) # Finished goods From fb1d865e9b6b0f47e09ab9521bc7bf6b8c0efd9b Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Tue, 7 Apr 2026 10:00:30 +0530 Subject: [PATCH 70/79] fix: set bom details on disassembly; abs batch qty (cherry picked from commit ab1fc2243141d69739bbe4467e2e2584171c199b) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 0f954c88493..5d24abaee98 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -369,7 +369,7 @@ class StockEntry(StockController, SubcontractingInwardController): for batch_no, batch_qty in source_bundle["batch_nos"].items(): if qty_remaining <= 0: break - alloc = min(flt(batch_qty) * scale_factor, qty_remaining) + alloc = min(abs(flt(batch_qty)) * scale_factor, qty_remaining) batches[batch_no] = alloc qty_remaining -= alloc elif source_row.batch_no: @@ -2431,6 +2431,7 @@ class StockEntry(StockController, SubcontractingInwardController): "type": source_row.type, "is_legacy_scrap_item": source_row.is_legacy_scrap_item, "bom_secondary_item": source_row.bom_secondary_item, + "bom_no": source_row.bom_no, # batch and serial bundles built on submit "use_serial_batch_fields": 1 if (source_row.batch_no or source_row.serial_no) else 0, } @@ -2507,6 +2508,7 @@ class StockEntry(StockController, SubcontractingInwardController): SED.use_serial_batch_fields, SED.s_warehouse, SED.t_warehouse, + SED.bom_no, ] if stock_entry: From d4fde552f42d623c293bb1ee5e36e31cc05ece84 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Tue, 7 Apr 2026 10:27:41 +0530 Subject: [PATCH 71/79] test: maintain sufficient stock for scrap item (cherry picked from commit b892139342ea8c2aa7a1c1a65b2db80992f7a8aa) --- .../manufacturing/doctype/work_order/test_work_order.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 8c135d297d9..4acf3fe0d23 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -2658,6 +2658,15 @@ class TestWorkOrder(ERPNextTestSuite): ) # -- BOM-path disassembly (no source_stock_entry, no work_order) -- + + make_stock_entry_test_record( + item_code=scrap_item, + purpose="Material Receipt", + target=wo.fg_warehouse, + qty=50, + basic_rate=10, + ) + bom_disassemble_qty = 2 bom_se = frappe.get_doc( { From 6cebea314d0b13ff5076d91e79196688e2047d1d Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Tue, 7 Apr 2026 10:54:34 +0530 Subject: [PATCH 72/79] test: enhance tests as per review comments (cherry picked from commit f13d37fbf93a7a3f18cf430f3a36515de8bb0a3a) --- .../doctype/work_order/test_work_order.py | 155 +++++++++++++----- 1 file changed, 113 insertions(+), 42 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 4acf3fe0d23..8a13ed11fe2 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -2915,49 +2915,81 @@ class TestWorkOrder(ERPNextTestSuite): status="Not Started", ) - # Stock up RM — batch auto-created on receipt - rm_receipt = make_stock_entry_test_record( - item_code=rm_item, purpose="Material Receipt", target=wip_wh, qty=18, basic_rate=100 + # Two separate RM receipts → two distinct batches (batch_1, batch_2) + rm_receipt_1 = make_stock_entry_test_record( + item_code=rm_item, purpose="Material Receipt", target=wip_wh, qty=6, basic_rate=100 ) - rm_bundle = frappe.db.get_value( - "Stock Entry Detail", {"parent": rm_receipt.name, "item_code": rm_item}, "serial_and_batch_bundle" + rm_batch_1 = get_batch_from_bundle( + frappe.db.get_value( + "Stock Entry Detail", + {"parent": rm_receipt_1.name, "item_code": rm_item}, + "serial_and_batch_bundle", + ) ) - rm_batch = get_batch_from_bundle(rm_bundle) - # Pre-create FG batch so we can assign it to the manufacture row - fg_batch = make_batch(frappe._dict(item=fg_item)) + rm_receipt_2 = make_stock_entry_test_record( + item_code=rm_item, purpose="Material Receipt", target=wip_wh, qty=6, basic_rate=100 + ) + rm_batch_2 = get_batch_from_bundle( + frappe.db.get_value( + "Stock Entry Detail", + {"parent": rm_receipt_2.name, "item_code": rm_item}, + "serial_and_batch_bundle", + ) + ) - # Manufacture 3 units: assign batches explicitly on RM and FG rows - se_manufacture = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 3)) - for row in se_manufacture.items: + self.assertNotEqual(rm_batch_1, rm_batch_2, "Two receipts must create two distinct RM batches") + + fg_batch_1 = make_batch(frappe._dict(item=fg_item)) + fg_batch_2 = make_batch(frappe._dict(item=fg_item)) + + # Manufacture entry 1 — 3 FG using batch_1 RM/FG + se_manufacture_1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 3)) + for row in se_manufacture_1.items: if row.item_code == rm_item: - row.batch_no = rm_batch + row.batch_no = rm_batch_1 row.use_serial_batch_fields = 1 elif row.item_code == fg_item: - row.batch_no = fg_batch + row.batch_no = fg_batch_1 row.use_serial_batch_fields = 1 - se_manufacture.save() - se_manufacture.submit() + se_manufacture_1.save() + se_manufacture_1.submit() - # Disassemble 2 of the 3 manufactured units linked to the manufacture SE + # Manufacture entry 2 — 3 FG using batch_2 RM/FG + se_manufacture_2 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 3)) + for row in se_manufacture_2.items: + if row.item_code == rm_item: + row.batch_no = rm_batch_2 + row.use_serial_batch_fields = 1 + elif row.item_code == fg_item: + row.batch_no = fg_batch_2 + row.use_serial_batch_fields = 1 + se_manufacture_2.save() + se_manufacture_2.submit() + + # Disassemble 2 units from SE_1 only — must use SE_1's batches, not SE_2's disassemble_qty = 2 stock_entry = frappe.get_doc( - make_stock_entry(wo.name, "Disassemble", disassemble_qty, source_stock_entry=se_manufacture.name) + make_stock_entry( + wo.name, "Disassemble", disassemble_qty, source_stock_entry=se_manufacture_1.name + ) ) stock_entry.save() stock_entry.submit() - # FG row: consuming batch from FG warehouse — bundle must use FG batch + # FG row: must use fg_batch_1 exclusively (fg_batch_2 must not appear) fg_row = next((i for i in stock_entry.items if i.item_code == fg_item), None) self.assertIsNotNone(fg_row) self.assertTrue(fg_row.serial_and_batch_bundle, "FG row must have a serial_and_batch_bundle") - self.assertEqual(get_batch_from_bundle(fg_row.serial_and_batch_bundle), fg_batch) + self.assertEqual(get_batch_from_bundle(fg_row.serial_and_batch_bundle), fg_batch_1) + self.assertNotEqual(get_batch_from_bundle(fg_row.serial_and_batch_bundle), fg_batch_2) - # RM row: returning to WIP warehouse — bundle must use RM batch + # RM row: must use rm_batch_1 exclusively (rm_batch_2 must not appear) rm_row = next((i for i in stock_entry.items if i.item_code == rm_item), None) self.assertIsNotNone(rm_row) self.assertTrue(rm_row.serial_and_batch_bundle, "RM row must have a serial_and_batch_bundle") - self.assertEqual(get_batch_from_bundle(rm_row.serial_and_batch_bundle), rm_batch) + self.assertEqual(get_batch_from_bundle(rm_row.serial_and_batch_bundle), rm_batch_1) + self.assertNotEqual(get_batch_from_bundle(rm_row.serial_and_batch_bundle), rm_batch_2) # RM qty: 2 FG disassembled x 2 RM per FG = 4 self.assertAlmostEqual(rm_row.qty, 4.0, places=3) @@ -2990,55 +3022,94 @@ class TestWorkOrder(ERPNextTestSuite): status="Not Started", ) - # Stock up 6 RM serials — series auto-generates them - rm_receipt = make_stock_entry_test_record( + # Two separate RM receipts → two disjoint sets of serial numbers + rm_receipt_1 = make_stock_entry_test_record( item_code=rm_item, purpose="Material Receipt", target=wip_wh, qty=6, basic_rate=100 ) - rm_bundle = frappe.db.get_value( - "Stock Entry Detail", {"parent": rm_receipt.name, "item_code": rm_item}, "serial_and_batch_bundle" + rm_serials_1 = get_serial_nos_from_bundle( + frappe.db.get_value( + "Stock Entry Detail", + {"parent": rm_receipt_1.name, "item_code": rm_item}, + "serial_and_batch_bundle", + ) ) - all_rm_serials = get_serial_nos_from_bundle(rm_bundle) - self.assertEqual(len(all_rm_serials), 6) + self.assertEqual(len(rm_serials_1), 6) - # Pre-generate 3 FG serial numbers + rm_receipt_2 = make_stock_entry_test_record( + item_code=rm_item, purpose="Material Receipt", target=wip_wh, qty=6, basic_rate=100 + ) + rm_serials_2 = get_serial_nos_from_bundle( + frappe.db.get_value( + "Stock Entry Detail", + {"parent": rm_receipt_2.name, "item_code": rm_item}, + "serial_and_batch_bundle", + ) + ) + self.assertEqual(len(rm_serials_2), 6) + self.assertFalse( + set(rm_serials_1) & set(rm_serials_2), "Two receipts must produce disjoint RM serial sets" + ) + + # Pre-generate two sets of FG serial numbers series = frappe.db.get_value("Item", fg_item, "serial_no_series") - fg_serials = [make_autoname(series) for _ in range(3)] + fg_serials_1 = [make_autoname(series) for _ in range(3)] + fg_serials_2 = [make_autoname(series) for _ in range(3)] - # Manufacture 3 units: consume first 6 RM serials, produce 3 FG serials - se_manufacture = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 3)) - for row in se_manufacture.items: + # Manufacture entry 1 — consumes rm_serials_1, produces fg_serials_1 + se_manufacture_1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 3)) + for row in se_manufacture_1.items: if row.item_code == rm_item: - row.serial_no = "\n".join(all_rm_serials) + row.serial_no = "\n".join(rm_serials_1) row.use_serial_batch_fields = 1 elif row.item_code == fg_item: - row.serial_no = "\n".join(fg_serials) + row.serial_no = "\n".join(fg_serials_1) row.use_serial_batch_fields = 1 - se_manufacture.save() - se_manufacture.submit() + se_manufacture_1.save() + se_manufacture_1.submit() - # Disassemble 2 of the 3 manufactured units + # Manufacture entry 2 — consumes rm_serials_2, produces fg_serials_2 + se_manufacture_2 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 3)) + for row in se_manufacture_2.items: + if row.item_code == rm_item: + row.serial_no = "\n".join(rm_serials_2) + row.use_serial_batch_fields = 1 + elif row.item_code == fg_item: + row.serial_no = "\n".join(fg_serials_2) + row.use_serial_batch_fields = 1 + se_manufacture_2.save() + se_manufacture_2.submit() + + # Disassemble 2 units from SE_1 only — must use SE_1's serials, not SE_2's disassemble_qty = 2 stock_entry = frappe.get_doc( - make_stock_entry(wo.name, "Disassemble", disassemble_qty, source_stock_entry=se_manufacture.name) + make_stock_entry( + wo.name, "Disassemble", disassemble_qty, source_stock_entry=se_manufacture_1.name + ) ) stock_entry.save() stock_entry.submit() - # FG row: 2 serials consumed — must be a subset of the manufacture FG serials + # FG row: 2 serials consumed — must be subset of fg_serials_1, disjoint from fg_serials_2 fg_row = next((i for i in stock_entry.items if i.item_code == fg_item), None) self.assertIsNotNone(fg_row) self.assertTrue(fg_row.serial_and_batch_bundle, "FG row must have a serial_and_batch_bundle") fg_dasm_serials = get_serial_nos_from_bundle(fg_row.serial_and_batch_bundle) self.assertEqual(len(fg_dasm_serials), disassemble_qty) - self.assertTrue(set(fg_dasm_serials).issubset(set(fg_serials))) + self.assertTrue(set(fg_dasm_serials).issubset(set(fg_serials_1))) + self.assertFalse( + set(fg_dasm_serials) & set(fg_serials_2), "Disassembly must not use SE_2's FG serials" + ) - # RM row: 4 serials returned (2 FG x 2 RM each) — must be a subset of manufacture RM serials + # RM row: 4 serials returned (2 FG x 2 RM each) — subset of rm_serials_1, disjoint from rm_serials_2 rm_row = next((i for i in stock_entry.items if i.item_code == rm_item), None) self.assertIsNotNone(rm_row) self.assertTrue(rm_row.serial_and_batch_bundle, "RM row must have a serial_and_batch_bundle") rm_dasm_serials = get_serial_nos_from_bundle(rm_row.serial_and_batch_bundle) self.assertEqual(len(rm_dasm_serials), disassemble_qty * 2) - self.assertTrue(set(rm_dasm_serials).issubset(set(all_rm_serials))) + self.assertTrue(set(rm_dasm_serials).issubset(set(rm_serials_1))) + self.assertFalse( + set(rm_dasm_serials) & set(rm_serials_2), "Disassembly must not use SE_2's RM serials" + ) def test_components_alternate_item_for_bom_based_manufacture_entry(self): frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM") From 7bef9542d420fb89ed1dcf90345cbe409c08563e Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Tue, 7 Apr 2026 13:17:22 +0530 Subject: [PATCH 73/79] fix: remove unnecessary param, and use value from self (cherry picked from commit 98dfd64f6326cefc8882756eb0d30b26863a0556) --- .../stock/doctype/stock_entry/stock_entry.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 5d24abaee98..ae5c390fdae 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -345,9 +345,7 @@ class StockEntry(StockController, SubcontractingInwardController): scale_factor = flt(self.fg_completed_qty) / source_fg_qty if source_fg_qty else 0 bundle_data = get_voucher_wise_serial_batch_from_bundle(voucher_no=[self.source_stock_entry]) - source_rows_by_name = { - r.name: r for r in self.get_items_from_manufacture_stock_entry(self.source_stock_entry) - } + source_rows_by_name = {r.name: r for r in self.get_items_from_manufacture_stock_entry()} for row in self.items: if not row.ste_detail: @@ -2378,7 +2376,6 @@ class StockEntry(StockController, SubcontractingInwardController): self._append_disassembly_row_from_source( disassemble_qty=disassemble_qty, scale_factor=scale_factor, - source_stock_entry=self.source_stock_entry, ) def _add_items_for_disassembly_from_work_order(self): @@ -2399,8 +2396,8 @@ class StockEntry(StockController, SubcontractingInwardController): scale_factor=scale_factor, ) - def _append_disassembly_row_from_source(self, disassemble_qty, scale_factor, source_stock_entry=None): - for source_row in self.get_items_from_manufacture_stock_entry(self.source_stock_entry): + def _append_disassembly_row_from_source(self, disassemble_qty, scale_factor): + for source_row in self.get_items_from_manufacture_stock_entry(): if source_row.is_finished_item: qty = disassemble_qty s_warehouse = self.from_warehouse or source_row.t_warehouse @@ -2436,10 +2433,10 @@ class StockEntry(StockController, SubcontractingInwardController): "use_serial_batch_fields": 1 if (source_row.batch_no or source_row.serial_no) else 0, } - if source_stock_entry: + if self.source_stock_entry: item.update( { - "against_stock_entry": source_stock_entry, + "against_stock_entry": self.source_stock_entry, "ste_detail": source_row.name, } ) @@ -2486,7 +2483,7 @@ class StockEntry(StockController, SubcontractingInwardController): # Finished goods self.load_items_from_bom() - def get_items_from_manufacture_stock_entry(self, stock_entry=None): + def get_items_from_manufacture_stock_entry(self): SE = frappe.qb.DocType("Stock Entry") SED = frappe.qb.DocType("Stock Entry Detail") query = frappe.qb.from_(SED).join(SE).on(SED.parent == SE.name).where(SE.docstatus == 1) @@ -2511,10 +2508,10 @@ class StockEntry(StockController, SubcontractingInwardController): SED.bom_no, ] - if stock_entry: + if self.source_stock_entry: return ( query.select(SED.name, SED.qty, SED.transfer_qty, *common_fields) - .where(SE.name == stock_entry) + .where(SE.name == self.source_stock_entry) .orderby(SED.idx) .run(as_dict=True) ) From 9e83badbf5d100a01e94beb47ff71354ea0e1dab Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Tue, 7 Apr 2026 14:57:56 +0530 Subject: [PATCH 74/79] chore: resolve conflicts --- erpnext/manufacturing/doctype/work_order/work_order.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 268918eca00..5e18f68e8c0 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -2432,14 +2432,7 @@ def make_stock_entry( @frappe.whitelist() -<<<<<<< HEAD -<<<<<<< HEAD -def get_default_warehouse(company): -======= -def get_disassembly_available_qty(stock_entry_name: str) -> float: -======= def get_disassembly_available_qty(stock_entry_name: str, current_se_name: str | None = None) -> float: ->>>>>>> 6394dead72 (fix: validate qty that can be disassembled from source stock entry.) se = frappe.db.get_value("Stock Entry", stock_entry_name, ["fg_completed_qty"], as_dict=True) if not se: return 0.0 @@ -2459,8 +2452,7 @@ def get_disassembly_available_qty(stock_entry_name: str, current_se_name: str | @frappe.whitelist() -def get_default_warehouse(company: str): ->>>>>>> 68e97808c5 (fix: disassembly prompt with source stock entry field) +def get_default_warehouse(company): wip, fg, scrap = frappe.get_cached_value( "Company", company, ["default_wip_warehouse", "default_fg_warehouse", "default_scrap_warehouse"] ) From 093ca8745d9b9358ed5c163bb3ee675b56276f3f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:20:17 +0530 Subject: [PATCH 75/79] perf: optimize account balance data fetching for Chart Of Accounts (backport #53044) (#53802) Co-authored-by: Shllokkk <140623894+Shllokkk@users.noreply.github.com> --- .../accounts/doctype/account/account_tree.js | 97 +++++++++---------- erpnext/accounts/utils.py | 72 ++++++++++++++ 2 files changed, 118 insertions(+), 51 deletions(-) diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js index 315b41560ce..5ff4e4a47e2 100644 --- a/erpnext/accounts/doctype/account/account_tree.js +++ b/erpnext/accounts/doctype/account/account_tree.js @@ -52,60 +52,55 @@ frappe.treeview_settings["Account"] = { ], root_label: "Accounts", get_tree_nodes: "erpnext.accounts.utils.get_children", - on_get_node: function (nodes, deep = false) { - if (frappe.boot.user.can_read.indexOf("GL Entry") == -1) return; + on_node_render: function (node, deep) { + const render_balances = () => { + for (let account of cur_tree.account_balance_data) { + const node = cur_tree.nodes && cur_tree.nodes[account.value]; + if (!node || node.is_root) continue; - let accounts = []; - if (deep) { - // in case of `get_all_nodes` - accounts = nodes.reduce((acc, node) => [...acc, ...node.data], []); - } else { - accounts = nodes; - } + // show Dr if positive since balance is calculated as debit - credit else show Cr + const balance = account.balance_in_account_currency || account.balance; + const dr_or_cr = balance > 0 ? __("Dr") : __("Cr"); + const format = (value, currency) => format_currency(Math.abs(value), currency); - frappe.db.get_single_value("Accounts Settings", "show_balance_in_coa").then((value) => { - if (value) { - const get_balances = frappe.call({ - method: "erpnext.accounts.utils.get_account_balances", - args: { - accounts: accounts, - company: cur_tree.args.company, - include_default_fb_balances: true, - }, - }); - - get_balances.then((r) => { - if (!r.message || r.message.length == 0) return; - - for (let account of r.message) { - const node = cur_tree.nodes && cur_tree.nodes[account.value]; - if (!node || node.is_root) continue; - - // show Dr if positive since balance is calculated as debit - credit else show Cr - const balance = account.balance_in_account_currency || account.balance; - const dr_or_cr = balance > 0 ? __("Dr") : __("Cr"); - const format = (value, currency) => format_currency(Math.abs(value), currency); - - if (account.balance !== undefined) { - node.parent && node.parent.find(".balance-area").remove(); - $( - '' + - (account.balance_in_account_currency - ? format( - account.balance_in_account_currency, - account.account_currency - ) + " / " - : "") + - format(account.balance, account.company_currency) + - " " + - dr_or_cr + - "" - ).insertBefore(node.$ul); - } - } - }); + if (account.balance !== undefined) { + node.parent && node.parent.find(".balance-area").remove(); + $( + '' + + (account.account_currency != account.company_currency + ? format(account.balance_in_account_currency, account.account_currency) + + " / " + : "") + + format(account.balance, account.company_currency) + + " " + + dr_or_cr + + "" + ).insertBefore(node.$ul); + } } - }); + }; + + if (frappe.boot.user.can_read.indexOf("GL Entry") == -1) return; + if (!cur_tree.account_balance_data) { + frappe.db.get_single_value("Accounts Settings", "show_balance_in_coa").then((value) => { + if (value) { + frappe.call({ + method: "erpnext.accounts.utils.get_account_balances_coa", + args: { + company: cur_tree.args.company, + include_default_fb_balances: true, + }, + callback: function (r) { + if (!r.message || r.message.length === 0) return; + cur_tree.account_balance_data = r.message || []; + render_balances(); + }, + }); + } + }); + } else { + render_balances(); + } }, add_tree_node: "erpnext.accounts.utils.add_ac", menu_items: [ diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 7a23a39497b..5b1bce2cb35 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1404,6 +1404,78 @@ def get_account_balances(accounts, company, finance_book=None, include_default_f return accounts +@frappe.whitelist() +def get_account_balances_coa(company: str, include_default_fb_balances: bool = False): + company_currency = frappe.get_cached_value("Company", company, "default_currency") + + Account = DocType("Account") + account_list = ( + frappe.qb.from_(Account) + .select(Account.name, Account.parent_account, Account.account_currency) + .where(Account.company == company) + .orderby(Account.lft) + .run(as_dict=True) + ) + + account_balances_cc = {account.get("name"): 0 for account in account_list} + + account_balances_ac = {account.get("name"): 0 for account in account_list} + + GLEntry = DocType("GL Entry") + precision = get_currency_precision() + get_ledger_balances_query = ( + frappe.qb.from_(GLEntry) + .select( + GLEntry.account, + (Sum(Round(GLEntry.debit, precision)) - Sum(Round(GLEntry.credit, precision))).as_("balance"), + ( + Sum(Round(GLEntry.debit_in_account_currency, precision)) + - Sum(Round(GLEntry.credit_in_account_currency, precision)) + ).as_("balance_in_account_currency"), + ) + .groupby(GLEntry.account) + ) + + condition_list = [GLEntry.company == company, GLEntry.is_cancelled == 0] + + default_finance_book = None + + if include_default_fb_balances: + default_finance_book = frappe.get_cached_value("Company", company, "default_finance_book") + + if default_finance_book: + condition_list.append( + (GLEntry.finance_book == default_finance_book) | (GLEntry.finance_book.isnull()) + ) + + for condition in condition_list: + get_ledger_balances_query = get_ledger_balances_query.where(condition) + + ledger_balances = get_ledger_balances_query.run(as_dict=True) + + for ledger_entry in ledger_balances: + account_balances_cc[ledger_entry.get("account")] = ledger_entry.get("balance") + account_balances_ac[ledger_entry.get("account")] = ledger_entry.get("balance_in_account_currency") + + for account in reversed(account_list): + parent = account.get("parent_account") + if parent: + account_balances_cc[parent] += account_balances_cc.get(account.get("name")) + + accounts_data = [ + { + "value": account.get("name"), + "company_currency": company_currency, + "balance": account_balances_cc.get(account.get("name")), + "account_currency": account.get("account_currency"), + "balance_in_account_currency": account_balances_ac.get(account.get("name")), + } + for account in account_list + ] + + return accounts_data + + def create_payment_gateway_account(gateway, payment_channel="Email", company=None): from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account From 7062b7153e8f4fc27c90533160b5b869816cc6b8 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 12:05:33 +0000 Subject: [PATCH 76/79] fix: skip validate_stock_accounts in Journal Entry when perpetual inventory is disabled (backport #53554) (#53558) Co-authored-by: Saeed Kola Co-authored-by: diptanilsaha --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 23fb4fd0825..01caa360dbe 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -353,8 +353,11 @@ class JournalEntry(AccountsController): frappe.throw(_("Account {0} should be of type Expense").format(d.account)) def validate_stock_accounts(self): - if self.voucher_type == "Periodic Accounting Entry": - # Skip validation for periodic accounting entry + if ( + not erpnext.is_perpetual_inventory_enabled(self.company) + or self.voucher_type == "Periodic Accounting Entry" + ): + # Skip validation for periodic accounting entry and Perpetual Inventory Disabled Company. return stock_accounts = get_stock_accounts(self.company, accounts=self.accounts) From df3f242331053d1412592e70048546f235b5b8f5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 13:06:39 +0000 Subject: [PATCH 77/79] fix: sync paid and received amount (backport #53039) (#54108) Co-authored-by: Vishnu Priya Baskaran <145791817+ervishnucs@users.noreply.github.com> fix: sync paid and received amount (#53039) --- erpnext/accounts/doctype/payment_entry/payment_entry.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index f1e816a9cbe..6509b2e3873 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -824,7 +824,7 @@ frappe.ui.form.on("Payment Entry", { paid_amount: function (frm) { frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate)); let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; - if (!frm.doc.received_amount) { + if (frm.doc.paid_amount) { if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) { frm.set_value("received_amount", frm.doc.paid_amount); } else if (company_currency == frm.doc.paid_to_account_currency) { @@ -845,7 +845,7 @@ frappe.ui.form.on("Payment Entry", { flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate) ); - if (!frm.doc.paid_amount) { + if (frm.doc.received_amount) { if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) { frm.set_value("paid_amount", frm.doc.received_amount); if (frm.doc.target_exchange_rate) { From 4a6fe477d4e82efdb01a61f53d6ce15a76bafbc8 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:55:18 +0530 Subject: [PATCH 78/79] =?UTF-8?q?fix(promotional=5Fscheme):=20toggle=20ena?= =?UTF-8?q?ble=20state=20between=20Buying=20and=20Selli=E2=80=A6=20(backpo?= =?UTF-8?q?rt=20#54110)=20(#54112)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ahmed AbuKhatwa <82771130+AhmedAbokhatwa@users.noreply.github.com> Co-authored-by: AhmedAbukhatwa fix(promotional_scheme): toggle enable state between Buying and Selli… (#54110) --- .../accounts/doctype/promotional_scheme/promotional_scheme.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.js b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.js index 920b9a99eac..8926461b01a 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.js +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.js @@ -21,10 +21,12 @@ frappe.ui.form.on("Promotional Scheme", { selling: function (frm) { frm.trigger("set_options_for_applicable_for"); + frm.toggle_enable("buying", !frm.doc.selling); }, buying: function (frm) { frm.trigger("set_options_for_applicable_for"); + frm.toggle_enable("selling", !frm.doc.buying); }, set_options_for_applicable_for: function (frm) { From 7b91566435e9acd150c70def486d4b7bda558f9f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:10:42 +0000 Subject: [PATCH 79/79] refactor: financial report template enhancements (backport #52687) (#54113) Co-authored-by: Abdeali Chharchhodawala <99460106+Abdeali099@users.noreply.github.com> --- .../account_category/account_category.json | 9 +++-- .../financial_report_template.js | 21 ++++++------ .../financial_report_template.json | 6 ++-- .../financial_report_template.py | 13 +++++++ .../financial_report_validation.py | 34 ++++++++++++------- .../test_financial_report_engine.py | 1 + 6 files changed, 56 insertions(+), 28 deletions(-) diff --git a/erpnext/accounts/doctype/account_category/account_category.json b/erpnext/accounts/doctype/account_category/account_category.json index d69d37bd78b..cc8f4103f21 100644 --- a/erpnext/accounts/doctype/account_category/account_category.json +++ b/erpnext/accounts/doctype/account_category/account_category.json @@ -26,8 +26,13 @@ ], "grid_page_length": 50, "index_web_pages_for_search": 1, - "links": [], - "modified": "2025-10-15 03:19:47.171349", + "links": [ + { + "link_doctype": "Account", + "link_fieldname": "account_category" + } + ], + "modified": "2026-02-23 01:19:49.589393", "modified_by": "Administrator", "module": "Accounts", "name": "Account Category", diff --git a/erpnext/accounts/doctype/financial_report_template/financial_report_template.js b/erpnext/accounts/doctype/financial_report_template/financial_report_template.js index 739956631fd..304da47577b 100644 --- a/erpnext/accounts/doctype/financial_report_template/financial_report_template.js +++ b/erpnext/accounts/doctype/financial_report_template/financial_report_template.js @@ -3,6 +3,8 @@ frappe.ui.form.on("Financial Report Template", { refresh(frm) { + if (frm.is_new() || frm.doc.rows.length === 0) return; + // add custom button to view missed accounts frm.add_custom_button(__("View Account Coverage"), function () { let selected_rows = frm.get_field("rows").grid.get_selected_children(); @@ -20,7 +22,7 @@ frappe.ui.form.on("Financial Report Template", { }); }, - validate(frm) { + after_save(frm) { if (!frm.doc.rows || frm.doc.rows.length === 0) { frappe.msgprint(__("At least one row is required for a financial report template")); } @@ -34,14 +36,6 @@ frappe.ui.form.on("Financial Report Row", { update_formula_label(frm, row.data_source); update_formula_description(frm, row.data_source); - if (row.data_source !== "Account Data") { - frappe.model.set_value(cdt, cdn, "balance_type", ""); - } - - if (["Blank Line", "Column Break", "Section Break"].includes(row.data_source)) { - frappe.model.set_value(cdt, cdn, "calculation_formula", ""); - } - set_up_filters_editor(frm, cdt, cdn); }, @@ -322,6 +316,8 @@ function update_formula_description(frm, data_source) { const list_style = `style="margin-bottom: var(--margin-sm); color: var(--text-muted); font-size: 0.9em;"`; const note_style = `style="margin-bottom: 0; color: var(--text-muted); font-size: 0.9em;"`; const tip_style = `style="margin-bottom: 0; color: var(--text-color); font-size: 0.85em;"`; + const code_style = `style="background: var(--bg-light-gray); padding: var(--padding-xs); border-radius: var(--border-radius); font-size: 0.85em; width: max-content; margin-bottom: var(--margin-sm);"`; + const pre_style = `style="margin: 0; border-radius: var(--border-radius)"`; let description_html = ""; @@ -382,8 +378,13 @@ function update_formula_description(frm, data_source) {
  • my_app.financial_reports.get_kpi_data
  • +
    Method Signature:
    +
    +
    def get_custom_data(filters, periods, row): 
      # filters: dict — report filters (company, period, etc.)
      # periods: list[dict] — period definitions
      # row: dict — the current report row

      return [1000.0, 1200.0, 1150.0] # one value per period
    +
    +
    Return Format:
    -

    Numbers for each period: [1000.0, 1200.0, 1150.0]

    +

    A list of numbers, one for each period: [1000.0, 1200.0, 1150.0]

    `; } else if (data_source === "Blank Line") { description_html = ` diff --git a/erpnext/accounts/doctype/financial_report_template/financial_report_template.json b/erpnext/accounts/doctype/financial_report_template/financial_report_template.json index 7383306f332..5bfd56810db 100644 --- a/erpnext/accounts/doctype/financial_report_template/financial_report_template.json +++ b/erpnext/accounts/doctype/financial_report_template/financial_report_template.json @@ -1,6 +1,5 @@ { "actions": [], - "allow_rename": 1, "autoname": "field:template_name", "creation": "2025-08-02 04:44:15.184541", "doctype": "DocType", @@ -31,7 +30,8 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Report Type", - "options": "\nProfit and Loss Statement\nBalance Sheet\nCash Flow\nCustom Financial Statement" + "options": "\nProfit and Loss Statement\nBalance Sheet\nCash Flow\nCustom Financial Statement", + "reqd": 1 }, { "depends_on": "eval:frappe.boot.developer_mode", @@ -66,7 +66,7 @@ "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2025-11-14 00:11:03.508139", + "modified": "2026-02-23 01:04:05.797161", "modified_by": "Administrator", "module": "Accounts", "name": "Financial Report Template", diff --git a/erpnext/accounts/doctype/financial_report_template/financial_report_template.py b/erpnext/accounts/doctype/financial_report_template/financial_report_template.py index 69ee7e4f7dd..f30ca7b1249 100644 --- a/erpnext/accounts/doctype/financial_report_template/financial_report_template.py +++ b/erpnext/accounts/doctype/financial_report_template/financial_report_template.py @@ -32,6 +32,19 @@ class FinancialReportTemplate(Document): template_name: DF.Data # end: auto-generated types + def before_validate(self): + self.clear_hidden_fields() + + def clear_hidden_fields(self): + style_data_sources = {"Blank Line", "Column Break", "Section Break"} + + for row in self.rows: + if row.data_source != "Account Data": + row.balance_type = None + + if row.data_source in style_data_sources: + row.calculation_formula = None + def validate(self): validator = TemplateValidator(self) result = validator.validate() diff --git a/erpnext/accounts/doctype/financial_report_template/financial_report_validation.py b/erpnext/accounts/doctype/financial_report_template/financial_report_validation.py index 306fb562585..170225fa74d 100644 --- a/erpnext/accounts/doctype/financial_report_template/financial_report_validation.py +++ b/erpnext/accounts/doctype/financial_report_template/financial_report_validation.py @@ -70,8 +70,8 @@ class ValidationResult: self.warnings.append(issue) def notify_user(self) -> None: - warnings = "

    ".join(str(w) for w in self.warnings) - errors = "

    ".join(str(e) for e in self.issues) + warnings = "

    ".join(str(w) for w in self.warnings if w) + errors = "

    ".join(str(e) for e in self.issues if e) if warnings: frappe.msgprint(warnings, title=_("Warnings"), indicator="orange") @@ -99,9 +99,8 @@ class TemplateValidator: result.merge(validator.validate(self.template)) # Run row-level validations - account_fields = {field.fieldname for field in frappe.get_meta("Account").fields} for row in self.template.rows: - result.merge(self.formula_validator.validate(row, account_fields)) + result.merge(self.formula_validator.validate(row)) return result @@ -383,7 +382,8 @@ class AccountFilterValidator(Validator): """Validates account filter expressions used in Account Data rows""" def __init__(self, account_fields: set | None = None): - self.account_fields = account_fields or set(frappe.get_meta("Account")._valid_columns) + self.account_meta = frappe.get_meta("Account") + self.account_fields = account_fields or set(self.account_meta._valid_columns) def validate(self, row) -> ValidationResult: result = ValidationResult() @@ -403,7 +403,11 @@ class AccountFilterValidator(Validator): try: filter_config = json.loads(row.calculation_formula) - error = self._validate_filter_structure(filter_config, self.account_fields) + error = self._validate_filter_structure( + filter_config, + self.account_fields, + row.advanced_filtering, + ) if error: result.add_error( @@ -425,7 +429,12 @@ class AccountFilterValidator(Validator): return result - def _validate_filter_structure(self, filter_config, account_fields: set) -> str | None: + def _validate_filter_structure( + self, + filter_config, + account_fields: set, + advanced_filtering: bool = False, + ) -> str | None: # simple condition: [field, operator, value] if isinstance(filter_config, list): if len(filter_config) != 3: @@ -436,8 +445,10 @@ class AccountFilterValidator(Validator): if not isinstance(field, str) or not isinstance(operator, str): return "Field and operator must be strings" + display = (field if advanced_filtering else self.account_meta.get_label(field)) or field + if field not in account_fields: - return f"Field '{field}' is not a valid account field" + return f"Field '{display}' is not a valid Account field" if operator.casefold() not in OPERATOR_MAP: return f"Invalid operator '{operator}'" @@ -460,7 +471,7 @@ class AccountFilterValidator(Validator): # recursive for condition in conditions: - error = self._validate_filter_structure(condition, account_fields) + error = self._validate_filter_structure(condition, account_fields, advanced_filtering) if error: return error else: @@ -476,7 +487,7 @@ class FormulaValidator(Validator): self.calculation_validator = CalculationFormulaValidator(reference_codes) self.account_filter_validator = AccountFilterValidator() - def validate(self, row, account_fields: set) -> ValidationResult: + def validate(self, row) -> ValidationResult: result = ValidationResult() if not row.calculation_formula: @@ -486,9 +497,6 @@ class FormulaValidator(Validator): return self.calculation_validator.validate(row) elif row.data_source == "Account Data": - # Update account fields if provided - if account_fields: - self.account_filter_validator.account_fields = account_fields return self.account_filter_validator.validate(row) elif row.data_source == "Custom API": diff --git a/erpnext/accounts/doctype/financial_report_template/test_financial_report_engine.py b/erpnext/accounts/doctype/financial_report_template/test_financial_report_engine.py index a23c6bb6883..ef6f2785184 100644 --- a/erpnext/accounts/doctype/financial_report_template/test_financial_report_engine.py +++ b/erpnext/accounts/doctype/financial_report_template/test_financial_report_engine.py @@ -1295,6 +1295,7 @@ class TestFilterExpressionParser(FinancialReportTemplateTestCase): self.data_source = "Account Data" self.idx = 1 self.reverse_sign = 0 + self.advanced_filtering = True return MockReportRow(formula, reference_code)