diff --git a/erpnext/accounts/custom/address.json b/erpnext/accounts/custom/address.json deleted file mode 100644 index 5c921da9b7a..00000000000 --- a/erpnext/accounts/custom/address.json +++ /dev/null @@ -1,126 +0,0 @@ -{ - "custom_fields": [ - { - "_assign": null, - "_comments": null, - "_liked_by": null, - "_user_tags": null, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "creation": "2018-12-28 22:29:21.828090", - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "dt": "Address", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "tax_category", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "idx": 15, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "fax", - "label": "Tax Category", - "length": 0, - "mandatory_depends_on": null, - "modified": "2018-12-28 22:29:21.828090", - "modified_by": "Administrator", - "name": "Address-tax_category", - "no_copy": 0, - "options": "Tax Category", - "owner": "Administrator", - "parent": null, - "parentfield": null, - "parenttype": null, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "_assign": null, - "_comments": null, - "_liked_by": null, - "_user_tags": null, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "creation": "2020-10-14 17:41:40.878179", - "default": "0", - "depends_on": null, - "description": null, - "docstatus": 0, - "dt": "Address", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "is_your_company_address", - "fieldtype": "Check", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "idx": 20, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "linked_with", - "label": "Is Your Company Address", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-10-14 17:41:40.878179", - "modified_by": "Administrator", - "name": "Address-is_your_company_address", - "no_copy": 0, - "options": null, - "owner": "Administrator", - "parent": null, - "parentfield": null, - "parenttype": null, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - } - ], - "custom_perms": [], - "doctype": "Address", - "property_setters": [], - "sync_on_migrate": 1 -} \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index cf15139b712..33f0dee0702 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -807,11 +807,14 @@ frappe.ui.form.on("Payment Entry", { 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_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) { + frm.set_value("base_received_amount", frm.doc.base_paid_amount); + if (company_currency == frm.doc.paid_to_account_currency) { frm.set_value("received_amount", frm.doc.base_paid_amount); - frm.set_value("base_received_amount", frm.doc.base_paid_amount); + } else if (frm.doc.target_exchange_rate) { + frm.set_value( + "received_amount", + flt(frm.doc.base_paid_amount) / flt(frm.doc.target_exchange_rate) + ); } } frm.trigger("reset_received_amount"); @@ -828,15 +831,14 @@ frappe.ui.form.on("Payment Entry", { ); if (!frm.doc.paid_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) { - frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate); - } - frm.set_value("base_paid_amount", frm.doc.base_received_amount); - } else if (company_currency == frm.doc.paid_from_account_currency) { + frm.set_value("base_paid_amount", frm.doc.base_received_amount); + if (company_currency == frm.doc.paid_from_account_currency) { frm.set_value("paid_amount", frm.doc.base_received_amount); - frm.set_value("base_paid_amount", frm.doc.base_received_amount); + } else if (frm.doc.source_exchange_rate) { + frm.set_value( + "paid_amount", + flt(frm.doc.base_received_amount) / flt(frm.doc.source_exchange_rate) + ); } } diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 80f7305b1bc..c48a3d6c09d 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1238,9 +1238,9 @@ class PaymentEntry(AccountsController): else: remarks = [ _("Amount {0} {1} {2} {3}").format( - _(self.paid_to_account_currency) + _(self.paid_from_account_currency) if self.payment_type == "Receive" - else _(self.paid_from_account_currency), + else _(self.paid_to_account_currency), self.paid_amount if self.payment_type == "Receive" else self.received_amount, _("received from") if self.payment_type == "Receive" else _("paid to"), self.party, @@ -1256,7 +1256,7 @@ class PaymentEntry(AccountsController): for d in self.get("references"): if d.allocated_amount: remarks.append( - _("Amount {0} {1} against {2} {3}").format( + _("Amount {0} {1} adjusted against {2} {3}").format( _(self.party_account_currency), d.allocated_amount, d.reference_doctype, @@ -1267,7 +1267,7 @@ class PaymentEntry(AccountsController): for d in self.get("deductions"): if d.amount: remarks.append( - _("Amount {0} {1} deducted against {2}").format( + _("Amount {0} {1} as adjustment to {2}").format( _(self.company_currency), d.amount, d.account ) ) diff --git a/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py b/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py index c2528040e98..dbc5d9e146a 100644 --- a/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py +++ b/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py @@ -3,7 +3,8 @@ import frappe from frappe import qb -from frappe.utils import nowdate +from frappe.query_builder.functions import Count, Sum +from frappe.utils import add_days, nowdate from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry @@ -90,6 +91,7 @@ class TestPaymentLedgerEntry(ERPNextTestSuite): posting_date = nowdate() sinv = create_sales_invoice( + posting_date=posting_date, qty=qty, rate=rate, company=self.company, @@ -531,3 +533,82 @@ class TestPaymentLedgerEntry(ERPNextTestSuite): # with references removed, deletion should be possible so.delete() self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, so.doctype, so.name) + + @ERPNextTestSuite.change_settings( + "Accounts Settings", + {"enable_immutable_ledger": 1}, + ) + def test_reverse_entries_on_cancel_for_immutable_ledger(self): + invoice_posting_date = add_days(nowdate(), -5) + gle = qb.DocType("GL Entry") + ple = qb.DocType("Payment Ledger Entry") + + si = self.create_sales_invoice(qty=1, rate=100, posting_date=invoice_posting_date) + + gles_before = ( + qb.from_(gle) + .select( + Count(gle.name), + ) + .where((gle.voucher_type == si.doctype) & (gle.voucher_no == si.name) & (gle.is_cancelled == 0)) + .run()[0][0] + ) + ples_before = ( + qb.from_(ple) + .select( + Count(ple.name), + ) + .where((ple.voucher_type == si.doctype) & (ple.voucher_no == si.name) & (ple.delinked.eq(0))) + .run()[0][0] + ) + + si.cancel() + + gles_after = ( + qb.from_(gle) + .select(Count(gle.account)) + .where((gle.voucher_type == si.doctype) & (gle.voucher_no == si.name) & (gle.is_cancelled == 0)) + .run()[0][0] + ) + self.assertEqual(gles_after, gles_before * 2) + + ples_after = ( + qb.from_(ple) + .select( + Count(ple.name), + ) + .where((ple.voucher_type == si.doctype) & (ple.voucher_no == si.name) & (ple.delinked.eq(0))) + .run()[0][0] + ) + self.assertEqual(ples_after, ples_before * 2) + + # assert debit/credit are reversed + gl_entries = ( + qb.from_(gle) + .select(gle.account, Sum(gle.debit).as_("total_debit"), Sum(gle.credit).as_("total_credit")) + .where((gle.voucher_type == si.doctype) & (gle.voucher_no == si.name) & (gle.is_cancelled == 0)) + .groupby(gle.account) + .run(as_dict=True) + ) + for gl in gl_entries: + with self.subTest(gl=gl): + self.assertEqual(gl.total_debit, gl.total_credit) + + # assert amounts are reversed + pl_entries = ( + qb.from_(ple) + .select(ple.account, Sum(ple.amount).as_("total_amount")) + .where((ple.voucher_type == si.doctype) & (ple.voucher_no == si.name) & (ple.delinked == 0)) + .groupby(ple.account) + .run(as_dict=True) + ) + for pl in pl_entries: + with self.subTest(pl=pl): + self.assertEqual(pl.total_amount, 0) + + self.assertFalse( + frappe.db.exists( + "Payment Ledger Entry", + {"voucher_type": si.doctype, "voucher_no": si.name, "delinked": 1}, + ) + ) diff --git a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py index 3258a8cbe8a..95ab540d657 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py @@ -8,6 +8,7 @@ from frappe.utils import today from erpnext.accounts.doctype.finance_book.test_finance_book import create_finance_book from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.general_ledger import make_reverse_gl_entries from erpnext.accounts.utils import get_fiscal_year from erpnext.tests.utils import ERPNextTestSuite @@ -334,6 +335,48 @@ class TestPeriodClosingVoucher(ERPNextTestSuite): return pcv + @ERPNextTestSuite.change_settings( + "Accounts Settings", + {"enable_immutable_ledger": 1}, + ) + def test_immutable_ledger_reverse_entry_uses_passed_posting_date_after_pcv(self): + company = create_company() + cost_center = create_cost_center("Test Cost Center 1") + + jv = make_journal_entry( + posting_date="2021-03-15", + amount=400, + account1="Cash - TPC", + account2="Sales - TPC", + cost_center=cost_center, + company=company, + save=False, + ) + jv.company = company + jv.save() + jv.submit() + + self.make_period_closing_voucher(posting_date="2021-03-31") + + # Passed posting_date is after PCV end date, so cancellation should not fail. + make_reverse_gl_entries( + voucher_type="Journal Entry", + voucher_no=jv.name, + posting_date="2022-01-01", + ) + + totals_after_cancel = frappe.db.sql( + """ + select sum(debit) as total_debit, sum(credit) as total_credit + from `tabGL Entry` + where voucher_type=%s and voucher_no=%s and is_cancelled=0 + """, + ("Journal Entry", jv.name), + as_dict=True, + )[0] + + self.assertEqual(totals_after_cancel.total_debit, totals_after_cancel.total_credit) + def create_company(): company = frappe.get_doc( diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json index 78d568b60b1..cdabfd3dadb 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.json +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json @@ -13,52 +13,69 @@ "column_break_9", "warehouse", "company_address", - "section_break_15", - "applicable_for_users", + "accounting_tab", "section_break_11", "payments", + "set_grand_total_to_default_mop", + "price_list_and_currency_section", + "currency", + "column_break_bptt", + "selling_price_list", + "write_off_section", + "write_off_account", + "column_break_ukpz", + "write_off_cost_center", + "column_break_pkca", + "write_off_limit", + "income_and_expense_account", + "income_account", + "column_break_byzk", + "expense_account", + "taxes_section", + "taxes_and_charges", + "column_break_cjpp", + "tax_category", + "section_break_19", + "account_for_change_amount", + "disable_rounded_total", + "column_break_23", + "apply_discount_on", + "allow_partial_payment", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break", + "project", + "pos_configurations_tab", "section_break_14", - "hide_images", - "hide_unavailable_items", - "auto_add_item_to_cart", - "validate_stock_on_save", - "print_receipt_on_order_complete", "action_on_new_invoice", + "validate_stock_on_save", "column_break_16", "update_stock", "ignore_pricing_rule", + "print_receipt_on_order_complete", + "pos_item_selector_section", + "hide_images", + "column_break_rpny", + "hide_unavailable_items", + "column_break_stcl", + "auto_add_item_to_cart", + "pos_item_details_section", "allow_rate_change", + "column_break_hwfg", "allow_discount_change", - "set_grand_total_to_default_mop", - "allow_partial_payment", + "section_break_15", + "applicable_for_users", "section_break_23", "item_groups", "column_break_25", "customer_groups", + "more_info_tab", "section_break_16", "print_format", "letter_head", "column_break0", "tc_name", "select_print_heading", - "section_break_19", - "selling_price_list", - "currency", - "write_off_account", - "write_off_cost_center", - "write_off_limit", - "account_for_change_amount", - "disable_rounded_total", - "column_break_23", - "income_account", - "expense_account", - "taxes_and_charges", - "tax_category", - "apply_discount_on", - "accounting_dimensions_section", - "cost_center", - "dimension_col_break", - "project", "utm_analytics_section", "utm_source", "column_break_tvls", @@ -133,8 +150,7 @@ }, { "fieldname": "section_break_14", - "fieldtype": "Section Break", - "label": "Configuration" + "fieldtype": "Section Break" }, { "description": "Only show Items from these Item Groups", @@ -155,6 +171,7 @@ "options": "POS Customer Group" }, { + "collapsible": 1, "fieldname": "section_break_16", "fieldtype": "Section Break", "label": "Print Settings" @@ -194,7 +211,7 @@ { "fieldname": "section_break_19", "fieldtype": "Section Break", - "label": "Accounting" + "label": "Miscellaneous" }, { "fieldname": "selling_price_list", @@ -430,6 +447,7 @@ }, { "default": "0", + "description": "Applicable on POS Invoice", "fieldname": "allow_partial_payment", "fieldtype": "Check", "label": "Allow Partial Payment" @@ -447,6 +465,83 @@ "fieldname": "utm_analytics_section", "fieldtype": "Section Break", "label": "Campaign" + }, + { + "fieldname": "accounting_tab", + "fieldtype": "Tab Break", + "label": "Accounting" + }, + { + "fieldname": "more_info_tab", + "fieldtype": "Tab Break", + "label": "More Info" + }, + { + "fieldname": "pos_configurations_tab", + "fieldtype": "Tab Break", + "label": "POS Configurations" + }, + { + "fieldname": "price_list_and_currency_section", + "fieldtype": "Section Break", + "label": "Price List & Currency" + }, + { + "fieldname": "column_break_bptt", + "fieldtype": "Column Break" + }, + { + "fieldname": "write_off_section", + "fieldtype": "Section Break", + "label": "Write Off" + }, + { + "fieldname": "column_break_ukpz", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_pkca", + "fieldtype": "Column Break" + }, + { + "fieldname": "income_and_expense_account", + "fieldtype": "Section Break", + "label": "Income and Expense" + }, + { + "fieldname": "column_break_byzk", + "fieldtype": "Column Break" + }, + { + "fieldname": "taxes_section", + "fieldtype": "Section Break", + "label": "Taxes" + }, + { + "fieldname": "column_break_cjpp", + "fieldtype": "Column Break" + }, + { + "fieldname": "pos_item_selector_section", + "fieldtype": "Section Break", + "label": "POS Item Selector" + }, + { + "fieldname": "column_break_rpny", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_stcl", + "fieldtype": "Column Break" + }, + { + "fieldname": "pos_item_details_section", + "fieldtype": "Section Break", + "label": "POS Item Details" + }, + { + "fieldname": "column_break_hwfg", + "fieldtype": "Column Break" } ], "grid_page_length": 50, @@ -475,7 +570,7 @@ "link_fieldname": "pos_profile" } ], - "modified": "2026-02-10 14:24:48.597412", + "modified": "2026-05-26 12:07:48.597412", "modified_by": "Administrator", "module": "Accounts", "name": "POS Profile", diff --git a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py index cf10d3152d5..24b9fabd998 100644 --- a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py +++ b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py @@ -137,9 +137,10 @@ def pause_pcv_processing(docname: str): ppcv = qb.DocType("Process Period Closing Voucher") qb.update(ppcv).set(ppcv.status, "Paused").where(ppcv.name.eq(docname)).run() + # If a date is stuck in 'Running' state, this will allow it to procced. if queued_dates := frappe.db.get_all( "Process Period Closing Voucher Detail", - filters={"parent": docname, "status": "Queued"}, + filters={"parent": docname, "status": ["in", ["Queued", "Running"]]}, pluck="name", ): ppcvd = qb.DocType("Process Period Closing Voucher Detail") @@ -173,6 +174,9 @@ def resume_pcv_processing(docname: str): ppcvd = qb.DocType("Process Period Closing Voucher Detail") qb.update(ppcvd).set(ppcvd.status, "Queued").where(ppcvd.name.isin(paused_dates)).run() start_pcv_processing(docname) + else: + # If a parent doc is stuck in 'Running' state, will allow it to proceed. + schedule_next_date(docname) def update_default_dimensions(dimension_fields, gl_entry, dimension_values): @@ -288,7 +292,21 @@ def schedule_next_date(docname: str): ) # Ensure both normal and opening balances are processed for all dates if total_no_of_dates == completed: - summarize_and_post_ledger_entries(docname) + from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import ( + is_job_running, + ) + + job_name = f"summarize_{docname}" + if not is_job_running(job_name): + frappe.enqueue( + method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.summarize_and_post_ledger_entries", + queue="long", + timeout="3600", + is_async=True, + job_name=job_name, + enqueue_after_commit=True, + docname=docname, + ) def make_dict_json_compliant(dimension_wise_balance) -> dict: diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 2f8e538a601..7a9c1c379c9 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -1921,7 +1921,7 @@ { "default": "0", "depends_on": "eval: !doc.is_return", - "description": "Issue a debit note with 0 qty against an existing Sales Invoice", + "description": "Issue a debit note against an existing Sales Invoice to adjust the rate. The quantity will be retained from the original invoice.", "fieldname": "is_debit_note", "fieldtype": "Check", "label": "Is Rate Adjustment Entry (Debit Note)" @@ -2367,7 +2367,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2026-05-01 02:37:29.742764", + "modified": "2026-05-21 17:31:11.190958", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index ef64a3ef79b..8a7648d20d8 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -968,9 +968,6 @@ class SalesInvoice(SellingController): if selling_price_list: self.set("selling_price_list", selling_price_list) - if not for_validate: - self.update_stock = cint(pos.get("update_stock")) - # set pos values in items for item in self.get("items"): if item.get("item_code"): @@ -981,6 +978,10 @@ class SalesInvoice(SellingController): if (not for_validate) or (for_validate and not item.get(fname)): item.set(fname, val) + if not for_validate: + dn_flag = any(d.get("dn_detail") for d in self.get("items")) + self.update_stock = 0 if dn_flag else cint(pos.get("update_stock")) + # fetch terms if self.tc_name and not self.terms: self.terms = frappe.db.get_value("Terms and Conditions", self.tc_name, "terms") diff --git a/erpnext/accounts/doctype/tax_rule/tax_rule.py b/erpnext/accounts/doctype/tax_rule/tax_rule.py index 3a36e57ae60..7ce68f926f4 100644 --- a/erpnext/accounts/doctype/tax_rule/tax_rule.py +++ b/erpnext/accounts/doctype/tax_rule/tax_rule.py @@ -14,6 +14,7 @@ from frappe.utils import cstr from frappe.utils.nestedset import get_root_of from erpnext.setup.doctype.customer_group.customer_group import get_parent_customer_groups +from erpnext.setup.doctype.supplier_group.supplier_group import get_parent_supplier_groups class IncorrectCustomerGroup(frappe.ValidationError): @@ -176,38 +177,44 @@ def get_party_details(party, party_type, args=None): def get_tax_template(posting_date, args): """Get matching tax rule""" args = frappe._dict(args) - conditions = [] + + TaxRule = DocType("Tax Rule") + query = frappe.qb.from_(TaxRule).select("*") if posting_date: - conditions.append( - f"""(from_date is null or from_date <= '{posting_date}') - and (to_date is null or to_date >= '{posting_date}')""" + query = query.where( + (TaxRule.from_date.isnull() | (TaxRule.from_date <= posting_date)) + & (TaxRule.to_date.isnull() | (TaxRule.to_date >= posting_date)) ) else: - conditions.append("(from_date is null) and (to_date is null)") + query = query.where(TaxRule.from_date.isnull() & TaxRule.to_date.isnull()) - conditions.append( - "ifnull(tax_category, '') = {}".format(frappe.db.escape(cstr(args.get("tax_category")), False)) - ) - if "tax_category" in args.keys(): - del args["tax_category"] + def get_group_ancestors(doctype, get_parents, value): + if not value: + value = get_root_of(doctype) + return [""] + [d.name for d in get_parents(value)] + + group_fields = { + "customer_group": ("Customer Group", get_parent_customer_groups), + "supplier_group": ("Supplier Group", get_parent_supplier_groups), + } + + args.setdefault("tax_category", "") for key, value in args.items(): if key == "use_for_shopping_cart": - conditions.append(f"use_for_shopping_cart = {1 if value else 0}") - elif key == "customer_group": - if not value: - value = get_root_of("Customer Group") - customer_group_condition = get_customer_group_condition(value) - conditions.append(f"ifnull({key}, '') in ('', {customer_group_condition})") + query = query.where(TaxRule.use_for_shopping_cart == value) + elif key == "tax_category": + query = query.where(IfNull(TaxRule.tax_category, "") == (value or "")) + elif key in group_fields: + doctype, get_parents = group_fields[key] + query = query.where( + IfNull(TaxRule[key], "").isin(get_group_ancestors(doctype, get_parents, value)) + ) else: - conditions.append(f"ifnull({key}, '') in ('', {frappe.db.escape(cstr(value))})") + query = query.where(IfNull(TaxRule[key], "").isin(["", value or ""])) - tax_rule = frappe.db.sql( - """select * from `tabTax Rule` - where {}""".format(" and ".join(conditions)), - as_dict=True, - ) + tax_rule = query.run(as_dict=True) if not tax_rule: return None @@ -236,11 +243,3 @@ def get_tax_template(posting_date, args): return None return tax_template - - -def get_customer_group_condition(customer_group): - condition = "" - customer_groups = ["%s" % (frappe.db.escape(d.name)) for d in get_parent_customer_groups(customer_group)] - if customer_groups: - condition = ",".join(["%s"] * len(customer_groups)) % (tuple(customer_groups)) - return condition diff --git a/erpnext/accounts/doctype/tax_rule/test_tax_rule.py b/erpnext/accounts/doctype/tax_rule/test_tax_rule.py index ed3d004cc7e..c94e4fb18df 100644 --- a/erpnext/accounts/doctype/tax_rule/test_tax_rule.py +++ b/erpnext/accounts/doctype/tax_rule/test_tax_rule.py @@ -62,6 +62,117 @@ class TestTaxRule(ERPNextTestSuite): "_Test Sales Taxes and Charges Template - _TC", ) + def test_for_parent_supplier_group(self): + purchase_template = "_Test Purchase Taxes and Charges Template - _TC" + if not frappe.db.exists("Purchase Taxes and Charges Template", purchase_template): + frappe.get_doc( + { + "doctype": "Purchase Taxes and Charges Template", + "title": "_Test Purchase Taxes and Charges Template", + "company": "_Test Company", + "taxes": [ + { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "description": "VAT", + "doctype": "Purchase Taxes and Charges", + "cost_center": "Main - _TC", + "rate": 6, + } + ], + } + ).insert() + + make_tax_rule( + supplier_group="All Supplier Groups", + tax_type="Purchase", + purchase_tax_template=purchase_template, + priority=1, + use_for_shopping_cart=0, + from_date="2015-01-01", + save=1, + ) + + # "_Test Supplier Group" has "All Supplier Groups" as its parent — should match hierarchically + self.assertEqual( + get_tax_template( + "2015-01-01", + { + "supplier_group": "_Test Supplier Group", + "tax_type": "Purchase", + "use_for_shopping_cart": 0, + }, + ), + purchase_template, + ) + + def test_use_for_shopping_cart_filter(self): + city = "Test Cart City" + # higher priority ensures this rule wins when use_for_shopping_cart is not filtered + make_tax_rule( + customer="_Test Customer", + billing_city=city, + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + use_for_shopping_cart=0, + priority=2, + save=1, + ) + make_tax_rule( + customer="_Test Customer", + billing_city=city, + sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC", + use_for_shopping_cart=1, + priority=1, + save=1, + ) + + # Cart request (use_for_shopping_cart=1) filters to cart rules only + self.assertEqual( + get_tax_template( + "2015-01-01", + {"customer": "_Test Customer", "billing_city": city, "use_for_shopping_cart": 1}, + ), + "_Test Sales Taxes and Charges Template 1 - _TC", + ) + + # Non-cart request omits use_for_shopping_cart — no filter is applied, both rules + # are candidates; non-cart rule wins by higher priority + self.assertEqual( + get_tax_template( + "2015-01-01", + {"customer": "_Test Customer", "billing_city": city}, + ), + "_Test Sales Taxes and Charges Template - _TC", + ) + + def test_use_for_shopping_cart_default(self): + city = "Test Default Cart City" + # use_for_shopping_cart not set — Check field defaults to 0 + make_tax_rule( + customer="_Test Customer", + billing_city=city, + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + use_for_shopping_cart=0, # Default is set to 1. + save=1, + ) + + # Non-cart request (no use_for_shopping_cart in args) matches the rule + self.assertEqual( + get_tax_template( + "2015-01-01", + {"customer": "_Test Customer", "billing_city": city}, + ), + "_Test Sales Taxes and Charges Template - _TC", + ) + + # Cart request (use_for_shopping_cart=1) does not match — rule has default 0 + self.assertIsNone( + get_tax_template( + "2015-01-01", + {"customer": "_Test Customer", "billing_city": city, "use_for_shopping_cart": 1}, + ) + ) + def test_conflict_with_overlapping_dates(self): tax_rule1 = make_tax_rule( customer="_Test Customer", diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index de7f5941fca..341488381a0 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -431,6 +431,8 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False): gle.flags.adv_adj = adv_adj gle.flags.update_outstanding = update_outstanding or "Yes" gle.flags.notify_update = False + if gle.is_cancelled or is_immutable_ledger_enabled(): + gle.flags.ignore_links = True gle.submit() if ( @@ -717,7 +719,12 @@ def make_reverse_gl_entries( check_freezing_date(gl_entries[0]["posting_date"], adv_adj) is_opening = any(d.get("is_opening") == "Yes" for d in gl_entries) - validate_against_pcv(is_opening, gl_entries[0]["posting_date"], gl_entries[0]["company"]) + + # For reverse entries, use the posting_date parameter if provided and valid + # Otherwise fall back to original posting_date + validation_date = posting_date if posting_date else gl_entries[0]["posting_date"] + validate_against_pcv(is_opening, validation_date, gl_entries[0]["company"]) + if partial_cancel: # Partial cancel is only used by `Advance` in separate account feature. # Only cancel GL entries for unlinked reference using `voucher_detail_no` diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 120d6bb2c96..5a3c6647261 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -750,7 +750,7 @@ def set_taxes( args.update({"tax_type": "Purchase"}) if use_for_shopping_cart: - args.update({"use_for_shopping_cart": use_for_shopping_cart}) + args.update({"use_for_shopping_cart": cint(use_for_shopping_cart)}) return get_tax_template(posting_date, args) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index c1e8a831b2c..2fd9bb30e34 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -40,7 +40,7 @@ import erpnext from erpnext.accounts.doctype.account.account import get_account_currency from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions from erpnext.stock import get_warehouse_account_map -from erpnext.stock.utils import get_stock_value_on +from erpnext.stock.utils import get_combine_datetime, get_stock_value_on if TYPE_CHECKING: from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import RepostItemValuation @@ -1752,31 +1752,31 @@ def sort_stock_vouchers_by_posting_date( def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, for_items=None, company=None): - values = [] - condition = "" + posting_datetime = get_combine_datetime(posting_date, posting_time) + + SLE = DocType("Stock Ledger Entry") + + query = ( + frappe.qb.from_(SLE) + .select(SLE.voucher_type, SLE.voucher_no) + .distinct() + .where(SLE.posting_datetime >= posting_datetime) + .where(SLE.is_cancelled == 0) + .orderby(SLE.posting_datetime) + .orderby(SLE.creation) + .for_update() + ) + if for_items: - condition += " and item_code in ({})".format(", ".join(["%s"] * len(for_items))) - values += for_items + query = query.where(SLE.item_code.isin(for_items)) if for_warehouses: - condition += " and warehouse in ({})".format(", ".join(["%s"] * len(for_warehouses))) - values += for_warehouses + query = query.where(SLE.warehouse.isin(for_warehouses)) if company: - condition += " and company = %s" - values.append(company) + query = query.where(SLE.company == company) - future_stock_vouchers = frappe.db.sql( - f"""select distinct sle.voucher_type, sle.voucher_no - from `tabStock Ledger Entry` sle - where - timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s) - and is_cancelled = 0 - {condition} - order by timestamp(sle.posting_date, sle.posting_time) asc, creation asc for update""", - tuple([posting_date, posting_time, *values]), - as_dict=True, - ) + future_stock_vouchers = query.run(as_dict=True) return [(d.voucher_type, d.voucher_no) for d in future_stock_vouchers] @@ -2130,8 +2130,9 @@ def create_payment_ledger_entry( ple = frappe.get_doc(entry) if cancel: - delink_original_entry(ple, partial_cancel=partial_cancel) - if is_immutable_ledger_enabled(): + if not is_immutable_ledger_enabled(): + delink_original_entry(ple, partial_cancel=partial_cancel) + else: ple.delinked = 0 ple.posting_date = frappe.form_dict.get("posting_date") or getdate() @@ -2220,6 +2221,7 @@ def delink_original_entry(pl_entry, partial_cancel=False): qb.update(ple) .set(ple.modified, now()) .set(ple.modified_by, frappe.session.user) + .set(ple.delinked, True) .where( (ple.company == pl_entry.company) & (ple.account_type == pl_entry.account_type) @@ -2236,9 +2238,6 @@ def delink_original_entry(pl_entry, partial_cancel=False): if partial_cancel: query = query.where(ple.voucher_detail_no == pl_entry.voucher_detail_no) - if not is_immutable_ledger_enabled(): - query = query.set(ple.delinked, True) - query.run() diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 5a5f22b5788..7482d5f789a 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -551,7 +551,9 @@ frappe.ui.form.on("Asset", { asset_type: function (frm) { if (frm.doc.docstatus == 0) { if (frm.doc.asset_type == "Composite Asset") { - frm.set_value("net_purchase_amount", 0); + if (!frm.doc.net_purchase_amount) { + frm.set_value("net_purchase_amount", 0); + } } else { frm.set_df_property("net_purchase_amount", "read_only", 0); } diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 580813b91d3..25906719a6d 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -72,6 +72,7 @@ from erpnext.stock.get_item_details import ( ItemDetailsCtx, _get_item_tax_template, _get_item_tax_template_from_item_group, + get_bin_details, get_conversion_factor, get_item_details, get_item_tax_map, @@ -3737,6 +3738,7 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child child_item.warehouse = get_item_warehouse_(p_doc, item, overwrite_warehouse=True) conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor")) child_item.conversion_factor = flt(trans_item.get("conversion_factor")) or conversion_factor + child_item.update(get_bin_details(child_item.item_code, child_item.warehouse, p_doc.get("company"))) if child_doctype in ["Purchase Order Item", "Supplier Quotation Item"]: # Initialized value will update in parent validation diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py index 1e2ef75de93..ab6d3c81878 100644 --- a/erpnext/controllers/item_variant.py +++ b/erpnext/controllers/item_variant.py @@ -209,7 +209,9 @@ def create_variant(item, args, use_template_image=False): variant_attributes = [] for d in template.attributes: - variant_attributes.append({"attribute": d.attribute, "attribute_value": args.get(_(d.attribute))}) + attribute_value = args.get(_(d.attribute)) or args.get(d.attribute) + if attribute_value: + variant_attributes.append({"attribute": d.attribute, "attribute_value": attribute_value}) variant.set("attributes", variant_attributes) copy_attributes_to_variant(template, variant) @@ -228,6 +230,12 @@ def enqueue_multiple_variant_creation(item, args, use_template_image=False): # There can be innumerable attribute combinations, enqueue if isinstance(args, str): variants = json.loads(args) + else: + variants = args + variants = {key: values for key, values in variants.items() if values} + if not variants: + frappe.throw(_("Please select at least one attribute value")) + total_variants = 1 for key in variants: total_variants *= len(variants[key]) @@ -251,6 +259,7 @@ def create_multiple_variants(item, args, use_template_image=False): count = 0 if isinstance(args, str): args = json.loads(args) + args = {key: values for key, values in args.items() if values} template_item = frappe.get_doc("Item", item) args_set = generate_keyed_value_combinations(args) @@ -285,6 +294,9 @@ def generate_keyed_value_combinations(args): """ # Return empty list if empty + if not args: + return [] + args = {key: values for key, values in args.items() if values} if not args: return [] diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index b2e7337a4ec..08d2e26a61a 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -17,6 +17,7 @@ from pypika import Order import erpnext from erpnext.accounts.utils import build_qb_match_conditions from erpnext.stock.get_item_details import ItemDetailsCtx, _get_item_tax_template +from erpnext.stock.utils import get_combine_datetime # searches for active employees @@ -210,16 +211,28 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals if filters and isinstance(filters, dict): if filters.get("customer") or filters.get("supplier"): + party_type = "Customer" if filters.get("customer") else "Supplier" party = filters.get("customer") or filters.get("supplier") + group = "Customer Group" if filters.get("customer") else "Supplier Group" item_rules_list = frappe.get_all( "Party Specific Item", filters={ "party": ["!=", party], - "party_type": "Customer" if filters.get("customer") else "Supplier", + "party_type": party_type, }, fields=["restrict_based_on", "based_on_value"], ) + party_group_rules_list = frappe.get_all( + "Party Specific Item", + filters={"party_type": group}, + fields=["party as party_group", "restrict_based_on", "based_on_value"], + ) + current_party_group = frappe.get_value(party_type, party, frappe.scrub(group)) + for rule in party_group_rules_list: + if current_party_group != rule.party_group: + item_rules_list.append(rule) + filters_dict = {} for rule in item_rules_list: if rule["restrict_based_on"] == "Item": @@ -484,6 +497,13 @@ def get_batches_from_stock_ledger_entries(searchfields, txt, filters, start=0, p .limit(page_len) ) + if not filters.get("is_inward"): + if filters.get("posting_date") and filters.get("posting_time"): + query = query.where( + stock_ledger_entry.posting_datetime + <= get_combine_datetime(filters.get("posting_date"), filters.get("posting_time")) + ) + if not filters.get("include_expired_batches"): query = query.where((batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull())) @@ -537,6 +557,13 @@ def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start=0 .limit(page_len) ) + if not filters.get("is_inward"): + if filters.get("posting_date") and filters.get("posting_time"): + bundle_query = bundle_query.where( + stock_ledger_entry.posting_datetime + <= get_combine_datetime(filters.get("posting_date"), filters.get("posting_time")) + ) + if not filters.get("include_expired_batches"): bundle_query = bundle_query.where( (batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull()) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 5a062e19daf..7209c54fb12 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -579,6 +579,7 @@ class SellingController(StockController): or ( get_valuation_method(d.item_code, self.company) == "Moving Average" and self.get("is_return") + and not is_standalone ) ): d.incoming_rate = get_incoming_rate( diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 95c25c497bf..a87a1bf4e99 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -281,10 +281,10 @@ class StatusUpdater(Document): # get unique transactions to update for d in self.get_all_children(): - if hasattr(d, "qty") and d.qty < 0 and not self.get("is_return"): + if hasattr(d, "qty") and flt(d.qty) < 0 and not self.get("is_return"): frappe.throw(_("For an item {0}, quantity must be positive number").format(d.item_code)) - if hasattr(d, "qty") and d.qty > 0 and self.get("is_return"): + if hasattr(d, "qty") and flt(d.qty) > 0 and self.get("is_return"): frappe.throw(_("For an item {0}, quantity must be negative number").format(d.item_code)) if ( diff --git a/erpnext/controllers/subcontracting_inward_controller.py b/erpnext/controllers/subcontracting_inward_controller.py index 6428ca10822..490f7204d2a 100644 --- a/erpnext/controllers/subcontracting_inward_controller.py +++ b/erpnext/controllers/subcontracting_inward_controller.py @@ -651,20 +651,25 @@ class SubcontractingInwardController: ).update_manufacturing_qty_fields() elif self.purpose in ["Subcontracting Delivery", "Subcontracting Return"]: fieldname = "delivered_qty" if self.purpose == "Subcontracting Delivery" else "returned_qty" + qty_map = defaultdict(lambda: defaultdict(float)) for item in self.items: doctype = ( "Subcontracting Inward Order Item" if not item.type and not item.is_legacy_scrap_item else "Subcontracting Inward Order Secondary Item" ) - frappe.db.set_value( - doctype, - item.scio_detail, - fieldname, - frappe.get_value(doctype, item.scio_detail, fieldname) - + (item.transfer_qty if self._action == "submit" else -item.transfer_qty), + qty_map[doctype][item.scio_detail] += ( + item.transfer_qty if self._action == "submit" else -item.transfer_qty ) + for doctype, item_qty_map in qty_map.items(): + table = frappe.qb.DocType(doctype) + field = table[fieldname] + doc_updates = { + scio_detail: {fieldname: field + qty} for scio_detail, qty in item_qty_map.items() + } + frappe.db.bulk_update(doctype, doc_updates, chunk_size=len(doc_updates)) + def update_inward_order_received_items(self): if self.subcontracting_inward_order: match self.purpose: @@ -679,14 +684,18 @@ class SubcontractingInwardController: else -item.transfer_qty for item in self.items } - case_expr = Case() table = frappe.qb.DocType("Subcontracting Inward Order Received Item") - for scio_rm_name, qty in scio_rm_names.items(): - case_expr = case_expr.when(table.name == scio_rm_name, table.returned_qty + qty) - - frappe.qb.update(table).set(table.returned_qty, case_expr).where( - (table.name.isin(list(scio_rm_names.keys()))) & (table.docstatus == 1) - ).run() + doc_updates = { + scio_rm_name: {"returned_qty": table.returned_qty + qty} + for scio_rm_name, qty in scio_rm_names.items() + } + if doc_updates: + frappe.db.bulk_update( + "Subcontracting Inward Order Received Item", + doc_updates, + chunk_size=len(doc_updates), + update_modified=False, + ) def update_inward_order_received_items_for_raw_materials_receipt(self): data = frappe._dict() @@ -737,9 +746,7 @@ class SubcontractingInwardController: fields=["rate", "name", "required_qty", "received_qty"], ) - deleted_docs = [] - table = frappe.qb.DocType("Subcontracting Inward Order Received Item") - case_expr_qty, case_expr_rate = Case(), Case() + doc_updates = {} for d in result: current_qty = flt(data[d.name].transfer_qty) * (1 if self._action == "submit" else -1) current_rate = flt(data[d.name].rate) @@ -754,16 +761,17 @@ class SubcontractingInwardController: ) if not d.required_qty and not d.received_qty: - deleted_docs.append(d.name) frappe.delete_doc("Subcontracting Inward Order Received Item", d.name) else: - case_expr_qty = case_expr_qty.when(table.name == d.name, d.received_qty) - case_expr_rate = case_expr_rate.when(table.name == d.name, d.rate) + doc_updates[d.name] = {"received_qty": d.received_qty, "rate": d.rate} - if final_list := list(set(data.keys()) - set(deleted_docs)): - frappe.qb.update(table).set(table.received_qty, case_expr_qty).set( - table.rate, case_expr_rate - ).where((table.name.isin(final_list)) & (table.docstatus == 1)).run() + if doc_updates: + frappe.db.bulk_update( + "Subcontracting Inward Order Received Item", + doc_updates, + chunk_size=len(doc_updates), + update_modified=False, + ) def update_inward_order_received_items_for_manufacture(self): customer_warehouse = frappe.get_cached_value( @@ -815,8 +823,8 @@ class SubcontractingInwardController: ) if data := data.run(as_dict=True): - deleted_docs, used_item_wh = [], [] - case_expr = Case() + used_item_wh = [] + doc_updates = {} for d in data: if not d.warehouse: d.warehouse = next( @@ -828,15 +836,17 @@ class SubcontractingInwardController: qty = d.consumed_qty + item_code_wh[(d.rm_item_code, d.warehouse)] if qty or d.is_customer_provided_item or not d.is_additional_item: - case_expr = case_expr.when((table.name == d.name), qty) + doc_updates[d.name] = {"consumed_qty": qty} else: - deleted_docs.append(d.name) frappe.delete_doc("Subcontracting Inward Order Received Item", d.name) - if final_list := list(set([d.name for d in data]) - set(deleted_docs)): - frappe.qb.update(table).set(table.consumed_qty, case_expr).where( - (table.name.isin(final_list)) & (table.docstatus == 1) - ).run() + if doc_updates: + frappe.db.bulk_update( + "Subcontracting Inward Order Received Item", + doc_updates, + chunk_size=len(doc_updates), + update_modified=False, + ) main_item_code = next(fg for fg in self.items if fg.is_finished_item).item_code for extra_item in [ @@ -908,27 +918,25 @@ class SubcontractingInwardController: for d in result } ) - deleted_docs = [] - case_expr = Case() - table = frappe.qb.DocType("Subcontracting Inward Order Secondary Item") + doc_updates = {} 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 Secondary Item", value.name) else: - case_expr = case_expr.when( - table.name == value.name, value.produced_qty + secondary_items.get(key) - ) + doc_updates[value.name] = { + "produced_qty": value.produced_qty + secondary_items.get(key) + } - if final_list := list( - 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() + if doc_updates: + frappe.db.bulk_update( + "Subcontracting Inward Order Secondary Item", + doc_updates, + chunk_size=len(doc_updates), + update_modified=False, + ) fg_item_code = next(fg for fg in self.items if fg.is_finished_item).item_code for secondary_item in [ diff --git a/erpnext/controllers/tests/test_item_variant.py b/erpnext/controllers/tests/test_item_variant.py index e5c9d05d4db..45c5e5c6ab9 100644 --- a/erpnext/controllers/tests/test_item_variant.py +++ b/erpnext/controllers/tests/test_item_variant.py @@ -3,7 +3,11 @@ import unittest import frappe -from erpnext.controllers.item_variant import copy_attributes_to_variant, make_variant_item_code +from erpnext.controllers.item_variant import ( + copy_attributes_to_variant, + generate_keyed_value_combinations, + make_variant_item_code, +) from erpnext.stock.doctype.item.test_item import set_item_variant_settings from erpnext.stock.doctype.quality_inspection.test_quality_inspection import ( create_quality_inspection_parameter, @@ -18,6 +22,19 @@ class TestItemVariant(ERPNextTestSuite): variant = make_item_variant() self.assertEqual(variant.get("quality_inspection_template"), "_Test QC Template") + def test_generate_keyed_value_combinations_ignores_empty_attributes(self): + combinations = generate_keyed_value_combinations( + {"Test Colour": ["Red", "Blue"], "Test Size": ["Small", "Large"], "Test Fit": []} + ) + + self.assertEqual(len(combinations), 4) + self.assertNotIn("Test Fit", combinations[0]) + + single_attribute_combinations = generate_keyed_value_combinations( + {"Test Colour": ["Red", "Blue"], "Test Size": []} + ) + self.assertEqual(single_attribute_combinations, [{"Test Colour": "Red"}, {"Test Colour": "Blue"}]) + def create_variant_with_tables(item, args): if isinstance(args, str): diff --git a/erpnext/erpnext_integrations/custom/contact.json b/erpnext/erpnext_integrations/custom/contact.json deleted file mode 100644 index 98a4bbc795b..00000000000 --- a/erpnext/erpnext_integrations/custom/contact.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "custom_fields": [ - { - "_assign": null, - "_comments": null, - "_liked_by": null, - "_user_tags": null, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "creation": "2019-12-02 11:00:03.432994", - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "dt": "Contact", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "is_billing_contact", - "fieldtype": "Check", - "hidden": 0, - "idx": 27, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "insert_after": "is_primary_contact", - "label": "Is Billing Contact", - "length": 0, - "modified": "2019-12-02 11:00:03.432994", - "modified_by": "Administrator", - "name": "Contact-is_billing_contact", - "no_copy": 0, - "options": null, - "owner": "Administrator", - "parent": null, - "parentfield": null, - "parenttype": null, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - } - ], - "custom_perms": [], - "doctype": "Contact", - "property_setters": [], - "sync_on_migrate": 1 -} \ No newline at end of file diff --git a/erpnext/locale/main.pot b/erpnext/locale/main.pot index 209ae9ed859..f0e2a2336ca 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-05-17 10:04+0000\n" -"PO-Revision-Date: 2026-05-17 10:04+0000\n" +"POT-Creation-Date: 2026-05-24 10:10+0000\n" +"PO-Revision-Date: 2026-05-24 10:10+0000\n" "Last-Translator: hello@frappe.io\n" "Language-Team: hello@frappe.io\n" "MIME-Version: 1.0\n" @@ -267,7 +267,7 @@ msgstr "" msgid "% of materials delivered against this Sales Order" msgstr "" -#: erpnext/controllers/accounts_controller.py:2386 +#: erpnext/controllers/accounts_controller.py:2387 msgid "'Account' in the Accounting section of Customer {0}" msgstr "" @@ -283,7 +283,7 @@ msgstr "" msgid "'Days Since Last Order' must be greater than or equal to zero" msgstr "" -#: erpnext/controllers/accounts_controller.py:2391 +#: erpnext/controllers/accounts_controller.py:2392 msgid "'Default {0} Account' in Company {1}" msgstr "" @@ -512,8 +512,8 @@ msgstr "" msgid "11-50" msgstr "" -#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:95 -#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:101 +#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:107 +#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:113 msgid "1{0}" msgstr "" @@ -602,8 +602,8 @@ msgstr "" msgid "90 Above" msgstr "" -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1338 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1339 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1261 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1262 msgid "<0" msgstr "" @@ -778,7 +778,7 @@ msgstr "" msgid "
  • Clearance date must be after cheque date for row(s): {0}
  • " msgstr "" -#: erpnext/controllers/accounts_controller.py:2269 +#: erpnext/controllers/accounts_controller.py:2270 msgid "
  • Item {0} in row(s) {1} billed more than {2}
  • " msgstr "" @@ -795,7 +795,7 @@ msgstr "" msgid "
  • {}
  • " msgstr "" -#: erpnext/controllers/accounts_controller.py:2266 +#: erpnext/controllers/accounts_controller.py:2267 msgid "

    Cannot overbill for the following Items:

    " msgstr "" @@ -840,7 +840,7 @@ msgstr "" msgid "

    Price List Rate has not been set as editable in Selling Settings. In this scenario, setting Update Price List Based On to Price List Rate will prevent auto-updation of Item Price.

    Are you sure you want to continue?" msgstr "" -#: erpnext/controllers/accounts_controller.py:2278 +#: erpnext/controllers/accounts_controller.py:2279 msgid "

    To allow over-billing, please set allowance in Accounts Settings.

    " msgstr "" @@ -1160,7 +1160,7 @@ msgstr "" msgid "Abbreviation: {0} must appear only once" msgstr "" -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1335 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1258 msgid "Above" msgstr "" @@ -1357,7 +1357,7 @@ msgid "Account Manager" msgstr "" #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1008 -#: erpnext/controllers/accounts_controller.py:2395 +#: erpnext/controllers/accounts_controller.py:2396 msgid "Account Missing" msgstr "" @@ -1578,7 +1578,7 @@ msgstr "" msgid "Account {0} is frozen" msgstr "" -#: erpnext/controllers/accounts_controller.py:1470 +#: erpnext/controllers/accounts_controller.py:1471 msgid "Account {0} is invalid. Account Currency must be {1}" msgstr "" @@ -1602,7 +1602,7 @@ msgstr "" msgid "Account {0}: You can not assign itself as parent account" msgstr "" -#: erpnext/accounts/general_ledger.py:465 +#: erpnext/accounts/general_ledger.py:467 msgid "Account: {0} is capital Work in progress and can not be updated by Journal Entry" msgstr "" @@ -1614,7 +1614,7 @@ msgstr "" msgid "Account: {0} is not permitted under Payment Entry" msgstr "" -#: erpnext/controllers/accounts_controller.py:3284 +#: erpnext/controllers/accounts_controller.py:3285 msgid "Account: {0} with currency: {1} can not be selected" msgstr "" @@ -1934,7 +1934,7 @@ msgstr "" msgid "Accounting Entry for {0}" msgstr "" -#: erpnext/controllers/accounts_controller.py:2436 +#: erpnext/controllers/accounts_controller.py:2437 msgid "Accounting Entry for {0}: {1} can only be made in currency: {2}" msgstr "" @@ -2007,7 +2007,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:394 +#: erpnext/setup/install.py:426 msgid "Accounts" msgstr "" @@ -2311,7 +2311,7 @@ msgstr "" #. Label of the enable_serial_and_batch_no_for_item (Check) field in DocType #. 'Stock Settings' -#: erpnext/stock/doctype/item/item.js:419 +#: erpnext/stock/doctype/item/item.js:412 #: erpnext/stock/doctype/stock_settings/stock_settings.json msgid "Activate Serial / Batch No for Item" msgstr "" @@ -2435,7 +2435,7 @@ msgstr "" msgid "Actual End Date (via Timesheet)" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:229 +#: erpnext/manufacturing/doctype/work_order/work_order.py:230 msgid "Actual End Date cannot be before Actual Start Date" msgstr "" @@ -2566,7 +2566,7 @@ msgstr "" msgid "Ad-hoc Qty" msgstr "" -#: erpnext/stock/doctype/item/item.js:688 +#: erpnext/stock/doctype/item/item.js:675 #: erpnext/stock/doctype/price_list/price_list.js:8 msgid "Add / Edit Prices" msgstr "" @@ -3037,7 +3037,7 @@ msgstr "" msgid "Additional Information updated successfully." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:836 +#: erpnext/manufacturing/doctype/work_order/work_order.js:818 msgid "Additional Material Transfer" msgstr "" @@ -3060,7 +3060,7 @@ msgstr "" msgid "Additional Transferred Qty" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:710 +#: erpnext/manufacturing/doctype/work_order/work_order.py:711 msgid "" "Additional Transferred Qty {0}\n" "\t\t\t\t\tcannot be greater than {1}.\n" @@ -3219,11 +3219,6 @@ msgstr "" msgid "Address used to determine Tax Category in transactions" msgstr "" -#. Label of the adjust_qty (Float) field in DocType 'Sales Forecast Item' -#: erpnext/manufacturing/doctype/sales_forecast_item/sales_forecast_item.json -msgid "Adjust Qty" -msgstr "" - #: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1160 msgid "Adjustment Against" msgstr "" @@ -3305,7 +3300,7 @@ msgstr "" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.json #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json -#: erpnext/controllers/accounts_controller.py:286 +#: erpnext/controllers/accounts_controller.py:287 #: erpnext/setup/doctype/company/company.json msgid "Advance Payments" msgstr "" @@ -3564,11 +3559,11 @@ msgstr "" #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html:154 #: erpnext/accounts/report/accounts_payable/accounts_payable.html:138 #: erpnext/accounts/report/accounts_receivable/accounts_receivable.html:139 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1276 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1199 msgid "Age (Days)" msgstr "" -#: erpnext/stock/report/stock_ageing/stock_ageing.py:226 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:214 msgid "Age ({0})" msgstr "" @@ -3864,7 +3859,7 @@ msgstr "" msgid "All the items have been already returned." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1274 +#: 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 "" @@ -4044,7 +4039,7 @@ msgstr "" msgid "Allow Internal Transfers at Arm's Length Price" msgstr "" -#: erpnext/controllers/selling_controller.py:858 +#: erpnext/controllers/selling_controller.py:859 msgid "Allow Item to Be Added Multiple Times in a Transaction" msgstr "" @@ -4429,8 +4424,8 @@ msgid "Also you can't switch back to FIFO after setting the valuation method to msgstr "" #: 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/manufacturing/doctype/work_order/work_order.js:146 +#: erpnext/manufacturing/doctype/work_order/work_order.js:161 #: erpnext/public/js/utils.js:587 #: erpnext/stock/doctype/stock_entry/stock_entry.js:322 msgid "Alternate Item" @@ -4572,7 +4567,7 @@ msgstr "" #: erpnext/accounts/doctype/bank_guarantee/bank_guarantee.json #: erpnext/accounts/doctype/budget_distribution/budget_distribution.json #: erpnext/accounts/doctype/cashier_closing_payments/cashier_closing_payments.json -#: erpnext/accounts/doctype/journal_entry/journal_entry.js:623 +#: erpnext/accounts/doctype/journal_entry/journal_entry.js:627 #: erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json #: erpnext/accounts/doctype/payment_entry/payment_entry.json #: erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json @@ -4602,7 +4597,6 @@ msgstr "" #: erpnext/accounts/doctype/share_balance/share_balance.json #: erpnext/accounts/doctype/share_transfer/share_transfer.json #: erpnext/accounts/print_format/payment_receipt_voucher/payment_receipt_voucher.html:10 -#: 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 @@ -4664,7 +4658,7 @@ msgstr "" msgid "Amount" msgstr "" -#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:22 +#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:34 msgid "Amount (AED)" msgstr "" @@ -4757,10 +4751,6 @@ msgstr "" msgid "Amount in Account Currency" msgstr "" -#: erpnext/accounts/print_format/sales_invoice_print/sales_invoice_print.html:119 -msgid "Amount in Words" -msgstr "" - #. Description of the 'Outstanding Amount' (Currency) field in DocType 'Payment #. Request' #: erpnext/accounts/doctype/payment_request/payment_request.json @@ -4782,11 +4772,11 @@ msgid "Amount to Bill" msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:1259 -msgid "Amount {0} {1} against {2} {3}" +msgid "Amount {0} {1} adjusted against {2} {3}" msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:1270 -msgid "Amount {0} {1} deducted against {2}" +msgid "Amount {0} {1} as adjustment to {2}" msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:1234 @@ -5343,7 +5333,7 @@ msgstr "" msgid "As there are sufficient Sub Assembly Items, Work Order is not required for Warehouse {0}." msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1834 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1836 msgid "As there are sufficient raw materials, Material Request is not required for Warehouse {0}." msgstr "" @@ -5927,7 +5917,7 @@ msgstr "" msgid "Assets {assets_link} created for {item_code}" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:240 +#: erpnext/manufacturing/doctype/job_card/job_card.js:709 msgid "Assign Job to Employee" msgstr "" @@ -6432,7 +6422,7 @@ msgstr "" #: 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:175 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:163 msgid "Available Qty" msgstr "" @@ -6533,8 +6523,8 @@ msgstr "" msgid "Available-for-use Date should be after purchase date" msgstr "" -#: erpnext/stock/report/stock_ageing/stock_ageing.py:176 -#: erpnext/stock/report/stock_ageing/stock_ageing.py:210 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:164 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:198 #: erpnext/stock/report/stock_balance/stock_balance.py:590 msgid "Average Age" msgstr "" @@ -6643,7 +6633,7 @@ msgstr "" #: erpnext/manufacturing/doctype/bom/bom.json #: erpnext/manufacturing/doctype/bom/bom_tree.js:8 #: erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json -#: erpnext/manufacturing/doctype/work_order/work_order.js:216 +#: erpnext/manufacturing/doctype/work_order/work_order.js:197 #: 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 @@ -6667,7 +6657,7 @@ msgstr "" msgid "BOM 1" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1802 +#: erpnext/manufacturing/doctype/bom/bom.py:1823 msgid "BOM 1 {0} and BOM 2 {1} should not be same" msgstr "" @@ -6968,7 +6958,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:386 +#: erpnext/manufacturing/doctype/work_order/work_order.js:367 #: erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json msgid "Backflush Materials From WIP Warehouse" msgstr "" @@ -7758,7 +7748,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:368 +#: erpnext/manufacturing/doctype/work_order/work_order.js:349 #: erpnext/manufacturing/doctype/work_order/work_order.json #: erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json msgid "Batch Size" @@ -7775,7 +7765,7 @@ msgstr "" msgid "Batch and Serial No" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:937 +#: erpnext/manufacturing/doctype/work_order/work_order.py:938 msgid "Batch not created for item {} since it does not have a batch series." msgstr "" @@ -7846,7 +7836,7 @@ msgstr "" #. Label of the bill_date (Date) field in DocType 'Journal Entry' #. Label of the bill_date (Date) field in DocType 'Subcontracting Receipt' #: erpnext/accounts/doctype/journal_entry/journal_entry.json -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1261 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1184 #: erpnext/accounts/report/purchase_register/purchase_register.py:214 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json msgid "Bill Date" @@ -7855,7 +7845,7 @@ msgstr "" #. Label of the bill_no (Data) field in DocType 'Journal Entry' #. Label of the bill_no (Data) field in DocType 'Subcontracting Receipt' #: erpnext/accounts/doctype/journal_entry/journal_entry.json -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1260 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1183 #: erpnext/accounts/report/purchase_register/purchase_register.py:213 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json msgid "Bill No" @@ -7974,7 +7964,7 @@ msgstr "" msgid "Billing Address Name" msgstr "" -#: erpnext/controllers/accounts_controller.py:573 +#: erpnext/controllers/accounts_controller.py:574 msgid "Billing Address does not belong to the {0}" msgstr "" @@ -8318,7 +8308,7 @@ msgstr "" msgid "Booked Fixed Asset" msgstr "" -#: erpnext/accounts/general_ledger.py:829 +#: erpnext/accounts/general_ledger.py:831 msgid "Books have been closed till the period ending on {0}" msgstr "" @@ -9036,7 +9026,7 @@ msgstr "" msgid "Can be approved by {0}" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:2575 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2576 msgid "Can not close Work Order. Since {0} Job Cards are in Work In Progress state." msgstr "" @@ -9070,7 +9060,7 @@ msgid "Can only make payment against unbilled {0}" msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1499 -#: erpnext/controllers/accounts_controller.py:3193 +#: erpnext/controllers/accounts_controller.py:3194 #: 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 "" @@ -9175,7 +9165,7 @@ msgstr "" msgid "Cannot cancel as processing of cancelled documents is pending." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:1115 +#: erpnext/manufacturing/doctype/work_order/work_order.py:1116 msgid "Cannot cancel because submitted Stock Entry {0} exists" msgstr "" @@ -9203,7 +9193,7 @@ msgstr "" msgid "Cannot change Attributes after stock transaction. Make a new Item and transfer stock to the new Item" msgstr "" -#: erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py:73 +#: erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py:74 msgid "Cannot change Reference Document Type." msgstr "" @@ -9243,7 +9233,7 @@ msgstr "" msgid "Cannot create Stock Reservation Entries for future dated Purchase Receipts." msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.py:1913 +#: erpnext/selling/doctype/sales_order/sales_order.py:1914 #: 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 "" @@ -9277,12 +9267,12 @@ msgstr "" msgid "Cannot delete Serial No {0}, as it is used in stock transactions" msgstr "" -#: erpnext/controllers/accounts_controller.py:3797 +#: erpnext/controllers/accounts_controller.py:3799 msgid "Cannot delete an item which has been ordered" msgstr "" #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:197 -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:782 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:783 msgid "Cannot delete protected core DocType: {0}" msgstr "" @@ -9302,7 +9292,7 @@ msgstr "" msgid "Cannot disable {0} as it may lead to incorrect stock valuation." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:727 +#: erpnext/manufacturing/doctype/work_order/work_order.py:728 msgid "Cannot disassemble more than produced quantity." msgstr "" @@ -9331,7 +9321,7 @@ msgstr "" msgid "Cannot find Item with this Barcode" msgstr "" -#: erpnext/controllers/accounts_controller.py:3749 +#: erpnext/controllers/accounts_controller.py:3751 msgid "Cannot find a default warehouse for item {0}. Please set one in the Item Master or in Stock Settings." msgstr "" @@ -9339,15 +9329,15 @@ msgstr "" msgid "Cannot merge {0} '{1}' into '{2}' as both have existing accounting entries in different currencies for company '{3}'." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:577 +#: erpnext/manufacturing/doctype/work_order/work_order.py:578 msgid "Cannot produce more Item {0} than Sales Order quantity {1} {2}" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:1472 +#: erpnext/manufacturing/doctype/work_order/work_order.py:1473 msgid "Cannot produce more item for {0}" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:1476 +#: erpnext/manufacturing/doctype/work_order/work_order.py:1477 msgid "Cannot produce more than {0} items for {1}" msgstr "" @@ -9355,12 +9345,12 @@ msgstr "" msgid "Cannot receive from customer against negative outstanding" msgstr "" -#: erpnext/controllers/accounts_controller.py:3945 +#: erpnext/controllers/accounts_controller.py:3947 msgid "Cannot reduce quantity than ordered or purchased quantity" msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1512 -#: erpnext/controllers/accounts_controller.py:3208 +#: erpnext/controllers/accounts_controller.py:3209 #: 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 "" @@ -9380,7 +9370,7 @@ msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1505 #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1683 #: erpnext/accounts/doctype/payment_entry/payment_entry.py:1827 -#: erpnext/controllers/accounts_controller.py:3198 +#: erpnext/controllers/accounts_controller.py:3199 #: erpnext/public/js/controllers/accounts.js:112 #: erpnext/public/js/controllers/taxes_and_totals.js:550 msgid "Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row" @@ -9398,11 +9388,11 @@ msgstr "" msgid "Cannot set multiple Item Defaults for a company." msgstr "" -#: erpnext/controllers/accounts_controller.py:3911 +#: erpnext/controllers/accounts_controller.py:3913 msgid "Cannot set quantity less than delivered quantity." msgstr "" -#: erpnext/controllers/accounts_controller.py:3912 +#: erpnext/controllers/accounts_controller.py:3914 msgid "Cannot set quantity less than received quantity." msgstr "" @@ -9414,7 +9404,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:3939 +#: erpnext/controllers/accounts_controller.py:3941 msgid "Cannot update rate as item {0} is already ordered or purchased against this quotation" msgstr "" @@ -9447,7 +9437,7 @@ msgstr "" msgid "Capacity Planning" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:1101 +#: erpnext/manufacturing/doctype/work_order/work_order.py:1102 msgid "Capacity Planning Error, planned start time can not be same as end time" msgstr "" @@ -9812,7 +9802,7 @@ msgstr "" msgid "Changes in {0}" msgstr "" -#: erpnext/stock/doctype/item/item.js:385 +#: erpnext/stock/doctype/item/item.js:378 msgid "Changing Customer Group for the selected Customer is not allowed." msgstr "" @@ -9827,7 +9817,7 @@ msgid "Channel Partner" msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2256 -#: erpnext/controllers/accounts_controller.py:3261 +#: erpnext/controllers/accounts_controller.py:3262 msgid "Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount" msgstr "" @@ -10241,7 +10231,7 @@ msgstr "" msgid "Closed Documents" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:2498 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2499 msgid "Closed Work Order can not be stopped or Re-opened" msgstr "" @@ -10488,7 +10478,7 @@ msgstr "" msgid "Communication Medium Type" msgstr "" -#: erpnext/setup/install.py:107 +#: erpnext/setup/install.py:108 msgid "Compact Item Print" msgstr "" @@ -10679,7 +10669,7 @@ msgstr "" #: erpnext/accounts/doctype/gl_entry/gl_entry.json #: erpnext/accounts/doctype/invoice_discounting/invoice_discounting.json #: erpnext/accounts/doctype/item_tax_template/item_tax_template.json -#: erpnext/accounts/doctype/journal_entry/journal_entry.js:157 +#: erpnext/accounts/doctype/journal_entry/journal_entry.js:161 #: erpnext/accounts/doctype/journal_entry/journal_entry.json #: erpnext/accounts/doctype/journal_entry_template/journal_entry_template.json #: erpnext/accounts/doctype/ledger_health_monitor_company/ledger_health_monitor_company.json @@ -10883,8 +10873,8 @@ msgstr "" #: erpnext/setup/doctype/employee/employee_tree.js:8 #: erpnext/setup/doctype/employee_external_work_history/employee_external_work_history.json #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json -#: erpnext/setup/doctype/vehicle/vehicle.json erpnext/setup/install.py:166 -#: erpnext/setup/install.py:175 erpnext/setup/workspace/home/home.json +#: erpnext/setup/doctype/vehicle/vehicle.json erpnext/setup/install.py:198 +#: erpnext/setup/install.py:207 erpnext/setup/workspace/home/home.json #: erpnext/stock/dashboard_chart_source/stock_value_by_item_group/stock_value_by_item_group.js:8 #: erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.js:8 #: erpnext/stock/doctype/bin/bin.json @@ -11026,11 +11016,11 @@ msgstr "" msgid "Company Address Name" msgstr "" -#: erpnext/controllers/accounts_controller.py:4377 +#: erpnext/controllers/accounts_controller.py:4379 msgid "Company Address is missing. You don't have permission to create an Address. Please contact your System Manager." msgstr "" -#: erpnext/controllers/accounts_controller.py:4365 +#: erpnext/controllers/accounts_controller.py:4367 msgid "Company Address is missing. You don't have permission to update it. Please contact your System Manager." msgstr "" @@ -11200,7 +11190,7 @@ msgstr "" msgid "Company {0} does not exist" msgstr "" -#: erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py:83 +#: erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py:105 msgid "Company {0} is added more than once" msgstr "" @@ -11242,7 +11232,7 @@ msgstr "" msgid "Competitors" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:277 +#: erpnext/manufacturing/doctype/job_card/job_card.js:661 #: erpnext/manufacturing/doctype/workstation/workstation.js:151 msgid "Complete Job" msgstr "" @@ -11285,12 +11275,12 @@ msgstr "" msgid "Completed Qty" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:1390 +#: erpnext/manufacturing/doctype/work_order/work_order.py:1391 msgid "Completed Qty cannot be greater than 'Qty to Manufacture'" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:325 -#: erpnext/manufacturing/doctype/job_card/job_card.js:446 +#: erpnext/manufacturing/doctype/job_card/job_card.js:259 +#: erpnext/manufacturing/doctype/job_card/job_card.js:393 #: erpnext/manufacturing/doctype/workstation/workstation.js:296 msgid "Completed Quantity" msgstr "" @@ -11448,7 +11438,7 @@ msgstr "" msgid "Consider Minimum Order Qty" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1096 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1078 msgid "Consider Process Loss" msgstr "" @@ -11645,7 +11635,7 @@ msgstr "" msgid "Consumed Qty" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:1766 +#: erpnext/manufacturing/doctype/work_order/work_order.py:1767 msgid "Consumed Qty cannot be greater than Reserved Qty for item {0}" msgstr "" @@ -11802,7 +11792,7 @@ msgstr "" msgid "Contact Person" msgstr "" -#: erpnext/controllers/accounts_controller.py:585 +#: erpnext/controllers/accounts_controller.py:586 msgid "Contact Person does not belong to the {0}" msgstr "" @@ -11811,10 +11801,6 @@ msgstr "" msgid "Contact:" msgstr "" -#: erpnext/accounts/print_format/sales_invoice_print/sales_invoice_print.html:55 -msgid "Contact: " -msgstr "" - #. Option for the 'Entry Type' (Select) field in DocType 'Journal Entry' #. Option for the 'Journal Entry Type' (Select) field in DocType 'Journal Entry #. Template' @@ -11996,15 +11982,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:2976 +#: erpnext/controllers/accounts_controller.py:2977 msgid "Conversion rate cannot be 0" msgstr "" -#: erpnext/controllers/accounts_controller.py:2983 +#: erpnext/controllers/accounts_controller.py:2984 msgid "Conversion rate is 1.00, but document currency is different from company currency" msgstr "" -#: erpnext/controllers/accounts_controller.py:2979 +#: erpnext/controllers/accounts_controller.py:2980 msgid "Conversion rate must be 1.00 if document currency is same as company currency" msgstr "" @@ -12076,13 +12062,13 @@ msgstr "" msgid "Corrective Action" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:503 +#: erpnext/manufacturing/doctype/job_card/job_card.js:447 msgid "Corrective Job Card" msgstr "" #. Label of the corrective_operation_section (Tab Break) field in DocType 'Job #. Card' -#: erpnext/manufacturing/doctype/job_card/job_card.js:510 +#: erpnext/manufacturing/doctype/job_card/job_card.js:456 #: erpnext/manufacturing/doctype/job_card/job_card.json msgid "Corrective Operation" msgstr "" @@ -12243,7 +12229,7 @@ msgstr "" #: erpnext/accounts/report/accounts_payable/accounts_payable.js:28 #: erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js:47 #: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:30 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1246 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1169 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js:47 #: erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.js:42 #: erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py:204 @@ -12674,7 +12660,7 @@ msgstr "" msgid "Create Grouped Asset" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.js:119 +#: erpnext/accounts/doctype/journal_entry/journal_entry.js:123 msgid "Create Inter Company Journal Entry" msgstr "" @@ -12690,7 +12676,7 @@ msgstr "" msgid "Create Item" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:206 +#: erpnext/manufacturing/doctype/work_order/work_order.js:187 msgid "Create Job Card" msgstr "" @@ -12785,7 +12771,7 @@ msgstr "" msgid "Create Payment Request" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:818 +#: erpnext/manufacturing/doctype/work_order/work_order.js:800 msgid "Create Pick List" msgstr "" @@ -12968,12 +12954,12 @@ msgstr "" msgid "Create Users" msgstr "" -#: erpnext/stock/doctype/item/item.js:984 +#: erpnext/stock/doctype/item/item.js:971 msgid "Create Variant" msgstr "" -#: erpnext/stock/doctype/item/item.js:798 -#: erpnext/stock/doctype/item/item.js:842 +#: erpnext/stock/doctype/item/item.js:785 +#: erpnext/stock/doctype/item/item.js:829 msgid "Create Variants" msgstr "" @@ -12992,12 +12978,12 @@ msgstr "" msgid "Create Workstation" msgstr "" -#: erpnext/stock/doctype/item/item.js:825 -#: erpnext/stock/doctype/item/item.js:977 +#: erpnext/stock/doctype/item/item.js:812 +#: erpnext/stock/doctype/item/item.js:964 msgid "Create a variant with the template image." msgstr "" -#: erpnext/stock/stock_ledger.py:2071 +#: erpnext/stock/stock_ledger.py:2037 msgid "Create an incoming stock transaction for the Item." msgstr "" @@ -13064,7 +13050,7 @@ msgstr "" msgid "Creating Delivery Schedule..." msgstr "" -#: erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py:140 +#: erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py:162 msgid "Creating Dimensions..." msgstr "" @@ -13172,7 +13158,7 @@ msgstr "" msgid "Credit ({0})" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.js:637 +#: erpnext/accounts/doctype/journal_entry/journal_entry.js:641 msgid "Credit Account" msgstr "" @@ -13296,7 +13282,7 @@ msgstr "" #: erpnext/accounts/doctype/journal_entry/journal_entry.json #: erpnext/accounts/doctype/journal_entry_template/journal_entry_template.json #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html:176 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1270 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1193 #: erpnext/controllers/sales_and_purchase_return.py:453 #: erpnext/setup/setup_wizard/operations/install_fixtures.py:303 #: erpnext/stock/doctype/delivery_note/delivery_note.js:89 @@ -13332,7 +13318,7 @@ msgstr "" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:376 #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:384 -#: erpnext/controllers/accounts_controller.py:2375 +#: erpnext/controllers/accounts_controller.py:2376 msgid "Credit To" msgstr "" @@ -13534,7 +13520,7 @@ msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:1604 #: erpnext/accounts/doctype/payment_entry/payment_entry.py:1672 -#: erpnext/accounts/utils.py:2533 +#: erpnext/accounts/utils.py:2532 msgid "Currency for {0} must be {1}" msgstr "" @@ -13989,7 +13975,7 @@ msgstr "" #. Label of the customer_contact_display (Small Text) field in DocType #. 'Purchase Order' #. Label of the customer_contact (Small Text) field in DocType 'Delivery Stop' -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1240 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1163 #: erpnext/buying/doctype/purchase_order/purchase_order.json #: erpnext/stock/doctype/delivery_stop/delivery_stop.json msgid "Customer Contact" @@ -14094,7 +14080,7 @@ msgstr "" #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/accounts/doctype/tax_rule/tax_rule.json #: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:115 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1298 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1221 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js:96 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:185 #: erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js:56 @@ -14154,7 +14140,7 @@ msgstr "" msgid "Customer Items" msgstr "" -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1289 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1212 msgid "Customer LPO" msgstr "" @@ -14206,7 +14192,7 @@ msgstr "" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.json #: erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1230 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1153 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:156 #: erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js:92 #: erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py:35 @@ -14748,7 +14734,7 @@ msgstr "" msgid "Debit / Credit Note Posting Date" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.js:627 +#: erpnext/accounts/doctype/journal_entry/journal_entry.js:631 msgid "Debit Account" msgstr "" @@ -14790,7 +14776,7 @@ msgstr "" #: erpnext/accounts/doctype/journal_entry/journal_entry.json #: erpnext/accounts/doctype/journal_entry_template/journal_entry_template.json #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html:178 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1273 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1196 #: erpnext/controllers/sales_and_purchase_return.py:457 #: erpnext/setup/setup_wizard/operations/install_fixtures.py:304 #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:45 @@ -14820,7 +14806,7 @@ msgstr "" #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1012 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1023 -#: erpnext/controllers/accounts_controller.py:2375 +#: erpnext/controllers/accounts_controller.py:2376 msgid "Debit To" msgstr "" @@ -14828,7 +14814,7 @@ msgstr "" msgid "Debit To is required" msgstr "" -#: erpnext/accounts/general_ledger.py:536 +#: erpnext/accounts/general_ledger.py:538 msgid "Debit and Credit not equal for {0} #{1}. Difference is {2}." msgstr "" @@ -14994,15 +14980,15 @@ msgstr "" msgid "Default BOM ({0}) must be active for this item or its template" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:2266 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2267 msgid "Default BOM for {0} not found" msgstr "" -#: erpnext/controllers/accounts_controller.py:3983 +#: erpnext/controllers/accounts_controller.py:3985 msgid "Default BOM not found for FG Item {0}" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:2263 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2264 msgid "Default BOM not found for Item {0} and Project {1}" msgstr "" @@ -15598,8 +15584,8 @@ msgstr "" msgid "Deleting {0} and all associated Common Code documents..." msgstr "" -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1099 -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1118 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1100 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1119 msgid "Deletion in Progress!" msgstr "" @@ -15859,7 +15845,7 @@ msgstr "" msgid "Delivery Note {0} is not submitted" msgstr "" -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1293 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1216 #: erpnext/stock/doctype/delivery_trip/delivery_trip.js:75 msgid "Delivery Notes" msgstr "" @@ -15945,10 +15931,6 @@ msgstr "" msgid "Delivery to" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.py:455 -msgid "Delivery warehouse required for stock item {0}" -msgstr "" - #. Label of the sales_orders_and_material_requests_tab (Tab Break) field in #. DocType 'Master Production Schedule' #: erpnext/manufacturing/doctype/master_production_schedule/master_production_schedule.json @@ -16151,7 +16133,7 @@ msgstr "" msgid "Depreciation Posting Date" msgstr "" -#: erpnext/assets/doctype/asset/asset.js:916 +#: erpnext/assets/doctype/asset/asset.js:918 msgid "Depreciation Posting Date cannot be before Available-for-use Date" msgstr "" @@ -16508,11 +16490,11 @@ msgstr "" msgid "Disabled items cannot be selected in any transaction." msgstr "" -#: erpnext/controllers/accounts_controller.py:903 +#: erpnext/controllers/accounts_controller.py:904 msgid "Disabled pricing rules since this {} is an internal transfer" msgstr "" -#: erpnext/controllers/accounts_controller.py:917 +#: erpnext/controllers/accounts_controller.py:918 msgid "Disabled tax included prices since this {} is an internal transfer" msgstr "" @@ -16528,7 +16510,7 @@ 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:1074 +#: 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 @@ -16536,7 +16518,7 @@ msgstr "" msgid "Disassemble" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:232 +#: erpnext/manufacturing/doctype/work_order/work_order.js:213 msgid "Disassemble Order" msgstr "" @@ -16544,7 +16526,7 @@ msgstr "" msgid "Disassemble Qty cannot be less than or equal to 0." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:463 +#: erpnext/manufacturing/doctype/work_order/work_order.js:445 msgid "Disassemble Qty cannot be less than or equal to 0." msgstr "" @@ -17083,7 +17065,7 @@ msgstr "" msgid "Do not update variants on save" msgstr "" -#: erpnext/assets/doctype/asset/asset.js:954 +#: erpnext/assets/doctype/asset/asset.js:956 msgid "Do you really want to restore this scrapped asset?" msgstr "" @@ -17107,7 +17089,7 @@ msgstr "" msgid "Do you want to submit the material request" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:111 +#: erpnext/manufacturing/doctype/job_card/job_card.js:108 msgid "Do you want to submit the stock entry?" msgstr "" @@ -17174,11 +17156,11 @@ msgstr "" msgid "Document Type " msgstr "" -#: erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py:65 +#: erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py:66 msgid "Document Type already used as a dimension" msgstr "" -#: erpnext/setup/install.py:198 +#: erpnext/setup/install.py:230 msgid "Documentation" msgstr "" @@ -17337,12 +17319,6 @@ msgstr "" msgid "Driving License Category" msgstr "" -#. Label of the drop_ar_procedures (Button) field in DocType 'Accounts -#. Settings' -#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json -msgid "Drop Procedures" -msgstr "" - #. Label of the drop_ship (Section Break) field in DocType 'POS Invoice Item' #. Label of the drop_ship (Section Break) field in DocType 'Sales Invoice Item' #. Label of the drop_ship (Tab Break) field in DocType 'Purchase Order' @@ -17355,12 +17331,6 @@ msgstr "" msgid "Drop Ship" msgstr "" -#. Description of the 'Drop Procedures' (Button) field in DocType 'Accounts -#. Settings' -#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json -msgid "Drops existing SQL Procedures and Function setup by Accounts Receivable report" -msgstr "" - #: erpnext/accounts/party.py:690 msgid "Due Date cannot be after {0}" msgstr "" @@ -17603,7 +17573,7 @@ msgstr "" msgid "Each Transaction" msgstr "" -#: erpnext/stock/report/stock_ageing/stock_ageing.py:182 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:170 msgid "Earliest" msgstr "" @@ -17713,6 +17683,10 @@ msgstr "" msgid "Either target qty or target amount is mandatory." msgstr "" +#: erpnext/manufacturing/doctype/job_card/job_card.js:675 +msgid "Elapsed Time" +msgstr "" + #. Option for the 'Fuel Type' (Select) field in DocType 'Vehicle' #: erpnext/setup/doctype/vehicle/vehicle.json msgid "Electric" @@ -18021,7 +17995,7 @@ msgstr "" msgid "Employee {0} does not belong to the company {1}" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:375 +#: erpnext/manufacturing/doctype/job_card/job_card.py:376 msgid "Employee {0} is currently working on another workstation. Please assign another employee." msgstr "" @@ -18037,7 +18011,7 @@ msgstr "" msgid "Empty" msgstr "" -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:754 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:755 msgid "Empty To Delete List" msgstr "" @@ -18361,8 +18335,8 @@ msgstr "" #. Label of the end_time (Time) field in DocType 'Stock Reposting Settings' #. Label of the end_time (Time) field in DocType 'Service Day' #. Label of the end_time (Datetime) field in DocType 'Call Log' -#: erpnext/manufacturing/doctype/job_card/job_card.js:383 -#: erpnext/manufacturing/doctype/job_card/job_card.js:453 +#: erpnext/manufacturing/doctype/job_card/job_card.js:332 +#: erpnext/manufacturing/doctype/job_card/job_card.js:400 #: 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 @@ -18451,8 +18425,8 @@ msgstr "" msgid "Enter Serial Nos" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:410 -#: erpnext/manufacturing/doctype/job_card/job_card.js:479 +#: erpnext/manufacturing/doctype/job_card/job_card.js:361 +#: erpnext/manufacturing/doctype/job_card/job_card.js:423 #: erpnext/manufacturing/doctype/workstation/workstation.js:312 msgid "Enter Value" msgstr "" @@ -18477,7 +18451,7 @@ msgstr "" msgid "Enter amount to be redeemed." msgstr "" -#: erpnext/stock/doctype/item/item.js:1146 +#: erpnext/stock/doctype/item/item.js:1133 msgid "Enter an Item Code, the name will be auto-filled the same as Item Code on clicking inside the Item Name field." msgstr "" @@ -18489,7 +18463,7 @@ msgstr "" msgid "Enter customer's phone number" msgstr "" -#: erpnext/assets/doctype/asset/asset.js:925 +#: erpnext/assets/doctype/asset/asset.js:927 msgid "Enter date to scrap asset" msgstr "" @@ -18529,7 +18503,7 @@ msgstr "" msgid "Enter the name of the bank or lending institution before submitting." msgstr "" -#: erpnext/stock/doctype/item/item.js:1172 +#: erpnext/stock/doctype/item/item.js:1159 msgid "Enter the opening stock units." msgstr "" @@ -18537,7 +18511,7 @@ msgstr "" 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:1236 +#: 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 "" @@ -18701,7 +18675,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:2334 +#: erpnext/stock/stock_ledger.py:2300 msgid "Example: Serial No {0} reserved in {1}." msgstr "" @@ -18719,7 +18693,7 @@ msgstr "" msgid "Excess Materials Consumed" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1132 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1140 msgid "Excess Transfer" msgstr "" @@ -18759,8 +18733,8 @@ msgstr "" msgid "Exchange Gain/Loss" msgstr "" -#: erpnext/controllers/accounts_controller.py:1776 -#: erpnext/controllers/accounts_controller.py:1861 +#: erpnext/controllers/accounts_controller.py:1777 +#: erpnext/controllers/accounts_controller.py:1862 msgid "Exchange Gain/Loss amount has been booked through {0}" msgstr "" @@ -18896,7 +18870,7 @@ msgstr "" msgid "Executive Search" msgstr "" -#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:67 +#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:79 msgid "Exempt Supplies" msgstr "" @@ -19236,7 +19210,7 @@ msgstr "" msgid "Extra Consumed Qty" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:262 +#: erpnext/manufacturing/doctype/job_card/job_card.py:263 msgid "Extra Job Card Quantity" msgstr "" @@ -19504,21 +19478,29 @@ msgstr "" msgid "Field in Bank Transaction" msgstr "" +#: erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py:95 +msgid "Fieldname Conflict" +msgstr "" + +#: erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py:87 +msgid "Fieldname {0} already exists in the following doctypes: {1}. A separate dimension field will not be added to these doctypes. GL Entries will use the value of the existing field as the dimension value." +msgstr "" + #. Description of the 'Do not update variants on save' (Check) field in DocType #. 'Item Variant Settings' #: erpnext/stock/doctype/item_variant_settings/item_variant_settings.json msgid "Fields will be copied over only at time of creation." msgstr "" -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1066 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1067 msgid "File does not belong to this Transaction Deletion Record" msgstr "" -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1060 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1061 msgid "File not found" msgstr "" -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1074 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1075 msgid "File not found on server" msgstr "" @@ -19721,9 +19703,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:902 -#: erpnext/manufacturing/doctype/work_order/work_order.js:917 -#: erpnext/manufacturing/doctype/work_order/work_order.js:926 +#: 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 "" @@ -19780,15 +19762,15 @@ msgstr "" msgid "Finished Good Item Quantity" msgstr "" -#: erpnext/controllers/accounts_controller.py:3969 +#: erpnext/controllers/accounts_controller.py:3971 msgid "Finished Good Item is not specified for service item {0}" msgstr "" -#: erpnext/controllers/accounts_controller.py:3986 +#: erpnext/controllers/accounts_controller.py:3988 msgid "Finished Good Item {0} Qty can not be zero" msgstr "" -#: erpnext/controllers/accounts_controller.py:3980 +#: erpnext/controllers/accounts_controller.py:3982 msgid "Finished Good Item {0} must be a sub-contracted item" msgstr "" @@ -20194,7 +20176,7 @@ msgid "For Job Card" msgstr "" #. Label of the for_operation (Link) field in DocType 'Job Card' -#: erpnext/manufacturing/doctype/job_card/job_card.js:523 +#: erpnext/manufacturing/doctype/job_card/job_card.js:465 #: erpnext/manufacturing/doctype/job_card/job_card.json msgid "For Operation" msgstr "" @@ -20225,7 +20207,7 @@ msgstr "" msgid "For Raw Materials" msgstr "" -#: erpnext/controllers/accounts_controller.py:1441 +#: erpnext/controllers/accounts_controller.py:1442 msgid "For Return Invoices with Stock effect, '0' qty Items are not allowed. Following rows are affected: {0}" msgstr "" @@ -20301,7 +20283,7 @@ msgstr "" msgid "For operation {0} at row {1}, please add raw materials or set a BOM against it." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:2645 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2646 msgid "For operation {0}: Quantity ({1}) can not be greater than pending quantity({2})" msgstr "" @@ -20332,7 +20314,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:1721 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1723 msgid "For row {0}: Enter Planned Qty" msgstr "" @@ -20382,11 +20364,6 @@ msgstr "" msgid "Forecast Demand" msgstr "" -#. Label of the forecast_qty (Float) field in DocType 'Sales Forecast Item' -#: erpnext/manufacturing/doctype/sales_forecast_item/sales_forecast_item.json -msgid "Forecast Qty" -msgstr "" - #. Label of a Workspace Sidebar Item #: erpnext/workspace_sidebar/manufacturing.json msgid "Forecasting" @@ -20432,7 +20409,7 @@ msgstr "" msgid "Forum URL" msgstr "" -#: erpnext/setup/install.py:210 +#: erpnext/setup/install.py:242 msgid "Frappe School" msgstr "" @@ -20930,13 +20907,13 @@ msgid "Further nodes can be only created under 'Group' type nodes" msgstr "" #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html:188 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1285 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1208 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:177 msgid "Future Payment Amount" msgstr "" #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html:187 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1284 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1207 msgid "Future Payment Ref" msgstr "" @@ -22086,7 +22063,7 @@ msgstr "" msgid "Here are the error logs for the aforementioned failed depreciation entries: {0}" msgstr "" -#: erpnext/stock/stock_ledger.py:2056 +#: erpnext/stock/stock_ledger.py:2022 msgid "Here are the options to proceed:" msgstr "" @@ -22687,7 +22664,7 @@ msgstr "" msgid "If no taxes are set, and Taxes and Charges Template is selected, the system will automatically apply the taxes from the chosen template." msgstr "" -#: erpnext/stock/stock_ledger.py:2066 +#: erpnext/stock/stock_ledger.py:2032 msgid "If not, you can Cancel / Submit this entry" msgstr "" @@ -22715,7 +22692,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:1269 +#: 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 "" @@ -22724,7 +22701,7 @@ msgstr "" msgid "If the account is frozen, entries are allowed to restricted users." msgstr "" -#: erpnext/stock/stock_ledger.py:2059 +#: erpnext/stock/stock_ledger.py:2025 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 "" @@ -22734,7 +22711,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:1288 +#: 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 "" @@ -22811,7 +22788,7 @@ msgstr "" msgid "If yes, then this warehouse will be used to store rejected materials" msgstr "" -#: erpnext/stock/doctype/item/item.js:1158 +#: erpnext/stock/doctype/item/item.js:1145 msgid "If you are maintaining stock of this Item in your Inventory, ERPNext will make a stock ledger entry for each transaction of this item." msgstr "" @@ -22825,7 +22802,7 @@ msgstr "" msgid "If you still want to proceed, please disable 'Skip Available Sub Assembly Items' checkbox." msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1839 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1841 msgid "If you still want to proceed, please enable {0}." msgstr "" @@ -22905,7 +22882,7 @@ msgstr "" msgid "Ignore Existing Ordered Qty" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1831 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1833 msgid "Ignore Existing Projected Quantity" msgstr "" @@ -23248,7 +23225,7 @@ msgstr "" msgid "In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent" msgstr "" -#: erpnext/stock/doctype/item/item.js:1191 +#: erpnext/stock/doctype/item/item.js:1178 msgid "In this section, you can define Company-wide transaction-related defaults for this Item. Eg. Default Warehouse, Default Price List, Supplier, etc." msgstr "" @@ -23791,7 +23768,7 @@ msgid "Inspected By" msgstr "" #: erpnext/controllers/stock_controller.py:1500 -#: erpnext/manufacturing/doctype/job_card/job_card.py:832 +#: erpnext/manufacturing/doctype/job_card/job_card.py:833 msgid "Inspection Rejected" msgstr "" @@ -23815,7 +23792,7 @@ msgid "Inspection Required before Purchase" msgstr "" #: erpnext/controllers/stock_controller.py:1485 -#: erpnext/manufacturing/doctype/job_card/job_card.py:813 +#: erpnext/manufacturing/doctype/job_card/job_card.py:814 msgid "Inspection Submission" msgstr "" @@ -23884,11 +23861,11 @@ msgstr "" msgid "Insufficient Capacity" msgstr "" -#: erpnext/controllers/accounts_controller.py:3863 -#: erpnext/controllers/accounts_controller.py:3887 -#: erpnext/controllers/accounts_controller.py:4407 -#: erpnext/controllers/accounts_controller.py:4413 -#: erpnext/controllers/accounts_controller.py:4435 +#: erpnext/controllers/accounts_controller.py:3865 +#: erpnext/controllers/accounts_controller.py:3889 +#: erpnext/controllers/accounts_controller.py:4409 +#: erpnext/controllers/accounts_controller.py:4415 +#: erpnext/controllers/accounts_controller.py:4437 msgid "Insufficient Permissions" msgstr "" @@ -23897,12 +23874,12 @@ msgstr "" #: erpnext/stock/doctype/pick_list/pick_list.py:163 #: erpnext/stock/doctype/pick_list/pick_list.py:1090 #: erpnext/stock/doctype/stock_entry/stock_entry.py:1050 -#: erpnext/stock/serial_batch_bundle.py:1225 erpnext/stock/stock_ledger.py:1747 -#: erpnext/stock/stock_ledger.py:2225 +#: erpnext/stock/serial_batch_bundle.py:1225 erpnext/stock/stock_ledger.py:1713 +#: erpnext/stock/stock_ledger.py:2191 msgid "Insufficient Stock" msgstr "" -#: erpnext/stock/stock_ledger.py:2240 +#: erpnext/stock/stock_ledger.py:2206 msgid "Insufficient Stock for Batch" msgstr "" @@ -24065,7 +24042,7 @@ msgstr "" msgid "Internal Purchase Order" msgstr "" -#: erpnext/controllers/accounts_controller.py:803 +#: erpnext/controllers/accounts_controller.py:804 msgid "Internal Sale or Delivery Reference missing." msgstr "" @@ -24073,7 +24050,7 @@ msgstr "" msgid "Internal Sales Order" msgstr "" -#: erpnext/controllers/accounts_controller.py:805 +#: erpnext/controllers/accounts_controller.py:806 msgid "Internal Sales Reference Missing" msgstr "" @@ -24104,7 +24081,7 @@ msgstr "" msgid "Internal Transfer" msgstr "" -#: erpnext/controllers/accounts_controller.py:814 +#: erpnext/controllers/accounts_controller.py:815 msgid "Internal Transfer Reference Missing" msgstr "" @@ -24137,8 +24114,8 @@ msgstr "" #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1028 #: erpnext/assets/doctype/asset_category/asset_category.py:69 #: erpnext/assets/doctype/asset_category/asset_category.py:97 -#: erpnext/controllers/accounts_controller.py:3222 -#: erpnext/controllers/accounts_controller.py:3230 +#: erpnext/controllers/accounts_controller.py:3223 +#: erpnext/controllers/accounts_controller.py:3231 msgid "Invalid Account" msgstr "" @@ -24159,7 +24136,7 @@ msgstr "" msgid "Invalid Attribute" msgstr "" -#: erpnext/controllers/accounts_controller.py:625 +#: erpnext/controllers/accounts_controller.py:626 msgid "Invalid Auto Repeat Date" msgstr "" @@ -24189,7 +24166,7 @@ msgstr "" #: erpnext/assets/doctype/asset/asset.py:361 #: erpnext/assets/doctype/asset/asset.py:368 -#: erpnext/controllers/accounts_controller.py:3245 +#: erpnext/controllers/accounts_controller.py:3246 msgid "Invalid Cost Center" msgstr "" @@ -24245,7 +24222,7 @@ msgid "Invalid Net Purchase Amount" msgstr "" #: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:77 -#: erpnext/accounts/general_ledger.py:821 +#: erpnext/accounts/general_ledger.py:823 msgid "Invalid Opening Entry" msgstr "" @@ -24287,12 +24264,12 @@ msgstr "" msgid "Invalid Purchase Invoice" msgstr "" -#: erpnext/controllers/accounts_controller.py:3907 -#: erpnext/controllers/accounts_controller.py:3921 +#: erpnext/controllers/accounts_controller.py:3909 +#: erpnext/controllers/accounts_controller.py:3923 msgid "Invalid Qty" msgstr "" -#: erpnext/controllers/accounts_controller.py:1459 +#: erpnext/controllers/accounts_controller.py:1460 msgid "Invalid Quantity" msgstr "" @@ -24347,7 +24324,7 @@ msgstr "" msgid "Invalid condition expression" msgstr "" -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1055 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1056 msgid "Invalid file URL" msgstr "" @@ -24381,8 +24358,8 @@ msgstr "" #: erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py:109 #: erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py:119 -#: erpnext/accounts/general_ledger.py:864 -#: erpnext/accounts/general_ledger.py:874 +#: erpnext/accounts/general_ledger.py:866 +#: erpnext/accounts/general_ledger.py:876 msgid "Invalid value {0} for {1} against account {2}" msgstr "" @@ -24400,7 +24377,7 @@ msgid "Invalid {0}: {1}" msgstr "" #. Label of the inventory_section (Tab Break) field in DocType 'Item' -#: erpnext/setup/install.py:384 erpnext/stock/doctype/item/item.json +#: erpnext/setup/install.py:416 erpnext/stock/doctype/item/item.json msgid "Inventory" msgstr "" @@ -24484,7 +24461,6 @@ msgstr "" #. Label of the invoice_date (Date) field in DocType 'Payment Reconciliation #. Invoice' #: erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json -#: erpnext/accounts/print_format/sales_invoice_print/sales_invoice_print.html:68 msgid "Invoice Date" msgstr "" @@ -24501,14 +24477,10 @@ msgstr "" msgid "Invoice Document Type Selection Error" msgstr "" -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1265 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1188 msgid "Invoice Grand Total" msgstr "" -#: erpnext/accounts/print_format/sales_invoice_print/sales_invoice_print.html:64 -msgid "Invoice ID" -msgstr "" - #. Label of the invoice_limit (Int) field in DocType 'Payment Reconciliation' #: erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json msgid "Invoice Limit" @@ -24605,7 +24577,7 @@ msgstr "" #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html:171 #: erpnext/accounts/report/accounts_payable/accounts_payable.html:139 #: erpnext/accounts/report/accounts_receivable/accounts_receivable.html:140 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1267 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1190 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:164 #: erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py:194 msgid "Invoiced Amount" @@ -24721,8 +24693,7 @@ msgstr "" msgid "Is Billable" msgstr "" -#. Label of the is_billing_contact (Check) field in DocType 'Contact' -#: erpnext/erpnext_integrations/custom/contact.json +#: erpnext/setup/install.py:170 msgid "Is Billing Contact" msgstr "" @@ -25162,8 +25133,7 @@ msgstr "" msgid "Is Transporter" msgstr "" -#. Label of the is_your_company_address (Check) field in DocType 'Address' -#: erpnext/accounts/custom/address.json +#: erpnext/setup/install.py:161 msgid "Is Your Company Address" msgstr "" @@ -25269,7 +25239,7 @@ msgstr "" #. Description of the 'Is Rate Adjustment Entry (Debit Note)' (Check) field in #. DocType 'Sales Invoice' #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json -msgid "Issue a debit note with 0 qty against an existing Sales Invoice" +msgid "Issue a debit note against an existing Sales Invoice to adjust the rate. The quantity will be retained from the original invoice." msgstr "" #. Option for the 'Current State' (Select) field in DocType 'Share Balance' @@ -25732,7 +25702,7 @@ 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:138 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:126 #: 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 @@ -25931,7 +25901,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:147 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:135 #: 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 @@ -26194,7 +26164,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:144 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:132 #: 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:293 @@ -26234,10 +26204,6 @@ msgstr "" msgid "Item Price" msgstr "" -#: erpnext/stock/get_item_details.py:1132 -msgid "Item Price Added for {0} in Price List {1}" -msgstr "" - #. Label of the item_price_settings_section (Section Break) field in DocType #. 'Accounts Settings' #: erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -26253,8 +26219,9 @@ msgstr "" msgid "Item Price Stock" msgstr "" +#: erpnext/stock/get_item_details.py:1132 #: erpnext/stock/get_item_details.py:1156 -msgid "Item Price added for {0} in Price List {1}" +msgid "Item Price added for {0} in Price List - {1}" msgstr "" #: erpnext/stock/doctype/item_price/item_price.py:140 @@ -26452,7 +26419,7 @@ msgstr "" msgid "Item Variant Settings" msgstr "" -#: erpnext/stock/doctype/item/item.js:1007 +#: erpnext/stock/doctype/item/item.js:994 msgid "Item Variant {0} already exists with same attributes" msgstr "" @@ -26587,7 +26554,7 @@ msgstr "" msgid "Item operation" msgstr "" -#: erpnext/controllers/accounts_controller.py:3961 +#: erpnext/controllers/accounts_controller.py:3963 msgid "Item qty can not be updated as raw materials are already processed." msgstr "" @@ -26643,7 +26610,7 @@ msgstr "" msgid "Item {0} does not exist." msgstr "" -#: erpnext/controllers/selling_controller.py:855 +#: erpnext/controllers/selling_controller.py:856 msgid "Item {0} entered multiple times." msgstr "" @@ -26739,7 +26706,7 @@ msgstr "" msgid "Item {0}: {1} qty produced. " msgstr "" -#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:1452 +#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:1374 msgid "Item {} does not exist." msgstr "" @@ -26809,7 +26776,7 @@ msgstr "" msgid "Items Filter" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1683 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1685 #: erpnext/selling/doctype/sales_order/sales_order.js:1719 msgid "Items Required" msgstr "" @@ -26833,11 +26800,11 @@ msgstr "" msgid "Items and Pricing" msgstr "" -#: erpnext/controllers/accounts_controller.py:4221 +#: erpnext/controllers/accounts_controller.py:4223 msgid "Items cannot be updated as Subcontracting Inward Order(s) exist against this Subcontracted Sales Order." msgstr "" -#: erpnext/controllers/accounts_controller.py:4214 +#: erpnext/controllers/accounts_controller.py:4216 msgid "Items cannot be updated as Subcontracting Order is created against the Purchase Order {0}." msgstr "" @@ -26859,7 +26826,7 @@ msgstr "" msgid "Items to Be Repost" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1682 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1684 msgid "Items to Manufacture are required to pull the Raw Materials associated with it." msgstr "" @@ -26924,9 +26891,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:997 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1003 #: erpnext/manufacturing/doctype/operation/operation.json -#: erpnext/manufacturing/doctype/work_order/work_order.js:415 +#: erpnext/manufacturing/doctype/work_order/work_order.js:396 #: 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 @@ -26988,7 +26955,7 @@ msgstr "" msgid "Job Card and Capacity Planning" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1473 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1490 msgid "Job Card {0} has been completed" msgstr "" @@ -27064,7 +27031,7 @@ msgstr "" msgid "Job Worker Warehouse" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:2700 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2701 msgid "Job card {0} created" msgstr "" @@ -27280,7 +27247,7 @@ msgstr "" msgid "Kilowatt-Hour" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:999 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1005 msgid "Kindly cancel the Manufacturing Entries first against the work order {0}." msgstr "" @@ -27482,7 +27449,7 @@ msgstr "" msgid "Last transacted" msgstr "" -#: erpnext/stock/report/stock_ageing/stock_ageing.py:183 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:171 msgid "Latest" msgstr "" @@ -27738,7 +27705,7 @@ msgstr "" msgid "Legal Expenses" msgstr "" -#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:19 +#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:31 msgid "Legend" msgstr "" @@ -28497,10 +28464,10 @@ msgid "Major/Optional Subjects" msgstr "" #. Label of the make (Data) field in DocType 'Vehicle' -#: erpnext/accounts/doctype/journal_entry/journal_entry.js:123 -#: erpnext/manufacturing/doctype/job_card/job_card.js:544 -#: erpnext/manufacturing/doctype/work_order/work_order.js:857 -#: erpnext/manufacturing/doctype/work_order/work_order.js:891 +#: erpnext/accounts/doctype/journal_entry/journal_entry.js:127 +#: erpnext/manufacturing/doctype/job_card/job_card.js:480 +#: 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 "" @@ -28520,7 +28487,7 @@ msgstr "" msgid "Make Difference Entry" msgstr "" -#: erpnext/stock/doctype/item/item.js:696 +#: erpnext/stock/doctype/item/item.js:683 msgid "Make Lead Time" msgstr "" @@ -28558,12 +28525,12 @@ msgstr "" msgid "Make Serial No / Batch from Work Order" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:109 +#: erpnext/manufacturing/doctype/job_card/job_card.js:106 #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:256 msgid "Make Stock Entry" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:418 +#: erpnext/manufacturing/doctype/job_card/job_card.js:369 msgid "Make Subcontracting PO" msgstr "" @@ -28579,11 +28546,11 @@ msgstr "" msgid "Make project from a template." msgstr "" -#: erpnext/stock/doctype/item/item.js:804 +#: erpnext/stock/doctype/item/item.js:791 msgid "Make {0} Variant" msgstr "" -#: erpnext/stock/doctype/item/item.js:806 +#: erpnext/stock/doctype/item/item.js:793 msgid "Make {0} Variants" msgstr "" @@ -28831,7 +28798,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:389 +#: erpnext/setup/doctype/company/company.json erpnext/setup/install.py:421 #: 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 @@ -29098,7 +29065,7 @@ msgstr "" msgid "Material" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:882 +#: erpnext/manufacturing/doctype/work_order/work_order.js:864 msgid "Material Consumption" msgstr "" @@ -29187,7 +29154,7 @@ msgstr "" #: 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:168 +#: erpnext/manufacturing/doctype/job_card/job_card.js:214 #: 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 @@ -29288,7 +29255,7 @@ msgstr "" msgid "Material Request already created for the ordered quantity" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.py:1859 +#: erpnext/selling/doctype/sales_order/sales_order.py:1860 msgid "Material Request not created, as quantity for Raw Materials already available." msgstr "" @@ -29356,7 +29323,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:182 +#: erpnext/manufacturing/doctype/job_card/job_card.js:225 #: 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 @@ -29427,8 +29394,8 @@ msgstr "" msgid "Materials are already received against the {0} {1}" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:183 -#: erpnext/manufacturing/doctype/job_card/job_card.py:853 +#: erpnext/manufacturing/doctype/job_card/job_card.py:184 +#: erpnext/manufacturing/doctype/job_card/job_card.py:854 msgid "Materials needs to be transferred to the work in progress warehouse for the job card {0}" msgstr "" @@ -29497,9 +29464,9 @@ msgstr "" msgid "Max discount allowed for item: {0} is {1}%" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1058 -#: erpnext/manufacturing/doctype/work_order/work_order.js:1065 -#: erpnext/manufacturing/doctype/work_order/work_order.js:1088 +#: 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:203 #: erpnext/stock/doctype/stock_entry/stock_entry.js:382 msgid "Max: {0}" @@ -29527,11 +29494,11 @@ msgstr "" msgid "Maximum Producible Items" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:4108 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:4113 msgid "Maximum Samples - {0} can be retained for Batch {1} and Item {2}." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:4099 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:4104 msgid "Maximum Samples - {0} have already been retained for Batch {1} and Item {2} in Batch {3}." msgstr "" @@ -29592,7 +29559,7 @@ msgstr "" msgid "Megawatt" msgstr "" -#: erpnext/stock/stock_ledger.py:2072 +#: erpnext/stock/stock_ledger.py:2038 msgid "Mention Valuation Rate in the Item master." msgstr "" @@ -29680,7 +29647,7 @@ msgstr "" msgid "Messages greater than 160 characters will be split into multiple messages" msgstr "" -#: erpnext/setup/install.py:137 +#: erpnext/setup/install.py:138 msgid "Messaging CRM Campaign" msgstr "" @@ -29879,7 +29846,7 @@ msgstr "" msgid "Min Qty should be greater than Recurse Over Qty" msgstr "" -#: erpnext/stock/doctype/item/item.js:958 +#: erpnext/stock/doctype/item/item.js:945 msgid "Min Value: {0}, Max Value: {1}, in Increments of: {2}" msgstr "" @@ -29966,7 +29933,7 @@ msgstr "" msgid "Mismatch" msgstr "" -#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:1453 +#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:1375 msgid "Missing" msgstr "" @@ -30041,7 +30008,7 @@ msgid "Missing required filter: {0}" msgstr "" #: erpnext/manufacturing/doctype/bom/bom.py:1219 -#: erpnext/manufacturing/doctype/work_order/work_order.py:1498 +#: erpnext/manufacturing/doctype/work_order/work_order.py:1499 msgid "Missing value" msgstr "" @@ -30052,10 +30019,6 @@ msgstr "" msgid "Mixed Conditions" msgstr "" -#: erpnext/accounts/print_format/sales_invoice_print/sales_invoice_print.html:58 -msgid "Mobile: " -msgstr "" - #: erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py:216 #: erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py:248 #: erpnext/accounts/report/purchase_register/purchase_register.py:201 @@ -30308,7 +30271,7 @@ msgstr "" msgid "Multiple company fields available: {0}. Please select manually." msgstr "" -#: erpnext/controllers/accounts_controller.py:1305 +#: erpnext/controllers/accounts_controller.py:1306 msgid "Multiple fiscal years exist for the date {0}. Please set company in Fiscal Year" msgstr "" @@ -30321,7 +30284,7 @@ msgid "Music" msgstr "" #. Label of the must_be_whole_number (Check) field in DocType 'UOM' -#: erpnext/manufacturing/doctype/work_order/work_order.py:1445 +#: erpnext/manufacturing/doctype/work_order/work_order.py:1446 #: erpnext/setup/doctype/uom/uom.json #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:267 #: erpnext/utilities/transaction_base.py:629 @@ -30408,7 +30371,7 @@ msgstr "" msgid "Naming Series updated" msgstr "" -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:936 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:937 msgid "Naming series '{0}' for DocType '{1}' does not contain standard '.' or '{{' separator. Using fallback extraction." msgstr "" @@ -30767,7 +30730,7 @@ msgstr "" msgid "Net Weight UOM" msgstr "" -#: erpnext/controllers/accounts_controller.py:1665 +#: erpnext/controllers/accounts_controller.py:1666 msgid "Net total calculation precision loss" msgstr "" @@ -31020,7 +30983,7 @@ msgstr "" msgid "No Delivery Note selected for Customer {}" msgstr "" -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:753 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:754 msgid "No DocTypes in To Delete list. Please generate or import the list before submitting." msgstr "" @@ -31108,7 +31071,7 @@ msgstr "" msgid "No Tax withholding account set for Company {0} in Tax Withholding Category {1}." msgstr "" -#: erpnext/accounts/report/gross_profit/gross_profit.py:998 +#: erpnext/accounts/report/gross_profit/gross_profit.py:990 msgid "No Terms" msgstr "" @@ -31524,7 +31487,7 @@ msgstr "" msgid "Not allow to set alternative item for the item {0}" msgstr "" -#: erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py:59 +#: erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py:60 msgid "Not allowed to create accounting dimension for {0}" msgstr "" @@ -31578,7 +31541,7 @@ msgstr "" msgid "Note: Item {0} added multiple times" msgstr "" -#: erpnext/controllers/accounts_controller.py:711 +#: erpnext/controllers/accounts_controller.py:712 msgid "Note: Payment Entry will not be created since 'Cash or Bank Account' was not specified" msgstr "" @@ -31948,7 +31911,7 @@ msgstr "" msgid "Once set, this invoice will be on hold till the set date" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:769 +#: erpnext/manufacturing/doctype/work_order/work_order.js:751 msgid "Once the Work Order is Closed. It can't be resumed." msgstr "" @@ -31988,7 +31951,7 @@ msgstr "" msgid "Only CSV and Excel files can be used to for importing data. Please check the file format you are trying to upload" msgstr "" -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1069 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1070 msgid "Only CSV files are allowed" msgstr "" @@ -32277,7 +32240,7 @@ msgstr "" msgid "Opening Entry" msgstr "" -#: erpnext/accounts/general_ledger.py:820 +#: erpnext/accounts/general_ledger.py:822 msgid "Opening Entry can not be created after Period Closing Voucher is created." msgstr "" @@ -32374,7 +32337,10 @@ msgid "Opening and Closing" msgstr "" #. Label of the operating_component (Link) field in DocType 'Workstation Cost' +#. Label of the operating_component (Data) field in DocType 'Landed Cost Taxes +#. and Charges' #: erpnext/manufacturing/doctype/workstation_cost/workstation_cost.json +#: erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json msgid "Operating Component" msgstr "" @@ -32406,7 +32372,7 @@ msgstr "" msgid "Operating Cost Per BOM Quantity" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1722 +#: erpnext/manufacturing/doctype/bom/bom.py:1740 msgid "Operating Cost as per Work Order / BOM" msgstr "" @@ -32449,15 +32415,15 @@ msgstr "" #. Label of the operation_row_id (Int) field in DocType 'BOM Item' #. Label of the operation_id (Data) field in DocType 'Job Card' +#. Label of the operation_id (Data) field in DocType 'Landed Cost Taxes and +#. Charges' #: erpnext/manufacturing/doctype/bom_item/bom_item.json #: erpnext/manufacturing/doctype/job_card/job_card.json +#: erpnext/manufacturing/doctype/work_order/work_order.js:332 +#: erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json msgid "Operation ID" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:351 -msgid "Operation Id" -msgstr "" - #. Label of the operation_row_id (Int) field in DocType 'Job Card' #: erpnext/manufacturing/doctype/job_card/job_card.json msgid "Operation Row ID" @@ -32482,7 +32448,7 @@ msgstr "" msgid "Operation Time" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:1504 +#: erpnext/manufacturing/doctype/work_order/work_order.py:1505 msgid "Operation Time must be greater than 0 for Operation {0}" msgstr "" @@ -32497,11 +32463,11 @@ msgstr "" msgid "Operation time does not depend on quantity to produce" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:586 +#: erpnext/manufacturing/doctype/job_card/job_card.js:518 msgid "Operation {0} added multiple times in the work order {1}" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1246 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1254 msgid "Operation {0} does not belong to the work order {1}" msgstr "" @@ -32517,7 +32483,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:332 +#: erpnext/manufacturing/doctype/work_order/work_order.js:313 #: erpnext/manufacturing/doctype/work_order/work_order.json #: erpnext/setup/doctype/company/company.py:469 #: erpnext/setup/doctype/email_digest/email_digest.json @@ -32692,7 +32658,7 @@ msgstr "" msgid "Optimize Route" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1035 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1017 msgid "Optional. Select a specific manufacture entry to reverse." msgstr "" @@ -33062,7 +33028,7 @@ msgstr "" #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/accounts/report/accounts_payable/accounts_payable.html:140 #: erpnext/accounts/report/accounts_receivable/accounts_receivable.html:141 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1274 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1197 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:167 #: erpnext/accounts/report/purchase_register/purchase_register.py:289 #: erpnext/accounts/report/sales_register/sales_register.py:319 @@ -33152,7 +33118,7 @@ msgstr "" msgid "Overbilling of {0} {1} ignored for item {2} because you have {3} role." msgstr "" -#: erpnext/controllers/accounts_controller.py:2183 +#: erpnext/controllers/accounts_controller.py:2184 msgid "Overbilling of {} ignored because you have {} role." msgstr "" @@ -33697,7 +33663,7 @@ msgstr "" #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html:173 #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1268 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1191 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:165 #: erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py:201 #: erpnext/accounts/report/pos_register/pos_register.py:209 @@ -34152,7 +34118,7 @@ msgstr "" #: erpnext/accounts/report/accounts_payable/accounts_payable.js:105 #: erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js:82 #: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:65 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1204 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1127 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js:82 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:147 #: erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js:49 @@ -34179,7 +34145,7 @@ msgstr "" #. Name of a DocType #: erpnext/accounts/doctype/party_account/party_account.json -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1215 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1138 msgid "Party Account" msgstr "" @@ -34206,7 +34172,7 @@ msgstr "" msgid "Party Account No. (Bank Statement)" msgstr "" -#: erpnext/controllers/accounts_controller.py:2467 +#: erpnext/controllers/accounts_controller.py:2468 msgid "Party Account {0} currency ({1}) and document currency ({2}) should be same" msgstr "" @@ -34336,7 +34302,7 @@ msgstr "" #: erpnext/accounts/report/accounts_payable/accounts_payable.js:92 #: erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js:69 #: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:52 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1198 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1121 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js:69 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:141 #: erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js:42 @@ -34428,7 +34394,7 @@ msgstr "" msgid "Pause" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:263 +#: erpnext/manufacturing/doctype/job_card/job_card.js:660 msgid "Pause Job" msgstr "" @@ -34479,7 +34445,7 @@ msgid "Payable" msgstr "" #: erpnext/accounts/report/accounts_payable/accounts_payable.js:50 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1213 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1136 #: erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py:209 #: erpnext/accounts/report/purchase_register/purchase_register.py:194 #: erpnext/accounts/report/purchase_register/purchase_register.py:235 @@ -34642,7 +34608,7 @@ msgstr "" msgid "Payment Entry is already created" msgstr "" -#: erpnext/controllers/accounts_controller.py:1616 +#: erpnext/controllers/accounts_controller.py:1617 msgid "Payment Entry {0} is linked against Order {1}, check if it should be pulled as advance in this invoice." msgstr "" @@ -34922,7 +34888,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:2747 +#: erpnext/controllers/accounts_controller.py:2748 #: erpnext/selling/doctype/quotation/quotation.json #: erpnext/selling/doctype/sales_order/sales_order.json msgid "Payment Schedule" @@ -34936,10 +34902,6 @@ msgstr "" msgid "Payment Schedules" msgstr "" -#: erpnext/accounts/print_format/sales_invoice_print/sales_invoice_print.html:123 -msgid "Payment Status" -msgstr "" - #. Label of the payment_term (Link) field in DocType 'Overdue Payment' #. Label of the payment_term (Link) field in DocType 'Payment Entry Reference' #. Label of the payment_term (Link) field in DocType 'Payment Reference' @@ -34955,7 +34917,7 @@ msgstr "" #: erpnext/accounts/doctype/payment_schedule/payment_schedule.json #: erpnext/accounts/doctype/payment_term/payment_term.json #: erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.json -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1264 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1187 #: erpnext/accounts/report/gross_profit/gross_profit.py:449 #: erpnext/accounts/workspace/invoicing/invoicing.json #: erpnext/public/js/controllers/transaction.js:498 @@ -35201,10 +35163,14 @@ msgstr "" msgid "Pending Amount" msgstr "" +#. Label of the pending_qty (Float) field in DocType 'Job Card' #. Label of the pending_qty (Float) field in DocType 'Production Plan Item' +#. Label of the pending_qty (Float) field in DocType 'Work Order Operation' #: erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py:254 +#: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json -#: erpnext/manufacturing/doctype/work_order/work_order.js:356 +#: erpnext/manufacturing/doctype/work_order/work_order.js:337 +#: erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json #: erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py:182 #: erpnext/selling/doctype/sales_order/sales_order.js:1688 #: erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py:45 @@ -35213,9 +35179,18 @@ msgstr "" #: erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.py:55 #: erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py:45 +#: erpnext/manufacturing/doctype/job_card/job_card.js:273 msgid "Pending Quantity" msgstr "" +#: erpnext/manufacturing/doctype/job_card/job_card.js:70 +msgid "Pending Quantity cannot be greater than {0}" +msgstr "" + +#: erpnext/manufacturing/doctype/job_card/job_card.js:62 +msgid "Pending Quantity cannot be less than 0" +msgstr "" + #. Option for the 'Status' (Select) field in DocType 'Task' #. Option in a Select field in the tasks Web Form #: erpnext/projects/doctype/task/task.json @@ -35245,6 +35220,14 @@ msgstr "" msgid "Pending processing" msgstr "" +#: erpnext/manufacturing/doctype/job_card/job_card.py:1463 +msgid "Pending quantity cannot be greater than the for quantity." +msgstr "" + +#: erpnext/manufacturing/doctype/job_card/job_card.py:1457 +msgid "Pending quantity cannot be negative." +msgstr "" + #: erpnext/setup/setup_wizard/data/industry_type.txt:36 msgid "Pension Funds" msgstr "" @@ -35355,7 +35338,7 @@ msgstr "" msgid "Period Based On" msgstr "" -#: erpnext/accounts/general_ledger.py:832 +#: erpnext/accounts/general_ledger.py:834 msgid "Period Closed" msgstr "" @@ -35982,7 +35965,7 @@ msgstr "" msgid "Please add at least one naming series." msgstr "" -#: erpnext/public/js/utils/serial_no_batch_selector.js:660 +#: erpnext/public/js/utils/serial_no_batch_selector.js:662 msgid "Please add atleast one Serial No / Batch No" msgstr "" @@ -36069,6 +36052,10 @@ msgstr "" msgid "Please click on 'Generate Schedule' to get schedule" msgstr "" +#: erpnext/manufacturing/doctype/job_card/job_card.js:58 +msgid "Please complete the job first before entering Pending Quantity" +msgstr "" + #: erpnext/selling/doctype/customer/customer.py:634 msgid "Please contact any of the following users to extend the credit limits for {0}: {1}" msgstr "" @@ -36093,11 +36080,11 @@ msgstr "" msgid "Please create Landed Cost Vouchers against Invoices that have 'Update Stock' enabled." msgstr "" -#: erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py:74 +#: erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py:75 msgid "Please create a new Accounting Dimension if required." msgstr "" -#: erpnext/controllers/accounts_controller.py:804 +#: erpnext/controllers/accounts_controller.py:805 msgid "Please create purchase from internal sale or delivery document itself" msgstr "" @@ -36141,7 +36128,7 @@ msgstr "" msgid "Please enable {0} in the {1}." msgstr "" -#: erpnext/controllers/selling_controller.py:857 +#: erpnext/controllers/selling_controller.py:858 msgid "Please enable {} in {} to allow same item in multiple rows" msgstr "" @@ -36215,7 +36202,7 @@ msgstr "" msgid "Please enter Planned Qty for Item {0} at row {1}" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:73 +#: erpnext/manufacturing/doctype/work_order/work_order.js:44 msgid "Please enter Production Item first" msgstr "" @@ -36272,7 +36259,7 @@ msgstr "" msgid "Please enter company name first" msgstr "" -#: erpnext/controllers/accounts_controller.py:2973 +#: erpnext/controllers/accounts_controller.py:2974 msgid "Please enter default currency in Company Master" msgstr "" @@ -36372,12 +36359,12 @@ msgstr "" msgid "Please make sure you really want to delete all the transactions for this company. Your master data will remain as it is. This action cannot be undone." msgstr "" -#: erpnext/stock/doctype/item/item.js:709 +#: erpnext/stock/doctype/item/item.js:696 msgid "Please mention 'Weight UOM' along with Weight." msgstr "" -#: erpnext/accounts/general_ledger.py:666 -#: erpnext/accounts/general_ledger.py:673 +#: erpnext/accounts/general_ledger.py:668 +#: erpnext/accounts/general_ledger.py:675 msgid "Please mention '{0}' in Company: {1}" msgstr "" @@ -36453,7 +36440,7 @@ msgstr "" msgid "Please select Charge Type first" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.js:490 +#: erpnext/accounts/doctype/journal_entry/journal_entry.js:494 msgid "Please select Company" msgstr "" @@ -36462,7 +36449,7 @@ msgstr "" msgid "Please select Company and Posting Date to getting entries" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.js:738 +#: erpnext/accounts/doctype/journal_entry/journal_entry.js:742 #: erpnext/manufacturing/doctype/plant_floor/plant_floor.js:28 msgid "Please select Company first" msgstr "" @@ -36486,8 +36473,8 @@ msgstr "" msgid "Please select Finished Good Item for Service Item {0}" msgstr "" -#: erpnext/assets/doctype/asset/asset.js:751 -#: erpnext/assets/doctype/asset/asset.js:766 +#: erpnext/assets/doctype/asset/asset.js:753 +#: erpnext/assets/doctype/asset/asset.js:768 msgid "Please select Item Code first" msgstr "" @@ -36511,7 +36498,7 @@ msgstr "" msgid "Please select Posting Date before selecting Party" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.js:739 +#: erpnext/accounts/doctype/journal_entry/journal_entry.js:743 msgid "Please select Posting Date first" msgstr "" @@ -36543,7 +36530,7 @@ msgstr "" msgid "Please select Subcontracting Order instead of Purchase Order {0}" msgstr "" -#: erpnext/controllers/accounts_controller.py:2822 +#: erpnext/controllers/accounts_controller.py:2823 msgid "Please select Unrealized Profit / Loss account or add default Unrealized Profit / Loss account account for company {0}" msgstr "" @@ -36580,11 +36567,11 @@ msgstr "" msgid "Please select a Supplier" msgstr "" -#: erpnext/public/js/utils/serial_no_batch_selector.js:664 +#: erpnext/public/js/utils/serial_no_batch_selector.js:666 msgid "Please select a Warehouse" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1600 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1617 msgid "Please select a Work Order first." msgstr "" @@ -36673,7 +36660,7 @@ msgstr "" msgid "Please select atleast one item to continue" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:399 +#: erpnext/manufacturing/doctype/work_order/work_order.js:380 msgid "Please select atleast one operation to create Job Card" msgstr "" @@ -36723,7 +36710,7 @@ msgstr "" msgid "Please select the Multiple Tier Program type for more than one collection rules." msgstr "" -#: erpnext/stock/doctype/item/item.js:371 +#: erpnext/stock/doctype/item/item.js:364 msgid "Please select the Warehouse first" msgstr "" @@ -36769,7 +36756,7 @@ msgstr "" msgid "Please set 'Gain/Loss Account on Asset Disposal' in Company {0}" msgstr "" -#: erpnext/accounts/general_ledger.py:560 +#: erpnext/accounts/general_ledger.py:562 msgid "Please set '{0}' in Company: {1}" msgstr "" @@ -36924,7 +36911,7 @@ msgstr "" msgid "Please set default Cash or Bank account in Mode of Payments {}" msgstr "" -#: erpnext/accounts/utils.py:2528 +#: erpnext/accounts/utils.py:2527 msgid "Please set default Exchange Gain/Loss Account in Company {}" msgstr "" @@ -36953,7 +36940,7 @@ msgstr "" msgid "Please set filter based on Item or Warehouse" msgstr "" -#: erpnext/controllers/accounts_controller.py:2383 +#: erpnext/controllers/accounts_controller.py:2384 msgid "Please set one of the following:" msgstr "" @@ -36973,15 +36960,15 @@ msgstr "" msgid "Please set the Default Cost Center in {0} company." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:686 +#: 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:1663 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1680 msgid "Please set the Target Warehouse in the Job Card" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1667 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1684 msgid "Please set the WIP Warehouse in the Job Card" msgstr "" @@ -37020,7 +37007,7 @@ msgstr "" msgid "Please set {0} in Company {1} to account for Exchange Gain / Loss" msgstr "" -#: erpnext/controllers/accounts_controller.py:593 +#: erpnext/controllers/accounts_controller.py:594 msgid "Please set {0} to {1}, the same account that was used in the original invoice {2}." msgstr "" @@ -37042,7 +37029,7 @@ msgstr "" msgid "Please specify Company to proceed" msgstr "" -#: erpnext/controllers/accounts_controller.py:3204 +#: erpnext/controllers/accounts_controller.py:3205 #: erpnext/public/js/controllers/accounts.js:117 msgid "Please specify a valid Row ID for row {0} in table {1}" msgstr "" @@ -37232,7 +37219,7 @@ msgstr "" #: erpnext/accounts/report/accounts_payable/accounts_payable.js:16 #: erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js:15 #: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:18 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1196 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1119 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js:15 #: erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py:38 #: erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.html:7 @@ -37646,7 +37633,7 @@ msgstr "" msgid "Price List Currency" msgstr "" -#: erpnext/stock/get_item_details.py:1330 +#: erpnext/stock/get_item_details.py:1334 msgid "Price List Currency not selected" msgstr "" @@ -38001,7 +37988,7 @@ msgstr "" msgid "Print Receipt on Order Complete" msgstr "" -#: erpnext/setup/install.py:114 +#: erpnext/setup/install.py:115 msgid "Print UOM after Quantity" msgstr "" @@ -38019,7 +38006,7 @@ msgstr "" msgid "Print settings updated in respective print format" msgstr "" -#: erpnext/setup/install.py:121 +#: erpnext/setup/install.py:122 msgid "Print taxes with zero amount" msgstr "" @@ -38122,10 +38109,6 @@ msgstr "" msgid "Procedure" msgstr "" -#: erpnext/accounts/doctype/accounts_settings/accounts_settings.js:47 -msgid "Procedures dropped" -msgstr "" - #. Label of the process_deferred_accounting (Link) field in DocType 'Journal #. Entry' #. Name of a DocType @@ -38179,7 +38162,7 @@ msgstr "" msgid "Process Loss Qty" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:340 +#: erpnext/manufacturing/doctype/job_card/job_card.js:289 msgid "Process Loss Quantity" msgstr "" @@ -38260,6 +38243,10 @@ msgstr "" msgid "Process in Single Transaction" msgstr "" +#: erpnext/manufacturing/doctype/job_card/job_card.py:1460 +msgid "Process loss quantity cannot be negative." +msgstr "" + #. Label of the processed_boms (Long Text) field in DocType 'BOM Update Log' #: erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json msgid "Processed BOMs" @@ -38956,7 +38943,7 @@ msgid "Prospects Engaged But Not Converted" msgstr "" #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:198 -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:783 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:784 msgid "Protected DocType" msgstr "" @@ -39043,7 +39030,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:403 +#: erpnext/setup/doctype/company/company.py:463 erpnext/setup/install.py:435 #: erpnext/stock/doctype/item/item.json #: erpnext/stock/doctype/item_lead_time/item_lead_time.json #: erpnext/stock/doctype/item_reorder/item_reorder.json @@ -39402,7 +39389,7 @@ msgstr "" msgid "Purchase Orders to Receive" msgstr "" -#: erpnext/controllers/accounts_controller.py:2015 +#: erpnext/controllers/accounts_controller.py:2016 msgid "Purchase Orders {0} are un-linked" msgstr "" @@ -39686,6 +39673,7 @@ msgstr "" #. Label of the qty (Float) field in DocType 'Delivery Schedule Item' #. Label of the qty (Float) field in DocType 'Product Bundle Item' #. Label of the qty (Float) field in DocType 'Landed Cost Item' +#. Label of the qty (Float) field in DocType 'Landed Cost Taxes and Charges' #. Option for the 'Distribute Charges Based On' (Select) field in DocType #. 'Landed Cost Voucher' #. Label of the qty (Float) field in DocType 'Packed Item' @@ -39700,7 +39688,6 @@ msgstr "" #. DocType 'Subcontracting Receipt' #: erpnext/accounts/doctype/pricing_rule/pricing_rule.json #: erpnext/accounts/doctype/promotional_scheme_product_discount/promotional_scheme_product_discount.json -#: erpnext/accounts/print_format/sales_invoice_print/sales_invoice_print.html:91 #: erpnext/accounts/report/gross_profit/gross_profit.py:345 #: erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.json #: erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py:240 @@ -39735,6 +39722,7 @@ msgstr "" #: erpnext/selling/doctype/sales_order/sales_order.js:1468 #: 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_taxes_and_charges/landed_cost_taxes_and_charges.json #: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json #: erpnext/stock/doctype/packed_item/packed_item.json #: erpnext/stock/doctype/pick_list_item/pick_list_item.json @@ -39843,11 +39831,11 @@ msgstr "" msgid "Qty To Manufacture" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:1441 +#: erpnext/manufacturing/doctype/work_order/work_order.py:1442 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:259 +#: erpnext/manufacturing/doctype/job_card/job_card.py:260 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 "" @@ -39898,8 +39886,8 @@ msgstr "" msgid "Qty for which recursion isn't applicable." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1063 -#: erpnext/manufacturing/doctype/work_order/work_order.js:1086 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1045 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1068 msgid "Qty for {0}" msgstr "" @@ -39954,8 +39942,8 @@ msgstr "" msgid "Qty to Fetch" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:312 -#: erpnext/manufacturing/doctype/job_card/job_card.py:889 +#: erpnext/manufacturing/doctype/job_card/job_card.js:247 +#: erpnext/manufacturing/doctype/job_card/job_card.py:892 msgid "Qty to Manufacture" msgstr "" @@ -40191,17 +40179,17 @@ msgstr "" msgid "Quality Inspection Template Name" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:798 +#: erpnext/manufacturing/doctype/job_card/job_card.py:799 msgid "Quality Inspection is required for the item {0} before completing the job card {1}" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:809 -#: erpnext/manufacturing/doctype/job_card/job_card.py:818 +#: erpnext/manufacturing/doctype/job_card/job_card.py:810 +#: erpnext/manufacturing/doctype/job_card/job_card.py:819 msgid "Quality Inspection {0} is not submitted for the item: {1}" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:828 -#: erpnext/manufacturing/doctype/job_card/job_card.py:837 +#: erpnext/manufacturing/doctype/job_card/job_card.py:829 +#: erpnext/manufacturing/doctype/job_card/job_card.py:838 msgid "Quality Inspection {0} is rejected for the item: {1}" msgstr "" @@ -40349,7 +40337,7 @@ msgstr "" #: erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json #: erpnext/public/js/controllers/buying.js:622 #: erpnext/public/js/stock_analytics.js:50 -#: erpnext/public/js/utils/serial_no_batch_selector.js:497 +#: erpnext/public/js/utils/serial_no_batch_selector.js:499 #: erpnext/selling/doctype/quotation_item/quotation_item.json #: erpnext/selling/doctype/sales_order_item/sales_order_item.json #: erpnext/selling/page/point_of_sale/pos_item_cart.js:51 @@ -40482,7 +40470,7 @@ msgstr "" msgid "Quantity must be less than or equal to {0}" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1116 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1098 #: erpnext/stock/doctype/pick_list/pick_list.js:209 msgid "Quantity must not be more than {0}" msgstr "" @@ -40492,21 +40480,21 @@ msgid "Quantity required for Item {0} in row {1}" msgstr "" #: erpnext/manufacturing/doctype/bom/bom.py:717 -#: erpnext/manufacturing/doctype/job_card/job_card.js:393 -#: erpnext/manufacturing/doctype/job_card/job_card.js:463 +#: erpnext/manufacturing/doctype/job_card/job_card.js:342 +#: erpnext/manufacturing/doctype/job_card/job_card.js:410 #: erpnext/manufacturing/doctype/workstation/workstation.js:303 msgid "Quantity should be greater than 0" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:361 +#: erpnext/manufacturing/doctype/work_order/work_order.js:342 msgid "Quantity to Manufacture" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:2638 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2639 msgid "Quantity to Manufacture can not be zero for the operation {0}" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:1433 +#: erpnext/manufacturing/doctype/work_order/work_order.py:1434 msgid "Quantity to Manufacture must be greater than 0." msgstr "" @@ -40543,7 +40531,7 @@ msgstr "" msgid "Queue Size should be between 5 and 100" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.js:621 +#: erpnext/accounts/doctype/journal_entry/journal_entry.js:625 msgid "Quick Journal Entry" msgstr "" @@ -40762,7 +40750,6 @@ msgstr "" #: erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json #: erpnext/accounts/doctype/share_balance/share_balance.json #: erpnext/accounts/doctype/share_transfer/share_transfer.json -#: erpnext/accounts/print_format/sales_invoice_print/sales_invoice_print.html:92 #: erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py:78 #: erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py:266 #: erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py:320 @@ -40962,7 +40949,7 @@ msgstr "" msgid "Rate at which this tax is applied" msgstr "" -#: erpnext/controllers/accounts_controller.py:4087 +#: erpnext/controllers/accounts_controller.py:4089 msgid "Rate of '{}' items cannot be changed" msgstr "" @@ -41173,12 +41160,6 @@ msgstr "" msgid "Raw Materials to Customer" msgstr "" -#. Option for the 'Data Fetch Method' (Select) field in DocType 'Accounts -#. Settings' -#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json -msgid "Raw SQL" -msgstr "" - #. Description of the 'Validate consumed quantity (as per BOM)' (Check) field #. in DocType 'Buying Settings' #: erpnext/buying/doctype/buying_settings/buying_settings.json @@ -41187,7 +41168,7 @@ msgstr "" #: erpnext/buying/doctype/purchase_order/purchase_order.js:369 #: erpnext/manufacturing/doctype/production_plan/production_plan.js:124 -#: erpnext/manufacturing/doctype/work_order/work_order.js:785 +#: erpnext/manufacturing/doctype/work_order/work_order.js:767 #: erpnext/selling/doctype/sales_order/sales_order.js:974 #: erpnext/selling/doctype/sales_order/sales_order_list.js:70 #: erpnext/stock/doctype/material_request/material_request.js:243 @@ -41368,7 +41349,7 @@ msgid "Receivable / Payable Account" msgstr "" #: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:79 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1211 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1134 #: erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py:241 #: erpnext/accounts/report/sales_register/sales_register.py:217 #: erpnext/accounts/report/sales_register/sales_register.py:271 @@ -41985,7 +41966,7 @@ msgstr "" msgid "Rejected Warehouse" msgstr "" -#: erpnext/public/js/utils/serial_no_batch_selector.js:668 +#: erpnext/public/js/utils/serial_no_batch_selector.js:670 msgid "Rejected Warehouse and Accepted Warehouse cannot be same." msgstr "" @@ -42029,13 +42010,13 @@ msgid "Remaining Amount" msgstr "" #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html:189 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1286 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1209 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:178 msgid "Remaining Balance" msgstr "" #. Label of the remark (Small Text) field in DocType 'Journal Entry' -#: erpnext/accounts/doctype/journal_entry/journal_entry.js:651 +#: erpnext/accounts/doctype/journal_entry/journal_entry.js:655 #: erpnext/accounts/doctype/journal_entry/journal_entry.json #: erpnext/selling/page/point_of_sale/pos_payment.js:489 msgid "Remark" @@ -42081,7 +42062,7 @@ msgstr "" #: erpnext/accounts/print_format/payment_receipt_voucher/payment_receipt_voucher.html:11 #: erpnext/accounts/report/accounts_payable/accounts_payable.html:135 #: erpnext/accounts/report/accounts_receivable/accounts_receivable.html:136 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1318 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1241 #: erpnext/accounts/report/general_ledger/general_ledger.html:163 #: erpnext/accounts/report/general_ledger/general_ledger.py:811 #: erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py:112 @@ -42286,7 +42267,7 @@ msgstr "" msgid "Report Type is mandatory" msgstr "" -#: erpnext/setup/install.py:216 +#: erpnext/setup/install.py:248 msgid "Report an Issue" msgstr "" @@ -42760,7 +42741,7 @@ msgstr "" msgid "Reservation Based On" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:943 +#: erpnext/manufacturing/doctype/work_order/work_order.js:925 #: erpnext/selling/doctype/sales_order/sales_order.js:107 #: erpnext/stock/doctype/pick_list/pick_list.js:153 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:180 @@ -42875,14 +42856,14 @@ msgstr "" msgid "Reserved Quantity for Production" msgstr "" -#: erpnext/stock/stock_ledger.py:2340 +#: erpnext/stock/stock_ledger.py:2306 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:959 +#: erpnext/manufacturing/doctype/work_order/work_order.js:941 #: erpnext/public/js/stock_reservation.js:236 #: erpnext/selling/doctype/sales_order/sales_order.js:135 #: erpnext/selling/doctype/sales_order/sales_order.js:465 @@ -42891,13 +42872,13 @@ msgstr "" #: erpnext/stock/doctype/pick_list/pick_list.js:173 #: erpnext/stock/report/reserved_stock/reserved_stock.json #: erpnext/stock/report/stock_balance/stock_balance.py:572 -#: erpnext/stock/stock_ledger.py:2324 +#: erpnext/stock/stock_ledger.py:2290 #: 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:2369 +#: erpnext/stock/stock_ledger.py:2335 msgid "Reserved Stock for Batch" msgstr "" @@ -43163,7 +43144,7 @@ msgstr "" msgid "Resume" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:247 +#: erpnext/manufacturing/doctype/job_card/job_card.js:659 msgid "Resume Job" msgstr "" @@ -43264,7 +43245,7 @@ msgstr "" msgid "Return Against Subcontracting Receipt" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:302 +#: erpnext/manufacturing/doctype/work_order/work_order.js:283 msgid "Return Components" msgstr "" @@ -43421,7 +43402,7 @@ msgstr "" msgid "Reversal Of" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.js:96 +#: erpnext/accounts/doctype/journal_entry/journal_entry.js:100 msgid "Reverse Journal Entry" msgstr "" @@ -43836,7 +43817,7 @@ msgstr "" msgid "Row # {0}: Returned Item {1} does not exist in {2} {3}" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:279 +#: erpnext/manufacturing/doctype/work_order/work_order.py:280 msgid "Row #1: Sequence ID must be 1 for Operation {0}." msgstr "" @@ -43871,7 +43852,7 @@ msgstr "" msgid "Row #{0}: Accepted Warehouse is mandatory for the accepted Item {1}" msgstr "" -#: erpnext/controllers/accounts_controller.py:1293 +#: erpnext/controllers/accounts_controller.py:1294 msgid "Row #{0}: Account {1} does not belong to company {2}" msgstr "" @@ -43936,31 +43917,31 @@ msgstr "" msgid "Row #{0}: Cannot create entry with different taxable AND withholding document links." msgstr "" -#: erpnext/controllers/accounts_controller.py:3790 +#: erpnext/controllers/accounts_controller.py:3792 msgid "Row #{0}: Cannot delete item {1} which has already been billed." msgstr "" -#: erpnext/controllers/accounts_controller.py:3764 +#: erpnext/controllers/accounts_controller.py:3766 msgid "Row #{0}: Cannot delete item {1} which has already been delivered" msgstr "" -#: erpnext/controllers/accounts_controller.py:3783 +#: erpnext/controllers/accounts_controller.py:3785 msgid "Row #{0}: Cannot delete item {1} which has already been received" msgstr "" -#: erpnext/controllers/accounts_controller.py:3770 +#: erpnext/controllers/accounts_controller.py:3772 msgid "Row #{0}: Cannot delete item {1} which has work order assigned to it." msgstr "" -#: erpnext/controllers/accounts_controller.py:3776 +#: erpnext/controllers/accounts_controller.py:3778 msgid "Row #{0}: Cannot delete item {1} which is already ordered against this Sales Order." msgstr "" -#: erpnext/controllers/accounts_controller.py:4097 +#: erpnext/controllers/accounts_controller.py:4099 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:1127 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1135 msgid "Row #{0}: Cannot transfer more than Required Qty {1} for Item {2} against Job Card {3}" msgstr "" @@ -44010,11 +43991,11 @@ msgstr "" msgid "Row #{0}: Customer Provided Item {1} cannot be added multiple times in the Subcontracting Inward process." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:356 +#: erpnext/manufacturing/doctype/work_order/work_order.py:357 msgid "Row #{0}: Customer Provided Item {1} cannot be added multiple times." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:381 +#: erpnext/manufacturing/doctype/work_order/work_order.py:382 msgid "Row #{0}: Customer Provided Item {1} does not exist in the Required Items table linked to the Subcontracting Inward Order." msgstr "" @@ -44022,7 +44003,7 @@ msgstr "" msgid "Row #{0}: Customer Provided Item {1} exceeds quantity available through Subcontracting Inward Order" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:369 +#: erpnext/manufacturing/doctype/work_order/work_order.py:370 msgid "Row #{0}: Customer Provided Item {1} has insufficient quantity in the Subcontracting Inward Order. Available quantity is {2}." msgstr "" @@ -44107,7 +44088,7 @@ msgstr "" msgid "Row #{0}: From Date cannot be before To Date" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:879 +#: erpnext/manufacturing/doctype/job_card/job_card.py:880 msgid "Row #{0}: From Time and To Time fields are required" msgstr "" @@ -44221,7 +44202,7 @@ msgstr "" msgid "Row #{0}: Please set reorder quantity" msgstr "" -#: erpnext/controllers/accounts_controller.py:616 +#: erpnext/controllers/accounts_controller.py:617 msgid "Row #{0}: Please update deferred revenue/expense account in item row or default account in company master" msgstr "" @@ -44259,7 +44240,7 @@ msgstr "" msgid "Row #{0}: Quantity cannot be a non-positive number. Please increase the quantity or remove the Item {1}" msgstr "" -#: erpnext/controllers/accounts_controller.py:1456 +#: erpnext/controllers/accounts_controller.py:1457 msgid "Row #{0}: Quantity for Item {1} cannot be zero." msgstr "" @@ -44275,8 +44256,8 @@ msgstr "" msgid "Row #{0}: Quantity to reserve for the Item {1} should be greater than 0." msgstr "" -#: erpnext/controllers/accounts_controller.py:871 -#: erpnext/controllers/accounts_controller.py:883 +#: erpnext/controllers/accounts_controller.py:872 +#: erpnext/controllers/accounts_controller.py:884 #: erpnext/utilities/transaction_base.py:172 #: erpnext/utilities/transaction_base.py:178 msgid "Row #{0}: Rate must be same as {1}: {2} ({3} / {4})" @@ -44326,7 +44307,7 @@ msgid "" "\t\t\t\t\tthis validation." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:285 +#: erpnext/manufacturing/doctype/work_order/work_order.py:286 msgid "Row #{0}: Sequence ID must be {1} or {2} for Operation {3}." msgstr "" @@ -44346,15 +44327,15 @@ msgstr "" msgid "Row #{0}: Serial No(s) {1} are not a part of the linked Subcontracting Inward Order. Please select valid Serial No(s)." msgstr "" -#: erpnext/controllers/accounts_controller.py:644 +#: erpnext/controllers/accounts_controller.py:645 msgid "Row #{0}: Service End Date cannot be before Invoice Posting Date" msgstr "" -#: erpnext/controllers/accounts_controller.py:638 +#: erpnext/controllers/accounts_controller.py:639 msgid "Row #{0}: Service Start Date cannot be greater than Service End Date" msgstr "" -#: erpnext/controllers/accounts_controller.py:632 +#: erpnext/controllers/accounts_controller.py:633 msgid "Row #{0}: Service Start and End Date is required for deferred accounting" msgstr "" @@ -44370,11 +44351,11 @@ msgstr "" msgid "Row #{0}: Source Warehouse must be same as Customer Warehouse {1} from the linked Subcontracting Inward Order" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:390 +#: erpnext/manufacturing/doctype/work_order/work_order.py:391 msgid "Row #{0}: Source Warehouse {1} for item {2} cannot be a customer warehouse." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:345 +#: erpnext/manufacturing/doctype/work_order/work_order.py:346 msgid "Row #{0}: Source Warehouse {1} for item {2} must be same as Source Warehouse {3} in the Work Order." msgstr "" @@ -44491,7 +44472,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:3904 +#: erpnext/controllers/accounts_controller.py:3906 msgid "Row #{0}:Quantity for Item {1} cannot be zero." msgstr "" @@ -44600,7 +44581,7 @@ msgstr "" msgid "Row No {0}: Warehouse is required. Please set a Default Warehouse for Item {1} and Company {2}" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:746 +#: erpnext/manufacturing/doctype/job_card/job_card.py:747 msgid "Row {0} : Operation is required against the raw material item {1}" msgstr "" @@ -44662,7 +44643,7 @@ msgstr "" msgid "Row {0}: Conversion Factor is mandatory" msgstr "" -#: erpnext/controllers/accounts_controller.py:3242 +#: erpnext/controllers/accounts_controller.py:3243 msgid "Row {0}: Cost Center {1} does not belong to Company {2}" msgstr "" @@ -44682,7 +44663,7 @@ msgstr "" msgid "Row {0}: Debit entry can not be linked with a {1}" msgstr "" -#: erpnext/controllers/selling_controller.py:879 +#: erpnext/controllers/selling_controller.py:880 msgid "Row {0}: Delivery Warehouse ({1}) and Customer Warehouse ({2}) can not be same" msgstr "" @@ -44690,7 +44671,7 @@ msgstr "" msgid "Row {0}: Delivery Warehouse cannot be same as Customer Warehouse for Item {1}." msgstr "" -#: erpnext/controllers/accounts_controller.py:2735 +#: erpnext/controllers/accounts_controller.py:2736 msgid "Row {0}: Due Date in the Payment Terms table cannot be before Posting Date" msgstr "" @@ -44735,7 +44716,7 @@ msgstr "" msgid "Row {0}: From Time and To Time is mandatory." msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:324 +#: erpnext/manufacturing/doctype/job_card/job_card.py:325 #: erpnext/projects/doctype/timesheet/timesheet.py:225 msgid "Row {0}: From Time and To Time of {1} is overlapping with {2}" msgstr "" @@ -44744,7 +44725,7 @@ msgstr "" msgid "Row {0}: From Warehouse is mandatory for internal transfers" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:315 +#: erpnext/manufacturing/doctype/job_card/job_card.py:316 msgid "Row {0}: From time must be less than to time" msgstr "" @@ -44760,7 +44741,7 @@ msgstr "" msgid "Row {0}: Item Tax template updated as per validity and rate applied" msgstr "" -#: erpnext/controllers/selling_controller.py:644 +#: erpnext/controllers/selling_controller.py:645 msgid "Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer" msgstr "" @@ -44896,7 +44877,7 @@ msgstr "" msgid "Row {0}: The item {1}, quantity must be positive number" msgstr "" -#: erpnext/controllers/accounts_controller.py:3219 +#: erpnext/controllers/accounts_controller.py:3220 msgid "Row {0}: The {3} Account {1} does not belong to the company {2}" msgstr "" @@ -44921,11 +44902,11 @@ msgid "Row {0}: Warehouse {1} is linked to company {2}. Please select a warehous msgstr "" #: erpnext/manufacturing/doctype/bom/bom.py:1239 -#: erpnext/manufacturing/doctype/work_order/work_order.py:419 +#: erpnext/manufacturing/doctype/work_order/work_order.py:420 msgid "Row {0}: Workstation or Workstation Type is mandatory for an operation {1}" msgstr "" -#: erpnext/controllers/accounts_controller.py:1175 +#: erpnext/controllers/accounts_controller.py:1176 msgid "Row {0}: user has not applied the rule {1} on the item {2}" msgstr "" @@ -44937,7 +44918,7 @@ msgstr "" msgid "Row {0}: {1} must be greater than 0" msgstr "" -#: erpnext/controllers/accounts_controller.py:781 +#: erpnext/controllers/accounts_controller.py:782 msgid "Row {0}: {1} {2} cannot be same as {3} (Party Account) {4}" msgstr "" @@ -44983,15 +44964,15 @@ msgstr "" msgid "Rows with Same Account heads will be merged on Ledger" msgstr "" -#: erpnext/controllers/accounts_controller.py:2746 +#: erpnext/controllers/accounts_controller.py:2747 msgid "Rows with duplicate due dates in other rows were found: {0}" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.js:144 +#: erpnext/accounts/doctype/journal_entry/journal_entry.js:148 msgid "Rows: {0} have 'Payment Entry' as reference_type. This should not be set manually." msgstr "" -#: erpnext/controllers/accounts_controller.py:282 +#: erpnext/controllers/accounts_controller.py:283 msgid "Rows: {0} in {1} section are Invalid. Reference Name should point to a valid Payment Entry or Journal Entry." msgstr "" @@ -45168,7 +45149,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:398 +#: erpnext/setup/install.py:430 #: 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 @@ -45576,8 +45557,8 @@ msgstr "" msgid "Sales Order {0} already exists against Customer's Purchase Order {1}. To allow multiple Sales Orders, Enable {2} in {3}" msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.py:1813 -#: erpnext/selling/doctype/sales_order/sales_order.py:1826 +#: erpnext/selling/doctype/sales_order/sales_order.py:1814 +#: erpnext/selling/doctype/sales_order/sales_order.py:1827 msgid "Sales Order {0} is not available for production" msgstr "" @@ -45585,12 +45566,12 @@ msgstr "" msgid "Sales Order {0} is not submitted" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:495 +#: erpnext/manufacturing/doctype/work_order/work_order.py:496 msgid "Sales Order {0} is not valid" msgstr "" #: erpnext/controllers/selling_controller.py:476 -#: erpnext/manufacturing/doctype/work_order/work_order.py:500 +#: erpnext/manufacturing/doctype/work_order/work_order.py:501 msgid "Sales Order {0} is {1}" msgstr "" @@ -45646,7 +45627,7 @@ msgstr "" #: erpnext/accounts/doctype/promotional_scheme/promotional_scheme.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:130 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1307 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1230 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js:114 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:194 #: erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js:74 @@ -45752,7 +45733,7 @@ msgstr "" #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html:158 #: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:136 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1304 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1227 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js:120 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:191 #: erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js:80 @@ -45845,7 +45826,7 @@ msgstr "" msgid "Sales Representative" msgstr "" -#: erpnext/accounts/report/gross_profit/gross_profit.py:997 +#: erpnext/accounts/report/gross_profit/gross_profit.py:989 #: erpnext/stock/doctype/delivery_note/delivery_note.js:270 msgid "Sales Return" msgstr "" @@ -46021,7 +46002,7 @@ msgstr "" msgid "Sample Size" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:4090 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:4095 msgid "Sample quantity {0} cannot be more than received quantity {1}" msgstr "" @@ -46388,7 +46369,7 @@ msgstr "" msgid "Select Alternative Items for Sales Order" msgstr "" -#: erpnext/stock/doctype/item/item.js:820 +#: erpnext/stock/doctype/item/item.js:807 msgid "Select Attribute Values" msgstr "" @@ -46422,7 +46403,7 @@ msgstr "" msgid "Select Columns and Filters" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.js:152 +#: erpnext/accounts/doctype/journal_entry/journal_entry.js:156 msgid "Select Company" msgstr "" @@ -46430,7 +46411,7 @@ msgstr "" msgid "Select Company Address" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:541 +#: erpnext/manufacturing/doctype/job_card/job_card.js:477 msgid "Select Corrective Operation" msgstr "" @@ -46466,7 +46447,7 @@ msgstr "" msgid "Select Dispatch Address " msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:229 +#: erpnext/manufacturing/doctype/job_card/job_card.js:702 msgid "Select Employees" msgstr "" @@ -46529,7 +46510,7 @@ msgstr "" msgid "Select Possible Supplier" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1122 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1104 #: erpnext/stock/doctype/pick_list/pick_list.js:219 msgid "Select Quantity" msgstr "" @@ -46612,7 +46593,7 @@ msgstr "" msgid "Select a company" msgstr "" -#: erpnext/stock/doctype/item/item.js:1153 +#: erpnext/stock/doctype/item/item.js:1140 msgid "Select an Item Group." msgstr "" @@ -46628,7 +46609,7 @@ msgstr "" msgid "Select an item from each set to be used in the Sales Order." msgstr "" -#: erpnext/stock/doctype/item/item.js:834 +#: erpnext/stock/doctype/item/item.js:821 msgid "Select at least one value from each of the attributes." msgstr "" @@ -46642,7 +46623,7 @@ msgstr "" msgid "Select company name first." msgstr "" -#: erpnext/controllers/accounts_controller.py:2994 +#: erpnext/controllers/accounts_controller.py:2995 msgid "Select finance book for the item {0} at row {1}" msgstr "" @@ -46663,7 +46644,7 @@ 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:1224 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1206 msgid "Select the Item to be manufactured." msgstr "" @@ -46680,7 +46661,7 @@ msgstr "" msgid "Select the customer or supplier." msgstr "" -#: erpnext/assets/doctype/asset/asset.js:928 +#: erpnext/assets/doctype/asset/asset.js:930 msgid "Select the date" msgstr "" @@ -46747,22 +46728,22 @@ msgstr "" msgid "Self delivery" msgstr "" -#: erpnext/assets/doctype/asset/asset.js:639 +#: erpnext/assets/doctype/asset/asset.js:641 #: erpnext/stock/doctype/batch/batch_dashboard.py:9 #: erpnext/stock/doctype/item/item_dashboard.py:20 msgid "Sell" msgstr "" #: erpnext/assets/doctype/asset/asset.js:171 -#: erpnext/assets/doctype/asset/asset.js:628 +#: erpnext/assets/doctype/asset/asset.js:630 msgid "Sell Asset" msgstr "" -#: erpnext/assets/doctype/asset/asset.js:633 +#: erpnext/assets/doctype/asset/asset.js:635 msgid "Sell Qty" msgstr "" -#: erpnext/assets/doctype/asset/asset.js:649 +#: erpnext/assets/doctype/asset/asset.js:651 msgid "Sell quantity cannot exceed the asset quantity" msgstr "" @@ -46770,7 +46751,7 @@ msgstr "" msgid "Sell quantity cannot exceed the asset quantity. Asset {0} has only {1} item(s)." msgstr "" -#: erpnext/assets/doctype/asset/asset.js:645 +#: erpnext/assets/doctype/asset/asset.js:647 msgid "Sell quantity must be greater than zero" msgstr "" @@ -47143,7 +47124,7 @@ msgstr "" msgid "Serial No is mandatory for Item {0}" msgstr "" -#: erpnext/public/js/utils/serial_no_batch_selector.js:601 +#: erpnext/public/js/utils/serial_no_batch_selector.js:603 msgid "Serial No {0} already exists" msgstr "" @@ -47222,7 +47203,7 @@ msgstr "" msgid "Serial Nos are created successfully" msgstr "" -#: erpnext/stock/stock_ledger.py:2330 +#: erpnext/stock/stock_ledger.py:2296 msgid "Serial Nos are reserved in Stock Reservation Entries, you need to unreserve them before proceeding." msgstr "" @@ -47429,7 +47410,7 @@ msgstr "" #: erpnext/accounts/doctype/budget/budget.json #: erpnext/accounts/doctype/cashier_closing/cashier_closing.json #: erpnext/accounts/doctype/dunning/dunning.json -#: erpnext/accounts/doctype/journal_entry/journal_entry.js:655 +#: erpnext/accounts/doctype/journal_entry/journal_entry.js:659 #: erpnext/accounts/doctype/journal_entry/journal_entry.json #: erpnext/accounts/doctype/journal_entry_template/journal_entry_template.json #: erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -47725,8 +47706,8 @@ msgstr "" msgid "Set Dropship Items Delivered Quantity" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:412 -#: erpnext/manufacturing/doctype/job_card/job_card.js:481 +#: erpnext/manufacturing/doctype/job_card/job_card.js:363 +#: erpnext/manufacturing/doctype/job_card/job_card.js:425 msgid "Set Finished Good Quantity" msgstr "" @@ -47925,7 +47906,7 @@ msgstr "" msgid "Set targets Item Group-wise for this Sales Person." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1281 +#: 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 "" @@ -48027,7 +48008,7 @@ msgid "Setting up company" msgstr "" #: erpnext/manufacturing/doctype/bom/bom.py:1218 -#: erpnext/manufacturing/doctype/work_order/work_order.py:1497 +#: erpnext/manufacturing/doctype/work_order/work_order.py:1498 msgid "Setting {0} is required" msgstr "" @@ -48277,7 +48258,7 @@ msgstr "" msgid "Shipping Address Template" msgstr "" -#: erpnext/controllers/accounts_controller.py:575 +#: erpnext/controllers/accounts_controller.py:576 msgid "Shipping Address does not belong to the {0}" msgstr "" @@ -48780,7 +48761,7 @@ msgstr "" #. Label of the skip_material_transfer (Check) field in DocType 'Work Order #. Operation' -#: erpnext/manufacturing/doctype/work_order/work_order.js:380 +#: erpnext/manufacturing/doctype/work_order/work_order.js:361 #: erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json #: erpnext/manufacturing/doctype/workstation/workstation.js:454 msgid "Skip Material Transfer" @@ -48847,7 +48828,7 @@ msgstr "" msgid "Solvency Ratios" msgstr "" -#: erpnext/controllers/accounts_controller.py:4357 +#: erpnext/controllers/accounts_controller.py:4359 msgid "Some required Company details are missing. You don't have permission to update them. Please contact your System Manager." msgstr "" @@ -48911,7 +48892,7 @@ msgstr "" msgid "Source Location" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1032 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1014 msgid "Source Manufacture Entry" msgstr "" @@ -48982,7 +48963,7 @@ msgstr "" msgid "Source Warehouse is mandatory for the Item {0}." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:304 +#: erpnext/manufacturing/doctype/work_order/work_order.py:305 msgid "Source Warehouse {0} must be same as Customer Warehouse {1} in the Subcontracting Inward Order." msgstr "" @@ -49009,6 +48990,10 @@ msgstr "" msgid "Source warehouse is mandatory for row {0}" msgstr "" +#: erpnext/selling/doctype/sales_order/sales_order.py:455 +msgid "Source warehouse required for stock item {0}" +msgstr "" + #. Label of the sourced_by_supplier (Check) field in DocType 'BOM Creator Item' #. Label of the sourced_by_supplier (Check) field in DocType 'BOM Explosion #. Item' @@ -49043,7 +49028,7 @@ msgstr "" msgid "Spending for Account {0} ({1}) between {2} and {3} has already exceeded the new allocated budget. Spent: {4}, Budget: {5}" msgstr "" -#: erpnext/assets/doctype/asset/asset.js:689 +#: erpnext/assets/doctype/asset/asset.js:691 #: erpnext/stock/doctype/batch/batch.js:104 #: erpnext/stock/doctype/batch/batch.js:185 #: erpnext/support/doctype/issue/issue.js:114 @@ -49051,7 +49036,7 @@ msgid "Split" msgstr "" #: erpnext/assets/doctype/asset/asset.js:147 -#: erpnext/assets/doctype/asset/asset.js:673 +#: erpnext/assets/doctype/asset/asset.js:675 msgid "Split Asset" msgstr "" @@ -49075,7 +49060,7 @@ msgstr "" msgid "Split Issue" msgstr "" -#: erpnext/assets/doctype/asset/asset.js:679 +#: erpnext/assets/doctype/asset/asset.js:681 msgid "Split Qty" msgstr "" @@ -49155,7 +49140,7 @@ msgstr "" msgid "Standard Description" msgstr "" -#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:115 +#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:127 msgid "Standard Rated Expenses" msgstr "" @@ -49182,8 +49167,8 @@ msgstr "" msgid "Standard Terms and Conditions that can be added to Sales and Purchases. Examples: Validity of the offer, Payment Terms, Safety and Usage, etc." msgstr "" -#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:96 -#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:102 +#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:108 +#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:114 msgid "Standard rated supplies in {0}" msgstr "" @@ -49218,7 +49203,7 @@ msgstr "" msgid "Start Date should be lower than End Date" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.js:223 +#: erpnext/manufacturing/doctype/job_card/job_card.js:658 #: erpnext/manufacturing/doctype/workstation/workstation.js:124 msgid "Start Job" msgstr "" @@ -49534,7 +49519,7 @@ msgstr "" msgid "Stock Entry {0} created" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1526 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1543 msgid "Stock Entry {0} has created" msgstr "" @@ -49775,9 +49760,9 @@ msgstr "" #: erpnext/manufacturing/doctype/production_plan/production_plan.js:289 #: erpnext/manufacturing/doctype/production_plan/production_plan.js:297 #: erpnext/manufacturing/doctype/production_plan/production_plan.js:303 -#: erpnext/manufacturing/doctype/work_order/work_order.js:945 -#: erpnext/manufacturing/doctype/work_order/work_order.js:954 -#: erpnext/manufacturing/doctype/work_order/work_order.js:961 +#: 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:109 @@ -49813,9 +49798,9 @@ msgstr "" msgid "Stock Reservation Entries Cancelled" msgstr "" -#: erpnext/controllers/subcontracting_inward_controller.py:1018 -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:2245 -#: erpnext/manufacturing/doctype/work_order/work_order.py:2146 +#: erpnext/controllers/subcontracting_inward_controller.py:1026 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:2247 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2147 #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1777 msgid "Stock Reservation Entries Created" msgstr "" @@ -49883,7 +49868,7 @@ msgstr "" #: erpnext/selling/doctype/selling_settings/selling_settings.py:115 #: erpnext/setup/doctype/company/company.json #: erpnext/setup/workspace/erpnext_settings/erpnext_settings.json -#: erpnext/stock/doctype/item/item.js:420 +#: erpnext/stock/doctype/item/item.js:413 #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:675 #: erpnext/stock/doctype/stock_settings/stock_settings.json #: erpnext/stock/workspace/stock/stock.json @@ -50193,7 +50178,7 @@ msgstr "" msgid "Stop Reason" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:1105 +#: erpnext/manufacturing/doctype/work_order/work_order.py:1106 msgid "Stopped Work Order cannot be cancelled, Unstop it first to cancel" msgstr "" @@ -50258,7 +50243,7 @@ msgstr "" #. Label of the operation (Link) field in DocType 'Job Card Time Log' #. Name of a DocType -#: erpnext/manufacturing/doctype/job_card/job_card.js:357 +#: erpnext/manufacturing/doctype/job_card/job_card.js:310 #: erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json #: erpnext/manufacturing/doctype/sub_operation/sub_operation.json msgid "Sub Operation" @@ -50278,10 +50263,6 @@ msgstr "" msgid "Sub Procedure" msgstr "" -#: erpnext/accounts/print_format/sales_invoice_print/sales_invoice_print.html:129 -msgid "Sub Total" -msgstr "" - #: 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 "" @@ -50640,7 +50621,7 @@ msgstr "" msgid "Submit Journal Entries" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:192 +#: erpnext/manufacturing/doctype/work_order/work_order.js:173 msgid "Submit this Work Order for further processing." msgstr "" @@ -51078,7 +51059,7 @@ msgstr "" #: erpnext/accounts/doctype/tax_rule/tax_rule.json #: erpnext/accounts/report/accounts_payable/accounts_payable.js:119 #: erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js:102 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1311 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1234 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:198 #: erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py:178 #: erpnext/accounts/report/purchase_register/purchase_register.js:27 @@ -51177,7 +51158,7 @@ msgstr "" #. Label of the supplier_name (Data) field in DocType 'Purchase Receipt' #. Label of the supplier_name (Data) field in DocType 'Stock Entry' #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1227 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1150 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:156 #: erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py:196 #: erpnext/accounts/report/purchase_register/purchase_register.py:177 @@ -51383,7 +51364,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:97 +#: erpnext/manufacturing/doctype/job_card/job_card.js:91 #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.json msgid "Supplier Warehouse" msgstr "" @@ -51423,8 +51404,8 @@ msgstr "" msgid "Suppliers" msgstr "" -#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:60 -#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:122 +#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:72 +#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:134 msgid "Supplies subject to the reverse charge provision" msgstr "" @@ -51542,7 +51523,7 @@ msgstr "" msgid "System will fetch all the entries if limit value is zero." msgstr "" -#: erpnext/controllers/accounts_controller.py:2228 +#: erpnext/controllers/accounts_controller.py:2229 msgid "System will not check over billing since amount for Item {0} in {1} is zero" msgstr "" @@ -51714,7 +51695,7 @@ msgstr "" msgid "Target Warehouse Address Link" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:249 +#: erpnext/manufacturing/doctype/work_order/work_order.py:250 msgid "Target Warehouse Reservation Error" msgstr "" @@ -51722,15 +51703,15 @@ msgstr "" 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 "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:793 +#: erpnext/manufacturing/doctype/work_order/work_order.py:794 msgid "Target Warehouse is required before Submit" msgstr "" -#: erpnext/controllers/selling_controller.py:885 +#: erpnext/controllers/selling_controller.py:886 msgid "Target Warehouse is set for some items but the customer is not an internal customer." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:320 +#: erpnext/manufacturing/doctype/work_order/work_order.py:321 msgid "Target Warehouse {0} must be same as Delivery Warehouse {1} in the Subcontracting Inward Order Item." msgstr "" @@ -51889,7 +51870,6 @@ msgstr "" msgid "Tax Breakup" msgstr "" -#. Label of the tax_category (Link) field in DocType 'Address' #. Label of the tax_category (Link) field in DocType 'POS Invoice' #. Label of the tax_category (Link) field in DocType 'POS Profile' #. Label of the tax_category (Link) field in DocType 'Purchase Invoice' @@ -51911,7 +51891,6 @@ msgstr "" #. Label of the tax_category (Link) field in DocType 'Item Tax' #. Label of the tax_category (Link) field in DocType 'Purchase Receipt' #. Label of a Workspace Sidebar Item -#: erpnext/accounts/custom/address.json #: erpnext/accounts/doctype/pos_invoice/pos_invoice.json #: erpnext/accounts/doctype/pos_profile/pos_profile.json #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -51927,6 +51906,7 @@ msgstr "" #: erpnext/selling/doctype/customer/customer.json #: erpnext/selling/doctype/quotation/quotation.json #: erpnext/selling/doctype/sales_order/sales_order.json +#: erpnext/setup/install.py:154 #: erpnext/stock/doctype/delivery_note/delivery_note.json #: erpnext/stock/doctype/item_tax/item_tax.json #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -52013,7 +51993,7 @@ msgstr "" msgid "Tax Rates" msgstr "" -#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:52 +#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:64 msgid "Tax Refunds provided to Tourists under the Tax Refunds for Tourists Scheme" msgstr "" @@ -52031,7 +52011,7 @@ msgstr "" msgid "Tax Rule" msgstr "" -#: erpnext/accounts/doctype/tax_rule/tax_rule.py:137 +#: erpnext/accounts/doctype/tax_rule/tax_rule.py:138 msgid "Tax Rule Conflicts with {0}" msgstr "" @@ -52046,7 +52026,7 @@ msgstr "" msgid "Tax Template" msgstr "" -#: erpnext/accounts/doctype/tax_rule/tax_rule.py:85 +#: erpnext/accounts/doctype/tax_rule/tax_rule.py:86 msgid "Tax Template is mandatory." msgstr "" @@ -52554,7 +52534,6 @@ msgstr "" #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json -#: erpnext/accounts/print_format/sales_invoice_print/sales_invoice_print.html:155 #: erpnext/accounts/workspace/invoicing/invoicing.json #: erpnext/buying/doctype/purchase_order/purchase_order.json #: erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -52638,7 +52617,7 @@ msgstr "" #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/accounts/doctype/territory_item/territory_item.json #: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:142 -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1295 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1218 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js:108 #: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py:182 #: erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js:68 @@ -52836,7 +52815,11 @@ msgstr "" msgid "The company {0} is not in South Africa. VAT Audit Report is only available for companies in South Africa." msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1319 +#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:21 +msgid "The company {0} is not in United Arab Emirates. UAE VAT 201 report is only available for companies in United Arab Emirates." +msgstr "" + +#: erpnext/manufacturing/doctype/job_card/job_card.py:1327 msgid "The completed quantity {0} of an operation {1} cannot be greater than the completed quantity {2} of a previous operation {3}." msgstr "" @@ -52848,7 +52831,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:1229 +#: 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 "" @@ -52910,7 +52893,7 @@ msgstr "" msgid "The following batches are expired, please restock them:
    {0}" msgstr "" -#: erpnext/controllers/accounts_controller.py:426 +#: erpnext/controllers/accounts_controller.py:427 msgid "The following cancelled repost entries exist for {0}:

    {1}

    Kindly delete these entries before continuing." msgstr "" @@ -53009,7 +52992,7 @@ msgstr "" msgid "The original invoice should be consolidated before or along with the return invoice." msgstr "" -#: erpnext/controllers/accounts_controller.py:204 +#: erpnext/controllers/accounts_controller.py:205 msgid "The outstanding amount {0} in {1} is lesser than {2}. Updating the outstanding to this invoice." msgstr "" @@ -53074,7 +53057,7 @@ msgstr "" msgid "The selected item cannot have Batch" msgstr "" -#: erpnext/assets/doctype/asset/asset.js:654 +#: erpnext/assets/doctype/asset/asset.js:656 msgid "The sell quantity is less than the total asset quantity. The remaining quantity will be split into a new asset. This action cannot be undone.

    Do you want to continue?" msgstr "" @@ -53173,19 +53156,19 @@ msgstr "" msgid "The value {0} is already assigned to an existing Item {1}." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1257 +#: 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:1250 +#: 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:1262 +#: 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:892 +#: erpnext/manufacturing/doctype/job_card/job_card.py:895 msgid "The {0} ({1}) must be equal to {2} ({3})" msgstr "" @@ -53205,7 +53188,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:995 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1001 msgid "The {0} {1} is used to calculate the valuation cost for the finished good {2}." msgstr "" @@ -53237,7 +53220,7 @@ msgstr "" msgid "There are no slots available on this date" msgstr "" -#: erpnext/stock/doctype/item/item.js:1177 +#: erpnext/stock/doctype/item/item.js:1164 msgid "There are two options to maintain valuation of stock. FIFO (first in - first out) and Moving Average. To understand this topic in detail please visit Item Valuation, FIFO and Moving Average." msgstr "" @@ -53316,7 +53299,7 @@ msgstr "" msgid "This Purchase Order has been fully subcontracted." msgstr "" -#: erpnext/selling/doctype/sales_order/sales_order.py:2077 +#: erpnext/selling/doctype/sales_order/sales_order.py:2078 msgid "This Sales Order has been fully subcontracted." msgstr "" @@ -53441,11 +53424,11 @@ msgstr "" msgid "This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1243 +#: 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 "" -#: erpnext/stock/doctype/item/item.js:1165 +#: erpnext/stock/doctype/item/item.js:1152 msgid "This is for raw material Items that'll be used to create finished goods. If the Item is an additional service like 'washing' that'll be used in the BOM, keep this unchecked." msgstr "" @@ -53552,7 +53535,7 @@ msgstr "" msgid "This will restrict user access to other employee records" msgstr "" -#: erpnext/controllers/selling_controller.py:886 +#: erpnext/controllers/selling_controller.py:887 msgid "This {} will be treated as material transfer." msgstr "" @@ -53663,7 +53646,7 @@ msgstr "" msgid "Time in mins." msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:871 +#: erpnext/manufacturing/doctype/job_card/job_card.py:872 msgid "Time logs are required for {0} {1}" msgstr "" @@ -53772,7 +53755,7 @@ msgstr "" msgid "To Currency" msgstr "" -#: erpnext/controllers/accounts_controller.py:625 +#: erpnext/controllers/accounts_controller.py:626 #: erpnext/setup/doctype/holiday_list/holiday_list.py:121 msgid "To Date cannot be before From Date" msgstr "" @@ -54046,7 +54029,7 @@ msgid "To include sub-assembly costs and secondary items in Finished Goods on a msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2247 -#: erpnext/controllers/accounts_controller.py:3252 +#: erpnext/controllers/accounts_controller.py:3253 msgid "To include tax in row {0} in Item rate, taxes in rows {1} must also be included" msgstr "" @@ -54335,12 +54318,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:888 +#: erpnext/manufacturing/doctype/job_card/job_card.py:891 #: erpnext/manufacturing/report/job_card_summary/job_card_summary.py:174 msgid "Total Completed Qty" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:190 +#: erpnext/manufacturing/doctype/job_card/job_card.py:191 msgid "Total Completed Qty is required for Job Card {0}, please start and complete the job card before submission" msgstr "" @@ -54614,7 +54597,7 @@ msgstr "" msgid "Total Paid Amount" msgstr "" -#: erpnext/controllers/accounts_controller.py:2800 +#: erpnext/controllers/accounts_controller.py:2801 msgid "Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total" msgstr "" @@ -55077,7 +55060,7 @@ msgstr "" msgid "Transaction Date" msgstr "" -#: erpnext/setup/doctype/company/company.py:1091 +#: erpnext/setup/doctype/company/company.py:1090 msgid "Transaction Deletion Document {0} has been triggered for company {1}" msgstr "" @@ -55101,11 +55084,11 @@ msgstr "" msgid "Transaction Deletion Record To Delete" msgstr "" -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1100 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1101 msgid "Transaction Deletion Record {0} is already running. {1}" msgstr "" -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1119 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:1120 msgid "Transaction Deletion Record {0} is currently deleting {1}. Cannot save documents until deletion completes." msgstr "" @@ -55191,7 +55174,7 @@ msgstr "" msgid "Transaction from which tax is withheld" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:864 +#: erpnext/manufacturing/doctype/job_card/job_card.py:865 msgid "Transaction not allowed against stopped Work Order {0}" msgstr "" @@ -55637,7 +55620,7 @@ msgstr "" #: erpnext/stock/report/item_prices/item_prices.py:55 #: erpnext/stock/report/item_wise_consumption/item_wise_consumption.py:60 #: erpnext/stock/report/product_bundle_balance/product_bundle_balance.py:94 -#: erpnext/stock/report/stock_ageing/stock_ageing.py:184 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:172 #: erpnext/stock/report/stock_analytics/stock_analytics.py:59 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:134 #: erpnext/subcontracting/doctype/subcontracting_inward_order_service_item/subcontracting_inward_order_service_item.json @@ -55698,7 +55681,7 @@ msgstr "" msgid "UOM Conversion Factor" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1468 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1470 msgid "UOM Conversion factor ({0} -> {1}) not found for item: {2}" msgstr "" @@ -55711,7 +55694,7 @@ msgstr "" msgid "UOM Name" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:4012 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:4017 msgid "UOM conversion factor required for UOM: {0} in Item: {1}" msgstr "" @@ -55783,7 +55766,7 @@ msgstr "" msgid "Unable to find score starting at {0}. You need to have standing scores covering 0 to 100" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:1063 +#: erpnext/manufacturing/doctype/work_order/work_order.py:1064 msgid "Unable to find the time slot in the next {0} days for the operation {1}. Please increase the 'Capacity Planning For (Days)' in the {2}." msgstr "" @@ -55855,7 +55838,7 @@ msgstr "" msgid "Under Working Hours table, you can add start and end times for a Workstation. For example, a Workstation may be active from 9 am to 1 pm, then 2 pm to 5 pm. You can also specify the working hours based on shifts. While scheduling a Work Order, the system will check for the availability of the Workstation based on the working hours specified." msgstr "" -#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:935 +#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:936 msgid "Unexpected Naming Series Pattern" msgstr "" @@ -55874,7 +55857,7 @@ msgstr "" msgid "Unit Of Measure" msgstr "" -#: erpnext/controllers/accounts_controller.py:4087 +#: erpnext/controllers/accounts_controller.py:4089 msgid "Unit Price" msgstr "" @@ -56019,7 +56002,7 @@ msgstr "" msgid "Unreconciled Entries" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:952 +#: erpnext/manufacturing/doctype/work_order/work_order.js:934 #: erpnext/selling/doctype/sales_order/sales_order.js:122 #: erpnext/stock/doctype/pick_list/pick_list.js:161 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:193 @@ -56240,7 +56223,7 @@ msgstr "" #. Invoice' #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json -#: erpnext/controllers/accounts_controller.py:197 +#: erpnext/controllers/accounts_controller.py:198 msgid "Update Outstanding for Self" msgstr "" @@ -56323,7 +56306,7 @@ msgstr "" msgid "Updating Variants..." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:1205 +#: erpnext/manufacturing/doctype/work_order/work_order.js:1187 msgid "Updating Work Order status" msgstr "" @@ -56551,7 +56534,7 @@ msgstr "" msgid "Used with Financial Report Template" msgstr "" -#: erpnext/setup/install.py:204 +#: erpnext/setup/install.py:236 msgid "User Forum" msgstr "" @@ -56645,7 +56628,7 @@ msgstr "" msgid "VAT Accounts" msgstr "" -#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:28 +#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:40 msgid "VAT Amount (AED)" msgstr "" @@ -56655,12 +56638,12 @@ msgid "VAT Audit Report" msgstr "" #: erpnext/regional/report/uae_vat_201/uae_vat_201.html:47 -#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:111 +#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:123 msgid "VAT on Expenses and All Other Inputs" msgstr "" #: erpnext/regional/report/uae_vat_201/uae_vat_201.html:15 -#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:45 +#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:57 msgid "VAT on Sales and All Other Outputs" msgstr "" @@ -56885,11 +56868,11 @@ msgstr "" msgid "Valuation Rate (In / Out)" msgstr "" -#: erpnext/stock/stock_ledger.py:2075 +#: erpnext/stock/stock_ledger.py:2041 msgid "Valuation Rate Missing" msgstr "" -#: erpnext/stock/stock_ledger.py:2053 +#: erpnext/stock/stock_ledger.py:2019 msgid "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}." msgstr "" @@ -56921,7 +56904,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:3276 +#: erpnext/controllers/accounts_controller.py:3277 msgid "Valuation type charges can not be marked as Inclusive" msgstr "" @@ -56933,7 +56916,7 @@ msgstr "" msgid "Value (G - D)" msgstr "" -#: erpnext/stock/report/stock_ageing/stock_ageing.py:227 +#: erpnext/stock/report/stock_ageing/stock_ageing.py:215 msgid "Value ({0})" msgstr "" @@ -57099,7 +57082,7 @@ msgstr "" msgid "Variant Of" msgstr "" -#: erpnext/stock/doctype/item/item.js:857 +#: erpnext/stock/doctype/item/item.js:844 msgid "Variant creation has been queued." msgstr "" @@ -57427,7 +57410,7 @@ msgstr "" #: erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.json #: erpnext/accounts/doctype/repost_payment_ledger_items/repost_payment_ledger_items.json #: erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.json -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1250 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1173 #: erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js:56 #: erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py:221 #: erpnext/accounts/report/general_ledger/general_ledger.js:49 @@ -57500,7 +57483,7 @@ msgstr "" #: erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json #: erpnext/accounts/doctype/repost_payment_ledger_items/repost_payment_ledger_items.json #: erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.json -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1248 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1171 #: erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py:212 #: erpnext/accounts/report/general_ledger/general_ledger.py:753 #: erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py:31 @@ -57733,7 +57716,7 @@ msgstr "" msgid "Warehouse {0} does not exist" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:246 +#: erpnext/manufacturing/doctype/work_order/work_order.py:247 msgid "Warehouse {0} is not allowed for Sales Order {1}, it should be {2}" msgstr "" @@ -57870,7 +57853,7 @@ msgstr "" msgid "Warning: Material Requested Qty is less than Minimum Order Qty" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:1482 +#: erpnext/manufacturing/doctype/work_order/work_order.py:1483 msgid "Warning: Quantity exceeds maximum producible quantity based on quantity of raw materials received through the Subcontracting Inward Order {0}." msgstr "" @@ -57964,7 +57947,7 @@ msgstr "" msgid "Wavelength In Megametres" msgstr "" -#: erpnext/controllers/accounts_controller.py:192 +#: erpnext/controllers/accounts_controller.py:193 msgid "We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, uncheck the '{2}' checkbox." msgstr "" @@ -58141,7 +58124,7 @@ msgstr "" msgid "When checked, the system will use the posting datetime of the document for naming the document instead of the creation datetime of the document." msgstr "" -#: erpnext/stock/doctype/item/item.js:1184 +#: erpnext/stock/doctype/item/item.js:1171 msgid "When creating an Item, entering a value for this field will automatically create an Item Price at the backend." msgstr "" @@ -58390,12 +58373,12 @@ msgstr "" msgid "Work Order cannot be created for following reason:
    {0}" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:1426 +#: erpnext/manufacturing/doctype/work_order/work_order.py:1427 msgid "Work Order cannot be raised against a Item Template" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:2502 -#: erpnext/manufacturing/doctype/work_order/work_order.py:2582 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2503 +#: erpnext/manufacturing/doctype/work_order/work_order.py:2583 msgid "Work Order has been {0}" msgstr "" @@ -58441,7 +58424,7 @@ msgstr "" msgid "Work-in-Progress Warehouse" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:791 +#: erpnext/manufacturing/doctype/work_order/work_order.py:792 msgid "Work-in-Progress Warehouse is required before Submit" msgstr "" @@ -58489,7 +58472,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:344 +#: erpnext/manufacturing/doctype/work_order/work_order.js:325 #: 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 @@ -58745,11 +58728,11 @@ msgstr "" msgid "You are importing data for the code list:" msgstr "" -#: erpnext/controllers/accounts_controller.py:3884 +#: erpnext/controllers/accounts_controller.py:3886 msgid "You are not allowed to update as per the conditions set in {} Workflow." msgstr "" -#: erpnext/accounts/general_ledger.py:811 +#: erpnext/accounts/general_ledger.py:813 msgid "You are not authorized to add or update entries before {0}" msgstr "" @@ -58810,11 +58793,11 @@ msgstr "" msgid "You can set it as a machine name or operation type. For example, stiching machine 12" msgstr "" -#: erpnext/controllers/accounts_controller.py:213 +#: erpnext/controllers/accounts_controller.py:214 msgid "You can use {0} to reconcile against {1} later." msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1331 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1339 msgid "You can't make any changes to Job Card since Work Order is closed." msgstr "" @@ -58838,7 +58821,7 @@ msgstr "" msgid "You cannot create or cancel any accounting entries with in the closed Accounting Period {0}" msgstr "" -#: erpnext/accounts/general_ledger.py:831 +#: erpnext/accounts/general_ledger.py:833 msgid "You cannot create/amend any accounting entries till this date." msgstr "" @@ -58890,7 +58873,7 @@ msgstr "" msgid "You do not have permission to edit this document" msgstr "" -#: erpnext/controllers/accounts_controller.py:3860 +#: erpnext/controllers/accounts_controller.py:3862 msgid "You do not have permissions to {} items in a {}." msgstr "" @@ -58902,11 +58885,11 @@ msgstr "" msgid "You don't have enough points to redeem." msgstr "" -#: erpnext/controllers/accounts_controller.py:4432 +#: erpnext/controllers/accounts_controller.py:4434 msgid "You don't have permission to create a Company Address. Please contact your System Manager." msgstr "" -#: erpnext/controllers/accounts_controller.py:4412 +#: erpnext/controllers/accounts_controller.py:4414 msgid "You don't have permission to update Company details. Please contact your System Manager." msgstr "" @@ -58914,7 +58897,7 @@ msgstr "" msgid "You don't have permission to update Received Qty DocField for item {0}" msgstr "" -#: erpnext/controllers/accounts_controller.py:4406 +#: erpnext/controllers/accounts_controller.py:4408 msgid "You don't have permission to update this document. Please contact your System Manager." msgstr "" @@ -58958,7 +58941,7 @@ msgstr "" msgid "You need to cancel POS Closing Entry {} to be able to cancel this document." msgstr "" -#: erpnext/controllers/accounts_controller.py:3227 +#: erpnext/controllers/accounts_controller.py:3228 msgid "You selected the account group {1} as {2} Account in row {0}. Please select a single account." msgstr "" @@ -59005,7 +58988,7 @@ msgstr "" msgid "Zero Balance" msgstr "" -#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:65 +#: erpnext/regional/report/uae_vat_201/uae_vat_201.py:77 msgid "Zero Rated" msgstr "" @@ -59035,7 +59018,7 @@ msgstr "" msgid "`Allow Negative rates for Items`" msgstr "" -#: erpnext/stock/stock_ledger.py:2067 +#: erpnext/stock/stock_ledger.py:2033 msgid "after" msgstr "" @@ -59219,7 +59202,7 @@ msgstr "" msgid "per hour" msgstr "" -#: erpnext/stock/stock_ledger.py:2068 +#: erpnext/stock/stock_ledger.py:2034 msgid "performing either one below:" msgstr "" @@ -59341,7 +59324,7 @@ msgstr "" msgid "you must select Capital Work in Progress Account in accounts table" msgstr "" -#: erpnext/controllers/accounts_controller.py:1285 +#: erpnext/controllers/accounts_controller.py:1286 msgid "{0} '{1}' is disabled" msgstr "" @@ -59349,7 +59332,7 @@ msgstr "" msgid "{0} '{1}' not in Fiscal Year {2}" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:677 +#: erpnext/manufacturing/doctype/work_order/work_order.py:678 msgid "{0} ({1}) cannot be greater than planned quantity ({2}) in Work Order {3}" msgstr "" @@ -59357,7 +59340,7 @@ msgstr "" msgid "{0} {1} has submitted Assets. Remove Item {2} from table to continue." msgstr "" -#: erpnext/controllers/accounts_controller.py:2382 +#: erpnext/controllers/accounts_controller.py:2383 msgid "{0} Account not found against Customer {1}." msgstr "" @@ -59390,11 +59373,11 @@ msgstr "" msgid "{0} Number {1} is already used in {2} {3}" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:1680 +#: erpnext/manufacturing/doctype/bom/bom.py:1694 msgid "{0} Operating Cost for operation {1}" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:578 +#: erpnext/manufacturing/doctype/work_order/work_order.js:560 msgid "{0} Operations: {1}" msgstr "" @@ -59498,7 +59481,7 @@ msgstr "" msgid "{0} does not belong to Company {1}" msgstr "" -#: erpnext/controllers/accounts_controller.py:352 +#: erpnext/controllers/accounts_controller.py:353 msgid "{0} does not belong to the Company {1}." msgstr "" @@ -59532,7 +59515,7 @@ msgstr "" msgid "{0} hours" msgstr "" -#: erpnext/controllers/accounts_controller.py:2740 +#: erpnext/controllers/accounts_controller.py:2741 msgid "{0} in row {1}" msgstr "" @@ -59554,7 +59537,7 @@ msgstr "" msgid "{0} is already running for {1}" msgstr "" -#: erpnext/controllers/accounts_controller.py:174 +#: erpnext/controllers/accounts_controller.py:175 msgid "{0} is blocked so this transaction cannot proceed" msgstr "" @@ -59567,7 +59550,7 @@ msgid "{0} is mandatory for Item {1}" msgstr "" #: erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py:100 -#: erpnext/accounts/general_ledger.py:855 +#: erpnext/accounts/general_ledger.py:857 msgid "{0} is mandatory for account {1}" msgstr "" @@ -59575,7 +59558,7 @@ msgstr "" msgid "{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}" msgstr "" -#: erpnext/controllers/accounts_controller.py:3184 +#: erpnext/controllers/accounts_controller.py:3185 msgid "{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}." msgstr "" @@ -59631,27 +59614,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:543 +#: erpnext/manufacturing/doctype/work_order/work_order.js:525 msgid "{0} items disassembled" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:507 +#: erpnext/manufacturing/doctype/work_order/work_order.js:489 msgid "{0} items in progress" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:531 +#: erpnext/manufacturing/doctype/work_order/work_order.js:513 msgid "{0} items lost during process." msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:488 +#: erpnext/manufacturing/doctype/work_order/work_order.js:470 msgid "{0} items produced" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:511 +#: erpnext/manufacturing/doctype/work_order/work_order.js:493 msgid "{0} items returned" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.js:514 +#: erpnext/manufacturing/doctype/work_order/work_order.js:496 msgid "{0} items to return" msgstr "" @@ -59695,16 +59678,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:1720 erpnext/stock/stock_ledger.py:2216 -#: erpnext/stock/stock_ledger.py:2230 +#: erpnext/stock/stock_ledger.py:1686 erpnext/stock/stock_ledger.py:2182 +#: erpnext/stock/stock_ledger.py:2196 msgid "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction." msgstr "" -#: erpnext/stock/stock_ledger.py:2317 erpnext/stock/stock_ledger.py:2362 +#: erpnext/stock/stock_ledger.py:2283 erpnext/stock/stock_ledger.py:2328 msgid "{0} units of {1} needed in {2} on {3} {4} to complete this transaction." msgstr "" -#: erpnext/stock/stock_ledger.py:1714 +#: erpnext/stock/stock_ledger.py:1680 msgid "{0} units of {1} needed in {2} to complete this transaction." msgstr "" @@ -59716,7 +59699,7 @@ msgstr "" msgid "{0} valid serial nos for Item {1}" msgstr "" -#: erpnext/stock/doctype/item/item.js:862 +#: erpnext/stock/doctype/item/item.js:849 msgid "{0} variants created." msgstr "" @@ -59732,7 +59715,7 @@ msgstr "" msgid "{0} will be set as the {1} in subsequently scanned items" msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1004 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1010 msgid "{0} {1}" msgstr "" @@ -59930,8 +59913,8 @@ msgstr "" msgid "{0}'s {1} cannot be after {2}'s Expected End Date." msgstr "" -#: erpnext/manufacturing/doctype/job_card/job_card.py:1303 #: erpnext/manufacturing/doctype/job_card/job_card.py:1311 +#: erpnext/manufacturing/doctype/job_card/job_card.py:1319 msgid "{0}, complete the operation {1} before the operation {2}." msgstr "" @@ -59951,11 +59934,11 @@ msgstr "" msgid "{0}: Virtual DocType (no database table)" msgstr "" -#: erpnext/controllers/accounts_controller.py:542 +#: erpnext/controllers/accounts_controller.py:543 msgid "{0}: {1} does not belong to the Company: {2}" msgstr "" -#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1400 +#: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1323 msgid "{0}: {1} does not exist" msgstr "" diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index ea86472cf46..1619a0053a9 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1638,12 +1638,12 @@ def add_non_stock_items_cost(stock_entry, work_order, expense_account, job_card= ) -def add_operating_cost_component_wise( - stock_entry, work_order=None, consumed_operating_cost=None, op_expense_account=None, job_card=None -): +def add_operating_cost_component_wise(stock_entry, work_order=None, op_expense_account=None, job_card=None): if not work_order: return False + from erpnext.stock.doctype.stock_entry.stock_entry import get_consumed_operating_cost + cost_added = False for row in work_order.operations: if job_card and job_card.operation_id != row.name: @@ -1661,18 +1661,32 @@ def add_operating_cost_component_wise( }, ) + consumed_operating_cost = ( + get_consumed_operating_cost(work_order.name, stock_entry.bom_no, row.name) or [] + ) for wc in workstation_cost: expense_account = ( get_component_account(wc.operating_component, stock_entry.company) or op_expense_account ) + consumed_op_cost = next( + ( + cost + for cost in consumed_operating_cost + if cost.get("operating_component") == wc.operating_component + ), + {}, + ) actual_cp_operating_cost = flt( - flt(wc.operating_cost) * flt(flt(row.actual_operation_time) / 60.0) - consumed_operating_cost, + flt(wc.operating_cost) * flt(flt(row.actual_operation_time) / 60.0) + - flt(consumed_op_cost.get("consumed_cost")), row.precision("actual_operating_cost"), ) - per_unit_cost = flt(actual_cp_operating_cost) / flt(row.completed_qty - work_order.produced_qty) + remaining_qty = row.completed_qty - consumed_op_cost.get("consumed_qty", 0) + per_unit_cost = actual_cp_operating_cost / (remaining_qty or 1) + operating_cost = per_unit_cost * stock_entry.fg_completed_qty - if per_unit_cost: + if actual_cp_operating_cost: stock_entry.append( "additional_costs", { @@ -1680,8 +1694,14 @@ def add_operating_cost_component_wise( "description": _("{0} Operating Cost for operation {1}").format( wc.operating_component, row.operation ), - "amount": per_unit_cost * flt(stock_entry.fg_completed_qty), + "amount": flt( + min(operating_cost, actual_cp_operating_cost), + frappe.get_precision("Landed Cost Taxes and Charges", "amount"), + ), "has_operating_cost": 1, + "operation_id": row.name, + "operating_component": wc.operating_component, + "qty": min(remaining_qty, stock_entry.fg_completed_qty), }, ) @@ -1699,17 +1719,15 @@ def get_component_account(parent, company): def add_operations_cost(stock_entry, work_order=None, expense_account=None, job_card=None): from erpnext.stock.doctype.stock_entry.stock_entry import ( - get_consumed_operating_cost, - get_operating_cost_per_unit, + get_remaining_operating_cost, ) - operating_cost_per_unit = get_operating_cost_per_unit(work_order, stock_entry.bom_no) + remaining_operating_cost = get_remaining_operating_cost(work_order, stock_entry.bom_no) - if operating_cost_per_unit: + if remaining_operating_cost: cost_added = add_operating_cost_component_wise( stock_entry, work_order, - get_consumed_operating_cost(work_order.name, stock_entry.bom_no), expense_account, job_card=job_card, ) @@ -1720,7 +1738,10 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None, job_ { "expense_account": expense_account, "description": _("Operating Cost as per Work Order / BOM"), - "amount": operating_cost_per_unit * flt(stock_entry.fg_completed_qty), + "amount": flt( + remaining_operating_cost * stock_entry.fg_completed_qty, + frappe.get_precision("Landed Cost Taxes and Charges", "amount"), + ), "has_operating_cost": 1, }, ) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 19132ecf9fd..795136d2374 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -2,89 +2,83 @@ // For license information, please see license.txt frappe.ui.form.on("Job Card", { - setup: function (frm) { - frm.set_query("operation", function () { - return { - query: "erpnext.manufacturing.doctype.job_card.job_card.get_operations", - filters: { - work_order: frm.doc.work_order, - }, - }; - }); + setup(frm) { + frm.set_query("operation", () => ({ + query: "erpnext.manufacturing.doctype.job_card.job_card.get_operations", + filters: { work_order: frm.doc.work_order }, + })); - frm.set_query("serial_and_batch_bundle", () => { - return { - filters: { - item_code: frm.doc.production_item, - voucher_type: frm.doc.doctype, - voucher_no: ["in", [frm.doc.name, ""]], - is_cancelled: 0, - }, - }; - }); + frm.set_query("serial_and_batch_bundle", () => ({ + filters: { + item_code: frm.doc.production_item, + voucher_type: frm.doc.doctype, + voucher_no: ["in", [frm.doc.name, ""]], + is_cancelled: 0, + }, + })); - frm.set_query("item_code", "secondary_items", () => { - return { - filters: { - disabled: 0, - }, - }; - }); + frm.set_query("item_code", "secondary_items", () => ({ + filters: { disabled: 0 }, + })); frm.set_query("operation", "time_logs", () => { - let operations = (frm.doc.sub_operations || []).map((d) => d.sub_operation); - return { - filters: { - name: ["in", operations], - }, - }; + const operations = (frm.doc.sub_operations || []).map((d) => d.sub_operation); + return { filters: { name: ["in", operations] } }; }); - frm.set_query("work_order", function () { - return { - filters: { - status: ["not in", ["Cancelled", "Closed", "Stopped"]], - }, - }; - }); + frm.set_query("work_order", () => ({ + 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"); - frm.set_query("source_warehouse", "items", () => { - return { - filters: { - company: frm.doc.company, - }, - }; + + frm.set_query("source_warehouse", "items", () => ({ + filters: { company: frm.doc.company }, + })); + + frm.set_indicator_formatter("sub_operation", (doc) => { + if (doc.status === "Pending") return "red"; + return doc.status === "Complete" ? "green" : "orange"; }); - frm.set_indicator_formatter("sub_operation", function (doc) { - if (doc.status == "Pending") { - return "red"; - } else { - return doc.status === "Complete" ? "green" : "orange"; - } - }); + frm.set_query("employee", () => ({ + filters: { + company: frm.doc.company, + status: "Active", + }, + })); + }, - frm.set_query("employee", () => { - return { - filters: { - company: frm.doc.company, - status: "Active", - }, - }; - }); + pending_qty(frm) { + if (frm.doc.total_completed_qty <= 0.0) { + frm.doc.pending_qty = 0.0; + refresh_field("pending_qty"); + frappe.throw(__("Please complete the job first before entering Pending Quantity")); + } + + if (frm.doc.pending_qty < 0) { + frappe.throw(__("Pending Quantity cannot be less than 0")); + } + + const remaining_qty = flt(frm.doc.for_quantity) - flt(frm.doc.total_completed_qty); + + if (remaining_qty < frm.doc.pending_qty) { + frm.doc.pending_qty = 0.0; + refresh_field("pending_qty"); + frappe.throw(__("Pending Quantity cannot be greater than {0}", [remaining_qty])); + } + + const process_loss_qty = flt(remaining_qty) - flt(frm.doc.pending_qty); + frm.doc.process_loss_qty = process_loss_qty >= 0 ? process_loss_qty : 0; + refresh_field("process_loss_qty"); }, set_company_filters(frm, fieldname) { - frm.set_query(fieldname, () => { - return { - filters: { - company: frm.doc.company, - }, - }; - }); + frm.set_query(fieldname, () => ({ + filters: { company: frm.doc.company }, + })); }, make_fields_read_only(frm) { @@ -99,33 +93,29 @@ frappe.ui.form.on("Job Card", { }, setup_stock_entry(frm) { - if ( - frm.doc.track_semi_finished_goods && - frm.doc.docstatus === 1 && - !frm.doc.is_subcontracted && - (frm.doc.skip_material_transfer || frm.doc.transferred_qty > 0) && - 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( - __("Do you want to submit the stock entry?"), - () => { - frm.events.make_manufacture_stock_entry(frm, 1); - }, - () => { - frm.events.make_manufacture_stock_entry(frm, 0); - } - ); - }).addClass("btn-primary"); - } + const { doc } = frm; + const can_make_stock_entry = + doc.track_semi_finished_goods && + doc.docstatus === 1 && + !doc.is_subcontracted && + (doc.skip_material_transfer || doc.transferred_qty > 0) && + flt(doc.manufactured_qty) + flt(doc.process_loss_qty) < flt(doc.for_quantity); + + if (!can_make_stock_entry) return; + + frm.add_custom_button(__("Make Stock Entry"), () => { + frappe.confirm( + __("Do you want to submit the stock entry?"), + () => frm.events.make_manufacture_stock_entry(frm, 1), + () => frm.events.make_manufacture_stock_entry(frm, 0) + ); + }).addClass("btn-primary"); }, make_manufacture_stock_entry(frm, submit_entry) { frm.call({ method: "make_stock_entry_for_semi_fg_item", - args: { - auto_submit: submit_entry, - }, + args: { auto_submit: submit_entry }, doc: frm.doc, freeze: true, callback() { @@ -134,190 +124,134 @@ frappe.ui.form.on("Job Card", { }); }, - refresh: function (frm) { - frm.trigger("setup_stock_entry"); + refresh(frm) { + const { doc } = frm; + const has_items = doc.items && doc.items.length; + + // Clear any running timer tick from a previous render. + if (frm._jcd_timer_interval) { + clearInterval(frm._jcd_timer_interval); + frm._jcd_timer_interval = null; + } - let has_items = frm.doc.items && frm.doc.items.length; frm.trigger("make_fields_read_only"); - if (!frm.is_new() && frm.doc.__onload?.work_order_closed) { + if (!frm.is_new() && doc.__onload?.work_order_closed) { frm.disable_save(); return; } - if (frm.doc.is_subcontracted) { + if (doc.is_subcontracted) { frm.trigger("make_subcontracting_po"); return; } - let has_stock_entry = frm.doc.__onload && frm.doc.__onload.has_stock_entry ? true : false; + if (doc.docstatus > 0) { + frm.set_df_property("pending_qty", "read_only", 1); + } + const has_stock_entry = !!doc.__onload?.has_stock_entry; frm.toggle_enable("for_quantity", !has_stock_entry); - if (frm.doc.docstatus != 0) { + if (doc.docstatus != 0) { frm.fields_dict["time_logs"].grid.update_docfield_property("completed_qty", "read_only", 1); frm.fields_dict["time_logs"].grid.update_docfield_property("time_in_mins", "read_only", 1); } - if (!frm.is_new() && !frm.doc.skip_material_transfer && frm.doc.docstatus < 2) { - let to_request = frm.doc.for_quantity > frm.doc.transferred_qty; - let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer; + frm.events.setup_material_transfer_buttons(frm, has_items); - if (has_items && (to_request || excess_transfer_allowed)) { - frm.add_custom_button( - __("Material Request"), - () => { - frm.trigger("make_material_request"); - }, - __("Create") - ); - } - - // check if any row has untransferred materials - // in case of multiple items in JC - let to_transfer = frm.doc.items.some((row) => row.transferred_qty < row.required_qty); - - if (has_items && (to_transfer || excess_transfer_allowed)) { - frm.add_custom_button( - __("Material Transfer"), - () => { - frm.trigger("make_stock_entry"); - }, - __("Create") - ); - } - } - - if (frm.doc.docstatus == 1 && !frm.doc.is_corrective_job_card && !frm.doc.finished_good) { + if (doc.docstatus == 1 && !doc.is_corrective_job_card && !doc.finished_good) { frm.trigger("setup_corrective_job_card"); } - frm.set_query("quality_inspection", function () { - return { - query: "erpnext.stock.doctype.quality_inspection.quality_inspection.quality_inspection_query", - filters: { - item_code: frm.doc.production_item, - reference_name: frm.doc.name, - }, - }; - }); + frm.set_query("quality_inspection", () => ({ + query: "erpnext.stock.doctype.quality_inspection.quality_inspection.quality_inspection_query", + filters: { + item_code: doc.production_item, + reference_name: doc.name, + }, + })); frm.trigger("toggle_operation_number"); - if ( - frm.doc.for_quantity + frm.doc.process_loss_qty > frm.doc.total_completed_qty && - (frm.doc.skip_material_transfer || - frm.doc.transferred_qty >= frm.doc.for_quantity + frm.doc.process_loss_qty || - !frm.doc.finished_good || - !has_items?.length) - ) { - let last_row = {}; - if (frm.doc.sub_operations?.length && frm.doc.time_logs?.length) { - last_row = get_last_row(frm.doc.time_logs); - } + const is_timer_running = frm.events.setup_job_action_buttons(frm, has_items); - if ( - (!frm.doc.time_logs?.length || (frm.doc.sub_operations?.length && last_row?.to_time)) && - !frm.doc.is_paused - ) { - frm.add_custom_button(__("Start Job"), () => { - let from_time = frappe.datetime.now_datetime(); - if ((frm.doc.employee && !frm.doc.employee.length) || !frm.doc.employee) { - frappe.prompt( - { - fieldtype: "Table MultiSelect", - label: __("Select Employees"), - options: "Job Card Time Log", - fieldname: "employees", - reqd: 1, - filters: { - status: "Active", - }, - }, - (d) => { - frm.events.start_timer(frm, from_time, d.employees); - }, - __("Assign Job to Employee") - ); - } else { - frm.events.start_timer(frm, from_time, frm.doc.employee); - } - }); - } else if (frm.doc.is_paused) { - frm.add_custom_button(__("Resume Job"), () => { - frm.call({ - method: "resume_job", - doc: frm.doc, - args: { - start_time: frappe.datetime.now_datetime(), - }, - callback() { - frm.reload_doc(); - }, - }); - }); - } else { - let manufactured_qty = frm.doc.manufactured_qty || frm.doc.total_completed_qty; - if (frm.doc.for_quantity - (manufactured_qty + frm.doc.process_loss_qty) > 0) { - if (!frm.doc.is_paused) { - frm.add_custom_button(__("Pause Job"), () => { - frm.call({ - method: "pause_job", - doc: frm.doc, - args: { - end_time: frappe.datetime.now_datetime(), - }, - callback() { - frm.reload_doc(); - }, - }); - }); - } - - frm.add_custom_button(__("Complete Job"), () => { - frm.trigger("complete_job_card"); - }); - } - - frm.trigger("make_dashboard"); - } + if (!is_timer_running) { + frm.trigger("setup_stock_entry"); } frm.trigger("setup_quality_inspection"); - if (frm.doc.work_order) { - frappe.db.get_value("Work Order", frm.doc.work_order, "transfer_material_against").then((r) => { - if (r.message.transfer_material_against == "Work Order" && !frm.doc.operation_row_id) { + if (doc.work_order) { + frappe.db.get_value("Work Order", doc.work_order, "transfer_material_against").then((r) => { + if (r.message.transfer_material_against == "Work Order" && !doc.operation_row_id) { frm.set_df_property("items", "hidden", 1); } }); } - let sbb_field = frm.get_docfield("serial_and_batch_bundle"); + const sbb_field = frm.get_docfield("serial_and_batch_bundle"); if (sbb_field) { - sbb_field.get_route_options_for_new_doc = () => { - return { - item_code: frm.doc.production_item, - warehouse: frm.doc.wip_warehouse, - voucher_type: frm.doc.doctype, - }; - }; + sbb_field.get_route_options_for_new_doc = () => ({ + item_code: doc.production_item, + warehouse: doc.wip_warehouse, + voucher_type: doc.doctype, + }); } }, + // Adds Material Request and Material Transfer buttons when items need to be transferred. + setup_material_transfer_buttons(frm, has_items) { + const { doc } = frm; + + if (frm.is_new() || doc.skip_material_transfer || doc.docstatus >= 2) return; + + const excess_transfer_allowed = doc.__onload.job_card_excess_transfer; + const to_request = doc.for_quantity > doc.transferred_qty; + + if (has_items && (to_request || excess_transfer_allowed)) { + frm.add_custom_button( + __("Material Request"), + () => frm.trigger("make_material_request"), + __("Create") + ); + } + + // check if any row has untransferred materials in case of multiple items in JC + const to_transfer = doc.items.some((row) => row.transferred_qty < row.required_qty); + + if (has_items && (to_transfer || excess_transfer_allowed)) { + frm.add_custom_button( + __("Material Transfer"), + () => frm.trigger("make_stock_entry"), + __("Create") + ); + } + }, + + // Renders the dashboard widget (info + timer + action buttons) into job_card_dashboard wrapper. + // Returns true if the job timer is actively running, so the caller can skip the stock entry button. + setup_job_action_buttons(frm, has_items) { + return frm.events.make_dashboard(frm, has_items); + }, + complete_job_card(frm) { - let fields = [ + let pending_qty = frm.doc.for_quantity - frm.doc.total_completed_qty; + if (frm.doc.pending_qty > 0) { + pending_qty = frm.doc.pending_qty; + } + + const fields = [ { fieldtype: "Float", label: __("Qty to Manufacture"), fieldname: "for_quantity", reqd: 1, - default: frm.doc.for_quantity, + default: pending_qty, change() { - let doc = frm.job_completion_dialog; - - doc.set_value("completed_qty", doc.get_value("for_quantity")); - doc.set_value("process_loss_qty", 0); + const dialog = frm.job_completion_dialog; + dialog.set_value("completed_qty", dialog.get_value("for_quantity")); + dialog.set_value("process_loss_qty", 0); }, }, { @@ -325,13 +259,28 @@ frappe.ui.form.on("Job Card", { label: __("Completed Quantity"), fieldname: "completed_qty", reqd: 1, - default: frm.doc.for_quantity - frm.doc.total_completed_qty, + default: pending_qty, change() { - let doc = frm.job_completion_dialog; - - let process_loss_qty = doc.get_value("for_quantity") - doc.get_value("completed_qty"); - if (process_loss_qty > 0 && process_loss_qty != doc.get_value("process_loss_qty")) { - doc.set_value("process_loss_qty", process_loss_qty); + const dialog = frm.job_completion_dialog; + const remaining = dialog.get_value("for_quantity") - dialog.get_value("completed_qty"); + if (remaining > 0 && remaining != dialog.get_value("pending_qty")) { + dialog.set_value("pending_qty", remaining); + } + }, + }, + { + fieldtype: "Float", + label: __("Pending Quantity"), + fieldname: "pending_qty", + default: 0.0, + change() { + const dialog = frm.job_completion_dialog; + const process_loss_qty = + dialog.get_value("for_quantity") - + dialog.get_value("completed_qty") - + dialog.get_value("pending_qty"); + if (process_loss_qty >= 0 && process_loss_qty != dialog.get_value("process_loss_qty")) { + dialog.set_value("process_loss_qty", process_loss_qty); } }, }, @@ -340,10 +289,14 @@ frappe.ui.form.on("Job Card", { label: __("Process Loss Quantity"), fieldname: "process_loss_qty", onchange() { - let doc = frm.job_completion_dialog; - - let completed_qty = doc.get_value("for_quantity") - doc.get_value("process_loss_qty"); - doc.set_value("completed_qty", completed_qty); + const dialog = frm.job_completion_dialog; + const remaining = + dialog.get_value("for_quantity") - + dialog.get_value("completed_qty") - + dialog.get_value("process_loss_qty"); + if (remaining >= 0 && remaining != dialog.get_value("pending_qty")) { + dialog.set_value("pending_qty", remaining); + } }, }, { @@ -358,20 +311,16 @@ frappe.ui.form.on("Job Card", { fieldname: "sub_operation", options: "Operation", get_query() { - let non_completed_operations = frm.doc.sub_operations.filter( - (d) => d.status === "Pending" - ); + const non_completed = frm.doc.sub_operations.filter((d) => d.status === "Pending"); return { - filters: { - name: ["in", non_completed_operations.map((d) => d.sub_operation)], - }, + filters: { name: ["in", non_completed.map((d) => d.sub_operation)] }, }; }, reqd: 1, }); } - let last_completed_row = get_last_completed_row(frm.doc.time_logs); + const last_completed_row = get_last_completed_row(frm.doc.time_logs); let last_row = {}; if (frm.doc.sub_operations?.length && frm.doc.time_logs?.length) { last_row = get_last_row(frm.doc.time_logs); @@ -399,10 +348,12 @@ frappe.ui.form.on("Job Card", { args: { qty: data.completed_qty, for_quantity: data.for_quantity, + pending_qty: data.pending_qty, + process_loss_qty: data.process_loss_qty, end_time: data.end_time, sub_operation: data.sub_operation, }, - callback: function (r) { + callback() { frm.reload_doc(); }, }); @@ -428,19 +379,15 @@ frappe.ui.form.on("Job Card", { frm.call({ method: "start_timer", doc: frm.doc, - args: { - start_time: start_time, - employees: employees, - }, - callback: function (r) { + args: { start_time, employees }, + callback() { frm.reload_doc(); - frm.trigger("make_dashboard"); }, }); }, make_finished_good(frm) { - let fields = [ + const fields = [ { fieldtype: "Float", label: __("Completed Quantity"), @@ -466,12 +413,9 @@ frappe.ui.form.on("Job Card", { frm.call({ method: "make_finished_good", doc: frm.doc, - args: { - qty: data.qty, - end_time: data.end_time, - }, - callback: function (r) { - var doc = frappe.model.sync(r.message); + args: { qty: data.qty, end_time: data.end_time }, + callback(r) { + const doc = frappe.model.sync(r.message); frappe.set_route("Form", doc[0].doctype, doc[0].name); }, }); @@ -482,8 +426,8 @@ frappe.ui.form.on("Job Card", { ); }, - setup_quality_inspection: function (frm) { - let quality_inspection_field = frm.get_docfield("quality_inspection"); + setup_quality_inspection(frm) { + const quality_inspection_field = frm.get_docfield("quality_inspection"); quality_inspection_field.get_route_options_for_new_doc = function (frm) { return { inspection_type: "In Process", @@ -498,24 +442,22 @@ frappe.ui.form.on("Job Card", { }; }, - setup_corrective_job_card: function (frm) { + setup_corrective_job_card(frm) { frm.add_custom_button( __("Corrective Job Card"), () => { - let operations = frm.doc.sub_operations.map((d) => d.sub_operation).concat(frm.doc.operation); + const operations = frm.doc.sub_operations + .map((d) => d.sub_operation) + .concat(frm.doc.operation); - let fields = [ + const fields = [ { fieldtype: "Link", label: __("Corrective Operation"), options: "Operation", fieldname: "operation", get_query() { - return { - filters: { - is_corrective_operation: 1, - }, - }; + return { filters: { is_corrective_operation: 1 } }; }, }, { @@ -524,20 +466,14 @@ frappe.ui.form.on("Job Card", { options: "Operation", fieldname: "for_operation", get_query() { - return { - filters: { - name: ["in", operations], - }, - }; + return { filters: { name: ["in", operations] } }; }, }, ]; frappe.prompt( fields, - (d) => { - frm.events.make_corrective_job_card(frm, d.operation, d.for_operation); - }, + (d) => frm.events.make_corrective_job_card(frm, d.operation, d.for_operation), __("Select Corrective Operation") ); }, @@ -545,7 +481,7 @@ frappe.ui.form.on("Job Card", { ); }, - make_corrective_job_card: function (frm, operation, for_operation) { + make_corrective_job_card(frm, operation, for_operation) { frappe.call({ method: "erpnext.manufacturing.doctype.job_card.job_card.make_corrective_job_card", args: { @@ -553,7 +489,7 @@ frappe.ui.form.on("Job Card", { operation: operation, for_operation: for_operation, }, - callback: function (r) { + callback(r) { if (r.message) { frappe.model.sync(r.message); frappe.set_route("Form", r.message.doctype, r.message.name); @@ -562,7 +498,7 @@ frappe.ui.form.on("Job Card", { }); }, - operation: function (frm) { + operation(frm) { frm.trigger("toggle_operation_number"); if (frm.doc.operation && frm.doc.work_order) { @@ -572,28 +508,22 @@ frappe.ui.form.on("Job Card", { work_order: frm.doc.work_order, operation: frm.doc.operation, }, - callback: function (r) { - if (r.message) { - if (r.message.length == 1) { - frm.set_value("operation_id", r.message[0].name); - } else { - let args = []; + callback(r) { + if (!r.message) return; - r.message.forEach((row) => { - args.push({ label: row.idx, value: row.name }); - }); - - let description = __("Operation {0} added multiple times in the work order {1}", [ - frm.doc.operation, - frm.doc.work_order, - ]); - - frm.set_df_property("operation_row_number", "options", args); - frm.set_df_property("operation_row_number", "description", description); - } - - frm.trigger("toggle_operation_number"); + if (r.message.length == 1) { + frm.set_value("operation_id", r.message[0].name); + } else { + const args = r.message.map((row) => ({ label: row.idx, value: row.name })); + const description = __("Operation {0} added multiple times in the work order {1}", [ + frm.doc.operation, + frm.doc.work_order, + ]); + frm.set_df_property("operation_row_number", "options", args); + frm.set_df_property("operation_row_number", "description", description); } + + frm.trigger("toggle_operation_number"); }, }); } @@ -610,89 +540,241 @@ frappe.ui.form.on("Job Card", { frm.toggle_reqd("operation_row_number", !frm.doc.operation_id && frm.doc.operation); }, - make_time_log: function (frm, args) { + make_time_log(frm, args) { frm.events.update_sub_operation(frm, args); frappe.call({ method: "erpnext.manufacturing.doctype.job_card.job_card.make_time_log", - args: { - args: args, - }, + args: { args }, freeze: true, - callback: function () { + callback() { frm.reload_doc(); frm.trigger("make_dashboard"); }, }); }, - update_sub_operation: function (frm, args) { - if (frm.doc.sub_operations && frm.doc.sub_operations.length) { - let sub_operations = frm.doc.sub_operations.filter((d) => d.status != "Complete"); - if (sub_operations && sub_operations.length) { - args["sub_operation"] = sub_operations[0].sub_operation; + update_sub_operation(frm, args) { + if (frm.doc.sub_operations?.length) { + const pending_sub_ops = frm.doc.sub_operations.filter((d) => d.status != "Complete"); + if (pending_sub_ops.length) { + args["sub_operation"] = pending_sub_ops[0].sub_operation; } } }, - make_dashboard: function (frm) { - if (frm.doc.__islocal) return; - var section = ""; + make_dashboard(frm, has_items) { + if (frm.doc.__islocal) return false; - function setCurrentIncrement() { - currentIncrement += 1; - return currentIncrement; + frm.dashboard.refresh(); + + // Clear any previously running timer tick before re-rendering. + if (frm._jcd_timer_interval) { + clearInterval(frm._jcd_timer_interval); + frm._jcd_timer_interval = null; } - function updateStopwatch(increment) { - var hours = Math.floor(increment / 3600); - var minutes = Math.floor((increment - hours * 3600) / 60); - var seconds = Math.floor(flt(increment - hours * 3600 - minutes * 60, 2)); + const wrapper = $(frm.fields_dict["job_card_dashboard"].wrapper); + wrapper.empty(); - $(section) - .find(".hours") - .text(hours < 10 ? "0" + hours.toString() : hours.toString()); - $(section) - .find(".minutes") - .text(minutes < 10 ? "0" + minutes.toString() : minutes.toString()); - $(section) - .find(".seconds") - .text(seconds < 10 ? "0" + seconds.toString() : seconds.toString()); + const { doc } = frm; + const { time_logs, status } = doc; + + // ── Determine which action buttons to show ──────────────────────── + const has_remaining_qty = doc.for_quantity + doc.process_loss_qty > doc.total_completed_qty; + const materials_ready = + doc.skip_material_transfer || + doc.transferred_qty >= doc.for_quantity + doc.process_loss_qty || + !doc.finished_good || + !has_items?.length; + + let last_row = {}; + const has_sub_ops_or_pending_qty = doc.sub_operations?.length || doc.pending_qty > 0; + if (has_sub_ops_or_pending_qty && time_logs?.length) { + last_row = get_last_row(time_logs); } - function initialiseTimer() { - const interval = setInterval(function () { - var current = setCurrentIncrement(); - updateStopwatch(current); + const no_time_logs_yet = !time_logs?.length; + const pending_qty_cycle_done = flt(doc.pending_qty) > 0.0 && last_row?.to_time; + const sub_operation_cycle_done = doc.sub_operations?.length && last_row?.to_time; + const should_show_start = + (no_time_logs_yet || pending_qty_cycle_done || sub_operation_cycle_done) && !doc.is_paused; + + const last_log_complete = time_logs?.length && time_logs[time_logs.length - 1].to_time; + const is_on_hold = status === "On Hold"; + const is_actively_running = !!( + time_logs?.length && + !last_log_complete && + !is_on_hold && + !doc.is_paused + ); + + let show_start = false, + show_pause = false, + show_resume = false, + show_complete = false, + is_timer_running = false; + + if (has_remaining_qty && materials_ready) { + const manufactured_qty = doc.manufactured_qty || doc.total_completed_qty; + const qty_yet_to_manufacture = doc.for_quantity - (manufactured_qty + doc.process_loss_qty); + + if (should_show_start) { + show_start = true; + } else if (doc.is_paused) { + show_resume = true; + } else if (qty_yet_to_manufacture > 0) { + show_pause = true; + show_complete = true; + is_timer_running = true; + } + } + + // ── Timer color reflects job state ──────────────────────────────── + const [timer_color, timer_bg, timer_border] = [ + "var(--gray-600,#6b7280)", + "var(--gray-100,#f3f4f6)", + "var(--gray-300,#d1d5db)", + ]; + + // ── Action button HTML ──────────────────────────────────────────── + const btn = (cls, icon_path, label, icon_color) => ` + `; + + const icons = { + play: { d: '', fill: "currentColor", stroke: "none" }, + pause: { + d: '', + fill: "currentColor", + stroke: "none", + }, + check: { d: '', sw: 3 }, + }; + + const buttons_html = [ + show_start && btn("btn-primary jcd-btn-start", "play", __("Start Job")), + show_resume && btn("btn-primary jcd-btn-resume", "play", __("Resume Job")), + show_pause && btn("btn-default jcd-btn-pause", "pause", __("Pause Job")), + show_complete && btn("btn-primary jcd-btn-complete", "check", __("Complete Job"), "white"), + ] + .filter(Boolean) + .join(""); + + // ── Render widget ───────────────────────────────────────────────── + wrapper.append(` +
    +
    +
    +
    + ${__("Elapsed Time")} +
    +
    + ${frappe.utils.icon("clock-4", "md", "", "", "", "", timer_color)} + + 00:00:00 + +
    +
    +
    + ${buttons_html} +
    +
    +
    `); + + // ── Wire up button click handlers ───────────────────────────────── + if (show_start) { + wrapper.find(".jcd-btn-start").on("click", () => { + const from_time = frappe.datetime.now_datetime(); + const has_no_employee = !frm.doc.employee || !frm.doc.employee.length; + + if (has_no_employee) { + frappe.prompt( + { + fieldtype: "Table MultiSelect", + label: __("Select Employees"), + options: "Job Card Time Log", + fieldname: "employees", + reqd: 1, + filters: { status: "Active" }, + }, + (d) => frm.events.start_timer(frm, from_time, d.employees), + __("Assign Job to Employee") + ); + } else { + frm.events.start_timer(frm, from_time, frm.doc.employee); + } + }); + } + + if (show_resume) { + wrapper.find(".jcd-btn-resume").on("click", () => { + frm.call({ + method: "resume_job", + doc: frm.doc, + args: { start_time: frappe.datetime.now_datetime() }, + callback() { + frm.reload_doc(); + }, + }); + }); + } + + if (show_pause) { + wrapper.find(".jcd-btn-pause").on("click", () => { + frm.call({ + method: "pause_job", + doc: frm.doc, + args: { end_time: frappe.datetime.now_datetime() }, + callback() { + frm.reload_doc(); + }, + }); + }); + } + + if (show_complete) { + wrapper.find(".jcd-btn-complete").on("click", () => { + frm.trigger("complete_job_card"); + }); + } + + // ── Timer tick ──────────────────────────────────────────────────── + const timer_el = wrapper.find(".jcd-stopwatch"); + const pad = (n) => String(n).padStart(2, "0"); + const update_stopwatch = (secs) => { + const h = Math.floor(secs / 3600); + const m = Math.floor((secs % 3600) / 60); + const s = Math.floor(secs % 60); + timer_el.text(`${pad(h)}:${pad(m)}:${pad(s)}`); + }; + + let current_increment = frm.events.get_current_time(frm); + update_stopwatch(current_increment); + + if (is_actively_running) { + frm._jcd_timer_interval = setInterval(() => { + current_increment += 1; + update_stopwatch(current_increment); }, 1000); } - frm.dashboard.refresh(); - const timer = ` -
    - 00 - : - 00 - : - 00 -
    `; - - if (frappe.utils.is_xs()) { - frm.dashboard.add_comment(timer, "white", true); - section = frm.layout.wrapper.find(".form-message-container"); - } else { - section = frm.toolbar.page.add_inner_message(timer); + // Demote Submit to btn-default when an action button is already primary. + const has_action_button = show_start || show_resume || show_complete; + if (frm.page.btn_primary) { + frm.page.btn_primary + .toggleClass("btn-primary", !has_action_button) + .toggleClass("btn-default", has_action_button); } - let currentIncrement = frm.events.get_current_time(frm); - if (frm.doc.time_logs?.length && frm.doc.time_logs[cint(frm.doc.time_logs.length) - 1].to_time) { - updateStopwatch(currentIncrement); - } else if (frm.doc.status == "On Hold") { - updateStopwatch(currentIncrement); - } else { - initialiseTimer(); - } + return is_timer_running; }, get_current_time(frm) { @@ -713,22 +795,26 @@ frappe.ui.form.on("Job Card", { return current_time; }, - hide_timer: function (frm) { - frm.toolbar.page.inner_toolbar.find(".stopwatch").remove(); + hide_timer(frm) { + if (frm._jcd_timer_interval) { + clearInterval(frm._jcd_timer_interval); + frm._jcd_timer_interval = null; + } + $(frm.fields_dict["job_card_dashboard"].wrapper).empty(); }, - for_quantity: function (frm) { + for_quantity(frm) { frm.doc.items = []; frm.call({ method: "get_required_items", doc: frm.doc, - callback: function () { + callback() { refresh_field("items"); }, }); }, - make_material_request: function (frm) { + make_material_request(frm) { frappe.model.open_mapped_doc({ method: "erpnext.manufacturing.doctype.job_card.job_card.make_material_request", frm: frm, @@ -736,7 +822,7 @@ frappe.ui.form.on("Job Card", { }); }, - make_stock_entry: function (frm) { + make_stock_entry(frm) { frappe.model.open_mapped_doc({ method: "erpnext.manufacturing.doctype.job_card.job_card.make_stock_entry", frm: frm, @@ -744,11 +830,7 @@ frappe.ui.form.on("Job Card", { }); }, - timer: function (frm) { - return ``; - }, - - set_total_completed_qty: function (frm) { + set_total_completed_qty(frm) { frm.doc.total_completed_qty = 0; frm.doc.time_logs.forEach((d) => { if (d.completed_qty) { @@ -757,10 +839,9 @@ frappe.ui.form.on("Job Card", { }); if (frm.doc.total_completed_qty && frm.doc.for_quantity > frm.doc.total_completed_qty) { - let flt_precision = precision("for_quantity", frm.doc); - let process_loss_qty = + const flt_precision = precision("for_quantity", frm.doc); + const process_loss_qty = flt(frm.doc.for_quantity, flt_precision) - flt(frm.doc.total_completed_qty, flt_precision); - frm.set_value("process_loss_qty", process_loss_qty); } @@ -777,8 +858,8 @@ frappe.ui.form.on("Job Card", { }); frappe.ui.form.on("Job Card Time Log", { - completed_qty: function (frm, cdt, cdn) { - let row = locals[cdt][cdn]; + completed_qty(frm, cdt, cdn) { + const row = locals[cdt][cdn]; if (!row.completed_qty) { frappe.model.set_value(row.doctype, row.name, { time_in_mins: 0, @@ -795,12 +876,8 @@ function get_seconds_diff(d1, d2) { } function get_last_completed_row(time_logs) { - let completed_rows = time_logs.filter((d) => d.to_time); - - if (completed_rows?.length) { - let last_completed_row = completed_rows[completed_rows.length - 1]; - return last_completed_row; - } + const completed_rows = time_logs.filter((d) => d.to_time); + return completed_rows[completed_rows.length - 1]; } function get_last_row(time_logs) { diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 21f0adbdd74..69a156009b1 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -1,30 +1,40 @@ { "actions": [], + "allow_bulk_edit": 1, "autoname": "naming_series:", "creation": "2026-03-31 21:06:16.282931", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "section_break_smqo", + "job_card_dashboard", + "section_break_fsba", + "work_order", + "column_break_uqjq", + "production_item", + "column_break_qrpg", + "for_quantity", + "column_break_yecz", + "bom_no", + "section_break_oisd", "company", "naming_series", - "work_order", - "employee", "column_break_4", "posting_date", - "project", - "bom_no", - "is_subcontracted", "semi_finished_good__finished_good_section", "finished_good", - "production_item", - "semi_fg_bom", - "total_completed_qty", "column_break_mcnb", - "for_quantity", - "transferred_qty", - "manufactured_qty", + "semi_fg_bom", + "section_break_folk", + "pending_qty", + "column_break_cyjw", "process_loss_qty", + "total_completed_qty", + "section_break_wpjf", + "transferred_qty", + "column_break_lgte", + "manufactured_qty", "production_section", "operation", "source_warehouse", @@ -35,6 +45,7 @@ "workstation_type", "workstation", "target_warehouse", + "employee", "section_break_8", "items", "quality_inspection_section", @@ -71,8 +82,10 @@ "item_name", "requested_qty", "is_paused", + "is_subcontracted", "track_semi_finished_goods", "column_break_20", + "project", "remarks", "section_break_dfoc", "status", @@ -155,6 +168,7 @@ "fieldname": "wip_warehouse", "fieldtype": "Link", "label": "WIP Warehouse", + "link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]", "mandatory_depends_on": "eval:!doc.finished_good || doc.skip_material_transfer === 0 || (doc.skip_material_transfer && doc.backflush_from_wip_warehouse)", "options": "Warehouse" }, @@ -506,6 +520,7 @@ "fieldname": "target_warehouse", "fieldtype": "Link", "label": "Target Warehouse", + "link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]", "mandatory_depends_on": "eval:doc.track_semi_finished_goods", "options": "Warehouse" }, @@ -518,6 +533,7 @@ "fieldname": "source_warehouse", "fieldtype": "Link", "label": "Source Warehouse", + "link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]", "options": "Warehouse" }, { @@ -622,12 +638,64 @@ "fieldname": "secondary_items_section", "fieldtype": "Tab Break", "label": "Secondary Items" + }, + { + "fieldname": "section_break_folk", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "fieldname": "column_break_cyjw", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "fieldname": "pending_qty", + "fieldtype": "Float", + "label": "Pending Qty" + }, + { + "fieldname": "section_break_wpjf", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_lgte", + "fieldtype": "Column Break" + }, + { + "fieldname": "job_card_dashboard", + "fieldtype": "HTML" + }, + { + "fieldname": "section_break_oisd", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_uqjq", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_qrpg", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_yecz", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_smqo", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "fieldname": "section_break_fsba", + "fieldtype": "Section Break" } ], "grid_page_length": 50, "is_submittable": 1, "links": [], - "modified": "2026-03-31 21:06:48.987740", + "modified": "2026-05-21 18:37:05.688342", "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 9716ae49dc0..137788346c2 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -103,6 +103,7 @@ class JobCard(Document): operation_id: DF.Data | None operation_row_id: DF.Int operation_row_number: DF.Literal[None] + pending_qty: DF.Float posting_date: DF.Date | None process_loss_qty: DF.Float production_item: DF.Link | None @@ -881,7 +882,9 @@ class JobCard(Document): precision = self.precision("total_completed_qty") total_completed_qty = flt( - flt(self.total_completed_qty, precision) + flt(self.process_loss_qty, precision) + flt(self.total_completed_qty, precision) + + flt(self.process_loss_qty, precision) + + flt(self.pending_qty, precision) ) if self.for_quantity and flt(total_completed_qty, precision) != flt(self.for_quantity, precision): @@ -928,8 +931,10 @@ class JobCard(Document): self.process_loss_qty = 0.0 if self.total_completed_qty and self.for_quantity > self.total_completed_qty: - self.process_loss_qty = flt(self.for_quantity, precision) - flt( - self.total_completed_qty, precision + self.process_loss_qty = ( + flt(self.for_quantity, precision) + - flt(self.total_completed_qty, precision) + - flt(self.pending_qty, precision) ) def update_work_order(self): @@ -943,13 +948,14 @@ class JobCard(Document): ): return - for_quantity, time_in_mins, process_loss_qty = 0, 0, 0 + for_quantity, time_in_mins, process_loss_qty, pending_qty = 0, 0, 0, 0 data = self.get_current_operation_data() if data and len(data) > 0: for_quantity = flt(data[0].completed_qty) time_in_mins = flt(data[0].time_in_mins) process_loss_qty = flt(data[0].process_loss_qty) + pending_qty = flt(data[0].pending_qty) wo = frappe.get_doc("Work Order", self.work_order) @@ -957,8 +963,8 @@ class JobCard(Document): self.update_corrective_in_work_order(wo) elif self.operation_id: - self.validate_produced_quantity(for_quantity, process_loss_qty, wo) - self.update_work_order_data(for_quantity, process_loss_qty, time_in_mins, wo) + self.validate_produced_quantity(for_quantity, process_loss_qty, pending_qty, wo) + self.update_work_order_data(for_quantity, process_loss_qty, pending_qty, time_in_mins, wo) def update_semi_finished_good_details(self): if self.operation_id: @@ -987,11 +993,11 @@ class JobCard(Document): wo.flags.ignore_validate_update_after_submit = True wo.save() - def validate_produced_quantity(self, for_quantity, process_loss_qty, wo): + def validate_produced_quantity(self, for_quantity, process_loss_qty, pending_qty, wo): if self.docstatus < 2: return - if wo.produced_qty > for_quantity + process_loss_qty: + if wo.produced_qty > for_quantity + process_loss_qty + pending_qty: first_part_msg = _( "The {0} {1} is used to calculate the valuation cost for the finished good {2}." ).format(frappe.bold(_("Job Card")), frappe.bold(self.name), frappe.bold(self.production_item)) @@ -1004,7 +1010,7 @@ class JobCard(Document): _("{0} {1}").format(first_part_msg, second_part_msg), JobCardCancelError, title=_("Error") ) - def update_work_order_data(self, for_quantity, process_loss_qty, time_in_mins, wo): + def update_work_order_data(self, for_quantity, process_loss_qty, pending_qty, time_in_mins, wo): workstation_hour_rate = frappe.get_value("Workstation", self.workstation, "hour_rate") jc = frappe.qb.DocType("Job Card") jctl = frappe.qb.DocType("Job Card Time Log") @@ -1026,6 +1032,7 @@ class JobCard(Document): if data.get("name") == self.operation_id: data.completed_qty = for_quantity data.process_loss_qty = process_loss_qty + data.pending_qty = pending_qty data.actual_operation_time = time_in_mins data.actual_start_time = time_data[0].start_time if time_data else None data.actual_end_time = time_data[0].end_time if time_data else None @@ -1051,6 +1058,7 @@ class JobCard(Document): {"SUM": "total_time_in_mins", "as": "time_in_mins"}, {"SUM": "total_completed_qty", "as": "completed_qty"}, {"SUM": "process_loss_qty", "as": "process_loss_qty"}, + {"SUM": "pending_qty", "as": "pending_qty"}, ], filters={ "docstatus": 1, @@ -1445,10 +1453,19 @@ class JobCard(Document): if isinstance(kwargs, dict): kwargs = frappe._dict(kwargs) - if kwargs.end_time: - if kwargs.for_quantity: - self.for_quantity = kwargs.for_quantity + if flt(kwargs.pending_qty) and flt(kwargs.pending_qty) < 0: + frappe.throw(_("Pending quantity cannot be negative.")) + if flt(kwargs.process_loss_qty) and flt(kwargs.process_loss_qty) < 0: + frappe.throw(_("Process loss quantity cannot be negative.")) + + if flt(kwargs.pending_qty) and flt(kwargs.pending_qty) > self.for_quantity: + frappe.throw(_("Pending quantity cannot be greater than the for quantity.")) + + self.pending_qty = flt(kwargs.pending_qty) + self.process_loss_qty = flt(kwargs.process_loss_qty) + + if kwargs.end_time: self.add_time_logs( to_time=kwargs.end_time, completed_qty=kwargs.qty, diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 9a6733232a1..7174347d5bb 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -720,6 +720,7 @@ class TestJobCard(ERPNextTestSuite): ) jc.time_logs[0].completed_qty = 8 + jc.pending_qty = 0.0 jc.save() jc.submit() @@ -1080,6 +1081,243 @@ class TestJobCard(ERPNextTestSuite): self.assertEqual(s.items[3].item_code, "_Test Item") self.assertEqual(s.items[3].transfer_qty, 2) + @ERPNextTestSuite.change_settings( + "Manufacturing Settings", {"overproduction_percentage_for_work_order": 100} + ) + def test_operating_cost_with_overproduction(self): + from erpnext.manufacturing.doctype.routing.test_routing import ( + create_routing, + setup_bom, + setup_operations, + ) + from erpnext.manufacturing.doctype.work_order.work_order import make_job_card + from erpnext.manufacturing.doctype.work_order.work_order import ( + make_stock_entry as make_stock_entry_for_wo, + ) + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + workstation = make_workstation( + workstation_name="Test Workstation for Overproduction", hour_rate_rent=10, hour_rate_labour=10 + ) + operations = [ + {"operation": "Test Operation 1", "workstation": workstation.name, "time_in_mins": 30}, + {"operation": "Test Operation 2", "workstation": workstation.name, "time_in_mins": 30}, + ] + warehouse = create_warehouse("Test Warehouse for Overproduction") + setup_operations(operations) + + fg = make_item("Test FG for Overproduction", {"stock_uom": "Nos", "is_stock_item": 1}) + rm = make_item("Test RM for Overproduction", {"stock_uom": "Nos", "is_stock_item": 1}) + + routing_doc = create_routing(routing_name="Testing Route", operations=operations) + bom_doc = setup_bom( + item_code=fg.name, + routing=routing_doc.name, + raw_materials=[rm.name], + source_warehouse=warehouse, + ) + + for row in bom_doc.items: + make_stock_entry( + item_code=row.item_code, + target=row.source_warehouse, + qty=100, + basic_rate=100, + ) + + wo_doc = make_wo_order_test_record( + production_item=fg.name, + bom_no=bom_doc.name, + qty=10, + skip_transfer=1, + source_warehouse=warehouse, + ) + + first_operation = frappe.get_all( + "Job Card", + filters={"work_order": wo_doc.name, "sequence_id": 1}, + fields=["name"], + order_by="sequence_id", + limit=1, + )[0].name + + jc = frappe.get_doc("Job Card", first_operation) + from_time = add_to_date(now(), days=1) + for _ in jc.scheduled_time_logs: + jc.append( + "time_logs", + { + "from_time": from_time, + "to_time": add_to_date(from_time, days=1), + "completed_qty": 4, + }, + ) + jc.for_quantity = 4 + jc.save() + jc.submit() + + second_operation = frappe.get_all( + "Job Card", + filters={"work_order": wo_doc.name, "sequence_id": 2}, + fields=["name"], + order_by="sequence_id", + limit=1, + )[0].name + + jc = frappe.get_doc("Job Card", second_operation) + from_time = add_to_date(now(), days=2) + for _ in jc.scheduled_time_logs: + jc.append( + "time_logs", + { + "from_time": from_time, + "to_time": add_to_date(from_time, days=2), + "completed_qty": 4, + }, + ) + jc.for_quantity = 4 + jc.save() + jc.submit() + + s = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 6)) # overproduction + s.submit() + + self.assertEqual(s.additional_costs[0].amount, 240) + self.assertEqual(s.additional_costs[1].amount, 240) + self.assertEqual(s.additional_costs[2].amount, 480) + self.assertEqual(s.additional_costs[3].amount, 480) + + make_job_card( + wo_doc.name, + [ + { + "name": wo_doc.operations[0].name, + "operation": "Test Operation 1", + "qty": 2, + "pending_qty": 2, + } + ], + ) + + job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name}) + from_time = add_to_date(now(), days=4) + job_card.append( + "time_logs", + { + "from_time": from_time, + "to_time": add_to_date(from_time, days=1), + "completed_qty": 2, + }, + ) + job_card.for_quantity = 2 + job_card.save() + job_card.submit() + + make_job_card( + wo_doc.name, + [ + { + "name": wo_doc.operations[1].name, + "operation": "Test Operation 2", + "qty": 2, + "pending_qty": 2, + } + ], + ) + + job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name}) + from_time = add_to_date(now(), days=5) + job_card.append( + "time_logs", + { + "from_time": from_time, + "to_time": add_to_date(from_time, days=2), + "completed_qty": 2, + }, + ) + job_card.for_quantity = 2 + job_card.save() + job_card.submit() + + s2 = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 1)) + s2.submit() + + self.assertEqual(s2.additional_costs[0].amount, 120) + self.assertEqual(s2.additional_costs[1].amount, 120) + self.assertEqual(s2.additional_costs[2].amount, 240) + self.assertEqual(s2.additional_costs[3].amount, 240) + + make_job_card( + wo_doc.name, + [ + { + "name": wo_doc.operations[0].name, + "operation": "Test Operation 1", + "qty": 2, + "pending_qty": 2, + } + ], + ) + + job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name}) + from_time = add_to_date(now(), days=7) + job_card.append( + "time_logs", + { + "from_time": from_time, + "to_time": add_to_date(from_time, days=1), + "completed_qty": 2, + }, + ) + job_card.for_quantity = 2 + job_card.save() + job_card.submit() + + make_job_card( + wo_doc.name, + [ + { + "name": wo_doc.operations[1].name, + "operation": "Test Operation 2", + "qty": 2, + "pending_qty": 2, + } + ], + ) + + job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name}) + from_time = add_to_date(now(), days=8) + job_card.append( + "time_logs", + { + "from_time": from_time, + "to_time": add_to_date(from_time, days=2), + "completed_qty": 2, + }, + ) + job_card.for_quantity = 2 + job_card.save() + job_card.submit() + + s = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 2)) + s.submit() + + self.assertEqual(s.additional_costs[0].amount, 240) + self.assertEqual(s.additional_costs[1].amount, 240) + self.assertEqual(s.additional_costs[2].amount, 480) + self.assertEqual(s.additional_costs[3].amount, 480) + + s2.cancel() + + s = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 3)) + s.submit() + + self.assertEqual(s.additional_costs[0].amount, 240) + self.assertEqual(s.additional_costs[1].amount, 240) + self.assertEqual(s.additional_costs[2].amount, 480) + self.assertEqual(s.additional_costs[3].amount, 480) + def create_bom_with_multiple_operations(): "Create a BOM with multiple operations and Material Transfer against Job Card" diff --git a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json index 69a7428f218..c8dd6403321 100644 --- a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json +++ b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_bulk_edit": 1, "creation": "2018-07-09 17:20:44.737289", "doctype": "DocType", "editable_grid": 1, @@ -34,6 +35,7 @@ "ignore_user_permissions": 1, "in_list_view": 1, "label": "Source Warehouse", + "link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]", "options": "Warehouse" }, { @@ -113,7 +115,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-12-04 14:30:19.472294", + "modified": "2026-05-12 12:22:18.506904", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card Item", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index fa4c812d5b8..fcf6a6c590b 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1315,6 +1315,7 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p item_uom.conversion_factor, item.safety_stock, bom.item.as_("main_bom_item"), + bom.name.as_("main_bom"), ) .where( (bei.docstatus < 2) @@ -1384,6 +1385,7 @@ def get_subitems( item.purchase_uom, item_uom.conversion_factor, bom.item.as_("main_bom_item"), + bom.name.as_("main_bom"), bom_item.is_phantom_item, ) .where( diff --git a/erpnext/manufacturing/doctype/sales_forecast/sales_forecast.js b/erpnext/manufacturing/doctype/sales_forecast/sales_forecast.js index a7acec295a9..340b35852b1 100644 --- a/erpnext/manufacturing/doctype/sales_forecast/sales_forecast.js +++ b/erpnext/manufacturing/doctype/sales_forecast/sales_forecast.js @@ -47,11 +47,3 @@ frappe.ui.form.on("Sales Forecast", { } }, }); - -frappe.ui.form.on("Sales Forecast Item", { - adjust_qty(frm, cdt, cdn) { - let row = locals[cdt][cdn]; - row.demand_qty = row.forecast_qty + row.adjust_qty; - frappe.model.set_value(cdt, cdn, "demand_qty", row.demand_qty); - }, -}); diff --git a/erpnext/manufacturing/doctype/sales_forecast_item/sales_forecast_item.json b/erpnext/manufacturing/doctype/sales_forecast_item/sales_forecast_item.json index 16c5275c0b8..96cec964beb 100644 --- a/erpnext/manufacturing/doctype/sales_forecast_item/sales_forecast_item.json +++ b/erpnext/manufacturing/doctype/sales_forecast_item/sales_forecast_item.json @@ -10,8 +10,6 @@ "item_name", "uom", "delivery_date", - "forecast_qty", - "adjust_qty", "demand_qty", "warehouse" ], @@ -55,22 +53,6 @@ "label": "Delivery Date", "read_only": 1 }, - { - "columns": 2, - "fieldname": "forecast_qty", - "fieldtype": "Float", - "in_list_view": 1, - "label": "Forecast Qty", - "non_negative": 1, - "read_only": 1 - }, - { - "columns": 2, - "fieldname": "adjust_qty", - "fieldtype": "Float", - "in_list_view": 1, - "label": "Adjust Qty" - }, { "columns": 3, "fieldname": "demand_qty", @@ -94,7 +76,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-08-18 21:59:38.859082", + "modified": "2026-05-21 12:38:47.636301", "modified_by": "Administrator", "module": "Manufacturing", "name": "Sales Forecast Item", diff --git a/erpnext/manufacturing/doctype/sales_forecast_item/sales_forecast_item.py b/erpnext/manufacturing/doctype/sales_forecast_item/sales_forecast_item.py index 5fcb718fb50..cebb6004158 100644 --- a/erpnext/manufacturing/doctype/sales_forecast_item/sales_forecast_item.py +++ b/erpnext/manufacturing/doctype/sales_forecast_item/sales_forecast_item.py @@ -14,10 +14,8 @@ class SalesForecastItem(Document): if TYPE_CHECKING: from frappe.types import DF - adjust_qty: DF.Float delivery_date: DF.Date | None demand_qty: DF.Float - forecast_qty: DF.Float item_code: DF.Link item_name: DF.Data | None parent: DF.Data diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index d26f6040f4c..5131b30c889 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -12,21 +12,10 @@ frappe.ui.form.on("Work Order", { frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"]; // Set query for warehouses - frm.set_query("wip_warehouse", function () { - return { - filters: { - company: frm.doc.company, - }, - }; - }); - - frm.set_query("source_warehouse", function () { - return { - filters: { - company: frm.doc.company, - }, - }; - }); + frm.events.set_company_filters(frm, "wip_warehouse"); + frm.events.set_company_filters(frm, "source_warehouse"); + frm.events.set_company_filters(frm, "fg_warehouse"); + frm.events.set_company_filters(frm, "scrap_warehouse"); frm.set_query("source_warehouse", "required_items", function () { return { @@ -44,24 +33,6 @@ frappe.ui.form.on("Work Order", { }; }); - frm.set_query("fg_warehouse", function () { - return { - filters: { - company: frm.doc.company, - is_group: 0, - }, - }; - }); - - frm.set_query("scrap_warehouse", function () { - return { - filters: { - company: frm.doc.company, - is_group: 0, - }, - }; - }); - // Set query for BOM frm.set_query("bom_no", function () { if (frm.doc.production_item) { @@ -118,6 +89,16 @@ frappe.ui.form.on("Work Order", { }); }, + set_company_filters(frm, fieldname) { + frm.set_query(fieldname, () => { + return { + filters: { + company: frm.doc.company, + }, + }; + }); + }, + onload: function (frm) { if (!frm.doc.status) frm.doc.status = "Draft"; @@ -348,7 +329,7 @@ frappe.ui.form.on("Work Order", { { fieldtype: "Data", fieldname: "name", - label: __("Operation Id"), + label: __("Operation ID"), }, { fieldtype: "Float", @@ -425,6 +406,7 @@ frappe.ui.form.on("Work Order", { if (pending_qty) { dialog.fields_dict.operations.df.data.push({ + __checked: 1, name: data.name, operation: data.operation, workstation: data.workstation, diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 2d5145141ae..9dc57a3b99c 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_bulk_edit": 1, "allow_import": 1, "autoname": "naming_series:", "creation": "2025-04-09 12:09:40.634472", @@ -266,6 +267,7 @@ "fieldname": "wip_warehouse", "fieldtype": "Link", "label": "Work-in-Progress Warehouse", + "link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]", "mandatory_depends_on": "eval:(!doc.skip_transfer || doc.from_wip_warehouse) && !doc.track_semi_finished_goods", "options": "Warehouse" }, @@ -274,6 +276,7 @@ "fieldname": "fg_warehouse", "fieldtype": "Link", "label": "Target Warehouse", + "link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]", "options": "Warehouse", "read_only_depends_on": "subcontracting_inward_order" }, @@ -286,6 +289,7 @@ "fieldname": "scrap_warehouse", "fieldtype": "Link", "label": "Scrap Warehouse", + "link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]", "options": "Warehouse" }, { @@ -513,6 +517,7 @@ "fieldname": "source_warehouse", "fieldtype": "Link", "label": "Source Warehouse", + "link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]", "options": "Warehouse", "read_only_depends_on": "eval:doc.subcontracting_inward_order" }, @@ -706,7 +711,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2026-04-17 13:42:12.374055", + "modified": "2026-05-19 12:20:38.102403", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 8bc4485785c..81047a8c391 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -167,18 +167,19 @@ class WorkOrder(Document): self.set_onload("backflush_raw_materials_based_on", based_on) def show_create_job_card_button(self): - operation_details = frappe._dict( - frappe.get_all( - "Job Card", - fields=["operation", {"SUM": "for_quantity"}], - filters={"docstatus": ("<", 2), "work_order": self.name}, - as_list=1, - group_by="operation_id", - ) + jc_doctype = frappe.qb.DocType("Job Card") + query = ( + frappe.qb.from_(jc_doctype) + .select(jc_doctype.operation_id, Sum(jc_doctype.for_quantity - IfNull(jc_doctype.pending_qty, 0))) + .where((jc_doctype.docstatus < 2) & (jc_doctype.work_order == self.name)) + .groupby(jc_doctype.operation_id) ) + operation_details = query.run(as_list=1) + operation_details = frappe._dict(operation_details) + for d in self.operations: - job_card_qty = self.qty - flt(operation_details.get(d.operation)) + job_card_qty = self.qty - flt(operation_details.get(d.name)) if job_card_qty > 0: return True diff --git a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json index e5f322ca367..dec4934ea4f 100644 --- a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json +++ b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_bulk_edit": 1, "creation": "2016-04-18 07:38:26.314642", "doctype": "DocType", "editable_grid": 1, @@ -53,6 +54,7 @@ "ignore_user_permissions": 1, "in_list_view": 1, "label": "Source Warehouse", + "link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]", "options": "Warehouse", "read_only_depends_on": "eval:parent.subcontracting_inward_order && doc.is_customer_provided_item" }, @@ -207,7 +209,7 @@ "grid_page_length": 50, "istable": 1, "links": [], - "modified": "2025-12-02 11:16:05.081613", + "modified": "2026-05-12 12:05:16.687866", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Item", 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 89ed830116b..918f2b21847 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_bulk_edit": 1, "creation": "2025-04-09 12:12:19.824560", "doctype": "DocType", "editable_grid": 1, @@ -10,6 +11,7 @@ "status", "completed_qty", "process_loss_qty", + "pending_qty", "column_break_4", "bom", "workstation_type", @@ -301,13 +303,20 @@ "fieldname": "quality_inspection_required", "fieldtype": "Check", "label": "Quality Inspection Required" + }, + { + "fieldname": "pending_qty", + "fieldtype": "Float", + "label": "Pending Qty", + "no_copy": 1, + "read_only": 1 } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-03-30 17:20:08.874381", + "modified": "2026-05-20 13:01:21.827200", "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 2e45434f94b..8950fd6b320 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.py +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.py @@ -32,6 +32,7 @@ class WorkOrderOperation(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data + pending_qty: DF.Float planned_end_time: DF.Datetime | None planned_operating_cost: DF.Currency planned_start_time: DF.Datetime | None diff --git a/erpnext/patches.txt b/erpnext/patches.txt index e2c7734ff7e..4e3ba999eb9 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -482,3 +482,4 @@ erpnext.patches.v16_0.packed_item_inv_dimen erpnext.patches.v16_0.fix_titles erpnext.patches.v16_0.set_not_applicable_on_german_item_tax_templates erpnext.patches.v16_0.clear_procedures_from_receivable_report +erpnext.patches.v16_0.migrate_address_contact_custom_fields diff --git a/erpnext/patches/v16_0/migrate_address_contact_custom_fields.py b/erpnext/patches/v16_0/migrate_address_contact_custom_fields.py new file mode 100644 index 00000000000..6edce540eff --- /dev/null +++ b/erpnext/patches/v16_0/migrate_address_contact_custom_fields.py @@ -0,0 +1,16 @@ +import frappe + +from erpnext.setup.install import create_address_and_contact_custom_fields + + +def execute(): + """Replace fixture-based custom fields on Address and Contact with programmatic ones.""" + for custom_field in ( + "Address-tax_category", + "Address-is_your_company_address", + "Contact-is_billing_contact", + ): + if frappe.db.exists("Custom Field", custom_field): + frappe.delete_doc("Custom Field", custom_field, ignore_missing=True, force=True) + + create_address_and_contact_custom_fields() diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index 95bf037c5e5..8f5a9b03813 100644 --- a/erpnext/projects/doctype/project/project.json +++ b/erpnext/projects/doctype/project/project.json @@ -181,6 +181,7 @@ "fieldtype": "Link", "in_global_search": 1, "label": "Customer", + "no_copy": 1, "oldfieldname": "customer", "oldfieldtype": "Link", "options": "Customer", @@ -195,6 +196,7 @@ "fieldname": "sales_order", "fieldtype": "Link", "label": "Sales Order", + "no_copy": 1, "options": "Sales Order" }, { @@ -480,7 +482,7 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 4, - "modified": "2026-04-14 18:17:40.676750", + "modified": "2026-05-22 16:45:50.762759", "modified_by": "Administrator", "module": "Projects", "name": "Project", diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 3999c45065f..d0eeb58495b 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2266,6 +2266,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe for (const [key, value] of Object.entries(child)) { if (!["doctype", "name"].includes(key)) { if (key === "price_list_rate") { + const doc = frappe.get_doc(child.doctype, child.name); + if (doc) doc.price_list_rate = value; // silent update so rate trigger uses correct value frappe.model.set_value(child.doctype, child.name, "rate", value); } diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 6d08648c6e0..9161903ba7f 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -483,6 +483,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { warehouse: this.item.s_warehouse || this.item.t_warehouse || this.item.warehouse, is_inward: is_inward, + posting_date: this.frm.doc.posting_date, + posting_time: this.frm.doc.posting_time, include_expired_batches: include_expired_batches, }, }; diff --git a/erpnext/selling/doctype/party_specific_item/party_specific_item.js b/erpnext/selling/doctype/party_specific_item/party_specific_item.js index 0decc70d7ac..779f2683102 100644 --- a/erpnext/selling/doctype/party_specific_item/party_specific_item.js +++ b/erpnext/selling/doctype/party_specific_item/party_specific_item.js @@ -2,6 +2,21 @@ // For license information, please see license.txt frappe.ui.form.on("Party Specific Item", { - // refresh: function(frm) { - // } + setup: function (frm) { + frm.trigger("party_type"); + }, + + party_type: function (frm) { + if (["Customer Group", "Supplier Group"].includes(frm.doc.party_type)) { + frm.set_query("party", function () { + return { + filters: { + is_group: 0, + }, + }; + }); + } else { + frm.set_query("party", null); + } + }, }); diff --git a/erpnext/selling/doctype/party_specific_item/party_specific_item.json b/erpnext/selling/doctype/party_specific_item/party_specific_item.json index 999cb61af61..b1aa5bfa59b 100644 --- a/erpnext/selling/doctype/party_specific_item/party_specific_item.json +++ b/erpnext/selling/doctype/party_specific_item/party_specific_item.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_bulk_edit": 1, "allow_import": 1, "creation": "2021-08-27 19:28:07.559978", "doctype": "DocType", @@ -18,7 +19,7 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Party Type", - "options": "Customer\nSupplier", + "options": "Customer\nCustomer Group\nSupplier\nSupplier Group", "reqd": 1 }, { @@ -52,7 +53,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-03-27 13:10:08.752476", + "modified": "2026-05-17 11:46:17.634855", "modified_by": "Administrator", "module": "Selling", "name": "Party Specific Item", @@ -71,9 +72,10 @@ "write": 1 } ], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "title_field": "party", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/selling/doctype/party_specific_item/party_specific_item.py b/erpnext/selling/doctype/party_specific_item/party_specific_item.py index d1775c2467c..77eb9095305 100644 --- a/erpnext/selling/doctype/party_specific_item/party_specific_item.py +++ b/erpnext/selling/doctype/party_specific_item/party_specific_item.py @@ -17,7 +17,7 @@ class PartySpecificItem(Document): based_on_value: DF.DynamicLink party: DF.DynamicLink - party_type: DF.Literal["Customer", "Supplier"] + party_type: DF.Literal["Customer", "Customer Group", "Supplier", "Supplier Group"] restrict_based_on: DF.Literal["Item", "Item Group", "Brand"] # end: auto-generated types diff --git a/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py b/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py index a94837452e6..eaa68232d27 100644 --- a/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py +++ b/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py @@ -49,6 +49,23 @@ class TestPartySpecificItem(ERPNextTestSuite): ) self.assertTrue(item in flatten(items)) + def test_party_group(self): + customer = "_Test Customer With Template" + item = "_Test Item" + frappe.set_value("Customer", customer, "customer_group", "Government") + + create_party_specific_item( + party_type="Customer Group", + party="Government", + restrict_based_on="Item", + based_on_value=item, + ) + filters = {"is_sales_item": 1, "customer": customer} + items = item_query( + doctype="Item", txt="", searchfield="name", start=0, page_len=20, filters=filters, as_dict=False + ) + self.assertTrue(item in flatten(items)) + def flatten(lst): result = [] diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 74262ab0873..2aff7d44327 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -452,7 +452,7 @@ class SalesOrder(SellingController): and not cint(d.delivered_by_supplier) ): frappe.throw( - _("Delivery warehouse required for stock item {0}").format(d.item_code), WarehouseRequired + _("Source warehouse required for stock item {0}").format(d.item_code), WarehouseRequired ) def validate_with_previous_doc(self): @@ -1798,6 +1798,7 @@ def make_work_orders(items, sales_order, company, project=None): return [p.name for p in out] +@frappe.whitelist() def make_production_plan(source_name, target_doc=None): sales_order = frappe.get_doc("Sales Order", source_name) diff --git a/erpnext/setup/doctype/customer_group/customer_group.py b/erpnext/setup/doctype/customer_group/customer_group.py index 9a05760ec29..d0af3af7ba3 100644 --- a/erpnext/setup/doctype/customer_group/customer_group.py +++ b/erpnext/setup/doctype/customer_group/customer_group.py @@ -75,13 +75,11 @@ class CustomerGroup(NestedSet): def get_parent_customer_groups(customer_group): lft, rgt = frappe.db.get_value("Customer Group", customer_group, ["lft", "rgt"]) - - return frappe.db.sql( - """select name from `tabCustomer Group` - where lft <= %s and rgt >= %s - order by lft asc""", - (lft, rgt), - as_dict=True, + return frappe.get_all( + "Customer Group", + filters=[["lft", "<=", lft], ["rgt", ">=", rgt]], + fields=["name"], + order_by="lft asc", ) diff --git a/erpnext/setup/doctype/employee/employee.js b/erpnext/setup/doctype/employee/employee.js index b4adc01b102..d400a620b0b 100755 --- a/erpnext/setup/doctype/employee/employee.js +++ b/erpnext/setup/doctype/employee/employee.js @@ -44,7 +44,7 @@ frappe.ui.form.on("Employee", { }, refresh: function (frm) { - frm.fields_dict.date_of_birth.datepicker.update({ maxDate: new Date() }); + frm.fields_dict.date_of_birth.datepicker?.update({ maxDate: new Date() }); if (!frm.is_new() && !frm.doc.user_id) { frm.add_custom_button(__("Create User"), () => { diff --git a/erpnext/setup/doctype/supplier_group/supplier_group.py b/erpnext/setup/doctype/supplier_group/supplier_group.py index 5e1cab40450..a5ff5b8b6b4 100644 --- a/erpnext/setup/doctype/supplier_group/supplier_group.py +++ b/erpnext/setup/doctype/supplier_group/supplier_group.py @@ -70,3 +70,13 @@ class SupplierGroup(NestedSet): def on_trash(self): NestedSet.validate_if_child_exists(self) frappe.utils.nestedset.update_nsm(self) + + +def get_parent_supplier_groups(supplier_group): + lft, rgt = frappe.db.get_value("Supplier Group", supplier_group, ["lft", "rgt"]) + return frappe.get_all( + "Supplier Group", + filters=[["lft", "<=", lft], ["rgt", ">=", rgt]], + fields=["name"], + order_by="lft asc", + ) diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index be75e44759f..b8162bb648c 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -25,6 +25,7 @@ def after_install(): setup_repost_defaults() create_print_setting_custom_fields() create_marketing_campaign_custom_fields() + create_address_and_contact_custom_fields() create_custom_company_links() add_all_roles_to("Administrator") create_default_success_action() @@ -145,6 +146,37 @@ def create_marketing_campaign_custom_fields(): ) +def create_address_and_contact_custom_fields(): + create_custom_fields( + { + "Address": [ + { + "label": _("Tax Category"), + "fieldname": "tax_category", + "fieldtype": "Link", + "options": "Tax Category", + "insert_after": "fax", + }, + { + "label": _("Is Your Company Address"), + "fieldname": "is_your_company_address", + "fieldtype": "Check", + "default": "0", + "insert_after": "linked_with", + }, + ], + "Contact": [ + { + "label": _("Is Billing Contact"), + "fieldname": "is_billing_contact", + "fieldtype": "Check", + "insert_after": "is_primary_contact", + }, + ], + } + ) + + def create_default_success_action(): for success_action in get_default_success_action(): if not frappe.db.exists("Success Action", success_action.get("ref_doctype")): diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index 1ba25cb44f3..ed4a8c5509c 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -543,6 +543,7 @@ class TestBatch(ERPNextTestSuite): "plc_conversion_rate": 1, "customer": "_Test Customer", "name": None, + "qty": 1, } ) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index c03199e4348..a37c0925b71 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -286,14 +286,6 @@ frappe.ui.form.on("Item", { frm.set_df_property(fieldname, "read_only", stock_exists); }); frm.set_df_property("is_fixed_asset", "read_only", frm.doc.__onload?.asset_exists ? 1 : 0); - frm.toggle_reqd("customer", frm.doc.is_customer_provided_item ? 1 : 0); - frm.set_query("item_group", () => { - return { - filters: { - is_group: 0, - }, - }; - }); }, validate: function (frm) { @@ -304,10 +296,6 @@ frappe.ui.form.on("Item", { refresh_field("image_view"); }, - is_customer_provided_item: function (frm) { - frm.toggle_reqd("customer", frm.doc.is_customer_provided_item ? 1 : 0); - }, - is_fixed_asset: function (frm) { // set serial no to false & toggles its visibility frm.set_value("has_serial_no", 0); @@ -548,12 +536,6 @@ $.extend(erpnext.item, { }; }; - frm.fields_dict["item_group"].get_query = function (doc, cdt, cdn) { - return { - filters: [["Item Group", "docstatus", "!=", 2]], - }; - }; - frm.fields_dict["item_defaults"].grid.get_field("deferred_revenue_account").get_query = function ( doc, cdt, @@ -790,11 +772,10 @@ $.extend(erpnext.item, { default: 0, onchange: function () { let selected_attributes = get_selected_attributes(); - let lengths = []; - Object.keys(selected_attributes).map((key) => { - lengths.push(selected_attributes[key].length); + let lengths = Object.keys(selected_attributes).map((key) => { + return selected_attributes[key].length; }); - if (lengths.includes(0)) { + if (!lengths.length) { me.multiple_variant_dialog.get_primary_btn().html(__("Create Variants")); me.multiple_variant_dialog.disable_primary_action(); } else { @@ -831,7 +812,7 @@ $.extend(erpnext.item, { fieldtype: "HTML", fieldname: "help", options: ``, }, ] @@ -893,6 +874,9 @@ $.extend(erpnext.item, { selected_attributes[attribute_name].push($(opt).attr("data-fieldname")); } }); + if (!selected_attributes[attribute_name].length) { + delete selected_attributes[attribute_name]; + } }); return selected_attributes; diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 1f2581a4981..78cfa788fc7 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -31,7 +31,6 @@ "column_break_kpmi", "is_purchase_item", "is_customer_provided_item", - "customer", "section_break_gjns", "standard_rate", "column_break_ixrh", @@ -632,13 +631,6 @@ "read_only_depends_on": "eval: doc.is_purchase_item", "show_description_on_click": 1 }, - { - "depends_on": "eval:doc.is_customer_provided_item", - "fieldname": "customer", - "fieldtype": "Link", - "label": "Customer", - "options": "Customer" - }, { "depends_on": "eval:!doc.is_fixed_asset", "fieldname": "supplier_details", @@ -1077,7 +1069,7 @@ "image_field": "image", "links": [], "make_attachments_public": 1, - "modified": "2026-04-28 17:31:47.613279", + "modified": "2026-05-26 10:18:46.862670", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 7bdb0682215..0d11100ed34 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -82,7 +82,6 @@ class Item(Document): brand: DF.Link | None country_of_origin: DF.Link | None create_new_batch: DF.Check - customer: DF.Link | None customer_code: DF.SmallText | None customer_items: DF.Table[ItemCustomerDetail] customs_tariff_number: DF.Link | None @@ -872,8 +871,13 @@ class Item(Document): if disabled: frappe.throw(_("Attribute {0} is disabled.").format(frappe.bold(d.attribute))) - if not numeric_values and not frappe.db.exists( - "Item Attribute Value", {"parent": d.attribute, "attribute_value": d.attribute_value} + if ( + not numeric_values + and d.attribute_value + and not frappe.db.exists( + "Item Attribute Value", + {"parent": d.attribute, "attribute_value": d.attribute_value}, + ) ): frappe.throw( _("Attribute Value {0} is not valid for the selected attribute {1}.").format( diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 6868916103a..58241ec3b98 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -161,6 +161,7 @@ class TestItem(ERPNextTestSuite): "conversion_factor": 1, "price_list_uom_dependant": 1, "ignore_pricing_rule": 1, + "qty": 1, } ) ) diff --git a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json index 1bbafc08446..6d638b6e59b 100644 --- a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json +++ b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_bulk_edit": 1, "creation": "2014-07-11 11:51:00.453717", "doctype": "DocType", "editable_grid": 1, @@ -13,7 +14,10 @@ "amount", "base_amount", "has_corrective_cost", - "has_operating_cost" + "has_operating_cost", + "operation_id", + "qty", + "operating_component" ], "fields": [ { @@ -78,13 +82,38 @@ "fieldtype": "Check", "label": "Has Operating Cost", "read_only": 1 + }, + { + "fieldname": "operation_id", + "fieldtype": "Data", + "hidden": 1, + "label": "Operation ID", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "qty", + "fieldtype": "Float", + "hidden": 1, + "label": "Qty", + "no_copy": 1, + "non_negative": 1, + "read_only": 1 + }, + { + "fieldname": "operating_component", + "fieldtype": "Data", + "hidden": 1, + "label": "Operating Component", + "no_copy": 1, + "read_only": 1 } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-07-16 15:27:59.175530", + "modified": "2026-05-19 12:21:07.953801", "modified_by": "Administrator", "module": "Stock", "name": "Landed Cost Taxes and Charges", diff --git a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.py b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.py index a4fb129a7ae..e6a9c0535cf 100644 --- a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.py +++ b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.py @@ -22,9 +22,12 @@ class LandedCostTaxesandCharges(Document): expense_account: DF.Link | None has_corrective_cost: DF.Check has_operating_cost: DF.Check + operating_component: DF.Data | None + operation_id: DF.Data | None parent: DF.Data parentfield: DF.Data parenttype: DF.Data + qty: DF.Float # end: auto-generated types pass diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 7806a51209b..c3331ef81c4 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1374,7 +1374,7 @@ def get_billed_qty_amount_against_purchase_receipt(pr_doc): .on(parent_table.name == table.parent) .select( table.pr_detail, - fn.Sum(table.amount * parent_table.conversion_rate).as_("amount"), + fn.Sum(table.base_net_amount).as_("amount"), fn.Sum(table.qty).as_("qty"), ) .where((table.pr_detail.isin(pr_names)) & (table.docstatus == 1)) @@ -1420,7 +1420,7 @@ def get_billed_qty_amount_against_purchase_order(pr_doc): .select( table.po_detail, fn.Sum(table.qty).as_("qty"), - fn.Sum(table.amount * parent_table.conversion_rate).as_("amount"), + fn.Sum(table.base_net_amount).as_("amount"), ) .where((table.po_detail.isin(po_names)) & (table.docstatus == 1) & (table.pr_detail.isnull())) .groupby(table.po_detail) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 66e228d7e90..fb5db1c95e2 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -5606,6 +5606,157 @@ class TestPurchaseReceipt(ERPNextTestSuite): self.assertIn(company.default_inventory_account, gl_accounts) pr.cancel() + @ERPNextTestSuite.change_settings( + "Buying Settings", {"set_landed_cost_based_on_purchase_invoice_rate": 1, "maintain_same_rate": 0} + ) + def test_srbnb_with_inclusive_tax_and_rate_change_in_pi(self): + """ + When 'Set Landed Cost Based on PI Rate' is enabled and PI has an inclusive tax: + - PR: qty=2, rate=1000 INR → base_net_amount=2000 + - PI: rate changed to 2000, 5% tax included in basic rate + → PI base_net_amount = 2 * 2000 / 1.05 ≈ 3809.52 + + The system must use PI's base_net_amount (not amount=4000) so that + SRBNB credit on PR = 3809.52, not 4000. + """ + company = "_Test Company with perpetual inventory" + warehouse = "Stores - TCP1" + cost_center = "Main - TCP1" + + item_code = make_item( + "Test Item for SRBNB Inclusive Tax Rate Change", + {"is_stock_item": 1}, + ).name + + pr = make_purchase_receipt( + item_code=item_code, + qty=2, + rate=1000, + company=company, + warehouse=warehouse, + cost_center=cost_center, + ) + + pi = make_purchase_invoice(pr.name) + pi.items[0].rate = 2000 + pi.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account VAT - TCP1", + "category": "Total", + "add_deduct_tax": "Add", + "included_in_print_rate": 1, + "rate": 5, + "description": "Test Inclusive Tax", + "cost_center": cost_center, + }, + ) + pi.save() + pi.submit() + + pr.reload() + + # PI base_net_amount = qty * (rate / (1 + tax_rate/100)) = 2 * (2000 / 1.05) + pi_base_net_amount = flt(2 * 2000 / 1.05, 2) + pr_base_net_amount = flt(pr.items[0].amount, 2) # 2 * 1000 = 2000 + expected_diff = flt(pi_base_net_amount - pr_base_net_amount, 2) + + self.assertAlmostEqual(pr.items[0].amount_difference_with_purchase_invoice, expected_diff, places=2) + + # Total SRBNB credit = PR base_net_amount + amount_difference = PI base_net_amount + srbnb_account = "Stock Received But Not Billed - TCP1" + gl_entries = get_gl_entries("Purchase Receipt", pr.name, skip_cancelled=True) + srbnb_credit = sum(flt(row.credit) for row in gl_entries if row.account == srbnb_account) + self.assertAlmostEqual(srbnb_credit, pi_base_net_amount, places=2) + + @ERPNextTestSuite.change_settings( + "Buying Settings", {"set_landed_cost_based_on_purchase_invoice_rate": 1, "maintain_same_rate": 0} + ) + def test_srbnb_with_inclusive_tax_and_exchange_rate_change_in_pi(self): + """ + When 'Set Landed Cost Based on PI Rate' is enabled, PI has an inclusive tax, and only + the exchange rate changes on the PI (rate stays the same): + - PR: qty=2, rate=100 USD, conversion_rate=70 → base_net_amount=14000 INR + - PI: same rate=100 USD, conversion_rate changed to 90, 5% tax included in basic rate + → PI base_net_amount = 2 * (100 / 1.05) * 90 ≈ 17142.86 INR + + The system must use PI's base_net_amount (not amount = 2*100*90 = 18000) so that + SRBNB credit on PR = 17142.86, not 18000. + """ + from erpnext.accounts.doctype.account.test_account import create_account + + company = "_Test Company with perpetual inventory" + warehouse = "Stores - TCP1" + cost_center = "Main - TCP1" + + party_account = create_account( + account_name="USD Payable For SRBNB Exchange Rate Test", + parent_account="Accounts Payable - TCP1", + account_type="Payable", + company=company, + account_currency="USD", + ) + + supplier = create_supplier( + supplier_name="_Test USD Supplier for SRBNB Exchange Rate", + default_currency="USD", + party_account=party_account, + ).name + + item_code = make_item( + "Test Item for SRBNB Inclusive Tax Exchange Rate Change", + {"is_stock_item": 1}, + ).name + + pr = make_purchase_receipt( + item_code=item_code, + qty=2, + rate=100, + currency="USD", + conversion_rate=70, + company=company, + warehouse=warehouse, + supplier=supplier, + ) + + pi = make_purchase_invoice(pr.name) + pi.conversion_rate = 90 + pi.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account VAT - TCP1", + "category": "Total", + "add_deduct_tax": "Add", + "included_in_print_rate": 1, + "rate": 5, + "description": "Test Inclusive Tax", + "cost_center": cost_center, + }, + ) + pi.save() + pi.submit() + + pr.reload() + + # PI base_net_amount = qty * (rate / (1 + tax_rate/100)) * new_conversion_rate + # = 2 * (100 / 1.05) * 90 ≈ 17142.86 INR + # PR base_net_amount = qty * rate * pr_conversion_rate = 2 * 100 * 70 = 14000 INR + tax_amount_pr = (200 - flt(200 / 1.05, 2)) * 90 + + pi_base_net_amount = flt(2 * 100 * 90) - flt(tax_amount_pr) + pr_base_net_amount = flt(2 * 100 * 70) + expected_diff = flt(pi_base_net_amount - pr_base_net_amount) + + self.assertAlmostEqual(pr.items[0].amount_difference_with_purchase_invoice, expected_diff, places=2) + + # Total SRBNB credit = PR base_net_amount + amount_difference = PI base_net_amount + srbnb_account = "Stock Received But Not Billed - TCP1" + gl_entries = get_gl_entries("Purchase Receipt", pr.name, skip_cancelled=True) + srbnb_credit = sum(flt(row.credit) for row in gl_entries if row.account == srbnb_account) + self.assertAlmostEqual(srbnb_credit, pi_base_net_amount, places=2) + def create_asset_category_for_pr_test(): category_name = "Test Asset Category for PR" diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 33301f152fe..35557dd0d1e 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -9,7 +9,7 @@ import frappe from frappe import _, bold from frappe.model.mapper import get_mapped_doc from frappe.query_builder import DocType -from frappe.query_builder.functions import Sum +from frappe.query_builder.functions import Max, Sum from frappe.utils import ( cint, comma_or, @@ -1288,13 +1288,21 @@ class StockEntry(StockController, SubcontractingInwardController): ) def get_basic_rate_for_repacked_items(self, finished_item_qty, outgoing_items_cost): - finished_items = [d.item_code for d in self.get("items") if d.is_finished_item] + finished_items = [ + d.item_code for d in self.get("items") if d.is_finished_item and not d.set_basic_rate_manually + ] if len(finished_items) == 1: return flt(outgoing_items_cost / finished_item_qty) else: unique_finished_items = set(finished_items) - if len(unique_finished_items) == 1: - total_fg_qty = sum([flt(d.transfer_qty) for d in self.items if d.is_finished_item]) + if unique_finished_items: + total_fg_qty = sum( + [ + flt(d.transfer_qty) + for d in self.items + if d.is_finished_item and not d.set_basic_rate_manually + ] + ) return flt(outgoing_items_cost / total_fg_qty) def get_basic_rate_for_manufactured_item(self, finished_item_qty, outgoing_items_cost=0) -> float: @@ -3895,28 +3903,33 @@ def get_work_order_details(work_order, company): } -def get_consumed_operating_cost(wo_name, bom_no): +def get_consumed_operating_cost(wo_name, bom_no, operation_id): table = frappe.qb.DocType("Stock Entry") child_table = frappe.qb.DocType("Landed Cost Taxes and Charges") query = ( frappe.qb.from_(child_table) .join(table) .on(child_table.parent == table.name) - .select(Sum(child_table.amount).as_("consumed_cost")) + .select( + Sum(child_table.amount).as_("consumed_cost"), + Sum(child_table.qty).as_("consumed_qty"), + child_table.operating_component, + ) .where( (table.docstatus == 1) & (table.work_order == wo_name) & (table.purpose == "Manufacture") & (table.bom_no == bom_no) & (child_table.has_operating_cost == 1) + & (child_table.operation_id == operation_id) ) + .groupby(child_table.operation_id, child_table.operating_component) ) - cost = query.run(pluck="consumed_cost") - return cost[0] if cost and cost[0] else 0 + return query.run(as_dict=True) -def get_operating_cost_per_unit(work_order=None, bom_no=None): - operating_cost_per_unit = 0 +def get_remaining_operating_cost(work_order=None, bom_no=None): + remaining_operating_cost = 0 if work_order: if ( bom_no @@ -3931,23 +3944,23 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None): bom_no = work_order.bom_no for d in work_order.get("operations"): + consumed_op_cost = get_consumed_operating_cost(work_order.name, bom_no, d.name) or [] + cost = 0 + for row in consumed_op_cost: + cost += flt(row.consumed_cost) + if flt(d.completed_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 - ) + remaining_operating_cost += flt(d.actual_operating_cost - cost) elif work_order.qty: - operating_cost_per_unit += flt(d.planned_operating_cost) / flt(work_order.qty) + remaining_operating_cost += flt(d.planned_operating_cost) / flt(work_order.qty) # Get operating cost from BOM if not found in work_order. - if not operating_cost_per_unit and bom_no: + if not remaining_operating_cost and bom_no: bom = frappe.db.get_value("BOM", bom_no, ["operating_cost", "quantity"], as_dict=1) if bom.quantity: - operating_cost_per_unit = flt(bom.operating_cost) / flt(bom.quantity) + remaining_operating_cost = flt(bom.operating_cost) / flt(bom.quantity) - return operating_cost_per_unit + return remaining_operating_cost def get_used_alternative_items( diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index d61c4086e1d..25f4f48ef33 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -567,15 +567,18 @@ class StockReconciliation(StockController): def calculate_difference_amount(self, item, item_dict): qty_precision = item.precision("qty") - val_precision = item.precision("valuation_rate") + amount_precision = item.precision("amount") new_qty = flt(item.qty, qty_precision) - new_valuation_rate = flt(item.valuation_rate or item_dict.get("rate"), val_precision) + new_valuation_rate = flt(item.valuation_rate or item_dict.get("rate")) current_qty = flt(item_dict.get("qty"), qty_precision) - current_valuation_rate = flt(item_dict.get("rate"), val_precision) + current_valuation_rate = flt(item_dict.get("rate")) - self.difference_amount += (new_qty * new_valuation_rate) - (current_qty * current_valuation_rate) + new_amount = flt(new_qty * new_valuation_rate, amount_precision) + current_amount = flt(current_qty * current_valuation_rate, amount_precision) + + self.difference_amount += new_amount - current_amount def validate_data(self): def _get_msg(row_num, msg): @@ -885,7 +888,7 @@ class StockReconciliation(StockController): "company": self.company, "stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"), "is_cancelled": 1 if self.docstatus == 2 else 0, - "valuation_rate": flt(row.valuation_rate, row.precision("valuation_rate")), + "valuation_rate": flt(row.valuation_rate), } ) @@ -1045,84 +1048,6 @@ class StockReconciliation(StockController): else: self._cancel() - def recalculate_current_qty(self, voucher_detail_no, sle_creation, add_new_sle=False): - for row in self.items: - if voucher_detail_no != row.name: - continue - - if row.current_qty < 0: - return - - val_rate = 0.0 - current_qty = 0.0 - if row.current_serial_and_batch_bundle: - current_qty = self.get_current_qty_for_serial_or_batch(row, sle_creation) - elif row.serial_no: - item_dict = get_stock_balance_for( - row.item_code, - row.warehouse, - self.posting_date, - self.posting_time, - row=row, - company=self.company, - ) - - current_qty = item_dict.get("qty") - row.current_serial_no = item_dict.get("serial_nos") - row.current_valuation_rate = item_dict.get("rate") - val_rate = item_dict.get("rate") - elif row.batch_no: - current_qty = get_batch_qty_for_stock_reco( - row.item_code, - row.warehouse, - row.batch_no, - self.posting_date, - self.posting_time, - self.name, - sle_creation, - ) - - precesion = row.precision("current_qty") - if flt(current_qty, precesion) != flt(row.current_qty, precesion): - if not row.serial_no: - val_rate = get_incoming_rate( - frappe._dict( - { - "item_code": row.item_code, - "warehouse": row.warehouse, - "qty": current_qty * -1, - "serial_and_batch_bundle": row.current_serial_and_batch_bundle, - "batch_no": row.batch_no, - "voucher_type": self.doctype, - "voucher_no": self.name, - "company": self.company, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - } - ) - ) - - row.current_valuation_rate = val_rate - row.current_qty = current_qty - row.db_set( - { - "current_qty": row.current_qty, - "current_valuation_rate": row.current_valuation_rate, - "current_amount": flt(row.current_qty * row.current_valuation_rate), - } - ) - - if add_new_sle and not frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_detail_no": row.name, "actual_qty": ("<", 0), "is_cancelled": 0}, - "name", - ): - if not row.current_serial_and_batch_bundle: - self.set_current_serial_and_batch_bundle(voucher_detail_no, save=True) - row.reload() - - self.add_missing_stock_ledger_entry(row, voucher_detail_no, sle_creation) - def add_missing_stock_ledger_entry(self, row, voucher_detail_no, sle_creation): if row.current_qty == 0: return diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 4c8b666ef5d..a0fc5a0161e 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -1044,7 +1044,7 @@ class TestStockReconciliation(ERPNextTestSuite, StockTestMixin): sr.reload() self.assertTrue(sr.items[0].serial_and_batch_bundle) - self.assertTrue(sr.items[0].current_serial_and_batch_bundle) + self.assertFalse(sr.items[0].current_serial_and_batch_bundle) def test_not_reconcile_all_batch(self): from erpnext.stock.doctype.batch.batch import get_batch_qty diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index 2d85675f2ea..8250186dc6d 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -320,9 +320,8 @@ def clean_all_descriptions(): @frappe.whitelist() def get_enable_stock_uom_editing(): - return frappe.get_cached_value( + return frappe.get_single_value( "Stock Settings", - None, ["allow_to_edit_stock_uom_qty_for_sales", "allow_to_edit_stock_uom_qty_for_purchase"], as_dict=1, ) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 5ec61576783..7c03575b870 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -1198,9 +1198,15 @@ def get_item_price( if not ignore_party: if pctx.customer: - query = query.where(ip.customer == pctx.customer) + query = query.where( + (ip.customer == pctx.customer) + | ((IfNull(ip.customer, "") == "") & (IfNull(ip.supplier, "") == "")) + ).orderby(IfNull(ip.customer, ""), order=frappe.qb.desc) elif pctx.supplier: - query = query.where(ip.supplier == pctx.supplier) + query = query.where( + (ip.supplier == pctx.supplier) + | ((IfNull(ip.customer, "") == "") & (IfNull(ip.supplier, "") == "")) + ).orderby(IfNull(ip.supplier, ""), order=frappe.qb.desc) else: query = query.where((IfNull(ip.customer, "") == "") & (IfNull(ip.supplier, "") == "")) @@ -1258,9 +1264,6 @@ def get_price_list_rate_for(ctx: ItemDetailsCtx, item_code): if desired_qty and check_packing_list(price_list_rate[0].name, desired_qty, item_code): item_price_data = price_list_rate else: - for field in ["customer", "supplier"]: - del pctx[field] - general_price_list_rate = get_item_price(pctx, item_code, ignore_party=ctx.get("ignore_party")) if not general_price_list_rate and ctx.get("uom") != ctx.get("stock_uom"): diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 0eb4943499e..bbe5392f4c3 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -7,7 +7,7 @@ from operator import itemgetter import frappe from frappe import _ -from frappe.query_builder.functions import Count +from frappe.query_builder.functions import Abs, Count from frappe.utils import cint, date_diff, flt, get_datetime from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -15,10 +15,25 @@ from erpnext.stock.valuation import round_off_if_near_zero Filters = frappe._dict +FIFO_POSTING_DATE_INDEX = -2 +FIFO_QTY_INDEX = 0 +FIFO_DATE_INDEX = 1 +FIFO_VALUE_INDEX = 2 + +BATCH_SLOT_SIZE = 5 +BATCH_SLOT_BATCH_INDEX = 0 +BATCH_SLOT_VALUATION_INDEX = 1 +BATCH_SLOT_QTY_INDEX = 2 +BATCH_SLOT_DATE_INDEX = 3 +BATCH_SLOT_VALUE_INDEX = 4 + +AVERAGE_AGE_COLUMN = 6 +MAX_CHART_ITEMS = 10 + def execute(filters: Filters = None) -> tuple: to_date = filters["to_date"] - filters.ranges = [num.strip() for num in filters.range.split(",") if num.strip().isdigit()] + filters.ranges = get_age_ranges(filters.range) columns = get_columns(filters) item_details = FIFOSlots(filters).generate() @@ -29,107 +44,125 @@ def execute(filters: Filters = None) -> tuple: return columns, data, None, chart_data -def format_report_data(filters: Filters, item_details: dict, to_date: str) -> list[dict]: +def get_age_ranges(age_range: str) -> list[str]: + return [num.strip() for num in age_range.split(",") if num.strip().isdigit()] + + +def get_float_precision() -> int: + return cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True)) + + +def format_report_data(filters: Filters, item_details: dict, to_date: str) -> list[list]: "Returns ordered, formatted data with ranges." - _func = itemgetter(1) data = [] - precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True)) + precision = get_float_precision() for _item, item_dict in item_details.items(): if not flt(item_dict.get("total_qty"), precision): continue - earliest_age, latest_age = 0, 0 details = item_dict["details"] - - fifo_queue = sorted(filter(_func, item_dict["fifo_queue"]), key=_func) - + fifo_queue = get_report_fifo_queue(item_dict["fifo_queue"], details.has_batch_no) if not fifo_queue: continue - average_age = get_average_age(fifo_queue, to_date) - earliest_age = date_diff(to_date, fifo_queue[0][1]) - latest_age = date_diff(to_date, fifo_queue[-1][1]) - range_values = get_range_age(filters, fifo_queue, to_date, item_dict) - - check_and_replace_valuations_if_moving_average( - range_values, details.valuation_method, details.valuation_rate, filters.get("company") - ) - - row = [details.name, details.item_name, details.description, details.item_group, details.brand] - - if filters.get("show_warehouse_wise_stock"): - row.append(details.warehouse) - - row.extend( - [ - flt(item_dict.get("total_qty"), precision), - average_age, - *range_values, - earliest_age, - latest_age, - details.stock_uom, - ] - ) - - data.append(row) + data.append(get_report_row(filters, item_dict, fifo_queue, to_date, precision)) return data -def check_and_replace_valuations_if_moving_average( - range_values, item_valuation_method, valuation_rate, company -): - if item_valuation_method == "Moving Average" or ( - not item_valuation_method - and frappe.get_cached_value("Company", company, "valuation_method") == "Moving Average" - ): - for i in range(0, len(range_values), 2): - range_values[i + 1] = range_values[i] * valuation_rate +def get_report_fifo_queue(fifo_queue: list, has_batch_no: bool) -> list: + get_posting_date = itemgetter(FIFO_POSTING_DATE_INDEX) + fifo_queue = sorted([slot for slot in fifo_queue if get_posting_date(slot)], key=get_posting_date) + + if has_batch_no: + return [get_batch_report_slot(slot) for slot in fifo_queue] + + return fifo_queue + + +def get_batch_report_slot(slot: list) -> list: + if is_batch_slot(slot): + return slot[BATCH_SLOT_QTY_INDEX:] + + return slot + + +def get_report_row(filters: Filters, item_dict: dict, fifo_queue: list, to_date: str, precision: int) -> list: + details = item_dict["details"] + range_values = get_range_age(filters, fifo_queue, to_date, item_dict, precision) + row = [details.name, details.item_name, details.description, details.item_group, details.brand] + + if filters.get("show_warehouse_wise_stock"): + row.append(details.warehouse) + + row.extend( + [ + flt(item_dict.get("total_qty"), precision), + get_average_age(fifo_queue, to_date), + *range_values, + date_diff(to_date, fifo_queue[0][FIFO_DATE_INDEX]), + date_diff(to_date, fifo_queue[-1][FIFO_DATE_INDEX]), + details.stock_uom, + ] + ) + + return row def get_average_age(fifo_queue: list, to_date: str) -> float: - batch_age = age_qty = total_qty = 0.0 - for batch in fifo_queue: - batch_age = date_diff(to_date, batch[1]) - - if isinstance(batch[0], int | float): - age_qty += batch_age * batch[0] - total_qty += batch[0] - else: - age_qty += batch_age * 1 - total_qty += 1 + age_qty = total_qty = 0.0 + for slot in fifo_queue: + qty = get_slot_qty(slot) + age_qty += date_diff(to_date, slot[FIFO_DATE_INDEX]) * qty + total_qty += qty return flt(age_qty / total_qty, 2) if total_qty else 0.0 -def get_range_age(filters: Filters, fifo_queue: list, to_date: str, item_dict: dict) -> list: - precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True)) +def get_slot_qty(slot: list) -> float: + if is_qty_slot(slot): + return slot[FIFO_QTY_INDEX] + + return 1.0 + + +def get_range_age( + filters: Filters, fifo_queue: list, to_date: str, item_dict: dict, precision: int | None = None +) -> list: + precision = precision if precision is not None else get_float_precision() range_values = [0.0] * ((len(filters.ranges) * 2) + 2) - for item in fifo_queue: - age = flt(date_diff(to_date, item[1])) - qty = flt(item[0]) if not item_dict["has_serial_no"] else 1.0 - stock_value = flt(item[2]) - - for i, age_limit in enumerate(filters.ranges): - if age <= flt(age_limit): - i *= 2 - range_values[i] = flt(range_values[i] + qty, precision) - range_values[i + 1] = flt(range_values[i + 1] + stock_value, precision) - if range_values[i] == 0.0 and round_off_if_near_zero(range_values[i + 1], 2) == 0: - range_values[i + 1] = 0.0 - break - else: - range_values[-2] = flt(range_values[-2] + qty, precision) - range_values[-1] = flt(range_values[-1] + stock_value, precision) - if range_values[-2] == 0.0 and round_off_if_near_zero(range_values[-1], 2) == 0: - range_values[-1] = 0.0 + for slot in fifo_queue: + bucket_index = get_age_bucket_index(filters.ranges, slot, to_date) + qty = 1.0 if item_dict["has_serial_no"] else flt(slot[FIFO_QTY_INDEX]) + stock_value = flt(slot[FIFO_VALUE_INDEX]) + add_to_range_bucket(range_values, bucket_index, qty, stock_value, precision) return range_values +def get_age_bucket_index(age_ranges: list, slot: list, to_date: str) -> int: + age = flt(date_diff(to_date, slot[FIFO_DATE_INDEX])) + + for index, age_limit in enumerate(age_ranges): + if age <= flt(age_limit): + return index * 2 + + return len(age_ranges) * 2 + + +def add_to_range_bucket( + range_values: list, bucket_index: int, qty: float, stock_value: float, precision: int +) -> None: + range_values[bucket_index] = flt(range_values[bucket_index] + qty, precision) + range_values[bucket_index + 1] = flt(range_values[bucket_index + 1] + stock_value, precision) + + if range_values[bucket_index] == 0.0 and round_off_if_near_zero(range_values[bucket_index + 1], 2) == 0: + range_values[bucket_index + 1] = 0.0 + + def get_columns(filters: Filters) -> list[dict]: range_columns = [] setup_ageing_columns(filters, range_columns) @@ -197,14 +230,14 @@ def get_chart_data(data: list, filters: Filters) -> dict: if filters.get("show_warehouse_wise_stock"): return {} - data.sort(key=lambda row: row[6], reverse=True) + data.sort(key=lambda row: row[AVERAGE_AGE_COLUMN], reverse=True) - if len(data) > 10: - data = data[:10] + if len(data) > MAX_CHART_ITEMS: + data = data[:MAX_CHART_ITEMS] for row in data: labels.append(row[0]) - datapoints.append(row[6]) + datapoints.append(row[AVERAGE_AGE_COLUMN]) return { "data": {"labels": labels, "datasets": [{"name": _("Average Age"), "values": datapoints}]}, @@ -215,9 +248,9 @@ def get_chart_data(data: list, filters: Filters) -> dict: def setup_ageing_columns(filters: Filters, range_columns: list): prev_range_value = 0 ranges = [] - for range in filters.ranges: - ranges.append(f"{prev_range_value} - {range}") - prev_range_value = cint(range) + 1 + for age_range in filters.ranges: + ranges.append(f"{prev_range_value} - {age_range}") + prev_range_value = cint(age_range) + 1 ranges.append(f"{prev_range_value} - Above") @@ -231,19 +264,29 @@ def add_column(range_columns: list, label: str, fieldname: str, fieldtype: str = range_columns.append(dict(label=label, fieldname=fieldname, fieldtype=fieldtype, width=width)) +def is_batch_slot(slot: list) -> bool: + return len(slot) == BATCH_SLOT_SIZE + + +def is_qty_slot(slot: list) -> bool: + return isinstance(slot[FIFO_QTY_INDEX], int | float) + + class FIFOSlots: "Returns FIFO computed slots of inwarded stock as per date." def __init__(self, filters: dict | None = None, sle: list | None = None): self.item_details = {} self.transferred_item_details = {} - self.serial_no_batch_purchase_details = {} + self.serial_no_details = {} + self.batch_no_details = {} + self.batchwise_valuation_by_batch = {} self.filters = filters self.sle = sle def generate(self) -> dict: """ - Returns dict of the foll.g structure: + Returns dict of the following structure: Key = Item A / (Item A, Warehouse A) Key: { 'details' -> Dict: ** item details **, @@ -251,80 +294,136 @@ class FIFOSlots: consumed/updated and maintained via FIFO. ** } """ - from erpnext.stock.serial_batch_bundle import get_serial_nos_from_bundle - stock_ledger_entries = self.sle - - bundle_wise_serial_nos = frappe._dict({}) - if stock_ledger_entries is None: - bundle_wise_serial_nos = self.__get_bundle_wise_serial_nos() + bundle_wise_serial_nos, bundle_wise_batch_nos = self._get_bundle_wise_details(stock_ledger_entries) # 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() + 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" 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 - 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 [] - if d.serial_and_batch_bundle and d.has_serial_no: - if bundle_wise_serial_nos: - serial_nos = bundle_wise_serial_nos.get(d.serial_and_batch_bundle) or [] - else: - serial_nos = sorted(get_serial_nos_from_bundle(d.serial_and_batch_bundle)) or [] - - serial_nos = self.uppercase_serial_nos(serial_nos) - if d.actual_qty > 0: - self.__compute_incoming_stock(d, fifo_queue, transferred_item_key, serial_nos) - else: - self.__compute_outgoing_stock(d, fifo_queue, transferred_item_key, serial_nos) - - 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] + for row in stock_ledger_entries: + self._process_stock_ledger_entry(row, bundle_wise_serial_nos, bundle_wise_batch_nos) # Note that stock_ledger_entries is an iterator, you can not reuse it like a list del stock_ledger_entries if not self.filters.get("show_warehouse_wise_stock"): # (Item 1, WH 1), (Item 1, WH 2) => (Item 1) - self.item_details = self.__aggregate_details_by_item(self.item_details) + self.item_details = self._aggregate_details_by_item(self.item_details) return self.item_details + def _get_bundle_wise_details(self, stock_ledger_entries: list | None) -> tuple[dict, dict]: + if stock_ledger_entries is not None: + return frappe._dict({}), frappe._dict({}) + + return self._get_bundle_wise_serial_nos(), self._get_bundle_wise_batch_nos() + + def _process_stock_ledger_entry( + self, row: dict, bundle_wise_serial_nos: dict, bundle_wise_batch_nos: dict + ) -> None: + key, fifo_queue, transferred_item_key = self._init_key_stores(row) + prev_balance_qty = self.item_details[key].get("qty_after_transaction", 0) + + self._set_stock_reconciliation_actual_qty(row, key, fifo_queue, prev_balance_qty) + serial_nos, batch_nos = self._get_serial_and_batch_nos( + row, bundle_wise_serial_nos, bundle_wise_batch_nos + ) + + if row.actual_qty > 0: + self._compute_incoming_stock(row, fifo_queue, transferred_item_key, serial_nos, batch_nos) + else: + self._compute_outgoing_stock(row, fifo_queue, transferred_item_key, serial_nos, batch_nos) + + self._update_balances(row, key) + self._trim_serial_fifo_queue(row, key, fifo_queue) + + def _set_stock_reconciliation_actual_qty( + self, row: dict, key: tuple, fifo_queue: list, prev_balance_qty: float + ) -> None: + if row.voucher_type != "Stock Reconciliation": + return + + if not row.batch_no or row.serial_no or row.serial_and_batch_bundle: + if row.voucher_detail_no in self.stock_reco_voucher_wise_count: + # Legacy reconciliation with a single SLE has qty_after_transaction and + # stock_value_difference without an outward entry, so reset the queue first. + row.stock_value_difference = flt(row.qty_after_transaction * row.valuation_rate) + row.actual_qty = row.qty_after_transaction + self.item_details[key]["qty_after_transaction"] = 0 + self.item_details[key]["total_qty"] = 0 + fifo_queue.clear() + return + + # Stock reconciliation stores the final balance; FIFO needs the movement delta. + row.actual_qty = flt(row.qty_after_transaction) - flt(prev_balance_qty) + + def _get_serial_and_batch_nos( + self, row: dict, bundle_wise_serial_nos: dict, bundle_wise_batch_nos: dict + ) -> tuple[list, list]: + from erpnext.stock.serial_batch_bundle import get_serial_nos_from_bundle + + serial_nos = get_serial_nos(row.serial_no) if row.serial_no else [] + batch_nos = self._get_row_batch_nos(row) + + if row.serial_and_batch_bundle: + if row.has_serial_no: + if bundle_wise_serial_nos: + serial_nos = bundle_wise_serial_nos.get(row.serial_and_batch_bundle) or [] + else: + serial_nos = sorted(get_serial_nos_from_bundle(row.serial_and_batch_bundle)) or [] + elif row.has_batch_no: + if bundle_wise_batch_nos: + batch_nos = bundle_wise_batch_nos.get(row.serial_and_batch_bundle) or [] + else: + batch_nos = ( + self._get_bundle_wise_batch_nos(row.serial_and_batch_bundle).get( + row.serial_and_batch_bundle + ) + or [] + ) + + return self.uppercase_serial_nos(serial_nos), batch_nos + + def _get_row_batch_nos(self, row: dict) -> list: + if not row.batch_no: + return [] + + return [ + [ + row.batch_no.upper(), + self._get_batchwise_valuation(row.batch_no), + abs(row.actual_qty), + abs(row.stock_value_difference), + ] + ] + + def _trim_serial_fifo_queue(self, row: dict, key: tuple, fifo_queue: list) -> None: + if not row.has_serial_no: + return + + 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] + def uppercase_serial_nos(self, serial_nos): "Convert serial nos to uppercase for uniformity." return [sn.upper() for sn in serial_nos] - def __init_key_stores(self, row: dict) -> tuple: + def _get_batchwise_valuation(self, batch_no: str): + if batch_no not in self.batchwise_valuation_by_batch: + self.batchwise_valuation_by_batch[batch_no] = frappe.db.get_value( + "Batch", batch_no, "use_batchwise_valuation" + ) + + return self.batchwise_valuation_by_batch[batch_no] + + def _init_key_stores(self, row: dict) -> tuple: "Initialise keys and FIFO Queue." key = (row.name, row.warehouse) @@ -336,57 +435,209 @@ class FIFOSlots: return key, fifo_queue, transferred_item_key - def __compute_incoming_stock(self, row: dict, fifo_queue: list, transfer_key: tuple, serial_nos: list): + def _compute_incoming_stock( + self, row: dict, fifo_queue: list, transfer_key: tuple, serial_nos: list, batch_nos: list + ): "Update FIFO Queue on inward stock." - transfer_data = self.transferred_item_details.get(transfer_key) if transfer_data: # inward/outward from same voucher, item & warehouse # eg: Repack with same item, Stock reco for batch item # consume transfer data and add stock to fifo queue - self.__adjust_incoming_transfer_qty(transfer_data, fifo_queue, row) + self._adjust_incoming_transfer_qty( + transfer_data, + fifo_queue, + row, + batch_nos, + serial_nos=serial_nos if row.get("has_serial_no") else None, + ) + elif serial_nos and row.get("has_serial_no"): + self._add_serial_fifo_slots(row, fifo_queue, serial_nos) + elif batch_nos and row.get("has_batch_no"): + self._add_batch_fifo_slots(row, fifo_queue, batch_nos) + elif fifo_queue and flt(fifo_queue[0][FIFO_QTY_INDEX]) <= 0: + self._add_to_negative_fifo_head(row, fifo_queue) else: - if not serial_nos and not row.get("has_serial_no"): - if fifo_queue and flt(fifo_queue[0][0]) <= 0: - # neutralize 0/negative stock by adding positive stock - fifo_queue[0][0] += flt(row.actual_qty) - fifo_queue[0][1] = row.posting_date - fifo_queue[0][2] += flt(row.stock_value_difference) - else: - fifo_queue.append( - [flt(row.actual_qty), row.posting_date, flt(row.stock_value_difference)] - ) - return + fifo_queue.append([flt(row.actual_qty), row.posting_date, flt(row.stock_value_difference)]) - valuation = row.stock_value_difference / row.actual_qty - for serial_no in serial_nos: - if self.serial_no_batch_purchase_details.get(serial_no): - fifo_queue.append( - [serial_no, self.serial_no_batch_purchase_details.get(serial_no), valuation] - ) - else: - self.serial_no_batch_purchase_details.setdefault(serial_no, row.posting_date) - fifo_queue.append([serial_no, row.posting_date, valuation]) + def _add_serial_fifo_slots(self, row: dict, fifo_queue: list, serial_nos: list) -> None: + valuation = row.stock_value_difference / row.actual_qty + for serial_no in serial_nos: + posting_date = self.serial_no_details.setdefault(serial_no, row.posting_date) + fifo_queue.append([serial_no, posting_date, valuation]) - def __compute_outgoing_stock(self, row: dict, fifo_queue: list, transfer_key: tuple, serial_nos: list): + def _add_batch_fifo_slots(self, row: dict, fifo_queue: list, batch_nos: list) -> None: + for batch_no, use_batchwise_valuation, qty, stock_value_difference in batch_nos: + qty, stock_value_difference = self._neutralize_negative_batch_stock( + fifo_queue, row, batch_no, use_batchwise_valuation, qty, stock_value_difference + ) + + if not qty: + continue + + posting_date = self.batch_no_details.setdefault(batch_no, row.posting_date) + fifo_queue.append([batch_no, use_batchwise_valuation, qty, posting_date, stock_value_difference]) + + def _neutralize_negative_batch_stock( + self, + fifo_queue: list, + row: dict, + batch_no: str, + use_batchwise_valuation: bool, + qty: float, + stock_value_difference: float, + ) -> tuple[float, float]: + qty = flt(qty) + stock_value_difference = flt(stock_value_difference) + + if not qty: + return qty, stock_value_difference + + for slot in list(fifo_queue): + if not self._is_matching_negative_batch_slot(slot, batch_no, use_batchwise_valuation): + continue + + qty_to_adjust = min(qty, abs(flt(slot[BATCH_SLOT_QTY_INDEX]))) + value_to_adjust = ( + stock_value_difference + if qty_to_adjust == qty + else flt(stock_value_difference * (qty_to_adjust / qty)) + ) + + slot[BATCH_SLOT_QTY_INDEX] = flt(slot[BATCH_SLOT_QTY_INDEX]) + qty_to_adjust + slot[BATCH_SLOT_DATE_INDEX] = row.posting_date + slot[BATCH_SLOT_VALUE_INDEX] = flt(slot[BATCH_SLOT_VALUE_INDEX]) + value_to_adjust + + qty = flt(qty - qty_to_adjust) + stock_value_difference = flt(stock_value_difference - value_to_adjust) + + if not flt(slot[BATCH_SLOT_QTY_INDEX]) and not flt(slot[BATCH_SLOT_VALUE_INDEX]): + fifo_queue.remove(slot) + + if not qty: + break + + return qty, stock_value_difference + + def _is_matching_negative_batch_slot( + self, slot: list, batch_no: str, use_batchwise_valuation: bool, include_zero_qty: bool = False + ) -> bool: + if not is_batch_slot(slot): + return False + + qty = flt(slot[BATCH_SLOT_QTY_INDEX]) + + return ( + slot[BATCH_SLOT_BATCH_INDEX] == batch_no + and slot[BATCH_SLOT_VALUATION_INDEX] == use_batchwise_valuation + and (qty <= 0 if include_zero_qty else qty < 0) + ) + + def _add_to_negative_fifo_head(self, row: dict, fifo_queue: list) -> None: + fifo_queue[0][FIFO_QTY_INDEX] += flt(row.actual_qty) + fifo_queue[0][FIFO_DATE_INDEX] = row.posting_date + fifo_queue[0][FIFO_VALUE_INDEX] += flt(row.stock_value_difference) + + def _compute_outgoing_stock( + self, row: dict, fifo_queue: list, transfer_key: tuple, serial_nos: list, batch_nos: list + ): "Update FIFO Queue on outward stock." if serial_nos: - fifo_queue[:] = [serial_no for serial_no in fifo_queue if serial_no[0] not in serial_nos] - return + self._consume_serial_fifo_slots(fifo_queue, serial_nos) + elif batch_nos: + self._consume_batch_fifo_slots(row, fifo_queue, transfer_key, batch_nos) + else: + self._consume_fifo_slots(row, fifo_queue, transfer_key) + def _consume_serial_fifo_slots(self, fifo_queue: list, serial_nos: list) -> None: + fifo_queue[:] = [slot for slot in fifo_queue if slot[FIFO_QTY_INDEX] not in serial_nos] + + def _consume_batch_fifo_slots( + self, row: dict, fifo_queue: list, transfer_key: tuple, batch_nos: list + ) -> None: + for batch_no, use_batchwise_valuation, qty, stock_value_difference in batch_nos: + items_to_remove = [] + + for slot in fifo_queue: + if not self._can_consume_batch_slot(slot, batch_no, use_batchwise_valuation): + continue + + slot_qty = flt(slot[BATCH_SLOT_QTY_INDEX]) + slot_stock_value = flt(slot[BATCH_SLOT_VALUE_INDEX]) + + if slot_qty <= qty: + qty -= slot_qty + stock_value_difference -= slot_stock_value + self.transferred_item_details[transfer_key].append( + [slot_qty, slot[BATCH_SLOT_DATE_INDEX], slot_stock_value] + ) + items_to_remove.append(slot) + else: + slot[BATCH_SLOT_QTY_INDEX] = slot_qty - qty + # Preserve ledger valuation (moving average / SLE value), not slot proportional value. + slot[BATCH_SLOT_VALUE_INDEX] = slot_stock_value - stock_value_difference + self.transferred_item_details[transfer_key].append( + [qty, slot[BATCH_SLOT_DATE_INDEX], stock_value_difference] + ) + qty = 0 + stock_value_difference = 0 + break + + for item in items_to_remove: + fifo_queue.remove(item) + + if qty: + self._append_negative_batch_slot( + row, + fifo_queue, + transfer_key, + batch_no, + use_batchwise_valuation, + qty, + stock_value_difference, + ) + + def _can_consume_batch_slot(self, slot: list, batch_no: str, use_batchwise_valuation: bool) -> bool: + if not is_batch_slot(slot): + return False + + if flt(slot[BATCH_SLOT_QTY_INDEX]) <= 0: + return False + + if use_batchwise_valuation: + return slot[BATCH_SLOT_BATCH_INDEX] == batch_no + + return not slot[BATCH_SLOT_VALUATION_INDEX] + + def _append_negative_batch_slot( + self, + row: dict, + fifo_queue: list, + transfer_key: tuple, + batch_no: str, + use_batchwise_valuation: bool, + qty: float, + stock_value_difference: float, + ) -> None: + fifo_queue.append( + [batch_no, use_batchwise_valuation, -(qty), row.posting_date, -(stock_value_difference)] + ) + self.transferred_item_details[transfer_key].append([qty, row.posting_date, stock_value_difference]) + + def _consume_fifo_slots(self, row: dict, fifo_queue: list, transfer_key: tuple) -> None: qty_to_pop = abs(row.actual_qty) stock_value = abs(row.stock_value_difference) while qty_to_pop: slot = fifo_queue[0] if fifo_queue else [0, None, 0] - if 0 < flt(slot[0]) <= qty_to_pop: - # qty to pop >= slot qty - # if +ve and not enough or exactly same balance in current slot, consume whole slot - qty_to_pop -= flt(slot[0]) - stock_value -= flt(slot[2]) + slot_qty = flt(slot[FIFO_QTY_INDEX]) + slot_value = flt(slot[FIFO_VALUE_INDEX]) + + if 0 < slot_qty <= qty_to_pop: + qty_to_pop -= slot_qty + stock_value -= slot_value self.transferred_item_details[transfer_key].append(fifo_queue.pop(0)) elif not fifo_queue: - # negative stock, no balance but qty yet to consume fifo_queue.append([-(qty_to_pop), row.posting_date, -(stock_value)]) self.transferred_item_details[transfer_key].append( [qty_to_pop, row.posting_date, stock_value] @@ -394,48 +645,179 @@ class FIFOSlots: qty_to_pop = 0 stock_value = 0 else: - # qty to pop < slot qty, ample balance - # consume actual_qty from first slot - slot[0] = flt(slot[0]) - qty_to_pop - slot[2] = flt(slot[2]) - stock_value - self.transferred_item_details[transfer_key].append([qty_to_pop, slot[1], stock_value]) + slot[FIFO_QTY_INDEX] = slot_qty - qty_to_pop + slot[FIFO_VALUE_INDEX] = slot_value - stock_value + self.transferred_item_details[transfer_key].append( + [qty_to_pop, slot[FIFO_DATE_INDEX], stock_value] + ) qty_to_pop = 0 stock_value = 0 - def __adjust_incoming_transfer_qty(self, transfer_data: dict, fifo_queue: list, row: dict): + def _adjust_incoming_transfer_qty( + self, + transfer_data: dict, + fifo_queue: list, + row: dict, + batch_nos: list | None = None, + serial_nos: list | None = None, + ): "Add previously removed stock back to FIFO Queue." transfer_qty_to_pop = flt(row.actual_qty) stock_value = flt(row.stock_value_difference) - - def add_to_fifo_queue(slot): - if fifo_queue and flt(fifo_queue[0][0]) <= 0: - # neutralize 0/negative stock by adding positive stock - fifo_queue[0][0] += flt(slot[0]) - fifo_queue[0][1] = slot[1] - fifo_queue[0][2] += flt(slot[2]) - else: - fifo_queue.append(slot) + batch_nos = [list(batch_no) for batch_no in batch_nos or []] + serial_nos = list(serial_nos or []) while transfer_qty_to_pop: - if transfer_data and 0 < transfer_data[0][0] <= transfer_qty_to_pop: + if transfer_data and 0 < flt(transfer_data[0][FIFO_QTY_INDEX]) <= transfer_qty_to_pop: # bucket qty is not enough, consume whole - transfer_qty_to_pop -= transfer_data[0][0] - stock_value -= transfer_data[0][2] - add_to_fifo_queue(transfer_data.pop(0)) + transfer_qty = flt(transfer_data[0][FIFO_QTY_INDEX]) + transfer_date = transfer_data[0][FIFO_DATE_INDEX] + transfer_value = flt(transfer_data[0][FIFO_VALUE_INDEX]) + transfer_qty_to_pop -= transfer_qty + stock_value -= transfer_value + self._add_incoming_transfer_slots( + fifo_queue, batch_nos, transfer_qty, transfer_date, transfer_value, serial_nos + ) + transfer_data.pop(0) elif not transfer_data: # transfer bucket is empty, extra incoming qty - add_to_fifo_queue([transfer_qty_to_pop, row.posting_date, stock_value]) + self._add_incoming_transfer_slots( + fifo_queue, batch_nos, transfer_qty_to_pop, row.posting_date, stock_value, serial_nos + ) transfer_qty_to_pop = 0 stock_value = 0 else: # ample bucket qty to consume - transfer_data[0][0] -= transfer_qty_to_pop - transfer_data[0][2] -= stock_value - add_to_fifo_queue([transfer_qty_to_pop, transfer_data[0][1], stock_value]) + transfer_data[0][FIFO_QTY_INDEX] -= transfer_qty_to_pop + transfer_data[0][FIFO_VALUE_INDEX] -= stock_value + self._add_incoming_transfer_slots( + fifo_queue, + batch_nos, + transfer_qty_to_pop, + transfer_data[0][FIFO_DATE_INDEX], + stock_value, + serial_nos, + ) transfer_qty_to_pop = 0 stock_value = 0 - def __update_balances(self, row: dict, key: tuple | str): + def _add_incoming_transfer_slots( + self, + fifo_queue: list, + batch_nos: list, + qty: float, + posting_date: str, + value: float, + serial_nos: list | None = None, + ) -> None: + for slot in self._get_incoming_transfer_slots(batch_nos, qty, posting_date, value, serial_nos): + self._add_transfer_slot_to_fifo_queue(fifo_queue, slot) + + def _get_incoming_transfer_slots( + self, + batch_nos: list, + qty: float, + posting_date: str, + value: float, + serial_nos: list | None = None, + ) -> list: + if serial_nos: + return self._get_serial_incoming_transfer_slots(serial_nos, qty, posting_date, value) + + if not batch_nos: + return [[qty, posting_date, value]] + + incoming_slots = [] + remaining_qty = flt(qty) + remaining_value = flt(value) + + while remaining_qty and batch_nos: + batch_no, use_batchwise_valuation, batch_qty, _ = batch_nos[0] + batch_qty = flt(batch_qty) + slot_qty = min(batch_qty, remaining_qty) + slot_value = ( + remaining_value + if slot_qty == remaining_qty + else flt(remaining_value * (slot_qty / remaining_qty)) + ) + + incoming_slots.append([batch_no, use_batchwise_valuation, slot_qty, posting_date, slot_value]) + + batch_nos[0][2] = flt(batch_qty - slot_qty) + if not batch_nos[0][2]: + batch_nos.pop(0) + + remaining_qty = flt(remaining_qty - slot_qty) + remaining_value = flt(remaining_value - slot_value) + + if remaining_qty: + incoming_slots.append([remaining_qty, posting_date, remaining_value]) + + return incoming_slots + + def _get_serial_incoming_transfer_slots( + self, serial_nos: list, qty: float, posting_date: str, value: float + ) -> list: + incoming_slots = [] + remaining_value = flt(value) + serial_count = min(cint(qty), len(serial_nos)) + + for index in range(serial_count): + serial_no = serial_nos.pop(0) + serial_value = remaining_value if index == serial_count - 1 else flt(value / serial_count) + serial_posting_date = self.serial_no_details.setdefault(serial_no, posting_date) + + incoming_slots.append([serial_no, serial_posting_date, serial_value]) + remaining_value = flt(remaining_value - serial_value) + + return incoming_slots + + def _add_transfer_slot_to_fifo_queue(self, fifo_queue: list, slot: list) -> None: + matching_negative_batch_slot = self._get_matching_negative_batch_slot(fifo_queue, slot) + + if ( + fifo_queue + and is_qty_slot(fifo_queue[0]) + and is_qty_slot(slot) + and flt(fifo_queue[0][FIFO_QTY_INDEX]) <= 0 + ): + fifo_queue[0][FIFO_QTY_INDEX] += flt(slot[FIFO_QTY_INDEX]) + fifo_queue[0][FIFO_DATE_INDEX] = slot[FIFO_DATE_INDEX] + fifo_queue[0][FIFO_VALUE_INDEX] += flt(slot[FIFO_VALUE_INDEX]) + elif matching_negative_batch_slot: + matching_negative_batch_slot[BATCH_SLOT_QTY_INDEX] += flt(slot[BATCH_SLOT_QTY_INDEX]) + matching_negative_batch_slot[BATCH_SLOT_DATE_INDEX] = slot[BATCH_SLOT_DATE_INDEX] + matching_negative_batch_slot[BATCH_SLOT_VALUE_INDEX] += flt(slot[BATCH_SLOT_VALUE_INDEX]) + if self._is_empty_batch_slot(matching_negative_batch_slot): + fifo_queue.remove(matching_negative_batch_slot) + else: + fifo_queue.append(slot) + + def _is_empty_batch_slot(self, slot: list) -> bool: + return ( + not flt(slot[BATCH_SLOT_QTY_INDEX]) + and round_off_if_near_zero(slot[BATCH_SLOT_VALUE_INDEX], 2) == 0 + ) + + def _get_matching_negative_batch_slot(self, fifo_queue: list, slot: list) -> list | None: + if not is_batch_slot(slot): + return None + + return next( + ( + existing_slot + for existing_slot in fifo_queue + if self._is_matching_negative_batch_slot( + existing_slot, + slot[BATCH_SLOT_BATCH_INDEX], + slot[BATCH_SLOT_VALUATION_INDEX], + include_zero_qty=True, + ) + ), + None, + ) + + 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 @@ -443,9 +825,10 @@ class FIFOSlots: self.item_details[key]["total_qty"] += row.actual_qty self.item_details[key]["has_serial_no"] = row.has_serial_no + self.item_details[key]["has_batch_no"] = row.has_batch_no self.item_details[key]["details"].valuation_rate = row.valuation_rate - def __aggregate_details_by_item(self, wh_wise_data: dict) -> dict: + def _aggregate_details_by_item(self, wh_wise_data: dict) -> dict: "Aggregate Item-Wh wise data into single Item entry." item_aggregated_data = {} for key, row in wh_wise_data.items(): @@ -466,12 +849,13 @@ class FIFOSlots: item_row["qty_after_transaction"] += flt(row["qty_after_transaction"]) item_row["total_qty"] += flt(row["total_qty"]) item_row["has_serial_no"] = row["has_serial_no"] + item_row["has_batch_no"] = row["has_batch_no"] return item_aggregated_data - def __get_stock_ledger_entries(self) -> Iterator[dict]: + def _get_stock_ledger_entries(self) -> Iterator[dict]: sle = frappe.qb.DocType("Stock Ledger Entry") - item = self.__get_item_query() # used as derived table in sle query + item = self._get_item_query() # used as derived table in sle query to_date = get_datetime(self.filters.get("to_date") + " 23:59:59") sle_query = ( @@ -484,8 +868,8 @@ class FIFOSlots: item.brand, item.description, item.stock_uom, + item.has_batch_no, item.has_serial_no, - item.valuation_method, sle.actual_qty, sle.stock_value_difference, sle.valuation_rate, @@ -508,7 +892,7 @@ class FIFOSlots: ) if self.filters.get("warehouse"): - sle_query = self.__get_warehouse_conditions(sle, sle_query) + sle_query = self._get_warehouse_conditions(sle, sle_query) elif self.filters.get("warehouse_type"): warehouses = frappe.get_all( "Warehouse", @@ -523,7 +907,7 @@ class FIFOSlots: return sle_query.run(as_dict=True, as_iterator=True) - def __get_bundle_wise_serial_nos(self) -> dict: + def _get_bundle_wise_serial_nos(self) -> dict: bundle = frappe.qb.DocType("Serial and Batch Bundle") entry = frappe.qb.DocType("Serial and Batch Entry") @@ -546,7 +930,7 @@ class FIFOSlots: query = query.where(bundle[field] == self.filters.get(field)) if self.filters.get("warehouse"): - query = self.__get_warehouse_conditions(bundle, query) + query = self._get_warehouse_conditions(bundle, query) bundle_wise_serial_nos = frappe._dict({}) for bundle_name, serial_no in query.run(): @@ -554,7 +938,52 @@ class FIFOSlots: return bundle_wise_serial_nos - def __get_item_query(self) -> str: + def _get_bundle_wise_batch_nos(self, sabb_name=None) -> dict: + bundle = frappe.qb.DocType("Serial and Batch Bundle") + entry = frappe.qb.DocType("Serial and Batch Entry") + batch = frappe.qb.DocType("Batch") + + to_date = get_datetime(self.filters.get("to_date") + " 23:59:59") + query = ( + frappe.qb.from_(bundle) + .join(entry) + .on(bundle.name == entry.parent) + .join(batch) + .on(entry.batch_no == batch.name) + .select( + bundle.name, + entry.batch_no, + batch.use_batchwise_valuation, + Abs(entry.qty).as_("qty"), + Abs(entry.stock_value_difference).as_("stock_value_difference"), + ) + .where( + (bundle.docstatus == 1) + & (entry.batch_no.isnotnull()) + & (bundle.company == self.filters.get("company")) + & (bundle.posting_datetime <= to_date) + ) + ) + + for field in ["item_code"]: + if self.filters.get(field): + query = query.where(bundle[field] == self.filters.get(field)) + + if self.filters.get("warehouse"): + query = self._get_warehouse_conditions(bundle, query) + + if sabb_name: + query = query.where(bundle.name == sabb_name) + + bundle_wise_batch_nos = frappe._dict({}) + for bundle_name, batch_no, use_batchwise_valuation, qty, stock_value_difference in query.run(): + bundle_wise_batch_nos.setdefault(bundle_name, []).append( + [batch_no.upper(), use_batchwise_valuation, qty, stock_value_difference] + ) + + return bundle_wise_batch_nos + + def _get_item_query(self) -> str: item_table = frappe.qb.DocType("Item") item = frappe.qb.from_("Item").select( @@ -565,7 +994,7 @@ class FIFOSlots: "brand", "item_group", "has_serial_no", - "valuation_method", + "has_batch_no", ) if self.filters.get("item_code"): @@ -576,7 +1005,7 @@ class FIFOSlots: return item - def __get_warehouse_conditions(self, sle, sle_query) -> str: + def _get_warehouse_conditions(self, sle, sle_query) -> str: warehouse = frappe.qb.DocType("Warehouse") lft, rgt = frappe.db.get_value("Warehouse", self.filters.get("warehouse"), ["lft", "rgt"]) diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index 4e2e5ca9d88..3fdae7ca281 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -868,6 +868,555 @@ class TestStockAgeing(ERPNextTestSuite): range_valuations = range_values[1::2] self.assertEqual(range_valuations, [15, 7.5, 20, 5]) + def test_batch_item_report_formatting_preserves_mixed_fifo_slots(self): + item_details = { + "Batch Mixed Item": { + "details": frappe._dict( + name="Batch Mixed Item", + item_name="Batch Mixed Item", + description="Batch Mixed Item", + item_group=None, + brand=None, + has_batch_no=True, + stock_uom="Nos", + ), + "fifo_queue": [ + ["SA-BATCH-MIXED-SLOT", 1, 5.0, "2021-12-01", 50.0], + [3.0, "2021-12-02", 30.0], + ], + "has_serial_no": False, + "total_qty": 8.0, + } + } + + report_data = format_report_data(self.filters, item_details, self.filters["to_date"]) + + self.assertEqual(report_data[0][7:15], [8.0, 80.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) + + def test_serial_transfer_replay_preserves_serial_slots(self): + fifo_slots = FIFOSlots(self.filters, []) + transfer_key = ("001", "Serial Item", "WH 1") + fifo_slots.transferred_item_details[transfer_key] = [[2, "2021-12-01", 20]] + + row = frappe._dict( + name="Serial Item", + actual_qty=2, + stock_value_difference=20, + posting_date="2021-12-05", + has_serial_no=True, + ) + fifo_queue = [] + + fifo_slots._compute_incoming_stock(row, fifo_queue, transfer_key, ["SN-A", "SN-B"], []) + + self.assertEqual(fifo_queue, [["SN-A", "2021-12-01", 10.0], ["SN-B", "2021-12-01", 10.0]]) + self.assertFalse(fifo_slots.transferred_item_details[transfer_key]) + + def test_batch_transfer_replay_removes_zeroed_negative_slot(self): + fifo_slots = FIFOSlots(self.filters, []) + fifo_queue = [["SA-ZERO-BATCH", 1, -4, "2021-12-01", -40]] + + fifo_slots._add_transfer_slot_to_fifo_queue(fifo_queue, ["SA-ZERO-BATCH", 1, 4, "2021-12-02", 40]) + + self.assertEqual(fifo_queue, []) + + def test_batchwise_valuation(self): + from erpnext.stock.doctype.item.test_item import make_item + + item_code = make_item( + "Test Stock Ageing Batchwise Valuation", + { + "is_stock_item": 1, + "has_batch_no": 1, + "valuation_method": "FIFO", + }, + ).name + + def make_batch(batch_id, use_batchwise_valuation): + if not frappe.db.exists("Batch", batch_id): + frappe.get_doc( + { + "doctype": "Batch", + "batch_id": batch_id, + "item": item_code, + } + ).insert(ignore_permissions=True) + + frappe.db.set_value("Batch", batch_id, "use_batchwise_valuation", use_batchwise_valuation) + + batchwise_above_90 = "SA-BATCHWISE-ABOVE-90" + non_batchwise_above_90 = "SA-NON-BATCHWISE-ABOVE-90" + batchwise_61_90 = "SA-BATCHWISE-61-90" + non_batchwise_61_90 = "SA-NON-BATCHWISE-61-90" + batchwise_31_60 = "SA-BATCHWISE-31-60" + non_batchwise_31_60 = "SA-NON-BATCHWISE-31-60" + batchwise_0_30 = "SA-BATCHWISE-0-30" + non_batchwise_0_30 = "SA-NON-BATCHWISE-0-30" + + for batch_id, use_batchwise_valuation in { + batchwise_above_90: 1, + non_batchwise_above_90: 0, + batchwise_61_90: 1, + non_batchwise_61_90: 0, + batchwise_31_60: 1, + non_batchwise_31_60: 0, + batchwise_0_30: 1, + non_batchwise_0_30: 0, + }.items(): + make_batch(batch_id, use_batchwise_valuation) + + qty_after_transaction = 0 + + def make_sle(posting_date, voucher_no, batch_no, actual_qty, stock_value_difference): + nonlocal qty_after_transaction + + qty_after_transaction += actual_qty + return frappe._dict( + name=item_code, + actual_qty=actual_qty, + qty_after_transaction=qty_after_transaction, + stock_value_difference=stock_value_difference, + warehouse="WH 1", + posting_date=posting_date, + voucher_type="Stock Entry", + voucher_no=voucher_no, + has_serial_no=False, + has_batch_no=True, + serial_no=None, + batch_no=batch_no, + valuation_rate=10, + ) + + sle = [ + make_sle("2021-08-01", "001", batchwise_above_90, 50, 500), + make_sle("2021-08-10", "002", non_batchwise_above_90, 60, 600), + make_sle("2021-08-20", "003", batchwise_above_90, -10, -100), + make_sle("2021-09-01", "004", non_batchwise_above_90, -15, -150), + make_sle("2021-09-20", "005", batchwise_61_90, 40, 400), + make_sle("2021-09-25", "006", non_batchwise_61_90, 50, 500), + make_sle("2021-09-30", "007", batchwise_61_90, -5, -50), + make_sle("2021-10-05", "008", non_batchwise_above_90, -20, -200), + make_sle("2021-10-20", "009", batchwise_31_60, 30, 300), + make_sle("2021-10-25", "010", non_batchwise_31_60, 40, 400), + make_sle("2021-10-30", "011", batchwise_31_60, -8, -80), + make_sle("2021-11-05", "012", non_batchwise_above_90, -25, -250), + make_sle("2021-11-20", "013", batchwise_0_30, 20, 200), + make_sle("2021-11-25", "014", non_batchwise_0_30, 30, 300), + make_sle("2021-11-30", "015", batchwise_0_30, -6, -60), + make_sle("2021-12-01", "016", non_batchwise_61_90, -10, -100), + ] + + slots = FIFOSlots(self.filters, sle).generate() + item_result = slots[item_code] + + self.assertEqual(item_result["qty_after_transaction"], item_result["total_qty"]) + self.assertEqual(item_result["total_qty"], 221.0) + self.assertEqual( + item_result["fifo_queue"], + [ + [batchwise_above_90, 1, 40.0, "2021-08-01", 400.0], + [batchwise_61_90, 1, 35.0, "2021-09-20", 350.0], + [non_batchwise_61_90, 0, 40.0, "2021-09-25", 400.0], + [batchwise_31_60, 1, 22.0, "2021-10-20", 220.0], + [non_batchwise_31_60, 0, 40, "2021-10-25", 400], + [batchwise_0_30, 1, 14.0, "2021-11-20", 140.0], + [non_batchwise_0_30, 0, 30, "2021-11-25", 300], + ], + ) + + report_data = format_report_data(self.filters, slots, self.filters["to_date"]) + range_values = report_data[0][7:15] + self.assertEqual(range_values, [44.0, 440.0, 62.0, 620.0, 75.0, 750.0, 40.0, 400.0]) + + def test_batchwise_valuation_same_voucher_transfer(self): + from erpnext.stock.doctype.item.test_item import make_item + + item_code = make_item( + "Test Stock Ageing Batchwise Transfer", + { + "is_stock_item": 1, + "has_batch_no": 1, + "valuation_method": "FIFO", + }, + ).name + + def make_batch(batch_id): + if not frappe.db.exists("Batch", batch_id): + frappe.get_doc( + { + "doctype": "Batch", + "batch_id": batch_id, + "item": item_code, + } + ).insert(ignore_permissions=True) + + frappe.db.set_value("Batch", batch_id, "use_batchwise_valuation", 1) + + source_batch = "SA-BATCHWISE-TRANSFER-SOURCE" + target_batch = "SA-BATCHWISE-TRANSFER-TARGET" + make_batch(source_batch) + make_batch(target_batch) + + sle = [ + frappe._dict( + name=item_code, + actual_qty=20, + qty_after_transaction=20, + stock_value_difference=200, + warehouse="WH 1", + posting_date="2021-09-01", + voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, + has_batch_no=True, + serial_no=None, + batch_no=source_batch, + valuation_rate=10, + ), + frappe._dict( + name=item_code, + actual_qty=-15, + qty_after_transaction=5, + stock_value_difference=-150, + warehouse="WH 1", + posting_date="2021-10-01", + voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, + has_batch_no=True, + serial_no=None, + batch_no=source_batch, + valuation_rate=10, + ), + frappe._dict( + name=item_code, + actual_qty=10, + qty_after_transaction=15, + stock_value_difference=100, + warehouse="WH 1", + posting_date="2021-10-01", + voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, + has_batch_no=True, + serial_no=None, + batch_no=target_batch, + valuation_rate=10, + ), + ] + + fifo_slots = FIFOSlots(self.filters, sle) + slots = fifo_slots.generate() + item_result = slots[item_code] + + self.assertEqual(item_result["total_qty"], 15.0) + self.assertEqual( + item_result["fifo_queue"], + [ + [source_batch, 1, 5.0, "2021-09-01", 50.0], + [target_batch, 1, 10.0, "2021-09-01", 100.0], + ], + ) + self.assertEqual( + fifo_slots.transferred_item_details[("002", item_code, "WH 1")], + [[5.0, "2021-09-01", 50.0]], + ) + + def test_batchwise_valuation_negative_stock_same_voucher(self): + from erpnext.stock.doctype.item.test_item import make_item + + item_code = make_item( + "Test Stock Ageing Batchwise Negative Stock", + { + "is_stock_item": 1, + "has_batch_no": 1, + "valuation_method": "FIFO", + }, + ).name + + batch_no = "SA-BATCHWISE-NEGATIVE-STOCK" + if not frappe.db.exists("Batch", batch_no): + frappe.get_doc( + { + "doctype": "Batch", + "batch_id": batch_no, + "item": item_code, + } + ).insert(ignore_permissions=True) + + frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1) + + sle = [ + frappe._dict( + name=item_code, + actual_qty=-10, + qty_after_transaction=-10, + stock_value_difference=-100, + warehouse="WH 1", + posting_date="2021-12-01", + voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, + has_batch_no=True, + serial_no=None, + batch_no=batch_no, + valuation_rate=10, + ) + ] + + fifo_slots = FIFOSlots(self.filters, sle) + slots = fifo_slots.generate() + item_result = slots[item_code] + + self.assertEqual(item_result["fifo_queue"], [[batch_no, 1, -10, "2021-12-01", -100]]) + self.assertEqual( + fifo_slots.transferred_item_details[("001", item_code, "WH 1")], [[10, "2021-12-01", 100]] + ) + + sle.append( + frappe._dict( + name=item_code, + actual_qty=6, + qty_after_transaction=-4, + stock_value_difference=60, + warehouse="WH 1", + posting_date="2021-12-01", + voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, + has_batch_no=True, + serial_no=None, + batch_no=batch_no, + valuation_rate=10, + ) + ) + + fifo_slots = FIFOSlots(self.filters, sle) + slots = fifo_slots.generate() + item_result = slots[item_code] + + self.assertEqual(item_result["fifo_queue"], [[batch_no, 1, -4.0, "2021-12-01", -40.0]]) + self.assertEqual( + fifo_slots.transferred_item_details[("001", item_code, "WH 1")], + [[4.0, "2021-12-01", 40.0]], + ) + + def test_batchwise_valuation_neutralizes_non_head_negative_batch(self): + from erpnext.stock.doctype.item.test_item import make_item + + item_code = make_item( + "Test Stock Ageing Batchwise Negative Non Head", + { + "is_stock_item": 1, + "has_batch_no": 1, + "valuation_method": "FIFO", + }, + ).name + + buffer_batch = "SA-BATCHWISE-NEGATIVE-BUFFER" + negative_batch = "SA-BATCHWISE-NEGATIVE-NON-HEAD" + for batch_no in [buffer_batch, negative_batch]: + if not frappe.db.exists("Batch", batch_no): + frappe.get_doc( + { + "doctype": "Batch", + "batch_id": batch_no, + "item": item_code, + } + ).insert(ignore_permissions=True) + + frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1) + + sle = [ + frappe._dict( + name=item_code, + actual_qty=5, + qty_after_transaction=5, + stock_value_difference=50, + warehouse="WH 1", + posting_date="2021-11-30", + voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, + has_batch_no=True, + serial_no=None, + batch_no=buffer_batch, + valuation_rate=10, + ), + frappe._dict( + name=item_code, + actual_qty=-10, + qty_after_transaction=-5, + stock_value_difference=-100, + warehouse="WH 1", + posting_date="2021-12-01", + voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, + has_batch_no=True, + serial_no=None, + batch_no=negative_batch, + valuation_rate=10, + ), + frappe._dict( + name=item_code, + actual_qty=6, + qty_after_transaction=1, + stock_value_difference=60, + warehouse="WH 1", + posting_date="2021-12-01", + voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, + has_batch_no=True, + serial_no=None, + batch_no=negative_batch, + valuation_rate=10, + ), + ] + + fifo_slots = FIFOSlots(self.filters, sle) + slots = fifo_slots.generate() + item_result = slots[item_code] + + self.assertEqual(item_result["qty_after_transaction"], item_result["total_qty"]) + self.assertEqual( + item_result["fifo_queue"], + [ + [buffer_batch, 1, 5, "2021-11-30", 50], + [negative_batch, 1, -4.0, "2021-12-01", -40.0], + ], + ) + self.assertEqual( + fifo_slots.transferred_item_details[("002", item_code, "WH 1")], + [[4.0, "2021-12-01", 40.0]], + ) + + def test_batchwise_valuation_negative_stock_later_voucher(self): + from erpnext.stock.doctype.item.test_item import make_item + + item_code = make_item( + "Test Stock Ageing Batchwise Negative Later Voucher", + { + "is_stock_item": 1, + "has_batch_no": 1, + "valuation_method": "FIFO", + }, + ).name + + batch_no = "SA-BATCHWISE-NEGATIVE-LATER-VOUCHER" + if not frappe.db.exists("Batch", batch_no): + frappe.get_doc( + { + "doctype": "Batch", + "batch_id": batch_no, + "item": item_code, + } + ).insert(ignore_permissions=True) + + frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1) + + sle = [ + frappe._dict( + name=item_code, + actual_qty=-10, + qty_after_transaction=-10, + stock_value_difference=-100, + warehouse="WH 1", + posting_date="2021-11-01", + voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, + has_batch_no=True, + serial_no=None, + batch_no=batch_no, + valuation_rate=10, + ), + frappe._dict( + name=item_code, + actual_qty=6, + qty_after_transaction=-4, + stock_value_difference=60, + warehouse="WH 1", + posting_date="2021-11-10", + voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, + has_batch_no=True, + serial_no=None, + batch_no=batch_no, + valuation_rate=10, + ), + ] + + slots = FIFOSlots(self.filters, sle).generate() + item_result = slots[item_code] + + self.assertEqual(item_result["qty_after_transaction"], item_result["total_qty"]) + self.assertEqual(item_result["total_qty"], -4.0) + self.assertEqual(item_result["fifo_queue"], [[batch_no, 1, -4.0, "2021-11-10", -40.0]]) + + def test_batchwise_valuation_stock_reconciliation_with_bundle(self): + from frappe.utils import add_days, getdate, nowdate + + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import ( + get_batch_from_bundle, + ) + from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( + create_stock_reconciliation, + ) + + suffix = frappe.generate_hash(length=8).upper() + item_code = make_item( + f"Test Stock Ageing Batch Reco {suffix}", + { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": f"SA-RECO-{suffix}-.###", + "valuation_method": "FIFO", + }, + ).name + warehouse = "_Test Warehouse - _TC" + base_date = nowdate() + + opening_reco = create_stock_reconciliation( + item_code=item_code, + warehouse=warehouse, + qty=12, + rate=10, + posting_date=add_days(base_date, -2), + posting_time="10:00:00", + ) + batch_no = get_batch_from_bundle(opening_reco.items[0].serial_and_batch_bundle) + frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1) + + create_stock_reconciliation( + item_code=item_code, + warehouse=warehouse, + qty=5, + rate=10, + batch_no=batch_no, + posting_date=add_days(base_date, -1), + posting_time="10:00:00", + ) + + filters = frappe._dict( + company="_Test Company", + to_date=base_date, + ranges=["30", "60", "90"], + item_code=item_code, + ) + slots = FIFOSlots(filters).generate() + item_result = slots[item_code] + + self.assertEqual(item_result["qty_after_transaction"], item_result["total_qty"]) + self.assertEqual(item_result["total_qty"], 5.0) + self.assertEqual( + item_result["fifo_queue"], [[batch_no.upper(), 1, 5.0, getdate(add_days(base_date, -2)), 50.0]] + ) + def generate_item_and_item_wh_wise_slots(filters, sle): "Return results with and without 'show_warehouse_wise_stock'" diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 2fe966d1202..4f7b5fbcf9d 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -874,15 +874,6 @@ class update_entries_after: if not self.args.get("sle_id"): self.get_dynamic_incoming_outgoing_rate(sle) - if ( - sle.voucher_type == "Stock Reconciliation" - and (sle.serial_and_batch_bundle) - and sle.voucher_detail_no - and not self.args.get("sle_id") - and sle.is_cancelled == 0 - ): - self.reset_actual_qty_for_stock_reco(sle) - if ( sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and sle.voucher_detail_no @@ -1059,31 +1050,6 @@ class update_entries_after: if not allow_zero_rate: self.wh_data.valuation_rate = self.get_fallback_rate(sle) - def reset_actual_qty_for_stock_reco(self, sle): - doc = frappe.get_doc("Stock Reconciliation", sle.voucher_no) - doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0) - - if sle.actual_qty < 0: - doc.reload() - - sle.actual_qty = ( - flt(frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "current_qty")) - * -1 - ) - - if abs(sle.actual_qty) == 0.0: - sle.is_cancelled = 1 - - if sle.serial_and_batch_bundle: - for row in doc.items: - if row.name == sle.voucher_detail_no: - row.db_set("current_serial_and_batch_bundle", "") - - sabb_doc = frappe.get_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle) - sabb_doc.voucher_detail_no = None - sabb_doc.voucher_no = None - sabb_doc.cancel() - def calculate_valuation_for_serial_batch_bundle(self, sle): if not frappe.db.exists("Serial and Batch Bundle", sle.serial_and_batch_bundle): return diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py index 70f12142fac..99c55b6d7ba 100644 --- a/erpnext/tests/utils.py +++ b/erpnext/tests/utils.py @@ -3003,6 +3003,9 @@ class ERPNextTestSuite(unittest.TestCase): def tearDown(self): frappe.db.rollback() + frappe.local.request_cache.clear() + if hasattr(frappe.local, "future_sle"): + frappe.local.future_sle.clear() def load_test_records(self, doctype): if doctype not in self.globalTestRecords: diff --git a/frappe-semgrep-rules b/frappe-semgrep-rules new file mode 160000 index 00000000000..a05bce32ad3 --- /dev/null +++ b/frappe-semgrep-rules @@ -0,0 +1 @@ +Subproject commit a05bce32ad3e37cf9a87a6913e9b08e45c8ba8cf