diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 6701673cc7f..37d8363beaa 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -43,3 +43,6 @@ jobs: - name: Run Semgrep rules run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness + + - name: Semgrep for Test Correctness + run: semgrep ci --include=**/test_*.py --config ./semgrep/test-correctness.yml diff --git a/erpnext/accounts/doctype/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/accounting_dimension/test_accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py index 81e639dc6b2..250442a3cd4 100644 --- a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py @@ -10,9 +10,6 @@ from erpnext.tests.utils import ERPNextTestSuite class TestAccountingDimension(ERPNextTestSuite): - def setUp(self): - create_dimension() - def test_dimension_against_sales_invoice(self): si = create_sales_invoice(do_not_save=1) @@ -77,63 +74,3 @@ class TestAccountingDimension(ERPNextTestSuite): si.save() self.assertRaises(frappe.ValidationError, si.submit) - - -def create_dimension(): - frappe.set_user("Administrator") - - if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}): - dimension = frappe.get_doc( - { - "doctype": "Accounting Dimension", - "document_type": "Department", - } - ) - dimension.append( - "dimension_defaults", - { - "company": "_Test Company", - "reference_document": "Department", - "default_dimension": "_Test Department - _TC", - }, - ) - dimension.insert() - dimension.save() - else: - dimension = frappe.get_doc("Accounting Dimension", "Department") - dimension.disabled = 0 - dimension.save() - - if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}): - dimension1 = frappe.get_doc( - { - "doctype": "Accounting Dimension", - "document_type": "Location", - } - ) - - dimension1.append( - "dimension_defaults", - { - "company": "_Test Company", - "reference_document": "Location", - "default_dimension": "Block 1", - }, - ) - - dimension1.insert() - dimension1.save() - else: - dimension1 = frappe.get_doc("Accounting Dimension", "Location") - dimension1.disabled = 0 - dimension1.save() - - -def disable_dimension(): - dimension1 = frappe.get_doc("Accounting Dimension", "Department") - dimension1.disabled = 1 - dimension1.save() - - dimension2 = frappe.get_doc("Accounting Dimension", "Location") - dimension2.disabled = 1 - dimension2.save() diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py index 9bed10824bb..fe7d4706967 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py @@ -5,10 +5,6 @@ import unittest import frappe -from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( - create_dimension, - disable_dimension, -) from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError from erpnext.tests.utils import ERPNextTestSuite @@ -16,7 +12,6 @@ from erpnext.tests.utils import ERPNextTestSuite class TestAccountingDimensionFilter(ERPNextTestSuite): def setUp(self): - create_dimension() create_accounting_dimension_filter() self.invoice_list = [] diff --git a/erpnext/accounts/doctype/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_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py index c0dc6467f8f..9fe5b4ba3fb 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.py +++ b/erpnext/accounts/doctype/bank_account/bank_account.py @@ -116,6 +116,7 @@ def get_default_company_bank_account(company, party_type, party): @frappe.whitelist() def get_bank_account_details(bank_account): + frappe.has_permission("Bank Account", doc=bank_account, ptype="read", throw=True) return frappe.get_cached_value( "Bank Account", bank_account, ["account", "bank", "bank_account_no"], as_dict=1 ) diff --git a/erpnext/accounts/doctype/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/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 3abfa176622..452164c728c 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -489,4 +489,5 @@ def rename_temporarily_named_docs(doctype): for hook in frappe.get_hooks(hook_type): frappe.call(hook, newname=newname, oldname=oldname) - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() diff --git a/erpnext/accounts/doctype/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/ledger_merge/ledger_merge.py b/erpnext/accounts/doctype/ledger_merge/ledger_merge.py index 008b4115f5f..dc3fd5a9d04 100644 --- a/erpnext/accounts/doctype/ledger_merge/ledger_merge.py +++ b/erpnext/accounts/doctype/ledger_merge/ledger_merge.py @@ -71,14 +71,16 @@ def start_merge(docname): ledger_merge.account, ) row.db_set("merged", 1) - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() successful_merges += 1 frappe.publish_realtime( "ledger_merge_progress", {"ledger_merge": ledger_merge.name, "current": successful_merges, "total": total}, ) except Exception: - frappe.db.rollback() + if not frappe.in_test: + frappe.db.rollback() ledger_merge.log_error("Ledger merge failed") finally: if successful_merges == total: diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js index 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/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py index 6a3df141f52..3949e242567 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py @@ -5,7 +5,7 @@ import frappe from frappe import _, scrub from frappe.model.document import Document -from frappe.utils import flt, nowdate +from frappe.utils import escape_html, flt, nowdate from frappe.utils.background_jobs import enqueue, is_job_enqueued from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( @@ -86,6 +86,11 @@ class OpeningInvoiceCreationTool(Document): ) prepare_invoice_summary(doctype, invoices) + invoices_summary_companies = list(invoices_summary.keys()) + + for company in invoices_summary_companies: + invoices_summary[escape_html(company)] = invoices_summary.pop(company) + return invoices_summary, max_count def validate_company(self): @@ -274,7 +279,8 @@ def start_import(invoices): doc.flags.ignore_mandatory = True doc.insert(set_name=invoice_number) doc.submit() - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() names.append(doc.name) except Exception: errors += 1 diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py index 3d57c781983..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 @@ -3,10 +3,6 @@ import frappe -from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( - create_dimension, - disable_dimension, -) from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import ( get_temporary_opening_account, ) @@ -14,11 +10,6 @@ from erpnext.tests.utils import ERPNextTestSuite class TestOpeningInvoiceCreationTool(ERPNextTestSuite): - def setUp(self): - if not frappe.db.exists("Company", "_Test Opening Invoice Company"): - make_company() - create_dimension() - def make_invoices( self, invoice_type="Sales", @@ -183,26 +174,13 @@ def get_opening_invoice_creation_dict(**args): return invoice_dict -def make_company(): - if frappe.db.exists("Company", "_Test Opening Invoice Company"): - return frappe.get_doc("Company", "_Test Opening Invoice Company") - - company = frappe.new_doc("Company") - company.company_name = "_Test Opening Invoice Company" - company.abbr = "_TOIC" - company.default_currency = "INR" - company.country = "Pakistan" - company.insert() - return company - - def make_customer(customer=None): customer_name = customer or "Opening Customer" customer = frappe.get_doc( { "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/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 289c19a5885..d60f77120b2 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -2376,9 +2376,7 @@ def get_outstanding_reference_documents(args, validate=False): vouchers=args.get("vouchers") or None, ) - outstanding_invoices = split_invoices_based_on_payment_terms( - outstanding_invoices, args.get("company") - ) + outstanding_invoices = split_refdocs_based_on_payment_terms(outstanding_invoices, args.get("company")) for d in outstanding_invoices: d["exchange_rate"] = 1 @@ -2416,6 +2414,8 @@ def get_outstanding_reference_documents(args, validate=False): filters=args, ) + orders_to_be_billed = split_refdocs_based_on_payment_terms(orders_to_be_billed, args.get("company")) + data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed if not data: @@ -2438,13 +2438,13 @@ def get_outstanding_reference_documents(args, validate=False): return data -def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list: +def split_refdocs_based_on_payment_terms(refdocs, company) -> list: """Split a list of invoices based on their payment terms.""" - exc_rates = get_currency_data(outstanding_invoices, company) + exc_rates = get_currency_data(refdocs, company) - outstanding_invoices_after_split = [] - for entry in outstanding_invoices: - if entry.voucher_type in ["Sales Invoice", "Purchase Invoice"]: + outstanding_refdoc_after_split = [] + for entry in refdocs: + if entry.voucher_type in ["Sales Invoice", "Purchase Invoice", "Sales Order", "Purchase Order"]: if payment_term_template := frappe.db.get_value( entry.voucher_type, entry.voucher_no, "payment_terms_template" ): @@ -2459,25 +2459,25 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list ), alert=True, ) - outstanding_invoices_after_split += split_rows + outstanding_refdoc_after_split += split_rows continue # If not an invoice or no payment terms template, add as it is - outstanding_invoices_after_split.append(entry) + outstanding_refdoc_after_split.append(entry) - return outstanding_invoices_after_split + return outstanding_refdoc_after_split -def get_currency_data(outstanding_invoices: list, company: str | None = None) -> dict: +def get_currency_data(outstanding_refdocs: list, company: str | None = None) -> dict: """Get currency and conversion data for a list of invoices.""" exc_rates = frappe._dict() company_currency = frappe.db.get_value("Company", company, "default_currency") if company else None - for doctype in ["Sales Invoice", "Purchase Invoice"]: - invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype] + for doctype in ["Sales Invoice", "Purchase Invoice", "Sales Order", "Purchase Order"]: + refdoc = [x.voucher_no for x in outstanding_refdocs if x.voucher_type == doctype] for x in frappe.db.get_all( doctype, - filters={"name": ["in", invoices]}, + filters={"name": ["in", refdoc]}, fields=["name", "currency", "conversion_rate", "party_account_currency"], ): exc_rates[x.name] = frappe._dict( diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 12f5276fbfb..9d2890f5e79 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -2019,6 +2019,92 @@ class TestPaymentEntry(ERPNextTestSuite): self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, pe.doctype, pe.name) self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, "Journal Entry", jv[0]) + def test_outstanding_orders_split_by_payment_terms(self): + create_payment_terms_template() + + so = make_sales_order(do_not_save=1, qty=1, rate=200) + so.payment_terms_template = "Test Receivable Template" + so.save().submit() + + args = { + "posting_date": nowdate(), + "company": so.company, + "party_type": "Customer", + "payment_type": "Receive", + "party": so.customer, + "party_account": "Debtors - _TC", + "get_orders_to_be_billed": True, + } + + references = get_outstanding_reference_documents(args) + + self.assertEqual(len(references), 2) + self.assertEqual(references[0].voucher_no, so.name) + self.assertEqual(references[1].voucher_no, so.name) + self.assertEqual(references[0].payment_term, "Basic Amount Receivable") + self.assertEqual(references[1].payment_term, "Tax Receivable") + + def test_outstanding_orders_no_split_when_allocate_disabled(self): + create_payment_terms_template() + + template = frappe.get_doc("Payment Terms Template", "Test Receivable Template") + template.allocate_payment_based_on_payment_terms = 0 + template.save() + + so = make_sales_order(do_not_save=1, qty=1, rate=200) + so.payment_terms_template = "Test Receivable Template" + so.save().submit() + + args = { + "posting_date": nowdate(), + "company": so.company, + "party_type": "Customer", + "payment_type": "Receive", + "party": so.customer, + "party_account": "Debtors - _TC", + "get_orders_to_be_billed": True, + } + + references = get_outstanding_reference_documents(args) + + self.assertEqual(len(references), 1) + self.assertIsNone(references[0].payment_term) + + template.allocate_payment_based_on_payment_terms = 1 + template.save() + + def test_outstanding_multicurrency_sales_order_split(self): + create_payment_terms_template() + + so = make_sales_order( + customer="_Test Customer USD", + currency="USD", + qty=1, + rate=100, + do_not_submit=True, + ) + so.payment_terms_template = "Test Receivable Template" + so.conversion_rate = 50 + so.save().submit() + + args = { + "posting_date": nowdate(), + "company": so.company, + "party_type": "Customer", + "payment_type": "Receive", + "party": so.customer, + "party_account": "Debtors - _TC", + "get_orders_to_be_billed": True, + } + + references = get_outstanding_reference_documents(args) + + # Should split without throwing currency errors + self.assertEqual(len(references), 2) + for ref in references: + self.assertEqual(ref.voucher_no, so.name) + self.assertIsNotNone(ref.payment_term) + def create_payment_entry(**args): payment_entry = frappe.new_doc("Payment Entry") diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index c95945bf6e2..e16e132957f 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -750,7 +750,8 @@ def make_payment_request(**args): pr.submit() if args.order_type == "Shopping Cart": - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() frappe.local.response["type"] = "redirect" frappe.local.response["location"] = pr.get_payment_url() diff --git a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py index af5f73a39ec..05e24d16a3a 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py +++ b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py @@ -4,10 +4,6 @@ import unittest import frappe -from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( - create_dimension, - disable_dimension, -) from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import ( make_closing_entry_from_opening, ) @@ -162,7 +158,6 @@ class TestPOSClosingEntry(ERPNextTestSuite): test case to check whether we can create POS Closing Entry without mandatory accounting dimension """ - create_dimension() location = frappe.get_doc("Accounting Dimension", "Location") location.dimension_defaults[0].mandatory_for_bs = True location.save() @@ -198,7 +193,6 @@ class TestPOSClosingEntry(ERPNextTestSuite): ) accounting_dimension_department.mandatory_for_bs = 0 accounting_dimension_department.save() - disable_dimension() def test_merging_into_sales_invoice_for_batched_item(self): frappe.flags.print_message = False diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 9cf27216b1e..a40cd03240e 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -563,10 +563,10 @@ def send_emails(document_name, from_scheduler=False, posting_date=None): new_from_date = add_months(new_to_date, -1 * doc.filter_duration) doc.add_comment("Comment", "Emails sent on: " + frappe.utils.format_datetime(frappe.utils.now())) if doc.report == "General Ledger": - doc.db_set("to_date", new_to_date, commit=True) - doc.db_set("from_date", new_from_date, commit=True) + frappe.db.set_value(doc.doctype, doc.name, "to_date", new_to_date) + frappe.db.set_value(doc.doctype, doc.name, "from_date", new_from_date) else: - doc.db_set("posting_date", new_to_date, commit=True) + frappe.db.set_value(doc.doctype, doc.name, "posting_date", new_to_date) return True else: return False diff --git a/erpnext/accounts/doctype/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.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index d2021e0f9a4..adb7dad6726 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -8,7 +8,6 @@ "email_append_to": 1, "engine": "InnoDB", "field_order": [ - "title", "naming_series", "supplier", "supplier_name", @@ -209,16 +208,6 @@ "connections_tab" ], "fields": [ - { - "allow_on_submit": 1, - "default": "{supplier_name}", - "fieldname": "title", - "fieldtype": "Data", - "hidden": 1, - "label": "Title", - "no_copy": 1, - "print_hide": 1 - }, { "fieldname": "naming_series", "fieldtype": "Select", @@ -1693,7 +1682,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2026-03-17 20:44:00.221219", + "modified": "2026-03-25 11:45:38.696888", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", @@ -1756,6 +1745,6 @@ "sort_order": "DESC", "states": [], "timeline_field": "supplier", - "title_field": "title", + "title_field": "supplier_name", "track_changes": 1 } diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 527c33225c6..c790f86633a 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -203,7 +203,6 @@ class PurchaseInvoice(BuyingController): taxes_and_charges_deducted: DF.Currency tc_name: DF.Link | None terms: DF.TextEditor | None - title: DF.Data | None to_date: DF.Date | None total: DF.Currency total_advance: DF.Currency @@ -333,9 +332,6 @@ class PurchaseInvoice(BuyingController): if self.bill_date: self.remarks += " " + _("dated {0}").format(formatdate(self.bill_date)) - else: - self.remarks = _("No Remarks") - def set_missing_values(self, for_validate=False): if not self.credit_to: self.credit_to = get_party_account("Supplier", self.supplier, self.company) @@ -617,12 +613,13 @@ class PurchaseInvoice(BuyingController): frappe.db.set_value(self.doctype, self.name, "against_expense_account", self.against_expense_account) def po_required(self): - if frappe.db.get_single_value("Buying Settings", "po_required") == "Yes": - if frappe.get_value( + if ( + frappe.db.get_single_value("Buying Settings", "po_required") == "Yes" + and not self.is_internal_transfer() + and not frappe.get_value( "Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_order" - ): - return - + ) + ): for d in self.get("items"): if not d.purchase_order: msg = _("Purchase Order Required for item {}").format(frappe.bold(d.item_code)) @@ -983,6 +980,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 +1162,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 +1203,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 33117c639dc..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, @@ -2189,11 +2200,6 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin): def test_offsetting_entries_for_accounting_dimensions(self): from erpnext.accounts.doctype.account.test_account import create_account - from erpnext.accounts.report.trial_balance.test_trial_balance import ( - clear_dimension_defaults, - create_accounting_dimension, - disable_dimension, - ) create_account( account_name="Offsetting", @@ -2201,7 +2207,16 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin): parent_account="Temporary Accounts - _TC", ) - create_accounting_dimension(company="_Test Company", offsetting_account="Offsetting - _TC") + dim = frappe.get_doc("Accounting Dimension", "Branch") + dim.append( + "dimension_defaults", + { + "company": "_Test Company", + "reference_document": "Branch", + "offsetting_account": "Offsetting - _TC", + }, + ) + dim.save() branch1 = frappe.new_doc("Branch") branch1.branch = "Location 1" @@ -2238,8 +2253,6 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin): voucher_type="Purchase Invoice", additional_columns=["branch"], ) - clear_dimension_defaults("Branch") - disable_dimension() def test_repost_accounting_entries(self): # update repost settings diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 00951fdd4cc..7399e987fa7 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", @@ -730,7 +740,6 @@ "label": "Valuation Rate", "no_copy": 1, "options": "Company:company:default_currency", - "precision": "6", "print_hide": 1, "read_only": 1 }, @@ -796,6 +805,7 @@ "fieldtype": "Link", "label": "Asset Category", "options": "Asset Category", + "print_hide": 1, "read_only": 1 }, { @@ -828,6 +838,7 @@ "label": "Rate of Stock UOM", "no_copy": 1, "options": "currency", + "print_hide": 1, "read_only": 1 }, { @@ -866,6 +877,7 @@ "fieldtype": "Currency", "label": "Rate With Margin", "options": "currency", + "print_hide": 1, "read_only": 1 }, { @@ -892,7 +904,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 +931,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 +944,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 +992,8 @@ "fieldname": "distributed_discount_amount", "fieldtype": "Currency", "label": "Distributed Discount Amount", - "options": "currency" + "options": "currency", + "print_hide": 1 }, { "fieldname": "tax_withholding_category", @@ -991,7 +1007,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2026-02-15 21:07:49.455930", + "modified": "2026-04-07 15:40:45.687554", "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/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 64728cd1e0a..90c0da74f26 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -165,13 +165,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( ); } } - - // Show buttons only when pos view is active - if (cint(doc.docstatus == 0) && this.frm.page.current_view_name !== "pos" && !doc.is_return) { - this.frm.cscript.sales_order_btn(); - this.frm.cscript.delivery_note_btn(); - this.frm.cscript.quotation_btn(); - } + this.toggle_get_items(); this.set_default_print_format(); if (doc.docstatus == 1 && !doc.inter_company_invoice_reference) { @@ -260,6 +254,93 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( } } + toggle_get_items() { + const buttons = ["Sales Order", "Quotation", "Timesheet", "Delivery Note"]; + + buttons.forEach((label) => { + this.frm.remove_custom_button(label, "Get Items From"); + }); + + if (cint(this.frm.doc.docstatus) !== 0 || this.frm.page.current_view_name === "pos") { + return; + } + + if (!this.frm.doc.is_return) { + this.frm.cscript.sales_order_btn(); + this.frm.cscript.quotation_btn(); + this.frm.cscript.timesheet_btn(); + } + + this.frm.cscript.delivery_note_btn(); + } + + timesheet_btn() { + var me = this; + + me.frm.add_custom_button( + __("Timesheet"), + function () { + let d = new frappe.ui.Dialog({ + title: __("Fetch Timesheet"), + fields: [ + { + label: __("From"), + fieldname: "from_time", + fieldtype: "Date", + reqd: 1, + }, + { + label: __("Item Code"), + fieldname: "item_code", + fieldtype: "Link", + options: "Item", + get_query: () => { + return { + query: "erpnext.controllers.queries.item_query", + filters: { + is_sales_item: 1, + customer: me.frm.doc.customer, + has_variants: 0, + }, + }; + }, + }, + { + fieldtype: "Column Break", + fieldname: "col_break_1", + }, + { + label: __("To"), + fieldname: "to_time", + fieldtype: "Date", + reqd: 1, + }, + { + label: __("Project"), + fieldname: "project", + fieldtype: "Link", + options: "Project", + default: me.frm.doc.project, + }, + ], + primary_action: function () { + const data = d.get_values(); + me.frm.events.add_timesheet_data(me.frm, { + from_time: data.from_time, + to_time: data.to_time, + project: data.project, + item_code: data.item_code, + }); + d.hide(); + }, + primary_action_label: __("Get Timesheets"), + }); + d.show(); + }, + __("Get Items From") + ); + } + sales_order_btn() { var me = this; @@ -331,6 +412,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( this.$delivery_note_btn = this.frm.add_custom_button( __("Delivery Note"), function () { + if (!me.frm.doc.customer) { + frappe.throw({ + title: __("Mandatory"), + message: __("Please Select a Customer"), + }); + } erpnext.utils.map_current_doc({ method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice", source_doctype: "Delivery Note", @@ -343,7 +430,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( var filters = { docstatus: 1, company: me.frm.doc.company, - is_return: 0, + is_return: me.frm.doc.is_return, }; if (me.frm.doc.customer) filters["customer"] = me.frm.doc.customer; return { @@ -610,6 +697,10 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( apply_tds(frm) { this.frm.clear_table("tax_withholding_entries"); } + + is_return() { + this.toggle_get_items(); + } }; // for backward compatibility: combine new and previous states @@ -1061,71 +1152,6 @@ frappe.ui.form.on("Sales Invoice", { }, refresh: function (frm) { - if (frm.doc.docstatus === 0 && !frm.doc.is_return) { - frm.add_custom_button( - __("Timesheet"), - function () { - let d = new frappe.ui.Dialog({ - title: __("Fetch Timesheet"), - fields: [ - { - label: __("From"), - fieldname: "from_time", - fieldtype: "Date", - reqd: 1, - }, - { - label: __("Item Code"), - fieldname: "item_code", - fieldtype: "Link", - options: "Item", - get_query: () => { - return { - query: "erpnext.controllers.queries.item_query", - filters: { - is_sales_item: 1, - customer: frm.doc.customer, - has_variants: 0, - }, - }; - }, - }, - { - fieldtype: "Column Break", - fieldname: "col_break_1", - }, - { - label: __("To"), - fieldname: "to_time", - fieldtype: "Date", - reqd: 1, - }, - { - label: __("Project"), - fieldname: "project", - fieldtype: "Link", - options: "Project", - default: frm.doc.project, - }, - ], - primary_action: function () { - const data = d.get_values(); - frm.events.add_timesheet_data(frm, { - from_time: data.from_time, - to_time: data.to_time, - project: data.project, - item_code: data.item_code, - }); - d.hide(); - }, - primary_action_label: __("Get Timesheets"), - }); - d.show(); - }, - __("Get Items From") - ); - } - if (frm.doc.is_debit_note) { frm.set_df_property("return_against", "label", __("Adjustment Against")); } diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index ba5aa9dfd93..0477c172587 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1101,9 +1101,6 @@ class SalesInvoice(SellingController): if self.po_date: self.remarks += " " + _("dated {0}").format(formatdate(self.po_date)) - else: - self.remarks = _("No Remarks") - def validate_auto_set_posting_time(self): # Don't auto set the posting date and time if invoice is amended if self.is_new() and self.amended_from: diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 3616f196bb7..03d8b2e7eb5 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2246,13 +2246,6 @@ class TestSalesInvoice(ERPNextTestSuite): @ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": True}) def test_rounding_adjustment_3(self): - from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import create_dimension - - # Dimension creates custom field, which does an implicit DB commit as it is a DDL command - # Ensure dimension don't have any mandatory fields - create_dimension() - - # rollback from tearDown() happens till here si = create_sales_invoice(do_not_save=True) si.items = [] for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]: @@ -2894,7 +2887,7 @@ class TestSalesInvoice(ERPNextTestSuite): si.submit() # Check if adjustment entry is created - self.assertTrue( + self.assertFalse( frappe.db.exists( "GL Entry", { 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/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 0b3da559e39..642f918c3b1 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -772,7 +772,8 @@ def process_all(subscription: list, posting_date: DateTimeLikeObject | None = No try: subscription = frappe.get_doc("Subscription", subscription_name) subscription.process(posting_date) - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() except frappe.ValidationError: frappe.db.rollback() subscription.log_error("Subscription failed") diff --git a/erpnext/accounts/doctype/tax_withholding_category/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 66dc090f7c7..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 @@ -18,7 +19,6 @@ class TestTaxWithholdingCategory(ERPNextTestSuite): # create relevant supplier, etc create_records() create_tax_withholding_category_records() - make_pan_no_field() def validate_tax_withholding_entries(self, doctype, docname, expected_entries): """Validate tax withholding entries for a document""" @@ -3542,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 @@ -3998,18 +4039,3 @@ def create_lower_deduction_certificate( "certificate_limit": limit, } ).insert() - - -def make_pan_no_field(): - pan_field = { - "Supplier": [ - { - "fieldname": "pan", - "label": "PAN", - "fieldtype": "Data", - "translatable": 0, - } - ] - } - - create_custom_fields(pan_field, update=1) diff --git a/erpnext/accounts/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..4b852e0583d 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() @@ -120,12 +120,12 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite): report = execute(filters) - expected_data = [[100, 30, "No Remarks"], [100, 50, "No Remarks"], [100, 20, "No Remarks"]] + expected_data = [[100, 30], [100, 50], [100, 20]] for i in range(3): row = report[1][i - 1] - self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks]) - + self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced]) + self.assertFalse(row.get("remarks")) # check invoice grand total, invoiced, paid and outstanding column's value after payment self.create_payment_entry(si.name) report = execute(filters) @@ -178,11 +178,11 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite): report = execute(filters) - expected_data = [[100, 30, "No Remarks"], [100, 50, "No Remarks"], [100, 20, "No Remarks"]] + expected_data = [[100, 30], [100, 50], [100, 20]] for i in range(3): row = report[1][i - 1] - self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks]) + self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced]) # check invoice grand total, invoiced, paid and outstanding column's value after credit note cr_note = self.create_credit_note(si.name, do_not_submit=True) @@ -225,9 +225,10 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite): report = execute(filters) row = report[1][0] - expected_data = [8000, 8000, "No Remarks"] # Data in company currency + expected_data = [8000, 8000] # Data in company currency - self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks]) + self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced]) + self.assertFalse(row.get("remarks")) # CASE 2: Transaction currency and party account currency are the same self.create_customer( @@ -258,18 +259,20 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite): report = execute(filters) row = report[1][0] - expected_data = [100, 100, "No Remarks"] # Data in Part Account Currency + expected_data = [100, 100] # Data in Part Account Currency - self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks]) + self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced]) + self.assertFalse(row.get("remarks")) # View in Company currency filters.pop("in_party_currency") report = execute(filters) row = report[1][0] - expected_data = [8000, 8000, "No Remarks"] # Data in Company Currency + expected_data = [8000, 8000] # Data in Company Currency - self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks]) + self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced]) + self.assertFalse(row.get("remarks")) def test_accounts_receivable_with_partial_payment(self): filters = { @@ -285,11 +288,12 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite): report = execute(filters) - expected_data = [[200, 60, "No Remarks"], [200, 100, "No Remarks"], [200, 40, "No Remarks"]] + expected_data = [[200, 60], [200, 100], [200, 40]] for i in range(3): row = report[1][i - 1] - self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks]) + self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced]) + self.assertFalse(row.get("remarks")) # check invoice grand total, invoiced, paid and outstanding column's value after payment self.create_payment_entry(si.name) @@ -348,11 +352,12 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite): report = execute(filters) - expected_data = [100, 100, "No Remarks"] + expected_data = [100, 100] self.assertEqual(len(report[1]), 1) row = report[1][0] - self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks]) + self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced]) + self.assertFalse(row.get("remarks")) # check invoice grand total, invoiced, paid and outstanding column's value after payment self.create_payment_entry(si.name) 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/deferred_revenue_and_expense/deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py index caa464c5447..22fabeabca6 100644 --- a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py +++ b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py @@ -48,6 +48,9 @@ class Deferred_Item: Generate report data for output """ ret_data = frappe._dict({"name": self.item_name}) + ret_data.service_start_date = self.service_start_date + ret_data.service_end_date = self.service_end_date + ret_data.amount = self.base_net_amount for period in self.period_total: ret_data[period.key] = period.total ret_data.indent = 1 @@ -205,6 +208,9 @@ class Deferred_Invoice: for item in self.uniq_items: self.items.append(Deferred_Item(item, self, [x for x in items if x.item == item])) + # roll-up amount from all deferred items + self.amount_total = sum(item.base_net_amount for item in self.items) + def calculate_invoice_revenue_expense_for_period(self): """ calculate deferred revenue/expense for all items in invoice @@ -232,7 +238,7 @@ class Deferred_Invoice: generate report data for invoice, includes invoice total """ ret_data = [] - inv_total = frappe._dict({"name": self.name}) + inv_total = frappe._dict({"name": self.name, "amount": self.amount_total}) for x in self.period_total: inv_total[x.key] = x.total inv_total.indent = 0 @@ -386,6 +392,24 @@ class Deferred_Revenue_and_Expense_Report: def get_columns(self): columns = [] columns.append({"label": _("Name"), "fieldname": "name", "fieldtype": "Data", "read_only": 1}) + columns.append( + { + "label": _("Service Start Date"), + "fieldname": "service_start_date", + "fieldtype": "Date", + "read_only": 1, + } + ) + columns.append( + { + "label": _("Service End Date"), + "fieldname": "service_end_date", + "fieldtype": "Date", + "read_only": 1, + } + ) + columns.append({"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "read_only": 1}) + for period in self.period_list: columns.append( { @@ -415,6 +439,8 @@ class Deferred_Revenue_and_Expense_Report: elif self.filters.type == "Expense": total_row = frappe._dict({"name": "Total Deferred Expense"}) + total_row["amount"] = sum(inv.amount_total for inv in self.deferred_invoices) + for idx, period in enumerate(self.period_list, 0): total_row[period.key] = self.period_total[idx].total ret.append(total_row) diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py index 2d0015a461e..394a9636b78 100644 --- a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py +++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py @@ -142,6 +142,7 @@ def prepare_data(accounts, filters, company_currency, dimension_list): total = 0 row = { "account": d.name, + "is_group": d.is_group, "parent_account": d.parent_account, "indent": d.indent, "from_date": filters.from_date, 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/report/trial_balance/test_trial_balance.py b/erpnext/accounts/report/trial_balance/test_trial_balance.py index 42cf62af0a0..c37f9d5a46a 100644 --- a/erpnext/accounts/report/trial_balance/test_trial_balance.py +++ b/erpnext/accounts/report/trial_balance/test_trial_balance.py @@ -14,7 +14,6 @@ class TestTrialBalance(ERPNextTestSuite): from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center from erpnext.accounts.utils import get_fiscal_year - self.company = create_company() create_cost_center( cost_center_name="Test Cost Center", company="Trial Balance Company", @@ -26,7 +25,16 @@ class TestTrialBalance(ERPNextTestSuite): parent_account="Temporary Accounts - TBC", ) self.fiscal_year = get_fiscal_year(today(), company="Trial Balance Company")[0] - create_accounting_dimension() + dim = frappe.get_doc("Accounting Dimension", "Branch") + dim.append( + "dimension_defaults", + { + "company": "Trial Balance Company", + "automatically_post_balancing_accounting_entry": 1, + "offsetting_account": "Offsetting - TBC", + }, + ) + dim.save() def test_offsetting_entries_for_accounting_dimensions(self): """ @@ -45,7 +53,7 @@ class TestTrialBalance(ERPNextTestSuite): branch2.insert(ignore_if_duplicate=True) si = create_sales_invoice( - company=self.company, + company="Trial Balance Company", debit_to="Debtors - TBC", cost_center="Test Cost Center - TBC", income_account="Sales - TBC", @@ -57,60 +65,7 @@ class TestTrialBalance(ERPNextTestSuite): si.submit() filters = frappe._dict( - {"company": self.company, "fiscal_year": self.fiscal_year, "branch": ["Location 1"]} + {"company": "Trial Balance Company", "fiscal_year": self.fiscal_year, "branch": ["Location 1"]} ) total_row = execute(filters)[1][-1] self.assertEqual(total_row["debit"], total_row["credit"]) - - -def create_company(**args): - args = frappe._dict(args) - company = frappe.get_doc( - { - "doctype": "Company", - "company_name": args.company_name or "Trial Balance Company", - "country": args.country or "India", - "default_currency": args.currency or "INR", - "parent_company": args.get("parent_company"), - "is_group": args.get("is_group"), - } - ) - company.insert(ignore_if_duplicate=True) - return company.name - - -def create_accounting_dimension(**args): - args = frappe._dict(args) - document_type = args.document_type or "Branch" - if frappe.db.exists("Accounting Dimension", document_type): - accounting_dimension = frappe.get_doc("Accounting Dimension", document_type) - accounting_dimension.disabled = 0 - else: - accounting_dimension = frappe.new_doc("Accounting Dimension") - accounting_dimension.document_type = document_type - accounting_dimension.insert() - - accounting_dimension.set("dimension_defaults", []) - accounting_dimension.append( - "dimension_defaults", - { - "company": args.company or "Trial Balance Company", - "automatically_post_balancing_accounting_entry": 1, - "offsetting_account": args.offsetting_account or "Offsetting - TBC", - }, - ) - accounting_dimension.save() - - -def disable_dimension(**args): - args = frappe._dict(args) - document_type = args.document_type or "Branch" - dimension = frappe.get_doc("Accounting Dimension", document_type) - dimension.disabled = 1 - dimension.save() - - -def clear_dimension_defaults(dimension_name): - accounting_dimension = frappe.get_doc("Accounting Dimension", dimension_name) - accounting_dimension.dimension_defaults = [] - accounting_dimension.save() diff --git a/erpnext/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..0c23353d1eb 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 @@ -1531,6 +1603,10 @@ def parse_naming_series_variable(doc, variable): else: data = {"YY": "%y", "YYYY": "%Y", "MM": "%m", "DD": "%d", "JJJ": "%j"} + + if doc and doc.doctype in ["Batch", "Serial No"] and doc.reference_doctype and doc.reference_name: + doc = frappe.get_doc(doc.reference_doctype, doc.reference_name) + date = ( ( getdate(doc.get("posting_date") or doc.get("transaction_date") or doc.get("posting_datetime")) diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 832e3736e7d..1a9788be48e 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -61,7 +61,9 @@ def book_depreciation_entries(date): accounting_dimensions, ) - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() + except Exception as e: frappe.db.rollback() failed_assets.append(asset_name) @@ -71,7 +73,8 @@ def book_depreciation_entries(date): if failed_assets: set_depr_entry_posting_status_for_failed_assets(failed_assets) notify_depr_entry_posting_error(failed_assets, error_logs) - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() def get_depreciable_assets_data(date): diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py index f245ac4f0a2..e37ac4c2bf3 100644 --- a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py @@ -380,6 +380,7 @@ class TestAssetCapitalization(ERPNextTestSuite): "asset_type": "Composite Component", "purchase_date": pr.posting_date, "available_for_use_date": pr.posting_date, + "location": "Test Location", } ) consumed_asset_doc.save() diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.js b/erpnext/assets/doctype/asset_movement/asset_movement.js index f56c1e31f27..2e73d0d9df8 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.js +++ b/erpnext/assets/doctype/asset_movement/asset_movement.js @@ -41,7 +41,7 @@ frappe.ui.form.on("Asset Movement", { }); }, - onload: (frm) => { + refresh: (frm) => { frm.trigger("set_required_fields"); }, diff --git a/erpnext/assets/doctype/asset_movement/test_asset_movement.py b/erpnext/assets/doctype/asset_movement/test_asset_movement.py index 88dd93a93cb..76d37d3abb4 100644 --- a/erpnext/assets/doctype/asset_movement/test_asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/test_asset_movement.py @@ -16,7 +16,6 @@ class TestAssetMovement(ERPNextTestSuite): frappe.db.set_value( "Company", "_Test Company", "capital_work_in_progress_account", "CWIP Account - _TC" ) - make_location() def test_movement(self): pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location") @@ -40,10 +39,6 @@ class TestAssetMovement(ERPNextTestSuite): if asset.docstatus == 0: asset.submit() - # check asset movement is created - if not frappe.db.exists("Location", "Test Location 2"): - frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert() - create_asset_movement( purpose="Transfer", company=asset.company, @@ -122,9 +117,6 @@ class TestAssetMovement(ERPNextTestSuite): if asset.docstatus == 0: asset.submit() - if not frappe.db.exists("Location", "Test Location 2"): - frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert() - movement = frappe.get_doc({"doctype": "Asset Movement", "reference_name": pr.name}) self.assertRaises(frappe.ValidationError, movement.cancel) @@ -150,9 +142,6 @@ class TestAssetMovement(ERPNextTestSuite): asset = create_asset(item_code="Macbook Pro", do_not_save=1) asset.save().submit() - if not frappe.db.exists("Location", "Test Location 2"): - frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert() - asset_creation_date = frappe.db.get_value( "Asset Movement", [["Asset Movement Item", "asset", "=", asset.name], ["docstatus", "=", 1]], @@ -197,9 +186,3 @@ def create_asset_movement(**args): movement.submit() return movement - - -def make_location(): - for location in ["Pune", "Mumbai", "Nagpur"]: - if not frappe.db.exists("Location", location): - frappe.get_doc({"doctype": "Location", "location_name": location}).insert(ignore_permissions=True) diff --git a/erpnext/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/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 2a1b37aae2a..260dc52ceac 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -9,7 +9,6 @@ "engine": "InnoDB", "field_order": [ "supplier_section", - "title", "naming_series", "supplier", "supplier_name", @@ -172,17 +171,6 @@ "fieldtype": "Section Break", "options": "fa fa-user" }, - { - "allow_on_submit": 1, - "default": "{supplier_name}", - "fieldname": "title", - "fieldtype": "Data", - "hidden": 1, - "label": "Title", - "no_copy": 1, - "print_hide": 1, - "reqd": 1 - }, { "fieldname": "naming_series", "fieldtype": "Select", @@ -1328,7 +1316,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2026-03-09 17:15:29.184682", + "modified": "2026-03-25 11:46:18.748951", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index a0daeca51f2..2cb285c14f3 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -159,7 +159,6 @@ class PurchaseOrder(BuyingController): taxes_and_charges_deducted: DF.Currency tc_name: DF.Link | None terms: DF.TextEditor | None - title: DF.Data to_date: DF.Date | None total: DF.Currency total_net_weight: DF.Float @@ -780,7 +779,8 @@ def make_purchase_invoice_from_portal(purchase_order_name): if frappe.session.user not in frappe.get_all("Portal User", {"parent": doc.supplier}, pluck="user"): frappe.throw(_("Not Permitted"), frappe.PermissionError) doc.save() - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() frappe.response["type"] = "redirect" frappe.response.location = "/purchase-invoices/" + doc.name @@ -802,18 +802,18 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions target.set_payment_schedule() target.credit_to = get_party_account("Supplier", source.supplier, source.company) + def get_billed_qty(po_item_name): + from frappe.query_builder.functions import Sum + + table = frappe.qb.DocType("Purchase Invoice Item") + query = ( + frappe.qb.from_(table) + .select(Sum(table.qty).as_("qty")) + .where((table.docstatus == 1) & (table.po_detail == po_item_name)) + ) + return query.run(pluck="qty")[0] or 0 + def update_item(obj, target, source_parent): - def get_billed_qty(po_item_name): - from frappe.query_builder.functions import Sum - - table = frappe.qb.DocType("Purchase Invoice Item") - query = ( - frappe.qb.from_(table) - .select(Sum(table.qty).as_("qty")) - .where((table.docstatus == 1) & (table.po_detail == po_item_name)) - ) - return query.run(pluck="qty")[0] or 0 - billed_qty = flt(get_billed_qty(obj.name)) target.qty = flt(obj.qty) - billed_qty @@ -853,7 +853,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions "wip_composite_asset": "wip_composite_asset", }, "postprocess": update_item, - "condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)) + "condition": lambda doc: ( + doc.base_amount == 0 + or abs(doc.billed_amt) < abs(doc.amount) + or doc.qty > flt(get_billed_qty(doc.name)) + ) and select_item(doc), }, "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True}, diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index fe4bb12c3db..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) @@ -1386,6 +1410,34 @@ class TestPurchaseOrder(ERPNextTestSuite): self.assertEqual(pi_2.status, "Paid") self.assertEqual(po.status, "Completed") + def test_purchase_order_over_billing_missing_item(self): + item1 = make_item( + "_Test Item for Overbilling", + ).name + + item2 = make_item( + "_Test Item for Overbilling 2", + ).name + + po = create_purchase_order(qty=10, rate=1000, item_code=item1, do_not_save=1) + po.append("items", {"item_code": item2, "qty": 5, "rate": 20, "warehouse": "_Test Warehouse - _TC"}) + po.taxes = [] + po.insert() + po.submit() + + pi1 = make_pi_from_po(po.name) + pi1.items[0].qty = 8 + pi1.items[0].rate = 1250 + pi1.remove(pi1.items[1]) + pi1.insert() + pi1.submit() + + self.assertEqual(pi1.grand_total, 10000.0) + self.assertTrue(len(pi1.items) == 1) + + pi2 = make_pi_from_po(po.name) + self.assertEqual(len(pi2.items), 2) + def create_po_for_sc_testing(): from erpnext.controllers.tests.test_subcontracting_controller import ( diff --git a/erpnext/buying/doctype/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/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index b71d0dd3006..8baeba950b9 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -100,6 +100,7 @@ frappe.ui.form.on("Request for Quotation", { fieldname: "print_format", options: "Print Format", placeholder: "Standard", + default: frappe.get_meta("Request for Quotation").default_print_format || "", get_query: () => { return { filters: { diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json index 18e1356b263..de8b4d28547 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -9,8 +9,6 @@ "field_order": [ "naming_series", "company", - "billing_address", - "billing_address_display", "vendor", "column_break1", "transaction_date", @@ -43,7 +41,13 @@ "select_print_heading", "letter_head", "more_info", - "opportunity" + "opportunity", + "address_and_contact_tab", + "billing_address", + "billing_address_display", + "column_break_czul", + "shipping_address", + "shipping_address_display" ], "fields": [ { @@ -346,6 +350,27 @@ "fieldtype": "Check", "hidden": 1, "label": "Use HTML" + }, + { + "fieldname": "address_and_contact_tab", + "fieldtype": "Tab Break", + "label": "Address & Contact" + }, + { + "fieldname": "column_break_czul", + "fieldtype": "Column Break" + }, + { + "fieldname": "shipping_address", + "fieldtype": "Link", + "label": "Company Shipping Address", + "options": "Address" + }, + { + "fieldname": "shipping_address_display", + "fieldtype": "Text Editor", + "label": "Shipping Address Details", + "read_only": 1 } ], "grid_page_length": 50, @@ -353,7 +378,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2026-03-09 17:15:29.774614", + "modified": "2026-03-19 15:27:56.730649", "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation", diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 38ab9af4eab..09d7d30ab39 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -56,6 +56,8 @@ class RequestforQuotation(BuyingController): select_print_heading: DF.Link | None send_attached_files: DF.Check send_document_print: DF.Check + shipping_address: DF.Link | None + shipping_address_display: DF.TextEditor | None status: DF.Literal["", "Draft", "Submitted", "Cancelled"] subject: DF.Data suppliers: DF.Table[RequestforQuotationSupplier] @@ -283,7 +285,7 @@ class RequestforQuotation(BuyingController): } ) user.save(ignore_permissions=True) - update_password_link = user.reset_password() + update_password_link = user._reset_password() return user, update_password_link 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/manufacturing/doctype/bom_scrap_item/__init__.py b/erpnext/buying/print_format/request_for_quotation_with_item_image/__init__.py similarity index 100% rename from erpnext/manufacturing/doctype/bom_scrap_item/__init__.py rename to erpnext/buying/print_format/request_for_quotation_with_item_image/__init__.py diff --git a/erpnext/buying/print_format/request_for_quotation_with_item_image/request_for_quotation_with_item_image.json b/erpnext/buying/print_format/request_for_quotation_with_item_image/request_for_quotation_with_item_image.json new file mode 100644 index 00000000000..26f131aec5b --- /dev/null +++ b/erpnext/buying/print_format/request_for_quotation_with_item_image/request_for_quotation_with_item_image.json @@ -0,0 +1,33 @@ +{ + "absolute_value": 0, + "align_labels_right": 0, + "creation": "2026-03-19 15:17:39.094444", + "custom_format": 1, + "default_print_language": "en", + "disabled": 0, + "doc_type": "Request for Quotation", + "docstatus": 0, + "doctype": "Print Format", + "font_size": 14, + "html": "{%- macro add_header(page_num, max_pages, doc, letter_head, no_letterhead, footer, print_settings=None, print_heading_template=None) -%}\n\n{% if letter_head and not no_letterhead %}\n
    {{ letter_head }}
    \n{% endif %}\n{% if print_heading_template %}\n{{ frappe.render_template(print_heading_template, {\"doc\":doc}) }}\n{% endif %}\n{%- endmacro -%}\n\n{% for page in layout %}\n
    \n\t
    \n\t\t{{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead, footer, print_settings) }}\n\t
    \n\t{%- if doc.meta.is_submittable and doc.docstatus==2-%}\n\t\t
    \n\t\t\t

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

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

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

    \n\t\t
    \n\t{%- endif -%}\n\n\t\n\n\t
    \n\t\t\n\t\t\t\n\t\t\t\t\n\n\t\t\t\t\n\t\t\t\n\t\t
    \n\t\t\t\t\t
    \n\t\t\t\t\t\t
    {{ _(\"Supplier Name:\") }}
    \n\t\t\t\t\t\t
    {{ _(\"Shipping Address:\") }}
    \n\t\t\t\t\t
    \n\t\t\t\t\t
    \n\t\t\t\t\t\t
    {{ doc.vendor }}
    \n\t\t\t\t\t\t
    \n \t\t\t\t\t{% if doc.shipping_address %}\n \t\t\t\t\t\t{% set shipping_address = frappe.db.get_value(\"Address\", doc.shipping_address, [\"address_line1\", \"address_line2\", \"city\", \"state\", \"pincode\", \"country\"], as_dict=True) %}\n {{ doc.shipping_address }}
    \n \t\t\t\t\t\t{{ shipping_address.address_line1 or \"\" }}
    \n \t\t\t\t\t\t{% if shipping_address.address_line2 %}{{ shipping_address.address_line2 }}
    {% endif %}\n \t\t\t\t\t\t{{ shipping_address.city or \"\" }} {{ shipping_address.state or \"\" }} {{ shipping_address.pincode or \"\" }} {{ shipping_address.country or \"\" }}
    \n \t\t\t\t\t{% endif %}\n\t\t\t\t\t\t
    \n\n\t\t\t\t\t
    \n\t\t\t\t
    \n\t\t\t\t\t
    \n\t\t\t\t\t\t
    {{ _(\"Order Date:\") }}
    \n\t\t\t\t\t
    \n\t\t\t\t\t
    \n\t\t\t\t\t\t
    {{ frappe.utils.format_date(doc.transaction_date) }}
    \n\t\t\t\t\t
    \n\t\t\t\t\t
    \n\t\t\t\t\t\t
    {{ _(\"Required By:\") }}
    \n\t\t\t\t\t
    \n\t\t\t\t\t
    \n\t\t\t\t\t\t
    {{ frappe.utils.format_date(doc.schedule_date) }}
    \n\t\t\t\t\t
    \n\t\t\t\t
    \n\n\t\t\n\t\t{% set item_naming_by = frappe.db.get_single_value(\"Stock Settings\", \"item_naming_by\") %}\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t{% if item_naming_by != \"Item Code\" %}\n\t\t\t\t\t\t\n\t\t\t\t\t{% endif %}\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t{% for item in doc.items %}\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t{% if item_naming_by != \"Item Code\" %}\n\t\t\t\t\t\t\n\t\t\t\t\t{% endif %}\n\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t{% endfor %}\n\t\t\t\n\t\t
    {{ _(\"No\") }}{{ _(\"Item\") }}{{ _(\"Item Code\") }}{{ _(\"Quantity\") }}
    {{ loop.index }}\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{% if item.image %}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{% endif %}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{{ item.item_name }}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
    \n\t\t\t\t\t
    {{ item.item_code }}{{ item.get_formatted(\"qty\", 0) }} {{ item.uom }}
    \n\n\n\t\t\n\t\t{% if doc.terms %}\n\t\t
    \n\t\t\t
    {{ _(\"Terms and Conditions\") }}
    \n\t\t\t{{ doc.terms}}\n\t\t
    \n\t\t{% endif %}\n\t
    \n
    \n{% endfor %}\n", + "idx": 0, + "line_breaks": 0, + "margin_bottom": 15.0, + "margin_left": 15.0, + "margin_right": 15.0, + "margin_top": 15.0, + "modified": "2026-03-23 14:29:41.591636", + "modified_by": "Administrator", + "module": "Buying", + "name": "Request for Quotation with Item Image", + "owner": "Administrator", + "page_number": "Hide", + "pdf_generator": "wkhtmltopdf", + "print_format_builder": 0, + "print_format_builder_beta": 0, + "print_format_for": "DocType", + "print_format_type": "Jinja", + "raw_printing": 0, + "show_section_headings": 0, + "standard": "Yes" +} diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index febcc596868..27385680b6a 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -4324,6 +4324,8 @@ def get_missing_company_details(doctype, docname): company = frappe.db.get_value(doctype, docname, "company") if doctype in ["Purchase Order", "Purchase Invoice"]: company_address = frappe.db.get_value(doctype, docname, "billing_address") + elif doctype in ["Request for Quotation"]: + company_address = frappe.db.get_value(doctype, docname, "shipping_address") else: company_address = frappe.db.get_value(doctype, docname, "company_address") diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 67ccc4c7fe4..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 @@ -503,11 +503,15 @@ class BuyingController(SubcontractingController): if d.category not in ["Valuation", "Valuation and Total"]: continue + amount = flt(d.base_tax_amount_after_discount_amount) * ( + -1 if d.get("add_deduct_tax") == "Deduct" else 1 + ) + if d.charge_type == "On Net Total": - total_valuation_amount += flt(d.base_tax_amount_after_discount_amount) + total_valuation_amount += amount tax_accounts.append(d.account_head) else: - total_actual_tax_amount += flt(d.base_tax_amount_after_discount_amount) + total_actual_tax_amount += amount return tax_accounts, total_valuation_amount, total_actual_tax_amount diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index b7845bc5698..b2e7337a4ec 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -364,38 +364,43 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs -def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, filters, as_dict): - doctype = "Delivery Note" +def get_delivery_notes_to_be_billed( + doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict, as_dict: bool = False +): + DeliveryNote = frappe.qb.DocType("Delivery Note") + fields = get_fields(doctype, ["name", "customer", "posting_date"]) - return frappe.db.sql( - """ - select {fields} - from `tabDelivery Note` - where `tabDelivery Note`.`{key}` like {txt} and - `tabDelivery Note`.docstatus = 1 - and status not in ('Stopped', 'Closed') {fcond} - and ( - (`tabDelivery Note`.is_return = 0 and `tabDelivery Note`.per_billed < 100) - or (`tabDelivery Note`.grand_total = 0 and `tabDelivery Note`.per_billed < 100) - or ( - `tabDelivery Note`.is_return = 1 - and return_against in (select name from `tabDelivery Note` where per_billed < 100) + original_dn = ( + frappe.qb.from_(DeliveryNote) + .select(DeliveryNote.name) + .where((DeliveryNote.docstatus == 1) & (DeliveryNote.is_return == 0) & (DeliveryNote.per_billed > 0)) + ) + + query = ( + frappe.qb.from_(DeliveryNote) + .select(*[DeliveryNote[f] for f in fields]) + .where( + (DeliveryNote.docstatus == 1) + & (DeliveryNote.status.notin(["Stopped", "Closed"])) + & (DeliveryNote[searchfield].like(f"%{txt}%")) + & ( + ((DeliveryNote.is_return == 0) & (DeliveryNote.per_billed < 100)) + | ((DeliveryNote.grand_total == 0) & (DeliveryNote.per_billed < 100)) + | ( + (DeliveryNote.is_return == 1) + & (DeliveryNote.per_billed < 100) + & (DeliveryNote.return_against.isin(original_dn)) ) ) - {mcond} order by `tabDelivery Note`.`{key}` asc limit {page_len} offset {start} - """.format( - fields=", ".join([f"`tabDelivery Note`.{f}" for f in fields]), - key=searchfield, - fcond=get_filters_cond(doctype, filters, []), - mcond=get_match_cond(doctype), - start=start, - page_len=page_len, - txt="%(txt)s", - ), - {"txt": ("%%%s%%" % txt)}, - as_dict=as_dict, + ) ) + if filters and isinstance(filters, dict): + for key, value in filters.items(): + query = query.where(DeliveryNote[key] == value) + + query = query.orderby(DeliveryNote[searchfield], order=Order.asc).limit(page_len).offset(start) + return query.run(as_dict=as_dict) @frappe.whitelist() @@ -1002,3 +1007,26 @@ def get_item_uom_query(doctype, txt, searchfield, start, page_len, filters): limit_page_length=page_len, as_list=1, ) + + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_warehouse_address(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict): + table = frappe.qb.DocType(doctype) + child_table = frappe.qb.DocType("Dynamic Link") + + query = ( + frappe.qb.from_(table) + .inner_join(child_table) + .on((table.name == child_table.parent) & (child_table.parenttype == doctype)) + .select(table.name) + .where( + (child_table.link_name == filters.get("warehouse")) + & (table.disabled == 0) + & (child_table.link_doctype == "Warehouse") + & (table.name.like(f"%{txt}%")) + ) + .offset(start) + .limit(page_len) + ) + return query.run(as_list=1) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 8138a94693c..5a062e19daf 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -634,11 +634,11 @@ class SellingController(StockController): if allow_at_arms_length_price: continue - rate = flt( - flt(d.incoming_rate, d.precision("incoming_rate")) * d.conversion_factor, - d.precision("rate"), - ) - if d.rate != rate: + rate = flt(flt(d.incoming_rate) * flt(d.conversion_factor or 1.0)) + + if flt(d.rate, d.precision("incoming_rate")) != flt( + rate, d.precision("incoming_rate") + ): d.rate = rate frappe.msgprint( _( 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..21743e57f11 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -164,8 +164,14 @@ 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" + ) + + do_not_round_fields = ["valuation_rate", "incoming_rate"] + for item in self.doc.items: - self.doc.round_floats_in(item) + self.doc.round_floats_in(item, do_not_round_fields=do_not_round_fields) if item.discount_percentage == 100: item.rate = 0.0 @@ -225,7 +231,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 +297,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 +391,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 +547,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 +622,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 +828,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_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 6bb9b2f91fc..021e034a70d 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -1567,25 +1567,10 @@ class TestAccountsController(ERPNextTestSuite): frappe.db.set_value("Company", self.company, "cost_center", cc) - def setup_dimensions(self): - # create dimension - from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( - create_dimension, - ) - - create_dimension() - # make it non-mandatory - loc = frappe.get_doc("Accounting Dimension", "Location") - for x in loc.dimension_defaults: - x.mandatory_for_bs = False - x.mandatory_for_pl = False - loc.save() - def test_90_dimensions_filter(self): """ Test workings of dimension filters """ - self.setup_dimensions() rate_in_account_currency = 1 # Invoices @@ -1653,7 +1638,6 @@ class TestAccountsController(ERPNextTestSuite): self.assertEqual(len(pr.payments), 1) def test_91_cr_note_should_inherit_dimension(self): - self.setup_dimensions() rate_in_account_currency = 1 # Invoice @@ -1698,7 +1682,6 @@ class TestAccountsController(ERPNextTestSuite): def test_92_dimension_inhertiance_exc_gain_loss(self): # Sales Invoice in Foreign Currency - self.setup_dimensions() rate_in_account_currency = 1 dpt = "Research & Development - _TC" @@ -1734,7 +1717,6 @@ class TestAccountsController(ERPNextTestSuite): ) def test_93_dimension_inheritance_on_advance(self): - self.setup_dimensions() dpt = "Research & Development - _TC" adv = self.create_payment_entry(amount=1, source_exc_rate=85) diff --git a/erpnext/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/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index b41064ce9e3..9a213aea5fc 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -120,7 +120,8 @@ class Appointment(Document): self.auto_assign() self.create_calendar_event() self.save(ignore_permissions=True) - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() def create_lead_and_link(self): # Return if already linked diff --git a/erpnext/crm/doctype/contract_template/contract_template.json b/erpnext/crm/doctype/contract_template/contract_template.json index 223464d3eb8..baa6b289005 100644 --- a/erpnext/crm/doctype/contract_template/contract_template.json +++ b/erpnext/crm/doctype/contract_template/contract_template.json @@ -56,7 +56,7 @@ } ], "links": [], - "modified": "2024-03-27 13:06:46.495091", + "modified": "2026-03-25 19:27:19.162421", "modified_by": "Administrator", "module": "CRM", "name": "Contract Template", @@ -75,44 +75,36 @@ "write": 1 }, { - "create": 1, - "delete": 1, "email": 1, "export": 1, "print": 1, "read": 1, "report": 1, "role": "Sales Manager", - "share": 1, - "write": 1 + "share": 1 }, { - "create": 1, - "delete": 1, "email": 1, "export": 1, "print": 1, "read": 1, "report": 1, "role": "Purchase Manager", - "share": 1, - "write": 1 + "share": 1 }, { - "create": 1, - "delete": 1, "email": 1, "export": 1, "print": 1, "read": 1, "report": 1, "role": "HR Manager", - "share": 1, - "write": 1 + "share": 1 } ], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py index 9e24a26caa8..4454ede5310 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -204,8 +204,22 @@ def send_mail(entry, email_campaign): # called from hooks on doc_event Email Unsubscribe def unsubscribe_recipient(unsubscribe, method): - if unsubscribe.reference_doctype == "Email Campaign": - frappe.db.set_value("Email Campaign", unsubscribe.reference_name, "status", "Unsubscribed") + if unsubscribe.reference_doctype != "Email Campaign": + return + + email_campaign = frappe.get_doc("Email Campaign", unsubscribe.reference_name) + + if email_campaign.email_campaign_for == "Email Group": + if unsubscribe.email: + frappe.db.set_value( + "Email Group Member", + {"email_group": email_campaign.recipient, "email": unsubscribe.email}, + "unsubscribed", + 1, + ) + else: + # For Lead or Contact + frappe.db.set_value("Email Campaign", email_campaign.name, "status", "Unsubscribed") # called through hooks to update email campaign status daily diff --git a/erpnext/crm/report/lost_opportunity/lost_opportunity.py b/erpnext/crm/report/lost_opportunity/lost_opportunity.py index eb09711667a..cfbee3901e2 100644 --- a/erpnext/crm/report/lost_opportunity/lost_opportunity.py +++ b/erpnext/crm/report/lost_opportunity/lost_opportunity.py @@ -4,6 +4,12 @@ import frappe from frappe import _ +from frappe.query_builder import DocType +from frappe.query_builder.custom import GROUP_CONCAT +from frappe.query_builder.functions import Date + +Opportunity = DocType("Opportunity") +OpportunityLostReasonDetail = DocType("Opportunity Lost Reason Detail") def execute(filters=None): @@ -66,58 +72,48 @@ def get_columns(): def get_data(filters): - return frappe.db.sql( - f""" - SELECT - `tabOpportunity`.name, - `tabOpportunity`.opportunity_from, - `tabOpportunity`.party_name, - `tabOpportunity`.customer_name, - `tabOpportunity`.opportunity_type, - GROUP_CONCAT(`tabOpportunity Lost Reason Detail`.lost_reason separator ', ') lost_reason, - `tabOpportunity`.sales_stage, - `tabOpportunity`.territory - FROM - `tabOpportunity` - {get_join(filters)} - WHERE - `tabOpportunity`.status = 'Lost' and `tabOpportunity`.company = %(company)s - AND DATE(`tabOpportunity`.modified) BETWEEN %(from_date)s AND %(to_date)s - {get_conditions(filters)} - GROUP BY - `tabOpportunity`.name - ORDER BY - `tabOpportunity`.creation asc """, - filters, - as_dict=1, + query = ( + frappe.qb.from_(Opportunity) + .left_join(OpportunityLostReasonDetail) + .on( + (OpportunityLostReasonDetail.parenttype == "Opportunity") + & (OpportunityLostReasonDetail.parent == Opportunity.name) + ) + .select( + Opportunity.name, + Opportunity.opportunity_from, + Opportunity.party_name, + Opportunity.customer_name, + Opportunity.opportunity_type, + GROUP_CONCAT(OpportunityLostReasonDetail.lost_reason, alias="lost_reason").separator(", "), + Opportunity.sales_stage, + Opportunity.territory, + ) + .where( + (Opportunity.status == "Lost") + & (Opportunity.company == filters.get("company")) + & (Date(Opportunity.modified).between(filters.get("from_date"), filters.get("to_date"))) + ) + .groupby(Opportunity.name) + .orderby(Opportunity.creation) ) + query = get_conditions(filters, query) -def get_conditions(filters): - conditions = [] + return query.run(as_dict=1) + +def get_conditions(filters, query): if filters.get("territory"): - conditions.append(" and `tabOpportunity`.territory=%(territory)s") + query = query.where(Opportunity.territory == filters.get("territory")) if filters.get("opportunity_from"): - conditions.append(" and `tabOpportunity`.opportunity_from=%(opportunity_from)s") + query = query.where(Opportunity.opportunity_from == filters.get("opportunity_from")) if filters.get("party_name"): - conditions.append(" and `tabOpportunity`.party_name=%(party_name)s") - - return " ".join(conditions) if conditions else "" - - -def get_join(filters): - join = """LEFT JOIN `tabOpportunity Lost Reason Detail` - ON `tabOpportunity Lost Reason Detail`.parenttype = 'Opportunity' and - `tabOpportunity Lost Reason Detail`.parent = `tabOpportunity`.name""" + query = query.where(Opportunity.party_name == filters.get("party_name")) if filters.get("lost_reason"): - join = """JOIN `tabOpportunity Lost Reason Detail` - ON `tabOpportunity Lost Reason Detail`.parenttype = 'Opportunity' and - `tabOpportunity Lost Reason Detail`.parent = `tabOpportunity`.name and - `tabOpportunity Lost Reason Detail`.lost_reason = '{}' - """.format(filters.get("lost_reason")) + query = query.where(OpportunityLostReasonDetail.lost_reason == filters.get("lost_reason")) - return join + return query 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/hooks.py b/erpnext/hooks.py index c997443b41d..cc4a08d8b67 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -62,7 +62,6 @@ welcome_email = "erpnext.setup.utils.welcome_email" # setup wizard setup_wizard_requires = "assets/erpnext/js/setup_wizard.js" setup_wizard_stages = "erpnext.setup.setup_wizard.setup_wizard.get_setup_stages" -setup_wizard_complete = "erpnext.setup.setup_wizard.setup_wizard.setup_demo" after_install = "erpnext.setup.install.after_install" diff --git a/erpnext/locale/main.pot b/erpnext/locale/main.pot index f627ccf1b41..06178e5e76e 100644 --- a/erpnext/locale/main.pot +++ b/erpnext/locale/main.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: ERPNext VERSION\n" "Report-Msgid-Bugs-To: hello@frappe.io\n" -"POT-Creation-Date: 2026-03-22 09:44+0000\n" -"PO-Revision-Date: 2026-03-22 09:44+0000\n" +"POT-Creation-Date: 2026-04-12 09:47+0000\n" +"PO-Revision-Date: 2026-04-12 09:47+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:1591 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:1019 #, python-format msgid "% Finished Item Quantity" msgstr "" @@ -262,7 +267,7 @@ msgstr "" msgid "% of materials delivered against this Sales Order" msgstr "" -#: erpnext/controllers/accounts_controller.py:2371 +#: erpnext/controllers/accounts_controller.py:2381 msgid "'Account' in the Accounting section of Customer {0}" msgstr "" @@ -278,11 +283,11 @@ msgstr "" msgid "'Days Since Last Order' must be greater than or equal to zero" msgstr "" -#: erpnext/controllers/accounts_controller.py:2376 +#: erpnext/controllers/accounts_controller.py:2386 msgid "'Default {0} Account' in Company {1}" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1226 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1229 msgid "'Entries' cannot be empty" msgstr "" @@ -865,11 +870,6 @@ msgstr "" msgid "Masters & Reports" msgstr "" -#. Header text in the Selling Workspace -#: erpnext/selling/workspace/selling/selling.json -msgid "Quick Access" -msgstr "" - #. Header text in the Invoicing Workspace #. Header text in the Assets Workspace #. Header text in the Buying Workspace @@ -915,11 +915,11 @@ msgstr "" msgid "Your Shortcuts" msgstr "" -#: erpnext/accounts/doctype/payment_request/payment_request.py:1133 +#: erpnext/accounts/doctype/payment_request/payment_request.py:1134 msgid "Grand Total: {0}" msgstr "" -#: erpnext/accounts/doctype/payment_request/payment_request.py:1134 +#: erpnext/accounts/doctype/payment_request/payment_request.py:1135 msgid "Outstanding Amount: {0}" msgstr "" @@ -974,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 "" @@ -1004,7 +1004,7 @@ msgstr "" msgid "A Reconciliation Job {0} is running for the same filters. Cannot reconcile now" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1760 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1763 msgid "A Reverse Journal Entry {0} already exists for this Journal Entry." msgstr "" @@ -1170,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 "" @@ -1182,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 "" @@ -1216,7 +1216,7 @@ msgstr "" msgid "According to CEFACT/ICG/2010/IC013 or CEFACT/ICG/2010/IC010" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:988 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1080 msgid "According to the BOM {0}, the Item '{1}' is missing in the stock entry." msgstr "" @@ -1229,7 +1229,7 @@ msgstr "" #. Name of a DocType #. Label of a Workspace Sidebar Item #: erpnext/accounts/doctype/account/account.json -#: erpnext/accounts/doctype/account/account_tree.js:167 +#: erpnext/accounts/doctype/account/account_tree.js:162 #: erpnext/accounts/doctype/account_category/account_category.json #: erpnext/workspace_sidebar/accounts_setup.json msgid "Account Category" @@ -1335,7 +1335,7 @@ msgid "Account Manager" msgstr "" #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1007 -#: erpnext/controllers/accounts_controller.py:2380 +#: erpnext/controllers/accounts_controller.py:2390 msgid "Account Missing" msgstr "" @@ -1359,7 +1359,7 @@ msgstr "" #. Label of the account_number (Data) field in DocType 'Account' #: erpnext/accounts/doctype/account/account.json -#: erpnext/accounts/doctype/account/account_tree.js:133 +#: erpnext/accounts/doctype/account/account_tree.js:128 #: erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.py:396 #: erpnext/accounts/report/financial_statements.py:685 #: erpnext/accounts/report/trial_balance/trial_balance.py:495 @@ -1406,7 +1406,7 @@ msgstr "" #. Label of the account_type (Select) field in DocType 'Party Type' #: erpnext/accounts/doctype/account/account.json #: erpnext/accounts/doctype/account/account.py:206 -#: erpnext/accounts/doctype/account/account_tree.js:159 +#: erpnext/accounts/doctype/account/account_tree.js:154 #: erpnext/accounts/doctype/bank_account/bank_account.json #: erpnext/accounts/doctype/bank_account_type/bank_account_type.json #: erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json @@ -1560,7 +1560,7 @@ msgstr "" msgid "Account: {0} is capital Work in progress and can not be updated by Journal Entry" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:368 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:371 msgid "Account: {0} can only be updated via Stock Transactions" msgstr "" @@ -1568,7 +1568,7 @@ msgstr "" msgid "Account: {0} is not permitted under Payment Entry" msgstr "" -#: erpnext/controllers/accounts_controller.py:3269 +#: erpnext/controllers/accounts_controller.py:3279 msgid "Account: {0} with currency: {1} can not be selected" msgstr "" @@ -1853,42 +1853,42 @@ 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:2056 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2076 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 "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:840 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:843 msgid "Accounting Entry for Service" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1016 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1037 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1055 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1076 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1097 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1125 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1232 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1468 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1490 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1017 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1038 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1056 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1077 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1098 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1126 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1238 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1474 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1496 #: erpnext/controllers/stock_controller.py:727 #: erpnext/controllers/stock_controller.py:744 -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:933 -#: 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/purchase_receipt/purchase_receipt.py:936 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2001 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2015 +#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:745 msgid "Accounting Entry for Stock" msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:737 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:740 msgid "Accounting Entry for {0}" msgstr "" -#: erpnext/controllers/accounts_controller.py:2421 +#: erpnext/controllers/accounts_controller.py:2431 msgid "Accounting Entry for {0}: {1} can only be made in currency: {2}" msgstr "" @@ -1961,7 +1961,7 @@ msgstr "" #: erpnext/setup/doctype/email_digest/email_digest.json #: erpnext/setup/doctype/incoterm/incoterm.json #: erpnext/setup/doctype/supplier_group/supplier_group.json -#: erpnext/setup/install.py:368 +#: erpnext/setup/install.py:369 msgid "Accounts" msgstr "" @@ -1978,12 +1978,12 @@ msgstr "" msgid "Accounts Frozen Till Date" msgstr "" -#: erpnext/accounts/doctype/financial_report_template/financial_report_template.js:192 +#: erpnext/accounts/doctype/financial_report_template/financial_report_template.js:186 msgid "Accounts Included in Report" msgstr "" -#: erpnext/accounts/doctype/financial_report_template/financial_report_template.js:166 -#: erpnext/accounts/doctype/financial_report_template/financial_report_template.js:191 +#: erpnext/accounts/doctype/financial_report_template/financial_report_template.js:160 +#: erpnext/accounts/doctype/financial_report_template/financial_report_template.js:185 msgid "Accounts Missing from Report" msgstr "" @@ -2083,7 +2083,7 @@ msgstr "" msgid "Accounts Setup" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1325 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1328 msgid "Accounts table cannot be blank." msgstr "" @@ -2414,7 +2414,7 @@ msgstr "" msgid "Actual Operation Time" msgstr "" -#: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py:430 +#: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py:456 msgid "Actual Posting" msgstr "" @@ -2518,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 "" @@ -2547,7 +2547,7 @@ msgid "Add Employees" msgstr "" #: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:256 -#: erpnext/selling/doctype/sales_order/sales_order.js:277 +#: erpnext/selling/doctype/sales_order/sales_order.js:284 #: erpnext/stock/dashboard/item_dashboard.js:216 msgid "Add Item" msgstr "" @@ -2609,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:1047 #: erpnext/manufacturing/doctype/bom_operation/bom_operation.json msgid "Add Raw Materials" msgstr "" @@ -2623,7 +2623,7 @@ msgid "Add Sales Partners" msgstr "" #. Label of the add_schedule (Button) field in DocType 'Sales Order Item' -#: erpnext/selling/doctype/sales_order/sales_order.js:649 +#: erpnext/selling/doctype/sales_order/sales_order.js:656 #: erpnext/selling/doctype/sales_order_item/sales_order_item.json msgid "Add Schedule" msgstr "" @@ -2670,7 +2670,7 @@ msgstr "" msgid "Add Sub Assembly" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:516 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:517 #: erpnext/public/js/event.js:32 msgid "Add Suppliers" msgstr "" @@ -2694,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 "" @@ -2885,7 +2885,7 @@ msgstr "" msgid "Additional Discount Amount (Company Currency)" msgstr "" -#: erpnext/controllers/taxes_and_totals.py:796 +#: erpnext/controllers/taxes_and_totals.py:837 msgid "Additional Discount Amount ({discount_amount}) cannot exceed the total before such discount ({total_before_discount})" msgstr "" @@ -2922,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' @@ -2965,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:818 msgid "Additional Material Transfer" msgstr "" @@ -3002,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 "" @@ -3015,6 +3030,8 @@ msgstr "" #. Invoice' #. Label of the address_and_contact_tab (Tab Break) field in DocType 'Purchase #. Order' +#. Label of the address_and_contact_tab (Tab Break) field in DocType 'Request +#. for Quotation' #. Label of the contact_and_address_tab (Tab Break) field in DocType 'Supplier' #. Label of the address_and_contact_tab (Tab Break) field in DocType 'Supplier #. Quotation' @@ -3035,6 +3052,7 @@ msgstr "" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/buying/doctype/purchase_order/purchase_order.json +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.json #: erpnext/buying/doctype/supplier/supplier.json #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.json #: erpnext/crm/doctype/opportunity/opportunity.json @@ -3149,11 +3167,11 @@ msgstr "" msgid "Adjust Qty" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1130 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1156 msgid "Adjustment Against" msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:665 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:668 msgid "Adjustment based on Purchase Invoice rate" msgstr "" @@ -3266,11 +3284,11 @@ msgstr "" msgid "Advance amount" msgstr "" -#: erpnext/controllers/taxes_and_totals.py:933 +#: erpnext/controllers/taxes_and_totals.py:974 msgid "Advance amount cannot be greater than {0} {1}" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:873 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:876 msgid "Advance paid against {0} {1} cannot be greater than Grand Total {2}" msgstr "" @@ -3310,12 +3328,6 @@ msgstr "" msgid "Aerospace" msgstr "" -#. Label of the affected_transactions (Code) field in DocType 'Repost Item -#. Valuation' -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json -msgid "Affected Transactions" -msgstr "" - #. Label of the against (Text) field in DocType 'GL Entry' #: erpnext/accounts/doctype/gl_entry/gl_entry.json #: erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.html:20 @@ -3398,7 +3410,7 @@ msgstr "" msgid "Against Income Account" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:735 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:738 #: erpnext/accounts/doctype/payment_entry/payment_entry.py:777 msgid "Against Journal Entry {0} does not have any unmatched {1} entry" msgstr "" @@ -3440,7 +3452,7 @@ msgstr "" msgid "Against Stock Entry" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:332 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:331 msgid "Against Supplier Invoice {0}" msgstr "" @@ -3488,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 "" @@ -3584,7 +3596,7 @@ msgstr "" #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js:165 #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js:185 #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:166 -#: erpnext/accounts/utils.py:1555 erpnext/public/js/setup_wizard.js:184 +#: erpnext/accounts/utils.py:1631 erpnext/public/js/setup_wizard.js:184 msgid "All Accounts" msgstr "" @@ -3608,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 "" @@ -3741,11 +3753,11 @@ msgstr "" msgid "All communications including and above this shall be moved into the new Issue" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:967 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:972 msgid "All items are already requested" msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:1426 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:1487 msgid "All items have already been Invoiced/Returned" msgstr "" @@ -3753,19 +3765,19 @@ 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:3335 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 "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1239 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1236 msgid "All items must be linked to a Sales Order or Subcontracting Inward Order for this Sales Invoice." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1250 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1247 msgid "All linked Sales Orders must be subcontracted." msgstr "" @@ -3779,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:1256 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 "" @@ -3986,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 "" @@ -4013,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 "" @@ -4316,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 "" @@ -4332,11 +4334,11 @@ 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/stock/doctype/stock_entry/stock_entry.js:288 +#: erpnext/public/js/utils.js:587 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:322 msgid "Alternate Item" msgstr "" @@ -4437,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' @@ -4510,6 +4511,7 @@ msgstr "" #: erpnext/accounts/print_format/sales_invoice_print/sales_invoice_print.html:93 #: erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py:48 #: erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py:79 +#: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py:411 #: erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py:44 #: erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py:273 #: erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py:327 @@ -4531,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 @@ -4727,8 +4728,8 @@ msgstr "" msgid "Ampere-Second" msgstr "" -#: erpnext/controllers/trends.py:269 erpnext/controllers/trends.py:281 -#: erpnext/controllers/trends.py:290 +#: erpnext/controllers/trends.py:277 erpnext/controllers/trends.py:289 +#: erpnext/controllers/trends.py:298 msgid "Amt" msgstr "" @@ -4737,7 +4738,7 @@ msgstr "" msgid "An Item Group is a way to classify items based on types." msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:535 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:558 msgid "An error has been appeared while reposting item valuation via {0}" msgstr "" @@ -4764,7 +4765,7 @@ msgstr "" msgid "Analytical Accounting" msgstr "" -#: erpnext/public/js/utils.js:164 +#: erpnext/public/js/utils.js:184 msgid "Annual Billing: {0}" msgstr "" @@ -4803,7 +4804,7 @@ msgstr "" msgid "Another Cost Center Allocation record {0} applicable from {1}, hence this allocation will be applicable upto {2}" msgstr "" -#: erpnext/accounts/doctype/payment_request/payment_request.py:883 +#: erpnext/accounts/doctype/payment_request/payment_request.py:884 msgid "Another Payment Request is already processed" msgstr "" @@ -5238,20 +5239,20 @@ 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 "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1087 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1092 msgid "As there are sufficient Sub Assembly Items, Work Order is not required for Warehouse {0}." msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1827 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py: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 "" @@ -5664,7 +5665,7 @@ msgstr "" msgid "Asset cannot be cancelled, as it is already {0}" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:393 +#: erpnext/assets/doctype/asset/depreciation.py:396 msgid "Asset cannot be scrapped before the last depreciation entry." msgstr "" @@ -5696,7 +5697,7 @@ msgstr "" msgid "Asset received at Location {0} and issued to Employee {1}" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:454 +#: erpnext/assets/doctype/asset/depreciation.py:457 msgid "Asset restored" msgstr "" @@ -5704,20 +5705,20 @@ msgstr "" msgid "Asset restored after Asset Capitalization {0} was cancelled" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1522 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1519 msgid "Asset returned" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:441 +#: erpnext/assets/doctype/asset/depreciation.py:444 msgid "Asset scrapped" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:443 +#: erpnext/assets/doctype/asset/depreciation.py:446 msgid "Asset scrapped via Journal Entry {0}" msgstr "" +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1519 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1522 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1525 msgid "Asset sold" msgstr "" @@ -5737,7 +5738,7 @@ msgstr "" msgid "Asset updated due to Asset Repair {0} {1}." msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:375 +#: erpnext/assets/doctype/asset/depreciation.py:378 msgid "Asset {0} cannot be scrapped, as it is already {1}" msgstr "" @@ -5778,11 +5779,11 @@ msgstr "" msgid "Asset {0} is not submitted. Please submit the asset before proceeding." msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:373 +#: erpnext/assets/doctype/asset/depreciation.py:376 msgid "Asset {0} must be submitted" msgstr "" -#: erpnext/controllers/buying_controller.py:1030 +#: erpnext/controllers/buying_controller.py:1034 msgid "Asset {assets_link} created for {item_code}" msgstr "" @@ -5820,15 +5821,15 @@ 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 "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:232 +#: erpnext/manufacturing/doctype/job_card/job_card.js:240 msgid "Assign Job to Employee" msgstr "" @@ -5848,15 +5849,15 @@ 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 "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1403 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1436 msgid "At Row {0}: In Serial and Batch Bundle {1} must have docstatus as 1 and not 0" msgstr "" @@ -5889,19 +5890,19 @@ msgstr "" msgid "At least one of the Selling or Buying must be selected" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:316 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:324 msgid "At least one raw material item must be present in the stock entry for the type {0}" msgstr "" -#: erpnext/accounts/doctype/financial_report_template/financial_report_template.js:25 +#: erpnext/accounts/doctype/financial_report_template/financial_report_template.js:27 msgid "At least one row is required for a financial report template" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:819 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:881 msgid "At least one warehouse is mandatory" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:721 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:783 msgid "At row #{0}: the Difference Account must not be a Stock type account, please change the Account Type for the account {1} or select a different account" msgstr "" @@ -5909,11 +5910,11 @@ msgstr "" msgid "At row #{0}: the sequence id {1} cannot be less than previous row sequence id {2}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:732 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:794 msgid "At row #{0}: you have selected the Difference Account {1}, which is a Cost of Goods Sold type account. Please select a different account" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1151 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1184 msgid "At row {0}: Batch No is mandatory for Item {1}" msgstr "" @@ -5921,11 +5922,11 @@ msgstr "" msgid "At row {0}: Parent Row No cannot be set for item {1}" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1136 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1169 msgid "At row {0}: Qty is mandatory for the batch {1}" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1143 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1176 msgid "At row {0}: Serial No is mandatory for Item {1}" msgstr "" @@ -6191,10 +6192,14 @@ 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 "" +#: erpnext/setup/doctype/employee/employee.py:170 +msgid "Auto User Creation Error" +msgstr "" + #. Description of the 'Close Replied Opportunity After Days' (Int) field in #. DocType 'CRM Settings' #: erpnext/crm/doctype/crm_settings/crm_settings.json @@ -6312,11 +6317,12 @@ msgstr "" #. Label of the available_quantity_section (Section Break) field in DocType #. 'Pick List Item' #: erpnext/manufacturing/doctype/workstation/workstation.js:505 -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py:88 -#: erpnext/public/js/utils.js:627 +#: 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:647 #: 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 "" @@ -6405,7 +6411,7 @@ msgstr "" msgid "Available for use date is required" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:952 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1044 msgid "Available quantity is {0}, you need {1}" msgstr "" @@ -6417,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 "" @@ -6530,14 +6536,13 @@ msgstr "" #: erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json #: erpnext/manufacturing/report/bom_explorer/bom_explorer.js:8 #: erpnext/manufacturing/report/bom_explorer/bom_explorer.py:67 -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js:8 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js:5 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js:8 #: erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py:109 #: erpnext/manufacturing/workspace/manufacturing/manufacturing.json -#: erpnext/selling/doctype/sales_order/sales_order.js:1415 +#: erpnext/selling/doctype/sales_order/sales_order.js:1422 #: erpnext/stock/doctype/material_request/material_request.js:351 #: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json -#: erpnext/stock/doctype/stock_entry/stock_entry.js:709 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:796 #: erpnext/stock/report/bom_search/bom_search.py:38 #: erpnext/subcontracting/doctype/subcontracting_inward_order_item/subcontracting_inward_order_item.json #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:525 @@ -6551,7 +6556,7 @@ msgstr "" msgid "BOM 1" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1757 +#: erpnext/manufacturing/doctype/bom/bom.py:1806 msgid "BOM 1 {0} and BOM 2 {1} should not be same" msgstr "" @@ -6692,19 +6697,10 @@ msgstr "" msgid "BOM Operations Time" msgstr "" -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py:26 -msgid "BOM Qty" -msgstr "" - #: erpnext/stock/report/item_prices/item_prices.py:60 msgid "BOM Rate" msgstr "" -#. 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 @@ -6714,16 +6710,26 @@ msgstr "" msgid "BOM Search" msgstr "" -#. Name of a report -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.json -msgid "BOM Stock Calculated" +#. 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" +msgstr "" + #. Label of a Link in the Manufacturing Workspace #. Label of a Workspace Sidebar Item -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html:1 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.json #: erpnext/manufacturing/workspace/manufacturing/manufacturing.json #: erpnext/workspace_sidebar/manufacturing.json msgid "BOM Stock Report" @@ -6734,10 +6740,6 @@ msgstr "" msgid "BOM Tree" msgstr "" -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py:27 -msgid "BOM UOM" -msgstr "" - #. Name of a DocType #: erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json msgid "BOM Update Batch" @@ -6789,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:2448 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 @@ -6804,7 +6802,7 @@ msgid "BOM and Production" msgstr "" #: erpnext/stock/doctype/material_request/material_request.js:386 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:761 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:848 msgid "BOM does not contain any stock item" msgstr "" @@ -6812,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:1492 +#: erpnext/manufacturing/doctype/bom/bom.py:1540 msgid "BOM {0} does not belong to Item {1}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1474 +#: erpnext/manufacturing/doctype/bom/bom.py:1522 msgid "BOM {0} must be active" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1477 +#: 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 "" @@ -6837,15 +6835,15 @@ msgstr "" msgid "BOMs Updated" msgstr "" -#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:297 +#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:299 msgid "BOMs created successfully" msgstr "" -#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:307 +#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:309 msgid "BOMs creation failed" msgstr "" -#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:247 +#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:249 msgid "BOMs creation has been enqueued, kindly check the status after some time" msgstr "" @@ -6861,7 +6859,7 @@ msgstr "" #. Order Operation' #: erpnext/manufacturing/doctype/bom_operation/bom_operation.json #: erpnext/manufacturing/doctype/job_card/job_card.json -#: erpnext/manufacturing/doctype/work_order/work_order.js:368 +#: erpnext/manufacturing/doctype/work_order/work_order.js:371 #: erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json msgid "Backflush Materials From WIP Warehouse" msgstr "" @@ -7370,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" @@ -7458,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 "" @@ -7558,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 @@ -7589,11 +7585,11 @@ msgstr "" msgid "Batch No" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1154 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1187 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:3468 msgid "Batch No {0} does not exists" msgstr "" @@ -7601,7 +7597,7 @@ msgstr "" msgid "Batch No {0} is linked with Item {1} which has serial no. Please scan serial no instead." msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:458 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:491 msgid "Batch No {0} is not present in the original {1} {2}, hence you can't return it against the {1} {2}" msgstr "" @@ -7616,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:2009 msgid "Batch Nos are created successfully" msgstr "" @@ -7653,7 +7649,7 @@ msgstr "" #. Label of the batch_size (Float) field in DocType 'Work Order Operation' #: erpnext/manufacturing/doctype/bom_operation/bom_operation.json #: erpnext/manufacturing/doctype/operation/operation.json -#: erpnext/manufacturing/doctype/work_order/work_order.js:350 +#: erpnext/manufacturing/doctype/work_order/work_order.js:353 #: erpnext/manufacturing/doctype/work_order/work_order.json #: erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json msgid "Batch Size" @@ -7682,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:3519 #: 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:3525 msgid "Batch {0} of Item {1} is disabled." msgstr "" @@ -7754,10 +7750,10 @@ msgstr "" #. Label of a Card Break in the Manufacturing Workspace #. Label of a Link in the Manufacturing Workspace #. Label of a Workspace Sidebar Item -#: erpnext/manufacturing/doctype/bom/bom.py:1324 +#: erpnext/manufacturing/doctype/bom/bom.py: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 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:782 #: erpnext/workspace_sidebar/subcontracting.json msgid "Bill of Materials" msgstr "" @@ -8193,10 +8189,6 @@ msgstr "" msgid "Booked Fixed Asset" msgstr "" -#: erpnext/stock/doctype/warehouse/warehouse.py:146 -msgid "Booking stock value across multiple accounts will make it harder to track stock and account value." -msgstr "" - #: erpnext/accounts/general_ledger.py:828 msgid "Books have been closed till the period ending on {0}" msgstr "" @@ -8283,7 +8275,7 @@ msgstr "" msgid "Brokerage" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:193 +#: erpnext/manufacturing/doctype/bom/bom.js:231 msgid "Browse BOM" msgstr "" @@ -8591,6 +8583,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 @@ -8900,7 +8907,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:2554 msgid "Can not close Work Order. Since {0} Job Cards are in Work In Progress state." msgstr "" @@ -8928,19 +8935,19 @@ msgstr "" msgid "Can not filter based on Voucher No, if grouped by Voucher" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1384 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1387 #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2874 msgid "Can only make payment against unbilled {0}" msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1517 -#: erpnext/controllers/accounts_controller.py:3178 +#: erpnext/controllers/accounts_controller.py:3188 #: erpnext/public/js/controllers/accounts.js:103 msgid "Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'" msgstr "" #: 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 "" @@ -8976,7 +8983,7 @@ msgstr "" msgid "Cannot Assign Cashier" msgstr "" -#: erpnext/stock/doctype/delivery_trip/delivery_trip.js:90 +#: erpnext/stock/doctype/delivery_trip/delivery_trip.js:92 #: erpnext/stock/doctype/delivery_trip/delivery_trip.py:219 msgid "Cannot Calculate Arrival Time as Driver Address is Missing." msgstr "" @@ -8995,11 +9002,11 @@ msgstr "" msgid "Cannot Merge" msgstr "" -#: erpnext/stock/doctype/delivery_trip/delivery_trip.js:123 +#: erpnext/stock/doctype/delivery_trip/delivery_trip.js:125 msgid "Cannot Optimize Route as Driver Address is Missing." msgstr "" -#: erpnext/setup/doctype/employee/employee.py:181 +#: erpnext/setup/doctype/employee/employee.py:295 msgid "Cannot Relieve Employee" msgstr "" @@ -9015,7 +9022,7 @@ msgstr "" msgid "Cannot amend {0} {1}, please create a new one instead." msgstr "" -#: erpnext/accounts/doctype/tax_withholding_entry/tax_withholding_entry.py:1293 +#: erpnext/accounts/doctype/tax_withholding_entry/tax_withholding_entry.py:1297 msgid "Cannot apply TDS against multiple parties in one entry" msgstr "" @@ -9035,7 +9042,7 @@ msgstr "" msgid "Cannot cancel Stock Reservation Entry {0}, as it has used in the work order {1}. Please cancel the work order first or unreserved the stock" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:252 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:273 msgid "Cannot cancel as processing of cancelled documents is pending." msgstr "" @@ -9043,23 +9050,23 @@ msgstr "" msgid "Cannot cancel because submitted Stock Entry {0} exists" msgstr "" -#: erpnext/stock/stock_ledger.py:209 +#: erpnext/stock/stock_ledger.py:179 msgid "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet." msgstr "" -#: 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 "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:575 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:578 msgid "Cannot cancel this document as it is linked with the submitted Asset Value Adjustment {0}. Please cancel the Asset Value Adjustment to continue." msgstr "" -#: erpnext/controllers/buying_controller.py:1136 +#: erpnext/controllers/buying_controller.py:1141 msgid "Cannot cancel this document as it is linked with the submitted asset {asset_link}. Please cancel the asset to continue." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:496 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:558 msgid "Cannot cancel transaction for Completed Work Order." msgstr "" @@ -9103,12 +9110,12 @@ msgstr "" msgid "Cannot covert to Group because Account Type is selected." msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:1014 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:1017 msgid "Cannot create Stock Reservation Entries for future dated Purchase Receipts." 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 "" @@ -9120,7 +9127,7 @@ msgstr "" msgid "Cannot create return for consolidated invoice {0}." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1175 +#: erpnext/manufacturing/doctype/bom/bom.py:1210 msgid "Cannot deactivate or cancel BOM as it is linked with other BOMs" msgstr "" @@ -9141,7 +9148,7 @@ msgstr "" msgid "Cannot delete Serial No {0}, as it is used in stock transactions" msgstr "" -#: erpnext/controllers/accounts_controller.py:3774 +#: erpnext/controllers/accounts_controller.py:3784 msgid "Cannot delete an item which has been ordered" msgstr "" @@ -9154,7 +9161,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 "" @@ -9162,7 +9169,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 "" @@ -9170,6 +9177,10 @@ msgstr "" msgid "Cannot disassemble more than produced quantity." msgstr "" +#: erpnext/stock/doctype/stock_entry/stock_entry.py:924 +msgid "Cannot disassemble {0} qty against Stock Entry {1}. Only {2} qty available to disassemble." +msgstr "" + #: erpnext/setup/doctype/company/company.py:223 msgid "Cannot enable Item-wise Inventory Account, as there are existing Stock Ledger Entries for the company {0} with Warehouse-wise Inventory Account. Please cancel the stock transactions first and try again." msgstr "" @@ -9191,7 +9202,7 @@ msgstr "" msgid "Cannot find Item with this Barcode" msgstr "" -#: erpnext/controllers/accounts_controller.py:3726 +#: erpnext/controllers/accounts_controller.py:3736 msgid "Cannot find a default warehouse for item {0}. Please set one in the Item Master or in Stock Settings." msgstr "" @@ -9203,11 +9214,11 @@ msgstr "" msgid "Cannot produce more Item {0} than Sales Order quantity {1} {2}" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:1450 +#: erpnext/manufacturing/doctype/work_order/work_order.py:1451 msgid "Cannot produce more item for {0}" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:1454 +#: erpnext/manufacturing/doctype/work_order/work_order.py:1455 msgid "Cannot produce more than {0} items for {1}" msgstr "" @@ -9215,12 +9226,12 @@ msgstr "" msgid "Cannot receive from customer against negative outstanding" msgstr "" -#: erpnext/controllers/accounts_controller.py:3922 +#: erpnext/controllers/accounts_controller.py:3932 msgid "Cannot reduce quantity than ordered or purchased quantity" msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1530 -#: erpnext/controllers/accounts_controller.py:3193 +#: erpnext/controllers/accounts_controller.py:3203 #: erpnext/public/js/controllers/accounts.js:120 msgid "Cannot refer row number greater than or equal to current row number for this Charge type" msgstr "" @@ -9233,10 +9244,14 @@ 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 -#: erpnext/controllers/accounts_controller.py:3183 +#: erpnext/controllers/accounts_controller.py:3193 #: erpnext/public/js/controllers/accounts.js:112 #: erpnext/public/js/controllers/taxes_and_totals.js:531 msgid "Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row" @@ -9254,11 +9269,11 @@ msgstr "" msgid "Cannot set multiple Item Defaults for a company." msgstr "" -#: erpnext/controllers/accounts_controller.py:3888 +#: erpnext/controllers/accounts_controller.py:3898 msgid "Cannot set quantity less than delivered quantity" msgstr "" -#: erpnext/controllers/accounts_controller.py:3889 +#: erpnext/controllers/accounts_controller.py:3899 msgid "Cannot set quantity less than received quantity" msgstr "" @@ -9270,7 +9285,7 @@ msgstr "" msgid "Cannot start deletion. Another deletion {0} is already queued/running. Please wait for it to complete." msgstr "" -#: erpnext/controllers/accounts_controller.py:3916 +#: erpnext/controllers/accounts_controller.py:3926 msgid "Cannot update rate as item {0} is already ordered or purchased against this quotation" msgstr "" @@ -9451,7 +9466,7 @@ msgstr "" msgid "Cash In Hand" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:322 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:321 msgid "Cash or Bank Account is mandatory for making payment entry" msgstr "" @@ -9499,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 "" @@ -9512,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 "" @@ -9524,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 "" @@ -9545,12 +9560,12 @@ msgstr "" msgid "Category-wise Asset Value" msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:298 -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:140 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:297 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:142 msgid "Caution" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:187 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:208 msgid "Caution: This might alter frozen accounts." msgstr "" @@ -9683,7 +9698,7 @@ msgid "Channel Partner" msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2256 -#: erpnext/controllers/accounts_controller.py:3246 +#: erpnext/controllers/accounts_controller.py:3256 msgid "Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount" msgstr "" @@ -9756,7 +9771,7 @@ msgstr "" #. Label of a Link in the Invoicing Workspace #. Label of a Workspace Sidebar Item -#: erpnext/accounts/doctype/account/account_tree.js:196 +#: erpnext/accounts/doctype/account/account_tree.js:191 #: erpnext/accounts/doctype/cost_center/cost_center.js:41 #: erpnext/accounts/workspace/invoicing/invoicing.json #: erpnext/workspace_sidebar/accounts_setup.json @@ -9869,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 "" @@ -9923,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 "" @@ -10095,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:2477 msgid "Closed Work Order can not be stopped or Re-opened" msgstr "" @@ -10180,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 @@ -10857,7 +10887,7 @@ msgstr "" msgid "Company Address Name" msgstr "" -#: erpnext/controllers/accounts_controller.py:4340 +#: erpnext/controllers/accounts_controller.py:4352 msgid "Company Address is missing. You don't have permission to update it. Please contact your System Manager." msgstr "" @@ -10927,7 +10957,7 @@ msgid "Company Field" msgstr "" #. Label of the company_logo (Attach Image) field in DocType 'Company' -#: erpnext/public/js/print.js:75 erpnext/setup/doctype/company/company.json +#: erpnext/public/js/print.js:77 erpnext/setup/doctype/company/company.json msgid "Company Logo" msgstr "" @@ -10939,7 +10969,10 @@ msgstr "" msgid "Company Not Linked" msgstr "" +#. Label of the shipping_address (Link) field in DocType 'Request for +#. Quotation' #. Label of the shipping_address (Link) field in DocType 'Subcontracting Order' +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.json #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json msgid "Company Shipping Address" msgstr "" @@ -10953,12 +10986,12 @@ msgstr "" msgid "Company and Posting Date is mandatory" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2581 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2578 msgid "Company currencies of both the companies should match for Inter Company Transactions." msgstr "" #: erpnext/stock/doctype/material_request/material_request.js:380 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:755 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:842 msgid "Company field is required" msgstr "" @@ -10988,6 +11021,10 @@ msgstr "" msgid "Company of asset {0} and purchase document {1} doesn't matches." msgstr "" +#: erpnext/setup/doctype/employee/employee.py:168 +msgid "Company or Personal Email is mandatory when 'Create User Automatically' is enabled" +msgstr "" + #. Description of the 'Registration Details' (Code) field in DocType 'Company' #: erpnext/setup/doctype/company/company.json msgid "Company registration numbers for your reference. Tax numbers etc." @@ -11058,7 +11095,7 @@ msgstr "" msgid "Competitors" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:269 +#: erpnext/manufacturing/doctype/job_card/job_card.js:277 #: erpnext/manufacturing/doctype/workstation/workstation.js:151 msgid "Complete Job" msgstr "" @@ -11101,12 +11138,12 @@ msgstr "" msgid "Completed Qty" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:1368 +#: erpnext/manufacturing/doctype/work_order/work_order.py:1369 msgid "Completed Qty cannot be greater than 'Qty to Manufacture'" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:317 -#: erpnext/manufacturing/doctype/job_card/job_card.js:438 +#: erpnext/manufacturing/doctype/job_card/job_card.js:325 +#: erpnext/manufacturing/doctype/job_card/job_card.js:446 #: erpnext/manufacturing/doctype/workstation/workstation.js:296 msgid "Completed Quantity" msgstr "" @@ -11242,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 "" @@ -11252,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:1078 msgid "Consider Process Loss" msgstr "" @@ -11443,7 +11480,7 @@ msgstr "" msgid "Consumed Qty" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:1744 +#: erpnext/manufacturing/doctype/work_order/work_order.py:1745 msgid "Consumed Qty cannot be greater than Reserved Qty for item {0}" msgstr "" @@ -11472,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 "" @@ -11725,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 @@ -11752,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:902 #: 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 @@ -11787,15 +11826,15 @@ msgstr "" msgid "Conversion factor for item {0} has been reset to 1.0 as the uom {1} is same as stock uom {2}." msgstr "" -#: erpnext/controllers/accounts_controller.py:2961 +#: erpnext/controllers/accounts_controller.py:2971 msgid "Conversion rate cannot be 0" msgstr "" -#: erpnext/controllers/accounts_controller.py:2968 +#: erpnext/controllers/accounts_controller.py:2978 msgid "Conversion rate is 1.00, but document currency is different from company currency" msgstr "" -#: erpnext/controllers/accounts_controller.py:2964 +#: erpnext/controllers/accounts_controller.py:2974 msgid "Conversion rate must be 1.00 if document currency is same as company currency" msgstr "" @@ -11861,13 +11900,13 @@ msgstr "" msgid "Corrective Action" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:495 +#: erpnext/manufacturing/doctype/job_card/job_card.js:503 msgid "Corrective Job Card" msgstr "" #. Label of the corrective_operation_section (Tab Break) field in DocType 'Job #. Card' -#: erpnext/manufacturing/doctype/job_card/job_card.js:502 +#: erpnext/manufacturing/doctype/job_card/job_card.js:510 #: erpnext/manufacturing/doctype/job_card/job_card.json msgid "Corrective Operation" msgstr "" @@ -11889,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' @@ -12014,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 @@ -12105,8 +12158,8 @@ msgstr "" msgid "Cost Center is a part of Cost Center Allocation, hence cannot be converted to a group" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1433 -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:899 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1439 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:902 msgid "Cost Center is required in row {0} in Taxes table for type {1}" msgstr "" @@ -12152,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" @@ -12172,7 +12229,7 @@ msgstr "" msgid "Cost of Goods Sold" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:735 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:797 msgid "Cost of Goods Sold Account in Items Table" msgstr "" @@ -12247,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 "" @@ -12337,7 +12394,7 @@ msgstr "" msgid "Coupon Type" msgstr "" -#: erpnext/accounts/doctype/account/account_tree.js:86 +#: erpnext/accounts/doctype/account/account_tree.js:63 #: erpnext/accounts/doctype/bank_clearance/bank_clearance.py:84 #: erpnext/templates/form_grid/bank_reconciliation_grid.html:16 msgid "Cr" @@ -12428,7 +12485,7 @@ msgstr "" msgid "Create Inter Company Journal Entry" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js:54 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js:55 msgid "Create Invoices" msgstr "" @@ -12531,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:800 msgid "Create Pick List" msgstr "" @@ -12562,7 +12619,7 @@ msgstr "" #. Title of an Onboarding Step #. Label of an action in the Onboarding Step 'Create Purchase Order' #: erpnext/buying/onboarding_step/create_purchase_order/create_purchase_order.json -#: erpnext/selling/doctype/sales_order/sales_order.js:1668 +#: erpnext/selling/doctype/sales_order/sales_order.js:1675 #: erpnext/utilities/activation.py:106 msgid "Create Purchase Order" msgstr "" @@ -12666,7 +12723,7 @@ msgstr "" msgid "Create Supplier" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:180 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:181 msgid "Create Supplier Quotation" msgstr "" @@ -12697,13 +12754,19 @@ msgstr "" msgid "Create Transfer Entry" msgstr "" -#. Label of the create_user (Button) field in DocType 'Employee' -#: erpnext/setup/doctype/employee/employee.json +#: erpnext/setup/doctype/employee/employee.js:50 +#: erpnext/setup/doctype/employee/employee.js:52 #: erpnext/utilities/activation.py:117 msgid "Create User" msgstr "" +#. Label of the create_user_automatically (Check) field in DocType 'Employee' +#: erpnext/setup/doctype/employee/employee.json +msgid "Create User Automatically" +msgstr "" + #. Label of the create_user_permission (Check) field in DocType 'Employee' +#: erpnext/setup/doctype/employee/employee.js:65 #: erpnext/setup/doctype/employee/employee.json msgid "Create User Permission" msgstr "" @@ -12741,7 +12804,7 @@ msgstr "" msgid "Create a variant with the template image." msgstr "" -#: erpnext/stock/stock_ledger.py:2018 +#: erpnext/stock/stock_ledger.py:2058 msgid "Create an incoming stock transaction for the Item." msgstr "" @@ -12760,12 +12823,6 @@ msgstr "" msgid "Create in Draft Status" msgstr "" -#. Description of the 'Create Missing Party' (Check) field in DocType 'Opening -#. Invoice Creation Tool' -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json -msgid "Create missing customer or supplier." -msgstr "" - #. Label of an action in the Onboarding Step 'Create Supplier' #: erpnext/buying/onboarding_step/create_supplier/create_supplier.json msgid "Create supplier" @@ -12785,15 +12842,21 @@ msgstr "" msgid "Created {0} scorecards for {1} between:" msgstr "" +#. Description of the 'Create User Automatically' (Check) field in DocType +#. 'Employee' +#: erpnext/setup/doctype/employee/employee.json +msgid "Creates a User account for this employee using the Preferred, Company, or Personal email." +msgstr "" + #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js:140 msgid "Creating Accounts..." msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:1543 +#: erpnext/selling/doctype/sales_order/sales_order.js:1550 msgid "Creating Delivery Note ..." msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:677 +#: erpnext/selling/doctype/sales_order/sales_order.js:684 msgid "Creating Delivery Schedule..." msgstr "" @@ -12809,11 +12872,11 @@ msgstr "" msgid "Creating Packing Slip ..." msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js:60 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js:61 msgid "Creating Purchase Invoices ..." msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:1692 +#: erpnext/selling/doctype/sales_order/sales_order.js:1699 msgid "Creating Purchase Order ..." msgstr "" @@ -12823,7 +12886,7 @@ msgstr "" msgid "Creating Purchase Receipt ..." msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js:58 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js:59 msgid "Creating Sales Invoices ..." msgstr "" @@ -12832,7 +12895,7 @@ msgstr "" msgid "Creating Stock Entry" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:1813 +#: erpnext/selling/doctype/sales_order/sales_order.js:1820 msgid "Creating Subcontracting Inward Order ..." msgstr "" @@ -12844,11 +12907,15 @@ msgstr "" msgid "Creating Subcontracting Receipt ..." msgstr "" -#: erpnext/setup/doctype/employee/employee.js:92 +#: erpnext/setup/doctype/employee/employee.js:85 msgid "Creating User..." msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:287 +#: erpnext/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 "" @@ -12979,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 "" @@ -13061,9 +13128,9 @@ msgstr "" #. Label of the credit_to (Link) field in DocType 'Purchase Invoice' #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:379 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:387 -#: erpnext/controllers/accounts_controller.py:2360 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:375 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:383 +#: erpnext/controllers/accounts_controller.py:2370 msgid "Credit To" msgstr "" @@ -13072,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 "" @@ -13136,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 "" @@ -13265,7 +13332,7 @@ msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:1604 #: erpnext/accounts/doctype/payment_entry/payment_entry.py:1672 -#: erpnext/accounts/utils.py:2456 +#: erpnext/accounts/utils.py:2532 msgid "Currency for {0} must be {1}" msgstr "" @@ -13273,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 "" @@ -13530,7 +13597,7 @@ msgstr "" #: erpnext/accounts/doctype/pricing_rule/pricing_rule.json #: erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json #: erpnext/accounts/doctype/promotional_scheme/promotional_scheme.json -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:309 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:390 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/accounts/doctype/sales_invoice_reference/sales_invoice_reference.json #: erpnext/accounts/doctype/tax_rule/tax_rule.json @@ -13570,7 +13637,7 @@ msgstr "" #: erpnext/selling/doctype/customer/customer.json #: erpnext/selling/doctype/installation_note/installation_note.json #: erpnext/selling/doctype/party_specific_item/party_specific_item.json -#: erpnext/selling/doctype/sales_order/sales_order.js:1183 +#: erpnext/selling/doctype/sales_order/sales_order.js:1190 #: erpnext/selling/doctype/sales_order/sales_order.json #: erpnext/selling/doctype/sales_order/sales_order_calendar.js:19 #: erpnext/selling/doctype/sms_center/sms_center.json @@ -13608,7 +13675,7 @@ msgstr "" #: erpnext/stock/doctype/pick_list/pick_list.json #: erpnext/stock/doctype/serial_no/serial_no.json #: erpnext/stock/doctype/shipment/shipment.json -#: erpnext/stock/doctype/stock_entry/stock_entry.js:385 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:472 #: erpnext/stock/doctype/warehouse/warehouse.json #: erpnext/stock/report/delayed_item_report/delayed_item_report.js:36 #: erpnext/stock/report/delayed_item_report/delayed_item_report.py:121 @@ -14087,7 +14154,7 @@ msgstr "" msgid "Customer required for 'Customerwise Discount'" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1143 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1140 #: erpnext/selling/doctype/sales_order/sales_order.py:434 #: erpnext/stock/doctype/delivery_note/delivery_note.py:436 msgid "Customer {0} does not belong to project {1}" @@ -14140,7 +14207,7 @@ msgstr "" msgid "Customer-wise Item Price" msgstr "" -#: erpnext/crm/report/lost_opportunity/lost_opportunity.py:38 +#: erpnext/crm/report/lost_opportunity/lost_opportunity.py:44 msgid "Customer/Lead Name" msgstr "" @@ -14283,7 +14350,7 @@ msgstr "" msgid "Date of Birth" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:146 +#: erpnext/setup/doctype/employee/employee.py:260 msgid "Date of Birth cannot be greater than today." msgstr "" @@ -14535,7 +14602,7 @@ msgstr "" #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1011 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1022 -#: erpnext/controllers/accounts_controller.py:2360 +#: erpnext/controllers/accounts_controller.py:2370 msgid "Debit To" msgstr "" @@ -14709,15 +14776,15 @@ msgstr "" msgid "Default BOM ({0}) must be active for this item or its template" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:2244 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2245 msgid "Default BOM for {0} not found" msgstr "" -#: erpnext/controllers/accounts_controller.py:3960 +#: erpnext/controllers/accounts_controller.py:3970 msgid "Default BOM not found for FG Item {0}" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:2241 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2242 msgid "Default BOM not found for Item {0} and Project {1}" msgstr "" @@ -15202,6 +15269,11 @@ msgstr "" msgid "Define Project type." msgstr "" +#. Description of the 'End of Life' (Date) field in DocType 'Item' +#: erpnext/stock/doctype/item/item.json +msgid "Defines the date after which the item can no longer be used in transactions or manufacturing" +msgstr "" + #. Name of a UOM #: erpnext/setup/setup_wizard/data/uom_data.json msgid "Dekagram/Litre" @@ -15325,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' @@ -15390,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 @@ -15400,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 "" @@ -15436,10 +15509,10 @@ 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:895 #: 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 +#: erpnext/selling/doctype/sales_order/sales_order.js:631 +#: erpnext/selling/doctype/sales_order/sales_order.js:1497 #: erpnext/selling/doctype/sales_order/sales_order.json #: erpnext/selling/doctype/sales_order_item/sales_order_item.json #: erpnext/selling/report/sales_order_analysis/sales_order_analysis.py:321 @@ -15479,19 +15552,19 @@ msgstr "" #. Label of a Workspace Sidebar Item #: erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:129 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:332 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:413 #: erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js:36 #: erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json #: erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.js:22 #: erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py:21 #: erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py:291 #: erpnext/accounts/report/sales_register/sales_register.py:245 -#: erpnext/selling/doctype/sales_order/sales_order.js:1042 +#: erpnext/selling/doctype/sales_order/sales_order.js:1049 #: erpnext/selling/doctype/sales_order/sales_order_list.js:81 #: erpnext/setup/doctype/authorization_rule/authorization_rule.json #: erpnext/stock/doctype/delivery_note/delivery_note.json #: erpnext/stock/doctype/delivery_stop/delivery_stop.json -#: erpnext/stock/doctype/delivery_trip/delivery_trip.js:52 +#: erpnext/stock/doctype/delivery_trip/delivery_trip.js:54 #: erpnext/stock/doctype/packing_slip/packing_slip.json #: erpnext/stock/doctype/pick_list/pick_list.js:134 #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:59 @@ -15539,12 +15612,12 @@ msgstr "" msgid "Delivery Note Trends" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1404 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1401 msgid "Delivery Note {0} is not submitted" msgstr "" #: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1285 -#: erpnext/stock/doctype/delivery_trip/delivery_trip.js:73 +#: erpnext/stock/doctype/delivery_trip/delivery_trip.js:75 msgid "Delivery Notes" msgstr "" @@ -15556,8 +15629,8 @@ msgstr "" msgid "Delivery Notes {0} updated" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:619 -#: erpnext/selling/doctype/sales_order/sales_order.js:646 +#: erpnext/selling/doctype/sales_order/sales_order.js:626 +#: erpnext/selling/doctype/sales_order/sales_order.js:653 msgid "Delivery Schedule" msgstr "" @@ -15662,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 "" @@ -15788,7 +15869,7 @@ msgstr "" msgid "Depreciation Entry against asset {0}" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:255 +#: erpnext/assets/doctype/asset/depreciation.py:258 msgid "Depreciation Entry against {0} worth {1}" msgstr "" @@ -15800,7 +15881,7 @@ msgstr "" msgid "Depreciation Expense Account" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:302 +#: erpnext/assets/doctype/asset/depreciation.py:305 msgid "Depreciation Expense Account should be an Income or Expense Account." msgstr "" @@ -15944,11 +16025,11 @@ msgstr "" msgid "Difference Account" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:724 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:786 msgid "Difference Account in Items Table" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:713 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:775 msgid "Difference Account must be a Asset/Liability type account (Temporary Opening), since this Stock Entry is an Opening Entry" msgstr "" @@ -16003,7 +16084,7 @@ msgstr "" msgid "Difference Posting Date" msgstr "" -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py:100 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:120 msgid "Difference Qty" msgstr "" @@ -16195,6 +16276,9 @@ msgstr "" #. Option for the 'Purpose' (Select) field in DocType 'Stock Entry' #. Option for the 'Purpose' (Select) field in DocType 'Stock Entry Type' +#: erpnext/manufacturing/doctype/work_order/work_order.js:1056 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:370 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:413 #: erpnext/stock/doctype/stock_entry/stock_entry.json #: erpnext/stock/doctype/stock_entry_type/stock_entry_type.json msgid "Disassemble" @@ -16204,7 +16288,11 @@ msgstr "" msgid "Disassemble Order" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:443 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2390 +msgid "Disassemble Qty cannot be less than or equal to 0." +msgstr "" + +#: erpnext/manufacturing/doctype/work_order/work_order.js:447 msgid "Disassemble Qty cannot be less than or equal to 0." msgstr "" @@ -16572,7 +16660,7 @@ msgstr "" msgid "Disposal Date" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:832 +#: erpnext/assets/doctype/asset/depreciation.py:835 msgid "Disposal date {0} cannot be before {1} date {2} of the asset." msgstr "" @@ -16610,12 +16698,6 @@ msgstr "" msgid "Distance from top edge" msgstr "" -#. Label of the distinct_item_and_warehouse (Code) field in DocType 'Repost -#. Item Valuation' -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json -msgid "Distinct Item and Warehouse" -msgstr "" - #. Description of a DocType #: erpnext/stock/doctype/serial_no/serial_no.json msgid "Distinct unit of an Item" @@ -16727,7 +16809,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 "" @@ -16759,7 +16841,7 @@ msgstr "" msgid "Do you want to change valuation method?" msgstr "" -#: erpnext/stock/doctype/delivery_trip/delivery_trip.js:156 +#: erpnext/stock/doctype/delivery_trip/delivery_trip.js:158 msgid "Do you want to notify all the customers by email?" msgstr "" @@ -16767,7 +16849,7 @@ msgstr "" msgid "Do you want to submit the material request" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:103 +#: erpnext/manufacturing/doctype/job_card/job_card.js:111 msgid "Do you want to submit the stock entry?" msgstr "" @@ -16823,10 +16905,6 @@ msgstr "" msgid "Document Type already used as a dimension" msgstr "" -#: erpnext/setup/install.py:189 -msgid "Documentation" -msgstr "" - #. Description of the 'Reconciliation Queue Size' (Int) field in DocType #. 'Accounts Settings' #: erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -16884,7 +16962,7 @@ msgstr "" msgid "Download CSV Template" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:144 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:145 msgid "Download PDF for Supplier" msgstr "" @@ -17014,7 +17092,7 @@ msgstr "" msgid "Due Date cannot be before {0}" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:144 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:165 msgid "Due to stock closing entry {0}, you cannot repost item valuation before {1}" msgstr "" @@ -17243,7 +17321,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 "" @@ -17450,10 +17528,18 @@ msgstr "" msgid "Email Receipt" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:371 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:373 msgid "Email Sent to Supplier {0}" msgstr "" +#: erpnext/setup/doctype/employee/employee.py:433 +msgid "Email is required to create a user" +msgstr "" + +#: erpnext/setup/doctype/employee/employee.js:72 +msgid "Email is required to create a user." +msgstr "" + #: erpnext/stock/doctype/shipment/shipment.js:174 msgid "Email or Phone/Mobile of the Contact are mandatory to continue." msgstr "" @@ -17584,11 +17670,6 @@ msgstr "" msgid "Employee Education" msgstr "" -#. Label of the exit (Tab Break) field in DocType 'Employee' -#: erpnext/setup/doctype/employee/employee.json -msgid "Employee Exit" -msgstr "" - #. Name of a DocType #: erpnext/setup/doctype/employee_external_work_history/employee_external_work_history.json msgid "Employee External Work History" @@ -17637,11 +17718,11 @@ msgstr "" msgid "Employee User Id" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:211 +#: erpnext/setup/doctype/employee/employee.py:325 msgid "Employee cannot report to himself." msgstr "" -#: erpnext/setup/doctype/employee/employee.py:443 +#: erpnext/setup/doctype/employee/employee.py:567 msgid "Employee is required" msgstr "" @@ -17649,16 +17730,20 @@ msgstr "" msgid "Employee is required while issuing Asset {0}" msgstr "" +#: erpnext/setup/doctype/employee/employee.py:430 +msgid "Employee {0} already has a linked user" +msgstr "" + #: erpnext/assets/doctype/asset_movement/asset_movement.py:92 #: erpnext/assets/doctype/asset_movement/asset_movement.py:113 msgid "Employee {0} does not belong to the company {1}" 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 "" -#: erpnext/setup/doctype/employee/employee.py:468 +#: erpnext/setup/doctype/employee/employee.py:592 msgid "Employee {0} not found" msgstr "" @@ -17831,6 +17916,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" @@ -17874,6 +17971,11 @@ msgstr "" msgid "Enable to apply SLA on every {0}" msgstr "" +#. Description of the 'Retain Sample' (Check) field in DocType 'Item' +#: erpnext/stock/doctype/item/item.json +msgid "Enable to reserve a small sample from each batch for any analysis arising ahead" +msgstr "" + #. Label of the enable_tracking_sales_commissions (Check) field in DocType #. 'Selling Settings' #: erpnext/selling/doctype/selling_settings/selling_settings.json @@ -17927,8 +18029,8 @@ msgstr "" #. Label of the end_time (Time) field in DocType 'Stock Reposting Settings' #. Label of the end_time (Time) field in DocType 'Service Day' #. Label of the end_time (Datetime) field in DocType 'Call Log' -#: erpnext/manufacturing/doctype/job_card/job_card.js:375 -#: erpnext/manufacturing/doctype/job_card/job_card.js:445 +#: erpnext/manufacturing/doctype/job_card/job_card.js:383 +#: erpnext/manufacturing/doctype/job_card/job_card.js:453 #: erpnext/manufacturing/doctype/workstation_working_hour/workstation_working_hour.json #: erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json #: erpnext/support/doctype/service_day/service_day.json @@ -17936,7 +18038,7 @@ msgstr "" msgid "End Time" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.js:311 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:345 msgid "End Transit" msgstr "" @@ -17987,12 +18089,6 @@ msgstr "" msgid "Engineer" msgstr "" -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html:13 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html:23 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py:30 -msgid "Enough Parts to Build" -msgstr "" - #. Label of the ensure_delivery_based_on_produced_serial_no (Check) field in #. DocType 'Sales Order Item' #: erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -18003,11 +18099,11 @@ msgstr "" msgid "Enter API key in Google Settings." msgstr "" -#: erpnext/public/js/print.js:62 +#: erpnext/public/js/print.js:64 msgid "Enter Company Details" msgstr "" -#: erpnext/setup/doctype/employee/employee.js:108 +#: erpnext/setup/doctype/employee/employee.js:148 msgid "Enter First and Last name of Employee, based on Which Full Name will be updated. IN transactions, it will be Full Name which will be fetched." msgstr "" @@ -18019,8 +18115,8 @@ msgstr "" msgid "Enter Serial Nos" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:402 -#: erpnext/manufacturing/doctype/job_card/job_card.js:471 +#: erpnext/manufacturing/doctype/job_card/job_card.js:410 +#: erpnext/manufacturing/doctype/job_card/job_card.js:479 #: erpnext/manufacturing/doctype/workstation/workstation.js:312 msgid "Enter Value" msgstr "" @@ -18096,11 +18192,11 @@ msgstr "" msgid "Enter the opening stock units." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:973 +#: erpnext/manufacturing/doctype/bom/bom.js:992 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:1218 msgid "Enter the quantity to manufacture. Raw material Items will be fetched only when this is set." msgstr "" @@ -18161,11 +18257,11 @@ msgstr "" msgid "Error Description" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:277 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:295 msgid "Error Occurred" msgstr "" -#: erpnext/telephony/doctype/call_log/call_log.py:195 +#: erpnext/telephony/doctype/call_log/call_log.py:197 msgid "Error during caller information update" msgstr "" @@ -18181,7 +18277,7 @@ msgstr "" msgid "Error in party matching for Bank Transaction {0}" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:319 +#: erpnext/assets/doctype/asset/depreciation.py:322 msgid "Error while posting depreciation entries" msgstr "" @@ -18189,7 +18285,7 @@ msgstr "" msgid "Error while processing deferred accounting for {0}" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:531 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:554 msgid "Error while reposting item valuation" msgstr "" @@ -18262,7 +18358,7 @@ msgstr "" msgid "Example: ABCD.#####. If series is set and Batch No is not mentioned in transactions, then automatic batch number will be created based on this series. If you always want to explicitly mention Batch No for this item, leave this blank. Note: this setting will take priority over the Naming Series Prefix in Stock Settings." msgstr "" -#: erpnext/stock/stock_ledger.py:2281 +#: erpnext/stock/stock_ledger.py:2321 msgid "Example: Serial No {0} reserved in {1}." msgstr "" @@ -18272,11 +18368,15 @@ msgstr "" msgid "Exception Budget Approver Role" msgstr "" +#: erpnext/stock/doctype/stock_entry/stock_entry.py:931 +msgid "Excess Disassembly" +msgstr "" + #: erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js:55 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 "" @@ -18417,7 +18517,7 @@ msgstr "" msgid "Excise Entry" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.js:1411 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:1501 msgid "Excise Invoice" msgstr "" @@ -18486,12 +18586,17 @@ msgstr "" msgid "Existing Customer" msgstr "" +#. Label of the exit (Tab Break) field in DocType 'Employee' +#: erpnext/setup/doctype/employee/employee.json +msgid "Exit" +msgstr "" + #. Label of the held_on (Date) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Exit Interview Held On" msgstr "" -#: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py:444 +#: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py:470 msgid "Expected" msgstr "" @@ -18605,7 +18710,7 @@ msgstr "" #: erpnext/accounts/doctype/cashier_closing/cashier_closing.json #: erpnext/accounts/doctype/ledger_merge/ledger_merge.json #: erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.json -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:605 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:601 #: erpnext/accounts/report/account_balance/account_balance.js:28 #: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js:89 #: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:183 @@ -18676,13 +18781,13 @@ msgstr "" msgid "Expense Head" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:499 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:523 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:543 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:495 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:519 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:539 msgid "Expense Head Changed" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:601 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:597 msgid "Expense account is mandatory for item {0}" msgstr "" @@ -18707,8 +18812,8 @@ msgstr "" msgid "Expenses Included In Valuation" msgstr "" -#: erpnext/stock/doctype/pick_list/pick_list.py:306 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:409 +#: erpnext/stock/doctype/pick_list/pick_list.py:307 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:496 msgid "Expired Batches" msgstr "" @@ -18781,7 +18886,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 "" @@ -18804,6 +18909,10 @@ msgstr "" msgid "FG / Semi FG Item" msgstr "" +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js:21 +msgid "FG Items to Make" +msgstr "" + #. Option for the 'Default Stock Valuation Method' (Select) field in DocType #. 'Company' #. Option for the 'Valuation Method' (Select) field in DocType 'Item' @@ -18859,20 +18968,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 @@ -18887,12 +18994,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 "" @@ -18969,11 +19080,17 @@ 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 "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1069 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:284 msgid "Fetch Timesheet" msgstr "" @@ -18996,7 +19113,7 @@ msgid "Fetch Value From" msgstr "" #: erpnext/stock/doctype/material_request/material_request.js:372 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:732 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:819 msgid "Fetch exploded BOM (including sub-assemblies)" msgstr "" @@ -19004,7 +19121,7 @@ msgstr "" msgid "Fetched only {0} available serial numbers." msgstr "" -#: erpnext/edi/doctype/code_list/code_list_import.py:27 +#: erpnext/edi/doctype/code_list/code_list_import.py:28 msgid "Fetching Error" msgstr "" @@ -19017,7 +19134,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 "" @@ -19257,9 +19374,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:884 +#: erpnext/manufacturing/doctype/work_order/work_order.js:899 +#: erpnext/manufacturing/doctype/work_order/work_order.js:908 msgid "Finish" msgstr "" @@ -19275,7 +19392,7 @@ msgstr "" #: erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json #: erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py:43 #: erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py:147 -#: erpnext/selling/doctype/sales_order/sales_order.js:824 +#: erpnext/selling/doctype/sales_order/sales_order.js:831 #: erpnext/selling/doctype/sales_order_item/sales_order_item.json #: erpnext/subcontracting/doctype/subcontracting_bom/subcontracting_bom.json msgid "Finished Good" @@ -19290,20 +19407,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:921 #: 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:939 msgid "Finished Good Item Qty" msgstr "" @@ -19316,15 +19433,15 @@ msgstr "" msgid "Finished Good Item Quantity" msgstr "" -#: erpnext/controllers/accounts_controller.py:3946 +#: erpnext/controllers/accounts_controller.py:3956 msgid "Finished Good Item is not specified for service item {0}" msgstr "" -#: erpnext/controllers/accounts_controller.py:3963 +#: erpnext/controllers/accounts_controller.py:3973 msgid "Finished Good Item {0} Qty can not be zero" msgstr "" -#: erpnext/controllers/accounts_controller.py:3957 +#: erpnext/controllers/accounts_controller.py:3967 msgid "Finished Good Item {0} must be a sub-contracted item" msgstr "" @@ -19410,11 +19527,11 @@ 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:1767 msgid "Finished Item {0} does not match with Work Order {1}" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:577 +#: erpnext/selling/doctype/sales_order/sales_order.js:584 msgid "First Delivery Date" msgstr "" @@ -19580,7 +19697,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 "" @@ -19658,7 +19775,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 "" @@ -19725,7 +19842,7 @@ msgid "For Job Card" msgstr "" #. Label of the for_operation (Link) field in DocType 'Job Card' -#: erpnext/manufacturing/doctype/job_card/job_card.js:515 +#: erpnext/manufacturing/doctype/job_card/job_card.js:523 #: erpnext/manufacturing/doctype/job_card/job_card.json msgid "For Operation" msgstr "" @@ -19746,7 +19863,7 @@ msgstr "" msgid "For Production" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:836 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:898 msgid "For Quantity (Manufactured Qty) is mandatory" msgstr "" @@ -19774,7 +19891,7 @@ msgstr "" #: erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json #: erpnext/manufacturing/doctype/production_plan/production_plan.js:471 #: erpnext/manufacturing/doctype/production_plan/production_plan.json -#: erpnext/selling/doctype/sales_order/sales_order.js:1407 +#: erpnext/selling/doctype/sales_order/sales_order.js:1414 #: erpnext/stock/doctype/material_request/material_request.js:361 #: erpnext/templates/form_grid/material_request_grid.html:36 msgid "For Warehouse" @@ -19814,7 +19931,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 "" @@ -19822,11 +19939,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:2624 msgid "For operation {0}: Quantity ({1}) can not be greater than pending quantity({2})" msgstr "" @@ -19843,7 +19960,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:1799 msgid "For quantity {0} should not be greater than allowed quantity {1}" msgstr "" @@ -19857,7 +19974,7 @@ msgstr "" msgid "For row {0} in {1}. To include {2} in Item rate, rows {3} must also be included" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1716 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1721 msgid "For row {0}: Enter Planned Qty" msgstr "" @@ -19876,11 +19993,11 @@ msgstr "" msgid "For the convenience of customers, these codes can be used in print formats like Invoices and Delivery Notes" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:977 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1069 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 "" @@ -20034,8 +20151,8 @@ msgstr "" msgid "From BOM" msgstr "" -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py:63 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py:25 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:105 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:169 msgid "From BOM No" msgstr "" @@ -20438,7 +20555,7 @@ msgstr "" msgid "Furniture and Fixtures" msgstr "" -#: erpnext/accounts/doctype/account/account_tree.js:140 +#: erpnext/accounts/doctype/account/account_tree.js:135 msgid "Further accounts can be made under Groups, but entries can be made against non-Groups" msgstr "" @@ -20468,7 +20585,7 @@ msgstr "" msgid "Future Payments" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:382 +#: erpnext/assets/doctype/asset/depreciation.py:385 msgid "Future date is not allowed" msgstr "" @@ -20727,7 +20844,7 @@ msgstr "" msgid "Get Customer Group Details" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:608 +#: erpnext/selling/doctype/sales_order/sales_order.js:615 msgid "Get Delivery Schedule" msgstr "" @@ -20765,15 +20882,15 @@ msgstr "" #. Label of the get_items_from (Select) field in DocType 'Production Plan' #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:166 #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:191 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:293 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:325 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:359 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1125 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:340 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:374 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:406 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:446 #: erpnext/buying/doctype/purchase_order/purchase_order.js:540 #: erpnext/buying/doctype/purchase_order/purchase_order.js:563 -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:379 -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:401 -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:446 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:380 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:402 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:447 #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.js:75 #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.js:108 #: erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js:80 @@ -20783,19 +20900,19 @@ msgstr "" #: erpnext/manufacturing/doctype/production_plan/production_plan.json #: erpnext/public/js/controllers/buying.js:327 #: erpnext/selling/doctype/quotation/quotation.js:183 -#: erpnext/selling/doctype/sales_order/sales_order.js:203 -#: erpnext/selling/doctype/sales_order/sales_order.js:1200 +#: erpnext/selling/doctype/sales_order/sales_order.js:210 +#: erpnext/selling/doctype/sales_order/sales_order.js:1207 #: erpnext/stock/doctype/delivery_note/delivery_note.js:187 #: erpnext/stock/doctype/delivery_note/delivery_note.js:239 #: erpnext/stock/doctype/material_request/material_request.js:141 #: erpnext/stock/doctype/material_request/material_request.js:238 #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:144 #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:244 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:352 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:399 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:432 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:523 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:699 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:439 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:486 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:519 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:610 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:786 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js:165 msgid "Get Items From" msgstr "" @@ -20811,12 +20928,12 @@ msgid "Get Items for Purchase Only" msgstr "" #: erpnext/stock/doctype/material_request/material_request.js:346 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:735 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:748 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:822 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:835 msgid "Get Items from BOM" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:418 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:419 msgid "Get Items from Material Requests against this Supplier" msgstr "" @@ -20885,10 +21002,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' @@ -20906,16 +21023,16 @@ msgstr "" msgid "Get Sub Assembly Items" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:460 -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:480 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:461 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:481 msgid "Get Suppliers" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:484 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:485 msgid "Get Suppliers By" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1121 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:336 msgid "Get Timesheets" msgstr "" @@ -20928,12 +21045,12 @@ msgstr "" msgid "Get Unreconciled Entries" msgstr "" -#: erpnext/stock/doctype/delivery_trip/delivery_trip.js:69 +#: erpnext/stock/doctype/delivery_trip/delivery_trip.js:71 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' @@ -20987,7 +21104,7 @@ msgstr "" msgid "Goods Transferred" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2220 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2317 msgid "Goods are already received against the outward entry {0}" msgstr "" @@ -21284,7 +21401,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 "" @@ -21558,8 +21675,8 @@ msgstr "" msgid "Hectopascal" msgstr "" -#. Label of the height (Int) field in DocType 'Shipment Parcel' -#. Label of the height (Int) field in DocType 'Shipment Parcel Template' +#. Label of the height (Float) field in DocType 'Shipment Parcel' +#. Label of the height (Float) field in DocType 'Shipment Parcel Template' #: erpnext/stock/doctype/shipment_parcel/shipment_parcel.json #: erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json msgid "Height (cm)" @@ -21585,11 +21702,11 @@ msgstr "" msgid "Helps you distribute the Budget/Target across months if you have seasonality in your business." msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:349 +#: erpnext/assets/doctype/asset/depreciation.py:352 msgid "Here are the error logs for the aforementioned failed depreciation entries: {0}" msgstr "" -#: erpnext/stock/stock_ledger.py:2003 +#: erpnext/stock/stock_ledger.py:2043 msgid "Here are the options to proceed:" msgstr "" @@ -21604,7 +21721,7 @@ msgstr "" msgid "Here you can maintain height, weight, allergies, medical concerns etc" msgstr "" -#: erpnext/setup/doctype/employee/employee.js:134 +#: erpnext/setup/doctype/employee/employee.js:174 msgid "Here, you can select a senior of this Employee. Based on this, Organization Chart will be populated." msgstr "" @@ -21617,7 +21734,7 @@ msgstr "" msgid "Hertz" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:533 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:556 msgid "Hi," msgstr "" @@ -21684,7 +21801,7 @@ msgid "History In Company" msgstr "" #: erpnext/buying/doctype/purchase_order/purchase_order.js:338 -#: erpnext/selling/doctype/sales_order/sales_order.js:989 +#: erpnext/selling/doctype/sales_order/sales_order.js:996 msgid "Hold" msgstr "" @@ -22064,10 +22181,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 @@ -22174,10 +22291,18 @@ msgstr "" msgid "If no taxes are set, and Taxes and Charges Template is selected, the system will automatically apply the taxes from the chosen template." msgstr "" -#: erpnext/stock/stock_ledger.py:2013 +#: erpnext/stock/stock_ledger.py:2053 msgid "If not, you can Cancel / Submit this entry" msgstr "" +#: 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 #. Rule' #: erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -22194,7 +22319,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:1251 msgid "If the BOM results in Scrap material, the Scrap Warehouse needs to be selected." msgstr "" @@ -22203,7 +22328,7 @@ msgstr "" msgid "If the account is frozen, entries are allowed to restricted users." msgstr "" -#: erpnext/stock/stock_ledger.py:2006 +#: erpnext/stock/stock_ledger.py:2046 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 "" @@ -22213,7 +22338,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:1270 msgid "If the selected BOM has Operations mentioned in it, the system will fetch all Operations from BOM, these values can be changed." msgstr "" @@ -22300,11 +22425,11 @@ msgstr "" msgid "If you need to reconcile particular transactions against each other, then please select accordingly. If not, all the transactions will be allocated in FIFO order." msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1092 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1097 msgid "If you still want to proceed, please disable 'Skip Available Sub Assembly Items' checkbox." msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1832 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1839 msgid "If you still want to proceed, please enable {0}." msgstr "" @@ -22376,15 +22501,15 @@ 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 "" -#: erpnext/selling/doctype/sales_order/sales_order.js:1390 +#: erpnext/selling/doctype/sales_order/sales_order.js:1397 msgid "Ignore Existing Ordered Qty" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1824 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1831 msgid "Ignore Existing Projected Quantity" msgstr "" @@ -22428,7 +22553,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 "" @@ -22496,6 +22621,10 @@ msgstr "" msgid "Import Data" msgstr "" +#: erpnext/setup/doctype/employee/employee_list.js:16 +msgid "Import Employees" +msgstr "" + #: erpnext/edi/doctype/code_list/code_list.js:7 #: erpnext/edi/doctype/code_list/code_list_list.js:3 #: erpnext/edi/doctype/common_code/common_code_list.js:3 @@ -22607,12 +22736,6 @@ msgstr "" msgid "In Stock" msgstr "" -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html:12 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html:22 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py:29 -msgid "In Stock Qty" -msgstr "" - #. Option for the 'Status' (Select) field in DocType 'Delivery Trip' #. Option for the 'Transfer Status' (Select) field in DocType 'Material #. Request' @@ -22806,7 +22929,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" @@ -22837,7 +22960,7 @@ msgstr "" #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json #: erpnext/buying/doctype/purchase_order_item/purchase_order_item.json #: erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json -#: erpnext/selling/doctype/sales_order/sales_order.js:1386 +#: erpnext/selling/doctype/sales_order/sales_order.js:1393 #: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json #: erpnext/subcontracting/doctype/subcontracting_inward_order_item/subcontracting_inward_order_item.json #: erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json @@ -23037,7 +23160,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 "" @@ -23054,11 +23177,11 @@ 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 "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:984 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1076 msgid "Incorrect Component Quantity" msgstr "" @@ -23067,7 +23190,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 "" @@ -23075,7 +23198,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 "" @@ -23102,9 +23225,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 "" @@ -23261,7 +23384,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 "" @@ -23285,7 +23408,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 "" @@ -23340,7 +23463,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 "" @@ -23354,22 +23477,22 @@ msgstr "" msgid "Insufficient Capacity" msgstr "" -#: erpnext/controllers/accounts_controller.py:3840 -#: erpnext/controllers/accounts_controller.py:3864 +#: erpnext/controllers/accounts_controller.py:3850 +#: erpnext/controllers/accounts_controller.py:3874 msgid "Insufficient Permissions" msgstr "" #: 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/stock_entry/stock_entry.py:956 -#: erpnext/stock/serial_batch_bundle.py:1205 erpnext/stock/stock_ledger.py:1713 -#: erpnext/stock/stock_ledger.py:2172 +#: erpnext/stock/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:1048 +#: erpnext/stock/serial_batch_bundle.py:1205 erpnext/stock/stock_ledger.py:1734 +#: erpnext/stock/stock_ledger.py:2212 msgid "Insufficient Stock" msgstr "" -#: erpnext/stock/stock_ledger.py:2187 +#: erpnext/stock/stock_ledger.py:2227 msgid "Insufficient Stock for Batch" msgstr "" @@ -23462,7 +23585,7 @@ msgstr "" msgid "Inter Company Order Reference" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:1137 +#: erpnext/selling/doctype/sales_order/sales_order.js:1144 msgid "Inter Company Purchase Order" msgstr "" @@ -23484,12 +23607,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" @@ -23530,11 +23647,11 @@ 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 "" -#: erpnext/selling/doctype/sales_order/sales_order.js:1136 +#: erpnext/selling/doctype/sales_order/sales_order.js:1143 msgid "Internal Purchase Order" msgstr "" @@ -23604,14 +23721,14 @@ msgstr "" msgid "Interval should be between 1 to 59 MInutes" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:380 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:388 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:376 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:384 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1017 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1027 #: erpnext/assets/doctype/asset_category/asset_category.py:69 #: erpnext/assets/doctype/asset_category/asset_category.py:97 -#: erpnext/controllers/accounts_controller.py:3207 -#: erpnext/controllers/accounts_controller.py:3215 +#: erpnext/controllers/accounts_controller.py:3217 +#: erpnext/controllers/accounts_controller.py:3225 msgid "Invalid Account" msgstr "" @@ -23620,7 +23737,7 @@ msgid "Invalid Accounting Dimension" msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:400 -#: erpnext/accounts/doctype/payment_request/payment_request.py:1004 +#: erpnext/accounts/doctype/payment_request/payment_request.py:1005 msgid "Invalid Allocated Amount" msgstr "" @@ -23640,7 +23757,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 "" @@ -23656,16 +23773,20 @@ msgstr "" msgid "Invalid Company Field" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2356 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2353 msgid "Invalid Company for Inter Company Transaction." msgstr "" #: erpnext/assets/doctype/asset/asset.py:361 #: erpnext/assets/doctype/asset/asset.py:368 -#: erpnext/controllers/accounts_controller.py:3230 +#: erpnext/controllers/accounts_controller.py:3240 msgid "Invalid Cost Center" msgstr "" +#: 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 "" @@ -23674,11 +23795,11 @@ msgstr "" msgid "Invalid Discount" msgstr "" -#: erpnext/controllers/taxes_and_totals.py:803 +#: erpnext/controllers/taxes_and_totals.py:844 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 "" @@ -23696,7 +23817,7 @@ msgid "Invalid Group By" msgstr "" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:499 -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:955 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:960 msgid "Invalid Item" msgstr "" @@ -23748,7 +23869,7 @@ msgstr "" msgid "Invalid Priority" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1230 +#: erpnext/manufacturing/doctype/bom/bom.py:1275 msgid "Invalid Process Loss Configuration" msgstr "" @@ -23756,8 +23877,8 @@ msgstr "" msgid "Invalid Purchase Invoice" msgstr "" -#: erpnext/controllers/accounts_controller.py:3884 -#: erpnext/controllers/accounts_controller.py:3898 +#: erpnext/controllers/accounts_controller.py:3894 +#: erpnext/controllers/accounts_controller.py:3908 msgid "Invalid Qty" msgstr "" @@ -23765,7 +23886,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 "" @@ -23786,12 +23907,12 @@ msgstr "" msgid "Invalid Selling Price" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1745 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1842 msgid "Invalid Serial and Batch Bundle" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1018 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1040 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1110 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1132 msgid "Invalid Source and Target Warehouse" msgstr "" @@ -23816,7 +23937,7 @@ msgstr "" msgid "Invalid file URL" msgstr "" -#: erpnext/accounts/doctype/financial_report_template/financial_report_template.js:93 +#: erpnext/accounts/doctype/financial_report_template/financial_report_template.js:87 msgid "Invalid filter formula. Please check the syntax." msgstr "" @@ -23840,7 +23961,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 "" @@ -23855,7 +23976,7 @@ msgstr "" msgid "Invalid {0}" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2354 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2351 msgid "Invalid {0} for Inter Company Transaction." msgstr "" @@ -23865,7 +23986,7 @@ msgid "Invalid {0}: {1}" msgstr "" #. Label of the inventory_section (Tab Break) field in DocType 'Item' -#: erpnext/setup/install.py:358 erpnext/stock/doctype/item/item.json +#: erpnext/setup/install.py:359 erpnext/stock/doctype/item/item.json msgid "Inventory" msgstr "" @@ -23879,7 +24000,7 @@ msgstr "" #. Label of a Workspace Sidebar Item #: erpnext/patches/v15_0/refactor_closing_stock_balance.py:43 #: erpnext/stock/doctype/inventory_dimension/inventory_dimension.json -#: erpnext/stock/doctype/inventory_dimension/inventory_dimension.py:176 +#: erpnext/stock/doctype/inventory_dimension/inventory_dimension.py:175 #: erpnext/workspace_sidebar/stock.json msgid "Inventory Dimension" msgstr "" @@ -24089,7 +24210,7 @@ msgstr "" #: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json #: erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json #: erpnext/accounts/doctype/pos_profile/pos_profile.json -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2405 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2402 #: erpnext/buying/doctype/supplier/supplier.json #: erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py:62 msgid "Invoices" @@ -24389,6 +24510,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" @@ -24539,14 +24674,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" @@ -24759,11 +24886,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 "" @@ -24818,16 +24945,14 @@ 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:1253 #: erpnext/manufacturing/doctype/blanket_order/blanket_order.json -#: erpnext/manufacturing/doctype/bom/bom.js:1066 +#: erpnext/manufacturing/doctype/bom/bom.js:1085 #: erpnext/manufacturing/doctype/bom/bom.json #: erpnext/manufacturing/doctype/plant_floor/plant_floor.js:109 #: erpnext/manufacturing/doctype/workstation/workstation_job_card.html:25 -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py:50 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html:9 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html:19 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py:22 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:101 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:165 #: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js:68 #: erpnext/manufacturing/report/process_loss_report/process_loss_report.js:15 #: erpnext/manufacturing/report/process_loss_report/process_loss_report.py:74 @@ -24841,7 +24966,7 @@ msgstr "" #: erpnext/public/js/stock_analytics.js:92 #: erpnext/selling/doctype/party_specific_item/party_specific_item.json #: erpnext/selling/doctype/product_bundle_item/product_bundle_item.json -#: erpnext/selling/doctype/sales_order/sales_order.js:1631 +#: erpnext/selling/doctype/sales_order/sales_order.js:1638 #: erpnext/selling/page/point_of_sale/pos_item_cart.js:50 #: erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.js:14 #: erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.js:36 @@ -25007,7 +25132,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 @@ -25051,7 +25176,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' @@ -25068,7 +25193,7 @@ msgstr "" #: erpnext/accounts/doctype/pricing_rule_item_code/pricing_rule_item_code.json #: erpnext/accounts/doctype/promotional_scheme/promotional_scheme.json #: erpnext/accounts/doctype/promotional_scheme_product_discount/promotional_scheme_product_discount.json -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1078 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:293 #: erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py:68 #: erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py:37 #: erpnext/accounts/report/gross_profit/gross_profit.py:312 @@ -25098,7 +25223,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 @@ -25123,19 +25248,19 @@ 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:579 +#: erpnext/public/js/utils.js:736 #: 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 #: erpnext/selling/doctype/quotation/quotation.js:298 #: erpnext/selling/doctype/quotation_item/quotation_item.json -#: erpnext/selling/doctype/sales_order/sales_order.js:368 -#: erpnext/selling/doctype/sales_order/sales_order.js:476 -#: erpnext/selling/doctype/sales_order/sales_order.js:1245 -#: erpnext/selling/doctype/sales_order/sales_order.js:1400 +#: erpnext/selling/doctype/sales_order/sales_order.js:375 +#: erpnext/selling/doctype/sales_order/sales_order.js:483 +#: erpnext/selling/doctype/sales_order/sales_order.js:1252 +#: erpnext/selling/doctype/sales_order/sales_order.js:1407 #: erpnext/selling/doctype/sales_order_item/sales_order_item.json #: erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py:29 #: erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py:27 @@ -25184,13 +25309,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 @@ -25217,7 +25342,7 @@ msgstr "" msgid "Item Code cannot be changed for Serial No." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:455 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:451 msgid "Item Code required at Row No {0}" msgstr "" @@ -25383,7 +25508,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 @@ -25504,7 +25629,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' @@ -25583,7 +25708,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 @@ -25596,7 +25721,6 @@ msgstr "" #: erpnext/manufacturing/notification/material_request_receipt_notification/material_request_receipt_notification.html:8 #: erpnext/manufacturing/report/bom_explorer/bom_explorer.py:66 #: erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py:109 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py:23 #: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py:106 #: erpnext/manufacturing/report/job_card_summary/job_card_summary.py:158 #: erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.py:958 @@ -25605,10 +25729,10 @@ 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:831 #: erpnext/selling/doctype/quotation_item/quotation_item.json -#: erpnext/selling/doctype/sales_order/sales_order.js:1252 +#: erpnext/selling/doctype/sales_order/sales_order.js:1259 #: erpnext/selling/doctype/sales_order_item/sales_order_item.json #: erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py:35 #: erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py:33 @@ -25644,7 +25768,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 @@ -25760,7 +25884,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 "" @@ -25983,7 +26107,7 @@ msgstr "" msgid "Item Wise Tax Details" msgstr "" -#: erpnext/controllers/taxes_and_totals.py:537 +#: erpnext/controllers/taxes_and_totals.py:562 msgid "Item Wise Tax Details do not match with Taxes and Charges at the following rows:" msgstr "" @@ -26003,7 +26127,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:3498 msgid "Item for row {0} does not match Material Request" msgstr "" @@ -26019,12 +26143,12 @@ 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 "" #: erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.py:42 -#: erpnext/selling/doctype/sales_order/sales_order.js:1638 +#: erpnext/selling/doctype/sales_order/sales_order.js:1645 msgid "Item name" msgstr "" @@ -26033,11 +26157,11 @@ msgstr "" msgid "Item operation" msgstr "" -#: erpnext/controllers/accounts_controller.py:3938 +#: erpnext/controllers/accounts_controller.py:3948 msgid "Item qty can not be updated as raw materials are already processed." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1148 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1247 msgid "Item rate has been updated to zero as Allow Zero Valuation Rate is checked for item {0}" msgstr "" @@ -26080,7 +26204,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 "" @@ -26108,7 +26232,7 @@ msgstr "" msgid "Item {0} has reached its end of life on {1}" msgstr "" -#: erpnext/stock/stock_ledger.py:118 +#: erpnext/stock/stock_ledger.py:117 msgid "Item {0} ignored since it is not a stock item" msgstr "" @@ -26132,11 +26256,11 @@ msgstr "" msgid "Item {0} is not a stock Item" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:954 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:959 msgid "Item {0} is not a subcontracted item" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2132 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2229 msgid "Item {0} is not active or end of life has been reached" msgstr "" @@ -26156,7 +26280,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:1580 msgid "Item {0} not found in 'Raw Materials Supplied' table in {1} {2}" msgstr "" @@ -26164,7 +26288,7 @@ msgstr "" msgid "Item {0} not found." msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:325 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:324 msgid "Item {0}: Ordered qty {1} cannot be less than minimum order qty {2} (defined in Item)." msgstr "" @@ -26222,7 +26346,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 "" @@ -26242,8 +26366,8 @@ msgstr "" msgid "Items Filter" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1678 -#: erpnext/selling/doctype/sales_order/sales_order.js:1676 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1683 +#: erpnext/selling/doctype/sales_order/sales_order.js:1683 msgid "Items Required" msgstr "" @@ -26266,15 +26390,15 @@ msgstr "" msgid "Items and Pricing" msgstr "" -#: erpnext/controllers/accounts_controller.py:4198 +#: erpnext/controllers/accounts_controller.py:4208 msgid "Items cannot be updated as Subcontracting Inward Order(s) exist against this Subcontracted Sales Order." msgstr "" -#: erpnext/controllers/accounts_controller.py:4191 +#: erpnext/controllers/accounts_controller.py:4201 msgid "Items cannot be updated as Subcontracting Order is created against the Purchase Order {0}." msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:1436 +#: erpnext/selling/doctype/sales_order/sales_order.js:1443 msgid "Items for Raw Material Request" msgstr "" @@ -26282,7 +26406,7 @@ msgstr "" msgid "Items not found." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1144 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1243 msgid "Items rate has been updated to zero as Allow Zero Valuation Rate is checked for the following items: {0}" msgstr "" @@ -26292,7 +26416,7 @@ msgstr "" msgid "Items to Be Repost" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1677 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1682 msgid "Items to Manufacture are required to pull the Raw Materials associated with it." msgstr "" @@ -26302,7 +26426,7 @@ msgid "Items to Order and Receive" msgstr "" #: erpnext/public/js/stock_reservation.js:72 -#: erpnext/selling/doctype/sales_order/sales_order.js:327 +#: erpnext/selling/doctype/sales_order/sales_order.js:334 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:226 msgid "Items to Reserve" msgstr "" @@ -26357,9 +26481,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:400 #: 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 @@ -26397,8 +26521,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 @@ -26421,7 +26545,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 "" @@ -26497,7 +26621,7 @@ msgstr "" msgid "Job Worker Warehouse" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:2652 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2678 msgid "Job card {0} created" msgstr "" @@ -26582,7 +26706,7 @@ msgstr "" msgid "Journal Entry Type" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:553 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:556 msgid "Journal Entry for Asset scrapping cannot be cancelled. Please restore the Asset." msgstr "" @@ -26595,7 +26719,7 @@ msgstr "" msgid "Journal Entry type should be set as Depreciation Entry for asset depreciation" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:723 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:726 msgid "Journal Entry {0} does not have account {1} or already matched against other voucher" msgstr "" @@ -26713,7 +26837,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 "" @@ -26868,7 +26992,7 @@ msgstr "" #: erpnext/buying/doctype/purchase_order_item/purchase_order_item.json #: erpnext/manufacturing/doctype/bom/bom.json #: erpnext/manufacturing/doctype/bom_creator/bom_creator.json -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py:106 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:123 #: erpnext/stock/doctype/item/item.json #: erpnext/stock/report/item_prices/item_prices.py:56 msgid "Last Purchase Rate" @@ -26915,7 +27039,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 "" @@ -27140,6 +27264,11 @@ msgstr "" msgid "Ledgers" msgstr "" +#. Label of the vouchers_posted (Int) field in DocType 'Repost Item Valuation' +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +msgid "Ledgers Posted" +msgstr "" + #. Label of the left_child (Link) field in DocType 'Bisect Nodes' #: erpnext/accounts/doctype/bisect_nodes/bisect_nodes.json msgid "Left Child" @@ -27170,8 +27299,8 @@ msgstr "" msgid "Legend" msgstr "" -#. Label of the length (Int) field in DocType 'Shipment Parcel' -#. Label of the length (Int) field in DocType 'Shipment Parcel Template' +#. Label of the length (Float) field in DocType 'Shipment Parcel' +#. Label of the length (Float) field in DocType 'Shipment Parcel Template' #: erpnext/stock/doctype/shipment_parcel/shipment_parcel.json #: erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json msgid "Length (cm)" @@ -27286,7 +27415,7 @@ msgstr "" msgid "Link to Material Request" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:451 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:452 #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.js:80 msgid "Link to Material Requests" msgstr "" @@ -27496,7 +27625,7 @@ msgstr "" #. Label of the lost_reasons_section (Section Break) field in DocType #. 'Quotation' #: erpnext/crm/doctype/opportunity/opportunity.json -#: erpnext/crm/report/lost_opportunity/lost_opportunity.py:49 +#: erpnext/crm/report/lost_opportunity/lost_opportunity.py:55 #: erpnext/public/js/utils/sales_common.js:596 #: erpnext/selling/doctype/quotation/quotation.json msgid "Lost Reasons" @@ -27583,7 +27712,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:200 msgid "Loyalty Points: {0}" msgstr "" @@ -27597,7 +27726,7 @@ msgstr "" #: erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.json #: erpnext/accounts/doctype/loyalty_program/loyalty_program.json #: erpnext/accounts/doctype/pos_invoice/pos_invoice.json -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1176 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1202 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/selling/doctype/customer/customer.json #: erpnext/selling/page/point_of_sale/pos_item_cart.js:952 @@ -27794,7 +27923,7 @@ msgstr "" #: erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.json #: erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js:81 #: erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json -#: erpnext/selling/doctype/sales_order/sales_order.js:1114 +#: erpnext/selling/doctype/sales_order/sales_order.js:1121 #: erpnext/support/workspace/support/support.json #: erpnext/workspace_sidebar/crm.json erpnext/workspace_sidebar/support.json msgid "Maintenance Schedule" @@ -27901,7 +28030,7 @@ msgstr "" #: erpnext/crm/workspace/crm/crm.json #: erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js:87 #: erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json -#: erpnext/selling/doctype/sales_order/sales_order.js:1107 +#: erpnext/selling/doctype/sales_order/sales_order.js:1114 #: erpnext/support/doctype/warranty_claim/warranty_claim.js:47 #: erpnext/support/workspace/support/support.json #: erpnext/workspace_sidebar/crm.json erpnext/workspace_sidebar/support.json @@ -27924,9 +28053,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:536 -#: 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:544 +#: erpnext/manufacturing/doctype/work_order/work_order.js:839 +#: erpnext/manufacturing/doctype/work_order/work_order.js:873 #: erpnext/setup/doctype/vehicle/vehicle.json msgid "Make" msgstr "" @@ -27984,12 +28113,12 @@ msgstr "" msgid "Make Serial No / Batch from Work Order" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:101 +#: erpnext/manufacturing/doctype/job_card/job_card.js:109 #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:256 msgid "Make Stock Entry" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:410 +#: erpnext/manufacturing/doctype/job_card/job_card.js:418 msgid "Make Subcontracting PO" msgstr "" @@ -28053,7 +28182,7 @@ msgstr "" msgid "Mandatory Accounting Dimension" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1882 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1879 msgid "Mandatory Field" msgstr "" @@ -28073,11 +28202,11 @@ msgstr "" msgid "Mandatory Missing" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:637 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:634 msgid "Mandatory Purchase Order" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:658 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:655 msgid "Mandatory Purchase Receipt" msgstr "" @@ -28152,8 +28281,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:1324 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1340 #: 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 @@ -28193,7 +28322,7 @@ msgstr "" #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json #: erpnext/buying/doctype/purchase_order_item/purchase_order_item.json #: erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py:70 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:110 #: erpnext/stock/doctype/item/item.json #: erpnext/stock/doctype/item_manufacturer/item_manufacturer.json #: erpnext/stock/doctype/manufacturer/manufacturer.json @@ -28223,7 +28352,7 @@ msgstr "" #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json #: erpnext/buying/doctype/purchase_order_item/purchase_order_item.json #: erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py:76 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:113 #: erpnext/stock/doctype/item_manufacturer/item_manufacturer.json #: erpnext/stock/doctype/material_request_item/material_request_item.json #: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -28257,7 +28386,7 @@ msgstr "" #: erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json #: erpnext/manufacturing/workspace/manufacturing/manufacturing.json #: erpnext/selling/doctype/sales_order/sales_order_dashboard.py:29 -#: erpnext/setup/doctype/company/company.json erpnext/setup/install.py:363 +#: erpnext/setup/doctype/company/company.json erpnext/setup/install.py:364 #: erpnext/setup/setup_wizard/data/industry_type.txt:31 #: erpnext/stock/doctype/batch/batch.json erpnext/stock/doctype/item/item.json #: erpnext/stock/doctype/item_lead_time/item_lead_time.json @@ -28302,7 +28431,7 @@ msgstr "" msgid "Manufacturing Manager" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2385 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2589 msgid "Manufacturing Quantity is mandatory" msgstr "" @@ -28379,7 +28508,7 @@ msgstr "" msgid "Mapping Subcontracting Order ..." msgstr "" -#: erpnext/public/js/utils.js:1046 +#: erpnext/public/js/utils.js:1066 msgid "Mapping {0} ..." msgstr "" @@ -28525,7 +28654,7 @@ msgstr "" msgid "Material" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:857 +#: erpnext/manufacturing/doctype/work_order/work_order.js:864 msgid "Material Consumption" msgstr "" @@ -28533,12 +28662,12 @@ 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:1325 #: erpnext/stock/doctype/stock_entry_type/stock_entry_type.json msgid "Material Consumption for Manufacture" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.js:579 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:666 msgid "Material Consumption is not set in Manufacturing Settings." msgstr "" @@ -28606,21 +28735,21 @@ msgstr "" #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json #: erpnext/buying/doctype/purchase_order/purchase_order.js:519 #: erpnext/buying/doctype/purchase_order_item/purchase_order_item.json -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:360 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:361 #: erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.js:56 #: erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json #: erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.js:33 #: erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py:184 #: erpnext/buying/workspace/buying/buying.json -#: erpnext/manufacturing/doctype/job_card/job_card.js:160 +#: erpnext/manufacturing/doctype/job_card/job_card.js:168 #: erpnext/manufacturing/doctype/production_plan/production_plan.js:159 #: erpnext/manufacturing/doctype/production_plan/production_plan.json #: erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json #: erpnext/manufacturing/doctype/production_plan_material_request/production_plan_material_request.json #: erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json #: erpnext/manufacturing/doctype/work_order/work_order.json -#: erpnext/selling/doctype/sales_order/sales_order.js:1078 +#: erpnext/selling/doctype/sales_order/sales_order.js:1085 #: erpnext/selling/doctype/sales_order_item/sales_order_item.json #: erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py:36 #: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -28630,8 +28759,8 @@ msgstr "" #: erpnext/stock/doctype/pick_list/pick_list.json #: erpnext/stock/doctype/pick_list_item/pick_list_item.json #: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json -#: erpnext/stock/doctype/stock_entry/stock_entry.js:253 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:356 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:287 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:443 #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json #: erpnext/stock/workspace/stock/stock.json #: erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json @@ -28728,11 +28857,11 @@ msgstr "" msgid "Material Request used to make this Stock Entry" msgstr "" -#: erpnext/controllers/subcontracting_controller.py:1337 +#: erpnext/controllers/subcontracting_controller.py:1349 msgid "Material Request {0} is cancelled or stopped" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:1452 +#: erpnext/selling/doctype/sales_order/sales_order.js:1459 msgid "Material Request {0} submitted." msgstr "" @@ -28782,7 +28911,7 @@ msgstr "" #. Option for the 'Purpose' (Select) field in DocType 'Pick List' #. Option for the 'Purpose' (Select) field in DocType 'Stock Entry' #. Option for the 'Purpose' (Select) field in DocType 'Stock Entry Type' -#: erpnext/manufacturing/doctype/job_card/job_card.js:174 +#: erpnext/manufacturing/doctype/job_card/job_card.js:182 #: erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json #: erpnext/setup/setup_wizard/operations/install_fixtures.py:83 #: erpnext/stock/doctype/item/item.json @@ -28847,12 +28976,12 @@ msgstr "" msgid "Materials To Be Transferred" msgstr "" -#: erpnext/controllers/subcontracting_controller.py:1570 +#: 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 "" @@ -28921,8 +29050,11 @@ 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:1040 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1047 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1070 #: erpnext/stock/doctype/pick_list/pick_list.js:200 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:382 msgid "Max: {0}" msgstr "" @@ -28943,11 +29075,16 @@ msgstr "" msgid "Maximum Payment Amount" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3870 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:82 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:151 +msgid "Maximum Producible Items" +msgstr "" + +#: erpnext/stock/doctype/stock_entry/stock_entry.py:4104 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:4095 msgid "Maximum Samples - {0} have already been retained for Batch {1} and Item {2} in Batch {3}." msgstr "" @@ -29002,7 +29139,7 @@ msgstr "" msgid "Megawatt" msgstr "" -#: erpnext/stock/stock_ledger.py:2019 +#: erpnext/stock/stock_ledger.py:2059 msgid "Mention Valuation Rate in the Item master." msgstr "" @@ -29047,7 +29184,7 @@ msgstr "" msgid "Merge Similar Account Heads" msgstr "" -#: erpnext/public/js/utils.js:1078 +#: erpnext/public/js/utils.js:1098 msgid "Merge taxes from multiple documents" msgstr "" @@ -29365,7 +29502,7 @@ msgstr "" msgid "Miscellaneous Expenses" msgstr "" -#: erpnext/controllers/buying_controller.py:702 +#: erpnext/controllers/buying_controller.py:706 msgid "Mismatch" msgstr "" @@ -29375,9 +29512,9 @@ msgstr "" #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:97 #: erpnext/accounts/doctype/pos_profile/pos_profile.py:200 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:597 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2422 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3030 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:593 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2419 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3027 #: erpnext/assets/doctype/asset_category/asset_category.py:116 msgid "Missing Account" msgstr "" @@ -29403,7 +29540,7 @@ msgstr "" msgid "Missing Finance Book" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1680 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1777 msgid "Missing Finished Good" msgstr "" @@ -29411,11 +29548,11 @@ msgstr "" msgid "Missing Formula" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:991 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1083 msgid "Missing Item" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:443 +#: erpnext/setup/doctype/employee/employee.py:567 msgid "Missing Parameter" msgstr "" @@ -29427,11 +29564,11 @@ 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 "" -#: erpnext/stock/doctype/delivery_trip/delivery_trip.js:154 +#: erpnext/stock/doctype/delivery_trip/delivery_trip.js:156 msgid "Missing email template for dispatch. Please set one in Delivery Settings." msgstr "" @@ -29439,8 +29576,8 @@ msgstr "" msgid "Missing required filter: {0}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1183 -#: erpnext/manufacturing/doctype/work_order/work_order.py:1476 +#: erpnext/manufacturing/doctype/bom/bom.py:1218 +#: erpnext/manufacturing/doctype/work_order/work_order.py:1477 msgid "Missing value" msgstr "" @@ -29675,11 +29812,11 @@ 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 "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1194 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1191 msgid "Multiple POS Opening Entry" msgstr "" @@ -29697,10 +29834,6 @@ msgstr "" msgid "Multiple Variants" msgstr "" -#: erpnext/stock/doctype/warehouse/warehouse.py:152 -msgid "Multiple Warehouse Accounts" -msgstr "" - #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js:244 msgid "Multiple company fields available: {0}. Please select manually." msgstr "" @@ -29709,7 +29842,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:1784 msgid "Multiple items cannot be marked as finished item" msgstr "" @@ -29718,10 +29851,10 @@ msgid "Music" msgstr "" #. Label of the must_be_whole_number (Check) field in DocType 'UOM' -#: erpnext/manufacturing/doctype/work_order/work_order.py:1423 +#: erpnext/manufacturing/doctype/work_order/work_order.py:1424 #: erpnext/setup/doctype/uom/uom.json #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:267 -#: erpnext/utilities/transaction_base.py:567 +#: erpnext/utilities/transaction_base.py:568 msgid "Must be Whole Number" msgstr "" @@ -29752,7 +29885,7 @@ msgstr "" msgid "Name of Beneficiary" msgstr "" -#: erpnext/accounts/doctype/account/account_tree.js:126 +#: erpnext/accounts/doctype/account/account_tree.js:121 msgid "Name of new Account. Note: Please don't create accounts for Customers and Suppliers" msgstr "" @@ -29846,7 +29979,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:1608 #: erpnext/stock/serial_batch_bundle.py:1528 msgid "Negative Stock Error" msgstr "" @@ -30161,7 +30294,7 @@ msgstr "" msgid "Net total calculation precision loss" msgstr "" -#: erpnext/accounts/doctype/account/account_tree.js:124 +#: erpnext/accounts/doctype/account/account_tree.js:119 msgid "New Account Name" msgstr "" @@ -30203,7 +30336,7 @@ msgstr "" msgid "New Batch Qty" msgstr "" -#: erpnext/accounts/doctype/account/account_tree.js:113 +#: erpnext/accounts/doctype/account/account_tree.js:108 #: erpnext/accounts/doctype/cost_center/cost_center_tree.js:18 #: erpnext/setup/doctype/company/company_tree.js:23 msgid "New Company" @@ -30317,7 +30450,7 @@ msgstr "" msgid "New Task" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:206 +#: erpnext/manufacturing/doctype/bom/bom.js:244 msgid "New Version" msgstr "" @@ -30330,7 +30463,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 "" @@ -30380,7 +30513,7 @@ msgstr "" msgid "Next email will be sent on:" msgstr "" -#: erpnext/accounts/doctype/financial_report_template/financial_report_template.js:161 +#: erpnext/accounts/doctype/financial_report_template/financial_report_template.js:155 msgid "No Account Data row found" msgstr "" @@ -30397,7 +30530,7 @@ msgstr "" msgid "No Answer" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2527 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2524 msgid "No Customer found for Inter Company Transactions which represents company {0}" msgstr "" @@ -30406,7 +30539,7 @@ msgstr "" msgid "No Customers found with selected options." msgstr "" -#: erpnext/stock/doctype/delivery_trip/delivery_trip.js:144 +#: erpnext/stock/doctype/delivery_trip/delivery_trip.js:146 msgid "No Delivery Note selected for Customer {}" msgstr "" @@ -30426,15 +30559,15 @@ msgstr "" msgid "No Item with Serial No {0}" msgstr "" -#: erpnext/controllers/subcontracting_controller.py:1488 +#: erpnext/controllers/subcontracting_controller.py:1500 msgid "No Items selected for transfer." msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:1226 +#: erpnext/selling/doctype/sales_order/sales_order.js:1233 msgid "No Items with Bill of Materials to Manufacture or all items already manufactured" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:1372 +#: erpnext/selling/doctype/sales_order/sales_order.js:1379 msgid "No Items with Bill of Materials." msgstr "" @@ -30454,14 +30587,14 @@ msgstr "" msgid "No POS Profile found. Please create a New POS Profile first" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1570 -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1630 -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1644 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1573 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1633 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1647 #: erpnext/stock/doctype/item/item.py:1388 msgid "No Permission" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:787 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:792 msgid "No Purchase Orders were created" msgstr "" @@ -30470,11 +30603,6 @@ msgstr "" msgid "No Records for these settings." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:337 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1105 -msgid "No Remarks" -msgstr "" - #: erpnext/public/js/utils/unreconcile.js:147 msgid "No Selection" msgstr "" @@ -30491,7 +30619,7 @@ msgstr "" msgid "No Summary" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2511 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2508 msgid "No Supplier found for Inter Company Transactions which represents company {0}" msgstr "" @@ -30515,13 +30643,13 @@ msgstr "" msgid "No Unreconciled Payments found for this party" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:784 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:789 #: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py:249 msgid "No Work Orders were created" msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:829 -#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:860 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:832 +#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:897 msgid "No accounting entries for the following warehouses" msgstr "" @@ -30569,7 +30697,7 @@ msgstr "" msgid "No employee was scheduled for call popup" msgstr "" -#: erpnext/controllers/subcontracting_controller.py:1379 +#: erpnext/controllers/subcontracting_controller.py:1391 msgid "No item available for transfer." msgstr "" @@ -30594,7 +30722,7 @@ msgstr "" msgid "No matches occurred via auto reconciliation" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1036 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1041 msgid "No material request created" msgstr "" @@ -30606,7 +30734,7 @@ msgstr "" msgid "No more children on Right" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:600 +#: erpnext/selling/doctype/sales_order/sales_order.js:607 msgid "No of Deliveries" msgstr "" @@ -30627,6 +30755,12 @@ msgstr "" msgid "No of Interactions" msgstr "" +#. Label of the total_reposting_count (Int) field in DocType 'Repost Item +#. Valuation' +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +msgid "No of Items to Repost" +msgstr "" + #. Label of the no_of_months_exp (Int) field in DocType 'Item' #: erpnext/stock/doctype/item/item.json msgid "No of Months (Expense)" @@ -30676,7 +30810,7 @@ msgstr "" msgid "No open Material Requests found for the given criteria." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1188 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1185 msgid "No open POS Opening Entry found for POS Profile {0}." msgstr "" @@ -30704,7 +30838,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 "" @@ -30765,7 +30899,7 @@ msgstr "" msgid "No values" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2575 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2572 msgid "No {0} found for Inter Company Transactions." msgstr "" @@ -30806,7 +30940,7 @@ msgstr "" msgid "Non Profit" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1590 +#: erpnext/manufacturing/doctype/bom/bom.py:1639 msgid "Non stock items" msgstr "" @@ -30935,7 +31069,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 "" @@ -30955,7 +31089,7 @@ msgstr "" msgid "Note: To merge the items, create a separate Stock Reconciliation for the old item {0}" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1027 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1030 msgid "Note: {0}" msgstr "" @@ -31012,7 +31146,7 @@ msgstr "" msgid "Notice (days)" msgstr "" -#: erpnext/stock/doctype/delivery_trip/delivery_trip.js:45 +#: erpnext/stock/doctype/delivery_trip/delivery_trip.js:47 msgid "Notify Customers via Email" msgstr "" @@ -31111,7 +31245,7 @@ msgstr "" msgid "Number of intervals for the interval field e.g if Interval is 'Days' and Billing Interval Count is 3, invoices will be generated every 3 days" msgstr "" -#: erpnext/accounts/doctype/account/account_tree.js:134 +#: erpnext/accounts/doctype/account/account_tree.js:129 msgid "Number of new Account, it will be included in the account name as a prefix" msgstr "" @@ -31317,7 +31451,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:751 msgid "Once the Work Order is Closed. It can't be resumed." msgstr "" @@ -31409,11 +31543,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:1339 msgid "Only one {0} entry can be created against the Work Order {1}" msgstr "" @@ -31650,14 +31784,14 @@ msgstr "" msgid "Opening Entry can not be created after Period Closing Voucher is created." msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:286 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:304 msgid "Opening Invoice Creation In Progress" msgstr "" #. Name of a DocType #. Label of a Link in the Invoicing Workspace #. Label of a Link in the Home Workspace -#: erpnext/accounts/doctype/account/account_tree.js:206 +#: erpnext/accounts/doctype/account/account_tree.js:201 #: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json #: erpnext/accounts/workspace/invoicing/invoicing.json #: erpnext/setup/workspace/home/home.json @@ -31669,7 +31803,7 @@ msgstr "" msgid "Opening Invoice Creation Tool Item" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:100 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:106 msgid "Opening Invoice Item" msgstr "" @@ -31678,8 +31812,8 @@ msgstr "" msgid "Opening Invoice Tool" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1647 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1991 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1653 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1988 msgid "Opening Invoice has rounding adjustment of {0}.

    '{1}' account is required to post these values. Please set it in Company: {2}.

    Or, '{3}' can be enabled to not post any rounding adjustment." msgstr "" @@ -31687,7 +31821,7 @@ msgstr "" msgid "Opening Invoices" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js:140 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js:142 msgid "Opening Invoices Summary" msgstr "" @@ -31767,7 +31901,7 @@ msgstr "" msgid "Operating Cost Per BOM Quantity" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1677 +#: erpnext/manufacturing/doctype/bom/bom.py:1726 msgid "Operating Cost as per Work Order / BOM" msgstr "" @@ -31815,7 +31949,7 @@ msgstr "" msgid "Operation ID" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:333 +#: erpnext/manufacturing/doctype/work_order/work_order.js:336 msgid "Operation Id" msgstr "" @@ -31843,7 +31977,7 @@ msgstr "" msgid "Operation Time" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:1482 +#: erpnext/manufacturing/doctype/work_order/work_order.py:1483 msgid "Operation Time must be greater than 0 for Operation {0}" msgstr "" @@ -31858,11 +31992,11 @@ msgstr "" msgid "Operation time does not depend on quantity to produce" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:578 +#: erpnext/manufacturing/doctype/job_card/job_card.js:586 msgid "Operation {0} added multiple times in the work order {1}" msgstr "" -#: 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 "" @@ -31878,7 +32012,7 @@ msgstr "" #. Label of the operations (Table) field in DocType 'Work Order' #. Label of the operation (Section Break) field in DocType 'Email Digest' #: erpnext/manufacturing/doctype/bom/bom.json -#: erpnext/manufacturing/doctype/work_order/work_order.js:314 +#: erpnext/manufacturing/doctype/work_order/work_order.js:317 #: erpnext/manufacturing/doctype/work_order/work_order.json #: erpnext/setup/doctype/company/company.py:469 #: erpnext/setup/doctype/email_digest/email_digest.json @@ -31892,7 +32026,7 @@ msgstr "" msgid "Operations Routing" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1192 +#: erpnext/manufacturing/doctype/bom/bom.py:1227 msgid "Operations cannot be left blank" msgstr "" @@ -31941,7 +32075,7 @@ msgstr "" #. Label of the opportunity_name (Link) field in DocType 'Customer' #. Label of the opportunity (Link) field in DocType 'Quotation' #. Label of a Workspace Sidebar Item -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:384 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:385 #: erpnext/buying/doctype/request_for_quotation/request_for_quotation.json #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.json #: erpnext/crm/doctype/crm_settings/crm_settings.json @@ -31950,7 +32084,7 @@ msgstr "" #: erpnext/crm/doctype/prospect/prospect.js:20 #: erpnext/crm/doctype/prospect_opportunity/prospect_opportunity.json #: erpnext/crm/report/lead_details/lead_details.js:36 -#: erpnext/crm/report/lost_opportunity/lost_opportunity.py:17 +#: erpnext/crm/report/lost_opportunity/lost_opportunity.py:23 #: erpnext/public/js/communication.js:35 #: erpnext/selling/doctype/customer/customer.json #: erpnext/selling/doctype/quotation/quotation.js:155 @@ -31979,7 +32113,7 @@ msgstr "" #. Label of the opportunity_from (Link) field in DocType 'Opportunity' #: erpnext/crm/doctype/opportunity/opportunity.json #: erpnext/crm/report/lost_opportunity/lost_opportunity.js:42 -#: erpnext/crm/report/lost_opportunity/lost_opportunity.py:24 +#: erpnext/crm/report/lost_opportunity/lost_opportunity.py:30 msgid "Opportunity From" msgstr "" @@ -32032,7 +32166,7 @@ msgstr "" #. Name of a DocType #: erpnext/crm/doctype/opportunity/opportunity.json #: erpnext/crm/doctype/opportunity_type/opportunity_type.json -#: erpnext/crm/report/lost_opportunity/lost_opportunity.py:44 +#: erpnext/crm/report/lost_opportunity/lost_opportunity.py:50 #: erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js:52 #: erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py:48 #: erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js:64 @@ -32053,15 +32187,19 @@ msgstr "" msgid "Optimize Route" msgstr "" -#: erpnext/accounts/doctype/account/account_tree.js:183 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1017 +msgid "Optional. Select a specific manufacture entry to reverse." +msgstr "" + +#: erpnext/accounts/doctype/account/account_tree.js:178 msgid "Optional. Sets company's default currency, if not specified." msgstr "" -#: erpnext/accounts/doctype/account/account_tree.js:162 +#: erpnext/accounts/doctype/account/account_tree.js:157 msgid "Optional. This setting will be used to filter in various transactions." msgstr "" -#: erpnext/accounts/doctype/account/account_tree.js:170 +#: erpnext/accounts/doctype/account/account_tree.js:165 msgid "Optional. Used with Financial Report Template" msgstr "" @@ -32335,7 +32473,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 "" @@ -32351,7 +32489,7 @@ msgstr "" msgid "Out of stock" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1201 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1198 #: erpnext/selling/page/point_of_sale/pos_controller.js:208 msgid "Outdated POS Opening Entry" msgstr "" @@ -32457,7 +32595,7 @@ msgstr "" msgid "Over Billing Allowance (%)" msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:1317 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:1344 msgid "Over Billing Allowance exceeded for Purchase Receipt Item {0} ({1}) by {2}%" msgstr "" @@ -32560,7 +32698,7 @@ msgstr "" msgid "Overlap in scoring between {0} and {1}" msgstr "" -#: erpnext/accounts/doctype/shipping_rule/shipping_rule.py:199 +#: erpnext/accounts/doctype/shipping_rule/shipping_rule.py:201 msgid "Overlapping conditions found between:" msgstr "" @@ -32785,7 +32923,7 @@ msgstr "" msgid "POS Opening Entry" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1202 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1199 msgid "POS Opening Entry - {0} is outdated. Please close the POS and create a new POS Opening Entry." msgstr "" @@ -32806,7 +32944,7 @@ msgstr "" msgid "POS Opening Entry Exists" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1187 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1184 msgid "POS Opening Entry Missing" msgstr "" @@ -32842,7 +32980,7 @@ msgstr "" msgid "POS Profile" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1195 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1192 msgid "POS Profile - {0} has multiple open POS Opening Entries. Please close or cancel the existing entries before proceeding." msgstr "" @@ -32860,11 +32998,11 @@ msgstr "" msgid "POS Profile doesn't match {}" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1155 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1152 msgid "POS Profile is mandatory to mark this invoice as POS Transaction." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1384 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1381 msgid "POS Profile required to make POS Entry" msgstr "" @@ -33098,8 +33236,8 @@ msgstr "" msgid "Paid To Account Type" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:327 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1151 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:326 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1148 msgid "Paid amount + Write Off Amount can not be greater than Grand Total" msgstr "" @@ -33255,7 +33393,7 @@ msgstr "" msgid "Parent Row No" msgstr "" -#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:534 +#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:535 msgid "Parent Row No not found for {0}" msgstr "" @@ -33302,7 +33440,7 @@ msgstr "" msgid "Parsed file is not in valid MT940 format or contains no transactions." msgstr "" -#: erpnext/edi/doctype/code_list/code_list_import.py:39 +#: erpnext/edi/doctype/code_list/code_list_import.py:45 msgid "Parsing Error" msgstr "" @@ -33311,7 +33449,7 @@ msgstr "" msgid "Partial Material Transferred" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1174 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1171 msgid "Partial Payment in POS Transactions are not allowed." msgstr "" @@ -33475,8 +33613,6 @@ msgstr "" #. Label of the party (Dynamic Link) field in DocType 'Journal Entry Account' #. Label of the party (Dynamic Link) field in DocType 'Journal Entry Template #. Account' -#. Label of the party (Dynamic Link) field in DocType 'Opening Invoice Creation -#. Tool Item' #. Label of the party (Dynamic Link) field in DocType 'Payment Entry' #. Label of the party (Dynamic Link) field in DocType 'Payment Ledger Entry' #. Label of the party (Dynamic Link) field in DocType 'Payment Reconciliation' @@ -33495,7 +33631,6 @@ msgstr "" #: erpnext/accounts/doctype/gl_entry/gl_entry.json #: erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json #: erpnext/accounts/doctype/journal_entry_template_account/journal_entry_template_account.json -#: erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json #: erpnext/accounts/doctype/payment_entry/payment_entry.json #: erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json #: erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json @@ -33530,7 +33665,7 @@ msgstr "" #: erpnext/crm/doctype/appointment/appointment.json #: erpnext/crm/doctype/opportunity/opportunity.json #: erpnext/crm/report/lost_opportunity/lost_opportunity.js:55 -#: erpnext/crm/report/lost_opportunity/lost_opportunity.py:31 +#: erpnext/crm/report/lost_opportunity/lost_opportunity.py:37 #: erpnext/public/js/bank_reconciliation_tool/data_table_manager.js:50 #: erpnext/public/js/bank_reconciliation_tool/dialog_manager.js:135 #: erpnext/selling/doctype/quotation/quotation.json @@ -33567,7 +33702,7 @@ msgstr "" msgid "Party Account No. (Bank Statement)" msgstr "" -#: erpnext/controllers/accounts_controller.py:2452 +#: erpnext/controllers/accounts_controller.py:2462 msgid "Party Account {0} currency ({1}) and document currency ({2}) should be same" msgstr "" @@ -33595,6 +33730,12 @@ msgstr "" msgid "Party IBAN (Bank Statement)" msgstr "" +#. Label of the party (Dynamic Link) field in DocType 'Opening Invoice Creation +#. Tool Item' +#: erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json +msgid "Party ID" +msgstr "" + #. Label of the section_break_7 (Section Break) field in DocType 'Pricing Rule' #. Label of the section_break_8 (Section Break) field in DocType 'Promotional #. Scheme' @@ -33617,13 +33758,16 @@ msgstr "" msgid "Party Mismatch" msgstr "" +#. Label of the party_name (Data) field in DocType 'Opening Invoice Creation +#. Tool Item' #. Label of the party_name (Data) field in DocType 'Payment Entry' #. Label of the party_name (Data) field in DocType 'Payment Request' #. Label of the party_name (Dynamic Link) field in DocType 'Contract' #. Label of the party (Dynamic Link) field in DocType 'Party Specific Item' +#: erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json #: erpnext/accounts/doctype/payment_entry/payment_entry.json #: erpnext/accounts/doctype/payment_request/payment_request.json -#: erpnext/accounts/report/general_ledger/general_ledger.js:110 +#: 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 @@ -33780,7 +33924,7 @@ msgstr "" msgid "Pause" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:255 +#: erpnext/manufacturing/doctype/job_card/job_card.js:263 msgid "Pause Job" msgstr "" @@ -33866,7 +34010,7 @@ msgstr "" #: erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js:42 #: erpnext/buying/doctype/purchase_order/purchase_order.js:421 #: erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py:24 -#: erpnext/selling/doctype/sales_order/sales_order.js:1159 +#: erpnext/selling/doctype/sales_order/sales_order.js:1166 #: erpnext/selling/doctype/sales_order/sales_order_dashboard.py:31 msgid "Payment" msgstr "" @@ -34028,7 +34172,7 @@ msgstr "" msgid "Payment Gateway Account" msgstr "" -#: erpnext/accounts/utils.py:1437 +#: erpnext/accounts/utils.py:1509 msgid "Payment Gateway Account not created, please create one manually." msgstr "" @@ -34218,7 +34362,7 @@ msgstr "" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:135 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:139 #: erpnext/buying/doctype/purchase_order/purchase_order.js:429 -#: erpnext/selling/doctype/sales_order/sales_order.js:1152 +#: erpnext/selling/doctype/sales_order/sales_order.js:1159 #: erpnext/workspace_sidebar/invoicing.json #: erpnext/workspace_sidebar/payments.json msgid "Payment Request" @@ -34274,7 +34418,7 @@ msgstr "" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/buying/doctype/purchase_order/purchase_order.json -#: erpnext/controllers/accounts_controller.py:2732 +#: erpnext/controllers/accounts_controller.py:2742 #: erpnext/selling/doctype/quotation/quotation.json #: erpnext/selling/doctype/sales_order/sales_order.json msgid "Payment Schedule" @@ -34284,7 +34428,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 "" @@ -34310,7 +34454,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" @@ -34419,7 +34563,7 @@ msgstr "" msgid "Payment Unlink Error" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:895 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:898 msgid "Payment against {0} {1} cannot be greater than Outstanding Amount {2}" msgstr "" @@ -34431,7 +34575,7 @@ msgstr "" msgid "Payment methods are mandatory. Please add at least one payment method." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3034 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3031 msgid "Payment methods refreshed. Please review before proceeding." msgstr "" @@ -34557,9 +34701,9 @@ msgstr "" #. Label of the pending_qty (Float) field in DocType 'Production Plan Item' #: erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py:254 #: erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json -#: erpnext/manufacturing/doctype/work_order/work_order.js:338 +#: erpnext/manufacturing/doctype/work_order/work_order.js:341 #: erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py:182 -#: erpnext/selling/doctype/sales_order/sales_order.js:1645 +#: erpnext/selling/doctype/sales_order/sales_order.js:1652 #: erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py:45 msgid "Pending Qty" msgstr "" @@ -34594,7 +34738,7 @@ msgstr "" msgid "Pending activities for today" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:254 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:275 msgid "Pending processing" msgstr "" @@ -34902,7 +35046,7 @@ msgstr "" #. Label of the phone_no (Data) field in DocType 'Company' #. Label of the phone_no (Data) field in DocType 'Warehouse' -#: erpnext/public/js/print.js:77 erpnext/setup/doctype/company/company.json +#: erpnext/public/js/print.js:79 erpnext/setup/doctype/company/company.json #: erpnext/stock/doctype/warehouse/warehouse.json msgid "Phone No" msgstr "" @@ -34921,7 +35065,7 @@ msgstr "" #. Reservation Entry' #. Label of a Link in the Stock Workspace #. Label of a Workspace Sidebar Item -#: erpnext/selling/doctype/sales_order/sales_order.js:1022 +#: erpnext/selling/doctype/sales_order/sales_order.js:1029 #: erpnext/stock/doctype/delivery_note/delivery_note.js:199 #: erpnext/stock/doctype/material_request/material_request.js:156 #: erpnext/stock/doctype/pick_list/pick_list.json @@ -34932,7 +35076,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 "" @@ -35251,7 +35395,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 "" @@ -35263,6 +35407,7 @@ msgstr "" msgid "Please Select a Company." msgstr "" +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:418 #: erpnext/stock/doctype/delivery_note/delivery_note.js:162 #: erpnext/stock/doctype/delivery_note/delivery_note.js:204 msgid "Please Select a Customer" @@ -35278,7 +35423,7 @@ msgstr "" msgid "Please Set Priority" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:155 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:171 msgid "Please Set Supplier Group in Buying Settings." msgstr "" @@ -35294,11 +35439,11 @@ 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 "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:210 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:212 msgid "Please add Request for Quotation to the sidebar in Portal Settings." msgstr "" @@ -35306,7 +35451,7 @@ msgstr "" msgid "Please add Root Account for - {0}" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:302 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:320 msgid "Please add a Temporary Opening account in Chart of Accounts" msgstr "" @@ -35318,7 +35463,7 @@ msgstr "" msgid "Please add the Bank Account column" msgstr "" -#: erpnext/accounts/doctype/account/account_tree.js:244 +#: erpnext/accounts/doctype/account/account_tree.js:239 msgid "Please add the account to root level Company - {0}" msgstr "" @@ -35338,7 +35483,7 @@ msgstr "" msgid "Please attach CSV file" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3173 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3170 msgid "Please cancel and amend the Payment Entry" msgstr "" @@ -35356,7 +35501,7 @@ msgstr "" msgid "Please capitalize this asset before submitting." msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:969 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:972 msgid "Please check Multi Currency option to allow accounts with other currency" msgstr "" @@ -35364,7 +35509,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 "" @@ -35372,7 +35517,7 @@ msgstr "" msgid "Please check the 'Enable Serial and Batch No for Item' checkbox in the {0} to make Serial and Batch Bundle for the item." msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:539 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:562 msgid "Please check the error message and take necessary actions to fix the error and then restart the reposting again." msgstr "" @@ -35397,7 +35542,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 "" @@ -35405,7 +35550,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 "" @@ -35417,7 +35562,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 "" @@ -35437,7 +35582,7 @@ msgstr "" msgid "Please delete Product Bundle {0}, before merging {1} into {2}" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:556 +#: erpnext/assets/doctype/asset/depreciation.py:559 msgid "Please disable workflow temporarily for Journal Entry {0}" msgstr "" @@ -35457,7 +35602,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 "" @@ -35473,11 +35618,11 @@ msgstr "" msgid "Please enable {} in {} to allow same item in multiple rows" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:377 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:373 msgid "Please ensure that the {0} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:385 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:381 msgid "Please ensure that the {0} account {1} is a Payable account. You can change the account type to Payable or select a different account." msgstr "" @@ -35489,12 +35634,12 @@ msgstr "" msgid "Please ensure {} account {} is a Receivable account." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:699 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:761 msgid "Please enter Difference Account or set default Stock Adjustment Account for company {0}" msgstr "" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:554 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1286 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1283 msgid "Please enter Account for Change Amount" msgstr "" @@ -35523,11 +35668,11 @@ msgid "Please enter Expense Account" msgstr "" #: erpnext/assets/doctype/asset_capitalization/asset_capitalization.js:84 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:87 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:97 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 "" @@ -35543,10 +35688,6 @@ msgstr "" msgid "Please enter Planned Qty for Item {0} at row {1}" msgstr "" -#: erpnext/setup/doctype/employee/employee.js:83 -msgid "Please enter Preferred Contact Email" -msgstr "" - #: erpnext/manufacturing/doctype/work_order/work_order.js:73 msgid "Please enter Production Item first" msgstr "" @@ -35555,11 +35696,11 @@ 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 "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1033 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1036 msgid "Please enter Reference date" msgstr "" @@ -35583,20 +35724,20 @@ msgstr "" msgid "Please enter Warehouse and Date" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:662 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1282 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:659 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1279 msgid "Please enter Write Off Account" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:717 +#: erpnext/selling/doctype/sales_order/sales_order.js:724 msgid "Please enter a valid number of deliveries" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:658 +#: erpnext/selling/doctype/sales_order/sales_order.js:665 msgid "Please enter a valid quantity" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:652 +#: erpnext/selling/doctype/sales_order/sales_order.js:659 msgid "Please enter at least one delivery date and quantity" msgstr "" @@ -35604,7 +35745,7 @@ msgstr "" msgid "Please enter company name first" msgstr "" -#: erpnext/controllers/accounts_controller.py:2958 +#: erpnext/controllers/accounts_controller.py:2968 msgid "Please enter default currency in Company Master" msgstr "" @@ -35624,7 +35765,7 @@ msgstr "" msgid "Please enter quantity for item {0}" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:183 +#: erpnext/setup/doctype/employee/employee.py:297 msgid "Please enter relieving date." msgstr "" @@ -35636,7 +35777,7 @@ msgstr "" msgid "Please enter the company name to confirm" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:713 +#: erpnext/selling/doctype/sales_order/sales_order.js:720 msgid "Please enter the first delivery date" msgstr "" @@ -35644,7 +35785,7 @@ msgstr "" msgid "Please enter the phone number first" msgstr "" -#: erpnext/controllers/buying_controller.py:1184 +#: erpnext/controllers/buying_controller.py:1189 msgid "Please enter the {schedule_date}." msgstr "" @@ -35652,7 +35793,7 @@ msgstr "" msgid "Please enter valid Financial Year Start and End Dates" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:219 +#: erpnext/setup/doctype/employee/employee.py:333 msgid "Please enter {0}" msgstr "" @@ -35692,7 +35833,7 @@ msgstr "" msgid "Please import accounts against parent company or enable {} in company master." msgstr "" -#: erpnext/setup/doctype/employee/employee.py:180 +#: erpnext/setup/doctype/employee/employee.py:294 msgid "Please make sure the employees above report to another Active employee." msgstr "" @@ -35746,7 +35887,7 @@ msgstr "" msgid "Please save first" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:859 +#: erpnext/selling/doctype/sales_order/sales_order.js:866 msgid "Please save the Sales Order before adding a delivery schedule." msgstr "" @@ -35754,7 +35895,7 @@ msgstr "" msgid "Please select Template Type to download template" msgstr "" -#: erpnext/controllers/taxes_and_totals.py:809 +#: erpnext/controllers/taxes_and_totals.py:850 #: erpnext/public/js/controllers/taxes_and_totals.js:796 msgid "Please select Apply Discount On" msgstr "" @@ -35767,7 +35908,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 "" @@ -35847,7 +35988,7 @@ msgstr "" msgid "Please select Posting Date first" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1243 +#: erpnext/manufacturing/doctype/bom/bom.py:1291 msgid "Please select Price List" msgstr "" @@ -35871,28 +36012,28 @@ 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:1703 msgid "Please select Subcontracting Order instead of Purchase Order {0}" msgstr "" -#: erpnext/controllers/accounts_controller.py:2807 +#: erpnext/controllers/accounts_controller.py:2817 msgid "Please select Unrealized Profit / Loss account or add default Unrealized Profit / Loss account account for company {0}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1498 +#: erpnext/manufacturing/doctype/bom/bom.py: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 "" @@ -35916,7 +36057,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 "" @@ -35944,7 +36085,7 @@ msgstr "" msgid "Please select a field to edit from numpad" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:709 +#: erpnext/selling/doctype/sales_order/sales_order.js:716 msgid "Please select a frequency for delivery schedule" msgstr "" @@ -35985,19 +36126,19 @@ 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 "" -#: erpnext/selling/doctype/sales_order/sales_order.js:1296 +#: erpnext/selling/doctype/sales_order/sales_order.js:1303 msgid "Please select atleast one item to continue" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:381 +#: erpnext/manufacturing/doctype/work_order/work_order.js:384 msgid "Please select atleast one operation to create Job Card" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1709 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1712 msgid "Please select correct account" msgstr "" @@ -36015,13 +36156,13 @@ msgid "Please select item code" msgstr "" #: erpnext/public/js/stock_reservation.js:212 -#: erpnext/selling/doctype/sales_order/sales_order.js:419 +#: erpnext/selling/doctype/sales_order/sales_order.js:426 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:301 msgid "Please select items to reserve." msgstr "" #: erpnext/public/js/stock_reservation.js:290 -#: erpnext/selling/doctype/sales_order/sales_order.js:523 +#: erpnext/selling/doctype/sales_order/sales_order.js:530 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:399 msgid "Please select items to unreserve." msgstr "" @@ -36035,7 +36176,7 @@ msgstr "" msgid "Please select rows to create Reposting Entries" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:92 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:98 msgid "Please select the Company" msgstr "" @@ -36079,11 +36220,11 @@ msgstr "" msgid "Please set 'Apply Additional Discount On'" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:783 +#: erpnext/assets/doctype/asset/depreciation.py:786 msgid "Please set 'Asset Depreciation Cost Center' in Company {0}" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:781 +#: erpnext/assets/doctype/asset/depreciation.py:784 msgid "Please set 'Gain/Loss Account on Asset Disposal' in Company {0}" msgstr "" @@ -36095,7 +36236,7 @@ msgstr "" msgid "Please set Account" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1882 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1879 msgid "Please set Account for Change Amount" msgstr "" @@ -36117,7 +36258,7 @@ msgstr "" #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js:58 #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js:68 #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js:78 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:789 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:880 msgid "Please set Company" msgstr "" @@ -36125,7 +36266,7 @@ msgstr "" msgid "Please set Customer Address to determine if the transaction is an export." msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:745 +#: erpnext/assets/doctype/asset/depreciation.py:748 msgid "Please set Depreciation related Accounts in Asset Category {0} or Company {1}" msgstr "" @@ -36143,15 +36284,15 @@ msgstr "" msgid "Please set Fiscal Code for the public administration '%s'" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:731 +#: erpnext/assets/doctype/asset/depreciation.py:734 msgid "Please set Fixed Asset Account in Asset Category {0}" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:594 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:590 msgid "Please set Fixed Asset Account in {} against {}." msgstr "" -#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:279 +#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:281 msgid "Please set Parent Row No for item {0}" msgstr "" @@ -36193,11 +36334,11 @@ msgstr "" msgid "Please set a default Holiday List for Company {0}" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:270 +#: erpnext/setup/doctype/employee/employee.py:384 msgid "Please set a default Holiday List for Employee {0} or Company {1}" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1116 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1117 msgid "Please set account in Warehouse {0}" msgstr "" @@ -36226,23 +36367,23 @@ msgstr "" msgid "Please set both the Tax ID and Fiscal Code on Company {0}" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2419 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2416 msgid "Please set default Cash or Bank account in Mode of Payment {0}" msgstr "" #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:94 #: erpnext/accounts/doctype/pos_profile/pos_profile.py:197 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3027 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3024 msgid "Please set default Cash or Bank account in Mode of Payment {}" msgstr "" #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:96 #: erpnext/accounts/doctype/pos_profile/pos_profile.py:199 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3029 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3026 msgid "Please set default Cash or Bank account in Mode of Payments {}" msgstr "" -#: erpnext/accounts/utils.py:2451 +#: erpnext/accounts/utils.py:2527 msgid "Please set default Exchange Gain/Loss Account in Company {}" msgstr "" @@ -36271,7 +36412,7 @@ msgstr "" msgid "Please set filter based on Item or Warehouse" msgstr "" -#: erpnext/controllers/accounts_controller.py:2368 +#: erpnext/controllers/accounts_controller.py:2378 msgid "Please set one of the following:" msgstr "" @@ -36279,7 +36420,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 "" @@ -36287,19 +36428,19 @@ msgstr "" msgid "Please set the Customer Address" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:170 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py: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:668 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 "" @@ -36330,7 +36471,7 @@ msgstr "" msgid "Please set {0} for address {1}" msgstr "" -#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:232 +#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:234 msgid "Please set {0} in BOM Creator {1}" msgstr "" @@ -36346,7 +36487,7 @@ msgstr "" msgid "Please setup and enable a group account with the Account Type - {0} for the company {1}" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:354 +#: erpnext/assets/doctype/asset/depreciation.py:357 msgid "Please share this email with your support team so that they can find and fix the issue." msgstr "" @@ -36356,11 +36497,11 @@ msgstr "" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.js:120 #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:419 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:526 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:613 msgid "Please specify Company to proceed" msgstr "" -#: erpnext/controllers/accounts_controller.py:3189 +#: erpnext/controllers/accounts_controller.py:3199 #: erpnext/public/js/controllers/accounts.js:117 msgid "Please specify a valid Row ID for row {0} in table {1}" msgstr "" @@ -36381,7 +36522,7 @@ msgstr "" msgid "Please specify from/to range" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:253 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:274 msgid "Please try again in an hour." msgstr "" @@ -36436,7 +36577,7 @@ msgstr "" msgid "Portal Users" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:406 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:407 msgid "Possible Supplier" msgstr "" @@ -36610,7 +36751,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 "" @@ -36673,10 +36814,14 @@ msgstr "" msgid "Posting Time" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2333 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2537 msgid "Posting date and posting time is mandatory" msgstr "" +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:99 +msgid "Posting date is required" +msgstr "" + #: erpnext/controllers/sales_and_purchase_return.py:66 msgid "Posting timestamp must be after {0}" msgstr "" @@ -36814,7 +36959,7 @@ msgid "Preventive Maintenance" msgstr "" #. Label of the preview (Button) field in DocType 'Request for Quotation' -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:266 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:267 #: erpnext/buying/doctype/request_for_quotation/request_for_quotation.json msgid "Preview Email" msgstr "" @@ -37074,7 +37219,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 "" @@ -37449,11 +37594,18 @@ msgstr "" msgid "Process Loss" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1226 +#. 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 @@ -37461,17 +37613,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:332 +#: erpnext/manufacturing/doctype/job_card/job_card.js:340 msgid "Process Loss Quantity" msgstr "" @@ -37610,7 +37766,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 @@ -37618,7 +37774,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 "" @@ -38050,7 +38206,7 @@ msgstr "" msgid "Project wise Stock Tracking " msgstr "" -#: erpnext/controllers/trends.py:421 +#: erpnext/controllers/trends.py:429 msgid "Project-wise data is not available for Quotation" msgstr "" @@ -38328,7 +38484,7 @@ msgstr "" #: erpnext/accounts/doctype/tax_rule/tax_rule.json #: erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json #: erpnext/projects/doctype/project/project_dashboard.py:16 -#: erpnext/setup/doctype/company/company.py:463 erpnext/setup/install.py:377 +#: erpnext/setup/doctype/company/company.py:463 erpnext/setup/install.py:378 #: erpnext/stock/doctype/item/item.json #: erpnext/stock/doctype/item_lead_time/item_lead_time.json #: erpnext/stock/doctype/item_reorder/item_reorder.json @@ -38444,7 +38600,7 @@ msgstr "" #: erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js:30 #: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json #: erpnext/stock/doctype/quality_inspection/quality_inspection.json -#: erpnext/stock/doctype/stock_entry/stock_entry.js:337 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:424 #: erpnext/workspace_sidebar/buying.json #: erpnext/workspace_sidebar/invoicing.json msgid "Purchase Invoice" @@ -38483,12 +38639,12 @@ msgstr "" msgid "Purchase Invoice cannot be made against an existing asset {0}" msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:449 -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:463 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:452 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:466 msgid "Purchase Invoice {0} is already submitted" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1932 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1938 msgid "Purchase Invoices" msgstr "" @@ -38527,12 +38683,12 @@ 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 -#: erpnext/selling/doctype/sales_order/sales_order.js:181 -#: erpnext/selling/doctype/sales_order/sales_order.js:1097 +#: erpnext/selling/doctype/sales_order/sales_order.js:188 +#: erpnext/selling/doctype/sales_order/sales_order.js:1104 #: erpnext/selling/doctype/sales_order_item/sales_order_item.json #: erpnext/setup/doctype/authorization_rule/authorization_rule.json #: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -38602,7 +38758,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 "" @@ -38615,11 +38771,11 @@ msgstr "" msgid "Purchase Order Pricing Rule" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:633 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:630 msgid "Purchase Order Required" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:628 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:625 msgid "Purchase Order Required for item {}" msgstr "" @@ -38633,11 +38789,11 @@ msgstr "" msgid "Purchase Order Trends" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:1589 +#: erpnext/selling/doctype/sales_order/sales_order.js:1596 msgid "Purchase Order already created for all Sales Order items" msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:335 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:338 msgid "Purchase Order number required for Item {0}" msgstr "" @@ -38645,11 +38801,11 @@ msgstr "" msgid "Purchase Order {0} created" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:671 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:668 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 "" @@ -38664,7 +38820,7 @@ msgstr "" msgid "Purchase Orders Items Overdue" msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:286 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:285 msgid "Purchase Orders are not allowed for {0} due to a scorecard standing of {1}." msgstr "" @@ -38762,11 +38918,11 @@ msgstr "" msgid "Purchase Receipt No" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:654 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:651 msgid "Purchase Receipt Required" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:649 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:646 msgid "Purchase Receipt Required for item {}" msgstr "" @@ -38790,11 +38946,11 @@ 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 "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:678 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:675 msgid "Purchase Receipt {0} is not submitted" msgstr "" @@ -38912,14 +39068,14 @@ msgstr "" #: erpnext/manufacturing/doctype/master_production_schedule/master_production_schedule.js:163 #: erpnext/stock/doctype/material_request/material_request.json #: erpnext/stock/doctype/pick_list/pick_list.json -#: erpnext/stock/doctype/stock_entry/stock_entry.js:372 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:459 #: erpnext/stock/doctype/stock_entry/stock_entry.json #: erpnext/stock/doctype/stock_entry_type/stock_entry_type.json #: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json msgid "Purpose" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:516 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:578 msgid "Purpose must be one of {0}" msgstr "" @@ -38953,10 +39109,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' @@ -38981,16 +39137,16 @@ msgstr "" #: erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.json #: erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py:240 #: erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py:224 -#: erpnext/controllers/trends.py:268 erpnext/controllers/trends.py:280 -#: erpnext/controllers/trends.py:285 +#: erpnext/controllers/trends.py:276 erpnext/controllers/trends.py:288 +#: erpnext/controllers/trends.py:293 #: erpnext/crm/doctype/opportunity_item/opportunity_item.json -#: erpnext/manufacturing/doctype/bom/bom.js:1086 +#: erpnext/manufacturing/doctype/bom/bom.js:1105 #: 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 @@ -39000,15 +39156,15 @@ 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:869 #: 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 -#: erpnext/selling/doctype/sales_order/sales_order.js:494 -#: erpnext/selling/doctype/sales_order/sales_order.js:584 -#: erpnext/selling/doctype/sales_order/sales_order.js:631 -#: erpnext/selling/doctype/sales_order/sales_order.js:1272 -#: erpnext/selling/doctype/sales_order/sales_order.js:1425 +#: erpnext/selling/doctype/sales_order/sales_order.js:397 +#: erpnext/selling/doctype/sales_order/sales_order.js:501 +#: erpnext/selling/doctype/sales_order/sales_order.js:591 +#: erpnext/selling/doctype/sales_order/sales_order.js:638 +#: erpnext/selling/doctype/sales_order/sales_order.js:1279 +#: erpnext/selling/doctype/sales_order/sales_order.js:1432 #: erpnext/selling/report/sales_order_analysis/sales_order_analysis.py:255 #: erpnext/stock/doctype/landed_cost_item/landed_cost_item.json #: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json @@ -39034,6 +39190,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' @@ -39098,24 +39260,25 @@ msgstr "" msgid "Qty In Stock" msgstr "" -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py:82 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:117 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:174 msgid "Qty Per Unit" msgstr "" #. 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 msgid "Qty To Manufacture" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:1419 +#: erpnext/manufacturing/doctype/work_order/work_order.py:1420 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 "" @@ -39166,7 +39329,8 @@ 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:1045 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1068 msgid "Qty for {0}" msgstr "" @@ -39184,7 +39348,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 "" @@ -39213,12 +39377,16 @@ msgstr "" msgid "Qty to Deliver" msgstr "" +#: erpnext/stock/doctype/stock_entry/stock_entry.js:379 +msgid "Qty to Disassemble" +msgstr "" + #: erpnext/public/js/utils/serial_no_batch_selector.js:373 msgid "Qty to Fetch" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:304 -#: erpnext/manufacturing/doctype/job_card/job_card.py:871 +#: erpnext/manufacturing/doctype/job_card/job_card.js:312 +#: erpnext/manufacturing/doctype/job_card/job_card.py:889 msgid "Qty to Manufacture" msgstr "" @@ -39369,7 +39537,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 @@ -39404,7 +39572,13 @@ msgid "Quality Inspection Reading" msgstr "" #. Label of the inspection_required (Check) field in DocType 'BOM' +#. Label of the quality_inspection_required (Check) field in DocType 'BOM +#. Operation' +#. Label of the quality_inspection_required (Check) field in DocType 'Work +#. Order Operation' #: erpnext/manufacturing/doctype/bom/bom.json +#: erpnext/manufacturing/doctype/bom_operation/bom_operation.json +#: erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json msgid "Quality Inspection Required" msgstr "" @@ -39449,22 +39623,22 @@ 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 "" #: erpnext/public/js/controllers/transaction.js:384 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:196 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:206 msgid "Quality Inspection(s)" msgstr "" @@ -39595,7 +39769,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 @@ -39617,7 +39791,7 @@ msgstr "" #: erpnext/stock/doctype/material_request_item/material_request_item.json #: erpnext/stock/doctype/packing_slip_item/packing_slip_item.json #: erpnext/stock/doctype/pick_list_item/pick_list_item.json -#: erpnext/stock/doctype/stock_entry/stock_entry.js:728 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:815 #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json #: erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json #: erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py:36 @@ -39679,10 +39853,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 @@ -39699,8 +39871,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 @@ -39728,10 +39899,14 @@ msgid "Quantity is required" msgstr "" #: erpnext/stock/dashboard/item_dashboard.js:285 -msgid "Quantity must be greater than zero, and less or equal to {0}" +msgid "Quantity must be greater than zero" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1037 +#: erpnext/stock/dashboard/item_dashboard.js:290 +msgid "Quantity must be less than or equal to {0}" +msgstr "" + +#: erpnext/manufacturing/doctype/work_order/work_order.js:1098 #: erpnext/stock/doctype/pick_list/pick_list.js:206 msgid "Quantity must not be more than {0}" msgstr "" @@ -39741,41 +39916,29 @@ 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:385 -#: erpnext/manufacturing/doctype/job_card/job_card.js:455 +#: erpnext/manufacturing/doctype/bom/bom.py:716 +#: erpnext/manufacturing/doctype/job_card/job_card.js:393 +#: erpnext/manufacturing/doctype/job_card/job_card.js:463 #: erpnext/manufacturing/doctype/workstation/workstation.js:303 msgid "Quantity should be greater than 0" msgstr "" -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js:21 -msgid "Quantity to Make" -msgstr "" - -#: erpnext/manufacturing/doctype/work_order/work_order.js:343 +#: erpnext/manufacturing/doctype/work_order/work_order.js:346 msgid "Quantity to Manufacture" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:2592 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2617 msgid "Quantity to Manufacture can not be zero for the operation {0}" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:1411 +#: erpnext/manufacturing/doctype/work_order/work_order.py:1412 msgid "Quantity to Manufacture must be greater than 0." msgstr "" -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js:24 -msgid "Quantity to Produce" -msgstr "" - -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py:37 -msgid "Quantity to Produce should be greater than zero." -msgstr "" - #: erpnext/public/js/utils/barcode_scanner.js:257 msgid "Quantity to Scan" msgstr "" @@ -39805,7 +39968,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 "" @@ -39851,7 +40014,7 @@ msgstr "" #. Label of a Link in the Selling Workspace #. Option for the 'Transaction' (Select) field in DocType 'Authorization Rule' #. Label of a Workspace Sidebar Item -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:300 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:381 #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.js:51 #: erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.html:20 #: erpnext/crm/doctype/contract/contract.json @@ -39862,7 +40025,7 @@ msgstr "" #: erpnext/crm/report/lead_details/lead_details.js:37 #: erpnext/manufacturing/doctype/blanket_order/blanket_order.js:38 #: erpnext/selling/doctype/quotation/quotation.json -#: erpnext/selling/doctype/sales_order/sales_order.js:1175 +#: erpnext/selling/doctype/sales_order/sales_order.js:1182 #: erpnext/selling/doctype/sales_order_item/sales_order_item.json #: erpnext/selling/workspace/selling/selling.json #: erpnext/setup/doctype/authorization_rule/authorization_rule.json @@ -39950,7 +40113,7 @@ msgstr "" msgid "RFQ and Purchase Order Settings" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:129 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:131 msgid "RFQs are not allowed for {0} due to a scorecard standing of {1}" msgstr "" @@ -39994,7 +40157,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' @@ -40043,9 +40206,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:879 #: 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 @@ -40227,7 +40390,7 @@ msgstr "" msgid "Rate at which this tax is applied" msgstr "" -#: erpnext/controllers/accounts_controller.py:4064 +#: erpnext/controllers/accounts_controller.py:4074 msgid "Rate of '{}' items cannot be changed" msgstr "" @@ -40367,8 +40530,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:1078 #: erpnext/manufacturing/doctype/bom/bom.json #: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -40398,7 +40561,7 @@ msgstr "" msgid "Raw Materials Consumption" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:319 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:327 msgid "Raw Materials Missing" msgstr "" @@ -40432,7 +40595,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 "" @@ -40454,8 +40617,8 @@ 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/selling/doctype/sales_order/sales_order.js:968 +#: erpnext/manufacturing/doctype/work_order/work_order.js:767 +#: erpnext/selling/doctype/sales_order/sales_order.js:975 #: erpnext/selling/doctype/sales_order/sales_order_list.js:70 #: erpnext/stock/doctype/material_request/material_request.js:243 #: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.js:116 @@ -40554,7 +40717,7 @@ msgid "Reason for Failure" msgstr "" #: erpnext/buying/doctype/purchase_order/purchase_order.js:679 -#: erpnext/selling/doctype/sales_order/sales_order.js:1760 +#: erpnext/selling/doctype/sales_order/sales_order.js:1767 msgid "Reason for Hold" msgstr "" @@ -40563,7 +40726,7 @@ msgstr "" msgid "Reason for Leaving" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:1775 +#: erpnext/selling/doctype/sales_order/sales_order.js:1782 msgid "Reason for hold:" msgstr "" @@ -40750,17 +40913,14 @@ 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 "" -#: erpnext/stock/doctype/stock_entry/stock_entry.js:321 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:355 msgid "Received Stock Entries" msgstr "" @@ -41000,11 +41160,11 @@ msgstr "" msgid "Ref Date" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1031 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1034 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 "" @@ -41038,7 +41198,7 @@ msgstr "" msgid "Reference No" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:645 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:648 msgid "Reference No & Reference Date is required for {0}" msgstr "" @@ -41046,7 +41206,7 @@ msgstr "" msgid "Reference No and Reference Date is mandatory for Bank transaction" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:650 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:653 msgid "Reference No is mandatory if you entered Reference Date" msgstr "" @@ -41195,20 +41355,20 @@ msgstr "" msgid "Regular" msgstr "" -#: erpnext/stock/doctype/inventory_dimension/inventory_dimension.py:198 +#: erpnext/stock/doctype/inventory_dimension/inventory_dimension.py:202 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 "" @@ -41281,7 +41441,7 @@ msgstr "" msgid "Release Date" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:318 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:317 msgid "Release date must be in the future" msgstr "" @@ -41623,7 +41783,7 @@ msgstr "" msgid "Repost Item Valuation" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:344 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:367 msgid "Repost Item Valuation restarted for selected failed records." msgstr "" @@ -41663,10 +41823,6 @@ msgstr "" msgid "Repost started in the background" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:119 -msgid "Reposting Completed {0}%" -msgstr "" - #. Label of the reposting_data_file (Attach) field in DocType 'Repost Item #. Valuation' #: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json @@ -41676,10 +41832,10 @@ msgstr "" #. Label of the reposting_info_section (Section Break) field in DocType 'Repost #. Item Valuation' #: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json -msgid "Reposting Info" +msgid "Reposting Item and Warehouse" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:127 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:131 msgid "Reposting Progress" msgstr "" @@ -41689,12 +41845,30 @@ msgstr "" msgid "Reposting Reference" msgstr "" +#. Label of the vouchers_based_on_item_and_warehouse_section (Section Break) +#. field in DocType 'Repost Item Valuation' +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +msgid "Reposting Vouchers" +msgstr "" + +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:149 +msgid "Reposting Vouchers Progress" +msgstr "" + #: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:216 #: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:327 msgid "Reposting entries created: {0}" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:103 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:123 +msgid "Reposting for Item-Wh Completed {0}%" +msgstr "" + +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:141 +msgid "Reposting for Vouchers Completed {0}%" +msgstr "" + +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:109 msgid "Reposting has been started in the background." msgstr "" @@ -41739,7 +41913,7 @@ msgstr "" msgid "Reqd Qty (BOM)" msgstr "" -#: erpnext/public/js/utils.js:875 +#: erpnext/public/js/utils.js:895 msgid "Reqd by date" msgstr "" @@ -41781,8 +41955,8 @@ msgstr "" #. Label of a Workspace Sidebar Item #: erpnext/buying/doctype/buying_settings/buying_settings.json #: erpnext/buying/doctype/request_for_quotation/request_for_quotation.json -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:324 -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:426 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:326 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:428 #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.js:88 #: erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json #: erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js:70 @@ -41806,7 +41980,7 @@ msgstr "" msgid "Request for Quotation Supplier" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:1084 +#: erpnext/selling/doctype/sales_order/sales_order.js:1091 msgid "Request for Raw Materials" msgstr "" @@ -41929,10 +42103,7 @@ msgstr "" #: erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json #: erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json #: erpnext/manufacturing/doctype/work_order_item/work_order_item.json -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py:94 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html:11 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html:21 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py:28 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:119 #: erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py:58 #: erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.py:1057 #: erpnext/manufacturing/report/production_planning_report/production_planning_report.py:426 @@ -42014,8 +42185,8 @@ msgstr "" msgid "Reservation Based On" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:918 -#: erpnext/selling/doctype/sales_order/sales_order.js:99 +#: erpnext/manufacturing/doctype/work_order/work_order.js:925 +#: erpnext/selling/doctype/sales_order/sales_order.js:106 #: erpnext/stock/doctype/pick_list/pick_list.js:150 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:180 msgid "Reserve" @@ -42029,7 +42200,7 @@ msgstr "" #: erpnext/manufacturing/doctype/production_plan/production_plan.json #: erpnext/manufacturing/doctype/work_order/work_order.json #: erpnext/public/js/stock_reservation.js:15 -#: erpnext/selling/doctype/sales_order/sales_order.js:397 +#: erpnext/selling/doctype/sales_order/sales_order.js:404 #: erpnext/selling/doctype/sales_order/sales_order.json #: erpnext/selling/doctype/sales_order_item/sales_order_item.json #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:278 @@ -42129,29 +42300,29 @@ msgstr "" msgid "Reserved Quantity for Production" msgstr "" -#: erpnext/stock/stock_ledger.py:2287 +#: erpnext/stock/stock_ledger.py:2327 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:941 #: 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 +#: erpnext/selling/doctype/sales_order/sales_order.js:134 +#: erpnext/selling/doctype/sales_order/sales_order.js:464 #: erpnext/stock/dashboard/item_dashboard_list.html:15 #: erpnext/stock/doctype/bin/bin.json #: erpnext/stock/doctype/pick_list/pick_list.js:170 #: erpnext/stock/report/reserved_stock/reserved_stock.json #: erpnext/stock/report/stock_balance/stock_balance.py:572 -#: erpnext/stock/stock_ledger.py:2271 +#: erpnext/stock/stock_ledger.py:2311 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:205 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:333 msgid "Reserved Stock" msgstr "" -#: erpnext/stock/stock_ledger.py:2316 +#: erpnext/stock/stock_ledger.py:2356 msgid "Reserved Stock for Batch" msgstr "" @@ -42163,7 +42334,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 "" @@ -42196,7 +42367,7 @@ msgid "Reserved for sub contracting" msgstr "" #: erpnext/public/js/stock_reservation.js:203 -#: erpnext/selling/doctype/sales_order/sales_order.js:410 +#: erpnext/selling/doctype/sales_order/sales_order.js:417 #: erpnext/stock/doctype/pick_list/pick_list.js:295 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:293 msgid "Reserving Stock..." @@ -42352,7 +42523,7 @@ msgstr "" msgid "Rest Of The World" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:84 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:90 msgid "Restart" msgstr "" @@ -42413,11 +42584,11 @@ msgstr "" #: erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js:43 #: erpnext/buying/doctype/purchase_order/purchase_order.js:344 #: erpnext/manufacturing/doctype/workstation/workstation_job_card.html:63 -#: erpnext/selling/doctype/sales_order/sales_order.js:954 +#: erpnext/selling/doctype/sales_order/sales_order.js:961 msgid "Resume" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:239 +#: erpnext/manufacturing/doctype/job_card/job_card.js:247 msgid "Resume Job" msgstr "" @@ -42518,7 +42689,7 @@ msgstr "" msgid "Return Against Subcontracting Receipt" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:284 +#: erpnext/manufacturing/doctype/work_order/work_order.js:287 msgid "Return Components" msgstr "" @@ -42554,7 +42725,7 @@ msgstr "" msgid "Return Raw Material to Customer" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1525 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1522 msgid "Return invoice of asset cancelled" msgstr "" @@ -42863,7 +43034,7 @@ msgstr "" #. Label of the root_type (Select) field in DocType 'Account' #. Label of the root_type (Select) field in DocType 'Ledger Merge' #: erpnext/accounts/doctype/account/account.json -#: erpnext/accounts/doctype/account/account_tree.js:152 +#: erpnext/accounts/doctype/account/account_tree.js:147 #: erpnext/accounts/doctype/ledger_merge/ledger_merge.json #: erpnext/accounts/report/account_balance/account_balance.js:22 msgid "Root Type" @@ -43084,12 +43255,12 @@ msgid "Row #1: Sequence ID must be 1 for Operation {0}." msgstr "" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:563 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2074 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2071 msgid "Row #{0} (Payment Table): Amount must be negative" msgstr "" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:561 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2069 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2066 msgid "Row #{0} (Payment Table): Amount must be positive" msgstr "" @@ -43106,11 +43277,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 "" @@ -43143,7 +43314,7 @@ msgstr "" msgid "Row #{0}: Asset {1} is already sold" msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:334 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:333 msgid "Row #{0}: BOM is not specified for subcontracting item {0}" msgstr "" @@ -43155,7 +43326,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 "" @@ -43163,15 +43334,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 "" @@ -43179,31 +43350,31 @@ msgstr "" msgid "Row #{0}: Cannot create entry with different taxable AND withholding document links." msgstr "" -#: erpnext/controllers/accounts_controller.py:3767 +#: erpnext/controllers/accounts_controller.py:3777 msgid "Row #{0}: Cannot delete item {1} which has already been billed." msgstr "" -#: erpnext/controllers/accounts_controller.py:3741 +#: erpnext/controllers/accounts_controller.py:3751 msgid "Row #{0}: Cannot delete item {1} which has already been delivered" msgstr "" -#: erpnext/controllers/accounts_controller.py:3760 +#: erpnext/controllers/accounts_controller.py:3770 msgid "Row #{0}: Cannot delete item {1} which has already been received" msgstr "" -#: erpnext/controllers/accounts_controller.py:3747 +#: erpnext/controllers/accounts_controller.py:3757 msgid "Row #{0}: Cannot delete item {1} which has work order assigned to it." msgstr "" -#: erpnext/controllers/accounts_controller.py:3753 +#: erpnext/controllers/accounts_controller.py:3763 msgid "Row #{0}: Cannot delete item {1} which is already ordered against this Sales Order." msgstr "" -#: erpnext/controllers/accounts_controller.py:4074 +#: erpnext/controllers/accounts_controller.py:4084 msgid "Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}." msgstr "" -#: 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 "" @@ -43243,13 +43414,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 "" @@ -43261,7 +43432,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 "" @@ -43269,12 +43440,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 "" @@ -43282,7 +43453,7 @@ msgstr "" msgid "Row #{0}: Dates overlapping with other row in group {1}" msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:358 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:357 msgid "Row #{0}: Default BOM not found for FG Item {1}" msgstr "" @@ -43306,39 +43477,39 @@ msgstr "" msgid "Row #{0}: Expense account {1} is not valid for Purchase Invoice {2}. Only expense accounts from non-stock items are allowed." msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:363 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:362 #: erpnext/selling/doctype/sales_order/sales_order.py:303 msgid "Row #{0}: Finished Good Item Qty can not be zero" msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:345 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:344 #: erpnext/selling/doctype/sales_order/sales_order.py:283 msgid "Row #{0}: Finished Good Item is not specified for service item {1}" msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:352 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:351 #: erpnext/selling/doctype/sales_order/sales_order.py:290 msgid "Row #{0}: Finished Good Item {1} must be a sub-contracted item" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:472 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:534 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 "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:693 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:696 msgid "Row #{0}: For {1}, you can select reference document only if account gets credited" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:703 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:706 msgid "Row #{0}: For {1}, you can select reference document only if account gets debited" msgstr "" @@ -43350,7 +43521,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 "" @@ -43358,7 +43529,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:1634 msgid "Row #{0}: Item {1} cannot be transferred more than {2} against {3} {4}" msgstr "" @@ -43382,7 +43553,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 "" @@ -43390,8 +43561,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 "" @@ -43403,11 +43574,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 "" @@ -43435,28 +43606,28 @@ msgstr "" msgid "Row #{0}: Opening Accumulated Depreciation must be less than or equal to {1}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:868 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:960 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 "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1051 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1056 msgid "Row #{0}: Please select Item Code in Assembly Items" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1054 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1059 msgid "Row #{0}: Please select the BOM No in Assembly Items" msgstr "" -#: 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 "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1048 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1053 msgid "Row #{0}: Please select the Sub Assembly Warehouse" msgstr "" @@ -43468,6 +43639,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 "" @@ -43501,10 +43677,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 "" @@ -43524,8 +43704,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 @@ -43540,16 +43720,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 @@ -43576,7 +43756,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 "" @@ -43596,11 +43776,11 @@ msgstr "" msgid "Row #{0}: Set Supplier for item {1}" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1058 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1063 msgid "Row #{0}: Since 'Track Semi Finished Goods' is enabled, the BOM {1} cannot be used for Sub Assembly Items" msgstr "" -#: 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 "" @@ -43612,11 +43792,11 @@ msgstr "" msgid "Row #{0}: Source Warehouse {1} for item {2} must be same as Source Warehouse {3} in the Work Order." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1015 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1107 msgid "Row #{0}: Source and Target Warehouse cannot be the same for Material Transfer" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1037 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1129 msgid "Row #{0}: Source, Target Warehouse and Inventory Dimensions cannot be the exact same for Material Transfer" msgstr "" @@ -43628,7 +43808,7 @@ msgstr "" msgid "Row #{0}: Status is mandatory" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:455 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:458 msgid "Row #{0}: Status must be {1} for Invoice Discounting {2}" msgstr "" @@ -43661,11 +43841,11 @@ msgstr "" msgid "Row #{0}: Stock not available to reserve for the Item {1} in Warehouse {2}." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1268 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1265 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 "" @@ -43697,7 +43877,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 "" @@ -43717,7 +43897,7 @@ msgstr "" msgid "Row #{0}: {1} is not a valid reading field. Please refer to the field description." msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:115 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:131 msgid "Row #{0}: {1} is required to create the Opening {2} Invoices" msgstr "" @@ -43725,7 +43905,7 @@ msgstr "" msgid "Row #{0}: {1} of {2} should be {3}. Please update the {1} or select a different account." msgstr "" -#: erpnext/controllers/accounts_controller.py:3881 +#: erpnext/controllers/accounts_controller.py:3891 msgid "Row #{0}:Quantity for Item {1} cannot be zero." msgstr "" @@ -43737,23 +43917,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 "" @@ -43761,7 +43941,7 @@ msgstr "" msgid "Row #{idx}: {from_warehouse_field} and {to_warehouse_field} cannot be same." msgstr "" -#: erpnext/controllers/buying_controller.py:1176 +#: erpnext/controllers/buying_controller.py:1181 msgid "Row #{idx}: {schedule_date} cannot be before {transaction_date}." msgstr "" @@ -43769,6 +43949,10 @@ msgstr "" msgid "Row #{}: Currency of {} - {} doesn't matches company currency." msgstr "" +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:113 +msgid "Row #{}: Either Party ID or Party Name is required" +msgstr "" + #: erpnext/assets/doctype/asset/asset.py:421 msgid "Row #{}: Finance Book should not be empty since you're using multiple." msgstr "" @@ -43785,6 +43969,10 @@ msgstr "" msgid "Row #{}: POS Invoice {} is not submitted yet" msgstr "" +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:123 +msgid "Row #{}: Party ID is required" +msgstr "" + #: erpnext/assets/doctype/asset_maintenance/asset_maintenance.py:41 msgid "Row #{}: Please assign task to a member." msgstr "" @@ -43805,7 +43993,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 "" @@ -43814,7 +44002,7 @@ msgstr "" msgid "Row #{}: {}" msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:110 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:126 msgid "Row #{}: {} {} does not exist." msgstr "" @@ -43822,19 +44010,19 @@ msgstr "" msgid "Row #{}: {} {} doesn't belong to Company {}. Please select valid {}." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:444 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:440 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:1658 msgid "Row {0}# Item {1} not found in 'Raw Materials Supplied' table in {2} {3}" msgstr "" @@ -43842,7 +44030,7 @@ msgstr "" msgid "Row {0}: Accepted Qty and Rejected Qty can't be zero at the same time." msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:608 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:611 msgid "Row {0}: Account {1} and Party Type {2} have different account types" msgstr "" @@ -43850,11 +44038,11 @@ msgstr "" msgid "Row {0}: Activity Type is mandatory." msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:674 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:677 msgid "Row {0}: Advance against Customer must be credit" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:676 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:679 msgid "Row {0}: Advance against Supplier must be debit" msgstr "" @@ -43866,7 +44054,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:1319 msgid "Row {0}: As {1} is enabled, raw materials cannot be added to {2} entry. Use {3} entry to consume raw materials." msgstr "" @@ -43874,11 +44062,11 @@ msgstr "" msgid "Row {0}: Bill of Materials not found for the Item {1}" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:927 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:930 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." @@ -43888,23 +44076,23 @@ msgstr "" msgid "Row {0}: Conversion Factor is mandatory" msgstr "" -#: erpnext/controllers/accounts_controller.py:3227 +#: erpnext/controllers/accounts_controller.py:3237 msgid "Row {0}: Cost Center {1} does not belong to Company {2}" msgstr "" -#: 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 "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:773 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:776 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 "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:768 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:771 msgid "Row {0}: Debit entry can not be linked with a {1}" msgstr "" @@ -43916,7 +44104,7 @@ msgstr "" msgid "Row {0}: Delivery Warehouse cannot be same as Customer Warehouse for Item {1}." msgstr "" -#: erpnext/controllers/accounts_controller.py:2720 +#: erpnext/controllers/accounts_controller.py:2730 msgid "Row {0}: Due Date in the Payment Terms table cannot be before Posting Date" msgstr "" @@ -43924,8 +44112,8 @@ msgstr "" 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/accounts/doctype/journal_entry/journal_entry.py:1021 +#: erpnext/controllers/taxes_and_totals.py:1381 msgid "Row {0}: Exchange Rate is mandatory" msgstr "" @@ -43937,23 +44125,23 @@ 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 "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:534 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:530 msgid "Row {0}: Expense Head changed to {1} as no Purchase Receipt is created against Item {2}." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:491 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:487 msgid "Row {0}: Expense Head changed to {1} because account {2} is not linked to warehouse {3} or it is not the default inventory account" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:516 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:512 msgid "Row {0}: Expense Head changed to {1} because expense is booked against this account in Purchase Receipt {2}" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:152 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:154 msgid "Row {0}: For Supplier {1}, Email Address is Required to send an email" msgstr "" @@ -43961,7 +44149,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 "" @@ -43970,7 +44158,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 "" @@ -43978,7 +44166,7 @@ msgstr "" msgid "Row {0}: Hours value must be greater than zero." msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:793 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:796 msgid "Row {0}: Invalid reference {1}" msgstr "" @@ -44006,7 +44194,7 @@ msgstr "" msgid "Row {0}: Item {1}'s quantity cannot be higher than the available quantity." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1209 +#: erpnext/manufacturing/doctype/bom/bom.py:1244 msgid "Row {0}: Operation time should be greater than 0 for operation {1}" msgstr "" @@ -44018,11 +44206,11 @@ msgstr "" msgid "Row {0}: Packing Slip is already created for Item {1}." msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:819 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:822 msgid "Row {0}: Party / Account does not match with {1} / {2} in {3} {4}" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:597 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:600 msgid "Row {0}: Party Type and Party is required for Receivable / Payable account {1}" msgstr "" @@ -44030,11 +44218,11 @@ msgstr "" msgid "Row {0}: Payment Term is mandatory" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:667 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:670 msgid "Row {0}: Payment against Sales/Purchase Order should always be marked as advance" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:660 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:663 msgid "Row {0}: Please check 'Is Advance' against Account {1} if this is an advance entry." msgstr "" @@ -44070,7 +44258,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 "" @@ -44078,7 +44266,7 @@ msgstr "" msgid "Row {0}: Qty cannot be greater than {1} for the Item {2}." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:563 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:625 msgid "Row {0}: Qty in Stock UOM can not be zero." msgstr "" @@ -44090,7 +44278,7 @@ msgstr "" msgid "Row {0}: Quantity cannot be negative." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:942 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1034 msgid "Row {0}: Quantity not available for {4} in warehouse {1} at posting time of the entry ({2} {3})" msgstr "" @@ -44102,7 +44290,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:1671 msgid "Row {0}: Subcontracted Item is mandatory for the raw material {1}" msgstr "" @@ -44118,11 +44306,11 @@ msgstr "" msgid "Row {0}: The entire expense amount for account {1} in {2} has already been allocated." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:609 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:671 msgid "Row {0}: The item {1}, quantity must be positive number" msgstr "" -#: erpnext/controllers/accounts_controller.py:3204 +#: erpnext/controllers/accounts_controller.py:3214 msgid "Row {0}: The {3} Account {1} does not belong to the company {2}" msgstr "" @@ -44130,23 +44318,23 @@ 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:3593 msgid "Row {0}: Transferred quantity cannot be greater than the requested quantity." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:557 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:619 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:1203 +#: 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 "" @@ -44167,23 +44355,23 @@ msgstr "" msgid "Row {0}: {1} {2} cannot be same as {3} (Party Account) {4}" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:833 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:836 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 "" -#: erpnext/utilities/transaction_base.py:562 +#: erpnext/utilities/transaction_base.py:563 msgid "Row {1}: Quantity ({0}) cannot be a fraction. To allow this, disable '{2}' in UOM {3}." msgstr "" -#: 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 "" @@ -44209,7 +44397,7 @@ msgstr "" msgid "Rows with Same Account heads will be merged on Ledger" msgstr "" -#: erpnext/controllers/accounts_controller.py:2731 +#: erpnext/controllers/accounts_controller.py:2741 msgid "Rows with duplicate due dates in other rows were found: {0}" msgstr "" @@ -44292,7 +44480,7 @@ msgstr "" msgid "SLA Paused On" msgstr "" -#: erpnext/public/js/utils.js:1239 +#: erpnext/public/js/utils.js:1259 msgid "SLA is on hold since {0}" msgstr "" @@ -44391,7 +44579,7 @@ msgstr "" #: erpnext/setup/doctype/company/company.py:649 #: erpnext/setup/doctype/company/company_dashboard.py:9 #: erpnext/setup/doctype/sales_person/sales_person_dashboard.py:12 -#: erpnext/setup/install.py:372 +#: erpnext/setup/install.py:373 #: erpnext/setup/setup_wizard/operations/install_fixtures.py:297 #: erpnext/stock/doctype/item/item.json #: erpnext/stock/doctype/pick_list/pick_list_dashboard.py:16 @@ -44499,7 +44687,7 @@ msgstr "" #: erpnext/projects/doctype/timesheet/timesheet.json #: erpnext/projects/doctype/timesheet_detail/timesheet_detail.json #: erpnext/selling/doctype/quotation/quotation_list.js:22 -#: erpnext/selling/doctype/sales_order/sales_order.js:1063 +#: erpnext/selling/doctype/sales_order/sales_order.js:1070 #: erpnext/selling/doctype/sales_order/sales_order_list.js:75 #: erpnext/selling/workspace/selling/selling.json #: erpnext/setup/doctype/authorization_rule/authorization_rule.json @@ -44647,7 +44835,7 @@ msgstr "" #. Label of a Workspace Sidebar Item #: erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json #: erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:278 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:359 #: erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json #: erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py:284 #: erpnext/accounts/report/sales_register/sales_register.py:238 @@ -44745,8 +44933,8 @@ msgstr "" #: erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json #: erpnext/manufacturing/doctype/work_order/work_order.json #: erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json -#: erpnext/selling/doctype/sales_order/sales_order.js:336 -#: erpnext/selling/doctype/sales_order/sales_order.js:1279 +#: erpnext/selling/doctype/sales_order/sales_order.js:343 +#: erpnext/selling/doctype/sales_order/sales_order.js:1286 #: erpnext/selling/doctype/sales_order_item/sales_order_item.json #: erpnext/stock/doctype/material_request_item/material_request_item.json #: erpnext/stock/doctype/pick_list_item/pick_list_item.json @@ -44797,7 +44985,7 @@ msgstr "" msgid "Sales Order {0} already exists against Customer's Purchase Order {1}. To allow multiple Sales Orders, Enable {2} in {3}" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1398 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1395 msgid "Sales Order {0} is not submitted" msgstr "" @@ -45073,7 +45261,7 @@ msgstr "" #. Label of a Workspace Sidebar Item #: erpnext/crm/doctype/opportunity/opportunity.json #: erpnext/crm/doctype/sales_stage/sales_stage.json -#: erpnext/crm/report/lost_opportunity/lost_opportunity.py:51 +#: erpnext/crm/report/lost_opportunity/lost_opportunity.py:57 #: erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py:70 #: erpnext/crm/workspace/crm/crm.json erpnext/workspace_sidebar/crm.json msgid "Sales Stage" @@ -45215,7 +45403,7 @@ msgstr "" msgid "Same item cannot be entered multiple times." msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:121 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:123 msgid "Same supplier has been entered multiple times" msgstr "" @@ -45227,7 +45415,7 @@ msgid "Sample Quantity" msgstr "" #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:269 -#: erpnext/stock/doctype/stock_entry/stock_entry.js:448 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:535 msgid "Sample Retention Stock Entry" msgstr "" @@ -45239,12 +45427,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:4086 msgid "Sample quantity {0} cannot be more than received quantity {1}" msgstr "" @@ -45345,7 +45533,7 @@ msgstr "" msgid "Schedule Date" msgstr "" -#: erpnext/public/js/controllers/transaction.js:486 +#: erpnext/public/js/controllers/transaction.js:490 msgid "Schedule Name" msgstr "" @@ -45447,65 +45635,31 @@ 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" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:384 +#: erpnext/assets/doctype/asset/depreciation.py:387 msgid "Scrap date cannot be before purchase date" msgstr "" @@ -45554,6 +45708,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" @@ -45603,7 +45801,7 @@ msgstr "" msgid "Select Accounting Dimension." msgstr "" -#: erpnext/public/js/utils.js:535 +#: erpnext/public/js/utils.js:555 msgid "Select Alternate Item" msgstr "" @@ -45615,15 +45813,15 @@ msgstr "" msgid "Select Attribute Values" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:1262 +#: erpnext/selling/doctype/sales_order/sales_order.js:1269 msgid "Select BOM" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:1239 +#: erpnext/selling/doctype/sales_order/sales_order.js:1246 msgid "Select BOM and Qty for Production" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:1395 +#: erpnext/selling/doctype/sales_order/sales_order.js:1402 msgid "Select BOM, Qty and For Warehouse" msgstr "" @@ -45653,11 +45851,11 @@ msgstr "" msgid "Select Company" msgstr "" -#: erpnext/public/js/print.js:113 +#: erpnext/public/js/print.js:115 msgid "Select Company Address" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:533 +#: erpnext/manufacturing/doctype/job_card/job_card.js:541 msgid "Select Corrective Operation" msgstr "" @@ -45667,11 +45865,11 @@ msgstr "" msgid "Select Customers By" msgstr "" -#: erpnext/setup/doctype/employee/employee.js:120 +#: erpnext/setup/doctype/employee/employee.js:160 msgid "Select Date of Birth. This will validate Employees age and prevent hiring of under-age staff." msgstr "" -#: erpnext/setup/doctype/employee/employee.js:127 +#: erpnext/setup/doctype/employee/employee.js:167 msgid "Select Date of joining. It will have impact on the first salary calculation, Leave allocation on pro-rata bases." msgstr "" @@ -45693,12 +45891,12 @@ msgstr "" msgid "Select Dispatch Address " msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:221 +#: erpnext/manufacturing/doctype/job_card/job_card.js:229 msgid "Select Employees" msgstr "" #: erpnext/buying/doctype/purchase_order/purchase_order.js:198 -#: erpnext/selling/doctype/sales_order/sales_order.js:818 +#: erpnext/selling/doctype/sales_order/sales_order.js:825 msgid "Select Finished Good" msgstr "" @@ -45708,24 +45906,24 @@ msgstr "" #. Forecast' #: erpnext/manufacturing/doctype/master_production_schedule/master_production_schedule.json #: erpnext/manufacturing/doctype/sales_forecast/sales_forecast.json -#: erpnext/selling/doctype/sales_order/sales_order.js:1596 -#: erpnext/selling/doctype/sales_order/sales_order.js:1624 +#: erpnext/selling/doctype/sales_order/sales_order.js:1603 +#: erpnext/selling/doctype/sales_order/sales_order.js:1631 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:493 msgid "Select Items" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:1482 +#: erpnext/selling/doctype/sales_order/sales_order.js:1489 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 "" #. Label of the select_items_to_manufacture_section (Section Break) field in #. DocType 'Production Plan' #: erpnext/manufacturing/doctype/production_plan/production_plan.json -#: erpnext/selling/doctype/sales_order/sales_order.js:1291 +#: erpnext/selling/doctype/sales_order/sales_order.js:1298 msgid "Select Items to Manufacture" msgstr "" @@ -45743,20 +45941,20 @@ msgstr "" msgid "Select Job Worker Address" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1173 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1199 #: erpnext/selling/page/point_of_sale/pos_item_cart.js:955 msgid "Select Loyalty Program" msgstr "" -#: erpnext/public/js/controllers/transaction.js:473 +#: erpnext/public/js/controllers/transaction.js:477 msgid "Select Payment Schedule" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:410 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:411 msgid "Select Possible Supplier" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1043 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1104 #: erpnext/stock/doctype/pick_list/pick_list.js:216 msgid "Select Quantity" msgstr "" @@ -45815,7 +46013,7 @@ msgstr "" msgid "Select a Company" msgstr "" -#: erpnext/setup/doctype/employee/employee.js:115 +#: erpnext/setup/doctype/employee/employee.js:155 msgid "Select a Company this Employee belongs to." msgstr "" @@ -45869,7 +46067,7 @@ msgstr "" msgid "Select company name first." msgstr "" -#: erpnext/controllers/accounts_controller.py:2979 +#: erpnext/controllers/accounts_controller.py:2989 msgid "Select finance book for the item {0} at row {1}" msgstr "" @@ -45877,7 +46075,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 "" @@ -45890,11 +46088,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:1206 msgid "Select the Item to be manufactured." msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:966 +#: erpnext/manufacturing/doctype/bom/bom.js:985 msgid "Select the Item to be manufactured. The Item name, UoM, Company, and Currency will be fetched automatically." msgstr "" @@ -45915,11 +46113,11 @@ msgstr "" msgid "Select the date and your timezone" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:985 +#: erpnext/manufacturing/doctype/bom/bom.js:1004 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 "" @@ -45943,7 +46141,7 @@ msgstr "" msgid "Selected POS Opening Entry should be open." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2570 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2567 msgid "Selected Price List should have buying and selling fields checked." msgstr "" @@ -45993,7 +46191,7 @@ msgstr "" msgid "Sell quantity cannot exceed the asset quantity" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1411 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1408 msgid "Sell quantity cannot exceed the asset quantity. Asset {0} has only {1} item(s)." msgstr "" @@ -46050,7 +46248,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 "" @@ -46108,7 +46306,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 "" @@ -46192,7 +46390,7 @@ msgstr "" msgid "Serial / Batch No" msgstr "" -#: erpnext/public/js/utils.js:197 +#: erpnext/public/js/utils.js:217 msgid "Serial / Batch Nos" msgstr "" @@ -46244,7 +46442,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 @@ -46305,7 +46503,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:2672 msgid "Serial No Reserved" msgstr "" @@ -46362,7 +46560,7 @@ msgstr "" msgid "Serial No and Batch Traceability" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1146 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1179 msgid "Serial No is mandatory" msgstr "" @@ -46391,7 +46589,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:3462 msgid "Serial No {0} does not exists" msgstr "" @@ -46407,7 +46605,7 @@ msgstr "" msgid "Serial No {0} is already assigned to customer {1}. Can only be returned against the customer {1}" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:451 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:484 msgid "Serial No {0} is not present in the {1} {2}, hence you can't return it against the {1} {2}" msgstr "" @@ -46445,11 +46643,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:1958 msgid "Serial Nos are created successfully" msgstr "" -#: erpnext/stock/stock_ledger.py:2277 +#: erpnext/stock/stock_ledger.py:2317 msgid "Serial Nos are reserved in Stock Reservation Entries, you need to unreserve them before proceeding." msgstr "" @@ -46526,11 +46724,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:2180 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:2252 msgid "Serial and Batch Bundle updated" msgstr "" @@ -46744,6 +46942,7 @@ msgstr "" #: erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.json #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json #: erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +#: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py:405 msgid "Service End Date" msgstr "" @@ -46887,6 +47086,7 @@ msgstr "" #: erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.json #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json #: erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +#: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py:397 msgid "Service Start Date" msgstr "" @@ -46901,12 +47101,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 "" @@ -46930,7 +47130,7 @@ msgstr "" #. Label of the set_basic_rate_manually (Check) field in DocType 'Stock Entry #. Detail' -#: erpnext/stock/doctype/stock_entry/stock_entry.py:299 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:307 #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json msgid "Set Basic Rate Manually" msgstr "" @@ -46945,8 +47145,8 @@ msgstr "" msgid "Set Delivery Warehouse" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:404 -#: erpnext/manufacturing/doctype/job_card/job_card.js:473 +#: erpnext/manufacturing/doctype/job_card/job_card.js:412 +#: erpnext/manufacturing/doctype/job_card/job_card.js:481 msgid "Set Finished Good Quantity" msgstr "" @@ -46983,7 +47183,7 @@ msgstr "" msgid "Set Landed Cost Based on Purchase Invoice Rate" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1185 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1211 msgid "Set Loyalty Program" msgstr "" @@ -46991,10 +47191,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 @@ -47012,7 +47212,7 @@ msgstr "" msgid "Set Posting Date" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:1012 +#: erpnext/manufacturing/doctype/bom/bom.js:1031 msgid "Set Process Loss Item Quantity" msgstr "" @@ -47055,7 +47255,7 @@ msgstr "" msgid "Set Source Warehouse" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:1602 +#: erpnext/selling/doctype/sales_order/sales_order.js:1609 msgid "Set Supplier" msgstr "" @@ -47085,7 +47285,7 @@ msgstr "" msgid "Set Valuation Rate for Rejected Materials" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:256 +#: erpnext/selling/doctype/sales_order/sales_order.js:263 msgid "Set Warehouse" msgstr "" @@ -47135,7 +47335,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:1021 msgid "Set quantity of process loss item:" msgstr "" @@ -47151,7 +47351,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:1263 msgid "Set the Planned Start Date (an Estimated Date at which you want the Production to begin)" msgstr "" @@ -47232,7 +47432,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 "" @@ -47242,12 +47442,12 @@ 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:1182 -#: erpnext/manufacturing/doctype/work_order/work_order.py:1475 +#: erpnext/manufacturing/doctype/bom/bom.py:1217 +#: erpnext/manufacturing/doctype/work_order/work_order.py:1476 msgid "Setting {0} is required" msgstr "" @@ -47471,10 +47671,13 @@ msgstr "" #. Label of the shipping_address_display (Text Editor) field in DocType #. 'Purchase Order' #. Label of the shipping_address_display (Text Editor) field in DocType +#. 'Request for Quotation' +#. Label of the shipping_address_display (Text Editor) field in DocType #. 'Supplier Quotation' #. Label of the shipping_address_display (Text Editor) field in DocType #. 'Subcontracting Order' #: erpnext/buying/doctype/purchase_order/purchase_order.json +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.json #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.json #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json msgid "Shipping Address Details" @@ -47662,7 +47865,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 "" @@ -47670,7 +47873,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 "" @@ -47753,7 +47956,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 "" @@ -47761,7 +47964,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 "" @@ -47794,7 +47997,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 "" @@ -47829,9 +48032,8 @@ msgstr "" msgid "Show Warehouse-wise Stock" msgstr "" -#: erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js:28 -#: erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js:19 -msgid "Show exploded view" +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js:26 +msgid "Show availability of exploded items" msgstr "" #: erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.js:88 @@ -47960,18 +48162,22 @@ msgstr "" msgid "Simultaneous" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:687 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:749 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 "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:111 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:132 msgid "Since {0} are Serial No/Batch No items, you cannot enable 'Recreate Stock Ledgers' in Repost Item Valuation." msgstr "" +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:112 +msgid "Since {0} has 'Update Stock' disabled, you cannot create repost item valuation against it" +msgstr "" + #. Option for the 'Marital Status' (Select) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Single" @@ -47994,7 +48200,7 @@ msgstr "" #. Label of the skip_material_transfer (Check) field in DocType 'Work Order #. Operation' -#: erpnext/manufacturing/doctype/work_order/work_order.js:362 +#: erpnext/manufacturing/doctype/work_order/work_order.js:365 #: erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json #: erpnext/manufacturing/doctype/workstation/workstation.js:454 msgid "Skip Material Transfer" @@ -48061,7 +48267,7 @@ msgstr "" msgid "Solvency Ratios" msgstr "" -#: erpnext/controllers/accounts_controller.py:4332 +#: erpnext/controllers/accounts_controller.py:4344 msgid "Some required Company details are missing. You don't have permission to update them. Please contact your System Manager." msgstr "" @@ -48125,6 +48331,23 @@ msgstr "" msgid "Source Location" msgstr "" +#: erpnext/manufacturing/doctype/work_order/work_order.js:1014 +msgid "Source Manufacture Entry" +msgstr "" + +#. Label of the source_stock_entry (Link) field in DocType 'Stock Entry' +#: erpnext/stock/doctype/stock_entry/stock_entry.json +msgid "Source Stock Entry (Manufacture)" +msgstr "" + +#: erpnext/stock/doctype/stock_entry/stock_entry.py:912 +msgid "Source Stock Entry {0} belongs to Work Order {1}, not {2}. Please use a manufacture entry from the same Work Order." +msgstr "" + +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2370 +msgid "Source Stock Entry {0} has no finished goods quantity" +msgstr "" + #. Label of the source_type (Select) field in DocType 'Support Search Source' #: erpnext/support/doctype/support_search_source/support_search_source.json msgid "Source Type" @@ -48145,7 +48368,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 @@ -48159,7 +48382,7 @@ msgstr "" #: erpnext/selling/doctype/sales_order_item/sales_order_item.json #: erpnext/stock/dashboard/item_dashboard.js:227 #: erpnext/stock/doctype/material_request_item/material_request_item.json -#: erpnext/stock/doctype/stock_entry/stock_entry.js:719 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:806 #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json msgid "Source Warehouse" msgstr "" @@ -48187,11 +48410,11 @@ msgstr "" msgid "Source and Target Location cannot be same" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:816 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:878 msgid "Source and target warehouse cannot be same for row {0}" msgstr "" -#: erpnext/stock/dashboard/item_dashboard.js:290 +#: erpnext/stock/dashboard/item_dashboard.js:295 msgid "Source and target warehouse must be different" msgstr "" @@ -48200,9 +48423,9 @@ msgstr "" msgid "Source of Funds (Liabilities)" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:782 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:799 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:806 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:844 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:861 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:868 msgid "Source warehouse is mandatory for row {0}" msgstr "" @@ -48338,13 +48561,13 @@ 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 "" #: erpnext/setup/setup_wizard/operations/defaults_setup.py:70 #: erpnext/setup/setup_wizard/operations/install_fixtures.py:485 -#: erpnext/tests/utils.py:297 +#: erpnext/tests/utils.py:316 msgid "Standard Buying" msgstr "" @@ -48358,8 +48581,8 @@ msgstr "" #: erpnext/setup/setup_wizard/operations/defaults_setup.py:70 #: erpnext/setup/setup_wizard/operations/install_fixtures.py:493 -#: erpnext/stock/doctype/item/item.py:267 erpnext/tests/utils.py:305 -#: erpnext/tests/utils.py:2494 +#: erpnext/stock/doctype/item/item.py:267 erpnext/tests/utils.py:324 +#: erpnext/tests/utils.py:2522 msgid "Standard Selling" msgstr "" @@ -48415,7 +48638,7 @@ msgstr "" msgid "Start Date should be lower than End Date" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:215 +#: erpnext/manufacturing/doctype/job_card/job_card.js:223 #: erpnext/manufacturing/doctype/workstation/workstation.js:124 msgid "Start Job" msgstr "" @@ -48424,7 +48647,7 @@ msgstr "" msgid "Start Merge" msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:99 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js:105 msgid "Start Reposting" msgstr "" @@ -48544,8 +48767,8 @@ msgstr "" #: erpnext/accounts/doctype/account/account.json #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:96 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:158 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1358 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1384 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1364 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1390 #: erpnext/accounts/report/account_balance/account_balance.js:58 msgid "Stock Adjustment" msgstr "" @@ -48652,7 +48875,7 @@ msgstr "" msgid "Stock Details" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:910 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1002 msgid "Stock Entries already created for Work Order {0}: {1}" msgstr "" @@ -48711,7 +48934,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 "" @@ -48719,11 +48942,11 @@ 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 "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1312 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1315 msgid "Stock Entry {0} is not submitted" msgstr "" @@ -48875,6 +49098,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 @@ -48882,6 +49106,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 @@ -48952,15 +49177,15 @@ 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:927 #: erpnext/manufacturing/doctype/work_order/work_order.js:936 +#: erpnext/manufacturing/doctype/work_order/work_order.js:943 #: 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 -#: erpnext/selling/doctype/sales_order/sales_order.js:116 -#: erpnext/selling/doctype/sales_order/sales_order.js:129 -#: erpnext/selling/doctype/sales_order/sales_order.js:250 +#: erpnext/selling/doctype/sales_order/sales_order.js:108 +#: erpnext/selling/doctype/sales_order/sales_order.js:123 +#: erpnext/selling/doctype/sales_order/sales_order.js:136 +#: erpnext/selling/doctype/sales_order/sales_order.js:257 #: erpnext/stock/doctype/pick_list/pick_list.js:152 #: erpnext/stock/doctype/pick_list/pick_list.js:167 #: erpnext/stock/doctype/pick_list/pick_list.js:172 @@ -48975,9 +49200,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 @@ -48990,9 +49215,9 @@ msgstr "" msgid "Stock Reservation Entries Cancelled" msgstr "" -#: erpnext/controllers/subcontracting_inward_controller.py:1003 -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:2238 -#: erpnext/manufacturing/doctype/work_order/work_order.py:2124 +#: erpnext/controllers/subcontracting_inward_controller.py:1018 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:2245 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2125 #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1777 msgid "Stock Reservation Entries Created" msgstr "" @@ -49003,7 +49228,7 @@ msgstr "" #. Name of a DocType #: erpnext/public/js/stock_reservation.js:309 -#: erpnext/selling/doctype/sales_order/sales_order.js:467 +#: erpnext/selling/doctype/sales_order/sales_order.js:474 #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:388 #: erpnext/stock/report/reserved_stock/reserved_stock.js:53 @@ -49104,9 +49329,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' @@ -49130,7 +49355,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' @@ -49151,9 +49376,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 @@ -49177,7 +49402,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 @@ -49192,7 +49417,7 @@ msgid "Stock UOM Quantity" msgstr "" #: erpnext/public/js/stock_reservation.js:230 -#: erpnext/selling/doctype/sales_order/sales_order.js:451 +#: erpnext/selling/doctype/sales_order/sales_order.js:458 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:327 msgid "Stock Unreservation" msgstr "" @@ -49206,7 +49431,7 @@ msgstr "" msgid "Stock Uom" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:739 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:736 msgid "Stock Update Not Allowed" msgstr "" @@ -49303,18 +49528,22 @@ msgstr "" msgid "Stock cannot be reserved in the group warehouse {0}." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1226 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1223 msgid "Stock cannot be updated against the following Delivery Notes: {0}" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1295 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1292 msgid "Stock cannot be updated because the invoice contains a drop shipping item. Please disable 'Update Stock' or remove the drop shipping item." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:736 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:733 msgid "Stock cannot be updated for Purchase Invoice {0} because a Purchase Receipt {1} has already been created for this transaction. Please disable the 'Update Stock' checkbox in the Purchase Invoice and save the invoice." msgstr "" +#: erpnext/stock/doctype/warehouse/warehouse.py:124 +msgid "Stock entries exist with the old account. Changing the account may lead to a mismatch between the warehouse closing balance and the account closing balance. The overall closing balance will still match, but not for the specific account." +msgstr "" + #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1131 msgid "Stock has been unreserved for work order {0}." msgstr "" @@ -49365,7 +49594,7 @@ msgstr "" #: erpnext/setup/doctype/company/company.py:384 #: erpnext/setup/setup_wizard/operations/defaults_setup.py:33 #: erpnext/setup/setup_wizard/operations/install_fixtures.py:537 -#: erpnext/stock/doctype/item/item.py:304 erpnext/tests/utils.py:270 +#: erpnext/stock/doctype/item/item.py:304 erpnext/tests/utils.py:289 msgid "Stores" msgstr "" @@ -49423,7 +49652,7 @@ msgstr "" #. Label of the operation (Link) field in DocType 'Job Card Time Log' #. Name of a DocType -#: erpnext/manufacturing/doctype/job_card/job_card.js:349 +#: erpnext/manufacturing/doctype/job_card/job_card.js:357 #: erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json #: erpnext/manufacturing/doctype/sub_operation/sub_operation.json msgid "Sub Operation" @@ -49447,7 +49676,7 @@ msgstr "" msgid "Sub Total" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:621 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:626 msgid "Sub assembly item references are missing. Please fetch the sub assemblies and raw materials again." msgstr "" @@ -49607,12 +49836,14 @@ msgstr "" #. Name of a DocType #. Label of a Card Break in the Subcontracting Workspace #. Label of a Link in the Subcontracting Workspace +#. Label of a Workspace Sidebar Item #: erpnext/manufacturing/doctype/work_order/work_order.json -#: erpnext/selling/doctype/sales_order/sales_order.js:1005 +#: erpnext/selling/doctype/sales_order/sales_order.js:1012 #: erpnext/stock/doctype/stock_entry/stock_entry.json #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json #: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.json #: erpnext/subcontracting/workspace/subcontracting/subcontracting.json +#: erpnext/workspace_sidebar/subcontracting.json msgid "Subcontracting Inward Order" msgstr "" @@ -49635,8 +49866,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 @@ -49661,7 +49892,7 @@ msgstr "" #. Receipt Supplied Item' #. Label of a Workspace Sidebar Item #: erpnext/buying/doctype/purchase_order/purchase_order.js:399 -#: erpnext/controllers/subcontracting_controller.py:1167 +#: erpnext/controllers/subcontracting_controller.py:1173 #: erpnext/manufacturing/workspace/manufacturing/manufacturing.json #: erpnext/stock/doctype/stock_entry/stock_entry.json #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json @@ -49699,7 +49930,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 "" @@ -49788,8 +50019,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 "" @@ -49813,7 +50044,7 @@ msgstr "" msgid "Submit this Work Order for further processing." msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:306 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:308 msgid "Submit your Quotation" msgstr "" @@ -50122,8 +50353,8 @@ msgstr "" #: erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js:37 #: erpnext/assets/doctype/asset/asset.json #: erpnext/buying/doctype/purchase_order/purchase_order.json -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:184 -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:269 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:185 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:270 #: erpnext/buying/doctype/request_for_quotation/request_for_quotation.json #: erpnext/buying/doctype/request_for_quotation_supplier/request_for_quotation_supplier.json #: erpnext/buying/doctype/supplier/supplier.json @@ -50150,8 +50381,8 @@ msgstr "" #: erpnext/regional/report/irs_1099/irs_1099.py:77 #: erpnext/selling/doctype/customer/customer.js:253 #: erpnext/selling/doctype/party_specific_item/party_specific_item.json -#: erpnext/selling/doctype/sales_order/sales_order.js:189 -#: erpnext/selling/doctype/sales_order/sales_order.js:1660 +#: erpnext/selling/doctype/sales_order/sales_order.js:196 +#: erpnext/selling/doctype/sales_order/sales_order.js:1667 #: erpnext/selling/doctype/sales_order_item/sales_order_item.json #: erpnext/selling/doctype/sms_center/sms_center.json #: erpnext/setup/workspace/home/home.json @@ -50256,7 +50487,7 @@ msgstr "" #: erpnext/accounts/report/purchase_register/purchase_register.py:186 #: erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js:55 #: erpnext/buying/doctype/purchase_order/purchase_order.json -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:502 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:503 #: erpnext/buying/doctype/supplier/supplier.json #: erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.py:105 #: erpnext/buying/workspace/buying/buying.json @@ -50310,7 +50541,7 @@ msgstr "" msgid "Supplier Invoice No" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1774 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1780 msgid "Supplier Invoice No exists in Purchase Invoice {0}" msgstr "" @@ -50455,7 +50686,7 @@ msgstr "" #. Name of a report #. Label of a Link in the Buying Workspace #. Label of a Workspace Sidebar Item -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:154 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:155 #: erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.json #: erpnext/buying/workspace/buying/buying.json #: erpnext/workspace_sidebar/buying.json @@ -50470,7 +50701,7 @@ msgstr "" msgid "Supplier Quotation Item" msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:495 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:497 msgid "Supplier Quotation {0} Created" msgstr "" @@ -50478,7 +50709,7 @@ msgstr "" msgid "Supplier Reference" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:1684 +#: erpnext/selling/doctype/sales_order/sales_order.js:1691 msgid "Supplier Required" msgstr "" @@ -50559,7 +50790,7 @@ msgstr "" #. Label of the supplier_warehouse (Link) field in DocType 'Purchase Receipt' #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/buying/doctype/purchase_order/purchase_order.json -#: erpnext/manufacturing/doctype/job_card/job_card.js:89 +#: erpnext/manufacturing/doctype/job_card/job_card.js:97 #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.json msgid "Supplier Warehouse" msgstr "" @@ -50572,7 +50803,7 @@ msgstr "" msgid "Supplier delivers to Customer" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:1683 +#: erpnext/selling/doctype/sales_order/sales_order.js:1690 msgid "Supplier is required for all selected Items" msgstr "" @@ -50740,7 +50971,7 @@ msgstr "" msgid "TDS Computation Summary" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1535 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1541 msgid "TDS Deducted" msgstr "" @@ -50879,7 +51110,7 @@ msgstr "" #: erpnext/stock/dashboard/item_dashboard.js:234 #: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json #: erpnext/stock/doctype/material_request_item/material_request_item.json -#: erpnext/stock/doctype/stock_entry/stock_entry.js:725 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:812 #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json msgid "Target Warehouse" msgstr "" @@ -50899,7 +51130,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 "" @@ -50915,9 +51146,9 @@ msgstr "" msgid "Target Warehouse {0} must be same as Delivery Warehouse {1} in the Subcontracting Inward Order Item." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:788 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:795 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:810 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:850 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:857 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:872 msgid "Target warehouse is mandatory for row {0}" msgstr "" @@ -51145,7 +51376,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 @@ -51177,7 +51408,7 @@ msgstr "" #. Label of the rate (Float) field in DocType 'Sales Taxes and Charges' #. Label of the tax_rate (Percent) field in DocType 'Tax Withholding Entry' #: erpnext/accounts/doctype/account/account.json -#: erpnext/accounts/doctype/account/account_tree.js:175 +#: erpnext/accounts/doctype/account/account_tree.js:170 #: erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json #: erpnext/accounts/doctype/item_tax_template_detail/item_tax_template_detail.json #: erpnext/accounts/doctype/item_wise_tax_detail/item_wise_tax_detail.json @@ -51388,7 +51619,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:1253 msgid "Taxable Amount" msgstr "" @@ -51594,7 +51825,7 @@ msgstr "" msgid "Television" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:413 +#: erpnext/manufacturing/doctype/bom/bom.js:452 msgid "Template Item" msgstr "" @@ -51837,7 +52068,7 @@ msgstr "" #: erpnext/crm/report/lead_details/lead_details.js:46 #: erpnext/crm/report/lead_details/lead_details.py:34 #: erpnext/crm/report/lost_opportunity/lost_opportunity.js:36 -#: erpnext/crm/report/lost_opportunity/lost_opportunity.py:58 +#: erpnext/crm/report/lost_opportunity/lost_opportunity.py:64 #: erpnext/crm/workspace/crm/crm.json #: erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.json #: erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json @@ -51920,7 +52151,7 @@ msgstr "" msgid "The 'From Package No.' field must neither be empty nor it's value less than 1." msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:411 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:413 msgid "The Access to Request for Quotation From Portal is Disabled. To Allow Access, Enable it in Portal Settings." msgstr "" @@ -51961,7 +52192,7 @@ msgstr "" msgid "The Loyalty Program isn't valid for the selected company" msgstr "" -#: erpnext/accounts/doctype/payment_request/payment_request.py:1106 +#: erpnext/accounts/doctype/payment_request/payment_request.py:1107 msgid "The Payment Request {0} is already paid, cannot process payment twice" msgstr "" @@ -51969,11 +52200,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:2820 msgid "The Process Loss Qty has reset as per job cards Process Loss Qty" msgstr "" @@ -51981,15 +52212,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:2669 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:1839 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 "" @@ -52003,7 +52234,7 @@ msgstr "" msgid "The account head under Liability or Equity, in which Profit/Loss will be booked" msgstr "" -#: erpnext/accounts/doctype/payment_request/payment_request.py:1001 +#: erpnext/accounts/doctype/payment_request/payment_request.py:1002 msgid "The allocated amount is greater than the outstanding amount of Payment Request {0}" msgstr "" @@ -52015,7 +52246,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 "" @@ -52027,7 +52258,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:1211 msgid "The default BOM for that item will be fetched by the system. You can also change the BOM." msgstr "" @@ -52076,11 +52307,11 @@ msgstr "" msgid "The following Purchase Invoices are not submitted:" msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:344 +#: erpnext/assets/doctype/asset/depreciation.py:347 msgid "The following assets have failed to automatically post depreciation entries: {0}" msgstr "" -#: 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 "" @@ -52092,7 +52323,7 @@ msgstr "" msgid "The following deleted attributes exist in Variants but not in the Template. You can either delete the Variants or keep the attribute(s) in template." msgstr "" -#: erpnext/setup/doctype/employee/employee.py:175 +#: erpnext/setup/doctype/employee/employee.py:289 msgid "The following employees are currently still reporting to {0}:" msgstr "" @@ -52123,7 +52354,7 @@ msgstr "" msgid "The holiday on {0} is not between From Date and To Date" msgstr "" -#: erpnext/controllers/buying_controller.py:1243 +#: erpnext/controllers/buying_controller.py: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 "" @@ -52131,7 +52362,7 @@ msgstr "" msgid "The items {0} and {1} are present in the following {2} :" msgstr "" -#: erpnext/controllers/buying_controller.py:1236 +#: erpnext/controllers/buying_controller.py: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 "" @@ -52213,7 +52444,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:967 msgid "The reserved stock will be released when you update items. Are you certain you wish to proceed?" msgstr "" @@ -52266,7 +52497,7 @@ msgstr "" msgid "The shares don't exist with the {0}" msgstr "" -#: erpnext/stock/stock_ledger.py:803 +#: erpnext/stock/stock_ledger.py:814 msgid "The stock for the item {0} in the {1} warehouse was negative on the {2}. You should create a positive entry {3} before the date {4} and time {5} to post the correct valuation rate. For more details, please read the documentation." msgstr "" @@ -52304,7 +52535,7 @@ msgstr "" msgid "The uploaded file does not appear to be in valid MT940 format." msgstr "" -#: erpnext/edi/doctype/code_list/code_list_import.py:48 +#: erpnext/edi/doctype/code_list/code_list_import.py:54 msgid "The uploaded file does not match the selected Code List." msgstr "" @@ -52332,23 +52563,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:1239 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:1232 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:1244 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 "" @@ -52364,7 +52595,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 "" @@ -52388,7 +52619,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 "" @@ -52428,7 +52659,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:1776 msgid "There must be atleast 1 Finished Good in this Stock Entry" msgstr "" @@ -52471,7 +52702,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 "" @@ -52517,7 +52748,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 "" @@ -52542,7 +52773,7 @@ msgstr "" msgid "This is a location where scraped materials are stored." msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:318 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:319 msgid "This is a preview of the email to be sent. A PDF of the document will automatically be attached with the email." msgstr "" @@ -52590,11 +52821,11 @@ msgstr "" msgid "This is considered dangerous from accounting point of view." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:540 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:536 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:1225 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 "" @@ -52632,7 +52863,7 @@ msgstr "" msgid "This schedule was created when Asset {0} was repaired through Asset Repair {1}." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1502 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1499 msgid "This schedule was created when Asset {0} was restored due to Sales Invoice {1} cancellation." msgstr "" @@ -52640,15 +52871,15 @@ msgstr "" msgid "This schedule was created when Asset {0} was restored on Asset Capitalization {1}'s cancellation." msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:458 +#: erpnext/assets/doctype/asset/depreciation.py:461 msgid "This schedule was created when Asset {0} was restored." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1498 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1495 msgid "This schedule was created when Asset {0} was returned through Sales Invoice {1}." msgstr "" -#: erpnext/assets/doctype/asset/depreciation.py:417 +#: erpnext/assets/doctype/asset/depreciation.py:420 msgid "This schedule was created when Asset {0} was scrapped." msgstr "" @@ -52656,7 +52887,7 @@ msgstr "" msgid "This schedule was created when Asset {0} was {1} into new Asset {2}." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1474 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1471 msgid "This schedule was created when Asset {0} was {1} through Sales Invoice {2}." msgstr "" @@ -52811,7 +53042,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 "" @@ -52842,7 +53073,7 @@ msgstr "" #. Name of a DocType #. Label of a Link in the Projects Workspace #. Label of a Workspace Sidebar Item -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1066 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:281 #: erpnext/projects/doctype/timesheet/timesheet.json #: erpnext/projects/report/daily_timesheet_summary/daily_timesheet_summary.py:26 #: erpnext/projects/report/timesheet_billing_summary/timesheet_billing_summary.py:59 @@ -53139,7 +53370,7 @@ msgstr "" msgid "To Warehouse (Optional)" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:980 +#: erpnext/manufacturing/doctype/bom/bom.js:999 msgid "To add Operations tick the 'With Operations' checkbox." msgstr "" @@ -53191,14 +53422,14 @@ 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 -#: erpnext/controllers/accounts_controller.py:3237 +#: erpnext/controllers/accounts_controller.py:3247 msgid "To include tax in row {0} in Item rate, taxes in rows {1} must also be included" msgstr "" @@ -53218,11 +53449,11 @@ msgstr "" msgid "To still proceed with editing this Attribute Value, enable {0} in Item Variant Settings." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:630 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:627 msgid "To submit the invoice without purchase order please set {0} as {1} in {2}" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:651 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:648 msgid "To submit the invoice without purchase receipt please set {0} as {1} in {2}" msgstr "" @@ -53398,7 +53629,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 "" @@ -53483,12 +53714,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 "" @@ -53544,7 +53775,7 @@ msgstr "" msgid "Total Debit" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:933 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:936 msgid "Total Debit must be equal to Total Credit. The difference is {0}" msgstr "" @@ -53648,6 +53879,11 @@ msgstr "" msgid "Total Landed Cost (Company Currency)" msgstr "" +#. Label of the total_vouchers (Int) field in DocType 'Repost Item Valuation' +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +msgid "Total Ledgers" +msgstr "" + #: erpnext/accounts/report/balance_sheet/balance_sheet.py:219 msgid "Total Liability" msgstr "" @@ -53753,7 +53989,7 @@ msgstr "" msgid "Total Paid Amount" msgstr "" -#: erpnext/controllers/accounts_controller.py:2785 +#: erpnext/controllers/accounts_controller.py:2795 msgid "Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total" msgstr "" @@ -53842,12 +54078,6 @@ msgstr "" msgid "Total Repair Cost" msgstr "" -#. Label of the total_reposting_count (Int) field in DocType 'Repost Item -#. Valuation' -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json -msgid "Total Reposting Count" -msgstr "" - #: erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py:44 msgid "Total Revenue" msgstr "" @@ -53978,7 +54208,7 @@ msgstr "" msgid "Total Time in Mins" msgstr "" -#: erpnext/public/js/utils.js:173 +#: erpnext/public/js/utils.js:193 msgid "Total Unpaid: {0}" msgstr "" @@ -54057,7 +54287,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 "" @@ -54082,7 +54312,7 @@ msgstr "" msgid "Total percentage against cost centers should be 100" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:665 +#: erpnext/selling/doctype/sales_order/sales_order.js:672 msgid "Total quantity in delivery schedule cannot be greater than the item quantity" msgstr "" @@ -54093,7 +54323,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 "" @@ -54211,7 +54441,7 @@ msgstr "" msgid "Transaction Date" msgstr "" -#: erpnext/setup/doctype/company/company.py:1104 +#: erpnext/setup/doctype/company/company.py:1106 msgid "Transaction Deletion Document {0} has been triggered for company {1}" msgstr "" @@ -54325,7 +54555,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 "" @@ -54354,7 +54584,7 @@ msgstr "" msgid "Transactions against the Company already exist! Chart of Accounts can only be imported for a Company with no transactions." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1159 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1156 msgid "Transactions using Sales Invoice in POS are disabled." msgstr "" @@ -54453,7 +54683,7 @@ msgstr "" msgid "Transit" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.js:502 +#: erpnext/stock/doctype/stock_entry/stock_entry.js:589 msgid "Transit Entry" msgstr "" @@ -54681,6 +54911,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' @@ -54729,6 +54960,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 @@ -54738,13 +54970,13 @@ 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:840 #: 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 #: erpnext/selling/doctype/product_bundle_item/product_bundle_item.json #: erpnext/selling/doctype/quotation_item/quotation_item.json -#: erpnext/selling/doctype/sales_order/sales_order.js:1653 +#: erpnext/selling/doctype/sales_order/sales_order.js:1660 #: erpnext/selling/doctype/sales_order_item/sales_order_item.json #: erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py:43 #: erpnext/selling/report/sales_analytics/sales_analytics.py:138 @@ -54765,7 +54997,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 @@ -54821,7 +55053,7 @@ msgstr "" msgid "UOM Conversion Factor" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1463 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1468 msgid "UOM Conversion factor ({0} -> {1}) not found for item: {2}" msgstr "" @@ -54834,7 +55066,7 @@ msgstr "" msgid "UOM Name" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3774 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:4008 msgid "UOM conversion factor required for UOM: {0} in Item: {1}" msgstr "" @@ -54997,7 +55229,7 @@ msgstr "" msgid "Unit" msgstr "" -#: erpnext/controllers/accounts_controller.py:4064 +#: erpnext/controllers/accounts_controller.py:4074 msgid "Unit Price" msgstr "" @@ -55148,15 +55380,15 @@ msgstr "" msgid "Unreconciled Entries" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:927 -#: erpnext/selling/doctype/sales_order/sales_order.js:114 +#: erpnext/manufacturing/doctype/work_order/work_order.js:934 +#: erpnext/selling/doctype/sales_order/sales_order.js:121 #: erpnext/stock/doctype/pick_list/pick_list.js:158 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:193 msgid "Unreserve" msgstr "" #: erpnext/public/js/stock_reservation.js:245 -#: erpnext/selling/doctype/sales_order/sales_order.js:502 +#: erpnext/selling/doctype/sales_order/sales_order.js:509 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:378 msgid "Unreserve Stock" msgstr "" @@ -55170,7 +55402,7 @@ msgid "Unreserve for Sub-assembly" msgstr "" #: erpnext/public/js/stock_reservation.js:281 -#: erpnext/selling/doctype/sales_order/sales_order.js:514 +#: erpnext/selling/doctype/sales_order/sales_order.js:521 #: erpnext/stock/doctype/pick_list/pick_list.js:310 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:390 msgid "Unreserving Stock..." @@ -55329,7 +55561,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" @@ -55356,10 +55588,10 @@ 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:946 #: 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 +#: erpnext/selling/doctype/sales_order/sales_order.js:89 +#: erpnext/selling/doctype/sales_order/sales_order.js:947 msgid "Update Items" msgstr "" @@ -55458,11 +55690,11 @@ msgstr "" msgid "Updating Variants..." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1126 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1187 msgid "Updating Work Order status" msgstr "" -#: erpnext/public/js/print.js:151 +#: erpnext/public/js/print.js:153 msgid "Updating details." msgstr "" @@ -55579,7 +55811,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" @@ -55701,7 +55933,7 @@ msgstr "" msgid "User has not applied rule on the invoice {0}" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:187 +#: erpnext/setup/doctype/employee/employee.py:301 msgid "User {0} does not exist" msgstr "" @@ -55709,15 +55941,15 @@ msgstr "" msgid "User {0} doesn't have any default POS Profile. Check Default at Row {1} for this User." msgstr "" -#: erpnext/setup/doctype/employee/employee.py:205 +#: erpnext/setup/doctype/employee/employee.py:319 msgid "User {0} is already assigned to Employee {1}" msgstr "" -#: erpnext/setup/doctype/employee/employee.py:243 +#: erpnext/setup/doctype/employee/employee.py:357 msgid "User {0}: Removed Employee Self Service role as there is no mapped employee." msgstr "" -#: erpnext/setup/doctype/employee/employee.py:238 +#: erpnext/setup/doctype/employee/employee.py:352 msgid "User {0}: Removed Employee role as there is no mapped employee." msgstr "" @@ -56010,11 +56242,11 @@ msgstr "" msgid "Valuation Rate (In / Out)" msgstr "" -#: erpnext/stock/stock_ledger.py:2022 +#: erpnext/stock/stock_ledger.py:2062 msgid "Valuation Rate Missing" msgstr "" -#: erpnext/stock/stock_ledger.py:2000 +#: erpnext/stock/stock_ledger.py:2040 msgid "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}." msgstr "" @@ -56046,7 +56278,7 @@ msgid "Valuation rate for the item as per Sales Invoice (Only for Internal Trans msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2271 -#: erpnext/controllers/accounts_controller.py:3261 +#: erpnext/controllers/accounts_controller.py:3271 msgid "Valuation type charges can not be marked as Inclusive" msgstr "" @@ -56058,7 +56290,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 "" @@ -56186,7 +56418,7 @@ msgstr "" msgid "Variant Attributes" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:226 +#: erpnext/manufacturing/doctype/bom/bom.js:264 msgid "Variant BOM" msgstr "" @@ -56208,8 +56440,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 "" @@ -56331,7 +56563,7 @@ msgstr "" msgid "Video Settings" msgstr "" -#: erpnext/accounts/doctype/financial_report_template/financial_report_template.js:7 +#: erpnext/accounts/doctype/financial_report_template/financial_report_template.js:9 msgid "View Account Coverage" msgstr "" @@ -56363,7 +56595,7 @@ msgstr "" msgid "View Leads" msgstr "" -#: erpnext/accounts/doctype/account/account_tree.js:279 +#: erpnext/accounts/doctype/account/account_tree.js:274 #: erpnext/stock/doctype/batch/batch.js:18 msgid "View Ledger" msgstr "" @@ -56580,7 +56812,7 @@ msgstr "" msgid "Voucher No" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1386 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1419 msgid "Voucher No is mandatory" msgstr "" @@ -56820,11 +57052,15 @@ msgstr "" msgid "Warehouse is mandatory" msgstr "" -#: erpnext/stock/doctype/warehouse/warehouse.py:259 +#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py:286 +msgid "Warehouse is required to get producible FG Items" +msgstr "" + +#: erpnext/stock/doctype/warehouse/warehouse.py:233 msgid "Warehouse not found against the account {0}" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1216 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1213 #: erpnext/stock/doctype/delivery_note/delivery_note.py:444 msgid "Warehouse required for stock Item {0}" msgstr "" @@ -56846,7 +57082,7 @@ msgstr "" msgid "Warehouse {0} does not belong to company {1}" msgstr "" -#: erpnext/stock/doctype/warehouse/warehouse.py:306 +#: erpnext/stock/doctype/warehouse/warehouse.py:280 msgid "Warehouse {0} does not exist" msgstr "" @@ -56858,10 +57094,6 @@ msgstr "" msgid "Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}." msgstr "" -#: erpnext/stock/doctype/warehouse/warehouse.py:144 -msgid "Warehouse's Stock Value has already been booked in the following accounts:" -msgstr "" - #: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:20 msgid "Warehouse: {0} does not belong to {1}" msgstr "" @@ -56875,15 +57107,15 @@ msgstr "" msgid "Warehouses" msgstr "" -#: erpnext/stock/doctype/warehouse/warehouse.py:173 +#: erpnext/stock/doctype/warehouse/warehouse.py:147 msgid "Warehouses with child nodes cannot be converted to ledger" msgstr "" -#: erpnext/stock/doctype/warehouse/warehouse.py:183 +#: erpnext/stock/doctype/warehouse/warehouse.py:157 msgid "Warehouses with existing transaction can not be converted to group." msgstr "" -#: erpnext/stock/doctype/warehouse/warehouse.py:175 +#: erpnext/stock/doctype/warehouse/warehouse.py:149 msgid "Warehouses with existing transaction can not be converted to ledger." msgstr "" @@ -56959,7 +57191,7 @@ msgstr "" msgid "Warning - Row {0}: Billing Hours are more than Actual Hours" msgstr "" -#: erpnext/stock/stock_ledger.py:813 +#: erpnext/stock/stock_ledger.py:824 msgid "Warning on Negative Stock" msgstr "" @@ -56967,7 +57199,11 @@ msgstr "" msgid "Warning!" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1318 +#: erpnext/stock/doctype/warehouse/warehouse.py:122 +msgid "Warning: Account changed for warehouse" +msgstr "" + +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1321 msgid "Warning: Another {0} # {1} exists against stock entry {2}" msgstr "" @@ -56975,7 +57211,7 @@ msgstr "" msgid "Warning: Material Requested Qty is less than Minimum Order Qty" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:1460 +#: erpnext/manufacturing/doctype/work_order/work_order.py:1461 msgid "Warning: Quantity exceeds maximum producible quantity based on quantity of raw materials received through the Subcontracting Inward Order {0}." msgstr "" @@ -57246,7 +57482,7 @@ msgstr "" msgid "When creating an Item, entering a value for this field will automatically create an Item Price at the backend." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:296 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:304 msgid "When there are multiple finished goods ({0}) in a Repack stock entry, the basic rate for all finished goods must be set manually. To set rate manually, enable the checkbox 'Set Basic Rate Manually' in the respective finished good row." msgstr "" @@ -57273,8 +57509,8 @@ msgstr "" msgid "Widowed" msgstr "" -#. Label of the width (Int) field in DocType 'Shipment Parcel' -#. Label of the width (Int) field in DocType 'Shipment Parcel Template' +#. Label of the width (Float) field in DocType 'Shipment Parcel' +#. Label of the width (Float) field in DocType 'Shipment Parcel Template' #: erpnext/stock/doctype/shipment_parcel/shipment_parcel.json #: erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json msgid "Width (cm)" @@ -57384,7 +57620,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 @@ -57398,7 +57634,7 @@ msgstr "" #: erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js:29 #: erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py:104 #: erpnext/manufacturing/workspace/manufacturing/manufacturing.json -#: erpnext/selling/doctype/sales_order/sales_order.js:1050 +#: erpnext/selling/doctype/sales_order/sales_order.js:1057 #: erpnext/stock/doctype/material_request/material_request.js:216 #: erpnext/stock/doctype/material_request/material_request.json #: erpnext/stock/doctype/material_request/material_request.py:871 @@ -57435,6 +57671,10 @@ msgstr "" msgid "Work Order Item" msgstr "" +#: erpnext/stock/doctype/stock_entry/stock_entry.py:915 +msgid "Work Order Mismatch" +msgstr "" + #. Name of a DocType #: erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json msgid "Work Order Operation" @@ -57476,16 +57716,16 @@ msgstr "" msgid "Work Order cannot be created for following reason:
    {0}" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:1404 +#: erpnext/manufacturing/doctype/work_order/work_order.py:1405 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:2481 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2561 msgid "Work Order has been {0}" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:1225 +#: erpnext/selling/doctype/sales_order/sales_order.js:1232 msgid "Work Order not created" msgstr "" @@ -57493,7 +57733,11 @@ msgstr "" msgid "Work Order {0} created" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:860 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2386 +msgid "Work Order {0} has no produced qty" +msgstr "" + +#: erpnext/stock/doctype/stock_entry/stock_entry.py:952 msgid "Work Order {0}: Job Card not found for the operation {1}" msgstr "" @@ -57502,7 +57746,7 @@ msgstr "" msgid "Work Orders" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:1318 +#: erpnext/selling/doctype/sales_order/sales_order.js:1325 msgid "Work Orders Created: {0}" msgstr "" @@ -57571,7 +57815,7 @@ msgstr "" #: erpnext/manufacturing/doctype/bom_operation/bom_operation.json #: erpnext/manufacturing/doctype/bom_website_operation/bom_website_operation.json #: erpnext/manufacturing/doctype/job_card/job_card.json -#: erpnext/manufacturing/doctype/work_order/work_order.js:326 +#: erpnext/manufacturing/doctype/work_order/work_order.js:329 #: erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json #: erpnext/manufacturing/doctype/workstation/workstation.json #: erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js:35 @@ -57819,7 +58063,7 @@ msgstr "" msgid "You are importing data for the code list:" msgstr "" -#: erpnext/controllers/accounts_controller.py:3861 +#: erpnext/controllers/accounts_controller.py:3871 msgid "You are not allowed to update as per the conditions set in {} Workflow." msgstr "" @@ -57835,7 +58079,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 "" @@ -57855,7 +58099,7 @@ msgstr "" msgid "You can change the parent account to a Balance Sheet account or select a different account." msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:709 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:712 msgid "You can not enter current voucher in 'Against Journal Entry' column" msgstr "" @@ -57864,7 +58108,7 @@ msgid "You can only have Plans with the same billing cycle in a Subscription" msgstr "" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.js:423 -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:928 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1019 msgid "You can only redeem max {0} points in this order." msgstr "" @@ -57884,7 +58128,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 "" @@ -57896,7 +58140,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 "" @@ -57912,7 +58156,7 @@ msgstr "" msgid "You cannot create/amend any accounting entries till this date." msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:942 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:945 msgid "You cannot credit and debit same account at the same time" msgstr "" @@ -57924,7 +58168,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 "" @@ -57936,7 +58180,7 @@ msgstr "" msgid "You cannot redeem more than {0}." msgstr "" -#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:189 +#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:210 msgid "You cannot repost item valuation before {}" msgstr "" @@ -57956,11 +58200,11 @@ 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:567 msgid "You do not have permission to edit this document" msgstr "" -#: erpnext/controllers/accounts_controller.py:3837 +#: erpnext/controllers/accounts_controller.py:3847 msgid "You do not have permissions to {} items in a {}." msgstr "" @@ -57972,11 +58216,11 @@ msgstr "" msgid "You don't have enough points to redeem." msgstr "" -#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:273 +#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py: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:1046 msgid "You have already selected items from {0} {1}" msgstr "" @@ -57984,7 +58228,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 "" @@ -58012,7 +58256,7 @@ msgstr "" msgid "You need to cancel POS Closing Entry {} to be able to cancel this document." msgstr "" -#: erpnext/controllers/accounts_controller.py:3212 +#: erpnext/controllers/accounts_controller.py:3222 msgid "You selected the account group {1} as {2} Account in row {0}. Please select a single account." msgstr "" @@ -58063,7 +58307,7 @@ msgstr "" msgid "Zero Rated" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:563 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:625 msgid "Zero quantity" msgstr "" @@ -58080,7 +58324,7 @@ msgstr "" msgid "`Allow Negative rates for Items`" msgstr "" -#: erpnext/stock/stock_ledger.py:2014 +#: erpnext/stock/stock_ledger.py:2054 msgid "after" msgstr "" @@ -58096,11 +58340,11 @@ msgstr "" msgid "as Title" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.js:1004 +#: erpnext/manufacturing/doctype/bom/bom.js:1023 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:1589 msgid "as of {0}" msgstr "" @@ -58120,7 +58364,7 @@ msgstr "" msgid "cannot be greater than 100" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:334 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:333 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1102 msgid "dated {0}" msgstr "" @@ -58228,7 +58472,7 @@ msgstr "" msgid "must be between 0 and 100" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.js:638 +#: erpnext/selling/doctype/sales_order/sales_order.js:645 msgid "name" msgstr "" @@ -58249,7 +58493,7 @@ msgstr "" msgid "paid to" msgstr "" -#: erpnext/public/js/utils.js:443 +#: erpnext/public/js/utils.js:463 msgid "payments app is not installed. Please install it from {0} or {1}" msgstr "" @@ -58270,7 +58514,7 @@ msgstr "" msgid "per hour" msgstr "" -#: erpnext/stock/stock_ledger.py:2015 +#: erpnext/stock/stock_ledger.py:2055 msgid "performing either one below:" msgstr "" @@ -58299,7 +58543,7 @@ msgstr "" msgid "received from" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1476 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1473 msgid "returned" msgstr "" @@ -58334,7 +58578,7 @@ msgstr "" msgid "sandbox" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1476 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1473 msgid "sold" msgstr "" @@ -58361,7 +58605,7 @@ msgstr "" msgid "to" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3175 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3172 msgid "to unallocate the amount of this Return Invoice before cancelling it." msgstr "" @@ -58400,11 +58644,11 @@ 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 "" -#: erpnext/controllers/accounts_controller.py:2367 +#: erpnext/controllers/accounts_controller.py:2377 msgid "{0} Account not found against Customer {1}." msgstr "" @@ -58428,15 +58672,15 @@ msgstr "" msgid "{0} Digest" msgstr "" -#: erpnext/accounts/utils.py:1497 +#: erpnext/accounts/utils.py:1569 msgid "{0} Number {1} is already used in {2} {3}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1635 +#: 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:562 msgid "{0} Operations: {1}" msgstr "" @@ -58460,23 +58704,23 @@ msgstr "" msgid "{0} account is not of type {1}" msgstr "" -#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:515 +#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:518 msgid "{0} account not found while submitting purchase receipt" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1062 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1065 msgid "{0} against Bill {1} dated {2}" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1071 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1074 msgid "{0} against Purchase Order {1}" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1038 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1041 msgid "{0} against Sales Invoice {1}" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1045 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1048 msgid "{0} against Sales Order {1}" msgstr "" @@ -58493,6 +58737,10 @@ msgstr "" msgid "{0} asset cannot be transferred" msgstr "" +#: erpnext/controllers/trends.py:60 +msgid "{0} can be either {1} or {2}." +msgstr "" + #: erpnext/accounts/doctype/pricing_rule/pricing_rule.py:279 msgid "{0} can not be negative" msgstr "" @@ -58509,9 +58757,9 @@ msgstr "" msgid "{0} cannot be zero" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:918 -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1034 -#: erpnext/stock/doctype/pick_list/pick_list.py:1297 +#: 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:1330 #: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py:322 msgid "{0} created" msgstr "" @@ -58524,11 +58772,11 @@ msgstr "" msgid "{0} currency must be same as company's default currency. Please select another account." msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:295 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:294 msgid "{0} currently has a {1} Supplier Scorecard standing, and Purchase Orders to this supplier should be issued with caution." msgstr "" -#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:137 +#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:139 msgid "{0} currently has a {1} Supplier Scorecard standing, and RFQs to this supplier should be issued with caution." msgstr "" @@ -58570,7 +58818,7 @@ msgstr "" msgid "{0} hours" msgstr "" -#: erpnext/controllers/accounts_controller.py:2725 +#: erpnext/controllers/accounts_controller.py:2735 msgid "{0} in row {1}" msgstr "" @@ -58600,7 +58848,7 @@ msgstr "" msgid "{0} is in Draft. Submit it before creating the Asset." msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1131 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1128 msgid "{0} is mandatory for Item {1}" msgstr "" @@ -58613,15 +58861,15 @@ msgstr "" msgid "{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}" msgstr "" -#: erpnext/controllers/accounts_controller.py:3169 +#: erpnext/controllers/accounts_controller.py:3179 msgid "{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}." msgstr "" -#: 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:1813 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 "" @@ -58629,7 +58877,7 @@ msgstr "" msgid "{0} is not a group node. Please select a group node as parent cost center" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:615 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:677 msgid "{0} is not a stock Item" msgstr "" @@ -58665,27 +58913,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:527 msgid "{0} items disassembled" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:484 +#: erpnext/manufacturing/doctype/work_order/work_order.js:491 msgid "{0} items in progress" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:508 +#: erpnext/manufacturing/doctype/work_order/work_order.js:515 msgid "{0} items lost during process." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:465 +#: erpnext/manufacturing/doctype/work_order/work_order.js:472 msgid "{0} items produced" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:488 +#: erpnext/manufacturing/doctype/work_order/work_order.js:495 msgid "{0} items returned" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:491 +#: erpnext/manufacturing/doctype/work_order/work_order.js:498 msgid "{0} items to return" msgstr "" @@ -58693,11 +58941,11 @@ msgstr "" msgid "{0} must be negative in return document" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2367 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2364 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 "" @@ -58717,11 +58965,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 "" @@ -58729,16 +58977,16 @@ msgstr "" msgid "{0} units of {1} are required in {2} with the inventory dimension: {3} on {4} {5} for {6} to complete the transaction." msgstr "" -#: erpnext/stock/stock_ledger.py:1686 erpnext/stock/stock_ledger.py:2163 -#: erpnext/stock/stock_ledger.py:2177 +#: erpnext/stock/stock_ledger.py:1707 erpnext/stock/stock_ledger.py:2203 +#: erpnext/stock/stock_ledger.py:2217 msgid "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction." msgstr "" -#: erpnext/stock/stock_ledger.py:2264 erpnext/stock/stock_ledger.py:2309 +#: erpnext/stock/stock_ledger.py:2304 erpnext/stock/stock_ledger.py:2349 msgid "{0} units of {1} needed in {2} on {3} {4} to complete this transaction." msgstr "" -#: erpnext/stock/stock_ledger.py:1680 +#: erpnext/stock/stock_ledger.py:1701 msgid "{0} units of {1} needed in {2} to complete this transaction." msgstr "" @@ -58766,7 +59014,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 "" @@ -58804,7 +59052,7 @@ msgstr "" msgid "{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts." msgstr "" -#: erpnext/buying/doctype/purchase_order/purchase_order.py:435 +#: erpnext/buying/doctype/purchase_order/purchase_order.py:434 #: erpnext/selling/doctype/sales_order/sales_order.py:598 #: erpnext/stock/doctype/material_request/material_request.py:255 msgid "{0} {1} has been modified. Please refresh." @@ -58827,7 +59075,7 @@ msgid "{0} {1} is associated with {2}, but Party Account is {3}" msgstr "" #: erpnext/controllers/selling_controller.py:495 -#: erpnext/controllers/subcontracting_controller.py:1167 +#: erpnext/controllers/subcontracting_controller.py:1173 msgid "{0} {1} is cancelled or closed" msgstr "" @@ -58839,7 +59087,7 @@ msgstr "" msgid "{0} {1} is cancelled so the action cannot be completed" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:857 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:860 msgid "{0} {1} is closed" msgstr "" @@ -58851,7 +59099,7 @@ msgstr "" msgid "{0} {1} is frozen" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:854 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:857 msgid "{0} {1} is fully billed" msgstr "" @@ -58867,8 +59115,8 @@ msgstr "" msgid "{0} {1} is not in any active Fiscal Year" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:851 -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:890 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:854 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:893 msgid "{0} {1} is not submitted" msgstr "" @@ -58964,8 +59212,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 "" @@ -59005,15 +59253,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 "" @@ -59021,7 +59269,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 "" @@ -59029,7 +59277,7 @@ msgstr "" msgid "{}" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2133 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2130 msgid "{} can't be cancelled since the Loyalty Points earned has been redeemed. First cancel the {} No {}" msgstr "" diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 454f1934e13..f0d9576d469 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: { @@ -938,6 +955,8 @@ frappe.ui.form.on("BOM Item", "sourced_by_supplier", function (frm, cdt, cdn) { if (d.sourced_by_supplier) { d.rate = 0; refresh_field("rate", d.name, d.parentfield); + } else { + get_bom_material_detail(frm.doc, cdt, cdn, false); } }); @@ -986,7 +1005,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 +1026,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 58fce82c208..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: @@ -944,12 +982,14 @@ class BOM(WebsiteGenerator): hour_rate / flt(self.conversion_rate) if self.conversion_rate and hour_rate else hour_rate ) - if row.hour_rate and row.time_in_mins: + if row.hour_rate: row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate) - row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0 - row.base_operating_cost = flt(row.operating_cost) * flt(self.conversion_rate) - row.cost_per_unit = row.operating_cost / (row.batch_size or 1.0) - row.base_cost_per_unit = row.base_operating_cost / (row.batch_size or 1.0) + + if row.time_in_mins: + row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0 + row.base_operating_cost = flt(row.operating_cost) * flt(self.conversion_rate) + row.cost_per_unit = row.operating_cost / (row.batch_size or 1.0) + row.base_cost_per_unit = row.base_operating_cost / (row.batch_size or 1.0) if update_hour_rate: row.db_update() @@ -995,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." @@ -1219,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": @@ -1330,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, ): @@ -1341,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" @@ -1353,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, @@ -1386,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, @@ -1409,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, ) @@ -1430,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, ) @@ -1480,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 ( @@ -1771,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", } @@ -1917,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", @@ -1933,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..ad47d4024b4 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -20,6 +20,7 @@ "is_subcontracted", "is_final_finished_good", "set_cost_based_on_bom_qty", + "quality_inspection_required", "warehouse_section", "skip_material_transfer", "backflush_from_wip_warehouse", @@ -55,7 +56,6 @@ }, { "columns": 2, - "depends_on": "eval:!doc.workstation_type", "fieldname": "workstation", "fieldtype": "Link", "in_list_view": 1, @@ -291,13 +291,20 @@ "fieldname": "backflush_from_wip_warehouse", "fieldtype": "Check", "label": "Backflush Materials From WIP Warehouse" + }, + { + "default": "0", + "depends_on": "eval:parent.inspection_required", + "fieldname": "quality_inspection_required", + "fieldtype": "Check", + "label": "Quality Inspection Required" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-02-17 15:33:28.495850", + "modified": "2026-04-01 17:09:48.771834", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Operation", diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.py b/erpnext/manufacturing/doctype/bom_operation/bom_operation.py index fd197e89e62..72d7f194fd8 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.py +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.py @@ -35,6 +35,7 @@ class BOMOperation(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data + quality_inspection_required: DF.Check sequence_id: DF.Int set_cost_based_on_bom_qty: DF.Check skip_material_transfer: DF.Check 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/job_card_scrap_item/__init__.py b/erpnext/manufacturing/doctype/bom_secondary_item/__init__.py similarity index 100% rename from erpnext/manufacturing/doctype/job_card_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 b392a2aa02b..19132ecf9fd 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, @@ -40,6 +40,14 @@ frappe.ui.form.on("Job Card", { }; }); + frm.set_query("work_order", function () { + return { + filters: { + status: ["not in", ["Cancelled", "Closed", "Stopped"]], + }, + }; + }); + frm.events.set_company_filters(frm, "target_warehouse"); frm.events.set_company_filters(frm, "source_warehouse"); frm.events.set_company_filters(frm, "wip_warehouse"); @@ -96,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( @@ -780,26 +788,12 @@ frappe.ui.form.on("Job Card Time Log", { frm.events.set_total_completed_qty(frm); }, - - time_in_mins(frm, cdt, cdn) { - let d = locals[cdt][cdn]; - if (d.time_in_mins) { - d.to_time = add_mins_to_time(d.from_time, d.time_in_mins); - frappe.model.set_value(cdt, cdn, "to_time", d.to_time); - } - }, }); function get_seconds_diff(d1, d2) { return moment(d1).diff(d2, "seconds"); } -function add_mins_to_time(datetime, mins) { - let new_date = moment(datetime).add(mins, "minutes"); - - return new_date.format("YYYY-MM-DD HH:mm:ss"); -} - function get_last_completed_row(time_logs) { let completed_rows = time_logs.filter((d) => d.to_time); diff --git a/erpnext/manufacturing/doctype/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..9716ae49dc0 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 @@ -770,27 +788,27 @@ class JobCard(Document): ["action_if_quality_inspection_is_not_submitted", "action_if_quality_inspection_is_rejected"], ) - item = self.finished_good or self.production_item - bom_inspection_required = frappe.db.get_value( - "BOM", self.semi_fg_bom or self.bom_no, "inspection_required" + bom_inspection_required = frappe.get_value("BOM", self.bom_no, "inspection_required") + operation_inspection_required = frappe.get_value( + "Work Order Operation", self.operation_id, "quality_inspection_required" ) - if bom_inspection_required: + if bom_inspection_required and operation_inspection_required: if not self.quality_inspection: frappe.throw( _( "Quality Inspection is required for the item {0} before completing the job card {1}" - ).format(get_link_to_form("Item", item), bold(self.name)) + ).format(get_link_to_form("Item", self.finished_good), bold(self.name)) ) - qa_status, docstatus = frappe.db.get_value( + + qa_status, docstatus = frappe.get_value( "Quality Inspection", self.quality_inspection, ["status", "docstatus"] ) - if docstatus != 1: if action_submit == "Stop": frappe.throw( _("Quality Inspection {0} is not submitted for the item: {1}").format( get_link_to_form("Quality Inspection", self.quality_inspection), - get_link_to_form("Item", item), + get_link_to_form("Item", self.finished_good), ), title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError, @@ -799,7 +817,7 @@ class JobCard(Document): frappe.msgprint( _("Quality Inspection {0} is not submitted for the item: {1}").format( get_link_to_form("Quality Inspection", self.quality_inspection), - get_link_to_form("Item", item), + get_link_to_form("Item", self.finished_good), ), alert=True, indicator="orange", @@ -809,7 +827,7 @@ class JobCard(Document): frappe.throw( _("Quality Inspection {0} is rejected for the item: {1}").format( get_link_to_form("Quality Inspection", self.quality_inspection), - get_link_to_form("Item", item), + get_link_to_form("Item", self.finished_good), ), title=_("Inspection Rejected"), exc=QualityInspectionRejectedError, @@ -818,7 +836,7 @@ class JobCard(Document): frappe.msgprint( _("Quality Inspection {0} is rejected for the item: {1}").format( get_link_to_form("Quality Inspection", self.quality_inspection), - get_link_to_form("Item", item), + get_link_to_form("Item", self.finished_good), ), alert=True, indicator="orange", @@ -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..9a6733232a1 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -87,6 +87,7 @@ class TestJobCard(ERPNextTestSuite): with_operations=1, track_semi_finished_goods=1, company="_Test Company", + inspection_required=1, ) final_bom.append("items", {"item_code": raw.name, "qty": 1}) final_bom.append( @@ -97,6 +98,7 @@ class TestJobCard(ERPNextTestSuite): "bom_no": cut_bom, "skip_material_transfer": 1, "time_in_mins": 60, + "quality_inspection_required": 1, }, ) final_bom.append( @@ -133,6 +135,15 @@ class TestJobCard(ERPNextTestSuite): work_order.submit() job_card = frappe.get_all("Job Card", filters={"work_order": work_order.name, "operation": "Cutting"}) job_card_doc = frappe.get_doc("Job Card", job_card[0].name) + job_card_doc.append( + "time_logs", + { + "from_time": "2024-01-01 08:00:00", + "to_time": "2024-01-01 09:00:00", + "time_in_mins": 60, + "completed_qty": 1, + }, + ) self.assertRaises(frappe.ValidationError, job_card_doc.submit) def test_job_card_operations(self): @@ -882,6 +893,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/report/bom_stock_calculated/__init__.py b/erpnext/manufacturing/doctype/job_card_secondary_item/__init__.py similarity index 100% rename from erpnext/manufacturing/report/bom_stock_calculated/__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/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 30b3968fc80..36364f6740b 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -616,7 +616,12 @@ class ProductionPlan(Document): None, ): item.db_set("sub_assembly_item_reference", reference) - elif self.reserve_stock and item.main_item_code and item.from_bom: + elif ( + self.reserve_stock + and item.main_item_code + and item.from_bom + and item.main_item_code != frappe.get_cached_value("BOM", item.from_bom, "item") + ): frappe.throw( _( "Sub assembly item references are missing. Please fetch the sub assemblies and raw materials again." @@ -1778,8 +1783,10 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d ) sales_order = data.get("sales_order") + qty_precision = frappe.get_precision("Material Request Plan Item", "quantity") for key, details in item_details.items(): + details.qty = flt(details.qty, qty_precision) so_item_details.setdefault(sales_order, frappe._dict()) if key in so_item_details.get(sales_order, {}): so_item_details[sales_order][key]["qty"] = so_item_details[sales_order][key].get( diff --git a/erpnext/manufacturing/doctype/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 3b39c58fbac..b2a1eba0232 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): @@ -508,10 +508,42 @@ class TestWorkOrder(ERPNextTestSuite): def test_work_order_material_transferred_qty_with_process_loss(self): stock_entries = [] - bom = frappe.get_doc( - "BOM", {"docstatus": 1, "with_operations": 1, "company": "_Test Company", "has_variants": 0} + item_code = make_item("_Test Item For Process Loss", {"is_stock_item": 1}).name + rm_item_code = make_item("Test Item For Process Loss RM", {"is_stock_item": 1}).name + + bom = make_bom( + item=item_code, + raw_materials=[rm_item_code], + with_operations=1, + do_not_save=True, ) + operation_name = "_Test Custom Operation" + workstation_name = "_Test Custom Workstation" + + if not frappe.db.exists("Workstation", workstation_name): + doc = frappe.new_doc("Workstation") + doc.workstation_name = workstation_name + doc.save() + + if not frappe.db.exists("Operation", operation_name): + doc = frappe.new_doc("Operation") + doc.name = operation_name + doc.workstation = workstation_name + doc.save() + + operation = { + "operation": operation_name, + "workstation": workstation_name, + "description": "Test Data", + "operating_cost": 100, + "time_in_mins": 40, + } + + bom.append("operations", operation) + bom.save() + bom.submit() + work_order = make_wo_order_test_record( item=bom.item, qty=2, @@ -997,7 +1029,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", @@ -1056,7 +1088,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 @@ -1068,7 +1100,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 @@ -2116,10 +2148,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, @@ -2151,16 +2185,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} @@ -2395,7 +2433,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 @@ -2435,27 +2473,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() @@ -2470,7 +2490,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 @@ -2494,10 +2514,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 @@ -2506,11 +2551,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") @@ -2585,7 +2638,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, @@ -2596,6 +2649,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 @@ -2607,19 +2671,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, @@ -2655,9 +2757,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", { @@ -2670,9 +2771,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", { @@ -2688,13 +2788,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 @@ -2707,16 +2809,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, @@ -2724,7 +2825,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) @@ -2740,6 +2841,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), @@ -2747,6 +2849,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) @@ -3933,7 +4311,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() @@ -3946,7 +4324,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() @@ -4141,7 +4519,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}, @@ -4181,17 +4559,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..19185a6ec58 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1277,6 +1277,7 @@ class WorkOrder(Document): "skip_material_transfer", "backflush_from_wip_warehouse", "set_cost_based_on_bom_qty", + "quality_inspection_required", ], order_by="idx", ) @@ -2356,7 +2357,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 +2377,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 +2418,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 +2432,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 +2503,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 +2520,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 +2530,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 +2665,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/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json index 6cbcc855d01..89ed830116b 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json @@ -15,6 +15,7 @@ "workstation_type", "workstation", "sequence_id", + "quality_inspection_required", "section_break_insy", "bom_no", "finished_good", @@ -294,13 +295,19 @@ "fieldtype": "Check", "label": "Backflush Materials From WIP Warehouse", "read_only": 1 + }, + { + "default": "0", + "fieldname": "quality_inspection_required", + "fieldtype": "Check", + "label": "Quality Inspection Required" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-05-15 15:10:06.885440", + "modified": "2026-03-30 17:20:08.874381", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Operation", diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.py b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.py index fb8b3feb4dd..2e45434f94b 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.py +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.py @@ -36,6 +36,7 @@ class WorkOrderOperation(Document): planned_operating_cost: DF.Currency planned_start_time: DF.Datetime | None process_loss_qty: DF.Float + quality_inspection_required: DF.Check sequence_id: DF.Int skip_material_transfer: DF.Check source_warehouse: DF.Link | None diff --git a/erpnext/manufacturing/doctype/workstation/workstation_list.js b/erpnext/manufacturing/doctype/workstation/workstation_list.js index 33722634b96..4c81ab082bf 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation_list.js +++ b/erpnext/manufacturing/doctype/workstation/workstation_list.js @@ -10,6 +10,6 @@ frappe.listview_settings["Workstation"] = { Setup: "blue", }; - return [__(doc.status), color_map[doc.status], true]; + return [__(doc.status), color_map[doc.status], "status,=," + doc.status]; }, }; diff --git a/erpnext/manufacturing/report/bom_stock_report/__init__.py b/erpnext/manufacturing/report/bom_stock_analysis/__init__.py similarity index 100% rename from erpnext/manufacturing/report/bom_stock_report/__init__.py rename to erpnext/manufacturing/report/bom_stock_analysis/__init__.py diff --git a/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js b/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js new file mode 100644 index 00000000000..7629c102d7c --- /dev/null +++ b/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js @@ -0,0 +1,59 @@ +// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.query_reports["BOM Stock Analysis"] = { + filters: [ + { + fieldname: "bom", + label: __("BOM"), + fieldtype: "Link", + options: "BOM", + reqd: 1, + }, + { + fieldname: "warehouse", + label: __("Warehouse"), + fieldtype: "Link", + options: "Warehouse", + }, + { + fieldname: "qty_to_make", + label: __("FG Items to Make"), + fieldtype: "Float", + }, + { + fieldname: "show_exploded_view", + label: __("Show availability of exploded items"), + fieldtype: "Check", + default: false, + }, + ], + formatter(value, row, column, data, default_formatter) { + if (data && data.bold && column.fieldname === "item") { + return value ? `${value}` : ""; + } + + value = default_formatter(value, row, column, data); + + if (column.fieldname === "difference_qty" && value !== "" && value !== undefined) { + const numeric = parseFloat(value.replace(/,/g, "")) || 0; + if (numeric < 0) { + value = `${value}`; + } else if (numeric > 0) { + value = `${value}`; + } + } + + if (data && data.bold) { + if (column.fieldname === "description") { + const qty_to_make = Number(frappe.query_report.get_filter_value("qty_to_make")) || 0; + const producible = Number(String(data.description ?? "").replace(/,/g, "")) || 0; + const colour = qty_to_make && producible < qty_to_make ? "red" : "green"; + return `${value}`; + } + return `${value}`; + } + + return value; + }, +}; diff --git a/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.json b/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.json new file mode 100644 index 00000000000..b0e68f77ba7 --- /dev/null +++ b/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.json @@ -0,0 +1,31 @@ +{ + "add_total_row": 0, + "add_translate_data": 0, + "columns": [], + "creation": "2026-03-23 15:42:06.064606", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letter_head": null, + "modified": "2026-03-23 15:48:56.933892", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "BOM Stock Analysis", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "BOM", + "report_name": "BOM Stock Analysis", + "report_type": "Script Report", + "roles": [ + { + "role": "Manufacturing Manager" + }, + { + "role": "Manufacturing User" + } + ], + "timeout": 0 +} diff --git a/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py b/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py new file mode 100644 index 00000000000..59578127f9f --- /dev/null +++ b/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.py @@ -0,0 +1,326 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.query_builder.functions import Floor, IfNull, Sum +from frappe.utils import flt, fmt_money +from frappe.utils.data import comma_and +from pypika.terms import ExistsCriterion + + +def execute(filters=None): + filters = filters or {} + if filters.get("qty_to_make"): + columns = get_columns_with_qty_to_make() + data = get_data_with_qty_to_make(filters) + else: + columns = get_columns_without_qty_to_make() + data = get_data_without_qty_to_make(filters) + + return columns, data + + +def fmt_qty(value): + """Format a float quantity for display as a string, so blank rows stay blank.""" + return frappe.utils.fmt_money(value, precision=2, currency=None) + + +def fmt_rate(value): + """Format a currency rate for display as a string.""" + currency = frappe.defaults.get_global_default("currency") + return frappe.utils.fmt_money(value, precision=2, currency=currency) + + +def get_data_with_qty_to_make(filters): + bom_data = get_bom_data(filters) + manufacture_details = get_manufacturer_records() + purchase_rates = batch_fetch_purchase_rates(bom_data) + qty_to_make = flt(filters.get("qty_to_make")) + + data = [] + for row in bom_data: + qty_per_unit = flt(row.qty_per_unit) if row.qty_per_unit > 0 else 0 + required_qty = qty_to_make * qty_per_unit + difference_qty = flt(row.actual_qty) - required_qty + rate = purchase_rates.get(row.item_code, 0) + + data.append( + { + "item": row.item_code, + "description": row.description, + "from_bom_no": row.from_bom_no, + "manufacturer": comma_and( + manufacture_details.get(row.item_code, {}).get("manufacturer", []), add_quotes=False + ), + "manufacturer_part_number": comma_and( + manufacture_details.get(row.item_code, {}).get("manufacturer_part", []), add_quotes=False + ), + "qty_per_unit": fmt_qty(qty_per_unit), + "available_qty": fmt_qty(row.actual_qty), + "required_qty": fmt_qty(required_qty), + "difference_qty": fmt_qty(difference_qty), + "last_purchase_rate": fmt_rate(rate), + "_available_qty": flt(row.actual_qty), + "_qty_per_unit": qty_per_unit, + } + ) + + min_producible = ( + min(int(r["_available_qty"] // r["_qty_per_unit"]) for r in data if r["_qty_per_unit"]) if data else 0 + ) + + for row in data: + row.pop("_available_qty", None) + row.pop("_qty_per_unit", None) + + # blank spacer row + data.append({}) + + data.append( + { + "item": _("Maximum Producible Items"), + "description": min_producible, + "from_bom_no": "", + "manufacturer": "", + "manufacturer_part_number": "", + "qty_per_unit": "", + "available_qty": "", + "required_qty": "", + "difference_qty": "", + "last_purchase_rate": "", + "bold": 1, + } + ) + + return data + + +def get_columns_with_qty_to_make(): + return [ + {"fieldname": "item", "label": _("Item"), "fieldtype": "Link", "options": "Item", "width": 180}, + {"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 160}, + { + "fieldname": "from_bom_no", + "label": _("From BOM No"), + "fieldtype": "Link", + "options": "BOM", + "width": 150, + }, + {"fieldname": "manufacturer", "label": _("Manufacturer"), "fieldtype": "Data", "width": 130}, + { + "fieldname": "manufacturer_part_number", + "label": _("Manufacturer Part Number"), + "fieldtype": "Data", + "width": 170, + }, + {"fieldname": "qty_per_unit", "label": _("Qty Per Unit"), "fieldtype": "Data", "width": 110}, + {"fieldname": "available_qty", "label": _("Available Qty"), "fieldtype": "Data", "width": 120}, + {"fieldname": "required_qty", "label": _("Required Qty"), "fieldtype": "Data", "width": 120}, + {"fieldname": "difference_qty", "label": _("Difference Qty"), "fieldtype": "Data", "width": 130}, + { + "fieldname": "last_purchase_rate", + "label": _("Last Purchase Rate"), + "fieldtype": "Data", + "width": 160, + }, + ] + + +def get_data_without_qty_to_make(filters): + raw_rows = get_producible_fg_items(filters) + + data = [] + for row in raw_rows: + data.append( + { + "item": row[0], + "description": row[1], + "from_bom_no": row[2], + "qty_per_unit": fmt_qty(row[3]), + "available_qty": fmt_qty(row[4]), + } + ) + + min_producible = min((row[5] or 0) for row in raw_rows) if raw_rows else 0 + # blank spacer row + data.append({}) + + data.append( + { + "item": _("Maximum Producible Items"), + "description": min_producible, + "from_bom_no": "", + "qty_per_unit": "", + "available_qty": "", + "bold": 1, + } + ) + + return data + + +def get_columns_without_qty_to_make(): + return [ + {"fieldname": "item", "label": _("Item"), "fieldtype": "Link", "options": "Item", "width": 180}, + {"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 200}, + { + "fieldname": "from_bom_no", + "label": _("From BOM No"), + "fieldtype": "Link", + "options": "BOM", + "width": 160, + }, + {"fieldname": "qty_per_unit", "label": _("Qty Per Unit"), "fieldtype": "Data", "width": 120}, + {"fieldname": "available_qty", "label": _("Available Qty"), "fieldtype": "Data", "width": 120}, + ] + + +def batch_fetch_purchase_rates(bom_data): + if not bom_data: + return {} + item_codes = [row.item_code for row in bom_data] + return { + r.name: r.last_purchase_rate + for r in frappe.get_all( + "Item", + filters={"name": ["in", item_codes]}, + fields=["name", "last_purchase_rate"], + ) + } + + +def get_bom_data(filters): + bom_item_table = "BOM Explosion Item" if filters.get("show_exploded_view") else "BOM Item" + + bom_item = frappe.qb.DocType(bom_item_table) + bin = frappe.qb.DocType("Bin") + + query = ( + frappe.qb.from_(bom_item) + .left_join(bin) + .on(bom_item.item_code == bin.item_code) + .select( + bom_item.item_code, + bom_item.description, + bom_item.parent.as_("from_bom_no"), + Sum(bom_item.qty_consumed_per_unit).as_("qty_per_unit"), + IfNull(Sum(bin.actual_qty), 0).as_("actual_qty"), + ) + .where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM")) + .groupby(bom_item.item_code) + .orderby(bom_item.idx) + ) + + if filters.get("warehouse"): + warehouse_details = frappe.db.get_value( + "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 + ) + if warehouse_details: + wh = frappe.qb.DocType("Warehouse") + query = query.where( + ExistsCriterion( + frappe.qb.from_(wh) + .select(wh.name) + .where( + (wh.lft >= warehouse_details.lft) + & (wh.rgt <= warehouse_details.rgt) + & (bin.warehouse == wh.name) + ) + ) + ) + else: + query = query.where(bin.warehouse == filters.get("warehouse")) + + if bom_item_table == "BOM Item": + query = query.select(bom_item.bom_no, bom_item.is_phantom_item) + + data = query.run(as_dict=True) + return explode_phantom_boms(data, filters) if bom_item_table == "BOM Item" else data + + +def explode_phantom_boms(data, filters): + original_bom = filters.get("bom") + replacements = [] + + for idx, item in enumerate(data): + if not item.is_phantom_item: + continue + + filters["bom"] = item.bom_no + children = get_bom_data(filters) + filters["bom"] = original_bom + + for child in children: + child.qty_per_unit = (child.qty_per_unit or 0) * (item.qty_per_unit or 0) + + replacements.append((idx, children)) + + for idx, children in reversed(replacements): + data.pop(idx) + data[idx:idx] = children + + return data + + +def get_manufacturer_records(): + details = frappe.get_all( + "Item Manufacturer", fields=["manufacturer", "manufacturer_part_no", "item_code"] + ) + manufacture_details = frappe._dict() + for detail in details: + dic = manufacture_details.setdefault(detail.get("item_code"), {}) + dic.setdefault("manufacturer", []).append(detail.get("manufacturer")) + dic.setdefault("manufacturer_part", []).append(detail.get("manufacturer_part_no")) + return manufacture_details + + +def get_producible_fg_items(filters): + BOM_ITEM = frappe.qb.DocType("BOM Item") + BOM = frappe.qb.DocType("BOM") + BIN = frappe.qb.DocType("Bin") + WH = frappe.qb.DocType("Warehouse") + + warehouse = filters.get("warehouse") + if not warehouse: + frappe.throw(_("Warehouse is required to get producible FG Items")) + + warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1) + + if warehouse_details: + bin_subquery = ( + frappe.qb.from_(BIN) + .join(WH) + .on(BIN.warehouse == WH.name) + .select(BIN.item_code, Sum(BIN.actual_qty).as_("actual_qty")) + .where((WH.lft >= warehouse_details.lft) & (WH.rgt <= warehouse_details.rgt)) + .groupby(BIN.item_code) + ) + else: + bin_subquery = ( + frappe.qb.from_(BIN) + .select(BIN.item_code, Sum(BIN.actual_qty).as_("actual_qty")) + .where(BIN.warehouse == warehouse) + .groupby(BIN.item_code) + ) + + query = ( + frappe.qb.from_(BOM_ITEM) + .join(BOM) + .on(BOM_ITEM.parent == BOM.name) + .left_join(bin_subquery) + .on(BOM_ITEM.item_code == bin_subquery.item_code) + .select( + BOM_ITEM.item_code, + BOM_ITEM.description, + BOM_ITEM.parent.as_("from_bom_no"), + (BOM_ITEM.stock_qty / BOM.quantity).as_("qty_per_unit"), + IfNull(bin_subquery.actual_qty, 0).as_("available_qty"), + Floor(bin_subquery.actual_qty / ((Sum(BOM_ITEM.stock_qty)) / BOM.quantity)), + ) + .where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM")) + .groupby(BOM_ITEM.item_code) + .orderby(BOM_ITEM.idx) + ) + + return query.run(as_list=True) diff --git a/erpnext/manufacturing/report/bom_stock_analysis/test_bom_stock_analysis.py b/erpnext/manufacturing/report/bom_stock_analysis/test_bom_stock_analysis.py new file mode 100644 index 00000000000..fd8a52afde0 --- /dev/null +++ b/erpnext/manufacturing/report/bom_stock_analysis/test_bom_stock_analysis.py @@ -0,0 +1,171 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt +import frappe +from frappe.utils import fmt_money + +from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom +from erpnext.manufacturing.report.bom_stock_analysis.bom_stock_analysis import ( + execute as bom_stock_analysis_report, +) +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.tests.utils import ERPNextTestSuite + + +def fmt_qty(value): + return fmt_money(value, precision=2, currency=None) + + +def fmt_rate(value): + currency = frappe.defaults.get_global_default("currency") + return fmt_money(value, precision=2, currency=currency) + + +class TestBOMStockAnalysis(ERPNextTestSuite): + def setUp(self): + self.fg_item, self.rm_items = create_items() + self.boms = create_boms(self.fg_item, self.rm_items) + + def test_bom_stock_analysis(self): + qty_to_make = 10 + + # Case 1: When Item(s) Qty and Stock Qty are equal. + raw_data = bom_stock_analysis_report( + filters={ + "qty_to_make": qty_to_make, + "bom": self.boms[0].name, + } + )[1] + + data, footer = split_data_and_footer(raw_data) + expected_data, expected_min = get_expected_data(self.boms[0], qty_to_make) + + self.assertSetEqual( + set(tuple(sorted(r.items())) for r in data), + set(tuple(sorted(r.items())) for r in expected_data), + ) + self.assertEqual(footer.get("description"), expected_min) + + # Case 2: When Item(s) Qty and Stock Qty are different and BOM Qty is 1. + raw_data = bom_stock_analysis_report( + filters={ + "qty_to_make": qty_to_make, + "bom": self.boms[1].name, + } + )[1] + + data, footer = split_data_and_footer(raw_data) + expected_data, expected_min = get_expected_data(self.boms[1], qty_to_make) + + self.assertSetEqual( + set(tuple(sorted(r.items())) for r in data), + set(tuple(sorted(r.items())) for r in expected_data), + ) + self.assertEqual(footer.get("description"), expected_min) + + # Case 3: When Item(s) Qty and Stock Qty are different and BOM Qty is greater than 1. + raw_data = bom_stock_analysis_report( + filters={ + "qty_to_make": qty_to_make, + "bom": self.boms[2].name, + } + )[1] + + data, footer = split_data_and_footer(raw_data) + expected_data, expected_min = get_expected_data(self.boms[2], qty_to_make) + + self.assertSetEqual( + set(tuple(sorted(r.items())) for r in data), + set(tuple(sorted(r.items())) for r in expected_data), + ) + self.assertEqual(footer.get("description"), expected_min) + + +def split_data_and_footer(raw_data): + """Separate component rows from the footer row. Skips blank spacer rows.""" + data = [row for row in raw_data if row and not row.get("bold")] + footer = next((row for row in raw_data if row and row.get("bold")), {}) + return data, footer + + +def create_items(): + fg_item = make_item(properties={"is_stock_item": 1}).name + rm_item1 = make_item( + properties={ + "is_stock_item": 1, + "standard_rate": 100, + "opening_stock": 100, + "last_purchase_rate": 100, + "item_defaults": [{"company": "_Test Company", "default_warehouse": "Stores - _TC"}], + } + ).name + rm_item2 = make_item( + properties={ + "is_stock_item": 1, + "standard_rate": 200, + "opening_stock": 200, + "last_purchase_rate": 200, + "item_defaults": [{"company": "_Test Company", "default_warehouse": "Stores - _TC"}], + } + ).name + + return fg_item, [rm_item1, rm_item2] + + +def create_boms(fg_item, rm_items): + def update_bom_items(bom, uom, conversion_factor): + for item in bom.items: + item.uom = uom + item.conversion_factor = conversion_factor + return bom + + bom1 = make_bom(item=fg_item, quantity=1, raw_materials=rm_items, rm_qty=10) + + bom2 = make_bom(item=fg_item, quantity=1, raw_materials=rm_items, rm_qty=10, do_not_submit=True) + bom2 = update_bom_items(bom2, "Box", 10) + bom2.save() + bom2.submit() + + bom3 = make_bom(item=fg_item, quantity=2, raw_materials=rm_items, rm_qty=10, do_not_submit=True) + bom3 = update_bom_items(bom3, "Box", 10) + bom3.save() + bom3.submit() + + return [bom1, bom2, bom3] + + +def get_expected_data(bom, qty_to_make): + """ + Returns (component_rows, min_producible). + Component rows are dicts matching what the report produces. + min_producible is the expected footer value. + """ + expected_data = [] + producible_per_item = [] + + for idx, bom_item in enumerate(bom.items): + qty_per_unit = float(bom_item.stock_qty / bom.quantity) + available_qty = float(100 * (idx + 1)) + required_qty = float(qty_to_make * qty_per_unit) + difference_qty = available_qty - required_qty + last_purchase_rate = float(100 * (idx + 1)) + + expected_data.append( + { + "item": bom_item.item_code, + "description": bom_item.item_code, # description falls back to item_code in test items + "from_bom_no": bom.name, + "manufacturer": "", + "manufacturer_part_number": "", + "qty_per_unit": fmt_qty(qty_per_unit), + "available_qty": fmt_qty(available_qty), + "required_qty": fmt_qty(required_qty), + "difference_qty": fmt_qty(difference_qty), + "last_purchase_rate": fmt_rate(last_purchase_rate), + } + ) + + producible_per_item.append(int(available_qty // qty_per_unit) if qty_per_unit else 0) + + min_producible = min(producible_per_item) if producible_per_item else 0 + + return expected_data, min_producible diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js deleted file mode 100644 index 76a95127853..00000000000 --- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2016, Epoch Consulting and contributors -// For license information, please see license.txt - -frappe.query_reports["BOM Stock Calculated"] = { - filters: [ - { - fieldname: "bom", - label: __("BOM"), - fieldtype: "Link", - options: "BOM", - reqd: 1, - }, - { - fieldname: "warehouse", - label: __("Warehouse"), - fieldtype: "Link", - options: "Warehouse", - }, - { - fieldname: "qty_to_make", - label: __("Quantity to Make"), - fieldtype: "Float", - default: "1.0", - reqd: 1, - }, - { - fieldname: "show_exploded_view", - label: __("Show exploded view"), - fieldtype: "Check", - default: false, - }, - ], -}; diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.json b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.json deleted file mode 100644 index 73421cebf0e..00000000000 --- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "add_total_row": 0, - "creation": "2018-05-17 12:40:31.355049", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 0, - "is_standard": "Yes", - "letter_head": "", - "modified": "2018-06-18 13:33:18.103220", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "BOM Stock Calculated", - "owner": "Administrator", - "ref_doctype": "BOM", - "report_name": "BOM Stock Calculated", - "report_type": "Script Report", - "roles": [ - { - "role": "Manufacturing Manager" - }, - { - "role": "Manufacturing User" - } - ] -} diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py deleted file mode 100644 index 4b5df4df4b2..00000000000 --- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py +++ /dev/null @@ -1,199 +0,0 @@ -# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import frappe -from frappe import _ -from frappe.query_builder.functions import IfNull, Sum -from frappe.utils.data import comma_and -from pypika.terms import ExistsCriterion - - -def execute(filters=None): - columns = get_columns() - data = [] - - bom_data = get_bom_data(filters) - qty_to_make = filters.get("qty_to_make") - manufacture_details = get_manufacturer_records() - - for row in bom_data: - required_qty = qty_to_make * row.qty_per_unit - last_purchase_rate = frappe.db.get_value("Item", row.item_code, "last_purchase_rate") - - data.append(get_report_data(last_purchase_rate, required_qty, row, manufacture_details)) - - return columns, data - - -def get_report_data(last_purchase_rate, required_qty, row, manufacture_details): - qty_per_unit = row.qty_per_unit if row.qty_per_unit > 0 else 0 - difference_qty = row.actual_qty - required_qty - return [ - row.item_code, - row.description, - row.from_bom_no, - comma_and(manufacture_details.get(row.item_code, {}).get("manufacturer", []), add_quotes=False), - comma_and(manufacture_details.get(row.item_code, {}).get("manufacturer_part", []), add_quotes=False), - qty_per_unit, - row.actual_qty, - required_qty, - difference_qty, - last_purchase_rate, - ] - - -def get_columns(): - return [ - { - "fieldname": "item", - "label": _("Item"), - "fieldtype": "Link", - "options": "Item", - "width": 120, - }, - { - "fieldname": "description", - "label": _("Description"), - "fieldtype": "Data", - "width": 150, - }, - { - "fieldname": "from_bom_no", - "label": _("From BOM No"), - "fieldtype": "Link", - "options": "BOM", - "width": 150, - }, - { - "fieldname": "manufacturer", - "label": _("Manufacturer"), - "fieldtype": "Data", - "width": 120, - }, - { - "fieldname": "manufacturer_part_number", - "label": _("Manufacturer Part Number"), - "fieldtype": "Data", - "width": 150, - }, - { - "fieldname": "qty_per_unit", - "label": _("Qty Per Unit"), - "fieldtype": "Float", - "width": 110, - }, - { - "fieldname": "available_qty", - "label": _("Available Qty"), - "fieldtype": "Float", - "width": 120, - }, - { - "fieldname": "required_qty", - "label": _("Required Qty"), - "fieldtype": "Float", - "width": 120, - }, - { - "fieldname": "difference_qty", - "label": _("Difference Qty"), - "fieldtype": "Float", - "width": 130, - }, - { - "fieldname": "last_purchase_rate", - "label": _("Last Purchase Rate"), - "fieldtype": "Float", - "width": 160, - }, - ] - - -def get_bom_data(filters): - bom_item_table = "BOM Explosion Item" if filters.get("show_exploded_view") else "BOM Item" - - bom_item = frappe.qb.DocType(bom_item_table) - bin = frappe.qb.DocType("Bin") - - query = ( - frappe.qb.from_(bom_item) - .left_join(bin) - .on(bom_item.item_code == bin.item_code) - .select( - bom_item.item_code, - bom_item.description, - bom_item.parent.as_("from_bom_no"), - bom_item.qty_consumed_per_unit.as_("qty_per_unit"), - IfNull(Sum(bin.actual_qty), 0).as_("actual_qty"), - ) - .where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM")) - .groupby(bom_item.item_code) - .orderby(bom_item.idx) - ) - - if filters.get("warehouse"): - warehouse_details = frappe.db.get_value( - "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 - ) - - if warehouse_details: - wh = frappe.qb.DocType("Warehouse") - query = query.where( - ExistsCriterion( - frappe.qb.from_(wh) - .select(wh.name) - .where( - (wh.lft >= warehouse_details.lft) - & (wh.rgt <= warehouse_details.rgt) - & (bin.warehouse == wh.name) - ) - ) - ) - else: - query = query.where(bin.warehouse == filters.get("warehouse")) - - if bom_item_table == "BOM Item": - query = query.select(bom_item.bom_no, bom_item.is_phantom_item) - - data = query.run(as_dict=True) - return explode_phantom_boms(data, filters) if bom_item_table == "BOM Item" else data - - -def explode_phantom_boms(data, filters): - original_bom = filters.get("bom") - replacements = [] - - for idx, item in enumerate(data): - if not item.is_phantom_item: - continue - - filters["bom"] = item.bom_no - children = get_bom_data(filters) - filters["bom"] = original_bom - - for child in children: - child.qty_per_unit = (child.qty_per_unit or 0) * (item.qty_per_unit or 0) - - replacements.append((idx, children)) - - for idx, children in reversed(replacements): - data.pop(idx) - data[idx:idx] = children - - filters["bom"] = original_bom - return data - - -def get_manufacturer_records(): - details = frappe.get_all( - "Item Manufacturer", fields=["manufacturer", "manufacturer_part_no", "item_code"] - ) - - manufacture_details = frappe._dict() - for detail in details: - dic = manufacture_details.setdefault(detail.get("item_code"), {}) - dic.setdefault("manufacturer", []).append(detail.get("manufacturer")) - dic.setdefault("manufacturer_part", []).append(detail.get("manufacturer_part_no")) - - return manufacture_details diff --git a/erpnext/manufacturing/report/bom_stock_calculated/test_bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/test_bom_stock_calculated.py deleted file mode 100644 index e0105b114c5..00000000000 --- a/erpnext/manufacturing/report/bom_stock_calculated/test_bom_stock_calculated.py +++ /dev/null @@ -1,119 +0,0 @@ -# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import frappe - -from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom -from erpnext.manufacturing.report.bom_stock_calculated.bom_stock_calculated import ( - execute as bom_stock_calculated_report, -) -from erpnext.stock.doctype.item.test_item import make_item -from erpnext.tests.utils import ERPNextTestSuite - - -class TestBOMStockCalculated(ERPNextTestSuite): - def setUp(self): - self.fg_item, self.rm_items = create_items() - self.boms = create_boms(self.fg_item, self.rm_items) - - def test_bom_stock_calculated(self): - qty_to_make = 10 - - # Case 1: When Item(s) Qty and Stock Qty are equal. - data = bom_stock_calculated_report( - filters={ - "qty_to_make": qty_to_make, - "bom": self.boms[0].name, - } - )[1] - expected_data = get_expected_data(self.boms[0], qty_to_make) - self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) - - # Case 2: When Item(s) Qty and Stock Qty are different and BOM Qty is 1. - data = bom_stock_calculated_report( - filters={ - "qty_to_make": qty_to_make, - "bom": self.boms[1].name, - } - )[1] - expected_data = get_expected_data(self.boms[1], qty_to_make) - self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) - - # Case 3: When Item(s) Qty and Stock Qty are different and BOM Qty is greater than 1. - data = bom_stock_calculated_report( - filters={ - "qty_to_make": qty_to_make, - "bom": self.boms[2].name, - } - )[1] - expected_data = get_expected_data(self.boms[2], qty_to_make) - self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) - - -def create_items(): - fg_item = make_item(properties={"is_stock_item": 1}).name - rm_item1 = make_item( - properties={ - "is_stock_item": 1, - "standard_rate": 100, - "opening_stock": 100, - "last_purchase_rate": 100, - "item_defaults": [{"company": "_Test Company", "default_warehouse": "Stores - _TC"}], - } - ).name - rm_item2 = make_item( - properties={ - "is_stock_item": 1, - "standard_rate": 200, - "opening_stock": 200, - "last_purchase_rate": 200, - "item_defaults": [{"company": "_Test Company", "default_warehouse": "Stores - _TC"}], - } - ).name - - return fg_item, [rm_item1, rm_item2] - - -def create_boms(fg_item, rm_items): - def update_bom_items(bom, uom, conversion_factor): - for item in bom.items: - item.uom = uom - item.conversion_factor = conversion_factor - - return bom - - bom1 = make_bom(item=fg_item, quantity=1, raw_materials=rm_items, rm_qty=10) - - bom2 = make_bom(item=fg_item, quantity=1, raw_materials=rm_items, rm_qty=10, do_not_submit=True) - bom2 = update_bom_items(bom2, "Box", 10) - bom2.save() - bom2.submit() - - bom3 = make_bom(item=fg_item, quantity=2, raw_materials=rm_items, rm_qty=10, do_not_submit=True) - bom3 = update_bom_items(bom3, "Box", 10) - bom3.save() - bom3.submit() - - return [bom1, bom2, bom3] - - -def get_expected_data(bom, qty_to_make): - expected_data = [] - - for idx in range(len(bom.items)): - expected_data.append( - [ - bom.items[idx].item_code, - bom.items[idx].item_code, - bom.name, - "", - "", - float(bom.items[idx].stock_qty / bom.quantity), - float(100 * (idx + 1)), - float(qty_to_make * (bom.items[idx].stock_qty / bom.quantity)), - float((100 * (idx + 1)) - (qty_to_make * (bom.items[idx].stock_qty / bom.quantity))), - float(100 * (idx + 1)), - ] - ) - - return expected_data diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html deleted file mode 100644 index 2ae8848cc03..00000000000 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html +++ /dev/null @@ -1,27 +0,0 @@ -

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

    -
    {%= filters.bom %}
    -
    {%= filters.warehouse %}
    -
    - - - - - - - - - - - - - {% for(var i=0, l=data.length; i - - - - - - - {% } %} - -
    {%= __("Item") %}{%= __("Description") %}{%= __("Required Qty") %}{%= __("In Stock Qty") %}{%= __("Enough Parts to Build") %}
    {%= data[i][ __("Item")] %}{%= data[i][ __("Description")] %} {%= data[i][ __("Required Qty")] %} {%= data[i][ __("In Stock Qty")] %} {%= data[i][ __("Enough Parts to Build")] %}
    diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js deleted file mode 100644 index 91d73d0101c..00000000000 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js +++ /dev/null @@ -1,41 +0,0 @@ -frappe.query_reports["BOM Stock Report"] = { - filters: [ - { - fieldname: "bom", - label: __("BOM"), - fieldtype: "Link", - options: "BOM", - reqd: 1, - }, - { - fieldname: "warehouse", - label: __("Warehouse"), - fieldtype: "Link", - options: "Warehouse", - reqd: 1, - }, - { - fieldname: "show_exploded_view", - label: __("Show exploded view"), - fieldtype: "Check", - }, - { - fieldname: "qty_to_produce", - label: __("Quantity to Produce"), - fieldtype: "Int", - default: "1", - }, - ], - formatter: function (value, row, column, data, default_formatter) { - value = default_formatter(value, row, column, data); - - if (column.id == "item") { - if (data["in_stock_qty"] >= data["required_qty"]) { - value = `
    ${data["item"]}`; - } else { - value = `${data["item"]}`; - } - } - return value; - }, -}; diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.json b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.json deleted file mode 100644 index c563b87686d..00000000000 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2017-01-10 14:00:50.387244", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 0, - "is_standard": "Yes", - "letter_head": "", - "modified": "2017-06-23 04:46:43.209008", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "BOM Stock Report", - "owner": "Administrator", - "query": "SELECT \n\tbom_item.item_code as \"Item:Link/Item:200\",\n\tbom_item.description as \"Description:Data:300\",\n\tbom_item.qty as \"Required Qty:Float:100\",\n\tledger.actual_qty as \"In Stock Qty:Float:100\",\n\tFLOOR(ledger.actual_qty /bom_item.qty) as \"Enough Parts to Build:Int:100\"\nFROM\n\t`tabBOM Item` AS bom_item \n\tLEFT JOIN `tabBin` AS ledger\t\n\t\tON bom_item.item_code = ledger.item_code \n\t\tAND ledger.warehouse = %(warehouse)s\nWHERE\n\tbom_item.parent=%(bom)s\n\nGROUP BY bom_item.item_code", - "ref_doctype": "BOM", - "report_name": "BOM Stock Report", - "report_type": "Script Report", - "roles": [ - { - "role": "Manufacturing Manager" - }, - { - "role": "Manufacturing User" - } - ] -} \ No newline at end of file diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py deleted file mode 100644 index eeda32c64c7..00000000000 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import frappe -from frappe import _ -from frappe.query_builder.functions import Floor, Sum -from frappe.utils import cint - - -def execute(filters=None): - if not filters: - filters = {} - - columns = get_columns() - data = get_bom_stock(filters) - - return columns, data - - -def get_columns(): - return [ - _("Item") + ":Link/Item:150", - _("Item Name") + "::240", - _("Description") + "::300", - _("From BOM No") + "::200", - _("BOM Qty") + ":Float:160", - _("BOM UOM") + "::160", - _("Required Qty") + ":Float:120", - _("In Stock Qty") + ":Float:120", - _("Enough Parts to Build") + ":Float:200", - ] - - -def get_bom_stock(filters): - qty_to_produce = filters.get("qty_to_produce") - if cint(qty_to_produce) <= 0: - frappe.throw(_("Quantity to Produce should be greater than zero.")) - - bom_item_table = "BOM Explosion Item" if filters.get("show_exploded_view") else "BOM Item" - - warehouse = filters.get("warehouse") - warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1) - - BOM = frappe.qb.DocType("BOM") - BOM_ITEM = frappe.qb.DocType(bom_item_table) - BIN = frappe.qb.DocType("Bin") - WH = frappe.qb.DocType("Warehouse") - - if warehouse_details: - bin_subquery = ( - frappe.qb.from_(BIN) - .join(WH) - .on(BIN.warehouse == WH.name) - .select(BIN.item_code, Sum(BIN.actual_qty).as_("actual_qty")) - .where((WH.lft >= warehouse_details.lft) & (WH.rgt <= warehouse_details.rgt)) - .groupby(BIN.item_code) - ) - else: - bin_subquery = ( - frappe.qb.from_(BIN) - .select(BIN.item_code, Sum(BIN.actual_qty).as_("actual_qty")) - .where(BIN.warehouse == warehouse) - .groupby(BIN.item_code) - ) - - QUERY = ( - frappe.qb.from_(BOM) - .join(BOM_ITEM) - .on(BOM.name == BOM_ITEM.parent) - .left_join(bin_subquery) - .on(BOM_ITEM.item_code == bin_subquery.item_code) - .select( - BOM_ITEM.item_code, - BOM_ITEM.item_name, - BOM_ITEM.description, - BOM.name, - Sum(BOM_ITEM.stock_qty), - BOM_ITEM.stock_uom, - (Sum(BOM_ITEM.stock_qty) * qty_to_produce) / BOM.quantity, - bin_subquery.actual_qty, - Floor(bin_subquery.actual_qty / ((Sum(BOM_ITEM.stock_qty) * qty_to_produce) / BOM.quantity)), - ) - .where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM")) - .groupby(BOM_ITEM.item_code) - .orderby(BOM_ITEM.idx) - ) - - if bom_item_table == "BOM Item": - QUERY = QUERY.select(BOM_ITEM.bom_no, BOM_ITEM.is_phantom_item) - - data = QUERY.run(as_list=True) - return explode_phantom_boms(data, filters) if bom_item_table == "BOM Item" else data - - -def explode_phantom_boms(data, filters): - expanded = [] - for row in data: - if row[-1]: # last element is `is_phantom_item` - phantom_filters = filters.copy() - phantom_filters["qty_to_produce"] = row[-5] - phantom_filters["bom"] = row[-2] - expanded.extend(get_bom_stock(phantom_filters)) - else: - expanded.append(row) - - return expanded diff --git a/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py deleted file mode 100644 index 43706fcb4de..00000000000 --- a/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py +++ /dev/null @@ -1,112 +0,0 @@ -# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import frappe -from frappe.exceptions import ValidationError -from frappe.utils import floor - -from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom -from erpnext.manufacturing.report.bom_stock_report.bom_stock_report import ( - get_bom_stock as bom_stock_report, -) -from erpnext.stock.doctype.item.test_item import make_item -from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry -from erpnext.tests.utils import ERPNextTestSuite - - -class TestBomStockReport(ERPNextTestSuite): - def setUp(self): - self.warehouse = "_Test Warehouse - _TC" - self.fg_item, self.rm_items = create_items() - make_stock_entry(target=self.warehouse, item_code=self.rm_items[0], qty=20, basic_rate=100) - make_stock_entry(target=self.warehouse, item_code=self.rm_items[1], qty=40, basic_rate=200) - self.bom = make_bom(item=self.fg_item, quantity=1, raw_materials=self.rm_items, rm_qty=10) - - def test_bom_stock_report(self): - # Test 1: When `qty_to_produce` is 0. - filters = frappe._dict( - { - "bom": self.bom.name, - "warehouse": "Stores - _TC", - "qty_to_produce": 0, - } - ) - self.assertRaises(ValidationError, bom_stock_report, filters) - - # Test 2: When stock is not available. - data = bom_stock_report( - frappe._dict( - { - "bom": self.bom.name, - "warehouse": "Stores - _TC", - "qty_to_produce": 1, - } - ) - ) - expected_data = get_expected_data(self.bom, "Stores - _TC", 1) - self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) - - # Test 3: When stock is available. - data = bom_stock_report( - frappe._dict( - { - "bom": self.bom.name, - "warehouse": self.warehouse, - "qty_to_produce": 1, - } - ) - ) - expected_data = get_expected_data(self.bom, self.warehouse, 1) - self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) - - -def create_items(): - fg_item = make_item(properties={"is_stock_item": 1}).name - rm_item1 = make_item( - properties={ - "is_stock_item": 1, - "standard_rate": 100, - "opening_stock": 100, - "last_purchase_rate": 100, - } - ).name - rm_item2 = make_item( - properties={ - "is_stock_item": 1, - "standard_rate": 200, - "opening_stock": 200, - "last_purchase_rate": 200, - } - ).name - - return fg_item, [rm_item1, rm_item2] - - -def get_expected_data(bom, warehouse, qty_to_produce, show_exploded_view=False): - expected_data = [] - - for item in bom.get("exploded_items") if show_exploded_view else bom.get("items"): - in_stock_qty = frappe.get_cached_value( - "Bin", {"item_code": item.item_code, "warehouse": warehouse}, "actual_qty" - ) - - expected_data.append( - [ - item.item_code, - item.item_name, - item.description, - bom.name, - item.stock_qty, - item.stock_uom, - item.stock_qty * qty_to_produce / bom.quantity, - in_stock_qty, - floor(in_stock_qty / (item.stock_qty * qty_to_produce / bom.quantity)) - if in_stock_qty - else None, - item.bom_no, - item.is_phantom_item, - ] - ) - - return expected_data diff --git a/erpnext/manufacturing/report/test_reports.py b/erpnext/manufacturing/report/test_reports.py index adeddd996a5..505a82c3501 100644 --- a/erpnext/manufacturing/report/test_reports.py +++ b/erpnext/manufacturing/report/test_reports.py @@ -21,8 +21,7 @@ class TestManufacturingReports(ERPNextTestSuite): self.REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [ ("BOM Explorer", {"bom": self.last_bom}), ("BOM Operations Time", {}), - ("BOM Stock Calculated", {"bom": self.last_bom, "qty_to_make": 2}), - ("BOM Stock Report", {"bom": self.last_bom, "qty_to_produce": 2}), + ("BOM Stock Analysis", {"bom": self.last_bom, "_optional": ["warehouse"]}), ("Cost of Poor Quality Report", {"item": "_Test Item", "serial_no": "00"}), ("Downtime Analysis", {}), ( diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 4b1fc449473..d41924edb4a 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -472,3 +472,5 @@ 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 +erpnext.patches.v16_0.depends_on_inv_dimensions 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/patches/v16_0/depends_on_inv_dimensions.py b/erpnext/patches/v16_0/depends_on_inv_dimensions.py new file mode 100644 index 00000000000..0de46f68f11 --- /dev/null +++ b/erpnext/patches/v16_0/depends_on_inv_dimensions.py @@ -0,0 +1,89 @@ +import frappe + + +def get_inventory_dimensions(): + return frappe.get_all( + "Inventory Dimension", + fields=[ + "target_fieldname as fieldname", + "source_fieldname", + "reference_document as doctype", + "reqd", + "mandatory_depends_on", + ], + order_by="creation", + distinct=True, + ) + + +def get_display_depends_on(doctype, fieldname): + if doctype not in [ + "Stock Entry Detail", + "Sales Invoice Item", + "Delivery Note Item", + "Purchase Invoice Item", + "Purchase Receipt Item", + ]: + return None, None + + fieldname_start_with = "to" + display_depends_on = "" + + if doctype in ["Purchase Invoice Item", "Purchase Receipt Item"]: + display_depends_on = "eval:parent.is_internal_supplier == 1" + fieldname_start_with = "from" + elif doctype != "Stock Entry Detail": + display_depends_on = "eval:parent.is_internal_customer == 1" + elif doctype == "Stock Entry Detail": + display_depends_on = "eval:doc.t_warehouse" + + return f"{fieldname_start_with}_{fieldname}", display_depends_on + + +def execute(): + for dimension in get_inventory_dimensions(): + if frappe.db.exists( + "Custom Field", {"fieldname": dimension.source_fieldname, "dt": "Stock Entry Detail"} + ): + frappe.set_value( + "Custom Field", + {"fieldname": dimension.source_fieldname, "dt": "Stock Entry Detail"}, + "depends_on", + "eval:doc.s_warehouse", + ) + if frappe.db.exists( + "Custom Field", {"fieldname": dimension.source_fieldname, "dt": "Stock Entry Detail", "reqd": 1} + ): + frappe.set_value( + "Custom Field", + {"fieldname": dimension.source_fieldname, "dt": "Stock Entry Detail", "reqd": 1}, + {"mandatory_depends_on": "eval:doc.s_warehouse", "reqd": 0}, + ) + if frappe.db.exists( + "Custom Field", + { + "fieldname": f"to_{dimension.fieldname}", + "dt": "Stock Entry Detail", + "depends_on": "eval:parent.purpose != 'Material Issue'", + }, + ): + frappe.set_value( + "Custom Field", + { + "fieldname": f"to_{dimension.fieldname}", + "dt": "Stock Entry Detail", + "depends_on": "eval:parent.purpose != 'Material Issue'", + }, + "depends_on", + "eval:doc.t_warehouse", + ) + fieldname, display_depends_on = get_display_depends_on(dimension.doctype, dimension.fieldname) + if display_depends_on and frappe.db.exists( + "Custom Field", {"fieldname": fieldname, "dt": dimension.doctype} + ): + frappe.set_value( + "Custom Field", + {"fieldname": fieldname, "dt": dimension.doctype}, + "mandatory_depends_on", + display_depends_on if dimension.reqd else dimension.mandatory_depends_on, + ) 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/financial_statements.js b/erpnext/public/js/financial_statements.js index 25918a179b8..0c8366d4e95 100644 --- a/erpnext/public/js/financial_statements.js +++ b/erpnext/public/js/financial_statements.js @@ -230,7 +230,10 @@ erpnext.financial_statements = { value = default_formatter(value, row, column, data); - if (data && !data.parent_account && !data.parent_section) { + if ( + data && + ((!data.parent_account && !data.parent_section) || data.is_group_account || data.is_group) + ) { value = $(`${value}`); var $value = $(value).css("font-weight", "bold"); 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/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index b841f456bc9..9c02879284c 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -1029,11 +1029,11 @@ class TestQuotation(ERPNextTestSuite): def test_make_quotation_qar_to_inr(self): quotation = make_quotation( currency="QAR", - transaction_date="2026-06-04", + transaction_date="2026-01-01", ) cache = frappe.cache() - key = "currency_exchange_rate_{}:{}:{}".format("2026-06-04", "QAR", "INR") + key = "currency_exchange_rate_{}:{}:{}".format("2026-01-01", "QAR", "INR") value = cache.get(key) expected_rate = flt(value) / 3.64 diff --git a/erpnext/selling/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..bf8750cc1b8 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 @@ -93,10 +93,10 @@ class SellingSettings(Document): self.validate_fallback_to_default_price_list() - if old_doc.enable_tracking_sales_commissions != self.enable_tracking_sales_commissions: + if old_doc and old_doc.enable_tracking_sales_commissions != self.enable_tracking_sales_commissions: toggle_tracking_sales_commissions_section(not self.enable_tracking_sales_commissions) - if old_doc.enable_utm != self.enable_utm: + if old_doc and old_doc.enable_utm != self.enable_utm: toggle_utm_analytics_section(not self.enable_utm) def validate_fallback_to_default_price_list(self): diff --git a/erpnext/setup/demo.py b/erpnext/setup/demo.py index ec48a3e1447..c460b1520c4 100644 --- a/erpnext/setup/demo.py +++ b/erpnext/setup/demo.py @@ -7,7 +7,7 @@ from random import randint import frappe from frappe import _ -from frappe.utils import add_days, getdate +from frappe.utils import add_days, get_url_to_form, getdate from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.utils import get_fiscal_year @@ -16,21 +16,44 @@ from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account -def setup_demo_data(): +def setup_demo_data(company_name): from frappe.utils.telemetry import capture capture("demo_data_creation_started", "erpnext") try: - company = create_demo_company() + frappe.db.savepoint("demo_data") + company = create_demo_company(company_name) process_masters() make_transactions(company) - frappe.cache.delete_keys("bootinfo") - frappe.publish_realtime("demo_data_complete") + capture("demo_data_creation_completed", "erpnext") + frappe.clear_messages() except Exception: - frappe.log_error("Failed to create demo data") + frappe.db.rollback(save_point="demo_data") + error_log = frappe.log_error("Failed to create demo data") + log_demo_data_failed_notification(error_log) capture("demo_data_creation_failed", "erpnext", properties={"exception": frappe.get_traceback()}) - raise - capture("demo_data_creation_completed", "erpnext") + + +def log_demo_data_failed_notification(error_log): + from frappe.core.doctype.role.role import get_users + from frappe.desk.doctype.notification_log.notification_log import make_notification_logs + + frappe.msgprint( + _("Demo data creation failed. Check notifications for more info."), + alert=True, + indicator="red", + realtime=True, + ) + + users = get_users("System Manager") + + notif_log_doc = { + "subject": _("Demo Data creation failed."), + "type": "Alert", + "link": get_url_to_form("Error Log", error_log.name), + } + + make_notification_logs(notif_log_doc, users) @frappe.whitelist() @@ -56,21 +79,8 @@ def clear_demo_data(): ) -def create_demo_company(): - if frappe.flags.in_test: - hash = frappe.generate_hash(length=3) - company_doc = frappe._dict( - { - "company_name": "Test Company" + " " + hash, - "abbr": "TC" + hash, - "default_currency": "INR", - "country": "India", - "chart_of_accounts": "Standard", - } - ) - else: - company = frappe.db.get_all("Company")[0].name - company_doc = frappe.get_doc("Company", company).as_dict() +def create_demo_company(company): + company_doc = frappe.get_doc("Company", company).as_dict() # Make a dummy company new_company = frappe.new_doc("Company") diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 5da3ca40904..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), @@ -927,7 +927,7 @@ def update_transactions_annual_history(company, commit=False): transactions_history = get_all_transactions_annual_history(company) frappe.db.set_value("Company", company, "transactions_annual_history", json.dumps(transactions_history)) - if commit: + if commit and not frappe.in_test: frappe.db.commit() @@ -936,7 +936,9 @@ def cache_companies_monthly_sales_history(): for company in companies: update_company_monthly_sales(company) update_transactions_annual_history(company) - frappe.db.commit() + + if not frappe.in_test: + frappe.db.commit() @frappe.whitelist() diff --git a/erpnext/setup/doctype/company/test_company.py b/erpnext/setup/doctype/company/test_company.py index c3fb5dd6ff2..36606e90755 100644 --- a/erpnext/setup/doctype/company/test_company.py +++ b/erpnext/setup/doctype/company/test_company.py @@ -199,7 +199,9 @@ class TestCompany(ERPNextTestSuite): def test_demo_data(self): from erpnext.setup.demo import clear_demo_data, setup_demo_data - setup_demo_data() + self.load_test_records("Company") + + setup_demo_data(self.globalTestRecords["Company"][0]["company_name"]) company_name = frappe.db.get_value("Company", {"name": ("like", "%(Demo)")}) self.assertTrue(company_name) diff --git a/erpnext/setup/doctype/company/test_records.json b/erpnext/setup/doctype/company/test_records.json index 1b51d98413e..74615e60162 100644 --- a/erpnext/setup/doctype/company/test_records.json +++ b/erpnext/setup/doctype/company/test_records.json @@ -180,5 +180,39 @@ "default_currency": "ZAR", "doctype": "Company", "create_chart_of_accounts_based_on": "Standard Template" + }, + { + "abbr": "_TOIC", + "company_name": "_Test Opening Invoice Company", + "country": "Pakistan", + "default_currency": "INR", + "doctype": "Company", + "create_chart_of_accounts_based_on": "Standard Template" + }, + { + "abbr": "TBC", + "company_name": "Trial Balance Company", + "country": "India", + "default_currency": "INR", + "doctype": "Company", + "create_chart_of_accounts_based_on": "Standard Template" + }, + { + "abbr": "_TSS", + "company_name": "_Test Support SLA", + "country": "India", + "default_currency": "INR", + "doctype": "Company", + "chart_of_accounts": "Standard", + "create_chart_of_accounts_based_on": "Standard Template" + }, + { + "abbr": "TQC", + "company_name": "Test Quality Company", + "country": "India", + "default_currency": "INR", + "doctype": "Company", + "chart_of_accounts": "Standard", + "create_chart_of_accounts_based_on": "Standard Template" } ] diff --git a/erpnext/setup/doctype/employee/employee.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/setup/doctype/party_type/party_type.py b/erpnext/setup/doctype/party_type/party_type.py index 6730d1cbdce..cf72eb3bbdc 100644 --- a/erpnext/setup/doctype/party_type/party_type.py +++ b/erpnext/setup/doctype/party_type/party_type.py @@ -4,6 +4,7 @@ import frappe from frappe.model.document import Document +from frappe.query_builder import DocType class PartyType(Document): @@ -24,29 +25,36 @@ class PartyType(Document): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs -def get_party_type(doctype, txt, searchfield, start, page_len, filters): - cond = "" - account_type = None +def get_party_type(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict): + PartyType = DocType("Party Type") + get_party_type_query = frappe.qb.from_(PartyType).select(PartyType.name).orderby(PartyType.name) + + condition_list = [] if filters and filters.get("account"): account_type = frappe.db.get_value("Account", filters.get("account"), "account_type") if account_type: if account_type in ["Receivable", "Payable"]: # Include Employee regardless of its configured account_type, but still respect the text filter - cond = "and (account_type = %(account_type)s or name = 'Employee')" + condition_list.append( + (PartyType.account_type == account_type) | (PartyType.name == "Employee") + ) else: - cond = "and account_type = %(account_type)s" + condition_list.append(PartyType.account_type == account_type) - # Build parameters dictionary - params = {"txt": "%" + txt + "%", "start": start, "page_len": page_len} - if account_type: - params["account_type"] = account_type + for condition in condition_list: + get_party_type_query = get_party_type_query.where(condition) - result = frappe.db.sql( - f"""select name from `tabParty Type` - where `{searchfield}` LIKE %(txt)s {cond} - order by name limit %(page_len)s offset %(start)s""", - params, - ) + if frappe.local.lang == "en": + get_party_type_query = get_party_type_query.where(getattr(PartyType, searchfield).like(f"%{txt}%")) + get_party_type_query = get_party_type_query.limit(page_len) + get_party_type_query = get_party_type_query.offset(start) + + result = get_party_type_query.run() + else: + result = get_party_type_query.run() + test_str = txt.lower() + result = [row for row in result if test_str in frappe._(row[0]).lower()] + result = result[start : start + page_len] return result or [] diff --git a/erpnext/setup/setup_wizard/setup_wizard.py b/erpnext/setup/setup_wizard/setup_wizard.py index 9a49af2b10e..20330d89631 100644 --- a/erpnext/setup/setup_wizard/setup_wizard.py +++ b/erpnext/setup/setup_wizard/setup_wizard.py @@ -10,39 +10,34 @@ from erpnext.setup.setup_wizard.operations import install_fixtures as fixtures def get_setup_stages(args=None): - if frappe.db.sql("select name from tabCompany"): - stages = [ + stages = [ + { + "status": _("Installing presets"), + "fail_msg": _("Failed to install presets"), + "tasks": [{"fn": stage_fixtures, "args": args, "fail_msg": _("Failed to install presets")}], + }, + { + "status": _("Setting up company"), + "fail_msg": _("Failed to setup company"), + "tasks": [{"fn": setup_company, "args": args, "fail_msg": _("Failed to setup company")}], + }, + { + "status": _("Setting defaults"), + "fail_msg": _("Failed to set defaults"), + "tasks": [ + {"fn": setup_defaults, "args": args, "fail_msg": _("Failed to setup defaults")}, + ], + }, + ] + + if args.get("setup_demo"): + stages.append( { - "status": _("Wrapping up"), - "fail_msg": _("Failed to login"), - "tasks": [{"fn": fin, "args": args, "fail_msg": _("Failed to login")}], + "status": _("Creating demo data"), + "fail_msg": _("Failed to create demo data"), + "tasks": [{"fn": setup_demo, "args": args, "fail_msg": _("Failed to create demo data")}], } - ] - else: - stages = [ - { - "status": _("Installing presets"), - "fail_msg": _("Failed to install presets"), - "tasks": [{"fn": stage_fixtures, "args": args, "fail_msg": _("Failed to install presets")}], - }, - { - "status": _("Setting up company"), - "fail_msg": _("Failed to setup company"), - "tasks": [{"fn": setup_company, "args": args, "fail_msg": _("Failed to setup company")}], - }, - { - "status": _("Setting defaults"), - "fail_msg": "Failed to set defaults", - "tasks": [ - {"fn": setup_defaults, "args": args, "fail_msg": _("Failed to setup defaults")}, - ], - }, - { - "status": _("Wrapping up"), - "fail_msg": _("Failed to login"), - "tasks": [{"fn": fin, "args": args, "fail_msg": _("Failed to login")}], - }, - ] + ) return stages @@ -59,19 +54,8 @@ def setup_defaults(args): fixtures.install_defaults(frappe._dict(args)) -def fin(args): - frappe.local.message_log = [] - login_as_first_user(args) - - -def setup_demo(args): - if args.get("setup_demo"): - frappe.enqueue(setup_demo_data, enqueue_after_commit=True, at_front=True) - - -def login_as_first_user(args): - if args.get("email") and hasattr(frappe.local, "login_manager"): - frappe.local.login_manager.login_as(args.get("email")) +def setup_demo(args): # nosemgrep + setup_demo_data(args.get("company_name")) # Only for programmatical use @@ -79,4 +63,3 @@ def setup_complete(args=None): stage_fixtures(args) setup_company(args) setup_defaults(args) - fin(args) diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js index 17f65ce270c..018a4f86fa8 100644 --- a/erpnext/stock/dashboard/item_dashboard.js +++ b/erpnext/stock/dashboard/item_dashboard.js @@ -281,8 +281,13 @@ erpnext.stock.move_item = function (item, source, target, actual_qty, rate, stoc } dialog.set_primary_action(__("Create Stock Entry"), function () { - if (source && (dialog.get_value("qty") == 0 || dialog.get_value("qty") > actual_qty)) { - frappe.msgprint(__("Quantity must be greater than zero, and less or equal to {0}", [actual_qty])); + if (flt(dialog.get_value("qty")) <= 0) { + frappe.msgprint(__("Quantity must be greater than zero")); + return; + } + + if (source && dialog.get_value("qty") > actual_qty) { + frappe.msgprint(__("Quantity must be less than or equal to {0}", [actual_qty])); return; } diff --git a/erpnext/stock/dashboard/item_dashboard.py b/erpnext/stock/dashboard/item_dashboard.py index d77ed7a6212..5de54c55461 100644 --- a/erpnext/stock/dashboard/item_dashboard.py +++ b/erpnext/stock/dashboard/item_dashboard.py @@ -1,6 +1,6 @@ import frappe from frappe.desk.reportview import build_match_conditions -from frappe.utils import cint, flt +from frappe.utils import cint, escape_html, flt from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( get_sre_reserved_qty_for_items_and_warehouses as get_reserved_stock_details, @@ -70,8 +70,10 @@ def get_data( for item in items: item.update( { - "item_name": frappe.get_cached_value("Item", item.item_code, "item_name"), - "stock_uom": frappe.get_cached_value("Item", item.item_code, "stock_uom"), + "item_code": escape_html(item.item_code), + "item_name": escape_html(frappe.get_cached_value("Item", item.item_code, "item_name")), + "stock_uom": escape_html(frappe.get_cached_value("Item", item.item_code, "stock_uom")), + "warehouse": escape_html(item.warehouse), "disable_quick_entry": frappe.get_cached_value("Item", item.item_code, "has_batch_no") or frappe.get_cached_value("Item", item.item_code, "has_serial_no"), "projected_qty": flt(item.projected_qty, precision), diff --git a/erpnext/stock/dashboard/item_dashboard_list.html b/erpnext/stock/dashboard/item_dashboard_list.html index ae90ff80686..34d51814b2f 100644 --- a/erpnext/stock/dashboard/item_dashboard_list.html +++ b/erpnext/stock/dashboard/item_dashboard_list.html @@ -50,15 +50,15 @@ data-warehouse="{{ d.warehouse }}" data-actual_qty="{{ d.actual_qty }}" data-stock-uom="{{ d.stock_uom }}" - data-item="{{ escape(d.item_code) }}">{{ __("Move") }} + data-item="{{ d.item_code }}">{{ __("Move") }} {% endif %} {% endif %} diff --git a/erpnext/stock/dashboard/warehouse_capacity_dashboard.py b/erpnext/stock/dashboard/warehouse_capacity_dashboard.py index 75b2951e30b..39701ed3f0d 100644 --- a/erpnext/stock/dashboard/warehouse_capacity_dashboard.py +++ b/erpnext/stock/dashboard/warehouse_capacity_dashboard.py @@ -1,6 +1,6 @@ import frappe from frappe.desk.reportview import build_match_conditions -from frappe.utils import flt, nowdate +from frappe.utils import escape_html, flt, nowdate from erpnext.stock.utils import get_stock_balance @@ -75,6 +75,9 @@ def get_warehouse_capacity_data(filters, start): balance_qty = get_stock_balance(entry.item_code, entry.warehouse, nowdate()) or 0 entry.update( { + "warehouse": escape_html(entry.warehouse), + "item_code": escape_html(entry.item_code), + "company": escape_html(entry.company), "actual_qty": balance_qty, "percent_occupied": flt((flt(balance_qty) / flt(entry.stock_capacity)) * 100, 0), } diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index d88af98bca8..0175b790887 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -759,7 +759,6 @@ "label": "Incoming Rate", "no_copy": 1, "options": "Company:company:default_currency", - "precision": "6", "print_hide": 1, "read_only": 1 }, @@ -953,7 +952,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-05-31 19:51:32.651562", + "modified": "2026-04-07 15:43:20.892151", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.js b/erpnext/stock/doctype/delivery_trip/delivery_trip.js index 692b468e6ae..61c6743054f 100755 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.js +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.js @@ -41,6 +41,8 @@ frappe.ui.form.on("Delivery Trip", { }, refresh: function (frm) { + frm.ignore_doctypes_on_cancel_all = ["Delivery Note"]; + if (frm.doc.docstatus == 1 && frm.doc.delivery_stops.length > 0) { frm.add_custom_button(__("Notify Customers via Email"), function () { frm.trigger("notify_customers"); diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json index 376b09f9370..aae81a29eac 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json @@ -8,9 +8,8 @@ "field_order": [ "dimension_details_tab", "dimension_name", - "reference_document", "column_break_4", - "disabled", + "reference_document", "field_mapping_section", "source_fieldname", "column_break_9", @@ -93,12 +92,6 @@ "fieldtype": "Check", "label": "Apply to All Inventory Documents" }, - { - "default": "0", - "fieldname": "disabled", - "fieldtype": "Check", - "label": "Disabled" - }, { "fieldname": "target_fieldname", "fieldtype": "Data", @@ -159,6 +152,7 @@ "label": "Conditional Rule Examples" }, { + "depends_on": "eval:!doc.apply_to_all_doctypes", "description": "To apply condition on parent field use parent.field_name and to apply condition on child table use doc.field_name. Here field_name could be based on the actual column name of the respective field.", "fieldname": "mandatory_depends_on", "fieldtype": "Small Text", @@ -188,7 +182,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2025-07-07 15:51:29.329064", + "modified": "2026-04-08 10:10:16.884388", "modified_by": "Administrator", "module": "Stock", "name": "Inventory Dimension", diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py index 469e4d5e53a..fc5038db069 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py @@ -32,7 +32,6 @@ class InventoryDimension(Document): apply_to_all_doctypes: DF.Check condition: DF.Code | None dimension_name: DF.Data - disabled: DF.Check document_type: DF.Link | None fetch_from_parent: DF.Literal[None] istable: DF.Check @@ -76,7 +75,6 @@ class InventoryDimension(Document): old_doc = self._doc_before_save allow_to_edit_fields = [ - "disabled", "fetch_from_parent", "type_of_transaction", "condition", @@ -120,6 +118,7 @@ class InventoryDimension(Document): def reset_value(self): if self.apply_to_all_doctypes: self.type_of_transaction = "" + self.mandatory_depends_on = "" self.istable = 0 for field in ["document_type", "condition"]: @@ -182,9 +181,14 @@ 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, + reqd=1 + if self.reqd and not self.mandatory_depends_on and doctype != "Stock Entry Detail" + else 0, + mandatory_depends_on="eval:doc.s_warehouse" + if self.reqd and doctype == "Stock Entry Detail" + else self.mandatory_depends_on, ), ] @@ -273,7 +277,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}" @@ -295,12 +299,13 @@ class InventoryDimension(Document): options=self.reference_document, label=label, depends_on=display_depends_on, + mandatory_depends_on=display_depends_on if self.reqd else self.mandatory_depends_on, ), ] ) -def field_exists(doctype, fieldname) -> str or None: +def field_exists(doctype, fieldname) -> str | None: return frappe.db.get_value("DocField", {"parent": doctype, "fieldname": fieldname}, "name") @@ -371,7 +376,6 @@ def get_document_wise_inventory_dimensions(doctype) -> dict: "type_of_transaction", "fetch_from_parent", ], - filters={"disabled": 0}, or_filters={"document_type": doctype, "apply_to_all_doctypes": 1}, ) @@ -388,7 +392,6 @@ def get_inventory_dimensions(): "validate_negative_stock", "name as dimension_name", ], - filters={"disabled": 0}, order_by="creation", distinct=True, ) diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index cbf6059a812..bfe6864486b 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -22,9 +22,6 @@ from erpnext.tests.utils import ERPNextTestSuite class TestInventoryDimension(ERPNextTestSuite): - def setUp(self): - prepare_test_data() - def test_validate_inventory_dimension(self): # Can not be child doc inv_dim1 = create_inventory_dimension( @@ -77,6 +74,7 @@ class TestInventoryDimension(ERPNextTestSuite): self.assertFalse(custom_field) def test_inventory_dimension(self): + create_warehouse("Shelf Warehouse") warehouse = "Shelf Warehouse - _TC" item_code = "_Test Item" @@ -213,9 +211,9 @@ class TestInventoryDimension(ERPNextTestSuite): doc = create_inventory_dimension( reference_document="Pallet", type_of_transaction="Outward", - dimension_name="Pallet", + dimension_name="Pallet 75", apply_to_all_doctypes=0, - document_type="Stock Entry Detail", + document_type="Delivery Note Item", ) doc.reqd = 1 @@ -223,7 +221,7 @@ class TestInventoryDimension(ERPNextTestSuite): self.assertTrue( frappe.db.get_value( - "Custom Field", {"fieldname": "pallet", "dt": "Stock Entry Detail", "reqd": 1}, "name" + "Custom Field", {"fieldname": "pallet_75", "dt": "Delivery Note Item", "reqd": 1}, "name" ) ) @@ -556,28 +554,6 @@ def get_voucher_sl_entries(voucher_no, fields): ) -def prepare_test_data(): - for shelf in ["Shelf 1", "Shelf 2"]: - if not frappe.db.exists("Shelf", shelf): - frappe.get_doc({"doctype": "Shelf", "shelf_name": shelf}).insert(ignore_permissions=True) - - create_warehouse("Shelf Warehouse") - - for rack in ["Rack 1", "Rack 2"]: - if not frappe.db.exists("Rack", rack): - frappe.get_doc({"doctype": "Rack", "rack_name": rack}).insert(ignore_permissions=True) - - create_warehouse("Rack Warehouse") - - for site in ["Site 1", "Site 2"]: - if not frappe.db.exists("Inv Site", site): - frappe.get_doc({"doctype": "Inv Site", "site_name": site}).insert(ignore_permissions=True) - - for store in ["Store 1", "Store 2"]: - if not frappe.db.exists("Store", store): - frappe.get_doc({"doctype": "Store", "store_name": store}).insert(ignore_permissions=True) - - def create_inventory_dimension(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index b957b905258..e4996462278 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -47,7 +47,6 @@ "column_break_cqdk", "valuation_rate", "inventory_settings_section", - "shelf_life_in_days", "end_of_life", "default_material_request_type", "column_break1", @@ -64,6 +63,7 @@ "create_new_batch", "batch_number_series", "has_expiry_date", + "shelf_life_in_days", "retain_sample", "sample_quantity", "column_break_37", @@ -334,6 +334,7 @@ "options": "fa fa-truck" }, { + "depends_on": "has_expiry_date", "fieldname": "shelf_life_in_days", "fieldtype": "Int", "label": "Shelf Life In Days", @@ -343,11 +344,13 @@ { "default": "2099-12-31", "depends_on": "is_stock_item", + "description": "Defines the date after which the item can no longer be used in transactions or manufacturing", "fieldname": "end_of_life", "fieldtype": "Date", "label": "End of Life", "oldfieldname": "end_of_life", - "oldfieldtype": "Date" + "oldfieldtype": "Date", + "show_description_on_click": 1 }, { "default": "Purchase", @@ -467,9 +470,12 @@ { "default": "0", "depends_on": "has_batch_no", + "description": "Enable to reserve a small sample from each batch for any analysis arising ahead", + "documentation_url": "https://docs.frappe.io/erpnext/retain-sample-stock", "fieldname": "retain_sample", "fieldtype": "Check", - "label": "Retain Sample" + "label": "Retain Sample", + "show_description_on_click": 1 }, { "depends_on": "eval: (doc.retain_sample && doc.has_batch_no)", @@ -989,7 +995,7 @@ "image_field": "image", "links": [], "make_attachments_public": 1, - "modified": "2026-03-17 20:39:05.218344", + "modified": "2026-03-24 15:45:40.207531", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index fc4bc589f9d..4332b7429a6 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -10,6 +10,7 @@ from frappe.query_builder.custom import ConstantColumn from frappe.utils import cint, flt import erpnext +from erpnext import is_perpetual_inventory_enabled from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -175,6 +176,9 @@ class LandedCostVoucher(Document): ) def validate_expense_accounts(self): + if not is_perpetual_inventory_enabled(self.company): + return + for t in self.taxes: company = frappe.get_cached_value("Account", t.expense_account, "company") diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index ee2b2051e8b..26df7f59135 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -180,6 +180,8 @@ class TestLandedCostVoucher(ERPNextTestSuite): self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0) def test_lcv_validates_company(self): + from erpnext import is_perpetual_inventory_enabled + from erpnext.accounts.doctype.account.test_account import create_account from erpnext.stock.doctype.landed_cost_voucher.landed_cost_voucher import ( IncorrectCompanyValidationError, ) @@ -187,6 +189,20 @@ class TestLandedCostVoucher(ERPNextTestSuite): company_a = "_Test Company" company_b = "_Test Company with perpetual inventory" + srbnb = create_account( + account_name="Stock Received But Not Billed", + account_type="Stock Received But Not Billed", + parent_account="Stock Liabilities - _TC", + company=company_a, + account_currency="INR", + ) + + epi = is_perpetual_inventory_enabled(company_a) + company_doc = frappe.get_doc("Company", company_a) + company_doc.enable_perpetual_inventory = 1 + company_doc.stock_received_but_not_billed = srbnb + company_doc.save() + pr = make_purchase_receipt( company=company_a, warehouse="Stores - _TC", @@ -212,6 +228,9 @@ class TestLandedCostVoucher(ERPNextTestSuite): distribute_landed_cost_on_items(lcv) lcv.submit() + frappe.db.set_value("Company", company_a, "enable_perpetual_inventory", epi) + frappe.local.enable_perpetual_inventory = {} + def test_landed_cost_voucher_for_zero_purchase_rate(self): "Test impact of LCV on future stock balances." from erpnext.stock.doctype.item.test_item import make_item diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index e72637901b5..c25a6ecd62d 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -1083,7 +1083,9 @@ class TestMaterialRequest(ERPNextTestSuite): pl.locations[0].qty = 2 pl.locations[0].stock_qty = 2 - self.assertRaises(frappe.ValidationError, pl.submit) + + # System should allow picking qty for excess transfer + pl.submit() def test_mr_status_with_partial_and_excess_end_transit(self): material_request = make_material_request( diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index ef80966c2a6..7c31ec7a672 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -86,6 +86,7 @@ class PickList(TransactionBase): "join_field": "material_request_item", "target_ref_field": "stock_qty", "source_field": "stock_qty", + "validate_qty": False, } ] @@ -522,8 +523,26 @@ class PickList(TransactionBase): self.item_location_map = frappe._dict() from_warehouses = [self.parent_warehouse] if self.parent_warehouse else [] - if self.parent_warehouse: - from_warehouses.extend(get_descendants_of("Warehouse", self.parent_warehouse)) + + if self.work_order: + root_warehouse = frappe.db.get_value( + "Warehouse", {"company": self.company, "parent_warehouse": ["IS", "NOT SET"], "is_group": 1} + ) + + from_warehouses = [root_warehouse] + + if from_warehouses: + from_warehouses.extend(get_descendants_of("Warehouse", from_warehouses[0])) + + item_warehouse_dict = frappe._dict() + if self.work_order: + item_warehouse_list = frappe.get_all( + "Work Order Item", + filters={"parent": self.work_order}, + fields=["item_code", "source_warehouse"], + ) + if item_warehouse_list: + item_warehouse_dict = {item.item_code: item.source_warehouse for item in item_warehouse_list} # Create replica before resetting, to handle empty table on update after submit. locations_replica = self.get("locations") @@ -541,6 +560,13 @@ class PickList(TransactionBase): len_idx = len(self.get("locations")) or 0 for item_doc in items: item_code = item_doc.item_code + priority_warehouses = [] + + if self.work_order and item_warehouse_dict.get(item_code): + source_warehouse = item_warehouse_dict.get(item_code) + priority_warehouses = [source_warehouse] + priority_warehouses.extend(get_descendants_of("Warehouse", source_warehouse)) + from_warehouses = list(dict.fromkeys(priority_warehouses + from_warehouses)) self.item_location_map.setdefault( item_code, @@ -551,6 +577,7 @@ class PickList(TransactionBase): self.company, picked_item_details=picked_items_details.get(item_code), consider_rejected_warehouses=self.consider_rejected_warehouses, + priority_warehouses=priority_warehouses, ), ) @@ -968,6 +995,7 @@ def get_available_item_locations( ignore_validation=False, picked_item_details=None, consider_rejected_warehouses=False, + priority_warehouses=None, ): locations = [] @@ -1008,7 +1036,7 @@ def get_available_item_locations( locations = filter_locations_by_picked_materials(locations, picked_item_details) if locations: - locations = get_locations_based_on_required_qty(locations, required_qty) + locations = get_locations_based_on_required_qty(locations, required_qty, priority_warehouses) if not ignore_validation: validate_picked_materials(item_code, required_qty, locations, picked_item_details) @@ -1016,9 +1044,14 @@ def get_available_item_locations( return locations -def get_locations_based_on_required_qty(locations, required_qty): +def get_locations_based_on_required_qty(locations, required_qty, priority_warehouses): filtered_locations = [] + if priority_warehouses: + priority_locations = [loc for loc in locations if loc.warehouse in priority_warehouses] + fallback_locations = [loc for loc in locations if loc.warehouse not in priority_warehouses] + locations = priority_locations + fallback_locations + for location in locations: if location.qty >= required_qty: location.qty = required_qty diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 283e0207d60..85a45f1686b 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -1050,6 +1050,53 @@ class TestPickList(ERPNextTestSuite): pl = create_pick_list(so.name) self.assertFalse(pl.locations) + def test_pick_list_warehouse_for_work_order(self): + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + from erpnext.manufacturing.doctype.work_order.work_order import create_pick_list, make_work_order + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + # Create Warehouses for Work Order + source_warehouse = create_warehouse("_Test WO Warehouse") + wip_warehouse = create_warehouse("_Test WIP Warehouse", company="_Test Company") + fg_warehouse = create_warehouse("_Test Finished Goods Warehouse", company="_Test Company") + + # Create Finished Good Item + fg_item = make_item("Test Work Order Finished Good Item", properties={"is_stock_item": 1}).name + + # Create Raw Material Item + rm_item = make_item("Test Work Order Raw Material Item", properties={"is_stock_item": 1}).name + + # Create BOM + bom = make_bom(item=fg_item, rate=100, raw_materials=[rm_item]) + + # Create Inward entry for Raw Material + make_stock_entry(item=rm_item, to_warehouse=wip_warehouse, qty=10) + make_stock_entry(item=rm_item, to_warehouse=source_warehouse, qty=10) + + # Create Work Order + wo = make_work_order(item=fg_item, qty=5, bom_no=bom.name, company="_Test Company") + wo.required_items[0].source_warehouse = source_warehouse + wo.fg_warehouse = fg_warehouse + wo.skip_transfer = True + wo.submit() + + # Create Pick List + pl = create_pick_list(wo.name, for_qty=wo.qty) + + # System prioritises the Source Warehouse + self.assertEqual(pl.locations[0].warehouse, source_warehouse) + self.assertEqual(pl.locations[0].item_code, rm_item) + self.assertEqual(pl.locations[0].qty, 5) + + # Create Outward Entry from Source Warehouse + make_stock_entry(item=rm_item, from_warehouse=source_warehouse, qty=10) + pl.set_item_locations() + + # System should pick other available warehouses + self.assertEqual(pl.locations[0].warehouse, wip_warehouse) + self.assertEqual(pl.locations[0].item_code, rm_item) + self.assertEqual(pl.locations[0].qty, 5) + def test_pick_list_validation_for_serial_no(self): warehouse = "_Test Warehouse - _TC" item = make_item( diff --git a/erpnext/stock/doctype/purchase_receipt/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 74cdfb38f78..828ad603d8e 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1240,6 +1240,65 @@ class TestPurchaseReceipt(ERPNextTestSuite): pr.cancel() + def test_item_valuation_with_deduct_valuation_and_total_tax(self): + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work In Progress - TCP1", + qty=5, + rate=100, + do_not_save=1, + ) + + pr.append( + "taxes", + { + "charge_type": "Actual", + "add_deduct_tax": "Deduct", + "account_head": "_Test Account Shipping Charges - TCP1", + "category": "Valuation and Total", + "cost_center": "Main - TCP1", + "description": "Valuation Discount", + "tax_amount": 20, + }, + ) + + pr.insert() + + self.assertAlmostEqual(pr.items[0].item_tax_amount, -20.0, places=2) + self.assertAlmostEqual(pr.items[0].valuation_rate, 96.0, places=2) + + pr.delete() + + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work In Progress - TCP1", + qty=5, + rate=100, + do_not_save=1, + ) + + pr.append( + "taxes", + { + "charge_type": "On Net Total", + "add_deduct_tax": "Deduct", + "account_head": "_Test Account Shipping Charges - TCP1", + "category": "Valuation and Total", + "cost_center": "Main - TCP1", + "description": "Valuation Discount", + "rate": 10, + }, + ) + + pr.insert() + + self.assertAlmostEqual(pr.items[0].item_tax_amount, -50.0, places=2) + self.assertAlmostEqual(pr.items[0].valuation_rate, 90.0, places=2) + + pr.delete() + def test_po_to_pi_and_po_to_pr_worflow_full(self): """Test following behaviour: - Create PO @@ -4551,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" @@ -5364,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 @@ -5534,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..b3f2e2eed5c 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 }, { @@ -732,7 +735,6 @@ "oldfieldname": "valuation_rate", "oldfieldtype": "Currency", "options": "Company:company:default_currency", - "precision": "6", "print_hide": 1, "print_width": "80px", "read_only": 1, @@ -783,7 +785,8 @@ "fieldname": "expense_account", "fieldtype": "Link", "label": "Expense Account", - "options": "Account" + "options": "Account", + "print_hide": 1 }, { "fieldname": "accounting_dimensions_section", @@ -820,7 +823,8 @@ "fieldname": "asset_location", "fieldtype": "Link", "label": "Asset Location", - "options": "Location" + "options": "Location", + "print_hide": 1 }, { "depends_on": "is_fixed_asset", @@ -829,6 +833,7 @@ "fieldtype": "Link", "label": "Asset Category", "options": "Asset Category", + "print_hide": 1, "read_only": 1 }, { @@ -898,6 +903,7 @@ "label": "Rate of Stock UOM", "no_copy": 1, "options": "currency", + "print_hide": 1, "read_only": 1 }, { @@ -949,7 +955,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 +1110,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 +1134,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 +1149,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2026-02-04 14:42:10.646809", + "modified": "2026-04-07 15:40:47.032889", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.js b/erpnext/stock/doctype/quality_inspection/quality_inspection.js index 69bc03a8bd4..8d5764d5697 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.js +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.js @@ -58,6 +58,7 @@ frappe.ui.form.on("Quality Inspection", { if (doc.reference_type && doc.reference_name) { let filters = { from: doctype, + parent_doctype: doc.reference_type, inspection_type: doc.inspection_type, }; diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 67fc49acb8a..52ac45ec6fb 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -368,10 +368,11 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): from frappe.desk.reportview import get_match_cond from_doctype = cstr(filters.get("from")) + parent_doctype = cstr(filters.get("parent_doctype")) if not from_doctype or not frappe.db.exists("DocType", from_doctype): return [] - mcond = get_match_cond(from_doctype) + mcond = get_match_cond(parent_doctype or from_doctype) cond, qi_condition = "", "and (quality_inspection is null or quality_inspection = '')" if filters.get("parent"): diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js index 0f64949f621..134903f2309 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js @@ -69,9 +69,15 @@ frappe.ui.form.on("Repost Item Valuation", { } if (frm.doc.status == "In Progress") { - frm.doc.current_index = data.current_index; - frm.doc.items_to_be_repost = data.items_to_be_repost; - frm.doc.total_reposting_count = data.total_reposting_count; + if (data.current_index) { + frm.doc.current_index = data.current_index; + frm.doc.items_to_be_repost = data.items_to_be_repost; + } + + if (data.vouchers_posted) { + frm.doc.total_vouchers = data.total_vouchers; + frm.doc.vouchers_posted = data.vouchers_posted; + } frm.dashboard.reset(); frm.trigger("show_reposting_progress"); @@ -108,15 +114,31 @@ frappe.ui.form.on("Repost Item Valuation", { show_reposting_progress: function (frm) { var bars = []; - + let title = ""; + let progress = 0.0; let total_count = frm.doc.items_to_be_repost ? JSON.parse(frm.doc.items_to_be_repost).length : 0; - if (frm.doc?.total_reposting_count) { - total_count = frm.doc.total_reposting_count; + if (total_count > 1) { + progress = flt((cint(frm.doc.current_index) / total_count) * 100, 2) || 0.5; + title = __("Reposting for Item-Wh Completed {0}%", [progress]); + + bars.push({ + title: title, + width: progress + "%", + progress_class: "progress-bar-success", + }); + + frm.dashboard.add_progress(__("Reposting Progress"), bars); } - let progress = flt((cint(frm.doc.current_index) / total_count) * 100, 2) || 0.5; - var title = __("Reposting Completed {0}%", [progress]); + if (!frm.doc.vouchers_posted) { + return; + } + + // Show voucher posting progress if vouchers are being reposted + bars = []; + progress = flt((cint(frm.doc.vouchers_posted) / cint(frm.doc.total_vouchers)) * 100, 2) || 0.5; + title = __("Reposting for Vouchers Completed {0}%", [progress]); bars.push({ title: title, @@ -124,7 +146,7 @@ frappe.ui.form.on("Repost Item Valuation", { progress_class: "progress-bar-success", }); - frm.dashboard.add_progress(__("Reposting Progress"), bars); + frm.dashboard.add_progress(__("Reposting Vouchers Progress"), bars); }, restart_reposting: function (frm) { diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json index e1ae6d00cd9..ce43ae3a54f 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json @@ -27,14 +27,16 @@ "error_section", "error_log", "reposting_info_section", - "reposting_data_file", "items_to_be_repost", - "distinct_item_and_warehouse", "column_break_o1sj", "total_reposting_count", "current_index", "gl_reposting_index", - "affected_transactions" + "reposting_data_file", + "vouchers_based_on_item_and_warehouse_section", + "total_vouchers", + "column_break_yqwo", + "vouchers_posted" ], "fields": [ { @@ -167,15 +169,6 @@ "print_hide": 1, "read_only": 1 }, - { - "fieldname": "distinct_item_and_warehouse", - "fieldtype": "Code", - "hidden": 1, - "label": "Distinct Item and Warehouse", - "no_copy": 1, - "print_hide": 1, - "read_only": 1 - }, { "fieldname": "current_index", "fieldtype": "Int", @@ -185,14 +178,6 @@ "print_hide": 1, "read_only": 1 }, - { - "fieldname": "affected_transactions", - "fieldtype": "Code", - "hidden": 1, - "label": "Affected Transactions", - "no_copy": 1, - "read_only": 1 - }, { "default": "0", "fieldname": "gl_reposting_index", @@ -205,7 +190,7 @@ { "fieldname": "reposting_info_section", "fieldtype": "Section Break", - "label": "Reposting Info" + "label": "Reposting Item and Warehouse" }, { "fieldname": "column_break_o1sj", @@ -214,14 +199,7 @@ { "fieldname": "total_reposting_count", "fieldtype": "Int", - "label": "Total Reposting Count", - "no_copy": 1, - "read_only": 1 - }, - { - "fieldname": "reposting_data_file", - "fieldtype": "Attach", - "label": "Reposting Data File", + "label": "No of Items to Repost", "no_copy": 1, "read_only": 1 }, @@ -247,13 +225,44 @@ "fieldname": "repost_only_accounting_ledgers", "fieldtype": "Check", "label": "Repost Only Accounting Ledgers" + }, + { + "fieldname": "vouchers_based_on_item_and_warehouse_section", + "fieldtype": "Section Break", + "hidden": 1, + "label": "Reposting Vouchers" + }, + { + "fieldname": "total_vouchers", + "fieldtype": "Int", + "label": "Total Ledgers", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_yqwo", + "fieldtype": "Column Break" + }, + { + "fieldname": "vouchers_posted", + "fieldtype": "Int", + "label": "Ledgers Posted", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "reposting_data_file", + "fieldtype": "Attach", + "label": "Reposting Data File", + "no_copy": 1, + "read_only": 1 } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2026-02-25 14:22:21.681549", + "modified": "2026-03-27 18:59:58.637964", "modified_by": "Administrator", "module": "Stock", "name": "Repost Item Valuation", diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index f5b4ef3e8f5..c63c2d34916 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -35,14 +35,12 @@ class RepostItemValuation(Document): if TYPE_CHECKING: from frappe.types import DF - affected_transactions: DF.Code | None allow_negative_stock: DF.Check allow_zero_rate: DF.Check amended_from: DF.Link | None based_on: DF.Literal["Transaction", "Item and Warehouse"] company: DF.Link | None current_index: DF.Int - distinct_item_and_warehouse: DF.Code | None error_log: DF.LongText | None gl_reposting_index: DF.Int item_code: DF.Link | None @@ -55,9 +53,11 @@ class RepostItemValuation(Document): reposting_reference: DF.Data | None status: DF.Literal["Queued", "In Progress", "Completed", "Skipped", "Failed", "Cancelled"] total_reposting_count: DF.Int + total_vouchers: DF.Int via_landed_cost_voucher: DF.Check voucher_no: DF.DynamicLink | None voucher_type: DF.Link | None + vouchers_posted: DF.Int warehouse: DF.Link | None # end: auto-generated types @@ -80,8 +80,10 @@ class RepostItemValuation(Document): repost(self) def validate(self): + self.set_default_posting_time() 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() @@ -89,10 +91,29 @@ class RepostItemValuation(Document): self.reset_recreate_stock_ledgers() self.validate_recreate_stock_ledgers() + def set_default_posting_time(self): + if not self.posting_time: + self.posting_time = nowtime() + + if not self.posting_date: + frappe.throw(_("Posting date is required")) + def reset_repost_only_accounting_ledgers(self): 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 @@ -261,6 +282,8 @@ class RepostItemValuation(Document): self.items_to_be_repost = None self.gl_reposting_index = 0 self.total_reposting_count = 0 + self.total_vouchers = 0 + self.vouchers_posted = 0 self.clear_attachment() self.db_update() @@ -435,7 +458,7 @@ def repost_sl_entries(doc): ) else: repost_future_sle( - args=[ + items_to_be_repost=[ frappe._dict( { "item_code": doc.item_code, diff --git a/erpnext/stock/doctype/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/shipment_parcel/shipment_parcel.json b/erpnext/stock/doctype/shipment_parcel/shipment_parcel.json index 321599e2b4b..32d0df2c873 100644 --- a/erpnext/stock/doctype/shipment_parcel/shipment_parcel.json +++ b/erpnext/stock/doctype/shipment_parcel/shipment_parcel.json @@ -14,19 +14,19 @@ "fields": [ { "fieldname": "length", - "fieldtype": "Int", + "fieldtype": "Float", "in_list_view": 1, "label": "Length (cm)" }, { "fieldname": "width", - "fieldtype": "Int", + "fieldtype": "Float", "in_list_view": 1, "label": "Width (cm)" }, { "fieldname": "height", - "fieldtype": "Int", + "fieldtype": "Float", "in_list_view": 1, "label": "Height (cm)" }, @@ -49,7 +49,7 @@ ], "istable": 1, "links": [], - "modified": "2024-03-27 13:10:41.396354", + "modified": "2026-03-29 00:00:00.000000", "modified_by": "Administrator", "module": "Stock", "name": "Shipment Parcel", diff --git a/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json index 9eb9ba46762..6e55b59a497 100644 --- a/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json +++ b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json @@ -15,21 +15,21 @@ "fields": [ { "fieldname": "length", - "fieldtype": "Int", + "fieldtype": "Float", "in_list_view": 1, "label": "Length (cm)", "reqd": 1 }, { "fieldname": "width", - "fieldtype": "Int", + "fieldtype": "Float", "in_list_view": 1, "label": "Width (cm)", "reqd": 1 }, { "fieldname": "height", - "fieldtype": "Int", + "fieldtype": "Float", "in_list_view": 1, "label": "Height (cm)", "reqd": 1 @@ -52,7 +52,7 @@ } ], "links": [], - "modified": "2024-03-27 13:10:41.521126", + "modified": "2026-03-29 00:00:00.000000", "modified_by": "Administrator", "module": "Stock", "name": "Shipment Parcel Template", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 4fdd9df1adf..e4c1ffa4d26 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -36,20 +36,30 @@ frappe.ui.form.on("Stock Entry", { }; }); - frm.set_query("source_warehouse_address", function () { + frm.set_query("source_stock_entry", function () { return { filters: { - link_doctype: "Warehouse", - link_name: frm.doc.from_warehouse, + 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", + filters: { + warehouse: frm.doc.from_warehouse, }, }; }); frm.set_query("target_warehouse_address", function () { return { + query: "erpnext.controllers.queries.get_warehouse_address", filters: { - link_doctype: "Warehouse", - link_name: frm.doc.to_warehouse, + warehouse: frm.doc.to_warehouse, }, }; }); @@ -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..d62ce4e2cb4 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") ): @@ -3697,9 +3928,12 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None): for d in work_order.get("operations"): if flt(d.completed_qty): - operating_cost_per_unit += flt( - d.actual_operating_cost - get_consumed_operating_cost(work_order.name, bom_no) - ) / flt(d.completed_qty - work_order.produced_qty) + if not (remaining_qty := flt(d.completed_qty - work_order.produced_qty)): + continue + operating_cost_per_unit += ( + flt(d.actual_operating_cost - get_consumed_operating_cost(work_order.name, bom_no)) + / remaining_qty + ) elif work_order.qty: operating_cost_per_unit += flt(d.planned_operating_cost) / flt(work_order.qty) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 5d8ebdda56f..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) @@ -2422,12 +2422,11 @@ class TestStockEntry(ERPNextTestSuite): Unit test case to check the document naming rule with company condition For Quality Inspection, when created from Stock Entry. """ - from erpnext.accounts.report.trial_balance.test_trial_balance import create_company from erpnext.controllers.stock_controller import make_quality_inspections from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse # create a separate company to handle document naming rule with company condition - qc_company = create_company(company_name="Test Quality Company") + qc_company = "Test Quality Company" # create document naming rule based on that for Quality Inspection Doctype qc_naming_rule = frappe.new_doc( @@ -2465,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_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index e0fc0ba6d11..4c8b666ef5d 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -1801,6 +1801,47 @@ class TestStockReconciliation(ERPNextTestSuite, StockTestMixin): elif s.id_plant == plant_b.name: self.assertEqual(s.actual_qty, 3) + def test_serial_no_status_with_backdated_stock_reco(self): + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + item_code = self.make_item( + "Test Item", + { + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "SERIAL.###", + }, + ).name + + warehouse = "_Test Warehouse - _TC" + + reco = create_stock_reconciliation( + item_code=item_code, + posting_date=add_days(nowdate(), -2), + warehouse=warehouse, + qty=1, + rate=80, + purpose="Opening Stock", + ) + + serial_no = get_serial_nos_from_bundle(reco.items[0].serial_and_batch_bundle)[0] + + create_delivery_note( + item_code=item_code, warehouse=warehouse, qty=1, rate=100, posting_date=nowdate() + ) + + self.assertEqual(frappe.get_value("Serial No", serial_no, "status"), "Delivered") + + reco = create_stock_reconciliation( + item_code=item_code, + posting_date=add_days(nowdate(), -1), + warehouse=warehouse, + qty=1, + rate=90, + ) + + self.assertEqual(frappe.get_value("Serial No", serial_no, "status"), "Delivered") + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) 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/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index c2c553457b0..f4079cc54cb 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -109,49 +109,23 @@ class Warehouse(NestedSet): def warn_about_multiple_warehouse_account(self): "If Warehouse value is split across multiple accounts, warn." - def get_accounts_where_value_is_booked(name): - sle = frappe.qb.DocType("Stock Ledger Entry") - gle = frappe.qb.DocType("GL Entry") - ac = frappe.qb.DocType("Account") - - return ( - frappe.qb.from_(sle) - .join(gle) - .on(sle.voucher_no == gle.voucher_no) - .join(ac) - .on(ac.name == gle.account) - .select(gle.account) - .distinct() - .where((sle.warehouse == name) & (ac.account_type == "Stock")) - .orderby(sle.creation) - .run(as_dict=True) - ) - - if self.is_new(): + if not frappe.db.count("Stock Ledger Entry", {"warehouse": self.name}): return - old_wh_account = frappe.db.get_value("Warehouse", self.name, "account") + doc_before_save = self.get_doc_before_save() + old_wh_account = doc_before_save.account if doc_before_save else None - # WH account is being changed or set get all accounts against which wh value is booked - if self.account != old_wh_account: - accounts = get_accounts_where_value_is_booked(self.name) - accounts = [d.account for d in accounts] + if self.is_new() or (self.account and old_wh_account == self.account): + return - if not accounts or (len(accounts) == 1 and self.account in accounts): - # if same singular account has stock value booked ignore - return - - warning = _("Warehouse's Stock Value has already been booked in the following accounts:") - account_str = "
    " + ", ".join(frappe.bold(ac) for ac in accounts) - reason = "

    " + _( - "Booking stock value across multiple accounts will make it harder to track stock and account value." - ) - - frappe.msgprint( - warning + account_str + reason, - title=_("Multiple Warehouse Accounts"), - indicator="orange", - ) + frappe.msgprint( + title=_("Warning: Account changed for warehouse"), + indicator="orange", + msg=_( + "Stock entries exist with the old account. Changing the account may lead to a mismatch between the warehouse closing balance and the account closing balance. The overall closing balance will still match, but not for the specific account." + ), + alert=True, + ) def check_if_sle_exists(self, non_cancelled_only=False): filters = {"warehouse": self.name} 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/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index c6990c9492d..2ea52e91a8d 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -7,6 +7,7 @@ from operator import itemgetter import frappe from frappe import _ +from frappe.query_builder.functions import Count from frappe.utils import cint, date_diff, flt, get_datetime from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -240,9 +241,9 @@ class FIFOSlots: Returns dict of the foll.g structure: Key = Item A / (Item A, Warehouse A) Key: { - 'details' -> Dict: ** item details **, - 'fifo_queue' -> List: ** list of lists containing entries/slots for existing stock, - consumed/updated and maintained via FIFO. ** + 'details' -> Dict: ** item details **, + 'fifo_queue' -> List: ** list of lists containing entries/slots for existing stock, + consumed/updated and maintained via FIFO. ** } """ from erpnext.stock.serial_batch_bundle import get_serial_nos_from_bundle @@ -253,16 +254,33 @@ class FIFOSlots: if stock_ledger_entries is None: bundle_wise_serial_nos = self.__get_bundle_wise_serial_nos() + # prepare single sle voucher detail lookup + self.prepare_stock_reco_voucher_wise_count() + with frappe.db.unbuffered_cursor(): if stock_ledger_entries is None: stock_ledger_entries = self.__get_stock_ledger_entries() for d in stock_ledger_entries: key, fifo_queue, transferred_item_key = self.__init_key_stores(d) + prev_balance_qty = self.item_details[key].get("qty_after_transaction", 0) - if d.voucher_type == "Stock Reconciliation": + if d.voucher_type == "Stock Reconciliation" and ( + not d.batch_no or d.serial_no or d.serial_and_batch_bundle + ): + if d.voucher_detail_no in self.stock_reco_voucher_wise_count: + # for legacy recon with single sle has qty_after_transaction and stock_value_difference without outward entry + # for exisitng handle emptying the existing queue and details. + d.stock_value_difference = flt(d.qty_after_transaction * d.valuation_rate) + d.actual_qty = d.qty_after_transaction + self.item_details[key]["qty_after_transaction"] = 0 + self.item_details[key]["total_qty"] = 0 + fifo_queue.clear() + else: + d.actual_qty = flt(d.qty_after_transaction) - flt(prev_balance_qty) + + elif d.voucher_type == "Stock Reconciliation": # get difference in qty shift as actual qty - prev_balance_qty = self.item_details[key].get("qty_after_transaction", 0) d.actual_qty = flt(d.qty_after_transaction) - flt(prev_balance_qty) serial_nos = get_serial_nos(d.serial_no) if d.serial_no else [] @@ -280,6 +298,14 @@ class FIFOSlots: self.__update_balances(d, key) + # handle serial nos misconsumption + if d.has_serial_no: + qty_after = cint(self.item_details[key]["qty_after_transaction"]) + if qty_after <= 0: + fifo_queue.clear() + elif len(fifo_queue) > qty_after: + fifo_queue[:] = fifo_queue[:qty_after] + # Note that stock_ledger_entries is an iterator, you can not reuse it like a list del stock_ledger_entries @@ -406,7 +432,6 @@ class FIFOSlots: def __update_balances(self, row: dict, key: tuple | str): self.item_details[key]["qty_after_transaction"] = row.qty_after_transaction - if "total_qty" not in self.item_details[key]: self.item_details[key]["total_qty"] = row.actual_qty else: @@ -462,6 +487,7 @@ class FIFOSlots: sle.posting_date, sle.voucher_type, sle.voucher_no, + sle.voucher_detail_no, sle.serial_no, sle.batch_no, sle.qty_after_transaction, @@ -558,3 +584,36 @@ class FIFOSlots: warehouse_results = [x[0] for x in warehouse_results] return sle_query.where(sle.warehouse.isin(warehouse_results)) + + def prepare_stock_reco_voucher_wise_count(self): + self.stock_reco_voucher_wise_count = frappe._dict() + + doctype = frappe.qb.DocType("Stock Ledger Entry") + item = frappe.qb.DocType("Item") + + query = ( + frappe.qb.from_(doctype) + .inner_join(item) + .on(doctype.item_code == item.name) + .select(doctype.voucher_detail_no, Count(doctype.name).as_("count")) + .where( + (doctype.voucher_type == "Stock Reconciliation") + & (doctype.docstatus < 2) + & (doctype.is_cancelled == 0) + ) + .groupby(doctype.voucher_detail_no) + ) + + data = query.run(as_dict=True) + if not data: + return + + for row in data: + if row.count != 1: + continue + + sr_item = frappe.db.get_value( + "Stock Reconciliation Item", row.voucher_detail_no, ["current_qty", "qty"], as_dict=True + ) + if sr_item.qty and sr_item.current_qty: + self.stock_reco_voucher_wise_count[row.voucher_detail_no] = sr_item.current_qty diff --git a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py index 97243d57001..e0d39c5dc7a 100644 --- a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py +++ b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py @@ -248,12 +248,7 @@ def get_item_warehouse_combinations(filters: dict | None = None) -> dict: bin.warehouse, item.valuation_method, ) - .where( - (item.is_stock_item == 1) - & (item.has_serial_no == 0) - & (warehouse.is_group == 0) - & (warehouse.company == filters.company) - ) + .where((item.is_stock_item == 1) & (warehouse.is_group == 0) & (warehouse.company == filters.company)) ) if filters.item_code: diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index b2401da4f8f..7f6deda9b8c 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -31,7 +31,8 @@ def repost(only_actual=False, allow_negative_stock=False, allow_zero_rate=False, for d in item_warehouses: try: repost_stock(d[0], d[1], allow_zero_rate, only_actual, only_bin, allow_negative_stock) - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() except Exception: frappe.db.rollback() diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 1e6cec59a5c..7a4787123b6 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -4,6 +4,7 @@ import copy import gzip import json +from collections import deque import frappe from frappe import _, bold, scrub @@ -16,6 +17,7 @@ from frappe.utils import ( cstr, flt, format_date, + get_datetime, get_link_to_form, getdate, now, @@ -77,9 +79,6 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc future_sle_exists(args, sl_entries) for sle in sl_entries: - if sle.serial_no and not via_landed_cost_voucher: - validate_serial_no(sle) - if cancelled: sle["actual_qty"] = -flt(sle.get("actual_qty")) @@ -160,35 +159,6 @@ def get_args_for_future_sle(row): ) -def validate_serial_no(sle): - from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos - - for sn in get_serial_nos(sle.serial_no): - args = copy.deepcopy(sle) - args.serial_no = sn - args.warehouse = "" - - vouchers = [] - for row in get_stock_ledger_entries(args, ">"): - voucher_type = frappe.bold(row.voucher_type) - voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no)) - vouchers.append(f"{voucher_type} {voucher_no}") - - if vouchers: - serial_no = frappe.bold(sn) - msg = ( - f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first. - The list of the transactions are as below.""" - + "

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

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

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