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/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/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/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.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/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/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) 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) 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/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", 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/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) { 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/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) { 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/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/repost_accounting_ledger/test_repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py index b60c13fc8b8..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(AccountsTestMixin, ERPNextTestSuite): +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(AccountsTestMixin, ERPNextTestSuite): # 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(AccountsTestMixin, ERPNextTestSuite): 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(AccountsTestMixin, ERPNextTestSuite): 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(AccountsTestMixin, ERPNextTestSuite): 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(AccountsTestMixin, ERPNextTestSuite): 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(AccountsTestMixin, ERPNextTestSuite): # 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(AccountsTestMixin, ERPNextTestSuite): 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(AccountsTestMixin, ERPNextTestSuite): # 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(AccountsTestMixin, ERPNextTestSuite): 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(AccountsTestMixin, ERPNextTestSuite): 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(AccountsTestMixin, ERPNextTestSuite): ) 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/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/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 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/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; 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/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/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 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 }, 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) 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/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/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/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/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 9d98eed668d..7f41e8476ea 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 @@ -285,6 +294,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 @@ -372,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 @@ -521,7 +544,6 @@ 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) <= 0.5: detail_row = self.doc._item_wise_tax_details[last_idx] detail_row["amount"] = flt(detail_row["amount"] + diff, 5) @@ -597,14 +619,29 @@ 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 = 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": - 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 = 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 @@ -788,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 ): diff --git a/erpnext/controllers/tests/test_distributed_discount.py b/erpnext/controllers/tests/test_distributed_discount.py index 4f4911c8537..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(AccountsTestMixin, ERPNextTestSuite): +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) diff --git a/erpnext/controllers/tests/test_item_wise_tax_details.py b/erpnext/controllers/tests/test_item_wise_tax_details.py index f6d94c61eca..7e19c1dc057 100644 --- a/erpnext/controllers/tests/test_item_wise_tax_details.py +++ b/erpnext/controllers/tests/test_item_wise_tax_details.py @@ -1,9 +1,9 @@ import json import frappe +from frappe.utils import flt -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 class TestTaxesAndTotals(ERPNextTestSuite): @@ -124,3 +124,180 @@ class TestTaxesAndTotals(ERPNextTestSuite): ] self.assertEqual(actual_values, expected_values) + + @change_settings("Selling Settings", {"allow_multiple_items": 1}) + def test_item_wise_tax_detail_high_conversion_rate(self): + """ + 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( + { + "doctype": "Sales Invoice", + "customer": "_Test Customer", + "company": "_Test Company", + "currency": "USD", + "debit_to": "_Test Receivable USD - _TC", + "conversion_rate": 1300, + "items": [ + { + "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", + }, + { + "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", + }, + ], + "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, 0.0) + details_by_tax[detail.tax_row] = bucket + detail.amount + + 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 + for detail in doc.item_wise_tax_details: + self.assertEqual(detail.amount, flt(detail.amount, detail.precision("amount"))) + + 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) 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/controllers/tests/test_reactivity.py b/erpnext/controllers/tests/test_reactivity.py index fa3007087e1..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(AccountsTestMixin, ERPNextTestSuite): - 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(AccountsTestMixin, ERPNextTestSuite): 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) 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/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/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 "" diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 454f1934e13..32c543703bc 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" : ""; @@ -369,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; @@ -620,10 +659,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 +673,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 +722,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 +736,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 +744,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 +762,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 +781,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 +839,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); @@ -891,6 +903,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: { @@ -986,7 +1003,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 +1024,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..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) @@ -356,7 +358,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_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", 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..8a13ed11fe2 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 @@ -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} @@ -2413,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 @@ -2453,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() @@ -2488,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 @@ -2512,10 +2500,35 @@ 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 - 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 @@ -2524,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") @@ -2603,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, @@ -2614,6 +2635,17 @@ 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) + # 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: expected_qty = (bom_item.qty / bom.quantity) * disassemble_qty @@ -2625,19 +2657,57 @@ class TestWorkOrder(ERPNextTestSuite): msg=f"Raw material {bom_item.item_code} qty mismatch", ) + # -- 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( + { + "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 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, @@ -2673,9 +2743,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", { @@ -2688,9 +2757,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", { @@ -2706,13 +2774,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 @@ -2725,16 +2795,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, @@ -2742,7 +2811,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) @@ -2758,6 +2827,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), @@ -2765,6 +2835,282 @@ 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_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", + ) + + # 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_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_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", + ) + ) + + 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_1 + row.use_serial_batch_fields = 1 + elif row.item_code == fg_item: + row.batch_no = fg_batch_1 + row.use_serial_batch_fields = 1 + se_manufacture_1.save() + se_manufacture_1.submit() + + # 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_1.name + ) + ) + stock_entry.save() + stock_entry.submit() + + # 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_1) + self.assertNotEqual(get_batch_from_bundle(fg_row.serial_and_batch_bundle), fg_batch_2) + + # 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_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) + + 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", + ) + + # 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_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", + ) + ) + self.assertEqual(len(rm_serials_1), 6) + + 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_1 = [make_autoname(series) for _ in range(3)] + fg_serials_2 = [make_autoname(series) for _ in range(3)] + + # 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(rm_serials_1) + row.use_serial_batch_fields = 1 + elif row.item_code == fg_item: + row.serial_no = "\n".join(fg_serials_1) + row.use_serial_batch_fields = 1 + se_manufacture_1.save() + se_manufacture_1.submit() + + # 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_1.name + ) + ) + stock_entry.save() + stock_entry.submit() + + # 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_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) — 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(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") frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 1) @@ -3951,7 +4297,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 +4310,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 +4505,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 +4545,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..0e8729bf4ba 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) { @@ -387,6 +390,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(); @@ -437,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.")); @@ -447,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); + } }); }, @@ -998,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 f9d380964bc..5e18f68e8c0 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 @@ -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,10 +2417,13 @@ 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 stock_entry.get_items() + stock_entry.set_secondary_items_from_job_card() if purpose != "Disassemble": stock_entry.set_serial_no_batch_for_finished_good() @@ -2427,6 +2431,26 @@ def make_stock_entry( return stock_entry.as_dict() +@frappe.whitelist() +def get_disassembly_available_qty(stock_entry_name: str, current_se_name: str | None = None) -> float: + se = frappe.db.get_value("Stock Entry", stock_entry_name, ["fg_completed_qty"], as_dict=True) + if not se: + return 0.0 + + 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 + + @frappe.whitelist() def get_default_warehouse(company): wip, fg, scrap = frappe.get_cached_value( @@ -2478,14 +2502,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 +2519,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 +2529,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 +2664,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/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(", ")} `; diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 1100ab7a581..4971f914b1e 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; } @@ -1851,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 @@ -1869,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", @@ -1915,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 @@ -1980,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/public/js/utils.js b/erpnext/public/js/utils.js index 6fb1b88060c..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 ( @@ -44,7 +48,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"; @@ -56,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); @@ -68,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, @@ -81,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(); }, 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 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/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 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( diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index f6f4e8bea4f..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(AccountsTestMixin, ERPNextTestSuite): - def setUp(self): - self.create_customer("_Test Customer Credit") - +class TestSalesOrder(ERPNextTestSuite): @ERPNextTestSuite.change_settings( "Stock Settings", { @@ -2439,7 +2435,7 @@ class TestSalesOrder(AccountsTestMixin, ERPNextTestSuite): 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(AccountsTestMixin, ERPNextTestSuite): 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(AccountsTestMixin, ERPNextTestSuite): 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(AccountsTestMixin, ERPNextTestSuite): { "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(AccountsTestMixin, ERPNextTestSuite): { "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(AccountsTestMixin, ERPNextTestSuite): 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, 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..0f043a73fa4 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,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2026-02-21 16:39:00.200328", + "modified": "2026-02-22 16:40:00.200328", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", 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/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") 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}" 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 e78faa9511a..b0b4d9470d4 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 @@ -561,7 +560,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 @@ -1259,11 +1258,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 +1292,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 +1360,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 +1406,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 6eba41c3883..828ad603d8e 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,97 @@ 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 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 @@ -5593,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) 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", 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 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..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"), } ) @@ -1489,6 +1522,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 +1541,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..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 @@ -1077,6 +1077,205 @@ 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 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 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 diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index f71b67e1127..e4c1ffa4d26 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", @@ -232,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, @@ -330,6 +364,59 @@ frappe.ui.form.on("Stock Entry", { __("View") ); } + + if (frm.doc.purpose === "Manufacture") { + 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( + { + fieldtype: "Float", + label: __("Qty to Disassemble"), + fieldname: "qty", + default: available_qty, + description: __("Max: {0}", [available_qty]), + }, + async (data) => { + 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, + } + ); + 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"), + __("Create") + ); + }, + __("Create") + ); + } } if (frm.doc.docstatus === 0 && !frm.doc.subcontracting_inward_order) { @@ -1334,16 +1421,19 @@ 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 + 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, 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 4ce2bda3631..ae5c390fdae 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -29,9 +29,8 @@ 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_scrap_items_from_sub_assemblies, + get_secondary_items_from_sub_assemblies, validate_bom_no, ) from erpnext.setup.doctype.brand.brand import get_brand_defaults @@ -151,6 +150,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 @@ -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)) @@ -239,13 +246,14 @@ 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() 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 +280,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") @@ -323,6 +331,56 @@ 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()} + + 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(abs(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 @@ -348,33 +406,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() @@ -656,7 +718,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 +851,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)) @@ -800,7 +862,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)) @@ -839,6 +901,36 @@ 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 + + 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) + + 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) @@ -1093,11 +1185,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 +1202,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 +1297,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 +1311,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 +1713,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 +1730,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 @@ -2235,44 +2332,116 @@ 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 Manufacture Stock Entries (averaged reversal) + 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() 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_work_order(self): - items = self.get_items_from_manufacture_entry() + 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) + ) - s_warehouse = frappe.db.get_value("Work Order", self.work_order, "fg_warehouse") + disassemble_qty = flt(self.fg_completed_qty) + scale_factor = disassemble_qty / flt(source_fg_qty) - 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, + self._append_disassembly_row_from_source( + disassemble_qty=disassemble_qty, + scale_factor=scale_factor, ) - 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) + def _add_items_for_disassembly_from_work_order(self): + wo_produced_qty = frappe.db.get_value("Work Order", self.work_order, "produced_qty") - # 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) + 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)) - if row.is_finished_item: - child_row.qty = self.fg_completed_qty + disassemble_qty = flt(self.fg_completed_qty) + if disassemble_qty <= 0: + frappe.throw(_("Disassemble Qty cannot be less than or equal to 0.")) - 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 + scale_factor = disassemble_qty / wo_produced_qty + + 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): + 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 + 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, + "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, + } + + if self.source_stock_entry: + item.update( + { + "against_stock_entry": self.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: @@ -2288,37 +2457,72 @@ 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 + + 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 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): + 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, + SED.bom_no, + ] + + if self.source_stock_entry: + return ( + query.select(SED.name, SED.qty, SED.transfer_qty, *common_fields) + .where(SE.name == self.source_stock_entry) + .orderby(SED.idx) + .run(as_dict=True) + ) + + return ( + 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) ) @frappe.whitelist() @@ -2434,7 +2638,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) @@ -2515,7 +2719,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: @@ -2579,14 +2783,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 +2811,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 +2843,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 +2985,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 +3048,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 +3411,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 +3434,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 +3917,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/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"]): 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") }} 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 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 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], ] 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",