From adfa6768c95c25bb7964453424309ce9efc9d5f9 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:06:14 +0530 Subject: [PATCH 001/134] fix: incorrect batch-wise valuation rate for entries with same posting datetime (backport #57794) (#57797) fix: incorrect batch-wise valuation rate for entries with same posting datetime (#57794) * fix: incorrect batch-wise valuation rate for entries with same posting datetime The tie-breaker in get_batch_no_ledgers compared the bundle's creation against the SLE's creation. These are different timelines - a bundle can be created (drafted) much before its SLE (created at submission). For entries sharing a posting datetime (backdated / amended vouchers), this mis-ordered the entries against the ledger's replay order (SLE creation), causing double counting or omission of batch qty / value and runaway outgoing rates that no repost could heal. Now the tie is broken using the creation of the bundle's own SLE (same timeline on both sides). When the valuation runs through the bundle before its SLE exists, the entry is by definition last in its timestamp group, so all same-timestamp entries already in the ledger precede it. * test: batch-wise valuation ordering for same posting datetime entries Covers both tie-breaking branches of get_batch_no_ledgers: - submission (pre-insertion) branch: same-timestamp inward at a different rate plus a multi-row outward voucher (same item and warehouse), at submission and after a backdated repost - existing-SLE branch: a bundle created after its sibling's SLE, the ordering must follow the SLE creation and not the bundle creation Both tests fail with the previous parent.creation < sle.creation tie-breaker and pass with the fix. --------- Co-authored-by: rohitwaghchaure Co-authored-by: Claude Fable 5 --- .../test_serial_and_batch_bundle.py | 189 +++++++++++++++++- erpnext/stock/serial_batch_bundle.py | 37 +++- 2 files changed, 222 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index a566ab8216e..2202987c83e 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -4,7 +4,7 @@ import json import frappe -from frappe.utils import flt, nowtime, today +from frappe.utils import add_days, add_to_date, flt, nowtime, today from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( @@ -1601,3 +1601,190 @@ class TestSerialandBatchBundleLogic(ERPNextTestSuite): self.assertNotIn(bundles[1], bundle_wise_serial_nos) self.assertEqual(bundle_wise_serial_nos[bundles[0]], [serial_no]) + + @ERPNextTestSuite.change_settings( + "Stock Settings", {"auto_create_serial_and_batch_bundle_for_outward": 1} + ) + def test_batchwise_valuation_for_same_posting_datetime_entries(self): + # an inward at a different rate and multiple outward rows with the same + # item and warehouse share the same posting datetime, the tie-breaking + # must include the same-timestamp entries which are already part of the + # ledger and must not let the outward rows count each other + item_code = make_item( + "Test Batchwise Same Posting Datetime Item 1", + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TBSPD-ITEM1-.#####", + "valuation_method": "FIFO", + }, + ).name + + warehouse = "_Test Warehouse - _TC" + + receipt = make_stock_entry( + item_code=item_code, + qty=10, + rate=100, + target=warehouse, + posting_date=add_days(today(), -5), + posting_time="12:00:00", + ) + + batch_no = get_batch_from_bundle(receipt.items[0].serial_and_batch_bundle) + self.assertTrue(frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation")) + + # same posting datetime as the outward rows below, at a different rate + make_stock_entry( + item_code=item_code, + qty=20, + rate=250, + target=warehouse, + batch_no=batch_no, + use_serial_batch_fields=1, + posting_date=add_days(today(), -3), + posting_time="12:00:00", + ) + + issue = make_stock_entry( + item_code=item_code, + qty=2, + source=warehouse, + posting_date=add_days(today(), -3), + posting_time="12:00:00", + do_not_save=True, + ) + + for qty in [3, 4]: + issue.append( + "items", + { + "item_code": item_code, + "s_warehouse": warehouse, + "qty": qty, + "conversion_factor": 1, + }, + ) + + issue.save() + issue.submit() + + # (10 * 100 + 20 * 250) / 30 = 200 + self.assert_batchwise_outgoing_rate(item_code, outgoing_rate=200.0, balance_value=4200.0) + + # backdated receipt reposts the same posting datetime cluster + make_stock_entry( + item_code=item_code, + qty=10, + rate=100, + target=warehouse, + batch_no=batch_no, + use_serial_batch_fields=1, + posting_date=add_days(today(), -4), + posting_time="12:00:00", + ) + + # (20 * 100 + 20 * 250) / 40 = 175 + self.assert_batchwise_outgoing_rate(item_code, outgoing_rate=175.0, balance_value=5425.0) + + @ERPNextTestSuite.change_settings( + "Stock Settings", {"auto_create_serial_and_batch_bundle_for_outward": 1} + ) + def test_batchwise_valuation_when_bundle_created_before_the_sle(self): + # a bundle can be created (drafted) much before / after its SLE, the + # tie-breaking for the same posting datetime entries must follow the + # SLE creation and not the bundle creation + item_code = make_item( + "Test Batchwise Same Posting Datetime Item 2", + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TBSPD-ITEM2-.#####", + "valuation_method": "FIFO", + }, + ).name + + warehouse = "_Test Warehouse - _TC" + + receipt = make_stock_entry( + item_code=item_code, + qty=10, + rate=100, + target=warehouse, + posting_date=add_days(today(), -5), + posting_time="12:00:00", + ) + + batch_no = get_batch_from_bundle(receipt.items[0].serial_and_batch_bundle) + + # inward at a different rate, same posting datetime as the outward below + inward = make_stock_entry( + item_code=item_code, + qty=10, + rate=200, + target=warehouse, + batch_no=batch_no, + use_serial_batch_fields=1, + posting_date=add_days(today(), -3), + posting_time="12:00:00", + ) + + outward = make_stock_entry( + item_code=item_code, + qty=10, + source=warehouse, + posting_date=add_days(today(), -3), + posting_time="12:00:00", + ) + + # simulate the inward's bundle drafted after the outward's SLE, the + # bundle creation timeline no longer matches the SLE creation timeline + outward_sle_creation = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": outward.name, "is_cancelled": 0}, + "creation", + ) + + frappe.db.set_value( + "Serial and Batch Bundle", + inward.items[0].serial_and_batch_bundle, + "creation", + add_to_date(outward_sle_creation, minutes=30), + update_modified=False, + ) + + repost = frappe.get_doc( + { + "doctype": "Repost Item Valuation", + "based_on": "Item and Warehouse", + "item_code": item_code, + "warehouse": warehouse, + "posting_date": add_days(today(), -6), + "posting_time": "00:00:00", + "allow_negative_stock": 1, + } + ) + + repost.submit() + + # (10 * 100 + 10 * 200) / 20 = 150, the inward precedes the outward as + # per the SLE creation even though its bundle was created afterwards + self.assert_batchwise_outgoing_rate(item_code, outgoing_rate=150.0, balance_value=1500.0) + + def assert_batchwise_outgoing_rate(self, item_code, outgoing_rate, balance_value): + sl_entries = frappe.get_all( + "Stock Ledger Entry", + filters={"item_code": item_code, "is_cancelled": 0}, + fields=["actual_qty", "stock_value_difference", "stock_value"], + order_by="posting_datetime, creation", + ) + + for sle in sl_entries: + if sle.actual_qty > 0: + continue + + self.assertEqual(flt(sle.stock_value_difference, 2), flt(sle.actual_qty * outgoing_rate, 2)) + + self.assertEqual(flt(sl_entries[-1].stock_value, 2), flt(balance_value, 2)) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 633867dcc79..e7ccac40115 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -841,14 +841,45 @@ class BatchNoValuation(DeprecatedBatchNoValuation): child = frappe.qb.DocType("Serial and Batch Entry") + sle_creation = self.sle.creation if self.sle.get("name") else None + if not self.sle.get("name") and self.sle.get("serial_and_batch_bundle"): + sle_creation = frappe.db.get_value( + "Stock Ledger Entry", + {"serial_and_batch_bundle": self.sle.serial_and_batch_bundle, "is_cancelled": 0}, + "creation", + ) + timestamp_condition = "" if self.sle.posting_datetime: timestamp_condition = child.posting_datetime < self.sle.posting_datetime - if self.sle.creation: - timestamp_condition |= (child.posting_datetime == self.sle.posting_datetime) & ( - child.creation < self.sle.creation + sle_table = frappe.qb.DocType("Stock Ledger Entry") + if sle_creation: + # bundle creation and SLE creation are different timelines (a + # bundle can be created much before its SLE), so break the tie + # using the creation of the bundle's own SLE + tie_condition = ExistsCriterion( + frappe.qb.from_(sle_table) + .select(sle_table.name) + .where( + (sle_table.serial_and_batch_bundle == child.parent) + & (sle_table.is_cancelled == 0) + & (sle_table.creation < sle_creation) + ) ) + else: + # the current entry is not yet in the ledger and will get the + # latest creation, so the same-timestamp entries which are + # already in the ledger precede it + tie_condition = ExistsCriterion( + frappe.qb.from_(sle_table) + .select(sle_table.name) + .where( + (sle_table.serial_and_batch_bundle == child.parent) & (sle_table.is_cancelled == 0) + ) + ) + + timestamp_condition |= (child.posting_datetime == self.sle.posting_datetime) & tie_condition query = ( frappe.qb.from_(child) From eeb3cd238e4347ae6ec6d41ee3270f0a15029560 Mon Sep 17 00:00:00 2001 From: Jatin3128 <140256508+Jatin3128@users.noreply.github.com> Date: Wed, 5 Aug 2026 12:23:31 +0530 Subject: [PATCH 002/134] fix(subscription): don't reactivate a cancelled subscription (backport #57774) * fix(subscription): don't reactivate a cancelled subscription Backport of #57774 to version-16-hotfix. set_subscription_status() unconditionally set status to Active once there was no outstanding invoice, even if the subscription had been intentionally cancelled. Paying off an invoice issued before cancellation (directly, or via the Payment Entry -> refresh hook) flipped a Cancelled subscription back to Active while cancelation_date stayed set. process()'s cancel_at_period_end check compared posting_date against getdate(self.end_date), and getdate(None) returns today, so an empty end_date was silently treated as "cancel now" on every scheduler run. Combined with the reactivation bug, this let a cancelled subscription toggle Cancelled -> Active on each run and generate another invoice at the next period boundary. Fixes #57761 * test: fix flaky test_update_bom_cost_in_all_boms via valuation reset Backport of #56796 to version-16-hotfix. reset_item_valuation_rate() only reconciled warehouses where the item currently has positive stock (actual_qty > 0). get_valuation_rate() averages Sum(stock_value)/Sum(actual_qty) across all of an item's bins, so a negative balance left over in another warehouse by a prior test can cancel out the reset qty and collapse the average to 0, failing the assertion with 0.0 != 10.0. This branch never got #56796 (it predates the frappe.get_all refactor of this helper and still uses raw SQL), so applying the same fix here: reconcile every warehouse with a non-zero balance, not just positive ones. * fix(subscription): don't let period rollover defeat cancel_at_period_end process() can advance current_invoice_end to the next period (via update_subscription_period(), when generating the current period's invoice) before the cancel_at_period_end check further down runs. For a subscription with no end_date, that check now compared posting_date against the already-rolled-forward current_invoice_end, which is always in the future, so cancel_at_period_end was silently never honored. Snapshot current_invoice_end before any rollover and use that in the check instead, so it still targets the period that just ended. Found via review on the version-15-hotfix backport (#57780). --------- Co-authored-by: test --- .../doctype/subscription/subscription.py | 12 +++- .../doctype/subscription/test_subscription.py | 58 +++++++++++++++++++ erpnext/manufacturing/doctype/bom/test_bom.py | 7 ++- 3 files changed, 74 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 8cf237ba475..11dfac33d72 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -254,6 +254,9 @@ class Subscription(Document): """ Sets the status of the `Subscription` """ + if self.status == "Cancelled": + return + if self.is_trialling(): self.status = "Trialing" elif ( @@ -605,6 +608,11 @@ class Subscription(Document): 1. `process_for_active` 2. `process_for_past_due` """ + # Snapshot before update_subscription_period() below can roll this forward, + # so the cancel_at_period_end check further down still targets the period + # that just ended, not the next one. + current_period_end = self.current_invoice_end + if not self.is_current_invoice_generated( self.current_invoice_start, self.current_invoice_end ) and self.can_generate_new_invoice(posting_date): @@ -625,8 +633,8 @@ class Subscription(Document): self.update_subscription_period() if self.cancel_at_period_end and ( - getdate(posting_date) >= getdate(self.current_invoice_end) - or getdate(posting_date) >= getdate(self.end_date) + getdate(posting_date) >= getdate(current_period_end) + or (self.end_date and getdate(posting_date) >= getdate(self.end_date)) ): self.cancel_subscription() diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index 7df77a839be..87cc0a31c39 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -614,6 +614,32 @@ class TestSubscription(ERPNextTestSuite): self.assertRaises(frappe.ValidationError, subscription.process, posting_date=add_days(start_date, 7)) + def test_subscription_cancels_at_period_end_without_end_date(self): + # https://github.com/frappe/erpnext/issues/57761 -- generate_invoice() rolls + # current_invoice_end forward to the next period before this check runs, so + # with no end_date to fall back on, cancel_at_period_end must compare + # against the period that just ended, not the (already advanced) next one. + create_plan( + plan_name="_Test plan name 11", + cost=80, + currency="INR", + billing_interval="Day", + billing_interval_count=3, + ) + subscription = create_subscription( + start_date=nowdate(), + cancel_at_period_end=1, + generate_invoice_at="End of the current subscription period", + plans=[{"plan": "_Test plan name 11", "qty": 1}], + ) + self.assertEqual(len(subscription.invoices), 0) + period_end = subscription.current_invoice_end + + subscription.process(posting_date=period_end) + + self.assertEqual(subscription.status, "Cancelled") + self.assertEqual(len(subscription.invoices), 1) + def test_invoice_generated_when_scheduler_runs_one_day_late(self): # The trigger date (period end) is long past, yet catch-up still bills the period # on creation (Bug 1: the check is `>= trigger`, not `== trigger`). @@ -774,6 +800,38 @@ class TestSubscription(ERPNextTestSuite): subscription.reload() self.assertEqual(subscription.status, "Active") + def test_cancelled_subscription_stays_cancelled_after_payment_and_reprocess(self): + # https://github.com/frappe/erpnext/issues/57761 + subscription = create_subscription( + start_date=nowdate(), + generate_invoice_at="Beginning of the current subscription period", + submit_invoice=1, + cancel_at_period_end=1, + ) + subscription.process(posting_date=nowdate()) + invoice = subscription.get_current_invoice() + self.assertGreater(invoice.outstanding_amount, 0) + + subscription.cancel_subscription() + self.assertEqual(subscription.status, "Cancelled") + cancelation_date = getdate(subscription.cancelation_date) + self.assertIsNotNone(cancelation_date) + + payment_entry = get_payment_entry(invoice.doctype, invoice.name, bank_account="_Test Bank - _TC") + payment_entry.reference_no = "12345" + payment_entry.reference_date = nowdate() + payment_entry.submit() + + subscription.reload() + self.assertEqual(subscription.status, "Cancelled") + self.assertEqual(getdate(subscription.cancelation_date), cancelation_date) + + invoice_count = len(subscription.invoices) + subscription.process() + subscription.reload() + self.assertEqual(subscription.status, "Cancelled") + self.assertEqual(len(subscription.invoices), invoice_count) + def test_first_invoice_generated_on_create_for_prepaid(self): subscription = create_subscription( start_date=nowdate(), diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 77e0ecddb1d..f40f6bc499e 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -881,10 +881,15 @@ def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=Non warehouse_list = [warehouse_list] if not warehouse_list: + # Reconcile every warehouse the item has a non-zero balance in -- including + # negative balances left by other tests. get_valuation_rate averages + # Sum(stock_value)/Sum(actual_qty) across all bins, so a leftover negative + # balance in one warehouse can cancel the reset qty elsewhere and make the + # average collapse to 0, which is a source of flaky BOM-cost failures. warehouse_list = frappe.db.sql_list( """ select warehouse from `tabBin` - where item_code=%s and actual_qty > 0 + where item_code=%s and actual_qty != 0 """, item_code, ) From af3184c8b41d935184931b5ac01b1a5efc4edf82 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:15:40 +0530 Subject: [PATCH 003/134] fix(stock): handle multi-item opening balance in Stock Ledger report (backport #57591) (#57796) * fix(stock): handle multi-item opening balance in Stock Ledger report (#57591) * fix(stock): handle multi-item opening balance in Stock * test(stock): add unit test for multi-item Stock Ledger report --------- Co-authored-by: Afsal Syed (cherry picked from commit 0dbe410414b94649dcb2c47e419f507da13490df) # Conflicts: # erpnext/stock/report/stock_ledger/test_stock_ledger_report.py * fix(stock): resolve stock ledger backport conflicts --------- Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com> Co-authored-by: Sudharsanan11 --- .../stock/report/stock_ledger/stock_ledger.py | 174 ++++++--- .../stock_ledger/test_stock_ledger_report.py | 335 +++++++++++++++++- 2 files changed, 453 insertions(+), 56 deletions(-) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index e49279689f2..51460972992 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -7,8 +7,10 @@ from collections import defaultdict import frappe from frappe import _ -from frappe.query_builder.functions import Sum +from frappe.query_builder.functions import IfNull, Sum from frappe.utils import cint, flt, get_datetime +from pypika import Order +from pypika.analytics import RowNumber from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -53,14 +55,15 @@ def execute(filters=None): data = [] conversion_factors = [] - if opening_row: - data.append(opening_row) + opening_rows = opening_row if isinstance(opening_row, list) else ([opening_row] if opening_row else []) + for row in opening_rows: + data.append(row) conversion_factors.append(0) actual_qty = stock_value = 0 - if opening_row: - actual_qty = opening_row.get("qty_after_transaction") - stock_value = opening_row.get("stock_value") + if opening_rows: + actual_qty = opening_rows[0].get("qty_after_transaction", 0) + stock_value = opening_rows[0].get("stock_value", 0) available_serial_nos = {} @@ -693,43 +696,120 @@ def get_opening_balance(filters, columns, sl_entries, inv_dimension_wise_value=N if not (filters.item_code and filters.warehouse and filters.from_date): return - from erpnext.stock.stock_ledger import get_previous_sle + item_codes = filters.item_code + if isinstance(item_codes, str): + item_codes = [item_codes] - project = None - if filters.get("project") and not frappe.get_all( - "Inventory Dimension", filters={"reference_document": "Project"} - ): - project = filters.get("project") + warehouses = get_matching_warehouses(filters.warehouse) + if not warehouses: + return - last_entry = get_previous_sle( - { - "item_code": filters.item_code, - "warehouse_condition": get_warehouse_condition(filters.warehouse), - "posting_date": filters.from_date, - "posting_time": "00:00:00", - "project": project, - }, - for_report=True, + sle_doctype = frappe.qb.DocType("Stock Ledger Entry") + sr_doctype = frappe.qb.DocType("Stock Reconciliation") + + opening_reco_query = ( + frappe.qb.from_(sle_doctype) + .inner_join(sr_doctype) + .on(sle_doctype.voucher_no == sr_doctype.name) + .select(sle_doctype.voucher_no) + .where(sle_doctype.docstatus < 2) + .where(sle_doctype.is_cancelled == 0) + .where(sle_doctype.item_code.isin(item_codes)) + .where(sle_doctype.warehouse.isin(warehouses)) + .where(sle_doctype.voucher_type == "Stock Reconciliation") + .where(sle_doctype.posting_date == filters.from_date) + .where(sr_doctype.purpose == "Opening Stock") ) - # check if any SLEs are actually Opening Stock Reconciliation - for sle in list(sl_entries): - if ( - sle.get("voucher_type") == "Stock Reconciliation" - and sle.posting_date == filters.from_date - and frappe.db.get_value("Stock Reconciliation", sle.voucher_no, "purpose") == "Opening Stock" - ): - last_entry = sle - sl_entries.remove(sle) + opening_reco_vouchers = set(opening_reco_query.run(pluck=True)) - row = { + if opening_reco_vouchers: + sl_entries[:] = [sle for sle in sl_entries if sle.get("voucher_no") not in opening_reco_vouchers] + + sle_cond = (sle_doctype.posting_date < filters.from_date) | ( + (sle_doctype.posting_date == filters.from_date) & (sle_doctype.posting_time == "00:00:00") + ) + if opening_reco_vouchers: + sle_cond = sle_cond | ( + (sle_doctype.posting_date == filters.from_date) + & (sle_doctype.voucher_no.isin(list(opening_reco_vouchers))) + ) + + subq = ( + frappe.qb.from_(sle_doctype) + .select( + sle_doctype.qty_after_transaction, + sle_doctype.stock_value, + RowNumber() + .over(sle_doctype.item_code, sle_doctype.warehouse) + .orderby(sle_doctype.posting_datetime, sle_doctype.creation, sle_doctype.name, order=Order.desc) + .as_("rn"), + ) + .where(sle_doctype.docstatus < 2) + .where(sle_doctype.is_cancelled == 0) + .where(sle_doctype.item_code.isin(item_codes)) + .where(sle_doctype.warehouse.isin(warehouses)) + .where(sle_cond) + ) + + for field in ["voucher_no", "project", "company"]: + if filters.get(field): + subq = subq.where(sle_doctype[field] == filters.get(field)) + + inventory_dimension_fields = get_inventory_dimension_fields() + if inventory_dimension_fields: + for fieldname in inventory_dimension_fields: + if filters.get(fieldname): + subq = subq.where(sle_doctype[fieldname].isin(filters.get(fieldname))) + + query = ( + frappe.qb.from_(subq) + .select( + IfNull(Sum(subq.qty_after_transaction), 0.0).as_("total_qty"), + IfNull(Sum(subq.stock_value), 0.0).as_("total_stock_value"), + ) + .where(subq.rn == 1) + ) + + res = query.run(as_dict=True) + + total_qty = flt(res[0].total_qty) if res else 0.0 + total_stock_value = flt(res[0].total_stock_value) if res else 0.0 + valuation_rate = flt(total_stock_value / total_qty) if total_qty else 0.0 + + return { "item_code": _("'Opening'"), - "qty_after_transaction": last_entry.get("qty_after_transaction", 0), - "valuation_rate": last_entry.get("valuation_rate", 0), - "stock_value": last_entry.get("stock_value", 0), + "qty_after_transaction": total_qty, + "valuation_rate": valuation_rate, + "stock_value": total_stock_value, } - return row + +def get_matching_warehouses(warehouses): + if not warehouses: + return [] + + if isinstance(warehouses, str): + warehouses = [warehouses] + + warehouse_details = frappe.get_all( + "Warehouse", + filters={"name": ("in", warehouses)}, + fields=["lft", "rgt"], + ) + + if not warehouse_details: + return warehouses + + wh = frappe.qb.DocType("Warehouse") + cond = None + for d in warehouse_details: + c = (wh.lft >= d.lft) & (wh.rgt <= d.rgt) + cond = c if cond is None else (cond | c) + + matching = (frappe.qb.from_(wh).select(wh.name).where(cond)).run(pluck=True) + + return matching if matching else warehouses def get_warehouse_condition(warehouses): @@ -785,7 +865,15 @@ def get_opening_balance_for_inv_dimension(filters, inv_dimension_wise_value): if not filters.item_code or not filters.warehouse or not filters.from_date: return - if len(filters.get("item_code")) > 1 or len(filters.get("warehouse")) > 1: + item_codes = filters.get("item_code") + if isinstance(item_codes, str): + item_codes = [item_codes] + + warehouses = filters.get("warehouse") + if isinstance(warehouses, str): + warehouses = [warehouses] + + if len(item_codes) > 1 or len(warehouses) > 1: return sl_doctype = frappe.qb.DocType("Stock Ledger Entry") @@ -805,17 +893,11 @@ def get_opening_balance_for_inv_dimension(filters, inv_dimension_wise_value): ) ) - if filters.get("item_code"): - if isinstance(filters.item_code, list | tuple): - query = query.where(sl_doctype.item_code.isin(filters.item_code)) - else: - query = query.where(sl_doctype.item_code == filters.item_code) + if item_codes: + query = query.where(sl_doctype.item_code.isin(item_codes)) - if filters.get("warehouse"): - if isinstance(filters.warehouse, list | tuple): - query = query.where(sl_doctype.warehouse.isin(filters.warehouse)) - else: - query = query.where(sl_doctype.warehouse == filters.warehouse) + if warehouses: + query = query.where(sl_doctype.warehouse.isin(warehouses)) for key, value in inv_dimension_wise_value.items(): if isinstance(value, list | tuple): diff --git a/erpnext/stock/report/stock_ledger/test_stock_ledger_report.py b/erpnext/stock/report/stock_ledger/test_stock_ledger_report.py index 8a97a64d1b3..3ab290033f9 100644 --- a/erpnext/stock/report/stock_ledger/test_stock_ledger_report.py +++ b/erpnext/stock/report/stock_ledger/test_stock_ledger_report.py @@ -4,18 +4,333 @@ import frappe from frappe.utils import add_days, today -from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import ( - make_serial_item_with_serial, -) +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.stock_ledger.stock_ledger import execute from erpnext.tests.utils import ERPNextTestSuite +WAREHOUSE = "Stores - _TC" -class TestStockLedgerReeport(ERPNextTestSuite): - def setUp(self) -> None: - make_serial_item_with_serial(self, "_Test Stock Report Serial Item") - self.filters = frappe._dict( + +class TestStockLedgerReport(ERPNextTestSuite): + """Correctness tests for the Stock Ledger report. + + A shared `make_movements`/`run` pair keeps each test small without persisting + any data: movements are created per test and rolled back, while the report runs + read-only. Tests reuse bootstrap items and transact in `Stores - _TC`, which + starts clean (zero balance) for these items. + """ + + def make_movements(self, item_code, movements): + for movement in movements: + make_stock_entry(item_code=item_code, **movement) + + def run_report(self, item_code, from_date=None, to_date=None): + filters = frappe._dict( company="_Test Company", - from_date=today(), - to_date=add_days(today(), 30), - item_code=["_Test Stock Report Serial Item"], + from_date=from_date or add_days(today(), -1), + to_date=to_date or today(), + item_code=[item_code], + warehouse=WAREHOUSE, + ) + return list(execute(filters)[1]) + + def test_in_out_quantities_and_running_balance(self): + item = "_Test Item" + self.make_movements( + item, + [ + {"qty": 10, "to_warehouse": WAREHOUSE, "basic_rate": 100}, + {"qty": 4, "from_warehouse": WAREHOUSE}, + ], + ) + + rows = self.run_report(item) + receipt = next(row for row in rows if row.get("in_qty")) + issue = next(row for row in rows if row.get("out_qty")) + + self.assertEqual(receipt["in_qty"], 10) + self.assertEqual(receipt["qty_after_transaction"], 10) + self.assertEqual(issue["out_qty"], -4) + self.assertEqual(issue["qty_after_transaction"], 6) + + def test_opening_balance_reflects_movements_before_from_date(self): + item = "_Test Item" + self.make_movements( + item, + [ + { + "qty": 10, + "to_warehouse": WAREHOUSE, + "basic_rate": 100, + "posting_date": add_days(today(), -10), + }, + {"qty": 4, "from_warehouse": WAREHOUSE, "posting_date": today()}, + ], + ) + + rows = self.run_report(item, from_date=add_days(today(), -5), to_date=today()) + + # the receipt predates the range, so it surfaces as the opening balance + self.assertEqual(rows[0]["item_code"], "'Opening'") + self.assertEqual(rows[0]["qty_after_transaction"], 10) + + # the in-range issue draws down from the opening balance + issue = next(row for row in rows if row.get("out_qty")) + self.assertEqual(issue["qty_after_transaction"], 6) + + def test_filters_to_requested_item_only(self): + item_a = "_Test Item" + item_b = "_Test Item 2" + self.make_movements(item_a, [{"qty": 5, "to_warehouse": WAREHOUSE, "basic_rate": 100}]) + self.make_movements(item_b, [{"qty": 7, "to_warehouse": WAREHOUSE, "basic_rate": 100}]) + + rows = self.run_report(item_a) + item_codes = {row["item_code"] for row in rows if row.get("voucher_no")} + self.assertEqual(item_codes, {item_a}) + + def test_multi_item_opening_balance_with_and_without_transactions(self): + item_a = "_Test Item" + item_b = "_Test Item 2" + self.make_movements( + item_a, + [ + { + "qty": 10, + "to_warehouse": WAREHOUSE, + "basic_rate": 100, + "posting_date": add_days(today(), -10), + } + ], + ) + self.make_movements( + item_b, + [{"qty": 5, "to_warehouse": WAREHOUSE, "basic_rate": 50, "posting_date": add_days(today(), -10)}], + ) + self.make_movements( + item_a, + [{"qty": 2, "from_warehouse": WAREHOUSE, "posting_date": today()}], + ) + + filters = frappe._dict( + company="_Test Company", + from_date=add_days(today(), -5), + to_date=today(), + item_code=[item_a, item_b], + warehouse=WAREHOUSE, + ) + columns, rows = execute(filters) + + opening_rows = [row for row in rows if row.get("item_code") == "'Opening'"] + self.assertEqual(len(opening_rows), 1) + self.assertEqual(opening_rows[0]["qty_after_transaction"], 15) + + def test_multi_warehouse_opening_balance_aggregation(self): + item = "_Test Item" + warehouse_1 = "Stores - _TC" + warehouse_2 = "Finished Goods - _TC" + + self.make_movements( + item, + [ + { + "qty": 10, + "to_warehouse": warehouse_1, + "basic_rate": 100, + "posting_date": add_days(today(), -10), + }, + { + "qty": 20, + "to_warehouse": warehouse_2, + "basic_rate": 100, + "posting_date": add_days(today(), -10), + }, + ], + ) + + filters = frappe._dict( + company="_Test Company", + from_date=add_days(today(), -5), + to_date=today(), + item_code=[item], + warehouse=[warehouse_1, warehouse_2], + ) + columns, rows = execute(filters) + + opening_rows = [row for row in rows if row.get("item_code") == "'Opening'"] + self.assertEqual(len(opening_rows), 1) + self.assertEqual(opening_rows[0]["qty_after_transaction"], 30) + + def test_opening_stock_reconciliation_on_from_date_non_midnight_time(self): + from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( + create_stock_reconciliation, + ) + + item = "_Test Item" + from_date = today() + + sr = create_stock_reconciliation( + item_code=item, + warehouse=WAREHOUSE, + qty=25, + rate=100, + posting_date=from_date, + posting_time="10:30:00", + purpose="Opening Stock", + do_not_submit=False, + ) + + filters = frappe._dict( + company="_Test Company", + from_date=from_date, + to_date=from_date, + item_code=[item], + warehouse=WAREHOUSE, + ) + columns, rows = execute(filters) + + opening_rows = [row for row in rows if row.get("item_code") == "'Opening'"] + self.assertEqual(len(opening_rows), 1) + self.assertEqual(opening_rows[0]["qty_after_transaction"], 25) + + # Ensure the Opening Stock Reconciliation is not duplicated in detail transaction rows + reco_rows = [row for row in rows if row.get("voucher_no") == sr.name] + self.assertEqual(len(reco_rows), 0) + + def test_backdated_sle_independent_maxima_handling(self): + item = "_Test Item" + # Entry 1: Later posting date (2026-07-20), created first + self.make_movements( + item, + [ + { + "qty": 10, + "to_warehouse": WAREHOUSE, + "basic_rate": 100, + "posting_date": add_days(today(), -10), + } + ], + ) + # Entry 2: Backdated posting date (2026-07-15), created LATER + self.make_movements( + item, + [ + { + "qty": 5, + "to_warehouse": WAREHOUSE, + "basic_rate": 100, + "posting_date": add_days(today(), -15), + } + ], + ) + + filters = frappe._dict( + company="_Test Company", + from_date=add_days(today(), -5), + to_date=today(), + item_code=[item], + warehouse=WAREHOUSE, + ) + columns, rows = execute(filters) + + opening_rows = [row for row in rows if row.get("item_code") == "'Opening'"] + self.assertEqual(len(opening_rows), 1) + # Should correctly pick the latest posting date entry (15 Qty) despite backdated creation order + self.assertEqual(opening_rows[0]["qty_after_transaction"], 15) + + def test_filtered_opening_balance_does_not_pick_excluded_creation_entry(self): + item = "_Test Item" + posting_date = add_days(today(), -10) + posting_time = "09:00:00" + + included_entry = make_stock_entry( + item_code=item, + qty=10, + to_warehouse=WAREHOUSE, + basic_rate=100, + posting_date=posting_date, + posting_time=posting_time, + ) + make_stock_entry( + item_code=item, + qty=50, + to_warehouse=WAREHOUSE, + basic_rate=100, + posting_date=posting_date, + posting_time=posting_time, + ) + + filters = frappe._dict( + company="_Test Company", + from_date=add_days(today(), -5), + to_date=today(), + item_code=[item], + warehouse=WAREHOUSE, + voucher_no=included_entry.name, + ) + columns, rows = execute(filters) + + opening_rows = [row for row in rows if row.get("item_code") == "'Opening'"] + self.assertEqual(len(opening_rows), 1) + self.assertEqual(opening_rows[0]["qty_after_transaction"], 10) + + def test_tied_creation_terminal_sle_is_not_summed_twice(self): + item = "_Test Item" + posting_date = add_days(today(), -10) + posting_time = "09:00:00" + + stock_entry_1 = make_stock_entry( + item_code=item, + qty=10, + to_warehouse=WAREHOUSE, + basic_rate=100, + posting_date=posting_date, + posting_time=posting_time, + ) + stock_entry_2 = make_stock_entry( + item_code=item, + qty=5, + to_warehouse=WAREHOUSE, + basic_rate=100, + posting_date=posting_date, + posting_time=posting_time, + ) + + sle_rows = frappe.get_all( + "Stock Ledger Entry", + filters={ + "voucher_type": "Stock Entry", + "voucher_no": ("in", [stock_entry_1.name, stock_entry_2.name]), + "item_code": item, + "warehouse": WAREHOUSE, + "is_cancelled": 0, + }, + fields=["name", "qty_after_transaction"], + order_by="name desc", + ) + self.assertEqual(len(sle_rows), 2) + + for sle in sle_rows: + frappe.db.set_value( + "Stock Ledger Entry", + sle.name, + "creation", + "2026-01-01 00:00:00.000000", + update_modified=False, + ) + + filters = frappe._dict( + company="_Test Company", + from_date=add_days(today(), -5), + to_date=today(), + item_code=[item], + warehouse=WAREHOUSE, + ) + columns, rows = execute(filters) + + opening_rows = [row for row in rows if row.get("item_code") == "'Opening'"] + self.assertEqual(len(opening_rows), 1) + self.assertEqual(opening_rows[0]["qty_after_transaction"], sle_rows[0].qty_after_transaction) + self.assertNotEqual( + opening_rows[0]["qty_after_transaction"], + sum(sle.qty_after_transaction for sle in sle_rows), ) From 243266f5efd61bfc3974c0b078799a04d53ecf7f Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 5 Aug 2026 17:20:53 +0530 Subject: [PATCH 004/134] feat: stock validations in Period Closing Voucher and snapshot-seeded batch valuation (backport #57811) (#57816) * feat: validate stock value and stock closing entry before period closing (cherry picked from commit 20450bd4ec74761c0be7136521558e4f862dbcb5) * fix: do not accept scoped stock closing entries as period closing prerequisite (cherry picked from commit 359a347be2333d58570db4ac90f026dbbbaab608) * feat: seed batch valuation from stock closing balance and freeze closed-period stock (cherry picked from commit 49a127d59c4676eac9383f858a3d05b1ce187729) --- .../period_closing_voucher.py | 122 +++++++++- .../test_period_closing_voucher.py | 214 +++++++++++++++++- erpnext/stock/deprecated_serial_batch.py | 3 + .../stock_closing_entry.py | 65 +++++- erpnext/stock/serial_batch_bundle.py | 54 ++++- erpnext/stock/stock_ledger.py | 28 +++ 6 files changed, 478 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index 8671213c3cc..ac63caf2d2d 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -6,9 +6,10 @@ import copy import frappe from frappe import _ -from frappe.query_builder.functions import Sum -from frappe.utils import add_days, flt, formatdate, getdate +from frappe.query_builder.functions import Max, Sum +from frappe.utils import add_days, flt, fmt_money, formatdate, get_link_to_form, getdate +from erpnext import is_perpetual_inventory_enabled from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import ( make_closing_entries, ) @@ -18,6 +19,8 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.general_ledger import check_freezing_date, is_immutable_ledger_enabled from erpnext.accounts.utils import get_account_currency, get_fiscal_year from erpnext.controllers.accounts_controller import AccountsController +from erpnext.stock.doctype.stock_closing_entry.stock_closing_entry import apply_unscoped_filters +from erpnext.stock.utils import get_stock_value_on class PeriodClosingVoucher(AccountsController): @@ -139,6 +142,121 @@ class PeriodClosingVoucher(AccountsController): if account_currency != company_currency: frappe.throw(_("Currency of the Closing Account must be {0}").format(company_currency)) + def before_submit(self): + if not self.has_stock_transactions(): + return + + self.validate_stock_accounts_balance() + self.validate_stock_closing_entry() + + def has_stock_transactions(self): + if not is_perpetual_inventory_enabled(self.company): + return False + + return bool( + frappe.db.exists( + "Stock Ledger Entry", + { + "company": self.company, + "is_cancelled": 0, + "posting_date": ("<=", self.period_end_date), + }, + ) + ) + + def validate_stock_accounts_balance(self): + precision = frappe.get_precision("GL Entry", "debit") + account_balance = flt(self.get_stock_accounts_balance(), precision) + stock_value = flt( + get_stock_value_on(posting_date=self.period_end_date, company=self.company), precision + ) + + if account_balance == stock_value: + return + + currency = frappe.get_cached_value("Company", self.company, "default_currency") + frappe.throw( + _( + "The closing balance {0} of the Stock Asset accounts does not match the closing value {1} of the Stock Balance report as on {2}. Resolve the difference using the Stock Ledger Variance report before closing the period." + ).format( + frappe.bold(fmt_money(account_balance, currency=currency)), + frappe.bold(fmt_money(stock_value, currency=currency)), + frappe.bold(formatdate(self.period_end_date)), + ), + title=_("Stock Value Mismatch"), + ) + + def get_stock_accounts_balance(self): + gle = frappe.qb.DocType("GL Entry") + account = frappe.qb.DocType("Account") + + stock_accounts = ( + frappe.qb.from_(account) + .select(account.name) + .where( + (account.account_type == "Stock") + & (account.company == self.company) + & (account.is_group == 0) + ) + ) + + balance = ( + frappe.qb.from_(gle) + .select(Sum(gle.debit - gle.credit)) + .where( + (gle.company == self.company) + & (gle.is_cancelled == 0) + & (gle.posting_date <= self.period_end_date) + & gle.account.isin(stock_accounts) + ) + ).run() + + return flt(balance[0][0]) if balance else 0.0 + + def validate_stock_closing_entry(self): + closing_entry = frappe.db.get_value( + "Stock Closing Entry", + apply_unscoped_filters( + {"company": self.company, "to_date": self.period_end_date, "docstatus": 1} + ), + ["name", "status", "modified"], + as_dict=True, + ) + + if not closing_entry: + frappe.throw( + _( + "Create a Stock Closing Entry for the entire company with To Date as {0} before submitting the Period Closing Voucher." + ).format(frappe.bold(formatdate(self.period_end_date))), + title=_("Stock Closing Entry Required"), + ) + + if closing_entry.status != "Completed": + frappe.throw( + _( + "The Stock Closing Entry for {0} is not completed yet. Wait for it to complete before submitting the Period Closing Voucher." + ).format(frappe.bold(formatdate(self.period_end_date))), + title=_("Stock Closing Entry In Progress"), + ) + + self.validate_stock_closing_entry_is_fresh(closing_entry) + + def validate_stock_closing_entry_is_fresh(self, closing_entry): + sle = frappe.qb.DocType("Stock Ledger Entry") + last_change = ( + frappe.qb.from_(sle) + .select(Max(sle.modified)) + .where((sle.company == self.company) & (sle.posting_date <= self.period_end_date)) + ).run() + + if last_change and last_change[0][0] and last_change[0][0] > closing_entry.modified: + frappe.throw( + _( + "Stock transactions were created or modified after the Stock Closing Entry {0} was generated. Regenerate it before submitting the Period Closing Voucher." + ).format(get_link_to_form("Stock Closing Entry", closing_entry.name)), + title=_("Stock Closing Entry Outdated"), + ) + def on_submit(self): self.db_set("gle_processing_status", "In Progress") if frappe.get_single_value("Accounts Settings", "use_legacy_controller_for_pcv"): 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 8ab96447544..83aaacbe046 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 @@ -3,7 +3,7 @@ import unittest import frappe -from frappe.utils import today +from frappe.utils import flt, 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 @@ -307,6 +307,218 @@ class TestPeriodClosingVoucher(ERPNextTestSuite): repost_doc.posting_date = today() repost_doc.save() + def test_stock_validations_before_period_closing(self): + from unittest.mock import patch + + from frappe.custom.doctype.custom_field.custom_field import create_custom_fields + + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + create_custom_fields( + { + "Stock Closing Entry": [ + { + "fieldname": "warehouse", + "label": "Warehouse", + "fieldtype": "Link", + "options": "Warehouse", + } + ] + } + ) + + item = make_item("Test PCV Stock Item", {"is_stock_item": 1}) + se = make_stock_entry( + item_code=item.name, + qty=10, + rate=100, + to_warehouse="Stores - TPC", + company="Test PCV Company", + posting_date="2021-03-15", + ) + + pcv = self.make_period_closing_voucher(posting_date="2021-03-31", submit=False) + self.assertRaisesRegex(frappe.ValidationError, "Create a Stock Closing Entry", pcv.submit) + + sce = frappe.get_doc( + { + "doctype": "Stock Closing Entry", + "company": "Test PCV Company", + "from_date": pcv.period_start_date, + "to_date": pcv.period_end_date, + "warehouse": "Stores - TPC", + } + ).insert() + + with patch("erpnext.stock.doctype.stock_closing_entry.stock_closing_entry.enqueue"): + sce.submit() + + sce.db_set("status", "Completed") + + pcv.reload() + self.assertRaisesRegex(frappe.ValidationError, "Create a Stock Closing Entry", pcv.submit) + + frappe.db.set_value("Stock Closing Entry", sce.name, {"warehouse": None, "status": "In Progress"}) + + pcv.reload() + self.assertRaisesRegex(frappe.ValidationError, "is not completed yet", pcv.submit) + + sce.create_stock_closing_balance_entries() + sce.db_set("status", "Completed") + + sle = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": se.name}, + ["name", "stock_value_difference"], + as_dict=1, + ) + frappe.db.set_value( + "Stock Ledger Entry", sle.name, "stock_value_difference", sle.stock_value_difference + 100 + ) + + pcv.reload() + self.assertRaisesRegex(frappe.ValidationError, "does not match", pcv.submit) + + frappe.db.set_value( + "Stock Ledger Entry", sle.name, "stock_value_difference", sle.stock_value_difference + ) + + pcv.reload() + self.assertRaisesRegex(frappe.ValidationError, "Regenerate", pcv.submit) + + self.rebuild_stock_closing_balance(sce) + pcv.reload() + pcv.submit() + self.assertEqual(pcv.docstatus, 1) + + def test_batch_valuation_seeded_from_stock_closing_after_period_closing(self): + 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_entry.stock_entry_utils import make_stock_entry + + item = make_item( + "Test PCV Batch Item", + { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TPCVB.####", + }, + ) + se1 = make_stock_entry( + item_code=item.name, + qty=10, + rate=100, + to_warehouse="Stores - TPC", + company="Test PCV Company", + posting_date="2021-03-15", + ) + batch_no = get_batch_from_bundle(se1.items[0].serial_and_batch_bundle) + make_stock_entry( + item_code=item.name, + qty=10, + rate=200, + to_warehouse="Stores - TPC", + company="Test PCV Company", + posting_date="2021-06-15", + batch_no=batch_no, + ) + + pcv = self.make_period_closing_voucher(posting_date="2021-03-31", submit=False) + sce = self.make_completed_stock_closing_entry(pcv.period_start_date, pcv.period_end_date) + pcv.reload() + pcv.submit() + + outward = make_stock_entry( + item_code=item.name, + qty=5, + from_warehouse="Stores - TPC", + company="Test PCV Company", + posting_date="2022-04-01", + batch_no=batch_no, + ) + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": outward.name, "is_cancelled": 0}, + "stock_value_difference", + ) + self.assertEqual(flt(stock_value_difference, 2), -750.0) + + self.assertRaisesRegex( + frappe.ValidationError, + "frozen", + make_stock_entry, + item_code=item.name, + qty=1, + rate=100, + to_warehouse="Stores - TPC", + company="Test PCV Company", + posting_date="2021-05-01", + ) + self.assertRaisesRegex(frappe.ValidationError, "frozen", se1.cancel) + self.assertRaisesRegex(frappe.ValidationError, "closed accounting period", sce.cancel) + + def test_period_closing_blocks_stale_stock_closing_entry(self): + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + item = make_item("Test PCV Stock Item", {"is_stock_item": 1}) + make_stock_entry( + item_code=item.name, + qty=10, + rate=100, + to_warehouse="Stores - TPC", + company="Test PCV Company", + posting_date="2021-03-15", + ) + + pcv = self.make_period_closing_voucher(posting_date="2021-03-31", submit=False) + sce = self.make_completed_stock_closing_entry(pcv.period_start_date, pcv.period_end_date) + + make_stock_entry( + item_code=item.name, + qty=5, + rate=100, + to_warehouse="Stores - TPC", + company="Test PCV Company", + posting_date="2021-05-01", + ) + + pcv.reload() + self.assertRaisesRegex(frappe.ValidationError, "Regenerate", pcv.submit) + + self.rebuild_stock_closing_balance(sce) + pcv.reload() + pcv.submit() + self.assertEqual(pcv.docstatus, 1) + + def make_completed_stock_closing_entry(self, from_date, to_date): + from unittest.mock import patch + + sce = frappe.get_doc( + { + "doctype": "Stock Closing Entry", + "company": "Test PCV Company", + "from_date": from_date, + "to_date": to_date, + } + ).insert() + + with patch("erpnext.stock.doctype.stock_closing_entry.stock_closing_entry.enqueue"): + sce.submit() + + sce.create_stock_closing_balance_entries() + sce.db_set("status", "Completed") + return sce + + def rebuild_stock_closing_balance(self, sce): + sce.remove_stock_closing() + sce.create_stock_closing_balance_entries() + sce.db_set("status", "Completed") + def make_period_closing_voucher(self, posting_date, submit=True): surplus_account = create_account() cost_center = create_cost_center("Test Cost Center 1") diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index 9432e8c59ae..77d691837f1 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -159,6 +159,9 @@ class DeprecatedBatchNoValuation: if self.sle.name: query = query.where(sle.name != self.sle.name) + if getattr(self, "stock_closing_from_datetime", None): + query = query.where(sle.posting_datetime >= self.stock_closing_from_datetime) + return query.run(as_dict=True) @deprecated( diff --git a/erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py b/erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py index 5c523cc560e..30471ad817d 100644 --- a/erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py +++ b/erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py @@ -10,9 +10,51 @@ from frappe.desk.form.load import get_attachments from frappe.model.document import Document from frappe.utils import add_days, get_date_str, get_link_to_form, nowtime, parse_json from frappe.utils.background_jobs import enqueue +from frappe.utils.caching import request_cache from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions +SCOPE_FIELDS = ("warehouse", "item_code", "item_group", "warehouse_type") + + +def apply_unscoped_filters(filters): + meta = frappe.get_meta("Stock Closing Entry") + for fieldname in SCOPE_FIELDS: + if meta.has_field(fieldname): + filters[fieldname] = ("is", "not set") + + return filters + + +def get_closing_entry_for_closed_period(company): + closed_upto = frappe.db.get_value( + "Period Closing Voucher", {"docstatus": 1, "company": company}, [{"MAX": "period_end_date"}] + ) + if not closed_upto: + return None + + return _get_completed_closing_entry(company, str(closed_upto)) + + +@request_cache +def _get_completed_closing_entry(company, closed_upto): + filters = apply_unscoped_filters( + { + "company": company, + "docstatus": 1, + "status": "Completed", + "to_date": ("<=", closed_upto), + } + ) + + return frappe.db.get_value( + "Stock Closing Entry", + filters, + ["name", "to_date"], + order_by="to_date desc", + as_dict=True, + ) + class StockClosingEntry(Document): # begin: auto-generated types @@ -68,7 +110,7 @@ class StockClosingEntry(Document): ) ) - for fieldname in ["warehouse", "item_code", "item_group", "warehouse_type"]: + for fieldname in SCOPE_FIELDS: if self.get(fieldname): query = query.where(table[fieldname] == self.get(fieldname)) @@ -86,14 +128,30 @@ class StockClosingEntry(Document): self.enqueue_job() def on_cancel(self): + self.validate_closed_period_lock() self.set_status(save=True) self.remove_stock_closing() + def validate_closed_period_lock(self): + pcv = frappe.db.get_value( + "Period Closing Voucher", + {"company": self.company, "docstatus": 1, "period_end_date": (">=", self.to_date)}, + "name", + ) + + if pcv: + frappe.throw( + _( + "Stock Closing Entry {0} belongs to a closed accounting period. Cancel the Period Closing Voucher {1} first." + ).format(self.name, get_link_to_form("Period Closing Voucher", pcv)), + title=_("Closed Period"), + ) + def remove_stock_closing(self): table = frappe.qb.DocType("Stock Closing Balance") frappe.qb.from_(table).delete().where(table.stock_closing_entry == self.name).run() - @frappe.whitelist() + @frappe.whitelist(methods=["POST"]) def enqueue_job(self): self.db_set("status", "In Progress") enqueue(prepare_closing_stock_balance, name=self.name, queue="long", timeout=1500) @@ -103,8 +161,9 @@ class StockClosingEntry(Document): ).format(self.name) ) - @frappe.whitelist() + @frappe.whitelist(methods=["POST"]) def regenerate_closing_balance(self): + self.validate_closed_period_lock() self.remove_stock_closing() self.enqueue_job() diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index e7ccac40115..491a9667d2a 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -820,13 +820,14 @@ class BatchNoValuation(DeprecatedBatchNoValuation): "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "total_amount" ) else: - entries = self.get_batch_stock_before_date() self.stock_value_change = 0.0 self.batch_avg_rate = defaultdict(float) self.available_qty = defaultdict(float) self.stock_value_differece = defaultdict(float) - for ledger in entries: + self.seed_from_stock_closing_balance() + + for ledger in self.get_batch_stock_before_date(): self.stock_value_differece[ledger.batch_no] += flt(ledger.incoming_rate) self.available_qty[ledger.batch_no] += flt(ledger.qty) @@ -834,6 +835,52 @@ class BatchNoValuation(DeprecatedBatchNoValuation): self.calculate_avg_rate_for_non_batchwise_valuation() self.set_stock_value_difference() + def seed_from_stock_closing_balance(self): + self.stock_closing_from_datetime = None + closing_entry = self.get_closing_entry_for_seeding() + if not closing_entry: + return + + from erpnext.stock.utils import get_combine_datetime + + self.stock_closing_from_datetime = get_combine_datetime( + add_days(closing_entry.to_date, 1), "00:00:00" + ) + + for row in self.get_stock_closing_balance_entries(closing_entry.name): + self.stock_value_differece[row.batch_no] += flt(row.stock_value_difference) + self.available_qty[row.batch_no] += flt(row.actual_qty) + + def get_closing_entry_for_seeding(self): + from erpnext.stock.doctype.stock_closing_entry.stock_closing_entry import ( + get_closing_entry_for_closed_period, + ) + + if not self.batchwise_valuation_batches or not self.sle.posting_date: + return None + + company = self.sle.company or frappe.get_cached_value("Warehouse", self.sle.warehouse, "company") + closing_entry = get_closing_entry_for_closed_period(company) + if not closing_entry or getdate(self.sle.posting_date) <= getdate(closing_entry.to_date): + return None + + return closing_entry + + def get_stock_closing_balance_entries(self, closing_entry): + table = frappe.qb.DocType("Stock Closing Balance") + + return ( + frappe.qb.from_(table) + .select(table.batch_no, table.actual_qty, table.stock_value_difference) + .where( + (table.stock_closing_entry == closing_entry) + & (table.item_code == self.sle.item_code) + & (table.warehouse == self.sle.warehouse) + & table.batch_no.isin(self.batchwise_valuation_batches) + & (table.inventory_dimension_key.isnull() | (table.inventory_dimension_key == "")) + ) + ).run(as_dict=True) + def get_batch_stock_before_date(self) -> list[dict]: # Get batch wise stock value difference from Serial and Batch Bundle considering time condition if not self.batchwise_valuation_batches: @@ -909,6 +956,9 @@ class BatchNoValuation(DeprecatedBatchNoValuation): if timestamp_condition: query = query.where(timestamp_condition) + if self.stock_closing_from_datetime: + query = query.where(child.posting_datetime >= self.stock_closing_from_datetime) + return query.run(as_dict=True) def prepare_batches(self): diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 23e99899068..60fcec3f7cb 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -56,6 +56,32 @@ class SerialNoExistsInFutureTransaction(frappe.ValidationError): pass +def validate_stock_frozen_by_closing_entry(sl_entries): + from erpnext.stock.doctype.stock_closing_entry.stock_closing_entry import ( + get_closing_entry_for_closed_period, + ) + + company = sl_entries[0].get("company") + if not company: + company = frappe.get_cached_value("Warehouse", sl_entries[0].get("warehouse"), "company") + + closing_entry = get_closing_entry_for_closed_period(company) + if not closing_entry: + return + + for sle in sl_entries: + if sle.get("posting_date") and getdate(sle.get("posting_date")) <= getdate(closing_entry.to_date): + frappe.throw( + _( + "Stock transactions dated on or before {0} are frozen because the period is closed and the Stock Closing Entry {1} has been generated. To make changes, cancel the Period Closing Voucher first." + ).format( + frappe.bold(format_date(closing_entry.to_date)), + get_link_to_form("Stock Closing Entry", closing_entry.name), + ), + title=_("Stock Frozen"), + ) + + def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): """Create SL entries from SL entry dicts @@ -70,6 +96,8 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc from erpnext.controllers.stock_controller import future_sle_exists if sl_entries: + validate_stock_frozen_by_closing_entry(sl_entries) + cancelled = sl_entries[0].get("is_cancelled") if cancelled: validate_cancellation(sl_entries) From 0e26f9b1dbd3ce130680ec2d280ac33a577cea20 Mon Sep 17 00:00:00 2001 From: Henil Maru Date: Wed, 5 Aug 2026 17:58:58 +0530 Subject: [PATCH 005/134] fix(sales-invoice): respect Customize Form hidden setting on Update Stock (#57819) frm.toggle_display("update_stock", ...) unconditionally forced the field visible based only on has_subcontracted, overwriting whatever Customize Form had set on every refresh. OR it with the field's original (property-setter-driven) hidden value instead. Backport of #57818. --- .../accounts/doctype/sales_invoice/sales_invoice.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 7597eec0e57..3ee34f24d17 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -1180,7 +1180,16 @@ frappe.ui.form.on("Sales Invoice", { } frm.set_df_property("update_stock", "read_only", frm.doc.has_subcontracted); - frm.toggle_display("update_stock", !frm.doc.has_subcontracted); + // frm.set_df_property mutates a per-document copy, not the doctype's shared field + // metadata, so this always reflects the original (Customize Form) hidden value. + const hidden_by_customization = cint( + frappe.meta.get_docfield("Sales Invoice", "update_stock")?.hidden + ); + frm.set_df_property( + "update_stock", + "hidden", + cint(frm.doc.has_subcontracted) || hidden_by_customization + ); }, }); From 7e72e70cc11d10b20328ab65458e44c914dd8011 Mon Sep 17 00:00:00 2001 From: pandiyan Date: Thu, 6 Aug 2026 11:25:36 +0530 Subject: [PATCH 006/134] fix: clear deferred revenue/expense fields on uncheck (backport #57140) --- erpnext/controllers/accounts_controller.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index fbe8b348000..c934cfd4eb2 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -261,6 +261,7 @@ class AccountsController(TransactionBase): if self.is_return: self.validate_qty() else: + self.clear_stale_deferred_fields() self.validate_deferred_start_and_end_date() self.validate_inter_company_reference() @@ -644,6 +645,23 @@ class AccountsController(TransactionBase): if self.get("from_date") and self.get("to_date") and getdate(self.from_date) > getdate(self.to_date): frappe.throw(_("To Date cannot be before From Date"), title=_("Invalid Auto Repeat Date")) + def clear_stale_deferred_fields(self): + field_map = { + "Sales Invoice": "deferred_revenue_account", + "Purchase Invoice": "deferred_expense_account", + } + account_field = field_map.get(self.doctype) + + for item in self.get("items"): + if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): + continue + + item.service_start_date = None + item.service_end_date = None + item.service_stop_date = None + if account_field: + item.set(account_field, None) + def validate_deferred_start_and_end_date(self): for d in self.items: if d.get("enable_deferred_revenue") or d.get("enable_deferred_expense"): From 626e35135fd80819eb448fc1b6de48c7cdbf9298 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 6 Aug 2026 13:38:45 +0530 Subject: [PATCH 007/134] fix(stock): drop call to confirm_if_drafts_exist missing on v16 (#57833) --- erpnext/stock/doctype/material_request/material_request.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 4ff2c2c27af..c3ba08b16a7 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -533,7 +533,7 @@ frappe.ui.form.on("Material Request", { }, ], primary_action_label: __("Create"), - primary_action: async function (values) { + primary_action: function (values) { const item_suppliers = (values.items || []).filter((row) => row.__checked); if (!item_suppliers.length) { frappe.throw(__("Select at least one Item")); @@ -567,10 +567,6 @@ frappe.ui.form.on("Material Request", { ); } - if (!(await erpnext.utils.confirm_if_drafts_exist(frm.doc, "Purchase Order"))) { - return; - } - frappe.call({ method: "erpnext.stock.doctype.material_request.material_request.make_purchase_orders_by_supplier", args: { source_name: frm.doc.name, item_suppliers: item_suppliers }, From aa70d9bbc34d73813d80640d99f598d4cdd95512 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 6 Aug 2026 15:44:56 +0530 Subject: [PATCH 008/134] fix: purchase return of batchwise valuation batch valued at original receipt rate instead of batch avg rate (version-16-hotfix) (#57836) * fix: use current batch avg rate for outward returns of batchwise valuation batches * fix: honor zero batch average and avoid duplicate batch classification query --- .../purchase_receipt/test_purchase_receipt.py | 60 +++++++++++++++++++ .../serial_and_batch_bundle.py | 50 ++++++++++++++++ erpnext/stock/serial_batch_bundle.py | 5 ++ 3 files changed, 115 insertions(+) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index f7fba10ac5c..d1ec25650af 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -5611,6 +5611,66 @@ class TestPurchaseReceipt(ERPNextTestSuite): self.assertEqual(frappe.parse_json(stock_queue), [[20, 0.0]]) + def test_purchase_return_valuation_for_batchwise_valuation_batch(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + item_code = make_item( + "Test Purchase Return Batchwise Valn Item", + { + "is_stock_item": 1, + "has_batch_no": 1, + "batch_number_series": "BN-TPRBWV-.#####", + }, + ).name + + batch_no = "BN-TPRBWV-00001" + batch = frappe.new_doc("Batch").update({"batch_id": batch_no, "item": item_code}).insert() + self.assertEqual(batch.use_batchwise_valuation, 1) + + warehouse = "_Test Warehouse - _TC" + pr = make_purchase_receipt( + item_code=item_code, + qty=100, + rate=1000, + warehouse=warehouse, + batch_no=batch_no, + use_serial_batch_fields=1, + ) + make_purchase_receipt( + item_code=item_code, + qty=100, + rate=400, + warehouse=warehouse, + batch_no=batch_no, + use_serial_batch_fields=1, + ) + create_delivery_note( + item_code=item_code, + qty=100, + warehouse=warehouse, + batch_no=batch_no, + use_serial_batch_fields=1, + ) + + return_pr = make_return_doc("Purchase Receipt", pr.name) + return_pr.submit() + + sle = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": return_pr.name, "is_cancelled": 0}, + ["stock_value_difference", "qty_after_transaction", "stock_value", "serial_and_batch_bundle"], + as_dict=True, + ) + self.assertEqual(flt(sle.qty_after_transaction), 0.0) + self.assertEqual(flt(sle.stock_value_difference, 2), -70000.0) + self.assertEqual(flt(sle.stock_value, 2), 0.0) + + rate = frappe.db.get_value( + "Serial and Batch Entry", {"parent": sle.serial_and_batch_bundle}, "incoming_rate" + ) + self.assertEqual(flt(rate, 2), 700.0) + def test_negative_stock_error_for_purchase_return(self): from erpnext.controllers.sales_and_purchase_return import make_return_doc from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index b93f3aacc30..cd141849eed 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -414,6 +414,13 @@ class SerialandBatchBundle(Document): valuation_method = get_valuation_method(self.item_code, self.company) + # An outward return must go out at the batch's current average rate for a + # batchwise valuation batch. The original receipt rate is only correct while + # the batch still holds stock at that rate; once other receipts have changed + # the average, removing at the original rate strands a residue in the batch + # value (negative when returning the costlier receipt). + batchwise_avg_rates = self.get_batchwise_return_avg_rates() + stock_queue = [] non_batchwise_batches = [] if not self.has_serial_no and valuation_method == "FIFO": @@ -447,6 +454,12 @@ class SerialandBatchBundle(Document): batches = sorted(list(valuation_details["batches"].keys())) valuation_rate = valuation_details["batches"].get(batches[cint(row.idx) - 1]) + # a batch with an available balance goes out at its current average rate (a + # valid 0.0 included); the original receipt rate applies only when there is + # no balance to average + if not row.serial_no and row.batch_no in batchwise_avg_rates: + valuation_rate = batchwise_avg_rates[row.batch_no] + row.incoming_rate = flt(valuation_rate) row.stock_value_difference = flt(row.qty) * flt(row.incoming_rate) @@ -475,6 +488,43 @@ class SerialandBatchBundle(Document): elif self.type_of_transaction == "Inward": self.set_incoming_rate_for_inward_transaction(row, save, prev_sle=prev_sle) + def get_batchwise_return_avg_rates(self): + from erpnext.stock.utils import get_valuation_method + + if self.type_of_transaction != "Outward" or self.has_serial_no: + return {} + + batch_nos = [d.batch_no for d in self.entries if d.batch_no] + if not batch_nos: + return {} + + if get_valuation_method( + self.item_code, self.company + ) == "Moving Average" and frappe.db.get_single_value( + "Stock Settings", "do_not_use_batchwise_valuation" + ): + return {} + + batchwise_batches = frappe.get_all( + "Batch", + filters={"name": ("in", batch_nos), "use_batchwise_valuation": 1}, + pluck="name", + ) + if not batchwise_batches: + return {} + + # scoped to batchwise batches only, so BatchNoValuation's non-batchwise + # machinery never runs for them + sle = self.get_sle_for_outward_transaction() + sle.batch_nos = {batch_no: sle.batch_nos[batch_no] for batch_no in batchwise_batches} + sle.batchwise_valuation_batches = batchwise_batches + sn_obj = BatchNoValuation(sle=sle, item_code=self.item_code, warehouse=self.warehouse) + return { + batch_no: abs(flt(sn_obj.batch_avg_rate.get(batch_no))) + for batch_no in batchwise_batches + if flt(sn_obj.available_qty.get(batch_no)) + } + def validate_returned_serial_batch_no(self, return_against, row, original_inv_details): if frappe.flags.through_repost_item_valuation and not frappe.in_test: return diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 491a9667d2a..5fbb8a9c978 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -971,6 +971,11 @@ class BatchNoValuation(DeprecatedBatchNoValuation): self.batchwise_valuation_batches = [] self.non_batchwise_valuation_batches = [] + if batchwise_batches := self.sle.get("batchwise_valuation_batches"): + self.batchwise_valuation_batches = list(batchwise_batches) + self.non_batchwise_valuation_batches = list(set(self.batches) - set(batchwise_batches)) + return + if get_valuation_method( self.sle.item_code, self.sle.company ) == "Moving Average" and frappe.get_single_value("Stock Settings", "do_not_use_batchwise_valuation"): From c8125b8b5a305aad22e49f7849204dd3759d1962 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Thu, 6 Aug 2026 07:41:09 +0530 Subject: [PATCH 009/134] refactor(purchase_invoice): expose invoice hold actions as document methods --- .../purchase_invoice/purchase_invoice.js | 26 ++- .../purchase_invoice/purchase_invoice.json | 3 +- .../purchase_invoice/purchase_invoice.py | 70 ++++---- .../purchase_invoice/test_purchase_invoice.py | 154 +++++++++++++++++- 4 files changed, 208 insertions(+), 45 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 68dc8d26d1b..7936cee69b1 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -240,10 +240,8 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. unblock_invoice() { const me = this; - frappe.call({ - method: "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.unblock_invoice", - args: { name: me.frm.doc.name }, - callback: (r) => me.frm.reload_doc(), + me.frm.call("unblock_invoice", null, () => { + me.frm.reload_doc(); }); } @@ -294,15 +292,16 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. this.dialog.set_primary_action(__("Save"), function () { const dialog_data = me.dialog.get_values(); - frappe.call({ - method: "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.block_invoice", - args: { - name: me.frm.doc.name, + me.frm.call( + "block_invoice", + { hold_comment: dialog_data.hold_comment, release_date: dialog_data.release_date, }, - callback: (r) => me.frm.reload_doc(), - }); + () => { + me.frm.reload_doc(); + } + ); me.dialog.hide(); }); @@ -341,10 +340,9 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. } set_release_date(data) { - return frappe.call({ - method: "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.change_release_date", - args: data, - callback: (r) => this.frm.reload_doc(), + const me = this; + return me.frm.call("change_release_date", { release_date: data.release_date }, () => { + me.frm.reload_doc(); }); } diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 9a2bcd715ab..d2c45a8681f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -352,6 +352,7 @@ { "collapsible": 1, "collapsible_depends_on": "eval:doc.on_hold", + "depends_on": "eval:doc.on_hold", "fieldname": "sb_14", "fieldtype": "Section Break", "label": "Hold Invoice" @@ -1702,7 +1703,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2026-07-12 23:54:21.263951", + "modified": "2026-08-05 15:40:16.519774", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 8d91408a002..82fed84eafb 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -8,7 +8,7 @@ import frappe from frappe import _, qb, throw from frappe.model.mapper import get_mapped_doc from frappe.query_builder.functions import Sum -from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate +from frappe.utils import DateTimeLikeObject, cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate import erpnext from erpnext.accounts.deferred_revenue import validate_service_stop_date @@ -309,6 +309,9 @@ class PurchaseInvoice(BuyingController): PurchaseTaxWithholding(self).on_validate() self.set_percentage_received() + if self.on_hold: + self.validate_invoice_hold() + def set_percentage_received(self): total_billed_qty = 0.0 total_received_qty = 0.0 @@ -320,6 +323,13 @@ class PurchaseInvoice(BuyingController): if total_billed_qty and total_received_qty: self.per_received = total_received_qty / total_billed_qty * 100 + def validate_invoice_hold(self): + if self.is_return: + frappe.throw(_("Return Purchase Invoice cannot be held.")) + + if self.docstatus < 1: + frappe.throw(_("Purchase Invoice can be held after submitting.")) + def validate_release_date(self): if self.release_date and getdate(nowdate()) >= getdate(self.release_date): frappe.throw(_("Release date must be in the future")) @@ -1901,14 +1911,38 @@ class PurchaseInvoice(BuyingController): def on_recurring(self, reference_doc, auto_repeat_doc): self.due_date = None - def block_invoice(self, hold_comment=None, release_date=None): - self.db_set("on_hold", 1) - self.db_set("hold_comment", cstr(hold_comment)) + @frappe.whitelist(methods=["POST"]) + def block_invoice(self, hold_comment: str | None = None, release_date: DateTimeLikeObject | None = None): + self.check_permission("write") + self.on_hold = 1 + self.release_date = release_date + self.validate_block_invoice() + + self.db_set({"on_hold": 1, "hold_comment": cstr(hold_comment), "release_date": release_date}) + + @frappe.whitelist(methods=["POST"]) + def unblock_invoice(self): + self.check_permission("write") + self.db_set({"on_hold": 0, "release_date": None}) + + @frappe.whitelist(methods=["POST"]) + def change_release_date(self, release_date: DateTimeLikeObject | None = None): + self.check_permission("write") + + if not self.on_hold: + frappe.throw(_("Invoice is not blocked. Block the invoice to change the release date.")) + + self.release_date = release_date + self.validate_block_invoice() + self.db_set("release_date", release_date) - def unblock_invoice(self): - self.db_set("on_hold", 0) - self.db_set("release_date", None) + def validate_block_invoice(self): + self.validate_invoice_hold() + if self.outstanding_amount <= 0: + frappe.throw(_("Purchase Invoice without any outstanding amount cannot be held.")) + + self.validate_release_date() def set_status(self, update=False, status=None, update_modified=True): if self.is_new(): @@ -2033,28 +2067,6 @@ def make_stock_entry(source_name, target_doc=None): return doc -@frappe.whitelist() -def change_release_date(name, release_date=None): - if frappe.db.exists("Purchase Invoice", name): - pi = frappe.get_lazy_doc("Purchase Invoice", name) - pi.check_permission() - pi.db_set("release_date", release_date) - - -@frappe.whitelist() -def unblock_invoice(name): - if frappe.db.exists("Purchase Invoice", name): - pi = frappe.get_lazy_doc("Purchase Invoice", name) - pi.unblock_invoice() - - -@frappe.whitelist() -def block_invoice(name, release_date, hold_comment=None): - if frappe.db.exists("Purchase Invoice", name): - pi = frappe.get_lazy_doc("Purchase Invoice", name) - pi.block_invoice(hold_comment, release_date) - - @frappe.whitelist() def make_inter_company_sales_invoice(source_name, target_doc=None): from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index f0952bf3307..8d9e7309366 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -278,14 +278,166 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin): def test_purchase_invoice_explicit_block(self): pi = make_purchase_invoice() - pi.block_invoice() + release_date = add_days(nowdate(), 10) + + pi.block_invoice(hold_comment="Waiting for the goods", release_date=release_date) self.assertEqual(pi.on_hold, 1) + on_hold, hold_comment, saved_release_date = frappe.db.get_value( + "Purchase Invoice", pi.name, ["on_hold", "hold_comment", "release_date"] + ) + self.assertEqual(on_hold, 1) + self.assertEqual(hold_comment, "Waiting for the goods") + self.assertEqual(getdate(saved_release_date), getdate(release_date)) + pi.unblock_invoice() self.assertEqual(pi.on_hold, 0) + on_hold, saved_release_date = frappe.db.get_value( + "Purchase Invoice", pi.name, ["on_hold", "release_date"] + ) + self.assertEqual(on_hold, 0) + self.assertIsNone(saved_release_date) + + def test_purchase_invoice_cannot_be_held_before_submission(self): + pi = make_purchase_invoice(do_not_save=True) + pi.on_hold = 1 + + self.assertRaises(frappe.ValidationError, pi.save) + + pi.on_hold = 0 + pi.save() + pi.submit() + + pi.block_invoice() + self.assertEqual(frappe.db.get_value("Purchase Invoice", pi.name, "on_hold"), 1) + + def test_return_purchase_invoice_cannot_be_held(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + pi = make_purchase_invoice() + + return_pi = make_return_doc(pi.doctype, pi.name) + return_pi.on_hold = 1 + self.assertRaisesRegex(frappe.ValidationError, "cannot be held", return_pi.save) + + return_pi.on_hold = 0 + return_pi.save() + return_pi.submit() + + self.assertRaisesRegex(frappe.ValidationError, "cannot be held", return_pi.block_invoice) + + def test_return_purchase_invoice_is_not_affected_by_hold_validations(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + pi = make_purchase_invoice() + + # a return has a negative outstanding amount, which must not be mistaken + # for an invalid hold on a document that was never held + return_pi = make_return_doc(pi.doctype, pi.name) + return_pi.save() + return_pi.submit() + + self.assertEqual(return_pi.docstatus, 1) + self.assertEqual(return_pi.on_hold, 0) + self.assertLess(return_pi.outstanding_amount, 0) + + def test_settled_purchase_invoice_cannot_be_held(self): + pi = make_purchase_invoice() + + pe = get_payment_entry("Purchase Invoice", dn=pi.name, bank_account="_Test Bank - _TC") + pe.reference_no = "1" + pe.reference_date = nowdate() + pe.save() + pe.submit() + + pi.reload() + self.assertEqual(pi.outstanding_amount, 0) + + self.assertRaises(frappe.ValidationError, pi.block_invoice) + self.assertEqual(frappe.db.get_value("Purchase Invoice", pi.name, "on_hold"), 0) + + def test_release_date_of_held_invoice_must_be_in_future(self): + pi = make_purchase_invoice() + + self.assertRaises(frappe.ValidationError, pi.block_invoice, "Hold", add_days(nowdate(), -1)) + self.assertRaises(frappe.ValidationError, pi.block_invoice, "Hold", nowdate()) + + def test_rejected_hold_does_not_partially_update_invoice(self): + pi = make_purchase_invoice() + + self.assertRaises(frappe.ValidationError, pi.block_invoice, "Hold", add_days(nowdate(), -1)) + + pi.reload() + self.assertEqual(pi.on_hold, 0) + self.assertIsNone(pi.release_date) + + def test_change_release_date_of_held_invoice(self): + pi = make_purchase_invoice() + pi.block_invoice(hold_comment="Hold", release_date=add_days(nowdate(), 10)) + + new_release_date = add_days(nowdate(), 20) + pi.change_release_date(new_release_date) + + self.assertEqual( + getdate(frappe.db.get_value("Purchase Invoice", pi.name, "release_date")), + getdate(new_release_date), + ) + + self.assertRaises(frappe.ValidationError, pi.change_release_date, add_days(nowdate(), -1)) + + def test_release_date_cannot_be_changed_on_an_invoice_that_is_not_held(self): + pi = make_purchase_invoice() + + self.assertRaisesRegex( + frappe.ValidationError, + "Invoice is not blocked", + pi.change_release_date, + add_days(nowdate(), 10), + ) + + self.assertIsNone(frappe.db.get_value("Purchase Invoice", pi.name, "release_date")) + + def test_hold_methods_are_whitelisted_document_methods(self): + import erpnext.accounts.doctype.purchase_invoice.purchase_invoice as purchase_invoice_module + + pi = frappe.new_doc("Purchase Invoice") + + for method in ("block_invoice", "unblock_invoice", "change_release_date"): + # raises if the method is not whitelisted for client side calls + pi.is_whitelisted(method) + + self.assertFalse( + hasattr(purchase_invoice_module, method), + f"{method} should only be exposed as a document method", + ) + + def test_hold_methods_require_write_permission(self): + pi = make_purchase_invoice() + user = "test_pi_hold_permission@example.com" + + if not frappe.db.exists("User", user): + frappe.get_doc( + { + "doctype": "User", + "email": user, + "first_name": "Test PI Hold", + "roles": [{"role": "Employee"}], + } + ).insert(ignore_permissions=True) + + frappe.set_user(user) + try: + self.assertRaises(frappe.PermissionError, pi.block_invoice) + self.assertRaises(frappe.PermissionError, pi.unblock_invoice) + self.assertRaises(frappe.PermissionError, pi.change_release_date, add_days(nowdate(), 10)) + finally: + frappe.set_user("Administrator") + + self.assertEqual(frappe.db.get_value("Purchase Invoice", pi.name, "on_hold"), 0) + def test_gl_entries_with_perpetual_inventory_against_pr(self): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", From 04718e15c954a082197cfbc7f294ac95fff9e65f Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Thu, 6 Aug 2026 16:37:57 +0530 Subject: [PATCH 010/134] fix(journal_entry): validate blocked purchase invoices --- .../accounts/doctype/journal_entry/journal_entry.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index facb15f15f7..65db883c430 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -906,6 +906,18 @@ class JournalEntry(AccountsController): ) ) + if reference_type == "Purchase Invoice" and invoice.invoice_is_blocked(): + msg = ( + _("{0} {1} is blocked and on hold until {2}.").format( + invoice.doctype, invoice.name, invoice.release_date + ) + if invoice.release_date + else _("{0} {1} is blocked.").format( + invoice.doctype, invoice.name, invoice.release_date + ) + ) + frappe.throw(msg) + def set_against_account(self): accounts_debited, accounts_credited = [], [] if self.voucher_type in ("Deferred Revenue", "Deferred Expense"): From 0bb0f6d68994dabc88c245845ab628aa308d00cc Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Thu, 6 Aug 2026 12:31:00 +0530 Subject: [PATCH 011/134] test(journal_entry): added test cases for blocked purchase invoices --- .../journal_entry/test_journal_entry.py | 65 ++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py index 581a0866721..51922757d05 100644 --- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py @@ -2,7 +2,7 @@ # License: GNU General Public License v3. See license.txt import frappe -from frappe.utils import flt, nowdate +from frappe.utils import add_days, flt, nowdate from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.accounts.doctype.journal_entry.journal_entry import StockAccountInvalidTransaction @@ -609,6 +609,69 @@ class TestJournalEntry(ERPNextTestSuite): jv.save() self.assertRaises(frappe.ValidationError, jv.submit) + def make_jv_against_purchase_invoice(self, invoice, amount=100): + jv = make_journal_entry("Creditors - _TC", "_Test Cash - _TC", amount, save=False) + jv.accounts[0].party_type = "Supplier" + jv.accounts[0].party = invoice.supplier + jv.accounts[0].reference_type = "Purchase Invoice" + jv.accounts[0].reference_name = invoice.name + return jv + + def test_jv_against_purchase_invoice_respects_hold_state(self): + """Payment can be booked against a Purchase Invoice only while it is not on hold.""" + from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice + + release_date = add_days(nowdate(), 10) + + def never_held(): + return make_purchase_invoice() + + def held_until_a_future_date(): + invoice = make_purchase_invoice() + invoice.block_invoice(hold_comment="Waiting for the goods", release_date=release_date) + return invoice + + def held_without_a_release_date(): + invoice = make_purchase_invoice() + invoice.block_invoice(hold_comment="Under dispute") + return invoice + + def held_until_a_date_that_has_passed(): + invoice = held_until_a_future_date() + frappe.db.set_value("Purchase Invoice", invoice.name, "release_date", add_days(nowdate(), -1)) + return invoice + + def unblocked_again(): + invoice = held_until_a_future_date() + invoice.unblock_invoice() + return invoice + + for build_invoice in (held_until_a_future_date, held_without_a_release_date): + with self.subTest(build_invoice.__name__): + jv = self.make_jv_against_purchase_invoice(build_invoice()) + self.assertRaisesRegex(frappe.ValidationError, "is blocked", jv.insert) + + for build_invoice in (never_held, held_until_a_date_that_has_passed, unblocked_again): + with self.subTest(build_invoice.__name__): + invoice = build_invoice() + jv = self.make_jv_against_purchase_invoice(invoice) + jv.insert() + self.assertEqual(jv.reference_types[invoice.name], "Purchase Invoice") + + def test_jv_against_blocked_sales_invoice_reference_is_not_checked(self): + """A Sales Invoice has no hold state, so the check must skip it rather than fail.""" + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice + + invoice = create_sales_invoice(rate=500) + jv = make_journal_entry("_Test Cash - _TC", "Debtors - _TC", 100, save=False) + jv.accounts[1].party_type = "Customer" + jv.accounts[1].party = "_Test Customer" + jv.accounts[1].reference_type = "Sales Invoice" + jv.accounts[1].reference_name = invoice.name + jv.insert() + + self.assertEqual(jv.reference_types[invoice.name], "Sales Invoice") + def make_journal_entry( account1, From e1c1c5ed7eb727ad719cce16f835c9ab2223ac48 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 6 Aug 2026 20:48:19 +0530 Subject: [PATCH 012/134] refactor: remove unreachable UOM conversion in production plan The division by conversion_factor in _adjust_required_qty_for_uom sits directly after frappe.throw inside the same block, so it can never run. It has been dead since commit 2a8cd05b44 (#27278) re-indented it into the throw branch; the actual purchase-UOM conversion happens in _material_request_item_row via _mr_purchase_conversion_factor. (cherry picked from commit 44260b469f325770f7764d77a28ac920a8df92c5) # Conflicts: # erpnext/manufacturing/doctype/production_plan/services/material_request.py --- .../services/material_request.py | 731 ++++++++++++++++++ 1 file changed, 731 insertions(+) create mode 100644 erpnext/manufacturing/doctype/production_plan/services/material_request.py diff --git a/erpnext/manufacturing/doctype/production_plan/services/material_request.py b/erpnext/manufacturing/doctype/production_plan/services/material_request.py new file mode 100644 index 00000000000..c9073d5e648 --- /dev/null +++ b/erpnext/manufacturing/doctype/production_plan/services/material_request.py @@ -0,0 +1,731 @@ +# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +"""Material Request planning and creation for a Production Plan. + +Consolidates the former ``material_planning``, ``material_request_items`` and +``material_request_helpers`` modules. Also re-exports the planning helpers so +existing imports of ``...services.material_planning`` keep working through here. +""" + +import copy +import json +from collections import defaultdict + +import frappe +from frappe import _, msgprint +from frappe.model.document import Document +from frappe.utils import add_days, ceil, cint, comma_and, flt, get_link_to_form, nowdate +from frappe.utils.csvutils import build_csv_response + +from erpnext.manufacturing.doctype.production_plan.services.bom_explosion import ( + get_exploded_items, + get_subitems, +) +from erpnext.manufacturing.doctype.production_plan.services.planning_queries import ( + get_bin_details, + get_item_data, + get_sales_orders, + get_uom_conversion_factor, + get_warehouse_list, + set_default_warehouses, +) +from erpnext.manufacturing.doctype.production_plan.services.sub_assembly_queries import ( + get_raw_materials_of_sub_assembly_items, + get_sub_assembly_items, +) +from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults +from erpnext.stock.get_item_details import get_conversion_factor + + +class MaterialRequestService: + def __init__(self, doc): + self.doc = doc + + def validate_mr_subcontracted(self): + for row in self.doc.mr_items: + if row.material_request_type != "Subcontracting": + continue + if not frappe.db.get_value("Item", row.item_code, "is_sub_contracted_item"): + frappe.throw( + _("Item {0} is not a subcontracted item").format(row.item_code), + title=_("Invalid Item"), + ) + + def make_material_request(self): + """Create Material Requests grouped by Sales Order and Material Request Type""" + self.validate_mr_subcontracted() + + if all(item.requested_qty == item.quantity for item in self.doc.mr_items): + msgprint(_("All items are already requested")) + return + + material_request_map = {} + material_request_list = [] + for item in self.doc.mr_items: + if item.quantity == item.requested_qty: + continue + self._add_item_to_material_request(item, material_request_map, material_request_list) + + self._submit_material_requests(material_request_list) + + def _add_item_to_material_request(self, item, material_request_map, material_request_list): + item_doc = frappe.get_cached_doc("Item", item.item_code) + material_request_type = item.material_request_type or item_doc.default_material_request_type + + # key for Sales Order:Material Request Type:Customer + key = "{}:{}:{}".format(item.sales_order, material_request_type, "") + if key not in material_request_map: + material_request_map[key] = self._new_material_request(material_request_type) + material_request_list.append(material_request_map[key]) + + schedule_date = item.schedule_date or add_days(nowdate(), cint(item_doc.lead_time_days)) + row = self._material_request_item(item, material_request_type, schedule_date) + material_request_map[key].append("items", row) + + def _new_material_request(self, material_request_type): + mr = frappe.new_doc("Material Request") + mr.update( + { + "transaction_date": nowdate(), + "status": "Draft", + "company": self.doc.company, + "material_request_type": material_request_type, + } + ) + return mr + + def _material_request_item(self, item, material_request_type, schedule_date): + from_warehouse = item.from_warehouse if material_request_type == "Material Transfer" else None + # a group warehouse cannot receive stock; it must never reach a Material Request line + if item.warehouse and frappe.get_cached_value("Warehouse", item.warehouse, "is_group"): + frappe.throw( + _("Cannot create Material Request for item {0} in group warehouse {1}.").format( + frappe.bold(item.item_code), frappe.bold(item.warehouse) + ) + ) + project = ( + frappe.db.get_value("Sales Order", item.sales_order, "project") if item.sales_order else None + ) + return { + "item_code": item.item_code, + "from_warehouse": from_warehouse, + "qty": item.quantity - item.requested_qty, + "uom": item.uom, + "schedule_date": schedule_date, + "warehouse": item.warehouse, + "sales_order": item.sales_order, + "production_plan": self.doc.name, + "material_request_plan_item": item.name, + "project": project, + } + + def _submit_material_requests(self, material_request_list): + for material_request in material_request_list: + material_request.flags.ignore_permissions = 1 + material_request.run_method("set_missing_values") + material_request.save() + if self.doc.get("submit_material_request"): + material_request.submit() + + frappe.flags.mute_messages = False + if not material_request_list: + msgprint(_("No material request created")) + return + + links = [get_link_to_form("Material Request", m.name) for m in material_request_list] + msgprint(_("{0} created").format(comma_and(links))) + + +@frappe.whitelist() +def get_items_for_material_requests( + doc: str | dict | Document, + warehouses: str | list | None = None, + get_parent_warehouse_data: bool | int | None = None, +): + frappe.has_permission("Production Plan", "read", throw=True) + + doc = _normalize_mr_doc(doc) + _validate_group_warehouse_target(doc) + warehouses = _filter_warehouses(doc, warehouses, get_parent_warehouse_data) + doc["mr_items"] = [] + + po_items = _collect_po_items(doc) + _validate_po_items(po_items) + + ignore_ordered_qty = _effective_ignore_ordered_qty(doc, po_items) + so_item_details = _collect_item_details(doc, po_items) + + mr_items = _build_mr_items(doc, so_item_details, ignore_ordered_qty) + mr_items = _apply_other_locations( + doc, mr_items, warehouses, ignore_ordered_qty, get_parent_warehouse_data + ) + + if not mr_items: + _warn_no_mr_items(doc) + return mr_items + + +def _normalize_mr_doc(doc): + doc = frappe._dict(frappe.parse_json(doc)) + return doc + + +def _validate_group_warehouse_target(doc): + # the group only scopes availability; raw materials still need a concrete + # receiving warehouse, so for_warehouse is required once we generate items. + if doc.get("raw_material_group_warehouse") and not doc.get("for_warehouse"): + frappe.throw( + _("{0} is required to get raw materials when {1} is set.").format( + frappe.bold(_("For Warehouse")), frappe.bold(_("Raw Material Group Warehouse")) + ) + ) + + +def _filter_warehouses(doc, warehouses, get_parent_warehouse_data): + if not warehouses: + return warehouses + + warehouses = list(set(get_warehouse_list(warehouses))) + for_warehouse = doc.get("for_warehouse") + if for_warehouse and not get_parent_warehouse_data and for_warehouse in warehouses: + warehouses.remove(for_warehouse) + return warehouses + + +def _collect_po_items(doc): + po_items = doc.get("po_items") if doc.get("po_items") else doc.get("items") + for sa_row in doc.get("sub_assembly_items") or []: + sa_row = frappe._dict(sa_row) + if sa_row.type_of_manufacturing != "Material Request": + continue + po_items.append( + frappe._dict( + { + "item_code": sa_row.production_item, + "required_qty": sa_row.qty, + "include_exploded_items": 0, + } + ) + ) + return po_items + + +def _validate_po_items(po_items): + if not po_items or not [row.get("item_code") for row in po_items if row.get("item_code")]: + frappe.throw( + _("Items to Manufacture are required to pull the Raw Materials associated with it."), + title=_("Items Required"), + ) + + +def _effective_ignore_ordered_qty(doc, po_items): + if doc.get("ignore_existing_ordered_qty"): + return doc.get("ignore_existing_ordered_qty") + return any(data.get("ignore_existing_ordered_qty") for data in po_items) + + +def _build_sub_assembly_map(doc): + if not (doc.get("skip_available_sub_assembly_item") and doc.get("sub_assembly_items")): + return {} + + sub_assembly_items = defaultdict(int) + for d in doc.get("sub_assembly_items"): + key = (d.get("production_item"), d.get("bom_no"), d.get("type_of_manufacturing")) + sub_assembly_items[key] += d.get("qty") + return {k[:2]: v for k, v in sub_assembly_items.items()} + + +def _collect_item_details(doc, po_items): + company = doc.get("company") + sub_assembly_items = _build_sub_assembly_map(doc) + existing_sub_assembly_items = set() + so_item_details = frappe._dict() + qty_precision = frappe.get_precision("Material Request Plan Item", "quantity") + + for data in po_items: + if not data.get("include_exploded_items") and doc.get("sub_assembly_items"): + data["include_exploded_items"] = 1 + item_details = _item_details_for_row( + doc, data, company, sub_assembly_items, existing_sub_assembly_items + ) + _accumulate_so_items(so_item_details, data.get("sales_order"), item_details, qty_precision) + return so_item_details + + +def _item_details_for_row(doc, data, company, sub_assembly_items, existing_sub_assembly_items): + planned_qty = data.get("required_qty") or data.get("planned_qty") + if data.get("bom") or data.get("bom_no"): + return _bom_item_details( + doc, data, company, planned_qty, sub_assembly_items, existing_sub_assembly_items + ) + if data.get("item_code"): + return _plain_item_details(doc, data, planned_qty) + return {} + + +def _bom_item_details(doc, data, company, planned_qty, sub_assembly_items, existing_sub_assembly_items): + bom_no, include_non_stock_items, include_subcontracted_items = _bom_explosion_flags(doc, data) + if not planned_qty: + frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get("idx"))) + if not bom_no: + return {} + return _explode_bom_items( + doc, + data, + company, + bom_no, + planned_qty, + include_non_stock_items, + include_subcontracted_items, + sub_assembly_items, + existing_sub_assembly_items, + ) + + +def _bom_explosion_flags(doc, data): + if data.get("required_qty"): + include_subcontracted_items = 1 if data.get("include_exploded_items") else 0 + return data.get("bom"), 1, include_subcontracted_items + return data.get("bom_no"), doc.get("include_non_stock_items"), doc.get("include_subcontracted_items") + + +def _explode_bom_items( + doc, + data, + company, + bom_no, + planned_qty, + include_non_stock_items, + include_subcontracted_items, + sub_assembly_items, + existing_sub_assembly_items, +): + item_details = {} + if ( + data.get("include_exploded_items") + and doc.get("skip_available_sub_assembly_item") + and doc.get("sub_assembly_items") + ): + return get_raw_materials_of_sub_assembly_items( + existing_sub_assembly_items, + item_details, + company, + bom_no, + include_non_stock_items, + sub_assembly_items, + planned_qty=planned_qty, + ) + if data.get("include_exploded_items") and include_subcontracted_items: + return get_exploded_items( + item_details, company, bom_no, include_non_stock_items, planned_qty=planned_qty, doc=doc + ) + return get_subitems( + doc, + data, + item_details, + bom_no, + company, + include_non_stock_items, + include_subcontracted_items, + 1, + planned_qty=planned_qty, + ) + + +def _plain_item_details(doc, data, planned_qty): + item_master = frappe.get_doc("Item", data["item_code"]).as_dict() + purchase_uom = item_master.purchase_uom or item_master.stock_uom + conversion_factor = ( + get_uom_conversion_factor(item_master.name, purchase_uom) if item_master.purchase_uom else 1.0 + ) + return { + item_master.item_code: frappe._dict( + { + "item_name": item_master.item_name, + "default_bom": doc.bom, + "purchase_uom": purchase_uom, + "default_warehouse": item_master.default_warehouse, + "min_order_qty": item_master.min_order_qty, + "default_material_request_type": item_master.default_material_request_type, + "qty": planned_qty or 1, + "is_sub_contracted": item_master.is_sub_contracted_item, + "item_code": item_master.name, + "description": item_master.description, + "stock_uom": item_master.stock_uom, + "conversion_factor": conversion_factor, + "safety_stock": item_master.safety_stock, + } + ) + } + + +def _accumulate_so_items(so_item_details, sales_order, item_details, qty_precision): + for key, details in item_details.items(): + details.qty = flt(details.qty, qty_precision) + so_item_details.setdefault(sales_order, frappe._dict()) + if key in so_item_details[sales_order]: + existing = so_item_details[sales_order][key] + existing["qty"] = existing.get("qty", 0) + flt(details.qty) + else: + so_item_details[sales_order][key] = details + + +def _build_mr_items(doc, so_item_details, ignore_ordered_qty): + mr_items = [] + consumed_qty = defaultdict(float) + # raw_material_group_warehouse (optional, group) only widens the availability + # scope to its child warehouses; material is still received into for_warehouse. + target_warehouse = doc.get("for_warehouse") + scope_warehouse = doc.get("raw_material_group_warehouse") or target_warehouse + company = doc.get("company") + include_safety_stock = doc.get("include_safety_stock") + + for sales_order, item_dict in so_item_details.items(): + for details in item_dict.values(): + fallback = details.get("source_warehouse") or details.get("default_warehouse") + scope_warehouse = scope_warehouse or fallback + target_warehouse = target_warehouse or fallback + row = _mr_item_for_details( + doc, + details, + sales_order, + company, + ignore_ordered_qty, + include_safety_stock, + scope_warehouse, + target_warehouse, + consumed_qty, + ) + if row: + mr_items.append(row) + return mr_items + + +def _mr_item_for_details( + doc, + details, + sales_order, + company, + ignore_ordered_qty, + include_safety_stock, + warehouse, + target_warehouse, + consumed_qty, +): + # get_bin_details scopes to the warehouse's descendants, returning one row per + # child warehouse; sum them so a group warehouse reflects combined child stock. + bin_dict = _aggregate_bin_details(get_bin_details(details, doc.company, warehouse)) + if details.qty <= 0: + return None + return get_material_request_items( + doc, + details, + sales_order, + company, + ignore_ordered_qty, + include_safety_stock, + warehouse, + target_warehouse, + bin_dict, + consumed_qty, + ) + + +def _aggregate_bin_details(bin_list): + qty_fields = ( + "projected_qty", + "actual_qty", + "ordered_qty", + "reserved_qty_for_production", + "planned_qty", + ) + aggregated = {field: 0 for field in qty_fields} + for row in bin_list or []: + for field in qty_fields: + aggregated[field] += flt(row.get(field)) + return aggregated + + +def _apply_other_locations(doc, mr_items, warehouses, ignore_ordered_qty, get_parent_warehouse_data): + if not ((ignore_ordered_qty or get_parent_warehouse_data) and warehouses): + return mr_items + + new_mr_items = [] + for item in mr_items: + get_materials_from_other_locations( + item, + warehouses, + new_mr_items, + doc.get("company"), + consider_minimum_order_qty=doc.get("consider_minimum_order_qty"), + ) + return new_mr_items + + +def _warn_no_mr_items(doc): + to_enable = frappe.bold(frappe.get_meta("Production Plan").get_field("ignore_existing_ordered_qty").label) + warehouse = frappe.bold(doc.get("for_warehouse")) + message = ( + _( + "As there are sufficient raw materials, Material Request is not required for Warehouse {0}." + ).format(warehouse) + + "

" + ) + message += _("If you still want to proceed, please enable {0}.").format(to_enable) + frappe.msgprint(message, title=_("Note")) + + +def get_material_request_items( + doc, + row, + sales_order, + company, + ignore_existing_ordered_qty, + include_safety_stock, + warehouse, + target_warehouse, + bin_dict, + consumed_qty, +): + required_qty = _required_qty_for_mr( + doc, row, ignore_existing_ordered_qty, warehouse, bin_dict, consumed_qty, include_safety_stock + ) + item_group_defaults = get_item_group_defaults(row.item_code, company) + conversion_factor = _mr_purchase_conversion_factor(row) + return _material_request_item_row( + row, sales_order, target_warehouse, bin_dict, required_qty, conversion_factor, item_group_defaults + ) + + +def _required_qty_for_mr( + doc, row, ignore_existing_ordered_qty, warehouse, bin_dict, consumed_qty, include_safety_stock +): + safety_stock = flt(row["safety_stock"]) if include_safety_stock else 0 + qty = flt(row.get("qty")) + + if not ignore_existing_ordered_qty or bin_dict.get("projected_qty", 0) < 0: + required_qty = _apply_minimum_order_qty(doc, row, qty + safety_stock) + return _adjust_required_qty_for_uom(row, required_qty) + + key = (row.get("item_code"), warehouse) + available_qty = flt(bin_dict.get("projected_qty", 0)) - consumed_qty[key] + required_qty = _apply_minimum_order_qty(doc, row, max(0, qty - (available_qty - safety_stock))) + required_qty = _adjust_required_qty_for_uom(row, required_qty) + consumed_qty[key] += qty - required_qty + return required_qty + + +def _apply_minimum_order_qty(doc, row, required_qty): + if doc.get("consider_minimum_order_qty") and 0 < required_qty < row["min_order_qty"]: + return row["min_order_qty"] + return required_qty + + +def _adjust_required_qty_for_uom(row, required_qty): + if not row["purchase_uom"]: + row["purchase_uom"] = row["stock_uom"] + + if row["purchase_uom"] != row["stock_uom"]: + if not (row["conversion_factor"] or frappe.flags.show_qty_in_stock_uom): + frappe.throw( + _("UOM Conversion factor ({0} -> {1}) not found for item: {2}").format( + row["purchase_uom"], row["stock_uom"], row.item_code + ) + ) + + if frappe.db.get_value("UOM", row["purchase_uom"], "must_be_whole_number"): + required_qty = ceil(required_qty) + return required_qty + + +def _mr_purchase_conversion_factor(row): + item_details = frappe.get_cached_value("Item", row.item_code, ["purchase_uom", "stock_uom"], as_dict=1) + if ( + row.get("default_material_request_type") == "Purchase" + and item_details.purchase_uom + and item_details.purchase_uom != item_details.stock_uom + ): + return get_conversion_factor(row.item_code, item_details.purchase_uom).get("conversion_factor") or 1.0 + return 1.0 + + +def _material_request_item_row( + row, sales_order, warehouse, bin_dict, required_qty, conversion_factor, item_group_defaults +): + warehouse = ( + warehouse + or row.get("source_warehouse") + or row.get("default_warehouse") + or item_group_defaults.get("default_warehouse") + ) + return { + "item_code": row.item_code, + "item_name": row.item_name, + "quantity": required_qty / conversion_factor, + "conversion_factor": conversion_factor, + "required_bom_qty": row.get("qty"), + "stock_uom": row.get("stock_uom"), + "warehouse": warehouse, + "safety_stock": row.safety_stock, + "actual_qty": bin_dict.get("actual_qty", 0), + "projected_qty": bin_dict.get("projected_qty", 0), + "ordered_qty": bin_dict.get("ordered_qty", 0), + "reserved_qty_for_production": bin_dict.get("reserved_qty_for_production", 0), + "min_order_qty": row["min_order_qty"], + "material_request_type": row.get("default_material_request_type"), + "sales_order": sales_order, + "description": row.get("description"), + "uom": row.get("purchase_uom") or row.get("stock_uom"), + "main_item_code": row.get("main_bom_item"), + "from_bom": row.get("main_bom"), + } + + +def get_materials_from_other_locations( + item, warehouses, new_mr_items, company, consider_minimum_order_qty=False +): + from erpnext.stock.doctype.pick_list.pick_list import get_available_item_locations + + locations = get_available_item_locations( + item.get("item_code"), + warehouses, + item.get("quantity") * item.get("conversion_factor"), + company, + ignore_validation=True, + ) + + required_qty = item.get("quantity") + if item.get("conversion_factor") and item.get("purchase_uom") != item.get("stock_uom"): + # Convert qty to stock UOM + required_qty = required_qty * item.get("conversion_factor") + + required_qty = _transfer_from_locations(item, locations, new_mr_items, required_qty) + _add_remaining_purchase_request(item, new_mr_items, required_qty, consider_minimum_order_qty) + + +def _transfer_from_locations(item, locations, new_mr_items, required_qty): + # get available material by transferring to production warehouse + for d in locations: + if required_qty <= 0: + return required_qty + + new_dict = copy.deepcopy(item) + quantity = required_qty if d.get("qty") > required_qty else d.get("qty") + new_dict.update( + { + "quantity": quantity, + "material_request_type": "Material Transfer", + "uom": new_dict.get("stock_uom"), # internal transfer should be in stock UOM + "from_warehouse": d.get("warehouse"), + "conversion_factor": 1.0, + } + ) + required_qty -= quantity + new_mr_items.append(new_dict) + return required_qty + + +def _add_remaining_purchase_request(item, new_mr_items, required_qty, consider_minimum_order_qty=False): + # raise purchase request for remaining qty + precision = frappe.get_precision("Material Request Plan Item", "quantity") + if flt(required_qty, precision) <= 0: + return + + if consider_minimum_order_qty: + required_qty = max(required_qty, flt(item.get("min_order_qty"))) + + purchase_uom = frappe.db.get_value("Item", item.get("item_code"), "purchase_uom") + if frappe.db.get_value("UOM", purchase_uom, "must_be_whole_number"): + required_qty = ceil(required_qty) + + item["quantity"] = required_qty / item.get("conversion_factor") + new_mr_items.append(item) + + +@frappe.whitelist() +def download_raw_materials(doc: str | dict | Document, warehouses: str | list | None = None): + frappe.has_permission("Production Plan", "read", throw=True) + + doc = _normalize_mr_doc(doc) + item_list = [_raw_materials_header()] + + doc.warehouse = None + frappe.flags.show_qty_in_stock_uom = 1 + items = get_items_for_material_requests(doc, warehouses=warehouses, get_parent_warehouse_data=True) + + _build_download_rows(doc, items, item_list) + build_csv_response(item_list, doc.name) + + +def _raw_materials_header(): + return [ + "Item Code", + "Item Name", + "Description", + "Stock UOM", + "Warehouse", + "Required Qty as per BOM", + "Projected Qty", + "Available Qty In Hand", + "Ordered Qty", + "Planned Qty", + "Reserved Qty for Production", + "Safety Stock", + "Required Qty", + ] + + +def _build_download_rows(doc, items, item_list): + duplicate_item_wh_list = frappe._dict() + for d in items: + key = (d.get("item_code"), d.get("warehouse")) + if key in duplicate_item_wh_list: + duplicate_item_wh_list[key][12] += d.get("quantity") + continue + + rm_data = _raw_material_row(d) + duplicate_item_wh_list[key] = rm_data + item_list.append(rm_data) + + if not doc.get("for_warehouse"): + _append_other_warehouse_bins(item_list, d, doc) + + +def _raw_material_row(d): + return [ + d.get("item_code"), + d.get("item_name"), + d.get("description"), + d.get("stock_uom"), + d.get("warehouse"), + d.get("required_bom_qty"), + d.get("projected_qty"), + d.get("actual_qty"), + d.get("ordered_qty"), + d.get("planned_qty"), + d.get("reserved_qty_for_production"), + d.get("safety_stock"), + d.get("quantity"), + ] + + +def _append_other_warehouse_bins(item_list, d, doc): + row = {"item_code": d.get("item_code")} + for bin_dict in get_bin_details(row, doc.company, all_warehouse=True): + if d.get("warehouse") == bin_dict.get("warehouse"): + continue + + item_list.append( + [ + "", + "", + "", + bin_dict.get("warehouse"), + "", + bin_dict.get("projected_qty", 0), + bin_dict.get("actual_qty", 0), + bin_dict.get("ordered_qty", 0), + bin_dict.get("reserved_qty_for_production", 0), + ] + ) From 2d056aee3d919b7a2f51f6ec4f23597491f7b6e1 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 6 Aug 2026 20:48:42 +0530 Subject: [PATCH 013/134] fix: round production plan mr_items quantity to field precision The stock-UOM qty is rounded in _accumulate_so_items, but the purchase UOM conversion divided it by the conversion factor without re-rounding, storing values like 5738748.300863984 in mr_items.quantity. The raw value flowed into Material Request qty and the raw materials CSV, and make_material_request compares quantity to requested_qty with exact float equality, so any rounding downstream left dust quantities. (cherry picked from commit ffc515f04618caf294892f579be4091682266cea) --- .../doctype/production_plan/services/material_request.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/services/material_request.py b/erpnext/manufacturing/doctype/production_plan/services/material_request.py index c9073d5e648..ac689ba0a3f 100644 --- a/erpnext/manufacturing/doctype/production_plan/services/material_request.py +++ b/erpnext/manufacturing/doctype/production_plan/services/material_request.py @@ -559,10 +559,11 @@ def _material_request_item_row( or row.get("default_warehouse") or item_group_defaults.get("default_warehouse") ) + precision = frappe.get_precision("Material Request Plan Item", "quantity") return { "item_code": row.item_code, "item_name": row.item_name, - "quantity": required_qty / conversion_factor, + "quantity": flt(required_qty / conversion_factor, precision), "conversion_factor": conversion_factor, "required_bom_qty": row.get("qty"), "stock_uom": row.get("stock_uom"), @@ -639,7 +640,7 @@ def _add_remaining_purchase_request(item, new_mr_items, required_qty, consider_m if frappe.db.get_value("UOM", purchase_uom, "must_be_whole_number"): required_qty = ceil(required_qty) - item["quantity"] = required_qty / item.get("conversion_factor") + item["quantity"] = flt(required_qty / item.get("conversion_factor"), precision) new_mr_items.append(item) From 9f9cb5c3b62feb973236592650915f016ad23191 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 6 Aug 2026 20:49:42 +0530 Subject: [PATCH 014/134] test: mr_items quantity is rounded to field precision (cherry picked from commit f5157bf3c42f6b3550c6b4eda97603dba36d60a3) --- .../production_plan/test_production_plan.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 1aeb36fe536..2dac3f58f4a 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1366,6 +1366,29 @@ class TestProductionPlan(ERPNextTestSuite): self.assertEqual(row.uom, "Nos") self.assertEqual(row.qty, 1) + def test_material_request_item_quantity_rounded_to_precision(self): + from erpnext.stock.doctype.item.test_item import make_item + + fg_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name + bom_item = make_item( + properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1", "purchase_uom": "Nos"} + ).name + + if not frappe.db.exists("UOM Conversion Detail", {"parent": bom_item, "uom": "Nos"}): + doc = frappe.get_doc("Item", bom_item) + doc.append("uoms", {"uom": "Nos", "conversion_factor": 3}) + doc.save() + + make_bom(item=fg_item, raw_materials=[bom_item], source_warehouse="_Test Warehouse - _TC") + + pln = create_production_plan( + item_code=fg_item, planned_qty=10, ignore_existing_ordered_qty=1, stock_uom="_Test UOM 1" + ) + + precision = frappe.get_precision("Material Request Plan Item", "quantity") + self.assertEqual(len(pln.mr_items), 1) + self.assertEqual(pln.mr_items[0].quantity, flt(10 / 3, precision)) + def test_material_request_for_sub_assembly_items(self): from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom From 9f8aa3cf1b41e30cc86ab043913e28bd41500b42 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 6 Aug 2026 20:59:04 +0530 Subject: [PATCH 015/134] test: remaining purchase qty is rounded to field precision Covers the _add_remaining_purchase_request path: partial stock in another warehouse is allocated as a transfer and the residual purchase qty goes through the second rounding site. (cherry picked from commit 75145cc72c9ed67921b53f4efca0e863bf7ff515) --- .../production_plan/test_production_plan.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 2dac3f58f4a..4c72c68f69f 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -2102,6 +2102,40 @@ class TestProductionPlan(ERPNextTestSuite): self.assertEqual(row.get("uom"), "Nos") self.assertEqual(row.get("conversion_factor"), 10.0) + def test_remaining_purchase_qty_rounded_to_precision(self): + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + fg_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name + bom_item = make_item( + properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1", "purchase_uom": "Nos"} + ).name + + store_warehouse = create_warehouse("Store Warehouse", company="_Test Company") + rm_warehouse = create_warehouse("RM Warehouse", company="_Test Company") + + make_stock_entry(item_code=bom_item, qty=4, target=store_warehouse, rate=100) + + if not frappe.db.exists("UOM Conversion Detail", {"parent": bom_item, "uom": "Nos"}): + doc = frappe.get_doc("Item", bom_item) + doc.append("uoms", {"uom": "Nos", "conversion_factor": 3}) + doc.save() + + make_bom(item=fg_item, raw_materials=[bom_item], source_warehouse="_Test Warehouse - _TC") + + pln = create_production_plan( + item_code=fg_item, planned_qty=30, stock_uom="_Test UOM 1", do_not_submit=1 + ) + pln.for_warehouse = rm_warehouse + pln.ignore_existing_ordered_qty = 1 + items = get_items_for_material_requests(pln.as_dict(), warehouses=[{"warehouse": store_warehouse}]) + + rows_by_type = {row.get("material_request_type"): row for row in items} + self.assertEqual(rows_by_type["Material Transfer"].get("quantity"), 4) + + precision = frappe.get_precision("Material Request Plan Item", "quantity") + self.assertEqual(rows_by_type["Purchase"].get("quantity"), flt(26 / 3, precision)) + def test_unreserve_qty_on_closing_of_pp(self): from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.utils import get_or_make_bin From 460fe9af3e7660c763ccfcde375e4c973ea411f4 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 6 Aug 2026 21:32:24 +0530 Subject: [PATCH 016/134] chore: resolve conflict --- .../production_plan/production_plan.py | 7 +- .../services/material_request.py | 732 ------------------ 2 files changed, 3 insertions(+), 736 deletions(-) delete mode 100644 erpnext/manufacturing/doctype/production_plan/services/material_request.py diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 97072a8642f..a9aaedc8894 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1478,8 +1478,6 @@ def get_material_request_items( ) ) - required_qty = required_qty / row["conversion_factor"] - if frappe.db.get_value("UOM", row["purchase_uom"], "must_be_whole_number"): required_qty = ceil(required_qty) @@ -1498,10 +1496,11 @@ def get_material_request_items( get_conversion_factor(row.item_code, item_details.purchase_uom).get("conversion_factor") or 1.0 ) + precision = frappe.get_precision("Material Request Plan Item", "quantity") return { "item_code": row.item_code, "item_name": row.item_name, - "quantity": required_qty / conversion_factor, + "quantity": flt(required_qty / conversion_factor, precision), "conversion_factor": conversion_factor, "required_bom_qty": row.get("qty"), "stock_uom": row.get("stock_uom"), @@ -1910,7 +1909,7 @@ def get_materials_from_other_locations( if frappe.db.get_value("UOM", purchase_uom, "must_be_whole_number"): required_qty = ceil(required_qty) - item["quantity"] = required_qty / item.get("conversion_factor") + item["quantity"] = flt(required_qty / item.get("conversion_factor"), precision) new_mr_items.append(item) diff --git a/erpnext/manufacturing/doctype/production_plan/services/material_request.py b/erpnext/manufacturing/doctype/production_plan/services/material_request.py deleted file mode 100644 index ac689ba0a3f..00000000000 --- a/erpnext/manufacturing/doctype/production_plan/services/material_request.py +++ /dev/null @@ -1,732 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -"""Material Request planning and creation for a Production Plan. - -Consolidates the former ``material_planning``, ``material_request_items`` and -``material_request_helpers`` modules. Also re-exports the planning helpers so -existing imports of ``...services.material_planning`` keep working through here. -""" - -import copy -import json -from collections import defaultdict - -import frappe -from frappe import _, msgprint -from frappe.model.document import Document -from frappe.utils import add_days, ceil, cint, comma_and, flt, get_link_to_form, nowdate -from frappe.utils.csvutils import build_csv_response - -from erpnext.manufacturing.doctype.production_plan.services.bom_explosion import ( - get_exploded_items, - get_subitems, -) -from erpnext.manufacturing.doctype.production_plan.services.planning_queries import ( - get_bin_details, - get_item_data, - get_sales_orders, - get_uom_conversion_factor, - get_warehouse_list, - set_default_warehouses, -) -from erpnext.manufacturing.doctype.production_plan.services.sub_assembly_queries import ( - get_raw_materials_of_sub_assembly_items, - get_sub_assembly_items, -) -from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults -from erpnext.stock.get_item_details import get_conversion_factor - - -class MaterialRequestService: - def __init__(self, doc): - self.doc = doc - - def validate_mr_subcontracted(self): - for row in self.doc.mr_items: - if row.material_request_type != "Subcontracting": - continue - if not frappe.db.get_value("Item", row.item_code, "is_sub_contracted_item"): - frappe.throw( - _("Item {0} is not a subcontracted item").format(row.item_code), - title=_("Invalid Item"), - ) - - def make_material_request(self): - """Create Material Requests grouped by Sales Order and Material Request Type""" - self.validate_mr_subcontracted() - - if all(item.requested_qty == item.quantity for item in self.doc.mr_items): - msgprint(_("All items are already requested")) - return - - material_request_map = {} - material_request_list = [] - for item in self.doc.mr_items: - if item.quantity == item.requested_qty: - continue - self._add_item_to_material_request(item, material_request_map, material_request_list) - - self._submit_material_requests(material_request_list) - - def _add_item_to_material_request(self, item, material_request_map, material_request_list): - item_doc = frappe.get_cached_doc("Item", item.item_code) - material_request_type = item.material_request_type or item_doc.default_material_request_type - - # key for Sales Order:Material Request Type:Customer - key = "{}:{}:{}".format(item.sales_order, material_request_type, "") - if key not in material_request_map: - material_request_map[key] = self._new_material_request(material_request_type) - material_request_list.append(material_request_map[key]) - - schedule_date = item.schedule_date or add_days(nowdate(), cint(item_doc.lead_time_days)) - row = self._material_request_item(item, material_request_type, schedule_date) - material_request_map[key].append("items", row) - - def _new_material_request(self, material_request_type): - mr = frappe.new_doc("Material Request") - mr.update( - { - "transaction_date": nowdate(), - "status": "Draft", - "company": self.doc.company, - "material_request_type": material_request_type, - } - ) - return mr - - def _material_request_item(self, item, material_request_type, schedule_date): - from_warehouse = item.from_warehouse if material_request_type == "Material Transfer" else None - # a group warehouse cannot receive stock; it must never reach a Material Request line - if item.warehouse and frappe.get_cached_value("Warehouse", item.warehouse, "is_group"): - frappe.throw( - _("Cannot create Material Request for item {0} in group warehouse {1}.").format( - frappe.bold(item.item_code), frappe.bold(item.warehouse) - ) - ) - project = ( - frappe.db.get_value("Sales Order", item.sales_order, "project") if item.sales_order else None - ) - return { - "item_code": item.item_code, - "from_warehouse": from_warehouse, - "qty": item.quantity - item.requested_qty, - "uom": item.uom, - "schedule_date": schedule_date, - "warehouse": item.warehouse, - "sales_order": item.sales_order, - "production_plan": self.doc.name, - "material_request_plan_item": item.name, - "project": project, - } - - def _submit_material_requests(self, material_request_list): - for material_request in material_request_list: - material_request.flags.ignore_permissions = 1 - material_request.run_method("set_missing_values") - material_request.save() - if self.doc.get("submit_material_request"): - material_request.submit() - - frappe.flags.mute_messages = False - if not material_request_list: - msgprint(_("No material request created")) - return - - links = [get_link_to_form("Material Request", m.name) for m in material_request_list] - msgprint(_("{0} created").format(comma_and(links))) - - -@frappe.whitelist() -def get_items_for_material_requests( - doc: str | dict | Document, - warehouses: str | list | None = None, - get_parent_warehouse_data: bool | int | None = None, -): - frappe.has_permission("Production Plan", "read", throw=True) - - doc = _normalize_mr_doc(doc) - _validate_group_warehouse_target(doc) - warehouses = _filter_warehouses(doc, warehouses, get_parent_warehouse_data) - doc["mr_items"] = [] - - po_items = _collect_po_items(doc) - _validate_po_items(po_items) - - ignore_ordered_qty = _effective_ignore_ordered_qty(doc, po_items) - so_item_details = _collect_item_details(doc, po_items) - - mr_items = _build_mr_items(doc, so_item_details, ignore_ordered_qty) - mr_items = _apply_other_locations( - doc, mr_items, warehouses, ignore_ordered_qty, get_parent_warehouse_data - ) - - if not mr_items: - _warn_no_mr_items(doc) - return mr_items - - -def _normalize_mr_doc(doc): - doc = frappe._dict(frappe.parse_json(doc)) - return doc - - -def _validate_group_warehouse_target(doc): - # the group only scopes availability; raw materials still need a concrete - # receiving warehouse, so for_warehouse is required once we generate items. - if doc.get("raw_material_group_warehouse") and not doc.get("for_warehouse"): - frappe.throw( - _("{0} is required to get raw materials when {1} is set.").format( - frappe.bold(_("For Warehouse")), frappe.bold(_("Raw Material Group Warehouse")) - ) - ) - - -def _filter_warehouses(doc, warehouses, get_parent_warehouse_data): - if not warehouses: - return warehouses - - warehouses = list(set(get_warehouse_list(warehouses))) - for_warehouse = doc.get("for_warehouse") - if for_warehouse and not get_parent_warehouse_data and for_warehouse in warehouses: - warehouses.remove(for_warehouse) - return warehouses - - -def _collect_po_items(doc): - po_items = doc.get("po_items") if doc.get("po_items") else doc.get("items") - for sa_row in doc.get("sub_assembly_items") or []: - sa_row = frappe._dict(sa_row) - if sa_row.type_of_manufacturing != "Material Request": - continue - po_items.append( - frappe._dict( - { - "item_code": sa_row.production_item, - "required_qty": sa_row.qty, - "include_exploded_items": 0, - } - ) - ) - return po_items - - -def _validate_po_items(po_items): - if not po_items or not [row.get("item_code") for row in po_items if row.get("item_code")]: - frappe.throw( - _("Items to Manufacture are required to pull the Raw Materials associated with it."), - title=_("Items Required"), - ) - - -def _effective_ignore_ordered_qty(doc, po_items): - if doc.get("ignore_existing_ordered_qty"): - return doc.get("ignore_existing_ordered_qty") - return any(data.get("ignore_existing_ordered_qty") for data in po_items) - - -def _build_sub_assembly_map(doc): - if not (doc.get("skip_available_sub_assembly_item") and doc.get("sub_assembly_items")): - return {} - - sub_assembly_items = defaultdict(int) - for d in doc.get("sub_assembly_items"): - key = (d.get("production_item"), d.get("bom_no"), d.get("type_of_manufacturing")) - sub_assembly_items[key] += d.get("qty") - return {k[:2]: v for k, v in sub_assembly_items.items()} - - -def _collect_item_details(doc, po_items): - company = doc.get("company") - sub_assembly_items = _build_sub_assembly_map(doc) - existing_sub_assembly_items = set() - so_item_details = frappe._dict() - qty_precision = frappe.get_precision("Material Request Plan Item", "quantity") - - for data in po_items: - if not data.get("include_exploded_items") and doc.get("sub_assembly_items"): - data["include_exploded_items"] = 1 - item_details = _item_details_for_row( - doc, data, company, sub_assembly_items, existing_sub_assembly_items - ) - _accumulate_so_items(so_item_details, data.get("sales_order"), item_details, qty_precision) - return so_item_details - - -def _item_details_for_row(doc, data, company, sub_assembly_items, existing_sub_assembly_items): - planned_qty = data.get("required_qty") or data.get("planned_qty") - if data.get("bom") or data.get("bom_no"): - return _bom_item_details( - doc, data, company, planned_qty, sub_assembly_items, existing_sub_assembly_items - ) - if data.get("item_code"): - return _plain_item_details(doc, data, planned_qty) - return {} - - -def _bom_item_details(doc, data, company, planned_qty, sub_assembly_items, existing_sub_assembly_items): - bom_no, include_non_stock_items, include_subcontracted_items = _bom_explosion_flags(doc, data) - if not planned_qty: - frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get("idx"))) - if not bom_no: - return {} - return _explode_bom_items( - doc, - data, - company, - bom_no, - planned_qty, - include_non_stock_items, - include_subcontracted_items, - sub_assembly_items, - existing_sub_assembly_items, - ) - - -def _bom_explosion_flags(doc, data): - if data.get("required_qty"): - include_subcontracted_items = 1 if data.get("include_exploded_items") else 0 - return data.get("bom"), 1, include_subcontracted_items - return data.get("bom_no"), doc.get("include_non_stock_items"), doc.get("include_subcontracted_items") - - -def _explode_bom_items( - doc, - data, - company, - bom_no, - planned_qty, - include_non_stock_items, - include_subcontracted_items, - sub_assembly_items, - existing_sub_assembly_items, -): - item_details = {} - if ( - data.get("include_exploded_items") - and doc.get("skip_available_sub_assembly_item") - and doc.get("sub_assembly_items") - ): - return get_raw_materials_of_sub_assembly_items( - existing_sub_assembly_items, - item_details, - company, - bom_no, - include_non_stock_items, - sub_assembly_items, - planned_qty=planned_qty, - ) - if data.get("include_exploded_items") and include_subcontracted_items: - return get_exploded_items( - item_details, company, bom_no, include_non_stock_items, planned_qty=planned_qty, doc=doc - ) - return get_subitems( - doc, - data, - item_details, - bom_no, - company, - include_non_stock_items, - include_subcontracted_items, - 1, - planned_qty=planned_qty, - ) - - -def _plain_item_details(doc, data, planned_qty): - item_master = frappe.get_doc("Item", data["item_code"]).as_dict() - purchase_uom = item_master.purchase_uom or item_master.stock_uom - conversion_factor = ( - get_uom_conversion_factor(item_master.name, purchase_uom) if item_master.purchase_uom else 1.0 - ) - return { - item_master.item_code: frappe._dict( - { - "item_name": item_master.item_name, - "default_bom": doc.bom, - "purchase_uom": purchase_uom, - "default_warehouse": item_master.default_warehouse, - "min_order_qty": item_master.min_order_qty, - "default_material_request_type": item_master.default_material_request_type, - "qty": planned_qty or 1, - "is_sub_contracted": item_master.is_sub_contracted_item, - "item_code": item_master.name, - "description": item_master.description, - "stock_uom": item_master.stock_uom, - "conversion_factor": conversion_factor, - "safety_stock": item_master.safety_stock, - } - ) - } - - -def _accumulate_so_items(so_item_details, sales_order, item_details, qty_precision): - for key, details in item_details.items(): - details.qty = flt(details.qty, qty_precision) - so_item_details.setdefault(sales_order, frappe._dict()) - if key in so_item_details[sales_order]: - existing = so_item_details[sales_order][key] - existing["qty"] = existing.get("qty", 0) + flt(details.qty) - else: - so_item_details[sales_order][key] = details - - -def _build_mr_items(doc, so_item_details, ignore_ordered_qty): - mr_items = [] - consumed_qty = defaultdict(float) - # raw_material_group_warehouse (optional, group) only widens the availability - # scope to its child warehouses; material is still received into for_warehouse. - target_warehouse = doc.get("for_warehouse") - scope_warehouse = doc.get("raw_material_group_warehouse") or target_warehouse - company = doc.get("company") - include_safety_stock = doc.get("include_safety_stock") - - for sales_order, item_dict in so_item_details.items(): - for details in item_dict.values(): - fallback = details.get("source_warehouse") or details.get("default_warehouse") - scope_warehouse = scope_warehouse or fallback - target_warehouse = target_warehouse or fallback - row = _mr_item_for_details( - doc, - details, - sales_order, - company, - ignore_ordered_qty, - include_safety_stock, - scope_warehouse, - target_warehouse, - consumed_qty, - ) - if row: - mr_items.append(row) - return mr_items - - -def _mr_item_for_details( - doc, - details, - sales_order, - company, - ignore_ordered_qty, - include_safety_stock, - warehouse, - target_warehouse, - consumed_qty, -): - # get_bin_details scopes to the warehouse's descendants, returning one row per - # child warehouse; sum them so a group warehouse reflects combined child stock. - bin_dict = _aggregate_bin_details(get_bin_details(details, doc.company, warehouse)) - if details.qty <= 0: - return None - return get_material_request_items( - doc, - details, - sales_order, - company, - ignore_ordered_qty, - include_safety_stock, - warehouse, - target_warehouse, - bin_dict, - consumed_qty, - ) - - -def _aggregate_bin_details(bin_list): - qty_fields = ( - "projected_qty", - "actual_qty", - "ordered_qty", - "reserved_qty_for_production", - "planned_qty", - ) - aggregated = {field: 0 for field in qty_fields} - for row in bin_list or []: - for field in qty_fields: - aggregated[field] += flt(row.get(field)) - return aggregated - - -def _apply_other_locations(doc, mr_items, warehouses, ignore_ordered_qty, get_parent_warehouse_data): - if not ((ignore_ordered_qty or get_parent_warehouse_data) and warehouses): - return mr_items - - new_mr_items = [] - for item in mr_items: - get_materials_from_other_locations( - item, - warehouses, - new_mr_items, - doc.get("company"), - consider_minimum_order_qty=doc.get("consider_minimum_order_qty"), - ) - return new_mr_items - - -def _warn_no_mr_items(doc): - to_enable = frappe.bold(frappe.get_meta("Production Plan").get_field("ignore_existing_ordered_qty").label) - warehouse = frappe.bold(doc.get("for_warehouse")) - message = ( - _( - "As there are sufficient raw materials, Material Request is not required for Warehouse {0}." - ).format(warehouse) - + "

" - ) - message += _("If you still want to proceed, please enable {0}.").format(to_enable) - frappe.msgprint(message, title=_("Note")) - - -def get_material_request_items( - doc, - row, - sales_order, - company, - ignore_existing_ordered_qty, - include_safety_stock, - warehouse, - target_warehouse, - bin_dict, - consumed_qty, -): - required_qty = _required_qty_for_mr( - doc, row, ignore_existing_ordered_qty, warehouse, bin_dict, consumed_qty, include_safety_stock - ) - item_group_defaults = get_item_group_defaults(row.item_code, company) - conversion_factor = _mr_purchase_conversion_factor(row) - return _material_request_item_row( - row, sales_order, target_warehouse, bin_dict, required_qty, conversion_factor, item_group_defaults - ) - - -def _required_qty_for_mr( - doc, row, ignore_existing_ordered_qty, warehouse, bin_dict, consumed_qty, include_safety_stock -): - safety_stock = flt(row["safety_stock"]) if include_safety_stock else 0 - qty = flt(row.get("qty")) - - if not ignore_existing_ordered_qty or bin_dict.get("projected_qty", 0) < 0: - required_qty = _apply_minimum_order_qty(doc, row, qty + safety_stock) - return _adjust_required_qty_for_uom(row, required_qty) - - key = (row.get("item_code"), warehouse) - available_qty = flt(bin_dict.get("projected_qty", 0)) - consumed_qty[key] - required_qty = _apply_minimum_order_qty(doc, row, max(0, qty - (available_qty - safety_stock))) - required_qty = _adjust_required_qty_for_uom(row, required_qty) - consumed_qty[key] += qty - required_qty - return required_qty - - -def _apply_minimum_order_qty(doc, row, required_qty): - if doc.get("consider_minimum_order_qty") and 0 < required_qty < row["min_order_qty"]: - return row["min_order_qty"] - return required_qty - - -def _adjust_required_qty_for_uom(row, required_qty): - if not row["purchase_uom"]: - row["purchase_uom"] = row["stock_uom"] - - if row["purchase_uom"] != row["stock_uom"]: - if not (row["conversion_factor"] or frappe.flags.show_qty_in_stock_uom): - frappe.throw( - _("UOM Conversion factor ({0} -> {1}) not found for item: {2}").format( - row["purchase_uom"], row["stock_uom"], row.item_code - ) - ) - - if frappe.db.get_value("UOM", row["purchase_uom"], "must_be_whole_number"): - required_qty = ceil(required_qty) - return required_qty - - -def _mr_purchase_conversion_factor(row): - item_details = frappe.get_cached_value("Item", row.item_code, ["purchase_uom", "stock_uom"], as_dict=1) - if ( - row.get("default_material_request_type") == "Purchase" - and item_details.purchase_uom - and item_details.purchase_uom != item_details.stock_uom - ): - return get_conversion_factor(row.item_code, item_details.purchase_uom).get("conversion_factor") or 1.0 - return 1.0 - - -def _material_request_item_row( - row, sales_order, warehouse, bin_dict, required_qty, conversion_factor, item_group_defaults -): - warehouse = ( - warehouse - or row.get("source_warehouse") - or row.get("default_warehouse") - or item_group_defaults.get("default_warehouse") - ) - precision = frappe.get_precision("Material Request Plan Item", "quantity") - return { - "item_code": row.item_code, - "item_name": row.item_name, - "quantity": flt(required_qty / conversion_factor, precision), - "conversion_factor": conversion_factor, - "required_bom_qty": row.get("qty"), - "stock_uom": row.get("stock_uom"), - "warehouse": warehouse, - "safety_stock": row.safety_stock, - "actual_qty": bin_dict.get("actual_qty", 0), - "projected_qty": bin_dict.get("projected_qty", 0), - "ordered_qty": bin_dict.get("ordered_qty", 0), - "reserved_qty_for_production": bin_dict.get("reserved_qty_for_production", 0), - "min_order_qty": row["min_order_qty"], - "material_request_type": row.get("default_material_request_type"), - "sales_order": sales_order, - "description": row.get("description"), - "uom": row.get("purchase_uom") or row.get("stock_uom"), - "main_item_code": row.get("main_bom_item"), - "from_bom": row.get("main_bom"), - } - - -def get_materials_from_other_locations( - item, warehouses, new_mr_items, company, consider_minimum_order_qty=False -): - from erpnext.stock.doctype.pick_list.pick_list import get_available_item_locations - - locations = get_available_item_locations( - item.get("item_code"), - warehouses, - item.get("quantity") * item.get("conversion_factor"), - company, - ignore_validation=True, - ) - - required_qty = item.get("quantity") - if item.get("conversion_factor") and item.get("purchase_uom") != item.get("stock_uom"): - # Convert qty to stock UOM - required_qty = required_qty * item.get("conversion_factor") - - required_qty = _transfer_from_locations(item, locations, new_mr_items, required_qty) - _add_remaining_purchase_request(item, new_mr_items, required_qty, consider_minimum_order_qty) - - -def _transfer_from_locations(item, locations, new_mr_items, required_qty): - # get available material by transferring to production warehouse - for d in locations: - if required_qty <= 0: - return required_qty - - new_dict = copy.deepcopy(item) - quantity = required_qty if d.get("qty") > required_qty else d.get("qty") - new_dict.update( - { - "quantity": quantity, - "material_request_type": "Material Transfer", - "uom": new_dict.get("stock_uom"), # internal transfer should be in stock UOM - "from_warehouse": d.get("warehouse"), - "conversion_factor": 1.0, - } - ) - required_qty -= quantity - new_mr_items.append(new_dict) - return required_qty - - -def _add_remaining_purchase_request(item, new_mr_items, required_qty, consider_minimum_order_qty=False): - # raise purchase request for remaining qty - precision = frappe.get_precision("Material Request Plan Item", "quantity") - if flt(required_qty, precision) <= 0: - return - - if consider_minimum_order_qty: - required_qty = max(required_qty, flt(item.get("min_order_qty"))) - - purchase_uom = frappe.db.get_value("Item", item.get("item_code"), "purchase_uom") - if frappe.db.get_value("UOM", purchase_uom, "must_be_whole_number"): - required_qty = ceil(required_qty) - - item["quantity"] = flt(required_qty / item.get("conversion_factor"), precision) - new_mr_items.append(item) - - -@frappe.whitelist() -def download_raw_materials(doc: str | dict | Document, warehouses: str | list | None = None): - frappe.has_permission("Production Plan", "read", throw=True) - - doc = _normalize_mr_doc(doc) - item_list = [_raw_materials_header()] - - doc.warehouse = None - frappe.flags.show_qty_in_stock_uom = 1 - items = get_items_for_material_requests(doc, warehouses=warehouses, get_parent_warehouse_data=True) - - _build_download_rows(doc, items, item_list) - build_csv_response(item_list, doc.name) - - -def _raw_materials_header(): - return [ - "Item Code", - "Item Name", - "Description", - "Stock UOM", - "Warehouse", - "Required Qty as per BOM", - "Projected Qty", - "Available Qty In Hand", - "Ordered Qty", - "Planned Qty", - "Reserved Qty for Production", - "Safety Stock", - "Required Qty", - ] - - -def _build_download_rows(doc, items, item_list): - duplicate_item_wh_list = frappe._dict() - for d in items: - key = (d.get("item_code"), d.get("warehouse")) - if key in duplicate_item_wh_list: - duplicate_item_wh_list[key][12] += d.get("quantity") - continue - - rm_data = _raw_material_row(d) - duplicate_item_wh_list[key] = rm_data - item_list.append(rm_data) - - if not doc.get("for_warehouse"): - _append_other_warehouse_bins(item_list, d, doc) - - -def _raw_material_row(d): - return [ - d.get("item_code"), - d.get("item_name"), - d.get("description"), - d.get("stock_uom"), - d.get("warehouse"), - d.get("required_bom_qty"), - d.get("projected_qty"), - d.get("actual_qty"), - d.get("ordered_qty"), - d.get("planned_qty"), - d.get("reserved_qty_for_production"), - d.get("safety_stock"), - d.get("quantity"), - ] - - -def _append_other_warehouse_bins(item_list, d, doc): - row = {"item_code": d.get("item_code")} - for bin_dict in get_bin_details(row, doc.company, all_warehouse=True): - if d.get("warehouse") == bin_dict.get("warehouse"): - continue - - item_list.append( - [ - "", - "", - "", - bin_dict.get("warehouse"), - "", - bin_dict.get("projected_qty", 0), - bin_dict.get("actual_qty", 0), - bin_dict.get("ordered_qty", 0), - bin_dict.get("reserved_qty_for_production", 0), - ] - ) From 29349711e429aff51385a4573b7bca3b5c0cea52 Mon Sep 17 00:00:00 2001 From: Krishna Shirsath Date: Fri, 7 Aug 2026 13:44:57 +0530 Subject: [PATCH 017/134] fix: optimize product bundle item search (cherry picked from commit b3867f142890c46e178afe1ebe7d96f32582a22a) # Conflicts: # erpnext/selling/doctype/product_bundle/product_bundle.js --- .../doctype/product_bundle/product_bundle.js | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.js b/erpnext/selling/doctype/product_bundle/product_bundle.js index 3096b692a7e..931a83af429 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.js +++ b/erpnext/selling/doctype/product_bundle/product_bundle.js @@ -9,5 +9,28 @@ frappe.ui.form.on("Product Bundle", { query: "erpnext.selling.doctype.product_bundle.product_bundle.get_new_item_code", }; }); +<<<<<<< HEAD +======= + frm.set_query("item_code", "items", () => { + return { + query: "erpnext.controllers.queries.item_query", + }; + }); + + // A submitted bundle is immutable. To change it, create a new version + // (a fresh draft copied from this one) and submit that instead. + if (frm.doc.docstatus === 1) { + frm.add_custom_button( + __("New Version"), + () => { + frappe.model.open_mapped_doc({ + method: "erpnext.selling.doctype.product_bundle.product_bundle.make_new_version", + frm: frm, + }); + }, + __("Actions") + ); + } +>>>>>>> b3867f1428 (fix: optimize product bundle item search) }, }); From 693cade177054e4353078601677903cdfbc7e0cb Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 7 Aug 2026 16:07:36 +0530 Subject: [PATCH 018/134] chore: resolve conflict --- .../doctype/product_bundle/product_bundle.js | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.js b/erpnext/selling/doctype/product_bundle/product_bundle.js index 931a83af429..adbe4e728cf 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.js +++ b/erpnext/selling/doctype/product_bundle/product_bundle.js @@ -9,28 +9,10 @@ frappe.ui.form.on("Product Bundle", { query: "erpnext.selling.doctype.product_bundle.product_bundle.get_new_item_code", }; }); -<<<<<<< HEAD -======= frm.set_query("item_code", "items", () => { return { query: "erpnext.controllers.queries.item_query", }; }); - - // A submitted bundle is immutable. To change it, create a new version - // (a fresh draft copied from this one) and submit that instead. - if (frm.doc.docstatus === 1) { - frm.add_custom_button( - __("New Version"), - () => { - frappe.model.open_mapped_doc({ - method: "erpnext.selling.doctype.product_bundle.product_bundle.make_new_version", - frm: frm, - }); - }, - __("Actions") - ); - } ->>>>>>> b3867f1428 (fix: optimize product bundle item search) }, }); From 21fbfa541d5821c9199a65a66639f6bea3d61940 Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Fri, 7 Aug 2026 19:33:45 +0530 Subject: [PATCH 019/134] fix: guard reconciliation table deletes when tables are missing (cherry picked from commit 8a2b2a2b68608cb626dbf22fd5672f55da9830d6) --- .../patches/v14_0/clear_reconciliation_values_from_singles.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/patches/v14_0/clear_reconciliation_values_from_singles.py b/erpnext/patches/v14_0/clear_reconciliation_values_from_singles.py index c1f5b60a406..21b31e3bda9 100644 --- a/erpnext/patches/v14_0/clear_reconciliation_values_from_singles.py +++ b/erpnext/patches/v14_0/clear_reconciliation_values_from_singles.py @@ -1,3 +1,4 @@ +import frappe from frappe import qb @@ -13,5 +14,8 @@ def execute(): "Payment Reconciliation Allocation", ] for x in doctypes: + # child tables may not exist yet on sites where this pre-model-sync patch runs first + if not frappe.db.table_exists(x): + continue dt = qb.DocType(x) qb.from_(dt).delete().run() From db49b039136aef1a2417a0e000c50e7a5ba14a79 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 7 Aug 2026 17:28:39 +0530 Subject: [PATCH 020/134] fix: declare precision 9 on all conversion_factor fields The Float control parses values with the field precision, falling back to the global float precision when the docfield declares none (frappe ControlFloat.parse / get_precision). On a site with float precision 2, a fetched UOM factor of 0.453592292 was written back to the model as 0.45, silently corrupting every derived quantity by 0.8 percent. A ratio must not inherit display precision meant for quantities, so declare the same precision 9 the UOM Conversion Factor master already uses on every transaction-level conversion_factor field. (cherry picked from commit 69a35a12cb4d8b755b718c055997d095b65b58ac) # Conflicts: # erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json # erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json # erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json # erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json # erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json # erpnext/selling/doctype/quotation_item/quotation_item.json # erpnext/selling/doctype/sales_order_item/sales_order_item.json # erpnext/stock/doctype/delivery_note_item/delivery_note_item.json # erpnext/stock/doctype/packed_item/packed_item.json # erpnext/stock/doctype/pick_list_item/pick_list_item.json # erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json # erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json # erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json # erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json --- .../doctype/pos_invoice_item/pos_invoice_item.json | 5 +++++ .../purchase_invoice_item/purchase_invoice_item.json | 5 +++++ .../doctype/sales_invoice_item/sales_invoice_item.json | 5 +++++ .../doctype/purchase_order_item/purchase_order_item.json | 3 ++- .../purchase_receipt_item_supplied.json | 3 ++- .../request_for_quotation_item.json | 5 +++++ .../supplier_quotation_item/supplier_quotation_item.json | 3 ++- .../doctype/bom_creator_item/bom_creator_item.json | 5 +++-- erpnext/manufacturing/doctype/bom_item/bom_item.json | 5 +++-- .../doctype/bom_secondary_item/bom_secondary_item.json | 5 +++++ .../material_request_plan_item.json | 3 ++- .../delivery_schedule_item/delivery_schedule_item.json | 3 ++- erpnext/selling/doctype/quotation_item/quotation_item.json | 5 +++++ .../selling/doctype/sales_order_item/sales_order_item.json | 5 +++++ .../doctype/delivery_note_item/delivery_note_item.json | 5 +++++ .../material_request_item/material_request_item.json | 3 ++- erpnext/stock/doctype/packed_item/packed_item.json | 7 ++++++- erpnext/stock/doctype/pick_list_item/pick_list_item.json | 5 +++++ .../purchase_receipt_item/purchase_receipt_item.json | 5 +++++ erpnext/stock/doctype/putaway_rule/putaway_rule.json | 3 ++- .../doctype/stock_entry_detail/stock_entry_detail.json | 5 +++++ .../uom_conversion_detail/uom_conversion_detail.json | 5 +++-- .../doctype/subcontracting_bom/subcontracting_bom.json | 3 ++- .../subcontracting_inward_order_item.json | 3 ++- .../subcontracting_order_item.json | 3 ++- .../subcontracting_order_supplied_item.json | 3 ++- .../subcontracting_receipt_item.json | 5 +++++ .../subcontracting_receipt_supplied_item.json | 5 +++++ 28 files changed, 102 insertions(+), 18 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json index ad9f8751942..1826e57f4b7 100644 --- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json +++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json @@ -238,6 +238,7 @@ "fieldname": "conversion_factor", "fieldtype": "Float", "label": "UOM Conversion Factor", + "precision": "9", "print_hide": 1, "reqd": 1 }, @@ -858,7 +859,11 @@ ], "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2026-04-20 16:16:12.322024", +======= + "modified": "2026-08-07 17:31:31.732720", +>>>>>>> 69a35a12cb (fix: declare precision 9 on all conversion_factor fields) "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice Item", diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 0dbbd793606..04143fe6f3f 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -237,6 +237,7 @@ "fieldname": "conversion_factor", "fieldtype": "Float", "label": "UOM Conversion Factor", + "precision": "9", "print_hide": 1, "read_only": 1, "reqd": 1 @@ -1017,7 +1018,11 @@ "idx": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2026-05-06 08:08:40.782395", +======= + "modified": "2026-08-07 17:31:31.732720", +>>>>>>> 69a35a12cb (fix: declare precision 9 on all conversion_factor fields) "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index 5f1e0b1444b..4c198870115 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -228,6 +228,7 @@ "fieldname": "conversion_factor", "fieldtype": "Float", "label": "UOM Conversion Factor", + "precision": "9", "print_hide": 1, "reqd": 1 }, @@ -1036,7 +1037,11 @@ "idx": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2026-06-03 13:17:36.145788", +======= + "modified": "2026-08-07 17:31:31.732720", +>>>>>>> 69a35a12cb (fix: declare precision 9 on all conversion_factor fields) "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index ebe58d1a484..bde9c8304c5 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -260,6 +260,7 @@ "label": "UOM Conversion Factor", "oldfieldname": "conversion_factor", "oldfieldtype": "Currency", + "precision": "9", "print_hide": 1, "print_width": "100px", "reqd": 1, @@ -953,7 +954,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-07-15 10:30:04.600510", + "modified": "2026-08-07 17:31:31.732720", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json index 48680aceeff..6ace8bddf39 100644 --- a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json +++ b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json @@ -132,6 +132,7 @@ "label": "Conversion Factor", "oldfieldname": "conversion_factor", "oldfieldtype": "Currency", + "precision": "9", "read_only": 1 }, { @@ -207,7 +208,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2024-03-27 13:10:26.235916", + "modified": "2026-08-07 17:31:31.732720", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Receipt Item Supplied", diff --git a/erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json b/erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json index e96cd169883..406088517e9 100644 --- a/erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json +++ b/erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json @@ -239,6 +239,7 @@ "fieldname": "conversion_factor", "fieldtype": "Float", "label": "UOM Conversion Factor", + "precision": "9", "print_hide": 1, "read_only": 1, "reqd": 1 @@ -261,7 +262,11 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2026-01-31 19:46:27.884592", +======= + "modified": "2026-08-07 17:31:31.732720", +>>>>>>> 69a35a12cb (fix: declare precision 9 on all conversion_factor fields) "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation Item", diff --git a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json index 31efaa6690b..b11da04f94c 100644 --- a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json +++ b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json @@ -217,6 +217,7 @@ "fieldname": "conversion_factor", "fieldtype": "Float", "label": "UOM Conversion Factor", + "precision": "9", "print_hide": 1, "read_only": 1, "reqd": 1 @@ -614,7 +615,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-07-15 10:33:24.855979", + "modified": "2026-08-07 17:31:31.732720", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation Item", diff --git a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json index c5b39d88735..cadc8dfd8e2 100644 --- a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json +++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json @@ -140,7 +140,8 @@ { "fieldname": "conversion_factor", "fieldtype": "Float", - "label": "Conversion Factor" + "label": "Conversion Factor", + "precision": "9" }, { "fetch_from": "item_code.stock_uom", @@ -264,7 +265,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-11-05 21:15:55.187671", + "modified": "2026-08-07 17:31:31.732720", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Creator Item", diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.json b/erpnext/manufacturing/doctype/bom_item/bom_item.json index 52e7d4da609..12d5090ef0e 100644 --- a/erpnext/manufacturing/doctype/bom_item/bom_item.json +++ b/erpnext/manufacturing/doctype/bom_item/bom_item.json @@ -177,7 +177,8 @@ { "fieldname": "conversion_factor", "fieldtype": "Float", - "label": "Conversion Factor" + "label": "Conversion Factor", + "precision": "9" }, { "fieldname": "rate_amount_section", @@ -327,7 +328,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-11-05 19:00:38.646539", + "modified": "2026-08-07 17:31:31.732720", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Item", diff --git a/erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json b/erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json index ad2f69af5d9..b9866a76bcc 100644 --- a/erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json +++ b/erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json @@ -99,6 +99,7 @@ "fieldtype": "Float", "label": "Conversion Factor", "non_negative": 1, + "precision": "9", "reqd": 1 }, { @@ -217,7 +218,11 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2026-06-16 16:49:19.000000", +======= + "modified": "2026-08-07 17:31:31.732720", +>>>>>>> 69a35a12cb (fix: declare precision 9 on all conversion_factor fields) "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Secondary Item", diff --git a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json index 8bc37d2e02d..088338b4f2d 100644 --- a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json +++ b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json @@ -193,6 +193,7 @@ "fieldname": "conversion_factor", "fieldtype": "Float", "label": "Conversion Factor", + "precision": "9", "read_only": 1 }, { @@ -266,7 +267,7 @@ "grid_page_length": 50, "istable": 1, "links": [], - "modified": "2025-10-30 17:01:25.996352", + "modified": "2026-08-07 17:31:31.732720", "modified_by": "Administrator", "module": "Manufacturing", "name": "Material Request Plan Item", diff --git a/erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json b/erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json index 908251ea343..6db056bec04 100644 --- a/erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json +++ b/erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json @@ -38,6 +38,7 @@ "fieldname": "conversion_factor", "fieldtype": "Float", "label": "Conversion Factor", + "precision": "9", "read_only": 1 }, { @@ -106,7 +107,7 @@ "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2025-08-21 18:11:30.134073", + "modified": "2026-08-07 17:31:31.732720", "modified_by": "Administrator", "module": "Selling", "name": "Delivery Schedule Item", diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json index 92d7895c57b..5c7143b045c 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.json +++ b/erpnext/selling/doctype/quotation_item/quotation_item.json @@ -198,6 +198,7 @@ "fieldname": "conversion_factor", "fieldtype": "Float", "label": "UOM Conversion Factor", + "precision": "9", "print_hide": 1, "read_only": 1, "reqd": 1 @@ -711,7 +712,11 @@ "idx": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2026-01-30 12:56:08.320190", +======= + "modified": "2026-08-07 17:31:31.732720", +>>>>>>> 69a35a12cb (fix: declare precision 9 on all conversion_factor fields) "modified_by": "Administrator", "module": "Selling", "name": "Quotation Item", diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index 0f043a73fa4..f7f8ac89bf7 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -251,6 +251,7 @@ "fieldname": "conversion_factor", "fieldtype": "Float", "label": "UOM Conversion Factor", + "precision": "9", "print_hide": 1, "read_only": 1, "reqd": 1 @@ -1035,7 +1036,11 @@ "idx": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2026-02-22 16:40:00.200328", +======= + "modified": "2026-08-07 17:31:31.732720", +>>>>>>> 69a35a12cb (fix: declare precision 9 on all conversion_factor fields) "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index 0175b790887..0c4451ea1cb 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -236,6 +236,7 @@ "fieldname": "conversion_factor", "fieldtype": "Float", "label": "UOM Conversion Factor", + "precision": "9", "print_hide": 1, "read_only": 1, "reqd": 1 @@ -952,7 +953,11 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2026-04-07 15:43:20.892151", +======= + "modified": "2026-08-07 17:31:31.732720", +>>>>>>> 69a35a12cb (fix: declare precision 9 on all conversion_factor fields) "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.json b/erpnext/stock/doctype/material_request_item/material_request_item.json index 5f38ffc7462..4e7027f11db 100644 --- a/erpnext/stock/doctype/material_request_item/material_request_item.json +++ b/erpnext/stock/doctype/material_request_item/material_request_item.json @@ -159,6 +159,7 @@ "fieldname": "conversion_factor", "fieldtype": "Float", "label": "UOM Conversion Factor", + "precision": "9", "print_hide": 1, "reqd": 1 }, @@ -545,7 +546,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-01-06 20:47:27.317226", + "modified": "2026-08-07 17:31:31.732720", "modified_by": "Administrator", "module": "Stock", "name": "Material Request Item", diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json index f77661c4245..4c702487ed4 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.json +++ b/erpnext/stock/doctype/packed_item/packed_item.json @@ -228,7 +228,8 @@ { "fieldname": "conversion_factor", "fieldtype": "Float", - "label": "Conversion Factor" + "label": "Conversion Factor", + "precision": "9" }, { "fieldname": "rate", @@ -315,7 +316,11 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2026-04-27 14:12:53.236906", +======= + "modified": "2026-08-07 17:31:31.732720", +>>>>>>> 69a35a12cb (fix: declare precision 9 on all conversion_factor fields) "modified_by": "Administrator", "module": "Stock", "name": "Packed Item", diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item.json index 4ee5c820ab7..2f207e1713b 100644 --- a/erpnext/stock/doctype/pick_list_item/pick_list_item.json +++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.json @@ -124,6 +124,7 @@ "fieldname": "conversion_factor", "fieldtype": "Float", "label": "UOM Conversion Factor", + "precision": "9", "read_only": 1 }, { @@ -296,7 +297,11 @@ ], "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2026-07-06 18:17:18.000000", +======= + "modified": "2026-08-07 17:31:31.732720", +>>>>>>> 69a35a12cb (fix: declare precision 9 on all conversion_factor fields) "modified_by": "Administrator", "module": "Stock", "name": "Pick List Item", diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 9202fd84862..a70b320e863 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -290,6 +290,7 @@ "label": "Conversion Factor", "oldfieldname": "conversion_factor", "oldfieldtype": "Currency", + "precision": "9", "print_hide": 1, "print_width": "100px", "reqd": 1, @@ -1149,7 +1150,11 @@ "idx": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2026-04-29 16:01:34.154697", +======= + "modified": "2026-08-07 17:31:31.732720", +>>>>>>> 69a35a12cb (fix: declare precision 9 on all conversion_factor fields) "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.json b/erpnext/stock/doctype/putaway_rule/putaway_rule.json index 90f486f2352..38ef543632a 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.json +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.json @@ -106,12 +106,13 @@ "fieldtype": "Float", "label": "Conversion Factor", "no_copy": 1, + "precision": "9", "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-07-08 09:19:26.711470", + "modified": "2026-08-07 17:31:31.732720", "modified_by": "Administrator", "module": "Stock", "name": "Putaway Rule", diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index ad941013153..7faff4aebb1 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -255,6 +255,7 @@ "label": "Conversion Factor", "oldfieldname": "conversion_factor", "oldfieldtype": "Currency", + "precision": "9", "print_hide": 1, "reqd": 1 }, @@ -689,7 +690,11 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2026-07-06 18:17:18.000000", +======= + "modified": "2026-08-07 17:31:31.732720", +>>>>>>> 69a35a12cb (fix: declare precision 9 on all conversion_factor fields) "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/stock/doctype/uom_conversion_detail/uom_conversion_detail.json b/erpnext/stock/doctype/uom_conversion_detail/uom_conversion_detail.json index 2ab7f5e6600..90bf08be897 100644 --- a/erpnext/stock/doctype/uom_conversion_detail/uom_conversion_detail.json +++ b/erpnext/stock/doctype/uom_conversion_detail/uom_conversion_detail.json @@ -28,7 +28,8 @@ "label": "Conversion Factor", "non_negative": 1, "oldfieldname": "conversion_factor", - "oldfieldtype": "Float" + "oldfieldtype": "Float", + "precision": "9" }, { "fieldname": "column_break_nmeg", @@ -38,7 +39,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2026-06-11 23:02:54.800673", + "modified": "2026-08-07 17:31:31.732720", "modified_by": "Administrator", "module": "Stock", "name": "UOM Conversion Detail", diff --git a/erpnext/subcontracting/doctype/subcontracting_bom/subcontracting_bom.json b/erpnext/subcontracting/doctype/subcontracting_bom/subcontracting_bom.json index b55f02f9f52..e51a4ea6bf3 100644 --- a/erpnext/subcontracting/doctype/subcontracting_bom/subcontracting_bom.json +++ b/erpnext/subcontracting/doctype/subcontracting_bom/subcontracting_bom.json @@ -107,6 +107,7 @@ "fieldname": "conversion_factor", "fieldtype": "Float", "label": "Conversion Factor", + "precision": "9", "read_only": 1 }, { @@ -128,7 +129,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-03-27 13:10:45.904619", + "modified": "2026-08-07 17:31:31.732720", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting BOM", diff --git a/erpnext/subcontracting/doctype/subcontracting_inward_order_item/subcontracting_inward_order_item.json b/erpnext/subcontracting/doctype/subcontracting_inward_order_item/subcontracting_inward_order_item.json index 11da413fc14..9b20def35c2 100644 --- a/erpnext/subcontracting/doctype/subcontracting_inward_order_item/subcontracting_inward_order_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_inward_order_item/subcontracting_inward_order_item.json @@ -87,6 +87,7 @@ "fieldtype": "Float", "hidden": 1, "label": "Conversion Factor", + "precision": "9", "read_only": 1 }, { @@ -186,7 +187,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-10-18 18:04:04.204651", + "modified": "2026-08-07 17:31:31.732720", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Inward Order Item", diff --git a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json index 44ec2185ce6..19df4007581 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json @@ -174,6 +174,7 @@ "fieldtype": "Float", "hidden": 1, "label": "Conversion Factor", + "precision": "9", "read_only": 1 }, { @@ -425,7 +426,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-02-27 23:03:36.436504", + "modified": "2026-08-07 17:31:31.732720", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Order Item", diff --git a/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.json b/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.json index acd6aae6220..8a1c41ed499 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.json @@ -63,6 +63,7 @@ "fieldtype": "Float", "hidden": 1, "label": "Conversion Factor", + "precision": "9", "read_only": 1 }, { @@ -176,7 +177,7 @@ "hide_toolbar": 1, "istable": 1, "links": [], - "modified": "2025-10-30 16:00:43.379828", + "modified": "2026-08-07 17:31:31.732720", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Order Supplied Item", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json index b6d07f66b98..721daa59ae9 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -201,6 +201,7 @@ "fieldtype": "Float", "hidden": 1, "label": "Conversion Factor", + "precision": "9", "read_only": 1 }, { @@ -635,7 +636,11 @@ "idx": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2026-03-09 15:11:16.977539", +======= + "modified": "2026-08-07 17:31:31.732720", +>>>>>>> 69a35a12cb (fix: declare precision 9 on all conversion_factor fields) "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Item", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json index ce3494e879d..e9182b77c09 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json @@ -132,6 +132,7 @@ "fieldtype": "Float", "hidden": 1, "label": "Conversion Factor", + "precision": "9", "read_only": 1 }, { @@ -264,7 +265,11 @@ "idx": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2025-05-27 12:33:58.772638", +======= + "modified": "2026-08-07 17:31:31.732720", +>>>>>>> 69a35a12cb (fix: declare precision 9 on all conversion_factor fields) "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Supplied Item", From 206ed2892446d6505126768a786dc2d0d75cbc43 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 7 Aug 2026 17:47:50 +0530 Subject: [PATCH 021/134] fix: round computed conversion factors to field precision The inverse (1 / value) and intermediate-UOM branches of get_uom_conv_factor returned raw float quotients like 0.4535922921968971, bypassing the precision the docfields now declare. Same for the client-side back-calculation from an edited stock qty. Round both to the UOM Conversion Factor value precision. (cherry picked from commit ca5a6734096b3e106a41be3d23eecaba38384c51) --- erpnext/public/js/controllers/transaction.js | 5 ++++- erpnext/stock/doctype/item/item.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 8f62a139005..9d64ef09dd0 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1771,7 +1771,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe let item = frappe.get_doc(cdt, cdn); item.conversion_factor = 1.0; if (item.stock_qty) { - item.conversion_factor = flt(item.stock_qty) / flt(item.qty); + item.conversion_factor = flt( + flt(item.stock_qty) / flt(item.qty), + precision("conversion_factor", item) + ); } refresh_field("conversion_factor", item.name, item.parentfield); diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 13842784f9a..b9f35a46770 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -1472,7 +1472,7 @@ def get_uom_conv_factor(uom, stock_uom): "UOM Conversion Factor", {"to_uom": from_uom, "from_uom": to_uom}, ["value"], as_dict=1 ) if inverse_match: - return 1 / inverse_match.value + return flt(1 / inverse_match.value, frappe.get_precision("UOM Conversion Factor", "value")) # This attempts to try and get conversion from intermediate UOM. # case: @@ -1495,7 +1495,7 @@ def get_uom_conv_factor(uom, stock_uom): ) if intermediate_match: - return intermediate_match[0].value + return flt(intermediate_match[0].value, frappe.get_precision("UOM Conversion Factor", "value")) @frappe.whitelist() From 2cd8e39f04a47a18d307bf5304d03e8eec61a5a6 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 7 Aug 2026 22:28:30 +0530 Subject: [PATCH 022/134] chore: resolve conflict --- .../accounts/doctype/pos_invoice_item/pos_invoice_item.json | 4 ---- .../doctype/purchase_invoice_item/purchase_invoice_item.json | 4 ---- .../doctype/sales_invoice_item/sales_invoice_item.json | 4 ---- .../request_for_quotation_item.json | 4 ---- .../doctype/bom_secondary_item/bom_secondary_item.json | 4 ---- erpnext/selling/doctype/quotation_item/quotation_item.json | 4 ---- .../selling/doctype/sales_order_item/sales_order_item.json | 4 ---- .../stock/doctype/delivery_note_item/delivery_note_item.json | 4 ---- erpnext/stock/doctype/packed_item/packed_item.json | 4 ---- erpnext/stock/doctype/pick_list_item/pick_list_item.json | 4 ---- .../doctype/purchase_receipt_item/purchase_receipt_item.json | 4 ---- .../stock/doctype/stock_entry_detail/stock_entry_detail.json | 4 ---- .../subcontracting_receipt_item.json | 4 ---- .../subcontracting_receipt_supplied_item.json | 4 ---- 14 files changed, 56 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json index 1826e57f4b7..078b14a9fe3 100644 --- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json +++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json @@ -859,11 +859,7 @@ ], "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2026-04-20 16:16:12.322024", -======= "modified": "2026-08-07 17:31:31.732720", ->>>>>>> 69a35a12cb (fix: declare precision 9 on all conversion_factor fields) "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice Item", diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 04143fe6f3f..4227fac608c 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -1018,11 +1018,7 @@ "idx": 1, "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2026-05-06 08:08:40.782395", -======= "modified": "2026-08-07 17:31:31.732720", ->>>>>>> 69a35a12cb (fix: declare precision 9 on all conversion_factor fields) "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index 4c198870115..a5e34b2d45f 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -1037,11 +1037,7 @@ "idx": 1, "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2026-06-03 13:17:36.145788", -======= "modified": "2026-08-07 17:31:31.732720", ->>>>>>> 69a35a12cb (fix: declare precision 9 on all conversion_factor fields) "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json b/erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json index 406088517e9..8bd31f91f9f 100644 --- a/erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json +++ b/erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json @@ -262,11 +262,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2026-01-31 19:46:27.884592", -======= "modified": "2026-08-07 17:31:31.732720", ->>>>>>> 69a35a12cb (fix: declare precision 9 on all conversion_factor fields) "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation Item", diff --git a/erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json b/erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json index b9866a76bcc..a76be016de7 100644 --- a/erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json +++ b/erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json @@ -218,11 +218,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2026-06-16 16:49:19.000000", -======= "modified": "2026-08-07 17:31:31.732720", ->>>>>>> 69a35a12cb (fix: declare precision 9 on all conversion_factor fields) "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Secondary Item", diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json index 5c7143b045c..acca03b5dd8 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.json +++ b/erpnext/selling/doctype/quotation_item/quotation_item.json @@ -712,11 +712,7 @@ "idx": 1, "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2026-01-30 12:56:08.320190", -======= "modified": "2026-08-07 17:31:31.732720", ->>>>>>> 69a35a12cb (fix: declare precision 9 on all conversion_factor fields) "modified_by": "Administrator", "module": "Selling", "name": "Quotation Item", diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index f7f8ac89bf7..9e6390df115 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -1036,11 +1036,7 @@ "idx": 1, "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2026-02-22 16:40:00.200328", -======= "modified": "2026-08-07 17:31:31.732720", ->>>>>>> 69a35a12cb (fix: declare precision 9 on all conversion_factor fields) "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index 0c4451ea1cb..873a7db9443 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -953,11 +953,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2026-04-07 15:43:20.892151", -======= "modified": "2026-08-07 17:31:31.732720", ->>>>>>> 69a35a12cb (fix: declare precision 9 on all conversion_factor fields) "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json index 4c702487ed4..3a0c8059891 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.json +++ b/erpnext/stock/doctype/packed_item/packed_item.json @@ -316,11 +316,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2026-04-27 14:12:53.236906", -======= "modified": "2026-08-07 17:31:31.732720", ->>>>>>> 69a35a12cb (fix: declare precision 9 on all conversion_factor fields) "modified_by": "Administrator", "module": "Stock", "name": "Packed Item", diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item.json index 2f207e1713b..4733b4613a7 100644 --- a/erpnext/stock/doctype/pick_list_item/pick_list_item.json +++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.json @@ -297,11 +297,7 @@ ], "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2026-07-06 18:17:18.000000", -======= "modified": "2026-08-07 17:31:31.732720", ->>>>>>> 69a35a12cb (fix: declare precision 9 on all conversion_factor fields) "modified_by": "Administrator", "module": "Stock", "name": "Pick List Item", diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index a70b320e863..fbb3e2b75d9 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -1150,11 +1150,7 @@ "idx": 1, "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2026-04-29 16:01:34.154697", -======= "modified": "2026-08-07 17:31:31.732720", ->>>>>>> 69a35a12cb (fix: declare precision 9 on all conversion_factor fields) "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 7faff4aebb1..defb69d548b 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -690,11 +690,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2026-07-06 18:17:18.000000", -======= "modified": "2026-08-07 17:31:31.732720", ->>>>>>> 69a35a12cb (fix: declare precision 9 on all conversion_factor fields) "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json index 721daa59ae9..8622889515e 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -636,11 +636,7 @@ "idx": 1, "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2026-03-09 15:11:16.977539", -======= "modified": "2026-08-07 17:31:31.732720", ->>>>>>> 69a35a12cb (fix: declare precision 9 on all conversion_factor fields) "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Item", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json index e9182b77c09..a76f289100c 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json @@ -265,11 +265,7 @@ "idx": 1, "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2025-05-27 12:33:58.772638", -======= "modified": "2026-08-07 17:31:31.732720", ->>>>>>> 69a35a12cb (fix: declare precision 9 on all conversion_factor fields) "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Supplied Item", From dbfe7e199ea20dba8fca12356287330881e6295f Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 7 Aug 2026 22:47:17 +0530 Subject: [PATCH 023/134] fix: add type hints to conversion factor API --- erpnext/stock/doctype/item/item.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index b9f35a46770..a11c9b1c6dc 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -1453,7 +1453,7 @@ def get_item_details(item_code, company=None): @frappe.whitelist() -def get_uom_conv_factor(uom, stock_uom): +def get_uom_conv_factor(uom: str | None, stock_uom: str | None): """Get UOM conversion factor from uom to stock_uom e.g. uom = "Kg", stock_uom = "Gram" then returns 1000.0 """ From f7bae888cfdccc706f1b6ce674af886d7ceeaf8f Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 11:01:21 +0530 Subject: [PATCH 024/134] fix: incorrect entry detection in Stock Ledger Invariant Check (#57886) (cherry picked from commit b3f97cd38965c08fa10a27216fea03a45cd2684d) --- .../stock_ledger_invariant_check.py | 15 ++++++++-- .../test_stock_ledger_invariant_check.py | 30 +++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py index ffb024acfb1..7827203ae92 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py +++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py @@ -7,6 +7,8 @@ import frappe from frappe import _ from frappe.utils import cint, flt, get_link_to_form, parse_json +from erpnext.stock.utils import get_valuation_method + SLE_FIELDS = ( "name", "posting_date", @@ -53,6 +55,9 @@ def add_invariant_check_fields(sles, filters): balance_qty = 0.0 balance_stock_value = 0.0 + company = frappe.get_cached_value("Warehouse", filters.warehouse, "company") + valuation_method = get_valuation_method(filters.item_code, company) + incorrect_idx = None float_precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) or 3 currency_precision = ( @@ -90,7 +95,7 @@ def add_invariant_check_fields(sles, filters): ) sle.diff_value_diff = sle.stock_value_from_diff - sle.stock_value - if maintains_fifo_queue(sle): + if maintains_fifo_queue(sle, valuation_method): add_fifo_fields(sle, sles[idx - 1] if idx else None) if incorrect_idx is None and not is_sle_has_correct_data(sle, float_precision, currency_precision): @@ -104,8 +109,10 @@ def add_invariant_check_fields(sles, filters): return sles -def maintains_fifo_queue(sle): - # no queue is maintained for serialized/batchwise-valued stock +def maintains_fifo_queue(sle, valuation_method): + if valuation_method == "Moving Average": + return False + return not ( sle.serial_and_batch_bundle or sle.serial_no or (sle.batch_no and sle.use_batchwise_valuation) ) @@ -138,6 +145,8 @@ def is_sle_has_correct_data(sle, float_precision, currency_precision): return ( flt(sle.difference_in_qty, float_precision) == 0.0 and flt(sle.diff_value_diff, currency_precision) == 0.0 + and flt(sle.fifo_qty_diff, float_precision) == 0.0 + and flt(sle.fifo_value_diff, currency_precision) == 0.0 ) diff --git a/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py index 0f71a8834b2..ae692617a77 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py +++ b/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py @@ -1,6 +1,8 @@ # Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt +import json + import frappe from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry @@ -59,6 +61,34 @@ class TestStockLedgerInvariantCheck(ERPNextTestSuite): self.assertEqual(len(data), 2) # incorrect entry + one before it for context self.assertEqual(data[-1].name, sle.name) + def test_show_incorrect_entries_catches_queue_mismatch(self): + item = self.make_movements() + + sle = frappe.get_last_doc( + "Stock Ledger Entry", {"item_code": item, "warehouse": WAREHOUSE, "is_cancelled": 0} + ) + tampered_queue = json.dumps([[sle.qty_after_transaction + 5, 100]]) + frappe.db.set_value("Stock Ledger Entry", sle.name, "stock_queue", tampered_queue) + + data = self.run_report(item_code=item, show_incorrect_entries=1) + self.assertEqual(len(data), 2) + self.assertEqual(data[-1].name, sle.name) + + def test_moving_average_item_skips_fifo_queue_checks(self): + from erpnext.stock.doctype.item.test_item import make_item + + item = make_item(properties={"valuation_method": "Moving Average"}).name + make_stock_entry(item_code=item, to_warehouse=WAREHOUSE, qty=10, rate=100) + make_stock_entry(item_code=item, from_warehouse=WAREHOUSE, qty=4) + + data = self.run_report(item_code=item) + self.assertTrue(data) + for row in data: + self.assertIsNone(row.fifo_qty_diff) + self.assertIsNone(row.fifo_value_diff) + + self.assertEqual(self.run_report(item_code=item, show_incorrect_entries=1), []) + def test_batch_item_skips_fifo_queue_checks(self): from erpnext.stock.doctype.item.test_item import make_item From ffa65b0c481bbacb1c7bd57a85f3aed3203698bf Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 10:48:24 +0530 Subject: [PATCH 025/134] fix: repost read stale sibling SLE rate for moving average returns During repost, a return line with recalculate_rate resolved its moving average rate through get_incoming_rate -> get_previous_sle, which matches posting_datetime <= and orders by creation desc. For a multi-line return of the same item, every line shares one posting_datetime, so the query landed on a sibling line of the same voucher whose stored valuation_rate was still the previous repost run's output, not the rate before the voucher. Each repost run therefore re-seeded the voucher from its own prior output. The error gain per run is (qty returned at the stale rate) / (qty remaining after the return), so whenever a return removes most of the stock the loop diverges instead of converging, alternating sign and growing until stock_value overflows decimal(21,9) and the repost dies with 'Out of range value for column stock_value'. Use the in-memory running valuation rate that update_entries_after already tracks for the warehouse at this point in the repost. It is the authoritative pre-entry state, is immune to sibling rows, and makes the repost idempotent. The database lookup is kept only as a fallback for a zero in-memory rate, preserving the existing zero-rate fallback chain. --- erpnext/stock/stock_ledger.py | 36 ++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 60fcec3f7cb..f2b1544b212 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1301,23 +1301,25 @@ class update_entries_after: and not sle.get("batch_no") and not sle.get("serial_and_batch_bundle") ): - rate = get_incoming_rate( - { - "item_code": sle.item_code, - "warehouse": sle.warehouse, - "posting_date": sle.posting_date, - "posting_time": sle.posting_time, - "qty": sle.actual_qty, - "serial_no": sle.get("serial_no"), - "batch_no": sle.get("batch_no"), - "serial_and_batch_bundle": sle.get("serial_and_batch_bundle"), - "company": sle.company, - "voucher_type": sle.voucher_type, - "voucher_no": sle.voucher_no, - "allow_zero_valuation": self.allow_zero_rate, - "sle": sle.name, - } - ) + rate = flt(self.wh_data.valuation_rate) + if not rate: + rate = get_incoming_rate( + { + "item_code": sle.item_code, + "warehouse": sle.warehouse, + "posting_date": sle.posting_date, + "posting_time": sle.posting_time, + "qty": sle.actual_qty, + "serial_no": sle.get("serial_no"), + "batch_no": sle.get("batch_no"), + "serial_and_batch_bundle": sle.get("serial_and_batch_bundle"), + "company": sle.company, + "voucher_type": sle.voucher_type, + "voucher_no": sle.voucher_no, + "allow_zero_valuation": self.allow_zero_rate, + "sle": sle.name, + } + ) if not rate and sle.voucher_type in ["Delivery Note", "Sales Invoice"]: rate = get_rate_for_return( From 4571a8fa145fde440347788ce215be186d51c861 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 10:53:04 +0530 Subject: [PATCH 026/134] test: repost of multi-line moving average return is idempotent Reposting a return that removes most of the stock across several lines of the same item must keep every line at the running average and produce identical results on a second repost. Before the fix the first repost already drifted, seeding each line from a sibling row of the same voucher. --- .../test_repost_item_valuation.py | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py index a941b64856a..780210126f4 100644 --- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py @@ -480,6 +480,55 @@ class TestRepostItemValuation(ERPNextTestSuite, StockTestMixin): # incoming rate after reposting should be 150 self.assertSLEs(se, [{"incoming_rate": 150}]) + def test_repost_multi_line_moving_average_return(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + item = self.make_item(properties={"valuation_method": "Moving Average"}).name + warehouse = "_Test Warehouse - _TC" + + make_purchase_receipt(item_code=item, qty=100, rate=100, warehouse=warehouse) + + pr = make_purchase_receipt(item_code=item, qty=400, rate=200, warehouse=warehouse, do_not_submit=1) + for qty in (100, 300, 100): + pr.append( + "items", + { + "item_code": item, + "warehouse": warehouse, + "qty": qty, + "received_qty": qty, + "rate": 200, + "uom": pr.items[0].uom, + "conversion_factor": 1.0, + }, + ) + pr.save() + pr.submit() + + return_pr = make_return_doc(pr.doctype, pr.name) + return_pr.save() + return_pr.submit() + + expected_sles = [ + {"outgoing_rate": 190.0, "valuation_rate": 190.0, "qty_after_transaction": 600.0}, + {"outgoing_rate": 190.0, "valuation_rate": 190.0, "qty_after_transaction": 500.0}, + {"outgoing_rate": 190.0, "valuation_rate": 190.0, "qty_after_transaction": 200.0}, + {"outgoing_rate": 190.0, "valuation_rate": 190.0, "qty_after_transaction": 100.0}, + ] + + for _ in range(2): + riv = frappe.get_doc( + doctype="Repost Item Valuation", + based_on="Transaction", + voucher_type=pr.doctype, + voucher_no=pr.name, + posting_date=pr.posting_date, + posting_time=pr.posting_time, + ) + riv.submit() + + self.assertSLEs(return_pr, expected_sles) + def test_remove_attached_file(self): item_code = make_item("_Test Remove Attached File Item", properties={"is_stock_item": 1}) From 81f81fff32d31e3a14a4cfd3e545586bae18505b Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 11:00:47 +0530 Subject: [PATCH 027/134] fix: zero-rate repost fallback could still read sibling SLE When the in-memory running rate is zero, the fallback went through get_incoming_rate, whose previous-SLE lookup matches the same posting_datetime and can land on a sibling line of the voucher being replayed. Replace it with get_previous_sle_of_current_voucher excluding the current voucher, keeping the get_valuation_rate chain when no previous entry exists. get_incoming_rate is no longer used in this module. --- erpnext/stock/stock_ledger.py | 53 ++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index f2b1544b212..775b5c95dad 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -39,7 +39,6 @@ from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry impor from erpnext.stock.utils import ( get_combine_datetime, get_incoming_outgoing_rate_for_cancel, - get_incoming_rate, get_or_make_bin, get_serial_nos_data, get_stock_balance, @@ -1301,25 +1300,7 @@ class update_entries_after: and not sle.get("batch_no") and not sle.get("serial_and_batch_bundle") ): - rate = flt(self.wh_data.valuation_rate) - if not rate: - rate = get_incoming_rate( - { - "item_code": sle.item_code, - "warehouse": sle.warehouse, - "posting_date": sle.posting_date, - "posting_time": sle.posting_time, - "qty": sle.actual_qty, - "serial_no": sle.get("serial_no"), - "batch_no": sle.get("batch_no"), - "serial_and_batch_bundle": sle.get("serial_and_batch_bundle"), - "company": sle.company, - "voucher_type": sle.voucher_type, - "voucher_no": sle.voucher_no, - "allow_zero_valuation": self.allow_zero_rate, - "sle": sle.name, - } - ) + rate = self.get_moving_average_rate_for_return(sle) if not rate and sle.voucher_type in ["Delivery Note", "Sales Invoice"]: rate = get_rate_for_return( @@ -1387,6 +1368,38 @@ class update_entries_after: return rate + def get_moving_average_rate_for_return(self, sle): + """Rate just before this entry, taken from the in-memory running state so a + multi-line return never reads a sibling row of its own voucher.""" + rate = flt(self.wh_data.valuation_rate) + if rate: + return rate + + previous_sle = get_previous_sle_of_current_voucher( + frappe._dict( + item_code=sle.item_code, + warehouse=sle.warehouse, + posting_date=sle.posting_date, + posting_time=sle.posting_time, + voucher_no=sle.voucher_no, + ), + exclude_current_voucher=True, + ) + + rate = previous_sle.get("valuation_rate") + if rate is None: + rate = get_valuation_rate( + sle.item_code, + sle.warehouse, + sle.voucher_type, + sle.voucher_no, + self.allow_zero_rate, + currency=erpnext.get_company_currency(sle.company), + company=sle.company, + ) + + return flt(rate) + def update_outgoing_rate_on_transaction(self, sle): """ Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return From f0adbd2bd39087fe659998ac35443b86fa17c1be Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Sat, 8 Aug 2026 19:06:05 +0530 Subject: [PATCH 028/134] fix(journal_entry): validation message for blocked purchase invoice (#57896) --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 65db883c430..c4367223909 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -912,9 +912,7 @@ class JournalEntry(AccountsController): invoice.doctype, invoice.name, invoice.release_date ) if invoice.release_date - else _("{0} {1} is blocked.").format( - invoice.doctype, invoice.name, invoice.release_date - ) + else _("{0} {1} is blocked.").format(invoice.doctype, invoice.name) ) frappe.throw(msg) From ea5cbb116c95a8fe2314d220b4784dd67615ac03 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Sat, 8 Aug 2026 19:17:53 +0530 Subject: [PATCH 029/134] feat: sync serial no status from stock ledger in Stock Qty vs Serial No Count report (version-16-hotfix) (#57864) * feat: sync serial no status from stock ledger in Stock Qty vs Serial No Count report * fix: pick last bundle move in SQL ordered by posting datetime and SLE creation * fix: derive synced serial no status from stock ledger helper and validate sync args --- .../stock_qty_vs_serial_no_count.js | 24 +++ .../stock_qty_vs_serial_no_count.py | 175 ++++++++++++++++++ .../test_stock_qty_vs_serial_no_count.py | 41 ++++ 3 files changed, 240 insertions(+) create mode 100644 erpnext/stock/report/stock_qty_vs_serial_no_count/test_stock_qty_vs_serial_no_count.py diff --git a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js index c38f0237436..6df8458fd3c 100644 --- a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js +++ b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js @@ -2,6 +2,30 @@ // For license information, please see license.txt frappe.query_reports["Stock Qty vs Serial No Count"] = { + onload: function (report) { + report.page.add_inner_button(__("Sync Serial No Status"), () => { + const warehouse = report.get_filter_value("warehouse"); + if (!warehouse) { + frappe.msgprint(__("Please select a warehouse first.")); + return; + } + + frappe.confirm( + __( + "This will update the warehouse and status of Serial Nos counted in {0} to match the stock ledger. Continue?", + [warehouse.bold()] + ), + () => { + frappe.call({ + method: "erpnext.stock.report.stock_qty_vs_serial_no_count.stock_qty_vs_serial_no_count.sync_serial_no_status", + args: { warehouse: warehouse }, + freeze: true, + }); + } + ); + }); + }, + filters: [ { fieldname: "company", diff --git a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py index 6087c747374..2ea732180ad 100644 --- a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py +++ b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py @@ -4,6 +4,12 @@ import frappe from frappe import _ +from frappe.query_builder import Order +from frappe.query_builder.functions import Coalesce +from frappe.utils import cstr, flt +from pypika import analytics as an + +from erpnext.stock.serial_batch_bundle import get_serial_no_status def execute(filters=None): @@ -77,3 +83,172 @@ def get_data(warehouse, show_disabled_items): data.append(row) return data + + +SYNC_CHUNK_SIZE = 1000 + + +@frappe.whitelist(methods=["POST"]) +def sync_serial_no_status(warehouse: str, item_code: str | None = None): + if not frappe.has_permission("Serial No", "write"): + frappe.throw(_("Not permitted to update Serial No"), frappe.PermissionError) + + warehouse = cstr(warehouse) + item_code = cstr(item_code) if item_code else None + if not frappe.db.exists("Warehouse", warehouse): + frappe.throw(_("Warehouse {0} does not exist").format(warehouse)) + + if item_code and not frappe.db.exists("Item", item_code): + frappe.throw(_("Item {0} does not exist").format(item_code)) + + frappe.enqueue( + sync_serial_no_status_for_warehouse, + queue="long", + warehouse=warehouse, + item_code=item_code, + ) + frappe.msgprint( + _("Serial No status sync has been queued. Reload the report after a few minutes."), + alert=True, + ) + + +def sync_serial_no_status_for_warehouse(warehouse, item_code=None): + filters = {"has_serial_no": 1} + if item_code: + filters["name"] = item_code + + for item in frappe.get_all("Item", filters=filters, pluck="name"): + sync_serial_no_status_for_item(item, warehouse) + + +def sync_serial_no_status_for_item(item_code, warehouse): + """Correct Serial No records this report counts in the warehouse but whose last + stock ledger movement says the stock left it. Reposting rebuilds qty and valuation + from the ledger but never rewrites Serial No warehouse/status, so records orphaned + by cancelled or amended vouchers keep inflating the serial count.""" + serial_nos = frappe.get_all( + "Serial No", + filters={"item_code": item_code, "warehouse": warehouse, "status": ("in", ["Active", "Expired"])}, + pluck="name", + ) + if not serial_nos: + return + + last_moves = get_last_ledger_moves(item_code, serial_nos) + for serial_no in serial_nos: + row = last_moves.get(serial_no) + if row and flt(row.qty) > 0 and row.warehouse == warehouse: + continue + + set_serial_no_state_from_ledger(serial_no, row) + + +def set_serial_no_state_from_ledger(serial_no, row): + if not row: + frappe.db.set_value( + "Serial No", serial_no, {"warehouse": None, "status": "Inactive"}, update_modified=False + ) + return + + status = get_serial_no_status( + frappe._dict( + actual_qty=flt(row.qty), + warehouse=row.warehouse, + voucher_type=row.voucher_type, + voucher_no=row.voucher_no, + is_cancelled=0, + ) + ) + warehouse = row.warehouse if status == "Active" else None + frappe.db.set_value( + "Serial No", serial_no, {"warehouse": warehouse, "status": status}, update_modified=False + ) + + +def get_last_ledger_moves(item_code, serial_nos): + last_moves = get_last_bundle_moves(item_code, serial_nos) + if missing := [serial_no for serial_no in serial_nos if serial_no not in last_moves]: + set_legacy_last_moves(item_code, missing, last_moves) + + return last_moves + + +def get_last_bundle_moves(item_code, serial_nos): + last_moves = {} + for start in range(0, len(serial_nos), SYNC_CHUNK_SIZE): + for row in get_last_bundle_moves_chunk(item_code, serial_nos[start : start + SYNC_CHUNK_SIZE]): + last_moves[row.serial_no] = row + + return last_moves + + +def get_last_bundle_moves_chunk(item_code, serial_nos): + """A bundle can be created much before its Stock Ledger Entry, so same-posting-datetime + ties are broken on the creation of the bundle's own SLE. The SLE join also keeps only + real stock movements - reservation bundles (Pick List) carry no SLE.""" + entry = frappe.qb.DocType("Serial and Batch Entry") + bundle = frappe.qb.DocType("Serial and Batch Bundle") + sle = frappe.qb.DocType("Stock Ledger Entry") + + row_number = ( + an.RowNumber() + .over(entry.serial_no) + .orderby(Coalesce(entry.posting_datetime, bundle.posting_datetime), order=Order.desc) + .orderby(sle.creation, order=Order.desc) + ) + + ranked = ( + frappe.qb.from_(entry) + .inner_join(bundle) + .on(entry.parent == bundle.name) + .inner_join(sle) + .on(sle.serial_and_batch_bundle == bundle.name) + .select( + entry.serial_no, + entry.qty, + Coalesce(entry.warehouse, bundle.warehouse).as_("warehouse"), + bundle.voucher_type, + bundle.voucher_no, + row_number.as_("row_no"), + ) + .where( + (bundle.docstatus == 1) + & (Coalesce(bundle.is_cancelled, 0) == 0) + & (sle.is_cancelled == 0) + & (bundle.item_code == item_code) + & (entry.serial_no.isin(serial_nos)) + ) + ).as_("ranked") + + return ( + frappe.qb.from_(ranked) + .select(ranked.serial_no, ranked.qty, ranked.warehouse, ranked.voucher_type, ranked.voucher_no) + .where(ranked.row_no == 1) + .run(as_dict=True) + ) + + +def set_legacy_last_moves(item_code, serial_nos, last_moves): + """Movements posted before Serial and Batch Bundle exist only as newline-separated + text on Stock Ledger Entry.""" + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + + pending = set(serial_nos) + rows = frappe.get_all( + "Stock Ledger Entry", + filters={"item_code": item_code, "is_cancelled": 0, "serial_no": ("is", "set")}, + fields=["serial_no", "actual_qty", "warehouse", "voucher_type", "voucher_no"], + order_by="posting_datetime asc, creation asc", + ) + + for row in rows: + qty = 1 if flt(row.actual_qty) > 0 else -1 + for serial_no in get_serial_nos(row.serial_no): + if serial_no in pending: + last_moves[serial_no] = frappe._dict( + qty=qty, + warehouse=row.warehouse, + voucher_type=row.voucher_type, + voucher_no=row.voucher_no, + ) diff --git a/erpnext/stock/report/stock_qty_vs_serial_no_count/test_stock_qty_vs_serial_no_count.py b/erpnext/stock/report/stock_qty_vs_serial_no_count/test_stock_qty_vs_serial_no_count.py new file mode 100644 index 00000000000..ba72eaff63f --- /dev/null +++ b/erpnext/stock/report/stock_qty_vs_serial_no_count/test_stock_qty_vs_serial_no_count.py @@ -0,0 +1,41 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.tests.utils import ERPNextTestSuite + + +class TestStockQtyVsSerialNoCount(ERPNextTestSuite): + def test_sync_serial_no_status(self): + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + from erpnext.stock.report.stock_qty_vs_serial_no_count.stock_qty_vs_serial_no_count import ( + sync_serial_no_status_for_warehouse, + ) + + item = "_Test Serialized Item With Series" + warehouse = "Stores - _TC" + se = make_stock_entry(item_code=item, to_warehouse=warehouse, qty=2, rate=100) + serial_no = frappe.get_all( + "Serial and Batch Entry", + {"parent": se.items[0].serial_and_batch_bundle}, + pluck="serial_no", + )[0] + + create_delivery_note( + item_code=item, + warehouse=warehouse, + qty=1, + serial_no=serial_no, + use_serial_batch_fields=1, + ) + self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Delivered") + + frappe.db.set_value("Serial No", serial_no, {"status": "Active", "warehouse": warehouse}) + + sync_serial_no_status_for_warehouse(warehouse, item_code=item) + + details = frappe.db.get_value("Serial No", serial_no, ["status", "warehouse"], as_dict=True) + self.assertEqual(details.status, "Delivered") + self.assertFalse(details.warehouse) From 6b45002abcbe42c4607949030fae29d9f8191b80 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 9 Aug 2026 00:04:36 +0530 Subject: [PATCH 030/134] fix: set `restrict_globals=True` in `frappe.render_template` (backport #57899) (#57902) Co-authored-by: Diptanil Saha --- erpnext/accounts/custom/address.py | 4 +++- erpnext/accounts/doctype/payment_request/payment_request.py | 2 +- .../doctype/request_for_quotation/request_for_quotation.py | 4 ++-- erpnext/crm/doctype/contract_template/contract_template.py | 4 ++-- erpnext/crm/doctype/email_campaign/email_campaign.py | 4 ++-- erpnext/stock/doctype/delivery_trip/delivery_trip.py | 2 +- 6 files changed, 11 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/custom/address.py b/erpnext/accounts/custom/address.py index 8f57489a82f..68fdee9924c 100644 --- a/erpnext/accounts/custom/address.py +++ b/erpnext/accounts/custom/address.py @@ -71,4 +71,6 @@ def get_shipping_address(company, address=None): if address: address_as_dict = address[0] name, address_template = get_address_templates(address_as_dict) - return address_as_dict.get("name"), frappe.render_template(address_template, address_as_dict) + return address_as_dict.get("name"), frappe.render_template( + address_template, address_as_dict, restrict_globals=True + ) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index c2cc317e5e3..51d11eff16e 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -474,7 +474,7 @@ class PaymentRequest(Document): } if self.message: - return frappe.render_template(self.message, context) + return frappe.render_template(self.message, context, restrict_globals=True) def set_failed(self): pass diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 2aefbded9d2..b902a66ee6a 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -328,14 +328,14 @@ class RequestforQuotation(BuyingController): message_template = self.mfs_html if self.use_html else self.message_for_supplier # nosemgrep: frappe-semgrep-rules.rules.security.frappe-ssti - rendered_message = frappe.render_template(message_template, doc_args) + rendered_message = frappe.render_template(message_template, doc_args, restrict_globals=True) subject_source = ( self.subject or frappe.get_value("Email Template", self.email_template, "subject") or _("Request for Quotation") ) - rendered_subject = frappe.render_template(subject_source, doc_args) + rendered_subject = frappe.render_template(subject_source, doc_args, restrict_globals=True) if preview: return { "message": rendered_message, diff --git a/erpnext/crm/doctype/contract_template/contract_template.py b/erpnext/crm/doctype/contract_template/contract_template.py index 700197500fb..d0bf738d79d 100644 --- a/erpnext/crm/doctype/contract_template/contract_template.py +++ b/erpnext/crm/doctype/contract_template/contract_template.py @@ -30,7 +30,7 @@ class ContractTemplate(Document): def validate(self): if self.contract_terms: - validate_template(self.contract_terms) + validate_template(self.contract_terms, restrict_globals=True) @frappe.whitelist() @@ -42,6 +42,6 @@ def get_contract_template(template_name, doc): contract_terms = None if contract_template.contract_terms: - contract_terms = frappe.render_template(contract_template.contract_terms, doc) + contract_terms = frappe.render_template(contract_template.contract_terms, doc, restrict_globals=True) return {"contract_template": contract_template, "contract_terms": contract_terms} diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py index 4454ede5310..dbc4382a041 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -171,8 +171,8 @@ def send_mail(entry, email_campaign): context = {"doc": frappe.get_doc("Email Group", recipient)} # Render template - subject = frappe.render_template(email_template.get("subject"), context) - content = frappe.render_template(email_template.response_, context) + subject = frappe.render_template(email_template.get("subject"), context, restrict_globals=True) + content = frappe.render_template(email_template.response_, context, restrict_globals=True) try: comm = make( diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.py b/erpnext/stock/doctype/delivery_trip/delivery_trip.py index 07359df830f..b2307a5948f 100644 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.py @@ -438,7 +438,7 @@ def notify_customers(delivery_trip): frappe.sendmail( recipients=contact_info.email_id, subject=dispatch_template.subject, - message=frappe.render_template(dispatch_template.response, context), + message=frappe.render_template(dispatch_template.response, context, restrict_globals=True), attachments=get_attachments(stop), ) From 838fb8e8dfea51c618745dd435f37b61354e07c8 Mon Sep 17 00:00:00 2001 From: Suhas Bharadwaj Date: Thu, 6 Aug 2026 16:38:46 +0530 Subject: [PATCH 031/134] fix: condition check with empty object for falsy case (cherry picked from commit e0b9351d492bdf5cf009690ea86b2fbed295fcbc) --- erpnext/projects/doctype/timesheet/timesheet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js index bc63ba79a80..3408c8f1843 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.js +++ b/erpnext/projects/doctype/timesheet/timesheet.js @@ -456,7 +456,7 @@ const set_employee_and_company = function (frm) { const options = { user_id: frappe.session.user }; const fields = ["name", "company"]; frappe.db.get_value("Employee", options, fields).then(({ message }) => { - if (message) { + if (message.name && message.company) { // there is an employee with the currently logged in user_id frm.set_value("employee", message.name); frm.set_value("company", message.company); From 8d98fe81872dddfef0b84f19ac62dfe8f0d03701 Mon Sep 17 00:00:00 2001 From: nishkagosalia Date: Thu, 6 Aug 2026 12:51:19 +0530 Subject: [PATCH 032/134] fix: validate webform for project (cherry picked from commit 126966d1db973957725f87fcbe87e9630d49328d) --- erpnext/projects/doctype/task/task.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 9b1e3bfcde4..708558f62cd 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -90,6 +90,7 @@ class Task(NestedSet): self.validate_completed_on() self.set_default_end_date_if_missing() self.validate_parent_is_group() + self.validate_web_form_project_permission() def validate_dates(self): self.validate_from_to_dates("exp_start_date", "exp_end_date") @@ -300,6 +301,23 @@ class Task(NestedSet): if project_user: return True + def validate_web_form_project_permission(self): + project_unchanged = not self.is_new() and self.project == self.get_db_value("project") + + if ( + not frappe.flags.in_web_form + or not self.project + or project_unchanged + or frappe.has_permission("Project", "write", doc=self.project) + or self.has_webform_permission() + ): + return + + frappe.throw( + _("You are not permitted to create a Task for Project {0}").format(self.project), + frappe.PermissionError, + ) + def populate_depends_on(self): if self.parent_task: parent = frappe.get_doc("Task", self.parent_task) From c9977be5d4a4d7547bf6589e82dd6d241d9b7050 Mon Sep 17 00:00:00 2001 From: Pandiyan P Date: Fri, 7 Aug 2026 23:02:29 +0530 Subject: [PATCH 033/134] fix: allow selecting a warehouse for new items in the update items dialog (#57876) (cherry picked from commit 55fe26904622273a0089695197e666237bc87c8b) # Conflicts: # erpnext/accounts/services/child_item_update.py # erpnext/public/js/utils.js --- .../accounts/services/child_item_update.py | 618 ++++++++++++++++++ .../purchase_order/test_purchase_order.py | 53 +- erpnext/public/js/utils.js | 30 + .../doctype/sales_order/test_sales_order.py | 111 ++++ 4 files changed, 809 insertions(+), 3 deletions(-) create mode 100644 erpnext/accounts/services/child_item_update.py diff --git a/erpnext/accounts/services/child_item_update.py b/erpnext/accounts/services/child_item_update.py new file mode 100644 index 00000000000..d66c5621f7a --- /dev/null +++ b/erpnext/accounts/services/child_item_update.py @@ -0,0 +1,618 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +"""Child item update service: ChildItemUpdater class and helpers for the update_child_qty_rate API.""" + +import frappe +from frappe import _ +from frappe.model.workflow import get_workflow_name +from frappe.utils import flt, get_link_to_form, getdate + +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions +from erpnext.buying.utils import update_last_purchase_rate +from erpnext.stock.doctype.packed_item.packed_item import make_packing_list +from erpnext.stock.get_item_details import ( + get_bin_details, + get_conversion_factor, + get_item_warehouse_, +) +from erpnext.stock.utils import ( + is_group_warehouse, + validate_disabled_warehouse, + validate_warehouse_company, +) + + +class ChildItemUpdater: + """Validates and applies item-level edits on submitted orders and quotations.""" + + def __init__(self, parent_doctype: str, parent_doctype_name: str, child_docname: str = "items"): + self.parent_doctype = parent_doctype + self.parent_doctype_name = parent_doctype_name + self.child_docname = child_docname + self.parent = frappe.get_doc(parent_doctype, parent_doctype_name) + self.allow_zero_qty = get_allow_zero_qty(parent_doctype) + self._ordered_items: dict | None = None + self._purchased_items: dict | None = None + + def update(self, trans_items: str | list) -> None: + """Process item additions, edits, and deletions from trans_items JSON.""" + from erpnext.buying.doctype.supplier_quotation.supplier_quotation import get_purchased_items + from erpnext.selling.doctype.quotation.mapper import get_ordered_items + + data = frappe.parse_json(trans_items) + any_qty_changed = False + items_added_or_removed = False + any_conversion_factor_changed = False + + self._check_permissions("write") + + if self.parent_doctype == "Quotation": + self._ordered_items = get_ordered_items(self.parent.name) + items_added_or_removed |= validate_and_delete_children(self.parent, data, self._ordered_items) + elif self.parent_doctype == "Supplier Quotation": + self._purchased_items = get_purchased_items(self.parent.name) + items_added_or_removed |= validate_and_delete_children(self.parent, data, self._purchased_items) + else: + items_added_or_removed |= validate_and_delete_children(self.parent, data) + + for d in data: + new_child_flag = False + rate_unchanged = None + + if not d.get("item_code"): + continue + + if not d.get("docname"): + new_child_flag = True + items_added_or_removed = True + self._check_permissions("create") + child_item = self._get_new_child_item(d) + else: + self._check_permissions("write") + child_item = frappe.get_doc(self.parent_doctype + " Item", d.get("docname")) + + change_state = get_child_item_change_state(self.parent_doctype, child_item, d) + rate_unchanged = change_state.rate_unchanged + any_conversion_factor_changed |= not change_state.conversion_factor_unchanged + if is_child_item_unchanged(change_state): + continue + + self._validate_quantity_and_rate(child_item, d, rate_unchanged) + + if flt(child_item.get("qty")) != flt(d.get("qty")): + any_qty_changed = True + + if self.parent.doctype in ("Sales Order", "Purchase Order") and self.parent.is_subcontracted: + self._validate_fg_item_for_subcontracting(d, new_child_flag) + child_item.fg_item_qty = flt(d["fg_item_qty"]) + if new_child_flag: + child_item.fg_item = d["fg_item"] + + child_item.qty = flt(d.get("qty")) + child_item.description = d.get("description") + update_child_item_rate_and_discount( + self.parent_doctype, child_item, d, self.allow_zero_qty, rate_unchanged=rate_unchanged + ) + update_child_item_uom_and_weight(child_item, d) + + if d.get("delivery_date") and self.parent_doctype == "Sales Order": + child_item.delivery_date = d.get("delivery_date") + + if d.get("schedule_date") and self.parent_doctype == "Purchase Order": + child_item.schedule_date = d.get("schedule_date") + + if d.get("bom_no") and self.parent_doctype == "Sales Order": + child_item.bom_no = d.get("bom_no") + + child_item.flags.ignore_validate_update_after_submit = True + if new_child_flag: + self.parent.load_from_db() + child_item.idx = len(self.parent.items) + 1 + child_item.insert() + else: + child_item.save(ignore_permissions=True) + + self._post_update(any_qty_changed, items_added_or_removed, any_conversion_factor_changed) + + def _post_update( + self, any_qty_changed: bool, items_added_or_removed: bool, any_conversion_factor_changed: bool + ) -> None: + parent = self.parent + parent.reload() + parent.flags.ignore_validate_update_after_submit = True + parent.set_qty_as_per_stock_uom() + parent.calculate_taxes_and_totals() + parent.set_total_in_words() + + if self.parent_doctype == "Sales Order" and not parent.is_subcontracted: + make_packing_list(parent) + parent.set_gross_profit() + + frappe.get_cached_doc("Authorization Control").validate_approving_authority( + parent.doctype, parent.company, parent.base_grand_total + ) + + if self.parent_doctype != "Supplier Quotation": + from erpnext.accounts.services.payment_schedule import PaymentScheduleService + + PaymentScheduleService(parent).set_payment_schedule() + + if self.parent_doctype == "Purchase Order": + parent.validate_minimum_order_qty() + parent.validate_budget() + if parent.is_against_so(): + parent.update_status_updater() + elif self.parent_doctype == "Sales Order": + parent.check_credit_limit() + + for idx, row in enumerate(parent.get(self.child_docname), start=1): + row.idx = idx + + parent.save() + + if self.parent_doctype == "Purchase Order": + update_last_purchase_rate(parent, is_submit=1) + + if any_qty_changed or items_added_or_removed or any_conversion_factor_changed: + parent.update_prevdoc_status() + + parent.update_requested_qty() + parent.update_ordered_qty() + parent.update_ordered_and_reserved_qty() + parent.update_receiving_percentage() + + if parent.is_subcontracted and not parent.can_update_items(): + frappe.throw( + _( + "Items cannot be updated as Subcontracting Order is created against the Purchase Order {0}." + ).format(frappe.bold(parent.name)) + ) + + elif self.parent_doctype == "Sales Order": + if parent.is_subcontracted and not parent.can_update_items(): + frappe.throw( + _( + "Items cannot be updated as Subcontracting Inward Order(s) exist against this Subcontracted Sales Order." + ) + ) + parent.validate_selling_price() + parent.validate_for_duplicate_items() + parent.validate_warehouse() + parent.update_reserved_qty() + parent.update_project() + parent.update_prevdoc_status("submit") + parent.update_delivery_status() + + parent.reload() + self._validate_workflow() + + if self.parent_doctype in ("Purchase Order", "Sales Order"): + parent.update_blanket_order() + parent.update_billing_percentage() + parent.set_status() + + parent.validate_uom_is_integer("uom", "qty") + parent.validate_uom_is_integer("stock_uom", "stock_qty") + + if self.parent_doctype == "Sales Order" and not parent.is_subcontracted: + from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( + cancel_stock_reservation_entries, + has_reserved_stock, + ) + + if has_reserved_stock(parent.doctype, parent.name): + cancel_stock_reservation_entries(parent.doctype, parent.name) + if parent.per_picked == 0: + parent.create_stock_reservation_entries() + + def _check_permissions(self, perm_type: str = "create") -> None: + try: + self.parent.check_permission(perm_type) + except frappe.PermissionError: + actions = {"create": "add", "write": "update"} + frappe.throw( + _("You do not have permissions to {0} items in a {1}.").format( + actions[perm_type], self.parent_doctype + ), + title=_("Insufficient Permissions"), + ) + + def _validate_workflow(self) -> None: + workflow = get_workflow_name(self.parent.doctype) + if not workflow: + return + + workflow_doc = frappe.get_doc("Workflow", workflow) + current_state = self.parent.get(workflow_doc.workflow_state_field) + roles = frappe.get_roles() + + allowed = any( + state.state == current_state and (not state.allow_edit or state.allow_edit in roles) + for state in workflow_doc.states + ) + + if not allowed: + frappe.throw( + _("You are not allowed to update as per the conditions set in {0} Workflow.").format( + get_link_to_form("Workflow", workflow) + ), + title=_("Insufficient Permissions"), + ) + + def _get_new_child_item(self, item_row) -> "frappe.model.document.Document": + child_doctype = self.parent_doctype + " Item" + return set_order_defaults( + self.parent_doctype, + self.parent_doctype_name, + child_doctype, + self.child_docname, + item_row, + ) + + def _validate_quantity_and_rate(self, child_item, new_data: dict, rate_unchanged: bool | None) -> None: + if not flt(new_data.get("qty")) and not self.allow_zero_qty: + frappe.throw( + _("Row #{0}:Quantity for Item {1} cannot be zero.").format( + new_data.get("idx"), frappe.bold(new_data.get("item_code")) + ), + title=_("Invalid Qty"), + ) + + qty_limits = { + "Sales Order": ("delivered_qty", _("Cannot set quantity less than delivered quantity.")), + "Purchase Order": ("received_qty", _("Cannot set quantity less than received quantity.")), + } + + if self.parent_doctype in qty_limits: + qty_field, error_message = qty_limits[self.parent_doctype] + if flt(new_data.get("qty")) < flt(child_item.get(qty_field)): + frappe.throw( + _("Row #{0}:").format(new_data.get("idx")) + error_message, + title=_("Invalid Qty"), + ) + + if self.parent_doctype not in ("Quotation", "Supplier Quotation"): + return + + items_map = self._ordered_items if self.parent_doctype == "Quotation" else self._purchased_items + if not items_map: + return + + qty_to_check = items_map.get(child_item.name) + if not qty_to_check: + return + + if not rate_unchanged: + frappe.throw( + _( + "Cannot update rate as item {0} is already ordered or purchased against this quotation" + ).format(frappe.bold(new_data.get("item_code"))) + ) + + if flt(new_data.get("qty")) < qty_to_check: + frappe.throw(_("Cannot reduce quantity than ordered or purchased quantity")) + + def _validate_fg_item_for_subcontracting(self, new_data: dict, is_new: bool) -> None: + if is_new: + if not new_data.get("fg_item"): + frappe.throw( + _("Finished Good Item is not specified for service item {0}").format( + new_data["item_code"] + ) + ) + + is_sub_contracted_item, default_bom = frappe.db.get_value( + "Item", new_data["fg_item"], ["is_sub_contracted_item", "default_bom"] + ) + + if not is_sub_contracted_item: + frappe.throw( + _("Finished Good Item {0} must be a sub-contracted item").format(new_data["fg_item"]) + ) + elif not default_bom: + frappe.throw(_("Default BOM not found for FG Item {0}").format(new_data["fg_item"])) + + if not new_data.get("fg_item_qty"): + frappe.throw(_("Finished Good Item {0} Qty can not be zero").format(new_data["fg_item"])) + + +@frappe.whitelist() +def update_child_qty_rate( + parent_doctype: str, trans_items: str | list, parent_doctype_name: str, child_docname: str = "items" +) -> None: + ChildItemUpdater(parent_doctype, parent_doctype_name, child_docname).update(trans_items) + + +def set_order_defaults( + parent_doctype: str, + parent_doctype_name: str, + child_doctype: str, + child_docname: str, + trans_item: dict, +) -> "frappe.model.document.Document": + """Return a new child item populated with item master defaults.""" + from erpnext.accounts.services.taxes import add_taxes_from_tax_template, set_child_tax_template_and_map + + p_doc = frappe.get_doc(parent_doctype, parent_doctype_name) + child_item = frappe.new_doc(child_doctype, parent_doc=p_doc, parentfield=child_docname) + item = frappe.get_doc("Item", trans_item.get("item_code")) + + for field in ("item_code", "item_name", "description", "item_group", "weight_per_unit", "weight_uom"): + child_item.update({field: item.get(field)}) + + date_fieldname = "delivery_date" if child_doctype == "Sales Order Item" else "schedule_date" + child_item.update({date_fieldname: trans_item.get(date_fieldname) or p_doc.get(date_fieldname)}) + child_item.stock_uom = item.stock_uom + child_item.uom = trans_item.get("uom") or item.stock_uom + child_item.warehouse = get_new_child_item_warehouse(p_doc, item, trans_item, child_doctype) + 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"): + child_item.base_rate = 1 + child_item.base_amount = 1 + + set_child_tax_template_and_map(item, child_item, p_doc) + add_taxes_from_tax_template(child_item, p_doc) + return child_item + + +def get_new_child_item_warehouse(p_doc, item, trans_item: dict, child_doctype: str) -> str | None: + """Return the warehouse picked in the Update Items dialog, else the configured default. + + Validates whichever warehouse was resolved, since a submitted parent skips validate(). + """ + warehouse = trans_item.get("warehouse") or get_item_warehouse_(p_doc, item, overwrite_warehouse=True) + + if not warehouse: + if is_warehouse_required_for_new_child_item(child_doctype, item, trans_item): + frappe.throw( + _( + "Cannot find a default warehouse for item {0}. Please select one in the Update Items dialog, or set a default in the Item Master or in the Company." + ).format(frappe.bold(item.item_code)) + ) + return None + + validate_warehouse_company(warehouse, p_doc.company) + validate_disabled_warehouse(warehouse) + is_group_warehouse(warehouse) + return warehouse + + +def is_warehouse_required_for_new_child_item(child_doctype: str, item, trans_item: dict) -> bool: + """Sales Order always needs one; buying documents only for stock rows, as in validate_stock_item_warehouse.""" + if child_doctype == "Sales Order Item": + return True + + if child_doctype in ("Purchase Order Item", "Supplier Quotation Item"): + return bool(item.is_stock_item and flt(trans_item.get("qty")) and not item.delivered_by_supplier) + + return False + + +def validate_child_on_delete(row, parent, ordered_item=None) -> None: + """Raise if a partially transacted child item is being deleted.""" + if parent.doctype == "Sales Order": + if flt(row.delivered_qty): + frappe.throw( + _("Row #{0}: Cannot delete item {1} which has already been delivered").format( + row.idx, row.item_code + ) + ) + if flt(row.work_order_qty): + frappe.throw( + _("Row #{0}: Cannot delete item {1} which has work order assigned to it.").format( + row.idx, row.item_code + ) + ) + if flt(row.ordered_qty): + frappe.throw( + _( + "Row #{0}: Cannot delete item {1} which is already ordered against this Sales Order." + ).format(row.idx, row.item_code) + ) + + if parent.doctype == "Purchase Order" and flt(row.received_qty): + frappe.throw( + _("Row #{0}: Cannot delete item {1} which has already been received").format( + row.idx, row.item_code + ) + ) + + if parent.doctype in ("Purchase Order", "Sales Order") and flt(row.billed_amt): + frappe.throw( + _("Row #{0}: Cannot delete item {1} which has already been billed.").format( + row.idx, row.item_code + ) + ) + + if parent.doctype == "Quotation" and ordered_item and ordered_item.get(row.name): + frappe.throw(_("Cannot delete an item which has been ordered")) + + +def update_bin_on_delete(row, doctype: str) -> None: + """Update bin quantities after a child item row is deleted.""" + from erpnext.stock.stock_balance import ( + get_indented_qty, + get_ordered_qty, + get_reserved_qty, + update_bin_qty, + ) + + qty_dict = {} + + if doctype == "Sales Order": + qty_dict["reserved_qty"] = get_reserved_qty(row.item_code, row.warehouse) + else: + if row.material_request_item: + qty_dict["indented_qty"] = get_indented_qty(row.item_code, row.warehouse) + qty_dict["ordered_qty"] = get_ordered_qty(row.item_code, row.warehouse) + + if row.warehouse: + update_bin_qty(row.item_code, row.warehouse, qty_dict) + + +def validate_and_delete_children(parent, data, ordered_item=None) -> bool: + """Delete child rows not present in data; return True if any were removed.""" + updated_item_names = [d.get("docname") for d in data] + deleted_children = [item for item in parent.items if item.name not in updated_item_names] + + for d in deleted_children: + validate_child_on_delete(d, parent, ordered_item) + d.flags.ignore_permissions = True + d.cancel() + d.delete() + + if parent.doctype == "Purchase Order": + parent.update_ordered_qty_in_so_for_removed_items(deleted_children) + + if parent.doctype not in ("Quotation", "Supplier Quotation"): + parent.update_prevdoc_status() + for d in deleted_children: + update_bin_on_delete(d, parent.doctype) + + return bool(deleted_children) + + +def get_allow_zero_qty(parent_doctype: str) -> bool: + if parent_doctype == "Sales Order": + return frappe.db.get_single_value("Selling Settings", "allow_zero_qty_in_sales_order") or False + if parent_doctype == "Purchase Order": + return frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_purchase_order") or False + return False + + +def get_child_item_change_state(parent_doctype: str, child_item, new_data) -> frappe._dict: + prev_rate, new_rate = flt(child_item.get("rate")), flt(new_data.get("rate")) + prev_qty, new_qty = flt(child_item.get("qty")), flt(new_data.get("qty")) + prev_fg_qty, new_fg_qty = flt(child_item.get("fg_item_qty")), flt(new_data.get("fg_item_qty")) + prev_con_fac = flt(child_item.get("conversion_factor")) + new_con_fac = flt(new_data.get("conversion_factor")) + + if parent_doctype == "Sales Order": + prev_date, new_date = child_item.get("delivery_date"), new_data.get("delivery_date") + elif parent_doctype == "Purchase Order": + prev_date, new_date = child_item.get("schedule_date"), new_data.get("schedule_date") + else: + prev_date, new_date = None, None + + if parent_doctype in ("Quotation", "Supplier Quotation"): + date_unchanged = False + else: + prev_date = getdate(prev_date) if prev_date else None + new_date = getdate(new_date) if new_date else None + date_unchanged = prev_date == new_date + + return frappe._dict( + rate_unchanged=prev_rate == new_rate, + qty_unchanged=prev_qty == new_qty, + fg_qty_unchanged=prev_fg_qty == new_fg_qty, + uom_unchanged=child_item.get("uom") == new_data.get("uom"), + conversion_factor_unchanged=prev_con_fac == new_con_fac, + date_unchanged=date_unchanged, + description_unchanged=child_item.get("description") == new_data.get("description"), + ) + + +def is_child_item_unchanged(change_state: frappe._dict) -> bool: + return ( + change_state.rate_unchanged + and change_state.qty_unchanged + and change_state.fg_qty_unchanged + and change_state.conversion_factor_unchanged + and change_state.uom_unchanged + and change_state.date_unchanged + and change_state.description_unchanged + ) + + +def update_child_item_rate_and_discount( + parent_doctype: str, + child_item, + new_data, + allow_zero_qty: bool, + rate_unchanged: bool | None = None, +) -> None: + rate_precision = child_item.precision("rate") or 2 + qty_precision = child_item.precision("qty") or 2 + + if rate_unchanged is None: + rate_unchanged = flt(child_item.get("rate")) == flt(new_data.get("rate")) + + if not rate_unchanged and not child_item.get("qty") and allow_zero_qty: + frappe.throw(_("Rate of '{0}' items cannot be changed").format(frappe.bold(_("Unit Price")))) + + row_rate = flt(new_data.get("rate"), rate_precision) + + if parent_doctype in ("Purchase Order", "Sales Order"): + amount_below_billed_amt = flt(child_item.billed_amt, rate_precision) > flt( + row_rate * flt(new_data.get("qty"), qty_precision), rate_precision + ) + if amount_below_billed_amt and row_rate > 0.0: + frappe.throw( + _( + "Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}." + ).format(child_item.idx, child_item.item_code) + ) + + child_item.rate = row_rate + + if parent_doctype not in ("Sales Order", "Purchase Order") or not flt(child_item.price_list_rate): + return + + if flt(child_item.rate) > flt(child_item.price_list_rate): + child_item.discount_percentage = 0 + child_item.discount_amount = 0 + child_item.margin_type = "Amount" + child_item.margin_rate_or_amount = flt( + child_item.rate - child_item.price_list_rate, + child_item.precision("margin_rate_or_amount"), + ) + child_item.rate_with_margin = child_item.rate + else: + child_item.margin_type = "" + child_item.margin_rate_or_amount = 0 + child_item.rate_with_margin = child_item.price_list_rate + child_item.discount_percentage = 0 + child_item.discount_amount = flt(child_item.rate_with_margin) - flt(child_item.rate) + + +def update_child_item_uom_and_weight(child_item, new_data) -> None: + conv_fac_precision = child_item.precision("conversion_factor") or 2 + + if new_data.get("conversion_factor"): + if child_item.stock_uom == child_item.uom: + child_item.conversion_factor = 1 + else: + child_item.conversion_factor = flt(new_data.get("conversion_factor"), conv_fac_precision) + + if new_data.get("uom"): + child_item.uom = new_data.get("uom") + conversion_factor = flt( + get_conversion_factor(child_item.item_code, child_item.uom).get("conversion_factor") + ) + child_item.conversion_factor = ( + flt(new_data.get("conversion_factor"), conv_fac_precision) or conversion_factor + ) + + if child_item.get("weight_per_unit"): + child_item.total_weight = flt( + child_item.weight_per_unit * child_item.qty * child_item.conversion_factor, + child_item.precision("total_weight"), + ) + + +def check_if_child_table_updated( + child_table_before_update, child_table_after_update, fields_to_check +) -> bool: + """Return True if any accounting-relevant field changed in a child table.""" + fields_to_check = list(fields_to_check) + get_accounting_dimensions() + ["cost_center", "project"] + + for index, item in enumerate(child_table_before_update): + for field in fields_to_check: + if child_table_after_update[index].get(field) != item.get(field): + return True + + return False diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index b021925bd9d..423338b39ac 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -266,6 +266,7 @@ class TestPurchaseOrder(ERPNextTestSuite): po.load_from_db() existing_ordered_qty = get_ordered_qty() + existing_ordered_qty_in_new_warehouse = get_ordered_qty(warehouse="_Test Warehouse 2 - _TC") first_item_of_po = po.get("items")[0] trans_item = json.dumps( @@ -276,16 +277,62 @@ class TestPurchaseOrder(ERPNextTestSuite): "qty": first_item_of_po.qty, "docname": first_item_of_po.name, }, - {"item_code": "_Test Item", "rate": 200, "qty": 7}, + {"item_code": "_Test Item", "rate": 200, "qty": 7, "warehouse": "_Test Warehouse 2 - _TC"}, ] ) update_child_qty_rate("Purchase Order", trans_item, po.name) po.reload() self.assertEqual(len(po.get("items")), 2) + self.assertEqual(po.get("items")[-1].warehouse, "_Test Warehouse 2 - _TC") self.assertEqual(po.status, "To Receive and Bill") - # ordered qty should increase on row addition - self.assertEqual(get_ordered_qty(), existing_ordered_qty + 7) + # ordered qty should increase on row addition, in the warehouse passed for the new row + self.assertEqual(get_ordered_qty(), existing_ordered_qty) + self.assertEqual( + get_ordered_qty(warehouse="_Test Warehouse 2 - _TC"), + existing_ordered_qty_in_new_warehouse + 7, + ) + + def test_update_child_adding_new_item_without_any_default_warehouse(self): + stock_item = make_item("_Test PO Item Without Default Warehouse", {"is_stock_item": 1}).name + service_item = make_item("_Test PO Item Non Stock", {"is_stock_item": 0}).name + + po = create_purchase_order(do_not_save=1) + po.save() + po.submit() + first_item_of_po = po.get("items")[0] + + company_default = frappe.db.get_value("Company", po.company, "default_warehouse") + frappe.db.set_value("Company", po.company, "default_warehouse", None) + self.addCleanup(frappe.db.set_value, "Company", po.company, "default_warehouse", company_default) + + def get_trans_items(item_code): + return json.dumps( + [ + { + "item_code": first_item_of_po.item_code, + "rate": first_item_of_po.rate, + "qty": first_item_of_po.qty, + "docname": first_item_of_po.name, + }, + {"item_code": item_code, "rate": 200, "qty": 7}, + ] + ) + + self.assertRaisesRegex( + frappe.ValidationError, + "Cannot find a default warehouse", + update_child_qty_rate, + "Purchase Order", + get_trans_items(stock_item), + po.name, + ) + + update_child_qty_rate("Purchase Order", get_trans_items(service_item), po.name) + + po.reload() + self.assertEqual(po.get("items")[-1].item_code, service_item) + self.assertFalse(po.get("items")[-1].warehouse) def test_update_child_removing_item(self): po = create_purchase_order(do_not_save=1) diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 9f9da2d9a94..3514c89b469 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -730,6 +730,7 @@ erpnext.utils.update_child_items = function (opts) { qty: d.qty, rate: d.rate, uom: d.uom, + warehouse: d.warehouse, fg_item: d.fg_item, fg_item_qty: d.fg_item_qty, description: d.description, @@ -822,6 +823,7 @@ erpnext.utils.update_child_items = function (opts) { item_name, bom_no, description, + warehouse, } = r.message; const row = dialog.fields_dict.trans_items.df.data.find( (row) => row.name == me.doc.name @@ -835,6 +837,7 @@ erpnext.utils.update_child_items = function (opts) { item_name: item_name, bom_no: bom_no, description: me.doc.description || description, + warehouse: me.doc.docname ? me.doc.warehouse : warehouse, }); dialog.fields_dict.trans_items.grid.refresh(); } @@ -922,11 +925,38 @@ erpnext.utils.update_child_items = function (opts) { }); } +<<<<<<< HEAD if ( ["Purchase Order", "Sales Order"].includes(frm.doc.doctype) && frm.doc.is_subcontracted && !frm.doc.is_old_subcontracting_flow ) { +======= + const warehouse_df = child_meta.fields.find((f) => f.fieldname == "warehouse"); + if (warehouse_df) { + fields.splice(3, 0, { + fieldtype: "Link", + fieldname: "warehouse", + options: "Warehouse", + in_list_view: 1, + label: __(warehouse_df.label), + // only new rows may set it, existing rows would leave their + // reserved qty stranded in the previous warehouse's bin + read_only_depends_on: "eval:doc.docname", + get_query: () => { + return { + filters: { + company: frm.doc.company, + is_group: 0, + disabled: 0, + }, + }; + }, + }); + } + + if (["Purchase Order", "Sales Order"].includes(frm.doc.doctype) && frm.doc.is_subcontracted) { +>>>>>>> 55fe269046 (fix: allow selecting a warehouse for new items in the update items dialog (#57876)) fields.push( { fieldtype: "Link", diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index f45632573b0..25b1cf6887b 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -33,6 +33,7 @@ from erpnext.selling.doctype.sales_order.sales_order import ( from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.get_item_details import get_bin_details +from erpnext.stock.utils import InvalidWarehouseCompany from erpnext.tests.utils import ERPNextTestSuite @@ -584,6 +585,116 @@ class TestSalesOrder(ERPNextTestSuite): self.assertEqual(updated_total, prev_total + 1400) self.assertNotEqual(updated_total_in_words, prev_total_in_words) + def test_update_child_adding_new_item_with_warehouse(self): + so = make_sales_order(item_code="_Test Item", qty=4) + + first_item_of_so = so.get("items")[0] + self.assertNotEqual(first_item_of_so.warehouse, "_Test Warehouse 2 - _TC") + + def get_trans_item(warehouse): + return json.dumps( + [ + { + "item_code": first_item_of_so.item_code, + "rate": first_item_of_so.rate, + "qty": first_item_of_so.qty, + "docname": first_item_of_so.name, + "warehouse": warehouse, + }, + {"item_code": "_Test Item 2", "rate": 200, "qty": 7, "warehouse": warehouse}, + ] + ) + + self.assertRaises( + InvalidWarehouseCompany, + update_child_qty_rate, + "Sales Order", + get_trans_item("_Test Warehouse 2 - _TC1"), + so.name, + ) + + self.assertRaisesRegex( + frappe.ValidationError, + "Group node warehouse", + update_child_qty_rate, + "Sales Order", + get_trans_item("_Test Warehouse Group - _TC"), + so.name, + ) + + if not frappe.db.exists("Warehouse", "_Test Disabled Warehouse - _TC"): + frappe.get_doc( + { + "doctype": "Warehouse", + "warehouse_name": "_Test Disabled Warehouse", + "company": "_Test Company", + "disabled": 1, + } + ).insert() + + self.assertRaisesRegex( + frappe.ValidationError, + "Disabled Warehouse", + update_child_qty_rate, + "Sales Order", + get_trans_item("_Test Disabled Warehouse - _TC"), + so.name, + ) + + update_child_qty_rate("Sales Order", get_trans_item("_Test Warehouse 2 - _TC"), so.name) + + so.reload() + # the new row picks up the warehouse selected in the dialog + self.assertEqual(so.get("items")[-1].item_code, "_Test Item 2") + self.assertEqual(so.get("items")[-1].warehouse, "_Test Warehouse 2 - _TC") + # existing rows keep theirs, so their reserved qty stays in the same bin + self.assertEqual(so.get("items")[0].warehouse, first_item_of_so.warehouse) + + def test_update_child_adding_new_item_without_any_default_warehouse(self): + item_code = make_item("_Test Item Without Default Warehouse", {"is_stock_item": 1}).name + so = make_sales_order(item_code="_Test Item", qty=4) + existing_item = so.get("items")[0] + + # a company gets a default warehouse when its warehouses are created + company_default = frappe.db.get_value("Company", so.company, "default_warehouse") + frappe.db.set_value("Company", so.company, "default_warehouse", None) + self.addCleanup(frappe.db.set_value, "Company", so.company, "default_warehouse", company_default) + + def get_trans_items(warehouse=None): + new_row = {"item_code": item_code, "rate": 200, "qty": 7} + if warehouse: + new_row["warehouse"] = warehouse + + return json.dumps( + [ + { + "item_code": existing_item.item_code, + "rate": existing_item.rate, + "qty": existing_item.qty, + "docname": existing_item.name, + }, + new_row, + ] + ) + + # no default in the Item Master, Item Group, Brand or Company + self.assertRaisesRegex( + frappe.ValidationError, + "Cannot find a default warehouse", + update_child_qty_rate, + "Sales Order", + get_trans_items(), + so.name, + ) + + update_child_qty_rate("Sales Order", get_trans_items("_Test Warehouse - _TC"), so.name) + + so.reload() + self.assertEqual(len(so.get("items")), 2) + self.assertEqual(so.get("items")[0].warehouse, existing_item.warehouse) + self.assertEqual(so.get("items")[-1].item_code, item_code) + self.assertEqual(so.get("items")[-1].warehouse, "_Test Warehouse - _TC") + def test_update_child_removing_item(self): so = make_sales_order(**{"item_list": [{"item_code": "_Test Item", "qty": 5, "rate": 1000}]}) create_dn_against_so(so.name, 2) From 39f15bb3e9ca020586e99e0c52d249db48653573 Mon Sep 17 00:00:00 2001 From: pandiyan Date: Sun, 9 Aug 2026 11:25:12 +0530 Subject: [PATCH 034/134] fix: tolerate floating-point drift in sales team allocated percentage the total of allocated_percentage was compared to 100 with exact float equality, so a correct allocation could be rejected when the sum drifts in binary floating point (10.0 + 58.02 + 31.98 -> 100.00000000000001). round the total to the field precision before comparing, in both SellingController.calculate_contribution and Customer.validate. (cherry picked from commit f7b277582940cbb5f7a427ef8250b18db773004b) --- erpnext/controllers/selling_controller.py | 2 +- erpnext/selling/doctype/customer/customer.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 6cdcb7090d2..51c821df93f 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -253,7 +253,7 @@ class SellingController(StockController): total += sales_person.allocated_percentage - if sales_team and total != 100.0: + if sales_team and flt(total, self.precision("allocated_percentage", "sales_team")) != 100.0: throw(_("Total allocated percentage for sales team should be 100")) def validate_sales_team(self, sales_team): diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 126145792dd..76495a94959 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -194,7 +194,8 @@ class Customer(TransactionBase): self.loyalty_program_tier = customer.loyalty_program_tier if self.sales_team: - if sum(member.allocated_percentage or 0 for member in self.sales_team) != 100: + total = sum(flt(member.allocated_percentage) for member in self.sales_team) + if flt(total, self.precision("allocated_percentage", "sales_team")) != 100: frappe.throw(_("Total contribution percentage should be equal to 100")) @frappe.whitelist() From ee9026d62d956049a738a63abe055906100ff977 Mon Sep 17 00:00:00 2001 From: pandiyan Date: Sun, 9 Aug 2026 11:25:24 +0530 Subject: [PATCH 035/134] test: sales team allocation totalling 100 in floating point covers the case where the percentages are correct but the accumulated sum is 100.00000000000001. two rows can never drift, since the second reconstructs exactly as 100 - first, so the case needs three rows. (cherry picked from commit 4afba94d1c3dffec6cb789f69f537883b88dc32d) # Conflicts: # erpnext/selling/doctype/sales_order/test_sales_order.py --- .../doctype/sales_order/test_sales_order.py | 276 ++++++++++++++++++ 1 file changed, 276 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index f45632573b0..36fcf226059 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -2890,6 +2890,282 @@ class TestSalesOrder(ERPNextTestSuite): so = make_sales_order(item_code=fg_item, qty=10, rate=50, warehouse=fg_warehouse, do_not_save=1) self.assertRaises(frappe.ValidationError, so.save) +<<<<<<< HEAD +======= + @ERPNextTestSuite.change_settings( + "Stock Settings", {"enable_stock_reservation": 1, "use_serial_batch_fields": 0} + ) + def test_product_bundle_reservation(self): + pb_item = make_item("Product Bundle Item", {"is_stock_item": 0}) + simple_item = make_item("Simple Item", {"is_stock_item": 1}) + sb_item = make_item( + "Serial Batch Item", + { + "is_stock_item": 1, + "has_serial_no": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BAT-TSBIFRM-.#####", + "serial_no_series": "SN-TSBIFRM-.#####", + }, + ) + make_product_bundle(pb_item.name, [simple_item.name, sb_item.name]) + + warehouse = "_Test Warehouse - _TC" + + make_stock_entry( + item_code=simple_item.name, + target=warehouse, + qty=10, + ) + + # two different stock entries on purpose to get two batches + make_stock_entry( + item_code=sb_item.name, + target=warehouse, + qty=5, + ) + make_stock_entry( + item_code=sb_item.name, + target=warehouse, + qty=5, + ) + + so = make_sales_order(item_code=pb_item.name, do_not_submit=1) + so.reserve_stock = 1 + for item in so.packed_items: + item.reserve_stock = 1 + so.submit() + + from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( + get_sre_reserved_batch_nos_details, + get_sre_reserved_qty_for_voucher_detail_no, + get_sre_reserved_serial_nos_details, + ) + + for item in so.packed_items: + self.assertEqual( + get_sre_reserved_qty_for_voucher_detail_no(item.item_code, "Sales Order", so.name, item.name), + item.qty, + ) + + sre_serial_nos = list(get_sre_reserved_serial_nos_details(sb_item.name, warehouse).keys()) + sre_batch_nos = list(get_sre_reserved_batch_nos_details(sb_item.name, warehouse).keys()) + + dn = make_delivery_note(so.name, kwargs={"for_reserved_stock": True}) + dn.save() + + self.assertTrue(dn.packed_items[1].serial_and_batch_bundle) + + from erpnext.stock.serial_batch_bundle import get_batches_from_bundle, get_serial_nos + + serial_nos_in_bundle = get_serial_nos(dn.packed_items[1].serial_and_batch_bundle) + batches_in_bundle = list(get_batches_from_bundle(dn.packed_items[1].serial_and_batch_bundle).keys()) + + self.assertEqual(sre_serial_nos, serial_nos_in_bundle) + self.assertEqual(sre_batch_nos, batches_in_bundle) + + dn.items[0].qty = 5 + dn.save() + sabb_doc = frappe.get_doc("Serial and Batch Bundle", dn.packed_items[1].serial_and_batch_bundle) + sabb_doc.entries = sabb_doc.entries[:5] + sabb_doc.company = dn.company + sabb_doc.save() + dn.submit() + + serial_nos = set(sre_serial_nos) - set(get_serial_nos(sabb_doc.name)) + batch_nos = set(sre_batch_nos) - set(get_batches_from_bundle(sabb_doc.name).keys()) + + dn1 = make_delivery_note(so.name, kwargs={"for_reserved_stock": True}) + dn1.save() + + self.assertTrue(dn1.packed_items[1].serial_and_batch_bundle) + + from erpnext.stock.serial_batch_bundle import get_batches_from_bundle, get_serial_nos + + serial_nos_in_bundle = set(get_serial_nos(dn1.packed_items[1].serial_and_batch_bundle)) + batches_in_bundle = set(get_batches_from_bundle(dn1.packed_items[1].serial_and_batch_bundle).keys()) + + self.assertEqual(serial_nos, serial_nos_in_bundle) + self.assertEqual(batch_nos, batches_in_bundle) + + dn.cancel() + + # test the same thing with sales invoice as well + + si = make_sales_invoice(so.name) + si.update_stock = 1 + si.save() + + self.assertTrue(si.packed_items[1].serial_and_batch_bundle) + + from erpnext.stock.serial_batch_bundle import get_batches_from_bundle, get_serial_nos + + serial_nos_in_bundle = get_serial_nos(si.packed_items[1].serial_and_batch_bundle) + batches_in_bundle = list(get_batches_from_bundle(si.packed_items[1].serial_and_batch_bundle).keys()) + + self.assertEqual(sre_serial_nos, serial_nos_in_bundle) + self.assertEqual(sre_batch_nos, batches_in_bundle) + + si.items[0].qty = 5 + si.save() + sabb_doc = frappe.get_doc("Serial and Batch Bundle", si.packed_items[1].serial_and_batch_bundle) + sabb_doc.entries = sabb_doc.entries[:5] + sabb_doc.company = si.company + sabb_doc.save() + si.submit() + + serial_nos = set(sre_serial_nos) - set(get_serial_nos(sabb_doc.name)) + batch_nos = set(sre_batch_nos) - set(get_batches_from_bundle(sabb_doc.name).keys()) + + si1 = make_delivery_note(so.name, kwargs={"for_reserved_stock": True}) + si1.save() + + self.assertTrue(si1.packed_items[1].serial_and_batch_bundle) + + from erpnext.stock.serial_batch_bundle import get_batches_from_bundle, get_serial_nos + + serial_nos_in_bundle = set(get_serial_nos(si1.packed_items[1].serial_and_batch_bundle)) + batches_in_bundle = set(get_batches_from_bundle(si1.packed_items[1].serial_and_batch_bundle).keys()) + + self.assertEqual(serial_nos, serial_nos_in_bundle) + self.assertEqual(batch_nos, batches_in_bundle) + + def test_sales_team_contribution_follows_grant_commission(self): + """Sales-person allocation tracks the grant-commission-eligible amount, not the gross total. + + The Item "Grant Commission" flag includes an item in both Sales Partner and Sales Person + commission, so each sales person's allocated_amount is a share of + amount_eligible_for_commission rather than net_total. + """ + frappe.db.set_value("Item", "_Test Item", "grant_commission", 1) + frappe.db.set_value("Item", "_Test FG Item", "grant_commission", 0) + try: + so = make_sales_order( + do_not_save=True, + item_list=[ + {"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC", "qty": 10, "rate": 100}, + { + "item_code": "_Test FG Item", + "warehouse": "_Test Warehouse - _TC", + "qty": 10, + "rate": 100, + }, + ], + ) + so.append( + "sales_team", + {"sales_person": "_Test Sales Person 1", "allocated_percentage": 60, "commission_rate": 10}, + ) + so.append( + "sales_team", + {"sales_person": "_Test Sales Person 2", "allocated_percentage": 40, "commission_rate": 0}, + ) + so.save() + + self.assertEqual(so.net_total, 2000) + self.assertEqual(so.amount_eligible_for_commission, 1000) # only the grant_commission item + + first, second = so.sales_team + # allocation follows the eligible amount (1000), not net_total (2000) + self.assertEqual(first.allocated_amount, 600) + self.assertEqual(first.incentives, 60) # 600 * 10% + self.assertEqual(second.allocated_amount, 400) + self.assertEqual(second.incentives, 0) + finally: + # grant_commission defaults to 1 for both items; restore + frappe.db.set_value("Item", "_Test Item", "grant_commission", 1) + frappe.db.set_value("Item", "_Test FG Item", "grant_commission", 1) + + def test_sales_team_allocated_percentage_must_total_100(self): + with self.subTest("partial allocation is rejected"): + so = make_sales_order(do_not_save=True) + so.append("sales_team", {"sales_person": "_Test Sales Person 1", "allocated_percentage": 60}) + self.assertRaises(frappe.ValidationError, so.save) + + with self.subTest("allocation totalling 100 is accepted"): + so = make_sales_order(do_not_save=True) + so.append("sales_team", {"sales_person": "_Test Sales Person 1", "allocated_percentage": 60}) + so.append("sales_team", {"sales_person": "_Test Sales Person 2", "allocated_percentage": 40}) + so.save() + self.assertEqual(sum(d.allocated_percentage for d in so.sales_team), 100) + + with self.subTest("floating-point drift in the total is tolerated"): + # 10.0 + 58.02 + 31.98 accumulates to 100.00000000000001 in binary floating point + so = make_sales_order(do_not_save=True) + for sales_person, percentage in ( + ("_Test Sales Person", 10.0), + ("_Test Sales Person 1", 58.02), + ("_Test Sales Person 2", 31.98), + ): + so.append("sales_team", {"sales_person": sales_person, "allocated_percentage": percentage}) + so.save() + + def test_sales_team_disabled_sales_person_rejected(self): + frappe.db.set_value("Sales Person", "_Test Sales Person 2", "enabled", 0) + try: + so = make_sales_order(do_not_save=True) + so.append("sales_team", {"sales_person": "_Test Sales Person 2", "allocated_percentage": 100}) + self.assertRaises(frappe.ValidationError, so.save) + finally: + frappe.db.set_value("Sales Person", "_Test Sales Person 2", "enabled", 1) + + def test_sales_partner_commission(self): + """Sales Partner commission: total_commission = amount_eligible_for_commission * rate / 100.""" + frappe.db.set_value("Item", "_Test Item", "grant_commission", 1) + try: + so = make_sales_order(qty=10, rate=100, do_not_save=True) + so.sales_partner = "_Test Sales Partner India - 1" + so.commission_rate = 7 + so.save() + + self.assertEqual(so.amount_eligible_for_commission, 1000) + self.assertEqual(so.total_commission, 70) # 1000 * 7% + + with self.subTest("commission rate above 100 is rejected"): + so.commission_rate = 101 + self.assertRaises(frappe.ValidationError, so.save) + finally: + frappe.db.set_value("Item", "_Test Item", "grant_commission", 1) + + def test_commission_fields_not_copied_on_duplicate(self): + """Commission rate/amount fields are no_copy; only the sales partner carries to a copy.""" + frappe.db.set_value("Item", "_Test Item", "grant_commission", 1) + try: + so = make_sales_order(qty=10, rate=100, do_not_save=True) + so.sales_partner = "_Test Sales Partner India - 1" + so.commission_rate = 7 + so.save() + self.assertEqual(so.total_commission, 70) + + # ignore_no_copy=False mirrors UI "Duplicate"/amend, which honour no_copy + duplicate = frappe.copy_doc(so, ignore_no_copy=False) + self.assertEqual(duplicate.sales_partner, "_Test Sales Partner India - 1") + self.assertFalse(duplicate.commission_rate) + self.assertFalse(duplicate.total_commission) + self.assertFalse(duplicate.amount_eligible_for_commission) + finally: + frappe.db.set_value("Item", "_Test Item", "grant_commission", 1) + + def test_commission_rate_carried_through_mapper(self): + """commission_rate is no_copy, but Make Delivery Note / Sales Invoice still carries it.""" + from erpnext.selling.doctype.sales_order.mapper import make_delivery_note, make_sales_invoice + + original = frappe.db.get_value("Item", "_Test Item", "grant_commission") + frappe.db.set_value("Item", "_Test Item", "grant_commission", 1) + try: + so = make_sales_order(qty=10, rate=100, do_not_save=True) + so.sales_partner = "_Test Sales Partner India - 1" + so.commission_rate = 7 + so.submit() + + # carried to the mapped (unsaved) documents even though the field is no_copy + self.assertEqual(make_delivery_note(so.name).commission_rate, 7) + self.assertEqual(make_sales_invoice(so.name).commission_rate, 7) + finally: + frappe.db.set_value("Item", "_Test Item", "grant_commission", original) + +>>>>>>> 4afba94d1c (test: sales team allocation totalling 100 in floating point) def compare_payment_schedules(doc, doc1, doc2): for index, schedule in enumerate(doc1.get("payment_schedule")): From e0d39074be7a232feaa2e66e3f14a81cfbb293e8 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 9 Aug 2026 12:39:39 +0530 Subject: [PATCH 036/134] chore: resolve conflict --- .../accounts/services/child_item_update.py | 618 ------------------ erpnext/controllers/accounts_controller.py | 48 +- erpnext/public/js/utils.js | 14 +- 3 files changed, 44 insertions(+), 636 deletions(-) delete mode 100644 erpnext/accounts/services/child_item_update.py diff --git a/erpnext/accounts/services/child_item_update.py b/erpnext/accounts/services/child_item_update.py deleted file mode 100644 index d66c5621f7a..00000000000 --- a/erpnext/accounts/services/child_item_update.py +++ /dev/null @@ -1,618 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -"""Child item update service: ChildItemUpdater class and helpers for the update_child_qty_rate API.""" - -import frappe -from frappe import _ -from frappe.model.workflow import get_workflow_name -from frappe.utils import flt, get_link_to_form, getdate - -from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions -from erpnext.buying.utils import update_last_purchase_rate -from erpnext.stock.doctype.packed_item.packed_item import make_packing_list -from erpnext.stock.get_item_details import ( - get_bin_details, - get_conversion_factor, - get_item_warehouse_, -) -from erpnext.stock.utils import ( - is_group_warehouse, - validate_disabled_warehouse, - validate_warehouse_company, -) - - -class ChildItemUpdater: - """Validates and applies item-level edits on submitted orders and quotations.""" - - def __init__(self, parent_doctype: str, parent_doctype_name: str, child_docname: str = "items"): - self.parent_doctype = parent_doctype - self.parent_doctype_name = parent_doctype_name - self.child_docname = child_docname - self.parent = frappe.get_doc(parent_doctype, parent_doctype_name) - self.allow_zero_qty = get_allow_zero_qty(parent_doctype) - self._ordered_items: dict | None = None - self._purchased_items: dict | None = None - - def update(self, trans_items: str | list) -> None: - """Process item additions, edits, and deletions from trans_items JSON.""" - from erpnext.buying.doctype.supplier_quotation.supplier_quotation import get_purchased_items - from erpnext.selling.doctype.quotation.mapper import get_ordered_items - - data = frappe.parse_json(trans_items) - any_qty_changed = False - items_added_or_removed = False - any_conversion_factor_changed = False - - self._check_permissions("write") - - if self.parent_doctype == "Quotation": - self._ordered_items = get_ordered_items(self.parent.name) - items_added_or_removed |= validate_and_delete_children(self.parent, data, self._ordered_items) - elif self.parent_doctype == "Supplier Quotation": - self._purchased_items = get_purchased_items(self.parent.name) - items_added_or_removed |= validate_and_delete_children(self.parent, data, self._purchased_items) - else: - items_added_or_removed |= validate_and_delete_children(self.parent, data) - - for d in data: - new_child_flag = False - rate_unchanged = None - - if not d.get("item_code"): - continue - - if not d.get("docname"): - new_child_flag = True - items_added_or_removed = True - self._check_permissions("create") - child_item = self._get_new_child_item(d) - else: - self._check_permissions("write") - child_item = frappe.get_doc(self.parent_doctype + " Item", d.get("docname")) - - change_state = get_child_item_change_state(self.parent_doctype, child_item, d) - rate_unchanged = change_state.rate_unchanged - any_conversion_factor_changed |= not change_state.conversion_factor_unchanged - if is_child_item_unchanged(change_state): - continue - - self._validate_quantity_and_rate(child_item, d, rate_unchanged) - - if flt(child_item.get("qty")) != flt(d.get("qty")): - any_qty_changed = True - - if self.parent.doctype in ("Sales Order", "Purchase Order") and self.parent.is_subcontracted: - self._validate_fg_item_for_subcontracting(d, new_child_flag) - child_item.fg_item_qty = flt(d["fg_item_qty"]) - if new_child_flag: - child_item.fg_item = d["fg_item"] - - child_item.qty = flt(d.get("qty")) - child_item.description = d.get("description") - update_child_item_rate_and_discount( - self.parent_doctype, child_item, d, self.allow_zero_qty, rate_unchanged=rate_unchanged - ) - update_child_item_uom_and_weight(child_item, d) - - if d.get("delivery_date") and self.parent_doctype == "Sales Order": - child_item.delivery_date = d.get("delivery_date") - - if d.get("schedule_date") and self.parent_doctype == "Purchase Order": - child_item.schedule_date = d.get("schedule_date") - - if d.get("bom_no") and self.parent_doctype == "Sales Order": - child_item.bom_no = d.get("bom_no") - - child_item.flags.ignore_validate_update_after_submit = True - if new_child_flag: - self.parent.load_from_db() - child_item.idx = len(self.parent.items) + 1 - child_item.insert() - else: - child_item.save(ignore_permissions=True) - - self._post_update(any_qty_changed, items_added_or_removed, any_conversion_factor_changed) - - def _post_update( - self, any_qty_changed: bool, items_added_or_removed: bool, any_conversion_factor_changed: bool - ) -> None: - parent = self.parent - parent.reload() - parent.flags.ignore_validate_update_after_submit = True - parent.set_qty_as_per_stock_uom() - parent.calculate_taxes_and_totals() - parent.set_total_in_words() - - if self.parent_doctype == "Sales Order" and not parent.is_subcontracted: - make_packing_list(parent) - parent.set_gross_profit() - - frappe.get_cached_doc("Authorization Control").validate_approving_authority( - parent.doctype, parent.company, parent.base_grand_total - ) - - if self.parent_doctype != "Supplier Quotation": - from erpnext.accounts.services.payment_schedule import PaymentScheduleService - - PaymentScheduleService(parent).set_payment_schedule() - - if self.parent_doctype == "Purchase Order": - parent.validate_minimum_order_qty() - parent.validate_budget() - if parent.is_against_so(): - parent.update_status_updater() - elif self.parent_doctype == "Sales Order": - parent.check_credit_limit() - - for idx, row in enumerate(parent.get(self.child_docname), start=1): - row.idx = idx - - parent.save() - - if self.parent_doctype == "Purchase Order": - update_last_purchase_rate(parent, is_submit=1) - - if any_qty_changed or items_added_or_removed or any_conversion_factor_changed: - parent.update_prevdoc_status() - - parent.update_requested_qty() - parent.update_ordered_qty() - parent.update_ordered_and_reserved_qty() - parent.update_receiving_percentage() - - if parent.is_subcontracted and not parent.can_update_items(): - frappe.throw( - _( - "Items cannot be updated as Subcontracting Order is created against the Purchase Order {0}." - ).format(frappe.bold(parent.name)) - ) - - elif self.parent_doctype == "Sales Order": - if parent.is_subcontracted and not parent.can_update_items(): - frappe.throw( - _( - "Items cannot be updated as Subcontracting Inward Order(s) exist against this Subcontracted Sales Order." - ) - ) - parent.validate_selling_price() - parent.validate_for_duplicate_items() - parent.validate_warehouse() - parent.update_reserved_qty() - parent.update_project() - parent.update_prevdoc_status("submit") - parent.update_delivery_status() - - parent.reload() - self._validate_workflow() - - if self.parent_doctype in ("Purchase Order", "Sales Order"): - parent.update_blanket_order() - parent.update_billing_percentage() - parent.set_status() - - parent.validate_uom_is_integer("uom", "qty") - parent.validate_uom_is_integer("stock_uom", "stock_qty") - - if self.parent_doctype == "Sales Order" and not parent.is_subcontracted: - from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( - cancel_stock_reservation_entries, - has_reserved_stock, - ) - - if has_reserved_stock(parent.doctype, parent.name): - cancel_stock_reservation_entries(parent.doctype, parent.name) - if parent.per_picked == 0: - parent.create_stock_reservation_entries() - - def _check_permissions(self, perm_type: str = "create") -> None: - try: - self.parent.check_permission(perm_type) - except frappe.PermissionError: - actions = {"create": "add", "write": "update"} - frappe.throw( - _("You do not have permissions to {0} items in a {1}.").format( - actions[perm_type], self.parent_doctype - ), - title=_("Insufficient Permissions"), - ) - - def _validate_workflow(self) -> None: - workflow = get_workflow_name(self.parent.doctype) - if not workflow: - return - - workflow_doc = frappe.get_doc("Workflow", workflow) - current_state = self.parent.get(workflow_doc.workflow_state_field) - roles = frappe.get_roles() - - allowed = any( - state.state == current_state and (not state.allow_edit or state.allow_edit in roles) - for state in workflow_doc.states - ) - - if not allowed: - frappe.throw( - _("You are not allowed to update as per the conditions set in {0} Workflow.").format( - get_link_to_form("Workflow", workflow) - ), - title=_("Insufficient Permissions"), - ) - - def _get_new_child_item(self, item_row) -> "frappe.model.document.Document": - child_doctype = self.parent_doctype + " Item" - return set_order_defaults( - self.parent_doctype, - self.parent_doctype_name, - child_doctype, - self.child_docname, - item_row, - ) - - def _validate_quantity_and_rate(self, child_item, new_data: dict, rate_unchanged: bool | None) -> None: - if not flt(new_data.get("qty")) and not self.allow_zero_qty: - frappe.throw( - _("Row #{0}:Quantity for Item {1} cannot be zero.").format( - new_data.get("idx"), frappe.bold(new_data.get("item_code")) - ), - title=_("Invalid Qty"), - ) - - qty_limits = { - "Sales Order": ("delivered_qty", _("Cannot set quantity less than delivered quantity.")), - "Purchase Order": ("received_qty", _("Cannot set quantity less than received quantity.")), - } - - if self.parent_doctype in qty_limits: - qty_field, error_message = qty_limits[self.parent_doctype] - if flt(new_data.get("qty")) < flt(child_item.get(qty_field)): - frappe.throw( - _("Row #{0}:").format(new_data.get("idx")) + error_message, - title=_("Invalid Qty"), - ) - - if self.parent_doctype not in ("Quotation", "Supplier Quotation"): - return - - items_map = self._ordered_items if self.parent_doctype == "Quotation" else self._purchased_items - if not items_map: - return - - qty_to_check = items_map.get(child_item.name) - if not qty_to_check: - return - - if not rate_unchanged: - frappe.throw( - _( - "Cannot update rate as item {0} is already ordered or purchased against this quotation" - ).format(frappe.bold(new_data.get("item_code"))) - ) - - if flt(new_data.get("qty")) < qty_to_check: - frappe.throw(_("Cannot reduce quantity than ordered or purchased quantity")) - - def _validate_fg_item_for_subcontracting(self, new_data: dict, is_new: bool) -> None: - if is_new: - if not new_data.get("fg_item"): - frappe.throw( - _("Finished Good Item is not specified for service item {0}").format( - new_data["item_code"] - ) - ) - - is_sub_contracted_item, default_bom = frappe.db.get_value( - "Item", new_data["fg_item"], ["is_sub_contracted_item", "default_bom"] - ) - - if not is_sub_contracted_item: - frappe.throw( - _("Finished Good Item {0} must be a sub-contracted item").format(new_data["fg_item"]) - ) - elif not default_bom: - frappe.throw(_("Default BOM not found for FG Item {0}").format(new_data["fg_item"])) - - if not new_data.get("fg_item_qty"): - frappe.throw(_("Finished Good Item {0} Qty can not be zero").format(new_data["fg_item"])) - - -@frappe.whitelist() -def update_child_qty_rate( - parent_doctype: str, trans_items: str | list, parent_doctype_name: str, child_docname: str = "items" -) -> None: - ChildItemUpdater(parent_doctype, parent_doctype_name, child_docname).update(trans_items) - - -def set_order_defaults( - parent_doctype: str, - parent_doctype_name: str, - child_doctype: str, - child_docname: str, - trans_item: dict, -) -> "frappe.model.document.Document": - """Return a new child item populated with item master defaults.""" - from erpnext.accounts.services.taxes import add_taxes_from_tax_template, set_child_tax_template_and_map - - p_doc = frappe.get_doc(parent_doctype, parent_doctype_name) - child_item = frappe.new_doc(child_doctype, parent_doc=p_doc, parentfield=child_docname) - item = frappe.get_doc("Item", trans_item.get("item_code")) - - for field in ("item_code", "item_name", "description", "item_group", "weight_per_unit", "weight_uom"): - child_item.update({field: item.get(field)}) - - date_fieldname = "delivery_date" if child_doctype == "Sales Order Item" else "schedule_date" - child_item.update({date_fieldname: trans_item.get(date_fieldname) or p_doc.get(date_fieldname)}) - child_item.stock_uom = item.stock_uom - child_item.uom = trans_item.get("uom") or item.stock_uom - child_item.warehouse = get_new_child_item_warehouse(p_doc, item, trans_item, child_doctype) - 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"): - child_item.base_rate = 1 - child_item.base_amount = 1 - - set_child_tax_template_and_map(item, child_item, p_doc) - add_taxes_from_tax_template(child_item, p_doc) - return child_item - - -def get_new_child_item_warehouse(p_doc, item, trans_item: dict, child_doctype: str) -> str | None: - """Return the warehouse picked in the Update Items dialog, else the configured default. - - Validates whichever warehouse was resolved, since a submitted parent skips validate(). - """ - warehouse = trans_item.get("warehouse") or get_item_warehouse_(p_doc, item, overwrite_warehouse=True) - - if not warehouse: - if is_warehouse_required_for_new_child_item(child_doctype, item, trans_item): - frappe.throw( - _( - "Cannot find a default warehouse for item {0}. Please select one in the Update Items dialog, or set a default in the Item Master or in the Company." - ).format(frappe.bold(item.item_code)) - ) - return None - - validate_warehouse_company(warehouse, p_doc.company) - validate_disabled_warehouse(warehouse) - is_group_warehouse(warehouse) - return warehouse - - -def is_warehouse_required_for_new_child_item(child_doctype: str, item, trans_item: dict) -> bool: - """Sales Order always needs one; buying documents only for stock rows, as in validate_stock_item_warehouse.""" - if child_doctype == "Sales Order Item": - return True - - if child_doctype in ("Purchase Order Item", "Supplier Quotation Item"): - return bool(item.is_stock_item and flt(trans_item.get("qty")) and not item.delivered_by_supplier) - - return False - - -def validate_child_on_delete(row, parent, ordered_item=None) -> None: - """Raise if a partially transacted child item is being deleted.""" - if parent.doctype == "Sales Order": - if flt(row.delivered_qty): - frappe.throw( - _("Row #{0}: Cannot delete item {1} which has already been delivered").format( - row.idx, row.item_code - ) - ) - if flt(row.work_order_qty): - frappe.throw( - _("Row #{0}: Cannot delete item {1} which has work order assigned to it.").format( - row.idx, row.item_code - ) - ) - if flt(row.ordered_qty): - frappe.throw( - _( - "Row #{0}: Cannot delete item {1} which is already ordered against this Sales Order." - ).format(row.idx, row.item_code) - ) - - if parent.doctype == "Purchase Order" and flt(row.received_qty): - frappe.throw( - _("Row #{0}: Cannot delete item {1} which has already been received").format( - row.idx, row.item_code - ) - ) - - if parent.doctype in ("Purchase Order", "Sales Order") and flt(row.billed_amt): - frappe.throw( - _("Row #{0}: Cannot delete item {1} which has already been billed.").format( - row.idx, row.item_code - ) - ) - - if parent.doctype == "Quotation" and ordered_item and ordered_item.get(row.name): - frappe.throw(_("Cannot delete an item which has been ordered")) - - -def update_bin_on_delete(row, doctype: str) -> None: - """Update bin quantities after a child item row is deleted.""" - from erpnext.stock.stock_balance import ( - get_indented_qty, - get_ordered_qty, - get_reserved_qty, - update_bin_qty, - ) - - qty_dict = {} - - if doctype == "Sales Order": - qty_dict["reserved_qty"] = get_reserved_qty(row.item_code, row.warehouse) - else: - if row.material_request_item: - qty_dict["indented_qty"] = get_indented_qty(row.item_code, row.warehouse) - qty_dict["ordered_qty"] = get_ordered_qty(row.item_code, row.warehouse) - - if row.warehouse: - update_bin_qty(row.item_code, row.warehouse, qty_dict) - - -def validate_and_delete_children(parent, data, ordered_item=None) -> bool: - """Delete child rows not present in data; return True if any were removed.""" - updated_item_names = [d.get("docname") for d in data] - deleted_children = [item for item in parent.items if item.name not in updated_item_names] - - for d in deleted_children: - validate_child_on_delete(d, parent, ordered_item) - d.flags.ignore_permissions = True - d.cancel() - d.delete() - - if parent.doctype == "Purchase Order": - parent.update_ordered_qty_in_so_for_removed_items(deleted_children) - - if parent.doctype not in ("Quotation", "Supplier Quotation"): - parent.update_prevdoc_status() - for d in deleted_children: - update_bin_on_delete(d, parent.doctype) - - return bool(deleted_children) - - -def get_allow_zero_qty(parent_doctype: str) -> bool: - if parent_doctype == "Sales Order": - return frappe.db.get_single_value("Selling Settings", "allow_zero_qty_in_sales_order") or False - if parent_doctype == "Purchase Order": - return frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_purchase_order") or False - return False - - -def get_child_item_change_state(parent_doctype: str, child_item, new_data) -> frappe._dict: - prev_rate, new_rate = flt(child_item.get("rate")), flt(new_data.get("rate")) - prev_qty, new_qty = flt(child_item.get("qty")), flt(new_data.get("qty")) - prev_fg_qty, new_fg_qty = flt(child_item.get("fg_item_qty")), flt(new_data.get("fg_item_qty")) - prev_con_fac = flt(child_item.get("conversion_factor")) - new_con_fac = flt(new_data.get("conversion_factor")) - - if parent_doctype == "Sales Order": - prev_date, new_date = child_item.get("delivery_date"), new_data.get("delivery_date") - elif parent_doctype == "Purchase Order": - prev_date, new_date = child_item.get("schedule_date"), new_data.get("schedule_date") - else: - prev_date, new_date = None, None - - if parent_doctype in ("Quotation", "Supplier Quotation"): - date_unchanged = False - else: - prev_date = getdate(prev_date) if prev_date else None - new_date = getdate(new_date) if new_date else None - date_unchanged = prev_date == new_date - - return frappe._dict( - rate_unchanged=prev_rate == new_rate, - qty_unchanged=prev_qty == new_qty, - fg_qty_unchanged=prev_fg_qty == new_fg_qty, - uom_unchanged=child_item.get("uom") == new_data.get("uom"), - conversion_factor_unchanged=prev_con_fac == new_con_fac, - date_unchanged=date_unchanged, - description_unchanged=child_item.get("description") == new_data.get("description"), - ) - - -def is_child_item_unchanged(change_state: frappe._dict) -> bool: - return ( - change_state.rate_unchanged - and change_state.qty_unchanged - and change_state.fg_qty_unchanged - and change_state.conversion_factor_unchanged - and change_state.uom_unchanged - and change_state.date_unchanged - and change_state.description_unchanged - ) - - -def update_child_item_rate_and_discount( - parent_doctype: str, - child_item, - new_data, - allow_zero_qty: bool, - rate_unchanged: bool | None = None, -) -> None: - rate_precision = child_item.precision("rate") or 2 - qty_precision = child_item.precision("qty") or 2 - - if rate_unchanged is None: - rate_unchanged = flt(child_item.get("rate")) == flt(new_data.get("rate")) - - if not rate_unchanged and not child_item.get("qty") and allow_zero_qty: - frappe.throw(_("Rate of '{0}' items cannot be changed").format(frappe.bold(_("Unit Price")))) - - row_rate = flt(new_data.get("rate"), rate_precision) - - if parent_doctype in ("Purchase Order", "Sales Order"): - amount_below_billed_amt = flt(child_item.billed_amt, rate_precision) > flt( - row_rate * flt(new_data.get("qty"), qty_precision), rate_precision - ) - if amount_below_billed_amt and row_rate > 0.0: - frappe.throw( - _( - "Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}." - ).format(child_item.idx, child_item.item_code) - ) - - child_item.rate = row_rate - - if parent_doctype not in ("Sales Order", "Purchase Order") or not flt(child_item.price_list_rate): - return - - if flt(child_item.rate) > flt(child_item.price_list_rate): - child_item.discount_percentage = 0 - child_item.discount_amount = 0 - child_item.margin_type = "Amount" - child_item.margin_rate_or_amount = flt( - child_item.rate - child_item.price_list_rate, - child_item.precision("margin_rate_or_amount"), - ) - child_item.rate_with_margin = child_item.rate - else: - child_item.margin_type = "" - child_item.margin_rate_or_amount = 0 - child_item.rate_with_margin = child_item.price_list_rate - child_item.discount_percentage = 0 - child_item.discount_amount = flt(child_item.rate_with_margin) - flt(child_item.rate) - - -def update_child_item_uom_and_weight(child_item, new_data) -> None: - conv_fac_precision = child_item.precision("conversion_factor") or 2 - - if new_data.get("conversion_factor"): - if child_item.stock_uom == child_item.uom: - child_item.conversion_factor = 1 - else: - child_item.conversion_factor = flt(new_data.get("conversion_factor"), conv_fac_precision) - - if new_data.get("uom"): - child_item.uom = new_data.get("uom") - conversion_factor = flt( - get_conversion_factor(child_item.item_code, child_item.uom).get("conversion_factor") - ) - child_item.conversion_factor = ( - flt(new_data.get("conversion_factor"), conv_fac_precision) or conversion_factor - ) - - if child_item.get("weight_per_unit"): - child_item.total_weight = flt( - child_item.weight_per_unit * child_item.qty * child_item.conversion_factor, - child_item.precision("total_weight"), - ) - - -def check_if_child_table_updated( - child_table_before_update, child_table_after_update, fields_to_check -) -> bool: - """Return True if any accounting-relevant field changed in a child table.""" - fields_to_check = list(fields_to_check) + get_accounting_dimensions() + ["cost_center", "project"] - - for index, item in enumerate(child_table_before_update): - for field in fields_to_check: - if child_table_after_update[index].get(field) != item.get(field): - return True - - return False diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index fbe8b348000..238a4ddf783 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -77,6 +77,11 @@ from erpnext.stock.get_item_details import ( get_item_tax_map, get_item_warehouse_, ) +from erpnext.stock.utils import ( + is_group_warehouse, + validate_disabled_warehouse, + validate_warehouse_company, +) from erpnext.utilities.regional import temporary_flag from erpnext.utilities.transaction_base import TransactionBase @@ -3777,7 +3782,7 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child child_item.update({date_fieldname: trans_item.get(date_fieldname) or p_doc.get(date_fieldname)}) child_item.stock_uom = item.stock_uom child_item.uom = trans_item.get("uom") or item.stock_uom - child_item.warehouse = get_item_warehouse_(p_doc, item, overwrite_warehouse=True) + child_item.warehouse = get_new_child_item_warehouse(p_doc, item, trans_item, child_doctype) 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"))) @@ -3786,20 +3791,45 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child # Initialized value will update in parent validation child_item.base_rate = 1 child_item.base_amount = 1 - if child_doctype == "Sales Order Item": - child_item.warehouse = get_item_warehouse_(p_doc, item, overwrite_warehouse=True) - if not child_item.warehouse: - frappe.throw( - _( - "Cannot find a default warehouse for item {0}. Please set one in the Item Master or in Stock Settings." - ).format(frappe.bold(item.item_code)) - ) set_child_tax_template_and_map(item, child_item, p_doc) add_taxes_from_tax_template(child_item, p_doc) return child_item +def get_new_child_item_warehouse(p_doc, item, trans_item: dict, child_doctype: str) -> str | None: + """Return the warehouse picked in the Update Items dialog, else the configured default. + + Validates whichever warehouse was resolved, since a submitted parent skips validate(). + """ + warehouse = trans_item.get("warehouse") or get_item_warehouse_(p_doc, item, overwrite_warehouse=True) + + if not warehouse: + if is_warehouse_required_for_new_child_item(child_doctype, item, trans_item): + frappe.throw( + _( + "Cannot find a default warehouse for item {0}. Please select one in the Update Items dialog, or set a default in the Item Master or in the Company." + ).format(frappe.bold(item.item_code)) + ) + return None + + validate_warehouse_company(warehouse, p_doc.company) + validate_disabled_warehouse(warehouse) + is_group_warehouse(warehouse) + return warehouse + + +def is_warehouse_required_for_new_child_item(child_doctype: str, item, trans_item: dict) -> bool: + """Sales Order always needs one; buying documents only for stock rows, as in validate_stock_item_warehouse.""" + if child_doctype == "Sales Order Item": + return True + + if child_doctype in ("Purchase Order Item", "Supplier Quotation Item"): + return bool(item.is_stock_item and flt(trans_item.get("qty")) and not item.delivered_by_supplier) + + return False + + def validate_child_on_delete(row, parent, ordered_item=None): """Check if partially transacted item (row) is being deleted.""" if parent.doctype == "Sales Order": diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 3514c89b469..b847595d021 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -925,13 +925,6 @@ erpnext.utils.update_child_items = function (opts) { }); } -<<<<<<< HEAD - if ( - ["Purchase Order", "Sales Order"].includes(frm.doc.doctype) && - frm.doc.is_subcontracted && - !frm.doc.is_old_subcontracting_flow - ) { -======= const warehouse_df = child_meta.fields.find((f) => f.fieldname == "warehouse"); if (warehouse_df) { fields.splice(3, 0, { @@ -955,8 +948,11 @@ erpnext.utils.update_child_items = function (opts) { }); } - if (["Purchase Order", "Sales Order"].includes(frm.doc.doctype) && frm.doc.is_subcontracted) { ->>>>>>> 55fe269046 (fix: allow selecting a warehouse for new items in the update items dialog (#57876)) + if ( + ["Purchase Order", "Sales Order"].includes(frm.doc.doctype) && + frm.doc.is_subcontracted && + !frm.doc.is_old_subcontracting_flow + ) { fields.push( { fieldtype: "Link", From c507d5f09bcd5558aaa5239dd6eb9412f664c98f Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 9 Aug 2026 12:49:07 +0530 Subject: [PATCH 037/134] chore: resolve conflict --- .../doctype/sales_order/test_sales_order.py | 285 +----------------- 1 file changed, 10 insertions(+), 275 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 36fcf226059..b5e6568c82b 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -2890,282 +2890,17 @@ class TestSalesOrder(ERPNextTestSuite): so = make_sales_order(item_code=fg_item, qty=10, rate=50, warehouse=fg_warehouse, do_not_save=1) self.assertRaises(frappe.ValidationError, so.save) -<<<<<<< HEAD -======= - @ERPNextTestSuite.change_settings( - "Stock Settings", {"enable_stock_reservation": 1, "use_serial_batch_fields": 0} - ) - def test_product_bundle_reservation(self): - pb_item = make_item("Product Bundle Item", {"is_stock_item": 0}) - simple_item = make_item("Simple Item", {"is_stock_item": 1}) - sb_item = make_item( - "Serial Batch Item", - { - "is_stock_item": 1, - "has_serial_no": 1, - "has_batch_no": 1, - "create_new_batch": 1, - "batch_number_series": "BAT-TSBIFRM-.#####", - "serial_no_series": "SN-TSBIFRM-.#####", - }, - ) - make_product_bundle(pb_item.name, [simple_item.name, sb_item.name]) + def test_sales_team_allocated_percentage_tolerates_floating_point_drift(self): + # 10.0 + 58.02 + 31.98 accumulates to 100.00000000000001 in binary floating point + so = make_sales_order(do_not_save=True) + for sales_person, percentage in ( + ("_Test Sales Person", 10.0), + ("_Test Sales Person 1", 58.02), + ("_Test Sales Person 2", 31.98), + ): + so.append("sales_team", {"sales_person": sales_person, "allocated_percentage": percentage}) + so.save() - warehouse = "_Test Warehouse - _TC" - - make_stock_entry( - item_code=simple_item.name, - target=warehouse, - qty=10, - ) - - # two different stock entries on purpose to get two batches - make_stock_entry( - item_code=sb_item.name, - target=warehouse, - qty=5, - ) - make_stock_entry( - item_code=sb_item.name, - target=warehouse, - qty=5, - ) - - so = make_sales_order(item_code=pb_item.name, do_not_submit=1) - so.reserve_stock = 1 - for item in so.packed_items: - item.reserve_stock = 1 - so.submit() - - from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( - get_sre_reserved_batch_nos_details, - get_sre_reserved_qty_for_voucher_detail_no, - get_sre_reserved_serial_nos_details, - ) - - for item in so.packed_items: - self.assertEqual( - get_sre_reserved_qty_for_voucher_detail_no(item.item_code, "Sales Order", so.name, item.name), - item.qty, - ) - - sre_serial_nos = list(get_sre_reserved_serial_nos_details(sb_item.name, warehouse).keys()) - sre_batch_nos = list(get_sre_reserved_batch_nos_details(sb_item.name, warehouse).keys()) - - dn = make_delivery_note(so.name, kwargs={"for_reserved_stock": True}) - dn.save() - - self.assertTrue(dn.packed_items[1].serial_and_batch_bundle) - - from erpnext.stock.serial_batch_bundle import get_batches_from_bundle, get_serial_nos - - serial_nos_in_bundle = get_serial_nos(dn.packed_items[1].serial_and_batch_bundle) - batches_in_bundle = list(get_batches_from_bundle(dn.packed_items[1].serial_and_batch_bundle).keys()) - - self.assertEqual(sre_serial_nos, serial_nos_in_bundle) - self.assertEqual(sre_batch_nos, batches_in_bundle) - - dn.items[0].qty = 5 - dn.save() - sabb_doc = frappe.get_doc("Serial and Batch Bundle", dn.packed_items[1].serial_and_batch_bundle) - sabb_doc.entries = sabb_doc.entries[:5] - sabb_doc.company = dn.company - sabb_doc.save() - dn.submit() - - serial_nos = set(sre_serial_nos) - set(get_serial_nos(sabb_doc.name)) - batch_nos = set(sre_batch_nos) - set(get_batches_from_bundle(sabb_doc.name).keys()) - - dn1 = make_delivery_note(so.name, kwargs={"for_reserved_stock": True}) - dn1.save() - - self.assertTrue(dn1.packed_items[1].serial_and_batch_bundle) - - from erpnext.stock.serial_batch_bundle import get_batches_from_bundle, get_serial_nos - - serial_nos_in_bundle = set(get_serial_nos(dn1.packed_items[1].serial_and_batch_bundle)) - batches_in_bundle = set(get_batches_from_bundle(dn1.packed_items[1].serial_and_batch_bundle).keys()) - - self.assertEqual(serial_nos, serial_nos_in_bundle) - self.assertEqual(batch_nos, batches_in_bundle) - - dn.cancel() - - # test the same thing with sales invoice as well - - si = make_sales_invoice(so.name) - si.update_stock = 1 - si.save() - - self.assertTrue(si.packed_items[1].serial_and_batch_bundle) - - from erpnext.stock.serial_batch_bundle import get_batches_from_bundle, get_serial_nos - - serial_nos_in_bundle = get_serial_nos(si.packed_items[1].serial_and_batch_bundle) - batches_in_bundle = list(get_batches_from_bundle(si.packed_items[1].serial_and_batch_bundle).keys()) - - self.assertEqual(sre_serial_nos, serial_nos_in_bundle) - self.assertEqual(sre_batch_nos, batches_in_bundle) - - si.items[0].qty = 5 - si.save() - sabb_doc = frappe.get_doc("Serial and Batch Bundle", si.packed_items[1].serial_and_batch_bundle) - sabb_doc.entries = sabb_doc.entries[:5] - sabb_doc.company = si.company - sabb_doc.save() - si.submit() - - serial_nos = set(sre_serial_nos) - set(get_serial_nos(sabb_doc.name)) - batch_nos = set(sre_batch_nos) - set(get_batches_from_bundle(sabb_doc.name).keys()) - - si1 = make_delivery_note(so.name, kwargs={"for_reserved_stock": True}) - si1.save() - - self.assertTrue(si1.packed_items[1].serial_and_batch_bundle) - - from erpnext.stock.serial_batch_bundle import get_batches_from_bundle, get_serial_nos - - serial_nos_in_bundle = set(get_serial_nos(si1.packed_items[1].serial_and_batch_bundle)) - batches_in_bundle = set(get_batches_from_bundle(si1.packed_items[1].serial_and_batch_bundle).keys()) - - self.assertEqual(serial_nos, serial_nos_in_bundle) - self.assertEqual(batch_nos, batches_in_bundle) - - def test_sales_team_contribution_follows_grant_commission(self): - """Sales-person allocation tracks the grant-commission-eligible amount, not the gross total. - - The Item "Grant Commission" flag includes an item in both Sales Partner and Sales Person - commission, so each sales person's allocated_amount is a share of - amount_eligible_for_commission rather than net_total. - """ - frappe.db.set_value("Item", "_Test Item", "grant_commission", 1) - frappe.db.set_value("Item", "_Test FG Item", "grant_commission", 0) - try: - so = make_sales_order( - do_not_save=True, - item_list=[ - {"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC", "qty": 10, "rate": 100}, - { - "item_code": "_Test FG Item", - "warehouse": "_Test Warehouse - _TC", - "qty": 10, - "rate": 100, - }, - ], - ) - so.append( - "sales_team", - {"sales_person": "_Test Sales Person 1", "allocated_percentage": 60, "commission_rate": 10}, - ) - so.append( - "sales_team", - {"sales_person": "_Test Sales Person 2", "allocated_percentage": 40, "commission_rate": 0}, - ) - so.save() - - self.assertEqual(so.net_total, 2000) - self.assertEqual(so.amount_eligible_for_commission, 1000) # only the grant_commission item - - first, second = so.sales_team - # allocation follows the eligible amount (1000), not net_total (2000) - self.assertEqual(first.allocated_amount, 600) - self.assertEqual(first.incentives, 60) # 600 * 10% - self.assertEqual(second.allocated_amount, 400) - self.assertEqual(second.incentives, 0) - finally: - # grant_commission defaults to 1 for both items; restore - frappe.db.set_value("Item", "_Test Item", "grant_commission", 1) - frappe.db.set_value("Item", "_Test FG Item", "grant_commission", 1) - - def test_sales_team_allocated_percentage_must_total_100(self): - with self.subTest("partial allocation is rejected"): - so = make_sales_order(do_not_save=True) - so.append("sales_team", {"sales_person": "_Test Sales Person 1", "allocated_percentage": 60}) - self.assertRaises(frappe.ValidationError, so.save) - - with self.subTest("allocation totalling 100 is accepted"): - so = make_sales_order(do_not_save=True) - so.append("sales_team", {"sales_person": "_Test Sales Person 1", "allocated_percentage": 60}) - so.append("sales_team", {"sales_person": "_Test Sales Person 2", "allocated_percentage": 40}) - so.save() - self.assertEqual(sum(d.allocated_percentage for d in so.sales_team), 100) - - with self.subTest("floating-point drift in the total is tolerated"): - # 10.0 + 58.02 + 31.98 accumulates to 100.00000000000001 in binary floating point - so = make_sales_order(do_not_save=True) - for sales_person, percentage in ( - ("_Test Sales Person", 10.0), - ("_Test Sales Person 1", 58.02), - ("_Test Sales Person 2", 31.98), - ): - so.append("sales_team", {"sales_person": sales_person, "allocated_percentage": percentage}) - so.save() - - def test_sales_team_disabled_sales_person_rejected(self): - frappe.db.set_value("Sales Person", "_Test Sales Person 2", "enabled", 0) - try: - so = make_sales_order(do_not_save=True) - so.append("sales_team", {"sales_person": "_Test Sales Person 2", "allocated_percentage": 100}) - self.assertRaises(frappe.ValidationError, so.save) - finally: - frappe.db.set_value("Sales Person", "_Test Sales Person 2", "enabled", 1) - - def test_sales_partner_commission(self): - """Sales Partner commission: total_commission = amount_eligible_for_commission * rate / 100.""" - frappe.db.set_value("Item", "_Test Item", "grant_commission", 1) - try: - so = make_sales_order(qty=10, rate=100, do_not_save=True) - so.sales_partner = "_Test Sales Partner India - 1" - so.commission_rate = 7 - so.save() - - self.assertEqual(so.amount_eligible_for_commission, 1000) - self.assertEqual(so.total_commission, 70) # 1000 * 7% - - with self.subTest("commission rate above 100 is rejected"): - so.commission_rate = 101 - self.assertRaises(frappe.ValidationError, so.save) - finally: - frappe.db.set_value("Item", "_Test Item", "grant_commission", 1) - - def test_commission_fields_not_copied_on_duplicate(self): - """Commission rate/amount fields are no_copy; only the sales partner carries to a copy.""" - frappe.db.set_value("Item", "_Test Item", "grant_commission", 1) - try: - so = make_sales_order(qty=10, rate=100, do_not_save=True) - so.sales_partner = "_Test Sales Partner India - 1" - so.commission_rate = 7 - so.save() - self.assertEqual(so.total_commission, 70) - - # ignore_no_copy=False mirrors UI "Duplicate"/amend, which honour no_copy - duplicate = frappe.copy_doc(so, ignore_no_copy=False) - self.assertEqual(duplicate.sales_partner, "_Test Sales Partner India - 1") - self.assertFalse(duplicate.commission_rate) - self.assertFalse(duplicate.total_commission) - self.assertFalse(duplicate.amount_eligible_for_commission) - finally: - frappe.db.set_value("Item", "_Test Item", "grant_commission", 1) - - def test_commission_rate_carried_through_mapper(self): - """commission_rate is no_copy, but Make Delivery Note / Sales Invoice still carries it.""" - from erpnext.selling.doctype.sales_order.mapper import make_delivery_note, make_sales_invoice - - original = frappe.db.get_value("Item", "_Test Item", "grant_commission") - frappe.db.set_value("Item", "_Test Item", "grant_commission", 1) - try: - so = make_sales_order(qty=10, rate=100, do_not_save=True) - so.sales_partner = "_Test Sales Partner India - 1" - so.commission_rate = 7 - so.submit() - - # carried to the mapped (unsaved) documents even though the field is no_copy - self.assertEqual(make_delivery_note(so.name).commission_rate, 7) - self.assertEqual(make_sales_invoice(so.name).commission_rate, 7) - finally: - frappe.db.set_value("Item", "_Test Item", "grant_commission", original) - ->>>>>>> 4afba94d1c (test: sales team allocation totalling 100 in floating point) def compare_payment_schedules(doc, doc1, doc2): for index, schedule in enumerate(doc1.get("payment_schedule")): From d8bbe865a8c1d6602599d32d3df06d81d752bd19 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 9 Aug 2026 13:01:00 +0530 Subject: [PATCH 038/134] fix: use stock settings for warehouse defaults --- .../doctype/purchase_order/test_purchase_order.py | 10 +++++++--- erpnext/controllers/accounts_controller.py | 2 +- .../selling/doctype/sales_order/test_sales_order.py | 11 ++++++----- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 423338b39ac..2cc53d6ad95 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -302,9 +302,11 @@ class TestPurchaseOrder(ERPNextTestSuite): po.submit() first_item_of_po = po.get("items")[0] - company_default = frappe.db.get_value("Company", po.company, "default_warehouse") - frappe.db.set_value("Company", po.company, "default_warehouse", None) - self.addCleanup(frappe.db.set_value, "Company", po.company, "default_warehouse", company_default) + stock_settings_default = frappe.db.get_single_value("Stock Settings", "default_warehouse") + frappe.db.set_single_value("Stock Settings", "default_warehouse", None) + self.addCleanup( + frappe.db.set_single_value, "Stock Settings", "default_warehouse", stock_settings_default + ) def get_trans_items(item_code): return json.dumps( @@ -517,11 +519,13 @@ class TestPurchaseOrder(ERPNextTestSuite): "item_code": item, "rate": 100, "qty": 1, + "warehouse": po.items[0].warehouse, }, # added item whose tax account head already exists in PO { "item_code": new_item_with_tax.name, "rate": 100, "qty": 1, + "warehouse": po.items[0].warehouse, }, # added item whose tax account head is missing in PO ] ) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 238a4ddf783..dd4f51025f7 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -3808,7 +3808,7 @@ def get_new_child_item_warehouse(p_doc, item, trans_item: dict, child_doctype: s if is_warehouse_required_for_new_child_item(child_doctype, item, trans_item): frappe.throw( _( - "Cannot find a default warehouse for item {0}. Please select one in the Update Items dialog, or set a default in the Item Master or in the Company." + "Cannot find a default warehouse for item {0}. Please select one in the Update Items dialog, or set a default in the Item Master or in Stock Settings." ).format(frappe.bold(item.item_code)) ) return None diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 25b1cf6887b..5a52e051882 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -655,10 +655,11 @@ class TestSalesOrder(ERPNextTestSuite): so = make_sales_order(item_code="_Test Item", qty=4) existing_item = so.get("items")[0] - # a company gets a default warehouse when its warehouses are created - company_default = frappe.db.get_value("Company", so.company, "default_warehouse") - frappe.db.set_value("Company", so.company, "default_warehouse", None) - self.addCleanup(frappe.db.set_value, "Company", so.company, "default_warehouse", company_default) + stock_settings_default = frappe.db.get_single_value("Stock Settings", "default_warehouse") + frappe.db.set_single_value("Stock Settings", "default_warehouse", None) + self.addCleanup( + frappe.db.set_single_value, "Stock Settings", "default_warehouse", stock_settings_default + ) def get_trans_items(warehouse=None): new_row = {"item_code": item_code, "rate": 200, "qty": 7} @@ -677,7 +678,7 @@ class TestSalesOrder(ERPNextTestSuite): ] ) - # no default in the Item Master, Item Group, Brand or Company + # no default in the Item Master, Item Group, Brand or Stock Settings self.assertRaisesRegex( frappe.ValidationError, "Cannot find a default warehouse", From bcf40ac3183b5529c4582d7f32b2f60ec75eaf44 Mon Sep 17 00:00:00 2001 From: MochaMind Date: Sun, 9 Aug 2026 15:34:19 +0530 Subject: [PATCH 039/134] chore: update POT file (#57916) --- erpnext/locale/main.pot | 1496 +++++++++++++++++++++------------------ 1 file changed, 803 insertions(+), 693 deletions(-) diff --git a/erpnext/locale/main.pot b/erpnext/locale/main.pot index dc47a7ad5a1..6ce1377b298 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-08-02 10:09+0000\n" -"PO-Revision-Date: 2026-08-02 10:09+0000\n" +"POT-Creation-Date: 2026-08-09 09:47+0000\n" +"PO-Revision-Date: 2026-08-09 09:47+0000\n" "Last-Translator: hello@frappe.io\n" "Language-Team: hello@frappe.io\n" "MIME-Version: 1.0\n" @@ -16,7 +16,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.16.0\n" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1657 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1707 msgid "" "\n" "\t\t\tThe Batch {0} of an item {1} has negative stock in the warehouse {2}{3}.\n" @@ -39,7 +39,7 @@ msgstr "" msgid " Amount" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:133 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:122 msgid " BOM" msgstr "" @@ -58,7 +58,7 @@ msgstr "" msgid " Is Subcontracted" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:215 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:204 msgid " Item" msgstr "" @@ -67,8 +67,8 @@ msgstr "" msgid " Name" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:163 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:204 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:152 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:193 msgid " Phantom Item" msgstr "" @@ -76,7 +76,7 @@ msgstr "" msgid " Rate" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:141 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:130 msgid " Raw Material" msgstr "" @@ -85,8 +85,8 @@ msgstr "" msgid " Skip Material Transfer" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:152 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:193 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:141 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:182 msgid " Sub Assembly" msgstr "" @@ -271,7 +271,7 @@ msgstr "" msgid "% of materials delivered against this Sales Order" msgstr "" -#: erpnext/controllers/accounts_controller.py:2414 +#: erpnext/controllers/accounts_controller.py:2419 msgid "'Account' in the Accounting section of Customer {0}" msgstr "" @@ -287,11 +287,11 @@ msgstr "" msgid "'Days Since Last Order' must be greater than or equal to zero" msgstr "" -#: erpnext/controllers/accounts_controller.py:2419 +#: erpnext/controllers/accounts_controller.py:2424 msgid "'Default {0} Account' in Company {1}" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1235 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1245 msgid "'Entries' cannot be empty" msgstr "" @@ -309,17 +309,17 @@ msgstr "" msgid "'Has Serial No' can not be 'Yes' for non-stock item" msgstr "" -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:147 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:151 msgid "'Inspection Required before Delivery' has disabled for the item {0}, no need to create the QI" msgstr "" -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:138 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:142 msgid "'Inspection Required before Purchase' has disabled for the item {0}, no need to create the QI" msgstr "" -#: erpnext/stock/report/stock_ledger/stock_ledger.py:685 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:726 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:831 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:688 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:781 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:913 msgid "'Opening'" msgstr "" @@ -359,17 +359,17 @@ msgid "'{0}' should be in company currency {1}." msgstr "" #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:174 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:214 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:223 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:106 msgid "(A) Qty After Transaction" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:219 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:228 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:111 msgid "(B) Expected Qty After Transaction" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:234 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:243 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:126 msgid "(C) Total Qty in Queue" msgstr "" @@ -379,7 +379,7 @@ msgid "(C) Total qty in queue" msgstr "" #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:194 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:244 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:253 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:136 msgid "(D) Balance Stock Value" msgstr "" @@ -390,12 +390,12 @@ msgid "(Daily Yield * No of Units Produced) / 100" msgstr "" #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:199 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:249 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:258 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:141 msgid "(E) Balance Stock Value in Queue" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:259 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:268 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:151 msgid "(F) Change in Stock Value" msgstr "" @@ -404,7 +404,7 @@ msgstr "" msgid "(Forecast)" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:264 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:273 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:156 msgid "(G) Sum of Change in Stock Value" msgstr "" @@ -415,7 +415,7 @@ msgstr "" msgid "(Good Units Produced / Total Units Produced) × 100" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:274 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:283 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:166 msgid "(H) Change in Stock Value (FIFO Queue)" msgstr "" @@ -430,17 +430,17 @@ msgstr "" msgid "(Hour Rate / 60) * Actual Operation Time" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:284 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:293 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:176 msgid "(I) Valuation Rate" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:289 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:298 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:181 msgid "(J) Valuation Rate as per FIFO" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:299 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:308 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:191 msgid "(K) Valuation = Value (D) ÷ Qty (A)" msgstr "" @@ -800,7 +800,7 @@ msgstr "" msgid "
  • Clearance date must be after cheque date for row(s): {0}
  • " msgstr "" -#: erpnext/controllers/accounts_controller.py:2297 +#: erpnext/controllers/accounts_controller.py:2302 msgid "
  • Item {0} in row(s) {1} billed more than {2}
  • " msgstr "" @@ -817,7 +817,7 @@ msgstr "" msgid "
  • {}
  • " msgstr "" -#: erpnext/controllers/accounts_controller.py:2294 +#: erpnext/controllers/accounts_controller.py:2299 msgid "

    Cannot overbill for the following Items:

    " msgstr "" @@ -862,7 +862,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:2306 +#: erpnext/controllers/accounts_controller.py:2311 msgid "

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

    " msgstr "" @@ -995,18 +995,18 @@ msgid "" "\n" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:224 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:233 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:116 msgid "A - B" msgstr "" #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:189 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:239 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:248 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:131 msgid "A - C" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:365 +#: erpnext/selling/doctype/customer/customer.py:366 msgid "A Customer Group exists with same name please change the Customer name or rename the Customer Group" msgstr "" @@ -1040,7 +1040,7 @@ msgstr "" msgid "A Reconciliation Job {0} is running for the same filters. Cannot reconcile now" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1773 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1783 msgid "A Reverse Journal Entry {0} already exists for this Journal Entry." msgstr "" @@ -1081,7 +1081,7 @@ msgstr "" msgid "A logical Warehouse against which stock entries are made." msgstr "" -#: erpnext/stock/serial_batch_bundle.py:1479 +#: erpnext/stock/serial_batch_bundle.py:1565 msgid "A naming series conflict occurred while creating serial numbers. Please change the naming series for the item {0}." msgstr "" @@ -1265,7 +1265,7 @@ msgid "Accepted Qty in Stock UOM" msgstr "" #. Label of the qty (Float) field in DocType 'Purchase Receipt Item' -#: erpnext/public/js/controllers/transaction.js:2870 +#: erpnext/public/js/controllers/transaction.js:2875 #: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json msgid "Accepted Quantity" msgstr "" @@ -1301,7 +1301,7 @@ msgstr "" msgid "According to CEFACT/ICG/2010/IC013 or CEFACT/ICG/2010/IC010" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1281 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1290 msgid "According to the BOM {0}, the Item '{1}' is missing in the stock entry." msgstr "" @@ -1425,7 +1425,7 @@ msgid "Account Manager" msgstr "" #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1063 -#: erpnext/controllers/accounts_controller.py:2423 +#: erpnext/controllers/accounts_controller.py:2428 msgid "Account Missing" msgstr "" @@ -1665,7 +1665,7 @@ msgstr "" msgid "Account {0} is frozen" msgstr "" -#: erpnext/controllers/accounts_controller.py:1498 +#: erpnext/controllers/accounts_controller.py:1503 msgid "Account {0} is invalid. Account Currency must be {1}" msgstr "" @@ -1701,7 +1701,7 @@ msgstr "" msgid "Account: {0} is not permitted under Payment Entry" msgstr "" -#: erpnext/controllers/accounts_controller.py:3307 +#: erpnext/controllers/accounts_controller.py:3312 msgid "Account: {0} with currency: {1} can not be selected" msgstr "" @@ -1986,8 +1986,8 @@ msgstr "" msgid "Accounting Entry for Asset" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2364 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2384 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2396 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2416 msgid "Accounting Entry for LCV in Stock Entry {0}" msgstr "" @@ -1999,20 +1999,20 @@ msgstr "" msgid "Accounting Entry for Service" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1046 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1067 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1085 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1106 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1127 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1155 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1267 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1532 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1554 -#: erpnext/controllers/stock_controller.py:773 -#: erpnext/controllers/stock_controller.py:790 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1056 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1077 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1095 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1116 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1137 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1165 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1277 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1542 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1564 +#: erpnext/controllers/stock_controller.py:787 +#: erpnext/controllers/stock_controller.py:804 #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:930 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2309 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2323 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2341 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2355 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:753 msgid "Accounting Entry for Stock" msgstr "" @@ -2021,7 +2021,7 @@ msgstr "" msgid "Accounting Entry for {0}" msgstr "" -#: erpnext/controllers/accounts_controller.py:2464 +#: erpnext/controllers/accounts_controller.py:2469 msgid "Accounting Entry for {0}: {1} can only be made in currency: {2}" msgstr "" @@ -2218,7 +2218,7 @@ msgstr "" msgid "Accounts Setup" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1338 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1348 msgid "Accounts table cannot be blank." msgstr "" @@ -2685,7 +2685,7 @@ msgstr "" msgid "Add Employees" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:275 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:264 #: erpnext/selling/doctype/sales_order/sales_order.js:285 #: erpnext/stock/dashboard/item_dashboard.js:216 msgid "Add Item" @@ -2737,8 +2737,8 @@ msgstr "" msgid "Add Order Discount" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:300 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:435 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:289 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:424 msgid "Add Phantom Item" msgstr "" @@ -2815,8 +2815,8 @@ msgstr "" msgid "Add Stock" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:300 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:435 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:289 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:424 msgid "Add Sub Assembly" msgstr "" @@ -3417,7 +3417,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:306 +#: erpnext/controllers/accounts_controller.py:311 #: erpnext/setup/doctype/company/company.json msgid "Advance Payments" msgstr "" @@ -3636,7 +3636,7 @@ msgstr "" msgid "Against Stock Entry" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:339 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:349 msgid "Against Supplier Invoice {0}" msgstr "" @@ -3967,11 +3967,11 @@ msgstr "" msgid "All items have already been received" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3696 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3728 msgid "All items have already been transferred for this Work Order." msgstr "" -#: erpnext/public/js/controllers/transaction.js:2993 +#: erpnext/public/js/controllers/transaction.js:2998 msgid "All items in this document already have a linked Quality Inspection." msgstr "" @@ -4205,8 +4205,8 @@ msgstr "" #. Valuation' #: erpnext/stock/doctype/item/item.json #: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json -#: erpnext/stock/doctype/stock_settings/stock_settings.py:217 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:229 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:222 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:234 msgid "Allow Negative Stock" msgstr "" @@ -4826,7 +4826,7 @@ msgstr "" #: erpnext/manufacturing/doctype/bom_item/bom_item.json #: erpnext/manufacturing/doctype/work_order_additional_item/work_order_additional_item.json #: erpnext/manufacturing/doctype/work_order_item/work_order_item.json -#: erpnext/public/js/controllers/transaction.js:569 +#: erpnext/public/js/controllers/transaction.js:571 #: erpnext/selling/doctype/quotation/quotation.js:315 #: erpnext/selling/doctype/quotation_item/quotation_item.json #: erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -5572,7 +5572,7 @@ msgstr "" msgid "Are you sure you want to clear all demo data?" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:499 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:488 msgid "Are you sure you want to delete this Item?" msgstr "" @@ -5650,11 +5650,11 @@ msgstr "" msgid "As the field {0} is enabled, the value of the field {1} should be more than 1." msgstr "" -#: erpnext/stock/doctype/item/item.py:1094 +#: erpnext/stock/doctype/item/item.py:1104 msgid "As there are existing submitted transactions against item {0}, you can not change the value of {1}." msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:242 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:247 msgid "As there are reserved stock, you cannot disable {0}." msgstr "" @@ -5662,12 +5662,12 @@ msgstr "" msgid "As there are sufficient Sub Assembly Items, Work Order is not required for Warehouse {0}." msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1850 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1849 msgid "As there are sufficient raw materials, Material Request is not required for Warehouse {0}." msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:216 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:228 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:221 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:233 msgid "As {0} is enabled, you can not enable {1}." msgstr "" @@ -6277,7 +6277,7 @@ msgstr "" msgid "At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} in the warehouse {4}." msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1502 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1552 msgid "At Row {0}: In Serial and Batch Bundle {1} must have docstatus as 1 and not 0" msgstr "" @@ -6293,7 +6293,7 @@ msgstr "" msgid "At least one invoice has to be selected." msgstr "" -#: erpnext/controllers/sales_and_purchase_return.py:168 +#: erpnext/controllers/sales_and_purchase_return.py:186 msgid "At least one item should be entered with negative quantity in return document" msgstr "" @@ -6310,7 +6310,7 @@ msgstr "" msgid "At least one of the Selling or Buying must be selected" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:428 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:437 msgid "At least one raw material item must be present in the stock entry for the type {0}" msgstr "" @@ -6318,11 +6318,11 @@ msgstr "" msgid "At least one row is required for a financial report template" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:993 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1002 msgid "At least one warehouse is mandatory" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:896 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:905 msgid "At row #{0}: the Difference Account must not be a Stock type account, please change the Account Type for the account {1} or select a different account" msgstr "" @@ -6330,11 +6330,11 @@ msgstr "" msgid "At row #{0}: the sequence id {1} cannot be less than previous row sequence id {2}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:907 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:916 msgid "At row #{0}: you have selected the Difference Account {1}, which is a Cost of Goods Sold type account. Please select a different account" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1250 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1300 msgid "At row {0}: Batch No is mandatory for Item {1}" msgstr "" @@ -6342,15 +6342,15 @@ msgstr "" msgid "At row {0}: Parent Row No cannot be set for item {1}" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1235 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1285 msgid "At row {0}: Qty is mandatory for the batch {1}" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1242 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1292 msgid "At row {0}: Serial No is mandatory for Item {1}" msgstr "" -#: erpnext/controllers/stock_controller.py:721 +#: erpnext/controllers/stock_controller.py:735 msgid "At row {0}: Serial and Batch Bundle {1} has already created. Please remove the values from the serial no or batch no fields." msgstr "" @@ -6414,11 +6414,11 @@ msgstr "" msgid "Attribute Value" msgstr "" -#: erpnext/stock/doctype/item/item.py:884 +#: erpnext/stock/doctype/item/item.py:894 msgid "Attribute Value {0} is not valid for the selected attribute {1}." msgstr "" -#: erpnext/stock/doctype/item/item.py:1030 +#: erpnext/stock/doctype/item/item.py:1040 msgid "Attribute table is mandatory" msgstr "" @@ -6426,19 +6426,19 @@ msgstr "" msgid "Attribute value: {0} must appear only once" msgstr "" -#: erpnext/stock/doctype/item/item.py:873 +#: erpnext/stock/doctype/item/item.py:883 msgid "Attribute {0} is disabled." msgstr "" -#: erpnext/stock/doctype/item/item.py:861 +#: erpnext/stock/doctype/item/item.py:871 msgid "Attribute {0} is not valid for the selected template." msgstr "" -#: erpnext/stock/doctype/item/item.py:1034 +#: erpnext/stock/doctype/item/item.py:1044 msgid "Attribute {0} selected multiple times in Attributes Table" msgstr "" -#: erpnext/stock/doctype/item/item.py:962 +#: erpnext/stock/doctype/item/item.py:972 msgid "Attributes" msgstr "" @@ -6863,7 +6863,7 @@ msgstr "" msgid "Available for use date is required" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1243 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1252 msgid "Available quantity is {0}, you need {1}" msgstr "" @@ -6926,7 +6926,7 @@ msgid "Avg Rate" msgstr "" #: erpnext/stock/report/available_serial_no/available_serial_no.py:154 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:369 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:372 msgid "Avg Rate (Balance Stock)" msgstr "" @@ -7258,7 +7258,7 @@ msgstr "" msgid "BOM Website Operation" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2802 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2834 msgid "BOM and Finished Good Quantity is mandatory for Disassembly" msgstr "" @@ -7390,7 +7390,7 @@ msgstr "" #: erpnext/stock/report/available_serial_no/available_serial_no.py:126 #: erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py:84 #: erpnext/stock/report/stock_balance/stock_balance.py:515 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:332 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:335 msgid "Balance Qty" msgstr "" @@ -7463,7 +7463,7 @@ msgstr "" #: erpnext/stock/report/available_serial_no/available_serial_no.py:174 #: erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py:86 #: erpnext/stock/report/stock_balance/stock_balance.py:522 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:389 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:392 msgid "Balance Value" msgstr "" @@ -8069,8 +8069,8 @@ msgstr "" #: erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py:34 #: erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py:80 #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:158 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:419 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:182 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:422 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:191 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:80 #: erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.js:19 #: erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py:32 @@ -8150,7 +8150,7 @@ msgstr "" #: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js:89 #: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py:115 -#: erpnext/public/js/controllers/transaction.js:2896 +#: erpnext/public/js/controllers/transaction.js:2901 #: erpnext/public/js/utils/barcode_scanner.js:286 #: erpnext/public/js/utils/serial_no_batch_selector.js:449 #: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -8181,11 +8181,11 @@ msgstr "" msgid "Batch No" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1253 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1303 msgid "Batch No is mandatory" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:3547 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:3597 msgid "Batch No {0} does not exists" msgstr "" @@ -8193,7 +8193,7 @@ msgstr "" msgid "Batch No {0} is linked with Item {1} which has serial no. Please scan serial no instead." msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:491 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:541 msgid "Batch No {0} is not present in the original {1} {2}, hence you can't return it against the {1} {2}" msgstr "" @@ -8208,11 +8208,11 @@ msgstr "" msgid "Batch Nos" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2075 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2125 msgid "Batch Nos are created successfully" msgstr "" -#: erpnext/controllers/sales_and_purchase_return.py:1196 +#: erpnext/controllers/sales_and_purchase_return.py:1214 msgid "Batch Not Available for Return" msgstr "" @@ -8281,16 +8281,16 @@ msgstr "" msgid "Batch {0} and Warehouse" msgstr "" -#: erpnext/controllers/sales_and_purchase_return.py:1195 +#: erpnext/controllers/sales_and_purchase_return.py:1213 msgid "Batch {0} is not available in warehouse {1}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3880 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3912 #: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py:290 msgid "Batch {0} of Item {1} has expired." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3886 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3918 msgid "Batch {0} of Item {1} is disabled." msgstr "" @@ -8304,7 +8304,7 @@ msgid "Batch-Wise Balance History" msgstr "" #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:164 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:194 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:203 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:86 msgid "Batchwise Valuation" msgstr "" @@ -8326,7 +8326,7 @@ msgstr "" msgid "Beginning of the current subscription period" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:360 +#: erpnext/accounts/doctype/subscription/subscription.py:363 msgid "Below Subscription Plans are of different currency to the party default billing currency/Company currency: {0}" msgstr "" @@ -8473,7 +8473,7 @@ msgstr "" msgid "Billing Address Name" msgstr "" -#: erpnext/controllers/accounts_controller.py:593 +#: erpnext/controllers/accounts_controller.py:598 msgid "Billing Address does not belong to the {0}" msgstr "" @@ -8550,7 +8550,7 @@ msgstr "" msgid "Billing Interval Count cannot be less than 1" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:409 +#: erpnext/accounts/doctype/subscription/subscription.py:412 msgid "Billing Interval in Subscription Plan must be Month to follow calendar months" msgstr "" @@ -8710,7 +8710,7 @@ msgid "Blanket Orders" msgstr "" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:109 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:271 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:269 msgid "Block Invoice" msgstr "" @@ -8857,7 +8857,7 @@ msgstr "" msgid "Both Receivable Account: {0} and Advance Account: {1} must be of same currency for company: {2}" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:379 +#: erpnext/accounts/doctype/subscription/subscription.py:382 msgid "Both Trial Period Start Date and Trial Period End Date must be set" msgstr "" @@ -9599,19 +9599,19 @@ msgstr "" msgid "Can not filter based on Voucher No, if grouped by Voucher" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1397 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1407 #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2899 msgid "Can only make payment against unbilled {0}" msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1510 -#: erpnext/controllers/accounts_controller.py:3216 +#: erpnext/controllers/accounts_controller.py:3221 #: erpnext/public/js/controllers/accounts.js:100 msgid "Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'" msgstr "" #: erpnext/setup/doctype/company/company.py:210 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:183 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:188 msgid "Can't change the valuation method, as there are transactions against some items which do not have its own valuation method" msgstr "" @@ -9660,7 +9660,7 @@ msgstr "" msgid "Cannot Change Inventory Account Setting" msgstr "" -#: erpnext/controllers/sales_and_purchase_return.py:438 +#: erpnext/controllers/sales_and_purchase_return.py:456 msgid "Cannot Create Return" msgstr "" @@ -9718,7 +9718,7 @@ msgstr "" msgid "Cannot cancel because submitted Stock Entry {0} exists" msgstr "" -#: erpnext/stock/stock_ledger.py:179 +#: erpnext/stock/stock_ledger.py:206 msgid "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet." msgstr "" @@ -9734,15 +9734,15 @@ msgstr "" msgid "Cannot cancel this document as it is linked with the submitted asset {asset_link}. Please cancel the asset to continue." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:671 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:680 msgid "Cannot cancel transaction for Completed Work Order." msgstr "" -#: erpnext/stock/doctype/item/item.py:982 +#: erpnext/stock/doctype/item/item.py:992 msgid "Cannot change Attributes after stock transaction. Make a new Item and transfer stock to the new Item" msgstr "" -#: erpnext/stock/doctype/item/item.py:1119 +#: erpnext/stock/doctype/item/item.py:1129 msgid "Cannot change Item {0} from serialized to non-serialized because a Serial and Batch Bundle exists for it. Please delete or cancel the Serial and Batch Bundle first." msgstr "" @@ -9754,7 +9754,7 @@ msgstr "" msgid "Cannot change Service Stop Date for item in row {0}" msgstr "" -#: erpnext/stock/doctype/item/item.py:973 +#: erpnext/stock/doctype/item/item.py:983 msgid "Cannot change Variant properties after stock transaction. You will have to make a new Item to do this." msgstr "" @@ -9762,7 +9762,7 @@ msgstr "" msgid "Cannot change company's default currency, because there are existing transactions. Transactions must be cancelled to change the default currency." msgstr "" -#: erpnext/projects/doctype/task/task.py:147 +#: erpnext/projects/doctype/task/task.py:148 msgid "Cannot complete task {0} as its dependant task {1} are not completed / cancelled." msgstr "" @@ -9799,7 +9799,7 @@ msgstr "" msgid "Cannot create accounting entries against disabled accounts: {0}" msgstr "" -#: erpnext/controllers/sales_and_purchase_return.py:437 +#: erpnext/controllers/sales_and_purchase_return.py:455 msgid "Cannot create return for consolidated invoice {0}." msgstr "" @@ -9807,7 +9807,7 @@ msgstr "" msgid "Cannot deactivate or cancel BOM as it is linked with other BOMs" msgstr "" -#: erpnext/crm/doctype/opportunity/opportunity.py:282 +#: erpnext/crm/doctype/opportunity/opportunity.py:292 msgid "Cannot declare as Lost because an active Quotation exists." msgstr "" @@ -9824,7 +9824,7 @@ msgstr "" msgid "Cannot delete Serial No {0}, as it is used in stock transactions" msgstr "" -#: erpnext/controllers/accounts_controller.py:3841 +#: erpnext/controllers/accounts_controller.py:3871 msgid "Cannot delete an item which has been ordered" msgstr "" @@ -9837,7 +9837,7 @@ msgstr "" msgid "Cannot delete virtual DocType: {0}. Virtual DocTypes do not have database tables." msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:148 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:153 msgid "Cannot disable Serial and Batch No for Item, as there are existing records for serial / batch." msgstr "" @@ -9845,7 +9845,7 @@ msgstr "" msgid "Cannot disable perpetual inventory, as there are existing Stock Ledger Entries for the company {0}. Please cancel the stock transactions first and try again." msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:129 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:134 msgid "Cannot disable {0} as it may lead to incorrect stock valuation." msgstr "" @@ -9853,7 +9853,7 @@ msgstr "" msgid "Cannot disassemble more than produced quantity." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1036 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1045 msgid "Cannot disassemble {0} qty against Stock Entry {1}. Only {2} qty available to disassemble." msgstr "" @@ -9882,11 +9882,11 @@ msgstr "" msgid "Cannot find Item with this Barcode" msgstr "" -#: erpnext/controllers/accounts_controller.py:3793 -msgid "Cannot find a default warehouse for item {0}. Please set one in the Item Master or in Stock Settings." +#: erpnext/controllers/accounts_controller.py:3810 +msgid "Cannot find a default warehouse for item {0}. Please select one in the Update Items dialog, or set a default in the Item Master or in Stock Settings." msgstr "" -#: erpnext/accounts/party.py:1108 +#: erpnext/accounts/party.py:1110 msgid "Cannot merge {0} '{1}' into '{2}' as both have existing accounting entries in different currencies for company '{3}'." msgstr "" @@ -9906,12 +9906,12 @@ msgstr "" msgid "Cannot receive from customer against negative outstanding" msgstr "" -#: erpnext/controllers/accounts_controller.py:3990 +#: erpnext/controllers/accounts_controller.py:4020 msgid "Cannot reduce quantity than ordered or purchased quantity" msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1523 -#: erpnext/controllers/accounts_controller.py:3231 +#: erpnext/controllers/accounts_controller.py:3236 #: erpnext/public/js/controllers/accounts.js:117 msgid "Cannot refer row number greater than or equal to current row number for this Charge type" msgstr "" @@ -9928,14 +9928,14 @@ msgstr "" msgid "Cannot retrieve link token. Check Error Log for more information" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:378 +#: erpnext/selling/doctype/customer/customer.py:379 msgid "Cannot select a Group type Customer Group. Please select a non-group Customer Group." msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1516 #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1694 #: erpnext/accounts/doctype/payment_entry/payment_entry.py:1848 -#: erpnext/controllers/accounts_controller.py:3221 +#: erpnext/controllers/accounts_controller.py:3226 #: erpnext/public/js/controllers/accounts.js:109 #: erpnext/public/js/controllers/taxes_and_totals.js:570 msgid "Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row" @@ -9953,11 +9953,11 @@ msgstr "" msgid "Cannot set multiple Item Defaults for a company." msgstr "" -#: erpnext/controllers/accounts_controller.py:3956 +#: erpnext/controllers/accounts_controller.py:3986 msgid "Cannot set quantity less than delivered quantity." msgstr "" -#: erpnext/controllers/accounts_controller.py:3957 +#: erpnext/controllers/accounts_controller.py:3987 msgid "Cannot set quantity less than received quantity." msgstr "" @@ -9973,7 +9973,7 @@ msgstr "" msgid "Cannot submit Job Card {0} while it is On Hold. Please resume and complete the job before submission." msgstr "" -#: erpnext/controllers/accounts_controller.py:3984 +#: erpnext/controllers/accounts_controller.py:4014 msgid "Cannot update rate as item {0} is already ordered or purchased against this quotation" msgstr "" @@ -10154,7 +10154,7 @@ msgstr "" msgid "Cash In Hand" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:329 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:339 msgid "Cash or Bank Account is mandatory for making payment entry" msgstr "" @@ -10388,7 +10388,7 @@ msgid "Channel Partner" msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2277 -#: erpnext/controllers/accounts_controller.py:3284 +#: erpnext/controllers/accounts_controller.py:3289 msgid "Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount" msgstr "" @@ -10582,7 +10582,7 @@ msgstr "" #. Label of the reference_date (Date) field in DocType 'Payment Entry' #: erpnext/accounts/doctype/payment_entry/payment_entry.json -#: erpnext/public/js/controllers/transaction.js:2807 +#: erpnext/public/js/controllers/transaction.js:2812 msgid "Cheque/Reference Date" msgstr "" @@ -10640,7 +10640,7 @@ msgstr "" #. Label of the child_row_reference (Data) field in DocType 'Quality #. Inspection' -#: erpnext/public/js/controllers/transaction.js:2902 +#: erpnext/public/js/controllers/transaction.js:2907 #: erpnext/stock/doctype/quality_inspection/quality_inspection.json msgid "Child Row Reference" msgstr "" @@ -10649,7 +10649,7 @@ msgstr "" msgid "Child Table Not Allowed" msgstr "" -#: erpnext/projects/doctype/task/task.py:314 +#: erpnext/projects/doctype/task/task.py:332 msgid "Child Task exists for this Task. You can not delete this Task." msgstr "" @@ -10667,7 +10667,7 @@ msgstr "" msgid "Child warehouse exists for this warehouse. You can not delete this warehouse." msgstr "" -#: erpnext/projects/doctype/task/task.py:262 +#: erpnext/projects/doctype/task/task.py:263 msgid "Circular Reference Error" msgstr "" @@ -10843,6 +10843,10 @@ msgstr "" msgid "Closed Documents" msgstr "" +#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:147 +msgid "Closed Period" +msgstr "" + #: erpnext/manufacturing/doctype/work_order/work_order.py:2775 msgid "Closed Work Order can not be stopped or Re-opened" msgstr "" @@ -10878,7 +10882,7 @@ msgstr "" msgid "Closing Account Head" msgstr "" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:133 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:136 msgid "Closing Account {0} must be of type Liability / Equity" msgstr "" @@ -11597,10 +11601,10 @@ msgstr "" #: erpnext/stock/report/stock_balance/stock_balance.js:8 #: erpnext/stock/report/stock_balance/stock_balance.py:576 #: erpnext/stock/report/stock_ledger/stock_ledger.js:8 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:442 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:445 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js:18 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.js:8 -#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js:8 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js:32 #: erpnext/stock/report/total_stock_summary/total_stock_summary.js:17 #: erpnext/stock/report/total_stock_summary/total_stock_summary.py:29 #: erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.js:8 @@ -11682,11 +11686,11 @@ msgstr "" msgid "Company Address Name" msgstr "" -#: erpnext/controllers/accounts_controller.py:4420 +#: erpnext/controllers/accounts_controller.py:4450 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:4408 +#: erpnext/controllers/accounts_controller.py:4438 msgid "Company Address is missing. You don't have permission to update it. Please contact your System Manager." msgstr "" @@ -11806,7 +11810,7 @@ msgstr "" msgid "Company is mandatory for company account" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:438 +#: erpnext/accounts/doctype/subscription/subscription.py:441 msgid "Company is mandatory for generating an invoice. Please set a default company in Global Defaults." msgstr "" @@ -11929,7 +11933,7 @@ msgstr "" msgid "Completed On" msgstr "" -#: erpnext/projects/doctype/task/task.py:187 +#: erpnext/projects/doctype/task/task.py:188 msgid "Completed On cannot be greater than Today" msgstr "" @@ -12082,7 +12086,7 @@ msgstr "" msgid "Configure Chart of Accounts" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:56 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:45 msgid "Configure Product Assembly" msgstr "" @@ -12384,7 +12388,7 @@ msgstr "" msgid "Consumer Products" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:209 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:218 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:101 msgid "Consumption Rate" msgstr "" @@ -12504,7 +12508,7 @@ msgstr "" msgid "Contact Person" msgstr "" -#: erpnext/controllers/accounts_controller.py:605 +#: erpnext/controllers/accounts_controller.py:610 msgid "Contact Person does not belong to the {0}" msgstr "" @@ -12672,7 +12676,7 @@ msgstr "" #: erpnext/manufacturing/doctype/bom_item/bom_item.json #: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json #: erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json -#: erpnext/public/js/utils.js:920 +#: erpnext/public/js/utils.js:923 #: erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json #: erpnext/stock/doctype/packed_item/packed_item.json #: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -12702,19 +12706,19 @@ msgstr "" msgid "Conversion factor for default Unit of Measure must be 1 in row {0}" msgstr "" -#: erpnext/controllers/stock_controller.py:163 +#: erpnext/controllers/stock_controller.py:177 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:2999 +#: erpnext/controllers/accounts_controller.py:3004 msgid "Conversion rate cannot be 0" msgstr "" -#: erpnext/controllers/accounts_controller.py:3006 +#: erpnext/controllers/accounts_controller.py:3011 msgid "Conversion rate is 1.00, but document currency is different from company currency" msgstr "" -#: erpnext/controllers/accounts_controller.py:3002 +#: erpnext/controllers/accounts_controller.py:3007 msgid "Conversion rate must be 1.00 if document currency is same as company currency" msgstr "" @@ -13068,7 +13072,7 @@ msgstr "" msgid "Cost Center is required" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1498 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1508 #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:898 msgid "Cost Center is required in row {0} in Taxes table for type {1}" msgstr "" @@ -13151,7 +13155,7 @@ msgstr "" msgid "Cost of Goods Sold" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:910 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:919 msgid "Cost of Goods Sold Account in Items Table" msgstr "" @@ -13539,7 +13543,7 @@ msgstr "" msgid "Create Payment Entry for Consolidated POS Invoices." msgstr "" -#: erpnext/public/js/controllers/transaction.js:577 +#: erpnext/public/js/controllers/transaction.js:579 msgid "Create Payment Request" msgstr "" @@ -13643,7 +13647,7 @@ msgid "Create Service Item" msgstr "" #: erpnext/stock/dashboard/item_dashboard.js:283 -#: erpnext/stock/doctype/material_request/material_request.js:653 +#: erpnext/stock/doctype/material_request/material_request.js:649 msgid "Create Stock Entry" msgstr "" @@ -13750,6 +13754,10 @@ msgstr "" msgid "Create Workstation" msgstr "" +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:228 +msgid "Create a Stock Closing Entry for the entire company with To Date as {0} before submitting the Period Closing Voucher." +msgstr "" + #: banking/src/components/features/BankReconciliation/MatchAndReconcile.tsx:683 msgid "Create a journal entry for expenses, income or split transactions" msgstr "" @@ -13767,7 +13775,7 @@ msgstr "" msgid "Create a variant with the template image." msgstr "" -#: erpnext/stock/stock_ledger.py:2052 +#: erpnext/stock/stock_ledger.py:2095 msgid "Create an incoming stock transaction for the Item." msgstr "" @@ -13859,7 +13867,7 @@ msgstr "" msgid "Creating Purchase Order ..." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:729 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:727 #: erpnext/buying/doctype/purchase_order/purchase_order.js:506 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js:74 msgid "Creating Purchase Receipt ..." @@ -13902,7 +13910,7 @@ msgid "Creating {} out of {} {}" msgstr "" #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:141 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:165 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:174 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:46 msgid "Creation" msgstr "" @@ -14040,7 +14048,7 @@ msgstr "" msgid "Credit Limit" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:657 +#: erpnext/selling/doctype/customer/customer.py:658 msgid "Credit Limit Crossed" msgstr "" @@ -14076,7 +14084,7 @@ msgstr "" #: 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:1223 -#: erpnext/controllers/sales_and_purchase_return.py:455 +#: erpnext/controllers/sales_and_purchase_return.py:473 #: erpnext/setup/setup_wizard/operations/install_fixtures.py:306 #: erpnext/stock/doctype/delivery_note/delivery_note.js:89 #: erpnext/stock/doctype/stock_entry/stock_entry.json @@ -14109,9 +14117,9 @@ msgstr "" #. Label of the credit_to (Link) field in DocType 'Purchase Invoice' #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:383 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:391 -#: erpnext/controllers/accounts_controller.py:2403 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:393 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:401 +#: erpnext/controllers/accounts_controller.py:2408 msgid "Credit To" msgstr "" @@ -14120,16 +14128,16 @@ msgstr "" msgid "Credit in Company Currency" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:623 -#: erpnext/selling/doctype/customer/customer.py:678 +#: erpnext/selling/doctype/customer/customer.py:624 +#: erpnext/selling/doctype/customer/customer.py:679 msgid "Credit limit has been crossed for customer {0} ({1}/{2})" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:405 +#: erpnext/selling/doctype/customer/customer.py:406 msgid "Credit limit is already defined for the Company {0}" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:677 +#: erpnext/selling/doctype/customer/customer.py:678 msgid "Credit limit reached for customer {0}" msgstr "" @@ -14322,7 +14330,7 @@ msgstr "" msgid "Currency for {0} must be {1}" msgstr "" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:140 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:143 msgid "Currency of the Closing Account must be {0}" msgstr "" @@ -15261,7 +15269,7 @@ msgid "Cycle/Second" msgstr "" #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:204 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:254 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:263 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:146 msgid "D - E" msgstr "" @@ -15586,7 +15594,7 @@ msgstr "" #: 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:1226 -#: erpnext/controllers/sales_and_purchase_return.py:459 +#: erpnext/controllers/sales_and_purchase_return.py:477 #: erpnext/setup/setup_wizard/operations/install_fixtures.py:307 #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:45 #: erpnext/workspace_sidebar/invoicing.json @@ -15615,7 +15623,7 @@ msgstr "" #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1067 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1078 -#: erpnext/controllers/accounts_controller.py:2403 +#: erpnext/controllers/accounts_controller.py:2408 msgid "Debit To" msgstr "" @@ -15803,7 +15811,7 @@ msgstr "" msgid "Default BOM for {0} not found" msgstr "" -#: erpnext/controllers/accounts_controller.py:4028 +#: erpnext/controllers/accounts_controller.py:4058 msgid "Default BOM not found for FG Item {0}" msgstr "" @@ -16139,15 +16147,15 @@ msgstr "" msgid "Default Unit of Measure" msgstr "" -#: erpnext/stock/doctype/item/item.py:1396 +#: erpnext/stock/doctype/item/item.py:1406 msgid "Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You need to either cancel the linked documents or create a new Item." msgstr "" -#: erpnext/stock/doctype/item/item.py:1379 +#: erpnext/stock/doctype/item/item.py:1389 msgid "Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM." msgstr "" -#: erpnext/stock/doctype/item/item.py:1008 +#: erpnext/stock/doctype/item/item.py:1018 msgid "Default Unit of Measure for Variant '{0}' must be same as in Template '{1}'" msgstr "" @@ -16546,7 +16554,7 @@ msgstr "" #: erpnext/manufacturing/doctype/master_production_schedule_item/master_production_schedule_item.json #: erpnext/manufacturing/doctype/sales_forecast_item/sales_forecast_item.json #: erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.py:1069 -#: erpnext/public/js/utils.js:913 +#: erpnext/public/js/utils.js:916 #: erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json #: erpnext/selling/doctype/sales_order/sales_order.js:632 #: erpnext/selling/doctype/sales_order/sales_order.js:1533 @@ -16802,7 +16810,7 @@ msgstr "" msgid "Dependent Task" msgstr "" -#: erpnext/projects/doctype/task/task.py:180 +#: erpnext/projects/doctype/task/task.py:181 msgid "Dependent Task {0} is not a Template Task" msgstr "" @@ -17095,7 +17103,7 @@ msgstr "" #: erpnext/public/js/bank_reconciliation_tool/number_card.js:30 #: erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py:130 #: erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py:35 -#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:35 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:41 msgid "Difference" msgstr "" @@ -17121,11 +17129,11 @@ msgstr "" msgid "Difference Account" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:899 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:908 msgid "Difference Account in Items Table" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:888 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:897 msgid "Difference Account must be a Asset/Liability type account (Temporary Opening), since this Stock Entry is an Opening Entry" msgstr "" @@ -17347,7 +17355,7 @@ msgstr "" msgid "Disabled items cannot be selected in any transaction." msgstr "" -#: erpnext/controllers/accounts_controller.py:931 +#: erpnext/controllers/accounts_controller.py:936 msgid "Disabled pricing rules since this {} is an internal transfer" msgstr "" @@ -17356,7 +17364,7 @@ msgstr "" msgid "Disabled suppliers are hidden from selection in new transactions but remain in historical records" msgstr "" -#: erpnext/controllers/accounts_controller.py:945 +#: erpnext/controllers/accounts_controller.py:950 msgid "Disabled tax included prices since this {} is an internal transfer" msgstr "" @@ -17384,7 +17392,7 @@ msgstr "" msgid "Disassemble Order" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2744 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2776 msgid "Disassemble Qty cannot be less than or equal to 0." msgstr "" @@ -17899,7 +17907,7 @@ msgstr "" msgid "Do Not Explode" msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:130 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:135 msgid "Do Not Use Batchwise Valuation" msgstr "" @@ -18030,7 +18038,7 @@ msgstr "" msgid "Documents Processed on each trigger. Queue Size should be between 5 and 100" msgstr "" -#: erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py:486 +#: erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py:491 msgid "Documents: {0} have deferred revenue/expense enabled for them. Cannot repost." msgstr "" @@ -18324,11 +18332,11 @@ msgstr "" msgid "Duplicate Sales Invoices found" msgstr "" -#: erpnext/stock/serial_batch_bundle.py:1482 +#: erpnext/stock/serial_batch_bundle.py:1568 msgid "Duplicate Serial Number Error" msgstr "" -#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:81 +#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:123 msgid "Duplicate Stock Closing Entry" msgstr "" @@ -18474,7 +18482,7 @@ msgstr "" msgid "Earnest Money" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:544 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:533 msgid "Edit BOM" msgstr "" @@ -18562,8 +18570,8 @@ msgstr "" msgid "Either 'Selling' or 'Buying' must be selected" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:309 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:460 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:298 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:449 msgid "Either Workstation or Workstation Type is mandatory" msgstr "" @@ -18917,7 +18925,7 @@ msgstr "" msgid "Ems(Pica)" msgstr "" -#: erpnext/public/js/controllers/transaction.js:2965 +#: erpnext/public/js/controllers/transaction.js:2970 msgid "Enable {0} on the Item master to proceed with {1} inspection." msgstr "" @@ -18949,7 +18957,7 @@ msgstr "" msgid "Enable Auto Email" msgstr "" -#: erpnext/stock/doctype/item/item.py:1188 +#: erpnext/stock/doctype/item/item.py:1198 msgid "Enable Auto Re-Order" msgstr "" @@ -19620,7 +19628,7 @@ msgstr "" msgid "Example URL" msgstr "" -#: erpnext/stock/doctype/item/item.py:1100 +#: erpnext/stock/doctype/item/item.py:1110 msgid "Example of a linked document: {0}" msgstr "" @@ -19640,7 +19648,7 @@ msgstr "" msgid "Example: If the transaction amount is 200, then this will be calculated as {} = {}" msgstr "" -#: erpnext/stock/stock_ledger.py:2334 +#: erpnext/stock/stock_ledger.py:2377 msgid "Example: Serial No {0} reserved in {1}." msgstr "" @@ -19650,11 +19658,11 @@ msgstr "" msgid "Exception Budget Approver Role" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1043 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1052 msgid "Excess Disassembly" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1339 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1348 msgid "Excess Material Transfer" msgstr "" @@ -19702,8 +19710,8 @@ msgstr "" msgid "Exchange Gain/Loss" msgstr "" -#: erpnext/controllers/accounts_controller.py:1804 -#: erpnext/controllers/accounts_controller.py:1889 +#: erpnext/controllers/accounts_controller.py:1809 +#: erpnext/controllers/accounts_controller.py:1894 msgid "Exchange Gain/Loss amount has been booked through {0}" msgstr "" @@ -19947,7 +19955,7 @@ msgstr "" msgid "Expected End Date" msgstr "" -#: erpnext/projects/doctype/task/task.py:114 +#: erpnext/projects/doctype/task/task.py:115 msgid "Expected End Date should be less than or equal to parent task's Expected End Date {0}." msgstr "" @@ -20005,7 +20013,7 @@ msgstr "" #: erpnext/accounts/doctype/cashier_closing/cashier_closing.json #: erpnext/accounts/doctype/ledger_merge/ledger_merge.json #: erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.json -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:601 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:611 #: erpnext/accounts/report/account_balance/account_balance.js:28 #: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js:89 #: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:192 @@ -20013,7 +20021,7 @@ msgstr "" msgid "Expense" msgstr "" -#: erpnext/controllers/stock_controller.py:1067 +#: erpnext/controllers/stock_controller.py:1081 msgid "Expense / Difference account ({0}) must be a 'Profit or Loss' account" msgstr "" @@ -20061,7 +20069,7 @@ msgstr "" msgid "Expense Account" msgstr "" -#: erpnext/controllers/stock_controller.py:1047 +#: erpnext/controllers/stock_controller.py:1061 msgid "Expense Account Missing" msgstr "" @@ -20076,13 +20084,13 @@ msgstr "" msgid "Expense Head" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:495 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:519 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:539 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:505 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:529 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:549 msgid "Expense Head Changed" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:597 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:607 msgid "Expense account is mandatory for item {0}" msgstr "" @@ -20114,7 +20122,7 @@ msgstr "" msgid "Expenses Added To Stock Contra Account" msgstr "" -#: erpnext/controllers/stock_controller.py:920 +#: erpnext/controllers/stock_controller.py:934 msgid "Expenses Added To Stock for Item {0}" msgstr "" @@ -20267,7 +20275,7 @@ msgid "FIFO Stock Queue (qty, rate)" msgstr "" #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:179 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:229 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:238 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:121 msgid "FIFO/LIFO Queue" msgstr "" @@ -20486,7 +20494,7 @@ msgid "Fetching Sales Orders..." msgstr "" #: erpnext/accounts/doctype/dunning/dunning.js:135 -#: erpnext/public/js/controllers/transaction.js:1617 +#: erpnext/public/js/controllers/transaction.js:1619 msgid "Fetching exchange rates ..." msgstr "" @@ -20773,7 +20781,7 @@ msgstr "" #. Service Item' #. Label of the fg_item (Link) field in DocType 'Subcontracting Order Service #. Item' -#: erpnext/public/js/utils.js:939 +#: erpnext/public/js/utils.js:965 #: erpnext/subcontracting/doctype/subcontracting_inward_order_service_item/subcontracting_inward_order_service_item.json #: erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json msgid "Finished Good Item" @@ -20786,7 +20794,7 @@ msgstr "" msgid "Finished Good Item Code" msgstr "" -#: erpnext/public/js/utils.js:957 +#: erpnext/public/js/utils.js:983 msgid "Finished Good Item Qty" msgstr "" @@ -20799,15 +20807,15 @@ msgstr "" msgid "Finished Good Item Quantity" msgstr "" -#: erpnext/controllers/accounts_controller.py:4014 +#: erpnext/controllers/accounts_controller.py:4044 msgid "Finished Good Item is not specified for service item {0}" msgstr "" -#: erpnext/controllers/accounts_controller.py:4031 +#: erpnext/controllers/accounts_controller.py:4061 msgid "Finished Good Item {0} Qty can not be zero" msgstr "" -#: erpnext/controllers/accounts_controller.py:4025 +#: erpnext/controllers/accounts_controller.py:4055 msgid "Finished Good Item {0} must be a sub-contracted item" msgstr "" @@ -20894,11 +20902,11 @@ msgstr "" msgid "Finished Goods based Operating Cost" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2070 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2102 msgid "Finished Item {0} does not match with Work Order {1}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1060 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1069 msgid "Finished good quantity being consumed ({0} in stock UOM) must equal the quantity to disassemble ({1}). Do not change the UOM, conversion factor or quantity of the finished good row." msgstr "" @@ -21147,7 +21155,7 @@ msgstr "" msgid "Following Material Requests have been raised automatically based on Item's re-order level" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:966 +#: erpnext/selling/doctype/customer/customer.py:967 msgid "Following fields are mandatory to create address:" msgstr "" @@ -21204,7 +21212,7 @@ msgstr "" msgid "For Item" msgstr "" -#: erpnext/controllers/stock_controller.py:1769 +#: erpnext/controllers/stock_controller.py:1783 msgid "For Item {0} cannot be received more than {1} qty against the {2} {3}" msgstr "" @@ -21239,7 +21247,7 @@ msgstr "" msgid "For Production" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1010 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1019 msgid "For Quantity (Manufactured Qty) is mandatory" msgstr "" @@ -21249,7 +21257,7 @@ msgstr "" msgid "For Raw Materials" msgstr "" -#: erpnext/controllers/accounts_controller.py:1469 +#: erpnext/controllers/accounts_controller.py:1474 msgid "For Return Invoices with Stock effect, '0' qty Items are not allowed. Following rows are affected: {0}" msgstr "" @@ -21350,7 +21358,7 @@ msgstr "" msgid "For projected and forecast quantities, the system will consider all child warehouses under the selected parent warehouse." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2102 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2134 msgid "For quantity {0} should not be greater than allowed quantity {1}" msgstr "" @@ -21364,7 +21372,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:1729 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1728 msgid "For row {0}: Enter Planned Qty" msgstr "" @@ -21383,20 +21391,20 @@ msgstr "" msgid "For the convenience of customers, these codes can be used in print formats like Invoices and Delivery Notes" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1270 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1279 msgid "For the item {0}, the consumed quantity should be {1} according to the BOM {2}." msgstr "" -#: erpnext/public/js/controllers/transaction.js:1427 +#: erpnext/public/js/controllers/transaction.js:1429 msgctxt "Clear payment terms template and/or payment schedule when due date is changed" msgid "For the new {0} to take effect, would you like to clear the current {1}?" msgstr "" -#: erpnext/controllers/stock_controller.py:488 +#: erpnext/controllers/stock_controller.py:502 msgid "For the {0}, no stock is available for the return in the warehouse {1}." msgstr "" -#: erpnext/controllers/sales_and_purchase_return.py:1247 +#: erpnext/controllers/sales_and_purchase_return.py:1265 msgid "For the {0}, the quantity is required to make the return entry" msgstr "" @@ -22005,7 +22013,7 @@ msgstr "" msgid "Future date is not allowed" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:269 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:278 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:161 msgid "G - D" msgstr "" @@ -22553,7 +22561,7 @@ msgstr "" msgid "Goods Transferred" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2671 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2703 msgid "Goods are already received against the outward entry {0}" msgstr "" @@ -22852,7 +22860,7 @@ msgstr "" msgid "Group Same Items" msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:158 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:163 msgid "Group Warehouses cannot be used in transactions. Please change the value of {0}" msgstr "" @@ -22915,7 +22923,7 @@ msgstr "" msgid "Growth View" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:279 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:288 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:171 msgid "H - F" msgstr "" @@ -23184,7 +23192,7 @@ msgstr "" msgid "Here are the error logs for the aforementioned failed depreciation entries: {0}" msgstr "" -#: erpnext/stock/stock_ledger.py:2037 +#: erpnext/stock/stock_ledger.py:2080 msgid "Here are the options to proceed:" msgstr "" @@ -23433,12 +23441,12 @@ msgstr "" msgid "Hundredweight (US)" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:294 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:303 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:186 msgid "I - J" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:304 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:313 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:196 msgid "I - K" msgstr "" @@ -23843,7 +23851,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:2047 +#: erpnext/stock/stock_ledger.py:2090 msgid "If not, you can Cancel / Submit this entry" msgstr "" @@ -23889,7 +23897,7 @@ msgstr "" msgid "If the account is frozen, entries are allowed to restricted users." msgstr "" -#: erpnext/stock/stock_ledger.py:2040 +#: erpnext/stock/stock_ledger.py:2083 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 "" @@ -23990,7 +23998,7 @@ msgstr "" msgid "If you still want to proceed, please disable '{0}' checkbox." msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1855 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1854 msgid "If you still want to proceed, please enable {0}." msgstr "" @@ -24330,7 +24338,7 @@ msgstr "" #: erpnext/stock/report/available_serial_no/available_serial_no.py:112 #: erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py:82 #: erpnext/stock/report/stock_balance/stock_balance.py:543 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:318 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:321 msgid "In Qty" msgstr "" @@ -24348,11 +24356,11 @@ msgstr "" msgid "In Transit" msgstr "" -#: erpnext/stock/doctype/material_request/material_request.js:652 +#: erpnext/stock/doctype/material_request/material_request.js:648 msgid "In Transit Transfer" msgstr "" -#: erpnext/stock/doctype/material_request/material_request.js:621 +#: erpnext/stock/doctype/material_request/material_request.js:617 msgid "In Transit Warehouse" msgstr "" @@ -24773,8 +24781,8 @@ msgstr "" #: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json #: erpnext/stock/report/available_serial_no/available_serial_no.py:146 #: erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py:167 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:361 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:204 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:364 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:213 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:96 msgid "Incoming Rate" msgstr "" @@ -24813,7 +24821,7 @@ msgstr "" msgid "Incorrect Company" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1277 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1286 msgid "Incorrect Component Quantity" msgstr "" @@ -24863,7 +24871,7 @@ msgstr "" #: erpnext/stock/doctype/pick_list/pick_list.py:192 #: erpnext/stock/doctype/pick_list/pick_list.py:216 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:161 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:166 msgid "Incorrect Warehouse" msgstr "" @@ -25027,14 +25035,14 @@ msgstr "" msgid "Inspected By" msgstr "" -#: erpnext/controllers/stock_controller.py:1663 +#: erpnext/controllers/stock_controller.py:1677 #: erpnext/manufacturing/doctype/job_card/job_card.py:834 msgid "Inspection Rejected" msgstr "" #. Label of the inspection_required (Check) field in DocType 'Stock Entry' -#: erpnext/controllers/stock_controller.py:1633 -#: erpnext/controllers/stock_controller.py:1635 +#: erpnext/controllers/stock_controller.py:1647 +#: erpnext/controllers/stock_controller.py:1649 #: erpnext/stock/doctype/stock_entry/stock_entry.json msgid "Inspection Required" msgstr "" @@ -25051,7 +25059,7 @@ msgstr "" msgid "Inspection Required before Purchase" msgstr "" -#: erpnext/controllers/stock_controller.py:1648 +#: erpnext/controllers/stock_controller.py:1662 #: erpnext/manufacturing/doctype/job_card/job_card.py:815 msgid "Inspection Submission" msgstr "" @@ -25121,11 +25129,11 @@ msgstr "" msgid "Insufficient Capacity" msgstr "" -#: erpnext/controllers/accounts_controller.py:3910 -#: erpnext/controllers/accounts_controller.py:3932 -#: erpnext/controllers/accounts_controller.py:4450 -#: erpnext/controllers/accounts_controller.py:4456 -#: erpnext/controllers/accounts_controller.py:4478 +#: erpnext/controllers/accounts_controller.py:3940 +#: erpnext/controllers/accounts_controller.py:3962 +#: erpnext/controllers/accounts_controller.py:4480 +#: erpnext/controllers/accounts_controller.py:4486 +#: erpnext/controllers/accounts_controller.py:4508 msgid "Insufficient Permissions" msgstr "" @@ -25133,13 +25141,13 @@ msgstr "" #: erpnext/stock/doctype/pick_list/pick_list.py:150 #: erpnext/stock/doctype/pick_list/pick_list.py:168 #: erpnext/stock/doctype/pick_list/pick_list.py:1123 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1247 -#: erpnext/stock/serial_batch_bundle.py:1225 erpnext/stock/stock_ledger.py:1728 -#: erpnext/stock/stock_ledger.py:2225 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1256 +#: erpnext/stock/serial_batch_bundle.py:1311 erpnext/stock/stock_ledger.py:1771 +#: erpnext/stock/stock_ledger.py:2268 msgid "Insufficient Stock" msgstr "" -#: erpnext/stock/stock_ledger.py:2240 +#: erpnext/stock/stock_ledger.py:2283 msgid "Insufficient Stock for Batch" msgstr "" @@ -25294,7 +25302,7 @@ msgstr "" msgid "Internal Customer Accounting" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:264 +#: erpnext/selling/doctype/customer/customer.py:265 msgid "Internal Customer for company {0} already exists" msgstr "" @@ -25302,7 +25310,7 @@ msgstr "" msgid "Internal Purchase Order" msgstr "" -#: erpnext/controllers/accounts_controller.py:831 +#: erpnext/controllers/accounts_controller.py:836 msgid "Internal Sale or Delivery Reference missing." msgstr "" @@ -25310,7 +25318,7 @@ msgstr "" msgid "Internal Sales Order" msgstr "" -#: erpnext/controllers/accounts_controller.py:833 +#: erpnext/controllers/accounts_controller.py:838 msgid "Internal Sales Reference Missing" msgstr "" @@ -25341,7 +25349,7 @@ msgstr "" msgid "Internal Transfer" msgstr "" -#: erpnext/controllers/accounts_controller.py:842 +#: erpnext/controllers/accounts_controller.py:847 msgid "Internal Transfer Reference Missing" msgstr "" @@ -25365,7 +25373,7 @@ msgstr "" msgid "Internal notes about this customer. Not visible on transactions or the portal." msgstr "" -#: erpnext/controllers/stock_controller.py:1730 +#: erpnext/controllers/stock_controller.py:1744 msgid "Internal transfers can only be done in company's default currency" msgstr "" @@ -25379,14 +25387,14 @@ msgstr "" msgid "Interval should be between 1 to 59 MInutes" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:384 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:392 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:394 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:402 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1073 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1083 #: erpnext/assets/doctype/asset_category/asset_category.py:69 #: erpnext/assets/doctype/asset_category/asset_category.py:97 -#: erpnext/controllers/accounts_controller.py:3245 -#: erpnext/controllers/accounts_controller.py:3253 +#: erpnext/controllers/accounts_controller.py:3250 +#: erpnext/controllers/accounts_controller.py:3258 msgid "Invalid Account" msgstr "" @@ -25411,7 +25419,7 @@ msgstr "" msgid "Invalid Attribute Values" msgstr "" -#: erpnext/controllers/accounts_controller.py:645 +#: erpnext/controllers/accounts_controller.py:650 msgid "Invalid Auto Repeat Date" msgstr "" @@ -25424,7 +25432,7 @@ msgstr "" msgid "Invalid Barcode. There is no Item attached to this barcode." msgstr "" -#: erpnext/public/js/controllers/transaction.js:3186 +#: erpnext/public/js/controllers/transaction.js:3191 msgid "Invalid Blanket Order for the selected Customer and Item" msgstr "" @@ -25446,11 +25454,11 @@ msgstr "" #: erpnext/assets/doctype/asset/asset.py:365 #: erpnext/assets/doctype/asset/asset.py:372 -#: erpnext/controllers/accounts_controller.py:3268 +#: erpnext/controllers/accounts_controller.py:3273 msgid "Invalid Cost Center" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:379 +#: erpnext/selling/doctype/customer/customer.py:380 msgid "Invalid Customer Group" msgstr "" @@ -25458,12 +25466,12 @@ msgstr "" msgid "Invalid Delivery Date" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1099 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1108 msgid "Invalid Disassembly Item" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1065 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1114 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1074 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1123 msgid "Invalid Disassembly Quantity" msgstr "" @@ -25491,8 +25499,8 @@ msgstr "" msgid "Invalid File Type" msgstr "" -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:329 -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:334 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:374 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:379 msgid "Invalid Formula" msgstr "" @@ -25505,7 +25513,7 @@ msgstr "" msgid "Invalid Item" msgstr "" -#: erpnext/stock/doctype/item/item.py:1534 +#: erpnext/stock/doctype/item/item.py:1544 msgid "Invalid Item Defaults" msgstr "" @@ -25561,12 +25569,12 @@ msgstr "" msgid "Invalid Purchase Invoice" msgstr "" -#: erpnext/controllers/accounts_controller.py:3952 -#: erpnext/controllers/accounts_controller.py:3966 +#: erpnext/controllers/accounts_controller.py:3982 +#: erpnext/controllers/accounts_controller.py:3996 msgid "Invalid Qty" msgstr "" -#: erpnext/controllers/accounts_controller.py:1487 +#: erpnext/controllers/accounts_controller.py:1492 msgid "Invalid Quantity" msgstr "" @@ -25574,6 +25582,10 @@ msgstr "" msgid "Invalid Query" msgstr "" +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:325 +msgid "Invalid Reading" +msgstr "" + #: erpnext/selling/page/point_of_sale/pos_past_order_summary.js:202 msgid "Invalid Return" msgstr "" @@ -25591,12 +25603,12 @@ msgstr "" msgid "Invalid Selling Price" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2145 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2177 msgid "Invalid Serial and Batch Bundle" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1366 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1388 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1375 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1397 msgid "Invalid Source and Target Warehouse" msgstr "" @@ -25899,6 +25911,10 @@ msgstr "" msgid "Invoice can't be made for zero billing hour" msgstr "" +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1933 +msgid "Invoice is not blocked. Block the invoice to change the release date." +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 @@ -26610,7 +26626,7 @@ msgstr "" msgid "It can take upto few hours for accurate stock values to be visible after merging items." msgstr "" -#: erpnext/public/js/controllers/transaction.js:2564 +#: erpnext/public/js/controllers/transaction.js:2569 msgid "It is needed to fetch Item Details." msgstr "" @@ -26688,8 +26704,8 @@ msgstr "" #: erpnext/manufacturing/report/process_loss_report/process_loss_report.js:15 #: erpnext/manufacturing/report/process_loss_report/process_loss_report.py:74 #: erpnext/manufacturing/workspace/manufacturing/manufacturing.json -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:253 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:404 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:242 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:393 #: erpnext/public/js/purchase_trends_filters.js:48 #: erpnext/public/js/purchase_trends_filters.js:63 #: erpnext/public/js/sales_trends_filters.js:23 @@ -26736,7 +26752,7 @@ msgstr "" #: erpnext/stock/report/stock_analytics/stock_analytics.js:15 #: erpnext/stock/report/stock_analytics/stock_analytics.py:43 #: erpnext/stock/report/stock_balance/stock_balance.py:470 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:288 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:291 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js:27 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:51 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.js:28 @@ -26990,10 +27006,10 @@ msgstr "" #: erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py:86 #: erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py:119 #: erpnext/projects/doctype/timesheet/timesheet.js:216 -#: erpnext/public/js/controllers/transaction.js:2858 +#: erpnext/public/js/controllers/transaction.js:2863 #: erpnext/public/js/stock_reservation.js:112 #: erpnext/public/js/stock_reservation.js:318 erpnext/public/js/utils.js:596 -#: erpnext/public/js/utils.js:753 +#: erpnext/public/js/utils.js:754 #: erpnext/public/js/utils/serial_no_batch_selector.js:96 #: erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.json #: erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json @@ -27056,7 +27072,7 @@ msgstr "" #: erpnext/stock/report/stock_ageing/stock_ageing.py:177 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:105 #: erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py:25 -#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:26 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:32 #: erpnext/subcontracting/doctype/subcontracting_inward_order_item/subcontracting_inward_order_item.json #: erpnext/subcontracting/doctype/subcontracting_inward_order_received_item/subcontracting_inward_order_received_item.json #: erpnext/subcontracting/doctype/subcontracting_inward_order_secondary_item/subcontracting_inward_order_secondary_item.json @@ -27086,7 +27102,7 @@ msgstr "" msgid "Item Code cannot be changed for Serial No." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:451 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:461 msgid "Item Code required at Row No {0}" msgstr "" @@ -27259,7 +27275,7 @@ msgstr "" #: erpnext/stock/report/stock_balance/stock_balance.js:32 #: erpnext/stock/report/stock_balance/stock_balance.py:478 #: erpnext/stock/report/stock_ledger/stock_ledger.js:71 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:346 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:349 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.js:39 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:114 #: erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.js:33 @@ -27477,8 +27493,8 @@ msgstr "" #: erpnext/manufacturing/report/production_planning_report/production_planning_report.py:371 #: erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py:92 #: erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py:138 -#: erpnext/public/js/controllers/transaction.js:2864 -#: erpnext/public/js/utils.js:849 +#: erpnext/public/js/controllers/transaction.js:2869 +#: erpnext/public/js/utils.js:852 #: erpnext/selling/doctype/quotation_item/quotation_item.json #: erpnext/selling/doctype/sales_order/sales_order.js:1286 #: erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -27521,10 +27537,10 @@ msgstr "" #: erpnext/stock/report/stock_ageing/stock_ageing.py:183 #: erpnext/stock/report/stock_analytics/stock_analytics.py:45 #: erpnext/stock/report/stock_balance/stock_balance.py:476 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:294 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:297 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:111 #: erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py:31 -#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:32 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:38 #: erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py:98 #: erpnext/subcontracting/doctype/subcontracting_inward_order_item/subcontracting_inward_order_item.json #: erpnext/subcontracting/doctype/subcontracting_inward_order_service_item/subcontracting_inward_order_service_item.json @@ -27886,15 +27902,15 @@ msgstr "" msgid "Item and Warranty Details" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3859 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3891 msgid "Item for row {0} does not match Material Request" msgstr "" -#: erpnext/stock/doctype/item/item.py:895 +#: erpnext/stock/doctype/item/item.py:905 msgid "Item has variants." msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:455 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:444 msgid "Item is mandatory in Raw Materials table." msgstr "" @@ -27916,11 +27932,11 @@ msgstr "" msgid "Item operation" msgstr "" -#: erpnext/controllers/accounts_controller.py:4006 +#: erpnext/controllers/accounts_controller.py:4036 msgid "Item qty can not be updated as raw materials are already processed." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1520 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1531 msgid "Item rate has been updated to zero as Allow Zero Valuation Rate is checked for item {0}" msgstr "" @@ -27943,7 +27959,7 @@ msgstr "" msgid "Item valuation reposting in progress. Report might show incorrect item valuation." msgstr "" -#: erpnext/stock/doctype/item/item.py:1052 +#: erpnext/stock/doctype/item/item.py:1062 msgid "Item variant {0} exists with same attributes" msgstr "" @@ -27969,6 +27985,7 @@ msgstr "" #: erpnext/assets/doctype/asset/asset.py:347 #: erpnext/stock/doctype/item/item.py:687 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:102 msgid "Item {0} does not exist" msgstr "" @@ -27976,7 +27993,7 @@ msgstr "" msgid "Item {0} does not exist in the system or has expired" msgstr "" -#: erpnext/controllers/stock_controller.py:602 +#: erpnext/controllers/stock_controller.py:616 msgid "Item {0} does not exist." msgstr "" @@ -27984,7 +28001,7 @@ msgstr "" msgid "Item {0} entered multiple times." msgstr "" -#: erpnext/controllers/sales_and_purchase_return.py:221 +#: erpnext/controllers/sales_and_purchase_return.py:239 msgid "Item {0} has already been returned" msgstr "" @@ -28000,11 +28017,11 @@ msgstr "" msgid "Item {0} has no changes in delivered quantity. Please unselect the row if you do not wish to update its quantity." msgstr "" -#: erpnext/stock/doctype/item/item.py:1250 +#: erpnext/stock/doctype/item/item.py:1260 msgid "Item {0} has reached its end of life on {1}" msgstr "" -#: erpnext/stock/stock_ledger.py:117 +#: erpnext/stock/stock_ledger.py:144 msgid "Item {0} ignored since it is not a stock item" msgstr "" @@ -28012,11 +28029,11 @@ msgstr "" msgid "Item {0} is already reserved/delivered against Sales Order {1}." msgstr "" -#: erpnext/stock/doctype/item/item.py:1270 +#: erpnext/stock/doctype/item/item.py:1280 msgid "Item {0} is cancelled" msgstr "" -#: erpnext/stock/doctype/item/item.py:1254 +#: erpnext/stock/doctype/item/item.py:1264 msgid "Item {0} is disabled" msgstr "" @@ -28028,7 +28045,7 @@ msgstr "" msgid "Item {0} is not a serialized Item" msgstr "" -#: erpnext/stock/doctype/item/item.py:1262 +#: erpnext/stock/doctype/item/item.py:1272 msgid "Item {0} is not a stock Item" msgstr "" @@ -28040,7 +28057,7 @@ msgstr "" msgid "Item {0} is not a template item." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2583 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2615 msgid "Item {0} is not active or end of life has been reached" msgstr "" @@ -28060,7 +28077,7 @@ msgstr "" msgid "Item {0} must be a non-stock item" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1883 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1913 msgid "Item {0} not found in 'Raw Materials Supplied' table in {1} {2}" msgstr "" @@ -28146,7 +28163,7 @@ msgstr "" msgid "Items Filter" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1691 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1690 #: erpnext/selling/doctype/sales_order/sales_order.js:1719 msgid "Items Required" msgstr "" @@ -28170,11 +28187,11 @@ msgstr "" msgid "Items and Pricing" msgstr "" -#: erpnext/controllers/accounts_controller.py:4264 +#: erpnext/controllers/accounts_controller.py:4294 msgid "Items cannot be updated as Subcontracting Inward Order(s) exist against this Subcontracted Sales Order." msgstr "" -#: erpnext/controllers/accounts_controller.py:4257 +#: erpnext/controllers/accounts_controller.py:4287 msgid "Items cannot be updated as Subcontracting Order is created against the Purchase Order {0}." msgstr "" @@ -28186,7 +28203,7 @@ msgstr "" msgid "Items not found." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1516 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1527 msgid "Items rate has been updated to zero as Allow Zero Valuation Rate is checked for the following items: {0}" msgstr "" @@ -28196,7 +28213,7 @@ msgstr "" msgid "Items to Be Repost" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1690 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1689 msgid "Items to Manufacture are required to pull the Raw Materials associated with it." msgstr "" @@ -28216,7 +28233,7 @@ msgstr "" msgid "Items under this warehouse will be suggested" msgstr "" -#: erpnext/controllers/stock_controller.py:207 +#: erpnext/controllers/stock_controller.py:221 msgid "Items {0} do not exist in the Item master." msgstr "" @@ -28699,7 +28716,7 @@ msgstr "" #. Name of a DocType #. Label of a Link in the Stock Workspace #. Label of a Workspace Sidebar Item -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:671 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:669 #: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:88 #: erpnext/stock/workspace/stock/stock.json @@ -29166,7 +29183,7 @@ msgstr "" msgid "License Plate" msgstr "" -#: erpnext/controllers/status_updater.py:512 +#: erpnext/controllers/status_updater.py:513 msgid "Limit Crossed" msgstr "" @@ -29248,7 +29265,7 @@ msgstr "" msgid "Linked Location" msgstr "" -#: erpnext/stock/doctype/item/item.py:1104 +#: erpnext/stock/doctype/item/item.py:1114 msgid "Linked with submitted documents" msgstr "" @@ -29529,7 +29546,7 @@ msgstr "" #: erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.json #: erpnext/accounts/doctype/loyalty_program/loyalty_program.json #: erpnext/accounts/doctype/pos_invoice/pos_invoice.json -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1225 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1234 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/selling/doctype/customer/customer.json #: erpnext/selling/page/point_of_sale/pos_item_cart.js:963 @@ -30007,11 +30024,11 @@ msgstr "" msgid "Mandatory Missing" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:634 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:644 msgid "Mandatory Purchase Order" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:656 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:666 msgid "Mandatory Purchase Receipt" msgstr "" @@ -30086,8 +30103,8 @@ msgstr "" #: erpnext/stock/doctype/material_request_item/material_request_item.json #: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json #: erpnext/stock/doctype/stock_entry/stock_entry.json -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1625 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1641 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1668 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1684 #: erpnext/stock/doctype/stock_entry_type/stock_entry_type.json #: erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json #: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -30237,7 +30254,7 @@ msgstr "" msgid "Manufacturing Manager" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2941 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2973 msgid "Manufacturing Quantity is mandatory" msgstr "" @@ -30314,7 +30331,7 @@ msgstr "" msgid "Mapping Subcontracting Order ..." msgstr "" -#: erpnext/public/js/utils.js:1084 +#: erpnext/public/js/utils.js:1110 msgid "Mapping {0} ..." msgstr "" @@ -30517,7 +30534,7 @@ msgstr "" #. Option for the 'Purpose' (Select) field in DocType 'Stock Entry Type' #: erpnext/setup/setup_wizard/operations/install_fixtures.py:117 #: erpnext/stock/doctype/stock_entry/stock_entry.json -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1626 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1669 #: erpnext/stock/doctype/stock_entry_type/stock_entry_type.json msgid "Material Consumption for Manufacture" msgstr "" @@ -30946,11 +30963,11 @@ msgstr "" msgid "Maximum Producible Items" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:4475 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:4507 msgid "Maximum Samples - {0} can be retained for Batch {1} and Item {2}." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:4466 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:4498 msgid "Maximum Samples - {0} have already been retained for Batch {1} and Item {2} in Batch {3}." msgstr "" @@ -31011,7 +31028,7 @@ msgstr "" msgid "Megawatt" msgstr "" -#: erpnext/stock/stock_ledger.py:2053 +#: erpnext/stock/stock_ledger.py:2096 msgid "Mention Valuation Rate in the Item master." msgstr "" @@ -31046,7 +31063,7 @@ msgstr "" msgid "Merge similar Account Heads" msgstr "" -#: erpnext/public/js/utils.js:1116 +#: erpnext/public/js/utils.js:1142 msgid "Merge taxes from multiple documents" msgstr "" @@ -31403,7 +31420,7 @@ msgstr "" #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:97 #: erpnext/accounts/doctype/pos_profile/pos_profile.py:200 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:593 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:603 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2484 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3100 #: erpnext/assets/doctype/asset_category/asset_category.py:116 @@ -31435,15 +31452,15 @@ msgstr "" msgid "Missing Finance Book" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2080 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2112 msgid "Missing Finished Good" msgstr "" -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:314 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:359 msgid "Missing Formula" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1284 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1293 msgid "Missing Item" msgstr "" @@ -31725,7 +31742,7 @@ msgstr "" msgid "Multiple Accounts (Journal Template)" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:453 +#: erpnext/selling/doctype/customer/customer.py:454 msgid "Multiple Loyalty Programs found for Customer {}. Please select manually." msgstr "" @@ -31751,11 +31768,11 @@ msgstr "" msgid "Multiple company fields available: {0}. Please select manually." msgstr "" -#: erpnext/controllers/accounts_controller.py:1333 +#: erpnext/controllers/accounts_controller.py:1338 msgid "Multiple fiscal years exist for the date {0}. Please set company in Fiscal Year" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2087 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2119 msgid "Multiple items cannot be marked as finished item" msgstr "" @@ -31906,8 +31923,8 @@ msgstr "" msgid "Negative Stock" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1674 -#: erpnext/stock/serial_batch_bundle.py:1548 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1724 +#: erpnext/stock/serial_batch_bundle.py:1634 msgid "Negative Stock Error" msgstr "" @@ -32217,7 +32234,7 @@ msgstr "" msgid "Net Weight UOM" msgstr "" -#: erpnext/controllers/accounts_controller.py:1693 +#: erpnext/controllers/accounts_controller.py:1698 msgid "Net total calculation precision loss" msgstr "" @@ -32396,7 +32413,7 @@ msgstr "" msgid "New Workplace" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:418 +#: erpnext/selling/doctype/customer/customer.py:419 msgid "New credit limit is less than current outstanding amount for the customer. Credit limit has to be atleast {0}" msgstr "" @@ -32406,7 +32423,7 @@ msgstr "" msgid "New invoices will be generated as per schedule even if current invoices are unpaid or past due date" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:261 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:259 msgid "New release date should be in the future" msgstr "" @@ -32524,10 +32541,10 @@ msgstr "" msgid "No POS Profile found. Please create a New POS Profile first" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1583 -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1643 -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1657 -#: erpnext/stock/doctype/item/item.py:1495 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1593 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1653 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1667 +#: erpnext/stock/doctype/item/item.py:1505 msgid "No Permission" msgstr "" @@ -32544,7 +32561,7 @@ msgstr "" msgid "No Selection" msgstr "" -#: erpnext/controllers/sales_and_purchase_return.py:975 +#: erpnext/controllers/sales_and_purchase_return.py:993 msgid "No Serial / Batches are available for return" msgstr "" @@ -32642,7 +32659,7 @@ msgstr "" msgid "No billing email found for customer: {0}" msgstr "" -#: banking/src/components/features/BankReconciliation/CompanySelector.tsx:66 +#: banking/src/components/features/BankReconciliation/CompanySelector.tsx:79 msgid "No company found." msgstr "" @@ -33113,6 +33130,10 @@ msgstr "" msgid "Not permitted to make Purchase Orders" msgstr "" +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:94 +msgid "Not permitted to update Serial No" +msgstr "" + #: erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js:21 msgid "Note: Automatic log deletion only applies to logs of type Update Cost" msgstr "" @@ -33135,7 +33156,7 @@ msgstr "" msgid "Note: Item {0} added multiple times" msgstr "" -#: erpnext/controllers/accounts_controller.py:731 +#: erpnext/controllers/accounts_controller.py:736 msgid "Note: Payment Entry will not be created since 'Cash or Bank Account' was not specified" msgstr "" @@ -33635,7 +33656,7 @@ msgstr "" msgid "Only one operation can have 'Is Final Finished Good' checked when 'Track Semi Finished Goods' is enabled." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1640 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1683 msgid "Only one {0} entry can be created against the Work Order {1}" msgstr "" @@ -33913,7 +33934,7 @@ msgstr "" msgid "Opening Invoice Tool" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1712 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1722 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2044 msgid "Opening Invoice has rounding adjustment of {0}.

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

    Or, '{3}' can be enabled to not post any rounding adjustment." msgstr "" @@ -34565,7 +34586,7 @@ msgstr "" #: erpnext/stock/report/available_serial_no/available_serial_no.py:119 #: erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py:83 #: erpnext/stock/report/stock_balance/stock_balance.py:551 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:325 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:328 msgid "Out Qty" msgstr "" @@ -34622,7 +34643,7 @@ msgstr "" #. Label of the outgoing_rate (Currency) field in DocType 'Stock Ledger Entry' #: erpnext/stock/doctype/serial_and_batch_entry/serial_and_batch_entry.json #: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json -#: erpnext/stock/report/stock_ledger/stock_ledger.py:379 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:382 msgid "Outgoing Rate" msgstr "" @@ -34739,11 +34760,11 @@ msgstr "" msgid "Over Picking Allowance (%)" msgstr "" -#: erpnext/controllers/stock_controller.py:1900 +#: erpnext/controllers/stock_controller.py:1914 msgid "Over Receipt" msgstr "" -#: erpnext/controllers/status_updater.py:517 +#: erpnext/controllers/status_updater.py:518 msgid "Over Receipt/Delivery of {0} {1} ignored for item {2} because you have {3} role." msgstr "" @@ -34760,11 +34781,11 @@ msgstr "" msgid "Over Withheld" msgstr "" -#: erpnext/controllers/status_updater.py:519 +#: erpnext/controllers/status_updater.py:520 msgid "Overbilling of {0} {1} ignored for item {2} because you have {3} role." msgstr "" -#: erpnext/controllers/accounts_controller.py:2211 +#: erpnext/controllers/accounts_controller.py:2216 msgid "Overbilling of {} ignored because you have {} role." msgstr "" @@ -34801,11 +34822,11 @@ msgstr "" msgid "Overdue Limit" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:707 +#: erpnext/selling/doctype/customer/customer.py:708 msgid "Overdue Limit Crossed" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:702 +#: erpnext/selling/doctype/customer/customer.py:703 msgid "Overdue Limit crossed for customer {0}. Overdue amount {1} exceeds the allowed limit {2}." msgstr "" @@ -35277,7 +35298,7 @@ msgstr "" msgid "Packed Items" msgstr "" -#: erpnext/controllers/stock_controller.py:1734 +#: erpnext/controllers/stock_controller.py:1748 msgid "Packed Items cannot be transferred internally" msgstr "" @@ -35424,7 +35445,7 @@ msgstr "" msgid "Paid To Account Type" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:334 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:344 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1204 msgid "Paid amount + Write Off Amount can not be greater than Grand Total" msgstr "" @@ -35604,11 +35625,11 @@ msgstr "" msgid "Parent Task" msgstr "" -#: erpnext/projects/doctype/task/task.py:170 +#: erpnext/projects/doctype/task/task.py:171 msgid "Parent Task {0} is not a Template Task" msgstr "" -#: erpnext/projects/doctype/task/task.py:193 +#: erpnext/projects/doctype/task/task.py:194 msgid "Parent Task {0} must be a Group Task" msgstr "" @@ -35933,7 +35954,7 @@ msgstr "" msgid "Party Account No. (Bank Statement)" msgstr "" -#: erpnext/controllers/accounts_controller.py:2495 +#: erpnext/controllers/accounts_controller.py:2500 msgid "Party Account {0} currency ({1}) and document currency ({2}) should be same" msgstr "" @@ -36437,7 +36458,7 @@ msgstr "" msgid "Payment Entry is already created" msgstr "" -#: erpnext/controllers/accounts_controller.py:1644 +#: erpnext/controllers/accounts_controller.py:1649 msgid "Payment Entry {0} is linked against Order {1}, check if it should be pulled as advance in this invoice." msgstr "" @@ -36721,7 +36742,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:2777 +#: erpnext/controllers/accounts_controller.py:2782 #: erpnext/selling/doctype/quotation/quotation.json #: erpnext/selling/doctype/sales_order/sales_order.json msgid "Payment Schedule" @@ -36731,7 +36752,7 @@ msgstr "" msgid "Payment Schedule based Payment Requests cannot be created because a Payment Entry already exists for this document." msgstr "" -#: erpnext/public/js/controllers/transaction.js:532 +#: erpnext/public/js/controllers/transaction.js:534 msgid "Payment Schedules" msgstr "" @@ -36753,7 +36774,7 @@ msgstr "" #: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1217 #: erpnext/accounts/report/gross_profit/gross_profit.py:451 #: erpnext/accounts/workspace/invoicing/invoicing.json -#: erpnext/public/js/controllers/transaction.js:547 +#: erpnext/public/js/controllers/transaction.js:549 #: erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py:30 #: erpnext/workspace_sidebar/accounts_setup.json msgid "Payment Term" @@ -37206,11 +37227,11 @@ msgstr "" msgid "Period Closing Voucher" msgstr "" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:509 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:627 msgid "Period Closing Voucher {0} GL Entry Cancellation Failed" msgstr "" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:488 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:606 msgid "Period Closing Voucher {0} GL Entry Processing Failed" msgstr "" @@ -37230,7 +37251,7 @@ msgstr "" msgid "Period End Date" msgstr "" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:78 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:81 msgid "Period End Date cannot be greater than Fiscal Year End Date" msgstr "" @@ -37272,11 +37293,11 @@ msgstr "" msgid "Period Start Date" msgstr "" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:75 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:78 msgid "Period Start Date cannot be greater than Period End Date" msgstr "" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:72 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:75 msgid "Period Start Date must be {0}" msgstr "" @@ -37378,11 +37399,11 @@ msgid "Phantom BOM cannot be created for stock item {0}." msgstr "" #: erpnext/manufacturing/doctype/bom/bom_item_preview.html:16 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:340 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:329 msgid "Phantom Item" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:449 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:438 msgid "Phantom Item is mandatory" msgstr "" @@ -37854,7 +37875,7 @@ msgstr "" msgid "Please add {1} role to user {0}." msgstr "" -#: erpnext/controllers/stock_controller.py:1911 +#: erpnext/controllers/stock_controller.py:1925 msgid "Please adjust the qty or edit {0} to proceed." msgstr "" @@ -37880,7 +37901,7 @@ msgstr "" msgid "Please capitalize this asset before submitting." msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:978 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:988 msgid "Please check Multi Currency option to allow accounts with other currency" msgstr "" @@ -37932,7 +37953,7 @@ msgstr "" msgid "Please configure accounts for the Bank Entry rule." msgstr "" -#: erpnext/selling/doctype/customer/customer.py:649 +#: erpnext/selling/doctype/customer/customer.py:650 msgid "Please contact any of the following users to extend the credit limits for {0}: {1}" msgstr "" @@ -37940,7 +37961,7 @@ msgstr "" msgid "Please contact any of the following users to {} this transaction." msgstr "" -#: erpnext/selling/doctype/customer/customer.py:642 +#: erpnext/selling/doctype/customer/customer.py:643 msgid "Please contact your administrator to extend the credit limits for {0}." msgstr "" @@ -37960,7 +37981,7 @@ msgstr "" msgid "Please create a new Accounting Dimension if required." msgstr "" -#: erpnext/controllers/accounts_controller.py:832 +#: erpnext/controllers/accounts_controller.py:837 msgid "Please create purchase from internal sale or delivery document itself" msgstr "" @@ -38008,11 +38029,11 @@ msgstr "" msgid "Please enable {} in {} to allow same item in multiple rows" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:381 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:391 msgid "Please ensure that the {0} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:389 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:399 msgid "Please ensure that the {0} account {1} is a Payable account. You can change the account type to Payable or select a different account." msgstr "" @@ -38024,7 +38045,7 @@ msgstr "" msgid "Please ensure {} account {} is a Receivable account." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:874 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:883 msgid "Please enter Difference Account or set default Stock Adjustment Account for company {0}" msgstr "" @@ -38062,7 +38083,7 @@ msgstr "" msgid "Please enter Item Code to get Batch Number" msgstr "" -#: erpnext/public/js/controllers/transaction.js:3043 +#: erpnext/public/js/controllers/transaction.js:3048 msgid "Please enter Item Code to get batch no" msgstr "" @@ -38090,7 +38111,7 @@ msgstr "" msgid "Please enter Receipt Document" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1042 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1052 msgid "Please enter Reference date" msgstr "" @@ -38114,16 +38135,16 @@ msgstr "" msgid "Please enter Warehouse and Date" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:660 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:670 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1335 msgid "Please enter Write Off Account" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:670 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:680 msgid "Please enter a valid Write Off Account" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:681 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:691 msgid "Please enter a valid Write Off Cost Center" msgstr "" @@ -38143,7 +38164,7 @@ msgstr "" msgid "Please enter company name first" msgstr "" -#: erpnext/controllers/accounts_controller.py:2996 +#: erpnext/controllers/accounts_controller.py:3001 msgid "Please enter default currency in Company Master" msgstr "" @@ -38418,11 +38439,11 @@ msgstr "" msgid "Please select Stock Asset Account" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2006 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2036 msgid "Please select Subcontracting Order instead of Purchase Order {0}" msgstr "" -#: erpnext/controllers/accounts_controller.py:2852 +#: erpnext/controllers/accounts_controller.py:2857 msgid "Please select Unrealized Profit / Loss account or add default Unrealized Profit / Loss account account for company {0}" msgstr "" @@ -38439,7 +38460,7 @@ msgstr "" #: erpnext/manufacturing/doctype/bom/bom.js:734 #: erpnext/manufacturing/doctype/bom/bom.py:279 #: erpnext/public/js/controllers/accounts.js:274 -#: erpnext/public/js/controllers/transaction.js:3342 +#: erpnext/public/js/controllers/transaction.js:3347 msgid "Please select a Company first." msgstr "" @@ -38540,6 +38561,10 @@ msgstr "" msgid "Please select a value for {0} quotation_to {1}" msgstr "" +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js:9 +msgid "Please select a warehouse first." +msgstr "" + #: erpnext/assets/doctype/asset_repair/asset_repair.js:203 msgid "Please select an item code before setting the warehouse." msgstr "" @@ -38564,7 +38589,7 @@ msgstr "" msgid "Please select at least one row with difference value" msgstr "" -#: erpnext/public/js/controllers/transaction.js:584 +#: erpnext/public/js/controllers/transaction.js:586 msgid "Please select at least one schedule." msgstr "" @@ -38576,7 +38601,7 @@ msgstr "" msgid "Please select atleast one operation to create Job Card" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1722 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1732 msgid "Please select correct account" msgstr "" @@ -38664,7 +38689,7 @@ msgstr "" msgid "Please select {0} first" msgstr "" -#: erpnext/public/js/controllers/transaction.js:150 +#: erpnext/public/js/controllers/transaction.js:152 msgid "Please set 'Apply Additional Discount On'" msgstr "" @@ -38736,7 +38761,7 @@ msgstr "" msgid "Please set Fixed Asset Account in Asset Category {0}" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:590 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:600 msgid "Please set Fixed Asset Account in {} against {}." msgstr "" @@ -38782,7 +38807,7 @@ msgstr "" msgid "Please set a default Holiday List for Employee {0} or Company {1}" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1146 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1156 msgid "Please set account in Warehouse {0}" msgstr "" @@ -38795,7 +38820,7 @@ msgstr "" msgid "Please set an Address on the Company '%s'" msgstr "" -#: erpnext/controllers/stock_controller.py:1042 +#: erpnext/controllers/stock_controller.py:1056 msgid "Please set an Expense Account in the Items table" msgstr "" @@ -38839,11 +38864,11 @@ msgstr "" msgid "Please set default UOM in Stock Settings" msgstr "" -#: erpnext/controllers/stock_controller.py:821 +#: erpnext/controllers/stock_controller.py:835 msgid "Please set default cost of goods sold account in company {0} for booking rounding gain and loss during stock transfer" msgstr "" -#: erpnext/controllers/stock_controller.py:272 +#: erpnext/controllers/stock_controller.py:286 msgid "Please set default inventory account for item {0}, or their item group or brand." msgstr "" @@ -38856,7 +38881,7 @@ msgstr "" msgid "Please set filter based on Item or Warehouse" msgstr "" -#: erpnext/controllers/accounts_controller.py:2411 +#: erpnext/controllers/accounts_controller.py:2416 msgid "Please set one of the following:" msgstr "" @@ -38864,7 +38889,7 @@ msgstr "" msgid "Please set opening number of booked depreciations" msgstr "" -#: erpnext/public/js/controllers/transaction.js:2707 +#: erpnext/public/js/controllers/transaction.js:2712 msgid "Please set recurring after saving" msgstr "" @@ -38920,7 +38945,7 @@ msgid "Please set {0} in BOM Creator {1}" msgstr "" #: erpnext/controllers/buying_controller.py:345 -#: erpnext/controllers/stock_controller.py:912 +#: erpnext/controllers/stock_controller.py:926 msgid "Please set {0} in Company {1} or in the Item Defaults of Item {2}" msgstr "" @@ -38928,7 +38953,7 @@ msgstr "" msgid "Please set {0} in Company {1} to account for Exchange Gain / Loss" msgstr "" -#: erpnext/controllers/accounts_controller.py:613 +#: erpnext/controllers/accounts_controller.py:618 msgid "Please set {0} to {1}, the same account that was used in the original invoice {2}." msgstr "" @@ -38945,12 +38970,12 @@ msgid "Please specify Company" msgstr "" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.js:120 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:430 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:428 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:636 msgid "Please specify Company to proceed" msgstr "" -#: erpnext/controllers/accounts_controller.py:3227 +#: erpnext/controllers/accounts_controller.py:3232 #: erpnext/public/js/controllers/accounts.js:114 msgid "Please specify a valid Row ID for row {0} in table {1}" msgstr "" @@ -39190,7 +39215,7 @@ msgstr "" #: erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py:86 #: erpnext/stock/report/serial_no_ledger/serial_no_ledger.py:25 #: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:155 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:155 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:164 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:36 #: erpnext/templates/form_grid/bank_reconciliation_grid.html:6 msgid "Posting Date" @@ -39207,7 +39232,7 @@ msgstr "" msgid "Posting Date inheritance for exchange gain / loss" msgstr "" -#: erpnext/public/js/controllers/transaction.js:1137 +#: erpnext/public/js/controllers/transaction.js:1139 msgid "Posting Date will change to today's date as Edit Posting Date and Time is unchecked. Are you sure want to proceed?" msgstr "" @@ -39264,13 +39289,13 @@ msgstr "" #: erpnext/stock/report/serial_no_ledger/serial_no_ledger.js:63 #: erpnext/stock/report/serial_no_ledger/serial_no_ledger.py:26 #: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:156 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:160 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:169 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:41 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json msgid "Posting Time" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2891 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2923 msgid "Posting date and posting time is mandatory" msgstr "" @@ -39478,7 +39503,7 @@ msgstr "" msgid "Previous Work Experience" msgstr "" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:109 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:112 msgid "Previous Year is not closed, please close it first" msgstr "" @@ -40601,7 +40626,7 @@ msgstr "" msgid "Profitability Analysis" msgstr "" -#: erpnext/projects/doctype/task/task.py:156 +#: erpnext/projects/doctype/task/task.py:157 #, python-format msgid "Progress % for a task cannot be more than 100." msgstr "" @@ -41179,11 +41204,19 @@ msgstr "" msgid "Purchase Invoice Trends" msgstr "" +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:331 +msgid "Purchase Invoice can be held after submitting." +msgstr "" + #: erpnext/assets/doctype/asset/asset.py:340 msgid "Purchase Invoice cannot be made against an existing asset {0}" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1999 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1943 +msgid "Purchase Invoice without any outstanding amount cannot be held." +msgstr "" + +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:2033 msgid "Purchase Invoices" msgstr "" @@ -41312,11 +41345,11 @@ msgstr "" msgid "Purchase Order Pricing Rule" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:630 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:640 msgid "Purchase Order Required" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:625 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:635 msgid "Purchase Order Required for item {}" msgstr "" @@ -41342,7 +41375,7 @@ msgstr "" msgid "Purchase Order {0} created" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:690 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:700 msgid "Purchase Order {0} is not submitted" msgstr "" @@ -41376,7 +41409,7 @@ msgstr "" msgid "Purchase Orders to Receive" msgstr "" -#: erpnext/controllers/accounts_controller.py:2043 +#: erpnext/controllers/accounts_controller.py:2048 msgid "Purchase Orders {0} are un-linked" msgstr "" @@ -41401,8 +41434,8 @@ msgstr "" #. Label of a Workspace Sidebar Item #: erpnext/accounts/doctype/accounts_settings/accounts_settings.js:62 #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:181 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:647 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:657 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:645 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:655 #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js:49 #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json #: erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py:244 @@ -41462,11 +41495,11 @@ msgstr "" msgid "Purchase Receipt No" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:652 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:662 msgid "Purchase Receipt Required" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:647 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:657 msgid "Purchase Receipt Required for item {}" msgstr "" @@ -41494,7 +41527,7 @@ msgstr "" msgid "Purchase Receipt {0} created." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:697 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:707 msgid "Purchase Receipt {0} is not submitted" msgstr "" @@ -41620,7 +41653,7 @@ msgstr "" msgid "Purpose" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:691 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:700 msgid "Purpose must be one of {0}" msgstr "" @@ -41720,12 +41753,12 @@ msgstr "" #: erpnext/manufacturing/doctype/work_order_item/work_order_item.json #: erpnext/manufacturing/doctype/workstation/workstation_job_card.html:28 #: erpnext/manufacturing/report/bom_explorer/bom_explorer.py:69 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:254 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:352 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:417 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:517 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:243 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:341 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:406 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:506 #: erpnext/public/js/stock_reservation.js:134 -#: erpnext/public/js/stock_reservation.js:336 erpnext/public/js/utils.js:887 +#: erpnext/public/js/stock_reservation.js:336 erpnext/public/js/utils.js:890 #: erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json #: erpnext/selling/doctype/product_bundle_item/product_bundle_item.json #: erpnext/selling/doctype/sales_order/sales_order.js:398 @@ -41813,7 +41846,7 @@ msgstr "" #: erpnext/stock/doctype/stock_closing_balance/stock_closing_balance.json #: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:169 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:199 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:208 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:91 msgid "Qty Change" msgstr "" @@ -42127,7 +42160,7 @@ msgstr "" msgid "Quality Inspection Analysis" msgstr "" -#: erpnext/public/js/controllers/transaction.js:2964 +#: erpnext/public/js/controllers/transaction.js:2969 msgid "Quality Inspection Not Configured" msgstr "" @@ -42206,7 +42239,7 @@ msgstr "" msgid "Quality Inspection {0} is rejected for the item: {1}" msgstr "" -#: erpnext/public/js/controllers/transaction.js:431 +#: erpnext/public/js/controllers/transaction.js:433 #: erpnext/stock/doctype/stock_entry/stock_entry.js:212 msgid "Quality Inspection(s)" msgstr "" @@ -42802,7 +42835,7 @@ msgstr "" #: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json #: erpnext/manufacturing/doctype/work_order_additional_item/work_order_additional_item.json #: erpnext/manufacturing/doctype/work_order_item/work_order_item.json -#: erpnext/public/js/utils.js:897 +#: erpnext/public/js/utils.js:900 #: erpnext/selling/doctype/product_bundle_item/product_bundle_item.json #: erpnext/selling/doctype/quotation_item/quotation_item.json #: erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -42985,7 +43018,7 @@ msgstr "" msgid "Rate at which this tax is applied" msgstr "" -#: erpnext/controllers/accounts_controller.py:4132 +#: erpnext/controllers/accounts_controller.py:4162 msgid "Rate of '{}' items cannot be changed" msgstr "" @@ -43129,7 +43162,7 @@ msgstr "" #: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/manufacturing/doctype/production_plan/production_plan.json #: erpnext/manufacturing/doctype/workstation/workstation.js:462 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:398 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:387 msgid "Raw Materials" msgstr "" @@ -43154,7 +43187,7 @@ msgstr "" msgid "Raw Materials Consumption" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:431 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:440 msgid "Raw Materials Missing" msgstr "" @@ -43301,7 +43334,7 @@ msgid "Real Estate" msgstr "" #. Label of the hold_comment (Small Text) field in DocType 'Purchase Invoice' -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:285 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:283 #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json msgid "Reason For Putting On Hold" msgstr "" @@ -43856,11 +43889,11 @@ msgstr "" msgid "Reference #" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1040 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1050 msgid "Reference #{0} dated {1}" msgstr "" -#: erpnext/public/js/controllers/transaction.js:2820 +#: erpnext/public/js/controllers/transaction.js:2825 msgid "Reference Date for Early Payment Discount" msgstr "" @@ -44140,15 +44173,15 @@ msgstr "" #. Label of the release_date (Date) field in DocType 'Purchase Invoice' #. Label of the release_date (Date) field in DocType 'Supplier' -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:277 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:321 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:275 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:320 #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/buying/doctype/supplier/supplier.json #: erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.py:1079 msgid "Release Date" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:325 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:335 msgid "Release date must be in the future" msgstr "" @@ -44597,7 +44630,7 @@ msgid "Reposting cannot be started when status is {0}." msgstr "" #: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:227 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:338 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:347 msgid "Reposting entries created: {0}" msgstr "" @@ -44662,7 +44695,7 @@ msgstr "" msgid "Reqd Qty (BOM)" msgstr "" -#: erpnext/public/js/utils.js:913 +#: erpnext/public/js/utils.js:916 msgid "Reqd by date" msgstr "" @@ -44979,7 +45012,7 @@ msgstr "" msgid "Reserved" msgstr "" -#: erpnext/controllers/stock_controller.py:1491 +#: erpnext/controllers/stock_controller.py:1505 msgid "Reserved Batch Conflict" msgstr "" @@ -45053,7 +45086,7 @@ msgstr "" msgid "Reserved Quantity for Production" msgstr "" -#: erpnext/stock/stock_ledger.py:2340 +#: erpnext/stock/stock_ledger.py:2383 msgid "Reserved Serial No." msgstr "" @@ -45071,13 +45104,13 @@ msgstr "" #: erpnext/stock/report/reserved_stock/reserved_stock.json #: erpnext/stock/report/stock_balance/stock_balance.py:569 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:205 -#: erpnext/stock/stock_ledger.py:2324 +#: erpnext/stock/stock_ledger.py:2367 #: 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:2412 msgid "Reserved Stock for Batch" msgstr "" @@ -45457,6 +45490,10 @@ msgstr "" msgid "Return Issued" msgstr "" +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:328 +msgid "Return Purchase Invoice cannot be held." +msgstr "" + #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:329 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js:127 msgid "Return Qty" @@ -45993,8 +46030,8 @@ msgstr "" msgid "Rounding Loss Allowance should be between 0 and 1" msgstr "" -#: erpnext/controllers/stock_controller.py:833 -#: erpnext/controllers/stock_controller.py:848 +#: erpnext/controllers/stock_controller.py:847 +#: erpnext/controllers/stock_controller.py:862 msgid "Rounding gain/loss Entry for Stock Transfer" msgstr "" @@ -46017,7 +46054,7 @@ msgstr "" msgid "Routing Name" msgstr "" -#: erpnext/controllers/sales_and_purchase_return.py:225 +#: erpnext/controllers/sales_and_purchase_return.py:243 msgid "Row # {0}: Cannot return more than {1} for Item {2}" msgstr "" @@ -46055,11 +46092,11 @@ msgstr "" msgid "Row #{0}: A reorder entry already exists for warehouse {1} with reorder type {2}." msgstr "" -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:333 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:378 msgid "Row #{0}: Acceptance Criteria Formula is incorrect." msgstr "" -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:313 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:358 msgid "Row #{0}: Acceptance Criteria Formula is required." msgstr "" @@ -46072,7 +46109,7 @@ msgstr "" msgid "Row #{0}: Accepted Warehouse is mandatory for the accepted Item {1}" msgstr "" -#: erpnext/controllers/accounts_controller.py:1321 +#: erpnext/controllers/accounts_controller.py:1326 msgid "Row #{0}: Account {1} does not belong to company {2}" msgstr "" @@ -46137,27 +46174,27 @@ msgstr "" msgid "Row #{0}: Cannot create entry with different taxable AND withholding document links." msgstr "" -#: erpnext/controllers/accounts_controller.py:3834 +#: erpnext/controllers/accounts_controller.py:3864 msgid "Row #{0}: Cannot delete item {1} which has already been billed." msgstr "" -#: erpnext/controllers/accounts_controller.py:3808 +#: erpnext/controllers/accounts_controller.py:3838 msgid "Row #{0}: Cannot delete item {1} which has already been delivered" msgstr "" -#: erpnext/controllers/accounts_controller.py:3827 +#: erpnext/controllers/accounts_controller.py:3857 msgid "Row #{0}: Cannot delete item {1} which has already been received" msgstr "" -#: erpnext/controllers/accounts_controller.py:3814 +#: erpnext/controllers/accounts_controller.py:3844 msgid "Row #{0}: Cannot delete item {1} which has work order assigned to it." msgstr "" -#: erpnext/controllers/accounts_controller.py:3820 +#: erpnext/controllers/accounts_controller.py:3850 msgid "Row #{0}: Cannot delete item {1} which is already ordered against this Sales Order." msgstr "" -#: erpnext/controllers/accounts_controller.py:4142 +#: erpnext/controllers/accounts_controller.py:4172 msgid "Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}." msgstr "" @@ -46165,7 +46202,7 @@ msgstr "" msgid "Row #{0}: Cannot transfer more than Required Qty {1} for Item {2} against Job Card {3}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1329 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1338 msgid "Row #{0}: Cannot transfer {1} {2} of Item {3}. Maximum transferable quantity is {4} {2}." msgstr "" @@ -46260,7 +46297,7 @@ msgstr "" msgid "Row #{0}: Expected Delivery Date cannot be before Purchase Order Date" msgstr "" -#: erpnext/controllers/stock_controller.py:1044 +#: erpnext/controllers/stock_controller.py:1058 msgid "Row #{0}: Expense Account not set for the Item {1}. {2}" msgstr "" @@ -46287,7 +46324,7 @@ msgstr "" msgid "Row #{0}: Finished Good Item {1} must be a sub-contracted item" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:647 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:656 msgid "Row #{0}: Finished Good must be {1}" msgstr "" @@ -46324,7 +46361,7 @@ msgstr "" msgid "Row #{0}: Item added" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1937 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1967 msgid "Row #{0}: Item {1} cannot be transferred more than {2} against {3} {4}" msgstr "" @@ -46340,7 +46377,7 @@ msgstr "" msgid "Row #{0}: Item {1} has no stock in warehouse {2}." msgstr "" -#: erpnext/controllers/stock_controller.py:189 +#: erpnext/controllers/stock_controller.py:203 msgid "Row #{0}: Item {1} has zero rate but '{2}' is not enabled." msgstr "" @@ -46369,7 +46406,7 @@ msgstr "" msgid "Row #{0}: Item {1} is not a stock item" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1095 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1104 msgid "Row #{0}: Item {1} is not part of the source manufacture entry and cannot be added to this disassembly." msgstr "" @@ -46381,7 +46418,7 @@ msgstr "" msgid "Row #{0}: Item {1} mismatch. Changing of item code is not permitted." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1104 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1113 msgid "Row #{0}: Item {1} quantity ({2} in stock UOM) does not match the quantity derived from the source ({3}). Do not change the UOM, conversion factor or quantity of disassembly rows." msgstr "" @@ -46409,7 +46446,7 @@ msgstr "" msgid "Row #{0}: Opening Accumulated Depreciation must be less than or equal to {1}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1159 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1168 msgid "Row #{0}: Operation {1} is not completed for {2} qty of finished goods in Work Order {3}. Please update operation status via Job Card {4}." msgstr "" @@ -46438,7 +46475,7 @@ msgstr "" msgid "Row #{0}: Please set reorder quantity" msgstr "" -#: erpnext/controllers/accounts_controller.py:636 +#: erpnext/controllers/accounts_controller.py:641 msgid "Row #{0}: Please update deferred revenue/expense account in item row or default account in company master" msgstr "" @@ -46460,15 +46497,15 @@ msgstr "" msgid "Row #{0}: Qty should be less than or equal to Available Qty to Reserve (Actual Qty - Reserved Qty) {1} for Iem {2} against Batch {3} in Warehouse {4}." msgstr "" -#: erpnext/controllers/stock_controller.py:1629 +#: erpnext/controllers/stock_controller.py:1643 msgid "Row #{0}: Quality Inspection is required for Item {1}" msgstr "" -#: erpnext/controllers/stock_controller.py:1644 +#: erpnext/controllers/stock_controller.py:1658 msgid "Row #{0}: Quality Inspection {1} is not submitted for the item: {2}" msgstr "" -#: erpnext/controllers/stock_controller.py:1659 +#: erpnext/controllers/stock_controller.py:1673 msgid "Row #{0}: Quality Inspection {1} was rejected for item {2}" msgstr "" @@ -46476,10 +46513,14 @@ 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:1484 +#: erpnext/controllers/accounts_controller.py:1489 msgid "Row #{0}: Quantity for Item {1} cannot be zero." msgstr "" +#: erpnext/crm/doctype/opportunity/opportunity.py:152 +msgid "Row #{0}: Quantity must be greater than 0 for Item {1}" +msgstr "" + #: erpnext/controllers/subcontracting_inward_controller.py:537 msgid "Row #{0}: Quantity of Item {1} cannot be more than {2} {3} against Subcontracting Inward Order {4}" msgstr "" @@ -46488,13 +46529,17 @@ msgstr "" msgid "Row #{0}: Quantity to reserve for the Item {1} should be greater than 0." msgstr "" -#: erpnext/controllers/accounts_controller.py:899 -#: erpnext/controllers/accounts_controller.py:911 +#: erpnext/controllers/accounts_controller.py:904 +#: erpnext/controllers/accounts_controller.py:916 #: erpnext/utilities/transaction_base.py:172 #: erpnext/utilities/transaction_base.py:178 msgid "Row #{0}: Rate must be same as {1}: {2} ({3} / {4})" msgstr "" +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:316 +msgid "Row #{0}: Reading {1} {2} is not a valid number in the {3} number format. Use {4} as the decimal separator." +msgstr "" + #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1251 msgid "Row #{0}: Reference Document Type must be one of Purchase Order, Purchase Invoice or Journal Entry" msgstr "" @@ -46543,7 +46588,7 @@ msgstr "" msgid "Row #{0}: Sequence ID must be {1} or {2} for Operation {3}." msgstr "" -#: erpnext/controllers/stock_controller.py:344 +#: erpnext/controllers/stock_controller.py:358 msgid "Row #{0}: Serial No {1} does not belong to Batch {2}" msgstr "" @@ -46559,15 +46604,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:664 +#: erpnext/controllers/accounts_controller.py:669 msgid "Row #{0}: Service End Date cannot be before Invoice Posting Date" msgstr "" -#: erpnext/controllers/accounts_controller.py:658 +#: erpnext/controllers/accounts_controller.py:663 msgid "Row #{0}: Service Start Date cannot be greater than Service End Date" msgstr "" -#: erpnext/controllers/accounts_controller.py:652 +#: erpnext/controllers/accounts_controller.py:657 msgid "Row #{0}: Service Start and End Date is required for deferred accounting" msgstr "" @@ -46591,11 +46636,11 @@ msgstr "" msgid "Row #{0}: Source Warehouse {1} for item {2} must be same as Source Warehouse {3} in the Work Order." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1363 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1372 msgid "Row #{0}: Source and Target Warehouse cannot be the same for Material Transfer" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1385 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1394 msgid "Row #{0}: Source, Target Warehouse and Inventory Dimensions cannot be the exact same for Material Transfer" msgstr "" @@ -46603,7 +46648,7 @@ msgstr "" msgid "Row #{0}: Start Time must be before End Time" msgstr "" -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:213 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:217 msgid "Row #{0}: Status is mandatory" msgstr "" @@ -46648,7 +46693,7 @@ msgstr "" msgid "Row #{0}: Target Warehouse must be same as Customer Warehouse {1} from the linked Subcontracting Inward Order" msgstr "" -#: erpnext/controllers/stock_controller.py:357 +#: erpnext/controllers/stock_controller.py:371 msgid "Row #{0}: The batch {1} has already expired." msgstr "" @@ -46668,7 +46713,7 @@ msgstr "" msgid "Row #{0}: Total Number of Depreciations must be greater than zero" msgstr "" -#: erpnext/controllers/stock_controller.py:141 +#: erpnext/controllers/stock_controller.py:155 msgid "Row #{0}: Warehouse {1} does not match with the warehouse {2} in Serial and Batch Bundle {3}." msgstr "" @@ -46696,11 +46741,11 @@ msgstr "" msgid "Row #{0}: {1} can not be negative for item {2}" msgstr "" -#: erpnext/controllers/stock_controller.py:1308 +#: erpnext/controllers/stock_controller.py:1322 msgid "Row #{0}: {1} is mandatory for the Inventory Dimension {2}." msgstr "" -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:326 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:371 msgid "Row #{0}: {1} is not a valid reading field. Please refer to the field description." msgstr "" @@ -46712,7 +46757,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:3949 +#: erpnext/controllers/accounts_controller.py:3979 msgid "Row #{0}:Quantity for Item {1} cannot be zero." msgstr "" @@ -46813,11 +46858,11 @@ msgstr "" msgid "Row #{}: {} {} does not exist." msgstr "" -#: erpnext/stock/doctype/item/item.py:1527 +#: erpnext/stock/doctype/item/item.py:1537 msgid "Row #{}: {} {} doesn't belong to Company {}. Please select valid {}." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:440 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:450 msgid "Row No {0}: Warehouse is required. Please set a Default Warehouse for Item {1} and Company {2}" msgstr "" @@ -46829,7 +46874,7 @@ msgstr "" msgid "Row {0} picked quantity is less than the required quantity, additional {1} {2} required." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1961 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1991 msgid "Row {0}# Item {1} not found in 'Raw Materials Supplied' table in {2} {3}" msgstr "" @@ -46861,7 +46906,7 @@ msgstr "" msgid "Row {0}: Allocated amount {1} must be less than or equal to remaining payment amount {2}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1620 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1663 msgid "Row {0}: As {1} is enabled, raw materials cannot be added to {2} entry. Use {3} entry to consume raw materials." msgstr "" @@ -46869,7 +46914,7 @@ msgstr "" msgid "Row {0}: Bill of Materials not found for the Item {1}" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:936 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:946 msgid "Row {0}: Both Debit and Credit values cannot be zero" msgstr "" @@ -46881,7 +46926,7 @@ msgstr "" msgid "Row {0}: Conversion Factor is mandatory" msgstr "" -#: erpnext/controllers/accounts_controller.py:3265 +#: erpnext/controllers/accounts_controller.py:3270 msgid "Row {0}: Cost Center {1} does not belong to Company {2}" msgstr "" @@ -46909,7 +46954,7 @@ msgstr "" msgid "Row {0}: Delivery Warehouse cannot be same as Customer Warehouse for Item {1}." msgstr "" -#: erpnext/controllers/accounts_controller.py:2765 +#: erpnext/controllers/accounts_controller.py:2770 msgid "Row {0}: Due Date in the Payment Terms table cannot be before Posting Date" msgstr "" @@ -46917,7 +46962,7 @@ msgstr "" msgid "Row {0}: Either Delivery Note Item or Packed Item reference is mandatory." msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1027 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1037 #: erpnext/controllers/taxes_and_totals.py:1382 msgid "Row {0}: Exchange Rate is mandatory" msgstr "" @@ -46934,15 +46979,15 @@ msgstr "" msgid "Row {0}: Expense Account {1} is linked to company {2}. Please select an account belonging to company {3}." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:530 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:540 msgid "Row {0}: Expense Head changed to {1} as no Purchase Receipt is created against Item {2}." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:487 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:497 msgid "Row {0}: Expense Head changed to {1} because account {2} is not linked to warehouse {3} or it is not the default inventory account" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:512 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:522 msgid "Row {0}: Expense Head changed to {1} because expense is booked against this account in Purchase Receipt {2}" msgstr "" @@ -46959,7 +47004,7 @@ msgstr "" msgid "Row {0}: From Time and To Time of {1} is overlapping with {2}" msgstr "" -#: erpnext/controllers/stock_controller.py:1725 +#: erpnext/controllers/stock_controller.py:1739 msgid "Row {0}: From Warehouse is mandatory for internal transfers" msgstr "" @@ -47071,7 +47116,7 @@ msgstr "" msgid "Row {0}: Qty cannot be greater than {1} for the Item {2}." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:738 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:747 msgid "Row {0}: Qty in Stock UOM can not be zero." msgstr "" @@ -47083,7 +47128,7 @@ msgstr "" msgid "Row {0}: Quantity cannot be negative." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1233 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1242 msgid "Row {0}: Quantity not available for {4} in warehouse {1} at posting time of the entry ({2} {3})" msgstr "" @@ -47091,7 +47136,7 @@ msgstr "" msgid "Row {0}: Sales Invoice {1} is already created for {2}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:344 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:353 msgid "Row {0}: Serial/Batch has been reset to values linked with Work Order {1} because the previously selected serial/batch does not belong to this Work Order." msgstr "" @@ -47099,11 +47144,11 @@ msgstr "" msgid "Row {0}: Shift cannot be changed since the depreciation has already been processed" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1974 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2004 msgid "Row {0}: Subcontracted Item is mandatory for the raw material {1}" msgstr "" -#: erpnext/controllers/stock_controller.py:1716 +#: erpnext/controllers/stock_controller.py:1730 msgid "Row {0}: Target Warehouse is mandatory for internal transfers" msgstr "" @@ -47115,11 +47160,11 @@ msgstr "" msgid "Row {0}: The entire expense amount for account {1} in {2} has already been allocated." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:784 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:793 msgid "Row {0}: The item {1}, quantity must be positive number" msgstr "" -#: erpnext/controllers/accounts_controller.py:3242 +#: erpnext/controllers/accounts_controller.py:3247 msgid "Row {0}: The {3} Account {1} does not belong to the company {2}" msgstr "" @@ -47127,11 +47172,11 @@ msgstr "" msgid "Row {0}: To set {1} periodicity, difference between from and to date must be greater than or equal to {2}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3954 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3986 msgid "Row {0}: Transferred quantity cannot be greater than the requested quantity." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:732 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:741 msgid "Row {0}: UOM Conversion Factor is mandatory" msgstr "" @@ -47152,7 +47197,7 @@ msgstr "" msgid "Row {0}: Workstation or Workstation Type is mandatory for an operation {1}" msgstr "" -#: erpnext/controllers/accounts_controller.py:1203 +#: erpnext/controllers/accounts_controller.py:1208 msgid "Row {0}: user has not applied the rule {1} on the item {2}" msgstr "" @@ -47164,7 +47209,7 @@ msgstr "" msgid "Row {0}: {1} must be greater than 0" msgstr "" -#: erpnext/controllers/accounts_controller.py:809 +#: erpnext/controllers/accounts_controller.py:814 msgid "Row {0}: {1} {2} cannot be same as {3} (Party Account) {4}" msgstr "" @@ -47210,7 +47255,7 @@ msgstr "" msgid "Rows with Same Account heads will be merged on Ledger" msgstr "" -#: erpnext/controllers/accounts_controller.py:2776 +#: erpnext/controllers/accounts_controller.py:2781 msgid "Rows with duplicate due dates in other rows were found: {0}" msgstr "" @@ -47218,7 +47263,7 @@ msgstr "" msgid "Rows: {0} have 'Payment Entry' as reference_type. This should not be set manually." msgstr "" -#: erpnext/controllers/accounts_controller.py:302 +#: erpnext/controllers/accounts_controller.py:307 msgid "Rows: {0} in {1} section are Invalid. Reference Name should point to a valid Payment Entry or Journal Entry." msgstr "" @@ -47341,7 +47386,7 @@ msgstr "" msgid "SLA Paused On" msgstr "" -#: erpnext/public/js/utils.js:1277 +#: erpnext/public/js/utils.js:1303 msgid "SLA is on hold since {0}" msgstr "" @@ -47431,7 +47476,7 @@ msgstr "" #: erpnext/accounts/doctype/tax_category/tax_category_dashboard.py:9 #: erpnext/accounts/doctype/tax_rule/tax_rule.json #: erpnext/crm/doctype/opportunity/opportunity.js:288 -#: erpnext/crm/doctype/opportunity/opportunity.py:158 +#: erpnext/crm/doctype/opportunity/opportunity.py:168 #: erpnext/projects/doctype/project/project_dashboard.py:15 #: erpnext/regional/report/vat_audit_report/vat_audit_report.py:145 #: erpnext/selling/doctype/quotation/quotation.json @@ -48295,12 +48340,12 @@ msgstr "" #. Label of the sample_size (Float) field in DocType 'Quality Inspection' #: erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py:93 -#: erpnext/public/js/controllers/transaction.js:2877 +#: erpnext/public/js/controllers/transaction.js:2882 #: erpnext/stock/doctype/quality_inspection/quality_inspection.json msgid "Sample Size" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:4457 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:4489 msgid "Sample quantity {0} cannot be more than received quantity {1}" msgstr "" @@ -48405,7 +48450,7 @@ msgstr "" msgid "Schedule Date" msgstr "" -#: erpnext/public/js/controllers/transaction.js:541 +#: erpnext/public/js/controllers/transaction.js:543 msgid "Schedule Name" msgstr "" @@ -48586,7 +48631,7 @@ msgstr "" msgid "Search by item code, serial number or barcode" msgstr "" -#: banking/src/components/features/BankReconciliation/CompanySelector.tsx:64 +#: banking/src/components/features/BankReconciliation/CompanySelector.tsx:77 msgid "Search company..." msgstr "" @@ -48818,7 +48863,7 @@ msgstr "" msgid "Select Items based on Delivery Date" msgstr "" -#: erpnext/public/js/controllers/transaction.js:2912 +#: erpnext/public/js/controllers/transaction.js:2917 msgid "Select Items for Quality Inspection" msgstr "" @@ -48843,12 +48888,12 @@ msgstr "" msgid "Select Job Worker Address" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1222 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1231 #: erpnext/selling/page/point_of_sale/pos_item_cart.js:966 msgid "Select Loyalty Program" msgstr "" -#: erpnext/public/js/controllers/transaction.js:527 +#: erpnext/public/js/controllers/transaction.js:529 msgid "Select Payment Schedule" msgstr "" @@ -49003,7 +49048,7 @@ msgstr "" msgid "Select date" msgstr "" -#: erpnext/controllers/accounts_controller.py:3017 +#: erpnext/controllers/accounts_controller.py:3022 msgid "Select finance book for the item {0} at row {1}" msgstr "" @@ -49206,7 +49251,7 @@ msgstr "" #: erpnext/selling/doctype/selling_settings/selling_settings.json #: erpnext/selling/workspace/selling/selling.json #: erpnext/setup/workspace/erpnext_settings/erpnext_settings.json -#: erpnext/stock/doctype/stock_settings/stock_settings.py:260 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:265 #: erpnext/workspace_sidebar/erpnext_settings.json msgid "Selling Settings" msgstr "" @@ -49264,7 +49309,7 @@ msgid "Send Emails to Suppliers" msgstr "" #. Label of the send_sms (Button) field in DocType 'SMS Center' -#: erpnext/public/js/controllers/transaction.js:727 +#: erpnext/public/js/controllers/transaction.js:729 #: erpnext/selling/doctype/sms_center/sms_center.json msgid "Send SMS" msgstr "" @@ -49411,7 +49456,7 @@ msgstr "" #: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js:74 #: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py:114 -#: erpnext/public/js/controllers/transaction.js:2890 +#: erpnext/public/js/controllers/transaction.js:2895 #: erpnext/public/js/utils/serial_no_batch_selector.js:432 #: erpnext/selling/doctype/installation_note_item/installation_note_item.json #: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -49431,7 +49476,7 @@ msgstr "" #: erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.py:450 #: erpnext/stock/report/serial_no_ledger/serial_no_ledger.js:38 #: erpnext/stock/report/serial_no_ledger/serial_no_ledger.py:61 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:427 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:430 #: erpnext/stock/workspace/stock/stock.json #: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json #: erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json @@ -49455,7 +49500,7 @@ msgstr "" msgid "Serial No Already Assigned" msgstr "" -#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:33 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:39 msgid "Serial No Count" msgstr "" @@ -49472,7 +49517,7 @@ msgstr "" msgid "Serial No Range" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2752 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2802 msgid "Serial No Reserved" msgstr "" @@ -49529,7 +49574,7 @@ msgstr "" msgid "Serial No and Batch Traceability" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1245 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1295 msgid "Serial No is mandatory" msgstr "" @@ -49537,6 +49582,10 @@ msgstr "" msgid "Serial No is mandatory for Item {0}" msgstr "" +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:111 +msgid "Serial No status sync has been queued. Reload the report after a few minutes." +msgstr "" + #: erpnext/public/js/utils/serial_no_batch_selector.js:603 msgid "Serial No {0} already exists" msgstr "" @@ -49558,7 +49607,7 @@ msgstr "" msgid "Serial No {0} does not exist" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:3541 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:3591 msgid "Serial No {0} does not exists" msgstr "" @@ -49574,7 +49623,7 @@ msgstr "" msgid "Serial No {0} is already assigned to customer {1}. Can only be returned against the customer {1}" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:484 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:534 msgid "Serial No {0} is not present in the {1} {2}, hence you can't return it against the {1} {2}" msgstr "" @@ -49612,11 +49661,11 @@ msgstr "" msgid "Serial Nos / Batches" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2024 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2074 msgid "Serial Nos are created successfully" msgstr "" -#: erpnext/stock/stock_ledger.py:2330 +#: erpnext/stock/stock_ledger.py:2373 msgid "Serial Nos are reserved in Stock Reservation Entries, you need to unreserve them before proceeding." msgstr "" @@ -49690,26 +49739,26 @@ msgstr "" #: erpnext/stock/report/available_serial_no/available_serial_no.py:188 #: erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.py:31 #: erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py:80 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:411 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:188 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:414 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:197 #: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json #: erpnext/workspace_sidebar/stock.json msgid "Serial and Batch Bundle" msgstr "" -#: erpnext/stock/doctype/item/item.py:1122 +#: erpnext/stock/doctype/item/item.py:1132 msgid "Serial and Batch Bundle Exists" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2253 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2303 msgid "Serial and Batch Bundle created" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2349 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2399 msgid "Serial and Batch Bundle updated" msgstr "" -#: erpnext/controllers/stock_controller.py:237 +#: erpnext/controllers/stock_controller.py:251 msgid "Serial and Batch Bundle {0} is already used in {1} {2}." msgstr "" @@ -49717,7 +49766,7 @@ msgstr "" msgid "Serial and Batch Bundle {0} is not submitted" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2323 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2373 msgid "Serial and Batch Bundle {0} is submitted and its entries cannot be modified." msgstr "" @@ -49973,12 +50022,12 @@ msgid "Service Stop Date" msgstr "" #: erpnext/accounts/deferred_revenue.py:45 -#: erpnext/public/js/controllers/transaction.js:1799 +#: erpnext/public/js/controllers/transaction.js:1804 msgid "Service Stop Date cannot be after Service End Date" msgstr "" #: erpnext/accounts/deferred_revenue.py:42 -#: erpnext/public/js/controllers/transaction.js:1796 +#: erpnext/public/js/controllers/transaction.js:1801 msgid "Service Stop Date cannot be before Service Start Date" msgstr "" @@ -50002,7 +50051,7 @@ msgstr "" #. Label of the set_basic_rate_manually (Check) field in DocType 'Stock Entry #. Detail' -#: erpnext/stock/doctype/stock_entry/stock_entry.py:411 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:420 #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json msgid "Set Basic Rate Manually" msgstr "" @@ -50053,11 +50102,11 @@ msgstr "" msgid "Set Landed Cost Based on Purchase Invoice Rate" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1234 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1243 msgid "Set Loyalty Program" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:315 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:314 msgid "Set New Release Date" msgstr "" @@ -50590,7 +50639,7 @@ msgstr "" msgid "Shipping Address Template" msgstr "" -#: erpnext/controllers/accounts_controller.py:595 +#: erpnext/controllers/accounts_controller.py:600 msgid "Shipping Address does not belong to the {0}" msgstr "" @@ -50775,7 +50824,7 @@ msgstr "" msgid "Show Dimension Wise Stock" msgstr "" -#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js:29 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js:53 msgid "Show Disabled Items" msgstr "" @@ -51067,7 +51116,7 @@ msgstr "" msgid "Simultaneous" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:862 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:871 msgid "Since there is a process loss of {0} units for the finished good {1}, you should reduce the quantity by {0} units for the finished good {1} in the Items Table." msgstr "" @@ -51179,7 +51228,7 @@ msgstr "" msgid "Solvency Ratios" msgstr "" -#: erpnext/controllers/accounts_controller.py:4400 +#: erpnext/controllers/accounts_controller.py:4430 msgid "Some required Company details are missing. You don't have permission to update them. Please contact your System Manager." msgstr "" @@ -51252,11 +51301,11 @@ msgstr "" msgid "Source Stock Entry (Manufacture)" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1024 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1033 msgid "Source Stock Entry {0} belongs to Work Order {1}, not {2}. Please use a manufacture entry from the same Work Order." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2724 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2756 msgid "Source Stock Entry {0} has no finished goods quantity" msgstr "" @@ -51322,7 +51371,7 @@ msgstr "" msgid "Source and Target Location cannot be same" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:990 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:999 msgid "Source and target warehouse cannot be same for row {0}" msgstr "" @@ -51335,9 +51384,9 @@ msgstr "" msgid "Source of Funds (Liabilities)" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:957 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:973 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:980 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:966 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:982 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:989 msgid "Source warehouse is mandatory for row {0}" msgstr "" @@ -51710,7 +51759,7 @@ msgstr "" msgid "Status must be one of {0}" msgstr "" -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:279 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:283 msgid "Status set to rejected as there are one or more rejected readings." msgstr "" @@ -51740,8 +51789,8 @@ msgstr "" #: erpnext/accounts/doctype/account/account.json #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:96 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:158 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1406 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1445 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1416 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1455 #: erpnext/accounts/report/account_balance/account_balance.js:58 msgid "Stock Adjustment" msgstr "" @@ -51827,11 +51876,27 @@ msgstr "" msgid "Stock Closing Entry" msgstr "" -#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:80 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:239 +msgid "Stock Closing Entry In Progress" +msgstr "" + +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:257 +msgid "Stock Closing Entry Outdated" +msgstr "" + +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:231 +msgid "Stock Closing Entry Required" +msgstr "" + +#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:122 msgid "Stock Closing Entry {0} already exists for the selected date range" msgstr "" -#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:101 +#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:144 +msgid "Stock Closing Entry {0} belongs to a closed accounting period. Cancel the Period Closing Voucher {1} first." +msgstr "" + +#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:159 msgid "Stock Closing Entry {0} has been queued for processing, system will take sometime to complete it." msgstr "" @@ -51848,7 +51913,7 @@ msgstr "" msgid "Stock Details" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1201 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1210 msgid "Stock Entries already created for Work Order {0}: {1}" msgstr "" @@ -51916,7 +51981,7 @@ msgstr "" msgid "Stock Entry {0} has created" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1325 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1335 msgid "Stock Entry {0} is not submitted" msgstr "" @@ -51937,6 +52002,10 @@ msgstr "" msgid "Stock Expenses" msgstr "" +#: erpnext/stock/stock_ledger.py:80 +msgid "Stock Frozen" +msgstr "" + #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:37 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:60 msgid "Stock In Hand" @@ -51970,7 +52039,7 @@ msgstr "" #. Name of a DocType #: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:113 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:149 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:158 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:30 msgid "Stock Ledger Entry" msgstr "" @@ -52094,7 +52163,7 @@ msgstr "" #: erpnext/stock/doctype/material_request_item/material_request_item.json #: erpnext/stock/report/item_where_used/item_where_used.py:76 #: erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py:34 -#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:34 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:40 msgid "Stock Qty" msgstr "" @@ -52185,9 +52254,9 @@ msgstr "" #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1706 #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1723 #: erpnext/stock/doctype/stock_settings/stock_settings.json -#: erpnext/stock/doctype/stock_settings/stock_settings.py:217 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:229 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:243 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:222 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:234 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:248 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:182 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:195 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:207 @@ -52201,7 +52270,7 @@ msgid "Stock Reservation Entries Cancelled" msgstr "" #: erpnext/controllers/subcontracting_inward_controller.py:1037 -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:2262 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:2261 #: erpnext/manufacturing/doctype/work_order/work_order.py:2416 #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1784 msgid "Stock Reservation Entries Created" @@ -52380,7 +52449,7 @@ msgstr "" #: erpnext/stock/report/item_where_used/item_where_used.py:82 #: erpnext/stock/report/reserved_stock/reserved_stock.py:110 #: erpnext/stock/report/stock_balance/stock_balance.py:508 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:296 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:299 #: erpnext/subcontracting/doctype/subcontracting_inward_order_item/subcontracting_inward_order_item.json #: erpnext/subcontracting/doctype/subcontracting_inward_order_received_item/subcontracting_inward_order_received_item.json #: erpnext/subcontracting/doctype/subcontracting_inward_order_secondary_item/subcontracting_inward_order_secondary_item.json @@ -52406,7 +52475,7 @@ msgstr "" msgid "Stock Uom" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:758 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:768 msgid "Stock Update Not Allowed" msgstr "" @@ -52481,6 +52550,10 @@ msgstr "" msgid "Stock Value" msgstr "" +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:186 +msgid "Stock Value Mismatch" +msgstr "" + #. Label of a chart in the Stock Workspace #: erpnext/stock/workspace/stock/stock.json msgid "Stock Value by Item Group" @@ -52522,7 +52595,7 @@ msgstr "" msgid "Stock cannot be updated because the invoice contains a drop shipping item. Please disable 'Update Stock' or remove the drop shipping item." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:755 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:765 msgid "Stock cannot be updated for Purchase Invoice {0} because a Purchase Receipt {1} has already been created for this transaction. Please disable the 'Update Stock' checkbox in the Purchase Invoice and save the invoice." msgstr "" @@ -52555,12 +52628,20 @@ msgstr "" msgid "Stock transactions before {0} are frozen" msgstr "" +#: erpnext/stock/stock_ledger.py:74 +msgid "Stock transactions dated on or before {0} are frozen because the period is closed and the Stock Closing Entry {1} has been generated. To make changes, cancel the Period Closing Voucher first." +msgstr "" + #. Description of the 'Freeze stocks older than (days)' (Int) field in DocType #. 'Stock Settings' #: erpnext/stock/doctype/stock_settings/stock_settings.json msgid "Stock transactions that are older than the mentioned days cannot be modified." msgstr "" +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:254 +msgid "Stock transactions were created or modified after the Stock Closing Entry {0} was generated. Regenerate it before submitting the Period Closing Voucher." +msgstr "" + #. Description of the 'Auto reserve Stock for Sales Order on Purchase' (Check) #. field in DocType 'Stock Settings' #: erpnext/stock/doctype/stock_settings/stock_settings.json @@ -52613,7 +52694,7 @@ msgstr "" msgid "Sub Assemblies & Raw Materials" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:340 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:329 msgid "Sub Assembly Item" msgstr "" @@ -52629,7 +52710,7 @@ msgstr "" msgid "Sub Assembly Item Reference" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:449 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:438 msgid "Sub Assembly Item is mandatory" msgstr "" @@ -53084,11 +53165,11 @@ msgstr "" msgid "Subscription End Date" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:406 +#: erpnext/accounts/doctype/subscription/subscription.py:409 msgid "Subscription End Date is mandatory to follow calendar months" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:396 +#: erpnext/accounts/doctype/subscription/subscription.py:399 msgid "Subscription End Date must be after {0} as per the subscription plan" msgstr "" @@ -53148,7 +53229,7 @@ msgstr "" msgid "Subscription Start Date" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:774 +#: erpnext/accounts/doctype/subscription/subscription.py:782 msgid "Subscription for Future dates cannot be processed." msgstr "" @@ -53546,7 +53627,7 @@ msgstr "" msgid "Supplier Invoice No" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1841 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1851 msgid "Supplier Invoice No exists in Purchase Invoice {0}" msgstr "" @@ -53898,6 +53979,10 @@ msgstr "" msgid "Sync Now" msgstr "" +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js:6 +msgid "Sync Serial No Status" +msgstr "" + #: erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js:36 msgid "Sync Started" msgstr "" @@ -53938,7 +54023,7 @@ msgstr "" msgid "System will fetch all the entries if limit value is zero." msgstr "" -#: erpnext/controllers/accounts_controller.py:2256 +#: erpnext/controllers/accounts_controller.py:2261 msgid "System will not check over billing since amount for Item {0} in {1} is zero" msgstr "" @@ -53961,7 +54046,7 @@ msgstr "" msgid "TDS Computation Summary" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1599 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1609 msgid "TDS Deducted" msgstr "" @@ -54148,9 +54233,9 @@ msgstr "" msgid "Target Warehouse {0} must be same as Delivery Warehouse {1} in the Subcontracting Inward Order Item." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:963 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:969 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:984 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:972 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:978 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:993 msgid "Target warehouse is mandatory for row {0}" msgstr "" @@ -55146,7 +55231,7 @@ msgstr "" msgid "The BOM which will be replaced" msgstr "" -#: erpnext/stock/serial_batch_bundle.py:1545 +#: erpnext/stock/serial_batch_bundle.py:1631 msgid "The Batch {0} has negative batch quantity {1}. To fix this, go to the batch and click on Recalculate Batch Qty. If the issue still persists, create an inward entry." msgstr "" @@ -55166,11 +55251,11 @@ msgstr "" msgid "The Excluded Fee is bigger than the Deposit it is deducted from." msgstr "" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:188 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:306 msgid "The GL Entries and closing balances will be processed in the background, it can take a few minutes." msgstr "" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:461 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:579 msgid "The GL Entries will be cancelled in the background, it can take a few minutes." msgstr "" @@ -55190,7 +55275,7 @@ msgstr "" msgid "The Pick List having Stock Reservation Entries cannot be updated. If you need to make changes, we recommend canceling the existing Stock Reservation Entries before updating the Pick List." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3176 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3208 msgid "The Process Loss Qty has reset as per job cards Process Loss Qty" msgstr "" @@ -55202,14 +55287,18 @@ msgstr "" msgid "The Serial No at Row #{0}: {1} is not available in warehouse {2}." msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2749 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2799 msgid "The Serial No {0} is reserved against the {1} {2} and cannot be used for any other transaction." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2142 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2174 msgid "The Serial and Batch Bundle {0} is not valid for this transaction. The 'Type of Transaction' should be 'Outward' instead of 'Inward' in Serial and Batch Bundle {0}" msgstr "" +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:236 +msgid "The Stock Closing Entry for {0} is not completed yet. Wait for it to complete before submitting the Period Closing Voucher." +msgstr "" + #: erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.js:17 msgid "The Stock Entry of type 'Manufacture' is known as backflush. Raw materials being consumed to manufacture finished goods is known as backflushing.

    When creating Manufacture Entry, raw-material items are backflushed based on BOM of production item. If you want raw-material items to be backflushed based on Material Transfer entry made against that Work Order instead, then you can set it under this field." msgstr "" @@ -55246,10 +55335,14 @@ msgstr "" msgid "The bank account is not a company account. Please select a company account" msgstr "" -#: erpnext/controllers/stock_controller.py:1482 +#: erpnext/controllers/stock_controller.py:1496 msgid "The batch {0} is reserved for {1} in the warehouse {2} and the remaining quantity is not enough to cover the reservations. So, cannot proceed with the {3} {4}." msgstr "" +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:179 +msgid "The closing balance {0} of the Stock Asset accounts does not match the closing value {1} of the Stock Balance report as on {2}. Resolve the difference using the Stock Ledger Variance report before closing the period." +msgstr "" + #: erpnext/regional/report/vat_audit_report/vat_audit_report.py:43 msgid "The company {0} is not in South Africa. VAT Audit Report is only available for companies in South Africa." msgstr "" @@ -55352,11 +55445,11 @@ msgstr "" msgid "The following batches are expired, please restock them:
    {0}" msgstr "" -#: erpnext/controllers/accounts_controller.py:446 +#: erpnext/controllers/accounts_controller.py:451 msgid "The following cancelled repost entries exist for {0}:

    {1}

    Kindly delete these entries before continuing." msgstr "" -#: erpnext/stock/doctype/item/item.py:949 +#: erpnext/stock/doctype/item/item.py:959 msgid "The following deleted attributes exist in Variants but not in the Template. You can either delete the Variants or keep the attribute(s) in template." msgstr "" @@ -55467,7 +55560,7 @@ msgstr "" msgid "The original invoice should be consolidated before or along with the return invoice." msgstr "" -#: erpnext/controllers/accounts_controller.py:224 +#: erpnext/controllers/accounts_controller.py:229 msgid "The outstanding amount {0} in {1} is lesser than {2}. Updating the outstanding to this invoice." msgstr "" @@ -55518,7 +55611,7 @@ msgstr "" msgid "The reference number of the transaction" msgstr "" -#: erpnext/public/js/utils.js:985 +#: erpnext/public/js/utils.js:1011 msgid "The reserved stock will be released when you update items. Are you certain you wish to proceed?" msgstr "" @@ -55571,7 +55664,7 @@ msgstr "" msgid "The shares don't exist with the {0}" msgstr "" -#: erpnext/stock/stock_ledger.py:839 +#: erpnext/stock/stock_ledger.py:866 msgid "The stock for the item {0} in the {1} warehouse was negative on the {2}. You should create a positive entry {3} before the date {4} and time {5} to post the correct valuation rate. For more details, please read the documentation." msgstr "" @@ -55673,7 +55766,7 @@ msgstr "" msgid "The {0} ({1}) must be equal to {2} ({3})" msgstr "" -#: erpnext/public/js/controllers/transaction.js:3382 +#: erpnext/public/js/controllers/transaction.js:3387 msgid "The {0} contains Unit Price Items." msgstr "" @@ -55774,7 +55867,7 @@ msgstr "" msgid "There is one unreconciled transaction before {0}." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2079 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2111 msgid "There must be atleast 1 Finished Good in this Stock Entry" msgstr "" @@ -55886,7 +55979,7 @@ msgstr "" msgid "This covers all scorecards tied to this Setup" msgstr "" -#: erpnext/controllers/status_updater.py:501 +#: erpnext/controllers/status_updater.py:502 msgid "This document is over limit by {0} {1} for item {4}. Are you making another {3} against the same {2}?" msgstr "" @@ -55989,7 +56082,7 @@ msgstr "" msgid "This is considered dangerous from accounting point of view." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:536 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:546 msgid "This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice" msgstr "" @@ -56191,6 +56284,10 @@ msgstr "" msgid "This will restrict user access to other employee records" msgstr "" +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js:16 +msgid "This will update the warehouse and status of Serial Nos counted in {0} to match the stock ledger. Continue?" +msgstr "" + #: erpnext/controllers/selling_controller.py:886 msgid "This {} will be treated as material transfer." msgstr "" @@ -56417,7 +56514,7 @@ msgstr "" msgid "To Currency" msgstr "" -#: erpnext/controllers/accounts_controller.py:645 +#: erpnext/controllers/accounts_controller.py:650 #: erpnext/setup/doctype/holiday_list/holiday_list.py:121 msgid "To Date cannot be before From Date" msgstr "" @@ -56644,15 +56741,15 @@ msgstr "" msgid "To add subcontracted Item's raw materials if include exploded items is disabled." msgstr "" -#: erpnext/controllers/status_updater.py:494 +#: erpnext/controllers/status_updater.py:495 msgid "To allow over billing, update \"Over Billing Allowance\" in Accounts Settings or the Item." msgstr "" -#: erpnext/controllers/status_updater.py:488 +#: erpnext/controllers/status_updater.py:489 msgid "To allow over ordering, update \"Over Order Allowance\" in Buying Settings." msgstr "" -#: erpnext/controllers/status_updater.py:490 +#: erpnext/controllers/status_updater.py:491 msgid "To allow over receipt / delivery, update \"Over Receipt/Delivery Allowance\" in Stock Settings or the Item." msgstr "" @@ -56689,7 +56786,7 @@ msgid "To include sub-assembly costs and secondary items in Finished Goods on a msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2268 -#: erpnext/controllers/accounts_controller.py:3275 +#: erpnext/controllers/accounts_controller.py:3280 msgid "To include tax in row {0} in Item rate, taxes in rows {1} must also be included" msgstr "" @@ -56713,11 +56810,11 @@ msgstr "" msgid "To still proceed with editing this Attribute Value, enable {0} in Item Variant Settings." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:627 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:637 msgid "To submit the invoice without purchase order please set {0} as {1} in {2}" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:649 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:659 msgid "To submit the invoice without purchase receipt please set {0} as {1} in {2}" msgstr "" @@ -57099,7 +57196,7 @@ msgstr "" msgid "Total Debit Transactions" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:942 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:952 msgid "Total Debit must be equal to Total Credit. The difference is {0}" msgstr "" @@ -57324,7 +57421,7 @@ msgstr "" msgid "Total Paid Amount" msgstr "" -#: erpnext/controllers/accounts_controller.py:2830 +#: erpnext/controllers/accounts_controller.py:2835 msgid "Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total" msgstr "" @@ -57626,7 +57723,7 @@ msgstr "" msgid "Total allocated percentage for sales team should be 100" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:198 +#: erpnext/selling/doctype/customer/customer.py:199 msgid "Total contribution percentage should be equal to 100" msgstr "" @@ -58215,7 +58312,7 @@ msgstr "" msgid "Trial Period End Date" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:376 +#: erpnext/accounts/doctype/subscription/subscription.py:379 msgid "Trial Period End Date Cannot be before Trial Period Start Date" msgstr "" @@ -58224,7 +58321,7 @@ msgstr "" msgid "Trial Period Start Date" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:382 +#: erpnext/accounts/doctype/subscription/subscription.py:385 msgid "Trial Period Start date cannot be after Subscription Start Date" msgstr "" @@ -58416,7 +58513,7 @@ msgstr "" #: erpnext/manufacturing/doctype/workstation/workstation.js:480 #: erpnext/manufacturing/report/bom_explorer/bom_explorer.py:70 #: erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py:110 -#: erpnext/public/js/stock_analytics.js:94 erpnext/public/js/utils.js:858 +#: erpnext/public/js/stock_analytics.js:94 erpnext/public/js/utils.js:861 #: erpnext/quality_management/doctype/quality_goal_objective/quality_goal_objective.json #: erpnext/quality_management/doctype/quality_review_objective/quality_review_objective.json #: erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json @@ -58530,7 +58627,7 @@ msgstr "" msgid "UOM Name" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:4379 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:4411 msgid "UOM conversion factor required for UOM: {0} in Item: {1}" msgstr "" @@ -58714,7 +58811,7 @@ msgstr "" msgid "Unit Of Measure" msgstr "" -#: erpnext/controllers/accounts_controller.py:4132 +#: erpnext/controllers/accounts_controller.py:4162 msgid "Unit Price" msgstr "" @@ -59078,7 +59175,7 @@ msgstr "" #: erpnext/buying/doctype/purchase_order/purchase_order.js:324 #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.js:43 -#: erpnext/public/js/utils.js:964 +#: erpnext/public/js/utils.js:990 #: erpnext/selling/doctype/quotation/quotation.js:136 #: erpnext/selling/doctype/sales_order/sales_order.js:90 #: erpnext/selling/doctype/sales_order/sales_order.js:946 @@ -59091,7 +59188,7 @@ msgstr "" #. Invoice' #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json -#: erpnext/controllers/accounts_controller.py:217 +#: erpnext/controllers/accounts_controller.py:222 msgid "Update Outstanding for Self" msgstr "" @@ -59176,7 +59273,7 @@ msgstr "" msgid "Updating Costing and Billing fields against this Project..." msgstr "" -#: erpnext/stock/doctype/item/item.py:1511 +#: erpnext/stock/doctype/item/item.py:1521 msgid "Updating Variants..." msgstr "" @@ -59794,11 +59891,11 @@ msgstr "" msgid "Valuation Rate (In / Out)" msgstr "" -#: erpnext/stock/stock_ledger.py:2056 +#: erpnext/stock/stock_ledger.py:2099 msgid "Valuation Rate Missing" msgstr "" -#: erpnext/stock/stock_ledger.py:2034 +#: erpnext/stock/stock_ledger.py:2077 msgid "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}." msgstr "" @@ -59830,7 +59927,7 @@ msgid "Valuation rate for the item as per Sales Invoice (Only for Internal Trans msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2292 -#: erpnext/controllers/accounts_controller.py:3299 +#: erpnext/controllers/accounts_controller.py:3304 msgid "Valuation type charges can not be marked as Inclusive" msgstr "" @@ -59966,7 +60063,7 @@ msgstr "" msgid "Variant" msgstr "" -#: erpnext/stock/doctype/item/item.py:964 +#: erpnext/stock/doctype/item/item.py:974 msgid "Variant Attribute Error" msgstr "" @@ -59985,7 +60082,7 @@ msgstr "" msgid "Variant Based On" msgstr "" -#: erpnext/stock/doctype/item/item.py:992 +#: erpnext/stock/doctype/item/item.py:1002 msgid "Variant Based On cannot be changed" msgstr "" @@ -60003,7 +60100,7 @@ msgstr "" msgid "Variant Item" msgstr "" -#: erpnext/stock/doctype/item/item.py:962 +#: erpnext/stock/doctype/item/item.py:972 msgid "Variant Items" msgstr "" @@ -60326,7 +60423,7 @@ msgstr "" #: erpnext/stock/report/available_serial_no/available_serial_no.js:56 #: erpnext/stock/report/available_serial_no/available_serial_no.py:196 #: erpnext/stock/report/stock_ledger/stock_ledger.js:97 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:404 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:407 msgid "Voucher #" msgstr "" @@ -60425,12 +60522,12 @@ msgstr "" #: erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py:112 #: erpnext/stock/report/serial_no_ledger/serial_no_ledger.py:34 #: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:159 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:176 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:185 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:74 msgid "Voucher No" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1485 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1535 msgid "Voucher No is mandatory" msgstr "" @@ -60499,8 +60596,8 @@ msgstr "" #: erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.py:486 #: erpnext/stock/report/serial_no_ledger/serial_no_ledger.py:28 #: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:157 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:402 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:170 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:405 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:179 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:68 msgid "Voucher Type" msgstr "" @@ -60708,6 +60805,7 @@ msgid "Warehouse {0} does not belong to company {1}" msgstr "" #: erpnext/stock/doctype/warehouse/warehouse.py:288 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:99 msgid "Warehouse {0} does not exist" msgstr "" @@ -60715,11 +60813,11 @@ msgstr "" msgid "Warehouse {0} is not allowed for Sales Order {1}, it should be {2}" msgstr "" -#: erpnext/controllers/stock_controller.py:861 +#: erpnext/controllers/stock_controller.py:875 msgid "Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}." msgstr "" -#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:20 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:26 msgid "Warehouse: {0} does not belong to {1}" msgstr "" @@ -60828,7 +60926,7 @@ msgstr "" msgid "Warning - Row {0}: Billing Hours are more than Actual Hours" msgstr "" -#: erpnext/stock/stock_ledger.py:849 +#: erpnext/stock/stock_ledger.py:876 msgid "Warning on Negative Stock" msgstr "" @@ -60840,11 +60938,11 @@ msgstr "" msgid "Warning: Account changed for warehouse" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1331 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1341 msgid "Warning: Another {0} # {1} exists against stock entry {2}" msgstr "" -#: erpnext/stock/doctype/material_request/material_request.js:709 +#: erpnext/stock/doctype/material_request/material_request.js:705 msgid "Warning: Material Requested Qty is less than Minimum Order Qty" msgstr "" @@ -60942,7 +61040,7 @@ msgstr "" msgid "Wavelength In Megametres" msgstr "" -#: erpnext/controllers/accounts_controller.py:212 +#: erpnext/controllers/accounts_controller.py:217 msgid "We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, uncheck the '{2}' checkbox." msgstr "" @@ -61164,7 +61262,7 @@ msgstr "" msgid "When enabled, transactions with this supplier will be blocked based on the Hold Type below" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:408 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:417 msgid "When there are multiple finished goods ({0}) in a Repack stock entry, the basic rate for all finished goods must be set manually. To set rate manually, enable the checkbox 'Set Basic Rate Manually' in the respective finished good row." msgstr "" @@ -61403,7 +61501,7 @@ msgstr "" msgid "Work Order Item" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1027 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1036 msgid "Work Order Mismatch" msgstr "" @@ -61465,11 +61563,11 @@ msgstr "" msgid "Work Order {0} created" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2740 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2772 msgid "Work Order {0} has no produced qty" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1151 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1160 msgid "Work Order {0}: Job Card not found for the operation {1}" msgstr "" @@ -61797,7 +61895,7 @@ msgstr "" msgid "You are importing data for the code list:" msgstr "" -#: erpnext/controllers/accounts_controller.py:3929 +#: erpnext/controllers/accounts_controller.py:3959 msgid "You are not allowed to update as per the conditions set in {} Workflow." msgstr "" @@ -61813,6 +61911,10 @@ msgstr "" msgid "You are not authorized to set Frozen value" msgstr "" +#: erpnext/projects/doctype/task/task.py:317 +msgid "You are not permitted to create a Task for Project {0}" +msgstr "" + #: erpnext/stock/doctype/pick_list/pick_list.py:546 msgid "You are picking more than required quantity for the item {0}. Check if there is any other pick list created for the sales order {1}." msgstr "" @@ -61870,7 +61972,7 @@ msgstr "" msgid "You can set up the rule to split the transaction across multiple accounts." msgstr "" -#: erpnext/controllers/accounts_controller.py:233 +#: erpnext/controllers/accounts_controller.py:238 msgid "You can use {0} to reconcile against {1} later." msgstr "" @@ -61902,7 +62004,7 @@ msgstr "" msgid "You cannot create/amend any accounting entries till this date." msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:951 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:961 msgid "You cannot credit and debit same account at the same time" msgstr "" @@ -61930,7 +62032,7 @@ msgstr "" msgid "You cannot repost item valuation before {}" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:758 +#: erpnext/accounts/doctype/subscription/subscription.py:766 msgid "You cannot restart a Subscription that is not cancelled." msgstr "" @@ -61942,7 +62044,7 @@ msgstr "" msgid "You cannot submit the order without payment." msgstr "" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:116 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:119 msgid "You cannot {0} this document because another Period Closing Entry {1} exists after {2}" msgstr "" @@ -61959,7 +62061,7 @@ msgstr "" msgid "You do not have permission to import bank transactions" msgstr "" -#: erpnext/controllers/accounts_controller.py:3907 +#: erpnext/controllers/accounts_controller.py:3937 msgid "You do not have permissions to {} items in a {}." msgstr "" @@ -61971,11 +62073,11 @@ msgstr "" msgid "You don't have enough points to redeem." msgstr "" -#: erpnext/controllers/accounts_controller.py:4475 +#: erpnext/controllers/accounts_controller.py:4505 msgid "You don't have permission to create a Company Address. Please contact your System Manager." msgstr "" -#: erpnext/controllers/accounts_controller.py:4455 +#: erpnext/controllers/accounts_controller.py:4485 msgid "You don't have permission to update Company details. Please contact your System Manager." msgstr "" @@ -61983,7 +62085,7 @@ msgstr "" msgid "You don't have permission to update Received Qty DocField for item {0}" msgstr "" -#: erpnext/controllers/accounts_controller.py:4449 +#: erpnext/controllers/accounts_controller.py:4479 msgid "You don't have permission to update this document. Please contact your System Manager." msgstr "" @@ -61991,7 +62093,7 @@ msgstr "" msgid "You had {} errors while creating opening invoices. Check {} for more details" msgstr "" -#: erpnext/public/js/utils.js:1064 +#: erpnext/public/js/utils.js:1090 msgid "You have already selected items from {0} {1}" msgstr "" @@ -61999,7 +62101,7 @@ msgstr "" msgid "You have been invited to collaborate on the project {0}." msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:255 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:260 msgid "You have enabled {0} and {1} in {2}. This can lead to prices from the default price list being inserted in the transaction price list." msgstr "" @@ -62019,7 +62121,7 @@ msgstr "" msgid "You have not performed any reconciliations in this session yet." msgstr "" -#: erpnext/stock/doctype/item/item.py:1187 +#: erpnext/stock/doctype/item/item.py:1197 msgid "You have to enable auto re-order in Stock Settings to maintain re-order levels." msgstr "" @@ -62039,7 +62141,7 @@ msgstr "" msgid "You need to cancel POS Closing Entry {} to be able to cancel this document." msgstr "" -#: erpnext/controllers/accounts_controller.py:3250 +#: erpnext/controllers/accounts_controller.py:3255 msgid "You selected the account group {1} as {2} Account in row {0}. Please select a single account." msgstr "" @@ -62099,7 +62201,7 @@ msgstr "" msgid "Zero Rated" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:738 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:747 msgid "Zero quantity" msgstr "" @@ -62125,7 +62227,7 @@ msgstr "" msgid "`Allow Negative rates for Items`" msgstr "" -#: erpnext/stock/stock_ledger.py:2048 +#: erpnext/stock/stock_ledger.py:2091 msgid "after" msgstr "" @@ -62145,7 +62247,7 @@ msgstr "" msgid "as a percentage of finished item quantity" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1655 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1705 msgid "as of {0}" msgstr "" @@ -62165,7 +62267,7 @@ msgstr "" msgid "cannot be greater than 100" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:341 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:351 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1158 msgid "dated {0}" msgstr "" @@ -62317,7 +62419,7 @@ msgstr "" msgid "per hour" msgstr "" -#: erpnext/stock/stock_ledger.py:2049 +#: erpnext/stock/stock_ledger.py:2092 msgid "performing either one below:" msgstr "" @@ -62389,12 +62491,12 @@ msgstr "" msgid "sold" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:734 +#: erpnext/accounts/doctype/subscription/subscription.py:742 msgid "subscription is already cancelled." msgstr "" -#: erpnext/controllers/status_updater.py:504 -#: erpnext/controllers/status_updater.py:523 +#: erpnext/controllers/status_updater.py:505 +#: erpnext/controllers/status_updater.py:524 msgid "target_ref_field" msgstr "" @@ -62461,7 +62563,7 @@ msgstr "" msgid "you must select Capital Work in Progress Account in accounts table" msgstr "" -#: erpnext/controllers/accounts_controller.py:1313 +#: erpnext/controllers/accounts_controller.py:1318 msgid "{0} '{1}' is disabled" msgstr "" @@ -62477,7 +62579,7 @@ msgstr "" msgid "{0} {1} has submitted Assets. Remove Item {2} from table to continue." msgstr "" -#: erpnext/controllers/accounts_controller.py:2410 +#: erpnext/controllers/accounts_controller.py:2415 msgid "{0} Account not found against Customer {1}." msgstr "" @@ -62537,19 +62639,19 @@ msgstr "" msgid "{0} account not found while submitting purchase receipt" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1071 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1081 msgid "{0} against Bill {1} dated {2}" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1080 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1090 msgid "{0} against Purchase Order {1}" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1047 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1057 msgid "{0} against Sales Invoice {1}" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1054 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1064 msgid "{0} against Sales Order {1}" msgstr "" @@ -62615,7 +62717,7 @@ msgstr "" msgid "{0} does not belong to Company {1}" msgstr "" -#: erpnext/controllers/accounts_controller.py:372 +#: erpnext/controllers/accounts_controller.py:377 msgid "{0} does not belong to the Company {1}." msgstr "" @@ -62657,7 +62759,7 @@ msgstr "" msgid "{0} hours" msgstr "" -#: erpnext/controllers/accounts_controller.py:2770 +#: erpnext/controllers/accounts_controller.py:2775 msgid "{0} in row {1}" msgstr "" @@ -62687,7 +62789,7 @@ msgstr "" msgid "{0} is already running for {1}" msgstr "" -#: erpnext/controllers/accounts_controller.py:194 +#: erpnext/controllers/accounts_controller.py:199 msgid "{0} is blocked so this transaction cannot proceed" msgstr "" @@ -62716,15 +62818,15 @@ msgstr "" msgid "{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}" msgstr "" -#: erpnext/controllers/accounts_controller.py:3207 +#: erpnext/controllers/accounts_controller.py:3212 msgid "{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}." msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1879 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1929 msgid "{0} is not a CSV file." msgstr "" -#: erpnext/selling/doctype/customer/customer.py:244 +#: erpnext/selling/doctype/customer/customer.py:245 msgid "{0} is not a company bank account" msgstr "" @@ -62732,7 +62834,7 @@ msgstr "" msgid "{0} is not a group node. Please select a group node as parent cost center" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:790 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:799 msgid "{0} is not a stock Item" msgstr "" @@ -62804,7 +62906,7 @@ msgstr "" msgid "{0} languages are marked as default languages. Please select only one of them." msgstr "" -#: erpnext/controllers/sales_and_purchase_return.py:218 +#: erpnext/controllers/sales_and_purchase_return.py:236 msgid "{0} must be negative in return document" msgstr "" @@ -62824,7 +62926,7 @@ msgstr "" msgid "{0} payment entries can not be filtered by {1}" msgstr "" -#: erpnext/controllers/stock_controller.py:1903 +#: erpnext/controllers/stock_controller.py:1917 msgid "{0} qty of Item {1} is being received into Warehouse {2} with capacity {3}." msgstr "" @@ -62853,16 +62955,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:1701 erpnext/stock/stock_ledger.py:2216 -#: erpnext/stock/stock_ledger.py:2230 +#: erpnext/stock/stock_ledger.py:1744 erpnext/stock/stock_ledger.py:2259 +#: erpnext/stock/stock_ledger.py:2273 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:2360 erpnext/stock/stock_ledger.py:2405 msgid "{0} units of {1} needed in {2} on {3} {4} to complete this transaction." msgstr "" -#: erpnext/stock/stock_ledger.py:1695 +#: erpnext/stock/stock_ledger.py:1738 msgid "{0} units of {1} needed in {2} to complete this transaction." msgstr "" @@ -62954,6 +63056,14 @@ msgstr "" msgid "{0} {1} is associated with {2}, but Party Account is {3}" msgstr "" +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:911 +msgid "{0} {1} is blocked and on hold until {2}." +msgstr "" + +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:915 +msgid "{0} {1} is blocked." +msgstr "" + #: erpnext/controllers/selling_controller.py:494 #: erpnext/controllers/subcontracting_controller.py:1174 msgid "{0} {1} is cancelled or closed" @@ -63008,7 +63118,7 @@ msgstr "" msgid "{0} {1} must be submitted" msgstr "" -#: erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py:501 +#: erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py:506 msgid "{0} {1} not allowed to be reposted. You can enable it by adding it '{2}' table in {3}." msgstr "" @@ -63043,7 +63153,7 @@ msgstr "" msgid "{0} {1}: Accounting Entry for {2} can only be made in currency: {3}" msgstr "" -#: erpnext/controllers/stock_controller.py:1073 +#: erpnext/controllers/stock_controller.py:1087 msgid "{0} {1}: Cost Center is mandatory for Item {2}" msgstr "" @@ -63088,7 +63198,7 @@ msgstr "" msgid "{0}% of total invoice value will be given as discount." msgstr "" -#: erpnext/projects/doctype/task/task.py:130 +#: erpnext/projects/doctype/task/task.py:131 msgid "{0}'s {1} cannot be after {2}'s Expected End Date." msgstr "" @@ -63125,7 +63235,7 @@ msgstr "" msgid "{0}: select the typed value {1} from the list or clear it" msgstr "" -#: erpnext/controllers/accounts_controller.py:562 +#: erpnext/controllers/accounts_controller.py:567 msgid "{0}: {1} does not belong to the Company: {2}" msgstr "" @@ -63153,11 +63263,11 @@ msgstr "" msgid "{field_label} is mandatory for sub-contracted {doctype}." msgstr "" -#: erpnext/controllers/stock_controller.py:2369 +#: erpnext/controllers/stock_controller.py:2383 msgid "{item_name}'s Sample Size ({sample_size}) cannot be greater than the Accepted Quantity ({accepted_quantity})" msgstr "" -#: erpnext/controllers/stock_controller.py:2132 +#: erpnext/controllers/stock_controller.py:2146 msgid "{ref_doctype} {ref_name} status is {status}." msgstr "" From 33eb6199d933da71ad245a62d2d97c8bbc9e42a6 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 9 Aug 2026 15:45:07 +0530 Subject: [PATCH 040/134] fix: sync open reference forms after Quality Inspection updates them update_qc_reference() writes the QI link and bumps the reference document's modified timestamp via raw db writes, which emit no realtime event. A reference form (Purchase Receipt, Delivery Note, Stock Entry, Job Card) still open in the browser keeps the old timestamp and fails the timestamp conflict check on the next save/submit, forcing a manual refresh after every QI submit/cancel/delete. Calling notify_update() on the reference publishes the standard doc_update event, so an open, unedited form silently reloads and syncs its timestamp. get_lazy_doc skips child table loading since notify_update only needs the parent row. (cherry picked from commit 647452c95befc0cb6b749478372b95899e059a1d) --- erpnext/stock/doctype/quality_inspection/quality_inspection.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index f213656fcd4..58fa5a56a60 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -265,6 +265,9 @@ class QualityInspection(Document): self.modified, ) + if self.reference_type and self.reference_name: + frappe.get_lazy_doc(self.reference_type, self.reference_name).notify_update() + def inspect_and_set_status(self): for reading in self.readings: if not reading.manual_inspection: # dont auto set status if manual From 546def2c5a29febae630df07420fd7203a040a49 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 9 Aug 2026 15:53:37 +0530 Subject: [PATCH 041/134] test: doc_update published for reference on Quality Inspection submit (cherry picked from commit e8a6884d5ec7b1ed6b4ca21b21fb5a392e7b157b) --- .../test_quality_inspection.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py index 7284c71ab20..4988deb008d 100644 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py @@ -2,6 +2,7 @@ # See license.txt from contextlib import contextmanager +from unittest.mock import patch import frappe from frappe.utils import nowdate @@ -78,6 +79,27 @@ class TestQualityInspection(ERPNextTestSuite): qa.delete() dn.delete() + def test_doc_update_published_for_reference_on_submit(self): + """Submitting a QI publishes doc_update so open reference forms resync their timestamp.""" + dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) + qa = create_quality_inspection( + reference_type="Delivery Note", reference_name=dn.name, do_not_submit=True + ) + + with patch.object(frappe, "publish_realtime") as publish_realtime: + qa.submit() + + reference_updates = [ + call + for call in publish_realtime.call_args_list + if call.args and call.args[0] == "doc_update" and call.kwargs.get("docname") == dn.name + ] + self.assertEqual(len(reference_updates), 1) + + message = reference_updates[0].args[1] + self.assertEqual(message["doctype"], "Delivery Note") + self.assertEqual(message["modified"], frappe.db.get_value("Delivery Note", dn.name, "modified")) + def test_value_based_qi_readings(self): # Test QI based on acceptance values (Non formula) dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) From f56867d8437f568e717ea28ab5d2efb20fa6c5ad Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 9 Aug 2026 16:20:32 +0530 Subject: [PATCH 042/134] fix(regional): rename Italy's duplicate Customer name fields The Italy regional setup created Custom Fields first_name/last_name on Customer. Since #46281 added standard quick-entry fields with the same names, every Italian site carries duplicate field definitions: - the setup wizard creates the duplicates silently because it skips validation, and any later Custom Field on Customer then raises UniqueFieldnameError (#50915) - without the duplicates, creating an Italian company aborts inside install_country_fixtures; on MariaDB an interrupted fixture run persists Custom Field documents whose columns were never added, after which every Company insert fails with "Unknown column 'fiscal_regime'" (#57215) Re-land the rename from #50921 (reverted in #53409): the fields become italy_customer_first_name/italy_customer_last_name and the e-invoice template reads the new names. The migration patch runs only on sites with Italy fixtures, re-runs them, explicitly syncs the schema of every affected doctype (create_custom_fields skips unchanged fields, so its own schema sync cannot restore missing columns), copies the old column values wherever the new field is empty (also on sites that removed the duplicate fields with the documented manual workaround), and deletes the duplicate Custom Fields last so an interrupted run stays resumable. The old insert_after anchor "salutation" no longer exists on Customer; the renamed fields anchor after customer_type. (cherry picked from commit 110d0a38a6728977e6d11207ad9fa3bca4590ac3) # Conflicts: # erpnext/patches.txt --- erpnext/patches.txt | 6 +++ .../rename_italy_customer_name_fields.py | 53 +++++++++++++++++++ erpnext/regional/italy/e-invoice.xml | 4 +- erpnext/regional/italy/setup.py | 16 +++--- 4 files changed, 70 insertions(+), 9 deletions(-) create mode 100644 erpnext/patches/v16_0/rename_italy_customer_name_fields.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index e56aa77abd2..fa3826dbb61 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -496,3 +496,9 @@ erpnext.patches.v16_0.rename_ar_ap_ageing_filter erpnext.patches.v16_0.fix_subcontracting_titles erpnext.patches.v16_0.backfill_repost_accounting_ledger_status erpnext.patches.v16_0.merge_seeded_item_group_root +<<<<<<< HEAD +======= +erpnext.patches.v16_0.set_stock_uom_in_job_card +erpnext.patches.v16_0.set_work_order_requested_and_picked_qty +erpnext.patches.v16_0.rename_italy_customer_name_fields +>>>>>>> 110d0a38a6 (fix(regional): rename Italy's duplicate Customer name fields) diff --git a/erpnext/patches/v16_0/rename_italy_customer_name_fields.py b/erpnext/patches/v16_0/rename_italy_customer_name_fields.py new file mode 100644 index 00000000000..4e1b13a947b --- /dev/null +++ b/erpnext/patches/v16_0/rename_italy_customer_name_fields.py @@ -0,0 +1,53 @@ +import frappe + +RENAMED_FIELDS = { + "first_name": "italy_customer_first_name", + "last_name": "italy_customer_last_name", +} + + +def execute(): + """Rename Italy's Customer name fields, which clash with the standard quick-entry + first_name/last_name fields, and restore any Italy custom field columns that a + previously interrupted fixture run left missing.""" + if not has_italy_fixtures(): + return + + duplicate_fieldnames = [ + fieldname for fieldname in RENAMED_FIELDS if frappe.db.exists("Custom Field", f"Customer-{fieldname}") + ] + + from erpnext.regional.italy.setup import get_custom_fields, make_custom_fields + + make_custom_fields() + for doctype in get_custom_fields(): + frappe.clear_cache(doctype=doctype) + frappe.db.updatedb(doctype) + + for old_fieldname, new_fieldname in RENAMED_FIELDS.items(): + copy_customer_names(old_fieldname, new_fieldname) + + for old_fieldname in duplicate_fieldnames: + frappe.delete_doc("Custom Field", f"Customer-{old_fieldname}", force=True) + + if duplicate_fieldnames: + frappe.clear_cache(doctype="Customer") + + +def has_italy_fixtures(): + return bool( + frappe.db.exists("Company", {"country": "Italy"}) + or frappe.db.exists("Custom Field", "Company-fiscal_regime") + ) + + +def copy_customer_names(old_fieldname, new_fieldname): + customer = frappe.qb.DocType("Customer") + old_column = customer[old_fieldname] + new_column = customer[new_fieldname] + ( + frappe.qb.update(customer) + .set(new_column, old_column) + .where(old_column.isnotnull() & (old_column != "")) + .where(new_column.isnull() | (new_column == "")) + ).run() diff --git a/erpnext/regional/italy/e-invoice.xml b/erpnext/regional/italy/e-invoice.xml index ef1e94ff27b..713e85a556e 100644 --- a/erpnext/regional/italy/e-invoice.xml +++ b/erpnext/regional/italy/e-invoice.xml @@ -99,8 +99,8 @@ {%- if doc.customer_data.customer_type == "Individual" %} {{ doc.customer_data.fiscal_code }} - {{ doc.customer_data.first_name }} - {{ doc.customer_data.last_name }} + {{ doc.customer_data.italy_customer_first_name }} + {{ doc.customer_data.italy_customer_last_name }} {%- else %} diff --git a/erpnext/regional/italy/setup.py b/erpnext/regional/italy/setup.py index 9f9115ca12d..a21be948650 100644 --- a/erpnext/regional/italy/setup.py +++ b/erpnext/regional/italy/setup.py @@ -23,6 +23,10 @@ def setup(company=None, patch=True): def make_custom_fields(update=True): + create_custom_fields(get_custom_fields(), ignore_validate=frappe.flags.in_patch, update=update) + + +def get_custom_fields(): invoice_item_fields = [ dict( fieldname="tax_rate", @@ -96,7 +100,7 @@ def make_custom_fields(update=True): ), ] - custom_fields = { + return { "Company": [ dict( fieldname="sb_e_invoicing", @@ -232,18 +236,18 @@ def make_custom_fields(update=True): depends_on='eval:doc.customer_type=="Company"', ), dict( - fieldname="first_name", + fieldname="italy_customer_first_name", label="First Name", fieldtype="Data", - insert_after="salutation", + insert_after="customer_type", print_hide=1, depends_on='eval:doc.customer_type!="Company"', ), dict( - fieldname="last_name", + fieldname="italy_customer_last_name", label="Last Name", fieldtype="Data", - insert_after="first_name", + insert_after="italy_customer_first_name", print_hide=1, depends_on='eval:doc.customer_type!="Company"', ), @@ -461,8 +465,6 @@ def make_custom_fields(update=True): ], } - create_custom_fields(custom_fields, ignore_validate=frappe.flags.in_patch, update=update) - def setup_report(): report_name = "Electronic Invoice Register" From e657a7f19f71fb8a0f3b4e807d1b515ccb923336 Mon Sep 17 00:00:00 2001 From: nishkagosalia Date: Tue, 28 Jul 2026 17:38:32 +0530 Subject: [PATCH 043/134] fix: handling negative grand total (cherry picked from commit 136f92db042f4c75ebcead930814d0b76d668116) --- .../sales_invoice/test_sales_invoice.py | 8 ++++++++ .../purchase_order/test_purchase_order.py | 13 ++++++++++++ erpnext/controllers/accounts_controller.py | 20 ++++++++++++++++++- erpnext/controllers/status_updater.py | 5 +++-- .../doctype/sales_order/test_sales_order.py | 13 ++++++++++++ 5 files changed, 56 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index ea1bc3195fc..617604582b5 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -108,6 +108,14 @@ class TestSalesInvoice(ERPNextTestSuite): si.save() self.assertEqual(si.items[0].qty, 1) + @ERPNextTestSuite.change_settings("Selling Settings", {"allow_negative_rates_for_items": 1}) + def test_sales_invoice_negative_grand_total_still_blocked_with_setting(self): + """allow_negative_rates_for_items must not bypass the >=0 guard for a non-return + invoice, since invoices post to the GL (unlike Sales Order).""" + si = create_sales_invoice(qty=1, rate=100, do_not_save=True) + si.append("items", {"item_code": "_Test Item 2", "qty": 1, "rate": -150}) + self.assertRaises(frappe.ValidationError, si.save) + def test_timestamp_change(self): w = frappe.copy_doc(self.globalTestRecords["Sales Invoice"][0]) w.docstatus = 0 diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 2cc53d6ad95..28996602552 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -54,6 +54,19 @@ class TestPurchaseOrder(ERPNextTestSuite): po.save() self.assertEqual(po.items[1].qty, 1) + def test_purchase_order_negative_grand_total_blocked_by_default(self): + po = create_purchase_order(qty=1, rate=100, do_not_save=True) + po.append("items", {"item_code": "_Test Item 2", "qty": 1, "rate": -150, "schedule_date": nowdate()}) + self.assertRaises(frappe.ValidationError, po.save) + + @ERPNextTestSuite.change_settings("Buying Settings", {"allow_negative_rates_for_items": 1}) + def test_purchase_order_negative_grand_total_allowed_with_setting(self): + """A supplier change order can net to a negative grand total (credit owed).""" + po = create_purchase_order(qty=1, rate=100, do_not_save=True) + po.append("items", {"item_code": "_Test Item 2", "qty": 1, "rate": -150, "schedule_date": nowdate()}) + po.save() + self.assertTrue(po.base_grand_total < 0) + def test_purchase_order_zero_qty(self): po = create_purchase_order(qty=0, do_not_save=True) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index dd4f51025f7..a3ec879bbea 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -241,6 +241,23 @@ class AccountsController(TransactionBase): ) frappe.msgprint(msg) + def is_negative_grand_total_allowed(self) -> bool: + """Return True if this document may save with a negative grand total. + + Sales Order and Purchase Order never post to the GL, so a negative + total is safe there whenever the user has explicitly opted into + negative rates via Selling/Buying Settings. Every other + AccountsController doctype (invoices, delivery notes, receipts, + quotations, ...) keeps relying on the `is_return` escape hatch only. + """ + if self.doctype == "Sales Order": + return bool(frappe.get_single_value("Selling Settings", "allow_negative_rates_for_items")) + + if self.doctype == "Purchase Order": + return bool(frappe.get_single_value("Buying Settings", "allow_negative_rates_for_items")) + + return False + def validate(self): if not self.get("is_return") and not self.get("is_debit_note"): self.validate_qty_is_not_zero() @@ -290,7 +307,8 @@ class AccountsController(TransactionBase): self.calculate_taxes_and_totals() if not self.meta.get_field("is_return") or not self.is_return: - self.validate_value("base_grand_total", ">=", 0) + if not self.is_negative_grand_total_allowed(): + self.validate_value("base_grand_total", ">=", 0) validate_return(self) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index ddea50fb7ff..4781eee012c 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -292,10 +292,11 @@ class StatusUpdater(Document): frappe.throw(_("For an item {0}, quantity must be negative number").format(d.item_code)) if ( - not selling_negative_rate_allowed and self.doctype in ["Sales Invoice", "Delivery Note"] + not selling_negative_rate_allowed + and self.doctype in ["Sales Order", "Sales Invoice", "Delivery Note"] ) or ( not buying_negative_rate_allowed - and self.doctype in ["Purchase Invoice", "Purchase Receipt"] + and self.doctype in ["Purchase Order", "Purchase Invoice", "Purchase Receipt"] ): if hasattr(d, "item_code") and hasattr(d, "rate") and flt(d.rate) < 0: frappe.throw( diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 57d6fde087b..cff7fe23ee2 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -157,6 +157,19 @@ class TestSalesOrder(ERPNextTestSuite): ) update_child_qty_rate("Sales Order", trans_item, so.name) + def test_sales_order_negative_grand_total_blocked_by_default(self): + so = make_sales_order(qty=1, rate=100, do_not_save=True) + so.append("items", {"item_code": "_Test Item 2", "qty": 1, "rate": -150}) + self.assertRaises(frappe.ValidationError, so.save) + + @ERPNextTestSuite.change_settings("Selling Settings", {"allow_negative_rates_for_items": 1}) + def test_sales_order_negative_grand_total_allowed_with_setting(self): + """A subscription downgrade / change order can net to a negative grand total.""" + so = make_sales_order(qty=1, rate=100, do_not_save=True) + so.append("items", {"item_code": "_Test Item 2", "qty": 1, "rate": -150}) + so.save() + self.assertTrue(so.base_grand_total < 0) + @ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": 1}) def test_sales_order_qty(self): so = make_sales_order(qty=1, do_not_save=True) From 51aecec598e7c850ec20e7277f77a4e3ef132e9d Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 9 Aug 2026 16:29:28 +0530 Subject: [PATCH 044/134] fix(controllers): correct negative rate settings link (cherry picked from commit 4089f138f21576f0c4553547ce0c647ef9a270b2) --- .../purchase_order/test_purchase_order.py | 13 +++++++++-- erpnext/controllers/status_updater.py | 13 +++++------ .../doctype/sales_order/test_sales_order.py | 23 +++++++++++++++++-- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 28996602552..409e02f9eda 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -54,19 +54,28 @@ class TestPurchaseOrder(ERPNextTestSuite): po.save() self.assertEqual(po.items[1].qty, 1) - def test_purchase_order_negative_grand_total_blocked_by_default(self): + @ERPNextTestSuite.change_settings("Buying Settings", {"allow_negative_rates_for_items": 0}) + def test_purchase_order_negative_grand_total_blocked_without_setting(self): po = create_purchase_order(qty=1, rate=100, do_not_save=True) po.append("items", {"item_code": "_Test Item 2", "qty": 1, "rate": -150, "schedule_date": nowdate()}) self.assertRaises(frappe.ValidationError, po.save) @ERPNextTestSuite.change_settings("Buying Settings", {"allow_negative_rates_for_items": 1}) def test_purchase_order_negative_grand_total_allowed_with_setting(self): - """A supplier change order can net to a negative grand total (credit owed).""" + """Use a negative rate to represent a credit while order quantities remain positive.""" po = create_purchase_order(qty=1, rate=100, do_not_save=True) po.append("items", {"item_code": "_Test Item 2", "qty": 1, "rate": -150, "schedule_date": nowdate()}) po.save() + po.submit() + self.assertEqual(po.docstatus, 1) self.assertTrue(po.base_grand_total < 0) + @ERPNextTestSuite.change_settings("Buying Settings", {"allow_negative_rates_for_items": 1}) + def test_purchase_order_negative_rate_setting_does_not_allow_negative_quantity(self): + po = create_purchase_order(qty=1, rate=100, do_not_save=True) + po.append("items", {"item_code": "_Test Item 2", "qty": -1, "rate": 100}) + self.assertRaises(frappe.ValidationError, po.save) + def test_purchase_order_zero_qty(self): po = create_purchase_order(qty=0, do_not_save=True) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 4781eee012c..786bf5ccd60 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -264,6 +264,9 @@ class StatusUpdater(Document): def validate_qty(self): """Validates qty at row level""" + selling_doctypes = ("Sales Order", "Sales Invoice", "Delivery Note") + buying_doctypes = ("Purchase Order", "Purchase Invoice", "Purchase Receipt") + for args in self.status_updater: if "target_ref_field" not in args or args.get("validate_qty") is False: # if target_ref_field is not specified or validate_qty is explicitly set to False, skip validation @@ -291,12 +294,8 @@ class StatusUpdater(Document): 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 ( - not selling_negative_rate_allowed - and self.doctype in ["Sales Order", "Sales Invoice", "Delivery Note"] - ) or ( - not buying_negative_rate_allowed - and self.doctype in ["Purchase Order", "Purchase Invoice", "Purchase Receipt"] + if (not selling_negative_rate_allowed and self.doctype in selling_doctypes) or ( + not buying_negative_rate_allowed and self.doctype in buying_doctypes ): if hasattr(d, "item_code") and hasattr(d, "rate") and flt(d.rate) < 0: frappe.throw( @@ -307,7 +306,7 @@ class StatusUpdater(Document): frappe.bold(_("`Allow Negative rates for Items`")), get_link_to_form( "Selling Settings" - if self.doctype in ["Sales Invoice", "Delivery Note"] + if self.doctype in selling_doctypes else "Buying Settings" ), ), diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index cff7fe23ee2..c13e899c1c4 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -157,19 +157,38 @@ class TestSalesOrder(ERPNextTestSuite): ) update_child_qty_rate("Sales Order", trans_item, so.name) - def test_sales_order_negative_grand_total_blocked_by_default(self): + @ERPNextTestSuite.change_settings("Selling Settings", {"allow_negative_rates_for_items": 0}) + def test_sales_order_negative_grand_total_blocked_without_setting(self): so = make_sales_order(qty=1, rate=100, do_not_save=True) so.append("items", {"item_code": "_Test Item 2", "qty": 1, "rate": -150}) self.assertRaises(frappe.ValidationError, so.save) @ERPNextTestSuite.change_settings("Selling Settings", {"allow_negative_rates_for_items": 1}) def test_sales_order_negative_grand_total_allowed_with_setting(self): - """A subscription downgrade / change order can net to a negative grand total.""" + """Use a negative rate to represent a credit while order quantities remain positive.""" so = make_sales_order(qty=1, rate=100, do_not_save=True) so.append("items", {"item_code": "_Test Item 2", "qty": 1, "rate": -150}) so.save() + so.submit() + self.assertEqual(so.docstatus, 1) self.assertTrue(so.base_grand_total < 0) + @ERPNextTestSuite.change_settings("Selling Settings", {"allow_negative_rates_for_items": 0}) + def test_sales_order_negative_rate_error_links_to_selling_settings(self): + so = make_sales_order(qty=1, rate=100, do_not_save=True) + so.append("items", {"item_code": "_Test Item 2", "qty": 1, "rate": -10}) + so.save() + + with self.assertRaises(frappe.ValidationError) as error: + so.submit() + + self.assertIn("selling-settings", str(error.exception)) + + @ERPNextTestSuite.change_settings("Selling Settings", {"allow_negative_rates_for_items": 1}) + def test_sales_order_negative_rate_setting_does_not_allow_negative_quantity(self): + so = make_sales_order(qty=-1, rate=100, do_not_save=True) + self.assertRaises(frappe.NonNegativeError, so.save) + @ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": 1}) def test_sales_order_qty(self): so = make_sales_order(qty=1, do_not_save=True) From 9820bb66fe051481e7872c15b253975fc3babffc Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 9 Aug 2026 16:48:48 +0530 Subject: [PATCH 045/134] chore: resolve conflict --- erpnext/patches.txt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index fa3826dbb61..1d893d4d8ae 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -496,9 +496,4 @@ erpnext.patches.v16_0.rename_ar_ap_ageing_filter erpnext.patches.v16_0.fix_subcontracting_titles erpnext.patches.v16_0.backfill_repost_accounting_ledger_status erpnext.patches.v16_0.merge_seeded_item_group_root -<<<<<<< HEAD -======= -erpnext.patches.v16_0.set_stock_uom_in_job_card -erpnext.patches.v16_0.set_work_order_requested_and_picked_qty erpnext.patches.v16_0.rename_italy_customer_name_fields ->>>>>>> 110d0a38a6 (fix(regional): rename Italy's duplicate Customer name fields) From 03eeb839fb295570680a76670e7d394060ab89f6 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 14:39:42 +0530 Subject: [PATCH 046/134] fix: require FG / Semi FG Item on operations when tracking semi finished goods A BOM with track_semi_finished_goods enabled could be saved with no finished_good on any operation: validate_semi_finished_goods only checked that one row had 'Is Final Finished Good' set, and a list containing None passed the emptiness check. Such a BOM breaks every downstream step. The work order copies the empty finished_good into its operations, job cards inherit it, and Make Stock Entry finally fails with 'Item None not found' because the manufacture entry has no production item. Derive the finished good where it is unambiguous: an operation that references a BOM produces that BOM's item, and the final operation produces the BOM's own item. Otherwise require it on the row, since each operation's job card books its output through it. (cherry picked from commit 3497a6a6bf87d5ee2c80ef20a7b7750a06b658fe) --- erpnext/manufacturing/doctype/bom/bom.py | 13 +++++++++++++ .../doctype/bom_operation/bom_operation.json | 3 ++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index f744a38d9f0..8d607964e91 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -313,6 +313,19 @@ class BOM(WebsiteGenerator): fg_items = [] for row in self.operations: + if row.bom_no and not row.finished_good: + row.finished_good = frappe.get_cached_value("BOM", row.bom_no, "item") + + if row.is_final_finished_good and not row.finished_good: + row.finished_good = self.item + + if not row.finished_good: + frappe.throw( + _( + "Row #{0}: FG / Semi FG Item is required for the operation {1} as 'Track Semi Finished Goods' is enabled." + ).format(row.idx, bold(row.operation)), + ) + if not row.is_final_finished_good: continue diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json index 86fcd7082fd..e6ac3ee474e 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -213,6 +213,7 @@ "fieldtype": "Link", "in_list_view": 1, "label": "FG / Semi FG Item", + "mandatory_depends_on": "eval:parent.track_semi_finished_goods === 1", "options": "Item" }, { @@ -307,7 +308,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-05-25 17:15:42.044630", + "modified": "2026-08-08 12:00:00.000000", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Operation", From c23b16751ed6eb6fd9682fd1bd1964ad931da6e6 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 14:41:12 +0530 Subject: [PATCH 047/134] test: BOM tracking semi finished goods rejects operations without FG item (cherry picked from commit aed7c70b1c78a6240b4acb8515d0d5146363d53e) --- erpnext/manufacturing/doctype/bom/test_bom.py | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index f40f6bc499e..0c8359264d8 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -811,6 +811,53 @@ class TestBOM(ERPNextTestSuite): for row in bom.items: self.assertEqual(row.stock_uom, "Kg") + @timeout + def test_track_semi_finished_goods_requires_finished_good_on_operations(self): + from erpnext.manufacturing.doctype.operation.test_operation import make_operation + from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation + + fg_item = make_item(properties={"is_stock_item": 1}).name + sfg_item = make_item(properties={"is_stock_item": 1}).name + rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100.0}).name + make_workstation({"workstation": "_Test SFG Workstation"}) + for operation in ("_Test SFG Operation", "_Test SFG Final Operation"): + make_operation({"operation": operation, "workstation": "_Test SFG Workstation"}) + + bom = frappe.new_doc("BOM") + bom.company = "_Test Company" + bom.item = fg_item + bom.quantity = 1 + bom.with_operations = 1 + bom.track_semi_finished_goods = 1 + bom.append( + "operations", + { + "operation": "_Test SFG Operation", + "workstation": "_Test SFG Workstation", + "time_in_mins": 30, + }, + ) + bom.append( + "operations", + { + "operation": "_Test SFG Final Operation", + "workstation": "_Test SFG Workstation", + "time_in_mins": 30, + "is_final_finished_good": 1, + }, + ) + bom.append("items", {"item_code": rm_item, "qty": 1, "operation_row_id": 1}) + bom.append("items", {"item_code": sfg_item, "qty": 1, "operation_row_id": 2}) + + # the first operation produces nothing derivable: no FG item, no BOM to take it from + self.assertRaises(frappe.ValidationError, bom.insert) + + bom.operations[0].finished_good = sfg_item + bom.insert() + + # the final operation's FG item is derived from the BOM's own item + self.assertEqual(bom.operations[1].finished_good, fg_item) + def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) From 99d9d845bd6a1bc332dfa1ca5866087f3268cf1f Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 14:41:34 +0530 Subject: [PATCH 048/134] fix: don't demand raw material transfer for semi FG job cards on submit validate_transfer_qty uses an empty finished_good to detect legacy job cards, and unlike validate_semi_finished_goods it ignores skip_material_transfer. A job card tracking semi finished goods whose operation had no finished_good fell into the legacy branch and could not be submitted even with 'Skip Material Transfer' checked on the work order. Return early for semi FG job cards; validate_semi_finished_goods already enforces the transfer requirement for them and honours skip_material_transfer. (cherry picked from commit 6c8f0b9b56778349f02ad14b21c1de8ad557f12a) --- erpnext/manufacturing/doctype/job_card/job_card.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 3b4f8008f08..accde70fa97 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -845,6 +845,9 @@ class JobCard(Document): ) def validate_transfer_qty(self): + if self.track_semi_finished_goods: + return + if ( not self.finished_good and not self.is_corrective_job_card From 87e725e43b5c347f5ce1e1f9587f5b27b7bc0bd4 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 14:42:11 +0530 Subject: [PATCH 049/134] test: semi FG job card is exempt from the legacy transfer qty check (cherry picked from commit 4b3904c6d7958469614ff5eede5648a4815563c7) # Conflicts: # erpnext/manufacturing/doctype/job_card/test_job_card.py --- .../doctype/job_card/test_job_card.py | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 826d558e830..815f2056c41 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -1879,3 +1879,113 @@ def create_semi_fg_bom(semi_fg_item, raw_item, inspection_required): bom.append("items", {"item_code": raw_item, "qty": 1}) bom.submit() return bom.name +<<<<<<< HEAD +======= + + +class TestJobCardLogic(ERPNextTestSuite): + """Field-level validations and pure quantity/capacity helpers, exercised on the + document directly so they don't need a Work Order / BOM (the integration suite does).""" + + def test_processing_a_submitted_or_cancelled_card_is_blocked(self): + submitted = frappe.new_doc("Job Card") + submitted.docstatus = 1 + self.assertRaises(frappe.ValidationError, submitted.validate_docstatus) + + cancelled = frappe.new_doc("Job Card") + cancelled.docstatus = 2 + self.assertRaises(frappe.ValidationError, cancelled.validate_docstatus) + + def test_complete_job_card_qty_guards(self): + jc = frappe.new_doc("Job Card") + jc.for_quantity = 5 + jc.validate_complete_job_card_qty(frappe._dict(pending_qty=3)) # within range -> passes + self.assertRaises( + frappe.ValidationError, jc.validate_complete_job_card_qty, frappe._dict(pending_qty=-1) + ) + self.assertRaises( + frappe.ValidationError, jc.validate_complete_job_card_qty, frappe._dict(process_loss_qty=-1) + ) + self.assertRaises( + frappe.ValidationError, jc.validate_complete_job_card_qty, frappe._dict(pending_qty=10) + ) + + def test_qty_in_messages_carries_the_uom(self): + jc = frappe.new_doc("Job Card") + jc.stock_uom = "Nos" + + self.assertEqual(jc.get_qty_with_uom(5), "5.0 Nos") + self.assertEqual(jc.get_qty_with_uom(0), "0.0 Nos") + + def test_completion_qty_split_must_add_up(self): + jc = frappe.new_doc("Job Card") + jc.for_quantity = 5 + + # 3 completed + 2 pending + 0 lost == 5 to manufacture -> passes + jc.validate_complete_job_card_qty( + frappe._dict(for_quantity=5, qty=3, pending_qty=2, process_loss_qty=0) + ) + + self.assertRaises( + frappe.ValidationError, + jc.validate_complete_job_card_qty, + frappe._dict(for_quantity=3, qty=3, pending_qty=2, process_loss_qty=0), + ) + + def test_completed_qty_must_reconcile_with_for_quantity(self): + jc = frappe.new_doc("Job Card") + jc.for_quantity = 10 + jc.total_completed_qty = 6 + jc.process_loss_qty = 0 + jc.pending_qty = 0 + # 6 + 0 + 0 != 10 -> throws + self.assertRaises(frappe.ValidationError, jc.validate_completed_qty_matches_for_quantity) + # completed + loss + pending == for_quantity -> passes + jc.pending_qty = 4 + jc.validate_completed_qty_matches_for_quantity() + + def test_set_process_loss(self): + jc = frappe.new_doc("Job Card") + jc.for_quantity = 10 + jc.total_completed_qty = 6 + jc.pending_qty = 1 + jc.set_process_loss() + self.assertEqual(jc.process_loss_qty, 3) # 10 - 6 - 1 + + # no loss when nothing completed yet + nothing_done = frappe.new_doc("Job Card") + nothing_done.for_quantity = 10 + nothing_done.total_completed_qty = 0 + nothing_done.set_process_loss() + self.assertEqual(nothing_done.process_loss_qty, 0) + + def test_capacity_overlap_detection(self): + jc = frappe.new_doc("Job Card") + sequential = [ + {"from_time": "2026-01-01 10:00:00", "to_time": "2026-01-01 11:00:00"}, + {"from_time": "2026-01-01 11:00:00", "to_time": "2026-01-01 12:00:00"}, + ] + overlapping = [ + {"from_time": "2026-01-01 10:00:00", "to_time": "2026-01-01 11:00:00"}, + {"from_time": "2026-01-01 10:30:00", "to_time": "2026-01-01 11:30:00"}, + ] + # sequential logs share one capacity slot; overlapping logs need two + self.assertEqual(len(jc.get_alloted_capacity(sequential)), 1) + self.assertEqual(len(jc.get_alloted_capacity(overlapping)), 2) + # capacity 1 overlaps with any log; capacity 2 only when both slots are taken + self.assertTrue(jc.has_overlap(1, sequential)) + self.assertFalse(jc.has_overlap(2, sequential)) + self.assertTrue(jc.has_overlap(2, overlapping)) + + def test_semi_fg_job_card_is_exempt_from_transfer_qty_check(self): + jc = frappe.new_doc("Job Card") + jc.track_semi_finished_goods = 1 + jc.for_quantity = 10 + jc.transferred_qty = 0 + jc.append("items", {"item_code": "_Test Item"}) + + jc.validate_transfer_qty() + + jc.track_semi_finished_goods = 0 + self.assertRaises(frappe.ValidationError, jc.validate_transfer_qty) +>>>>>>> 4b3904c6d7 (test: semi FG job card is exempt from the legacy transfer qty check) From 24cd5f22b53d7903810649e208bdfe9869eefeee Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 14:43:22 +0530 Subject: [PATCH 050/134] fix: require WIP warehouse for work orders tracking semi finished goods Work orders with track_semi_finished_goods were exempt from the Work-in-Progress Warehouse requirement in three places: the field's mandatory_depends_on, the fg_warehouse reqd toggle in the form script, and validate_warehouse on submit. The exemption was misleading. The flow still transfers materials to a WIP warehouse when 'Skip Material Transfer' is unchecked: operations default their WIP warehouse from the work order, and set_default_warehouse silently restores the company default after the user clears the field. Make the field genuinely required instead of pretending it is optional. (cherry picked from commit 198eb60df7875d0e4ed300c259ddd1a866c8d418) --- erpnext/manufacturing/doctype/work_order/work_order.js | 3 +-- erpnext/manufacturing/doctype/work_order/work_order.json | 4 ++-- erpnext/manufacturing/doctype/work_order/work_order.py | 3 --- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 04f259f1508..9992c2466fd 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -281,8 +281,7 @@ frappe.ui.form.on("Work Order", { }, set_fg_warehouse_mandatory(frm) { - let mandatory = frm.doc.skip_transfer === 1 || frm.doc.track_semi_finished_goods === 1 ? false : true; - frm.toggle_reqd("fg_warehouse", mandatory); + frm.toggle_reqd("fg_warehouse", frm.doc.skip_transfer !== 1); }, add_custom_button_to_return_components: function (frm) { diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 04b970be3e1..cfe140726df 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -272,7 +272,7 @@ "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", + "mandatory_depends_on": "eval:!doc.skip_transfer || doc.from_wip_warehouse", "options": "Warehouse" }, { @@ -739,7 +739,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2026-06-03 21:35:34.175667", + "modified": "2026-08-08 12:00:00.000000", "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 eecf06b15c4..a1fd433ad22 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -915,9 +915,6 @@ class WorkOrder(Document): production_plan.run_method("update_produced_pending_qty", produced_qty, self.production_plan_item) def validate_warehouse(self): - if self.track_semi_finished_goods: - return - if not self.wip_warehouse and not self.skip_transfer: frappe.throw(_("Work-in-Progress Warehouse is required before Submit")) if not self.fg_warehouse: From e822efe6a11c8af4d854896db4f5b6aea9c5334a Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 14:44:02 +0530 Subject: [PATCH 051/134] test: WIP warehouse required for work orders tracking semi finished goods (cherry picked from commit f61f6523b963ebed361b7f0d66d6514af0069ce1) # Conflicts: # erpnext/manufacturing/doctype/work_order/test_work_order.py --- .../doctype/work_order/test_work_order.py | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index fb2e49ed571..d58bb078d2a 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -4839,6 +4839,71 @@ class TestWorkOrder(ERPNextTestSuite): # generated qty (3.0 for 8 units) differs from the BOM-scaled qty (7.5 for 20 units) self.assertEqual(flt(row.qty, 6), 3.0) +<<<<<<< HEAD +======= + def test_transferred_qty_not_misattributed_between_item_and_its_substitute(self): + """When one item is transferred both for itself and as a substitute for another required item, + each transfer must be credited to the right required item. + + _material_transfer_qty_by_item grouped Stock Entry Detail by item_code only and picked + Max(original_item); for item B transferred once for itself (original_item NULL) and once as a + substitute for A (original_item=A), Max picked A and credited B's whole transfer to A, leaving + B at 0. Grouping by (item_code, original_item) and accumulating into the keyed dict attributes + each transfer correctly, deterministically on MariaDB and Postgres. + """ + from erpnext.manufacturing.doctype.work_order.services.required_items import RequiredItemsService + + source_warehouse = "Stores - _TC" + fg_item = make_item("Test WO SelfSub FG", {"is_stock_item": 1}).name + item_a = make_item("Test WO SelfSub RM A", {"is_stock_item": 1, "allow_alternative_item": 1}).name + item_b = make_item("Test WO SelfSub RM B", {"is_stock_item": 1, "allow_alternative_item": 1}).name + + # B is a registered alternative for A + if not frappe.db.exists("Item Alternative", {"item_code": item_a, "alternative_item_code": item_b}): + frappe.get_doc( + { + "doctype": "Item Alternative", + "item_code": item_a, + "alternative_item_code": item_b, + "two_way": 1, + } + ).insert() + + # stock B generously (covers B-for-A plus B-for-itself) + for item, qty in ((item_a, 50), (item_b, 100)): + test_stock_entry.make_stock_entry( + item_code=item, target=source_warehouse, qty=qty, basic_rate=100 + ) + + make_bom(item=fg_item, source_warehouse=source_warehouse, raw_materials=[item_a, item_b]) + wo = make_wo_order_test_record(item=fg_item, qty=10, source_warehouse=source_warehouse) + + transfer = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 10)) + transfer.save() + # substitute B for the A line; the existing B line stays as B's own transfer + for d in transfer.items: + if d.item_code == item_a: + d.item_code = item_b + d.original_item = item_a + transfer.submit() + + qty_by_item = RequiredItemsService(wo)._material_transfer_qty_by_item(is_return=0) + # B transferred as a substitute for A -> credited to A; B transferred for itself -> credited to B. + self.assertEqual(flt(qty_by_item.get(item_a)), 10.0) + self.assertEqual(flt(qty_by_item.get(item_b)), 10.0) + + def test_wip_warehouse_required_when_tracking_semi_finished_goods(self): + wo = frappe.new_doc("Work Order") + wo.track_semi_finished_goods = 1 + wo.skip_transfer = 0 + wo.fg_warehouse = "_Test Warehouse 1 - _TC" + + self.assertRaises(frappe.ValidationError, wo.validate_warehouse) + + wo.wip_warehouse = "_Test Warehouse - _TC" + wo.validate_warehouse() + +>>>>>>> f61f6523b9 (test: WIP warehouse required for work orders tracking semi finished goods) def get_reserved_entries(voucher_no, warehouse=None): doctype = frappe.qb.DocType("Stock Reservation Entry") From 104c8df7656cac89834e673192d4b67f82754462 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 14:45:08 +0530 Subject: [PATCH 052/134] fix: stop asking for a manufacturing entry when process loss explains the shortfall MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a previous operation manufactured less than the current job card is completing, the error always said 'Submit the manufacturing entry for the operation first' — even when the entry was already submitted and the missing quantity was booked as process loss, which made the advice a dead end. Sum the process loss of the previous operation's job cards alongside the manufactured quantity. When manufactured + process loss covers the requested quantity, say the shortfall is process loss so the user knows to reduce the completed quantity; keep the submit-first message for genuinely pending manufacturing entries. (cherry picked from commit 1e22695eaef905ad141cb0e3f57fc7513e81c1ce) # Conflicts: # erpnext/manufacturing/doctype/job_card/job_card.py --- .../doctype/job_card/job_card.py | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index accde70fa97..61577623236 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -1342,6 +1342,57 @@ class JobCard(Document): if not (self.work_order and self.sequence_id): return +<<<<<<< HEAD +======= + current_operation_qty = self.get_current_operation_completed_qty() + + for row in self.get_previous_operations(): + if self.track_semi_finished_goods: + self.validate_previous_operation_manufactured_qty(row, current_operation_qty) + else: + self.validate_previous_operation(row, current_operation_qty) + + def get_previous_operations(self): + previous_operations = frappe.get_all( + "Work Order Operation", + fields=["name", "operation", "status", "completed_qty", "sequence_id", "finished_good"], + filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ("<", self.sequence_id)}, + order_by="sequence_id, idx", + ) + + if self.track_semi_finished_goods and previous_operations: + totals = self.get_manufactured_qty_per_operation([row.name for row in previous_operations]) + + for row in previous_operations: + operation_totals = totals.get(row.name) + row.manufactured_qty = flt(operation_totals and operation_totals.manufactured_qty) + row.process_loss_qty = flt(operation_totals and operation_totals.process_loss_qty) + + return previous_operations + + def get_manufactured_qty_per_operation(self, operation_ids): + job_card = frappe.qb.DocType("Job Card") + + data = ( + frappe.qb.from_(job_card) + .select( + job_card.operation_id, + Sum(job_card.manufactured_qty).as_("manufactured_qty"), + Sum(job_card.process_loss_qty).as_("process_loss_qty"), + ) + .where( + (job_card.work_order == self.work_order) + & (job_card.docstatus == 1) + & (IfNull(job_card.is_corrective_job_card, 0) == 0) + & (job_card.operation_id.isin(operation_ids)) + ) + .groupby(job_card.operation_id) + ).run(as_dict=True) + + return {row.operation_id: row for row in data} + + def get_current_operation_completed_qty(self): +>>>>>>> 1e22695eae (fix: stop asking for a manufacturing entry when process loss explains the shortfall) current_operation_qty = 0.0 data = self.get_current_operation_data() if data and len(data) > 0: @@ -1377,6 +1428,7 @@ class JobCard(Document): OperationSequenceError, ) +<<<<<<< HEAD if row.completed_qty < current_operation_qty: frappe.throw( _( @@ -1388,6 +1440,49 @@ class JobCard(Document): bold(row.operation), ) ) +======= + if not manufactured_qty: + frappe.throw( + _( + "Job Card {0}: As per the sequence of the operations in the work order {1}, submit the manufacturing entry for the operation {2} before the operation {3}." + ).format( + bold(self.name), + bold(get_link_to_form("Work Order", self.work_order)), + bold(row.operation), + bold(self.operation), + ), + OperationSequenceError, + ) + + if manufactured_qty >= current_operation_qty: + return + + if manufactured_qty + flt(row.process_loss_qty) >= current_operation_qty: + frappe.throw( + _( + "The completed quantity {0} of an operation {1} cannot be greater than the manufactured quantity {2} of a previous operation {3}, as {4} was booked as process loss there." + ).format( + bold(self.get_qty_with_uom(current_operation_qty)), + bold(self.operation), + bold(self.get_qty_with_uom(manufactured_qty, row.finished_good)), + bold(row.operation), + bold(self.get_qty_with_uom(flt(row.process_loss_qty), row.finished_good)), + ), + OperationSequenceError, + ) +>>>>>>> 1e22695eae (fix: stop asking for a manufacturing entry when process loss explains the shortfall) + + frappe.throw( + _( + "The completed quantity {0} of an operation {1} cannot be greater than the manufactured quantity {2} of a previous operation {3}. Submit the manufacturing entry for the operation {3} first." + ).format( + bold(self.get_qty_with_uom(current_operation_qty)), + bold(self.operation), + bold(self.get_qty_with_uom(manufactured_qty, row.finished_good)), + bold(row.operation), + ), + OperationSequenceError, + ) def validate_work_order(self): if self.is_work_order_closed(): From f58c0adbf5ec2f5c3e3a3d3d9cd22c0a332697f6 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 14:45:43 +0530 Subject: [PATCH 053/134] test: previous operation shortfall from process loss gets the right message (cherry picked from commit 335dbdaca40e2c9639a94648c02feaf7dfd000bb) --- .../doctype/job_card/test_job_card.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 815f2056c41..b5a7a5c6099 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -1977,6 +1977,25 @@ class TestJobCardLogic(ERPNextTestSuite): self.assertFalse(jc.has_overlap(2, sequential)) self.assertTrue(jc.has_overlap(2, overlapping)) + def test_previous_operation_shortfall_from_process_loss_gets_the_right_message(self): + jc = frappe.new_doc("Job Card") + jc.operation = "_Test Painting" + jc.stock_uom = "Nos" + row = frappe._dict( + operation="_Test Assembly", manufactured_qty=8, process_loss_qty=2, finished_good=None + ) + + with self.assertRaises(OperationSequenceError) as loss_error: + jc.validate_previous_operation_manufactured_qty(row, 10) + self.assertIn("process loss", str(loss_error.exception)) + + row.process_loss_qty = 0 + with self.assertRaises(OperationSequenceError) as pending_error: + jc.validate_previous_operation_manufactured_qty(row, 10) + self.assertIn("Submit the manufacturing entry", str(pending_error.exception)) + + jc.validate_previous_operation_manufactured_qty(row, 8) + def test_semi_fg_job_card_is_exempt_from_transfer_qty_check(self): jc = frappe.new_doc("Job Card") jc.track_semi_finished_goods = 1 From a0b370b2e949548d4e215e80f40e027052711121 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 15:20:54 +0530 Subject: [PATCH 054/134] fix: scope manufacture entry process loss to its own job card set_process_loss_qty stamped MAX(process_loss_qty) across every operation of the work order onto each manufacture entry. With semi finished goods tracking, one operation's process loss leaked into the entries of every other operation: validate_fg_completed_qty then rejected the entry when it had a BOM, or the wrong loss was recorded silently when it did not, double-counting the loss across operations. When the entry belongs to a job card, use that job card's loss net of what its earlier entries already booked. The MAX fallback stays for work-order level entries without a job card. Fixes frappe/erpnext#57892 (cherry picked from commit 1b335973b7c58c623693d7eec68aafca5581de32) # Conflicts: # erpnext/stock/doctype/stock_entry/stock_entry.py --- .../stock/doctype/stock_entry/stock_entry.py | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 6f8a4644a56..258963ed9c5 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -3192,13 +3192,16 @@ class StockEntry(StockController, SubcontractingInwardController): return precision = self.precision("process_loss_qty") - if self.work_order: - data = frappe.get_all( - "Work Order Operation", - filters={"parent": self.work_order}, - fields=[{"MAX": "process_loss_qty", "as": "process_loss_qty"}], + process_loss_qty = self.get_pending_process_loss_qty() + if process_loss_qty and flt(self.process_loss_qty, precision) != flt(process_loss_qty, precision): + self.process_loss_qty = flt(process_loss_qty, precision) + + frappe.msgprint( + _("The Process Loss Qty has been reset as per the job card's Process Loss Qty"), + alert=True, ) +<<<<<<< HEAD if data and data[0].process_loss_qty: process_loss_qty = data[0].process_loss_qty if flt(self.process_loss_qty, precision) != flt(process_loss_qty, precision): @@ -3208,6 +3211,8 @@ class StockEntry(StockController, SubcontractingInwardController): _("The Process Loss Qty has reset as per job cards Process Loss Qty"), alert=True ) +======= +>>>>>>> 1b335973b7 (fix: scope manufacture entry process loss to its own job card) if not self.process_loss_percentage and not self.process_loss_qty: self.process_loss_percentage = frappe.get_cached_value( "BOM", self.bom_no, "process_loss_percentage" @@ -3222,6 +3227,23 @@ class StockEntry(StockController, SubcontractingInwardController): (flt(self.process_loss_qty) / flt(self.fg_completed_qty)) * 100 ) + def get_pending_process_loss_qty(self): + """Loss this entry should still book: the job card's unbooked loss when the entry + belongs to one, else the largest operation loss on the work order (legacy flow).""" + if self.job_card: + job_card = frappe.get_doc("Job Card", self.job_card) + return max(flt(job_card.process_loss_qty) - flt(job_card.get_consumed_process_loss()), 0) + + if self.work_order: + data = frappe.get_all( + "Work Order Operation", + filters={"parent": self.work_order}, + fields=[{"MAX": "process_loss_qty", "as": "process_loss_qty"}], + ) + return flt(data[0].process_loss_qty) if data else 0 + + return 0 + def set_work_order_details(self): if not getattr(self, "pro_doc", None): self.pro_doc = frappe._dict() From 0de97159ea06ac4ef61ecd48e1616ea6794c9711 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 15:23:28 +0530 Subject: [PATCH 055/134] test: manufacture entry keeps process loss scoped to its own operation (cherry picked from commit 5e0f056284cd670438a35cb6c5a1cb34e2c32a08) --- .../doctype/job_card/test_job_card.py | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index b5a7a5c6099..f7834c82cfe 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -1402,6 +1402,113 @@ class TestJobCard(ERPNextTestSuite): consumed_batches = get_batches_from_bundle(sfg_consume_row.serial_and_batch_bundle) self.assertEqual(set(consumed_batches.keys()), set(produced_batches.keys())) + def test_manufacture_entry_process_loss_not_taken_from_previous_operation(self): + from erpnext.manufacturing.doctype.operation.test_operation import make_operation + from erpnext.stock.doctype.item.test_item import make_item + + warehouse = "Stores - _TC" + rm1 = make_item("PL Scope RM 1", {"is_stock_item": 1}).name + rm2 = make_item("PL Scope RM 2", {"is_stock_item": 1}).name + sfg = make_item("PL Scope SFG 1", {"is_stock_item": 1}).name + fg1 = make_item("PL Scope FG 1", {"is_stock_item": 1}).name + + sfg_bom = frappe.new_doc("BOM", company="_Test Company", item=sfg, quantity=1) + sfg_bom.append("items", {"item_code": rm1, "qty": 1}) + sfg_bom.insert() + sfg_bom.submit() + + fg_bom = frappe.new_doc( + "BOM", + company="_Test Company", + item=fg1, + quantity=1, + with_operations=1, + track_semi_finished_goods=1, + ) + operation1 = { + "operation": "PL Scope Op A", + "workstation": "_Test Workstation A", + "finished_good": sfg, + "bom_no": sfg_bom.name, + "finished_good_qty": 1, + "sequence_id": 1, + "time_in_mins": 60, + "source_warehouse": warehouse, + "fg_warehouse": warehouse, + "skip_material_transfer": 1, + } + operation2 = { + "operation": "PL Scope Op B", + "workstation": "_Test Workstation A", + "finished_good": fg1, + "finished_good_qty": 1, + "is_final_finished_good": 1, + "sequence_id": 2, + "time_in_mins": 60, + "source_warehouse": warehouse, + "fg_warehouse": warehouse, + "skip_material_transfer": 1, + } + make_workstation(operation1) + make_operation(operation1) + make_operation(operation2) + fg_bom.append("operations", operation1) + fg_bom.append("operations", operation2) + fg_bom.append("items", {"item_code": rm2, "qty": 1}) + fg_bom.append("items", {"item_code": sfg, "qty": 1, "operation_row_id": 2}) + fg_bom.insert() + fg_bom.submit() + + work_order = make_wo_order_test_record( + item=fg1, + qty=5, + source_warehouse=warehouse, + fg_warehouse=warehouse, + bom_no=fg_bom.name, + skip_transfer=1, + ) + + make_stock_entry(item_code=rm1, target=warehouse, qty=10, basic_rate=100) + make_stock_entry(item_code=rm2, target=warehouse, qty=10, basic_rate=100) + make_stock_entry(item_code=sfg, target=warehouse, qty=10, basic_rate=100) + + jc_a = frappe.get_doc( + "Job Card", + frappe.db.get_value( + "Job Card", {"work_order": work_order.name, "operation": "PL Scope Op A"}, "name" + ), + ) + jc_a.append( + "time_logs", + {"from_time": "2024-01-01 08:00:00", "to_time": "2024-01-01 09:00:00", "completed_qty": 3}, + ) + jc_a.pending_qty = 0 + jc_a.process_loss_qty = 2 + jc_a.submit() + me_a = frappe.get_doc(jc_a.make_stock_entry_for_semi_fg_item()) + me_a.submit() + self.assertEqual(flt(me_a.process_loss_qty), 2.0) + + jc_b = frappe.get_doc( + "Job Card", + frappe.db.get_value( + "Job Card", {"work_order": work_order.name, "operation": "PL Scope Op B"}, "name" + ), + ) + jc_b.append( + "time_logs", + {"from_time": "2024-02-01 08:00:00", "to_time": "2024-02-01 09:00:00", "completed_qty": 3}, + ) + jc_b.pending_qty = 2 + jc_b.submit() + me_b = frappe.get_doc(jc_b.make_stock_entry_for_semi_fg_item()) + + # operation A's loss must not leak into operation B's entry + self.assertEqual(flt(me_b.process_loss_qty), 0.0) + fg_row = next(row for row in me_b.items if row.is_finished_item) + self.assertEqual(flt(fg_row.qty), 3.0) + me_b.submit() + def test_semi_fg_auto_pull_with_uom_conversion(self): from erpnext.manufacturing.doctype.operation.test_operation import make_operation from erpnext.stock.doctype.item.test_item import make_item From 35f9bec988611cb2427a9ad1b6b2b792fbacbbdc Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 15:27:39 +0530 Subject: [PATCH 056/134] fix: add raw material to its operation even when another operation uses the item MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit get_item_details returns the whole Item document, so the dialog row's name became the item code. get_item_data then matched that item code against every Components row regardless of operation, so adding an item already used by another operation silently updated that row's qty instead of appending one for the target operation — which stayed empty and failed 'please add raw materials or set a BOM' on submit. Match the existing row by item code within the same operation: same operation updates the qty, any other match appends a new row. (cherry picked from commit 24f1f3dea88d76fa70ef83a4e20b851fbfc32d7a) # Conflicts: # erpnext/manufacturing/doctype/bom/bom.py --- erpnext/manufacturing/doctype/bom/bom.py | 25 ++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 8d607964e91..54fe063fcf0 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -870,6 +870,27 @@ class BOM(WebsiteGenerator): self.save() +<<<<<<< HEAD +======= + def _add_raw_material_row(self, operation_row_id, row): + row = parse_json(row) + + row.update(get_item_details(row.get("item_code"))) + row.operation_row_id = operation_row_id + + item_row = self.get_item_data(row.item_code, operation_row_id) + + if item_row: + item_row.qty = row.get("qty") + else: + row.idx = None + row.name = None + row.do_not_explode = 1 + row.is_sub_assembly_item = self.is_sub_assembly_item(row.item_code) + + self.append("items", row) + +>>>>>>> 24f1f3dea8 (fix: add raw material to its operation even when another operation uses the item) def is_sub_assembly_item(self, item_code): if not self.operations: return False @@ -880,9 +901,9 @@ class BOM(WebsiteGenerator): return False - def get_item_data(self, name): + def get_item_data(self, item_code, operation_row_id): for row in self.items: - if row.item_code == name: + if row.item_code == item_code and cint(row.operation_row_id) == cint(operation_row_id): return row @frappe.whitelist() From b4eceeda2d96debbade43a76627db005f1b9265f Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 15:28:31 +0530 Subject: [PATCH 057/134] test: raw material dialog adds a row for its operation despite duplicates (cherry picked from commit 0aec62a8dd5481f482c6c24a4742e0c2059a17b3) --- erpnext/manufacturing/doctype/bom/test_bom.py | 60 ++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 0c8359264d8..37e73c4dbe6 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -7,7 +7,7 @@ from functools import partial import frappe from frappe.tests import timeout -from frappe.utils import cstr, flt +from frappe.utils import cint, cstr, flt from erpnext.controllers.tests.test_subcontracting_controller import ( set_backflush_based_on, @@ -858,6 +858,64 @@ class TestBOM(ERPNextTestSuite): # the final operation's FG item is derived from the BOM's own item self.assertEqual(bom.operations[1].finished_good, fg_item) + @timeout + def test_add_raw_materials_when_item_is_used_by_another_operation(self): + from erpnext.manufacturing.doctype.operation.test_operation import make_operation + from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation + + fg_item = make_item(properties={"is_stock_item": 1}).name + sfg_item = make_item(properties={"is_stock_item": 1}).name + rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100.0}).name + make_workstation({"workstation": "_Test SFG Workstation"}) + for operation in ("_Test SFG Operation", "_Test SFG Final Operation"): + make_operation({"operation": operation, "workstation": "_Test SFG Workstation"}) + + bom = frappe.new_doc("BOM") + bom.company = "_Test Company" + bom.item = fg_item + bom.quantity = 1 + bom.with_operations = 1 + bom.track_semi_finished_goods = 1 + bom.append( + "operations", + { + "operation": "_Test SFG Operation", + "workstation": "_Test SFG Workstation", + "time_in_mins": 30, + "finished_good": sfg_item, + }, + ) + bom.append( + "operations", + { + "operation": "_Test SFG Final Operation", + "workstation": "_Test SFG Workstation", + "time_in_mins": 30, + "is_final_finished_good": 1, + }, + ) + bom.append("items", {"item_code": rm_item, "qty": 1, "operation_row_id": 1}) + bom.append("items", {"item_code": sfg_item, "qty": 1, "operation_row_id": 2}) + bom.insert() + + def rows_for(item_code, operation_row_id): + return [ + row + for row in bom.items + if row.item_code == item_code and cint(row.operation_row_id) == operation_row_id + ] + + # the item already used by operation 1 gets its own new row under operation 2 + bom.add_raw_materials(2, [{"item_code": rm_item, "qty": 3}]) + self.assertEqual(len(rows_for(rm_item, 2)), 1) + self.assertEqual(flt(rows_for(rm_item, 2)[0].qty), 3.0) + self.assertEqual(flt(rows_for(rm_item, 1)[0].qty), 1.0) + + # adding it again for the same operation updates the row instead of stacking another + bom.add_raw_materials(2, [{"item_code": rm_item, "qty": 5}]) + self.assertEqual(len(rows_for(rm_item, 2)), 1) + self.assertEqual(flt(rows_for(rm_item, 2)[0].qty), 5.0) + def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) From 3e0d0b2d68372a629ffd489882024e9b35736772 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 15:58:07 +0530 Subject: [PATCH 058/134] fix: derive operation FG items before material expansion, keep the final one the BOM's item The finished_good derivation ran in validate_semi_finished_goods, after set_materials_based_on_operation_bom had already expanded operation BOM materials. A single-pass insert-and-submit (API or import) with bom_no set but finished_good empty skipped the expansion, persisting a submitted BOM without the referenced components. The derivation also let a final operation inherit another item from its bom_no, so downstream job cards would produce the wrong item. Move the derivation into set_operation_finished_goods, called before the expansion, prefer the BOM's own item for the final operation, and reject a final operation whose FG item is not the BOM's item. (cherry picked from commit 1e2e87daaca9d5df11b732a80699b1ae3b896631) # Conflicts: # erpnext/manufacturing/doctype/bom/bom.py --- erpnext/manufacturing/doctype/bom/bom.py | 29 +++++++++++++++++++----- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 54fe063fcf0..e33e81d5aaa 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -282,6 +282,7 @@ class BOM(WebsiteGenerator): self.clear_inspection() self.validate_main_item() self.validate_currency() + self.set_operation_finished_goods() self.set_materials_based_on_operation_bom() self.set_conversion_rate() self.set_plc_conversion_rate() @@ -304,8 +305,23 @@ class BOM(WebsiteGenerator): self.set_fg_cost_allocation() self.validate_total_cost_allocation() +<<<<<<< HEAD if self.docstatus == 1: self.validate_raw_materials_of_operation() +======= + def set_operation_finished_goods(self): + """Fill each operation's FG item where it is unambiguous: the final operation produces + this BOM's item, an operation with a BOM produces that BOM's item. Runs before + set_materials_based_on_operation_bom so derived rows get their materials expanded.""" + if not self.track_semi_finished_goods: + return + + for row in self.operations: + if row.is_final_finished_good and not row.finished_good: + row.finished_good = self.item + elif row.bom_no and not row.finished_good: + row.finished_good = frappe.get_cached_value("BOM", row.bom_no, "item") +>>>>>>> 1e2e87daac (fix: derive operation FG items before material expansion, keep the final one the BOM's item) def validate_semi_finished_goods(self): if not self.track_semi_finished_goods or not self.operations: @@ -313,12 +329,6 @@ class BOM(WebsiteGenerator): fg_items = [] for row in self.operations: - if row.bom_no and not row.finished_good: - row.finished_good = frappe.get_cached_value("BOM", row.bom_no, "item") - - if row.is_final_finished_good and not row.finished_good: - row.finished_good = self.item - if not row.finished_good: frappe.throw( _( @@ -329,6 +339,13 @@ class BOM(WebsiteGenerator): if not row.is_final_finished_good: continue + if row.finished_good != self.item: + frappe.throw( + _( + "Row #{0}: The operation {1} has 'Is Final Finished Good' checked, so its FG / Semi FG Item must be {2}." + ).format(row.idx, bold(row.operation), bold(self.item)), + ) + fg_items.append(row.finished_good) if not fg_items: From 605821f04c75d15c620684c04f2ca406e3290dda Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 16:01:36 +0530 Subject: [PATCH 059/134] test: operation BOM materials expand on single-pass submit, final FG must match the BOM item (cherry picked from commit 9ef386dfd2dd7d225fd0db7689825b468a2e70c1) --- erpnext/manufacturing/doctype/bom/test_bom.py | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 37e73c4dbe6..48eb41fdb11 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -916,6 +916,102 @@ class TestBOM(ERPNextTestSuite): self.assertEqual(len(rows_for(rm_item, 2)), 1) self.assertEqual(flt(rows_for(rm_item, 2)[0].qty), 5.0) + @timeout + def test_operation_bom_materials_expand_on_single_pass_submit(self): + from erpnext.manufacturing.doctype.operation.test_operation import make_operation + from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation + + fg_item = make_item(properties={"is_stock_item": 1}).name + sfg_item = make_item(properties={"is_stock_item": 1}).name + rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100.0}).name + make_workstation({"workstation": "_Test SFG Workstation"}) + for operation in ("_Test SFG Operation", "_Test SFG Final Operation"): + make_operation({"operation": operation, "workstation": "_Test SFG Workstation"}) + + sfg_bom = frappe.new_doc("BOM", company="_Test Company", item=sfg_item, quantity=1) + sfg_bom.append("items", {"item_code": rm_item, "qty": 1}) + sfg_bom.insert() + sfg_bom.submit() + + bom = frappe.new_doc("BOM") + bom.company = "_Test Company" + bom.item = fg_item + bom.quantity = 1 + bom.with_operations = 1 + bom.track_semi_finished_goods = 1 + bom.append( + "operations", + { + "operation": "_Test SFG Operation", + "workstation": "_Test SFG Workstation", + "time_in_mins": 30, + "bom_no": sfg_bom.name, + }, + ) + bom.append( + "operations", + { + "operation": "_Test SFG Final Operation", + "workstation": "_Test SFG Workstation", + "time_in_mins": 30, + "is_final_finished_good": 1, + }, + ) + bom.append("items", {"item_code": sfg_item, "qty": 1, "operation_row_id": 2}) + bom.submit() + + self.assertEqual(bom.docstatus, 1) + self.assertEqual(bom.operations[0].finished_good, sfg_item) + self.assertTrue( + any(row.item_code == rm_item and cint(row.operation_row_id) == 1 for row in bom.items) + ) + + @timeout + def test_final_operation_must_produce_the_bom_item(self): + from erpnext.manufacturing.doctype.operation.test_operation import make_operation + from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation + + fg_item = make_item(properties={"is_stock_item": 1}).name + sfg_item = make_item(properties={"is_stock_item": 1}).name + rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100.0}).name + make_workstation({"workstation": "_Test SFG Workstation"}) + for operation in ("_Test SFG Operation", "_Test SFG Final Operation"): + make_operation({"operation": operation, "workstation": "_Test SFG Workstation"}) + + bom = frappe.new_doc("BOM") + bom.company = "_Test Company" + bom.item = fg_item + bom.quantity = 1 + bom.with_operations = 1 + bom.track_semi_finished_goods = 1 + bom.append( + "operations", + { + "operation": "_Test SFG Operation", + "workstation": "_Test SFG Workstation", + "time_in_mins": 30, + "finished_good": sfg_item, + }, + ) + bom.append( + "operations", + { + "operation": "_Test SFG Final Operation", + "workstation": "_Test SFG Workstation", + "time_in_mins": 30, + "is_final_finished_good": 1, + "finished_good": sfg_item, + }, + ) + bom.append("items", {"item_code": rm_item, "qty": 1, "operation_row_id": 1}) + bom.append("items", {"item_code": sfg_item, "qty": 1, "operation_row_id": 2}) + + # the final operation claims to produce the semi FG, not this BOM's item + self.assertRaises(frappe.ValidationError, bom.insert) + + bom.operations[1].finished_good = fg_item + bom.insert() + def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) From 79ad410cc1418cddd0b851a0bb9f04f7714888b1 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 16:11:20 +0530 Subject: [PATCH 060/134] fix: cap a manufacture entry at the job card's pending production Entries from operations without their own BOM carry no For Quantity, so the finished-good reconciliation cannot run for them and a draft created before other entries were submitted could still over-produce. Validate every job-card manufacture entry against the job card directly: finished goods plus process loss must fit in what the job card still has left to produce after earlier submitted entries. (cherry picked from commit 94cd27ce5daf62e7c8b6022953f5e397ed5d67d0) --- .../stock/doctype/stock_entry/stock_entry.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 258963ed9c5..4feabd092c2 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -296,6 +296,7 @@ class StockEntry(StockController, SubcontractingInwardController): self.validate_batch() self.validate_inspection() self.validate_fg_completed_qty() + self.validate_job_card_pending_production() self.validate_difference_account() self.set_job_card_data() self.validate_job_card_item() @@ -3227,6 +3228,40 @@ class StockEntry(StockController, SubcontractingInwardController): (flt(self.process_loss_qty) / flt(self.fg_completed_qty)) * 100 ) + def validate_job_card_pending_production(self): + """A draft created before other entries were submitted must not book more than the job + card still has left; without this, a stale draft over-produces the finished good.""" + if self.purpose != "Manufacture" or not self.job_card: + return + + job_card = frappe.get_doc("Job Card", self.job_card) + if job_card.is_corrective_job_card or job_card.is_subcontracted: + return + + precision = frappe.get_precision("Stock Entry Detail", "qty") + pending_qty = flt( + flt(job_card.get_qty_to_produce()) + - flt(job_card.manufactured_qty) + - flt(job_card.get_consumed_process_loss()), + precision, + ) + finished_qty = flt(sum(flt(d.transfer_qty) for d in self.items if d.is_finished_item), precision) + entry_qty = flt(finished_qty + flt(self.process_loss_qty), precision) + + if entry_qty > pending_qty: + uom = job_card.stock_uom + frappe.throw( + _( + "The Job Card {0} has only {1} left to produce, but this entry books {2} ({3} finished goods and {4} process loss). Cancel or update its other manufacture entries first." + ).format( + frappe.bold(self.job_card), + frappe.bold(f"{pending_qty} {uom}"), + frappe.bold(f"{entry_qty} {uom}"), + f"{finished_qty} {uom}", + f"{flt(self.process_loss_qty, precision)} {uom}", + ) + ) + def get_pending_process_loss_qty(self): """Loss this entry should still book: the job card's unbooked loss when the entry belongs to one, else the largest operation loss on the work order (legacy flow).""" From f25c54e9d7ba2de368b9449ffaa7d0d349119743 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 16:11:20 +0530 Subject: [PATCH 061/134] test: stale manufacture draft cannot over-produce without an operation BOM (cherry picked from commit 7157e4357b63d8914e76b1462268e0009aa0a51f) --- .../doctype/job_card/test_job_card.py | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index f7834c82cfe..89b9dfb159b 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -1509,6 +1509,114 @@ class TestJobCard(ERPNextTestSuite): self.assertEqual(flt(fg_row.qty), 3.0) me_b.submit() + def make_semi_fg_work_order(self, prefix, qty=5): + """Two-operation semi FG work order: Op A makes the SFG from RM 1, final Op B + consumes it. Both operations skip material transfer; stock is pre-seeded.""" + from erpnext.manufacturing.doctype.operation.test_operation import make_operation + from erpnext.stock.doctype.item.test_item import make_item + + warehouse = "Stores - _TC" + rm1 = make_item(f"{prefix} RM 1", {"is_stock_item": 1}).name + rm2 = make_item(f"{prefix} RM 2", {"is_stock_item": 1}).name + sfg = make_item(f"{prefix} SFG 1", {"is_stock_item": 1}).name + fg1 = make_item(f"{prefix} FG 1", {"is_stock_item": 1}).name + + sfg_bom = frappe.new_doc("BOM", company="_Test Company", item=sfg, quantity=1) + sfg_bom.append("items", {"item_code": rm1, "qty": 1}) + sfg_bom.insert() + sfg_bom.submit() + + fg_bom = frappe.new_doc( + "BOM", + company="_Test Company", + item=fg1, + quantity=1, + with_operations=1, + track_semi_finished_goods=1, + ) + operation1 = { + "operation": f"{prefix} Op A", + "workstation": "_Test Workstation A", + "finished_good": sfg, + "bom_no": sfg_bom.name, + "finished_good_qty": 1, + "sequence_id": 1, + "time_in_mins": 60, + "source_warehouse": warehouse, + "fg_warehouse": warehouse, + "skip_material_transfer": 1, + } + operation2 = { + "operation": f"{prefix} Op B", + "workstation": "_Test Workstation A", + "finished_good": fg1, + "finished_good_qty": 1, + "is_final_finished_good": 1, + "sequence_id": 2, + "time_in_mins": 60, + "source_warehouse": warehouse, + "fg_warehouse": warehouse, + "skip_material_transfer": 1, + } + make_workstation(operation1) + make_operation(operation1) + make_operation(operation2) + fg_bom.append("operations", operation1) + fg_bom.append("operations", operation2) + fg_bom.append("items", {"item_code": rm2, "qty": 1}) + fg_bom.append("items", {"item_code": sfg, "qty": 1, "operation_row_id": 2}) + fg_bom.insert() + fg_bom.submit() + + work_order = make_wo_order_test_record( + item=fg1, + qty=qty, + source_warehouse=warehouse, + fg_warehouse=warehouse, + bom_no=fg_bom.name, + skip_transfer=1, + ) + + for item_code in (rm1, rm2, sfg): + make_stock_entry(item_code=item_code, target=warehouse, qty=10, basic_rate=100) + + return work_order + + def get_semi_fg_job_card(self, work_order, operation): + return frappe.get_doc( + "Job Card", + frappe.db.get_value("Job Card", {"work_order": work_order.name, "operation": operation}, "name"), + ) + + def test_stale_manufacture_draft_cannot_over_produce_without_operation_bom(self): + work_order = self.make_semi_fg_work_order("PL NoBom") + + jc_a = self.get_semi_fg_job_card(work_order, "PL NoBom Op A") + jc_a.append( + "time_logs", + {"from_time": "2024-01-01 08:00:00", "to_time": "2024-01-01 09:00:00", "completed_qty": 5}, + ) + jc_a.submit() + frappe.get_doc(jc_a.make_stock_entry_for_semi_fg_item()).submit() + + # Op B has no operation BOM, so its entries carry no For Quantity to validate against + jc_b = self.get_semi_fg_job_card(work_order, "PL NoBom Op B") + jc_b.append( + "time_logs", + {"from_time": "2024-02-01 08:00:00", "to_time": "2024-02-01 09:00:00", "completed_qty": 3}, + ) + jc_b.pending_qty = 0 + jc_b.process_loss_qty = 2 + jc_b.submit() + + draft_one = frappe.get_doc(jc_b.make_stock_entry_for_semi_fg_item()) + draft_two = frappe.get_doc(jc_b.make_stock_entry_for_semi_fg_item()) + + draft_one.submit() + + stale = frappe.get_doc("Stock Entry", draft_two.name) + self.assertRaises(frappe.ValidationError, stale.submit) + def test_semi_fg_auto_pull_with_uom_conversion(self): from erpnext.manufacturing.doctype.operation.test_operation import make_operation from erpnext.stock.doctype.item.test_item import make_item From 2548751673d7853ce5f282bd32fa1746f3019f20 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 16:25:04 +0530 Subject: [PATCH 062/134] fix: generate the next manufacture entry net of booked process loss After a partial entry booked the job card's full process loss, the next generated entry was sized qty-to-produce minus manufactured only. It exceeded the pending production cap, so Make Stock Entry could not finish the card. Subtract the consumed loss when sizing the entry. (cherry picked from commit b8dd886cd424e3347a92a7eff4f357758197f1f3) # Conflicts: # erpnext/manufacturing/doctype/job_card/job_card.py --- erpnext/manufacturing/doctype/job_card/job_card.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 61577623236..512de2ee2db 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -1674,10 +1674,18 @@ class JobCard(Document): from erpnext.stock.doctype.stock_entry_type.stock_entry_type import ManufactureEntry +<<<<<<< HEAD ste = ManufactureEntry( { "for_quantity": self.for_quantity - self.manufactured_qty, "process_loss_qty": max(self.process_loss_qty - get_consumed_process_loss(), 0), +======= + consumed_process_loss = self.get_consumed_process_loss() + return ManufactureEntry( + { + "for_quantity": self.get_qty_to_produce() - self.manufactured_qty - consumed_process_loss, + "process_loss_qty": max(self.process_loss_qty - consumed_process_loss, 0), +>>>>>>> b8dd886cd4 (fix: generate the next manufacture entry net of booked process loss) "job_card": self.name, "skip_material_transfer": self.skip_material_transfer, "backflush_from_wip_warehouse": self.backflush_from_wip_warehouse, From e8859028645e4d51dc02a3c07f4d293878819a93 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 16:25:05 +0530 Subject: [PATCH 063/134] test: partial manufacture entry then finishing the job card (cherry picked from commit eb7537c8dfab7c7619ba5d450e01601f0513e69a) --- .../doctype/job_card/test_job_card.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 89b9dfb159b..7160e067653 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -1588,6 +1588,44 @@ class TestJobCard(ERPNextTestSuite): frappe.db.get_value("Job Card", {"work_order": work_order.name, "operation": operation}, "name"), ) + def test_partial_manufacture_entry_then_finish(self): + work_order = self.make_semi_fg_work_order("PL Partial") + + jc_a = self.get_semi_fg_job_card(work_order, "PL Partial Op A") + jc_a.append( + "time_logs", + {"from_time": "2024-01-01 08:00:00", "to_time": "2024-01-01 09:00:00", "completed_qty": 5}, + ) + jc_a.submit() + frappe.get_doc(jc_a.make_stock_entry_for_semi_fg_item()).submit() + + jc_b = self.get_semi_fg_job_card(work_order, "PL Partial Op B") + jc_b.append( + "time_logs", + {"from_time": "2024-02-01 08:00:00", "to_time": "2024-02-01 09:00:00", "completed_qty": 3}, + ) + jc_b.pending_qty = 0 + jc_b.process_loss_qty = 2 + jc_b.submit() + + # book 1 of the 3 finished units now; the full process loss goes with this first entry + first = frappe.get_doc(jc_b.make_stock_entry_for_semi_fg_item()) + fg_row = next(row for row in first.items if row.is_finished_item) + fg_row.qty = 1 + first.save() + first.submit() + + # the follow-up entry must be generated net of the already-booked loss and still submit + jc_b.reload() + second = frappe.get_doc(jc_b.make_stock_entry_for_semi_fg_item()) + fg_row = next(row for row in second.items if row.is_finished_item) + self.assertEqual(flt(fg_row.qty), 2.0) + self.assertEqual(flt(second.process_loss_qty), 0.0) + second.submit() + + jc_b.reload() + self.assertEqual(flt(jc_b.manufactured_qty), 3.0) + def test_stale_manufacture_draft_cannot_over_produce_without_operation_bom(self): work_order = self.make_semi_fg_work_order("PL NoBom") From c598cf9010b41f1549c0b3f2fd2a5c562f0b9fa8 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 16:25:06 +0530 Subject: [PATCH 064/134] fix: keep Target Warehouse optional for work orders tracking semi finished goods The WIP warehouse change also removed the Target Warehouse exemption for semi FG orders, but those may validly carry the target on each operation instead. Restore the exemption in the form and the submit check; the WIP warehouse requirement stays. (cherry picked from commit 9df527bf3f98ceeac4545b17452d5b9e010d104a) --- erpnext/manufacturing/doctype/work_order/work_order.js | 3 ++- erpnext/manufacturing/doctype/work_order/work_order.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 9992c2466fd..04f259f1508 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -281,7 +281,8 @@ frappe.ui.form.on("Work Order", { }, set_fg_warehouse_mandatory(frm) { - frm.toggle_reqd("fg_warehouse", frm.doc.skip_transfer !== 1); + let mandatory = frm.doc.skip_transfer === 1 || frm.doc.track_semi_finished_goods === 1 ? false : true; + frm.toggle_reqd("fg_warehouse", mandatory); }, add_custom_button_to_return_components: function (frm) { diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index a1fd433ad22..30ed33a66a4 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -917,7 +917,7 @@ class WorkOrder(Document): def validate_warehouse(self): if not self.wip_warehouse and not self.skip_transfer: frappe.throw(_("Work-in-Progress Warehouse is required before Submit")) - if not self.fg_warehouse: + if not self.fg_warehouse and not self.track_semi_finished_goods: frappe.throw(_("Target Warehouse is required before Submit")) def before_submit(self): From e252329df4d95c1a3082ce5ec9cd747859df3c5a Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 16:25:07 +0530 Subject: [PATCH 065/134] test: target warehouse stays optional for semi FG work orders (cherry picked from commit db99657c470ddf4c0a55b92e2e723ff54c16047d) # Conflicts: # erpnext/manufacturing/doctype/work_order/test_work_order.py --- .../doctype/work_order/test_work_order.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index d58bb078d2a..3e5304180bf 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -4903,7 +4903,17 @@ class TestWorkOrder(ERPNextTestSuite): wo.wip_warehouse = "_Test Warehouse - _TC" wo.validate_warehouse() +<<<<<<< HEAD >>>>>>> f61f6523b9 (test: WIP warehouse required for work orders tracking semi finished goods) +======= + # the top-level target warehouse stays optional; operations may carry their own + wo.fg_warehouse = None + wo.validate_warehouse() + + wo.track_semi_finished_goods = 0 + self.assertRaises(frappe.ValidationError, wo.validate_warehouse) + +>>>>>>> db99657c47 (test: target warehouse stays optional for semi FG work orders) def get_reserved_entries(voucher_no, warehouse=None): doctype = frappe.qb.DocType("Stock Reservation Entry") From 491f9fa3fe54891744fe3ac45ae488ae179ec363 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 16:42:28 +0530 Subject: [PATCH 066/134] fix: skip the pending production check on update-after-submit saves Saving a submitted manufacture entry to change an allowed field re-ran the pending production cap with a manufactured aggregate that already includes the entry itself, so the save was rejected against the post-entry remainder. Quantities are not editable after submit, so the check has nothing to protect there. (cherry picked from commit bed957fa677c76b9c1e2adca80aeb5910e9380cc) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 4feabd092c2..36da536c328 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -3234,6 +3234,9 @@ class StockEntry(StockController, SubcontractingInwardController): if self.purpose != "Manufacture" or not self.job_card: return + if self._action == "update_after_submit": + return + job_card = frappe.get_doc("Job Card", self.job_card) if job_card.is_corrective_job_card or job_card.is_subcontracted: return From a815a756b770d4401a6b28a4c3efefd22be0e9b0 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 16:42:29 +0530 Subject: [PATCH 067/134] test: update-after-submit save keeps the manufacture entry intact (cherry picked from commit 424a1dfa87ffb063498540e10c9199c5cadd5c17) --- .../doctype/job_card/test_job_card.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 7160e067653..f0a1931e689 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -1626,6 +1626,31 @@ class TestJobCard(ERPNextTestSuite): jc_b.reload() self.assertEqual(flt(jc_b.manufactured_qty), 3.0) + def test_update_after_submit_keeps_manufacture_entry_intact(self): + work_order = self.make_semi_fg_work_order("PL Update") + + jc_a = self.get_semi_fg_job_card(work_order, "PL Update Op A") + jc_a.append( + "time_logs", + {"from_time": "2024-01-01 08:00:00", "to_time": "2024-01-01 09:00:00", "completed_qty": 3}, + ) + jc_a.pending_qty = 0 + jc_a.process_loss_qty = 2 + jc_a.submit() + + entry = frappe.get_doc(jc_a.make_stock_entry_for_semi_fg_item()) + entry.submit() + + if not frappe.db.exists("Print Heading", "_Test SFG Heading"): + frappe.get_doc({"doctype": "Print Heading", "print_heading": "_Test SFG Heading"}).insert() + + entry.reload() + entry.select_print_heading = "_Test SFG Heading" + entry.save() + + entry.reload() + self.assertEqual(flt(entry.process_loss_qty), 2.0) + def test_stale_manufacture_draft_cannot_over_produce_without_operation_bom(self): work_order = self.make_semi_fg_work_order("PL NoBom") From acba9945b0221d7fa32b714a600e0be0fa845fc5 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 16:47:07 +0530 Subject: [PATCH 068/134] fix: scale generated raw materials to the manufacture entry's production share Every generated entry copied each Job Card Item's full required_qty in the skip-transfer and BOM-backflush paths, so two entries for one job card consumed the requirement twice. Scale the rows to the share of production this entry accounts for and cap them at the requirement still unconsumed, dropping rows that have nothing left. An entry whose materials are exhausted then fails the existing at-least-one-raw-material check instead of minting finished goods from nothing. (cherry picked from commit 0428cddf5b61a3f71dd105dfe233294730fc9ebe) --- .../stock_entry_type/stock_entry_type.py | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py index 6a809d6f1b2..74996b96a22 100644 --- a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py +++ b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py @@ -125,6 +125,7 @@ class ManufactureEntry: if backflush_based_on != "BOM": available_serial_batches = self.get_transferred_serial_batches() + production_share = self.get_production_share() for item_code, _dict in item_dict.items(): _dict.from_warehouse = self.source_wh.get(item_code) or self.wip_warehouse _dict.to_warehouse = "" @@ -138,11 +139,29 @@ class ManufactureEntry: _dict.qty = calculated_qty self.update_available_serial_batches(_dict, available_serial_batches) - elif self.skip_material_transfer: - set_previous_operation_serial_batch(self.stock_entry, _dict) + else: + remaining_qty = max(flt(_dict.qty) - flt(_dict.consumed_qty), 0) + _dict.qty = min(flt(_dict.qty) * production_share, remaining_qty) + if not _dict.qty: + continue + + if self.skip_material_transfer: + set_previous_operation_serial_batch(self.stock_entry, _dict) self.stock_entry.add_to_stock_entry_detail(item_dict) + def get_production_share(self): + """Fraction of the job card's production this entry accounts for; raw materials are + generated proportionally so several partial entries never consume more than required.""" + for_quantity, pending_qty = frappe.db.get_value( + "Job Card", self.job_card, ["for_quantity", "pending_qty"] + ) + qty_to_produce = flt(for_quantity) - flt(pending_qty) + if not qty_to_produce: + return 1 + + return min(flt(self.for_quantity) / qty_to_produce, 1) + def parse_available_serial_batches(self, item_dict, available_serial_batches): key = (item_dict.item_code, item_dict.from_warehouse) if key not in available_serial_batches: From 5f391a2531c51f29bfbc69052bc408d9425b1176 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 16:47:08 +0530 Subject: [PATCH 069/134] test: partial entries consume exactly the job card's material requirement (cherry picked from commit 8f0617c83470d958f79b557d651cdc5fa925bbd2) --- .../doctype/job_card/test_job_card.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index f0a1931e689..7aacb300ec2 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -1608,10 +1608,14 @@ class TestJobCard(ERPNextTestSuite): jc_b.process_loss_qty = 2 jc_b.submit() - # book 1 of the 3 finished units now; the full process loss goes with this first entry + # book 1 of the 3 finished units now; the full process loss goes with this first entry, + # so it accounts for 3 of 5 and its materials are trimmed to the same share first = frappe.get_doc(jc_b.make_stock_entry_for_semi_fg_item()) fg_row = next(row for row in first.items if row.is_finished_item) fg_row.qty = 1 + for row in first.items: + if row.s_warehouse and not row.is_finished_item: + row.qty = flt(row.qty) * 3 / 5 first.save() first.submit() @@ -1626,6 +1630,17 @@ class TestJobCard(ERPNextTestSuite): jc_b.reload() self.assertEqual(flt(jc_b.manufactured_qty), 3.0) + # across both entries, consumption adds up to the job card's requirement of 5, no more + consumed = frappe.get_all( + "Stock Entry Detail", + filters={"parent": ["in", [first.name, second.name]], "s_warehouse": ["is", "set"]}, + fields=["item_code", {"SUM": "qty", "as": "qty"}], + group_by="item_code", + ) + self.assertTrue(consumed) + for row in consumed: + self.assertEqual(flt(row.qty), 5.0, f"{row.item_code} mis-consumed across partial entries") + def test_update_after_submit_keeps_manufacture_entry_intact(self): work_order = self.make_semi_fg_work_order("PL Update") From f810d780c01339174c4e151e422bb44139f05a76 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 17:25:34 +0530 Subject: [PATCH 070/134] fix: keep the transfer qty check for legacy semi FG cards without an FG item Existing submitted BOMs may carry operations without a finished good, and no migration repairs them. Exempting every semi FG job card from the transfer check let such a card submit after a partial transfer. Exempt only cards that skip material transfer; legacy cards with transfer enabled keep the strict transferred qty check. (cherry picked from commit 1deae664ce75cea04c344069d8fee8840cfdff8f) --- erpnext/manufacturing/doctype/job_card/job_card.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 512de2ee2db..8ed1897d51e 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -845,7 +845,7 @@ class JobCard(Document): ) def validate_transfer_qty(self): - if self.track_semi_finished_goods: + if self.track_semi_finished_goods and self.skip_material_transfer: return if ( From ae00a09cdf672feb1681121d02e45770a081180f Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 17:25:35 +0530 Subject: [PATCH 071/134] test: transfer qty exemption only applies when material transfer is skipped (cherry picked from commit 1478e2a4cb24639694d8ac81ee7b6d285f319c64) --- erpnext/manufacturing/doctype/job_card/test_job_card.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 7aacb300ec2..a0ea71aaab0 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -2292,12 +2292,21 @@ class TestJobCardLogic(ERPNextTestSuite): def test_semi_fg_job_card_is_exempt_from_transfer_qty_check(self): jc = frappe.new_doc("Job Card") jc.track_semi_finished_goods = 1 + jc.skip_material_transfer = 1 jc.for_quantity = 10 jc.transferred_qty = 0 jc.append("items", {"item_code": "_Test Item"}) jc.validate_transfer_qty() + # with transfer enabled, a legacy card without an FG item keeps the strict check + jc.skip_material_transfer = 0 + self.assertRaises(frappe.ValidationError, jc.validate_transfer_qty) + + jc.finished_good = "_Test Item" + jc.validate_transfer_qty() + + jc.finished_good = None jc.track_semi_finished_goods = 0 self.assertRaises(frappe.ValidationError, jc.validate_transfer_qty) >>>>>>> 4b3904c6d7 (test: semi FG job card is exempt from the legacy transfer qty check) From 27130d8e49f1e283594ab3f7d5d71d48e35740de Mon Sep 17 00:00:00 2001 From: pandiyan Date: Sat, 8 Aug 2026 23:04:28 +0530 Subject: [PATCH 072/134] fix: roll up process loss to the work order for semi finished goods update_work_order_qty() returns early when track_semi_finished_goods is enabled, so set_process_loss_qty() never ran and Work Order.process_loss_qty stayed at zero even though the job cards and the work order operations had booked the loss. The work order also never reached the Completed status, since that needs produced_qty + process_loss_qty to cover the ordered qty. Calling set_process_loss_qty() from that early return is not enough: the final operation has no semi finished good bom, so its manufacture entry is not from a bom, remove_fg_completed_qty() zeroes fg_completed_qty and update_work_order_qty() is never reached at all. The manufacture entries cannot be summed either. Each one is reset to MAX(Work Order Operation.process_loss_qty), so every entry of a multi operation chain carries the running maximum instead of the loss of its own operation. Aggregate the operations instead, and refresh the work order from the job card, which is where the operation loss is written. (cherry picked from commit 0eb61c9fac7f685de303288446cb32375c37b02d) # Conflicts: # erpnext/manufacturing/doctype/job_card/job_card.py # erpnext/manufacturing/doctype/work_order/services/status.py --- .../doctype/job_card/job_card.py | 25 + .../doctype/work_order/services/status.py | 471 ++++++++++++++++++ 2 files changed, 496 insertions(+) create mode 100644 erpnext/manufacturing/doctype/work_order/services/status.py diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 8ed1897d51e..970f81d5e03 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -1041,7 +1041,32 @@ class JobCard(Document): ) def update_work_order_data(self, for_quantity, process_loss_qty, pending_qty, time_in_mins, wo): +<<<<<<< HEAD workstation_hour_rate = frappe.get_value("Workstation", self.workstation, "hour_rate") +======= + time_data = self.get_operation_time_data() + + for data in wo.operations: + if data.get("name") == self.operation_id: + self.update_wo_operation_row( + data, for_quantity, process_loss_qty, pending_qty, time_in_mins, time_data + ) + + wo.flags.ignore_validate_update_after_submit = True + wo.update_operation_status() + wo.calculate_operating_cost() + wo.set_actual_dates() + + if wo.track_semi_finished_goods: + wo.set_process_loss_qty() + + if time_data: + wo.status = "In Process" + + wo.save() + + def get_operation_time_data(self): +>>>>>>> 0eb61c9fac (fix: roll up process loss to the work order for semi finished goods) jc = frappe.qb.DocType("Job Card") jctl = frappe.qb.DocType("Job Card Time Log") diff --git a/erpnext/manufacturing/doctype/work_order/services/status.py b/erpnext/manufacturing/doctype/work_order/services/status.py new file mode 100644 index 00000000000..74f204acd40 --- /dev/null +++ b/erpnext/manufacturing/doctype/work_order/services/status.py @@ -0,0 +1,471 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +"""Status and quantity-rollup logic for Work Order. + +Extracted from work_order.py. ``StatusService`` wraps a Work Order document +(composition); work_order.py keeps thin delegating stubs so the many external +callers (job cards, sales orders, production plans, patches) keep working. +""" + +import frappe +from frappe import _ +from frappe.query_builder.functions import Sum +from frappe.utils import cint, flt, get_link_to_form + +from erpnext.stock.stock_balance import get_planned_qty, update_bin_qty + +_QTY_PURPOSES = ( + ("Manufacture", "produced_qty"), + ("Material Transfer for Manufacture", "material_transferred_for_manufacturing"), + ("Material Transfer for Manufacture", "additional_transferred_qty"), +) + + +class StatusService: + def __init__(self, doc): + self.doc = doc + + def validate_work_order_against_so(self): + from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError + + total_qty = flt(self._ordered_qty_against_so()) + flt(self.doc.qty) + so_qty = flt(self._so_item_qty()) + flt(self._packed_item_qty()) + allowance_percentage = flt( + frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_sales_order") + ) + if total_qty <= so_qty + (allowance_percentage / 100 * so_qty): + return + + frappe.throw( + _("Cannot produce more Item {0} than Sales Order quantity {1} {2}").format( + get_link_to_form("Item", self.doc.production_item), + frappe.bold(so_qty), + frappe.bold(frappe.get_value("Item", self.doc.production_item, "stock_uom")), + ), + OverProductionError, + ) + + def _ordered_qty_against_so(self): + wo = frappe.qb.DocType("Work Order") + return ( + frappe.qb.from_(wo) + .select(Sum(wo.qty - wo.process_loss_qty)) + .where( + (wo.production_item == self.doc.production_item) + & (wo.sales_order == self.doc.sales_order) + & (wo.docstatus == 1) + & (wo.status != "Closed") + & (wo.name != self.doc.name) + ) + ).run()[0][0] + + def _so_item_qty(self): + so_item = frappe.qb.DocType("Sales Order Item") + return ( + frappe.qb.from_(so_item) + .select(Sum(so_item.stock_qty)) + .where( + (so_item.parent == self.doc.sales_order) + & (so_item.item_code == self.doc.production_item) + & (so_item.docstatus == 1) + ) + ).run()[0][0] + + def _packed_item_qty(self): + packed_item = frappe.qb.DocType("Packed Item") + return ( + frappe.qb.from_(packed_item) + .select(Sum(packed_item.qty)) + .where( + (packed_item.parent == self.doc.sales_order) + & (packed_item.parenttype == "Sales Order") + & (packed_item.item_code == self.doc.production_item) + & (packed_item.docstatus == 1) + ) + ).run()[0][0] + + def update_status(self, status=None): + """Update status of work order if unknown""" + if self.doc.docstatus == 1: + # Refresh material_transferred_for_manufacturing before deciding status so pick-list- + # driven transfers (where this qty is derived from item transfers, not fg_completed_qty) + # are reflected immediately, instead of only after the next status update call. + self.doc.refresh_material_transferred_for_manufacturing() + + if self.doc.status != "Closed": + if status not in ["Stopped", "Closed"]: + status = self.get_status(status) + + if status != self.doc.status: + self.doc.db_set("status", status) + + self.doc.update_required_items() + + return status or self.doc.status + + def get_status(self, status=None): + """Return the status based on stock entries against this work order""" + status = status or self.doc.status + + if self.doc.docstatus == 0: + status = "Draft" + elif self.doc.docstatus == 1: + status = self._submitted_status(status) + else: + status = "Cancelled" + + if self._is_partial_skip_transfer(): + status = "In Process" + + if status != "Completed" and not all(d.status == "Pending" for d in self.doc.operations): + status = "In Process" + + if status == "Not Started" and self.doc.reserve_stock: + status = self._reservation_status(status) + + return status + + def _submitted_status(self, status): + if status in ["Closed", "Stopped"]: + return status + + status = ( + "In Process" + if flt(self.doc.material_transferred_for_manufacturing) > 0 + or self.doc.skip_transfer + or self._has_transferred_material() + else "Not Started" + ) + precision = frappe.get_precision("Work Order", "produced_qty") + total_qty = flt(self.doc.produced_qty, precision) + flt(self.doc.process_loss_qty, precision) + if flt(total_qty, precision) >= flt(self.doc.qty, precision): + status = "Completed" + return status + + def _has_transferred_material(self): + """True if any raw material was transferred against this work order via a pick list + or a material request (these leave material_transferred_for_manufacturing at 0 via + the min-fraction rule).""" + ste = frappe.qb.DocType("Stock Entry") + ste_child = frappe.qb.DocType("Stock Entry Detail") + mr_child = frappe.qb.DocType("Stock Entry Detail") + # Stock Entry only carries `material_request` at the child-row level, so a Stock + # Entry is "MR-sourced" if *any* of its rows link back to a Material Request; once + # that's established, sum every row's transfer_qty, not just the linked ones (a + # manually appended extra row on the same entry has no material_request of its own). + mr_sourced_stock_entries = ( + frappe.qb.from_(mr_child).select(mr_child.parent).where(mr_child.material_request.isnotnull()) + ) + qty = ( + frappe.qb.from_(ste) + .inner_join(ste_child) + .on(ste_child.parent == ste.name) + .select(Sum(ste_child.transfer_qty)) + .where( + (ste.work_order == self.doc.name) + & (ste.docstatus == 1) + & (ste.purpose == "Material Transfer for Manufacture") + & (ste.is_return == 0) + & (ste.pick_list.isnotnull() | ste.name.isin(mr_sourced_stock_entries)) + ) + ).run()[0][0] + return flt(qty) > 0 + + def _is_partial_skip_transfer(self): + return bool( + self.doc.skip_transfer + and self.doc.produced_qty + and self.doc.qty > (flt(self.doc.produced_qty) + flt(self.doc.process_loss_qty)) + ) + + def _reservation_status(self, status): + for row in self.doc.required_items: + if not row.stock_reserved_qty: + continue + + if row.stock_reserved_qty >= row.required_qty: + status = "Stock Reserved" + else: + return "Stock Partially Reserved" + return status + + def update_work_order_qty(self): + """Update Manufactured Qty and Material Transferred for Qty based on Stock Entry""" + if self.doc.track_semi_finished_goods: + return + + for purpose, fieldname in _QTY_PURPOSES: + self._update_qty_for_purpose(purpose, fieldname) + + if self.doc.production_plan: + self.set_produced_qty_for_sub_assembly_item() + self.update_production_plan_status() + + if self.doc.additional_transferred_qty: + self.doc.validate_additional_transferred_qty() + + def _update_qty_for_purpose(self, purpose, fieldname): + from erpnext.manufacturing.doctype.work_order.work_order import StockOverProductionError + + if self._skip_transfer_purpose(purpose): + return + + qty = self.get_transferred_or_manufactured_qty(purpose, fieldname) + completed_qty = self.doc.qty + (self._qty_allowance(purpose) / 100 * self.doc.qty) + if qty > completed_qty: + frappe.throw( + _("{0} ({1}) cannot be greater than planned quantity ({2}) in Work Order {3}").format( + _(self.doc.meta.get_label(fieldname)), qty, completed_qty, self.doc.name + ), + StockOverProductionError, + ) + + self.doc.db_set(fieldname, qty) + self.set_process_loss_qty() + self._update_produced_qty_in_so() + + def _skip_transfer_purpose(self, purpose): + return bool( + purpose == "Material Transfer for Manufacture" + and self.doc.operations + and self.doc.transfer_material_against == "Job Card" + ) + + def _qty_allowance(self, purpose): + allowance = flt( + frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order") + ) + if not allowance and purpose == "Material Transfer for Manufacture": + allowance = flt( + frappe.db.get_single_value("Manufacturing Settings", "transfer_extra_materials_percentage") + ) + return allowance + + def _update_produced_qty_in_so(self): + from erpnext.selling.doctype.sales_order.sales_order import update_produced_qty_in_so_item + + if ( + self.doc.sales_order + and self.doc.sales_order_item + and not self.doc.production_plan_sub_assembly_item + ): + update_produced_qty_in_so_item(self.doc.sales_order, self.doc.sales_order_item) + + def update_disassembled_qty(self, qty, is_cancel=False): + if is_cancel: + self.doc.disassembled_qty = max(0, self.doc.disassembled_qty - qty) + else: + if self.doc.docstatus == 1: + self.doc.disassembled_qty += qty + + if not is_cancel and self.doc.disassembled_qty > self.doc.produced_qty: + frappe.throw(_("Cannot disassemble more than produced quantity.")) + + self.doc.db_set("disassembled_qty", self.doc.disassembled_qty) + + def get_transferred_or_manufactured_qty(self, purpose, fieldname): + parent = frappe.qb.DocType("Stock Entry") + is_additional = cint(fieldname == "additional_transferred_qty") + query = frappe.qb.from_(parent).where(self._stock_entry_filter(parent, purpose, is_additional)) + + if purpose == "Manufacture": + child = frappe.qb.DocType("Stock Entry Detail") + query = ( + query.join(child) + .on(parent.name == child.parent) + .select(Sum(child.transfer_qty)) + .where(child.is_finished_item == 1) + ) + else: + query = query.select(Sum(parent.fg_completed_qty)) + + return flt(query.run()[0][0]) + + def _stock_entry_filter(self, parent, purpose, is_additional): + return ( + (parent.work_order == self.doc.name) + & (parent.docstatus == 1) + & (parent.purpose == purpose) + & (parent.is_additional_transfer_entry == is_additional) + ) + + def set_process_loss_qty(self): + self.doc.db_set("process_loss_qty", self._process_loss_qty()) + + def _process_loss_qty(self): + if self.doc.track_semi_finished_goods: + return flt(sum(flt(row.process_loss_qty) for row in self.doc.operations)) + + table = frappe.qb.DocType("Stock Entry") + process_loss_qty = ( + frappe.qb.from_(table) + .select(Sum(table.process_loss_qty)) + .where( + (table.work_order == self.doc.name) + & (table.purpose == "Manufacture") + & (table.docstatus == 1) + ) + ).run()[0][0] + + return flt(process_loss_qty) + + def update_production_plan_status(self): + production_plan = frappe.get_doc("Production Plan", self.doc.production_plan) + produced_qty = 0 + if self.doc.production_plan_item: + total_qty = frappe.get_all( + "Work Order", + fields=[{"SUM": "produced_qty", "as": "produced_qty"}], + filters={ + "docstatus": 1, + "production_plan": self.doc.production_plan, + "production_plan_item": self.doc.production_plan_item, + }, + as_list=1, + ) + + produced_qty = total_qty[0][0] if total_qty else 0 + + self.update_status() + production_plan.run_method("update_produced_pending_qty", produced_qty, self.doc.production_plan_item) + + def update_planned_qty(self): + if self.doc.track_semi_finished_goods: + return + + update_bin_qty(self.doc.production_item, self.doc.fg_warehouse, self._planned_qty_dict()) + + if self.doc.material_request: + mr_obj = frappe.get_doc("Material Request", self.doc.material_request) + mr_obj.update_requested_qty([self.doc.material_request_item]) + + def _planned_qty_dict(self): + from erpnext.manufacturing.doctype.production_plan.production_plan import ( + get_reserved_qty_for_sub_assembly, + ) + + qty_dict = {"planned_qty": get_planned_qty(self.doc.production_item, self.doc.fg_warehouse)} + if self.doc.production_plan_sub_assembly_item and self.doc.production_plan: + qty_dict["reserved_qty_for_production_plan"] = get_reserved_qty_for_sub_assembly( + self.doc.production_item, self.doc.fg_warehouse + ) + return qty_dict + + def set_produced_qty_for_sub_assembly_item(self): + produced_qty = self._sub_assembly_produced_qty() + frappe.db.set_value( + "Production Plan Sub Assembly Item", + self.doc.production_plan_sub_assembly_item, + "wo_produced_qty", + produced_qty, + ) + + def _sub_assembly_produced_qty(self): + table = frappe.qb.DocType("Work Order") + query = ( + frappe.qb.from_(table) + .select(Sum(table.produced_qty)) + .where( + (table.production_plan == self.doc.production_plan) + & (table.production_plan_sub_assembly_item == self.doc.production_plan_sub_assembly_item) + & (table.docstatus == 1) + ) + ).run() + return flt(query[0][0]) if query else 0 + + def update_ordered_qty(self): + if not ( + self.doc.production_plan + and (self.doc.production_plan_item or self.doc.production_plan_sub_assembly_item) + ): + return + + qty = self._production_plan_ordered_qty() + if self.doc.production_plan_item: + frappe.db.set_value("Production Plan Item", self.doc.production_plan_item, "ordered_qty", qty) + elif self.doc.production_plan_sub_assembly_item: + field = self.doc.production_plan_sub_assembly_item + frappe.db.set_value("Production Plan Sub Assembly Item", field, "ordered_qty", qty) + + doc = frappe.get_doc("Production Plan", self.doc.production_plan) + doc.set_status() + doc.db_set("status", doc.status) + + def _production_plan_ordered_qty(self): + table = frappe.qb.DocType("Work Order") + query = ( + frappe.qb.from_(table) + .select(Sum(table.qty)) + .where((table.production_plan == self.doc.production_plan) & (table.docstatus == 1)) + ) + if self.doc.production_plan_item: + query = query.where(table.production_plan_item == self.doc.production_plan_item) + elif self.doc.production_plan_sub_assembly_item: + query = query.where( + table.production_plan_sub_assembly_item == self.doc.production_plan_sub_assembly_item + ) + + result = query.run() + return flt(result[0][0]) if result else 0 + + def update_work_order_qty_in_so(self): + if ( + not self.doc.sales_order and not self.doc.sales_order_item + ) or self.doc.production_plan_sub_assembly_item: + return + + total_bundle_qty = self._total_bundle_qty() + work_order_qty = self._sales_order_work_order_qty() + frappe.db.set_value( + "Sales Order Item", + self.doc.sales_order_item, + "work_order_qty", + flt(work_order_qty / total_bundle_qty, 2), + ) + + def _sales_order_work_order_qty(self): + wo = frappe.qb.DocType("Work Order") + query = ( + frappe.qb.from_(wo) + .select(Sum(wo.qty)) + .where((wo.sales_order == self.doc.sales_order) & (wo.docstatus == 1) & (wo.status != "Closed")) + ) + if self.doc.product_bundle_item: + query = query.where(wo.product_bundle_item == self.doc.product_bundle_item) + else: + query = query.where(wo.production_item == self.doc.production_item) + + qty = query.run(as_list=1) + return qty[0][0] if qty and qty[0][0] else 0 + + def update_work_order_qty_in_combined_so(self): + total_bundle_qty = self._total_bundle_qty() + prod_plan = frappe.get_doc("Production Plan", self.doc.production_plan) + item_reference = frappe.get_value( + "Production Plan Item", self.doc.production_plan_item, "sales_order_item" + ) + + for plan_reference in prod_plan.prod_plan_references: + if plan_reference.item_reference != item_reference: + continue + + qty = flt(plan_reference.qty) / total_bundle_qty if self.doc.docstatus == 1 else 0.0 + frappe.db.set_value("Sales Order Item", plan_reference.sales_order_item, "work_order_qty", qty) + + def _total_bundle_qty(self): + if not self.doc.product_bundle_item: + return 1 + + pbi = frappe.qb.DocType("Product Bundle Item") + total_bundle_qty = ( + frappe.qb.from_(pbi).select(Sum(pbi.qty)).where(pbi.parent == self.doc.product_bundle_item) + ).run()[0][0] + # product bundle is 0 (product bundle allows 0 qty for items) + return total_bundle_qty or 1 + + def update_completed_qty_in_material_request(self): + if self.doc.material_request and self.doc.material_request_item: + frappe.get_doc("Material Request", self.doc.material_request).update_completed_qty( + [self.doc.material_request_item] + ) From 27625a6f1fe5e0a2c7e70b223e64bfc54118a255 Mon Sep 17 00:00:00 2001 From: pandiyan Date: Sat, 8 Aug 2026 23:04:28 +0530 Subject: [PATCH 073/134] test: work order process loss for semi finished goods Cover both shapes: a single operation that books the loss itself, and a chain where an earlier operation books it and the final operation loses nothing, so the sum over the operations is the only correct source. (cherry picked from commit 24de81f9faca2abac250bf580b36e03ea226d11e) # Conflicts: # erpnext/manufacturing/doctype/job_card/test_job_card.py --- .../doctype/job_card/test_job_card.py | 420 ++++++++++++++++++ 1 file changed, 420 insertions(+) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index a0ea71aaab0..c1f48953e5b 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -1265,6 +1265,426 @@ class TestJobCard(ERPNextTestSuite): 8, ) +<<<<<<< HEAD +======= + def test_semi_fg_pending_qty_is_left_to_another_job_card(self): + from erpnext.manufacturing.doctype.operation.test_operation import make_operation + from erpnext.stock.doctype.item.test_item import make_item + + warehouse = "Stores - _TC" + rm = make_item("Pending Qty RM 1", {"is_stock_item": 1}).name + fg = make_item("Pending Qty FG 1", {"is_stock_item": 1}).name + + fg_bom = frappe.new_doc( + "BOM", + company="_Test Company", + item=fg, + quantity=1, + with_operations=1, + track_semi_finished_goods=1, + ) + fg_bom.append("items", {"item_code": rm, "qty": 1, "operation_row_id": 1}) + + operation = { + "operation": "Pending Qty Op A", + "workstation": "_Test Workstation A", + "finished_good": fg, + "finished_good_qty": 1, + "is_final_finished_good": 1, + "sequence_id": 1, + "time_in_mins": 60, + "source_warehouse": warehouse, + "fg_warehouse": warehouse, + "skip_material_transfer": 1, + } + + make_workstation(operation) + make_operation(operation) + fg_bom.append("operations", operation) + fg_bom.insert() + fg_bom.submit() + + work_order = make_wo_order_test_record( + item=fg, + qty=5, + source_warehouse=warehouse, + fg_warehouse=warehouse, + bom_no=fg_bom.name, + skip_transfer=1, + do_not_save=True, + ) + work_order.operations[0].time_in_mins = 60 + work_order.save() + work_order.submit() + + make_stock_entry(item_code=rm, target=warehouse, qty=100, basic_rate=100) + + job_card = self.get_first_job_card(work_order.name) + job_card.append("time_logs", {"from_time": "2024-04-01 08:00:00"}) + job_card.save() + + job_card.complete_job_card( + qty=3, + for_quantity=5, + pending_qty=2, + process_loss_qty=0, + end_time="2024-04-01 09:00:00", + ) + + job_card.reload() + self.assertEqual(flt(job_card.for_quantity), 5) + self.assertEqual(flt(job_card.pending_qty), 2) + self.assertEqual(flt(job_card.process_loss_qty), 0) + + job_card.submit() + self.assertEqual(job_card.status, "To Manufacture") + + manufacturing_entry = frappe.get_doc(job_card.make_stock_entry_for_semi_fg_item()) + finished_item = next(row for row in manufacturing_entry.items if row.is_finished_item) + self.assertEqual(flt(finished_item.qty), 3) + manufacturing_entry.submit() + + job_card.reload() + self.assertEqual(flt(job_card.manufactured_qty), 3) + self.assertEqual(job_card.status, "Completed") + + def test_semi_fg_process_loss_rolls_up_to_work_order(self): + from erpnext.manufacturing.doctype.operation.test_operation import make_operation + from erpnext.stock.doctype.item.test_item import make_item + + warehouse = "Stores - _TC" + rm = make_item("Process Loss Rollup RM 1", {"is_stock_item": 1}).name + fg = make_item("Process Loss Rollup FG 1", {"is_stock_item": 1}).name + + fg_bom = frappe.new_doc( + "BOM", + company="_Test Company", + item=fg, + quantity=1, + with_operations=1, + track_semi_finished_goods=1, + ) + fg_bom.append("items", {"item_code": rm, "qty": 1, "operation_row_id": 1}) + + operation = { + "operation": "Process Loss Rollup Op A", + "workstation": "_Test Workstation A", + "finished_good": fg, + "finished_good_qty": 1, + "is_final_finished_good": 1, + "sequence_id": 1, + "time_in_mins": 60, + "source_warehouse": warehouse, + "fg_warehouse": warehouse, + "skip_material_transfer": 1, + } + + make_workstation(operation) + make_operation(operation) + fg_bom.append("operations", operation) + fg_bom.insert() + fg_bom.submit() + + work_order = make_wo_order_test_record( + item=fg, + qty=10, + source_warehouse=warehouse, + fg_warehouse=warehouse, + bom_no=fg_bom.name, + skip_transfer=1, + do_not_save=True, + ) + work_order.operations[0].time_in_mins = 60 + work_order.save() + work_order.submit() + + make_stock_entry(item_code=rm, target=warehouse, qty=100, basic_rate=100) + + job_card = self.get_first_job_card(work_order.name) + job_card.append("time_logs", {"from_time": "2024-05-01 08:00:00"}) + job_card.save() + + job_card.complete_job_card( + qty=8, + for_quantity=10, + pending_qty=0, + process_loss_qty=2, + end_time="2024-05-01 09:00:00", + ) + + job_card.reload() + self.assertEqual(flt(job_card.process_loss_qty), 2) + + job_card.submit() + frappe.get_doc(job_card.make_stock_entry_for_semi_fg_item()).submit() + + self.assertEqual( + flt( + frappe.db.get_value("Work Order Operation", work_order.operations[0].name, "process_loss_qty") + ), + 2, + ) + + work_order.reload() + self.assertEqual(flt(work_order.produced_qty), 8) + self.assertEqual(flt(work_order.process_loss_qty), 2) + self.assertEqual(work_order.status, "Completed") + + def test_semi_fg_process_loss_of_an_intermediate_operation_rolls_up_to_work_order(self): + """Loss booked by an earlier operation shrinks what the final operation can produce, + so it has to show up on the work order even though the final operation loses nothing.""" + from erpnext.manufacturing.doctype.operation.test_operation import make_operation + from erpnext.stock.doctype.item.test_item import make_item + + warehouse = "Stores - _TC" + rm = make_item("Intermediate Loss RM 1", {"is_stock_item": 1}).name + sfg = make_item("Intermediate Loss SFG 1", {"is_stock_item": 1}).name + fg = make_item("Intermediate Loss FG 1", {"is_stock_item": 1}).name + + sfg_bom = frappe.new_doc("BOM", company="_Test Company", item=sfg, quantity=1) + sfg_bom.append("items", {"item_code": rm, "qty": 1}) + sfg_bom.insert() + sfg_bom.submit() + + fg_bom = frappe.new_doc( + "BOM", + company="_Test Company", + item=fg, + quantity=1, + with_operations=1, + track_semi_finished_goods=1, + ) + + operations = [ + { + "operation": "Intermediate Loss Op A", + "finished_good": sfg, + "bom_no": sfg_bom.name, + "sequence_id": 1, + }, + { + "operation": "Intermediate Loss Op B", + "finished_good": fg, + "is_final_finished_good": 1, + "sequence_id": 2, + }, + ] + + for row in operations: + row.update( + { + "workstation": "_Test Workstation A", + "finished_good_qty": 1, + "time_in_mins": 60, + "source_warehouse": warehouse, + "fg_warehouse": warehouse, + "skip_material_transfer": 1, + } + ) + make_workstation(row) + make_operation(row) + fg_bom.append("operations", row) + + fg_bom.append("items", {"item_code": sfg, "qty": 1, "operation_row_id": 2}) + fg_bom.insert() + fg_bom.submit() + + work_order = make_wo_order_test_record( + item=fg, + qty=10, + source_warehouse=warehouse, + fg_warehouse=warehouse, + bom_no=fg_bom.name, + skip_transfer=1, + do_not_save=True, + ) + for row in work_order.operations: + row.time_in_mins = 60 + work_order.save() + work_order.submit() + + make_stock_entry(item_code=rm, target=warehouse, qty=100, basic_rate=100) + + def get_job_card(operation): + return frappe.get_doc( + "Job Card", + frappe.db.get_value( + "Job Card", + {"work_order": work_order.name, "operation": operation, "docstatus": 0}, + "name", + ), + ) + + jc_a = get_job_card("Intermediate Loss Op A") + jc_a.append("time_logs", {"from_time": "2024-06-01 08:00:00"}) + jc_a.save() + jc_a.complete_job_card( + qty=8, for_quantity=10, pending_qty=0, process_loss_qty=2, end_time="2024-06-01 09:00:00" + ) + jc_a.reload() + jc_a.submit() + frappe.get_doc(jc_a.make_stock_entry_for_semi_fg_item()).submit() + + work_order.reload() + self.assertEqual(flt(work_order.process_loss_qty), 2) + + # Operation A handed over only 8 units, so the final operation works on 8. + jc_b = get_job_card("Intermediate Loss Op B") + jc_b.for_quantity = 8 + for row in jc_b.items: + row.required_qty = 8 + jc_b.append( + "time_logs", + {"from_time": "2024-06-02 08:00:00", "to_time": "2024-06-02 09:00:00", "completed_qty": 8}, + ) + jc_b.save() + jc_b.submit() + frappe.get_doc(jc_b.make_stock_entry_for_semi_fg_item()).submit() + + work_order.reload() + self.assertEqual(flt(work_order.produced_qty), 8) + self.assertEqual(flt(work_order.process_loss_qty), 2) + self.assertEqual(work_order.status, "Completed") + + def test_semi_fg_sequence_needs_previous_operations_manufactured(self): + from erpnext.manufacturing.doctype.operation.test_operation import make_operation + from erpnext.stock.doctype.item.test_item import make_item + + warehouse = "Stores - _TC" + rm1 = make_item("Sequence Check RM 1", {"is_stock_item": 1}).name + rm2 = make_item("Sequence Check RM 2", {"is_stock_item": 1}).name + sfg1 = make_item("Sequence Check SFG 1", {"is_stock_item": 1}).name + sfg2 = make_item("Sequence Check SFG 2", {"is_stock_item": 1}).name + fg = make_item("Sequence Check FG 1", {"is_stock_item": 1}).name + + semi_fg_boms = {} + for semi_fg_item, raw_material in ((sfg1, rm1), (sfg2, rm2)): + bom = frappe.new_doc("BOM", company="_Test Company", item=semi_fg_item, quantity=1) + bom.append("items", {"item_code": raw_material, "qty": 1}) + bom.insert() + bom.submit() + semi_fg_boms[semi_fg_item] = bom.name + + fg_bom = frappe.new_doc( + "BOM", + company="_Test Company", + item=fg, + quantity=1, + with_operations=1, + track_semi_finished_goods=1, + ) + + operations = [ + { + "operation": "Sequence Check Op A", + "finished_good": sfg1, + "bom_no": semi_fg_boms[sfg1], + "sequence_id": 1, + }, + { + "operation": "Sequence Check Op B", + "finished_good": sfg2, + "bom_no": semi_fg_boms[sfg2], + "sequence_id": 1, + }, + { + "operation": "Sequence Check Op C", + "finished_good": fg, + "is_final_finished_good": 1, + "sequence_id": 2, + }, + ] + + for row in operations: + row.update( + { + "workstation": "_Test Workstation A", + "finished_good_qty": 1, + "time_in_mins": 60, + "source_warehouse": warehouse, + "fg_warehouse": warehouse, + "skip_material_transfer": 1, + } + ) + + make_workstation(row) + make_operation(row) + fg_bom.append("operations", row) + + fg_bom.append("items", {"item_code": sfg1, "qty": 1, "operation_row_id": 3}) + fg_bom.append("items", {"item_code": sfg2, "qty": 1, "operation_row_id": 3}) + fg_bom.insert() + fg_bom.submit() + + work_order = make_wo_order_test_record( + item=fg, + qty=5, + source_warehouse=warehouse, + fg_warehouse=warehouse, + bom_no=fg_bom.name, + skip_transfer=1, + do_not_save=True, + ) + + for row in work_order.operations: + row.time_in_mins = 60 + + work_order.save() + work_order.submit() + + make_stock_entry(item_code=rm1, target=warehouse, qty=10, basic_rate=100) + make_stock_entry(item_code=rm2, target=warehouse, qty=10, basic_rate=100) + + def get_job_card(operation): + return frappe.get_doc( + "Job Card", + frappe.db.get_value( + "Job Card", + {"work_order": work_order.name, "operation": operation, "docstatus": 0}, + "name", + ), + ) + + def add_time_log(job_card, day, qty): + job_card.append( + "time_logs", + { + "from_time": f"2024-01-{day} 08:00:00", + "to_time": f"2024-01-{day} 09:00:00", + "completed_qty": qty, + }, + ) + + jc_a = get_job_card("Sequence Check Op A") + jc_a.for_quantity = 3 + add_time_log(jc_a, "01", 3) + jc_a.submit() + + jc_b = get_job_card("Sequence Check Op B") + add_time_log(jc_b, "02", jc_b.for_quantity) + jc_b.submit() + frappe.get_doc(jc_b.make_stock_entry_for_semi_fg_item()).submit() + + jc_c = get_job_card("Sequence Check Op C") + jc_c.for_quantity = 3 + add_time_log(jc_c, "03", 3) + self.assertRaises(OperationSequenceError, jc_c.save) + + frappe.get_doc(jc_a.make_stock_entry_for_semi_fg_item()).submit() + + jc_c.reload() + jc_c.for_quantity = 4 + add_time_log(jc_c, "03", 4) + self.assertRaises(OperationSequenceError, jc_c.save) + + jc_c.reload() + jc_c.for_quantity = 3 + add_time_log(jc_c, "03", 3) + jc_c.submit() + + self.assertEqual(jc_c.docstatus, 1) + +>>>>>>> 24de81f9fa (test: work order process loss for semi finished goods) def test_semi_fg_batch_auto_pull_on_manufacture(self): from erpnext.manufacturing.doctype.operation.test_operation import make_operation from erpnext.stock.doctype.item.test_item import make_item From a22a7fddba07916f796012136ae64db2781c65e1 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 1 Aug 2026 18:34:29 +0530 Subject: [PATCH 074/134] fix(job_card): require the previous operation to be manufactured (#57684) * fix(job_card): block next operation until previous operation is manufactured With track semi finished goods, Work Order Operation completed_qty is set from the submitted job cards' total completed qty, so a job card of the next operation could be started and completed even when no Manufacture entry existed for the previous operation. The semi-finished goods it consumes were never produced. Validate the sequence against the qty actually manufactured against the previous operations' job cards (Manufacture entries / Subcontracting Receipts) when the work order tracks semi finished goods. * test(job_card): cover manufactured qty check across previous operations Work order with operations A and B at sequence 1 and C at sequence 2, tracking semi finished goods. C stays blocked while A's job card is submitted but its Manufacture entry is missing, and once A is manufactured for 3, C can only be completed for 3. (cherry picked from commit 3bd33541521d978ca085006091254bb854649859) # Conflicts: # erpnext/manufacturing/doctype/job_card/job_card.py # erpnext/manufacturing/doctype/job_card/test_job_card.py --- .../doctype/job_card/job_card.py | 71 ++++++++- .../doctype/job_card/test_job_card.py | 142 ++++++++++++++++++ 2 files changed, 212 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 3b4f8008f08..b8fa052e801 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -1344,18 +1344,58 @@ class JobCard(Document): if data and len(data) > 0: current_operation_qty = flt(data[0].completed_qty) +<<<<<<< HEAD current_operation_qty += flt(self.total_completed_qty) data = frappe.get_all( +======= + for row in self.get_previous_operations(): + if self.track_semi_finished_goods: + self.validate_previous_operation_manufactured_qty(row, current_operation_qty) + else: + self.validate_previous_operation(row, current_operation_qty) + + def get_previous_operations(self): + previous_operations = frappe.get_all( +>>>>>>> 3bd3354152 (fix(job_card): require the previous operation to be manufactured (#57684)) "Work Order Operation", - fields=["operation", "status", "completed_qty", "sequence_id"], + fields=["name", "operation", "status", "completed_qty", "sequence_id"], filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ("<", self.sequence_id)}, order_by="sequence_id, idx", ) +<<<<<<< HEAD message = "Job Card {}: As per the sequence of the operations in the work order {}".format( bold(self.name), bold(get_link_to_form("Work Order", self.work_order)) ) +======= + if self.track_semi_finished_goods and previous_operations: + manufactured_qty = self.get_manufactured_qty_per_operation( + [row.name for row in previous_operations] + ) + + for row in previous_operations: + row.manufactured_qty = flt(manufactured_qty.get(row.name)) + + return previous_operations + + def get_manufactured_qty_per_operation(self, operation_ids): + job_card = frappe.qb.DocType("Job Card") + + data = ( + frappe.qb.from_(job_card) + .select(job_card.operation_id, Sum(job_card.manufactured_qty)) + .where( + (job_card.work_order == self.work_order) + & (job_card.docstatus == 1) + & (IfNull(job_card.is_corrective_job_card, 0) == 0) + & (job_card.operation_id.isin(operation_ids)) + ) + .groupby(job_card.operation_id) + ).run() + + return dict(data) +>>>>>>> 3bd3354152 (fix(job_card): require the previous operation to be manufactured (#57684)) for row in data: if not row.completed_qty: @@ -1386,6 +1426,35 @@ class JobCard(Document): ) ) + def validate_previous_operation_manufactured_qty(self, row, current_operation_qty): + manufactured_qty = flt(row.manufactured_qty) + + if not manufactured_qty: + frappe.throw( + _( + "Job Card {0}: As per the sequence of the operations in the work order {1}, submit the manufacturing entry for the operation {2} before the operation {3}." + ).format( + bold(self.name), + bold(get_link_to_form("Work Order", self.work_order)), + bold(row.operation), + bold(self.operation), + ), + OperationSequenceError, + ) + + if manufactured_qty < current_operation_qty: + frappe.throw( + _( + "The completed quantity {0} of an operation {1} cannot be greater than the manufactured quantity {2} of a previous operation {3}. Submit the manufacturing entry for the operation {3} first." + ).format( + bold(current_operation_qty), + bold(self.operation), + bold(manufactured_qty), + bold(row.operation), + ), + OperationSequenceError, + ) + def validate_work_order(self): if self.is_work_order_closed(): frappe.throw(_("You can't make any changes to Job Card since Work Order is closed.")) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 826d558e830..db79f149345 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -10,7 +10,11 @@ from frappe.utils.data import add_to_date, now, today from erpnext.manufacturing.doctype.job_card.job_card import ( JobCardOverTransferError, +<<<<<<< HEAD OperationMismatchError, +======= + OperationSequenceError, +>>>>>>> 3bd3354152 (fix(job_card): require the previous operation to be manufactured (#57684)) OverlapError, make_corrective_job_card, make_material_request, @@ -1265,6 +1269,144 @@ class TestJobCard(ERPNextTestSuite): 8, ) + def test_semi_fg_sequence_needs_previous_operations_manufactured(self): + from erpnext.manufacturing.doctype.operation.test_operation import make_operation + from erpnext.stock.doctype.item.test_item import make_item + + warehouse = "Stores - _TC" + rm1 = make_item("Sequence Check RM 1", {"is_stock_item": 1}).name + rm2 = make_item("Sequence Check RM 2", {"is_stock_item": 1}).name + sfg1 = make_item("Sequence Check SFG 1", {"is_stock_item": 1}).name + sfg2 = make_item("Sequence Check SFG 2", {"is_stock_item": 1}).name + fg = make_item("Sequence Check FG 1", {"is_stock_item": 1}).name + + semi_fg_boms = {} + for semi_fg_item, raw_material in ((sfg1, rm1), (sfg2, rm2)): + bom = frappe.new_doc("BOM", company="_Test Company", item=semi_fg_item, quantity=1) + bom.append("items", {"item_code": raw_material, "qty": 1}) + bom.insert() + bom.submit() + semi_fg_boms[semi_fg_item] = bom.name + + fg_bom = frappe.new_doc( + "BOM", + company="_Test Company", + item=fg, + quantity=1, + with_operations=1, + track_semi_finished_goods=1, + ) + + operations = [ + { + "operation": "Sequence Check Op A", + "finished_good": sfg1, + "bom_no": semi_fg_boms[sfg1], + "sequence_id": 1, + }, + { + "operation": "Sequence Check Op B", + "finished_good": sfg2, + "bom_no": semi_fg_boms[sfg2], + "sequence_id": 1, + }, + { + "operation": "Sequence Check Op C", + "finished_good": fg, + "is_final_finished_good": 1, + "sequence_id": 2, + }, + ] + + for row in operations: + row.update( + { + "workstation": "_Test Workstation A", + "finished_good_qty": 1, + "time_in_mins": 60, + "source_warehouse": warehouse, + "fg_warehouse": warehouse, + "skip_material_transfer": 1, + } + ) + + make_workstation(row) + make_operation(row) + fg_bom.append("operations", row) + + fg_bom.append("items", {"item_code": sfg1, "qty": 1, "operation_row_id": 3}) + fg_bom.append("items", {"item_code": sfg2, "qty": 1, "operation_row_id": 3}) + fg_bom.insert() + fg_bom.submit() + + work_order = make_wo_order_test_record( + item=fg, + qty=5, + source_warehouse=warehouse, + fg_warehouse=warehouse, + bom_no=fg_bom.name, + skip_transfer=1, + do_not_save=True, + ) + + for row in work_order.operations: + row.time_in_mins = 60 + + work_order.save() + work_order.submit() + + make_stock_entry(item_code=rm1, target=warehouse, qty=10, basic_rate=100) + make_stock_entry(item_code=rm2, target=warehouse, qty=10, basic_rate=100) + + def get_job_card(operation): + return frappe.get_doc( + "Job Card", + frappe.db.get_value( + "Job Card", + {"work_order": work_order.name, "operation": operation, "docstatus": 0}, + "name", + ), + ) + + def add_time_log(job_card, day, qty): + job_card.append( + "time_logs", + { + "from_time": f"2024-01-{day} 08:00:00", + "to_time": f"2024-01-{day} 09:00:00", + "completed_qty": qty, + }, + ) + + jc_a = get_job_card("Sequence Check Op A") + jc_a.for_quantity = 3 + add_time_log(jc_a, "01", 3) + jc_a.submit() + + jc_b = get_job_card("Sequence Check Op B") + add_time_log(jc_b, "02", jc_b.for_quantity) + jc_b.submit() + frappe.get_doc(jc_b.make_stock_entry_for_semi_fg_item()).submit() + + jc_c = get_job_card("Sequence Check Op C") + jc_c.for_quantity = 3 + add_time_log(jc_c, "03", 3) + self.assertRaises(OperationSequenceError, jc_c.save) + + frappe.get_doc(jc_a.make_stock_entry_for_semi_fg_item()).submit() + + jc_c.reload() + jc_c.for_quantity = 4 + add_time_log(jc_c, "03", 4) + self.assertRaises(OperationSequenceError, jc_c.save) + + jc_c.reload() + jc_c.for_quantity = 3 + add_time_log(jc_c, "03", 3) + jc_c.submit() + + self.assertEqual(jc_c.docstatus, 1) + def test_semi_fg_batch_auto_pull_on_manufacture(self): from erpnext.manufacturing.doctype.operation.test_operation import make_operation from erpnext.stock.doctype.item.test_item import make_item From 3907d93f9fe6d0b8667f92d52326729634ca381a Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 1 Aug 2026 18:34:30 +0530 Subject: [PATCH 075/134] fix(job_card): reject a completion split that cannot add up (#57687) * fix(job_card): reject a completion split that cannot add up The completion dialogs silently dropped a recalculation whose result went negative, so entering a pending qty larger than what is left of the qty to manufacture kept the contradiction (3 to manufacture, 3 completed, 2 pending) and the job card only failed much later, on submission. Keep the split consistent while it is entered: reset the pending qty when the qty to manufacture changes, and refuse a completed, pending or process loss qty that leaves the others negative. complete_job_card validates the same rule, so the shop floor and the API cannot store a split that will never submit. Also name the three parts in the submission error instead of calling their sum the Total Completed Qty, which read as a contradiction of the field itself. * test(job_card): cover the completion qty split guard (cherry picked from commit 7bffd844828475d60562161d7e91640a13501d7c) # Conflicts: # erpnext/manufacturing/doctype/job_card/job_card.py # erpnext/manufacturing/doctype/job_card/test_job_card.py # erpnext/public/js/shop_floor/shop_floor.js --- .../doctype/job_card/job_card.js | 44 +- .../doctype/job_card/job_card.py | 42 +- .../doctype/job_card/test_job_card.py | 91 + erpnext/public/js/shop_floor/shop_floor.js | 1747 +++++++++++++++++ 4 files changed, 1919 insertions(+), 5 deletions(-) create mode 100644 erpnext/public/js/shop_floor/shop_floor.js diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 0a4026672cf..32ab1f290a4 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -250,6 +250,7 @@ frappe.ui.form.on("Job Card", { change() { const dialog = frm.job_completion_dialog; dialog.set_value("completed_qty", dialog.get_value("for_quantity")); + dialog.set_value("pending_qty", 0); dialog.set_value("process_loss_qty", 0); }, }, @@ -261,8 +262,21 @@ frappe.ui.form.on("Job Card", { default: pending_qty, change() { 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")) { + const remaining = + dialog.get_value("for_quantity") - + dialog.get_value("completed_qty") - + dialog.get_value("process_loss_qty"); + + if (remaining < 0) { + const max_completed_qty = + flt(dialog.get_value("for_quantity")) - flt(dialog.get_value("process_loss_qty")); + dialog.set_value("completed_qty", max_completed_qty); + frappe.throw( + __("Completed Quantity cannot be greater than {0}", [max_completed_qty]) + ); + } + + if (remaining != dialog.get_value("pending_qty")) { dialog.set_value("pending_qty", remaining); } }, @@ -278,7 +292,18 @@ frappe.ui.form.on("Job Card", { 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")) { + + if (process_loss_qty < 0) { + dialog.set_value("pending_qty", 0); + frappe.throw( + __("Pending Quantity cannot be greater than {0}", [ + flt(dialog.get_value("for_quantity")) - + flt(dialog.get_value("completed_qty")), + ]) + ); + } + + if (process_loss_qty != dialog.get_value("process_loss_qty")) { dialog.set_value("process_loss_qty", process_loss_qty); } }, @@ -293,7 +318,18 @@ frappe.ui.form.on("Job Card", { 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")) { + + if (remaining < 0) { + dialog.set_value("process_loss_qty", 0); + frappe.throw( + __("Process Loss Quantity cannot be greater than {0}", [ + flt(dialog.get_value("for_quantity")) - + flt(dialog.get_value("completed_qty")), + ]) + ); + } + + if (remaining != dialog.get_value("pending_qty")) { dialog.set_value("pending_qty", remaining); } }, diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 3b4f8008f08..7ca2c7fb136 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -895,12 +895,13 @@ class JobCard(Document): ) precision = self.precision("total_completed_qty") - total_completed_qty = flt( + accounted_qty = flt( flt(self.total_completed_qty, precision) + flt(self.process_loss_qty, precision) + flt(self.pending_qty, precision) ) +<<<<<<< HEAD if self.for_quantity and flt(total_completed_qty, precision) != flt(self.for_quantity, precision): total_completed_qty_label = bold(_("Total Completed Qty")) qty_to_manufacture = bold(_("Qty to Manufacture")) @@ -911,6 +912,17 @@ class JobCard(Document): bold(flt(total_completed_qty, precision)), qty_to_manufacture, bold(self.for_quantity), +======= + if self.for_quantity and flt(accounted_qty, precision) != flt(self.for_quantity, precision): + frappe.throw( + _( + "Total Completed Qty ({0}), Process Loss Qty ({1}) and Pending Qty ({2}) must add up to the Qty to Manufacture ({3})." + ).format( + bold(flt(self.total_completed_qty, precision)), + bold(flt(self.process_loss_qty, precision)), + bold(flt(self.pending_qty, precision)), + bold(flt(self.for_quantity, precision)), +>>>>>>> 7bffd84482 (fix(job_card): reject a completion split that cannot add up (#57687)) ) ) @@ -1515,6 +1527,7 @@ class JobCard(Document): kwargs = frappe._dict(kwargs) self.validate_complete_job_card_qty(kwargs) + self.set_for_quantity(kwargs) def validate_docstatus(self): if self.docstatus == 2: @@ -1533,9 +1546,36 @@ class JobCard(Document): if flt(kwargs.pending_qty) and flt(kwargs.pending_qty) > self.for_quantity: frappe.throw(_("Pending quantity cannot be greater than the for quantity.")) +<<<<<<< HEAD self.pending_qty = flt(kwargs.pending_qty) self.process_loss_qty = flt(kwargs.process_loss_qty) +======= + self.validate_completion_qty_split(kwargs) + + def validate_completion_qty_split(self, kwargs): + if not flt(kwargs.for_quantity): + return + + precision = self.precision("total_completed_qty") + accounted_qty = flt(kwargs.qty) + flt(kwargs.pending_qty) + flt(kwargs.process_loss_qty) + + if flt(accounted_qty, precision) == flt(kwargs.for_quantity, precision): + return + + frappe.throw( + _( + "Completed Quantity ({0}), Pending Quantity ({1}) and Process Loss Quantity ({2}) must add up to the Qty to Manufacture ({3})." + ).format( + bold(flt(kwargs.qty, precision)), + bold(flt(kwargs.pending_qty, precision)), + bold(flt(kwargs.process_loss_qty, precision)), + bold(flt(kwargs.for_quantity, precision)), + ) + ) + + def add_completion_time_logs(self, kwargs): +>>>>>>> 7bffd84482 (fix(job_card): reject a completion split that cannot add up (#57687)) if kwargs.end_time: self.add_time_logs( to_time=kwargs.end_time, diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 826d558e830..27789e93713 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -1879,3 +1879,94 @@ def create_semi_fg_bom(semi_fg_item, raw_item, inspection_required): bom.append("items", {"item_code": raw_item, "qty": 1}) bom.submit() return bom.name +<<<<<<< HEAD +======= + + +class TestJobCardLogic(ERPNextTestSuite): + """Field-level validations and pure quantity/capacity helpers, exercised on the + document directly so they don't need a Work Order / BOM (the integration suite does).""" + + def test_processing_a_submitted_or_cancelled_card_is_blocked(self): + submitted = frappe.new_doc("Job Card") + submitted.docstatus = 1 + self.assertRaises(frappe.ValidationError, submitted.validate_docstatus) + + cancelled = frappe.new_doc("Job Card") + cancelled.docstatus = 2 + self.assertRaises(frappe.ValidationError, cancelled.validate_docstatus) + + def test_complete_job_card_qty_guards(self): + jc = frappe.new_doc("Job Card") + jc.for_quantity = 5 + jc.validate_complete_job_card_qty(frappe._dict(pending_qty=3)) # within range -> passes + self.assertRaises( + frappe.ValidationError, jc.validate_complete_job_card_qty, frappe._dict(pending_qty=-1) + ) + self.assertRaises( + frappe.ValidationError, jc.validate_complete_job_card_qty, frappe._dict(process_loss_qty=-1) + ) + self.assertRaises( + frappe.ValidationError, jc.validate_complete_job_card_qty, frappe._dict(pending_qty=10) + ) + + def test_completion_qty_split_must_add_up(self): + jc = frappe.new_doc("Job Card") + jc.for_quantity = 5 + + # 3 completed + 2 pending + 0 lost == 5 to manufacture -> passes + jc.validate_complete_job_card_qty( + frappe._dict(for_quantity=5, qty=3, pending_qty=2, process_loss_qty=0) + ) + + self.assertRaises( + frappe.ValidationError, + jc.validate_complete_job_card_qty, + frappe._dict(for_quantity=3, qty=3, pending_qty=2, process_loss_qty=0), + ) + + def test_completed_qty_must_reconcile_with_for_quantity(self): + jc = frappe.new_doc("Job Card") + jc.for_quantity = 10 + jc.total_completed_qty = 6 + jc.process_loss_qty = 0 + jc.pending_qty = 0 + # 6 + 0 + 0 != 10 -> throws + self.assertRaises(frappe.ValidationError, jc.validate_completed_qty_matches_for_quantity) + # completed + loss + pending == for_quantity -> passes + jc.pending_qty = 4 + jc.validate_completed_qty_matches_for_quantity() + + def test_set_process_loss(self): + jc = frappe.new_doc("Job Card") + jc.for_quantity = 10 + jc.total_completed_qty = 6 + jc.pending_qty = 1 + jc.set_process_loss() + self.assertEqual(jc.process_loss_qty, 3) # 10 - 6 - 1 + + # no loss when nothing completed yet + nothing_done = frappe.new_doc("Job Card") + nothing_done.for_quantity = 10 + nothing_done.total_completed_qty = 0 + nothing_done.set_process_loss() + self.assertEqual(nothing_done.process_loss_qty, 0) + + def test_capacity_overlap_detection(self): + jc = frappe.new_doc("Job Card") + sequential = [ + {"from_time": "2026-01-01 10:00:00", "to_time": "2026-01-01 11:00:00"}, + {"from_time": "2026-01-01 11:00:00", "to_time": "2026-01-01 12:00:00"}, + ] + overlapping = [ + {"from_time": "2026-01-01 10:00:00", "to_time": "2026-01-01 11:00:00"}, + {"from_time": "2026-01-01 10:30:00", "to_time": "2026-01-01 11:30:00"}, + ] + # sequential logs share one capacity slot; overlapping logs need two + self.assertEqual(len(jc.get_alloted_capacity(sequential)), 1) + self.assertEqual(len(jc.get_alloted_capacity(overlapping)), 2) + # capacity 1 overlaps with any log; capacity 2 only when both slots are taken + self.assertTrue(jc.has_overlap(1, sequential)) + self.assertFalse(jc.has_overlap(2, sequential)) + self.assertTrue(jc.has_overlap(2, overlapping)) +>>>>>>> 7bffd84482 (fix(job_card): reject a completion split that cannot add up (#57687)) diff --git a/erpnext/public/js/shop_floor/shop_floor.js b/erpnext/public/js/shop_floor/shop_floor.js new file mode 100644 index 00000000000..6e57b77ed7a --- /dev/null +++ b/erpnext/public/js/shop_floor/shop_floor.js @@ -0,0 +1,1747 @@ +// Shop Floor — an immersive, keyboard-first operator/manager interface. +// +// Two experiences share one app shell (see get_shop_floor_context on the server): +// • manager — a paginated board of work orders bucketed Pending / In Progress and Completed. +// Drilling into a work order opens its job cards in the operator pane. +// • operator — a focused workstation/work-order view to start, pause, complete and submit jobs. +// +// The whole surface is driveable from the keyboard (press ? for the cheat sheet) so an operator +// at a terminal never needs the mouse. + +// Job Card status → indicator colour, mirrored from workstation.get_status_color so the manager +// board can paint per-operation chips without a round-trip. +const JC_STATUS_COLORS = { + Completed: "green", + Submitted: "blue", + "Work In Progress": "orange", + "Material Transferred": "yellow", + "On Hold": "red", + Open: "gray", + "Not Started": "gray", +}; + +const MANAGER_BUCKETS = [ + { key: "open", label: __("Pending / In Progress"), dot: "orange" }, + { key: "completed", label: __("Completed"), dot: "green" }, +]; + +const PAGE_LENGTH = 20; + +class ShopFloor { + constructor({ wrapper }, page) { + this.wrapper = $(wrapper); + this.page = page; + this.timer_intervals = {}; + this.capacity = 1; + this.mode = null; + // Remembers each Materials panel's open/closed state (keyed by job card) so it + // survives re-renders — otherwise a reload right after a click resets the panel. + this.materials_open = {}; + // Same idea for the per-operation Work Instructions panel. + this.instructions_open = {}; + + // View state. + this.view = "operator"; // overwritten once context loads + this.active_bucket = "open"; + this.with_job_cards_only = true; // board default: hide WOs that have no job cards + this.buckets = {}; // key -> { rows, total, start, loaded } + this.selected_wo = null; + this.focus_index = -1; + this.op_state = { workstation: null, work_order: null }; + + this.make(); + this.bind_realtime(); + this.bind_lifecycle(); + this.init(); + } + + init() { + frappe.call("erpnext.manufacturing.page.shop_floor.shop_floor.get_shop_floor_context").then((r) => { + const ctx = r.message || {}; + this.view = ctx.role_view === "manager" ? "manager" : "operator"; + this.can_manage = !!ctx.can_manage; + this.user_employee = ctx.user_employee || null; + this.render_shell_controls(); + this.render_view(); + this.bind_keys(); + this.initialized = true; + this.apply_route_options(); + }); + } + + // ── App shell ──────────────────────────────────────────────────────────── + make() { + this.wrapper.append(` + ${this.styles()} +
    +
    +
    +
    +
    + + + + + +
    +
    +
    +
    +
    +
    +
    +
    + `); + + this.app = this.wrapper.find(".sf-app"); + this.brand_icon = `${__(
+			`; + this.topbar_left = this.wrapper.find(".sf-topbar-left"); + this.topbar_center = this.wrapper.find(".sf-topbar-center"); + this.body = this.wrapper.find(".sf-body"); + this.board_container = this.wrapper.find(".sf-board"); + this.detail_container = this.wrapper.find(".sf-detail"); + this.op_container = this.wrapper.find(".sf-operator"); + + this.wrapper.find(".sf-btn-home").on("click", () => (window.location.href = "/app")); + this.wrapper.find(".sf-btn-refresh").on("click", () => this.refresh()); + this.wrapper.find(".sf-btn-scan").on("click", () => this.open_scanner()); + this.wrapper.find(".sf-btn-help").on("click", () => this.show_help()); + this.wrapper.find(".sf-btn-theme").on("click", () => this.toggle_theme()); + this.update_theme_button(); + } + + // Kiosk-friendly light/dark switch: flips the standard desk theme and persists it on the + // User (same as the Ctrl+Shift+G switcher), so the choice survives reloads and follows the + // operator's login on any device. + toggle_theme() { + const next = frappe.ui.get_current_theme() === "dark" ? "light" : "dark"; + document.documentElement.setAttribute("data-theme-mode", next); + frappe.ui.set_theme(next); + frappe.xcall("frappe.core.doctype.user.user.switch_theme", { + theme: next.charAt(0).toUpperCase() + next.slice(1), + }); + this.update_theme_button(); + } + + update_theme_button() { + const dark = frappe.ui.get_current_theme() === "dark"; + this.wrapper + .find(".sf-btn-theme") + .html(dark ? "☀" : "☾") + .attr("title", dark ? __("Switch to Light Theme") : __("Switch to Dark Theme")); + } + + render_shell_controls() { + this.topbar_left.empty(); + this.topbar_center.empty(); + + // View toggle — only managers can flip between the board and a bare operator view. + const toggle = this.can_manage + ? `
    + + +
    ` + : ""; + + if (this.view === "manager") { + this.topbar_left.html(` + ${this.brand_icon}${__("Shop Floor")} + ${toggle} +
    + ${MANAGER_BUCKETS.map( + (b) => `` + ).join("")} +
    + `); + this.topbar_center.html(` + + + `); + + this.topbar_left.find(".sf-tab").on("click", (e) => { + this.switch_bucket($(e.currentTarget).attr("data-bucket")); + }); + let timer = null; + this.topbar_center.find(".sf-search-input").on("input", (e) => { + const val = e.target.value; + clearTimeout(timer); + timer = setTimeout(() => this.search_work_orders(val), 300); + }); + this.topbar_center.find(".sf-jc-toggle").on("change", (e) => { + this.toggle_job_cards_only(e.target.checked); + }); + } else { + this.topbar_left.html( + `${this.brand_icon}${__("Shop Floor")}${toggle}` + ); + this.build_operator_filters(); + } + + this.topbar_left.find(".sf-view-btn").on("click", (e) => { + this.set_view($(e.currentTarget).attr("data-view")); + }); + } + + build_operator_filters() { + this.topbar_center.html('
    '); + const $filters = this.topbar_center.find(".sf-filters"); + + this.workstation_filter = frappe.ui.form.make_control({ + df: { + fieldtype: "Link", + options: "Workstation", + fieldname: "workstation", + placeholder: __("Machine"), + onchange: () => this.load_operator(), + }, + parent: $filters, + render_input: true, + }); + this.workstation_filter.$wrapper.addClass("sf-filter-control"); + + this.work_order_filter = frappe.ui.form.make_control({ + df: { + fieldtype: "Link", + options: "Work Order", + fieldname: "work_order", + placeholder: __("Work Order"), + onchange: () => this.load_operator(), + }, + parent: $filters, + render_input: true, + }); + this.work_order_filter.$wrapper.addClass("sf-filter-control"); + } + + set_view(view) { + if (!view || view === this.view) return; + this.view = view; + this.selected_wo = null; + this.focus_index = -1; + this.render_shell_controls(); + this.render_view(); + } + + render_view() { + const manager = this.view === "manager"; + this.board_container.toggle(manager); + this.detail_container.toggle(manager && !!this.selected_wo); + this.op_container.toggle(!manager); + this.body.toggleClass("detail-open", manager && !!this.selected_wo); + + if (manager) { + this.load_bucket(this.active_bucket); + } else { + this.load_operator(); + } + } + + // ── Manager board ──────────────────────────────────────────────────────── + switch_bucket(bucket) { + if (!bucket || bucket === this.active_bucket) return; + this.active_bucket = bucket; + this.selected_wo = null; + this.focus_index = -1; + this.topbar_left.find(".sf-tab").removeClass("active"); + this.topbar_left.find(`.sf-tab[data-bucket="${bucket}"]`).addClass("active"); + this.detail_container.hide(); + this.body.removeClass("detail-open"); + this.load_bucket(bucket); + } + + search_work_orders(term) { + this.search_term = term; + // Re-query every bucket from scratch on the next visit; reload the active one now. + this.buckets = {}; + this.load_bucket(this.active_bucket); + } + + toggle_job_cards_only(checked) { + this.with_job_cards_only = !!checked; + // Filter changes every bucket's contents + counts; drop caches and clear stale counts. + this.buckets = {}; + this.topbar_left.find(".sf-tab-count").text(""); + this.load_bucket(this.active_bucket); + } + + load_bucket(bucket, append = false) { + const state = this.buckets[bucket] || { rows: [], total: 0, start: 0, loaded: false }; + const start = append ? state.start : 0; + + frappe.call({ + method: "erpnext.manufacturing.page.shop_floor.shop_floor.get_work_orders", + args: { + status_group: bucket, + start: start, + page_length: PAGE_LENGTH, + search: this.search_term || null, + with_job_cards_only: this.with_job_cards_only ? 1 : 0, + }, + callback: (r) => { + const data = r.message || {}; + const rows = data.work_orders || []; + this.buckets[bucket] = { + rows: append ? state.rows.concat(rows) : rows, + total: cint(data.total), + start: start + rows.length, + loaded: true, + }; + this.update_tab_count(bucket); + if (bucket === this.active_bucket) this.render_board(); + }, + }); + } + + update_tab_count(bucket) { + const state = this.buckets[bucket]; + if (!state) return; + this.topbar_left.find(`[data-bucket-count="${bucket}"]`).text(state.total ? state.total : ""); + } + + render_board() { + const state = this.buckets[this.active_bucket] || { rows: [], total: 0 }; + this.focus_index = -1; + + if (!state.rows.length) { + this.board_container.html(`
    ${__("No work orders here.")}
    `); + return; + } + + const cards = state.rows.map((wo) => this.work_order_card(wo)).join(""); + const more = + state.rows.length < state.total + ? `` + : `
    ${__("Showing all {0}", [state.total])}
    `; + + this.board_container.html( + `
    ${cards}
    ${more}
    ` + ); + + this.board_container.find(".sf-wo-card").on("click", (e) => { + this.open_wo($(e.currentTarget).attr("data-name")); + }); + this.board_container + .find(".sf-load-more") + .on("click", () => this.load_bucket(this.active_bucket, true)); + } + + work_order_card(wo) { + const item = wo.item_name || wo.production_item; + + // Hero image = the current operation's workstation. No item-image fallback — when the + // workstation has no image uploaded we show its initials, never the product image. + const image = wo.workstation_image + ? `` + : `${frappe.get_abbr(wo.workstation_name || item, 2)}`; + + const workstation_line = wo.workstation_name + ? `
    🏭 ${frappe.utils.escape_html( + wo.workstation_name + )}${wo.current_operation ? ` · ${frappe.utils.escape_html(wo.current_operation)}` : ""}
    ` + : ""; + + // Operations bar: green segment (done) + orange segment (in progress); grey track = pending. + const done_pct = Math.min(cint(wo.per_operations), 100); + const wip_pct = Math.min(cint(wo.per_in_progress), 100 - done_pct); + + return ` +
    + +
    +
    + ${__("Operations")} + ${cint(wo.completed_operations)} / ${cint(wo.total_operations)} +
    +
    +
    +
    +
    +
    +
    + `; + } + + open_wo(name) { + if (!name) return; + this.selected_wo = name; + this.op_state = { workstation: null, work_order: name }; + this.detail_container.show(); + this.body.addClass("detail-open"); + this.board_container + .find(".sf-wo-card") + .removeClass("sf-selected") + .filter(`[data-name="${name}"]`) + .addClass("sf-selected"); + // The detail pane reuses the operator rendering for a single work order. + this.detail_container.html(` +
    + + ${frappe.utils.escape_html(name)} + ${__("Open")} +
    +
    + `); + this.detail_container.find(".sf-detail-back").on("click", () => this.close_wo()); + this.op_container_target = this.detail_container.find(".sf-detail-body"); + this.load_operator_data(this.op_container_target, { work_order: name }); + } + + close_wo() { + this.selected_wo = null; + this.op_container_target = null; + this.detail_container.hide().empty(); + this.body.removeClass("detail-open"); + this.board_container.find(".sf-wo-card").removeClass("sf-selected"); + } + + // ── Operator pane ────────────────────────────────────────────────────────── + // Resolves the container the operator content renders into: the standalone operator + // view, or the manager's drill-down detail pane. + current_op_container() { + return this.view === "manager" ? this.op_container_target : this.op_container; + } + + load_operator() { + const workstation = this.workstation_filter ? this.workstation_filter.get_value() : null; + const work_order = this.work_order_filter ? this.work_order_filter.get_value() : null; + this.op_state = { workstation, work_order }; + + if (!workstation && !work_order) { + this.clear_timers(); + this.op_container.html( + `
    ${__("Select a machine or work order to begin")}
    ` + ); + return; + } + this.load_operator_data(this.op_container, { workstation, work_order }); + } + + load_operator_data($container, { workstation, work_order }) { + frappe.call({ + method: "erpnext.manufacturing.page.shop_floor.shop_floor.get_data", + args: { + workstation: work_order ? null : workstation, + work_order: work_order || null, + }, + callback: (r) => { + const data = r.message || {}; + this.job_cards = data.job_cards || []; + this.capacity = cint(data.capacity) || 1; + this.mode = data.mode || (work_order ? "work_order" : "workstation"); + this.oee = data.oee || null; + if (data.user_employee) this.user_employee = data.user_employee; + this.today_sessions = data.today_sessions || []; + this.workstation = workstation; + this.work_order = work_order; + this.compute_state(); + this.dedupe_today_sessions(); + this.render_operator($container); + }, + }); + } + + // A job card already shown under Completed Operations shouldn't repeat in + // Today's Sessions — keep it in Completed Operations only. + dedupe_today_sessions() { + const shown = new Set((this.completed || []).map((jc) => jc.name)); + this.today_sessions = (this.today_sessions || []).filter((s) => !shown.has(s.name)); + } + + // Re-fetch whichever operator content is currently on screen (used after every action). + reload() { + if (this.view === "manager" && this.selected_wo) { + this.load_operator_data(this.op_container_target, { work_order: this.selected_wo }); + // Keep the board chips fresh too. + this.buckets = {}; + this.load_bucket(this.active_bucket); + } else if (this.view === "manager") { + this.load_bucket(this.active_bucket); + } else { + this.load_operator(); + } + } + + refresh() { + if (this.view === "manager") { + this.buckets = {}; + } + this.reload(); + } + + compute_state() { + this.active_jobs = []; + this.queue = []; + this.pending_submission = []; + this.completed = []; + // Submitted but the finished goods aren't booked yet (status "To Manufacture") — its own + // actionable section, kept out of Completed Operations / Today's Sessions. + this.to_manufacture = []; + + for (const jc of this.job_cards) { + // Same materials-ready rule as job_card.js make_dashboard. + jc._materials_ready = !!( + jc.skip_material_transfer || + flt(jc.transferred_qty) >= flt(jc.for_quantity) + flt(jc.process_loss_qty) || + !jc.finished_good + ); + + // Submitted JCs are historical from the Shop Floor's POV — only appear here in work_order + // mode (and, for "To Manufacture", in workstation mode too — see _fetch_job_cards). + if (jc.docstatus === 1) { + if (jc.status === "To Manufacture") { + this.to_manufacture.push(jc); + } else { + this.completed.push(jc); + } + continue; + } + + const last_log = + jc.time_logs && jc.time_logs.length ? jc.time_logs[jc.time_logs.length - 1] : null; + const is_running = last_log && !last_log.to_time && !jc.is_paused; + const is_paused = jc.is_paused; + + if (is_running || is_paused) { + this.active_jobs.push(jc); + } else if (jc.status === "Completed") { + // All qty accounted for but still draft — waiting on Submit. + this.pending_submission.push(jc); + } else { + this.queue.push(jc); + } + } + + // Slot rules — all active jobs are always shown; the grid (col-md-6) wraps them 2 per row. + // workstation mode: capacity-many slots, expanded to fit every active job (+ empty placeholders). + // work_order mode: one slot per active job (no empty placeholders). + let slot_count; + if (this.mode === "work_order") { + slot_count = this.active_jobs.length; + } else { + slot_count = Math.max(this.capacity, this.active_jobs.length, 1); + } + + this.slots = []; + for (let i = 0; i < slot_count; i++) { + this.slots.push(this.active_jobs[i] || null); + } + + // Auto-pick: when nothing is running, surface the next queue item in the slot. + if (this.active_jobs.length === 0 && this.queue.length > 0) { + const next_up = this.queue.shift(); + next_up._is_next_up = true; + this.slots[0] = next_up; + } + + this.summary = { + active_count: this.active_jobs.length, + // "To Manufacture" (submitted, qty done, but the Manufacture Stock Entry is still pending) + // isn't actually finished — count it as Pending, not Completed. + queue_count: this.queue.length + this.to_manufacture.length, + completed_count: this.completed.length + this.pending_submission.length, + capacity: this.capacity, + }; + } + + render_operator($container) { + this.clear_timers(); + $container.empty(); + + const html = frappe.render_template("shop_floor_template", { + workstation: this.workstation, + work_order: this.work_order, + mode: this.mode, + slots: this.slots, + active_jobs: this.active_jobs, + queue: this.queue, + pending_submission: this.pending_submission, + to_manufacture: this.to_manufacture, + completed: this.completed, + today_sessions: this.today_sessions || [], + summary: this.summary, + oee: this.oee, + }); + $container.html(html); + + // Restore each Materials panel to its remembered open/closed state. + $container.find(".mes-materials-inline").each((i, el) => { + const $el = $(el); + const name = $el.attr("data-job-card"); + if (!name) return; + if (name in this.materials_open) { + $el.toggleClass("is-open", this.materials_open[name]); + } else { + this.materials_open[name] = $el.hasClass("is-open"); + } + }); + + // Restore each Work Instructions panel to its remembered open/closed state. + $container.find(".mes-instructions-inline").each((i, el) => { + const $el = $(el); + const name = $el.attr("data-job-card"); + if (name && name in this.instructions_open) { + $el.toggleClass("is-open", this.instructions_open[name]); + } + }); + + this.bind_events($container); + + for (const jc of this.active_jobs) { + if (jc.is_paused) { + this.render_timer(jc.name, this.elapsed_seconds(jc), $container); + } else { + this.start_timer_for(jc, $container); + } + } + } + + clear_timers() { + for (const id of Object.values(this.timer_intervals)) { + clearInterval(id); + } + this.timer_intervals = {}; + } + + bind_events($container) { + const me = this; + + $container.find(".mes-materials-summary").on("click", function (e) { + if ($(e.target).closest(".mes-btn-transfer").length) return; + const $inline = $(this).closest(".mes-materials-inline"); + const open = !$inline.hasClass("is-open"); + $inline.toggleClass("is-open", open); + const name = $inline.attr("data-job-card"); + if (name) me.materials_open[name] = open; + }); + + $container.find(".mes-instructions-summary").on("click", function () { + const $inline = $(this).closest(".mes-instructions-inline"); + const open = !$inline.hasClass("is-open"); + $inline.toggleClass("is-open", open); + const name = $inline.attr("data-job-card"); + if (name) me.instructions_open[name] = open; + }); + + // Clicking a "QC Required" / "QC Available" pill runs the inline check ahead of End Session. + $container.find(".mes-qc-pill").on("click", function () { + const name = $(this).attr("data-job-card"); + const jc = (me.active_jobs || []).find((j) => j.name === name); + if (jc) me.run_quality_check(jc, () => me.reload()); + }); + + $container.find(".mes-btn-start").on("click", function () { + me.start_job($(this).attr("data-job-card")); + }); + $container.find(".mes-btn-pause").on("click", function () { + me.pause_job($(this).attr("data-job-card")); + }); + $container.find(".mes-btn-resume").on("click", function () { + me.resume_job($(this).attr("data-job-card")); + }); + $container.find(".mes-btn-end-session").on("click", function () { + me.end_session($(this).attr("data-job-card")); + }); + $container.find(".mes-btn-submit").on("click", function () { + me.submit_job_card($(this).attr("data-job-card")); + }); + $container.find(".mes-btn-make-entry").on("click", function () { + me.make_manufacture_entry($(this).attr("data-job-card")); + }); + $container.find(".mes-btn-transfer").on("click", function (e) { + e.preventDefault(); + me.transfer_materials($(this).attr("data-job-card")); + }); + } + + // ── Operator actions (unchanged behaviour, reload() instead of load()) ───── + start_job(job_card) { + const me = this; + if (this.mode === "workstation" && this.active_jobs.length >= this.capacity) { + frappe.msgprint({ + title: __("Capacity Reached"), + message: __( + "This machine can run at most {0} job(s) in parallel. Pause or complete a running job before starting another.", + [this.capacity] + ), + indicator: "orange", + }); + return; + } + + const default_employee = this.user_employee; + const dialog = new frappe.ui.Dialog({ + title: __("Start Job"), + fields: [ + { + label: __("Start Time"), + fieldname: "start_time", + fieldtype: "Datetime", + default: frappe.datetime.now_datetime(), + }, + { fieldtype: "Section Break" }, + { + label: __("Employees"), + fieldname: "employees", + fieldtype: "Table", + data: default_employee ? [{ employee: default_employee }] : [], + fields: [ + { + label: __("Employee"), + fieldname: "employee", + fieldtype: "Link", + options: "Employee", + in_list_view: 1, + }, + ], + }, + ], + primary_action_label: __("Start"), + primary_action: (values) => { + dialog.hide(); + me.update_job_card(job_card, "start_timer", { + start_time: values.start_time, + employees: values.employees || [], + }); + }, + }); + dialog.show(); + this.bind_enter_submit(dialog); + } + + // Make a dialog fully keyboard-operable: Enter triggers the primary action, so an operator + // never has to reach for the mouse. Enter is left alone inside multi-line fields and while an + // autocomplete (Link/Select) dropdown is open, so it can still pick a value. + bind_enter_submit(dialog) { + dialog.$wrapper.on("keydown.sfenter", (e) => { + if (e.key !== "Enter" || e.shiftKey) return; + if ($(e.target).is("textarea")) return; + if ($(".awesomplete > ul:not([hidden])").length) return; + const $btn = dialog.get_primary_btn(); + if ( + $btn && + $btn.length && + $btn.is(":visible") && + !$btn.hasClass("disabled") && + !$btn.prop("disabled") + ) { + e.preventDefault(); + e.stopPropagation(); + $btn.trigger("click"); + } + }); + } + + pause_job(jc_name) { + this.update_job_card(jc_name, "pause_job", { end_time: frappe.datetime.now_datetime() }); + } + + resume_job(jc_name) { + this.update_job_card(jc_name, "resume_job", { start_time: frappe.datetime.now_datetime() }); + } + + end_session(jc_name) { + const me = this; + const jc = this.active_jobs.find((j) => j.name === jc_name); + if (!jc) return; + + let pending = flt(jc.for_quantity) - flt(jc.total_completed_qty); + if (flt(jc.pending_qty) > 0) { + pending = flt(jc.pending_qty); + } + + const fields = [ + { + fieldtype: "Float", + label: __("Qty to Manufacture"), + fieldname: "for_quantity", + reqd: 1, + default: pending, + change() { + const d = me.session_dialog; + d.set_value("completed_qty", d.get_value("for_quantity")); + d.set_value("pending_qty", 0); + d.set_value("process_loss_qty", 0); + }, + }, + { + fieldtype: "Float", + label: __("Completed Quantity"), + fieldname: "completed_qty", + reqd: 1, + default: pending, + change() { + const d = me.session_dialog; + const remaining = + flt(d.get_value("for_quantity")) - + flt(d.get_value("completed_qty")) - + flt(d.get_value("process_loss_qty")); + + if (remaining < 0) { + const max_completed_qty = + flt(d.get_value("for_quantity")) - flt(d.get_value("process_loss_qty")); + d.set_value("completed_qty", max_completed_qty); + frappe.throw( + __("Completed Quantity cannot be greater than {0}", [max_completed_qty]) + ); + } + + if (remaining !== flt(d.get_value("pending_qty"))) { + d.set_value("pending_qty", remaining); + } + }, + }, + { + fieldtype: "Float", + label: __("Pending Quantity"), + fieldname: "pending_qty", + default: 0.0, + change() { + const d = me.session_dialog; + const pl = + flt(d.get_value("for_quantity")) - + flt(d.get_value("completed_qty")) - + flt(d.get_value("pending_qty")); + + if (pl < 0) { + d.set_value("pending_qty", 0); + frappe.throw( + __("Pending Quantity cannot be greater than {0}", [ + flt(d.get_value("for_quantity")) - flt(d.get_value("completed_qty")), + ]) + ); + } + + if (pl !== flt(d.get_value("process_loss_qty"))) { + d.set_value("process_loss_qty", pl); + } + }, + }, + { + fieldtype: "Float", + label: __("Process Loss Quantity"), + fieldname: "process_loss_qty", + default: 0.0, + change() { + const d = me.session_dialog; + const remaining = + flt(d.get_value("for_quantity")) - + flt(d.get_value("completed_qty")) - + flt(d.get_value("process_loss_qty")); + + if (remaining < 0) { + d.set_value("process_loss_qty", 0); + frappe.throw( + __("Process Loss Quantity cannot be greater than {0}", [ + flt(d.get_value("for_quantity")) - flt(d.get_value("completed_qty")), + ]) + ); + } + + if (remaining !== flt(d.get_value("pending_qty"))) { + d.set_value("pending_qty", remaining); + } + }, + }, + { fieldtype: "Section Break" }, + { + fieldtype: "Datetime", + label: __("End Time"), + fieldname: "end_time", + default: frappe.datetime.now_datetime(), + }, + ]; + + const get_payload = () => { + const data = me.session_dialog.get_values(); + if (!data) return null; + if (flt(data.completed_qty) <= 0) { + frappe.throw(__("Completed Quantity should be greater than 0")); + } + return { + job_card: jc.name, + qty: flt(data.completed_qty), + for_quantity: flt(data.for_quantity), + pending_qty: flt(data.pending_qty), + process_loss_qty: flt(data.process_loss_qty), + end_time: data.end_time, + }; + }; + + const save_and_continue = () => { + const args = get_payload(); + if (!args) return; + me.session_dialog.hide(); + frappe.call({ + method: "erpnext.manufacturing.page.shop_floor.shop_floor.save_and_continue", + args: args, + freeze: true, + freeze_message: __("Saving job card..."), + callback: () => me.reload(), + }); + }; + + const finalize_submit = (args) => { + frappe.call({ + method: "erpnext.manufacturing.page.shop_floor.shop_floor.complete_and_submit", + args: args, + freeze: true, + freeze_message: __("Submitting job card..."), + callback: (r) => { + me.reload(); + if (r.message && r.message.finished_good) { + me.prompt_manufacture_entry(jc.name); + } + }, + }); + }; + + const submit_session = () => { + const args = get_payload(); + if (!args) return; + me.session_dialog.hide(); + // Guided QC gate: a job card that requires inspection must pass an inline Quality Check + // before it is submitted (mirrors Job Card.validate_inspection on the server). Once the + // inspection is recorded, finalize the session submit. + if (jc.qc && jc.qc.required && jc.qc.status !== "Accepted") { + me.run_quality_check(jc, () => finalize_submit(args)); + } else { + finalize_submit(args); + } + }; + + me.session_dialog = new frappe.ui.Dialog({ + title: __("End Session"), + fields: fields, + primary_action_label: __("Submit"), + primary_action: submit_session, + secondary_action_label: __("Save & Continue"), + secondary_action: save_and_continue, + }); + me.session_dialog.show(); + me.bind_enter_submit(me.session_dialog); + } + + // ── Inline Quality Check ───────────────────────────────────────────────────── + // Fetch the operation's Quality Inspection template and open a guided pass/fail checklist. + // `on_pass` runs once the inspection has been recorded (and is not rejected). + run_quality_check(jc, on_pass) { + const me = this; + frappe.call({ + method: "erpnext.manufacturing.page.shop_floor.shop_floor.get_quality_inspection_checklist", + args: { job_card: jc.name }, + freeze: true, + freeze_message: __("Loading quality checklist..."), + callback: (r) => { + const info = r.message || {}; + if (!info.template || !(info.parameters || []).length) { + // Inspection is required but the operation has no template/parameters to fill — + // there is nothing to capture inline. Point the user at the configuration. + frappe.msgprint({ + title: __("Quality Inspection Template Missing"), + indicator: "orange", + message: __( + "This operation requires a Quality Inspection but no template with parameters is configured. Set a Quality Inspection Template on Operation {0} to inspect from the Shop Floor.", + [jc.operation || ""] + ), + }); + return; + } + me.show_qc_dialog(jc, info, on_pass); + }, + }); + } + + show_qc_dialog(jc, info, on_pass) { + const me = this; + const params = info.parameters || []; + // Per-row operator input, keyed by row index (avoids escaping issues with parameter names). + const state = {}; // idx -> "Accepted" | "Rejected" + + const rows = params + .map((p, i) => { + const spec = frappe.utils.escape_html(p.specification); + let criteria = ""; + if (p.numeric) { + const lo = p.min_value !== null && p.min_value !== undefined ? p.min_value : "−∞"; + const hi = p.max_value !== null && p.max_value !== undefined ? p.max_value : "∞"; + criteria = __("Acceptable range: {0} to {1}", [lo, hi]); + } else if (p.value) { + criteria = __("Expected: {0}", [frappe.utils.escape_html(p.value)]); + } + const control = p.numeric + ? `` + : ` + + + `; + return `
    +
    +
    ${spec}
    + ${criteria ? `
    ${criteria}
    ` : ""} +
    +
    ${control}
    +
    `; + }) + .join(""); + + const dialog = new frappe.ui.Dialog({ + title: __("Quality Check"), + size: "large", + fields: [ + { + fieldtype: "HTML", + options: `
    ${__( + "Inspect {0} for job card {1}", + [frappe.utils.escape_html(info.item_code || ""), frappe.utils.escape_html(jc.name)] + )}
    ${rows}
    `, + }, + ], + primary_action_label: __("Submit Inspection"), + primary_action: () => { + const readings = []; + let missing = false; + params.forEach((p, i) => { + if (p.numeric) { + const val = dialog.$wrapper.find(`.mes-qc-reading[data-idx="${i}"]`).val(); + if (val === "" || val === undefined || val === null) missing = true; + readings.push({ specification: p.specification, reading_value: val }); + } else { + if (!state[i]) missing = true; + readings.push({ + specification: p.specification, + status: state[i], + reading_value: "", + }); + } + }); + if (missing) { + frappe.msgprint(__("Please complete every check before submitting the inspection.")); + return; + } + dialog.hide(); + frappe.call({ + method: "erpnext.manufacturing.page.shop_floor.shop_floor.submit_quality_inspection", + args: { job_card: jc.name, readings: JSON.stringify(readings) }, + freeze: true, + freeze_message: __("Recording inspection..."), + callback: (r) => { + const res = r.message || {}; + if (res.status === "Rejected") { + // Don't auto-proceed on a rejected inspection — the server gate may block the + // submit anyway (per Stock Settings), and the operator should decide next steps. + frappe.msgprint({ + title: __("Inspection Rejected"), + indicator: "red", + message: __( + "Quality Inspection {0} is Rejected. Resolve the issue or follow your rejection process before submitting the job card.", + [res.name || ""] + ), + }); + me.reload(); + return; + } + if (on_pass) on_pass(); + }, + }); + }, + }); + + dialog.show(); + // Pass/Fail toggles for qualitative parameters. + dialog.$wrapper.find(".mes-qc-passfail button").on("click", function () { + const $btn = $(this); + const $grp = $btn.closest(".mes-qc-passfail"); + $grp.find("button").removeClass("active"); + $btn.addClass("active"); + state[$grp.attr("data-idx")] = $btn.attr("data-val"); + }); + } + + prompt_manufacture_entry(jc_name) { + const me = this; + const dialog = new frappe.ui.Dialog({ + title: __("Job Card Submitted"), + fields: [ + { + fieldtype: "HTML", + options: ` +
    +
    + ${__("Job card {0} has been submitted.", [frappe.utils.escape_html(jc_name)])} +
    +
    + ${__("Create a Manufacture stock entry for the finished goods?")} +
    +
    + `, + }, + ], + primary_action_label: __("Make Manufacture Entry"), + primary_action: () => { + dialog.hide(); + me.make_manufacture_entry(jc_name); + }, + secondary_action_label: __("Skip"), + secondary_action: () => dialog.hide(), + }); + dialog.show(); + this.bind_enter_submit(dialog); + } + + submit_job_card(jc_name) { + const me = this; + frappe.confirm(__("Submit job card {0}? This finalizes the job card.", [jc_name]), () => { + frappe.call({ + method: "erpnext.manufacturing.page.shop_floor.shop_floor.submit_job_card", + args: { job_card: jc_name }, + freeze: true, + freeze_message: __("Submitting job card..."), + callback: () => me.reload(), + }); + }); + } + + make_manufacture_entry(jc_name) { + frappe.call({ + method: "erpnext.manufacturing.page.shop_floor.shop_floor.make_manufacture_stock_entry", + args: { job_card: jc_name }, + freeze: true, + freeze_message: __("Preparing stock entry..."), + callback: (r) => { + if (r.message && r.message.name) { + window.open(`/app/stock-entry/${encodeURIComponent(r.message.name)}`, "_blank"); + } + }, + }); + } + + transfer_materials(jc_name) { + if (!jc_name) return; + frappe.call({ + method: "erpnext.manufacturing.doctype.job_card.job_card.make_stock_entry", + args: { source_name: jc_name }, + callback: (r) => { + const doc = frappe.model.sync(r.message); + frappe.set_route("Form", doc[0].doctype, doc[0].name); + }, + }); + } + + update_job_card(job_card, method, data, on_success) { + const me = this; + frappe.call({ + method: "erpnext.manufacturing.doctype.workstation.workstation.update_job_card", + args: { + job_card: job_card, + method: method, + start_time: data.start_time || "", + employees: data.employees || [], + end_time: data.end_time || "", + qty: data.qty || 0, + for_quantity: data.for_quantity || 0, + pending_qty: data.pending_qty || 0, + process_loss_qty: data.process_loss_qty || 0, + auto_submit: data.auto_submit || 0, + }, + freeze: true, + freeze_message: __("Updating job card..."), + callback: () => { + me.reload(); + if (on_success) on_success(); + }, + }); + } + + // ── Timers ──────────────────────────────────────────────────────────────── + start_timer_for(jc, $container) { + let elapsed = this.elapsed_seconds(jc); + this.render_timer(jc.name, elapsed, $container); + this.timer_intervals[jc.name] = setInterval(() => { + elapsed += 1; + this.render_timer(jc.name, elapsed, $container); + }, 1000); + } + + elapsed_seconds(jc) { + let total = 0; + for (const log of jc.time_logs || []) { + if (log.to_time) { + if (log.time_in_mins) { + total += flt(log.time_in_mins, 2) * 60; + } else { + total += moment(log.to_time).diff(log.from_time, "seconds"); + } + } else { + total += moment().diff(log.from_time, "seconds"); + } + } + return total; + } + + render_timer(jc_name, seconds, $container) { + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds - h * 3600) / 60); + const s = cint(seconds - h * 3600 - m * 60); + const pad = (n) => (n < 10 ? "0" + n : String(n)); + + const scope = $container || this.wrapper; + const timer = scope.find(`.mes-job-timer[data-job-card="${jc_name}"]`); + timer.find(".h").text(pad(h)); + timer.find(".m").text(pad(m)); + timer.find(".s").text(pad(s)); + } + + // ── Realtime + lifecycle ─────────────────────────────────────────────────── + bind_realtime() { + frappe.realtime.on("update_workstation_status", (data) => { + if (data && data.name === this.op_state.workstation) { + this.reload(); + } + }); + } + + bind_lifecycle() { + // Frappe has no on_page_hide hook, so toggle immersive mode + keyboard binding on + // route changes ourselves. + this._route_handler = () => { + const on_page = (frappe.get_route_str() || "").startsWith("shop-floor"); + if (on_page) { + $(document.body).addClass("shop-floor-active"); + this.bind_keys(); + } else { + $(document.body).removeClass("shop-floor-active"); + this.unbind_keys(); + this.clear_timers(); + } + }; + frappe.router.on("change", this._route_handler); + } + + on_show() { + $(document.body).addClass("shop-floor-active"); + this.bind_keys(); + // Cached re-navigation (e.g. the Work Order "Shop Floor" button) lands here with fresh + // route_options; init() handles the very first load before we're initialized. + if (this.initialized) this.apply_route_options(); + } + + // ── Keyboard ──────────────────────────────────────────────────────────────── + bind_keys() { + $(document).off("keydown.shopfloor"); + $(document).on("keydown.shopfloor", (e) => this.handle_key(e)); + } + + unbind_keys() { + $(document).off("keydown.shopfloor"); + } + + is_typing(e) { + const tag = (e.target.tagName || "").toLowerCase(); + return tag === "input" || tag === "textarea" || tag === "select" || e.target.isContentEditable; + } + + handle_key(e) { + // Let dialogs own the keyboard while open. + if ($(".modal:visible").length) return; + + const typing = this.is_typing(e); + + // Escape works even while typing (blur the search / close the detail pane). + if (e.key === "Escape") { + if (typing) { + e.target.blur(); + return; + } + if (this.view === "manager" && this.selected_wo) { + this.close_wo(); + e.preventDefault(); + } + return; + } + + if (typing) return; + + switch (e.key) { + case "?": + this.show_help(); + e.preventDefault(); + return; + case "/": + this.topbar_center.find(".sf-search-input").focus(); + e.preventDefault(); + return; + case "r": + this.refresh(); + e.preventDefault(); + return; + case "b": + this.open_scanner(); + e.preventDefault(); + return; + case "1": + case "2": + if (this.view === "manager" && MANAGER_BUCKETS[cint(e.key) - 1]) { + this.switch_bucket(MANAGER_BUCKETS[cint(e.key) - 1].key); + e.preventDefault(); + } + return; + } + + // View switch chord: "g" then "m"/"o". + if (e.key === "g") { + this._g_pending = true; + setTimeout(() => (this._g_pending = false), 600); + return; + } + if (this._g_pending && (e.key === "m" || e.key === "o")) { + this._g_pending = false; + if (this.can_manage) this.set_view(e.key === "m" ? "manager" : "operator"); + return; + } + + // Navigation. + if (e.key === "ArrowDown" || e.key === "j") { + this.move_focus(1); + e.preventDefault(); + return; + } + if (e.key === "ArrowUp" || e.key === "k") { + this.move_focus(-1); + e.preventDefault(); + return; + } + if (e.key === "Enter") { + this.activate_focus(); + e.preventDefault(); + return; + } + + // Job actions on the focused card — reuse the rendered buttons. + const map = { + s: ".mes-btn-start, .mes-btn-resume", + p: ".mes-btn-pause, .mes-btn-resume", + e: ".mes-btn-end-session", + t: ".mes-btn-transfer", + }; + if (e.key === "S" && e.shiftKey) { + this.click_job_action(".mes-btn-submit"); + e.preventDefault(); + return; + } + if (map[e.key]) { + this.click_job_action(map[e.key]); + e.preventDefault(); + } + } + + // Job actions act on the focused job card (operator view); when the focus is on a board + // work order (manager view with the detail open) they fall back to the detail's active job. + click_job_action(selector) { + const $el = this.focused_el(); + if ($el && $el.attr("data-kind") === "job") { + const $btn = $el.find(selector).filter(":visible").first(); + if ($btn.length) { + $btn.trigger("click"); + return; + } + } + const scope = this.current_op_container(); + if (scope && scope.length) { + const $btn = scope.find(selector).filter(":visible").first(); + if ($btn.length) $btn.trigger("click"); + } + } + + focusables() { + // Manager always navigates the board work orders — even with the detail open, so the + // arrow keys switch work orders. The standalone operator view navigates its job cards. + const scope = this.view === "manager" ? this.board_container : this.current_op_container(); + if (!scope || !scope.length) return $(); + return scope.find("[data-sf-focusable]"); + } + + move_focus(delta) { + const $items = this.focusables(); + if (!$items.length) return; + this.focus_index = Math.max(0, Math.min($items.length - 1, this.focus_index + delta)); + $items.removeClass("sf-focused"); + const $target = $items.eq(this.focus_index); + $target.addClass("sf-focused"); + $target[0].scrollIntoView({ block: "nearest", behavior: "smooth" }); + // Browsing work orders with the detail already open → switch the detail to the focused one. + if (this.view === "manager" && this.selected_wo && $target.attr("data-kind") === "wo") { + this.open_wo($target.attr("data-name")); + } + } + + focused_el() { + const $items = this.focusables(); + if (this.focus_index < 0 || this.focus_index >= $items.length) return null; + return $items.eq(this.focus_index); + } + + activate_focus() { + const $el = this.focused_el(); + if (!$el) return; + if ($el.attr("data-kind") === "wo") { + this.open_wo($el.attr("data-name")); + } else { + // First visible primary button drives the job card (Start / Resume / End Session). + const $btn = $el.find(".btn-primary:visible").first(); + if ($btn.length) $btn.trigger("click"); + } + } + + show_help() { + const rows = [ + ["?", __("Show this help")], + ["/", __("Search work orders")], + ["r", __("Refresh")], + ["b", __("Scan job card")], + ["g then m / o", __("Switch Board / Operator view")], + ["1 / 2", __("Switch board tab")], + ["↑ / ↓ or j / k", __("Move selection")], + ["Enter", __("Open work order / run primary action")], + ["Esc", __("Close detail / blur search")], + ["s", __("Start / Resume job")], + ["p", __("Pause / Resume job")], + ["e", __("End session for active job")], + ["t", __("Transfer materials")], + ["Shift + S", __("Submit focused job card")], + ]; + const html = `
    ${rows + .map((r) => `
    ${r[0]}${r[1]}
    `) + .join("")}
    `; + const d = new frappe.ui.Dialog({ + title: __("Keyboard Shortcuts"), + fields: [{ fieldtype: "HTML", options: html }], + }); + d.show(); + } + + // ── Scanner ────────────────────────────────────────────────────────────── + open_scanner() { + const me = this; + const dialog = new frappe.ui.Dialog({ + title: __("Scan Job Card"), + fields: [ + { + label: __("Scan or enter Job Card"), + fieldname: "job_card", + fieldtype: "Data", + options: "Barcode", + }, + ], + primary_action_label: __("Continue"), + primary_action: (values) => { + if (!values.job_card) return; + dialog.hide(); + me.handle_scanned_job_card(values.job_card); + }, + }); + dialog.show(); + this.bind_enter_submit(dialog); + } + + handle_scanned_job_card(job_card) { + const me = this; + const jc = (this.job_cards || []).find((j) => j.name === job_card); + if (jc) { + me.route_scanned_action(jc); + return; + } + frappe.db.get_value("Job Card", job_card, ["status", "is_paused", "docstatus"]).then((r) => { + const data = r && r.message; + if (!data || !data.status) { + frappe.msgprint(__("Job Card {0} was not found.", [job_card])); + return; + } + if (cint(data.docstatus) === 1) { + frappe.msgprint(__("Job Card {0} is already submitted.", [job_card])); + } else if (cint(data.is_paused)) { + me.resume_job(job_card); + } else if (data.status === "Work In Progress") { + frappe.msgprint( + __( + "Job Card {0} is already running. Open its machine or work order to pause or complete it.", + [job_card] + ) + ); + } else if (data.status === "Completed") { + me.submit_job_card(job_card); + } else { + me.start_job(job_card); + } + }); + } + + route_scanned_action(jc) { + const me = this; + if (jc.docstatus === 1) { + frappe.msgprint(__("Job Card {0} is already submitted.", [jc.name])); + return; + } + if (jc.status === "Completed") { + me.submit_job_card(jc.name); + return; + } + if (jc.is_paused) { + me.resume_job(jc.name); + return; + } + const last_log = jc.time_logs && jc.time_logs.length ? jc.time_logs[jc.time_logs.length - 1] : null; + const is_running = !!(last_log && !last_log.to_time); + if (is_running) { + me.prompt_running_action(jc); + } else { + me.start_job(jc.name); + } + } + + prompt_running_action(jc) { + const me = this; + const dialog = new frappe.ui.Dialog({ + title: __("Job {0} is running", [jc.name]), + fields: [ + { + fieldtype: "HTML", + options: ` +
    + ${__("{0} is already in progress. Pause it or complete the session.", [ + frappe.utils.escape_html(jc.finished_good || jc.production_item || jc.name), + ])} +
    + `, + }, + ], + primary_action_label: __("Complete"), + primary_action: () => { + dialog.hide(); + me.end_session(jc.name); + }, + secondary_action_label: __("Pause"), + secondary_action: () => { + dialog.hide(); + me.pause_job(jc.name); + }, + }); + dialog.show(); + this.bind_enter_submit(dialog); + } + + // ── Route options (e.g. the Work Order "Shop Floor" button) ──────────────── + apply_route_options() { + const opts = frappe.route_options; + if (!opts || (!opts.work_order && !opts.workstation)) { + return; + } + frappe.route_options = null; + + // A specific work order / machine was requested — show it in the operator view. + this.view = "operator"; + this.render_shell_controls(); + this.render_view(); + Promise.all([ + this.work_order_filter.set_value(opts.work_order || ""), + this.workstation_filter.set_value(opts.workstation || ""), + ]).then(() => this.load_operator()); + } + + // ── Styles ────────────────────────────────────────────────────────────────── + styles() { + return ``; + } +} + +frappe.ui.ShopFloor = ShopFloor; From 3f2e0c177bdf07ad885ce140c9df8a450f53433c Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 1 Aug 2026 18:34:31 +0530 Subject: [PATCH 076/134] refactor(job_card): make the completion dialog say what it asks for (#57688) * refactor(job_card): drop the unused make_finished_good handler Nothing triggered it and Job Card has no make_finished_good method to call. * refactor(job_card): make the completion dialog say what it asks for The dialog qty shares the Qty to Manufacture label with the field on the form while it means the current cycle only, its title fell back to the generic Enter Value because frappe.prompt takes four arguments and it was passed five, and nothing on it stated that the three quantities have to add up. Name the cycle in the label, title the dialog after the button that opens it, and describe the split on the fields. Same wording in the shop floor dialog. (cherry picked from commit 0ddf72dae935b6fe221df32d4c1a7ac32e868ce9) # Conflicts: # erpnext/public/js/shop_floor/shop_floor.js --- .../doctype/job_card/job_card.js | 50 +- erpnext/public/js/shop_floor/shop_floor.js | 1750 +++++++++++++++++ 2 files changed, 1756 insertions(+), 44 deletions(-) create mode 100644 erpnext/public/js/shop_floor/shop_floor.js diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 0a4026672cf..e11e233fc97 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -243,10 +243,11 @@ frappe.ui.form.on("Job Card", { const fields = [ { fieldtype: "Float", - label: __("Qty to Manufacture"), + label: __("Qty to Manufacture in this Cycle"), fieldname: "for_quantity", reqd: 1, default: pending_qty, + description: __("Completed, Pending and Process Loss quantities must add up to this."), change() { const dialog = frm.job_completion_dialog; dialog.set_value("completed_qty", dialog.get_value("for_quantity")); @@ -272,6 +273,7 @@ frappe.ui.form.on("Job Card", { label: __("Pending Quantity"), fieldname: "pending_qty", default: 0.0, + description: __("Qty left for a later cycle or for another job card."), change() { const dialog = frm.job_completion_dialog; const process_loss_qty = @@ -287,6 +289,7 @@ frappe.ui.form.on("Job Card", { fieldtype: "Float", label: __("Process Loss Quantity"), fieldname: "process_loss_qty", + description: __("Qty scrapped in this cycle, nobody will produce it."), onchange() { const dialog = frm.job_completion_dialog; const remaining = @@ -357,9 +360,8 @@ frappe.ui.form.on("Job Card", { }, }); }, - __("Enter Value"), - __("Update"), - __("Set Finished Good Quantity") + __("Complete Job"), + __("Update") ); }, @@ -385,46 +387,6 @@ frappe.ui.form.on("Job Card", { }); }, - make_finished_good(frm) { - const fields = [ - { - fieldtype: "Float", - label: __("Completed Quantity"), - fieldname: "qty", - reqd: 1, - default: frm.doc.for_quantity - frm.doc.manufactured_qty, - }, - { - fieldtype: "Datetime", - label: __("End Time"), - fieldname: "end_time", - default: frappe.datetime.now_datetime(), - }, - ]; - - frappe.prompt( - fields, - (data) => { - if (data.qty <= 0) { - frappe.throw(__("Quantity should be greater than 0")); - } - - frm.call({ - method: "make_finished_good", - doc: frm.doc, - 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); - }, - }); - }, - __("Enter Value"), - __("Update"), - __("Set Finished Good Quantity") - ); - }, - setup_quality_inspection(frm) { const quality_inspection_field = frm.get_docfield("quality_inspection"); quality_inspection_field.get_route_options_for_new_doc = function (frm) { diff --git a/erpnext/public/js/shop_floor/shop_floor.js b/erpnext/public/js/shop_floor/shop_floor.js new file mode 100644 index 00000000000..13b8ca657d2 --- /dev/null +++ b/erpnext/public/js/shop_floor/shop_floor.js @@ -0,0 +1,1750 @@ +// Shop Floor — an immersive, keyboard-first operator/manager interface. +// +// Two experiences share one app shell (see get_shop_floor_context on the server): +// • manager — a paginated board of work orders bucketed Pending / In Progress and Completed. +// Drilling into a work order opens its job cards in the operator pane. +// • operator — a focused workstation/work-order view to start, pause, complete and submit jobs. +// +// The whole surface is driveable from the keyboard (press ? for the cheat sheet) so an operator +// at a terminal never needs the mouse. + +// Job Card status → indicator colour, mirrored from workstation.get_status_color so the manager +// board can paint per-operation chips without a round-trip. +const JC_STATUS_COLORS = { + Completed: "green", + Submitted: "blue", + "Work In Progress": "orange", + "Material Transferred": "yellow", + "On Hold": "red", + Open: "gray", + "Not Started": "gray", +}; + +const MANAGER_BUCKETS = [ + { key: "open", label: __("Pending / In Progress"), dot: "orange" }, + { key: "completed", label: __("Completed"), dot: "green" }, +]; + +const PAGE_LENGTH = 20; + +class ShopFloor { + constructor({ wrapper }, page) { + this.wrapper = $(wrapper); + this.page = page; + this.timer_intervals = {}; + this.capacity = 1; + this.mode = null; + // Remembers each Materials panel's open/closed state (keyed by job card) so it + // survives re-renders — otherwise a reload right after a click resets the panel. + this.materials_open = {}; + // Same idea for the per-operation Work Instructions panel. + this.instructions_open = {}; + + // View state. + this.view = "operator"; // overwritten once context loads + this.active_bucket = "open"; + this.with_job_cards_only = true; // board default: hide WOs that have no job cards + this.buckets = {}; // key -> { rows, total, start, loaded } + this.selected_wo = null; + this.focus_index = -1; + this.op_state = { workstation: null, work_order: null }; + + this.make(); + this.bind_realtime(); + this.bind_lifecycle(); + this.init(); + } + + init() { + frappe.call("erpnext.manufacturing.page.shop_floor.shop_floor.get_shop_floor_context").then((r) => { + const ctx = r.message || {}; + this.view = ctx.role_view === "manager" ? "manager" : "operator"; + this.can_manage = !!ctx.can_manage; + this.user_employee = ctx.user_employee || null; + this.render_shell_controls(); + this.render_view(); + this.bind_keys(); + this.initialized = true; + this.apply_route_options(); + }); + } + + // ── App shell ──────────────────────────────────────────────────────────── + make() { + this.wrapper.append(` + ${this.styles()} +
    +
    +
    +
    +
    + + + + + +
    +
    +
    +
    +
    +
    +
    +
    + `); + + this.app = this.wrapper.find(".sf-app"); + this.brand_icon = `${__(
+			`; + this.topbar_left = this.wrapper.find(".sf-topbar-left"); + this.topbar_center = this.wrapper.find(".sf-topbar-center"); + this.body = this.wrapper.find(".sf-body"); + this.board_container = this.wrapper.find(".sf-board"); + this.detail_container = this.wrapper.find(".sf-detail"); + this.op_container = this.wrapper.find(".sf-operator"); + + this.wrapper.find(".sf-btn-home").on("click", () => (window.location.href = "/app")); + this.wrapper.find(".sf-btn-refresh").on("click", () => this.refresh()); + this.wrapper.find(".sf-btn-scan").on("click", () => this.open_scanner()); + this.wrapper.find(".sf-btn-help").on("click", () => this.show_help()); + this.wrapper.find(".sf-btn-theme").on("click", () => this.toggle_theme()); + this.update_theme_button(); + } + + // Kiosk-friendly light/dark switch: flips the standard desk theme and persists it on the + // User (same as the Ctrl+Shift+G switcher), so the choice survives reloads and follows the + // operator's login on any device. + toggle_theme() { + const next = frappe.ui.get_current_theme() === "dark" ? "light" : "dark"; + document.documentElement.setAttribute("data-theme-mode", next); + frappe.ui.set_theme(next); + frappe.xcall("frappe.core.doctype.user.user.switch_theme", { + theme: next.charAt(0).toUpperCase() + next.slice(1), + }); + this.update_theme_button(); + } + + update_theme_button() { + const dark = frappe.ui.get_current_theme() === "dark"; + this.wrapper + .find(".sf-btn-theme") + .html(dark ? "☀" : "☾") + .attr("title", dark ? __("Switch to Light Theme") : __("Switch to Dark Theme")); + } + + render_shell_controls() { + this.topbar_left.empty(); + this.topbar_center.empty(); + + // View toggle — only managers can flip between the board and a bare operator view. + const toggle = this.can_manage + ? `
    + + +
    ` + : ""; + + if (this.view === "manager") { + this.topbar_left.html(` + ${this.brand_icon}${__("Shop Floor")} + ${toggle} +
    + ${MANAGER_BUCKETS.map( + (b) => `` + ).join("")} +
    + `); + this.topbar_center.html(` + + + `); + + this.topbar_left.find(".sf-tab").on("click", (e) => { + this.switch_bucket($(e.currentTarget).attr("data-bucket")); + }); + let timer = null; + this.topbar_center.find(".sf-search-input").on("input", (e) => { + const val = e.target.value; + clearTimeout(timer); + timer = setTimeout(() => this.search_work_orders(val), 300); + }); + this.topbar_center.find(".sf-jc-toggle").on("change", (e) => { + this.toggle_job_cards_only(e.target.checked); + }); + } else { + this.topbar_left.html( + `${this.brand_icon}${__("Shop Floor")}${toggle}` + ); + this.build_operator_filters(); + } + + this.topbar_left.find(".sf-view-btn").on("click", (e) => { + this.set_view($(e.currentTarget).attr("data-view")); + }); + } + + build_operator_filters() { + this.topbar_center.html('
    '); + const $filters = this.topbar_center.find(".sf-filters"); + + this.workstation_filter = frappe.ui.form.make_control({ + df: { + fieldtype: "Link", + options: "Workstation", + fieldname: "workstation", + placeholder: __("Machine"), + onchange: () => this.load_operator(), + }, + parent: $filters, + render_input: true, + }); + this.workstation_filter.$wrapper.addClass("sf-filter-control"); + + this.work_order_filter = frappe.ui.form.make_control({ + df: { + fieldtype: "Link", + options: "Work Order", + fieldname: "work_order", + placeholder: __("Work Order"), + onchange: () => this.load_operator(), + }, + parent: $filters, + render_input: true, + }); + this.work_order_filter.$wrapper.addClass("sf-filter-control"); + } + + set_view(view) { + if (!view || view === this.view) return; + this.view = view; + this.selected_wo = null; + this.focus_index = -1; + this.render_shell_controls(); + this.render_view(); + } + + render_view() { + const manager = this.view === "manager"; + this.board_container.toggle(manager); + this.detail_container.toggle(manager && !!this.selected_wo); + this.op_container.toggle(!manager); + this.body.toggleClass("detail-open", manager && !!this.selected_wo); + + if (manager) { + this.load_bucket(this.active_bucket); + } else { + this.load_operator(); + } + } + + // ── Manager board ──────────────────────────────────────────────────────── + switch_bucket(bucket) { + if (!bucket || bucket === this.active_bucket) return; + this.active_bucket = bucket; + this.selected_wo = null; + this.focus_index = -1; + this.topbar_left.find(".sf-tab").removeClass("active"); + this.topbar_left.find(`.sf-tab[data-bucket="${bucket}"]`).addClass("active"); + this.detail_container.hide(); + this.body.removeClass("detail-open"); + this.load_bucket(bucket); + } + + search_work_orders(term) { + this.search_term = term; + // Re-query every bucket from scratch on the next visit; reload the active one now. + this.buckets = {}; + this.load_bucket(this.active_bucket); + } + + toggle_job_cards_only(checked) { + this.with_job_cards_only = !!checked; + // Filter changes every bucket's contents + counts; drop caches and clear stale counts. + this.buckets = {}; + this.topbar_left.find(".sf-tab-count").text(""); + this.load_bucket(this.active_bucket); + } + + load_bucket(bucket, append = false) { + const state = this.buckets[bucket] || { rows: [], total: 0, start: 0, loaded: false }; + const start = append ? state.start : 0; + + frappe.call({ + method: "erpnext.manufacturing.page.shop_floor.shop_floor.get_work_orders", + args: { + status_group: bucket, + start: start, + page_length: PAGE_LENGTH, + search: this.search_term || null, + with_job_cards_only: this.with_job_cards_only ? 1 : 0, + }, + callback: (r) => { + const data = r.message || {}; + const rows = data.work_orders || []; + this.buckets[bucket] = { + rows: append ? state.rows.concat(rows) : rows, + total: cint(data.total), + start: start + rows.length, + loaded: true, + }; + this.update_tab_count(bucket); + if (bucket === this.active_bucket) this.render_board(); + }, + }); + } + + update_tab_count(bucket) { + const state = this.buckets[bucket]; + if (!state) return; + this.topbar_left.find(`[data-bucket-count="${bucket}"]`).text(state.total ? state.total : ""); + } + + render_board() { + const state = this.buckets[this.active_bucket] || { rows: [], total: 0 }; + this.focus_index = -1; + + if (!state.rows.length) { + this.board_container.html(`
    ${__("No work orders here.")}
    `); + return; + } + + const cards = state.rows.map((wo) => this.work_order_card(wo)).join(""); + const more = + state.rows.length < state.total + ? `` + : `
    ${__("Showing all {0}", [state.total])}
    `; + + this.board_container.html( + `
    ${cards}
    ${more}
    ` + ); + + this.board_container.find(".sf-wo-card").on("click", (e) => { + this.open_wo($(e.currentTarget).attr("data-name")); + }); + this.board_container + .find(".sf-load-more") + .on("click", () => this.load_bucket(this.active_bucket, true)); + } + + work_order_card(wo) { + const item = wo.item_name || wo.production_item; + + // Hero image = the current operation's workstation. No item-image fallback — when the + // workstation has no image uploaded we show its initials, never the product image. + const image = wo.workstation_image + ? `` + : `${frappe.get_abbr(wo.workstation_name || item, 2)}`; + + const workstation_line = wo.workstation_name + ? `
    🏭 ${frappe.utils.escape_html( + wo.workstation_name + )}${wo.current_operation ? ` · ${frappe.utils.escape_html(wo.current_operation)}` : ""}
    ` + : ""; + + // Operations bar: green segment (done) + orange segment (in progress); grey track = pending. + const done_pct = Math.min(cint(wo.per_operations), 100); + const wip_pct = Math.min(cint(wo.per_in_progress), 100 - done_pct); + + return ` +
    +
    +
    ${image}
    +
    +
    ${frappe.utils.escape_html(item)}
    + ${workstation_line} +
    + + ${wo.name} +
    +
    +
    +
    +
    + ${__("Operations")} + ${cint(wo.completed_operations)} / ${cint(wo.total_operations)} +
    +
    +
    +
    +
    +
    +
    + `; + } + + open_wo(name) { + if (!name) return; + this.selected_wo = name; + this.op_state = { workstation: null, work_order: name }; + this.detail_container.show(); + this.body.addClass("detail-open"); + this.board_container + .find(".sf-wo-card") + .removeClass("sf-selected") + .filter(`[data-name="${name}"]`) + .addClass("sf-selected"); + // The detail pane reuses the operator rendering for a single work order. + this.detail_container.html(` +
    + + ${frappe.utils.escape_html(name)} + ${__("Open")} +
    +
    + `); + this.detail_container.find(".sf-detail-back").on("click", () => this.close_wo()); + this.op_container_target = this.detail_container.find(".sf-detail-body"); + this.load_operator_data(this.op_container_target, { work_order: name }); + } + + close_wo() { + this.selected_wo = null; + this.op_container_target = null; + this.detail_container.hide().empty(); + this.body.removeClass("detail-open"); + this.board_container.find(".sf-wo-card").removeClass("sf-selected"); + } + + // ── Operator pane ────────────────────────────────────────────────────────── + // Resolves the container the operator content renders into: the standalone operator + // view, or the manager's drill-down detail pane. + current_op_container() { + return this.view === "manager" ? this.op_container_target : this.op_container; + } + + load_operator() { + const workstation = this.workstation_filter ? this.workstation_filter.get_value() : null; + const work_order = this.work_order_filter ? this.work_order_filter.get_value() : null; + this.op_state = { workstation, work_order }; + + if (!workstation && !work_order) { + this.clear_timers(); + this.op_container.html( + `
    ${__("Select a machine or work order to begin")}
    ` + ); + return; + } + this.load_operator_data(this.op_container, { workstation, work_order }); + } + + load_operator_data($container, { workstation, work_order }) { + frappe.call({ + method: "erpnext.manufacturing.page.shop_floor.shop_floor.get_data", + args: { + workstation: work_order ? null : workstation, + work_order: work_order || null, + }, + callback: (r) => { + const data = r.message || {}; + this.job_cards = data.job_cards || []; + this.capacity = cint(data.capacity) || 1; + this.mode = data.mode || (work_order ? "work_order" : "workstation"); + this.oee = data.oee || null; + if (data.user_employee) this.user_employee = data.user_employee; + this.today_sessions = data.today_sessions || []; + this.workstation = workstation; + this.work_order = work_order; + this.compute_state(); + this.dedupe_today_sessions(); + this.render_operator($container); + }, + }); + } + + // A job card already shown under Completed Operations shouldn't repeat in + // Today's Sessions — keep it in Completed Operations only. + dedupe_today_sessions() { + const shown = new Set((this.completed || []).map((jc) => jc.name)); + this.today_sessions = (this.today_sessions || []).filter((s) => !shown.has(s.name)); + } + + // Re-fetch whichever operator content is currently on screen (used after every action). + reload() { + if (this.view === "manager" && this.selected_wo) { + this.load_operator_data(this.op_container_target, { work_order: this.selected_wo }); + // Keep the board chips fresh too. + this.buckets = {}; + this.load_bucket(this.active_bucket); + } else if (this.view === "manager") { + this.load_bucket(this.active_bucket); + } else { + this.load_operator(); + } + } + + refresh() { + if (this.view === "manager") { + this.buckets = {}; + } + this.reload(); + } + + compute_state() { + this.active_jobs = []; + this.queue = []; + this.pending_submission = []; + this.completed = []; + // Submitted but the finished goods aren't booked yet (status "To Manufacture") — its own + // actionable section, kept out of Completed Operations / Today's Sessions. + this.to_manufacture = []; + + for (const jc of this.job_cards) { + // Same materials-ready rule as job_card.js make_dashboard. + jc._materials_ready = !!( + jc.skip_material_transfer || + flt(jc.transferred_qty) >= flt(jc.for_quantity) + flt(jc.process_loss_qty) || + !jc.finished_good + ); + + // Submitted JCs are historical from the Shop Floor's POV — only appear here in work_order + // mode (and, for "To Manufacture", in workstation mode too — see _fetch_job_cards). + if (jc.docstatus === 1) { + if (jc.status === "To Manufacture") { + this.to_manufacture.push(jc); + } else { + this.completed.push(jc); + } + continue; + } + + const last_log = + jc.time_logs && jc.time_logs.length ? jc.time_logs[jc.time_logs.length - 1] : null; + const is_running = last_log && !last_log.to_time && !jc.is_paused; + const is_paused = jc.is_paused; + + if (is_running || is_paused) { + this.active_jobs.push(jc); + } else if (jc.status === "Completed") { + // All qty accounted for but still draft — waiting on Submit. + this.pending_submission.push(jc); + } else { + this.queue.push(jc); + } + } + + // Slot rules — all active jobs are always shown; the grid (col-md-6) wraps them 2 per row. + // workstation mode: capacity-many slots, expanded to fit every active job (+ empty placeholders). + // work_order mode: one slot per active job (no empty placeholders). + let slot_count; + if (this.mode === "work_order") { + slot_count = this.active_jobs.length; + } else { + slot_count = Math.max(this.capacity, this.active_jobs.length, 1); + } + + this.slots = []; + for (let i = 0; i < slot_count; i++) { + this.slots.push(this.active_jobs[i] || null); + } + + // Auto-pick: when nothing is running, surface the next queue item in the slot. + if (this.active_jobs.length === 0 && this.queue.length > 0) { + const next_up = this.queue.shift(); + next_up._is_next_up = true; + this.slots[0] = next_up; + } + + this.summary = { + active_count: this.active_jobs.length, + // "To Manufacture" (submitted, qty done, but the Manufacture Stock Entry is still pending) + // isn't actually finished — count it as Pending, not Completed. + queue_count: this.queue.length + this.to_manufacture.length, + completed_count: this.completed.length + this.pending_submission.length, + capacity: this.capacity, + }; + } + + render_operator($container) { + this.clear_timers(); + $container.empty(); + + const html = frappe.render_template("shop_floor_template", { + workstation: this.workstation, + work_order: this.work_order, + mode: this.mode, + slots: this.slots, + active_jobs: this.active_jobs, + queue: this.queue, + pending_submission: this.pending_submission, + to_manufacture: this.to_manufacture, + completed: this.completed, + today_sessions: this.today_sessions || [], + summary: this.summary, + oee: this.oee, + }); + $container.html(html); + + // Restore each Materials panel to its remembered open/closed state. + $container.find(".mes-materials-inline").each((i, el) => { + const $el = $(el); + const name = $el.attr("data-job-card"); + if (!name) return; + if (name in this.materials_open) { + $el.toggleClass("is-open", this.materials_open[name]); + } else { + this.materials_open[name] = $el.hasClass("is-open"); + } + }); + + // Restore each Work Instructions panel to its remembered open/closed state. + $container.find(".mes-instructions-inline").each((i, el) => { + const $el = $(el); + const name = $el.attr("data-job-card"); + if (name && name in this.instructions_open) { + $el.toggleClass("is-open", this.instructions_open[name]); + } + }); + + this.bind_events($container); + + for (const jc of this.active_jobs) { + if (jc.is_paused) { + this.render_timer(jc.name, this.elapsed_seconds(jc), $container); + } else { + this.start_timer_for(jc, $container); + } + } + } + + clear_timers() { + for (const id of Object.values(this.timer_intervals)) { + clearInterval(id); + } + this.timer_intervals = {}; + } + + bind_events($container) { + const me = this; + + $container.find(".mes-materials-summary").on("click", function (e) { + if ($(e.target).closest(".mes-btn-transfer").length) return; + const $inline = $(this).closest(".mes-materials-inline"); + const open = !$inline.hasClass("is-open"); + $inline.toggleClass("is-open", open); + const name = $inline.attr("data-job-card"); + if (name) me.materials_open[name] = open; + }); + + $container.find(".mes-instructions-summary").on("click", function () { + const $inline = $(this).closest(".mes-instructions-inline"); + const open = !$inline.hasClass("is-open"); + $inline.toggleClass("is-open", open); + const name = $inline.attr("data-job-card"); + if (name) me.instructions_open[name] = open; + }); + + // Clicking a "QC Required" / "QC Available" pill runs the inline check ahead of End Session. + $container.find(".mes-qc-pill").on("click", function () { + const name = $(this).attr("data-job-card"); + const jc = (me.active_jobs || []).find((j) => j.name === name); + if (jc) me.run_quality_check(jc, () => me.reload()); + }); + + $container.find(".mes-btn-start").on("click", function () { + me.start_job($(this).attr("data-job-card")); + }); + $container.find(".mes-btn-pause").on("click", function () { + me.pause_job($(this).attr("data-job-card")); + }); + $container.find(".mes-btn-resume").on("click", function () { + me.resume_job($(this).attr("data-job-card")); + }); + $container.find(".mes-btn-end-session").on("click", function () { + me.end_session($(this).attr("data-job-card")); + }); + $container.find(".mes-btn-submit").on("click", function () { + me.submit_job_card($(this).attr("data-job-card")); + }); + $container.find(".mes-btn-make-entry").on("click", function () { + me.make_manufacture_entry($(this).attr("data-job-card")); + }); + $container.find(".mes-btn-transfer").on("click", function (e) { + e.preventDefault(); + me.transfer_materials($(this).attr("data-job-card")); + }); + } + + // ── Operator actions (unchanged behaviour, reload() instead of load()) ───── + start_job(job_card) { + const me = this; + if (this.mode === "workstation" && this.active_jobs.length >= this.capacity) { + frappe.msgprint({ + title: __("Capacity Reached"), + message: __( + "This machine can run at most {0} job(s) in parallel. Pause or complete a running job before starting another.", + [this.capacity] + ), + indicator: "orange", + }); + return; + } + + const default_employee = this.user_employee; + const dialog = new frappe.ui.Dialog({ + title: __("Start Job"), + fields: [ + { + label: __("Start Time"), + fieldname: "start_time", + fieldtype: "Datetime", + default: frappe.datetime.now_datetime(), + }, + { fieldtype: "Section Break" }, + { + label: __("Employees"), + fieldname: "employees", + fieldtype: "Table", + data: default_employee ? [{ employee: default_employee }] : [], + fields: [ + { + label: __("Employee"), + fieldname: "employee", + fieldtype: "Link", + options: "Employee", + in_list_view: 1, + }, + ], + }, + ], + primary_action_label: __("Start"), + primary_action: (values) => { + dialog.hide(); + me.update_job_card(job_card, "start_timer", { + start_time: values.start_time, + employees: values.employees || [], + }); + }, + }); + dialog.show(); + this.bind_enter_submit(dialog); + } + + // Make a dialog fully keyboard-operable: Enter triggers the primary action, so an operator + // never has to reach for the mouse. Enter is left alone inside multi-line fields and while an + // autocomplete (Link/Select) dropdown is open, so it can still pick a value. + bind_enter_submit(dialog) { + dialog.$wrapper.on("keydown.sfenter", (e) => { + if (e.key !== "Enter" || e.shiftKey) return; + if ($(e.target).is("textarea")) return; + if ($(".awesomplete > ul:not([hidden])").length) return; + const $btn = dialog.get_primary_btn(); + if ( + $btn && + $btn.length && + $btn.is(":visible") && + !$btn.hasClass("disabled") && + !$btn.prop("disabled") + ) { + e.preventDefault(); + e.stopPropagation(); + $btn.trigger("click"); + } + }); + } + + pause_job(jc_name) { + this.update_job_card(jc_name, "pause_job", { end_time: frappe.datetime.now_datetime() }); + } + + resume_job(jc_name) { + this.update_job_card(jc_name, "resume_job", { start_time: frappe.datetime.now_datetime() }); + } + + end_session(jc_name) { + const me = this; + const jc = this.active_jobs.find((j) => j.name === jc_name); + if (!jc) return; + + let pending = flt(jc.for_quantity) - flt(jc.total_completed_qty); + if (flt(jc.pending_qty) > 0) { + pending = flt(jc.pending_qty); + } + + const fields = [ + { + fieldtype: "Float", + label: __("Qty to Manufacture in this Cycle"), + fieldname: "for_quantity", + reqd: 1, + default: pending, + description: __("Completed, Pending and Process Loss quantities must add up to this."), + change() { + const d = me.session_dialog; + d.set_value("completed_qty", d.get_value("for_quantity")); + d.set_value("pending_qty", 0); + d.set_value("process_loss_qty", 0); + }, + }, + { + fieldtype: "Float", + label: __("Completed Quantity"), + fieldname: "completed_qty", + reqd: 1, + default: pending, + change() { + const d = me.session_dialog; + const remaining = + flt(d.get_value("for_quantity")) - + flt(d.get_value("completed_qty")) - + flt(d.get_value("process_loss_qty")); + + if (remaining < 0) { + const max_completed_qty = + flt(d.get_value("for_quantity")) - flt(d.get_value("process_loss_qty")); + d.set_value("completed_qty", max_completed_qty); + frappe.throw( + __("Completed Quantity cannot be greater than {0}", [max_completed_qty]) + ); + } + + if (remaining !== flt(d.get_value("pending_qty"))) { + d.set_value("pending_qty", remaining); + } + }, + }, + { + fieldtype: "Float", + label: __("Pending Quantity"), + fieldname: "pending_qty", + default: 0.0, + description: __("Qty left for a later cycle or for another job card."), + change() { + const d = me.session_dialog; + const pl = + flt(d.get_value("for_quantity")) - + flt(d.get_value("completed_qty")) - + flt(d.get_value("pending_qty")); + + if (pl < 0) { + d.set_value("pending_qty", 0); + frappe.throw( + __("Pending Quantity cannot be greater than {0}", [ + flt(d.get_value("for_quantity")) - flt(d.get_value("completed_qty")), + ]) + ); + } + + if (pl !== flt(d.get_value("process_loss_qty"))) { + d.set_value("process_loss_qty", pl); + } + }, + }, + { + fieldtype: "Float", + label: __("Process Loss Quantity"), + fieldname: "process_loss_qty", + default: 0.0, + description: __("Qty scrapped in this cycle, nobody will produce it."), + change() { + const d = me.session_dialog; + const remaining = + flt(d.get_value("for_quantity")) - + flt(d.get_value("completed_qty")) - + flt(d.get_value("process_loss_qty")); + + if (remaining < 0) { + d.set_value("process_loss_qty", 0); + frappe.throw( + __("Process Loss Quantity cannot be greater than {0}", [ + flt(d.get_value("for_quantity")) - flt(d.get_value("completed_qty")), + ]) + ); + } + + if (remaining !== flt(d.get_value("pending_qty"))) { + d.set_value("pending_qty", remaining); + } + }, + }, + { fieldtype: "Section Break" }, + { + fieldtype: "Datetime", + label: __("End Time"), + fieldname: "end_time", + default: frappe.datetime.now_datetime(), + }, + ]; + + const get_payload = () => { + const data = me.session_dialog.get_values(); + if (!data) return null; + if (flt(data.completed_qty) <= 0) { + frappe.throw(__("Completed Quantity should be greater than 0")); + } + return { + job_card: jc.name, + qty: flt(data.completed_qty), + for_quantity: flt(data.for_quantity), + pending_qty: flt(data.pending_qty), + process_loss_qty: flt(data.process_loss_qty), + end_time: data.end_time, + }; + }; + + const save_and_continue = () => { + const args = get_payload(); + if (!args) return; + me.session_dialog.hide(); + frappe.call({ + method: "erpnext.manufacturing.page.shop_floor.shop_floor.save_and_continue", + args: args, + freeze: true, + freeze_message: __("Saving job card..."), + callback: () => me.reload(), + }); + }; + + const finalize_submit = (args) => { + frappe.call({ + method: "erpnext.manufacturing.page.shop_floor.shop_floor.complete_and_submit", + args: args, + freeze: true, + freeze_message: __("Submitting job card..."), + callback: (r) => { + me.reload(); + if (r.message && r.message.finished_good) { + me.prompt_manufacture_entry(jc.name); + } + }, + }); + }; + + const submit_session = () => { + const args = get_payload(); + if (!args) return; + me.session_dialog.hide(); + // Guided QC gate: a job card that requires inspection must pass an inline Quality Check + // before it is submitted (mirrors Job Card.validate_inspection on the server). Once the + // inspection is recorded, finalize the session submit. + if (jc.qc && jc.qc.required && jc.qc.status !== "Accepted") { + me.run_quality_check(jc, () => finalize_submit(args)); + } else { + finalize_submit(args); + } + }; + + me.session_dialog = new frappe.ui.Dialog({ + title: __("End Session"), + fields: fields, + primary_action_label: __("Submit"), + primary_action: submit_session, + secondary_action_label: __("Save & Continue"), + secondary_action: save_and_continue, + }); + me.session_dialog.show(); + me.bind_enter_submit(me.session_dialog); + } + + // ── Inline Quality Check ───────────────────────────────────────────────────── + // Fetch the operation's Quality Inspection template and open a guided pass/fail checklist. + // `on_pass` runs once the inspection has been recorded (and is not rejected). + run_quality_check(jc, on_pass) { + const me = this; + frappe.call({ + method: "erpnext.manufacturing.page.shop_floor.shop_floor.get_quality_inspection_checklist", + args: { job_card: jc.name }, + freeze: true, + freeze_message: __("Loading quality checklist..."), + callback: (r) => { + const info = r.message || {}; + if (!info.template || !(info.parameters || []).length) { + // Inspection is required but the operation has no template/parameters to fill — + // there is nothing to capture inline. Point the user at the configuration. + frappe.msgprint({ + title: __("Quality Inspection Template Missing"), + indicator: "orange", + message: __( + "This operation requires a Quality Inspection but no template with parameters is configured. Set a Quality Inspection Template on Operation {0} to inspect from the Shop Floor.", + [jc.operation || ""] + ), + }); + return; + } + me.show_qc_dialog(jc, info, on_pass); + }, + }); + } + + show_qc_dialog(jc, info, on_pass) { + const me = this; + const params = info.parameters || []; + // Per-row operator input, keyed by row index (avoids escaping issues with parameter names). + const state = {}; // idx -> "Accepted" | "Rejected" + + const rows = params + .map((p, i) => { + const spec = frappe.utils.escape_html(p.specification); + let criteria = ""; + if (p.numeric) { + const lo = p.min_value !== null && p.min_value !== undefined ? p.min_value : "−∞"; + const hi = p.max_value !== null && p.max_value !== undefined ? p.max_value : "∞"; + criteria = __("Acceptable range: {0} to {1}", [lo, hi]); + } else if (p.value) { + criteria = __("Expected: {0}", [frappe.utils.escape_html(p.value)]); + } + const control = p.numeric + ? `` + : ` + + + `; + return `
    +
    +
    ${spec}
    + ${criteria ? `
    ${criteria}
    ` : ""} +
    +
    ${control}
    +
    `; + }) + .join(""); + + const dialog = new frappe.ui.Dialog({ + title: __("Quality Check"), + size: "large", + fields: [ + { + fieldtype: "HTML", + options: `
    ${__( + "Inspect {0} for job card {1}", + [frappe.utils.escape_html(info.item_code || ""), frappe.utils.escape_html(jc.name)] + )}
    ${rows}
    `, + }, + ], + primary_action_label: __("Submit Inspection"), + primary_action: () => { + const readings = []; + let missing = false; + params.forEach((p, i) => { + if (p.numeric) { + const val = dialog.$wrapper.find(`.mes-qc-reading[data-idx="${i}"]`).val(); + if (val === "" || val === undefined || val === null) missing = true; + readings.push({ specification: p.specification, reading_value: val }); + } else { + if (!state[i]) missing = true; + readings.push({ + specification: p.specification, + status: state[i], + reading_value: "", + }); + } + }); + if (missing) { + frappe.msgprint(__("Please complete every check before submitting the inspection.")); + return; + } + dialog.hide(); + frappe.call({ + method: "erpnext.manufacturing.page.shop_floor.shop_floor.submit_quality_inspection", + args: { job_card: jc.name, readings: JSON.stringify(readings) }, + freeze: true, + freeze_message: __("Recording inspection..."), + callback: (r) => { + const res = r.message || {}; + if (res.status === "Rejected") { + // Don't auto-proceed on a rejected inspection — the server gate may block the + // submit anyway (per Stock Settings), and the operator should decide next steps. + frappe.msgprint({ + title: __("Inspection Rejected"), + indicator: "red", + message: __( + "Quality Inspection {0} is Rejected. Resolve the issue or follow your rejection process before submitting the job card.", + [res.name || ""] + ), + }); + me.reload(); + return; + } + if (on_pass) on_pass(); + }, + }); + }, + }); + + dialog.show(); + // Pass/Fail toggles for qualitative parameters. + dialog.$wrapper.find(".mes-qc-passfail button").on("click", function () { + const $btn = $(this); + const $grp = $btn.closest(".mes-qc-passfail"); + $grp.find("button").removeClass("active"); + $btn.addClass("active"); + state[$grp.attr("data-idx")] = $btn.attr("data-val"); + }); + } + + prompt_manufacture_entry(jc_name) { + const me = this; + const dialog = new frappe.ui.Dialog({ + title: __("Job Card Submitted"), + fields: [ + { + fieldtype: "HTML", + options: ` +
    +
    + ${__("Job card {0} has been submitted.", [frappe.utils.escape_html(jc_name)])} +
    +
    + ${__("Create a Manufacture stock entry for the finished goods?")} +
    +
    + `, + }, + ], + primary_action_label: __("Make Manufacture Entry"), + primary_action: () => { + dialog.hide(); + me.make_manufacture_entry(jc_name); + }, + secondary_action_label: __("Skip"), + secondary_action: () => dialog.hide(), + }); + dialog.show(); + this.bind_enter_submit(dialog); + } + + submit_job_card(jc_name) { + const me = this; + frappe.confirm(__("Submit job card {0}? This finalizes the job card.", [jc_name]), () => { + frappe.call({ + method: "erpnext.manufacturing.page.shop_floor.shop_floor.submit_job_card", + args: { job_card: jc_name }, + freeze: true, + freeze_message: __("Submitting job card..."), + callback: () => me.reload(), + }); + }); + } + + make_manufacture_entry(jc_name) { + frappe.call({ + method: "erpnext.manufacturing.page.shop_floor.shop_floor.make_manufacture_stock_entry", + args: { job_card: jc_name }, + freeze: true, + freeze_message: __("Preparing stock entry..."), + callback: (r) => { + if (r.message && r.message.name) { + window.open(`/app/stock-entry/${encodeURIComponent(r.message.name)}`, "_blank"); + } + }, + }); + } + + transfer_materials(jc_name) { + if (!jc_name) return; + frappe.call({ + method: "erpnext.manufacturing.doctype.job_card.job_card.make_stock_entry", + args: { source_name: jc_name }, + callback: (r) => { + const doc = frappe.model.sync(r.message); + frappe.set_route("Form", doc[0].doctype, doc[0].name); + }, + }); + } + + update_job_card(job_card, method, data, on_success) { + const me = this; + frappe.call({ + method: "erpnext.manufacturing.doctype.workstation.workstation.update_job_card", + args: { + job_card: job_card, + method: method, + start_time: data.start_time || "", + employees: data.employees || [], + end_time: data.end_time || "", + qty: data.qty || 0, + for_quantity: data.for_quantity || 0, + pending_qty: data.pending_qty || 0, + process_loss_qty: data.process_loss_qty || 0, + auto_submit: data.auto_submit || 0, + }, + freeze: true, + freeze_message: __("Updating job card..."), + callback: () => { + me.reload(); + if (on_success) on_success(); + }, + }); + } + + // ── Timers ──────────────────────────────────────────────────────────────── + start_timer_for(jc, $container) { + let elapsed = this.elapsed_seconds(jc); + this.render_timer(jc.name, elapsed, $container); + this.timer_intervals[jc.name] = setInterval(() => { + elapsed += 1; + this.render_timer(jc.name, elapsed, $container); + }, 1000); + } + + elapsed_seconds(jc) { + let total = 0; + for (const log of jc.time_logs || []) { + if (log.to_time) { + if (log.time_in_mins) { + total += flt(log.time_in_mins, 2) * 60; + } else { + total += moment(log.to_time).diff(log.from_time, "seconds"); + } + } else { + total += moment().diff(log.from_time, "seconds"); + } + } + return total; + } + + render_timer(jc_name, seconds, $container) { + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds - h * 3600) / 60); + const s = cint(seconds - h * 3600 - m * 60); + const pad = (n) => (n < 10 ? "0" + n : String(n)); + + const scope = $container || this.wrapper; + const timer = scope.find(`.mes-job-timer[data-job-card="${jc_name}"]`); + timer.find(".h").text(pad(h)); + timer.find(".m").text(pad(m)); + timer.find(".s").text(pad(s)); + } + + // ── Realtime + lifecycle ─────────────────────────────────────────────────── + bind_realtime() { + frappe.realtime.on("update_workstation_status", (data) => { + if (data && data.name === this.op_state.workstation) { + this.reload(); + } + }); + } + + bind_lifecycle() { + // Frappe has no on_page_hide hook, so toggle immersive mode + keyboard binding on + // route changes ourselves. + this._route_handler = () => { + const on_page = (frappe.get_route_str() || "").startsWith("shop-floor"); + if (on_page) { + $(document.body).addClass("shop-floor-active"); + this.bind_keys(); + } else { + $(document.body).removeClass("shop-floor-active"); + this.unbind_keys(); + this.clear_timers(); + } + }; + frappe.router.on("change", this._route_handler); + } + + on_show() { + $(document.body).addClass("shop-floor-active"); + this.bind_keys(); + // Cached re-navigation (e.g. the Work Order "Shop Floor" button) lands here with fresh + // route_options; init() handles the very first load before we're initialized. + if (this.initialized) this.apply_route_options(); + } + + // ── Keyboard ──────────────────────────────────────────────────────────────── + bind_keys() { + $(document).off("keydown.shopfloor"); + $(document).on("keydown.shopfloor", (e) => this.handle_key(e)); + } + + unbind_keys() { + $(document).off("keydown.shopfloor"); + } + + is_typing(e) { + const tag = (e.target.tagName || "").toLowerCase(); + return tag === "input" || tag === "textarea" || tag === "select" || e.target.isContentEditable; + } + + handle_key(e) { + // Let dialogs own the keyboard while open. + if ($(".modal:visible").length) return; + + const typing = this.is_typing(e); + + // Escape works even while typing (blur the search / close the detail pane). + if (e.key === "Escape") { + if (typing) { + e.target.blur(); + return; + } + if (this.view === "manager" && this.selected_wo) { + this.close_wo(); + e.preventDefault(); + } + return; + } + + if (typing) return; + + switch (e.key) { + case "?": + this.show_help(); + e.preventDefault(); + return; + case "/": + this.topbar_center.find(".sf-search-input").focus(); + e.preventDefault(); + return; + case "r": + this.refresh(); + e.preventDefault(); + return; + case "b": + this.open_scanner(); + e.preventDefault(); + return; + case "1": + case "2": + if (this.view === "manager" && MANAGER_BUCKETS[cint(e.key) - 1]) { + this.switch_bucket(MANAGER_BUCKETS[cint(e.key) - 1].key); + e.preventDefault(); + } + return; + } + + // View switch chord: "g" then "m"/"o". + if (e.key === "g") { + this._g_pending = true; + setTimeout(() => (this._g_pending = false), 600); + return; + } + if (this._g_pending && (e.key === "m" || e.key === "o")) { + this._g_pending = false; + if (this.can_manage) this.set_view(e.key === "m" ? "manager" : "operator"); + return; + } + + // Navigation. + if (e.key === "ArrowDown" || e.key === "j") { + this.move_focus(1); + e.preventDefault(); + return; + } + if (e.key === "ArrowUp" || e.key === "k") { + this.move_focus(-1); + e.preventDefault(); + return; + } + if (e.key === "Enter") { + this.activate_focus(); + e.preventDefault(); + return; + } + + // Job actions on the focused card — reuse the rendered buttons. + const map = { + s: ".mes-btn-start, .mes-btn-resume", + p: ".mes-btn-pause, .mes-btn-resume", + e: ".mes-btn-end-session", + t: ".mes-btn-transfer", + }; + if (e.key === "S" && e.shiftKey) { + this.click_job_action(".mes-btn-submit"); + e.preventDefault(); + return; + } + if (map[e.key]) { + this.click_job_action(map[e.key]); + e.preventDefault(); + } + } + + // Job actions act on the focused job card (operator view); when the focus is on a board + // work order (manager view with the detail open) they fall back to the detail's active job. + click_job_action(selector) { + const $el = this.focused_el(); + if ($el && $el.attr("data-kind") === "job") { + const $btn = $el.find(selector).filter(":visible").first(); + if ($btn.length) { + $btn.trigger("click"); + return; + } + } + const scope = this.current_op_container(); + if (scope && scope.length) { + const $btn = scope.find(selector).filter(":visible").first(); + if ($btn.length) $btn.trigger("click"); + } + } + + focusables() { + // Manager always navigates the board work orders — even with the detail open, so the + // arrow keys switch work orders. The standalone operator view navigates its job cards. + const scope = this.view === "manager" ? this.board_container : this.current_op_container(); + if (!scope || !scope.length) return $(); + return scope.find("[data-sf-focusable]"); + } + + move_focus(delta) { + const $items = this.focusables(); + if (!$items.length) return; + this.focus_index = Math.max(0, Math.min($items.length - 1, this.focus_index + delta)); + $items.removeClass("sf-focused"); + const $target = $items.eq(this.focus_index); + $target.addClass("sf-focused"); + $target[0].scrollIntoView({ block: "nearest", behavior: "smooth" }); + // Browsing work orders with the detail already open → switch the detail to the focused one. + if (this.view === "manager" && this.selected_wo && $target.attr("data-kind") === "wo") { + this.open_wo($target.attr("data-name")); + } + } + + focused_el() { + const $items = this.focusables(); + if (this.focus_index < 0 || this.focus_index >= $items.length) return null; + return $items.eq(this.focus_index); + } + + activate_focus() { + const $el = this.focused_el(); + if (!$el) return; + if ($el.attr("data-kind") === "wo") { + this.open_wo($el.attr("data-name")); + } else { + // First visible primary button drives the job card (Start / Resume / End Session). + const $btn = $el.find(".btn-primary:visible").first(); + if ($btn.length) $btn.trigger("click"); + } + } + + show_help() { + const rows = [ + ["?", __("Show this help")], + ["/", __("Search work orders")], + ["r", __("Refresh")], + ["b", __("Scan job card")], + ["g then m / o", __("Switch Board / Operator view")], + ["1 / 2", __("Switch board tab")], + ["↑ / ↓ or j / k", __("Move selection")], + ["Enter", __("Open work order / run primary action")], + ["Esc", __("Close detail / blur search")], + ["s", __("Start / Resume job")], + ["p", __("Pause / Resume job")], + ["e", __("End session for active job")], + ["t", __("Transfer materials")], + ["Shift + S", __("Submit focused job card")], + ]; + const html = `
    ${rows + .map((r) => `
    ${r[0]}${r[1]}
    `) + .join("")}
    `; + const d = new frappe.ui.Dialog({ + title: __("Keyboard Shortcuts"), + fields: [{ fieldtype: "HTML", options: html }], + }); + d.show(); + } + + // ── Scanner ────────────────────────────────────────────────────────────── + open_scanner() { + const me = this; + const dialog = new frappe.ui.Dialog({ + title: __("Scan Job Card"), + fields: [ + { + label: __("Scan or enter Job Card"), + fieldname: "job_card", + fieldtype: "Data", + options: "Barcode", + }, + ], + primary_action_label: __("Continue"), + primary_action: (values) => { + if (!values.job_card) return; + dialog.hide(); + me.handle_scanned_job_card(values.job_card); + }, + }); + dialog.show(); + this.bind_enter_submit(dialog); + } + + handle_scanned_job_card(job_card) { + const me = this; + const jc = (this.job_cards || []).find((j) => j.name === job_card); + if (jc) { + me.route_scanned_action(jc); + return; + } + frappe.db.get_value("Job Card", job_card, ["status", "is_paused", "docstatus"]).then((r) => { + const data = r && r.message; + if (!data || !data.status) { + frappe.msgprint(__("Job Card {0} was not found.", [job_card])); + return; + } + if (cint(data.docstatus) === 1) { + frappe.msgprint(__("Job Card {0} is already submitted.", [job_card])); + } else if (cint(data.is_paused)) { + me.resume_job(job_card); + } else if (data.status === "Work In Progress") { + frappe.msgprint( + __( + "Job Card {0} is already running. Open its machine or work order to pause or complete it.", + [job_card] + ) + ); + } else if (data.status === "Completed") { + me.submit_job_card(job_card); + } else { + me.start_job(job_card); + } + }); + } + + route_scanned_action(jc) { + const me = this; + if (jc.docstatus === 1) { + frappe.msgprint(__("Job Card {0} is already submitted.", [jc.name])); + return; + } + if (jc.status === "Completed") { + me.submit_job_card(jc.name); + return; + } + if (jc.is_paused) { + me.resume_job(jc.name); + return; + } + const last_log = jc.time_logs && jc.time_logs.length ? jc.time_logs[jc.time_logs.length - 1] : null; + const is_running = !!(last_log && !last_log.to_time); + if (is_running) { + me.prompt_running_action(jc); + } else { + me.start_job(jc.name); + } + } + + prompt_running_action(jc) { + const me = this; + const dialog = new frappe.ui.Dialog({ + title: __("Job {0} is running", [jc.name]), + fields: [ + { + fieldtype: "HTML", + options: ` +
    + ${__("{0} is already in progress. Pause it or complete the session.", [ + frappe.utils.escape_html(jc.finished_good || jc.production_item || jc.name), + ])} +
    + `, + }, + ], + primary_action_label: __("Complete"), + primary_action: () => { + dialog.hide(); + me.end_session(jc.name); + }, + secondary_action_label: __("Pause"), + secondary_action: () => { + dialog.hide(); + me.pause_job(jc.name); + }, + }); + dialog.show(); + this.bind_enter_submit(dialog); + } + + // ── Route options (e.g. the Work Order "Shop Floor" button) ──────────────── + apply_route_options() { + const opts = frappe.route_options; + if (!opts || (!opts.work_order && !opts.workstation)) { + return; + } + frappe.route_options = null; + + // A specific work order / machine was requested — show it in the operator view. + this.view = "operator"; + this.render_shell_controls(); + this.render_view(); + Promise.all([ + this.work_order_filter.set_value(opts.work_order || ""), + this.workstation_filter.set_value(opts.workstation || ""), + ]).then(() => this.load_operator()); + } + + // ── Styles ────────────────────────────────────────────────────────────────── + styles() { + return ``; + } +} + +frappe.ui.ShopFloor = ShopFloor; From d97cf131a18f6108133b54e71009d7c50b32a018 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 1 Aug 2026 18:34:30 +0530 Subject: [PATCH 077/134] fix(job_card): apply the completion dialog's qty to manufacture (#57685) * fix(job_card): apply the completion dialog's qty to manufacture Both the desk dialog and the shop floor session dialog send for_quantity when completing a job card, but complete_job_card dropped it. Reducing Qty to Manufacture to 3 on a job card of 5 left for_quantity at 5, so set_process_loss turned the untouched 2 into process loss on the next save. The dialog qty covers the current cycle, so add it to the qty already completed by the earlier cycles of the job card instead of overwriting for_quantity, and validate the pending qty against the result. * test(job_card): cover qty to manufacture from the completion dialog Reducing the dialog qty resizes the job card without inventing process loss, and a pending qty split across two cycles leaves for_quantity untouched. (cherry picked from commit 0e1bc58b2e3078d662612033a8af312e93a7aea0) # Conflicts: # erpnext/manufacturing/doctype/job_card/job_card.py --- .../doctype/job_card/job_card.py | 20 ++++++ .../doctype/job_card/test_job_card.py | 68 +++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 3b4f8008f08..f28490e0e87 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -1514,8 +1514,28 @@ class JobCard(Document): if isinstance(kwargs, dict): kwargs = frappe._dict(kwargs) + self.set_for_quantity(kwargs) self.validate_complete_job_card_qty(kwargs) +<<<<<<< HEAD +======= + self.pending_qty = flt(kwargs.pending_qty) + self.process_loss_qty = flt(kwargs.process_loss_qty) + + self.add_completion_time_logs(kwargs) + + if kwargs.auto_submit: + self.auto_submit_job_card(kwargs.auto_submit) + + def set_for_quantity(self, kwargs): + """Qty to Manufacture of the completion dialog covers the current cycle only, + so the qty completed by the earlier cycles of this job card is kept.""" + if not flt(kwargs.for_quantity): + return + + self.for_quantity = flt(self.total_completed_qty) + flt(kwargs.for_quantity) + +>>>>>>> 0e1bc58b2e (fix(job_card): apply the completion dialog's qty to manufacture (#57685)) def validate_docstatus(self): if self.docstatus == 2: frappe.throw(_("Cancelled Job Card cannot be processed.")) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 826d558e830..98251a93ccc 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -888,6 +888,74 @@ class TestJobCard(ERPNextTestSuite): self.assertEqual(wo_doc.process_loss_qty, 2) self.assertEqual(wo_doc.status, "Completed") + def get_first_job_card(self, work_order): + return frappe.get_doc( + "Job Card", + frappe.get_all( + "Job Card", + filters={"work_order": work_order}, + order_by="sequence_id, creation", + limit=1, + pluck="name", + )[0], + ) + + def test_completion_qty_reduces_for_quantity_without_process_loss(self): + work_order = make_wo_order_test_record(item="_Test FG Item 2", qty=5) + + job_card = self.get_first_job_card(work_order.name) + job_card.append("time_logs", {"from_time": "2024-03-01 08:00:00"}) + job_card.save() + + job_card.complete_job_card( + qty=3, + for_quantity=3, + pending_qty=0, + process_loss_qty=0, + end_time="2024-03-01 09:00:00", + ) + + job_card.reload() + self.assertEqual(flt(job_card.for_quantity), 3) + self.assertEqual(flt(job_card.total_completed_qty), 3) + self.assertEqual(flt(job_card.process_loss_qty), 0) + + def test_completion_qty_keeps_for_quantity_across_cycles(self): + work_order = make_wo_order_test_record(item="_Test FG Item 2", qty=5) + + job_card = self.get_first_job_card(work_order.name) + job_card.append("time_logs", {"from_time": "2024-03-02 08:00:00"}) + job_card.save() + + job_card.complete_job_card( + qty=3, + for_quantity=5, + pending_qty=2, + process_loss_qty=0, + end_time="2024-03-02 09:00:00", + ) + + job_card.reload() + self.assertEqual(flt(job_card.for_quantity), 5) + self.assertEqual(flt(job_card.pending_qty), 2) + self.assertEqual(flt(job_card.process_loss_qty), 0) + + job_card.append("time_logs", {"from_time": "2024-03-02 10:00:00"}) + job_card.save() + + job_card.complete_job_card( + qty=2, + for_quantity=2, + pending_qty=0, + process_loss_qty=0, + end_time="2024-03-02 11:00:00", + ) + + job_card.reload() + self.assertEqual(flt(job_card.for_quantity), 5) + self.assertEqual(flt(job_card.total_completed_qty), 5) + self.assertEqual(flt(job_card.process_loss_qty), 0) + def test_op_cost_calculation(self): from erpnext.manufacturing.doctype.routing.test_routing import ( create_routing, From 0c7919429e564b3142a5a7fef32098700ae3030a Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 1 Aug 2026 18:34:30 +0530 Subject: [PATCH 078/134] fix(job_card): leave the pending qty out of the job card's own output (#57686) * fix(job_card): leave the pending qty out of the job card's own output Pending qty is the part of a job card handed over to another job card, but the status and the manufacturing entry still measured the card against its full for_quantity. A card submitted with 3 completed and 2 pending was stuck at Work In Progress with no way to change it, and its manufacturing entry was built for the full 5. Measure both against for_quantity minus pending qty, so the card reaches To Manufacture on submission, its manufacturing entry covers the completed qty, and it is Completed once that qty is manufactured. * test(job_card): cover a job card completed with a pending qty (cherry picked from commit 970039d8ecfca34b255777b69348234b5fdfbdaa) # Conflicts: # erpnext/manufacturing/doctype/job_card/job_card.py # erpnext/manufacturing/doctype/job_card/test_job_card.py --- .../doctype/job_card/job_card.js | 3 +- .../doctype/job_card/job_card.py | 52 ++++ .../doctype/job_card/test_job_card.py | 222 ++++++++++++++++++ 3 files changed, 276 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 0a4026672cf..a19d11adf5d 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -99,7 +99,8 @@ frappe.ui.form.on("Job Card", { 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); + flt(doc.manufactured_qty) + flt(doc.process_loss_qty) < + flt(doc.for_quantity) - flt(doc.pending_qty); if (!can_make_stock_entry) return; diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 3b4f8008f08..715941dd5e9 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -1280,6 +1280,53 @@ class JobCard(Document): if self.workstation: self.update_workstation_status() +<<<<<<< HEAD +======= + def get_qty_to_produce(self): + """Qty this job card is expected to produce, the pending qty is left to another job card.""" + return flt(self.for_quantity) - flt(self.pending_qty) + + def set_finished_good_status(self): + # Only reached for a submitted job card (docstatus == 1) with a finished good, see set_status(). + qty_to_produce = self.get_qty_to_produce() + + if (self.manufactured_qty + self.process_loss_qty) >= qty_to_produce: + self.status = "Completed" + elif (self.total_completed_qty + self.process_loss_qty) >= qty_to_produce: + # Production is done and the card is submitted, but the finished goods have not been + # booked into stock yet (Manufacture Stock Entry pending) — distinct from active WIP. + self.status = "To Manufacture" + elif self.transferred_qty > 0 or self.skip_material_transfer: + self.status = "Work In Progress" + + def set_non_semi_fg_status(self): + if self.items: + item_data = frappe.get_all( + "Job Card Item", + filters={"parent": self.name}, + fields=["transferred_qty", "required_qty"], + ) + all_transferred = item_data and all( + flt(d.transferred_qty) >= flt(d.required_qty) for d in item_data + ) + any_transferred = any(flt(d.transferred_qty) > 0 for d in item_data) + + if all_transferred: + self.status = "Material Transferred" + elif any_transferred: + self.status = "Partially Transferred" + elif flt(self.for_quantity) <= flt(self.transferred_qty): + self.status = "Material Transferred" + + if self.time_logs: + self.status = "Work In Progress" + + if self.docstatus == 1 and ( + self.get_qty_to_produce() <= (self.total_completed_qty + self.process_loss_qty) or not self.items + ): + self.status = "Completed" + +>>>>>>> 970039d8ec (fix(job_card): leave the pending qty out of the job card's own output (#57686)) def set_wip_warehouse(self): if not self.wip_warehouse: self.wip_warehouse = frappe.get_cached_value("Company", self.company, "default_wip_warehouse") @@ -1578,8 +1625,13 @@ class JobCard(Document): ste = ManufactureEntry( { +<<<<<<< HEAD "for_quantity": self.for_quantity - self.manufactured_qty, "process_loss_qty": max(self.process_loss_qty - get_consumed_process_loss(), 0), +======= + "for_quantity": self.get_qty_to_produce() - self.manufactured_qty, + "process_loss_qty": max(self.process_loss_qty - self.get_consumed_process_loss(), 0), +>>>>>>> 970039d8ec (fix(job_card): leave the pending qty out of the job card's own output (#57686)) "job_card": self.name, "skip_material_transfer": self.skip_material_transfer, "backflush_from_wip_warehouse": self.backflush_from_wip_warehouse, diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 826d558e830..85c73b4ff1a 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -1265,6 +1265,228 @@ class TestJobCard(ERPNextTestSuite): 8, ) +<<<<<<< HEAD +======= + def test_semi_fg_pending_qty_is_left_to_another_job_card(self): + from erpnext.manufacturing.doctype.operation.test_operation import make_operation + from erpnext.stock.doctype.item.test_item import make_item + + warehouse = "Stores - _TC" + rm = make_item("Pending Qty RM 1", {"is_stock_item": 1}).name + fg = make_item("Pending Qty FG 1", {"is_stock_item": 1}).name + + fg_bom = frappe.new_doc( + "BOM", + company="_Test Company", + item=fg, + quantity=1, + with_operations=1, + track_semi_finished_goods=1, + ) + fg_bom.append("items", {"item_code": rm, "qty": 1, "operation_row_id": 1}) + + operation = { + "operation": "Pending Qty Op A", + "workstation": "_Test Workstation A", + "finished_good": fg, + "finished_good_qty": 1, + "is_final_finished_good": 1, + "sequence_id": 1, + "time_in_mins": 60, + "source_warehouse": warehouse, + "fg_warehouse": warehouse, + "skip_material_transfer": 1, + } + + make_workstation(operation) + make_operation(operation) + fg_bom.append("operations", operation) + fg_bom.insert() + fg_bom.submit() + + work_order = make_wo_order_test_record( + item=fg, + qty=5, + source_warehouse=warehouse, + fg_warehouse=warehouse, + bom_no=fg_bom.name, + skip_transfer=1, + do_not_save=True, + ) + work_order.operations[0].time_in_mins = 60 + work_order.save() + work_order.submit() + + make_stock_entry(item_code=rm, target=warehouse, qty=100, basic_rate=100) + + job_card = self.get_first_job_card(work_order.name) + job_card.append("time_logs", {"from_time": "2024-04-01 08:00:00"}) + job_card.save() + + job_card.complete_job_card( + qty=3, + for_quantity=5, + pending_qty=2, + process_loss_qty=0, + end_time="2024-04-01 09:00:00", + ) + + job_card.reload() + self.assertEqual(flt(job_card.for_quantity), 5) + self.assertEqual(flt(job_card.pending_qty), 2) + self.assertEqual(flt(job_card.process_loss_qty), 0) + + job_card.submit() + self.assertEqual(job_card.status, "To Manufacture") + + manufacturing_entry = frappe.get_doc(job_card.make_stock_entry_for_semi_fg_item()) + finished_item = next(row for row in manufacturing_entry.items if row.is_finished_item) + self.assertEqual(flt(finished_item.qty), 3) + manufacturing_entry.submit() + + job_card.reload() + self.assertEqual(flt(job_card.manufactured_qty), 3) + self.assertEqual(job_card.status, "Completed") + + def test_semi_fg_sequence_needs_previous_operations_manufactured(self): + from erpnext.manufacturing.doctype.operation.test_operation import make_operation + from erpnext.stock.doctype.item.test_item import make_item + + warehouse = "Stores - _TC" + rm1 = make_item("Sequence Check RM 1", {"is_stock_item": 1}).name + rm2 = make_item("Sequence Check RM 2", {"is_stock_item": 1}).name + sfg1 = make_item("Sequence Check SFG 1", {"is_stock_item": 1}).name + sfg2 = make_item("Sequence Check SFG 2", {"is_stock_item": 1}).name + fg = make_item("Sequence Check FG 1", {"is_stock_item": 1}).name + + semi_fg_boms = {} + for semi_fg_item, raw_material in ((sfg1, rm1), (sfg2, rm2)): + bom = frappe.new_doc("BOM", company="_Test Company", item=semi_fg_item, quantity=1) + bom.append("items", {"item_code": raw_material, "qty": 1}) + bom.insert() + bom.submit() + semi_fg_boms[semi_fg_item] = bom.name + + fg_bom = frappe.new_doc( + "BOM", + company="_Test Company", + item=fg, + quantity=1, + with_operations=1, + track_semi_finished_goods=1, + ) + + operations = [ + { + "operation": "Sequence Check Op A", + "finished_good": sfg1, + "bom_no": semi_fg_boms[sfg1], + "sequence_id": 1, + }, + { + "operation": "Sequence Check Op B", + "finished_good": sfg2, + "bom_no": semi_fg_boms[sfg2], + "sequence_id": 1, + }, + { + "operation": "Sequence Check Op C", + "finished_good": fg, + "is_final_finished_good": 1, + "sequence_id": 2, + }, + ] + + for row in operations: + row.update( + { + "workstation": "_Test Workstation A", + "finished_good_qty": 1, + "time_in_mins": 60, + "source_warehouse": warehouse, + "fg_warehouse": warehouse, + "skip_material_transfer": 1, + } + ) + + make_workstation(row) + make_operation(row) + fg_bom.append("operations", row) + + fg_bom.append("items", {"item_code": sfg1, "qty": 1, "operation_row_id": 3}) + fg_bom.append("items", {"item_code": sfg2, "qty": 1, "operation_row_id": 3}) + fg_bom.insert() + fg_bom.submit() + + work_order = make_wo_order_test_record( + item=fg, + qty=5, + source_warehouse=warehouse, + fg_warehouse=warehouse, + bom_no=fg_bom.name, + skip_transfer=1, + do_not_save=True, + ) + + for row in work_order.operations: + row.time_in_mins = 60 + + work_order.save() + work_order.submit() + + make_stock_entry(item_code=rm1, target=warehouse, qty=10, basic_rate=100) + make_stock_entry(item_code=rm2, target=warehouse, qty=10, basic_rate=100) + + def get_job_card(operation): + return frappe.get_doc( + "Job Card", + frappe.db.get_value( + "Job Card", + {"work_order": work_order.name, "operation": operation, "docstatus": 0}, + "name", + ), + ) + + def add_time_log(job_card, day, qty): + job_card.append( + "time_logs", + { + "from_time": f"2024-01-{day} 08:00:00", + "to_time": f"2024-01-{day} 09:00:00", + "completed_qty": qty, + }, + ) + + jc_a = get_job_card("Sequence Check Op A") + jc_a.for_quantity = 3 + add_time_log(jc_a, "01", 3) + jc_a.submit() + + jc_b = get_job_card("Sequence Check Op B") + add_time_log(jc_b, "02", jc_b.for_quantity) + jc_b.submit() + frappe.get_doc(jc_b.make_stock_entry_for_semi_fg_item()).submit() + + jc_c = get_job_card("Sequence Check Op C") + jc_c.for_quantity = 3 + add_time_log(jc_c, "03", 3) + self.assertRaises(OperationSequenceError, jc_c.save) + + frappe.get_doc(jc_a.make_stock_entry_for_semi_fg_item()).submit() + + jc_c.reload() + jc_c.for_quantity = 4 + add_time_log(jc_c, "03", 4) + self.assertRaises(OperationSequenceError, jc_c.save) + + jc_c.reload() + jc_c.for_quantity = 3 + add_time_log(jc_c, "03", 3) + jc_c.submit() + + self.assertEqual(jc_c.docstatus, 1) + +>>>>>>> 970039d8ec (fix(job_card): leave the pending qty out of the job card's own output (#57686)) def test_semi_fg_batch_auto_pull_on_manufacture(self): from erpnext.manufacturing.doctype.operation.test_operation import make_operation from erpnext.stock.doctype.item.test_item import make_item From 7fcfea6db26f99520618b7c09d866324bb7f86ea Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 9 Aug 2026 20:42:02 +0530 Subject: [PATCH 079/134] chore: resolve conflict --- .../doctype/job_card/job_card.py | 55 ++++++++----------- .../doctype/job_card/test_job_card.py | 3 - 2 files changed, 22 insertions(+), 36 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index b8fa052e801..a2708bb5eac 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -1344,31 +1344,19 @@ class JobCard(Document): if data and len(data) > 0: current_operation_qty = flt(data[0].completed_qty) -<<<<<<< HEAD current_operation_qty += flt(self.total_completed_qty) - data = frappe.get_all( -======= - for row in self.get_previous_operations(): - if self.track_semi_finished_goods: - self.validate_previous_operation_manufactured_qty(row, current_operation_qty) - else: - self.validate_previous_operation(row, current_operation_qty) - - def get_previous_operations(self): previous_operations = frappe.get_all( ->>>>>>> 3bd3354152 (fix(job_card): require the previous operation to be manufactured (#57684)) "Work Order Operation", fields=["name", "operation", "status", "completed_qty", "sequence_id"], filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ("<", self.sequence_id)}, order_by="sequence_id, idx", ) -<<<<<<< HEAD message = "Job Card {}: As per the sequence of the operations in the work order {}".format( bold(self.name), bold(get_link_to_form("Work Order", self.work_order)) ) -======= + if self.track_semi_finished_goods and previous_operations: manufactured_qty = self.get_manufactured_qty_per_operation( [row.name for row in previous_operations] @@ -1377,27 +1365,11 @@ class JobCard(Document): for row in previous_operations: row.manufactured_qty = flt(manufactured_qty.get(row.name)) - return previous_operations + for row in previous_operations: + if self.track_semi_finished_goods: + self.validate_previous_operation_manufactured_qty(row, current_operation_qty) + continue - def get_manufactured_qty_per_operation(self, operation_ids): - job_card = frappe.qb.DocType("Job Card") - - data = ( - frappe.qb.from_(job_card) - .select(job_card.operation_id, Sum(job_card.manufactured_qty)) - .where( - (job_card.work_order == self.work_order) - & (job_card.docstatus == 1) - & (IfNull(job_card.is_corrective_job_card, 0) == 0) - & (job_card.operation_id.isin(operation_ids)) - ) - .groupby(job_card.operation_id) - ).run() - - return dict(data) ->>>>>>> 3bd3354152 (fix(job_card): require the previous operation to be manufactured (#57684)) - - for row in data: if not row.completed_qty: frappe.throw( _("{0}, complete the operation {1} before the operation {2}.").format( @@ -1426,6 +1398,23 @@ class JobCard(Document): ) ) + def get_manufactured_qty_per_operation(self, operation_ids): + job_card = frappe.qb.DocType("Job Card") + + data = ( + frappe.qb.from_(job_card) + .select(job_card.operation_id, Sum(job_card.manufactured_qty)) + .where( + (job_card.work_order == self.work_order) + & (job_card.docstatus == 1) + & (IfNull(job_card.is_corrective_job_card, 0) == 0) + & (job_card.operation_id.isin(operation_ids)) + ) + .groupby(job_card.operation_id) + ).run() + + return dict(data) + def validate_previous_operation_manufactured_qty(self, row, current_operation_qty): manufactured_qty = flt(row.manufactured_qty) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index db79f149345..4b80d68dd17 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -10,11 +10,8 @@ from frappe.utils.data import add_to_date, now, today from erpnext.manufacturing.doctype.job_card.job_card import ( JobCardOverTransferError, -<<<<<<< HEAD OperationMismatchError, -======= OperationSequenceError, ->>>>>>> 3bd3354152 (fix(job_card): require the previous operation to be manufactured (#57684)) OverlapError, make_corrective_job_card, make_material_request, From f176a4672219f18990e9f8698dae408a04b30c51 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 9 Aug 2026 20:43:30 +0530 Subject: [PATCH 080/134] chore: resolve conflict --- erpnext/manufacturing/doctype/job_card/job_card.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index f28490e0e87..ef71539da71 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -1517,16 +1517,6 @@ class JobCard(Document): self.set_for_quantity(kwargs) self.validate_complete_job_card_qty(kwargs) -<<<<<<< HEAD -======= - self.pending_qty = flt(kwargs.pending_qty) - self.process_loss_qty = flt(kwargs.process_loss_qty) - - self.add_completion_time_logs(kwargs) - - if kwargs.auto_submit: - self.auto_submit_job_card(kwargs.auto_submit) - def set_for_quantity(self, kwargs): """Qty to Manufacture of the completion dialog covers the current cycle only, so the qty completed by the earlier cycles of this job card is kept.""" @@ -1535,7 +1525,6 @@ class JobCard(Document): self.for_quantity = flt(self.total_completed_qty) + flt(kwargs.for_quantity) ->>>>>>> 0e1bc58b2e (fix(job_card): apply the completion dialog's qty to manufacture (#57685)) def validate_docstatus(self): if self.docstatus == 2: frappe.throw(_("Cancelled Job Card cannot be processed.")) From fab480af98d75c4994f99764fcd622e76a38ad3d Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 9 Aug 2026 20:48:19 +0530 Subject: [PATCH 081/134] chore: resolve conflict --- erpnext/public/js/shop_floor/shop_floor.js | 1750 -------------------- 1 file changed, 1750 deletions(-) delete mode 100644 erpnext/public/js/shop_floor/shop_floor.js diff --git a/erpnext/public/js/shop_floor/shop_floor.js b/erpnext/public/js/shop_floor/shop_floor.js deleted file mode 100644 index 13b8ca657d2..00000000000 --- a/erpnext/public/js/shop_floor/shop_floor.js +++ /dev/null @@ -1,1750 +0,0 @@ -// Shop Floor — an immersive, keyboard-first operator/manager interface. -// -// Two experiences share one app shell (see get_shop_floor_context on the server): -// • manager — a paginated board of work orders bucketed Pending / In Progress and Completed. -// Drilling into a work order opens its job cards in the operator pane. -// • operator — a focused workstation/work-order view to start, pause, complete and submit jobs. -// -// The whole surface is driveable from the keyboard (press ? for the cheat sheet) so an operator -// at a terminal never needs the mouse. - -// Job Card status → indicator colour, mirrored from workstation.get_status_color so the manager -// board can paint per-operation chips without a round-trip. -const JC_STATUS_COLORS = { - Completed: "green", - Submitted: "blue", - "Work In Progress": "orange", - "Material Transferred": "yellow", - "On Hold": "red", - Open: "gray", - "Not Started": "gray", -}; - -const MANAGER_BUCKETS = [ - { key: "open", label: __("Pending / In Progress"), dot: "orange" }, - { key: "completed", label: __("Completed"), dot: "green" }, -]; - -const PAGE_LENGTH = 20; - -class ShopFloor { - constructor({ wrapper }, page) { - this.wrapper = $(wrapper); - this.page = page; - this.timer_intervals = {}; - this.capacity = 1; - this.mode = null; - // Remembers each Materials panel's open/closed state (keyed by job card) so it - // survives re-renders — otherwise a reload right after a click resets the panel. - this.materials_open = {}; - // Same idea for the per-operation Work Instructions panel. - this.instructions_open = {}; - - // View state. - this.view = "operator"; // overwritten once context loads - this.active_bucket = "open"; - this.with_job_cards_only = true; // board default: hide WOs that have no job cards - this.buckets = {}; // key -> { rows, total, start, loaded } - this.selected_wo = null; - this.focus_index = -1; - this.op_state = { workstation: null, work_order: null }; - - this.make(); - this.bind_realtime(); - this.bind_lifecycle(); - this.init(); - } - - init() { - frappe.call("erpnext.manufacturing.page.shop_floor.shop_floor.get_shop_floor_context").then((r) => { - const ctx = r.message || {}; - this.view = ctx.role_view === "manager" ? "manager" : "operator"; - this.can_manage = !!ctx.can_manage; - this.user_employee = ctx.user_employee || null; - this.render_shell_controls(); - this.render_view(); - this.bind_keys(); - this.initialized = true; - this.apply_route_options(); - }); - } - - // ── App shell ──────────────────────────────────────────────────────────── - make() { - this.wrapper.append(` - ${this.styles()} -
    -
    -
    -
    -
    - - - - - -
    -
    -
    -
    -
    -
    -
    -
    - `); - - this.app = this.wrapper.find(".sf-app"); - this.brand_icon = `${__(
-			`; - this.topbar_left = this.wrapper.find(".sf-topbar-left"); - this.topbar_center = this.wrapper.find(".sf-topbar-center"); - this.body = this.wrapper.find(".sf-body"); - this.board_container = this.wrapper.find(".sf-board"); - this.detail_container = this.wrapper.find(".sf-detail"); - this.op_container = this.wrapper.find(".sf-operator"); - - this.wrapper.find(".sf-btn-home").on("click", () => (window.location.href = "/app")); - this.wrapper.find(".sf-btn-refresh").on("click", () => this.refresh()); - this.wrapper.find(".sf-btn-scan").on("click", () => this.open_scanner()); - this.wrapper.find(".sf-btn-help").on("click", () => this.show_help()); - this.wrapper.find(".sf-btn-theme").on("click", () => this.toggle_theme()); - this.update_theme_button(); - } - - // Kiosk-friendly light/dark switch: flips the standard desk theme and persists it on the - // User (same as the Ctrl+Shift+G switcher), so the choice survives reloads and follows the - // operator's login on any device. - toggle_theme() { - const next = frappe.ui.get_current_theme() === "dark" ? "light" : "dark"; - document.documentElement.setAttribute("data-theme-mode", next); - frappe.ui.set_theme(next); - frappe.xcall("frappe.core.doctype.user.user.switch_theme", { - theme: next.charAt(0).toUpperCase() + next.slice(1), - }); - this.update_theme_button(); - } - - update_theme_button() { - const dark = frappe.ui.get_current_theme() === "dark"; - this.wrapper - .find(".sf-btn-theme") - .html(dark ? "☀" : "☾") - .attr("title", dark ? __("Switch to Light Theme") : __("Switch to Dark Theme")); - } - - render_shell_controls() { - this.topbar_left.empty(); - this.topbar_center.empty(); - - // View toggle — only managers can flip between the board and a bare operator view. - const toggle = this.can_manage - ? `
    - - -
    ` - : ""; - - if (this.view === "manager") { - this.topbar_left.html(` - ${this.brand_icon}${__("Shop Floor")} - ${toggle} -
    - ${MANAGER_BUCKETS.map( - (b) => `` - ).join("")} -
    - `); - this.topbar_center.html(` - - - `); - - this.topbar_left.find(".sf-tab").on("click", (e) => { - this.switch_bucket($(e.currentTarget).attr("data-bucket")); - }); - let timer = null; - this.topbar_center.find(".sf-search-input").on("input", (e) => { - const val = e.target.value; - clearTimeout(timer); - timer = setTimeout(() => this.search_work_orders(val), 300); - }); - this.topbar_center.find(".sf-jc-toggle").on("change", (e) => { - this.toggle_job_cards_only(e.target.checked); - }); - } else { - this.topbar_left.html( - `${this.brand_icon}${__("Shop Floor")}${toggle}` - ); - this.build_operator_filters(); - } - - this.topbar_left.find(".sf-view-btn").on("click", (e) => { - this.set_view($(e.currentTarget).attr("data-view")); - }); - } - - build_operator_filters() { - this.topbar_center.html('
    '); - const $filters = this.topbar_center.find(".sf-filters"); - - this.workstation_filter = frappe.ui.form.make_control({ - df: { - fieldtype: "Link", - options: "Workstation", - fieldname: "workstation", - placeholder: __("Machine"), - onchange: () => this.load_operator(), - }, - parent: $filters, - render_input: true, - }); - this.workstation_filter.$wrapper.addClass("sf-filter-control"); - - this.work_order_filter = frappe.ui.form.make_control({ - df: { - fieldtype: "Link", - options: "Work Order", - fieldname: "work_order", - placeholder: __("Work Order"), - onchange: () => this.load_operator(), - }, - parent: $filters, - render_input: true, - }); - this.work_order_filter.$wrapper.addClass("sf-filter-control"); - } - - set_view(view) { - if (!view || view === this.view) return; - this.view = view; - this.selected_wo = null; - this.focus_index = -1; - this.render_shell_controls(); - this.render_view(); - } - - render_view() { - const manager = this.view === "manager"; - this.board_container.toggle(manager); - this.detail_container.toggle(manager && !!this.selected_wo); - this.op_container.toggle(!manager); - this.body.toggleClass("detail-open", manager && !!this.selected_wo); - - if (manager) { - this.load_bucket(this.active_bucket); - } else { - this.load_operator(); - } - } - - // ── Manager board ──────────────────────────────────────────────────────── - switch_bucket(bucket) { - if (!bucket || bucket === this.active_bucket) return; - this.active_bucket = bucket; - this.selected_wo = null; - this.focus_index = -1; - this.topbar_left.find(".sf-tab").removeClass("active"); - this.topbar_left.find(`.sf-tab[data-bucket="${bucket}"]`).addClass("active"); - this.detail_container.hide(); - this.body.removeClass("detail-open"); - this.load_bucket(bucket); - } - - search_work_orders(term) { - this.search_term = term; - // Re-query every bucket from scratch on the next visit; reload the active one now. - this.buckets = {}; - this.load_bucket(this.active_bucket); - } - - toggle_job_cards_only(checked) { - this.with_job_cards_only = !!checked; - // Filter changes every bucket's contents + counts; drop caches and clear stale counts. - this.buckets = {}; - this.topbar_left.find(".sf-tab-count").text(""); - this.load_bucket(this.active_bucket); - } - - load_bucket(bucket, append = false) { - const state = this.buckets[bucket] || { rows: [], total: 0, start: 0, loaded: false }; - const start = append ? state.start : 0; - - frappe.call({ - method: "erpnext.manufacturing.page.shop_floor.shop_floor.get_work_orders", - args: { - status_group: bucket, - start: start, - page_length: PAGE_LENGTH, - search: this.search_term || null, - with_job_cards_only: this.with_job_cards_only ? 1 : 0, - }, - callback: (r) => { - const data = r.message || {}; - const rows = data.work_orders || []; - this.buckets[bucket] = { - rows: append ? state.rows.concat(rows) : rows, - total: cint(data.total), - start: start + rows.length, - loaded: true, - }; - this.update_tab_count(bucket); - if (bucket === this.active_bucket) this.render_board(); - }, - }); - } - - update_tab_count(bucket) { - const state = this.buckets[bucket]; - if (!state) return; - this.topbar_left.find(`[data-bucket-count="${bucket}"]`).text(state.total ? state.total : ""); - } - - render_board() { - const state = this.buckets[this.active_bucket] || { rows: [], total: 0 }; - this.focus_index = -1; - - if (!state.rows.length) { - this.board_container.html(`
    ${__("No work orders here.")}
    `); - return; - } - - const cards = state.rows.map((wo) => this.work_order_card(wo)).join(""); - const more = - state.rows.length < state.total - ? `` - : `
    ${__("Showing all {0}", [state.total])}
    `; - - this.board_container.html( - `
    ${cards}
    ${more}
    ` - ); - - this.board_container.find(".sf-wo-card").on("click", (e) => { - this.open_wo($(e.currentTarget).attr("data-name")); - }); - this.board_container - .find(".sf-load-more") - .on("click", () => this.load_bucket(this.active_bucket, true)); - } - - work_order_card(wo) { - const item = wo.item_name || wo.production_item; - - // Hero image = the current operation's workstation. No item-image fallback — when the - // workstation has no image uploaded we show its initials, never the product image. - const image = wo.workstation_image - ? `` - : `${frappe.get_abbr(wo.workstation_name || item, 2)}`; - - const workstation_line = wo.workstation_name - ? `
    🏭 ${frappe.utils.escape_html( - wo.workstation_name - )}${wo.current_operation ? ` · ${frappe.utils.escape_html(wo.current_operation)}` : ""}
    ` - : ""; - - // Operations bar: green segment (done) + orange segment (in progress); grey track = pending. - const done_pct = Math.min(cint(wo.per_operations), 100); - const wip_pct = Math.min(cint(wo.per_in_progress), 100 - done_pct); - - return ` -
    -
    -
    ${image}
    -
    -
    ${frappe.utils.escape_html(item)}
    - ${workstation_line} -
    - - ${wo.name} -
    -
    -
    -
    -
    - ${__("Operations")} - ${cint(wo.completed_operations)} / ${cint(wo.total_operations)} -
    -
    -
    -
    -
    -
    -
    - `; - } - - open_wo(name) { - if (!name) return; - this.selected_wo = name; - this.op_state = { workstation: null, work_order: name }; - this.detail_container.show(); - this.body.addClass("detail-open"); - this.board_container - .find(".sf-wo-card") - .removeClass("sf-selected") - .filter(`[data-name="${name}"]`) - .addClass("sf-selected"); - // The detail pane reuses the operator rendering for a single work order. - this.detail_container.html(` -
    - - ${frappe.utils.escape_html(name)} - ${__("Open")} -
    -
    - `); - this.detail_container.find(".sf-detail-back").on("click", () => this.close_wo()); - this.op_container_target = this.detail_container.find(".sf-detail-body"); - this.load_operator_data(this.op_container_target, { work_order: name }); - } - - close_wo() { - this.selected_wo = null; - this.op_container_target = null; - this.detail_container.hide().empty(); - this.body.removeClass("detail-open"); - this.board_container.find(".sf-wo-card").removeClass("sf-selected"); - } - - // ── Operator pane ────────────────────────────────────────────────────────── - // Resolves the container the operator content renders into: the standalone operator - // view, or the manager's drill-down detail pane. - current_op_container() { - return this.view === "manager" ? this.op_container_target : this.op_container; - } - - load_operator() { - const workstation = this.workstation_filter ? this.workstation_filter.get_value() : null; - const work_order = this.work_order_filter ? this.work_order_filter.get_value() : null; - this.op_state = { workstation, work_order }; - - if (!workstation && !work_order) { - this.clear_timers(); - this.op_container.html( - `
    ${__("Select a machine or work order to begin")}
    ` - ); - return; - } - this.load_operator_data(this.op_container, { workstation, work_order }); - } - - load_operator_data($container, { workstation, work_order }) { - frappe.call({ - method: "erpnext.manufacturing.page.shop_floor.shop_floor.get_data", - args: { - workstation: work_order ? null : workstation, - work_order: work_order || null, - }, - callback: (r) => { - const data = r.message || {}; - this.job_cards = data.job_cards || []; - this.capacity = cint(data.capacity) || 1; - this.mode = data.mode || (work_order ? "work_order" : "workstation"); - this.oee = data.oee || null; - if (data.user_employee) this.user_employee = data.user_employee; - this.today_sessions = data.today_sessions || []; - this.workstation = workstation; - this.work_order = work_order; - this.compute_state(); - this.dedupe_today_sessions(); - this.render_operator($container); - }, - }); - } - - // A job card already shown under Completed Operations shouldn't repeat in - // Today's Sessions — keep it in Completed Operations only. - dedupe_today_sessions() { - const shown = new Set((this.completed || []).map((jc) => jc.name)); - this.today_sessions = (this.today_sessions || []).filter((s) => !shown.has(s.name)); - } - - // Re-fetch whichever operator content is currently on screen (used after every action). - reload() { - if (this.view === "manager" && this.selected_wo) { - this.load_operator_data(this.op_container_target, { work_order: this.selected_wo }); - // Keep the board chips fresh too. - this.buckets = {}; - this.load_bucket(this.active_bucket); - } else if (this.view === "manager") { - this.load_bucket(this.active_bucket); - } else { - this.load_operator(); - } - } - - refresh() { - if (this.view === "manager") { - this.buckets = {}; - } - this.reload(); - } - - compute_state() { - this.active_jobs = []; - this.queue = []; - this.pending_submission = []; - this.completed = []; - // Submitted but the finished goods aren't booked yet (status "To Manufacture") — its own - // actionable section, kept out of Completed Operations / Today's Sessions. - this.to_manufacture = []; - - for (const jc of this.job_cards) { - // Same materials-ready rule as job_card.js make_dashboard. - jc._materials_ready = !!( - jc.skip_material_transfer || - flt(jc.transferred_qty) >= flt(jc.for_quantity) + flt(jc.process_loss_qty) || - !jc.finished_good - ); - - // Submitted JCs are historical from the Shop Floor's POV — only appear here in work_order - // mode (and, for "To Manufacture", in workstation mode too — see _fetch_job_cards). - if (jc.docstatus === 1) { - if (jc.status === "To Manufacture") { - this.to_manufacture.push(jc); - } else { - this.completed.push(jc); - } - continue; - } - - const last_log = - jc.time_logs && jc.time_logs.length ? jc.time_logs[jc.time_logs.length - 1] : null; - const is_running = last_log && !last_log.to_time && !jc.is_paused; - const is_paused = jc.is_paused; - - if (is_running || is_paused) { - this.active_jobs.push(jc); - } else if (jc.status === "Completed") { - // All qty accounted for but still draft — waiting on Submit. - this.pending_submission.push(jc); - } else { - this.queue.push(jc); - } - } - - // Slot rules — all active jobs are always shown; the grid (col-md-6) wraps them 2 per row. - // workstation mode: capacity-many slots, expanded to fit every active job (+ empty placeholders). - // work_order mode: one slot per active job (no empty placeholders). - let slot_count; - if (this.mode === "work_order") { - slot_count = this.active_jobs.length; - } else { - slot_count = Math.max(this.capacity, this.active_jobs.length, 1); - } - - this.slots = []; - for (let i = 0; i < slot_count; i++) { - this.slots.push(this.active_jobs[i] || null); - } - - // Auto-pick: when nothing is running, surface the next queue item in the slot. - if (this.active_jobs.length === 0 && this.queue.length > 0) { - const next_up = this.queue.shift(); - next_up._is_next_up = true; - this.slots[0] = next_up; - } - - this.summary = { - active_count: this.active_jobs.length, - // "To Manufacture" (submitted, qty done, but the Manufacture Stock Entry is still pending) - // isn't actually finished — count it as Pending, not Completed. - queue_count: this.queue.length + this.to_manufacture.length, - completed_count: this.completed.length + this.pending_submission.length, - capacity: this.capacity, - }; - } - - render_operator($container) { - this.clear_timers(); - $container.empty(); - - const html = frappe.render_template("shop_floor_template", { - workstation: this.workstation, - work_order: this.work_order, - mode: this.mode, - slots: this.slots, - active_jobs: this.active_jobs, - queue: this.queue, - pending_submission: this.pending_submission, - to_manufacture: this.to_manufacture, - completed: this.completed, - today_sessions: this.today_sessions || [], - summary: this.summary, - oee: this.oee, - }); - $container.html(html); - - // Restore each Materials panel to its remembered open/closed state. - $container.find(".mes-materials-inline").each((i, el) => { - const $el = $(el); - const name = $el.attr("data-job-card"); - if (!name) return; - if (name in this.materials_open) { - $el.toggleClass("is-open", this.materials_open[name]); - } else { - this.materials_open[name] = $el.hasClass("is-open"); - } - }); - - // Restore each Work Instructions panel to its remembered open/closed state. - $container.find(".mes-instructions-inline").each((i, el) => { - const $el = $(el); - const name = $el.attr("data-job-card"); - if (name && name in this.instructions_open) { - $el.toggleClass("is-open", this.instructions_open[name]); - } - }); - - this.bind_events($container); - - for (const jc of this.active_jobs) { - if (jc.is_paused) { - this.render_timer(jc.name, this.elapsed_seconds(jc), $container); - } else { - this.start_timer_for(jc, $container); - } - } - } - - clear_timers() { - for (const id of Object.values(this.timer_intervals)) { - clearInterval(id); - } - this.timer_intervals = {}; - } - - bind_events($container) { - const me = this; - - $container.find(".mes-materials-summary").on("click", function (e) { - if ($(e.target).closest(".mes-btn-transfer").length) return; - const $inline = $(this).closest(".mes-materials-inline"); - const open = !$inline.hasClass("is-open"); - $inline.toggleClass("is-open", open); - const name = $inline.attr("data-job-card"); - if (name) me.materials_open[name] = open; - }); - - $container.find(".mes-instructions-summary").on("click", function () { - const $inline = $(this).closest(".mes-instructions-inline"); - const open = !$inline.hasClass("is-open"); - $inline.toggleClass("is-open", open); - const name = $inline.attr("data-job-card"); - if (name) me.instructions_open[name] = open; - }); - - // Clicking a "QC Required" / "QC Available" pill runs the inline check ahead of End Session. - $container.find(".mes-qc-pill").on("click", function () { - const name = $(this).attr("data-job-card"); - const jc = (me.active_jobs || []).find((j) => j.name === name); - if (jc) me.run_quality_check(jc, () => me.reload()); - }); - - $container.find(".mes-btn-start").on("click", function () { - me.start_job($(this).attr("data-job-card")); - }); - $container.find(".mes-btn-pause").on("click", function () { - me.pause_job($(this).attr("data-job-card")); - }); - $container.find(".mes-btn-resume").on("click", function () { - me.resume_job($(this).attr("data-job-card")); - }); - $container.find(".mes-btn-end-session").on("click", function () { - me.end_session($(this).attr("data-job-card")); - }); - $container.find(".mes-btn-submit").on("click", function () { - me.submit_job_card($(this).attr("data-job-card")); - }); - $container.find(".mes-btn-make-entry").on("click", function () { - me.make_manufacture_entry($(this).attr("data-job-card")); - }); - $container.find(".mes-btn-transfer").on("click", function (e) { - e.preventDefault(); - me.transfer_materials($(this).attr("data-job-card")); - }); - } - - // ── Operator actions (unchanged behaviour, reload() instead of load()) ───── - start_job(job_card) { - const me = this; - if (this.mode === "workstation" && this.active_jobs.length >= this.capacity) { - frappe.msgprint({ - title: __("Capacity Reached"), - message: __( - "This machine can run at most {0} job(s) in parallel. Pause or complete a running job before starting another.", - [this.capacity] - ), - indicator: "orange", - }); - return; - } - - const default_employee = this.user_employee; - const dialog = new frappe.ui.Dialog({ - title: __("Start Job"), - fields: [ - { - label: __("Start Time"), - fieldname: "start_time", - fieldtype: "Datetime", - default: frappe.datetime.now_datetime(), - }, - { fieldtype: "Section Break" }, - { - label: __("Employees"), - fieldname: "employees", - fieldtype: "Table", - data: default_employee ? [{ employee: default_employee }] : [], - fields: [ - { - label: __("Employee"), - fieldname: "employee", - fieldtype: "Link", - options: "Employee", - in_list_view: 1, - }, - ], - }, - ], - primary_action_label: __("Start"), - primary_action: (values) => { - dialog.hide(); - me.update_job_card(job_card, "start_timer", { - start_time: values.start_time, - employees: values.employees || [], - }); - }, - }); - dialog.show(); - this.bind_enter_submit(dialog); - } - - // Make a dialog fully keyboard-operable: Enter triggers the primary action, so an operator - // never has to reach for the mouse. Enter is left alone inside multi-line fields and while an - // autocomplete (Link/Select) dropdown is open, so it can still pick a value. - bind_enter_submit(dialog) { - dialog.$wrapper.on("keydown.sfenter", (e) => { - if (e.key !== "Enter" || e.shiftKey) return; - if ($(e.target).is("textarea")) return; - if ($(".awesomplete > ul:not([hidden])").length) return; - const $btn = dialog.get_primary_btn(); - if ( - $btn && - $btn.length && - $btn.is(":visible") && - !$btn.hasClass("disabled") && - !$btn.prop("disabled") - ) { - e.preventDefault(); - e.stopPropagation(); - $btn.trigger("click"); - } - }); - } - - pause_job(jc_name) { - this.update_job_card(jc_name, "pause_job", { end_time: frappe.datetime.now_datetime() }); - } - - resume_job(jc_name) { - this.update_job_card(jc_name, "resume_job", { start_time: frappe.datetime.now_datetime() }); - } - - end_session(jc_name) { - const me = this; - const jc = this.active_jobs.find((j) => j.name === jc_name); - if (!jc) return; - - let pending = flt(jc.for_quantity) - flt(jc.total_completed_qty); - if (flt(jc.pending_qty) > 0) { - pending = flt(jc.pending_qty); - } - - const fields = [ - { - fieldtype: "Float", - label: __("Qty to Manufacture in this Cycle"), - fieldname: "for_quantity", - reqd: 1, - default: pending, - description: __("Completed, Pending and Process Loss quantities must add up to this."), - change() { - const d = me.session_dialog; - d.set_value("completed_qty", d.get_value("for_quantity")); - d.set_value("pending_qty", 0); - d.set_value("process_loss_qty", 0); - }, - }, - { - fieldtype: "Float", - label: __("Completed Quantity"), - fieldname: "completed_qty", - reqd: 1, - default: pending, - change() { - const d = me.session_dialog; - const remaining = - flt(d.get_value("for_quantity")) - - flt(d.get_value("completed_qty")) - - flt(d.get_value("process_loss_qty")); - - if (remaining < 0) { - const max_completed_qty = - flt(d.get_value("for_quantity")) - flt(d.get_value("process_loss_qty")); - d.set_value("completed_qty", max_completed_qty); - frappe.throw( - __("Completed Quantity cannot be greater than {0}", [max_completed_qty]) - ); - } - - if (remaining !== flt(d.get_value("pending_qty"))) { - d.set_value("pending_qty", remaining); - } - }, - }, - { - fieldtype: "Float", - label: __("Pending Quantity"), - fieldname: "pending_qty", - default: 0.0, - description: __("Qty left for a later cycle or for another job card."), - change() { - const d = me.session_dialog; - const pl = - flt(d.get_value("for_quantity")) - - flt(d.get_value("completed_qty")) - - flt(d.get_value("pending_qty")); - - if (pl < 0) { - d.set_value("pending_qty", 0); - frappe.throw( - __("Pending Quantity cannot be greater than {0}", [ - flt(d.get_value("for_quantity")) - flt(d.get_value("completed_qty")), - ]) - ); - } - - if (pl !== flt(d.get_value("process_loss_qty"))) { - d.set_value("process_loss_qty", pl); - } - }, - }, - { - fieldtype: "Float", - label: __("Process Loss Quantity"), - fieldname: "process_loss_qty", - default: 0.0, - description: __("Qty scrapped in this cycle, nobody will produce it."), - change() { - const d = me.session_dialog; - const remaining = - flt(d.get_value("for_quantity")) - - flt(d.get_value("completed_qty")) - - flt(d.get_value("process_loss_qty")); - - if (remaining < 0) { - d.set_value("process_loss_qty", 0); - frappe.throw( - __("Process Loss Quantity cannot be greater than {0}", [ - flt(d.get_value("for_quantity")) - flt(d.get_value("completed_qty")), - ]) - ); - } - - if (remaining !== flt(d.get_value("pending_qty"))) { - d.set_value("pending_qty", remaining); - } - }, - }, - { fieldtype: "Section Break" }, - { - fieldtype: "Datetime", - label: __("End Time"), - fieldname: "end_time", - default: frappe.datetime.now_datetime(), - }, - ]; - - const get_payload = () => { - const data = me.session_dialog.get_values(); - if (!data) return null; - if (flt(data.completed_qty) <= 0) { - frappe.throw(__("Completed Quantity should be greater than 0")); - } - return { - job_card: jc.name, - qty: flt(data.completed_qty), - for_quantity: flt(data.for_quantity), - pending_qty: flt(data.pending_qty), - process_loss_qty: flt(data.process_loss_qty), - end_time: data.end_time, - }; - }; - - const save_and_continue = () => { - const args = get_payload(); - if (!args) return; - me.session_dialog.hide(); - frappe.call({ - method: "erpnext.manufacturing.page.shop_floor.shop_floor.save_and_continue", - args: args, - freeze: true, - freeze_message: __("Saving job card..."), - callback: () => me.reload(), - }); - }; - - const finalize_submit = (args) => { - frappe.call({ - method: "erpnext.manufacturing.page.shop_floor.shop_floor.complete_and_submit", - args: args, - freeze: true, - freeze_message: __("Submitting job card..."), - callback: (r) => { - me.reload(); - if (r.message && r.message.finished_good) { - me.prompt_manufacture_entry(jc.name); - } - }, - }); - }; - - const submit_session = () => { - const args = get_payload(); - if (!args) return; - me.session_dialog.hide(); - // Guided QC gate: a job card that requires inspection must pass an inline Quality Check - // before it is submitted (mirrors Job Card.validate_inspection on the server). Once the - // inspection is recorded, finalize the session submit. - if (jc.qc && jc.qc.required && jc.qc.status !== "Accepted") { - me.run_quality_check(jc, () => finalize_submit(args)); - } else { - finalize_submit(args); - } - }; - - me.session_dialog = new frappe.ui.Dialog({ - title: __("End Session"), - fields: fields, - primary_action_label: __("Submit"), - primary_action: submit_session, - secondary_action_label: __("Save & Continue"), - secondary_action: save_and_continue, - }); - me.session_dialog.show(); - me.bind_enter_submit(me.session_dialog); - } - - // ── Inline Quality Check ───────────────────────────────────────────────────── - // Fetch the operation's Quality Inspection template and open a guided pass/fail checklist. - // `on_pass` runs once the inspection has been recorded (and is not rejected). - run_quality_check(jc, on_pass) { - const me = this; - frappe.call({ - method: "erpnext.manufacturing.page.shop_floor.shop_floor.get_quality_inspection_checklist", - args: { job_card: jc.name }, - freeze: true, - freeze_message: __("Loading quality checklist..."), - callback: (r) => { - const info = r.message || {}; - if (!info.template || !(info.parameters || []).length) { - // Inspection is required but the operation has no template/parameters to fill — - // there is nothing to capture inline. Point the user at the configuration. - frappe.msgprint({ - title: __("Quality Inspection Template Missing"), - indicator: "orange", - message: __( - "This operation requires a Quality Inspection but no template with parameters is configured. Set a Quality Inspection Template on Operation {0} to inspect from the Shop Floor.", - [jc.operation || ""] - ), - }); - return; - } - me.show_qc_dialog(jc, info, on_pass); - }, - }); - } - - show_qc_dialog(jc, info, on_pass) { - const me = this; - const params = info.parameters || []; - // Per-row operator input, keyed by row index (avoids escaping issues with parameter names). - const state = {}; // idx -> "Accepted" | "Rejected" - - const rows = params - .map((p, i) => { - const spec = frappe.utils.escape_html(p.specification); - let criteria = ""; - if (p.numeric) { - const lo = p.min_value !== null && p.min_value !== undefined ? p.min_value : "−∞"; - const hi = p.max_value !== null && p.max_value !== undefined ? p.max_value : "∞"; - criteria = __("Acceptable range: {0} to {1}", [lo, hi]); - } else if (p.value) { - criteria = __("Expected: {0}", [frappe.utils.escape_html(p.value)]); - } - const control = p.numeric - ? `` - : ` - - - `; - return `
    -
    -
    ${spec}
    - ${criteria ? `
    ${criteria}
    ` : ""} -
    -
    ${control}
    -
    `; - }) - .join(""); - - const dialog = new frappe.ui.Dialog({ - title: __("Quality Check"), - size: "large", - fields: [ - { - fieldtype: "HTML", - options: `
    ${__( - "Inspect {0} for job card {1}", - [frappe.utils.escape_html(info.item_code || ""), frappe.utils.escape_html(jc.name)] - )}
    ${rows}
    `, - }, - ], - primary_action_label: __("Submit Inspection"), - primary_action: () => { - const readings = []; - let missing = false; - params.forEach((p, i) => { - if (p.numeric) { - const val = dialog.$wrapper.find(`.mes-qc-reading[data-idx="${i}"]`).val(); - if (val === "" || val === undefined || val === null) missing = true; - readings.push({ specification: p.specification, reading_value: val }); - } else { - if (!state[i]) missing = true; - readings.push({ - specification: p.specification, - status: state[i], - reading_value: "", - }); - } - }); - if (missing) { - frappe.msgprint(__("Please complete every check before submitting the inspection.")); - return; - } - dialog.hide(); - frappe.call({ - method: "erpnext.manufacturing.page.shop_floor.shop_floor.submit_quality_inspection", - args: { job_card: jc.name, readings: JSON.stringify(readings) }, - freeze: true, - freeze_message: __("Recording inspection..."), - callback: (r) => { - const res = r.message || {}; - if (res.status === "Rejected") { - // Don't auto-proceed on a rejected inspection — the server gate may block the - // submit anyway (per Stock Settings), and the operator should decide next steps. - frappe.msgprint({ - title: __("Inspection Rejected"), - indicator: "red", - message: __( - "Quality Inspection {0} is Rejected. Resolve the issue or follow your rejection process before submitting the job card.", - [res.name || ""] - ), - }); - me.reload(); - return; - } - if (on_pass) on_pass(); - }, - }); - }, - }); - - dialog.show(); - // Pass/Fail toggles for qualitative parameters. - dialog.$wrapper.find(".mes-qc-passfail button").on("click", function () { - const $btn = $(this); - const $grp = $btn.closest(".mes-qc-passfail"); - $grp.find("button").removeClass("active"); - $btn.addClass("active"); - state[$grp.attr("data-idx")] = $btn.attr("data-val"); - }); - } - - prompt_manufacture_entry(jc_name) { - const me = this; - const dialog = new frappe.ui.Dialog({ - title: __("Job Card Submitted"), - fields: [ - { - fieldtype: "HTML", - options: ` -
    -
    - ${__("Job card {0} has been submitted.", [frappe.utils.escape_html(jc_name)])} -
    -
    - ${__("Create a Manufacture stock entry for the finished goods?")} -
    -
    - `, - }, - ], - primary_action_label: __("Make Manufacture Entry"), - primary_action: () => { - dialog.hide(); - me.make_manufacture_entry(jc_name); - }, - secondary_action_label: __("Skip"), - secondary_action: () => dialog.hide(), - }); - dialog.show(); - this.bind_enter_submit(dialog); - } - - submit_job_card(jc_name) { - const me = this; - frappe.confirm(__("Submit job card {0}? This finalizes the job card.", [jc_name]), () => { - frappe.call({ - method: "erpnext.manufacturing.page.shop_floor.shop_floor.submit_job_card", - args: { job_card: jc_name }, - freeze: true, - freeze_message: __("Submitting job card..."), - callback: () => me.reload(), - }); - }); - } - - make_manufacture_entry(jc_name) { - frappe.call({ - method: "erpnext.manufacturing.page.shop_floor.shop_floor.make_manufacture_stock_entry", - args: { job_card: jc_name }, - freeze: true, - freeze_message: __("Preparing stock entry..."), - callback: (r) => { - if (r.message && r.message.name) { - window.open(`/app/stock-entry/${encodeURIComponent(r.message.name)}`, "_blank"); - } - }, - }); - } - - transfer_materials(jc_name) { - if (!jc_name) return; - frappe.call({ - method: "erpnext.manufacturing.doctype.job_card.job_card.make_stock_entry", - args: { source_name: jc_name }, - callback: (r) => { - const doc = frappe.model.sync(r.message); - frappe.set_route("Form", doc[0].doctype, doc[0].name); - }, - }); - } - - update_job_card(job_card, method, data, on_success) { - const me = this; - frappe.call({ - method: "erpnext.manufacturing.doctype.workstation.workstation.update_job_card", - args: { - job_card: job_card, - method: method, - start_time: data.start_time || "", - employees: data.employees || [], - end_time: data.end_time || "", - qty: data.qty || 0, - for_quantity: data.for_quantity || 0, - pending_qty: data.pending_qty || 0, - process_loss_qty: data.process_loss_qty || 0, - auto_submit: data.auto_submit || 0, - }, - freeze: true, - freeze_message: __("Updating job card..."), - callback: () => { - me.reload(); - if (on_success) on_success(); - }, - }); - } - - // ── Timers ──────────────────────────────────────────────────────────────── - start_timer_for(jc, $container) { - let elapsed = this.elapsed_seconds(jc); - this.render_timer(jc.name, elapsed, $container); - this.timer_intervals[jc.name] = setInterval(() => { - elapsed += 1; - this.render_timer(jc.name, elapsed, $container); - }, 1000); - } - - elapsed_seconds(jc) { - let total = 0; - for (const log of jc.time_logs || []) { - if (log.to_time) { - if (log.time_in_mins) { - total += flt(log.time_in_mins, 2) * 60; - } else { - total += moment(log.to_time).diff(log.from_time, "seconds"); - } - } else { - total += moment().diff(log.from_time, "seconds"); - } - } - return total; - } - - render_timer(jc_name, seconds, $container) { - const h = Math.floor(seconds / 3600); - const m = Math.floor((seconds - h * 3600) / 60); - const s = cint(seconds - h * 3600 - m * 60); - const pad = (n) => (n < 10 ? "0" + n : String(n)); - - const scope = $container || this.wrapper; - const timer = scope.find(`.mes-job-timer[data-job-card="${jc_name}"]`); - timer.find(".h").text(pad(h)); - timer.find(".m").text(pad(m)); - timer.find(".s").text(pad(s)); - } - - // ── Realtime + lifecycle ─────────────────────────────────────────────────── - bind_realtime() { - frappe.realtime.on("update_workstation_status", (data) => { - if (data && data.name === this.op_state.workstation) { - this.reload(); - } - }); - } - - bind_lifecycle() { - // Frappe has no on_page_hide hook, so toggle immersive mode + keyboard binding on - // route changes ourselves. - this._route_handler = () => { - const on_page = (frappe.get_route_str() || "").startsWith("shop-floor"); - if (on_page) { - $(document.body).addClass("shop-floor-active"); - this.bind_keys(); - } else { - $(document.body).removeClass("shop-floor-active"); - this.unbind_keys(); - this.clear_timers(); - } - }; - frappe.router.on("change", this._route_handler); - } - - on_show() { - $(document.body).addClass("shop-floor-active"); - this.bind_keys(); - // Cached re-navigation (e.g. the Work Order "Shop Floor" button) lands here with fresh - // route_options; init() handles the very first load before we're initialized. - if (this.initialized) this.apply_route_options(); - } - - // ── Keyboard ──────────────────────────────────────────────────────────────── - bind_keys() { - $(document).off("keydown.shopfloor"); - $(document).on("keydown.shopfloor", (e) => this.handle_key(e)); - } - - unbind_keys() { - $(document).off("keydown.shopfloor"); - } - - is_typing(e) { - const tag = (e.target.tagName || "").toLowerCase(); - return tag === "input" || tag === "textarea" || tag === "select" || e.target.isContentEditable; - } - - handle_key(e) { - // Let dialogs own the keyboard while open. - if ($(".modal:visible").length) return; - - const typing = this.is_typing(e); - - // Escape works even while typing (blur the search / close the detail pane). - if (e.key === "Escape") { - if (typing) { - e.target.blur(); - return; - } - if (this.view === "manager" && this.selected_wo) { - this.close_wo(); - e.preventDefault(); - } - return; - } - - if (typing) return; - - switch (e.key) { - case "?": - this.show_help(); - e.preventDefault(); - return; - case "/": - this.topbar_center.find(".sf-search-input").focus(); - e.preventDefault(); - return; - case "r": - this.refresh(); - e.preventDefault(); - return; - case "b": - this.open_scanner(); - e.preventDefault(); - return; - case "1": - case "2": - if (this.view === "manager" && MANAGER_BUCKETS[cint(e.key) - 1]) { - this.switch_bucket(MANAGER_BUCKETS[cint(e.key) - 1].key); - e.preventDefault(); - } - return; - } - - // View switch chord: "g" then "m"/"o". - if (e.key === "g") { - this._g_pending = true; - setTimeout(() => (this._g_pending = false), 600); - return; - } - if (this._g_pending && (e.key === "m" || e.key === "o")) { - this._g_pending = false; - if (this.can_manage) this.set_view(e.key === "m" ? "manager" : "operator"); - return; - } - - // Navigation. - if (e.key === "ArrowDown" || e.key === "j") { - this.move_focus(1); - e.preventDefault(); - return; - } - if (e.key === "ArrowUp" || e.key === "k") { - this.move_focus(-1); - e.preventDefault(); - return; - } - if (e.key === "Enter") { - this.activate_focus(); - e.preventDefault(); - return; - } - - // Job actions on the focused card — reuse the rendered buttons. - const map = { - s: ".mes-btn-start, .mes-btn-resume", - p: ".mes-btn-pause, .mes-btn-resume", - e: ".mes-btn-end-session", - t: ".mes-btn-transfer", - }; - if (e.key === "S" && e.shiftKey) { - this.click_job_action(".mes-btn-submit"); - e.preventDefault(); - return; - } - if (map[e.key]) { - this.click_job_action(map[e.key]); - e.preventDefault(); - } - } - - // Job actions act on the focused job card (operator view); when the focus is on a board - // work order (manager view with the detail open) they fall back to the detail's active job. - click_job_action(selector) { - const $el = this.focused_el(); - if ($el && $el.attr("data-kind") === "job") { - const $btn = $el.find(selector).filter(":visible").first(); - if ($btn.length) { - $btn.trigger("click"); - return; - } - } - const scope = this.current_op_container(); - if (scope && scope.length) { - const $btn = scope.find(selector).filter(":visible").first(); - if ($btn.length) $btn.trigger("click"); - } - } - - focusables() { - // Manager always navigates the board work orders — even with the detail open, so the - // arrow keys switch work orders. The standalone operator view navigates its job cards. - const scope = this.view === "manager" ? this.board_container : this.current_op_container(); - if (!scope || !scope.length) return $(); - return scope.find("[data-sf-focusable]"); - } - - move_focus(delta) { - const $items = this.focusables(); - if (!$items.length) return; - this.focus_index = Math.max(0, Math.min($items.length - 1, this.focus_index + delta)); - $items.removeClass("sf-focused"); - const $target = $items.eq(this.focus_index); - $target.addClass("sf-focused"); - $target[0].scrollIntoView({ block: "nearest", behavior: "smooth" }); - // Browsing work orders with the detail already open → switch the detail to the focused one. - if (this.view === "manager" && this.selected_wo && $target.attr("data-kind") === "wo") { - this.open_wo($target.attr("data-name")); - } - } - - focused_el() { - const $items = this.focusables(); - if (this.focus_index < 0 || this.focus_index >= $items.length) return null; - return $items.eq(this.focus_index); - } - - activate_focus() { - const $el = this.focused_el(); - if (!$el) return; - if ($el.attr("data-kind") === "wo") { - this.open_wo($el.attr("data-name")); - } else { - // First visible primary button drives the job card (Start / Resume / End Session). - const $btn = $el.find(".btn-primary:visible").first(); - if ($btn.length) $btn.trigger("click"); - } - } - - show_help() { - const rows = [ - ["?", __("Show this help")], - ["/", __("Search work orders")], - ["r", __("Refresh")], - ["b", __("Scan job card")], - ["g then m / o", __("Switch Board / Operator view")], - ["1 / 2", __("Switch board tab")], - ["↑ / ↓ or j / k", __("Move selection")], - ["Enter", __("Open work order / run primary action")], - ["Esc", __("Close detail / blur search")], - ["s", __("Start / Resume job")], - ["p", __("Pause / Resume job")], - ["e", __("End session for active job")], - ["t", __("Transfer materials")], - ["Shift + S", __("Submit focused job card")], - ]; - const html = `
    ${rows - .map((r) => `
    ${r[0]}${r[1]}
    `) - .join("")}
    `; - const d = new frappe.ui.Dialog({ - title: __("Keyboard Shortcuts"), - fields: [{ fieldtype: "HTML", options: html }], - }); - d.show(); - } - - // ── Scanner ────────────────────────────────────────────────────────────── - open_scanner() { - const me = this; - const dialog = new frappe.ui.Dialog({ - title: __("Scan Job Card"), - fields: [ - { - label: __("Scan or enter Job Card"), - fieldname: "job_card", - fieldtype: "Data", - options: "Barcode", - }, - ], - primary_action_label: __("Continue"), - primary_action: (values) => { - if (!values.job_card) return; - dialog.hide(); - me.handle_scanned_job_card(values.job_card); - }, - }); - dialog.show(); - this.bind_enter_submit(dialog); - } - - handle_scanned_job_card(job_card) { - const me = this; - const jc = (this.job_cards || []).find((j) => j.name === job_card); - if (jc) { - me.route_scanned_action(jc); - return; - } - frappe.db.get_value("Job Card", job_card, ["status", "is_paused", "docstatus"]).then((r) => { - const data = r && r.message; - if (!data || !data.status) { - frappe.msgprint(__("Job Card {0} was not found.", [job_card])); - return; - } - if (cint(data.docstatus) === 1) { - frappe.msgprint(__("Job Card {0} is already submitted.", [job_card])); - } else if (cint(data.is_paused)) { - me.resume_job(job_card); - } else if (data.status === "Work In Progress") { - frappe.msgprint( - __( - "Job Card {0} is already running. Open its machine or work order to pause or complete it.", - [job_card] - ) - ); - } else if (data.status === "Completed") { - me.submit_job_card(job_card); - } else { - me.start_job(job_card); - } - }); - } - - route_scanned_action(jc) { - const me = this; - if (jc.docstatus === 1) { - frappe.msgprint(__("Job Card {0} is already submitted.", [jc.name])); - return; - } - if (jc.status === "Completed") { - me.submit_job_card(jc.name); - return; - } - if (jc.is_paused) { - me.resume_job(jc.name); - return; - } - const last_log = jc.time_logs && jc.time_logs.length ? jc.time_logs[jc.time_logs.length - 1] : null; - const is_running = !!(last_log && !last_log.to_time); - if (is_running) { - me.prompt_running_action(jc); - } else { - me.start_job(jc.name); - } - } - - prompt_running_action(jc) { - const me = this; - const dialog = new frappe.ui.Dialog({ - title: __("Job {0} is running", [jc.name]), - fields: [ - { - fieldtype: "HTML", - options: ` -
    - ${__("{0} is already in progress. Pause it or complete the session.", [ - frappe.utils.escape_html(jc.finished_good || jc.production_item || jc.name), - ])} -
    - `, - }, - ], - primary_action_label: __("Complete"), - primary_action: () => { - dialog.hide(); - me.end_session(jc.name); - }, - secondary_action_label: __("Pause"), - secondary_action: () => { - dialog.hide(); - me.pause_job(jc.name); - }, - }); - dialog.show(); - this.bind_enter_submit(dialog); - } - - // ── Route options (e.g. the Work Order "Shop Floor" button) ──────────────── - apply_route_options() { - const opts = frappe.route_options; - if (!opts || (!opts.work_order && !opts.workstation)) { - return; - } - frappe.route_options = null; - - // A specific work order / machine was requested — show it in the operator view. - this.view = "operator"; - this.render_shell_controls(); - this.render_view(); - Promise.all([ - this.work_order_filter.set_value(opts.work_order || ""), - this.workstation_filter.set_value(opts.workstation || ""), - ]).then(() => this.load_operator()); - } - - // ── Styles ────────────────────────────────────────────────────────────────── - styles() { - return ``; - } -} - -frappe.ui.ShopFloor = ShopFloor; From c955f80675af2a48b7a8bce13f71cb63704b5175 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 9 Aug 2026 20:50:50 +0530 Subject: [PATCH 082/134] chore: resolve conflict --- .../doctype/job_card/job_card.py | 64 +- .../doctype/job_card/test_job_card.py | 105 +- erpnext/public/js/shop_floor/shop_floor.js | 1747 ----------------- 3 files changed, 37 insertions(+), 1879 deletions(-) delete mode 100644 erpnext/public/js/shop_floor/shop_floor.js diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 7ca2c7fb136..ba41a7c67fe 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -901,18 +901,6 @@ class JobCard(Document): + flt(self.pending_qty, precision) ) -<<<<<<< HEAD - if self.for_quantity and flt(total_completed_qty, precision) != flt(self.for_quantity, precision): - total_completed_qty_label = bold(_("Total Completed Qty")) - qty_to_manufacture = bold(_("Qty to Manufacture")) - - frappe.throw( - _("The {0} ({1}) must be equal to {2} ({3})").format( - total_completed_qty_label, - bold(flt(total_completed_qty, precision)), - qty_to_manufacture, - bold(self.for_quantity), -======= if self.for_quantity and flt(accounted_qty, precision) != flt(self.for_quantity, precision): frappe.throw( _( @@ -922,7 +910,6 @@ class JobCard(Document): bold(flt(self.process_loss_qty, precision)), bold(flt(self.pending_qty, precision)), bold(flt(self.for_quantity, precision)), ->>>>>>> 7bffd84482 (fix(job_card): reject a completion split that cannot add up (#57687)) ) ) @@ -1527,7 +1514,6 @@ class JobCard(Document): kwargs = frappe._dict(kwargs) self.validate_complete_job_card_qty(kwargs) - self.set_for_quantity(kwargs) def validate_docstatus(self): if self.docstatus == 2: @@ -1546,36 +1532,11 @@ class JobCard(Document): if flt(kwargs.pending_qty) and flt(kwargs.pending_qty) > self.for_quantity: frappe.throw(_("Pending quantity cannot be greater than the for quantity.")) -<<<<<<< HEAD + self.validate_completion_qty_split(kwargs) + self.pending_qty = flt(kwargs.pending_qty) self.process_loss_qty = flt(kwargs.process_loss_qty) -======= - self.validate_completion_qty_split(kwargs) - - def validate_completion_qty_split(self, kwargs): - if not flt(kwargs.for_quantity): - return - - precision = self.precision("total_completed_qty") - accounted_qty = flt(kwargs.qty) + flt(kwargs.pending_qty) + flt(kwargs.process_loss_qty) - - if flt(accounted_qty, precision) == flt(kwargs.for_quantity, precision): - return - - frappe.throw( - _( - "Completed Quantity ({0}), Pending Quantity ({1}) and Process Loss Quantity ({2}) must add up to the Qty to Manufacture ({3})." - ).format( - bold(flt(kwargs.qty, precision)), - bold(flt(kwargs.pending_qty, precision)), - bold(flt(kwargs.process_loss_qty, precision)), - bold(flt(kwargs.for_quantity, precision)), - ) - ) - - def add_completion_time_logs(self, kwargs): ->>>>>>> 7bffd84482 (fix(job_card): reject a completion split that cannot add up (#57687)) if kwargs.end_time: self.add_time_logs( to_time=kwargs.end_time, @@ -1601,6 +1562,27 @@ class JobCard(Document): _("Job Card {0} has been completed").format(get_link_to_form("Job Card", self.name)) ) + def validate_completion_qty_split(self, kwargs): + if not flt(kwargs.for_quantity): + return + + precision = self.precision("total_completed_qty") + accounted_qty = flt(kwargs.qty) + flt(kwargs.pending_qty) + flt(kwargs.process_loss_qty) + + if flt(accounted_qty, precision) == flt(kwargs.for_quantity, precision): + return + + frappe.throw( + _( + "Completed Quantity ({0}), Pending Quantity ({1}) and Process Loss Quantity ({2}) must add up to the Qty to Manufacture ({3})." + ).format( + bold(flt(kwargs.qty, precision)), + bold(flt(kwargs.pending_qty, precision)), + bold(flt(kwargs.process_loss_qty, precision)), + bold(flt(kwargs.for_quantity, precision)), + ) + ) + @frappe.whitelist() def make_stock_entry_for_semi_fg_item(self, auto_submit: bool = False): def get_consumed_process_loss(): diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 27789e93713..95e9e8dbfb6 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -1817,6 +1817,20 @@ class TestJobCard(ERPNextTestSuite): self.assertEqual(s.additional_costs[2].amount, 480) self.assertEqual(s.additional_costs[3].amount, 480) + def test_completion_qty_split_must_add_up(self): + jc = frappe.new_doc("Job Card") + jc.for_quantity = 5 + + jc.validate_complete_job_card_qty( + frappe._dict(for_quantity=5, qty=3, pending_qty=2, process_loss_qty=0) + ) + + self.assertRaises( + frappe.ValidationError, + jc.validate_complete_job_card_qty, + frappe._dict(for_quantity=3, qty=3, pending_qty=2, process_loss_qty=0), + ) + def create_bom_with_multiple_operations(): "Create a BOM with multiple operations and Material Transfer against Job Card" @@ -1879,94 +1893,3 @@ def create_semi_fg_bom(semi_fg_item, raw_item, inspection_required): bom.append("items", {"item_code": raw_item, "qty": 1}) bom.submit() return bom.name -<<<<<<< HEAD -======= - - -class TestJobCardLogic(ERPNextTestSuite): - """Field-level validations and pure quantity/capacity helpers, exercised on the - document directly so they don't need a Work Order / BOM (the integration suite does).""" - - def test_processing_a_submitted_or_cancelled_card_is_blocked(self): - submitted = frappe.new_doc("Job Card") - submitted.docstatus = 1 - self.assertRaises(frappe.ValidationError, submitted.validate_docstatus) - - cancelled = frappe.new_doc("Job Card") - cancelled.docstatus = 2 - self.assertRaises(frappe.ValidationError, cancelled.validate_docstatus) - - def test_complete_job_card_qty_guards(self): - jc = frappe.new_doc("Job Card") - jc.for_quantity = 5 - jc.validate_complete_job_card_qty(frappe._dict(pending_qty=3)) # within range -> passes - self.assertRaises( - frappe.ValidationError, jc.validate_complete_job_card_qty, frappe._dict(pending_qty=-1) - ) - self.assertRaises( - frappe.ValidationError, jc.validate_complete_job_card_qty, frappe._dict(process_loss_qty=-1) - ) - self.assertRaises( - frappe.ValidationError, jc.validate_complete_job_card_qty, frappe._dict(pending_qty=10) - ) - - def test_completion_qty_split_must_add_up(self): - jc = frappe.new_doc("Job Card") - jc.for_quantity = 5 - - # 3 completed + 2 pending + 0 lost == 5 to manufacture -> passes - jc.validate_complete_job_card_qty( - frappe._dict(for_quantity=5, qty=3, pending_qty=2, process_loss_qty=0) - ) - - self.assertRaises( - frappe.ValidationError, - jc.validate_complete_job_card_qty, - frappe._dict(for_quantity=3, qty=3, pending_qty=2, process_loss_qty=0), - ) - - def test_completed_qty_must_reconcile_with_for_quantity(self): - jc = frappe.new_doc("Job Card") - jc.for_quantity = 10 - jc.total_completed_qty = 6 - jc.process_loss_qty = 0 - jc.pending_qty = 0 - # 6 + 0 + 0 != 10 -> throws - self.assertRaises(frappe.ValidationError, jc.validate_completed_qty_matches_for_quantity) - # completed + loss + pending == for_quantity -> passes - jc.pending_qty = 4 - jc.validate_completed_qty_matches_for_quantity() - - def test_set_process_loss(self): - jc = frappe.new_doc("Job Card") - jc.for_quantity = 10 - jc.total_completed_qty = 6 - jc.pending_qty = 1 - jc.set_process_loss() - self.assertEqual(jc.process_loss_qty, 3) # 10 - 6 - 1 - - # no loss when nothing completed yet - nothing_done = frappe.new_doc("Job Card") - nothing_done.for_quantity = 10 - nothing_done.total_completed_qty = 0 - nothing_done.set_process_loss() - self.assertEqual(nothing_done.process_loss_qty, 0) - - def test_capacity_overlap_detection(self): - jc = frappe.new_doc("Job Card") - sequential = [ - {"from_time": "2026-01-01 10:00:00", "to_time": "2026-01-01 11:00:00"}, - {"from_time": "2026-01-01 11:00:00", "to_time": "2026-01-01 12:00:00"}, - ] - overlapping = [ - {"from_time": "2026-01-01 10:00:00", "to_time": "2026-01-01 11:00:00"}, - {"from_time": "2026-01-01 10:30:00", "to_time": "2026-01-01 11:30:00"}, - ] - # sequential logs share one capacity slot; overlapping logs need two - self.assertEqual(len(jc.get_alloted_capacity(sequential)), 1) - self.assertEqual(len(jc.get_alloted_capacity(overlapping)), 2) - # capacity 1 overlaps with any log; capacity 2 only when both slots are taken - self.assertTrue(jc.has_overlap(1, sequential)) - self.assertFalse(jc.has_overlap(2, sequential)) - self.assertTrue(jc.has_overlap(2, overlapping)) ->>>>>>> 7bffd84482 (fix(job_card): reject a completion split that cannot add up (#57687)) diff --git a/erpnext/public/js/shop_floor/shop_floor.js b/erpnext/public/js/shop_floor/shop_floor.js deleted file mode 100644 index 6e57b77ed7a..00000000000 --- a/erpnext/public/js/shop_floor/shop_floor.js +++ /dev/null @@ -1,1747 +0,0 @@ -// Shop Floor — an immersive, keyboard-first operator/manager interface. -// -// Two experiences share one app shell (see get_shop_floor_context on the server): -// • manager — a paginated board of work orders bucketed Pending / In Progress and Completed. -// Drilling into a work order opens its job cards in the operator pane. -// • operator — a focused workstation/work-order view to start, pause, complete and submit jobs. -// -// The whole surface is driveable from the keyboard (press ? for the cheat sheet) so an operator -// at a terminal never needs the mouse. - -// Job Card status → indicator colour, mirrored from workstation.get_status_color so the manager -// board can paint per-operation chips without a round-trip. -const JC_STATUS_COLORS = { - Completed: "green", - Submitted: "blue", - "Work In Progress": "orange", - "Material Transferred": "yellow", - "On Hold": "red", - Open: "gray", - "Not Started": "gray", -}; - -const MANAGER_BUCKETS = [ - { key: "open", label: __("Pending / In Progress"), dot: "orange" }, - { key: "completed", label: __("Completed"), dot: "green" }, -]; - -const PAGE_LENGTH = 20; - -class ShopFloor { - constructor({ wrapper }, page) { - this.wrapper = $(wrapper); - this.page = page; - this.timer_intervals = {}; - this.capacity = 1; - this.mode = null; - // Remembers each Materials panel's open/closed state (keyed by job card) so it - // survives re-renders — otherwise a reload right after a click resets the panel. - this.materials_open = {}; - // Same idea for the per-operation Work Instructions panel. - this.instructions_open = {}; - - // View state. - this.view = "operator"; // overwritten once context loads - this.active_bucket = "open"; - this.with_job_cards_only = true; // board default: hide WOs that have no job cards - this.buckets = {}; // key -> { rows, total, start, loaded } - this.selected_wo = null; - this.focus_index = -1; - this.op_state = { workstation: null, work_order: null }; - - this.make(); - this.bind_realtime(); - this.bind_lifecycle(); - this.init(); - } - - init() { - frappe.call("erpnext.manufacturing.page.shop_floor.shop_floor.get_shop_floor_context").then((r) => { - const ctx = r.message || {}; - this.view = ctx.role_view === "manager" ? "manager" : "operator"; - this.can_manage = !!ctx.can_manage; - this.user_employee = ctx.user_employee || null; - this.render_shell_controls(); - this.render_view(); - this.bind_keys(); - this.initialized = true; - this.apply_route_options(); - }); - } - - // ── App shell ──────────────────────────────────────────────────────────── - make() { - this.wrapper.append(` - ${this.styles()} -
    -
    -
    -
    -
    - - - - - -
    -
    -
    -
    -
    -
    -
    -
    - `); - - this.app = this.wrapper.find(".sf-app"); - this.brand_icon = `${__(
-			`; - this.topbar_left = this.wrapper.find(".sf-topbar-left"); - this.topbar_center = this.wrapper.find(".sf-topbar-center"); - this.body = this.wrapper.find(".sf-body"); - this.board_container = this.wrapper.find(".sf-board"); - this.detail_container = this.wrapper.find(".sf-detail"); - this.op_container = this.wrapper.find(".sf-operator"); - - this.wrapper.find(".sf-btn-home").on("click", () => (window.location.href = "/app")); - this.wrapper.find(".sf-btn-refresh").on("click", () => this.refresh()); - this.wrapper.find(".sf-btn-scan").on("click", () => this.open_scanner()); - this.wrapper.find(".sf-btn-help").on("click", () => this.show_help()); - this.wrapper.find(".sf-btn-theme").on("click", () => this.toggle_theme()); - this.update_theme_button(); - } - - // Kiosk-friendly light/dark switch: flips the standard desk theme and persists it on the - // User (same as the Ctrl+Shift+G switcher), so the choice survives reloads and follows the - // operator's login on any device. - toggle_theme() { - const next = frappe.ui.get_current_theme() === "dark" ? "light" : "dark"; - document.documentElement.setAttribute("data-theme-mode", next); - frappe.ui.set_theme(next); - frappe.xcall("frappe.core.doctype.user.user.switch_theme", { - theme: next.charAt(0).toUpperCase() + next.slice(1), - }); - this.update_theme_button(); - } - - update_theme_button() { - const dark = frappe.ui.get_current_theme() === "dark"; - this.wrapper - .find(".sf-btn-theme") - .html(dark ? "☀" : "☾") - .attr("title", dark ? __("Switch to Light Theme") : __("Switch to Dark Theme")); - } - - render_shell_controls() { - this.topbar_left.empty(); - this.topbar_center.empty(); - - // View toggle — only managers can flip between the board and a bare operator view. - const toggle = this.can_manage - ? `
    - - -
    ` - : ""; - - if (this.view === "manager") { - this.topbar_left.html(` - ${this.brand_icon}${__("Shop Floor")} - ${toggle} -
    - ${MANAGER_BUCKETS.map( - (b) => `` - ).join("")} -
    - `); - this.topbar_center.html(` - - - `); - - this.topbar_left.find(".sf-tab").on("click", (e) => { - this.switch_bucket($(e.currentTarget).attr("data-bucket")); - }); - let timer = null; - this.topbar_center.find(".sf-search-input").on("input", (e) => { - const val = e.target.value; - clearTimeout(timer); - timer = setTimeout(() => this.search_work_orders(val), 300); - }); - this.topbar_center.find(".sf-jc-toggle").on("change", (e) => { - this.toggle_job_cards_only(e.target.checked); - }); - } else { - this.topbar_left.html( - `${this.brand_icon}${__("Shop Floor")}${toggle}` - ); - this.build_operator_filters(); - } - - this.topbar_left.find(".sf-view-btn").on("click", (e) => { - this.set_view($(e.currentTarget).attr("data-view")); - }); - } - - build_operator_filters() { - this.topbar_center.html('
    '); - const $filters = this.topbar_center.find(".sf-filters"); - - this.workstation_filter = frappe.ui.form.make_control({ - df: { - fieldtype: "Link", - options: "Workstation", - fieldname: "workstation", - placeholder: __("Machine"), - onchange: () => this.load_operator(), - }, - parent: $filters, - render_input: true, - }); - this.workstation_filter.$wrapper.addClass("sf-filter-control"); - - this.work_order_filter = frappe.ui.form.make_control({ - df: { - fieldtype: "Link", - options: "Work Order", - fieldname: "work_order", - placeholder: __("Work Order"), - onchange: () => this.load_operator(), - }, - parent: $filters, - render_input: true, - }); - this.work_order_filter.$wrapper.addClass("sf-filter-control"); - } - - set_view(view) { - if (!view || view === this.view) return; - this.view = view; - this.selected_wo = null; - this.focus_index = -1; - this.render_shell_controls(); - this.render_view(); - } - - render_view() { - const manager = this.view === "manager"; - this.board_container.toggle(manager); - this.detail_container.toggle(manager && !!this.selected_wo); - this.op_container.toggle(!manager); - this.body.toggleClass("detail-open", manager && !!this.selected_wo); - - if (manager) { - this.load_bucket(this.active_bucket); - } else { - this.load_operator(); - } - } - - // ── Manager board ──────────────────────────────────────────────────────── - switch_bucket(bucket) { - if (!bucket || bucket === this.active_bucket) return; - this.active_bucket = bucket; - this.selected_wo = null; - this.focus_index = -1; - this.topbar_left.find(".sf-tab").removeClass("active"); - this.topbar_left.find(`.sf-tab[data-bucket="${bucket}"]`).addClass("active"); - this.detail_container.hide(); - this.body.removeClass("detail-open"); - this.load_bucket(bucket); - } - - search_work_orders(term) { - this.search_term = term; - // Re-query every bucket from scratch on the next visit; reload the active one now. - this.buckets = {}; - this.load_bucket(this.active_bucket); - } - - toggle_job_cards_only(checked) { - this.with_job_cards_only = !!checked; - // Filter changes every bucket's contents + counts; drop caches and clear stale counts. - this.buckets = {}; - this.topbar_left.find(".sf-tab-count").text(""); - this.load_bucket(this.active_bucket); - } - - load_bucket(bucket, append = false) { - const state = this.buckets[bucket] || { rows: [], total: 0, start: 0, loaded: false }; - const start = append ? state.start : 0; - - frappe.call({ - method: "erpnext.manufacturing.page.shop_floor.shop_floor.get_work_orders", - args: { - status_group: bucket, - start: start, - page_length: PAGE_LENGTH, - search: this.search_term || null, - with_job_cards_only: this.with_job_cards_only ? 1 : 0, - }, - callback: (r) => { - const data = r.message || {}; - const rows = data.work_orders || []; - this.buckets[bucket] = { - rows: append ? state.rows.concat(rows) : rows, - total: cint(data.total), - start: start + rows.length, - loaded: true, - }; - this.update_tab_count(bucket); - if (bucket === this.active_bucket) this.render_board(); - }, - }); - } - - update_tab_count(bucket) { - const state = this.buckets[bucket]; - if (!state) return; - this.topbar_left.find(`[data-bucket-count="${bucket}"]`).text(state.total ? state.total : ""); - } - - render_board() { - const state = this.buckets[this.active_bucket] || { rows: [], total: 0 }; - this.focus_index = -1; - - if (!state.rows.length) { - this.board_container.html(`
    ${__("No work orders here.")}
    `); - return; - } - - const cards = state.rows.map((wo) => this.work_order_card(wo)).join(""); - const more = - state.rows.length < state.total - ? `` - : `
    ${__("Showing all {0}", [state.total])}
    `; - - this.board_container.html( - `
    ${cards}
    ${more}
    ` - ); - - this.board_container.find(".sf-wo-card").on("click", (e) => { - this.open_wo($(e.currentTarget).attr("data-name")); - }); - this.board_container - .find(".sf-load-more") - .on("click", () => this.load_bucket(this.active_bucket, true)); - } - - work_order_card(wo) { - const item = wo.item_name || wo.production_item; - - // Hero image = the current operation's workstation. No item-image fallback — when the - // workstation has no image uploaded we show its initials, never the product image. - const image = wo.workstation_image - ? `` - : `${frappe.get_abbr(wo.workstation_name || item, 2)}`; - - const workstation_line = wo.workstation_name - ? `
    🏭 ${frappe.utils.escape_html( - wo.workstation_name - )}${wo.current_operation ? ` · ${frappe.utils.escape_html(wo.current_operation)}` : ""}
    ` - : ""; - - // Operations bar: green segment (done) + orange segment (in progress); grey track = pending. - const done_pct = Math.min(cint(wo.per_operations), 100); - const wip_pct = Math.min(cint(wo.per_in_progress), 100 - done_pct); - - return ` -
    -
    -
    ${image}
    -
    -
    ${frappe.utils.escape_html(item)}
    - ${workstation_line} -
    - - ${wo.name} -
    -
    -
    -
    -
    - ${__("Operations")} - ${cint(wo.completed_operations)} / ${cint(wo.total_operations)} -
    -
    -
    -
    -
    -
    -
    - `; - } - - open_wo(name) { - if (!name) return; - this.selected_wo = name; - this.op_state = { workstation: null, work_order: name }; - this.detail_container.show(); - this.body.addClass("detail-open"); - this.board_container - .find(".sf-wo-card") - .removeClass("sf-selected") - .filter(`[data-name="${name}"]`) - .addClass("sf-selected"); - // The detail pane reuses the operator rendering for a single work order. - this.detail_container.html(` -
    - - ${frappe.utils.escape_html(name)} - ${__("Open")} -
    -
    - `); - this.detail_container.find(".sf-detail-back").on("click", () => this.close_wo()); - this.op_container_target = this.detail_container.find(".sf-detail-body"); - this.load_operator_data(this.op_container_target, { work_order: name }); - } - - close_wo() { - this.selected_wo = null; - this.op_container_target = null; - this.detail_container.hide().empty(); - this.body.removeClass("detail-open"); - this.board_container.find(".sf-wo-card").removeClass("sf-selected"); - } - - // ── Operator pane ────────────────────────────────────────────────────────── - // Resolves the container the operator content renders into: the standalone operator - // view, or the manager's drill-down detail pane. - current_op_container() { - return this.view === "manager" ? this.op_container_target : this.op_container; - } - - load_operator() { - const workstation = this.workstation_filter ? this.workstation_filter.get_value() : null; - const work_order = this.work_order_filter ? this.work_order_filter.get_value() : null; - this.op_state = { workstation, work_order }; - - if (!workstation && !work_order) { - this.clear_timers(); - this.op_container.html( - `
    ${__("Select a machine or work order to begin")}
    ` - ); - return; - } - this.load_operator_data(this.op_container, { workstation, work_order }); - } - - load_operator_data($container, { workstation, work_order }) { - frappe.call({ - method: "erpnext.manufacturing.page.shop_floor.shop_floor.get_data", - args: { - workstation: work_order ? null : workstation, - work_order: work_order || null, - }, - callback: (r) => { - const data = r.message || {}; - this.job_cards = data.job_cards || []; - this.capacity = cint(data.capacity) || 1; - this.mode = data.mode || (work_order ? "work_order" : "workstation"); - this.oee = data.oee || null; - if (data.user_employee) this.user_employee = data.user_employee; - this.today_sessions = data.today_sessions || []; - this.workstation = workstation; - this.work_order = work_order; - this.compute_state(); - this.dedupe_today_sessions(); - this.render_operator($container); - }, - }); - } - - // A job card already shown under Completed Operations shouldn't repeat in - // Today's Sessions — keep it in Completed Operations only. - dedupe_today_sessions() { - const shown = new Set((this.completed || []).map((jc) => jc.name)); - this.today_sessions = (this.today_sessions || []).filter((s) => !shown.has(s.name)); - } - - // Re-fetch whichever operator content is currently on screen (used after every action). - reload() { - if (this.view === "manager" && this.selected_wo) { - this.load_operator_data(this.op_container_target, { work_order: this.selected_wo }); - // Keep the board chips fresh too. - this.buckets = {}; - this.load_bucket(this.active_bucket); - } else if (this.view === "manager") { - this.load_bucket(this.active_bucket); - } else { - this.load_operator(); - } - } - - refresh() { - if (this.view === "manager") { - this.buckets = {}; - } - this.reload(); - } - - compute_state() { - this.active_jobs = []; - this.queue = []; - this.pending_submission = []; - this.completed = []; - // Submitted but the finished goods aren't booked yet (status "To Manufacture") — its own - // actionable section, kept out of Completed Operations / Today's Sessions. - this.to_manufacture = []; - - for (const jc of this.job_cards) { - // Same materials-ready rule as job_card.js make_dashboard. - jc._materials_ready = !!( - jc.skip_material_transfer || - flt(jc.transferred_qty) >= flt(jc.for_quantity) + flt(jc.process_loss_qty) || - !jc.finished_good - ); - - // Submitted JCs are historical from the Shop Floor's POV — only appear here in work_order - // mode (and, for "To Manufacture", in workstation mode too — see _fetch_job_cards). - if (jc.docstatus === 1) { - if (jc.status === "To Manufacture") { - this.to_manufacture.push(jc); - } else { - this.completed.push(jc); - } - continue; - } - - const last_log = - jc.time_logs && jc.time_logs.length ? jc.time_logs[jc.time_logs.length - 1] : null; - const is_running = last_log && !last_log.to_time && !jc.is_paused; - const is_paused = jc.is_paused; - - if (is_running || is_paused) { - this.active_jobs.push(jc); - } else if (jc.status === "Completed") { - // All qty accounted for but still draft — waiting on Submit. - this.pending_submission.push(jc); - } else { - this.queue.push(jc); - } - } - - // Slot rules — all active jobs are always shown; the grid (col-md-6) wraps them 2 per row. - // workstation mode: capacity-many slots, expanded to fit every active job (+ empty placeholders). - // work_order mode: one slot per active job (no empty placeholders). - let slot_count; - if (this.mode === "work_order") { - slot_count = this.active_jobs.length; - } else { - slot_count = Math.max(this.capacity, this.active_jobs.length, 1); - } - - this.slots = []; - for (let i = 0; i < slot_count; i++) { - this.slots.push(this.active_jobs[i] || null); - } - - // Auto-pick: when nothing is running, surface the next queue item in the slot. - if (this.active_jobs.length === 0 && this.queue.length > 0) { - const next_up = this.queue.shift(); - next_up._is_next_up = true; - this.slots[0] = next_up; - } - - this.summary = { - active_count: this.active_jobs.length, - // "To Manufacture" (submitted, qty done, but the Manufacture Stock Entry is still pending) - // isn't actually finished — count it as Pending, not Completed. - queue_count: this.queue.length + this.to_manufacture.length, - completed_count: this.completed.length + this.pending_submission.length, - capacity: this.capacity, - }; - } - - render_operator($container) { - this.clear_timers(); - $container.empty(); - - const html = frappe.render_template("shop_floor_template", { - workstation: this.workstation, - work_order: this.work_order, - mode: this.mode, - slots: this.slots, - active_jobs: this.active_jobs, - queue: this.queue, - pending_submission: this.pending_submission, - to_manufacture: this.to_manufacture, - completed: this.completed, - today_sessions: this.today_sessions || [], - summary: this.summary, - oee: this.oee, - }); - $container.html(html); - - // Restore each Materials panel to its remembered open/closed state. - $container.find(".mes-materials-inline").each((i, el) => { - const $el = $(el); - const name = $el.attr("data-job-card"); - if (!name) return; - if (name in this.materials_open) { - $el.toggleClass("is-open", this.materials_open[name]); - } else { - this.materials_open[name] = $el.hasClass("is-open"); - } - }); - - // Restore each Work Instructions panel to its remembered open/closed state. - $container.find(".mes-instructions-inline").each((i, el) => { - const $el = $(el); - const name = $el.attr("data-job-card"); - if (name && name in this.instructions_open) { - $el.toggleClass("is-open", this.instructions_open[name]); - } - }); - - this.bind_events($container); - - for (const jc of this.active_jobs) { - if (jc.is_paused) { - this.render_timer(jc.name, this.elapsed_seconds(jc), $container); - } else { - this.start_timer_for(jc, $container); - } - } - } - - clear_timers() { - for (const id of Object.values(this.timer_intervals)) { - clearInterval(id); - } - this.timer_intervals = {}; - } - - bind_events($container) { - const me = this; - - $container.find(".mes-materials-summary").on("click", function (e) { - if ($(e.target).closest(".mes-btn-transfer").length) return; - const $inline = $(this).closest(".mes-materials-inline"); - const open = !$inline.hasClass("is-open"); - $inline.toggleClass("is-open", open); - const name = $inline.attr("data-job-card"); - if (name) me.materials_open[name] = open; - }); - - $container.find(".mes-instructions-summary").on("click", function () { - const $inline = $(this).closest(".mes-instructions-inline"); - const open = !$inline.hasClass("is-open"); - $inline.toggleClass("is-open", open); - const name = $inline.attr("data-job-card"); - if (name) me.instructions_open[name] = open; - }); - - // Clicking a "QC Required" / "QC Available" pill runs the inline check ahead of End Session. - $container.find(".mes-qc-pill").on("click", function () { - const name = $(this).attr("data-job-card"); - const jc = (me.active_jobs || []).find((j) => j.name === name); - if (jc) me.run_quality_check(jc, () => me.reload()); - }); - - $container.find(".mes-btn-start").on("click", function () { - me.start_job($(this).attr("data-job-card")); - }); - $container.find(".mes-btn-pause").on("click", function () { - me.pause_job($(this).attr("data-job-card")); - }); - $container.find(".mes-btn-resume").on("click", function () { - me.resume_job($(this).attr("data-job-card")); - }); - $container.find(".mes-btn-end-session").on("click", function () { - me.end_session($(this).attr("data-job-card")); - }); - $container.find(".mes-btn-submit").on("click", function () { - me.submit_job_card($(this).attr("data-job-card")); - }); - $container.find(".mes-btn-make-entry").on("click", function () { - me.make_manufacture_entry($(this).attr("data-job-card")); - }); - $container.find(".mes-btn-transfer").on("click", function (e) { - e.preventDefault(); - me.transfer_materials($(this).attr("data-job-card")); - }); - } - - // ── Operator actions (unchanged behaviour, reload() instead of load()) ───── - start_job(job_card) { - const me = this; - if (this.mode === "workstation" && this.active_jobs.length >= this.capacity) { - frappe.msgprint({ - title: __("Capacity Reached"), - message: __( - "This machine can run at most {0} job(s) in parallel. Pause or complete a running job before starting another.", - [this.capacity] - ), - indicator: "orange", - }); - return; - } - - const default_employee = this.user_employee; - const dialog = new frappe.ui.Dialog({ - title: __("Start Job"), - fields: [ - { - label: __("Start Time"), - fieldname: "start_time", - fieldtype: "Datetime", - default: frappe.datetime.now_datetime(), - }, - { fieldtype: "Section Break" }, - { - label: __("Employees"), - fieldname: "employees", - fieldtype: "Table", - data: default_employee ? [{ employee: default_employee }] : [], - fields: [ - { - label: __("Employee"), - fieldname: "employee", - fieldtype: "Link", - options: "Employee", - in_list_view: 1, - }, - ], - }, - ], - primary_action_label: __("Start"), - primary_action: (values) => { - dialog.hide(); - me.update_job_card(job_card, "start_timer", { - start_time: values.start_time, - employees: values.employees || [], - }); - }, - }); - dialog.show(); - this.bind_enter_submit(dialog); - } - - // Make a dialog fully keyboard-operable: Enter triggers the primary action, so an operator - // never has to reach for the mouse. Enter is left alone inside multi-line fields and while an - // autocomplete (Link/Select) dropdown is open, so it can still pick a value. - bind_enter_submit(dialog) { - dialog.$wrapper.on("keydown.sfenter", (e) => { - if (e.key !== "Enter" || e.shiftKey) return; - if ($(e.target).is("textarea")) return; - if ($(".awesomplete > ul:not([hidden])").length) return; - const $btn = dialog.get_primary_btn(); - if ( - $btn && - $btn.length && - $btn.is(":visible") && - !$btn.hasClass("disabled") && - !$btn.prop("disabled") - ) { - e.preventDefault(); - e.stopPropagation(); - $btn.trigger("click"); - } - }); - } - - pause_job(jc_name) { - this.update_job_card(jc_name, "pause_job", { end_time: frappe.datetime.now_datetime() }); - } - - resume_job(jc_name) { - this.update_job_card(jc_name, "resume_job", { start_time: frappe.datetime.now_datetime() }); - } - - end_session(jc_name) { - const me = this; - const jc = this.active_jobs.find((j) => j.name === jc_name); - if (!jc) return; - - let pending = flt(jc.for_quantity) - flt(jc.total_completed_qty); - if (flt(jc.pending_qty) > 0) { - pending = flt(jc.pending_qty); - } - - const fields = [ - { - fieldtype: "Float", - label: __("Qty to Manufacture"), - fieldname: "for_quantity", - reqd: 1, - default: pending, - change() { - const d = me.session_dialog; - d.set_value("completed_qty", d.get_value("for_quantity")); - d.set_value("pending_qty", 0); - d.set_value("process_loss_qty", 0); - }, - }, - { - fieldtype: "Float", - label: __("Completed Quantity"), - fieldname: "completed_qty", - reqd: 1, - default: pending, - change() { - const d = me.session_dialog; - const remaining = - flt(d.get_value("for_quantity")) - - flt(d.get_value("completed_qty")) - - flt(d.get_value("process_loss_qty")); - - if (remaining < 0) { - const max_completed_qty = - flt(d.get_value("for_quantity")) - flt(d.get_value("process_loss_qty")); - d.set_value("completed_qty", max_completed_qty); - frappe.throw( - __("Completed Quantity cannot be greater than {0}", [max_completed_qty]) - ); - } - - if (remaining !== flt(d.get_value("pending_qty"))) { - d.set_value("pending_qty", remaining); - } - }, - }, - { - fieldtype: "Float", - label: __("Pending Quantity"), - fieldname: "pending_qty", - default: 0.0, - change() { - const d = me.session_dialog; - const pl = - flt(d.get_value("for_quantity")) - - flt(d.get_value("completed_qty")) - - flt(d.get_value("pending_qty")); - - if (pl < 0) { - d.set_value("pending_qty", 0); - frappe.throw( - __("Pending Quantity cannot be greater than {0}", [ - flt(d.get_value("for_quantity")) - flt(d.get_value("completed_qty")), - ]) - ); - } - - if (pl !== flt(d.get_value("process_loss_qty"))) { - d.set_value("process_loss_qty", pl); - } - }, - }, - { - fieldtype: "Float", - label: __("Process Loss Quantity"), - fieldname: "process_loss_qty", - default: 0.0, - change() { - const d = me.session_dialog; - const remaining = - flt(d.get_value("for_quantity")) - - flt(d.get_value("completed_qty")) - - flt(d.get_value("process_loss_qty")); - - if (remaining < 0) { - d.set_value("process_loss_qty", 0); - frappe.throw( - __("Process Loss Quantity cannot be greater than {0}", [ - flt(d.get_value("for_quantity")) - flt(d.get_value("completed_qty")), - ]) - ); - } - - if (remaining !== flt(d.get_value("pending_qty"))) { - d.set_value("pending_qty", remaining); - } - }, - }, - { fieldtype: "Section Break" }, - { - fieldtype: "Datetime", - label: __("End Time"), - fieldname: "end_time", - default: frappe.datetime.now_datetime(), - }, - ]; - - const get_payload = () => { - const data = me.session_dialog.get_values(); - if (!data) return null; - if (flt(data.completed_qty) <= 0) { - frappe.throw(__("Completed Quantity should be greater than 0")); - } - return { - job_card: jc.name, - qty: flt(data.completed_qty), - for_quantity: flt(data.for_quantity), - pending_qty: flt(data.pending_qty), - process_loss_qty: flt(data.process_loss_qty), - end_time: data.end_time, - }; - }; - - const save_and_continue = () => { - const args = get_payload(); - if (!args) return; - me.session_dialog.hide(); - frappe.call({ - method: "erpnext.manufacturing.page.shop_floor.shop_floor.save_and_continue", - args: args, - freeze: true, - freeze_message: __("Saving job card..."), - callback: () => me.reload(), - }); - }; - - const finalize_submit = (args) => { - frappe.call({ - method: "erpnext.manufacturing.page.shop_floor.shop_floor.complete_and_submit", - args: args, - freeze: true, - freeze_message: __("Submitting job card..."), - callback: (r) => { - me.reload(); - if (r.message && r.message.finished_good) { - me.prompt_manufacture_entry(jc.name); - } - }, - }); - }; - - const submit_session = () => { - const args = get_payload(); - if (!args) return; - me.session_dialog.hide(); - // Guided QC gate: a job card that requires inspection must pass an inline Quality Check - // before it is submitted (mirrors Job Card.validate_inspection on the server). Once the - // inspection is recorded, finalize the session submit. - if (jc.qc && jc.qc.required && jc.qc.status !== "Accepted") { - me.run_quality_check(jc, () => finalize_submit(args)); - } else { - finalize_submit(args); - } - }; - - me.session_dialog = new frappe.ui.Dialog({ - title: __("End Session"), - fields: fields, - primary_action_label: __("Submit"), - primary_action: submit_session, - secondary_action_label: __("Save & Continue"), - secondary_action: save_and_continue, - }); - me.session_dialog.show(); - me.bind_enter_submit(me.session_dialog); - } - - // ── Inline Quality Check ───────────────────────────────────────────────────── - // Fetch the operation's Quality Inspection template and open a guided pass/fail checklist. - // `on_pass` runs once the inspection has been recorded (and is not rejected). - run_quality_check(jc, on_pass) { - const me = this; - frappe.call({ - method: "erpnext.manufacturing.page.shop_floor.shop_floor.get_quality_inspection_checklist", - args: { job_card: jc.name }, - freeze: true, - freeze_message: __("Loading quality checklist..."), - callback: (r) => { - const info = r.message || {}; - if (!info.template || !(info.parameters || []).length) { - // Inspection is required but the operation has no template/parameters to fill — - // there is nothing to capture inline. Point the user at the configuration. - frappe.msgprint({ - title: __("Quality Inspection Template Missing"), - indicator: "orange", - message: __( - "This operation requires a Quality Inspection but no template with parameters is configured. Set a Quality Inspection Template on Operation {0} to inspect from the Shop Floor.", - [jc.operation || ""] - ), - }); - return; - } - me.show_qc_dialog(jc, info, on_pass); - }, - }); - } - - show_qc_dialog(jc, info, on_pass) { - const me = this; - const params = info.parameters || []; - // Per-row operator input, keyed by row index (avoids escaping issues with parameter names). - const state = {}; // idx -> "Accepted" | "Rejected" - - const rows = params - .map((p, i) => { - const spec = frappe.utils.escape_html(p.specification); - let criteria = ""; - if (p.numeric) { - const lo = p.min_value !== null && p.min_value !== undefined ? p.min_value : "−∞"; - const hi = p.max_value !== null && p.max_value !== undefined ? p.max_value : "∞"; - criteria = __("Acceptable range: {0} to {1}", [lo, hi]); - } else if (p.value) { - criteria = __("Expected: {0}", [frappe.utils.escape_html(p.value)]); - } - const control = p.numeric - ? `` - : ` - - - `; - return `
    -
    -
    ${spec}
    - ${criteria ? `
    ${criteria}
    ` : ""} -
    -
    ${control}
    -
    `; - }) - .join(""); - - const dialog = new frappe.ui.Dialog({ - title: __("Quality Check"), - size: "large", - fields: [ - { - fieldtype: "HTML", - options: `
    ${__( - "Inspect {0} for job card {1}", - [frappe.utils.escape_html(info.item_code || ""), frappe.utils.escape_html(jc.name)] - )}
    ${rows}
    `, - }, - ], - primary_action_label: __("Submit Inspection"), - primary_action: () => { - const readings = []; - let missing = false; - params.forEach((p, i) => { - if (p.numeric) { - const val = dialog.$wrapper.find(`.mes-qc-reading[data-idx="${i}"]`).val(); - if (val === "" || val === undefined || val === null) missing = true; - readings.push({ specification: p.specification, reading_value: val }); - } else { - if (!state[i]) missing = true; - readings.push({ - specification: p.specification, - status: state[i], - reading_value: "", - }); - } - }); - if (missing) { - frappe.msgprint(__("Please complete every check before submitting the inspection.")); - return; - } - dialog.hide(); - frappe.call({ - method: "erpnext.manufacturing.page.shop_floor.shop_floor.submit_quality_inspection", - args: { job_card: jc.name, readings: JSON.stringify(readings) }, - freeze: true, - freeze_message: __("Recording inspection..."), - callback: (r) => { - const res = r.message || {}; - if (res.status === "Rejected") { - // Don't auto-proceed on a rejected inspection — the server gate may block the - // submit anyway (per Stock Settings), and the operator should decide next steps. - frappe.msgprint({ - title: __("Inspection Rejected"), - indicator: "red", - message: __( - "Quality Inspection {0} is Rejected. Resolve the issue or follow your rejection process before submitting the job card.", - [res.name || ""] - ), - }); - me.reload(); - return; - } - if (on_pass) on_pass(); - }, - }); - }, - }); - - dialog.show(); - // Pass/Fail toggles for qualitative parameters. - dialog.$wrapper.find(".mes-qc-passfail button").on("click", function () { - const $btn = $(this); - const $grp = $btn.closest(".mes-qc-passfail"); - $grp.find("button").removeClass("active"); - $btn.addClass("active"); - state[$grp.attr("data-idx")] = $btn.attr("data-val"); - }); - } - - prompt_manufacture_entry(jc_name) { - const me = this; - const dialog = new frappe.ui.Dialog({ - title: __("Job Card Submitted"), - fields: [ - { - fieldtype: "HTML", - options: ` -
    -
    - ${__("Job card {0} has been submitted.", [frappe.utils.escape_html(jc_name)])} -
    -
    - ${__("Create a Manufacture stock entry for the finished goods?")} -
    -
    - `, - }, - ], - primary_action_label: __("Make Manufacture Entry"), - primary_action: () => { - dialog.hide(); - me.make_manufacture_entry(jc_name); - }, - secondary_action_label: __("Skip"), - secondary_action: () => dialog.hide(), - }); - dialog.show(); - this.bind_enter_submit(dialog); - } - - submit_job_card(jc_name) { - const me = this; - frappe.confirm(__("Submit job card {0}? This finalizes the job card.", [jc_name]), () => { - frappe.call({ - method: "erpnext.manufacturing.page.shop_floor.shop_floor.submit_job_card", - args: { job_card: jc_name }, - freeze: true, - freeze_message: __("Submitting job card..."), - callback: () => me.reload(), - }); - }); - } - - make_manufacture_entry(jc_name) { - frappe.call({ - method: "erpnext.manufacturing.page.shop_floor.shop_floor.make_manufacture_stock_entry", - args: { job_card: jc_name }, - freeze: true, - freeze_message: __("Preparing stock entry..."), - callback: (r) => { - if (r.message && r.message.name) { - window.open(`/app/stock-entry/${encodeURIComponent(r.message.name)}`, "_blank"); - } - }, - }); - } - - transfer_materials(jc_name) { - if (!jc_name) return; - frappe.call({ - method: "erpnext.manufacturing.doctype.job_card.job_card.make_stock_entry", - args: { source_name: jc_name }, - callback: (r) => { - const doc = frappe.model.sync(r.message); - frappe.set_route("Form", doc[0].doctype, doc[0].name); - }, - }); - } - - update_job_card(job_card, method, data, on_success) { - const me = this; - frappe.call({ - method: "erpnext.manufacturing.doctype.workstation.workstation.update_job_card", - args: { - job_card: job_card, - method: method, - start_time: data.start_time || "", - employees: data.employees || [], - end_time: data.end_time || "", - qty: data.qty || 0, - for_quantity: data.for_quantity || 0, - pending_qty: data.pending_qty || 0, - process_loss_qty: data.process_loss_qty || 0, - auto_submit: data.auto_submit || 0, - }, - freeze: true, - freeze_message: __("Updating job card..."), - callback: () => { - me.reload(); - if (on_success) on_success(); - }, - }); - } - - // ── Timers ──────────────────────────────────────────────────────────────── - start_timer_for(jc, $container) { - let elapsed = this.elapsed_seconds(jc); - this.render_timer(jc.name, elapsed, $container); - this.timer_intervals[jc.name] = setInterval(() => { - elapsed += 1; - this.render_timer(jc.name, elapsed, $container); - }, 1000); - } - - elapsed_seconds(jc) { - let total = 0; - for (const log of jc.time_logs || []) { - if (log.to_time) { - if (log.time_in_mins) { - total += flt(log.time_in_mins, 2) * 60; - } else { - total += moment(log.to_time).diff(log.from_time, "seconds"); - } - } else { - total += moment().diff(log.from_time, "seconds"); - } - } - return total; - } - - render_timer(jc_name, seconds, $container) { - const h = Math.floor(seconds / 3600); - const m = Math.floor((seconds - h * 3600) / 60); - const s = cint(seconds - h * 3600 - m * 60); - const pad = (n) => (n < 10 ? "0" + n : String(n)); - - const scope = $container || this.wrapper; - const timer = scope.find(`.mes-job-timer[data-job-card="${jc_name}"]`); - timer.find(".h").text(pad(h)); - timer.find(".m").text(pad(m)); - timer.find(".s").text(pad(s)); - } - - // ── Realtime + lifecycle ─────────────────────────────────────────────────── - bind_realtime() { - frappe.realtime.on("update_workstation_status", (data) => { - if (data && data.name === this.op_state.workstation) { - this.reload(); - } - }); - } - - bind_lifecycle() { - // Frappe has no on_page_hide hook, so toggle immersive mode + keyboard binding on - // route changes ourselves. - this._route_handler = () => { - const on_page = (frappe.get_route_str() || "").startsWith("shop-floor"); - if (on_page) { - $(document.body).addClass("shop-floor-active"); - this.bind_keys(); - } else { - $(document.body).removeClass("shop-floor-active"); - this.unbind_keys(); - this.clear_timers(); - } - }; - frappe.router.on("change", this._route_handler); - } - - on_show() { - $(document.body).addClass("shop-floor-active"); - this.bind_keys(); - // Cached re-navigation (e.g. the Work Order "Shop Floor" button) lands here with fresh - // route_options; init() handles the very first load before we're initialized. - if (this.initialized) this.apply_route_options(); - } - - // ── Keyboard ──────────────────────────────────────────────────────────────── - bind_keys() { - $(document).off("keydown.shopfloor"); - $(document).on("keydown.shopfloor", (e) => this.handle_key(e)); - } - - unbind_keys() { - $(document).off("keydown.shopfloor"); - } - - is_typing(e) { - const tag = (e.target.tagName || "").toLowerCase(); - return tag === "input" || tag === "textarea" || tag === "select" || e.target.isContentEditable; - } - - handle_key(e) { - // Let dialogs own the keyboard while open. - if ($(".modal:visible").length) return; - - const typing = this.is_typing(e); - - // Escape works even while typing (blur the search / close the detail pane). - if (e.key === "Escape") { - if (typing) { - e.target.blur(); - return; - } - if (this.view === "manager" && this.selected_wo) { - this.close_wo(); - e.preventDefault(); - } - return; - } - - if (typing) return; - - switch (e.key) { - case "?": - this.show_help(); - e.preventDefault(); - return; - case "/": - this.topbar_center.find(".sf-search-input").focus(); - e.preventDefault(); - return; - case "r": - this.refresh(); - e.preventDefault(); - return; - case "b": - this.open_scanner(); - e.preventDefault(); - return; - case "1": - case "2": - if (this.view === "manager" && MANAGER_BUCKETS[cint(e.key) - 1]) { - this.switch_bucket(MANAGER_BUCKETS[cint(e.key) - 1].key); - e.preventDefault(); - } - return; - } - - // View switch chord: "g" then "m"/"o". - if (e.key === "g") { - this._g_pending = true; - setTimeout(() => (this._g_pending = false), 600); - return; - } - if (this._g_pending && (e.key === "m" || e.key === "o")) { - this._g_pending = false; - if (this.can_manage) this.set_view(e.key === "m" ? "manager" : "operator"); - return; - } - - // Navigation. - if (e.key === "ArrowDown" || e.key === "j") { - this.move_focus(1); - e.preventDefault(); - return; - } - if (e.key === "ArrowUp" || e.key === "k") { - this.move_focus(-1); - e.preventDefault(); - return; - } - if (e.key === "Enter") { - this.activate_focus(); - e.preventDefault(); - return; - } - - // Job actions on the focused card — reuse the rendered buttons. - const map = { - s: ".mes-btn-start, .mes-btn-resume", - p: ".mes-btn-pause, .mes-btn-resume", - e: ".mes-btn-end-session", - t: ".mes-btn-transfer", - }; - if (e.key === "S" && e.shiftKey) { - this.click_job_action(".mes-btn-submit"); - e.preventDefault(); - return; - } - if (map[e.key]) { - this.click_job_action(map[e.key]); - e.preventDefault(); - } - } - - // Job actions act on the focused job card (operator view); when the focus is on a board - // work order (manager view with the detail open) they fall back to the detail's active job. - click_job_action(selector) { - const $el = this.focused_el(); - if ($el && $el.attr("data-kind") === "job") { - const $btn = $el.find(selector).filter(":visible").first(); - if ($btn.length) { - $btn.trigger("click"); - return; - } - } - const scope = this.current_op_container(); - if (scope && scope.length) { - const $btn = scope.find(selector).filter(":visible").first(); - if ($btn.length) $btn.trigger("click"); - } - } - - focusables() { - // Manager always navigates the board work orders — even with the detail open, so the - // arrow keys switch work orders. The standalone operator view navigates its job cards. - const scope = this.view === "manager" ? this.board_container : this.current_op_container(); - if (!scope || !scope.length) return $(); - return scope.find("[data-sf-focusable]"); - } - - move_focus(delta) { - const $items = this.focusables(); - if (!$items.length) return; - this.focus_index = Math.max(0, Math.min($items.length - 1, this.focus_index + delta)); - $items.removeClass("sf-focused"); - const $target = $items.eq(this.focus_index); - $target.addClass("sf-focused"); - $target[0].scrollIntoView({ block: "nearest", behavior: "smooth" }); - // Browsing work orders with the detail already open → switch the detail to the focused one. - if (this.view === "manager" && this.selected_wo && $target.attr("data-kind") === "wo") { - this.open_wo($target.attr("data-name")); - } - } - - focused_el() { - const $items = this.focusables(); - if (this.focus_index < 0 || this.focus_index >= $items.length) return null; - return $items.eq(this.focus_index); - } - - activate_focus() { - const $el = this.focused_el(); - if (!$el) return; - if ($el.attr("data-kind") === "wo") { - this.open_wo($el.attr("data-name")); - } else { - // First visible primary button drives the job card (Start / Resume / End Session). - const $btn = $el.find(".btn-primary:visible").first(); - if ($btn.length) $btn.trigger("click"); - } - } - - show_help() { - const rows = [ - ["?", __("Show this help")], - ["/", __("Search work orders")], - ["r", __("Refresh")], - ["b", __("Scan job card")], - ["g then m / o", __("Switch Board / Operator view")], - ["1 / 2", __("Switch board tab")], - ["↑ / ↓ or j / k", __("Move selection")], - ["Enter", __("Open work order / run primary action")], - ["Esc", __("Close detail / blur search")], - ["s", __("Start / Resume job")], - ["p", __("Pause / Resume job")], - ["e", __("End session for active job")], - ["t", __("Transfer materials")], - ["Shift + S", __("Submit focused job card")], - ]; - const html = `
    ${rows - .map((r) => `
    ${r[0]}${r[1]}
    `) - .join("")}
    `; - const d = new frappe.ui.Dialog({ - title: __("Keyboard Shortcuts"), - fields: [{ fieldtype: "HTML", options: html }], - }); - d.show(); - } - - // ── Scanner ────────────────────────────────────────────────────────────── - open_scanner() { - const me = this; - const dialog = new frappe.ui.Dialog({ - title: __("Scan Job Card"), - fields: [ - { - label: __("Scan or enter Job Card"), - fieldname: "job_card", - fieldtype: "Data", - options: "Barcode", - }, - ], - primary_action_label: __("Continue"), - primary_action: (values) => { - if (!values.job_card) return; - dialog.hide(); - me.handle_scanned_job_card(values.job_card); - }, - }); - dialog.show(); - this.bind_enter_submit(dialog); - } - - handle_scanned_job_card(job_card) { - const me = this; - const jc = (this.job_cards || []).find((j) => j.name === job_card); - if (jc) { - me.route_scanned_action(jc); - return; - } - frappe.db.get_value("Job Card", job_card, ["status", "is_paused", "docstatus"]).then((r) => { - const data = r && r.message; - if (!data || !data.status) { - frappe.msgprint(__("Job Card {0} was not found.", [job_card])); - return; - } - if (cint(data.docstatus) === 1) { - frappe.msgprint(__("Job Card {0} is already submitted.", [job_card])); - } else if (cint(data.is_paused)) { - me.resume_job(job_card); - } else if (data.status === "Work In Progress") { - frappe.msgprint( - __( - "Job Card {0} is already running. Open its machine or work order to pause or complete it.", - [job_card] - ) - ); - } else if (data.status === "Completed") { - me.submit_job_card(job_card); - } else { - me.start_job(job_card); - } - }); - } - - route_scanned_action(jc) { - const me = this; - if (jc.docstatus === 1) { - frappe.msgprint(__("Job Card {0} is already submitted.", [jc.name])); - return; - } - if (jc.status === "Completed") { - me.submit_job_card(jc.name); - return; - } - if (jc.is_paused) { - me.resume_job(jc.name); - return; - } - const last_log = jc.time_logs && jc.time_logs.length ? jc.time_logs[jc.time_logs.length - 1] : null; - const is_running = !!(last_log && !last_log.to_time); - if (is_running) { - me.prompt_running_action(jc); - } else { - me.start_job(jc.name); - } - } - - prompt_running_action(jc) { - const me = this; - const dialog = new frappe.ui.Dialog({ - title: __("Job {0} is running", [jc.name]), - fields: [ - { - fieldtype: "HTML", - options: ` -
    - ${__("{0} is already in progress. Pause it or complete the session.", [ - frappe.utils.escape_html(jc.finished_good || jc.production_item || jc.name), - ])} -
    - `, - }, - ], - primary_action_label: __("Complete"), - primary_action: () => { - dialog.hide(); - me.end_session(jc.name); - }, - secondary_action_label: __("Pause"), - secondary_action: () => { - dialog.hide(); - me.pause_job(jc.name); - }, - }); - dialog.show(); - this.bind_enter_submit(dialog); - } - - // ── Route options (e.g. the Work Order "Shop Floor" button) ──────────────── - apply_route_options() { - const opts = frappe.route_options; - if (!opts || (!opts.work_order && !opts.workstation)) { - return; - } - frappe.route_options = null; - - // A specific work order / machine was requested — show it in the operator view. - this.view = "operator"; - this.render_shell_controls(); - this.render_view(); - Promise.all([ - this.work_order_filter.set_value(opts.work_order || ""), - this.workstation_filter.set_value(opts.workstation || ""), - ]).then(() => this.load_operator()); - } - - // ── Styles ────────────────────────────────────────────────────────────────── - styles() { - return ``; - } -} - -frappe.ui.ShopFloor = ShopFloor; From 1ededb70f4565dfddfc10b18fb9151d26bbd0f0b Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 9 Aug 2026 20:53:27 +0530 Subject: [PATCH 083/134] chore: resolve conflict --- .../doctype/job_card/job_card.py | 55 +------ .../doctype/job_card/test_job_card.py | 154 ++---------------- 2 files changed, 15 insertions(+), 194 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 715941dd5e9..572ff5f12bd 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -1236,7 +1236,7 @@ class JobCard(Document): def set_status(self, update_status=False): self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0] if self.finished_good and self.docstatus == 1: - if (self.manufactured_qty + self.process_loss_qty) >= self.for_quantity: + if (self.manufactured_qty + self.process_loss_qty) >= self.get_qty_to_produce(): self.status = "Completed" elif self.transferred_qty > 0 or self.skip_material_transfer: self.status = "Work In Progress" @@ -1267,7 +1267,8 @@ class JobCard(Document): self.status = "Work In Progress" if self.docstatus == 1 and ( - self.for_quantity <= (self.total_completed_qty + self.process_loss_qty) or not self.items + self.get_qty_to_produce() <= (self.total_completed_qty + self.process_loss_qty) + or not self.items ): self.status = "Completed" @@ -1280,53 +1281,10 @@ class JobCard(Document): if self.workstation: self.update_workstation_status() -<<<<<<< HEAD -======= def get_qty_to_produce(self): """Qty this job card is expected to produce, the pending qty is left to another job card.""" return flt(self.for_quantity) - flt(self.pending_qty) - def set_finished_good_status(self): - # Only reached for a submitted job card (docstatus == 1) with a finished good, see set_status(). - qty_to_produce = self.get_qty_to_produce() - - if (self.manufactured_qty + self.process_loss_qty) >= qty_to_produce: - self.status = "Completed" - elif (self.total_completed_qty + self.process_loss_qty) >= qty_to_produce: - # Production is done and the card is submitted, but the finished goods have not been - # booked into stock yet (Manufacture Stock Entry pending) — distinct from active WIP. - self.status = "To Manufacture" - elif self.transferred_qty > 0 or self.skip_material_transfer: - self.status = "Work In Progress" - - def set_non_semi_fg_status(self): - if self.items: - item_data = frappe.get_all( - "Job Card Item", - filters={"parent": self.name}, - fields=["transferred_qty", "required_qty"], - ) - all_transferred = item_data and all( - flt(d.transferred_qty) >= flt(d.required_qty) for d in item_data - ) - any_transferred = any(flt(d.transferred_qty) > 0 for d in item_data) - - if all_transferred: - self.status = "Material Transferred" - elif any_transferred: - self.status = "Partially Transferred" - elif flt(self.for_quantity) <= flt(self.transferred_qty): - self.status = "Material Transferred" - - if self.time_logs: - self.status = "Work In Progress" - - if self.docstatus == 1 and ( - self.get_qty_to_produce() <= (self.total_completed_qty + self.process_loss_qty) or not self.items - ): - self.status = "Completed" - ->>>>>>> 970039d8ec (fix(job_card): leave the pending qty out of the job card's own output (#57686)) def set_wip_warehouse(self): if not self.wip_warehouse: self.wip_warehouse = frappe.get_cached_value("Company", self.company, "default_wip_warehouse") @@ -1625,13 +1583,8 @@ class JobCard(Document): ste = ManufactureEntry( { -<<<<<<< HEAD - "for_quantity": self.for_quantity - self.manufactured_qty, - "process_loss_qty": max(self.process_loss_qty - get_consumed_process_loss(), 0), -======= "for_quantity": self.get_qty_to_produce() - self.manufactured_qty, - "process_loss_qty": max(self.process_loss_qty - self.get_consumed_process_loss(), 0), ->>>>>>> 970039d8ec (fix(job_card): leave the pending qty out of the job card's own output (#57686)) + "process_loss_qty": max(self.process_loss_qty - get_consumed_process_loss(), 0), "job_card": self.name, "skip_material_transfer": self.skip_material_transfer, "backflush_from_wip_warehouse": self.backflush_from_wip_warehouse, diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 85c73b4ff1a..9a8d978cc1f 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -1265,8 +1265,6 @@ class TestJobCard(ERPNextTestSuite): 8, ) -<<<<<<< HEAD -======= def test_semi_fg_pending_qty_is_left_to_another_job_card(self): from erpnext.manufacturing.doctype.operation.test_operation import make_operation from erpnext.stock.doctype.item.test_item import make_item @@ -1319,7 +1317,16 @@ class TestJobCard(ERPNextTestSuite): make_stock_entry(item_code=rm, target=warehouse, qty=100, basic_rate=100) - job_card = self.get_first_job_card(work_order.name) + job_card = frappe.get_doc( + "Job Card", + frappe.get_all( + "Job Card", + filters={"work_order": work_order.name}, + order_by="sequence_id, creation", + limit=1, + pluck="name", + )[0], + ) job_card.append("time_logs", {"from_time": "2024-04-01 08:00:00"}) job_card.save() @@ -1337,7 +1344,7 @@ class TestJobCard(ERPNextTestSuite): self.assertEqual(flt(job_card.process_loss_qty), 0) job_card.submit() - self.assertEqual(job_card.status, "To Manufacture") + self.assertEqual(job_card.status, "Work In Progress") manufacturing_entry = frappe.get_doc(job_card.make_stock_entry_for_semi_fg_item()) finished_item = next(row for row in manufacturing_entry.items if row.is_finished_item) @@ -1348,145 +1355,6 @@ class TestJobCard(ERPNextTestSuite): self.assertEqual(flt(job_card.manufactured_qty), 3) self.assertEqual(job_card.status, "Completed") - def test_semi_fg_sequence_needs_previous_operations_manufactured(self): - from erpnext.manufacturing.doctype.operation.test_operation import make_operation - from erpnext.stock.doctype.item.test_item import make_item - - warehouse = "Stores - _TC" - rm1 = make_item("Sequence Check RM 1", {"is_stock_item": 1}).name - rm2 = make_item("Sequence Check RM 2", {"is_stock_item": 1}).name - sfg1 = make_item("Sequence Check SFG 1", {"is_stock_item": 1}).name - sfg2 = make_item("Sequence Check SFG 2", {"is_stock_item": 1}).name - fg = make_item("Sequence Check FG 1", {"is_stock_item": 1}).name - - semi_fg_boms = {} - for semi_fg_item, raw_material in ((sfg1, rm1), (sfg2, rm2)): - bom = frappe.new_doc("BOM", company="_Test Company", item=semi_fg_item, quantity=1) - bom.append("items", {"item_code": raw_material, "qty": 1}) - bom.insert() - bom.submit() - semi_fg_boms[semi_fg_item] = bom.name - - fg_bom = frappe.new_doc( - "BOM", - company="_Test Company", - item=fg, - quantity=1, - with_operations=1, - track_semi_finished_goods=1, - ) - - operations = [ - { - "operation": "Sequence Check Op A", - "finished_good": sfg1, - "bom_no": semi_fg_boms[sfg1], - "sequence_id": 1, - }, - { - "operation": "Sequence Check Op B", - "finished_good": sfg2, - "bom_no": semi_fg_boms[sfg2], - "sequence_id": 1, - }, - { - "operation": "Sequence Check Op C", - "finished_good": fg, - "is_final_finished_good": 1, - "sequence_id": 2, - }, - ] - - for row in operations: - row.update( - { - "workstation": "_Test Workstation A", - "finished_good_qty": 1, - "time_in_mins": 60, - "source_warehouse": warehouse, - "fg_warehouse": warehouse, - "skip_material_transfer": 1, - } - ) - - make_workstation(row) - make_operation(row) - fg_bom.append("operations", row) - - fg_bom.append("items", {"item_code": sfg1, "qty": 1, "operation_row_id": 3}) - fg_bom.append("items", {"item_code": sfg2, "qty": 1, "operation_row_id": 3}) - fg_bom.insert() - fg_bom.submit() - - work_order = make_wo_order_test_record( - item=fg, - qty=5, - source_warehouse=warehouse, - fg_warehouse=warehouse, - bom_no=fg_bom.name, - skip_transfer=1, - do_not_save=True, - ) - - for row in work_order.operations: - row.time_in_mins = 60 - - work_order.save() - work_order.submit() - - make_stock_entry(item_code=rm1, target=warehouse, qty=10, basic_rate=100) - make_stock_entry(item_code=rm2, target=warehouse, qty=10, basic_rate=100) - - def get_job_card(operation): - return frappe.get_doc( - "Job Card", - frappe.db.get_value( - "Job Card", - {"work_order": work_order.name, "operation": operation, "docstatus": 0}, - "name", - ), - ) - - def add_time_log(job_card, day, qty): - job_card.append( - "time_logs", - { - "from_time": f"2024-01-{day} 08:00:00", - "to_time": f"2024-01-{day} 09:00:00", - "completed_qty": qty, - }, - ) - - jc_a = get_job_card("Sequence Check Op A") - jc_a.for_quantity = 3 - add_time_log(jc_a, "01", 3) - jc_a.submit() - - jc_b = get_job_card("Sequence Check Op B") - add_time_log(jc_b, "02", jc_b.for_quantity) - jc_b.submit() - frappe.get_doc(jc_b.make_stock_entry_for_semi_fg_item()).submit() - - jc_c = get_job_card("Sequence Check Op C") - jc_c.for_quantity = 3 - add_time_log(jc_c, "03", 3) - self.assertRaises(OperationSequenceError, jc_c.save) - - frappe.get_doc(jc_a.make_stock_entry_for_semi_fg_item()).submit() - - jc_c.reload() - jc_c.for_quantity = 4 - add_time_log(jc_c, "03", 4) - self.assertRaises(OperationSequenceError, jc_c.save) - - jc_c.reload() - jc_c.for_quantity = 3 - add_time_log(jc_c, "03", 3) - jc_c.submit() - - self.assertEqual(jc_c.docstatus, 1) - ->>>>>>> 970039d8ec (fix(job_card): leave the pending qty out of the job card's own output (#57686)) def test_semi_fg_batch_auto_pull_on_manufacture(self): from erpnext.manufacturing.doctype.operation.test_operation import make_operation from erpnext.stock.doctype.item.test_item import make_item From a5e4bbd4367d8d2472bfc4826430fcea8f98f2fb Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 9 Aug 2026 21:07:17 +0530 Subject: [PATCH 084/134] chore: resolve conflict --- erpnext/manufacturing/doctype/bom/bom.py | 36 +- .../doctype/job_card/job_card.py | 157 +----- .../doctype/job_card/test_job_card.py | 393 ++------------- .../doctype/work_order/services/status.py | 471 ------------------ .../doctype/work_order/test_work_order.py | 57 --- .../doctype/work_order/work_order.py | 8 +- .../stock/doctype/stock_entry/stock_entry.py | 20 +- .../stock_entry_type/stock_entry_type.py | 5 + 8 files changed, 66 insertions(+), 1081 deletions(-) delete mode 100644 erpnext/manufacturing/doctype/work_order/services/status.py diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index e33e81d5aaa..00e754ff561 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -305,10 +305,9 @@ class BOM(WebsiteGenerator): self.set_fg_cost_allocation() self.validate_total_cost_allocation() -<<<<<<< HEAD if self.docstatus == 1: self.validate_raw_materials_of_operation() -======= + def set_operation_finished_goods(self): """Fill each operation's FG item where it is unambiguous: the final operation produces this BOM's item, an operation with a BOM produces that BOM's item. Runs before @@ -321,7 +320,6 @@ class BOM(WebsiteGenerator): row.finished_good = self.item elif row.bom_no and not row.finished_good: row.finished_good = frappe.get_cached_value("BOM", row.bom_no, "item") ->>>>>>> 1e2e87daac (fix: derive operation FG items before material expansion, keep the final one the BOM's item) def validate_semi_finished_goods(self): if not self.track_semi_finished_goods or not self.operations: @@ -866,17 +864,10 @@ class BOM(WebsiteGenerator): row.update(get_item_details(row.get("item_code"))) row.operation_row_id = operation_row_id - item_row = None - if row.name: - item_row = self.get_item_data(row.name) + item_row = self.get_item_data(row.item_code, operation_row_id) if item_row: - item_row.update( - { - "item_code": row.get("item_code"), - "qty": row.get("qty"), - } - ) + item_row.qty = row.get("qty") else: row.idx = None row.name = None @@ -887,27 +878,6 @@ class BOM(WebsiteGenerator): self.save() -<<<<<<< HEAD -======= - def _add_raw_material_row(self, operation_row_id, row): - row = parse_json(row) - - row.update(get_item_details(row.get("item_code"))) - row.operation_row_id = operation_row_id - - item_row = self.get_item_data(row.item_code, operation_row_id) - - if item_row: - item_row.qty = row.get("qty") - else: - row.idx = None - row.name = None - row.do_not_explode = 1 - row.is_sub_assembly_item = self.is_sub_assembly_item(row.item_code) - - self.append("items", row) - ->>>>>>> 24f1f3dea8 (fix: add raw material to its operation even when another operation uses the item) def is_sub_assembly_item(self, item_code): if not self.operations: return False diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 970f81d5e03..9486ed14094 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -1041,32 +1041,7 @@ class JobCard(Document): ) def update_work_order_data(self, for_quantity, process_loss_qty, pending_qty, time_in_mins, wo): -<<<<<<< HEAD workstation_hour_rate = frappe.get_value("Workstation", self.workstation, "hour_rate") -======= - time_data = self.get_operation_time_data() - - for data in wo.operations: - if data.get("name") == self.operation_id: - self.update_wo_operation_row( - data, for_quantity, process_loss_qty, pending_qty, time_in_mins, time_data - ) - - wo.flags.ignore_validate_update_after_submit = True - wo.update_operation_status() - wo.calculate_operating_cost() - wo.set_actual_dates() - - if wo.track_semi_finished_goods: - wo.set_process_loss_qty() - - if time_data: - wo.status = "In Process" - - wo.save() - - def get_operation_time_data(self): ->>>>>>> 0eb61c9fac (fix: roll up process loss to the work order for semi finished goods) jc = frappe.qb.DocType("Job Card") jctl = frappe.qb.DocType("Job Card Time Log") @@ -1101,6 +1076,9 @@ class JobCard(Document): wo.calculate_operating_cost() wo.set_actual_dates() + if wo.track_semi_finished_goods: + wo.set_process_loss_qty() + if time_data: wo.status = "In Process" @@ -1367,57 +1345,6 @@ class JobCard(Document): if not (self.work_order and self.sequence_id): return -<<<<<<< HEAD -======= - current_operation_qty = self.get_current_operation_completed_qty() - - for row in self.get_previous_operations(): - if self.track_semi_finished_goods: - self.validate_previous_operation_manufactured_qty(row, current_operation_qty) - else: - self.validate_previous_operation(row, current_operation_qty) - - def get_previous_operations(self): - previous_operations = frappe.get_all( - "Work Order Operation", - fields=["name", "operation", "status", "completed_qty", "sequence_id", "finished_good"], - filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ("<", self.sequence_id)}, - order_by="sequence_id, idx", - ) - - if self.track_semi_finished_goods and previous_operations: - totals = self.get_manufactured_qty_per_operation([row.name for row in previous_operations]) - - for row in previous_operations: - operation_totals = totals.get(row.name) - row.manufactured_qty = flt(operation_totals and operation_totals.manufactured_qty) - row.process_loss_qty = flt(operation_totals and operation_totals.process_loss_qty) - - return previous_operations - - def get_manufactured_qty_per_operation(self, operation_ids): - job_card = frappe.qb.DocType("Job Card") - - data = ( - frappe.qb.from_(job_card) - .select( - job_card.operation_id, - Sum(job_card.manufactured_qty).as_("manufactured_qty"), - Sum(job_card.process_loss_qty).as_("process_loss_qty"), - ) - .where( - (job_card.work_order == self.work_order) - & (job_card.docstatus == 1) - & (IfNull(job_card.is_corrective_job_card, 0) == 0) - & (job_card.operation_id.isin(operation_ids)) - ) - .groupby(job_card.operation_id) - ).run(as_dict=True) - - return {row.operation_id: row for row in data} - - def get_current_operation_completed_qty(self): ->>>>>>> 1e22695eae (fix: stop asking for a manufacturing entry when process loss explains the shortfall) current_operation_qty = 0.0 data = self.get_current_operation_data() if data and len(data) > 0: @@ -1453,7 +1380,6 @@ class JobCard(Document): OperationSequenceError, ) -<<<<<<< HEAD if row.completed_qty < current_operation_qty: frappe.throw( _( @@ -1465,49 +1391,6 @@ class JobCard(Document): bold(row.operation), ) ) -======= - if not manufactured_qty: - frappe.throw( - _( - "Job Card {0}: As per the sequence of the operations in the work order {1}, submit the manufacturing entry for the operation {2} before the operation {3}." - ).format( - bold(self.name), - bold(get_link_to_form("Work Order", self.work_order)), - bold(row.operation), - bold(self.operation), - ), - OperationSequenceError, - ) - - if manufactured_qty >= current_operation_qty: - return - - if manufactured_qty + flt(row.process_loss_qty) >= current_operation_qty: - frappe.throw( - _( - "The completed quantity {0} of an operation {1} cannot be greater than the manufactured quantity {2} of a previous operation {3}, as {4} was booked as process loss there." - ).format( - bold(self.get_qty_with_uom(current_operation_qty)), - bold(self.operation), - bold(self.get_qty_with_uom(manufactured_qty, row.finished_good)), - bold(row.operation), - bold(self.get_qty_with_uom(flt(row.process_loss_qty), row.finished_good)), - ), - OperationSequenceError, - ) ->>>>>>> 1e22695eae (fix: stop asking for a manufacturing entry when process loss explains the shortfall) - - frappe.throw( - _( - "The completed quantity {0} of an operation {1} cannot be greater than the manufactured quantity {2} of a previous operation {3}. Submit the manufacturing entry for the operation {3} first." - ).format( - bold(self.get_qty_with_uom(current_operation_qty)), - bold(self.operation), - bold(self.get_qty_with_uom(manufactured_qty, row.finished_good)), - bold(row.operation), - ), - OperationSequenceError, - ) def validate_work_order(self): if self.is_work_order_closed(): @@ -1684,33 +1567,27 @@ class JobCard(Document): _("Job Card {0} has been completed").format(get_link_to_form("Job Card", self.name)) ) + def get_consumed_process_loss(self): + table = frappe.qb.DocType("Stock Entry") + query = ( + frappe.qb.from_(table) + .select(Sum(table.process_loss_qty)) + .where((table.purpose == "Manufacture") & (table.job_card == self.name) & (table.docstatus == 1)) + ) + return query.run()[0][0] or 0 + @frappe.whitelist() def make_stock_entry_for_semi_fg_item(self, auto_submit: bool = False): - def get_consumed_process_loss(): - table = frappe.qb.DocType("Stock Entry") - query = ( - frappe.qb.from_(table) - .select(Sum(table.process_loss_qty)) - .where( - (table.purpose == "Manufacture") & (table.job_card == self.name) & (table.docstatus == 1) - ) - ) - return query.run()[0][0] or 0 - from erpnext.stock.doctype.stock_entry_type.stock_entry_type import ManufactureEntry -<<<<<<< HEAD + consumed_process_loss = self.get_consumed_process_loss() ste = ManufactureEntry( { - "for_quantity": self.for_quantity - self.manufactured_qty, - "process_loss_qty": max(self.process_loss_qty - get_consumed_process_loss(), 0), -======= - consumed_process_loss = self.get_consumed_process_loss() - return ManufactureEntry( - { - "for_quantity": self.get_qty_to_produce() - self.manufactured_qty - consumed_process_loss, + "for_quantity": self.for_quantity + - self.pending_qty + - self.manufactured_qty + - consumed_process_loss, "process_loss_qty": max(self.process_loss_qty - consumed_process_loss, 0), ->>>>>>> b8dd886cd4 (fix: generate the next manufacture entry net of booked process loss) "job_card": self.name, "skip_material_transfer": self.skip_material_transfer, "backflush_from_wip_warehouse": self.backflush_from_wip_warehouse, diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index c1f48953e5b..9fa4b99e6be 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -1265,89 +1265,6 @@ class TestJobCard(ERPNextTestSuite): 8, ) -<<<<<<< HEAD -======= - def test_semi_fg_pending_qty_is_left_to_another_job_card(self): - from erpnext.manufacturing.doctype.operation.test_operation import make_operation - from erpnext.stock.doctype.item.test_item import make_item - - warehouse = "Stores - _TC" - rm = make_item("Pending Qty RM 1", {"is_stock_item": 1}).name - fg = make_item("Pending Qty FG 1", {"is_stock_item": 1}).name - - fg_bom = frappe.new_doc( - "BOM", - company="_Test Company", - item=fg, - quantity=1, - with_operations=1, - track_semi_finished_goods=1, - ) - fg_bom.append("items", {"item_code": rm, "qty": 1, "operation_row_id": 1}) - - operation = { - "operation": "Pending Qty Op A", - "workstation": "_Test Workstation A", - "finished_good": fg, - "finished_good_qty": 1, - "is_final_finished_good": 1, - "sequence_id": 1, - "time_in_mins": 60, - "source_warehouse": warehouse, - "fg_warehouse": warehouse, - "skip_material_transfer": 1, - } - - make_workstation(operation) - make_operation(operation) - fg_bom.append("operations", operation) - fg_bom.insert() - fg_bom.submit() - - work_order = make_wo_order_test_record( - item=fg, - qty=5, - source_warehouse=warehouse, - fg_warehouse=warehouse, - bom_no=fg_bom.name, - skip_transfer=1, - do_not_save=True, - ) - work_order.operations[0].time_in_mins = 60 - work_order.save() - work_order.submit() - - make_stock_entry(item_code=rm, target=warehouse, qty=100, basic_rate=100) - - job_card = self.get_first_job_card(work_order.name) - job_card.append("time_logs", {"from_time": "2024-04-01 08:00:00"}) - job_card.save() - - job_card.complete_job_card( - qty=3, - for_quantity=5, - pending_qty=2, - process_loss_qty=0, - end_time="2024-04-01 09:00:00", - ) - - job_card.reload() - self.assertEqual(flt(job_card.for_quantity), 5) - self.assertEqual(flt(job_card.pending_qty), 2) - self.assertEqual(flt(job_card.process_loss_qty), 0) - - job_card.submit() - self.assertEqual(job_card.status, "To Manufacture") - - manufacturing_entry = frappe.get_doc(job_card.make_stock_entry_for_semi_fg_item()) - finished_item = next(row for row in manufacturing_entry.items if row.is_finished_item) - self.assertEqual(flt(finished_item.qty), 3) - manufacturing_entry.submit() - - job_card.reload() - self.assertEqual(flt(job_card.manufactured_qty), 3) - self.assertEqual(job_card.status, "Completed") - def test_semi_fg_process_loss_rolls_up_to_work_order(self): from erpnext.manufacturing.doctype.operation.test_operation import make_operation from erpnext.stock.doctype.item.test_item import make_item @@ -1400,7 +1317,16 @@ class TestJobCard(ERPNextTestSuite): make_stock_entry(item_code=rm, target=warehouse, qty=100, basic_rate=100) - job_card = self.get_first_job_card(work_order.name) + job_card = frappe.get_doc( + "Job Card", + frappe.get_all( + "Job Card", + filters={"work_order": work_order.name}, + order_by="sequence_id, creation", + limit=1, + pluck="name", + )[0], + ) job_card.append("time_logs", {"from_time": "2024-05-01 08:00:00"}) job_card.save() @@ -1528,7 +1454,6 @@ class TestJobCard(ERPNextTestSuite): work_order.reload() self.assertEqual(flt(work_order.process_loss_qty), 2) - # Operation A handed over only 8 units, so the final operation works on 8. jc_b = get_job_card("Intermediate Loss Op B") jc_b.for_quantity = 8 for row in jc_b.items: @@ -1546,145 +1471,6 @@ class TestJobCard(ERPNextTestSuite): self.assertEqual(flt(work_order.process_loss_qty), 2) self.assertEqual(work_order.status, "Completed") - def test_semi_fg_sequence_needs_previous_operations_manufactured(self): - from erpnext.manufacturing.doctype.operation.test_operation import make_operation - from erpnext.stock.doctype.item.test_item import make_item - - warehouse = "Stores - _TC" - rm1 = make_item("Sequence Check RM 1", {"is_stock_item": 1}).name - rm2 = make_item("Sequence Check RM 2", {"is_stock_item": 1}).name - sfg1 = make_item("Sequence Check SFG 1", {"is_stock_item": 1}).name - sfg2 = make_item("Sequence Check SFG 2", {"is_stock_item": 1}).name - fg = make_item("Sequence Check FG 1", {"is_stock_item": 1}).name - - semi_fg_boms = {} - for semi_fg_item, raw_material in ((sfg1, rm1), (sfg2, rm2)): - bom = frappe.new_doc("BOM", company="_Test Company", item=semi_fg_item, quantity=1) - bom.append("items", {"item_code": raw_material, "qty": 1}) - bom.insert() - bom.submit() - semi_fg_boms[semi_fg_item] = bom.name - - fg_bom = frappe.new_doc( - "BOM", - company="_Test Company", - item=fg, - quantity=1, - with_operations=1, - track_semi_finished_goods=1, - ) - - operations = [ - { - "operation": "Sequence Check Op A", - "finished_good": sfg1, - "bom_no": semi_fg_boms[sfg1], - "sequence_id": 1, - }, - { - "operation": "Sequence Check Op B", - "finished_good": sfg2, - "bom_no": semi_fg_boms[sfg2], - "sequence_id": 1, - }, - { - "operation": "Sequence Check Op C", - "finished_good": fg, - "is_final_finished_good": 1, - "sequence_id": 2, - }, - ] - - for row in operations: - row.update( - { - "workstation": "_Test Workstation A", - "finished_good_qty": 1, - "time_in_mins": 60, - "source_warehouse": warehouse, - "fg_warehouse": warehouse, - "skip_material_transfer": 1, - } - ) - - make_workstation(row) - make_operation(row) - fg_bom.append("operations", row) - - fg_bom.append("items", {"item_code": sfg1, "qty": 1, "operation_row_id": 3}) - fg_bom.append("items", {"item_code": sfg2, "qty": 1, "operation_row_id": 3}) - fg_bom.insert() - fg_bom.submit() - - work_order = make_wo_order_test_record( - item=fg, - qty=5, - source_warehouse=warehouse, - fg_warehouse=warehouse, - bom_no=fg_bom.name, - skip_transfer=1, - do_not_save=True, - ) - - for row in work_order.operations: - row.time_in_mins = 60 - - work_order.save() - work_order.submit() - - make_stock_entry(item_code=rm1, target=warehouse, qty=10, basic_rate=100) - make_stock_entry(item_code=rm2, target=warehouse, qty=10, basic_rate=100) - - def get_job_card(operation): - return frappe.get_doc( - "Job Card", - frappe.db.get_value( - "Job Card", - {"work_order": work_order.name, "operation": operation, "docstatus": 0}, - "name", - ), - ) - - def add_time_log(job_card, day, qty): - job_card.append( - "time_logs", - { - "from_time": f"2024-01-{day} 08:00:00", - "to_time": f"2024-01-{day} 09:00:00", - "completed_qty": qty, - }, - ) - - jc_a = get_job_card("Sequence Check Op A") - jc_a.for_quantity = 3 - add_time_log(jc_a, "01", 3) - jc_a.submit() - - jc_b = get_job_card("Sequence Check Op B") - add_time_log(jc_b, "02", jc_b.for_quantity) - jc_b.submit() - frappe.get_doc(jc_b.make_stock_entry_for_semi_fg_item()).submit() - - jc_c = get_job_card("Sequence Check Op C") - jc_c.for_quantity = 3 - add_time_log(jc_c, "03", 3) - self.assertRaises(OperationSequenceError, jc_c.save) - - frappe.get_doc(jc_a.make_stock_entry_for_semi_fg_item()).submit() - - jc_c.reload() - jc_c.for_quantity = 4 - add_time_log(jc_c, "03", 4) - self.assertRaises(OperationSequenceError, jc_c.save) - - jc_c.reload() - jc_c.for_quantity = 3 - add_time_log(jc_c, "03", 3) - jc_c.submit() - - self.assertEqual(jc_c.docstatus, 1) - ->>>>>>> 24de81f9fa (test: work order process loss for semi finished goods) def test_semi_fg_batch_auto_pull_on_manufacture(self): from erpnext.manufacturing.doctype.operation.test_operation import make_operation from erpnext.stock.doctype.item.test_item import make_item @@ -2530,6 +2316,27 @@ class TestJobCard(ERPNextTestSuite): self.assertEqual(s.additional_costs[2].amount, 480) self.assertEqual(s.additional_costs[3].amount, 480) + def test_semi_fg_job_card_is_exempt_from_transfer_qty_check(self): + jc = frappe.new_doc("Job Card") + jc.track_semi_finished_goods = 1 + jc.skip_material_transfer = 1 + jc.for_quantity = 10 + jc.transferred_qty = 0 + jc.append("items", {"item_code": "_Test Item"}) + + jc.validate_transfer_qty() + + # with transfer enabled, a legacy card without an FG item keeps the strict check + jc.skip_material_transfer = 0 + self.assertRaises(frappe.ValidationError, jc.validate_transfer_qty) + + jc.finished_good = "_Test Item" + jc.validate_transfer_qty() + + jc.finished_good = None + jc.track_semi_finished_goods = 0 + self.assertRaises(frappe.ValidationError, jc.validate_transfer_qty) + def create_bom_with_multiple_operations(): "Create a BOM with multiple operations and Material Transfer against Job Card" @@ -2592,141 +2399,3 @@ def create_semi_fg_bom(semi_fg_item, raw_item, inspection_required): bom.append("items", {"item_code": raw_item, "qty": 1}) bom.submit() return bom.name -<<<<<<< HEAD -======= - - -class TestJobCardLogic(ERPNextTestSuite): - """Field-level validations and pure quantity/capacity helpers, exercised on the - document directly so they don't need a Work Order / BOM (the integration suite does).""" - - def test_processing_a_submitted_or_cancelled_card_is_blocked(self): - submitted = frappe.new_doc("Job Card") - submitted.docstatus = 1 - self.assertRaises(frappe.ValidationError, submitted.validate_docstatus) - - cancelled = frappe.new_doc("Job Card") - cancelled.docstatus = 2 - self.assertRaises(frappe.ValidationError, cancelled.validate_docstatus) - - def test_complete_job_card_qty_guards(self): - jc = frappe.new_doc("Job Card") - jc.for_quantity = 5 - jc.validate_complete_job_card_qty(frappe._dict(pending_qty=3)) # within range -> passes - self.assertRaises( - frappe.ValidationError, jc.validate_complete_job_card_qty, frappe._dict(pending_qty=-1) - ) - self.assertRaises( - frappe.ValidationError, jc.validate_complete_job_card_qty, frappe._dict(process_loss_qty=-1) - ) - self.assertRaises( - frappe.ValidationError, jc.validate_complete_job_card_qty, frappe._dict(pending_qty=10) - ) - - def test_qty_in_messages_carries_the_uom(self): - jc = frappe.new_doc("Job Card") - jc.stock_uom = "Nos" - - self.assertEqual(jc.get_qty_with_uom(5), "5.0 Nos") - self.assertEqual(jc.get_qty_with_uom(0), "0.0 Nos") - - def test_completion_qty_split_must_add_up(self): - jc = frappe.new_doc("Job Card") - jc.for_quantity = 5 - - # 3 completed + 2 pending + 0 lost == 5 to manufacture -> passes - jc.validate_complete_job_card_qty( - frappe._dict(for_quantity=5, qty=3, pending_qty=2, process_loss_qty=0) - ) - - self.assertRaises( - frappe.ValidationError, - jc.validate_complete_job_card_qty, - frappe._dict(for_quantity=3, qty=3, pending_qty=2, process_loss_qty=0), - ) - - def test_completed_qty_must_reconcile_with_for_quantity(self): - jc = frappe.new_doc("Job Card") - jc.for_quantity = 10 - jc.total_completed_qty = 6 - jc.process_loss_qty = 0 - jc.pending_qty = 0 - # 6 + 0 + 0 != 10 -> throws - self.assertRaises(frappe.ValidationError, jc.validate_completed_qty_matches_for_quantity) - # completed + loss + pending == for_quantity -> passes - jc.pending_qty = 4 - jc.validate_completed_qty_matches_for_quantity() - - def test_set_process_loss(self): - jc = frappe.new_doc("Job Card") - jc.for_quantity = 10 - jc.total_completed_qty = 6 - jc.pending_qty = 1 - jc.set_process_loss() - self.assertEqual(jc.process_loss_qty, 3) # 10 - 6 - 1 - - # no loss when nothing completed yet - nothing_done = frappe.new_doc("Job Card") - nothing_done.for_quantity = 10 - nothing_done.total_completed_qty = 0 - nothing_done.set_process_loss() - self.assertEqual(nothing_done.process_loss_qty, 0) - - def test_capacity_overlap_detection(self): - jc = frappe.new_doc("Job Card") - sequential = [ - {"from_time": "2026-01-01 10:00:00", "to_time": "2026-01-01 11:00:00"}, - {"from_time": "2026-01-01 11:00:00", "to_time": "2026-01-01 12:00:00"}, - ] - overlapping = [ - {"from_time": "2026-01-01 10:00:00", "to_time": "2026-01-01 11:00:00"}, - {"from_time": "2026-01-01 10:30:00", "to_time": "2026-01-01 11:30:00"}, - ] - # sequential logs share one capacity slot; overlapping logs need two - self.assertEqual(len(jc.get_alloted_capacity(sequential)), 1) - self.assertEqual(len(jc.get_alloted_capacity(overlapping)), 2) - # capacity 1 overlaps with any log; capacity 2 only when both slots are taken - self.assertTrue(jc.has_overlap(1, sequential)) - self.assertFalse(jc.has_overlap(2, sequential)) - self.assertTrue(jc.has_overlap(2, overlapping)) - - def test_previous_operation_shortfall_from_process_loss_gets_the_right_message(self): - jc = frappe.new_doc("Job Card") - jc.operation = "_Test Painting" - jc.stock_uom = "Nos" - row = frappe._dict( - operation="_Test Assembly", manufactured_qty=8, process_loss_qty=2, finished_good=None - ) - - with self.assertRaises(OperationSequenceError) as loss_error: - jc.validate_previous_operation_manufactured_qty(row, 10) - self.assertIn("process loss", str(loss_error.exception)) - - row.process_loss_qty = 0 - with self.assertRaises(OperationSequenceError) as pending_error: - jc.validate_previous_operation_manufactured_qty(row, 10) - self.assertIn("Submit the manufacturing entry", str(pending_error.exception)) - - jc.validate_previous_operation_manufactured_qty(row, 8) - - def test_semi_fg_job_card_is_exempt_from_transfer_qty_check(self): - jc = frappe.new_doc("Job Card") - jc.track_semi_finished_goods = 1 - jc.skip_material_transfer = 1 - jc.for_quantity = 10 - jc.transferred_qty = 0 - jc.append("items", {"item_code": "_Test Item"}) - - jc.validate_transfer_qty() - - # with transfer enabled, a legacy card without an FG item keeps the strict check - jc.skip_material_transfer = 0 - self.assertRaises(frappe.ValidationError, jc.validate_transfer_qty) - - jc.finished_good = "_Test Item" - jc.validate_transfer_qty() - - jc.finished_good = None - jc.track_semi_finished_goods = 0 - self.assertRaises(frappe.ValidationError, jc.validate_transfer_qty) ->>>>>>> 4b3904c6d7 (test: semi FG job card is exempt from the legacy transfer qty check) diff --git a/erpnext/manufacturing/doctype/work_order/services/status.py b/erpnext/manufacturing/doctype/work_order/services/status.py deleted file mode 100644 index 74f204acd40..00000000000 --- a/erpnext/manufacturing/doctype/work_order/services/status.py +++ /dev/null @@ -1,471 +0,0 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -"""Status and quantity-rollup logic for Work Order. - -Extracted from work_order.py. ``StatusService`` wraps a Work Order document -(composition); work_order.py keeps thin delegating stubs so the many external -callers (job cards, sales orders, production plans, patches) keep working. -""" - -import frappe -from frappe import _ -from frappe.query_builder.functions import Sum -from frappe.utils import cint, flt, get_link_to_form - -from erpnext.stock.stock_balance import get_planned_qty, update_bin_qty - -_QTY_PURPOSES = ( - ("Manufacture", "produced_qty"), - ("Material Transfer for Manufacture", "material_transferred_for_manufacturing"), - ("Material Transfer for Manufacture", "additional_transferred_qty"), -) - - -class StatusService: - def __init__(self, doc): - self.doc = doc - - def validate_work_order_against_so(self): - from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError - - total_qty = flt(self._ordered_qty_against_so()) + flt(self.doc.qty) - so_qty = flt(self._so_item_qty()) + flt(self._packed_item_qty()) - allowance_percentage = flt( - frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_sales_order") - ) - if total_qty <= so_qty + (allowance_percentage / 100 * so_qty): - return - - frappe.throw( - _("Cannot produce more Item {0} than Sales Order quantity {1} {2}").format( - get_link_to_form("Item", self.doc.production_item), - frappe.bold(so_qty), - frappe.bold(frappe.get_value("Item", self.doc.production_item, "stock_uom")), - ), - OverProductionError, - ) - - def _ordered_qty_against_so(self): - wo = frappe.qb.DocType("Work Order") - return ( - frappe.qb.from_(wo) - .select(Sum(wo.qty - wo.process_loss_qty)) - .where( - (wo.production_item == self.doc.production_item) - & (wo.sales_order == self.doc.sales_order) - & (wo.docstatus == 1) - & (wo.status != "Closed") - & (wo.name != self.doc.name) - ) - ).run()[0][0] - - def _so_item_qty(self): - so_item = frappe.qb.DocType("Sales Order Item") - return ( - frappe.qb.from_(so_item) - .select(Sum(so_item.stock_qty)) - .where( - (so_item.parent == self.doc.sales_order) - & (so_item.item_code == self.doc.production_item) - & (so_item.docstatus == 1) - ) - ).run()[0][0] - - def _packed_item_qty(self): - packed_item = frappe.qb.DocType("Packed Item") - return ( - frappe.qb.from_(packed_item) - .select(Sum(packed_item.qty)) - .where( - (packed_item.parent == self.doc.sales_order) - & (packed_item.parenttype == "Sales Order") - & (packed_item.item_code == self.doc.production_item) - & (packed_item.docstatus == 1) - ) - ).run()[0][0] - - def update_status(self, status=None): - """Update status of work order if unknown""" - if self.doc.docstatus == 1: - # Refresh material_transferred_for_manufacturing before deciding status so pick-list- - # driven transfers (where this qty is derived from item transfers, not fg_completed_qty) - # are reflected immediately, instead of only after the next status update call. - self.doc.refresh_material_transferred_for_manufacturing() - - if self.doc.status != "Closed": - if status not in ["Stopped", "Closed"]: - status = self.get_status(status) - - if status != self.doc.status: - self.doc.db_set("status", status) - - self.doc.update_required_items() - - return status or self.doc.status - - def get_status(self, status=None): - """Return the status based on stock entries against this work order""" - status = status or self.doc.status - - if self.doc.docstatus == 0: - status = "Draft" - elif self.doc.docstatus == 1: - status = self._submitted_status(status) - else: - status = "Cancelled" - - if self._is_partial_skip_transfer(): - status = "In Process" - - if status != "Completed" and not all(d.status == "Pending" for d in self.doc.operations): - status = "In Process" - - if status == "Not Started" and self.doc.reserve_stock: - status = self._reservation_status(status) - - return status - - def _submitted_status(self, status): - if status in ["Closed", "Stopped"]: - return status - - status = ( - "In Process" - if flt(self.doc.material_transferred_for_manufacturing) > 0 - or self.doc.skip_transfer - or self._has_transferred_material() - else "Not Started" - ) - precision = frappe.get_precision("Work Order", "produced_qty") - total_qty = flt(self.doc.produced_qty, precision) + flt(self.doc.process_loss_qty, precision) - if flt(total_qty, precision) >= flt(self.doc.qty, precision): - status = "Completed" - return status - - def _has_transferred_material(self): - """True if any raw material was transferred against this work order via a pick list - or a material request (these leave material_transferred_for_manufacturing at 0 via - the min-fraction rule).""" - ste = frappe.qb.DocType("Stock Entry") - ste_child = frappe.qb.DocType("Stock Entry Detail") - mr_child = frappe.qb.DocType("Stock Entry Detail") - # Stock Entry only carries `material_request` at the child-row level, so a Stock - # Entry is "MR-sourced" if *any* of its rows link back to a Material Request; once - # that's established, sum every row's transfer_qty, not just the linked ones (a - # manually appended extra row on the same entry has no material_request of its own). - mr_sourced_stock_entries = ( - frappe.qb.from_(mr_child).select(mr_child.parent).where(mr_child.material_request.isnotnull()) - ) - qty = ( - frappe.qb.from_(ste) - .inner_join(ste_child) - .on(ste_child.parent == ste.name) - .select(Sum(ste_child.transfer_qty)) - .where( - (ste.work_order == self.doc.name) - & (ste.docstatus == 1) - & (ste.purpose == "Material Transfer for Manufacture") - & (ste.is_return == 0) - & (ste.pick_list.isnotnull() | ste.name.isin(mr_sourced_stock_entries)) - ) - ).run()[0][0] - return flt(qty) > 0 - - def _is_partial_skip_transfer(self): - return bool( - self.doc.skip_transfer - and self.doc.produced_qty - and self.doc.qty > (flt(self.doc.produced_qty) + flt(self.doc.process_loss_qty)) - ) - - def _reservation_status(self, status): - for row in self.doc.required_items: - if not row.stock_reserved_qty: - continue - - if row.stock_reserved_qty >= row.required_qty: - status = "Stock Reserved" - else: - return "Stock Partially Reserved" - return status - - def update_work_order_qty(self): - """Update Manufactured Qty and Material Transferred for Qty based on Stock Entry""" - if self.doc.track_semi_finished_goods: - return - - for purpose, fieldname in _QTY_PURPOSES: - self._update_qty_for_purpose(purpose, fieldname) - - if self.doc.production_plan: - self.set_produced_qty_for_sub_assembly_item() - self.update_production_plan_status() - - if self.doc.additional_transferred_qty: - self.doc.validate_additional_transferred_qty() - - def _update_qty_for_purpose(self, purpose, fieldname): - from erpnext.manufacturing.doctype.work_order.work_order import StockOverProductionError - - if self._skip_transfer_purpose(purpose): - return - - qty = self.get_transferred_or_manufactured_qty(purpose, fieldname) - completed_qty = self.doc.qty + (self._qty_allowance(purpose) / 100 * self.doc.qty) - if qty > completed_qty: - frappe.throw( - _("{0} ({1}) cannot be greater than planned quantity ({2}) in Work Order {3}").format( - _(self.doc.meta.get_label(fieldname)), qty, completed_qty, self.doc.name - ), - StockOverProductionError, - ) - - self.doc.db_set(fieldname, qty) - self.set_process_loss_qty() - self._update_produced_qty_in_so() - - def _skip_transfer_purpose(self, purpose): - return bool( - purpose == "Material Transfer for Manufacture" - and self.doc.operations - and self.doc.transfer_material_against == "Job Card" - ) - - def _qty_allowance(self, purpose): - allowance = flt( - frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order") - ) - if not allowance and purpose == "Material Transfer for Manufacture": - allowance = flt( - frappe.db.get_single_value("Manufacturing Settings", "transfer_extra_materials_percentage") - ) - return allowance - - def _update_produced_qty_in_so(self): - from erpnext.selling.doctype.sales_order.sales_order import update_produced_qty_in_so_item - - if ( - self.doc.sales_order - and self.doc.sales_order_item - and not self.doc.production_plan_sub_assembly_item - ): - update_produced_qty_in_so_item(self.doc.sales_order, self.doc.sales_order_item) - - def update_disassembled_qty(self, qty, is_cancel=False): - if is_cancel: - self.doc.disassembled_qty = max(0, self.doc.disassembled_qty - qty) - else: - if self.doc.docstatus == 1: - self.doc.disassembled_qty += qty - - if not is_cancel and self.doc.disassembled_qty > self.doc.produced_qty: - frappe.throw(_("Cannot disassemble more than produced quantity.")) - - self.doc.db_set("disassembled_qty", self.doc.disassembled_qty) - - def get_transferred_or_manufactured_qty(self, purpose, fieldname): - parent = frappe.qb.DocType("Stock Entry") - is_additional = cint(fieldname == "additional_transferred_qty") - query = frappe.qb.from_(parent).where(self._stock_entry_filter(parent, purpose, is_additional)) - - if purpose == "Manufacture": - child = frappe.qb.DocType("Stock Entry Detail") - query = ( - query.join(child) - .on(parent.name == child.parent) - .select(Sum(child.transfer_qty)) - .where(child.is_finished_item == 1) - ) - else: - query = query.select(Sum(parent.fg_completed_qty)) - - return flt(query.run()[0][0]) - - def _stock_entry_filter(self, parent, purpose, is_additional): - return ( - (parent.work_order == self.doc.name) - & (parent.docstatus == 1) - & (parent.purpose == purpose) - & (parent.is_additional_transfer_entry == is_additional) - ) - - def set_process_loss_qty(self): - self.doc.db_set("process_loss_qty", self._process_loss_qty()) - - def _process_loss_qty(self): - if self.doc.track_semi_finished_goods: - return flt(sum(flt(row.process_loss_qty) for row in self.doc.operations)) - - table = frappe.qb.DocType("Stock Entry") - process_loss_qty = ( - frappe.qb.from_(table) - .select(Sum(table.process_loss_qty)) - .where( - (table.work_order == self.doc.name) - & (table.purpose == "Manufacture") - & (table.docstatus == 1) - ) - ).run()[0][0] - - return flt(process_loss_qty) - - def update_production_plan_status(self): - production_plan = frappe.get_doc("Production Plan", self.doc.production_plan) - produced_qty = 0 - if self.doc.production_plan_item: - total_qty = frappe.get_all( - "Work Order", - fields=[{"SUM": "produced_qty", "as": "produced_qty"}], - filters={ - "docstatus": 1, - "production_plan": self.doc.production_plan, - "production_plan_item": self.doc.production_plan_item, - }, - as_list=1, - ) - - produced_qty = total_qty[0][0] if total_qty else 0 - - self.update_status() - production_plan.run_method("update_produced_pending_qty", produced_qty, self.doc.production_plan_item) - - def update_planned_qty(self): - if self.doc.track_semi_finished_goods: - return - - update_bin_qty(self.doc.production_item, self.doc.fg_warehouse, self._planned_qty_dict()) - - if self.doc.material_request: - mr_obj = frappe.get_doc("Material Request", self.doc.material_request) - mr_obj.update_requested_qty([self.doc.material_request_item]) - - def _planned_qty_dict(self): - from erpnext.manufacturing.doctype.production_plan.production_plan import ( - get_reserved_qty_for_sub_assembly, - ) - - qty_dict = {"planned_qty": get_planned_qty(self.doc.production_item, self.doc.fg_warehouse)} - if self.doc.production_plan_sub_assembly_item and self.doc.production_plan: - qty_dict["reserved_qty_for_production_plan"] = get_reserved_qty_for_sub_assembly( - self.doc.production_item, self.doc.fg_warehouse - ) - return qty_dict - - def set_produced_qty_for_sub_assembly_item(self): - produced_qty = self._sub_assembly_produced_qty() - frappe.db.set_value( - "Production Plan Sub Assembly Item", - self.doc.production_plan_sub_assembly_item, - "wo_produced_qty", - produced_qty, - ) - - def _sub_assembly_produced_qty(self): - table = frappe.qb.DocType("Work Order") - query = ( - frappe.qb.from_(table) - .select(Sum(table.produced_qty)) - .where( - (table.production_plan == self.doc.production_plan) - & (table.production_plan_sub_assembly_item == self.doc.production_plan_sub_assembly_item) - & (table.docstatus == 1) - ) - ).run() - return flt(query[0][0]) if query else 0 - - def update_ordered_qty(self): - if not ( - self.doc.production_plan - and (self.doc.production_plan_item or self.doc.production_plan_sub_assembly_item) - ): - return - - qty = self._production_plan_ordered_qty() - if self.doc.production_plan_item: - frappe.db.set_value("Production Plan Item", self.doc.production_plan_item, "ordered_qty", qty) - elif self.doc.production_plan_sub_assembly_item: - field = self.doc.production_plan_sub_assembly_item - frappe.db.set_value("Production Plan Sub Assembly Item", field, "ordered_qty", qty) - - doc = frappe.get_doc("Production Plan", self.doc.production_plan) - doc.set_status() - doc.db_set("status", doc.status) - - def _production_plan_ordered_qty(self): - table = frappe.qb.DocType("Work Order") - query = ( - frappe.qb.from_(table) - .select(Sum(table.qty)) - .where((table.production_plan == self.doc.production_plan) & (table.docstatus == 1)) - ) - if self.doc.production_plan_item: - query = query.where(table.production_plan_item == self.doc.production_plan_item) - elif self.doc.production_plan_sub_assembly_item: - query = query.where( - table.production_plan_sub_assembly_item == self.doc.production_plan_sub_assembly_item - ) - - result = query.run() - return flt(result[0][0]) if result else 0 - - def update_work_order_qty_in_so(self): - if ( - not self.doc.sales_order and not self.doc.sales_order_item - ) or self.doc.production_plan_sub_assembly_item: - return - - total_bundle_qty = self._total_bundle_qty() - work_order_qty = self._sales_order_work_order_qty() - frappe.db.set_value( - "Sales Order Item", - self.doc.sales_order_item, - "work_order_qty", - flt(work_order_qty / total_bundle_qty, 2), - ) - - def _sales_order_work_order_qty(self): - wo = frappe.qb.DocType("Work Order") - query = ( - frappe.qb.from_(wo) - .select(Sum(wo.qty)) - .where((wo.sales_order == self.doc.sales_order) & (wo.docstatus == 1) & (wo.status != "Closed")) - ) - if self.doc.product_bundle_item: - query = query.where(wo.product_bundle_item == self.doc.product_bundle_item) - else: - query = query.where(wo.production_item == self.doc.production_item) - - qty = query.run(as_list=1) - return qty[0][0] if qty and qty[0][0] else 0 - - def update_work_order_qty_in_combined_so(self): - total_bundle_qty = self._total_bundle_qty() - prod_plan = frappe.get_doc("Production Plan", self.doc.production_plan) - item_reference = frappe.get_value( - "Production Plan Item", self.doc.production_plan_item, "sales_order_item" - ) - - for plan_reference in prod_plan.prod_plan_references: - if plan_reference.item_reference != item_reference: - continue - - qty = flt(plan_reference.qty) / total_bundle_qty if self.doc.docstatus == 1 else 0.0 - frappe.db.set_value("Sales Order Item", plan_reference.sales_order_item, "work_order_qty", qty) - - def _total_bundle_qty(self): - if not self.doc.product_bundle_item: - return 1 - - pbi = frappe.qb.DocType("Product Bundle Item") - total_bundle_qty = ( - frappe.qb.from_(pbi).select(Sum(pbi.qty)).where(pbi.parent == self.doc.product_bundle_item) - ).run()[0][0] - # product bundle is 0 (product bundle allows 0 qty for items) - return total_bundle_qty or 1 - - def update_completed_qty_in_material_request(self): - if self.doc.material_request and self.doc.material_request_item: - frappe.get_doc("Material Request", self.doc.material_request).update_completed_qty( - [self.doc.material_request_item] - ) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 3e5304180bf..18bd6ff7998 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -4839,59 +4839,6 @@ class TestWorkOrder(ERPNextTestSuite): # generated qty (3.0 for 8 units) differs from the BOM-scaled qty (7.5 for 20 units) self.assertEqual(flt(row.qty, 6), 3.0) -<<<<<<< HEAD -======= - def test_transferred_qty_not_misattributed_between_item_and_its_substitute(self): - """When one item is transferred both for itself and as a substitute for another required item, - each transfer must be credited to the right required item. - - _material_transfer_qty_by_item grouped Stock Entry Detail by item_code only and picked - Max(original_item); for item B transferred once for itself (original_item NULL) and once as a - substitute for A (original_item=A), Max picked A and credited B's whole transfer to A, leaving - B at 0. Grouping by (item_code, original_item) and accumulating into the keyed dict attributes - each transfer correctly, deterministically on MariaDB and Postgres. - """ - from erpnext.manufacturing.doctype.work_order.services.required_items import RequiredItemsService - - source_warehouse = "Stores - _TC" - fg_item = make_item("Test WO SelfSub FG", {"is_stock_item": 1}).name - item_a = make_item("Test WO SelfSub RM A", {"is_stock_item": 1, "allow_alternative_item": 1}).name - item_b = make_item("Test WO SelfSub RM B", {"is_stock_item": 1, "allow_alternative_item": 1}).name - - # B is a registered alternative for A - if not frappe.db.exists("Item Alternative", {"item_code": item_a, "alternative_item_code": item_b}): - frappe.get_doc( - { - "doctype": "Item Alternative", - "item_code": item_a, - "alternative_item_code": item_b, - "two_way": 1, - } - ).insert() - - # stock B generously (covers B-for-A plus B-for-itself) - for item, qty in ((item_a, 50), (item_b, 100)): - test_stock_entry.make_stock_entry( - item_code=item, target=source_warehouse, qty=qty, basic_rate=100 - ) - - make_bom(item=fg_item, source_warehouse=source_warehouse, raw_materials=[item_a, item_b]) - wo = make_wo_order_test_record(item=fg_item, qty=10, source_warehouse=source_warehouse) - - transfer = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 10)) - transfer.save() - # substitute B for the A line; the existing B line stays as B's own transfer - for d in transfer.items: - if d.item_code == item_a: - d.item_code = item_b - d.original_item = item_a - transfer.submit() - - qty_by_item = RequiredItemsService(wo)._material_transfer_qty_by_item(is_return=0) - # B transferred as a substitute for A -> credited to A; B transferred for itself -> credited to B. - self.assertEqual(flt(qty_by_item.get(item_a)), 10.0) - self.assertEqual(flt(qty_by_item.get(item_b)), 10.0) - def test_wip_warehouse_required_when_tracking_semi_finished_goods(self): wo = frappe.new_doc("Work Order") wo.track_semi_finished_goods = 1 @@ -4903,9 +4850,6 @@ class TestWorkOrder(ERPNextTestSuite): wo.wip_warehouse = "_Test Warehouse - _TC" wo.validate_warehouse() -<<<<<<< HEAD ->>>>>>> f61f6523b9 (test: WIP warehouse required for work orders tracking semi finished goods) -======= # the top-level target warehouse stays optional; operations may carry their own wo.fg_warehouse = None wo.validate_warehouse() @@ -4913,7 +4857,6 @@ class TestWorkOrder(ERPNextTestSuite): wo.track_semi_finished_goods = 0 self.assertRaises(frappe.ValidationError, wo.validate_warehouse) ->>>>>>> db99657c47 (test: target warehouse stays optional for semi FG work orders) def get_reserved_entries(voucher_no, warehouse=None): doctype = frappe.qb.DocType("Stock Reservation Entry") diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 30ed33a66a4..bb3d256ffee 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -883,6 +883,12 @@ class WorkOrder(Document): return flt(query.run()[0][0]) def set_process_loss_qty(self): + self.db_set("process_loss_qty", self._process_loss_qty()) + + def _process_loss_qty(self): + if self.track_semi_finished_goods: + return flt(sum(flt(row.process_loss_qty) for row in self.operations)) + table = frappe.qb.DocType("Stock Entry") process_loss_qty = ( frappe.qb.from_(table) @@ -892,7 +898,7 @@ class WorkOrder(Document): ) ).run()[0][0] - self.db_set("process_loss_qty", flt(process_loss_qty)) + return flt(process_loss_qty) def update_production_plan_status(self): production_plan = frappe.get_doc("Production Plan", self.production_plan) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 36da536c328..0c6c3bbee9b 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -3197,23 +3197,8 @@ class StockEntry(StockController, SubcontractingInwardController): if process_loss_qty and flt(self.process_loss_qty, precision) != flt(process_loss_qty, precision): self.process_loss_qty = flt(process_loss_qty, precision) - frappe.msgprint( - _("The Process Loss Qty has been reset as per the job card's Process Loss Qty"), - alert=True, - ) + frappe.msgprint(_("The Process Loss Qty has reset as per job cards Process Loss Qty"), alert=True) -<<<<<<< HEAD - if data and data[0].process_loss_qty: - process_loss_qty = data[0].process_loss_qty - if flt(self.process_loss_qty, precision) != flt(process_loss_qty, precision): - self.process_loss_qty = flt(process_loss_qty, precision) - - frappe.msgprint( - _("The Process Loss Qty has reset as per job cards Process Loss Qty"), alert=True - ) - -======= ->>>>>>> 1b335973b7 (fix: scope manufacture entry process loss to its own job card) if not self.process_loss_percentage and not self.process_loss_qty: self.process_loss_percentage = frappe.get_cached_value( "BOM", self.bom_no, "process_loss_percentage" @@ -3243,7 +3228,8 @@ class StockEntry(StockController, SubcontractingInwardController): precision = frappe.get_precision("Stock Entry Detail", "qty") pending_qty = flt( - flt(job_card.get_qty_to_produce()) + flt(job_card.for_quantity) + - flt(job_card.pending_qty) - flt(job_card.manufactured_qty) - flt(job_card.get_consumed_process_loss()), precision, diff --git a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py index 74996b96a22..39e77f0929d 100644 --- a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py +++ b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py @@ -126,6 +126,7 @@ class ManufactureEntry: available_serial_batches = self.get_transferred_serial_batches() production_share = self.get_production_share() + items_to_remove = [] for item_code, _dict in item_dict.items(): _dict.from_warehouse = self.source_wh.get(item_code) or self.wip_warehouse _dict.to_warehouse = "" @@ -143,11 +144,15 @@ class ManufactureEntry: remaining_qty = max(flt(_dict.qty) - flt(_dict.consumed_qty), 0) _dict.qty = min(flt(_dict.qty) * production_share, remaining_qty) if not _dict.qty: + items_to_remove.append(item_code) continue if self.skip_material_transfer: set_previous_operation_serial_batch(self.stock_entry, _dict) + for item_code in items_to_remove: + item_dict.pop(item_code) + self.stock_entry.add_to_stock_entry_detail(item_dict) def get_production_share(self): From beed05ac1844c0d775891ded7988b1e87287702d Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 9 Aug 2026 21:08:57 +0530 Subject: [PATCH 085/134] test(manufacturing): isolate quantity split validation --- erpnext/manufacturing/doctype/job_card/test_job_card.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 95e9e8dbfb6..aa9b1e1e651 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -1821,13 +1821,13 @@ class TestJobCard(ERPNextTestSuite): jc = frappe.new_doc("Job Card") jc.for_quantity = 5 - jc.validate_complete_job_card_qty( + jc.validate_completion_qty_split( frappe._dict(for_quantity=5, qty=3, pending_qty=2, process_loss_qty=0) ) self.assertRaises( frappe.ValidationError, - jc.validate_complete_job_card_qty, + jc.validate_completion_qty_split, frappe._dict(for_quantity=3, qty=3, pending_qty=2, process_loss_qty=0), ) From 87b456faa270df150eca7be9a729b844bfc4b7c9 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 9 Aug 2026 21:10:08 +0530 Subject: [PATCH 086/134] fix(manufacturing): type whitelisted BOM arguments --- erpnext/manufacturing/doctype/bom/bom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 00e754ff561..a706f5daa13 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -854,7 +854,7 @@ class BOM(WebsiteGenerator): self.add_materials_from_bom(row.finished_good, row.bom_no, row.idx, qty=row.finished_good_qty) @frappe.whitelist() - def add_raw_materials(self, operation_row_id, items): + def add_raw_materials(self, operation_row_id: str, items: str | list[dict]) -> None: if isinstance(items, str): items = parse_json(items) From a8060d5e996e1698ac6bdb2e872a70b079ef67aa Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 9 Aug 2026 21:22:17 +0530 Subject: [PATCH 087/134] fix(manufacturing): adapt version 16 compatibility --- erpnext/manufacturing/doctype/bom/bom.py | 2 +- erpnext/stock/doctype/stock_entry/stock_entry.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index a706f5daa13..06f7ff894d6 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -854,7 +854,7 @@ class BOM(WebsiteGenerator): self.add_materials_from_bom(row.finished_good, row.bom_no, row.idx, qty=row.finished_good_qty) @frappe.whitelist() - def add_raw_materials(self, operation_row_id: str, items: str | list[dict]) -> None: + def add_raw_materials(self, operation_row_id: str | int, items: str | list[dict]) -> None: if isinstance(items, str): items = parse_json(items) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 0c6c3bbee9b..cd442909b1c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -3238,7 +3238,8 @@ class StockEntry(StockController, SubcontractingInwardController): entry_qty = flt(finished_qty + flt(self.process_loss_qty), precision) if entry_qty > pending_qty: - uom = job_card.stock_uom + item_code = job_card.finished_good or job_card.production_item + uom = frappe.get_cached_value("Item", item_code, "stock_uom") frappe.throw( _( "The Job Card {0} has only {1} left to produce, but this entry books {2} ({3} finished goods and {4} process loss). Cancel or update its other manufacture entries first." From eb90186d3f58d9adb7dba66ff3fbdc5e282458fe Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 1 Aug 2026 18:34:31 +0530 Subject: [PATCH 088/134] feat(job_card): print quantities with their stock uom (#57689) * feat(job_card): carry the stock uom on the job card Every quantity the job card reports belongs to the item it produces, but the document had no unit of its own, so messages could only print bare numbers. Add the Stock UOM field, set from the finished good or the final product, and backfill the job cards that already exist. * fix(job_card): print quantities with their unit A bare 5 in an error says nothing about what was counted. Every message that reports a quantity now names its unit, taking it from the job card's stock uom, from the previous operation's finished good when the message compares two operations, and from the item itself for a raw material transfer. The completion dialogs read the same unit off the job card. * refactor(job_card): move the stock uom next to the qty it measures * fix(job_card): keep the stock uom backfill atomic Drop the auto commit toggle so the backfill is a single transaction with no connection flag left behind when it raises, and select the job cards to fill with an explicit unset filter instead of a value list. (cherry picked from commit 07ac4d83ef097c83e18b246dac5196d6c7b0656b) # Conflicts: # erpnext/manufacturing/doctype/job_card/job_card.js # erpnext/manufacturing/doctype/job_card/job_card.json # erpnext/manufacturing/doctype/job_card/job_card.py # erpnext/manufacturing/doctype/job_card/test_job_card.py # erpnext/manufacturing/page/shop_floor/shop_floor.py # erpnext/patches.txt # erpnext/public/js/shop_floor/shop_floor.js --- .../doctype/job_card/job_card.js | 66 +- .../doctype/job_card/job_card.json | 14 +- .../doctype/job_card/job_card.py | 255 +++ .../doctype/job_card/test_job_card.py | 177 ++ .../page/shop_floor/shop_floor.py | 834 ++++++++ erpnext/patches.txt | 4 + .../v16_0/set_stock_uom_in_job_card.py | 36 + erpnext/public/js/shop_floor/shop_floor.js | 1758 +++++++++++++++++ 8 files changed, 3142 insertions(+), 2 deletions(-) create mode 100644 erpnext/manufacturing/page/shop_floor/shop_floor.py create mode 100644 erpnext/patches/v16_0/set_stock_uom_in_job_card.py create mode 100644 erpnext/public/js/shop_floor/shop_floor.js diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 0a4026672cf..a4e4d58d253 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -67,7 +67,11 @@ frappe.ui.form.on("Job Card", { 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])); + frappe.throw( + __("Pending Quantity cannot be greater than {0}", [ + get_qty_with_uom(remaining_qty, frm.doc.stock_uom), + ]) + ); } const process_loss_qty = flt(remaining_qty) - flt(frm.doc.pending_qty); @@ -261,8 +265,28 @@ frappe.ui.form.on("Job Card", { default: pending_qty, change() { const dialog = frm.job_completion_dialog; +<<<<<<< HEAD const remaining = dialog.get_value("for_quantity") - dialog.get_value("completed_qty"); if (remaining > 0 && remaining != dialog.get_value("pending_qty")) { +======= + const remaining = + dialog.get_value("for_quantity") - + dialog.get_value("completed_qty") - + dialog.get_value("process_loss_qty"); + + if (remaining < 0) { + const max_completed_qty = + flt(dialog.get_value("for_quantity")) - flt(dialog.get_value("process_loss_qty")); + dialog.set_value("completed_qty", max_completed_qty); + frappe.throw( + __("Completed Quantity cannot be greater than {0}", [ + get_qty_with_uom(max_completed_qty, frm.doc.stock_uom), + ]) + ); + } + + if (remaining != dialog.get_value("pending_qty")) { +>>>>>>> 07ac4d83ef (feat(job_card): print quantities with their stock uom (#57689)) dialog.set_value("pending_qty", remaining); } }, @@ -278,7 +302,25 @@ frappe.ui.form.on("Job Card", { dialog.get_value("for_quantity") - dialog.get_value("completed_qty") - dialog.get_value("pending_qty"); +<<<<<<< HEAD if (process_loss_qty >= 0 && process_loss_qty != dialog.get_value("process_loss_qty")) { +======= + + if (process_loss_qty < 0) { + dialog.set_value("pending_qty", 0); + frappe.throw( + __("Pending Quantity cannot be greater than {0}", [ + get_qty_with_uom( + flt(dialog.get_value("for_quantity")) - + flt(dialog.get_value("completed_qty")), + frm.doc.stock_uom + ), + ]) + ); + } + + if (process_loss_qty != dialog.get_value("process_loss_qty")) { +>>>>>>> 07ac4d83ef (feat(job_card): print quantities with their stock uom (#57689)) dialog.set_value("process_loss_qty", process_loss_qty); } }, @@ -293,7 +335,25 @@ frappe.ui.form.on("Job Card", { dialog.get_value("for_quantity") - dialog.get_value("completed_qty") - dialog.get_value("process_loss_qty"); +<<<<<<< HEAD if (remaining >= 0 && remaining != dialog.get_value("pending_qty")) { +======= + + if (remaining < 0) { + dialog.set_value("process_loss_qty", 0); + frappe.throw( + __("Process Loss Quantity cannot be greater than {0}", [ + get_qty_with_uom( + flt(dialog.get_value("for_quantity")) - + flt(dialog.get_value("completed_qty")), + frm.doc.stock_uom + ), + ]) + ); + } + + if (remaining != dialog.get_value("pending_qty")) { +>>>>>>> 07ac4d83ef (feat(job_card): print quantities with their stock uom (#57689)) dialog.set_value("pending_qty", remaining); } }, @@ -886,3 +946,7 @@ function get_last_completed_row(time_logs) { function get_last_row(time_logs) { return time_logs[time_logs.length - 1] || {}; } + +function get_qty_with_uom(qty, stock_uom) { + return stock_uom ? `${flt(qty)} ${stock_uom}` : flt(qty); +} diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index c215aee42d4..a6fb3790d89 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -13,10 +13,11 @@ "work_order", "column_break_uqjq", "production_item", + "bom_no", "column_break_qrpg", "for_quantity", "column_break_yecz", - "bom_no", + "stock_uom", "section_break_oisd", "company", "naming_series", @@ -164,6 +165,13 @@ "in_preview": 1, "label": "Qty To Manufacture" }, + { + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM", + "read_only": 1 + }, { "fieldname": "wip_warehouse", "fieldtype": "Link", @@ -695,7 +703,11 @@ "grid_page_length": 50, "is_submittable": 1, "links": [], +<<<<<<< HEAD "modified": "2026-06-19 17:39:42.293242", +======= + "modified": "2026-08-01 14:22:19.926911", +>>>>>>> 07ac4d83ef (feat(job_card): print quantities with their stock uom (#57689)) "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 3b4f8008f08..db7cab39c44 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -130,6 +130,7 @@ class JobCard(Document): "Cancelled", "Completed", ] + stock_uom: DF.Link | None sub_operations: DF.Table[JobCardOperation] target_warehouse: DF.Link | None time_logs: DF.Table[JobCardTimeLog] @@ -158,6 +159,7 @@ class JobCard(Document): def before_validate(self): self.set_wip_warehouse() + self.set_stock_uom() def validate(self): self.validate_time_logs() @@ -906,11 +908,21 @@ class JobCard(Document): qty_to_manufacture = bold(_("Qty to Manufacture")) frappe.throw( +<<<<<<< HEAD _("The {0} ({1}) must be equal to {2} ({3})").format( total_completed_qty_label, bold(flt(total_completed_qty, precision)), qty_to_manufacture, bold(self.for_quantity), +======= + _( + "Total Completed Qty ({0}), Process Loss Qty ({1}) and Pending Qty ({2}) must add up to the Qty to Manufacture ({3})." + ).format( + bold(self.get_qty_with_uom(self.total_completed_qty)), + bold(self.get_qty_with_uom(self.process_loss_qty)), + bold(self.get_qty_with_uom(self.pending_qty)), + bold(self.get_qty_with_uom(self.for_quantity)), +>>>>>>> 07ac4d83ef (feat(job_card): print quantities with their stock uom (#57689)) ) ) @@ -1184,6 +1196,51 @@ class JobCard(Document): self.set_status(update_status=True) +<<<<<<< HEAD +======= + def get_job_card_items_transferred_qty(self, ste_doc): + from frappe.query_builder.functions import Sum + + job_card_items = [x.get("job_card_item") for x in ste_doc.get("items") if x.get("job_card_item")] + if not job_card_items: + return {} + + se = frappe.qb.DocType("Stock Entry") + sed = frappe.qb.DocType("Stock Entry Detail") + + query = ( + frappe.qb.from_(sed) + .join(se) + .on(sed.parent == se.name) + .select(sed.job_card_item, Sum(sed.qty)) + .where( + (sed.job_card_item.isin(job_card_items)) + & (se.docstatus == 1) + & (se.purpose == "Material Transfer for Manufacture") + ) + .groupby(sed.job_card_item) + ) + + return frappe._dict(query.run(as_list=True)) + + def validate_over_transfer(self, ste_doc, row, transferred_qty): + "Block over transfer of items if not allowed in settings." + required_qty = frappe.db.get_value("Job Card Item", row.job_card_item, "required_qty") + if flt(transferred_qty) > flt(required_qty): + frappe.throw( + _( + "Row #{0}: Cannot transfer more than Required Qty {1} for Item {2} against Job Card {3}" + ).format( + row.idx, + frappe.bold(self.get_qty_with_uom(required_qty, row.item_code)), + frappe.bold(row.item_code), + ste_doc.job_card, + ), + title=_("Excess Transfer"), + exc=JobCardOverTransferError, + ) + +>>>>>>> 07ac4d83ef (feat(job_card): print quantities with their stock uom (#57689)) def set_transferred_qty(self, update_status=False): from frappe.query_builder.functions import Sum @@ -1280,10 +1337,66 @@ class JobCard(Document): if self.workstation: self.update_workstation_status() +<<<<<<< HEAD +======= + def get_qty_to_produce(self): + """Qty this job card is expected to produce, the pending qty is left to another job card.""" + return flt(self.for_quantity) - flt(self.pending_qty) + + def get_qty_with_uom(self, qty, item_code=None): + """A quantity in a message reads as a count of nothing without the unit it is measured in.""" + uom = self.stock_uom + if item_code: + uom = frappe.get_cached_value("Item", item_code, "stock_uom") + + return f"{flt(qty, self.precision('total_completed_qty'))} {uom or ''}".strip() + + def set_finished_good_status(self): + # Only reached for a submitted job card (docstatus == 1) with a finished good, see set_status(). + qty_to_produce = self.get_qty_to_produce() + + if (self.manufactured_qty + self.process_loss_qty) >= qty_to_produce: + self.status = "Completed" + elif (self.total_completed_qty + self.process_loss_qty) >= qty_to_produce: + # Production is done and the card is submitted, but the finished goods have not been + # booked into stock yet (Manufacture Stock Entry pending) — distinct from active WIP. + self.status = "To Manufacture" + elif self.transferred_qty > 0 or self.skip_material_transfer: + self.status = "Work In Progress" + + def set_non_semi_fg_status(self): + if self.items: + item_data = frappe.get_all( + "Job Card Item", + filters={"parent": self.name}, + fields=["transferred_qty", "required_qty"], + ) + all_transferred = item_data and all( + flt(d.transferred_qty) >= flt(d.required_qty) for d in item_data + ) + any_transferred = any(flt(d.transferred_qty) > 0 for d in item_data) + + if all_transferred: + self.status = "Material Transferred" + elif any_transferred: + self.status = "Partially Transferred" + elif flt(self.for_quantity) <= flt(self.transferred_qty): + self.status = "Material Transferred" + + if self.time_logs: + self.status = "Work In Progress" + + if self.docstatus == 1 and ( + self.get_qty_to_produce() <= (self.total_completed_qty + self.process_loss_qty) or not self.items + ): + self.status = "Completed" + +>>>>>>> 07ac4d83ef (feat(job_card): print quantities with their stock uom (#57689)) def set_wip_warehouse(self): if not self.wip_warehouse: self.wip_warehouse = frappe.get_cached_value("Company", self.company, "default_wip_warehouse") +<<<<<<< HEAD def validate_operation_id(self): if ( self.get("operation_id") @@ -1294,6 +1407,36 @@ class JobCard(Document): != self.operation_id ): work_order = bold(get_link_to_form("Work Order", self.work_order)) +======= + def set_stock_uom(self): + item_code = self.finished_good or self.production_item + if item_code: + self.stock_uom = frappe.get_cached_value("Item", item_code, "stock_uom") + + def set_operation_id(self): + if not (self.work_order and self.operation): + return + + if self.operation_id and self.docstatus != 0: + return + + operation_rows = frappe.get_all( + "Work Order Operation", + filters={"parent": self.work_order, "operation": self.operation}, + pluck="name", + ) + + if self.operation_id: + if operation_rows and self.operation_id not in operation_rows: + frappe.throw( + _("Operation {0} does not belong to the work order {1}").format( + bold(self.operation), get_link_to_form("Work Order", self.work_order) + ) + ) + elif len(operation_rows) == 1: + self.operation_id = operation_rows[0] + elif operation_rows and self.docstatus == 0: +>>>>>>> 07ac4d83ef (feat(job_card): print quantities with their stock uom (#57689)) frappe.throw( _("Operation {0} does not belong to the work order {1}").format( bold(self.operation), work_order @@ -1339,6 +1482,53 @@ class JobCard(Document): if not (self.work_order and self.sequence_id): return +<<<<<<< HEAD +======= + current_operation_qty = self.get_current_operation_completed_qty() + + for row in self.get_previous_operations(): + if self.track_semi_finished_goods: + self.validate_previous_operation_manufactured_qty(row, current_operation_qty) + else: + self.validate_previous_operation(row, current_operation_qty) + + def get_previous_operations(self): + previous_operations = frappe.get_all( + "Work Order Operation", + fields=["name", "operation", "status", "completed_qty", "sequence_id", "finished_good"], + filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ("<", self.sequence_id)}, + order_by="sequence_id, idx", + ) + + if self.track_semi_finished_goods and previous_operations: + manufactured_qty = self.get_manufactured_qty_per_operation( + [row.name for row in previous_operations] + ) + + for row in previous_operations: + row.manufactured_qty = flt(manufactured_qty.get(row.name)) + + return previous_operations + + def get_manufactured_qty_per_operation(self, operation_ids): + job_card = frappe.qb.DocType("Job Card") + + data = ( + frappe.qb.from_(job_card) + .select(job_card.operation_id, Sum(job_card.manufactured_qty)) + .where( + (job_card.work_order == self.work_order) + & (job_card.docstatus == 1) + & (IfNull(job_card.is_corrective_job_card, 0) == 0) + & (job_card.operation_id.isin(operation_ids)) + ) + .groupby(job_card.operation_id) + ).run() + + return dict(data) + + def get_current_operation_completed_qty(self): +>>>>>>> 07ac4d83ef (feat(job_card): print quantities with their stock uom (#57689)) current_operation_qty = 0.0 data = self.get_current_operation_data() if data and len(data) > 0: @@ -1353,6 +1543,7 @@ class JobCard(Document): order_by="sequence_id, idx", ) +<<<<<<< HEAD message = "Job Card {}: As per the sequence of the operations in the work order {}".format( bold(self.name), bold(get_link_to_form("Work Order", self.work_order)) ) @@ -1364,6 +1555,17 @@ class JobCard(Document): message, bold(row.operation), bold(self.operation) ), OperationSequenceError, +======= + if row.completed_qty < current_operation_qty: + frappe.throw( + _( + "The completed quantity {0} of an operation {1} cannot be greater than the completed quantity {2} of a previous operation {3}." + ).format( + bold(self.get_qty_with_uom(current_operation_qty)), + bold(self.operation), + bold(self.get_qty_with_uom(row.completed_qty, row.finished_good)), + bold(row.operation), +>>>>>>> 07ac4d83ef (feat(job_card): print quantities with their stock uom (#57689)) ) if row.status != "Completed" and row.completed_qty < current_operation_qty: @@ -1374,6 +1576,7 @@ class JobCard(Document): OperationSequenceError, ) +<<<<<<< HEAD if row.completed_qty < current_operation_qty: frappe.throw( _( @@ -1385,6 +1588,33 @@ class JobCard(Document): bold(row.operation), ) ) +======= + if not manufactured_qty: + frappe.throw( + _( + "Job Card {0}: As per the sequence of the operations in the work order {1}, submit the manufacturing entry for the operation {2} before the operation {3}." + ).format( + bold(self.name), + bold(get_link_to_form("Work Order", self.work_order)), + bold(row.operation), + bold(self.operation), + ), + OperationSequenceError, + ) + + if manufactured_qty < current_operation_qty: + frappe.throw( + _( + "The completed quantity {0} of an operation {1} cannot be greater than the manufactured quantity {2} of a previous operation {3}. Submit the manufacturing entry for the operation {3} first." + ).format( + bold(self.get_qty_with_uom(current_operation_qty)), + bold(self.operation), + bold(self.get_qty_with_uom(manufactured_qty, row.finished_good)), + bold(row.operation), + ), + OperationSequenceError, + ) +>>>>>>> 07ac4d83ef (feat(job_card): print quantities with their stock uom (#57689)) def validate_work_order(self): if self.is_work_order_closed(): @@ -1536,6 +1766,31 @@ class JobCard(Document): self.pending_qty = flt(kwargs.pending_qty) self.process_loss_qty = flt(kwargs.process_loss_qty) +<<<<<<< HEAD +======= + def validate_completion_qty_split(self, kwargs): + if not flt(kwargs.for_quantity): + return + + precision = self.precision("total_completed_qty") + accounted_qty = flt(kwargs.qty) + flt(kwargs.pending_qty) + flt(kwargs.process_loss_qty) + + if flt(accounted_qty, precision) == flt(kwargs.for_quantity, precision): + return + + frappe.throw( + _( + "Completed Quantity ({0}), Pending Quantity ({1}) and Process Loss Quantity ({2}) must add up to the Qty to Manufacture ({3})." + ).format( + bold(self.get_qty_with_uom(kwargs.qty)), + bold(self.get_qty_with_uom(kwargs.pending_qty)), + bold(self.get_qty_with_uom(kwargs.process_loss_qty)), + bold(self.get_qty_with_uom(kwargs.for_quantity)), + ) + ) + + def add_completion_time_logs(self, kwargs): +>>>>>>> 07ac4d83ef (feat(job_card): print quantities with their stock uom (#57689)) if kwargs.end_time: self.add_time_logs( to_time=kwargs.end_time, diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 826d558e830..f07ae2ea2b1 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -888,6 +888,85 @@ class TestJobCard(ERPNextTestSuite): self.assertEqual(wo_doc.process_loss_qty, 2) self.assertEqual(wo_doc.status, "Completed") +<<<<<<< HEAD +======= + def get_first_job_card(self, work_order): + return frappe.get_doc( + "Job Card", + frappe.get_all( + "Job Card", + filters={"work_order": work_order}, + order_by="sequence_id, creation", + limit=1, + pluck="name", + )[0], + ) + + def test_stock_uom_is_set_from_the_produced_item(self): + work_order = make_wo_order_test_record(item="_Test FG Item 2", qty=5) + + job_card = self.get_first_job_card(work_order.name) + item_code = job_card.finished_good or job_card.production_item + + self.assertEqual(job_card.stock_uom, frappe.db.get_value("Item", item_code, "stock_uom")) + + def test_completion_qty_reduces_for_quantity_without_process_loss(self): + work_order = make_wo_order_test_record(item="_Test FG Item 2", qty=5) + + job_card = self.get_first_job_card(work_order.name) + job_card.append("time_logs", {"from_time": "2024-03-01 08:00:00"}) + job_card.save() + + job_card.complete_job_card( + qty=3, + for_quantity=3, + pending_qty=0, + process_loss_qty=0, + end_time="2024-03-01 09:00:00", + ) + + job_card.reload() + self.assertEqual(flt(job_card.for_quantity), 3) + self.assertEqual(flt(job_card.total_completed_qty), 3) + self.assertEqual(flt(job_card.process_loss_qty), 0) + + def test_completion_qty_keeps_for_quantity_across_cycles(self): + work_order = make_wo_order_test_record(item="_Test FG Item 2", qty=5) + + job_card = self.get_first_job_card(work_order.name) + job_card.append("time_logs", {"from_time": "2024-03-02 08:00:00"}) + job_card.save() + + job_card.complete_job_card( + qty=3, + for_quantity=5, + pending_qty=2, + process_loss_qty=0, + end_time="2024-03-02 09:00:00", + ) + + job_card.reload() + self.assertEqual(flt(job_card.for_quantity), 5) + self.assertEqual(flt(job_card.pending_qty), 2) + self.assertEqual(flt(job_card.process_loss_qty), 0) + + job_card.append("time_logs", {"from_time": "2024-03-02 10:00:00"}) + job_card.save() + + job_card.complete_job_card( + qty=2, + for_quantity=2, + pending_qty=0, + process_loss_qty=0, + end_time="2024-03-02 11:00:00", + ) + + job_card.reload() + self.assertEqual(flt(job_card.for_quantity), 5) + self.assertEqual(flt(job_card.total_completed_qty), 5) + self.assertEqual(flt(job_card.process_loss_qty), 0) + +>>>>>>> 07ac4d83ef (feat(job_card): print quantities with their stock uom (#57689)) def test_op_cost_calculation(self): from erpnext.manufacturing.doctype.routing.test_routing import ( create_routing, @@ -1879,3 +1958,101 @@ def create_semi_fg_bom(semi_fg_item, raw_item, inspection_required): bom.append("items", {"item_code": raw_item, "qty": 1}) bom.submit() return bom.name +<<<<<<< HEAD +======= + + +class TestJobCardLogic(ERPNextTestSuite): + """Field-level validations and pure quantity/capacity helpers, exercised on the + document directly so they don't need a Work Order / BOM (the integration suite does).""" + + def test_processing_a_submitted_or_cancelled_card_is_blocked(self): + submitted = frappe.new_doc("Job Card") + submitted.docstatus = 1 + self.assertRaises(frappe.ValidationError, submitted.validate_docstatus) + + cancelled = frappe.new_doc("Job Card") + cancelled.docstatus = 2 + self.assertRaises(frappe.ValidationError, cancelled.validate_docstatus) + + def test_complete_job_card_qty_guards(self): + jc = frappe.new_doc("Job Card") + jc.for_quantity = 5 + jc.validate_complete_job_card_qty(frappe._dict(pending_qty=3)) # within range -> passes + self.assertRaises( + frappe.ValidationError, jc.validate_complete_job_card_qty, frappe._dict(pending_qty=-1) + ) + self.assertRaises( + frappe.ValidationError, jc.validate_complete_job_card_qty, frappe._dict(process_loss_qty=-1) + ) + self.assertRaises( + frappe.ValidationError, jc.validate_complete_job_card_qty, frappe._dict(pending_qty=10) + ) + + def test_qty_in_messages_carries_the_uom(self): + jc = frappe.new_doc("Job Card") + jc.stock_uom = "Nos" + + self.assertEqual(jc.get_qty_with_uom(5), "5.0 Nos") + self.assertEqual(jc.get_qty_with_uom(0), "0.0 Nos") + + def test_completion_qty_split_must_add_up(self): + jc = frappe.new_doc("Job Card") + jc.for_quantity = 5 + + # 3 completed + 2 pending + 0 lost == 5 to manufacture -> passes + jc.validate_complete_job_card_qty( + frappe._dict(for_quantity=5, qty=3, pending_qty=2, process_loss_qty=0) + ) + + self.assertRaises( + frappe.ValidationError, + jc.validate_complete_job_card_qty, + frappe._dict(for_quantity=3, qty=3, pending_qty=2, process_loss_qty=0), + ) + + def test_completed_qty_must_reconcile_with_for_quantity(self): + jc = frappe.new_doc("Job Card") + jc.for_quantity = 10 + jc.total_completed_qty = 6 + jc.process_loss_qty = 0 + jc.pending_qty = 0 + # 6 + 0 + 0 != 10 -> throws + self.assertRaises(frappe.ValidationError, jc.validate_completed_qty_matches_for_quantity) + # completed + loss + pending == for_quantity -> passes + jc.pending_qty = 4 + jc.validate_completed_qty_matches_for_quantity() + + def test_set_process_loss(self): + jc = frappe.new_doc("Job Card") + jc.for_quantity = 10 + jc.total_completed_qty = 6 + jc.pending_qty = 1 + jc.set_process_loss() + self.assertEqual(jc.process_loss_qty, 3) # 10 - 6 - 1 + + # no loss when nothing completed yet + nothing_done = frappe.new_doc("Job Card") + nothing_done.for_quantity = 10 + nothing_done.total_completed_qty = 0 + nothing_done.set_process_loss() + self.assertEqual(nothing_done.process_loss_qty, 0) + + def test_capacity_overlap_detection(self): + jc = frappe.new_doc("Job Card") + sequential = [ + {"from_time": "2026-01-01 10:00:00", "to_time": "2026-01-01 11:00:00"}, + {"from_time": "2026-01-01 11:00:00", "to_time": "2026-01-01 12:00:00"}, + ] + overlapping = [ + {"from_time": "2026-01-01 10:00:00", "to_time": "2026-01-01 11:00:00"}, + {"from_time": "2026-01-01 10:30:00", "to_time": "2026-01-01 11:30:00"}, + ] + # sequential logs share one capacity slot; overlapping logs need two + self.assertEqual(len(jc.get_alloted_capacity(sequential)), 1) + self.assertEqual(len(jc.get_alloted_capacity(overlapping)), 2) + # capacity 1 overlaps with any log; capacity 2 only when both slots are taken + self.assertTrue(jc.has_overlap(1, sequential)) + self.assertFalse(jc.has_overlap(2, sequential)) + self.assertTrue(jc.has_overlap(2, overlapping)) +>>>>>>> 07ac4d83ef (feat(job_card): print quantities with their stock uom (#57689)) diff --git a/erpnext/manufacturing/page/shop_floor/shop_floor.py b/erpnext/manufacturing/page/shop_floor/shop_floor.py new file mode 100644 index 00000000000..0ef5b99613e --- /dev/null +++ b/erpnext/manufacturing/page/shop_floor/shop_floor.py @@ -0,0 +1,834 @@ +import frappe +from frappe import _ +from frappe.query_builder import Order +from frappe.query_builder.functions import Count, Date +from frappe.utils import cint, flt, get_datetime, getdate, now_datetime, time_diff_in_seconds +from pypika.terms import ExistsCriterion + +from erpnext.manufacturing.doctype.workstation.workstation import ( + get_status_color, + get_time_logs, +) +from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template import ( + get_template_details, +) + +JOB_CARD_FIELDS = [ + "name", + "docstatus", + "production_item", + "work_order", + "operation", + "total_completed_qty", + "for_quantity", + "process_loss_qty", + "stock_uom", + "finished_good", + "transferred_qty", + "status", + "expected_start_date", + "expected_end_date", + "time_required", + "wip_warehouse", + "skip_material_transfer", + "backflush_from_wip_warehouse", + "is_paused", + "manufactured_qty", + "is_subcontracted", + "workstation", + "sequence_id", + "bom_no", + "operation_id", + "quality_inspection", + "quality_inspection_template", +] + +TODAY_SESSION_FIELDS = [ + "name", + "docstatus", + "production_item", + "finished_good", + "operation", + "total_completed_qty", + "for_quantity", + "process_loss_qty", + "total_time_in_mins", + "status", + "modified", +] + +# Roles that unlock the Shop Floor manager board (work-order overview). Anyone else gets the +# operator view. System Manager is included so admins always see the full picture. +MANAGER_ROLES = {"Shop Floor Manager", "Manufacturing Manager", "System Manager"} + +# Maps the manager buckets to the underlying Work Order statuses. "open" spans pending AND +# in-progress: starting a job card flips the Work Order to In Process, and separate tabs made +# it jump tabs on the next refresh — the operator would lose the card they were working on. +WORK_ORDER_STATUS_GROUPS = { + "open": ["In Process", "Not Started", "Submitted", "Stock Reserved", "Stock Partially Reserved"], + "completed": ["Completed"], +} + +WORK_ORDER_FIELDS = [ + "name", + "production_item", + "item_name", + "qty", + "produced_qty", + "status", + "planned_start_date", + "sales_order", + "bom_no", +] + + +@frappe.whitelist() +def submit_job_card(job_card: str): + """Submit a draft job card whose quantity has already been recorded via End Session.""" + frappe.has_permission("Job Card", "submit", throw=True) + jc = frappe.get_doc("Job Card", job_card) + if jc.docstatus == 0: + jc.submit() + return {"name": jc.name, "docstatus": jc.docstatus} + + +def _record_session(job_card, qty, for_quantity, pending_qty, process_loss_qty, end_time): + """Record the session qty + close the active time log via Job Card's complete_job_card. + + auto_submit=0 so the backend doesn't auto-create+submit a Manufacture Stock Entry — that's + a separate manual step driven by the post-submit prompt. Returns the reloaded doc. + """ + frappe.has_permission("Job Card", "write", throw=True) + + doc = frappe.get_doc("Job Card", job_card) + doc.run_method( + "complete_job_card", + qty=flt(qty), + for_quantity=flt(for_quantity), + pending_qty=flt(pending_qty), + process_loss_qty=flt(process_loss_qty), + end_time=end_time, + auto_submit=0, + ) + doc.reload() + return doc + + +@frappe.whitelist() +def save_and_continue( + job_card: str, + qty: float, + for_quantity: float, + pending_qty: float, + process_loss_qty: float, + end_time: str, +): + """Record the session qty + close the time log, then mark the JC as paused + so the MES keeps it in the active slot for the next session.""" + frappe.has_permission("Job Card", "write", throw=True) + doc = _record_session(job_card, qty, for_quantity, pending_qty, process_loss_qty, end_time) + if doc.docstatus == 0: + doc.db_set("is_paused", 1) + return {"name": doc.name} + + +@frappe.whitelist() +def complete_and_submit( + job_card: str, + qty: float, + for_quantity: float, + pending_qty: float, + process_loss_qty: float, + end_time: str, +): + """Record the session qty + close the time log + submit the JC.""" + frappe.has_permission("Job Card", "submit", throw=True) + doc = _record_session(job_card, qty, for_quantity, pending_qty, process_loss_qty, end_time) + if doc.docstatus == 0: + doc.submit() + return {"name": doc.name, "finished_good": doc.finished_good} + + +@frappe.whitelist() +def make_manufacture_stock_entry(job_card: str): + """Build a "Manufacture" Stock Entry for the finished goods and save as draft. + + Mirrors the Job Card form's "Make Stock Entry" button — uses the doc's own + make_stock_entry_for_semi_fg_item (purpose="Manufacture", job card linked) rather than + the generic make_stock_entry, which would produce a "Material Transfer for Manufacture". + Returns the draft SE name so the client can open it in a new tab. + """ + frappe.has_permission("Job Card", "read", throw=True) + frappe.has_permission("Stock Entry", "submit", throw=True) + doc = frappe.get_doc("Job Card", job_card) + se = doc.make_stock_entry_for_semi_fg_item(auto_submit=False) + return {"name": se.get("name")} + + +@frappe.whitelist() +def get_quality_inspection_checklist(job_card: str): + """Template parameters for the inline quality check an operator fills before submitting a + job card. Returns the resolved template, its parameter rows, any already-linked inspection, + and the item being inspected. + """ + frappe.has_permission("Job Card", "read", throw=True) + + jc = frappe.db.get_value( + "Job Card", + job_card, + [ + "quality_inspection", + "quality_inspection_template", + "operation", + "production_item", + "finished_good", + ], + as_dict=True, + ) + if not jc: + frappe.throw(_("Job Card {0} not found").format(job_card)) + + template = jc.quality_inspection_template + if not template and jc.operation: + template = frappe.get_cached_value("Operation", jc.operation, "quality_inspection_template") + + parameters = [] + for p in get_template_details(template): + parameters.append( + { + "specification": p.specification, + "value": p.value, + "numeric": cint(p.numeric), + "min_value": p.min_value, + "max_value": p.max_value, + "formula_based_criteria": cint(p.formula_based_criteria), + "acceptance_formula": p.acceptance_formula, + } + ) + + return { + "template": template, + "parameters": parameters, + "existing": jc.quality_inspection, + "item_code": jc.finished_good or jc.production_item, + } + + +@frappe.whitelist() +def submit_quality_inspection(job_card: str, readings: str | None = None): + """Create + submit an In-Process Quality Inspection for the job card and link it back, so the + standard Job Card.validate_inspection() gate passes when the card is submitted. + + `readings` is a JSON list of {specification, status, reading_value} captured inline. For numeric + / formula parameters the measured value is stored and the Quality Inspection auto-evaluates + pass/fail against min/max (or the formula); for qualitative parameters the operator's explicit + Accepted/Rejected is taken as authoritative (manual_inspection). The QI's own validation then + sets the overall Accepted/Rejected status. + """ + frappe.has_permission("Job Card", "write", throw=True) + frappe.has_permission("Quality Inspection", "submit", throw=True) + + jc = frappe.get_doc("Job Card", job_card) + + # Idempotent: if a submitted inspection is already linked, don't create another. + if jc.quality_inspection: + existing = frappe.db.get_value( + "Quality Inspection", jc.quality_inspection, ["status", "docstatus"], as_dict=True + ) + if existing and existing.docstatus == 1: + return {"name": jc.quality_inspection, "status": existing.status} + + template = jc.quality_inspection_template + if not template and jc.operation: + template = frappe.get_cached_value("Operation", jc.operation, "quality_inspection_template") + if not template: + frappe.throw(_("No Quality Inspection Template is configured for this operation.")) + + reading_map = {r.get("specification"): r for r in (frappe.parse_json(readings) or [])} + + qi = frappe.new_doc("Quality Inspection") + qi.inspection_type = "In Process" + qi.reference_type = "Job Card" + qi.reference_name = job_card + qi.item_code = jc.finished_good or jc.production_item + qi.bom_no = jc.bom_no + qi.quality_inspection_template = template + qi.inspected_by = frappe.session.user + qi.get_item_specification_details() # load readings from the template + + for reading in qi.readings: + entry = reading_map.get(reading.specification) + if not entry: + continue + value = entry.get("reading_value") + if reading.numeric or reading.formula_based_criteria: + # Measured value → let the Quality Inspection judge it against min/max or the formula. + if value not in (None, ""): + reading.reading_value = value + reading.reading_1 = value + else: + # Qualitative check → the operator's explicit pass/fail wins. + reading.manual_inspection = 1 + reading.status = entry.get("status") or "Accepted" + if value not in (None, ""): + reading.reading_value = value + + qi.insert() + qi.submit() # validate() → inspect_and_set_status() sets the overall Accepted/Rejected status + + # Link explicitly: the QI's own back-reference matches on production_item, which can differ + # from the operation's finished_good, so we set it directly to be safe. + jc.db_set("quality_inspection", qi.name) + return {"name": qi.name, "status": qi.status} + + +@frappe.whitelist() +def get_shop_floor_context(): + """Which experience to render — the manager board or the operator view — plus the + signed-in operator's Employee (so Start Job can pre-fill them).""" + roles = set(frappe.get_roles()) + can_manage = bool(roles & MANAGER_ROLES) + return { + "role_view": "manager" if can_manage else "operator", + "can_manage": can_manage, + "user_employee": frappe.db.get_value("Employee", {"user_id": frappe.session.user}, "name"), + } + + +@frappe.whitelist() +def get_work_orders( + status_group: str, + start: int = 0, + page_length: int = 20, + search: str | None = None, + with_job_cards_only: bool | int = 0, +): + """Paginated Work Orders for one manager bucket (open / completed), + each decorated with a job-card status breakdown for the card's progress chip. + + When `with_job_cards_only` is set, only Work Orders that have at least one (non-cancelled) + Job Card are returned — the board's opt-in "With job cards only" toggle. + """ + frappe.has_permission("Work Order", "read", throw=True) + + if status_group not in WORK_ORDER_STATUS_GROUPS: + frappe.throw(_("Invalid status group: {0}").format(status_group)) + + start = cint(start) + page_length = cint(page_length) or 20 + with_job_cards_only = cint(with_job_cards_only) + + # Active buckets show the oldest-planned first (work the floor next); completed shows newest first. + order = Order.desc if status_group == "completed" else Order.asc + + wo = frappe.qb.DocType("Work Order") + query = _apply_work_order_filters(frappe.qb.from_(wo), wo, status_group, search, with_job_cards_only) + work_orders = ( + query.select(*[wo[field] for field in WORK_ORDER_FIELDS]) + .orderby(wo.planned_start_date, order=order) + .limit(page_length) + .offset(start) + ).run(as_dict=True) + + total = _count_work_orders(status_group, search, with_job_cards_only) + + _enrich_work_orders(work_orders) + + return { + "work_orders": work_orders, + "total": cint(total), + "start": start, + "page_length": page_length, + } + + +def _has_job_cards_criterion(wo, docstatus): + jc = frappe.qb.DocType("Job Card") + return ExistsCriterion( + frappe.qb.from_(jc).select(jc.name).where((jc.work_order == wo.name) & (jc.docstatus == docstatus)) + ) + + +def _bucket_criterion(wo, status_group): + """The floor is done with a Work Order once every job card is submitted, even though the + Work Order stays "In Process" until the finished goods are received. Such operationally + complete orders belong on the Completed tab — not stranded under Pending / In Progress.""" + open_statuses = WORK_ORDER_STATUS_GROUPS["open"] + all_job_cards_done = _has_job_cards_criterion(wo, 1) & _has_job_cards_criterion(wo, 0).negate() + if status_group == "completed": + return (wo.status == "Completed") | (wo.status.isin(open_statuses) & all_job_cards_done) + return wo.status.isin(open_statuses) & ( + _has_job_cards_criterion(wo, 1).negate() | _has_job_cards_criterion(wo, 0) + ) + + +def _apply_work_order_filters(query, wo, status_group, search, with_job_cards_only): + """Shared WHERE clauses for the board's row + count queries (bucket, search, job-card toggle).""" + query = query.where((wo.docstatus == 1) & _bucket_criterion(wo, status_group)) + if search: + like = f"%{search}%" + query = query.where(wo.name.like(like) | wo.production_item.like(like) | wo.item_name.like(like)) + if with_job_cards_only: + jc = frappe.qb.DocType("Job Card") + wo_with_job_cards = frappe.qb.from_(jc).select(jc.work_order).where(jc.docstatus < 2) + query = query.where(wo.name.isin(wo_with_job_cards)) + return query + + +def _count_work_orders(status_group: str, search: str | None, with_job_cards_only: int = 0) -> int: + """Total Work Orders in a bucket (drives pagination), honouring the same filters as the rows.""" + wo = frappe.qb.DocType("Work Order") + query = _apply_work_order_filters(frappe.qb.from_(wo), wo, status_group, search, with_job_cards_only) + return cint(query.select(Count("*")).run()[0][0]) + + +def _enrich_work_orders(work_orders: list[dict]) -> None: + """Attach item/workstation image, status colour, % complete and a job-card breakdown to each row.""" + wo_names = [row.name for row in work_orders] + jc_counts = _get_job_card_status_counts(wo_names) + workstation_map = _get_current_workstation_map(wo_names) + for row in work_orders: + row.item_image = ( + frappe.get_cached_value("Item", row.production_item, "image") if row.production_item else None + ) + row.status_colour = get_status_color(row.status) + row.per_completed = round(flt(row.produced_qty) / flt(row.qty) * 100, 1) if flt(row.qty) else 0 + counts = jc_counts.get(row.name, {}) + row.job_card_status = counts.get("by_status", {}) + row.total_operations = counts.get("total", 0) + row.completed_operations = counts.get("completed", 0) + row.in_progress_operations = counts.get("in_progress", 0) + # Two segments for the card's progress bar: green (done) + orange (in progress); the rest + # of the track stays grey (pending / not started). + row.per_operations = ( + round(row.completed_operations / row.total_operations * 100, 1) if row.total_operations else 0 + ) + row.per_in_progress = ( + round(row.in_progress_operations / row.total_operations * 100, 1) if row.total_operations else 0 + ) + # Current/active operation's workstation (name + Active-Status image) for the card header. + workstation = workstation_map.get(row.name, {}) + row.workstation = workstation.get("workstation") + row.workstation_name = workstation.get("workstation_name") + row.workstation_image = workstation.get("image") + row.current_operation = workstation.get("operation") + + +def _get_current_workstation_map(wo_names: list[str]) -> dict[str, dict]: + """Map each Work Order to its current operation's workstation (name + Active-Status image). + + The "current" operation is the first not-yet-completed operation in the routing (by idx); + if every operation is complete, the last one is used so finished cards still show a workstation. + """ + if not wo_names: + return {} + + operations = frappe.get_all( + "Work Order Operation", + filters={"parent": ["in", wo_names]}, + fields=["parent", "idx", "operation", "status", "workstation"], + order_by="parent, idx", + ) + + ops_by_wo: dict[str, list] = {} + for op in operations: + ops_by_wo.setdefault(op.parent, []).append(op) + + chosen_by_wo = {} + workstations = set() + for wo_name, ops in ops_by_wo.items(): + current = next((op for op in ops if (op.status or "") != "Completed"), ops[-1]) + if current.workstation: + chosen_by_wo[wo_name] = current + workstations.add(current.workstation) + + ws_details = {} + if workstations: + for ws in frappe.get_all( + "Workstation", + filters={"name": ["in", list(workstations)]}, + fields=["name", "workstation_name", "on_status_image"], + ): + ws_details[ws.name] = ws + + result = {} + for wo_name, op in chosen_by_wo.items(): + detail = ws_details.get(op.workstation, {}) + result[wo_name] = { + "workstation": op.workstation, + "workstation_name": detail.get("workstation_name") or op.workstation, + "image": detail.get("on_status_image"), + "operation": op.operation, + } + return result + + +def _get_job_card_status_counts(wo_names: list[str]) -> dict[str, dict]: + """One batched query → {work_order: {by_status: {status: n}, total, completed}} for the WO cards.""" + if not wo_names: + return {} + + rows = frappe.get_all( + "Job Card", + filters={"work_order": ["in", wo_names], "docstatus": ["<", 2]}, + fields=["work_order", "status"], + ) + result: dict[str, dict] = {} + for row in rows: + entry = result.setdefault( + row.work_order, {"by_status": {}, "total": 0, "completed": 0, "in_progress": 0} + ) + status = "Not Started" if (row.status or "Open") == "Open" else row.status + entry["by_status"][status] = entry["by_status"].get(status, 0) + 1 + entry["total"] += 1 + # "To Manufacture" = operation done, only the Manufacture Stock Entry is pending — count it + # as completed so the work order's progress bar reflects the finished operation. + if status in ("Completed", "Submitted", "To Manufacture"): + entry["completed"] += 1 + elif status == "Work In Progress": + entry["in_progress"] += 1 + return result + + +@frappe.whitelist() +def get_data(workstation: str | None = None, work_order: str | None = None): + """ + Returns job-card data for the Shop Floor page. + + When `work_order` is set it wins over `workstation` and the result spans every + operation of that Work Order (including subcontracted job cards). When only + `workstation` is set, the result is the open + in-progress job cards for that + workstation, excluding subcontracted. + """ + if not (workstation or work_order): + return {"job_cards": [], "capacity": 1, "mode": None} + if not frappe.has_permission("Job Card", "read"): + return {"job_cards": [], "capacity": 1, "mode": None} + + filters, mode = _build_job_card_filters(workstation, work_order) + jc_data = _fetch_job_cards(filters, mode) + _enrich_job_cards(jc_data) + + capacity, oee = 1, None + if mode == "workstation": + capacity = frappe.db.get_value("Workstation", workstation, "production_capacity") or 1 + oee = get_workstation_oee(workstation) + + return { + "job_cards": jc_data, + "capacity": capacity, + "mode": mode, + "oee": oee, + "user_employee": frappe.db.get_value("Employee", {"user_id": frappe.session.user}, "name"), + "today_sessions": get_today_sessions(workstation, work_order), + } + + +def _build_job_card_filters(workstation, work_order): + """Filters + mode for the job-card query. work_order spans all ops; workstation is operator view.""" + filters = {"docstatus": ("<", 2)} + if work_order: + filters["work_order"] = work_order + return filters, "work_order" + + filters["workstation"] = workstation + filters["is_subcontracted"] = 0 + filters["status"] = ["!=", "Stopped"] + return filters, "workstation" + + +def _fetch_job_cards(filters, mode): + """Job cards matching filters. In workstation mode only drafts matter — submitted JCs are + done from MES's perspective and missed ones are picked up via the standard Job Card list. + + In work_order mode the whole routing is shown (incl. completed/submitted job cards), ordered + by the operation sequence so the operator reads them in manufacturing order. + """ + order_by = ( + "sequence_id asc, expected_start_date, expected_end_date" + if mode == "work_order" + else "expected_start_date, expected_end_date" + ) + # Drafts are the operator's working set; submitted "To Manufacture" cards are also kept so + # the station shows what still needs a Manufacture Stock Entry (its own section, client-side). + # This must be part of the query, not a post-filter: a busy workstation's history would + # otherwise fill the row limit with old submitted cards and hide the active drafts. + or_filters = [["docstatus", "=", 0], ["status", "=", "To Manufacture"]] if mode == "workstation" else None + return frappe.get_all( + "Job Card", + fields=JOB_CARD_FIELDS, + filters=filters, + or_filters=or_filters, + order_by=order_by, + limit=50, + ) + + +def _enrich_job_cards(jc_data): + """Decorate every row with display + material-availability data for the page.""" + job_card_names = [row.name for row in jc_data] + time_logs = get_time_logs(job_card_names) if job_card_names else {} + allow_excess_transfer = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer") + for row in jc_data: + _enrich_job_card_row(row, time_logs, allow_excess_transfer) + + +def _enrich_job_card_row(row, time_logs, allow_excess_transfer): + """Attach status label, item image/uom, time logs and material availability to one row.""" + if row.status == "Open": + row.status = "Not Started" + + item_code = row.finished_good or row.production_item + row.fg_uom = frappe.get_cached_value("Item", item_code, "stock_uom") if item_code else None + row.item_image = frappe.get_cached_value("Item", item_code, "image") if item_code else None + row.status_colour = get_status_color(row.status) + row.time_logs = time_logs.get(row.name, []) + row.make_material_request = bool(row.for_quantity > row.transferred_qty or allow_excess_transfer) + # Required vs transferred + on-hand in source — operator sees shortages before starting work. + row.materials = get_job_card_materials(row.name) + # Guided execution: per-operation work instructions + quality-check state for the card. + row.instructions = _get_operation_instructions(row.operation) + row.qc = _get_job_card_qc(row) + + +def _get_operation_instructions(operation: str | None) -> dict | None: + """Description + rich Work Instructions from the Operation master, for the card's + Instructions panel. Returns None when the operation has neither, so the panel stays hidden. + + `work_instruction` is a Text Editor field (HTML) — Frappe bleach-sanitizes it on save, so it + is safe to render as-is on the client. `description` is plain text and must be escaped there. + """ + if not operation: + return None + + op = frappe.get_cached_value("Operation", operation, ["description", "work_instruction"], as_dict=True) + if not op: + return None + + description = (op.description or "").strip() + work_instruction = (op.work_instruction or "").strip() + if not description and not work_instruction: + return None + return {"description": description, "work_instruction": work_instruction} + + +def _get_job_card_qc(row) -> dict: + """Quality-check state for a job card row: whether an inspection is required before submit, + which template to use, and any inspection already linked (name + status + docstatus). + + "Required" mirrors Job Card.validate_inspection() — BOM inspection_required AND the Work Order + Operation's quality_inspection_required. When only a template is configured the check is + offered but not enforced. + """ + required = bool( + row.get("bom_no") + and frappe.get_cached_value("BOM", row.bom_no, "inspection_required") + and row.get("operation_id") + and frappe.db.get_value("Work Order Operation", row.operation_id, "quality_inspection_required") + ) + + template = row.get("quality_inspection_template") + if not template and row.get("operation"): + template = frappe.get_cached_value("Operation", row.operation, "quality_inspection_template") + + info = { + "required": required, + "template": template, + "has_checklist": bool(template), + "name": None, + "status": None, + "docstatus": None, + } + if row.get("quality_inspection"): + qi = frappe.db.get_value( + "Quality Inspection", row.quality_inspection, ["name", "status", "docstatus"], as_dict=True + ) + if qi: + info.update({"name": qi.name, "status": qi.status, "docstatus": qi.docstatus}) + return info + + +def get_job_card_materials(job_card: str) -> list[dict]: + """Required vs transferred + on-hand stock for each raw material in the source warehouse. + + Powers the active-job Materials side panel — operator sees shortages before starting work. + """ + items = frappe.get_all( + "Job Card Item", + filters={"parent": job_card}, + fields=["item_code", "item_name", "source_warehouse", "required_qty", "transferred_qty", "uom"], + order_by="idx", + ) + if not items: + return [] + + on_hand_map = _get_on_hand_map(items) + return [_build_material_row(it, on_hand_map) for it in items] + + +def _get_on_hand_map(items) -> dict[tuple[str, str], float]: + """Map (item_code, warehouse) → on-hand qty via batched Bin lookups.""" + pairs = {(it.item_code, it.source_warehouse) for it in items if it.source_warehouse} + if not pairs: + return {} + + bin_rows = frappe.get_all( + "Bin", + filters={ + "item_code": ["in", list({p[0] for p in pairs})], + "warehouse": ["in", list({p[1] for p in pairs})], + }, + fields=["item_code", "warehouse", "actual_qty"], + ) + return {(b.item_code, b.warehouse): flt(b.actual_qty) for b in bin_rows} + + +def _build_material_row(it, on_hand_map) -> dict: + """One material entry with shortage + status pill for the side panel.""" + required = flt(it.required_qty) + transferred = flt(it.transferred_qty) + on_hand = on_hand_map.get((it.item_code, it.source_warehouse), 0.0) + shortage = max(required - transferred, 0.0) + if transferred >= required: + status = "ready" + elif on_hand >= shortage: + status = "available" + else: + status = "short" + return { + "item_code": it.item_code, + "item_name": it.item_name or it.item_code, + "source_warehouse": it.source_warehouse, + "required_qty": required, + "transferred_qty": transferred, + "on_hand_qty": on_hand, + "shortage": shortage, + "uom": it.uom or "", + "status": status, + } + + +def get_today_sessions(workstation: str | None, work_order: str | None) -> list[dict]: + """Submitted job cards finalized today — used for the bottom 'Today's Sessions' strip. + + Filtered on docstatus=1 only (draft/cancelled excluded). The status pill follows the + job card's own status (e.g. Work In Progress → orange, Completed → green). + """ + filters = _today_sessions_filters(workstation, work_order) + if filters is None: + return [] + + rows = frappe.get_all( + "Job Card", + filters=filters, + fields=TODAY_SESSION_FIELDS, + order_by="modified desc", + limit=10, + ) + for r in rows: + item_code = r.finished_good or r.production_item + r.item_image = frappe.get_cached_value("Item", item_code, "image") if item_code else None + r.status_colour = get_status_color(r.status) + return rows + + +def _today_sessions_filters(workstation, work_order) -> dict | None: + """Submitted-today filter scoped to a work order or workstation; None if neither given. + + "To Manufacture" cards are excluded — they aren't finalized yet (Manufacture Stock Entry + pending) and get their own section, so they shouldn't appear among finished sessions. + """ + filters = { + "docstatus": 1, + "modified": [">=", get_datetime(f"{getdate()} 00:00:00")], + "status": ["!=", "To Manufacture"], + } + if work_order: + filters["work_order"] = work_order + elif workstation: + filters["workstation"] = workstation + else: + return None + return filters + + +def get_workstation_oee(workstation: str) -> dict | None: + """ + OEE = Availability X Performance X Quality, computed for today only. + + Caveat: without a downtime-reason capture step, Availability is just + (actual_run_time / scheduled_time) — it cannot distinguish planned breaks + from unplanned breakdowns. The number is directional, not audit-grade. + """ + today = getdate() + scheduled_min = flt(frappe.db.get_value("Workstation", workstation, "total_working_hours")) * 60 + actual_run_min, ideal_min = _get_run_and_ideal_minutes(workstation, today) + completed_jcs = _get_completed_jcs_today(workstation, today) + + # No activity at all today — nothing to display. + if actual_run_min == 0 and not completed_jcs: + return None + return _build_oee(scheduled_min, actual_run_min, ideal_min, completed_jcs) + + +def _get_run_and_ideal_minutes(workstation, today) -> tuple[float, float]: + """Sum actual run minutes (clipped to today) and the ideal minutes for produced qty.""" + today_start = get_datetime(f"{today} 00:00:00") + today_end = get_datetime(f"{today} 23:59:59") + now = now_datetime() + actual_run_min = 0.0 + ideal_min = 0.0 + for log in _get_oee_time_logs(workstation, today_start, today_end): + # Clip the log's interval to today's window for fair attribution. + start = max(get_datetime(log.from_time), today_start) + end = min(get_datetime(log.to_time) if log.to_time else now, today_end) + if end > start: + actual_run_min += time_diff_in_seconds(end, start) / 60 + if log.completed_qty and log.for_quantity and log.time_required: + ideal_min += (flt(log.time_required) / flt(log.for_quantity)) * flt(log.completed_qty) + return actual_run_min, ideal_min + + +def _get_oee_time_logs(workstation, today_start, today_end) -> list[dict]: + """Job Card time logs whose interval overlaps today's window.""" + tl = frappe.qb.DocType("Job Card Time Log") + jc = frappe.qb.DocType("Job Card") + return ( + frappe.qb.from_(tl) + .inner_join(jc) + .on(jc.name == tl.parent) + .select(tl.from_time, tl.to_time, tl.completed_qty, jc.for_quantity, jc.time_required) + .where(jc.workstation == workstation) + .where(jc.docstatus < 2) + .where(tl.from_time <= today_end) + .where(tl.to_time.isnull() | (tl.to_time >= today_start)) + ).run(as_dict=True) + + +def _get_completed_jcs_today(workstation, today) -> list[dict]: + """Job cards completed/submitted today — process loss is finalized at submission.""" + jc = frappe.qb.DocType("Job Card") + return ( + frappe.qb.from_(jc) + .select(jc.total_completed_qty, jc.process_loss_qty) + .where(jc.workstation == workstation) + .where(jc.status.isin(["Completed", "Submitted"])) + .where(Date(jc.modified) == today) + ).run(as_dict=True) + + +def _build_oee(scheduled_min, actual_run_min, ideal_min, completed_jcs) -> dict: + """Combine the three OEE factors into the response payload.""" + total_completed = sum(flt(j.total_completed_qty) for j in completed_jcs) + total_loss = sum(flt(j.process_loss_qty) for j in completed_jcs) + availability = min(actual_run_min / scheduled_min, 1.0) if scheduled_min > 0 else None + performance = min(ideal_min / actual_run_min, 1.0) if actual_run_min > 0 else 0.0 + quality = max(total_completed - total_loss, 0.0) / total_completed if total_completed > 0 else 1.0 + # OEE requires all three factors; without a schedule, Availability is unknown. + oee_val = round(availability * performance * quality * 100, 1) if availability is not None else None + return { + "oee": oee_val, + "availability": round(availability * 100, 1) if availability is not None else None, + "performance": round(performance * 100, 1), + "quality": round(quality * 100, 1), + } diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 1d893d4d8ae..0ec66c41087 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -496,4 +496,8 @@ erpnext.patches.v16_0.rename_ar_ap_ageing_filter erpnext.patches.v16_0.fix_subcontracting_titles erpnext.patches.v16_0.backfill_repost_accounting_ledger_status erpnext.patches.v16_0.merge_seeded_item_group_root +<<<<<<< HEAD erpnext.patches.v16_0.rename_italy_customer_name_fields +======= +erpnext.patches.v16_0.set_stock_uom_in_job_card +>>>>>>> 07ac4d83ef (feat(job_card): print quantities with their stock uom (#57689)) diff --git a/erpnext/patches/v16_0/set_stock_uom_in_job_card.py b/erpnext/patches/v16_0/set_stock_uom_in_job_card.py new file mode 100644 index 00000000000..35abf69df05 --- /dev/null +++ b/erpnext/patches/v16_0/set_stock_uom_in_job_card.py @@ -0,0 +1,36 @@ +import frappe + + +def execute(): + job_cards = frappe.get_all( + "Job Card", + filters={"stock_uom": ("is", "not set")}, + fields=["name", "finished_good", "production_item"], + ) + + if not job_cards: + return + + item_codes = {code for row in job_cards if (code := row.finished_good or row.production_item)} + if not item_codes: + return + + stock_uoms = dict( + frappe.get_all( + "Item", + filters={"name": ("in", list(item_codes))}, + fields=["name", "stock_uom"], + as_list=True, + ) + ) + + updates = {} + for row in job_cards: + stock_uom = stock_uoms.get(row.finished_good or row.production_item) + if stock_uom: + updates[row.name] = {"stock_uom": stock_uom} + + if not updates: + return + + frappe.db.bulk_update("Job Card", updates) diff --git a/erpnext/public/js/shop_floor/shop_floor.js b/erpnext/public/js/shop_floor/shop_floor.js new file mode 100644 index 00000000000..a595102c004 --- /dev/null +++ b/erpnext/public/js/shop_floor/shop_floor.js @@ -0,0 +1,1758 @@ +// Shop Floor — an immersive, keyboard-first operator/manager interface. +// +// Two experiences share one app shell (see get_shop_floor_context on the server): +// • manager — a paginated board of work orders bucketed Pending / In Progress and Completed. +// Drilling into a work order opens its job cards in the operator pane. +// • operator — a focused workstation/work-order view to start, pause, complete and submit jobs. +// +// The whole surface is driveable from the keyboard (press ? for the cheat sheet) so an operator +// at a terminal never needs the mouse. + +// Job Card status → indicator colour, mirrored from workstation.get_status_color so the manager +// board can paint per-operation chips without a round-trip. +const JC_STATUS_COLORS = { + Completed: "green", + Submitted: "blue", + "Work In Progress": "orange", + "Material Transferred": "yellow", + "On Hold": "red", + Open: "gray", + "Not Started": "gray", +}; + +const MANAGER_BUCKETS = [ + { key: "open", label: __("Pending / In Progress"), dot: "orange" }, + { key: "completed", label: __("Completed"), dot: "green" }, +]; + +const PAGE_LENGTH = 20; + +class ShopFloor { + constructor({ wrapper }, page) { + this.wrapper = $(wrapper); + this.page = page; + this.timer_intervals = {}; + this.capacity = 1; + this.mode = null; + // Remembers each Materials panel's open/closed state (keyed by job card) so it + // survives re-renders — otherwise a reload right after a click resets the panel. + this.materials_open = {}; + // Same idea for the per-operation Work Instructions panel. + this.instructions_open = {}; + + // View state. + this.view = "operator"; // overwritten once context loads + this.active_bucket = "open"; + this.with_job_cards_only = true; // board default: hide WOs that have no job cards + this.buckets = {}; // key -> { rows, total, start, loaded } + this.selected_wo = null; + this.focus_index = -1; + this.op_state = { workstation: null, work_order: null }; + + this.make(); + this.bind_realtime(); + this.bind_lifecycle(); + this.init(); + } + + init() { + frappe.call("erpnext.manufacturing.page.shop_floor.shop_floor.get_shop_floor_context").then((r) => { + const ctx = r.message || {}; + this.view = ctx.role_view === "manager" ? "manager" : "operator"; + this.can_manage = !!ctx.can_manage; + this.user_employee = ctx.user_employee || null; + this.render_shell_controls(); + this.render_view(); + this.bind_keys(); + this.initialized = true; + this.apply_route_options(); + }); + } + + // ── App shell ──────────────────────────────────────────────────────────── + make() { + this.wrapper.append(` + ${this.styles()} +
    +
    +
    +
    +
    + + + + + +
    +
    +
    +
    +
    +
    +
    +
    + `); + + this.app = this.wrapper.find(".sf-app"); + this.brand_icon = `${__(
+			`; + this.topbar_left = this.wrapper.find(".sf-topbar-left"); + this.topbar_center = this.wrapper.find(".sf-topbar-center"); + this.body = this.wrapper.find(".sf-body"); + this.board_container = this.wrapper.find(".sf-board"); + this.detail_container = this.wrapper.find(".sf-detail"); + this.op_container = this.wrapper.find(".sf-operator"); + + this.wrapper.find(".sf-btn-home").on("click", () => (window.location.href = "/app")); + this.wrapper.find(".sf-btn-refresh").on("click", () => this.refresh()); + this.wrapper.find(".sf-btn-scan").on("click", () => this.open_scanner()); + this.wrapper.find(".sf-btn-help").on("click", () => this.show_help()); + this.wrapper.find(".sf-btn-theme").on("click", () => this.toggle_theme()); + this.update_theme_button(); + } + + // Kiosk-friendly light/dark switch: flips the standard desk theme and persists it on the + // User (same as the Ctrl+Shift+G switcher), so the choice survives reloads and follows the + // operator's login on any device. + toggle_theme() { + const next = frappe.ui.get_current_theme() === "dark" ? "light" : "dark"; + document.documentElement.setAttribute("data-theme-mode", next); + frappe.ui.set_theme(next); + frappe.xcall("frappe.core.doctype.user.user.switch_theme", { + theme: next.charAt(0).toUpperCase() + next.slice(1), + }); + this.update_theme_button(); + } + + update_theme_button() { + const dark = frappe.ui.get_current_theme() === "dark"; + this.wrapper + .find(".sf-btn-theme") + .html(dark ? "☀" : "☾") + .attr("title", dark ? __("Switch to Light Theme") : __("Switch to Dark Theme")); + } + + render_shell_controls() { + this.topbar_left.empty(); + this.topbar_center.empty(); + + // View toggle — only managers can flip between the board and a bare operator view. + const toggle = this.can_manage + ? `
    + + +
    ` + : ""; + + if (this.view === "manager") { + this.topbar_left.html(` + ${this.brand_icon}${__("Shop Floor")} + ${toggle} +
    + ${MANAGER_BUCKETS.map( + (b) => `` + ).join("")} +
    + `); + this.topbar_center.html(` + + + `); + + this.topbar_left.find(".sf-tab").on("click", (e) => { + this.switch_bucket($(e.currentTarget).attr("data-bucket")); + }); + let timer = null; + this.topbar_center.find(".sf-search-input").on("input", (e) => { + const val = e.target.value; + clearTimeout(timer); + timer = setTimeout(() => this.search_work_orders(val), 300); + }); + this.topbar_center.find(".sf-jc-toggle").on("change", (e) => { + this.toggle_job_cards_only(e.target.checked); + }); + } else { + this.topbar_left.html( + `${this.brand_icon}${__("Shop Floor")}${toggle}` + ); + this.build_operator_filters(); + } + + this.topbar_left.find(".sf-view-btn").on("click", (e) => { + this.set_view($(e.currentTarget).attr("data-view")); + }); + } + + build_operator_filters() { + this.topbar_center.html('
    '); + const $filters = this.topbar_center.find(".sf-filters"); + + this.workstation_filter = frappe.ui.form.make_control({ + df: { + fieldtype: "Link", + options: "Workstation", + fieldname: "workstation", + placeholder: __("Machine"), + onchange: () => this.load_operator(), + }, + parent: $filters, + render_input: true, + }); + this.workstation_filter.$wrapper.addClass("sf-filter-control"); + + this.work_order_filter = frappe.ui.form.make_control({ + df: { + fieldtype: "Link", + options: "Work Order", + fieldname: "work_order", + placeholder: __("Work Order"), + onchange: () => this.load_operator(), + }, + parent: $filters, + render_input: true, + }); + this.work_order_filter.$wrapper.addClass("sf-filter-control"); + } + + set_view(view) { + if (!view || view === this.view) return; + this.view = view; + this.selected_wo = null; + this.focus_index = -1; + this.render_shell_controls(); + this.render_view(); + } + + render_view() { + const manager = this.view === "manager"; + this.board_container.toggle(manager); + this.detail_container.toggle(manager && !!this.selected_wo); + this.op_container.toggle(!manager); + this.body.toggleClass("detail-open", manager && !!this.selected_wo); + + if (manager) { + this.load_bucket(this.active_bucket); + } else { + this.load_operator(); + } + } + + // ── Manager board ──────────────────────────────────────────────────────── + switch_bucket(bucket) { + if (!bucket || bucket === this.active_bucket) return; + this.active_bucket = bucket; + this.selected_wo = null; + this.focus_index = -1; + this.topbar_left.find(".sf-tab").removeClass("active"); + this.topbar_left.find(`.sf-tab[data-bucket="${bucket}"]`).addClass("active"); + this.detail_container.hide(); + this.body.removeClass("detail-open"); + this.load_bucket(bucket); + } + + search_work_orders(term) { + this.search_term = term; + // Re-query every bucket from scratch on the next visit; reload the active one now. + this.buckets = {}; + this.load_bucket(this.active_bucket); + } + + toggle_job_cards_only(checked) { + this.with_job_cards_only = !!checked; + // Filter changes every bucket's contents + counts; drop caches and clear stale counts. + this.buckets = {}; + this.topbar_left.find(".sf-tab-count").text(""); + this.load_bucket(this.active_bucket); + } + + load_bucket(bucket, append = false) { + const state = this.buckets[bucket] || { rows: [], total: 0, start: 0, loaded: false }; + const start = append ? state.start : 0; + + frappe.call({ + method: "erpnext.manufacturing.page.shop_floor.shop_floor.get_work_orders", + args: { + status_group: bucket, + start: start, + page_length: PAGE_LENGTH, + search: this.search_term || null, + with_job_cards_only: this.with_job_cards_only ? 1 : 0, + }, + callback: (r) => { + const data = r.message || {}; + const rows = data.work_orders || []; + this.buckets[bucket] = { + rows: append ? state.rows.concat(rows) : rows, + total: cint(data.total), + start: start + rows.length, + loaded: true, + }; + this.update_tab_count(bucket); + if (bucket === this.active_bucket) this.render_board(); + }, + }); + } + + update_tab_count(bucket) { + const state = this.buckets[bucket]; + if (!state) return; + this.topbar_left.find(`[data-bucket-count="${bucket}"]`).text(state.total ? state.total : ""); + } + + render_board() { + const state = this.buckets[this.active_bucket] || { rows: [], total: 0 }; + this.focus_index = -1; + + if (!state.rows.length) { + this.board_container.html(`
    ${__("No work orders here.")}
    `); + return; + } + + const cards = state.rows.map((wo) => this.work_order_card(wo)).join(""); + const more = + state.rows.length < state.total + ? `` + : `
    ${__("Showing all {0}", [state.total])}
    `; + + this.board_container.html( + `
    ${cards}
    ${more}
    ` + ); + + this.board_container.find(".sf-wo-card").on("click", (e) => { + this.open_wo($(e.currentTarget).attr("data-name")); + }); + this.board_container + .find(".sf-load-more") + .on("click", () => this.load_bucket(this.active_bucket, true)); + } + + work_order_card(wo) { + const item = wo.item_name || wo.production_item; + + // Hero image = the current operation's workstation. No item-image fallback — when the + // workstation has no image uploaded we show its initials, never the product image. + const image = wo.workstation_image + ? `` + : `${frappe.get_abbr(wo.workstation_name || item, 2)}`; + + const workstation_line = wo.workstation_name + ? `
    🏭 ${frappe.utils.escape_html( + wo.workstation_name + )}${wo.current_operation ? ` · ${frappe.utils.escape_html(wo.current_operation)}` : ""}
    ` + : ""; + + // Operations bar: green segment (done) + orange segment (in progress); grey track = pending. + const done_pct = Math.min(cint(wo.per_operations), 100); + const wip_pct = Math.min(cint(wo.per_in_progress), 100 - done_pct); + + return ` +
    +
    +
    ${image}
    +
    +
    ${frappe.utils.escape_html(item)}
    + ${workstation_line} +
    + + ${wo.name} +
    +
    +
    +
    +
    + ${__("Operations")} + ${cint(wo.completed_operations)} / ${cint(wo.total_operations)} +
    +
    +
    +
    +
    +
    +
    + `; + } + + open_wo(name) { + if (!name) return; + this.selected_wo = name; + this.op_state = { workstation: null, work_order: name }; + this.detail_container.show(); + this.body.addClass("detail-open"); + this.board_container + .find(".sf-wo-card") + .removeClass("sf-selected") + .filter(`[data-name="${name}"]`) + .addClass("sf-selected"); + // The detail pane reuses the operator rendering for a single work order. + this.detail_container.html(` +
    + + ${frappe.utils.escape_html(name)} + ${__("Open")} +
    +
    + `); + this.detail_container.find(".sf-detail-back").on("click", () => this.close_wo()); + this.op_container_target = this.detail_container.find(".sf-detail-body"); + this.load_operator_data(this.op_container_target, { work_order: name }); + } + + close_wo() { + this.selected_wo = null; + this.op_container_target = null; + this.detail_container.hide().empty(); + this.body.removeClass("detail-open"); + this.board_container.find(".sf-wo-card").removeClass("sf-selected"); + } + + // ── Operator pane ────────────────────────────────────────────────────────── + // Resolves the container the operator content renders into: the standalone operator + // view, or the manager's drill-down detail pane. + current_op_container() { + return this.view === "manager" ? this.op_container_target : this.op_container; + } + + load_operator() { + const workstation = this.workstation_filter ? this.workstation_filter.get_value() : null; + const work_order = this.work_order_filter ? this.work_order_filter.get_value() : null; + this.op_state = { workstation, work_order }; + + if (!workstation && !work_order) { + this.clear_timers(); + this.op_container.html( + `
    ${__("Select a machine or work order to begin")}
    ` + ); + return; + } + this.load_operator_data(this.op_container, { workstation, work_order }); + } + + load_operator_data($container, { workstation, work_order }) { + frappe.call({ + method: "erpnext.manufacturing.page.shop_floor.shop_floor.get_data", + args: { + workstation: work_order ? null : workstation, + work_order: work_order || null, + }, + callback: (r) => { + const data = r.message || {}; + this.job_cards = data.job_cards || []; + this.capacity = cint(data.capacity) || 1; + this.mode = data.mode || (work_order ? "work_order" : "workstation"); + this.oee = data.oee || null; + if (data.user_employee) this.user_employee = data.user_employee; + this.today_sessions = data.today_sessions || []; + this.workstation = workstation; + this.work_order = work_order; + this.compute_state(); + this.dedupe_today_sessions(); + this.render_operator($container); + }, + }); + } + + // A job card already shown under Completed Operations shouldn't repeat in + // Today's Sessions — keep it in Completed Operations only. + dedupe_today_sessions() { + const shown = new Set((this.completed || []).map((jc) => jc.name)); + this.today_sessions = (this.today_sessions || []).filter((s) => !shown.has(s.name)); + } + + // Re-fetch whichever operator content is currently on screen (used after every action). + reload() { + if (this.view === "manager" && this.selected_wo) { + this.load_operator_data(this.op_container_target, { work_order: this.selected_wo }); + // Keep the board chips fresh too. + this.buckets = {}; + this.load_bucket(this.active_bucket); + } else if (this.view === "manager") { + this.load_bucket(this.active_bucket); + } else { + this.load_operator(); + } + } + + refresh() { + if (this.view === "manager") { + this.buckets = {}; + } + this.reload(); + } + + compute_state() { + this.active_jobs = []; + this.queue = []; + this.pending_submission = []; + this.completed = []; + // Submitted but the finished goods aren't booked yet (status "To Manufacture") — its own + // actionable section, kept out of Completed Operations / Today's Sessions. + this.to_manufacture = []; + + for (const jc of this.job_cards) { + // Same materials-ready rule as job_card.js make_dashboard. + jc._materials_ready = !!( + jc.skip_material_transfer || + flt(jc.transferred_qty) >= flt(jc.for_quantity) + flt(jc.process_loss_qty) || + !jc.finished_good + ); + + // Submitted JCs are historical from the Shop Floor's POV — only appear here in work_order + // mode (and, for "To Manufacture", in workstation mode too — see _fetch_job_cards). + if (jc.docstatus === 1) { + if (jc.status === "To Manufacture") { + this.to_manufacture.push(jc); + } else { + this.completed.push(jc); + } + continue; + } + + const last_log = + jc.time_logs && jc.time_logs.length ? jc.time_logs[jc.time_logs.length - 1] : null; + const is_running = last_log && !last_log.to_time && !jc.is_paused; + const is_paused = jc.is_paused; + + if (is_running || is_paused) { + this.active_jobs.push(jc); + } else if (jc.status === "Completed") { + // All qty accounted for but still draft — waiting on Submit. + this.pending_submission.push(jc); + } else { + this.queue.push(jc); + } + } + + // Slot rules — all active jobs are always shown; the grid (col-md-6) wraps them 2 per row. + // workstation mode: capacity-many slots, expanded to fit every active job (+ empty placeholders). + // work_order mode: one slot per active job (no empty placeholders). + let slot_count; + if (this.mode === "work_order") { + slot_count = this.active_jobs.length; + } else { + slot_count = Math.max(this.capacity, this.active_jobs.length, 1); + } + + this.slots = []; + for (let i = 0; i < slot_count; i++) { + this.slots.push(this.active_jobs[i] || null); + } + + // Auto-pick: when nothing is running, surface the next queue item in the slot. + if (this.active_jobs.length === 0 && this.queue.length > 0) { + const next_up = this.queue.shift(); + next_up._is_next_up = true; + this.slots[0] = next_up; + } + + this.summary = { + active_count: this.active_jobs.length, + // "To Manufacture" (submitted, qty done, but the Manufacture Stock Entry is still pending) + // isn't actually finished — count it as Pending, not Completed. + queue_count: this.queue.length + this.to_manufacture.length, + completed_count: this.completed.length + this.pending_submission.length, + capacity: this.capacity, + }; + } + + render_operator($container) { + this.clear_timers(); + $container.empty(); + + const html = frappe.render_template("shop_floor_template", { + workstation: this.workstation, + work_order: this.work_order, + mode: this.mode, + slots: this.slots, + active_jobs: this.active_jobs, + queue: this.queue, + pending_submission: this.pending_submission, + to_manufacture: this.to_manufacture, + completed: this.completed, + today_sessions: this.today_sessions || [], + summary: this.summary, + oee: this.oee, + }); + $container.html(html); + + // Restore each Materials panel to its remembered open/closed state. + $container.find(".mes-materials-inline").each((i, el) => { + const $el = $(el); + const name = $el.attr("data-job-card"); + if (!name) return; + if (name in this.materials_open) { + $el.toggleClass("is-open", this.materials_open[name]); + } else { + this.materials_open[name] = $el.hasClass("is-open"); + } + }); + + // Restore each Work Instructions panel to its remembered open/closed state. + $container.find(".mes-instructions-inline").each((i, el) => { + const $el = $(el); + const name = $el.attr("data-job-card"); + if (name && name in this.instructions_open) { + $el.toggleClass("is-open", this.instructions_open[name]); + } + }); + + this.bind_events($container); + + for (const jc of this.active_jobs) { + if (jc.is_paused) { + this.render_timer(jc.name, this.elapsed_seconds(jc), $container); + } else { + this.start_timer_for(jc, $container); + } + } + } + + clear_timers() { + for (const id of Object.values(this.timer_intervals)) { + clearInterval(id); + } + this.timer_intervals = {}; + } + + bind_events($container) { + const me = this; + + $container.find(".mes-materials-summary").on("click", function (e) { + if ($(e.target).closest(".mes-btn-transfer").length) return; + const $inline = $(this).closest(".mes-materials-inline"); + const open = !$inline.hasClass("is-open"); + $inline.toggleClass("is-open", open); + const name = $inline.attr("data-job-card"); + if (name) me.materials_open[name] = open; + }); + + $container.find(".mes-instructions-summary").on("click", function () { + const $inline = $(this).closest(".mes-instructions-inline"); + const open = !$inline.hasClass("is-open"); + $inline.toggleClass("is-open", open); + const name = $inline.attr("data-job-card"); + if (name) me.instructions_open[name] = open; + }); + + // Clicking a "QC Required" / "QC Available" pill runs the inline check ahead of End Session. + $container.find(".mes-qc-pill").on("click", function () { + const name = $(this).attr("data-job-card"); + const jc = (me.active_jobs || []).find((j) => j.name === name); + if (jc) me.run_quality_check(jc, () => me.reload()); + }); + + $container.find(".mes-btn-start").on("click", function () { + me.start_job($(this).attr("data-job-card")); + }); + $container.find(".mes-btn-pause").on("click", function () { + me.pause_job($(this).attr("data-job-card")); + }); + $container.find(".mes-btn-resume").on("click", function () { + me.resume_job($(this).attr("data-job-card")); + }); + $container.find(".mes-btn-end-session").on("click", function () { + me.end_session($(this).attr("data-job-card")); + }); + $container.find(".mes-btn-submit").on("click", function () { + me.submit_job_card($(this).attr("data-job-card")); + }); + $container.find(".mes-btn-make-entry").on("click", function () { + me.make_manufacture_entry($(this).attr("data-job-card")); + }); + $container.find(".mes-btn-transfer").on("click", function (e) { + e.preventDefault(); + me.transfer_materials($(this).attr("data-job-card")); + }); + } + + // ── Operator actions (unchanged behaviour, reload() instead of load()) ───── + start_job(job_card) { + const me = this; + if (this.mode === "workstation" && this.active_jobs.length >= this.capacity) { + frappe.msgprint({ + title: __("Capacity Reached"), + message: __( + "This machine can run at most {0} job(s) in parallel. Pause or complete a running job before starting another.", + [this.capacity] + ), + indicator: "orange", + }); + return; + } + + const default_employee = this.user_employee; + const dialog = new frappe.ui.Dialog({ + title: __("Start Job"), + fields: [ + { + label: __("Start Time"), + fieldname: "start_time", + fieldtype: "Datetime", + default: frappe.datetime.now_datetime(), + }, + { fieldtype: "Section Break" }, + { + label: __("Employees"), + fieldname: "employees", + fieldtype: "Table", + data: default_employee ? [{ employee: default_employee }] : [], + fields: [ + { + label: __("Employee"), + fieldname: "employee", + fieldtype: "Link", + options: "Employee", + in_list_view: 1, + }, + ], + }, + ], + primary_action_label: __("Start"), + primary_action: (values) => { + dialog.hide(); + me.update_job_card(job_card, "start_timer", { + start_time: values.start_time, + employees: values.employees || [], + }); + }, + }); + dialog.show(); + this.bind_enter_submit(dialog); + } + + // Make a dialog fully keyboard-operable: Enter triggers the primary action, so an operator + // never has to reach for the mouse. Enter is left alone inside multi-line fields and while an + // autocomplete (Link/Select) dropdown is open, so it can still pick a value. + bind_enter_submit(dialog) { + dialog.$wrapper.on("keydown.sfenter", (e) => { + if (e.key !== "Enter" || e.shiftKey) return; + if ($(e.target).is("textarea")) return; + if ($(".awesomplete > ul:not([hidden])").length) return; + const $btn = dialog.get_primary_btn(); + if ( + $btn && + $btn.length && + $btn.is(":visible") && + !$btn.hasClass("disabled") && + !$btn.prop("disabled") + ) { + e.preventDefault(); + e.stopPropagation(); + $btn.trigger("click"); + } + }); + } + + pause_job(jc_name) { + this.update_job_card(jc_name, "pause_job", { end_time: frappe.datetime.now_datetime() }); + } + + resume_job(jc_name) { + this.update_job_card(jc_name, "resume_job", { start_time: frappe.datetime.now_datetime() }); + } + + end_session(jc_name) { + const me = this; + const jc = this.active_jobs.find((j) => j.name === jc_name); + if (!jc) return; + + let pending = flt(jc.for_quantity) - flt(jc.total_completed_qty); + if (flt(jc.pending_qty) > 0) { + pending = flt(jc.pending_qty); + } + + const qty_with_uom = (qty) => `${flt(qty)} ${jc.stock_uom || ""}`.trim(); + + const fields = [ + { + fieldtype: "Float", + label: __("Qty to Manufacture in this Cycle"), + fieldname: "for_quantity", + reqd: 1, + default: pending, + description: __("Completed, Pending and Process Loss quantities must add up to this."), + change() { + const d = me.session_dialog; + d.set_value("completed_qty", d.get_value("for_quantity")); + d.set_value("pending_qty", 0); + d.set_value("process_loss_qty", 0); + }, + }, + { + fieldtype: "Float", + label: __("Completed Quantity"), + fieldname: "completed_qty", + reqd: 1, + default: pending, + change() { + const d = me.session_dialog; + const remaining = + flt(d.get_value("for_quantity")) - + flt(d.get_value("completed_qty")) - + flt(d.get_value("process_loss_qty")); + + if (remaining < 0) { + const max_completed_qty = + flt(d.get_value("for_quantity")) - flt(d.get_value("process_loss_qty")); + d.set_value("completed_qty", max_completed_qty); + frappe.throw( + __("Completed Quantity cannot be greater than {0}", [ + qty_with_uom(max_completed_qty), + ]) + ); + } + + if (remaining !== flt(d.get_value("pending_qty"))) { + d.set_value("pending_qty", remaining); + } + }, + }, + { + fieldtype: "Float", + label: __("Pending Quantity"), + fieldname: "pending_qty", + default: 0.0, + description: __("Qty left for a later cycle or for another job card."), + change() { + const d = me.session_dialog; + const pl = + flt(d.get_value("for_quantity")) - + flt(d.get_value("completed_qty")) - + flt(d.get_value("pending_qty")); + + if (pl < 0) { + d.set_value("pending_qty", 0); + frappe.throw( + __("Pending Quantity cannot be greater than {0}", [ + qty_with_uom( + flt(d.get_value("for_quantity")) - flt(d.get_value("completed_qty")) + ), + ]) + ); + } + + if (pl !== flt(d.get_value("process_loss_qty"))) { + d.set_value("process_loss_qty", pl); + } + }, + }, + { + fieldtype: "Float", + label: __("Process Loss Quantity"), + fieldname: "process_loss_qty", + default: 0.0, + description: __("Qty scrapped in this cycle, nobody will produce it."), + change() { + const d = me.session_dialog; + const remaining = + flt(d.get_value("for_quantity")) - + flt(d.get_value("completed_qty")) - + flt(d.get_value("process_loss_qty")); + + if (remaining < 0) { + d.set_value("process_loss_qty", 0); + frappe.throw( + __("Process Loss Quantity cannot be greater than {0}", [ + qty_with_uom( + flt(d.get_value("for_quantity")) - flt(d.get_value("completed_qty")) + ), + ]) + ); + } + + if (remaining !== flt(d.get_value("pending_qty"))) { + d.set_value("pending_qty", remaining); + } + }, + }, + { fieldtype: "Section Break" }, + { + fieldtype: "Datetime", + label: __("End Time"), + fieldname: "end_time", + default: frappe.datetime.now_datetime(), + }, + ]; + + const get_payload = () => { + const data = me.session_dialog.get_values(); + if (!data) return null; + if (flt(data.completed_qty) <= 0) { + frappe.throw(__("Completed Quantity should be greater than 0")); + } + return { + job_card: jc.name, + qty: flt(data.completed_qty), + for_quantity: flt(data.for_quantity), + pending_qty: flt(data.pending_qty), + process_loss_qty: flt(data.process_loss_qty), + end_time: data.end_time, + }; + }; + + const save_and_continue = () => { + const args = get_payload(); + if (!args) return; + me.session_dialog.hide(); + frappe.call({ + method: "erpnext.manufacturing.page.shop_floor.shop_floor.save_and_continue", + args: args, + freeze: true, + freeze_message: __("Saving job card..."), + callback: () => me.reload(), + }); + }; + + const finalize_submit = (args) => { + frappe.call({ + method: "erpnext.manufacturing.page.shop_floor.shop_floor.complete_and_submit", + args: args, + freeze: true, + freeze_message: __("Submitting job card..."), + callback: (r) => { + me.reload(); + if (r.message && r.message.finished_good) { + me.prompt_manufacture_entry(jc.name); + } + }, + }); + }; + + const submit_session = () => { + const args = get_payload(); + if (!args) return; + me.session_dialog.hide(); + // Guided QC gate: a job card that requires inspection must pass an inline Quality Check + // before it is submitted (mirrors Job Card.validate_inspection on the server). Once the + // inspection is recorded, finalize the session submit. + if (jc.qc && jc.qc.required && jc.qc.status !== "Accepted") { + me.run_quality_check(jc, () => finalize_submit(args)); + } else { + finalize_submit(args); + } + }; + + me.session_dialog = new frappe.ui.Dialog({ + title: __("End Session"), + fields: fields, + primary_action_label: __("Submit"), + primary_action: submit_session, + secondary_action_label: __("Save & Continue"), + secondary_action: save_and_continue, + }); + me.session_dialog.show(); + me.bind_enter_submit(me.session_dialog); + } + + // ── Inline Quality Check ───────────────────────────────────────────────────── + // Fetch the operation's Quality Inspection template and open a guided pass/fail checklist. + // `on_pass` runs once the inspection has been recorded (and is not rejected). + run_quality_check(jc, on_pass) { + const me = this; + frappe.call({ + method: "erpnext.manufacturing.page.shop_floor.shop_floor.get_quality_inspection_checklist", + args: { job_card: jc.name }, + freeze: true, + freeze_message: __("Loading quality checklist..."), + callback: (r) => { + const info = r.message || {}; + if (!info.template || !(info.parameters || []).length) { + // Inspection is required but the operation has no template/parameters to fill — + // there is nothing to capture inline. Point the user at the configuration. + frappe.msgprint({ + title: __("Quality Inspection Template Missing"), + indicator: "orange", + message: __( + "This operation requires a Quality Inspection but no template with parameters is configured. Set a Quality Inspection Template on Operation {0} to inspect from the Shop Floor.", + [jc.operation || ""] + ), + }); + return; + } + me.show_qc_dialog(jc, info, on_pass); + }, + }); + } + + show_qc_dialog(jc, info, on_pass) { + const me = this; + const params = info.parameters || []; + // Per-row operator input, keyed by row index (avoids escaping issues with parameter names). + const state = {}; // idx -> "Accepted" | "Rejected" + + const rows = params + .map((p, i) => { + const spec = frappe.utils.escape_html(p.specification); + let criteria = ""; + if (p.numeric) { + const lo = p.min_value !== null && p.min_value !== undefined ? p.min_value : "−∞"; + const hi = p.max_value !== null && p.max_value !== undefined ? p.max_value : "∞"; + criteria = __("Acceptable range: {0} to {1}", [lo, hi]); + } else if (p.value) { + criteria = __("Expected: {0}", [frappe.utils.escape_html(p.value)]); + } + const control = p.numeric + ? `` + : ` + + + `; + return `
    +
    +
    ${spec}
    + ${criteria ? `
    ${criteria}
    ` : ""} +
    +
    ${control}
    +
    `; + }) + .join(""); + + const dialog = new frappe.ui.Dialog({ + title: __("Quality Check"), + size: "large", + fields: [ + { + fieldtype: "HTML", + options: `
    ${__( + "Inspect {0} for job card {1}", + [frappe.utils.escape_html(info.item_code || ""), frappe.utils.escape_html(jc.name)] + )}
    ${rows}
    `, + }, + ], + primary_action_label: __("Submit Inspection"), + primary_action: () => { + const readings = []; + let missing = false; + params.forEach((p, i) => { + if (p.numeric) { + const val = dialog.$wrapper.find(`.mes-qc-reading[data-idx="${i}"]`).val(); + if (val === "" || val === undefined || val === null) missing = true; + readings.push({ specification: p.specification, reading_value: val }); + } else { + if (!state[i]) missing = true; + readings.push({ + specification: p.specification, + status: state[i], + reading_value: "", + }); + } + }); + if (missing) { + frappe.msgprint(__("Please complete every check before submitting the inspection.")); + return; + } + dialog.hide(); + frappe.call({ + method: "erpnext.manufacturing.page.shop_floor.shop_floor.submit_quality_inspection", + args: { job_card: jc.name, readings: JSON.stringify(readings) }, + freeze: true, + freeze_message: __("Recording inspection..."), + callback: (r) => { + const res = r.message || {}; + if (res.status === "Rejected") { + // Don't auto-proceed on a rejected inspection — the server gate may block the + // submit anyway (per Stock Settings), and the operator should decide next steps. + frappe.msgprint({ + title: __("Inspection Rejected"), + indicator: "red", + message: __( + "Quality Inspection {0} is Rejected. Resolve the issue or follow your rejection process before submitting the job card.", + [res.name || ""] + ), + }); + me.reload(); + return; + } + if (on_pass) on_pass(); + }, + }); + }, + }); + + dialog.show(); + // Pass/Fail toggles for qualitative parameters. + dialog.$wrapper.find(".mes-qc-passfail button").on("click", function () { + const $btn = $(this); + const $grp = $btn.closest(".mes-qc-passfail"); + $grp.find("button").removeClass("active"); + $btn.addClass("active"); + state[$grp.attr("data-idx")] = $btn.attr("data-val"); + }); + } + + prompt_manufacture_entry(jc_name) { + const me = this; + const dialog = new frappe.ui.Dialog({ + title: __("Job Card Submitted"), + fields: [ + { + fieldtype: "HTML", + options: ` +
    +
    + ${__("Job card {0} has been submitted.", [frappe.utils.escape_html(jc_name)])} +
    +
    + ${__("Create a Manufacture stock entry for the finished goods?")} +
    +
    + `, + }, + ], + primary_action_label: __("Make Manufacture Entry"), + primary_action: () => { + dialog.hide(); + me.make_manufacture_entry(jc_name); + }, + secondary_action_label: __("Skip"), + secondary_action: () => dialog.hide(), + }); + dialog.show(); + this.bind_enter_submit(dialog); + } + + submit_job_card(jc_name) { + const me = this; + frappe.confirm(__("Submit job card {0}? This finalizes the job card.", [jc_name]), () => { + frappe.call({ + method: "erpnext.manufacturing.page.shop_floor.shop_floor.submit_job_card", + args: { job_card: jc_name }, + freeze: true, + freeze_message: __("Submitting job card..."), + callback: () => me.reload(), + }); + }); + } + + make_manufacture_entry(jc_name) { + frappe.call({ + method: "erpnext.manufacturing.page.shop_floor.shop_floor.make_manufacture_stock_entry", + args: { job_card: jc_name }, + freeze: true, + freeze_message: __("Preparing stock entry..."), + callback: (r) => { + if (r.message && r.message.name) { + window.open(`/app/stock-entry/${encodeURIComponent(r.message.name)}`, "_blank"); + } + }, + }); + } + + transfer_materials(jc_name) { + if (!jc_name) return; + frappe.call({ + method: "erpnext.manufacturing.doctype.job_card.job_card.make_stock_entry", + args: { source_name: jc_name }, + callback: (r) => { + const doc = frappe.model.sync(r.message); + frappe.set_route("Form", doc[0].doctype, doc[0].name); + }, + }); + } + + update_job_card(job_card, method, data, on_success) { + const me = this; + frappe.call({ + method: "erpnext.manufacturing.doctype.workstation.workstation.update_job_card", + args: { + job_card: job_card, + method: method, + start_time: data.start_time || "", + employees: data.employees || [], + end_time: data.end_time || "", + qty: data.qty || 0, + for_quantity: data.for_quantity || 0, + pending_qty: data.pending_qty || 0, + process_loss_qty: data.process_loss_qty || 0, + auto_submit: data.auto_submit || 0, + }, + freeze: true, + freeze_message: __("Updating job card..."), + callback: () => { + me.reload(); + if (on_success) on_success(); + }, + }); + } + + // ── Timers ──────────────────────────────────────────────────────────────── + start_timer_for(jc, $container) { + let elapsed = this.elapsed_seconds(jc); + this.render_timer(jc.name, elapsed, $container); + this.timer_intervals[jc.name] = setInterval(() => { + elapsed += 1; + this.render_timer(jc.name, elapsed, $container); + }, 1000); + } + + elapsed_seconds(jc) { + let total = 0; + for (const log of jc.time_logs || []) { + if (log.to_time) { + if (log.time_in_mins) { + total += flt(log.time_in_mins, 2) * 60; + } else { + total += moment(log.to_time).diff(log.from_time, "seconds"); + } + } else { + total += moment().diff(log.from_time, "seconds"); + } + } + return total; + } + + render_timer(jc_name, seconds, $container) { + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds - h * 3600) / 60); + const s = cint(seconds - h * 3600 - m * 60); + const pad = (n) => (n < 10 ? "0" + n : String(n)); + + const scope = $container || this.wrapper; + const timer = scope.find(`.mes-job-timer[data-job-card="${jc_name}"]`); + timer.find(".h").text(pad(h)); + timer.find(".m").text(pad(m)); + timer.find(".s").text(pad(s)); + } + + // ── Realtime + lifecycle ─────────────────────────────────────────────────── + bind_realtime() { + frappe.realtime.on("update_workstation_status", (data) => { + if (data && data.name === this.op_state.workstation) { + this.reload(); + } + }); + } + + bind_lifecycle() { + // Frappe has no on_page_hide hook, so toggle immersive mode + keyboard binding on + // route changes ourselves. + this._route_handler = () => { + const on_page = (frappe.get_route_str() || "").startsWith("shop-floor"); + if (on_page) { + $(document.body).addClass("shop-floor-active"); + this.bind_keys(); + } else { + $(document.body).removeClass("shop-floor-active"); + this.unbind_keys(); + this.clear_timers(); + } + }; + frappe.router.on("change", this._route_handler); + } + + on_show() { + $(document.body).addClass("shop-floor-active"); + this.bind_keys(); + // Cached re-navigation (e.g. the Work Order "Shop Floor" button) lands here with fresh + // route_options; init() handles the very first load before we're initialized. + if (this.initialized) this.apply_route_options(); + } + + // ── Keyboard ──────────────────────────────────────────────────────────────── + bind_keys() { + $(document).off("keydown.shopfloor"); + $(document).on("keydown.shopfloor", (e) => this.handle_key(e)); + } + + unbind_keys() { + $(document).off("keydown.shopfloor"); + } + + is_typing(e) { + const tag = (e.target.tagName || "").toLowerCase(); + return tag === "input" || tag === "textarea" || tag === "select" || e.target.isContentEditable; + } + + handle_key(e) { + // Let dialogs own the keyboard while open. + if ($(".modal:visible").length) return; + + const typing = this.is_typing(e); + + // Escape works even while typing (blur the search / close the detail pane). + if (e.key === "Escape") { + if (typing) { + e.target.blur(); + return; + } + if (this.view === "manager" && this.selected_wo) { + this.close_wo(); + e.preventDefault(); + } + return; + } + + if (typing) return; + + switch (e.key) { + case "?": + this.show_help(); + e.preventDefault(); + return; + case "/": + this.topbar_center.find(".sf-search-input").focus(); + e.preventDefault(); + return; + case "r": + this.refresh(); + e.preventDefault(); + return; + case "b": + this.open_scanner(); + e.preventDefault(); + return; + case "1": + case "2": + if (this.view === "manager" && MANAGER_BUCKETS[cint(e.key) - 1]) { + this.switch_bucket(MANAGER_BUCKETS[cint(e.key) - 1].key); + e.preventDefault(); + } + return; + } + + // View switch chord: "g" then "m"/"o". + if (e.key === "g") { + this._g_pending = true; + setTimeout(() => (this._g_pending = false), 600); + return; + } + if (this._g_pending && (e.key === "m" || e.key === "o")) { + this._g_pending = false; + if (this.can_manage) this.set_view(e.key === "m" ? "manager" : "operator"); + return; + } + + // Navigation. + if (e.key === "ArrowDown" || e.key === "j") { + this.move_focus(1); + e.preventDefault(); + return; + } + if (e.key === "ArrowUp" || e.key === "k") { + this.move_focus(-1); + e.preventDefault(); + return; + } + if (e.key === "Enter") { + this.activate_focus(); + e.preventDefault(); + return; + } + + // Job actions on the focused card — reuse the rendered buttons. + const map = { + s: ".mes-btn-start, .mes-btn-resume", + p: ".mes-btn-pause, .mes-btn-resume", + e: ".mes-btn-end-session", + t: ".mes-btn-transfer", + }; + if (e.key === "S" && e.shiftKey) { + this.click_job_action(".mes-btn-submit"); + e.preventDefault(); + return; + } + if (map[e.key]) { + this.click_job_action(map[e.key]); + e.preventDefault(); + } + } + + // Job actions act on the focused job card (operator view); when the focus is on a board + // work order (manager view with the detail open) they fall back to the detail's active job. + click_job_action(selector) { + const $el = this.focused_el(); + if ($el && $el.attr("data-kind") === "job") { + const $btn = $el.find(selector).filter(":visible").first(); + if ($btn.length) { + $btn.trigger("click"); + return; + } + } + const scope = this.current_op_container(); + if (scope && scope.length) { + const $btn = scope.find(selector).filter(":visible").first(); + if ($btn.length) $btn.trigger("click"); + } + } + + focusables() { + // Manager always navigates the board work orders — even with the detail open, so the + // arrow keys switch work orders. The standalone operator view navigates its job cards. + const scope = this.view === "manager" ? this.board_container : this.current_op_container(); + if (!scope || !scope.length) return $(); + return scope.find("[data-sf-focusable]"); + } + + move_focus(delta) { + const $items = this.focusables(); + if (!$items.length) return; + this.focus_index = Math.max(0, Math.min($items.length - 1, this.focus_index + delta)); + $items.removeClass("sf-focused"); + const $target = $items.eq(this.focus_index); + $target.addClass("sf-focused"); + $target[0].scrollIntoView({ block: "nearest", behavior: "smooth" }); + // Browsing work orders with the detail already open → switch the detail to the focused one. + if (this.view === "manager" && this.selected_wo && $target.attr("data-kind") === "wo") { + this.open_wo($target.attr("data-name")); + } + } + + focused_el() { + const $items = this.focusables(); + if (this.focus_index < 0 || this.focus_index >= $items.length) return null; + return $items.eq(this.focus_index); + } + + activate_focus() { + const $el = this.focused_el(); + if (!$el) return; + if ($el.attr("data-kind") === "wo") { + this.open_wo($el.attr("data-name")); + } else { + // First visible primary button drives the job card (Start / Resume / End Session). + const $btn = $el.find(".btn-primary:visible").first(); + if ($btn.length) $btn.trigger("click"); + } + } + + show_help() { + const rows = [ + ["?", __("Show this help")], + ["/", __("Search work orders")], + ["r", __("Refresh")], + ["b", __("Scan job card")], + ["g then m / o", __("Switch Board / Operator view")], + ["1 / 2", __("Switch board tab")], + ["↑ / ↓ or j / k", __("Move selection")], + ["Enter", __("Open work order / run primary action")], + ["Esc", __("Close detail / blur search")], + ["s", __("Start / Resume job")], + ["p", __("Pause / Resume job")], + ["e", __("End session for active job")], + ["t", __("Transfer materials")], + ["Shift + S", __("Submit focused job card")], + ]; + const html = `
    ${rows + .map((r) => `
    ${r[0]}${r[1]}
    `) + .join("")}
    `; + const d = new frappe.ui.Dialog({ + title: __("Keyboard Shortcuts"), + fields: [{ fieldtype: "HTML", options: html }], + }); + d.show(); + } + + // ── Scanner ────────────────────────────────────────────────────────────── + open_scanner() { + const me = this; + const dialog = new frappe.ui.Dialog({ + title: __("Scan Job Card"), + fields: [ + { + label: __("Scan or enter Job Card"), + fieldname: "job_card", + fieldtype: "Data", + options: "Barcode", + }, + ], + primary_action_label: __("Continue"), + primary_action: (values) => { + if (!values.job_card) return; + dialog.hide(); + me.handle_scanned_job_card(values.job_card); + }, + }); + dialog.show(); + this.bind_enter_submit(dialog); + } + + handle_scanned_job_card(job_card) { + const me = this; + const jc = (this.job_cards || []).find((j) => j.name === job_card); + if (jc) { + me.route_scanned_action(jc); + return; + } + frappe.db.get_value("Job Card", job_card, ["status", "is_paused", "docstatus"]).then((r) => { + const data = r && r.message; + if (!data || !data.status) { + frappe.msgprint(__("Job Card {0} was not found.", [job_card])); + return; + } + if (cint(data.docstatus) === 1) { + frappe.msgprint(__("Job Card {0} is already submitted.", [job_card])); + } else if (cint(data.is_paused)) { + me.resume_job(job_card); + } else if (data.status === "Work In Progress") { + frappe.msgprint( + __( + "Job Card {0} is already running. Open its machine or work order to pause or complete it.", + [job_card] + ) + ); + } else if (data.status === "Completed") { + me.submit_job_card(job_card); + } else { + me.start_job(job_card); + } + }); + } + + route_scanned_action(jc) { + const me = this; + if (jc.docstatus === 1) { + frappe.msgprint(__("Job Card {0} is already submitted.", [jc.name])); + return; + } + if (jc.status === "Completed") { + me.submit_job_card(jc.name); + return; + } + if (jc.is_paused) { + me.resume_job(jc.name); + return; + } + const last_log = jc.time_logs && jc.time_logs.length ? jc.time_logs[jc.time_logs.length - 1] : null; + const is_running = !!(last_log && !last_log.to_time); + if (is_running) { + me.prompt_running_action(jc); + } else { + me.start_job(jc.name); + } + } + + prompt_running_action(jc) { + const me = this; + const dialog = new frappe.ui.Dialog({ + title: __("Job {0} is running", [jc.name]), + fields: [ + { + fieldtype: "HTML", + options: ` +
    + ${__("{0} is already in progress. Pause it or complete the session.", [ + frappe.utils.escape_html(jc.finished_good || jc.production_item || jc.name), + ])} +
    + `, + }, + ], + primary_action_label: __("Complete"), + primary_action: () => { + dialog.hide(); + me.end_session(jc.name); + }, + secondary_action_label: __("Pause"), + secondary_action: () => { + dialog.hide(); + me.pause_job(jc.name); + }, + }); + dialog.show(); + this.bind_enter_submit(dialog); + } + + // ── Route options (e.g. the Work Order "Shop Floor" button) ──────────────── + apply_route_options() { + const opts = frappe.route_options; + if (!opts || (!opts.work_order && !opts.workstation)) { + return; + } + frappe.route_options = null; + + // A specific work order / machine was requested — show it in the operator view. + this.view = "operator"; + this.render_shell_controls(); + this.render_view(); + Promise.all([ + this.work_order_filter.set_value(opts.work_order || ""), + this.workstation_filter.set_value(opts.workstation || ""), + ]).then(() => this.load_operator()); + } + + // ── Styles ────────────────────────────────────────────────────────────────── + styles() { + return ``; + } +} + +frappe.ui.ShopFloor = ShopFloor; From 317dd18ce57ec5a07b0c6560e1ecd6f724013b23 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 9 Aug 2026 21:43:00 +0530 Subject: [PATCH 089/134] fix(manufacturing): align quantity split rounding --- erpnext/manufacturing/doctype/job_card/job_card.py | 6 +++++- erpnext/manufacturing/doctype/job_card/test_job_card.py | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index ba41a7c67fe..572d1f6e290 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -1567,7 +1567,11 @@ class JobCard(Document): return precision = self.precision("total_completed_qty") - accounted_qty = flt(kwargs.qty) + flt(kwargs.pending_qty) + flt(kwargs.process_loss_qty) + accounted_qty = flt( + flt(kwargs.qty, precision) + + flt(kwargs.pending_qty, precision) + + flt(kwargs.process_loss_qty, precision) + ) if flt(accounted_qty, precision) == flt(kwargs.for_quantity, precision): return diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index aa9b1e1e651..7d70c2e8d90 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -1831,6 +1831,12 @@ class TestJobCard(ERPNextTestSuite): frappe._dict(for_quantity=3, qty=3, pending_qty=2, process_loss_qty=0), ) + self.assertRaises( + frappe.ValidationError, + jc.validate_completion_qty_split, + frappe._dict(for_quantity=1, qty=0.3334, pending_qty=0.3334, process_loss_qty=0.3334), + ) + def create_bom_with_multiple_operations(): "Create a BOM with multiple operations and Material Transfer against Job Card" From 94c81965a7c08c084119ecfb2356d5e2c9d1e1ae Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 9 Aug 2026 23:02:58 +0530 Subject: [PATCH 090/134] test(manufacturing): cover job card UOM backfill --- .../doctype/job_card/test_job_card.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index cfd559202be..1d58ebb5bce 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -22,6 +22,7 @@ from erpnext.manufacturing.doctype.job_card.job_card import ( from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record from erpnext.manufacturing.doctype.work_order.work_order import WorkOrder, make_work_order from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation +from erpnext.patches.v16_0.set_stock_uom_in_job_card import execute as set_stock_uom_in_job_card from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.tests.utils import ERPNextTestSuite @@ -909,6 +910,47 @@ class TestJobCard(ERPNextTestSuite): self.assertEqual(job_card.stock_uom, frappe.db.get_value("Item", item_code, "stock_uom")) + def test_stock_uom_patch_backfills_legacy_job_cards(self): + suffix = random_string(8) + finished_good = create_item(f"Stock UOM Patch FG {suffix}", stock_uom="Kg") + production_item = create_item(f"Stock UOM Patch Product {suffix}", stock_uom="Nos") + + finished_good_job_card = self.get_first_job_card( + make_wo_order_test_record(item="_Test FG Item 2", qty=5).name + ) + production_item_job_card = self.get_first_job_card( + make_wo_order_test_record(item="_Test FG Item 2", qty=6).name + ) + + frappe.db.set_value( + "Job Card", + finished_good_job_card.name, + { + "finished_good": finished_good.name, + "production_item": production_item.name, + "stock_uom": None, + }, + update_modified=False, + ) + frappe.db.set_value( + "Job Card", + production_item_job_card.name, + {"finished_good": None, "production_item": production_item.name, "stock_uom": None}, + update_modified=False, + ) + + set_stock_uom_in_job_card() + + self.assertEqual(frappe.db.get_value("Job Card", finished_good_job_card.name, "stock_uom"), "Kg") + self.assertEqual(frappe.db.get_value("Job Card", production_item_job_card.name, "stock_uom"), "Nos") + + frappe.db.set_value( + "Job Card", finished_good_job_card.name, "stock_uom", "Nos", update_modified=False + ) + set_stock_uom_in_job_card() + + self.assertEqual(frappe.db.get_value("Job Card", finished_good_job_card.name, "stock_uom"), "Nos") + def test_completion_qty_reduces_for_quantity_without_process_loss(self): work_order = make_wo_order_test_record(item="_Test FG Item 2", qty=5) From a90907fda6de78c432778e116e287ec1bb373bfa Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Sun, 9 Aug 2026 20:22:12 +0530 Subject: [PATCH 091/134] fix: reflect in-invoice receivable settlements in Sales Register ledger view (cherry picked from commit 40c356d1661427900276252d2acb294f50badaa9) --- .../report/sales_register/sales_register.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py index 79a2ef1d92d..d789bcd6452 100644 --- a/erpnext/accounts/report/sales_register/sales_register.py +++ b/erpnext/accounts/report/sales_register/sales_register.py @@ -160,7 +160,8 @@ def _execute(filters, additional_table_columns=None): row.update( { "debit": inv.base_grand_total, - "credit": 0.0, + # credits the invoice itself posts to the receivable (mirrors its GL) + "credit": get_in_invoice_receivable_credit(inv), "outstanding_amount": flt( (inv.outstanding_amount * (inv.conversion_rate or 1)), outstanding_precision ), @@ -181,6 +182,14 @@ def _execute(filters, additional_table_columns=None): return columns, res, None, None, None, include_payments +def get_in_invoice_receivable_credit(inv): + # amount the invoice settles against its own receivable, matching the invoice's GL entries + credit = flt(inv.loyalty_amount) # loyalty redemption, POS or not + if inv.is_pos: # POS payments and write-off credit the receivable only on POS invoices + credit += flt(inv.base_paid_amount) - flt(inv.base_change_amount) + flt(inv.base_write_off_amount) + return credit + + def get_columns(invoice_list, additional_table_columns, include_payments=False): """return columns based on filters""" columns = [ @@ -447,6 +456,11 @@ def get_invoices(filters, additional_query_columns): si.base_net_total, si.base_grand_total, si.base_rounded_total, + si.is_pos, + si.base_paid_amount, + si.base_change_amount, + si.base_write_off_amount, + si.loyalty_amount, si.outstanding_amount, si.is_internal_customer, si.represents_company, From 09c0110352fb8553089f0533c053806de56bb6bb Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Sun, 9 Aug 2026 20:22:14 +0530 Subject: [PATCH 092/134] test: cover POS-paid invoice in Sales Register ledger view (cherry picked from commit 45a929447622bcc08f3c61df6dae5725019a7522) --- .../sales_register/test_sales_register.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/erpnext/accounts/report/sales_register/test_sales_register.py b/erpnext/accounts/report/sales_register/test_sales_register.py index 6f0107630cf..5d5cc5074dc 100644 --- a/erpnext/accounts/report/sales_register/test_sales_register.py +++ b/erpnext/accounts/report/sales_register/test_sales_register.py @@ -1,6 +1,7 @@ import frappe from frappe.utils import add_days, flt, getdate, today +from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.report.sales_register.sales_register import execute from erpnext.accounts.test.accounts_mixin import AccountsTestMixin @@ -218,6 +219,46 @@ class TestItemWiseSalesRegister(ERPNextTestSuite, AccountsTestMixin): result_output = {k: v for k, v in filtered_output[0].items() if k in expected_result} self.assertDictEqual(result_output, expected_result) + def test_ledger_view_nets_pos_paid_invoice(self): + # A POS payment settles the receivable inside the invoice, so the ledger view must credit it + # and net to zero instead of showing a phantom outstanding. + make_pos_profile() + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + price_list_rate=100, + do_not_save=1, + ) + si.is_pos = 1 + si.append("payments", {"mode_of_payment": "Cash", "amount": 100}) + si = si.save().submit() + self.assertEqual(flt(si.outstanding_amount), 0.0) + + filters = frappe._dict( + { + "from_date": today(), + "to_date": today(), + "company": self.company, + "include_payments": True, + "customer": self.customer, + } + ) + rows = execute(filters)[1] + inv_row = next(x for x in rows if x.get("voucher_no") == si.name) + + self.assertEqual(flt(inv_row.get("debit")), 100.0) + self.assertEqual(flt(inv_row.get("credit")), 100.0) + + # running balance is unchanged by a fully-paid POS invoice + idx = rows.index(inv_row) + self.assertEqual(flt(inv_row.get("balance")), flt(rows[idx - 1].get("balance"))) + def test_outstanding_currency_conversion(self): foreign_invoice = create_sales_invoice( customer="_Test Customer", From dff4ec6a74c6022464bb17f0e5e02d68cccbeda8 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 9 Aug 2026 19:25:49 +0000 Subject: [PATCH 093/134] fix: escape `customer_details` on lead creation from appointment (backport #57947) (#57949) Co-authored-by: Diptanil Saha --- erpnext/crm/doctype/appointment/appointment.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index da91a73f105..8beed20befa 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -13,6 +13,7 @@ from frappe.model.document import Document from frappe.share import add_docshare from frappe.utils import add_to_date, cint, date_diff, get_datetime, get_url, getdate, now, now_datetime from frappe.utils.data import sha256_hash +from frappe.utils.html_utils import escape_html from erpnext.setup.doctype.holiday_list.holiday_list import is_holiday @@ -269,7 +270,11 @@ class Appointment(Document): if self.customer_details: lead.append( "notes", - {"note": self.customer_details, "added_by": frappe.session.user, "added_on": now()}, + { + "note": escape_html(self.customer_details), + "added_by": frappe.session.user, + "added_on": now(), + }, ) self.party = lead.insert(ignore_permissions=True).name From ca5418e2c92e8d245c50e785038ea53e6a7fb262 Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Thu, 6 Aug 2026 00:33:24 +0530 Subject: [PATCH 094/134] fix: keep asset repair downtime in sync with entered dates (cherry picked from commit 4406bb906890f7b431aba66c074ad82231f5ba8b) --- .../doctype/asset_repair/asset_repair.js | 45 ++++++++++++------- .../doctype/asset_repair/asset_repair.py | 8 ++++ 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 2920ff7e381..8e36b6d0be9 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -116,24 +116,39 @@ frappe.ui.form.on("Asset Repair", { }, repair_status: (frm) => { - if (frm.doc.completion_date && frm.doc.repair_status == "Completed") { - frappe.call({ - method: "erpnext.assets.doctype.asset_repair.asset_repair.get_downtime", - args: { - failure_date: frm.doc.failure_date, - completion_date: frm.doc.completion_date, - }, - callback: function (r) { - if (r.message) { - frm.set_value("downtime", r.message + " Hrs"); - } - }, - }); - } - if (frm.doc.repair_status == "Completed" && !frm.doc.completion_date) { frm.set_value("completion_date", frappe.datetime.now_datetime()); } + + frm.events.set_downtime(frm); + }, + + failure_date: (frm) => { + frm.events.set_downtime(frm); + }, + + completion_date: (frm) => { + frm.events.set_downtime(frm); + }, + + set_downtime: (frm) => { + if (frm.doc.repair_status != "Completed" || !frm.doc.failure_date || !frm.doc.completion_date) { + frm.set_value("downtime", null); + return; + } + + frappe.call({ + method: "erpnext.assets.doctype.asset_repair.asset_repair.get_downtime", + args: { + failure_date: frm.doc.failure_date, + completion_date: frm.doc.completion_date, + }, + callback: function (r) { + if (r.message) { + frm.set_value("downtime", r.message + " Hrs"); + } + }, + }); }, stock_items_on_form_rendered() { diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 4fc8981d200..a96c204382f 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -68,6 +68,7 @@ class AssetRepair(AccountsController): self.calculate_repair_cost() self.calculate_total_repair_cost() self.check_repair_status() + self.set_downtime() def validate_asset(self): if self.asset_doc.status in ("Sold", "Scrapped"): @@ -239,6 +240,13 @@ class AssetRepair(AccountsController): if self.repair_status == "Pending" and self.docstatus == 1: frappe.throw(_("Please update Repair Status.")) + def set_downtime(self): + # keep downtime in sync with the entered dates, regardless of edit order + if self.repair_status == "Completed" and self.failure_date and self.completion_date: + self.downtime = f"{get_downtime(self.failure_date, self.completion_date)} Hrs" + else: + self.downtime = None + def update_asset_value(self): total_repair_cost = self.total_repair_cost if self.docstatus == 1 else -1 * self.total_repair_cost From 4f3eef92f0b4c035a0ad12694624605e57bfc406 Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Thu, 6 Aug 2026 00:33:26 +0530 Subject: [PATCH 095/134] test: assert asset repair downtime recalculates on date change (cherry picked from commit 8269f8a36233375cdba73e0becb8fec6e6db410f) --- .../doctype/asset_repair/test_asset_repair.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 1f158e1ea8b..030a6a2f723 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -97,6 +97,21 @@ class TestAssetRepair(ERPNextTestSuite): asset_repair = create_asset_repair(submit=1) self.assertNotEqual(asset_repair.repair_status, "Pending") + def test_downtime_stays_in_sync_with_dates(self): + asset = create_asset(submit=1) + asset_repair = create_asset_repair(asset=asset) + + asset_repair.failure_date = "2026-07-31 09:00:00" + asset_repair.completion_date = "2026-07-31 11:00:00" + asset_repair.repair_status = "Completed" + asset_repair.save() + self.assertEqual(asset_repair.downtime, "2.0 Hrs") + + # editing a date must refresh downtime, not leave a stale value + asset_repair.completion_date = "2026-07-31 14:30:00" + asset_repair.save() + self.assertEqual(asset_repair.downtime, "5.5 Hrs") + def test_stock_items(self): asset_repair = create_asset_repair(stock_consumption=1) self.assertTrue(asset_repair.stock_consumption) From a4d94113457e4ac3873488998bd3f1ec65555aa4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 12:11:58 +0530 Subject: [PATCH 096/134] fix: skip incoming rate calc when serial no qty is zero (backport #57427) (#57957) fix: skip incoming rate calc when serial no qty is zero (#57427) (cherry picked from commit a25decfa50a355307ad187201352f7c465d79eba) Co-authored-by: Shllokkk <140623894+Shllokkk@users.noreply.github.com> --- erpnext/stock/serial_batch_bundle.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 5fbb8a9c978..e8e674ee7b2 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -787,6 +787,9 @@ class SerialNoValuation(DeprecatedSerialNoValuation): return is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse) def get_incoming_rate(self): + if not self.sle.actual_qty and self.sle.voucher_type == "Stock Reconciliation": + return 0.0 + return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty)) def get_incoming_rate_of_serial_no(self, serial_no): From 2b020c2fcf743d8728e7ec8cd74940efd7ef5065 Mon Sep 17 00:00:00 2001 From: Pandiyan P Date: Mon, 10 Aug 2026 14:03:16 +0530 Subject: [PATCH 097/134] fix: re-check future sle before queuing repost on submit (#57664) (#57961) * test: cover both repost branches and the no-repost case * fix: queue repost for entries backdated by a concurrent submit --------- (cherry picked from commit 399ff463ccf417a34134edd2bac40aa7263a6cfb) Co-authored-by: nareshkannasln --- erpnext/controllers/stock_controller.py | 5 + .../tests/test_stock_controller.py | 172 ++++++++++++++++++ erpnext/stock/stock_ledger.py | 4 +- 3 files changed, 180 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index f10706b2ac6..3d2929e29df 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -2417,6 +2417,11 @@ def is_reposting_pending(): ) +def invalidate_future_sle_cache(voucher_type, voucher_no): + if hasattr(frappe.local, "future_sle"): + frappe.local.future_sle.pop((voucher_type, voucher_no), None) + + def future_sle_exists(args, sl_entries=None): from erpnext.stock.utils import get_combine_datetime diff --git a/erpnext/controllers/tests/test_stock_controller.py b/erpnext/controllers/tests/test_stock_controller.py index 7720994419b..4a059e3db78 100644 --- a/erpnext/controllers/tests/test_stock_controller.py +++ b/erpnext/controllers/tests/test_stock_controller.py @@ -2,6 +2,7 @@ # For license information, please see license.txt import frappe +from frappe.utils import add_days, today from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry from erpnext.controllers.stock_controller import ( @@ -75,3 +76,174 @@ class TestLedgerPreviewPermission(ERPNextTestSuite): stock_ledger_result = show_stock_ledger_preview(company, "Purchase Receipt", pr.name) self.assertTrue(stock_ledger_result.get("sl_data")) + + +class TestStockControllerConversions(ERPNextTestSuite): + @staticmethod + def _cancel_and_delete(doctype, name): + if not frappe.db.exists(doctype, name): + return + doc = frappe.get_doc(doctype, name) + if doc.docstatus == 1: + doc.cancel() + frappe.delete_doc(doctype, name, force=1) + + def test_future_sle_exists_detects_later_entries(self): + # A later SLE for the same item+warehouse must be reported as a future entry, which + # exercises the GROUP BY query in future_sle_exists on both engines. + from erpnext.controllers.stock_controller import future_sle_exists + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + item = make_item("_Test Future SLE Item", {"is_stock_item": 1}).name + se = make_stock_entry(item_code=item, target="_Test Warehouse - _TC", qty=10, basic_rate=100) + self.addCleanup(self._cancel_and_delete, "Stock Entry", se.name) + + # Pretend a different voucher posts a day earlier for the same item/warehouse: the existing + # (later) SLE must be reported as a future entry. + args = frappe._dict( + voucher_type="Stock Entry", + voucher_no="_TEST-NONEXISTENT-SE", + posting_date=add_days(today(), -1), + posting_time="00:00:00", + ) + sl_entries = [frappe._dict(item_code=item, warehouse="_Test Warehouse - _TC")] + + self.assertTrue(future_sle_exists(args, sl_entries)) + + def _make_opening_entry(self, item, warehouse): + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + opening = make_stock_entry( + item_code=item, + target=warehouse, + qty=100, + basic_rate=100, + posting_date=add_days(today(), -5), + posting_time="01:00:00", + ) + self.addCleanup(self._cancel_and_delete, "Stock Entry", opening.name) + + return opening + + def _later_sle(self, item, warehouse, opening): + sle = frappe.get_doc( + { + "doctype": "Stock Ledger Entry", + "item_code": item, + "warehouse": warehouse, + "posting_date": today(), + "posting_time": "12:00:00", + "voucher_type": "Stock Entry", + "voucher_no": opening.name, + "actual_qty": 7, + "incoming_rate": 100, + "qty_after_transaction": 107, + "valuation_rate": 100, + "stock_value": 10700, + "company": opening.company, + "stock_uom": "Nos", + } + ) + sle.flags.ignore_permissions = True + sle.flags.ignore_links = True + + return sle + + def _submit_entry(self, item, warehouse, inject=None): + from erpnext.stock import stock_ledger + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + original_make_entry = stock_ledger.make_entry + injected = [] + + def make_entry_with_injection(*args, **kwargs): + if inject is not None and not injected: + injected.append(True) + inject.submit() + return original_make_entry(*args, **kwargs) + + stock_ledger.make_entry = make_entry_with_injection + try: + entry = make_stock_entry( + item_code=item, + target=warehouse, + qty=5, + basic_rate=500, + posting_date=today(), + posting_time="06:00:00", + ) + finally: + stock_ledger.make_entry = original_make_entry + + self.addCleanup(self._cancel_and_delete, "Stock Entry", entry.name) + if inject is not None: + self.assertTrue(injected, "the later SL Entry was not written during the submit") + + return entry + + def _reposts_queued_for(self, item, warehouse, voucher_no): + names = set( + frappe.get_all( + "Repost Item Valuation", + filters={"docstatus": 1, "item_code": item, "warehouse": warehouse}, + pluck="name", + ) + ) | set( + frappe.get_all( + "Repost Item Valuation", + filters={"docstatus": 1, "voucher_no": voucher_no}, + pluck="name", + ) + ) + for name in names: + self.addCleanup(frappe.delete_doc, "Repost Item Valuation", name, force=1) + + return names + + def test_repost_queued_for_entry_backdated_while_its_sl_entries_were_written(self): + from erpnext.stock.doctype.item.test_item import make_item + + item = make_item("_Test Concurrent Backdated Item", {"is_stock_item": 1}).name + warehouse = "_Test Warehouse - _TC" + + opening = self._make_opening_entry(item, warehouse) + backdated = self._submit_entry(item, warehouse, inject=self._later_sle(item, warehouse, opening)) + + self.assertTrue( + self._reposts_queued_for(item, warehouse, backdated.name), + "No Repost Item Valuation was queued for an entry that a later SL Entry made backdated", + ) + + def test_repost_queued_against_voucher_when_item_based_reposting_is_off(self): + from erpnext.stock.doctype.item.test_item import make_item + + item = make_item("_Test Voucher Based Repost Item", {"is_stock_item": 1}).name + warehouse = "_Test Warehouse - _TC" + + with self.change_settings("Stock Reposting Settings", item_based_reposting=0): + opening = self._make_opening_entry(item, warehouse) + backdated = self._submit_entry(item, warehouse, inject=self._later_sle(item, warehouse, opening)) + + self.assertTrue( + frappe.get_all( + "Repost Item Valuation", + filters={"docstatus": 1, "voucher_no": backdated.name}, + pluck="name", + ), + "No voucher based Repost Item Valuation was queued", + ) + + def test_no_repost_queued_when_nothing_was_written_after_the_entry(self): + from erpnext.stock.doctype.item.test_item import make_item + + item = make_item("_Test Unconcurrent Item", {"is_stock_item": 1}).name + warehouse = "_Test Warehouse - _TC" + + self._make_opening_entry(item, warehouse) + entry = self._submit_entry(item, warehouse) + + self.assertFalse( + self._reposts_queued_for(item, warehouse, entry.name), + "A Repost Item Valuation was queued for an entry with nothing posted after it", + ) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 775b5c95dad..0ee68469706 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -92,7 +92,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc such cases certain validations need to be ignored (like negative stock) """ - from erpnext.controllers.stock_controller import future_sle_exists + from erpnext.controllers.stock_controller import future_sle_exists, invalidate_future_sle_cache if sl_entries: validate_stock_frozen_by_closing_entry(sl_entries) @@ -144,6 +144,8 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc _("Item {0} ignored since it is not a stock item").format(args.get("item_code")) ) + invalidate_future_sle_cache(sl_entries[0].get("voucher_type"), sl_entries[0].get("voucher_no")) + def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False, cancelled=False): if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation": From 0a9b632925d69854c8f8013b92d1ce865f7f1150 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 09:36:20 +0000 Subject: [PATCH 098/134] fix: field validation and perm checks on `get_stock_reservation_entries_for_voucher` (backport #57968) (#57970) Co-authored-by: Diptanil Saha --- .../stock_reservation_entry.py | 24 ++++++++++++++----- .../test_stock_reservation_entry.py | 16 ++++++------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py index 837c4027c3c..d01434f35a0 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -1064,7 +1064,7 @@ def get_ssb_bundle_for_voucher(sre: dict) -> object: def has_reserved_stock(voucher_type: str, voucher_no: str, voucher_detail_no: str | None = None) -> bool: """Returns True if there is any Stock Reservation Entry for the given voucher.""" - if get_stock_reservation_entries_for_voucher( + if _get_stock_reservation_entries_for_voucher( voucher_type, voucher_no, voucher_detail_no, fields=["name"], ignore_status=True ): return True @@ -1800,7 +1800,7 @@ def cancel_stock_reservation_entries( sre_list = {} if voucher_type and voucher_no: - sre_list = get_stock_reservation_entries_for_voucher( + sre_list = _get_stock_reservation_entries_for_voucher( voucher_type, voucher_no, voucher_detail_no, fields=["name"] ) elif from_voucher_type and from_voucher_no: @@ -1843,6 +1843,21 @@ def get_stock_reservation_entries_for_voucher( ) -> list[dict]: """Returns list of Stock Reservation Entries against a Voucher.""" + return _get_stock_reservation_entries_for_voucher( + voucher_type, voucher_no, voucher_detail_no, fields, ignore_status, ignore_permissions=False + ) + + +def _get_stock_reservation_entries_for_voucher( + voucher_type: str, + voucher_no: str, + voucher_detail_no: str | None = None, + fields: list[str] | None = None, + ignore_status: bool = False, + ignore_permissions: bool = True, +) -> list[dict]: + """Returns list of Stock Reservation Entries against a Voucher.""" + if not fields or not isinstance(fields, list): fields = [ "name", @@ -1856,14 +1871,11 @@ def get_stock_reservation_entries_for_voucher( sre = frappe.qb.DocType("Stock Reservation Entry") query = ( - frappe.qb.from_(sre) + frappe.get_query(sre, fields=fields, ignore_permissions=ignore_permissions) .where((sre.docstatus == 1) & (sre.voucher_type == voucher_type) & (sre.voucher_no == voucher_no)) .orderby(sre.creation) ) - for field in fields: - query = query.select(sre[field]) - if voucher_detail_no: query = query.where(sre.voucher_detail_no == voucher_detail_no) diff --git a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py index c17e5131669..e029a0672be 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py @@ -12,9 +12,9 @@ from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry import StockEntry from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( + _get_stock_reservation_entries_for_voucher, cancel_stock_reservation_entries, get_sre_reserved_qty_details_for_voucher, - get_stock_reservation_entries_for_voucher, has_reserved_stock, ) from erpnext.stock.utils import get_stock_balance @@ -284,7 +284,7 @@ class TestStockReservationEntry(ERPNextTestSuite): self.assertTrue(has_reserved_stock("Sales Order", so.name)) for item in so.items: - sre_details = get_stock_reservation_entries_for_voucher( + sre_details = _get_stock_reservation_entries_for_voucher( "Sales Order", so.name, item.name, fields=["reserved_qty", "status"] )[0] self.assertEqual(item.stock_reserved_qty, sre_details.reserved_qty) @@ -354,7 +354,7 @@ class TestStockReservationEntry(ERPNextTestSuite): dn1.submit() for item in so.items: - sre_details = get_stock_reservation_entries_for_voucher( + sre_details = _get_stock_reservation_entries_for_voucher( "Sales Order", so.name, item.name, fields=["delivered_qty", "status"] )[0] self.assertGreater(sre_details.delivered_qty, 0) @@ -371,7 +371,7 @@ class TestStockReservationEntry(ERPNextTestSuite): dn2.submit() for item in so.items: - sre_details = get_stock_reservation_entries_for_voucher( + sre_details = _get_stock_reservation_entries_for_voucher( "Sales Order", so.name, item.name, @@ -415,7 +415,7 @@ class TestStockReservationEntry(ERPNextTestSuite): so.load_from_db() for item in so.items: - sre_details = get_stock_reservation_entries_for_voucher( + sre_details = _get_stock_reservation_entries_for_voucher( "Sales Order", so.name, item.name, fields=["status", "reserved_qty"] )[0] @@ -430,7 +430,7 @@ class TestStockReservationEntry(ERPNextTestSuite): dn.submit() for item in so.items: - sre_details = get_stock_reservation_entries_for_voucher( + sre_details = _get_stock_reservation_entries_for_voucher( "Sales Order", so.name, item.name, fields=["status", "delivered_qty", "reserved_qty"] )[0] @@ -478,7 +478,7 @@ class TestStockReservationEntry(ERPNextTestSuite): so.load_from_db() for item in so.items: - sre_details = get_stock_reservation_entries_for_voucher( + sre_details = _get_stock_reservation_entries_for_voucher( "Sales Order", so.name, item.name, @@ -539,7 +539,7 @@ class TestStockReservationEntry(ERPNextTestSuite): so.load_from_db() for item in so.items: - sre_details = get_stock_reservation_entries_for_voucher( + sre_details = _get_stock_reservation_entries_for_voucher( "Sales Order", so.name, item.name, fields=["reserved_qty"] )[0] From 4a2163ebf8d3b25dfd49016f04c7e621b804a847 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:03:49 +0530 Subject: [PATCH 099/134] fix: preserve custom title on new JV (backport #57987) (#57989) Co-authored-by: Diptanil Saha Co-authored-by: rehanrehman389 --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index c4367223909..331ed50dea3 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -162,7 +162,7 @@ class JournalEntry(AccountsController): JournalTaxWithholding(self).on_validate() - if self.is_new() or not self.title: + if not self.title or (self.is_new() and self.amended_from): self.title = self.get_title() def validate_advance_accounts(self): From fea1ec867ed38a0bdc2da2745a6bbf8fddcaa207 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 19:45:16 +0000 Subject: [PATCH 100/134] refactor(queries): using `frappe.get_query` in `get_filtered_child_rows` (backport #57991) (#57993) Co-authored-by: Diptanil Saha --- erpnext/controllers/queries.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 981375e9f0e..ad8f069e1f1 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -996,9 +996,8 @@ def get_payment_terms_for_references(doctype, txt, searchfield, start, page_len, def get_filtered_child_rows(doctype, txt, searchfield, start, page_len, filters) -> list: table = frappe.qb.DocType(doctype) query = ( - frappe.qb.from_(table) + frappe.get_query(table, filters=filters) .select( - table.name, Concat("#", table.idx, ", ", table.item_code), ) .orderby(table.idx) @@ -1006,10 +1005,6 @@ def get_filtered_child_rows(doctype, txt, searchfield, start, page_len, filters) .limit(page_len) ) - if filters: - for field, value in filters.items(): - query = query.where(table[field] == value) - if txt: txt += "%" query = query.where( From ee5316ecd00dc83c01efd69bace1061313d3c759 Mon Sep 17 00:00:00 2001 From: Jatin3128 <140256508+Jatin3128@users.noreply.github.com> Date: Tue, 11 Aug 2026 11:00:00 +0530 Subject: [PATCH 101/134] fix: keep source rate on re-fetch when maintain same rate is enabled (backport #57479) (#57792) * fix: keep source rate on re-fetch when maintain same rate is enabled (#57479) * fix: type-annotate get_item_details arguments --------- Co-authored-by: test --- erpnext/stock/get_item_details.py | 122 ++++++- erpnext/stock/tests/test_get_item_details.py | 333 +++++++++++++++++++ erpnext/utilities/transaction_base.py | 4 +- 3 files changed, 443 insertions(+), 16 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 53401106f32..abbac9a7710 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -43,6 +43,28 @@ purchase_doctypes = [ NOT_APPLICABLE_TAX = "N/A" +# For each transaction, the child-row link field(s) that point to the source +# document item, mapped to that source item doctype. When "maintain same rate" is +# on, a mapped row keeps the persisted source pricing (read straight from that row), +# so an unsaved edit on the target row can never lock in a non-source rate. +maintain_same_rate_source_fields = { + "Purchase Order": {"supplier_quotation_item": "Supplier Quotation Item"}, + "Purchase Receipt": {"purchase_order_item": "Purchase Order Item"}, + "Purchase Invoice": {"po_detail": "Purchase Order Item", "pr_detail": "Purchase Receipt Item"}, + "Sales Order": {"quotation_item": "Quotation Item"}, + "Delivery Note": {"so_detail": "Sales Order Item", "si_detail": "Sales Invoice Item"}, + "Sales Invoice": {"so_detail": "Sales Order Item", "dn_detail": "Delivery Note Item"}, +} + +LOCKED_RATE_FIELDS = [ + "price_list_rate", + "rate", + "discount_percentage", + "discount_amount", + "margin_type", + "margin_rate_or_amount", +] + def _preprocess_ctx(ctx): if not ctx.price_list: @@ -58,7 +80,12 @@ def _preprocess_ctx(ctx): @frappe.whitelist() @erpnext.normalize_ctx_input(ItemDetailsCtx) -def get_item_details(ctx, doc=None, for_validate=False, overwrite_warehouse=True) -> ItemDetails: +def get_item_details( + ctx: ItemDetailsCtx, + doc: Document | str | None = None, + for_validate: bool | None = False, + overwrite_warehouse: bool = True, +) -> ItemDetails: """ ctx = { "item_code": "", @@ -120,16 +147,20 @@ def get_item_details(ctx, doc=None, for_validate=False, overwrite_warehouse=True if ctx.doctype in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]: ctx.customer = None - out.update(get_price_list_rate(ctx, item)) + source_row = get_rate_locked_source_row(ctx, doc) + if source_row: + lock_source_rate(out, source_row) + else: + out.update(get_price_list_rate(ctx, item)) - if ( - not out.price_list_rate - and ctx.transaction_type == "selling" - and frappe.get_single_value("Selling Settings", "fallback_to_default_price_list") - ): - fallback_args = ctx.copy() - fallback_args.price_list = frappe.get_single_value("Selling Settings", "selling_price_list") - out.update(get_price_list_rate(fallback_args, item)) + if ( + not out.price_list_rate + and ctx.transaction_type == "selling" + and frappe.get_single_value("Selling Settings", "fallback_to_default_price_list") + ): + fallback_args = ctx.copy() + fallback_args.price_list = frappe.get_single_value("Selling Settings", "selling_price_list") + out.update(get_price_list_rate(fallback_args, item)) ctx.customer = current_customer @@ -144,9 +175,8 @@ def get_item_details(ctx, doc=None, for_validate=False, overwrite_warehouse=True if ctx.get(key) is None: ctx[key] = value - data = get_pricing_rule_for_item(ctx, doc=doc, for_validate=for_validate) - - out.update(data) + if not source_row: + out.update(get_pricing_rule_for_item(ctx, doc=doc, for_validate=for_validate)) if ( frappe.get_single_value("Stock Settings", "auto_create_serial_and_batch_bundle_for_outward") @@ -176,6 +206,61 @@ def remove_standard_fields(out: ItemDetails): return out +def get_rate_locked_source_row(ctx: ItemDetailsCtx, doc) -> frappe._dict | None: + """Return the persisted source-document row a mapped target row is locked to. + + The rate is read from the linked source row in the database (not the mutable + target row), so a re-fetch always restores the source pricing the maintain-same- + rate validator checks against, even after an unsaved edit on the target row. + """ + if isinstance(doc, str): + doc = json.loads(doc) + + source_fields = maintain_same_rate_source_fields.get(ctx.parenttype or ctx.doctype) + if not source_fields or not doc or ctx.get("is_return") or not maintain_same_rate_enabled(ctx): + return None + + row = next((d for d in doc.get("items") or [] if d.get("name") == ctx.child_docname), None) + if not row: + return None + + for link_field, source_doctype in source_fields.items(): + if source_name := row.get(link_field): + # a direct read would bypass permissions; only return source pricing to a + # caller allowed to read the source document + source = frappe.db.get_value( + source_doctype, source_name, [*LOCKED_RATE_FIELDS, "parent", "parenttype"], as_dict=True + ) + if source and frappe.has_permission(source.parenttype, doc=source.parent): + return source + return None + return None + + +def maintain_same_rate_enabled(ctx: ItemDetailsCtx) -> bool: + if (ctx.parenttype or ctx.doctype) in purchase_doctypes: + if ctx.get("is_internal_supplier"): + return False + return bool(cint(frappe.get_cached_value("Buying Settings", "None", "maintain_same_rate"))) + + if ctx.get("is_internal_customer"): + return False + return bool(cint(frappe.get_cached_value("Selling Settings", "None", "maintain_same_sales_rate"))) + + +def lock_source_rate(out: frappe._dict, source_row) -> None: + """Copy the source row's whole pricing block onto out so a mapped row keeps its + exact rate. Pricing rules are skipped for these rows, so nothing re-derives it and + the manual discount or margin that made rate differ from price_list_rate survives. + """ + out.price_list_rate = flt(source_row.get("price_list_rate")) or flt(source_row.get("rate")) + out.rate = flt(source_row.get("rate")) + out.discount_percentage = flt(source_row.get("discount_percentage")) + out.discount_amount = flt(source_row.get("discount_amount")) + out.margin_type = source_row.get("margin_type") + out.margin_rate_or_amount = flt(source_row.get("margin_rate_or_amount")) + + def set_valuation_rate(out: ItemDetails | dict, ctx: ItemDetailsCtx): if frappe.db.exists("Product Bundle", {"name": ctx.item_code, "disabled": 0}, cache=True): valuation_rate = 0.0 @@ -1614,14 +1699,21 @@ def apply_price_list(ctx, as_doc=False, doc=None): def apply_price_list_on_item(ctx, doc=None): item_doc = frappe.get_cached_doc("Item", ctx.item_code) - item_details = get_price_list_rate(ctx, item_doc) + + source_row = get_rate_locked_source_row(ctx, doc) + if source_row: + item_details = frappe._dict() + lock_source_rate(item_details, source_row) + else: + item_details = get_price_list_rate(ctx, item_doc) ctx.conversion_factor = flt(ctx.conversion_factor) or get_conversion_factor(ctx.item_code, ctx.uom).get( "conversion_factor", 1 ) ctx.stock_qty = flt(ctx.qty) * flt(ctx.conversion_factor) - item_details.update(get_pricing_rule_for_item(ctx, doc=doc)) + if not source_row: + item_details.update(get_pricing_rule_for_item(ctx, doc=doc)) return item_details diff --git a/erpnext/stock/tests/test_get_item_details.py b/erpnext/stock/tests/test_get_item_details.py index 99d94008221..d790c673e20 100644 --- a/erpnext/stock/tests/test_get_item_details.py +++ b/erpnext/stock/tests/test_get_item_details.py @@ -123,3 +123,336 @@ class TestGetItemDetail(ERPNextTestSuite): dn.save() self.assertEqual(dn.items[0].batch_no, "BATCH01") self.assertEqual(dn.items[0].rate, 50) + + def test_maintain_same_rate_keeps_source_rate_on_refetch(self): + """#57436: with "maintain same rate" on, re-fetching a PR row mapped from a + PO must keep the PO rate instead of pulling a newer, higher Item Price. + + The rate is validated on save, so it can never persist changed; assert the + fetched rate directly to prove the newer Item Price is never picked up. + """ + from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt + from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order + from erpnext.stock.doctype.item.test_item import make_item + + def set_maintain_same_rate(value): + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", value) + frappe.clear_cache(doctype="Buying Settings") + + set_maintain_same_rate(1) + + item_code = make_item(properties={"is_stock_item": 1}).name + po = create_purchase_order(item_code=item_code, qty=1, rate=100) + + # The PO may auto-insert an Item Price at 100; bump it to the newer, higher rate. + item_price = frappe.db.get_value( + "Item Price", {"item_code": item_code, "price_list": "Standard Buying"} + ) + if item_price: + frappe.db.set_value("Item Price", item_price, "price_list_rate", 120) + else: + frappe.get_doc( + { + "doctype": "Item Price", + "price_list": "Standard Buying", + "item_code": item_code, + "price_list_rate": 120, + } + ).insert() + + pr = make_purchase_receipt(po.name) + pr.insert() + + def fetch_price_list_rate(): + ctx = frappe._dict( + { + "item_code": item_code, + "doctype": "Purchase Receipt", + "name": pr.name, + "company": pr.company, + "supplier": pr.supplier, + "currency": pr.currency, + "conversion_rate": 1.0, + "price_list": "Standard Buying", + "price_list_currency": pr.currency, + "plc_conversion_rate": 1.0, + "warehouse": pr.items[0].warehouse, + "uom": pr.items[0].uom, + "stock_uom": pr.items[0].stock_uom, + "qty": pr.items[0].qty, + "child_doctype": pr.items[0].doctype, + "child_docname": pr.items[0].name, + "is_return": 0, + "is_internal_supplier": 0, + "ignore_pricing_rule": 1, + } + ) + return get_item_details(ctx, pr).get("price_list_rate") + + # Rate stays at the PO rate; the newer Item Price (120) is not fetched. + self.assertEqual(fetch_price_list_rate(), 100) + + # Control: without the setting the newer Item Price would be fetched. + set_maintain_same_rate(0) + self.assertEqual(fetch_price_list_rate(), 120) + + def test_maintain_same_rate_survives_refetch_with_discount(self): + """A mapped Purchase Receipt row that carries a source discount (rate != price + list rate) must keep its rate when the row is re-fetched, so maintain-same-rate + lets the document save. process_item_selection runs the same recompute the desk + mirrors, so it covers the "discount discarded on refresh" concern end to end. + """ + from frappe.utils import flt + + from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt + from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order + + item, price_list = "_Test Item", "_Test Buying Price List" + original = frappe.db.get_single_value("Buying Settings", "maintain_same_rate") + original_action = frappe.db.get_single_value("Buying Settings", "maintain_same_rate_action") + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1) + frappe.db.set_single_value("Buying Settings", "maintain_same_rate_action", "Stop") + frappe.clear_cache(doctype="Buying Settings") + + try: + for label, adjustment in ( + ("percentage", {"discount_percentage": 10}), + ("amount", {"discount_amount": 10}), + ): + with self.subTest(discount=label): + # a controlled discounted PO: list rate 100, effective rate 90 + frappe.flags.dont_fetch_price_list_rate = True + po = create_purchase_order(item_code=item, qty=1, do_not_save=True) + po.buying_price_list = price_list + po.items[0].price_list_rate = 100 + po.items[0].update(adjustment) + po.items[0].rate = 90 + po.insert() + po.submit() + frappe.flags.dont_fetch_price_list_rate = False + + # a newer Item Price must not leak onto the mapped row on re-fetch + item_price = frappe.db.get_value( + "Item Price", {"item_code": item, "price_list": price_list} + ) + if item_price: + frappe.db.set_value("Item Price", item_price, "price_list_rate", 250) + + pr = make_purchase_receipt(po.name) + pr.insert() + pr.process_item_selection(item_idx=pr.items[0].idx) + + self.assertEqual(flt(pr.items[0].rate), 90) + pr.save() # must not raise the maintain-same-rate check + finally: + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", original) + frappe.db.set_single_value("Buying Settings", "maintain_same_rate_action", original_action) + frappe.clear_cache(doctype="Buying Settings") + frappe.flags.dont_fetch_price_list_rate = False + + def test_apply_price_list_keeps_source_rate_when_maintain_same_rate(self): + """#57436: the bulk apply_price_list path (price list / party / conversion rate + change) must also keep the source rate on mapped rows, not just re-fetch of a + single row. Here a PR row carries its PO rate (175) while the current price list + rate is 100; the bulk apply must keep 175. + """ + from frappe.utils import flt, nowdate + + from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order + from erpnext.stock.get_item_details import apply_price_list + + item_code = "_Test Item" + price_list = "_Test Buying Price List" + + original = frappe.db.get_single_value("Buying Settings", "maintain_same_rate") + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1) + frappe.clear_cache(doctype="Buying Settings") + + try: + po = create_purchase_order(item_code=item_code, rate=175, qty=1) + + row_name = "pr-row-1" + pr_doc = { + "doctype": "Purchase Receipt", + "items": [ + { + "name": row_name, + "item_code": item_code, + "purchase_order_item": po.items[0].name, + "price_list_rate": 175, + "rate": 175, + } + ], + } + ctx = frappe._dict( + doctype="Purchase Receipt", + supplier=po.supplier, + company=po.company, + currency=po.currency, + conversion_rate=1.0, + price_list=price_list, + plc_conversion_rate=1.0, + transaction_date=nowdate(), + items=[ + frappe._dict( + doctype="Purchase Receipt Item", + parenttype="Purchase Receipt", + item_code=item_code, + child_docname=row_name, + qty=1, + uom=po.items[0].uom, + stock_uom=po.items[0].stock_uom, + conversion_factor=1.0, + ) + ], + ) + + result = apply_price_list(ctx, doc=pr_doc) + self.assertEqual(flt(result["children"][0].get("price_list_rate")), 175) + finally: + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", original) + frappe.clear_cache(doctype="Buying Settings") + + def test_maintain_same_rate_keeps_source_discount_on_refetch(self): + """A mapped source row with a discount has rate != price_list_rate. Re-fetch must + return the source's rate and discount, not just the pre-discount price, or the + recomputed rate diverges from the reference and fails maintain-same-rate on save. + """ + from frappe.utils import flt + + from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order + + item_code = "_Test Item" + price_list = "_Test Buying Price List" + + original = frappe.db.get_single_value("Buying Settings", "maintain_same_rate") + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1) + frappe.clear_cache(doctype="Buying Settings") + + try: + # source PO carries the discount: list rate 100, 10% off, effective rate 90 + frappe.flags.dont_fetch_price_list_rate = True + po = create_purchase_order(item_code=item_code, qty=1, do_not_save=True) + po.buying_price_list = price_list + po.items[0].price_list_rate = 100 + po.items[0].discount_percentage = 10 + po.items[0].rate = 90 + po.insert() + po.submit() + frappe.flags.dont_fetch_price_list_rate = False + + row_name = "pr-row-1" + pr_doc = { + "doctype": "Purchase Receipt", + "items": [ + {"name": row_name, "item_code": item_code, "purchase_order_item": po.items[0].name} + ], + } + ctx = frappe._dict( + item_code=item_code, + doctype="Purchase Receipt", + company=po.company, + supplier=po.supplier, + currency=po.currency, + conversion_rate=1.0, + price_list=price_list, + price_list_currency=po.currency, + plc_conversion_rate=1.0, + warehouse="_Test Warehouse - _TC", + uom=po.items[0].uom, + stock_uom=po.items[0].stock_uom, + qty=1, + child_docname=row_name, + is_return=0, + is_internal_supplier=0, + ignore_pricing_rule=1, + ) + + out = get_item_details(ctx, pr_doc) + self.assertEqual(flt(out.get("price_list_rate")), 100) + self.assertEqual(flt(out.get("rate")), 90) + self.assertEqual(flt(out.get("discount_percentage")), 10) + finally: + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", original) + frappe.clear_cache(doctype="Buying Settings") + frappe.flags.dont_fetch_price_list_rate = False + + def test_refetch_restores_source_rate_after_target_edit(self): + """Editing a mapped row's rate then re-fetching must restore the persisted source + rate (read from the linked row), not lock in the edit, so the document still saves. + """ + from frappe.utils import flt + + from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt + from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order + + item = "_Test Item" + original = frappe.db.get_single_value("Buying Settings", "maintain_same_rate") + original_action = frappe.db.get_single_value("Buying Settings", "maintain_same_rate_action") + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1) + frappe.db.set_single_value("Buying Settings", "maintain_same_rate_action", "Stop") + frappe.clear_cache(doctype="Buying Settings") + + try: + po = create_purchase_order(item_code=item, qty=1, rate=90) + pr = make_purchase_receipt(po.name) + pr.insert() + + # user edits the mapped row to a non-source rate + pr.items[0].price_list_rate = 200 + pr.items[0].rate = 200 + + # a re-fetch must restore the persisted source (PO) rate, not keep the edit + pr.process_item_selection(item_idx=pr.items[0].idx) + self.assertEqual(flt(pr.items[0].rate), 90) + pr.save() # must not raise the maintain-same-rate check + finally: + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", original) + frappe.db.set_single_value("Buying Settings", "maintain_same_rate_action", original_action) + frappe.clear_cache(doctype="Buying Settings") + + def test_rate_lock_source_lookup_checks_permission(self): + """The lock reads source pricing via a direct DB read, so it must not disclose a + source document's pricing to a caller who cannot read that document. + """ + from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order + from erpnext.stock.get_item_details import get_rate_locked_source_row + + original = frappe.db.get_single_value("Buying Settings", "maintain_same_rate") + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1) + frappe.clear_cache(doctype="Buying Settings") + + role, email = "_Test Role Without PO Access", "_test_rate_lock_probe@example.com" + try: + po = create_purchase_order(item_code="_Test Item", qty=1, rate=90) + pr_doc = { + "doctype": "Purchase Receipt", + "items": [{"name": "r1", "item_code": "_Test Item", "purchase_order_item": po.items[0].name}], + } + ctx = frappe._dict(doctype="Purchase Receipt", child_docname="r1") + + # an authorized caller receives the source row + self.assertIsNotNone(get_rate_locked_source_row(ctx.copy(), dict(pr_doc))) + + if not frappe.db.exists("Role", role): + frappe.get_doc({"doctype": "Role", "role_name": role, "desk_access": 1}).insert( + ignore_permissions=True + ) + if not frappe.db.exists("User", email): + frappe.get_doc( + { + "doctype": "User", + "email": email, + "first_name": "Probe", + "send_welcome_email": 0, + "roles": [{"role": role}], + } + ).insert(ignore_permissions=True) + + frappe.set_user(email) + # a caller who cannot read the Purchase Order gets nothing + self.assertIsNone(get_rate_locked_source_row(ctx.copy(), dict(pr_doc))) + finally: + frappe.set_user("Administrator") + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", original) + frappe.clear_cache(doctype="Buying Settings") diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 3ff42ed6a09..e6cad737a6b 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -548,7 +548,9 @@ class TransactionBase(StatusUpdater): from erpnext.stock.get_item_details import apply_price_list args = { - "items": [x.as_dict() for x in self.items], + # pass child_docname so the maintain-same-rate lock in apply_price_list can + # match each row, consistent with the desk (JS) callers + "items": [{**x.as_dict(), "child_docname": x.name} for x in self.items], "customer": self.customer or self.party_name, "quotation_to": self.quotation_to, "customer_group": self.customer_group, From 822e6d89247cfb8d42ffcaf0101f89766b789505 Mon Sep 17 00:00:00 2001 From: pandiyan Date: Mon, 10 Aug 2026 16:02:39 +0530 Subject: [PATCH 102/134] fix: convert hours to minutes in workstation complete_job `time_diff_in_hours` returns hours, so `time_in_mins` needs `* 60`, not `/ 60`. Matches `Job Card.validate_time_log_row`. No behaviour change: the `doc.save()` on the next line runs Job Card's `validate`, which recomputes `time_in_mins` correctly before the row is written. This only stops the expression from reading as a bug. (cherry picked from commit 422a9161ddb34a542a9fb226e3250f7ff6f4d8cb) --- erpnext/manufacturing/doctype/workstation/workstation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index 772318a6446..047cb4e2c7f 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -232,7 +232,7 @@ class Workstation(Document): for row in doc.time_logs: if not row.to_time: row.to_time = to_time - row.time_in_mins = time_diff_in_hours(row.to_time, row.from_time) / 60 + row.time_in_mins = time_diff_in_hours(row.to_time, row.from_time) * 60 row.completed_qty = qty doc.save() From 631cc2c218d082b1bd85055bd7a63ccfe0bee227 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 11:56:13 +0530 Subject: [PATCH 103/134] refactor: drop redundant time_in_mins assignment in complete_job (cherry picked from commit 3cffeb68e3e719eddc3c3ceb34166f8311782451) --- erpnext/manufacturing/doctype/workstation/workstation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index 047cb4e2c7f..1dceddad256 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -232,7 +232,6 @@ class Workstation(Document): for row in doc.time_logs: if not row.to_time: row.to_time = to_time - row.time_in_mins = time_diff_in_hours(row.to_time, row.from_time) * 60 row.completed_qty = qty doc.save() From 87c4009572ba1b256c2242ced42f4850f888e73c Mon Sep 17 00:00:00 2001 From: Kaushal Shriwas <64089478+kaulith@users.noreply.github.com> Date: Tue, 11 Aug 2026 11:58:33 +0530 Subject: [PATCH 104/134] fix(manufacturing): keep item code searchable when a barcode matches the same text (cherry picked from commit bf5d506637a0f74333310f1b8d0473b11254426e) # Conflicts: # erpnext/manufacturing/doctype/bom/mapper.py --- erpnext/manufacturing/doctype/bom/mapper.py | 206 ++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 erpnext/manufacturing/doctype/bom/mapper.py diff --git a/erpnext/manufacturing/doctype/bom/mapper.py b/erpnext/manufacturing/doctype/bom/mapper.py new file mode 100644 index 00000000000..f237172fb56 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom/mapper.py @@ -0,0 +1,206 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +"""Document-mapping and query helpers for BOM (extracted from bom.py).""" + +from functools import partial + +import frappe +from frappe import _ +from frappe.core.doctype.version.version import get_diff +from frappe.model.document import Document +from frappe.model.mapper import get_mapped_doc +from frappe.query_builder import Field +from frappe.query_builder.functions import IfNull +from frappe.utils import today + +from erpnext.stock.doctype.item.item import get_item_details + +_BOM_DIFF_IDENTIFIERS = { + "operations": "operation", + "items": "item_code", + "secondary_items": "item_code", + "exploded_items": "item_code", +} + +_VARIANT_BOM_MAPPING = { + "BOM": {"doctype": "BOM", "validation": {"docstatus": ["=", 1]}}, + "BOM Item": { + "doctype": "BOM Item", + # stop get_mapped_doc copying parent bom_no to children + "field_no_map": ["bom_no"], + "condition": lambda doc: doc.has_variants == 0, + }, +} + + +@frappe.whitelist() +def get_children(parent: str | None = None, is_root: bool = False, **filters): + frappe.has_permission("BOM", "read", throw=True) + + if not parent or parent == "BOM": + frappe.msgprint(_("Please select a BOM")) + return + + frappe.form_dict.parent = parent + bom_doc = frappe.get_cached_doc("BOM", parent) + frappe.has_permission("BOM", doc=bom_doc, throw=True) + + bom_items = _bom_child_items(parent) + _enrich_bom_items(bom_items, bom_doc) + return bom_items + + +def _bom_child_items(parent): + return frappe.get_all( + "BOM Item", + fields=["item_code", "bom_no as value", "stock_qty", "qty", "is_phantom_item", "bom_no"], + filters=[["parent", "=", parent]], + order_by="idx", + ) + + +def _enrich_bom_items(bom_items, bom_doc): + item_names = tuple(d.get("item_code") for d in bom_items) + items = frappe.get_list( + "Item", + fields=["image", "description", "name", "stock_uom", "item_name", "is_sub_contracted_item"], + filters=[["name", "in", item_names]], + ) + for bom_item in bom_items: + bom_item.update(next(item for item in items if item.get("name") == bom_item.get("item_code"))) + bom_item.parent_bom_qty = bom_doc.quantity + bom_item.expandable = 0 if bom_item.value in ("", None) else 1 + bom_item.image = frappe.db.escape(bom_item.image) + + +@frappe.whitelist() +def get_bom_diff(bom1: str, bom2: str): + frappe.has_permission("BOM", "read", throw=True) + if bom1 == bom2: + frappe.throw( + _("BOM 1 {0} and BOM 2 {1} should not be the same").format(frappe.bold(bom1), frappe.bold(bom2)) + ) + + doc1 = frappe.get_doc("BOM", bom1) + doc2 = frappe.get_doc("BOM", bom2) + + out = get_diff(doc1, doc2) + out.row_changed, out.added, out.removed = [], [], [] + for df in doc1.meta.fields: + _diff_table_field(df, doc1, doc2, out) + return out + + +def _diff_table_field(df, doc1, doc2, out): + from frappe.model import table_fields + + if df.fieldtype not in table_fields: + return + + identifier = _BOM_DIFF_IDENTIFIERS[df.fieldname] + old_value, new_value = doc1.get(df.fieldname), doc2.get(df.fieldname) + old_map = {d.get(identifier): d for d in old_value} + new_map = {d.get(identifier): d for d in new_value} + + _collect_row_changes(df, identifier, old_map, new_value, out) + for d in old_value: + if d.get(identifier) not in new_map: + out.removed.append([df.fieldname, d.as_dict()]) + + +def _collect_row_changes(df, identifier, old_map, new_value, out): + for i, d in enumerate(new_value): + if d.get(identifier) not in old_map: + out.added.append([df.fieldname, d.as_dict()]) + continue + + diff = get_diff(old_map[d.get(identifier)], d, for_child=True) + if diff and diff.changed: + out.row_changed.append((df.fieldname, i, d.get(identifier), diff.changed)) + + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def item_query( + doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict | None = None +): + frappe.has_permission("Item", "read", throw=True) + + searchfields = frappe.get_meta("Item", cached=True).get_search_fields() + fields = ["name", "item_name", "item_group", "description"] + fields.extend(f for f in searchfields if f not in ["name", "item_group", "description"]) + + query_filters = _item_query_filters(filters) + or_filters = _item_query_or_filters(txt, searchfields or ["name"], query_filters) + return frappe.get_list( + "Item", + fields=fields, + filters=query_filters, + or_filters=or_filters, + order_by="idx desc, name, item_name", + limit_start=start, + limit_page_length=page_len, + as_list=1, + ) + + +def _item_query_filters(filters): + query_filters = [["disabled", "=", 0], [IfNull(Field("end_of_life"), "3099-12-31"), ">", today()]] + if filters and filters.get("item_code"): + if not frappe.get_cached_value("Item", filters.get("item_code"), "has_variants"): + query_filters.append(["has_variants", "=", 0]) + + for fieldname, value in (filters or {}).items(): + query_filters.append([fieldname, "=", value]) + return query_filters + + +def _item_query_or_filters(txt, searchfields, query_filters): + if not txt: + return [] + + or_filters = [[s_field, "like", f"%{txt}%"] for s_field in searchfields] + barcodes = frappe.get_all( + "Item Barcode", + fields=["parent as item_code"], + filters={"barcode": ("like", f"%{txt}%")}, + distinct=True, + ) + barcode_codes = [d.item_code for d in barcodes] + if barcode_codes: + or_filters.append(["name", "in", barcode_codes]) + return or_filters + + +@frappe.whitelist() +def make_variant_bom( + source_name: str, + bom_no: str, + item: str, + variant_items: str | list, + target_doc: str | dict | Document | None = None, +): + frappe.has_permission("BOM", "write", throw=True) + + postprocess = partial( + _postprocess_variant_bom, item=item, variant_items=variant_items, source_name=source_name + ) + return get_mapped_doc("BOM", source_name, _VARIANT_BOM_MAPPING, target_doc, postprocess) + + +def _postprocess_variant_bom(source, doc, item, variant_items, source_name): + from erpnext.manufacturing.doctype.work_order.work_order import add_variant_item + + item_data = get_item_details(item) + doc.item = item + doc.quantity = 1 + doc.update( + { + "item_name": item_data.item_name, + "description": item_data.description, + "uom": item_data.stock_uom, + "allow_alternative_item": item_data.allow_alternative_item, + } + ) + add_variant_item(variant_items, doc, source_name) From 062976123a921f941df57897ccdf5a9d16876d55 Mon Sep 17 00:00:00 2001 From: Kaushal Shriwas <64089478+kaulith@users.noreply.github.com> Date: Tue, 11 Aug 2026 11:58:36 +0530 Subject: [PATCH 105/134] test(manufacturing): cover BOM item search when item code collides with a barcode (cherry picked from commit 23024d1ea91ed7e1b5cd9e658e468d18a42af411) --- erpnext/manufacturing/doctype/bom/test_bom.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 48eb41fdb11..9d46906d621 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -486,6 +486,29 @@ class TestBOM(ERPNextTestSuite): self.assertNotEqual(len(test_items), len(filtered), msg="Item filtering showing excessive results") self.assertTrue(0 < len(filtered) <= 3, msg="Item filtering showing excessive results") + @timeout + def test_bom_item_query_matches_item_code_colliding_with_another_barcode(self): + item = make_item( + "_Test BOM Query 2.5MM", + {"is_stock_item": 1, "item_name": "_Test BOM Query Sheet", "description": "sheet"}, + ) + make_item( + "_Test BOM Query Barcode Holder", + {"is_stock_item": 1}, + barcode=f"90{item.name}90", + ) + + results = item_query( + doctype="Item", + txt=item.name, + searchfield="name", + start=0, + page_len=20, + filters={"is_stock_item": 1}, + ) + + self.assertIn(item.name, [d[0] for d in results]) + @timeout def test_exclude_exploded_items_from_bom(self): bom_no = get_default_bom() From 60eab30bab64fe55e71f107a79fa39d9ff8a830d Mon Sep 17 00:00:00 2001 From: Kaushal Shriwas <64089478+kaulith@users.noreply.github.com> Date: Tue, 11 Aug 2026 12:23:00 +0530 Subject: [PATCH 106/134] chore: resolve backport conflict in bom.py --- erpnext/manufacturing/doctype/bom/bom.py | 6 +- erpnext/manufacturing/doctype/bom/mapper.py | 206 -------------------- 2 files changed, 3 insertions(+), 209 deletions(-) delete mode 100644 erpnext/manufacturing/doctype/bom/mapper.py diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 06f7ff894d6..aa010590bc5 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1909,10 +1909,10 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): [IfNull(Field("end_of_life"), "3099-12-31"), ">", today()], ] - or_cond_filters = {} + or_cond_filters = [] if txt: for s_field in searchfields: - or_cond_filters[s_field] = ("like", f"%{txt}%") + or_cond_filters.append([s_field, "like", f"%{txt}%"]) barcodes = frappe.get_all( "Item Barcode", @@ -1923,7 +1923,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): barcodes = [d.item_code for d in barcodes] if barcodes: - or_cond_filters["name"] = ("in", barcodes) + or_cond_filters.append(["name", "in", barcodes]) if filters and filters.get("item_code"): has_variants = frappe.get_cached_value("Item", filters.get("item_code"), "has_variants") diff --git a/erpnext/manufacturing/doctype/bom/mapper.py b/erpnext/manufacturing/doctype/bom/mapper.py deleted file mode 100644 index f237172fb56..00000000000 --- a/erpnext/manufacturing/doctype/bom/mapper.py +++ /dev/null @@ -1,206 +0,0 @@ -# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -"""Document-mapping and query helpers for BOM (extracted from bom.py).""" - -from functools import partial - -import frappe -from frappe import _ -from frappe.core.doctype.version.version import get_diff -from frappe.model.document import Document -from frappe.model.mapper import get_mapped_doc -from frappe.query_builder import Field -from frappe.query_builder.functions import IfNull -from frappe.utils import today - -from erpnext.stock.doctype.item.item import get_item_details - -_BOM_DIFF_IDENTIFIERS = { - "operations": "operation", - "items": "item_code", - "secondary_items": "item_code", - "exploded_items": "item_code", -} - -_VARIANT_BOM_MAPPING = { - "BOM": {"doctype": "BOM", "validation": {"docstatus": ["=", 1]}}, - "BOM Item": { - "doctype": "BOM Item", - # stop get_mapped_doc copying parent bom_no to children - "field_no_map": ["bom_no"], - "condition": lambda doc: doc.has_variants == 0, - }, -} - - -@frappe.whitelist() -def get_children(parent: str | None = None, is_root: bool = False, **filters): - frappe.has_permission("BOM", "read", throw=True) - - if not parent or parent == "BOM": - frappe.msgprint(_("Please select a BOM")) - return - - frappe.form_dict.parent = parent - bom_doc = frappe.get_cached_doc("BOM", parent) - frappe.has_permission("BOM", doc=bom_doc, throw=True) - - bom_items = _bom_child_items(parent) - _enrich_bom_items(bom_items, bom_doc) - return bom_items - - -def _bom_child_items(parent): - return frappe.get_all( - "BOM Item", - fields=["item_code", "bom_no as value", "stock_qty", "qty", "is_phantom_item", "bom_no"], - filters=[["parent", "=", parent]], - order_by="idx", - ) - - -def _enrich_bom_items(bom_items, bom_doc): - item_names = tuple(d.get("item_code") for d in bom_items) - items = frappe.get_list( - "Item", - fields=["image", "description", "name", "stock_uom", "item_name", "is_sub_contracted_item"], - filters=[["name", "in", item_names]], - ) - for bom_item in bom_items: - bom_item.update(next(item for item in items if item.get("name") == bom_item.get("item_code"))) - bom_item.parent_bom_qty = bom_doc.quantity - bom_item.expandable = 0 if bom_item.value in ("", None) else 1 - bom_item.image = frappe.db.escape(bom_item.image) - - -@frappe.whitelist() -def get_bom_diff(bom1: str, bom2: str): - frappe.has_permission("BOM", "read", throw=True) - if bom1 == bom2: - frappe.throw( - _("BOM 1 {0} and BOM 2 {1} should not be the same").format(frappe.bold(bom1), frappe.bold(bom2)) - ) - - doc1 = frappe.get_doc("BOM", bom1) - doc2 = frappe.get_doc("BOM", bom2) - - out = get_diff(doc1, doc2) - out.row_changed, out.added, out.removed = [], [], [] - for df in doc1.meta.fields: - _diff_table_field(df, doc1, doc2, out) - return out - - -def _diff_table_field(df, doc1, doc2, out): - from frappe.model import table_fields - - if df.fieldtype not in table_fields: - return - - identifier = _BOM_DIFF_IDENTIFIERS[df.fieldname] - old_value, new_value = doc1.get(df.fieldname), doc2.get(df.fieldname) - old_map = {d.get(identifier): d for d in old_value} - new_map = {d.get(identifier): d for d in new_value} - - _collect_row_changes(df, identifier, old_map, new_value, out) - for d in old_value: - if d.get(identifier) not in new_map: - out.removed.append([df.fieldname, d.as_dict()]) - - -def _collect_row_changes(df, identifier, old_map, new_value, out): - for i, d in enumerate(new_value): - if d.get(identifier) not in old_map: - out.added.append([df.fieldname, d.as_dict()]) - continue - - diff = get_diff(old_map[d.get(identifier)], d, for_child=True) - if diff and diff.changed: - out.row_changed.append((df.fieldname, i, d.get(identifier), diff.changed)) - - -@frappe.whitelist() -@frappe.validate_and_sanitize_search_inputs -def item_query( - doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict | None = None -): - frappe.has_permission("Item", "read", throw=True) - - searchfields = frappe.get_meta("Item", cached=True).get_search_fields() - fields = ["name", "item_name", "item_group", "description"] - fields.extend(f for f in searchfields if f not in ["name", "item_group", "description"]) - - query_filters = _item_query_filters(filters) - or_filters = _item_query_or_filters(txt, searchfields or ["name"], query_filters) - return frappe.get_list( - "Item", - fields=fields, - filters=query_filters, - or_filters=or_filters, - order_by="idx desc, name, item_name", - limit_start=start, - limit_page_length=page_len, - as_list=1, - ) - - -def _item_query_filters(filters): - query_filters = [["disabled", "=", 0], [IfNull(Field("end_of_life"), "3099-12-31"), ">", today()]] - if filters and filters.get("item_code"): - if not frappe.get_cached_value("Item", filters.get("item_code"), "has_variants"): - query_filters.append(["has_variants", "=", 0]) - - for fieldname, value in (filters or {}).items(): - query_filters.append([fieldname, "=", value]) - return query_filters - - -def _item_query_or_filters(txt, searchfields, query_filters): - if not txt: - return [] - - or_filters = [[s_field, "like", f"%{txt}%"] for s_field in searchfields] - barcodes = frappe.get_all( - "Item Barcode", - fields=["parent as item_code"], - filters={"barcode": ("like", f"%{txt}%")}, - distinct=True, - ) - barcode_codes = [d.item_code for d in barcodes] - if barcode_codes: - or_filters.append(["name", "in", barcode_codes]) - return or_filters - - -@frappe.whitelist() -def make_variant_bom( - source_name: str, - bom_no: str, - item: str, - variant_items: str | list, - target_doc: str | dict | Document | None = None, -): - frappe.has_permission("BOM", "write", throw=True) - - postprocess = partial( - _postprocess_variant_bom, item=item, variant_items=variant_items, source_name=source_name - ) - return get_mapped_doc("BOM", source_name, _VARIANT_BOM_MAPPING, target_doc, postprocess) - - -def _postprocess_variant_bom(source, doc, item, variant_items, source_name): - from erpnext.manufacturing.doctype.work_order.work_order import add_variant_item - - item_data = get_item_details(item) - doc.item = item - doc.quantity = 1 - doc.update( - { - "item_name": item_data.item_name, - "description": item_data.description, - "uom": item_data.stock_uom, - "allow_alternative_item": item_data.allow_alternative_item, - } - ) - add_variant_item(variant_items, doc, source_name) From b4929e273703a21f0885b4e960f3101748ff1248 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 13:00:13 +0530 Subject: [PATCH 107/134] fix(manufacturing): correct nested BOM quantities --- .../report/bom_explorer/bom_explorer.py | 10 +++- .../report/bom_explorer/test_bom_explorer.py | 47 +++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 erpnext/manufacturing/report/bom_explorer/test_bom_explorer.py diff --git a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py index 680cb83b312..f7e9d9caef2 100644 --- a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py +++ b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py @@ -24,7 +24,7 @@ def get_exploded_items(bom, data, indent=0, qty=1): fields=[ "qty", "bom_no", - "qty", + "stock_qty", "item_code", "item_name", "description", @@ -51,7 +51,13 @@ def get_exploded_items(bom, data, indent=0, qty=1): } ) if item.bom_no: - get_exploded_items(item.bom_no, data, indent=indent + 1, qty=item.qty) + child_bom_qty = frappe.get_cached_value("BOM", item.bom_no, "quantity") + get_exploded_items( + item.bom_no, + data, + indent=indent + 1, + qty=qty * item.stock_qty / child_bom_qty, + ) def get_columns(): diff --git a/erpnext/manufacturing/report/bom_explorer/test_bom_explorer.py b/erpnext/manufacturing/report/bom_explorer/test_bom_explorer.py new file mode 100644 index 00000000000..c9e4cac279e --- /dev/null +++ b/erpnext/manufacturing/report/bom_explorer/test_bom_explorer.py @@ -0,0 +1,47 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import unittest +from unittest.mock import patch + +import frappe + +from erpnext.manufacturing.report.bom_explorer.bom_explorer import get_exploded_items + + +class TestBOMExplorer(unittest.TestCase): + def test_nested_bom_normalizes_and_accumulates_qty(self): + def item(item_code, qty, stock_qty, bom_no="", uom="Nos"): + return frappe._dict( + item_code=item_code, + item_name=item_code, + description="", + qty=qty, + stock_qty=stock_qty, + bom_no=bom_no, + uom=uom, + idx=1, + is_phantom_item=0, + ) + + children = { + "root": [item("parent", 2, 20, "parent-bom", "Box")], + "parent-bom": [item("child", 3, 12, "child-bom", "Pack")], + "child-bom": [item("raw-material", 2, 2, uom="Kg")], + } + bom_quantities = {"parent-bom": 5, "child-bom": 4} + + def get_items(_doctype, filters, **kwargs): + return children[filters["parent"]] + + def get_bom_quantity(_doctype, name, _fieldname): + return bom_quantities[name] + + data = [] + with ( + patch.object(frappe, "get_all", side_effect=get_items), + patch.object(frappe, "get_cached_value", side_effect=get_bom_quantity), + ): + get_exploded_items("root", data) + + self.assertEqual([row["qty"] for row in data], [2, 12, 24]) From fb877515ca0d64ea192088fc7b4cb54532cff387 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 13:13:03 +0530 Subject: [PATCH 108/134] fix(manufacturing): avoid child BOM cache lookups --- .../report/bom_explorer/bom_explorer.py | 4 ++-- .../report/bom_explorer/test_bom_explorer.py | 17 ++++++----------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py index f7e9d9caef2..232bc6821c3 100644 --- a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py +++ b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py @@ -24,6 +24,7 @@ def get_exploded_items(bom, data, indent=0, qty=1): fields=[ "qty", "bom_no", + "bom_no.quantity as child_bom_qty", "stock_qty", "item_code", "item_name", @@ -51,12 +52,11 @@ def get_exploded_items(bom, data, indent=0, qty=1): } ) if item.bom_no: - child_bom_qty = frappe.get_cached_value("BOM", item.bom_no, "quantity") get_exploded_items( item.bom_no, data, indent=indent + 1, - qty=qty * item.stock_qty / child_bom_qty, + qty=qty * item.stock_qty / item.child_bom_qty, ) diff --git a/erpnext/manufacturing/report/bom_explorer/test_bom_explorer.py b/erpnext/manufacturing/report/bom_explorer/test_bom_explorer.py index c9e4cac279e..bff5f2487fc 100644 --- a/erpnext/manufacturing/report/bom_explorer/test_bom_explorer.py +++ b/erpnext/manufacturing/report/bom_explorer/test_bom_explorer.py @@ -11,7 +11,7 @@ from erpnext.manufacturing.report.bom_explorer.bom_explorer import get_exploded_ class TestBOMExplorer(unittest.TestCase): def test_nested_bom_normalizes_and_accumulates_qty(self): - def item(item_code, qty, stock_qty, bom_no="", uom="Nos"): + def item(item_code, qty, stock_qty, bom_no="", uom="Nos", child_bom_qty=None): return frappe._dict( item_code=item_code, item_name=item_code, @@ -19,29 +19,24 @@ class TestBOMExplorer(unittest.TestCase): qty=qty, stock_qty=stock_qty, bom_no=bom_no, + child_bom_qty=child_bom_qty, uom=uom, idx=1, is_phantom_item=0, ) children = { - "root": [item("parent", 2, 20, "parent-bom", "Box")], - "parent-bom": [item("child", 3, 12, "child-bom", "Pack")], + "root": [item("parent", 2, 20, "parent-bom", "Box", 5)], + "parent-bom": [item("child", 3, 12, "child-bom", "Pack", 4)], "child-bom": [item("raw-material", 2, 2, uom="Kg")], } - bom_quantities = {"parent-bom": 5, "child-bom": 4} def get_items(_doctype, filters, **kwargs): + self.assertIn("bom_no.quantity as child_bom_qty", kwargs["fields"]) return children[filters["parent"]] - def get_bom_quantity(_doctype, name, _fieldname): - return bom_quantities[name] - data = [] - with ( - patch.object(frappe, "get_all", side_effect=get_items), - patch.object(frappe, "get_cached_value", side_effect=get_bom_quantity), - ): + with patch.object(frappe, "get_all", side_effect=get_items): get_exploded_items("root", data) self.assertEqual([row["qty"] for row in data], [2, 12, 24]) From 6fb09b1e218e372759e5255d211002cb25bdffd2 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 13:26:01 +0530 Subject: [PATCH 109/134] fix(selling): bill re-delivered sales order quantities --- .../doctype/sales_order/sales_order.py | 68 +++++++++++++------ 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 37cc541f411..6550d1d5d0c 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1319,6 +1319,13 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None): return target_doc +def get_qty_net_of_returns(so_item) -> float: + """Return the ordered quantity billable after returns and re-deliveries.""" + qty = flt(so_item.qty) + + return min(qty, max(qty - flt(so_item.returned_qty), flt(so_item.delivered_qty))) + + @frappe.whitelist() def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, args=None): if args is None: @@ -1328,10 +1335,40 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, a # 0 qty is accepted, as the qty is uncertain for some items has_unit_price_items = frappe.db.get_value("Sales Order", source_name, "has_unit_price_items") + billed_qty_by_item = None + pending_qty_by_item = {} def is_unit_price_row(source): return has_unit_price_items and source.qty == 0 + def get_billed_qty_by_item(): + nonlocal billed_qty_by_item + + if billed_qty_by_item is None: + invoice_item = frappe.qb.DocType("Sales Invoice Item") + sales_order_item = frappe.qb.DocType("Sales Order Item") + rows = ( + frappe.qb.from_(invoice_item) + .inner_join(sales_order_item) + .on(invoice_item.so_detail == sales_order_item.name) + .select(invoice_item.so_detail, Sum(invoice_item.qty).as_("qty")) + .where((invoice_item.docstatus == 1) & (sales_order_item.parent == source_name)) + .groupby(invoice_item.so_detail) + ).run(as_dict=True) + billed_qty_by_item = {row.so_detail: flt(row.qty) for row in rows} + + return billed_qty_by_item + + def get_pending_qty(source): + if source.name not in pending_qty_by_item: + billable_qty = get_qty_net_of_returns(source) + if source.qty and source.billed_amt: + billable_qty -= get_billed_qty_by_item().get(source.name, 0) + + pending_qty_by_item[source.name] = max(flt(billable_qty, source.precision("qty")), 0) + + return pending_qty_by_item[source.name] + def postprocess(source, target): set_missing_values(source, target) # Get the advance paid Journal Entries in Sales Invoice Advance @@ -1362,17 +1399,6 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, a target.debit_to = get_party_account("Customer", source.customer, source.company) def update_item(source, target, source_parent): - def get_billed_qty(so_item_name): - from frappe.query_builder.functions import Sum - - table = frappe.qb.DocType("Sales Invoice Item") - query = ( - frappe.qb.from_(table) - .select(Sum(table.qty).as_("qty")) - .where((table.docstatus == 1) & (table.so_detail == so_item_name)) - ) - return query.run(pluck="qty")[0] or 0 - if source_parent.has_unit_price_items: # 0 Amount rows (as seen in Unit Price Items) should be mapped as it is pending_amount = flt(source.amount) - flt(source.billed_amt) @@ -1381,11 +1407,7 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, a target.amount = flt(source.amount) - flt(source.billed_amt) target.base_amount = target.amount * flt(source_parent.conversion_rate) - target.qty = ( - source.qty - get_billed_qty(source.name) - if (source.qty and source.billed_amt) - else (source.qty if is_unit_price_row(source) else source.qty - source.returned_qty) - ) + target.qty = source.qty if is_unit_price_row(source) else get_pending_qty(source) if source_parent.project: target.cost_center = frappe.db.get_value("Project", source_parent.project, "cost_center") @@ -1461,13 +1483,17 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, a "parent": "sales_order", }, "postprocess": update_item, - "condition": lambda doc: ( + "condition": lambda doc: not args.get("skip_item_mapping") + and select_item(doc) + and ( True if is_unit_price_row(doc) - else (doc.qty and (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount))) - ) - and select_item(doc) - and not args.get("skip_item_mapping"), + else ( + doc.qty + and (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)) + and get_pending_qty(doc) > 0 + ) + ), }, "Sales Taxes and Charges": { "doctype": "Sales Taxes and Charges", From 0440c971b2984ed96ebc6e5a8b14dc8b65062a59 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 13:26:01 +0530 Subject: [PATCH 110/134] test(selling): cover invoicing after returns and re-deliveries --- .../doctype/sales_order/test_sales_order.py | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index c13e899c1c4..4b42b77f2b8 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -285,6 +285,95 @@ class TestSalesOrder(ERPNextTestSuite): si1 = make_sales_invoice(so.name) self.assertEqual(len(si1.get("items")), 0) + def test_make_sales_invoice_after_return_and_redelivery(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + so = make_sales_order(qty=10, rate=100) + dn = create_dn_against_so(so.name, 10) + + dn_return = frappe.get_doc(make_sales_return(dn.name).as_dict()) + dn_return.insert() + dn_return.submit() + + self.assertEqual(len(make_sales_invoice(so.name).get("items")), 0) + + create_dn_against_so(so.name, 10) + + so.load_from_db() + item = so.get("items")[0] + self.assertEqual(item.delivered_qty, 10) + self.assertEqual(item.returned_qty, 10) + + si = make_sales_invoice(so.name) + self.assertEqual(si.get("items")[0].qty, 10) + + def test_make_sales_invoice_bills_ordered_qty_for_partial_delivery(self): + so = make_sales_order(qty=10, rate=100) + create_dn_against_so(so.name, 4) + + si = make_sales_invoice(so.name) + self.assertEqual(si.get("items")[0].qty, 10) + + def test_make_sales_invoice_after_partial_billing_return_and_redelivery(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + so = make_sales_order(qty=10, rate=100) + dn = create_dn_against_so(so.name, 10) + + si = make_sales_invoice(so.name) + si.get("items")[0].qty = 4 + si.insert() + si.submit() + + dn_return = frappe.get_doc(make_sales_return(dn.name).as_dict()) + dn_return.insert() + dn_return.submit() + create_dn_against_so(so.name, 5) + + so.load_from_db() + item = so.get("items")[0] + self.assertEqual(item.delivered_qty, 5) + self.assertEqual(item.returned_qty, 10) + self.assertEqual(item.billed_amt, 400) + + pending_invoice = make_sales_invoice(so.name) + self.assertEqual(pending_invoice.get("items")[0].qty, 1) + pending_invoice.insert() + pending_invoice.submit() + + so.load_from_db() + self.assertEqual(so.get("items")[0].billed_amt, 500) + + def test_make_sales_invoice_after_partial_billing_multiple_items(self): + so = make_sales_order( + item_list=[ + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "qty": 10, + "rate": 100, + }, + { + "item_code": "_Test FG Item", + "warehouse": "_Test Warehouse - _TC", + "qty": 10, + "rate": 100, + }, + ] + ) + + si = make_sales_invoice(so.name) + si.get("items")[0].qty = 4 + si.get("items")[1].qty = 6 + si.insert() + si.submit() + + pending_invoice = make_sales_invoice(so.name) + self.assertEqual( + {item.so_detail: item.qty for item in pending_invoice.get("items")}, + {so.get("items")[0].name: 6, so.get("items")[1].name: 4}, + ) + def test_so_billed_amount_against_return_entry(self): from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return From a7f75de1aa1b42bef6147fa506e47fc8b2496ace Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 13:08:33 +0530 Subject: [PATCH 111/134] fix: require material transfer before job card start and completion When the work order transfers material against Job Card, the Start Job and Complete Job actions (and the whitelisted start_timer and complete_job_card methods behind them) accepted work before any Material Transfer for Manufacture existed; the transfer gate only fired on job card submission. Run validate_transfer_qty on both actions, and drop the finished_good escape in materials_ready so the dashboard hides the buttons while transfer is pending. Job cards that skip material transfer, corrective job cards, and work orders transferring against Work Order are exempt, as on submit. (cherry picked from commit 808b2e298432dfec7558da3262eff292b12b9027) --- erpnext/manufacturing/doctype/job_card/job_card.js | 3 +-- erpnext/manufacturing/doctype/job_card/job_card.py | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 2f62c5d1304..e55b25d5422 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -598,8 +598,7 @@ frappe.ui.form.on("Job Card", { const has_remaining_qty = doc.for_quantity + doc.process_loss_qty > doc.total_completed_qty; const pending_transfer = has_items && doc.items.some((row) => flt(row.transferred_qty) < flt(row.required_qty)); - const materials_ready = - doc.skip_material_transfer || !pending_transfer || !doc.finished_good || !has_items; + const materials_ready = doc.skip_material_transfer || !pending_transfer; let last_row = {}; const has_sub_ops_or_pending_qty = doc.sub_operations?.length || doc.pending_qty > 0; diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 5c248a3e3e3..0e05bfb18a8 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -1579,6 +1579,7 @@ class JobCard(Document): def start_timer(self, **kwargs): frappe.has_permission("Job Card", "write", doc=self, throw=True) self.validate_docstatus() + self.validate_transfer_qty() if isinstance(kwargs, dict): kwargs = frappe._dict(kwargs) @@ -1596,6 +1597,7 @@ class JobCard(Document): frappe.has_permission("Job Card", "write", doc=self, throw=True) self.validate_docstatus() + self.validate_transfer_qty() if isinstance(kwargs, dict): kwargs = frappe._dict(kwargs) From 94e4c53b8b86e0aca592ab02ee8af15ff2c2332e Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 13:14:38 +0530 Subject: [PATCH 112/134] test: job card start and completion blocked until material transfer (cherry picked from commit c95705dc64489069d4b7042700f5a7b0b4620168) --- .../doctype/job_card/test_job_card.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 1d58ebb5bce..c12710318bd 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -361,6 +361,31 @@ class TestJobCard(ERPNextTestSuite): # JC is Completed with excess transfer self.assertEqual(job_card.status, "Completed") + def test_job_card_actions_blocked_until_material_transfer(self): + "Start and Complete must wait for the transfer when RMs move against Job Card." + self.transfer_material_against = "Job Card" + self.source_warehouse = "Stores - _TC" + + self.generate_required_stock(self.work_order) + job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name}) + + self.assertRaises(frappe.ValidationError, job_card.start_timer, start_time=now()) + self.assertRaises(frappe.ValidationError, job_card.complete_job_card, qty=2, for_quantity=2) + + transfer_entry = make_stock_entry_from_jc(job_card.name) + transfer_entry.insert() + transfer_entry.submit() + + job_card.reload() + job_card.append("time_logs", {"from_time": "2024-03-01 08:00:00"}) + job_card.save() + job_card.complete_job_card( + qty=2, for_quantity=2, pending_qty=0, process_loss_qty=0, end_time="2024-03-01 09:00:00" + ) + + job_card.reload() + self.assertEqual(flt(job_card.total_completed_qty), 2) + @ERPNextTestSuite.change_settings("Manufacturing Settings", {"job_card_excess_transfer": 0}) def test_job_card_excess_material_transfer_block(self): self.transfer_material_against = "Job Card" From 4ee276d3dbc27e48097875a53ad2eefff48dbfe5 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 13:18:55 +0530 Subject: [PATCH 113/134] fix: keep job card actions visible for corrective job cards Corrective job cards regenerate required items but are exempt from the transfer gate on the server; mirror that exemption in materials_ready. (cherry picked from commit e9533495fcf52257e620d6fd0a5f3e30c3b7f7a3) --- erpnext/manufacturing/doctype/job_card/job_card.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index e55b25d5422..cddc15475ab 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -598,7 +598,7 @@ frappe.ui.form.on("Job Card", { const has_remaining_qty = doc.for_quantity + doc.process_loss_qty > doc.total_completed_qty; const pending_transfer = has_items && doc.items.some((row) => flt(row.transferred_qty) < flt(row.required_qty)); - const materials_ready = doc.skip_material_transfer || !pending_transfer; + const materials_ready = doc.skip_material_transfer || doc.is_corrective_job_card || !pending_transfer; let last_row = {}; const has_sub_ops_or_pending_qty = doc.sub_operations?.length || doc.pending_qty > 0; From a5f0b6605ea3584afb6707e9e6f97790f8a36b19 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 13:49:52 +0530 Subject: [PATCH 114/134] chore(selling): annotate make_sales_invoice arguments --- erpnext/selling/doctype/sales_order/sales_order.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 6550d1d5d0c..b77145f8b69 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -10,6 +10,7 @@ import frappe.utils from frappe import _, qb from frappe.contacts.doctype.address.address import get_company_address from frappe.desk.notifications import clear_doctype_notifications +from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc from frappe.model.utils import get_fetch_values from frappe.query_builder.functions import Sum @@ -1327,7 +1328,12 @@ def get_qty_net_of_returns(so_item) -> float: @frappe.whitelist() -def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, args=None): +def make_sales_invoice( + source_name: str, + target_doc: str | dict | Document | None = None, + ignore_permissions: bool = False, + args: str | dict | None = None, +): if args is None: args = {} if isinstance(args, str): From fee003a82dd2b0ee8c8a15e76e8e8f86e49492ae Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 14:08:20 +0530 Subject: [PATCH 115/134] fix: preserve original operation idx in manually created Job Cards The Create Job Card dialog on Work Order lists only pending operations, so the row idx sent to make_job_card is the dialog's position, not the Work Order Operation idx. create_job_card stamped that dialog idx into operation_row_id, and get_required_items then matched raw materials of whichever operation held that idx originally. Resolve idx server-side from the Work Order Operation row that get_operation_details already looks up by name. Fixes https://github.com/frappe/erpnext/issues/57985 --- erpnext/manufacturing/doctype/work_order/work_order.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index bb3d256ffee..2af7497564b 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -2825,6 +2825,7 @@ def get_operation_details(name, work_order, parent_bom): for row in work_order.operations: if row.name == name: return { + "idx": row.idx, "workstation": row.workstation, "workstation_type": row.workstation_type, "source_warehouse": row.source_warehouse, From 62a851bebb96533419340bc9cdf38c5f0713638b Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 15:09:48 +0530 Subject: [PATCH 116/134] fix(setup): fetch driver address by supplier link (cherry picked from commit 3ffb888d266031f8411030b4bc89bd2521c28fe4) --- erpnext/setup/doctype/driver/driver.js | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/erpnext/setup/doctype/driver/driver.js b/erpnext/setup/doctype/driver/driver.js index 35f8bff5874..3372b75bc35 100644 --- a/erpnext/setup/doctype/driver/driver.js +++ b/erpnext/setup/doctype/driver/driver.js @@ -23,15 +23,20 @@ frappe.ui.form.on("Driver", { }, transporter: function (frm, cdt, cdn) { - // this assumes that supplier's address has same title as supplier's name if (!frm.doc.transporter) return; - frappe.db - .get_doc("Address", null, { address_title: frm.doc.transporter }) - .then((r) => { - frappe.model.set_value(cdt, cdn, "address", r.name); - }) - .catch((err) => { - console.log(err); - }); + + const transporter = frm.doc.transporter; + frappe.call({ + method: "frappe.contacts.doctype.address.address.get_default_address", + args: { + doctype: "Supplier", + name: transporter, + }, + callback: function (r) { + if (frm.doc.transporter === transporter) { + frappe.model.set_value(cdt, cdn, "address", r.message); + } + }, + }); }, }); From 0dfa54f8127c44ae774ea8d6288d24315fd89e0e Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 16:33:53 +0530 Subject: [PATCH 117/134] fix(stock): validate warehouse accounts when used --- erpnext/controllers/stock_controller.py | 17 ++- erpnext/setup/doctype/company/company.py | 1 + erpnext/stock/__init__.py | 11 +- .../purchase_receipt/purchase_receipt.py | 15 ++- .../stock/doctype/warehouse/test_warehouse.py | 107 ++++++++++++++++++ erpnext/stock/doctype/warehouse/warehouse.py | 10 +- 6 files changed, 147 insertions(+), 14 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 3d2929e29df..6567d10fb4e 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -32,7 +32,7 @@ from erpnext.exceptions import ( ) from erpnext.setup.doctype.brand.brand import get_brand_defaults from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults -from erpnext.stock import get_warehouse_account_map +from erpnext.stock import get_warehouse_account, get_warehouse_account_map from erpnext.stock.doctype.batch.batch import get_batch_qty from erpnext.stock.doctype.inventory_dimension.inventory_dimension import ( get_evaluated_inventory_dimension, @@ -268,7 +268,9 @@ class StockController(AccountsController): def use_item_inventory_account(self): return frappe.get_cached_value("Company", self.company, "enable_item_wise_inventory_account") - def get_inventory_account_dict(self, row, inventory_account_map, warehouse_field=None): + def get_inventory_account_dict( + self, row, inventory_account_map, warehouse_field=None, *, raise_error=True + ): account_dict = frappe._dict() if isinstance(row, dict): @@ -297,8 +299,15 @@ class StockController(AccountsController): if not warehouse: warehouse = self.get(warehouse_field) - if warehouse and warehouse in inventory_account_map: - account_dict = inventory_account_map[warehouse] + if warehouse: + account_dict = inventory_account_map.get(warehouse) + if not account_dict and raise_error: + account = get_warehouse_account(frappe.get_cached_doc("Warehouse", warehouse)) + account_dict = frappe._dict( + account=account, + account_currency=frappe.get_cached_value("Account", account, "account_currency"), + ) + inventory_account_map[warehouse] = account_dict return account_dict diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 20c7142b9a9..e08ac8d7fd4 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -410,6 +410,7 @@ class Company(NestedSet): ) warehouse.flags.ignore_permissions = True warehouse.flags.ignore_mandatory = True + warehouse.flags.ignore_inventory_account_validation = True warehouse.insert() if wh_detail["is_group"]: diff --git a/erpnext/stock/__init__.py b/erpnext/stock/__init__.py index 032bf25a125..319da20c614 100644 --- a/erpnext/stock/__init__.py +++ b/erpnext/stock/__init__.py @@ -37,7 +37,7 @@ def get_warehouse_account_map(company=None): order_by="lft, rgt", ): if not d.account: - d.account = get_warehouse_account(d, warehouse_account) + d.account = get_warehouse_account(d, warehouse_account, raise_error=False) if d.account: d.account_currency = frappe.db.get_value("Account", d.account, "account_currency", cache=True) @@ -47,10 +47,13 @@ def get_warehouse_account_map(company=None): else: frappe.flags.warehouse_account_map = warehouse_account - return frappe.flags.warehouse_account_map.get(company) or frappe.flags.warehouse_account_map + if company: + return frappe.flags.warehouse_account_map.get(company, frappe._dict()) + + return frappe.flags.warehouse_account_map -def get_warehouse_account(warehouse, warehouse_account=None): +def get_warehouse_account(warehouse, warehouse_account=None, *, raise_error=True): account = warehouse.account if not account and warehouse.parent_warehouse: if warehouse_account: @@ -86,7 +89,7 @@ def get_warehouse_account(warehouse, warehouse_account=None): if len(inventory_accounts) == 1: account = inventory_accounts[0] - if not account and warehouse.company and not warehouse.is_group: + if raise_error and not account and warehouse.company and not warehouse.is_group: frappe.throw( _("Please set Account in Warehouse {0} or Default Inventory Account in Company {1}").format( warehouse.name, warehouse.company diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index d1a8cb16a70..272a6705a86 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -17,6 +17,7 @@ from erpnext.accounts.utils import get_account_currency from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled from erpnext.controllers.accounts_controller import merge_taxes from erpnext.controllers.buying_controller import BuyingController +from erpnext.stock import get_warehouse_account from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_transaction from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import StockReservation from erpnext.stock.serial_batch_bundle import ( @@ -765,11 +766,15 @@ class PurchaseReceipt(BuyingController): supplier_warehouse_account = None supplier_warehouse_account_currency = None if self.supplier_warehouse: - if _inv_dict := self.get_inventory_account_dict( - d, inventory_account_map, "supplier_warehouse" - ): - supplier_warehouse_account = _inv_dict["account"] - supplier_warehouse_account_currency = _inv_dict["account_currency"] + # The account is optional only when this lookup can skip a duplicate entry. + supplier_warehouse_account = get_warehouse_account( + frappe.get_cached_doc("Warehouse", self.supplier_warehouse), + raise_error=bool(flt(d.rm_supp_cost)), + ) + if supplier_warehouse_account: + supplier_warehouse_account_currency = get_account_currency( + supplier_warehouse_account + ) # If PR is sub-contracted and fg item rate is zero # in that case if account for source and target warehouse are same, diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py index f7bf8670f25..1dfb1b6337b 100644 --- a/erpnext/stock/doctype/warehouse/test_warehouse.py +++ b/erpnext/stock/doctype/warehouse/test_warehouse.py @@ -117,6 +117,86 @@ class TestWarehouse(ERPNextTestSuite): ) self.assertRaises(frappe.ValidationError, get_warehouse_account, warehouse) + def test_unrelated_warehouse_without_inventory_account_is_ignored(self): + from erpnext.stock import get_warehouse_account_map + + company, warehouse = create_ambiguous_inventory_account_warehouse() + warehouse_account_map = get_warehouse_account_map(company) + resolved_warehouse = next(iter(warehouse_account_map)) + stock_entry = frappe.get_doc({"doctype": "Stock Entry", "company": company}) + + self.assertNotIn(warehouse.name, warehouse_account_map) + self.assertTrue( + stock_entry.get_inventory_account_dict( + frappe._dict(warehouse=resolved_warehouse), warehouse_account_map + ).account + ) + self.assertFalse( + stock_entry.get_inventory_account_dict( + frappe._dict(supplier_warehouse=warehouse.name), + warehouse_account_map, + "supplier_warehouse", + raise_error=False, + ) + ) + + def test_warehouse_without_inventory_account_is_validated_when_used(self): + from erpnext.stock import get_warehouse_account_map + + company, warehouse = create_ambiguous_inventory_account_warehouse() + stock_entry = frappe.get_doc({"doctype": "Stock Entry", "company": company}) + + with self.assertRaises(frappe.ValidationError): + stock_entry.get_inventory_account_dict( + frappe._dict(warehouse=warehouse.name), get_warehouse_account_map(company) + ) + + def test_new_warehouse_requires_inventory_account(self): + company, _warehouse = create_ambiguous_inventory_account_warehouse() + frappe.db.set_value("Company", company, "enable_perpetual_inventory", 1) + parent_warehouse = frappe.db.get_value("Warehouse", {"company": company, "is_group": 1}, "name") + frappe.db.set_value("Warehouse", parent_warehouse, "account", None) + warehouse = frappe.get_doc( + { + "doctype": "Warehouse", + "warehouse_name": "Missing Inventory Account", + "parent_warehouse": parent_warehouse, + "company": company, + } + ) + + self.assertRaises(frappe.ValidationError, warehouse.insert) + + def test_new_warehouse_can_inherit_inventory_account(self): + from erpnext.stock import get_warehouse_account + + company, _warehouse = create_ambiguous_inventory_account_warehouse() + frappe.db.set_value("Company", company, "enable_perpetual_inventory", 1) + parent_warehouse = frappe.db.get_value("Warehouse", {"company": company, "is_group": 1}, "name") + inventory_account = frappe.db.get_value( + "Account", {"company": company, "account_type": "Stock", "is_group": 0}, "name" + ) + frappe.db.set_value("Warehouse", parent_warehouse, "account", inventory_account) + + warehouse = frappe.get_doc( + { + "doctype": "Warehouse", + "warehouse_name": "Inherited Inventory Account", + "parent_warehouse": parent_warehouse, + "company": company, + } + ).insert() + + self.assertEqual(get_warehouse_account(warehouse), inventory_account) + + def test_warehouse_onload_allows_missing_inventory_account(self): + company, warehouse = create_ambiguous_inventory_account_warehouse() + frappe.db.set_value("Company", company, "enable_perpetual_inventory", 1) + + warehouse.run_method("onload") + + self.assertNotIn("account", warehouse.get_onload()) + def create_inventory_fallback_company(): company = "_Test Company Inventory Fallback" @@ -134,6 +214,33 @@ def create_inventory_fallback_company(): return company +def create_ambiguous_inventory_account_warehouse(): + company = create_inventory_fallback_company() + frappe.db.set_value("Company", company, "default_inventory_account", None) + + single_account = frappe.db.get_value( + "Account", {"account_type": "Stock", "is_group": 0, "company": company}, "name" + ) + warehouses = frappe.get_all( + "Warehouse", filters={"company": company, "is_group": 0}, pluck="name", order_by="name" + ) + for warehouse_name in warehouses: + frappe.db.set_value("Warehouse", warehouse_name, "account", single_account) + + warehouse = frappe.get_doc("Warehouse", warehouses[0]) + warehouse.db_set({"account": None, "disabled": 0}) + + if not frappe.db.exists("Account", "Extra Inventory Account - _TCIF"): + create_account( + account_name="Extra Inventory Account", + parent_account=frappe.db.get_value("Account", single_account, "parent_account"), + account_type="Stock", + company=company, + ) + + return company, warehouse + + def create_warehouse(warehouse_name, properties=None, company=None): if not company: company = "_Test Company" diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index a55e8351446..52d9d8775e1 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -60,9 +60,17 @@ class Warehouse(NestedSet): self.name = self.warehouse_name + def before_insert(self): + if ( + self.company + and not self.flags.ignore_inventory_account_validation + and frappe.get_cached_value("Company", self.company, "enable_perpetual_inventory") + ): + get_warehouse_account(self, get_warehouse_account_map(self.company)) + def onload(self): if self.company and cint(frappe.db.get_value("Company", self.company, "enable_perpetual_inventory")): - account = self.account or get_warehouse_account(self) + account = self.account or get_warehouse_account(self, raise_error=False) if account: self.set_onload("account", account) From fa733e691b19e3e8fbf9ebd621109f05d2e1c0e5 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 16:15:31 +0530 Subject: [PATCH 118/134] fix(stock): repair duplicated purchase receipt billing (cherry picked from commit 8b7e04eae14d636b234d2295fac98aeef2b4c0ec) --- erpnext/patches.txt | 1 + ...lculate_purchase_receipt_billing_status.py | 90 ++++++++++++++++ .../purchase_receipt/purchase_receipt.py | 9 ++ .../purchase_receipt/test_purchase_receipt.py | 100 ++++++++++++++++++ 4 files changed, 200 insertions(+) create mode 100644 erpnext/patches/v16_0/recalculate_purchase_receipt_billing_status.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index fc536cce751..a9fa908e29d 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -498,3 +498,4 @@ erpnext.patches.v16_0.backfill_repost_accounting_ledger_status erpnext.patches.v16_0.merge_seeded_item_group_root erpnext.patches.v16_0.rename_italy_customer_name_fields erpnext.patches.v16_0.set_stock_uom_in_job_card +erpnext.patches.v16_0.recalculate_purchase_receipt_billing_status diff --git a/erpnext/patches/v16_0/recalculate_purchase_receipt_billing_status.py b/erpnext/patches/v16_0/recalculate_purchase_receipt_billing_status.py new file mode 100644 index 00000000000..4acbe0375f8 --- /dev/null +++ b/erpnext/patches/v16_0/recalculate_purchase_receipt_billing_status.py @@ -0,0 +1,90 @@ +from collections import defaultdict + +import frappe +from frappe.query_builder.functions import Count +from frappe.utils import flt + +from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( + get_billed_amount_against_po, + get_billed_amount_against_pr, + get_purchase_receipts_against_po_details, + update_billed_amount_based_on_po, + update_billing_percentage, +) + + +def execute(): + purchase_order_items = get_affected_purchase_order_items() + if not purchase_order_items: + return + + updated_purchase_receipts = update_billed_amount_based_on_po(purchase_order_items) + for purchase_receipt in set(updated_purchase_receipts): + update_billing_percentage(frappe.get_doc("Purchase Receipt", purchase_receipt)) + + +def get_affected_purchase_order_items() -> list[str]: + purchase_order_items = get_candidate_purchase_order_items() + if not purchase_order_items: + return [] + + purchase_receipt_items = get_purchase_receipts_against_po_details(purchase_order_items) + direct_billed_amounts = get_billed_amount_against_pr([item.name for item in purchase_receipt_items]) + po_billed_amounts = get_billed_amount_against_po(purchase_order_items) + + current_billed_amounts = defaultdict(float) + available_billed_amounts = defaultdict(float) + for purchase_order_item, billed_details in po_billed_amounts.items(): + available_billed_amounts[purchase_order_item] = flt(billed_details["billed_amt"]) + + for item in purchase_receipt_items: + current_billed_amounts[item.purchase_order_item] += flt(item.billed_amt) + available_billed_amounts[item.purchase_order_item] += flt(direct_billed_amounts.get(item.name)) + + precision = frappe.get_precision("Purchase Receipt Item", "billed_amt") or 2 + return [ + purchase_order_item + for purchase_order_item in purchase_order_items + if flt(po_billed_amounts.get(purchase_order_item, {}).get("billed_amt")) > 0 + and flt(po_billed_amounts.get(purchase_order_item, {}).get("billed_qty")) > 0 + and flt( + current_billed_amounts[purchase_order_item] - available_billed_amounts[purchase_order_item], + precision, + ) + > 0 + ] + + +def get_candidate_purchase_order_items() -> list[str]: + purchase_receipt = frappe.qb.DocType("Purchase Receipt") + purchase_receipt_item = frappe.qb.DocType("Purchase Receipt Item") + purchase_invoice = frappe.qb.DocType("Purchase Invoice") + purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item") + + purchase_order_items_with_multiple_receipts = ( + frappe.qb.from_(purchase_receipt_item) + .inner_join(purchase_receipt) + .on(purchase_receipt_item.parent == purchase_receipt.name) + .select(purchase_receipt_item.purchase_order_item) + .where( + (purchase_receipt.docstatus == 1) + & (purchase_receipt.is_return == 0) + & purchase_receipt_item.purchase_order_item.isnotnull() + ) + .groupby(purchase_receipt_item.purchase_order_item) + .having(Count(purchase_receipt_item.name) > 1) + ) + + return ( + frappe.qb.from_(purchase_invoice_item) + .inner_join(purchase_invoice) + .on(purchase_invoice_item.parent == purchase_invoice.name) + .select(purchase_invoice_item.po_detail) + .distinct() + .where( + (purchase_invoice.docstatus == 1) + & (purchase_invoice.update_stock == 0) + & purchase_invoice_item.pr_detail.isnull() + & purchase_invoice_item.po_detail.isin(purchase_order_items_with_multiple_receipts) + ) + ).run(pluck=True) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index d1a8cb16a70..a1073df1f1b 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1164,6 +1164,15 @@ def update_billed_amount_based_on_po(po_details, update_modified=True, pr_doc=No billed_amt_against_pr = flt(flt(billed_amt_against_po) * flt(pr_item.qty)) / flt( billed_qty_against_po ) + + # Deduct the amount and qty consumed by this PR so that the next PR + # against the same PO Item does not get billed for the same amount again. + po_billed_amt_details[pr_item.purchase_order_item]["billed_amt"] = ( + billed_amt_against_po - billed_amt_against_pr + ) + po_billed_amt_details[pr_item.purchase_order_item]["billed_qty"] = ( + billed_qty_against_po - pr_item.qty + ) else: pending_to_bill = flt(pr_item.amount) - billed_amt_against_pr if pending_to_bill <= billed_amt_against_po: diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index d1ec25650af..284573342bf 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1,6 +1,8 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt +from unittest.mock import patch + import frappe from frappe.utils import add_days, cint, cstr, flt, get_datetime, getdate, nowtime, today from pypika import functions as fn @@ -762,6 +764,104 @@ class TestPurchaseReceipt(ERPNextTestSuite): po.reload() po.cancel() + def test_pr_billing_status_for_po_invoice_across_multiple_receipts(self): + """When a Purchase Invoice is raised directly from a PO and the invoiced qty + spans more than one Purchase Receipt, the billed amount must be split between + the receipts (FIFO), not duplicated. A receipt with no amount left to consume + must not show as fully billed / Completed. + + Flow: + 1. PO (Qty: 10, Rate: 500) -> PI for Qty 5 (Amount 2500) + 2. PO -> PR1 (Qty 3) -> gets 1500 billed (fully billed) + 3. PO -> PR2 (Qty 3) -> gets the remaining 1000 billed (partly billed) + """ + from erpnext.buying.doctype.purchase_order.purchase_order import ( + make_purchase_invoice as make_purchase_invoice_from_po, + ) + from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt + from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order + + # Qty: 10, Rate: 500 + po = create_purchase_order() + + pi = make_purchase_invoice_from_po(po.name) + pi.get("items")[0].qty = 5 + pi.submit() + + pr1 = make_purchase_receipt(po.name) + pr1.posting_date = today() + pr1.posting_time = "08:00" + pr1.get("items")[0].received_qty = 3 + pr1.get("items")[0].qty = 3 + pr1.submit() + + pr2 = make_purchase_receipt(po.name) + pr2.posting_date = today() + pr2.posting_time = "10:00" + pr2.get("items")[0].received_qty = 3 + pr2.get("items")[0].qty = 3 + pr2.submit() + + # PR1 consumes 3 * 500 = 1500 out of the 2500 invoiced -> fully billed. + pr1.load_from_db() + self.assertEqual(pr1.get("items")[0].billed_amt, 1500) + self.assertEqual(pr1.per_billed, 100) + self.assertEqual(pr1.status, "Completed") + + # PR2 must only get the remaining 1000 (not 1500 again) -> partly billed. + pr2.load_from_db() + self.assertEqual(pr2.get("items")[0].billed_amt, 1000) + self.assertEqual(flt(pr2.per_billed, 2), 66.67) + self.assertEqual(pr2.status, "Partly Billed") + + from erpnext.patches.v16_0 import recalculate_purchase_receipt_billing_status + + purchase_order_item = po.items[0].name + with patch.object( + recalculate_purchase_receipt_billing_status, + "get_candidate_purchase_order_items", + return_value=[purchase_order_item], + ): + self.assertEqual( + recalculate_purchase_receipt_billing_status.get_affected_purchase_order_items(), [] + ) + + frappe.db.set_value( + "Purchase Receipt Item", + pr2.items[0].name, + "billed_amt", + 1500, + update_modified=False, + ) + frappe.db.set_value( + "Purchase Receipt", + pr2.name, + {"per_billed": 100, "status": "Completed"}, + update_modified=False, + ) + + self.assertEqual( + recalculate_purchase_receipt_billing_status.get_affected_purchase_order_items(), + [purchase_order_item], + ) + recalculate_purchase_receipt_billing_status.execute() + self.assertEqual( + recalculate_purchase_receipt_billing_status.get_affected_purchase_order_items(), [] + ) + + pr2.load_from_db() + self.assertEqual(pr2.get("items")[0].billed_amt, 1000) + self.assertEqual(flt(pr2.per_billed, 2), 66.67) + self.assertEqual(pr2.status, "Partly Billed") + + pr2.cancel() + pr1.reload() + pr1.cancel() + pi.reload() + pi.cancel() + po.reload() + po.cancel() + def test_serial_no_against_purchase_receipt(self): item_code = "Test Manual Created Serial No" if not frappe.db.exists("Item", item_code): From 9139994686bbdb34f03a70c7bbf0d5d101db36ac Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 18:16:38 +0530 Subject: [PATCH 119/134] fix: skip PO items with invoice-created receipts in billing repair patch A Purchase Receipt row created from a Purchase Invoice carries both purchase_order_item and purchase_invoice_item, and its billed_amt is pinned to the row amount by update_billing_status. Redistributing the PO-invoiced pool over such rows zeroes the invoice-created receipt and flips it from Completed to To Bill, so the repair leaves those PO Items untouched. (cherry picked from commit ace4230f973d13fbd9d67a2d11c18e4661ed13b5) --- ...lculate_purchase_receipt_billing_status.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/erpnext/patches/v16_0/recalculate_purchase_receipt_billing_status.py b/erpnext/patches/v16_0/recalculate_purchase_receipt_billing_status.py index 4acbe0375f8..9f0afb79c2e 100644 --- a/erpnext/patches/v16_0/recalculate_purchase_receipt_billing_status.py +++ b/erpnext/patches/v16_0/recalculate_purchase_receipt_billing_status.py @@ -25,6 +25,11 @@ def execute(): def get_affected_purchase_order_items() -> list[str]: purchase_order_items = get_candidate_purchase_order_items() + if purchase_order_items: + purchase_order_items = exclude_purchase_order_items_with_invoice_created_receipts( + purchase_order_items + ) + if not purchase_order_items: return [] @@ -55,6 +60,21 @@ def get_affected_purchase_order_items() -> list[str]: ] +def exclude_purchase_order_items_with_invoice_created_receipts(purchase_order_items: list[str]) -> list[str]: + invoice_created_receipt_items = set( + frappe.get_all( + "Purchase Receipt Item", + filters={ + "purchase_order_item": ("in", purchase_order_items), + "purchase_invoice_item": ("is", "set"), + "docstatus": 1, + }, + pluck="purchase_order_item", + ) + ) + return [item for item in purchase_order_items if item not in invoice_created_receipt_items] + + def get_candidate_purchase_order_items() -> list[str]: purchase_receipt = frappe.qb.DocType("Purchase Receipt") purchase_receipt_item = frappe.qb.DocType("Purchase Receipt Item") From e9b7d8195bf82ff88746fed95723ae8600cd471d Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 18:16:38 +0530 Subject: [PATCH 120/134] test: cover repair patch exclusion for invoice-created receipts (cherry picked from commit d34519f536ac166e5b9cf5dccf18f261e35ad2e1) --- .../purchase_receipt/test_purchase_receipt.py | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 284573342bf..4e9dc7f747a 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -862,6 +862,76 @@ class TestPurchaseReceipt(ERPNextTestSuite): po.reload() po.cancel() + def test_billing_repair_patch_skips_invoice_created_receipts(self): + """A Purchase Receipt created from a Purchase Invoice keeps billed_amt = amount + by definition. When such a receipt coexists with a receipt made directly from + the PO, the stored total can exceed the PO-invoiced amount, but the repair + patch must leave those PO Items alone instead of stripping the invoice-created + receipt. + + Flow: + 1. PO (Qty: 10, Rate: 500) -> PI for Qty 5 (Amount 2500) + 2. PO -> PR1 (Qty 5, direct) -> absorbs the full 2500 (fully billed) + 3. PI -> PR2 (Qty 5, created from the invoice) -> billed 2500 via invoice link + """ + from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import ( + make_purchase_receipt as make_purchase_receipt_from_pi, + ) + from erpnext.buying.doctype.purchase_order.purchase_order import ( + make_purchase_invoice as make_purchase_invoice_from_po, + ) + from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt + from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order + + po = create_purchase_order() + + pi = make_purchase_invoice_from_po(po.name) + pi.get("items")[0].qty = 5 + pi.submit() + + pr_direct = make_purchase_receipt(po.name) + pr_direct.get("items")[0].received_qty = 5 + pr_direct.get("items")[0].qty = 5 + pr_direct.submit() + + pr_direct.load_from_db() + self.assertEqual(pr_direct.get("items")[0].billed_amt, 2500) + self.assertEqual(pr_direct.per_billed, 100) + + pr_from_invoice = make_purchase_receipt_from_pi(pi.name) + pr_from_invoice.submit() + + pr_from_invoice.load_from_db() + self.assertEqual(pr_from_invoice.get("items")[0].billed_amt, 2500) + self.assertEqual(pr_from_invoice.per_billed, 100) + + from erpnext.patches.v16_0 import recalculate_purchase_receipt_billing_status + + purchase_order_item = po.items[0].name + with patch.object( + recalculate_purchase_receipt_billing_status, + "get_candidate_purchase_order_items", + return_value=[purchase_order_item], + ): + self.assertEqual( + recalculate_purchase_receipt_billing_status.get_affected_purchase_order_items(), [] + ) + recalculate_purchase_receipt_billing_status.execute() + + pr_direct.load_from_db() + self.assertEqual(pr_direct.get("items")[0].billed_amt, 2500) + pr_from_invoice.load_from_db() + self.assertEqual(pr_from_invoice.get("items")[0].billed_amt, 2500) + self.assertEqual(pr_from_invoice.status, "Completed") + + pr_from_invoice.cancel() + pr_direct.reload() + pr_direct.cancel() + pi.reload() + pi.cancel() + po.reload() + po.cancel() + def test_serial_no_against_purchase_receipt(self): item_code = "Test Manual Created Serial No" if not frappe.db.exists("Item", item_code): From fce0eb1577e0fd4d19ea3bf00d5676ef853e00ed Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 18:26:08 +0530 Subject: [PATCH 121/134] fix(stock): validate new warehouse inventory account after naming Move the insert-time check from before_insert to validate. before_insert runs before set_new_name, so the validation message rendered the warehouse name as None. validate runs after naming and only applies to new documents via is_new(). Resolve inheritance through the parent's lft/rgt bounds instead of the request-cached warehouse account map. The cached map can be stale within a request (a parent created moments earlier is missing from it), which made get_warehouse_account trigger a full nested-set rebuild_tree and could falsely reject a child whose parent carries a valid account. rebuild_tree enables auto_commit_on_many_writes, which must not run inside a document insert. --- erpnext/stock/doctype/warehouse/warehouse.py | 28 ++++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index 52d9d8775e1..28cdc730680 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -60,14 +60,6 @@ class Warehouse(NestedSet): self.name = self.warehouse_name - def before_insert(self): - if ( - self.company - and not self.flags.ignore_inventory_account_validation - and frappe.get_cached_value("Company", self.company, "enable_perpetual_inventory") - ): - get_warehouse_account(self, get_warehouse_account_map(self.company)) - def onload(self): if self.company and cint(frappe.db.get_value("Company", self.company, "enable_perpetual_inventory")): account = self.account or get_warehouse_account(self, raise_error=False) @@ -78,8 +70,28 @@ class Warehouse(NestedSet): self.set_onload("stock_exists", self.check_if_sle_exists(non_cancelled_only=True)) def validate(self): + self.validate_inventory_account() self.warn_about_multiple_warehouse_account() + def validate_inventory_account(self): + if ( + not self.is_new() + or not self.company + or self.flags.ignore_inventory_account_validation + or not frappe.get_cached_value("Company", self.company, "enable_perpetual_inventory") + ): + return + + warehouse = frappe._dict(self.as_dict()) + if not self.account and self.parent_warehouse: + parent_bounds = frappe.db.get_value( + "Warehouse", self.parent_warehouse, ["lft", "rgt"], as_dict=True + ) + if parent_bounds: + warehouse.update(parent_bounds) + + get_warehouse_account(warehouse) + def on_update(self): self.update_nsm_model() From f1ca9e26310bbc066609ccb0a93214ec2b6935ad Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 18:26:10 +0530 Subject: [PATCH 122/134] test(stock): cover named validation error and same-transaction parent inheritance --- .../stock/doctype/warehouse/test_warehouse.py | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py index 1dfb1b6337b..b5035b00e9b 100644 --- a/erpnext/stock/doctype/warehouse/test_warehouse.py +++ b/erpnext/stock/doctype/warehouse/test_warehouse.py @@ -165,7 +165,7 @@ class TestWarehouse(ERPNextTestSuite): } ) - self.assertRaises(frappe.ValidationError, warehouse.insert) + self.assertRaisesRegex(frappe.ValidationError, "Missing Inventory Account - _TCIF", warehouse.insert) def test_new_warehouse_can_inherit_inventory_account(self): from erpnext.stock import get_warehouse_account @@ -189,6 +189,37 @@ class TestWarehouse(ERPNextTestSuite): self.assertEqual(get_warehouse_account(warehouse), inventory_account) + def test_new_warehouse_inherits_from_parent_created_in_same_transaction(self): + from erpnext.stock import get_warehouse_account + + company, _warehouse = create_ambiguous_inventory_account_warehouse() + frappe.db.set_value("Company", company, "enable_perpetual_inventory", 1) + root_warehouse = frappe.db.get_value("Warehouse", {"company": company, "is_group": 1}, "name") + inventory_account = frappe.db.get_value( + "Account", {"company": company, "account_type": "Stock", "is_group": 0}, "name" + ) + + parent_warehouse = frappe.get_doc( + { + "doctype": "Warehouse", + "warehouse_name": "New Parent Warehouse", + "parent_warehouse": root_warehouse, + "company": company, + "is_group": 1, + "account": inventory_account, + } + ).insert() + child_warehouse = frappe.get_doc( + { + "doctype": "Warehouse", + "warehouse_name": "New Child Warehouse", + "parent_warehouse": parent_warehouse.name, + "company": company, + } + ).insert() + + self.assertEqual(get_warehouse_account(child_warehouse), inventory_account) + def test_warehouse_onload_allows_missing_inventory_account(self): company, warehouse = create_ambiguous_inventory_account_warehouse() frappe.db.set_value("Company", company, "enable_perpetual_inventory", 1) From c8d0d457cba5108080e37cfd3a714985824f8364 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 18:30:57 +0530 Subject: [PATCH 123/134] chore(stock): drop redundant supplier warehouse comment --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 272a6705a86..bb3270f8136 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -766,7 +766,6 @@ class PurchaseReceipt(BuyingController): supplier_warehouse_account = None supplier_warehouse_account_currency = None if self.supplier_warehouse: - # The account is optional only when this lookup can skip a duplicate entry. supplier_warehouse_account = get_warehouse_account( frappe.get_cached_doc("Warehouse", self.supplier_warehouse), raise_error=bool(flt(d.rm_supp_cost)), From b5d727fb81e668a82ad5c562c3c14cfe7273b907 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 20:41:47 +0530 Subject: [PATCH 124/134] fix(selling): reset stale item details on item change (cherry picked from commit 009961edc73246d972b100b4e63e7c8e214d890d) --- erpnext/controllers/tests/test_reactivity.py | 36 ++++++++++++++++++++ erpnext/utilities/transaction_base.py | 11 ++++++ 2 files changed, 47 insertions(+) diff --git a/erpnext/controllers/tests/test_reactivity.py b/erpnext/controllers/tests/test_reactivity.py index 17f6f480589..a4f652722ae 100644 --- a/erpnext/controllers/tests/test_reactivity.py +++ b/erpnext/controllers/tests/test_reactivity.py @@ -46,3 +46,39 @@ class TestReactivity(ERPNextTestSuite): with self.subTest(field=field): self.assertIsNotNone(itm.get(field[0])) si.save().submit() + + def test_item_change_clears_stale_item_details(self): + from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order + from erpnext.stock.doctype.item.test_item import make_item + + old_item = make_item(properties={"is_stock_item": 0, "stock_uom": "Nos"}) + new_item = make_item( + properties={ + "is_stock_item": 0, + "stock_uom": "Kg", + "weight_per_unit": 2, + "weight_uom": "Kg", + } + ) + sales_order = make_sales_order(item_code=old_item.name, do_not_submit=True) + + item = sales_order.items[0] + self.assertEqual(item.uom, "Nos") + row_state = (item.qty, item.warehouse, item.delivery_date) + + sales_order.ignore_pricing_rule = 1 + item.weight_per_unit = 10 + item.weight_uom = "Nos" + item.barcode = "OLD-BARCODE" + item.pricing_rules = "OLD-PRICING-RULE" + item.item_code = new_item.name + sales_order.process_item_selection(item.idx) + + self.assertEqual(item.uom, "Kg") + self.assertEqual(item.stock_uom, "Kg") + self.assertEqual(item.conversion_factor, 1) + self.assertEqual(item.weight_per_unit, 2) + self.assertEqual(item.weight_uom, "Kg") + self.assertIsNone(item.barcode) + self.assertFalse(item.pricing_rules) + self.assertEqual((item.qty, item.warehouse, item.delivery_date), row_state) diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index e6cad737a6b..9f1886da98e 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -357,6 +357,17 @@ class TransactionBase(StatusUpdater): if not item_obj.item_code: return + # Do not carry item-specific values from the previously selected item. + for fieldname in ( + "weight_per_unit", + "weight_uom", + "uom", + "conversion_factor", + "barcode", + "pricing_rules", + ): + item_obj.set(fieldname, None) + # 'item_details' has latest item related values item_details = self.fetch_item_details(item_obj) From 218397e78d276b2e0d55824df36df0ad76bad975 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 20:54:04 +0530 Subject: [PATCH 125/134] fix(selling): preserve explicit UOM during item selection (cherry picked from commit e8c890a8443f4a8f995486ffba0e42902494453a) # Conflicts: # erpnext/utilities/transaction_base.py --- erpnext/controllers/tests/test_reactivity.py | 24 ++++++++++++++++++- erpnext/public/js/controllers/transaction.js | 1 + erpnext/utilities/transaction_base.py | 25 ++++++++++++-------- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/erpnext/controllers/tests/test_reactivity.py b/erpnext/controllers/tests/test_reactivity.py index a4f652722ae..448025ddddc 100644 --- a/erpnext/controllers/tests/test_reactivity.py +++ b/erpnext/controllers/tests/test_reactivity.py @@ -72,7 +72,7 @@ class TestReactivity(ERPNextTestSuite): item.barcode = "OLD-BARCODE" item.pricing_rules = "OLD-PRICING-RULE" item.item_code = new_item.name - sales_order.process_item_selection(item.idx) + sales_order.process_item_selection(item.idx, reset_item_details=True) self.assertEqual(item.uom, "Kg") self.assertEqual(item.stock_uom, "Kg") @@ -82,3 +82,25 @@ class TestReactivity(ERPNextTestSuite): self.assertIsNone(item.barcode) self.assertFalse(item.pricing_rules) self.assertEqual((item.qty, item.warehouse, item.delivery_date), row_state) + + def test_programmatic_item_selection_preserves_explicit_uom(self): + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice + from erpnext.stock.doctype.item.test_item import make_item + + item = make_item( + properties={ + "is_stock_item": 0, + "stock_uom": "Kg", + "sales_uom": "Nos", + "weight_per_unit": 2, + "weight_uom": "Kg", + }, + uoms=[{"uom": "Nos", "conversion_factor": 10}], + ) + sales_invoice = create_sales_invoice(item_code=item.name, uom="Kg", do_not_save=True) + + sales_invoice.process_item_selection(sales_invoice.items[0].idx) + + self.assertEqual(sales_invoice.items[0].uom, "Kg") + self.assertEqual(sales_invoice.items[0].conversion_factor, 1) + self.assertEqual(sales_invoice.items[0].stock_qty, sales_invoice.items[0].qty) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 9d64ef09dd0..d6e3a308bf5 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -761,6 +761,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe method: "process_item_selection", args: { item_idx: item.idx, + reset_item_details: true, }, callback: function (r) { if (!r.exc) { diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 9f1886da98e..7efc43936e7 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -350,23 +350,28 @@ class TransactionBase(StatusUpdater): ) @frappe.whitelist() +<<<<<<< HEAD def process_item_selection(self, item_idx): +======= + def process_item_selection(self, item_idx: int, reset_item_details: bool = False): +>>>>>>> e8c890a844 (fix(selling): preserve explicit UOM during item selection) # Server side 'item' doc. Update this to reflect in UI item_obj = self.get("items", {"idx": item_idx})[0] if not item_obj.item_code: return - # Do not carry item-specific values from the previously selected item. - for fieldname in ( - "weight_per_unit", - "weight_uom", - "uom", - "conversion_factor", - "barcode", - "pricing_rules", - ): - item_obj.set(fieldname, None) + if cint(reset_item_details): + # Do not carry item-specific values from the previously selected item. + for fieldname in ( + "weight_per_unit", + "weight_uom", + "uom", + "conversion_factor", + "barcode", + "pricing_rules", + ): + item_obj.set(fieldname, None) # 'item_details' has latest item related values item_details = self.fetch_item_details(item_obj) From bc6cb75bf4be6cfe44f1e27a3d9b64a00a7f4ca4 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 21:19:00 +0530 Subject: [PATCH 126/134] chore: resolve conflict --- erpnext/utilities/transaction_base.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 7efc43936e7..97dbcfaa4d6 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -350,11 +350,7 @@ class TransactionBase(StatusUpdater): ) @frappe.whitelist() -<<<<<<< HEAD - def process_item_selection(self, item_idx): -======= def process_item_selection(self, item_idx: int, reset_item_details: bool = False): ->>>>>>> e8c890a844 (fix(selling): preserve explicit UOM during item selection) # Server side 'item' doc. Update this to reflect in UI item_obj = self.get("items", {"idx": item_idx})[0] From f11d0d5cec4a4147ae6cb02dbf2aac644966706f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 16:42:43 +0000 Subject: [PATCH 127/134] =?UTF-8?q?fix(consolidated=20cash=20flow):=20corr?= =?UTF-8?q?ect=20totals=20and=20labels=20in=20section=20foo=E2=80=A6=20(ba?= =?UTF-8?q?ckport=20#57336)=20(#58056)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Vishnu Priya Baskaran <145791817+ervishnucs@users.noreply.github.com> --- erpnext/accounts/report/cash_flow/cash_flow.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/report/cash_flow/cash_flow.py b/erpnext/accounts/report/cash_flow/cash_flow.py index f90345abab0..35e5549d139 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.py +++ b/erpnext/accounts/report/cash_flow/cash_flow.py @@ -269,9 +269,13 @@ def add_total_row_account( consolidated=False, add_blank_row=True, ): + name_key = "account" if consolidated else "section" + parent_key = "parent_account" if consolidated else "parent_section" + label_str = "'" + str(label) + "'" + total_row = { - "section_name": "'" + _("{0}").format(label) + "'", - "section": "'" + _("{0}").format(label) + "'", + f"{name_key}_name": label_str, + name_key: label_str, "currency": currency, } @@ -282,15 +286,15 @@ def add_total_row_account( period_list = get_filtered_list_for_consolidated_report(filters, period_list) for row in data: - if row.get("parent_section"): + if row.get(parent_key): for period in period_list: key = period if consolidated else period["key"] total_row.setdefault(key, 0.0) total_row[key] += row.get(key, 0.0) - summary_data[label] += row.get(key) + summary_data[label] += row.get(key) or 0.0 total_row.setdefault("total", 0.0) - total_row["total"] += row["total"] + total_row["total"] += row.get("total", 0.0) out.append(total_row) @@ -431,7 +435,6 @@ def get_opening_range_using_fiscal_year(company, period_list): def get_report_summary(summary_data, currency): report_summary = [] - for label, value in summary_data.items(): report_summary.append({"value": value, "label": label, "datatype": "Currency", "currency": currency}) From b89229a93d7b68c89e198efe12dbf7ed233b2cfb Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Tue, 11 Aug 2026 22:22:10 +0530 Subject: [PATCH 128/134] fix(selling): read overdue amount from payment ledger, not gl tags (backport #57786) (#58057) Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com> --- erpnext/selling/doctype/customer/customer.py | 25 ++++----- .../selling/doctype/customer/test_customer.py | 52 +++++++++++++++++++ 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 76495a94959..0f776858cb8 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -731,8 +731,8 @@ def get_overdue_billing_threshold(customer: str, company: str) -> float: def get_customer_overdue_amount(customer: str, company: str) -> float: """Amount the customer owes past its due date, in company currency. - Follows the same rule as the Overdue invoice status, so a customer is only - blocked for what the invoice list already shows as overdue. + Reads the Payment Ledger, the same source as `outstanding_amount`, so this agrees + with the Overdue status the invoice list already shows. """ invoices = get_outstanding_invoices_for_customer(customer, company) if not invoices: @@ -745,27 +745,28 @@ def get_customer_overdue_amount(customer: str, company: str) -> float: def get_outstanding_invoices_for_customer(customer: str, company: str) -> list[frappe._dict]: from frappe.query_builder.functions import Sum - gl_entry = frappe.qb.DocType("GL Entry") + ple = frappe.qb.DocType("Payment Ledger Entry") sales_invoice = frappe.qb.DocType("Sales Invoice") - # debit - credit is always booked in company currency, so this is comparable to the overdue limit - outstanding = Sum(gl_entry.debit) - Sum(gl_entry.credit) + # the Payment Ledger, not the GL, carries allocations made after submit (reconciled advances). + # `amount` is booked in company currency, so this is comparable to the overdue limit. + outstanding = Sum(ple.amount) return ( - frappe.qb.from_(gl_entry) + frappe.qb.from_(ple) .inner_join(sales_invoice) - .on(sales_invoice.name == gl_entry.against_voucher) + .on(sales_invoice.name == ple.against_voucher_no) .select( sales_invoice.name, sales_invoice.due_date, sales_invoice.base_grand_total, outstanding.as_("outstanding"), ) - .where(gl_entry.party_type == "Customer") - .where(gl_entry.party == customer) - .where(gl_entry.company == company) - .where(gl_entry.is_cancelled == 0) - .where(gl_entry.against_voucher_type == "Sales Invoice") + .where(ple.party_type == "Customer") + .where(ple.party == customer) + .where(ple.company == company) + .where(ple.delinked == 0) + .where(ple.against_voucher_type == "Sales Invoice") .groupby(sales_invoice.name, sales_invoice.due_date, sales_invoice.base_grand_total) .having(outstanding > 0) ).run(as_dict=True) diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index e7fcf2519fd..d2c44b63a1c 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -422,6 +422,37 @@ class TestCustomer(ERPNextTestSuite): pe.submit() self.assertEqual(get_customer_overdue_amount("_Test Customer", "_Test Company"), baseline) + def test_get_customer_overdue_amount_ignores_advance_reconciled_after_submit(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice + + baseline = get_customer_overdue_amount("_Test Customer", "_Test Company") + + # advance received before the invoice exists, so it carries no reference row + pe = create_payment_entry( + company="_Test Company", + party_type="Customer", + party="_Test Customer", + payment_type="Receive", + paid_from="Debtors - _TC", + paid_to="Cash - _TC", + paid_amount=800, + ) + pe.posting_date = add_days(nowdate(), -60) + pe.submit() + + si = create_sales_invoice(qty=1, rate=800, posting_date=add_days(nowdate(), -30)) + self.assertEqual(get_customer_overdue_amount("_Test Customer", "_Test Company"), baseline + 800) + + reconcile_payment_against_invoice(pe, si) + + # reconciliation settles the invoice without re-tagging the payment's GL entries, so an + # overdue amount read off the GL would still count the full 800 here + si.reload() + self.assertEqual(si.outstanding_amount, 0) + self.assertEqual(si.status, "Paid") + self.assertEqual(get_customer_overdue_amount("_Test Customer", "_Test Company"), baseline) + def test_overdue_billing_threshold_on_submit(self): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice @@ -591,6 +622,27 @@ def set_credit_limit(customer, company, credit_limit): customer.credit_limits[-1].db_insert() +def reconcile_payment_against_invoice(payment_entry, sales_invoice): + """Allocate an unlinked payment against an invoice through the reconciliation tool.""" + pr = frappe.get_doc( + doctype="Payment Reconciliation", + company=sales_invoice.company, + party_type="Customer", + party=sales_invoice.customer, + receivable_payable_account=sales_invoice.debit_to, + ) + pr.get_unreconciled_entries() + pr.allocate_entries( + frappe._dict( + { + "invoices": [d.as_dict() for d in pr.invoices if d.invoice_number == sales_invoice.name], + "payments": [d.as_dict() for d in pr.payments if d.reference_name == payment_entry.name], + } + ) + ) + pr.reconcile() + + def set_overdue_billing_threshold(customer, company, threshold): customer = frappe.get_doc("Customer", customer) for d in customer.credit_limits: From 5f43c89c4521c076b991fb2a3b234140841b8ff4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:32:18 +0000 Subject: [PATCH 129/134] fix: item property updates in POS and transactions and add styling (backport #57189) (#58058) Co-authored-by: Vishnu Priya Baskaran <145791817+ervishnucs@users.noreply.github.com> Co-authored-by: Afsal Syed --- erpnext/public/scss/point-of-sale.scss | 1 + .../selling/page/point_of_sale/pos_item_details.js | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/erpnext/public/scss/point-of-sale.scss b/erpnext/public/scss/point-of-sale.scss index 5ef6e8fc5db..c16632d9a2f 100644 --- a/erpnext/public/scss/point-of-sale.scss +++ b/erpnext/public/scss/point-of-sale.scss @@ -837,6 +837,7 @@ flex-direction: column; padding: var(--padding-lg); padding-top: var(--padding-md); + overflow-y: auto; > .item-details-header { display: flex; diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index 988ab60d548..b1f56b8df4c 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -83,6 +83,17 @@ erpnext.PointOfSale.ItemDetails = class { this.item_row = item; this.currency = this.events.get_frm().doc.currency; + if (item.has_serial_no == null || item.has_batch_no == null) { + const r = await frappe.db.get_value("Item", item.item_code, [ + "has_serial_no", + "has_batch_no", + ]); + if (r && r.message) { + item.has_serial_no = r.message.has_serial_no; + item.has_batch_no = r.message.has_batch_no; + } + } + this.current_item = item; this.render_dom(item); From a707c82c0c05c4634bc346c889c74fcbcc584f7d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 00:03:02 +0530 Subject: [PATCH 130/134] fix: allow non-admin roles to import chart of accounts (backport #57454) (#58060) Co-authored-by: Vishnu Priya Baskaran <145791817+ervishnucs@users.noreply.github.com> --- .../chart_of_accounts_importer.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index 6a0958c7bb2..7cd489edc62 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -459,16 +459,12 @@ def unset_existing_data(company): frappe.db.set_value("Company", company, update_values, update_values) # remove accounts data from various doctypes - for doctype in [ - "Account", - "Party Account", - "Mode of Payment Account", - "Tax Withholding Account", - "Sales Taxes and Charges Template", - "Purchase Taxes and Charges Template", - ]: + for doctype in ["Account", "Sales Taxes and Charges Template", "Purchase Taxes and Charges Template"]: frappe.get_query(doctype, delete=True, filters={"company": company}, ignore_permissions=False).run() + for doctype in ["Party Account", "Mode of Payment Account", "Tax Withholding Account"]: + frappe.get_query(doctype, delete=True, filters={"company": company}, ignore_permissions=True).run() + def set_default_accounts(company): from erpnext.setup.doctype.company.company import install_country_fixtures From d680115fcbc3200a637bf62c4f98350aa2439615 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 18:38:32 +0000 Subject: [PATCH 131/134] fix: mirror rounding adjustment on distributed_discount_amount (backport #58047) (#58055) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- erpnext/controllers/taxes_and_totals.py | 3 ++- .../tests/test_distributed_discount.py | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index bd8db461524..e50a34e2f14 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -901,8 +901,9 @@ class calculate_taxes_and_totals: item.net_amount = flt( item.net_amount + rounding_difference, item.precision("net_amount") ) + # net_amount went up by rounding_difference, so its discount share goes down item.distributed_discount_amount = flt( - distributed_amount + rounding_difference, + distributed_amount - rounding_difference, item.precision("distributed_discount_amount"), ) net_total += rounding_difference diff --git a/erpnext/controllers/tests/test_distributed_discount.py b/erpnext/controllers/tests/test_distributed_discount.py index e5efe9518b5..4540e1fb7d8 100644 --- a/erpnext/controllers/tests/test_distributed_discount.py +++ b/erpnext/controllers/tests/test_distributed_discount.py @@ -60,6 +60,30 @@ class TestTaxesAndTotals(ERPNextTestSuite): self.assertAlmostEqual(so.net_total, 1272.73, places=2) self.assertEqual(so.grand_total, 1400) + @ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": 1}) + def test_distributed_discount_amount_with_rounding_adjustment(self): + so = make_sales_order(do_not_save=1) + so.apply_discount_on = "Net Total" + so.discount_amount = 10 + so.items[0].qty = 1 + so.items[0].rate = 100 + so.append("items", so.items[0].as_dict()) + so.append("items", so.items[0].as_dict()) + so.save() + + calculate_taxes_and_totals(so) + + # the rounding adjustment lands on the second line + self.assertAlmostEqual(so.items[1].net_amount, 96.66, places=2) + self.assertAlmostEqual(so.items[1].distributed_discount_amount, 3.34, places=2) + + for item in so.items: + self.assertAlmostEqual(item.amount - item.distributed_discount_amount, item.net_amount, places=2) + self.assertAlmostEqual( + sum(i.distributed_discount_amount for i in so.items), so.discount_amount, places=2 + ) + self.assertEqual(so.net_total, 290) + def test_100_percent_discount_with_inclusive_tax(self): """Test that 100% discount with inclusive taxes results in zero net_total""" so = make_sales_order(do_not_save=1) From a9d139ae6b83b3117a34efffcf224903e43b85b5 Mon Sep 17 00:00:00 2001 From: MochaMind Date: Wed, 12 Aug 2026 01:17:44 +0530 Subject: [PATCH 132/134] fix: sync translations from crowdin (#57841) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- erpnext/locale/ar.po | 1498 ++++++++++++++++++--------------- erpnext/locale/bg.po | 1496 +++++++++++++++++--------------- erpnext/locale/bs.po | 1778 +++++++++++++++++++++------------------ erpnext/locale/cs.po | 1496 +++++++++++++++++--------------- erpnext/locale/da.po | 1498 ++++++++++++++++++--------------- erpnext/locale/de.po | 1498 ++++++++++++++++++--------------- erpnext/locale/eo.po | 1498 ++++++++++++++++++--------------- erpnext/locale/es.po | 1498 ++++++++++++++++++--------------- erpnext/locale/fa.po | 1582 ++++++++++++++++++---------------- erpnext/locale/fr.po | 1496 +++++++++++++++++--------------- erpnext/locale/hi.po | 1496 +++++++++++++++++--------------- erpnext/locale/hr.po | 1672 +++++++++++++++++++----------------- erpnext/locale/hu.po | 1496 +++++++++++++++++--------------- erpnext/locale/id.po | 1496 +++++++++++++++++--------------- erpnext/locale/it.po | 1496 +++++++++++++++++--------------- erpnext/locale/ko.po | 1500 ++++++++++++++++++--------------- erpnext/locale/my.po | 1496 +++++++++++++++++--------------- erpnext/locale/nb.po | 1496 +++++++++++++++++--------------- erpnext/locale/nl.po | 1498 ++++++++++++++++++--------------- erpnext/locale/pl.po | 1496 +++++++++++++++++--------------- erpnext/locale/pt.po | 1496 +++++++++++++++++--------------- erpnext/locale/pt_BR.po | 1496 +++++++++++++++++--------------- erpnext/locale/ro.po | 1496 +++++++++++++++++--------------- erpnext/locale/ru.po | 1498 ++++++++++++++++++--------------- erpnext/locale/sl.po | 1496 +++++++++++++++++--------------- erpnext/locale/sr.po | 1498 ++++++++++++++++++--------------- erpnext/locale/sr_CS.po | 1498 ++++++++++++++++++--------------- erpnext/locale/sv.po | 1678 +++++++++++++++++++----------------- erpnext/locale/th.po | 1498 ++++++++++++++++++--------------- erpnext/locale/tr.po | 1498 ++++++++++++++++++--------------- erpnext/locale/uz.po | 1498 ++++++++++++++++++--------------- erpnext/locale/vi.po | 1498 ++++++++++++++++++--------------- erpnext/locale/zh.po | 1496 +++++++++++++++++--------------- 33 files changed, 26877 insertions(+), 23247 deletions(-) diff --git a/erpnext/locale/ar.po b/erpnext/locale/ar.po index c7631dfdb35..3980deafdb5 100644 --- a/erpnext/locale/ar.po +++ b/erpnext/locale/ar.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: hello@frappe.io\n" -"POT-Creation-Date: 2026-08-02 10:09+0000\n" -"PO-Revision-Date: 2026-08-03 08:58\n" +"POT-Creation-Date: 2026-08-09 09:47+0000\n" +"PO-Revision-Date: 2026-08-09 11:01\n" "Last-Translator: hello@frappe.io\n" "Language-Team: Arabic\n" "MIME-Version: 1.0\n" @@ -18,7 +18,7 @@ msgstr "" "X-Crowdin-File-ID: 169\n" "Language: ar_SA\n" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1657 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1707 msgid "\n" "\t\t\tThe Batch {0} of an item {1} has negative stock in the warehouse {2}{3}.\n" "\t\t\tPlease add a stock quantity of {4} to proceed with this entry.\n" @@ -45,7 +45,7 @@ msgstr " العنوان" msgid " Amount" msgstr " مبلغ" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:133 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:122 msgid " BOM" msgstr "" @@ -64,7 +64,7 @@ msgstr "" msgid " Is Subcontracted" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:215 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:204 msgid " Item" msgstr " سلعة" @@ -73,8 +73,8 @@ msgstr " سلعة" msgid " Name" msgstr " الاسم" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:163 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:204 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:152 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:193 msgid " Phantom Item" msgstr " عنصر شبح" @@ -82,7 +82,7 @@ msgstr " عنصر شبح" msgid " Rate" msgstr " سعر السلعة المفردة" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:141 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:130 msgid " Raw Material" msgstr "" @@ -91,8 +91,8 @@ msgstr "" msgid " Skip Material Transfer" msgstr " تخطي نقل المواد" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:152 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:193 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:141 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:182 msgid " Sub Assembly" msgstr " التجميع الفرعي" @@ -277,7 +277,7 @@ msgstr "" msgid "% of materials delivered against this Sales Order" msgstr "" -#: erpnext/controllers/accounts_controller.py:2414 +#: erpnext/controllers/accounts_controller.py:2419 msgid "'Account' in the Accounting section of Customer {0}" msgstr "" @@ -293,11 +293,11 @@ msgstr "'على أساس' و 'المجموعة حسب' لا يمكن أن يكو msgid "'Days Since Last Order' must be greater than or equal to zero" msgstr "يجب أن تكون \"الأيام منذ آخر طلب\" أكبر من أو تساوي الصفر" -#: erpnext/controllers/accounts_controller.py:2419 +#: erpnext/controllers/accounts_controller.py:2424 msgid "'Default {0} Account' in Company {1}" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1235 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1245 msgid "'Entries' cannot be empty" msgstr "المدخلات لا يمكن أن تكون فارغة" @@ -315,17 +315,17 @@ msgstr "\"من تاريخ \" يجب أن يكون بعد \" إلى تاريخ \" msgid "'Has Serial No' can not be 'Yes' for non-stock item" msgstr "\"لهُ رقم تسلسل\" لا يمكن ان يكون \"نعم\" لبند غير قابل للتخزين" -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:147 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:151 msgid "'Inspection Required before Delivery' has disabled for the item {0}, no need to create the QI" msgstr "تم تعطيل خيار \"الفحص مطلوب قبل التسليم\" للعنصر {0}، ولا حاجة لإنشاء QI" -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:138 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:142 msgid "'Inspection Required before Purchase' has disabled for the item {0}, no need to create the QI" msgstr "تم تعطيل 'الفحص مطلوب قبل الشراء' للعنصر {0}، لا حاجة لإنشاء QI" -#: erpnext/stock/report/stock_ledger/stock_ledger.py:685 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:726 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:831 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:688 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:781 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:913 msgid "'Opening'" msgstr "'افتتاحي'" @@ -365,17 +365,17 @@ msgid "'{0}' should be in company currency {1}." msgstr "" #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:174 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:214 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:223 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:106 msgid "(A) Qty After Transaction" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:219 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:228 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:111 msgid "(B) Expected Qty After Transaction" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:234 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:243 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:126 msgid "(C) Total Qty in Queue" msgstr "(ج) إجمالي الكمية في قائمة الانتظار" @@ -385,7 +385,7 @@ msgid "(C) Total qty in queue" msgstr "(ج) إجمالي الكمية في قائمة الانتظار" #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:194 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:244 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:253 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:136 msgid "(D) Balance Stock Value" msgstr "" @@ -396,12 +396,12 @@ msgid "(Daily Yield * No of Units Produced) / 100" msgstr "(العائد اليومي * عدد الوحدات المنتجة) / 100" #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:199 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:249 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:258 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:141 msgid "(E) Balance Stock Value in Queue" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:259 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:268 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:151 msgid "(F) Change in Stock Value" msgstr "" @@ -410,7 +410,7 @@ msgstr "" msgid "(Forecast)" msgstr "(توقعات)" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:264 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:273 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:156 msgid "(G) Sum of Change in Stock Value" msgstr "(ز) مجموع التغير في قيمة الأسهم" @@ -421,7 +421,7 @@ msgstr "(ز) مجموع التغير في قيمة الأسهم" msgid "(Good Units Produced / Total Units Produced) × 100" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:274 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:283 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:166 msgid "(H) Change in Stock Value (FIFO Queue)" msgstr "" @@ -436,17 +436,17 @@ msgstr "" msgid "(Hour Rate / 60) * Actual Operation Time" msgstr "(سعر الساعة / 60) * وقت العمل الفعلي" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:284 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:293 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:176 msgid "(I) Valuation Rate" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:289 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:298 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:181 msgid "(J) Valuation Rate as per FIFO" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:299 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:308 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:191 msgid "(K) Valuation = Value (D) ÷ Qty (A)" msgstr "" @@ -781,7 +781,7 @@ msgstr "" msgid "
  • Clearance date must be after cheque date for row(s): {0}
  • " msgstr "" -#: erpnext/controllers/accounts_controller.py:2297 +#: erpnext/controllers/accounts_controller.py:2302 msgid "
  • Item {0} in row(s) {1} billed more than {2}
  • " msgstr "" @@ -798,7 +798,7 @@ msgstr "" msgid "
  • {}
  • " msgstr "
  • {}
  • " -#: erpnext/controllers/accounts_controller.py:2294 +#: erpnext/controllers/accounts_controller.py:2299 msgid "

    Cannot overbill for the following Items:

    " msgstr "" @@ -842,7 +842,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:2306 +#: erpnext/controllers/accounts_controller.py:2311 msgid "

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

    " msgstr "" @@ -953,18 +953,18 @@ msgid "\n" "
    \n\n\n\n\n\n\n" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:224 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:233 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:116 msgid "A - B" msgstr "" #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:189 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:239 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:248 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:131 msgid "A - C" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:365 +#: erpnext/selling/doctype/customer/customer.py:366 msgid "A Customer Group exists with same name please change the Customer name or rename the Customer Group" msgstr "" @@ -998,7 +998,7 @@ msgstr "" msgid "A Reconciliation Job {0} is running for the same filters. Cannot reconcile now" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1773 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1783 msgid "A Reverse Journal Entry {0} already exists for this Journal Entry." msgstr "" @@ -1039,7 +1039,7 @@ msgstr "" msgid "A logical Warehouse against which stock entries are made." msgstr "" -#: erpnext/stock/serial_batch_bundle.py:1479 +#: erpnext/stock/serial_batch_bundle.py:1565 msgid "A naming series conflict occurred while creating serial numbers. Please change the naming series for the item {0}." msgstr "حدث تعارض في سلسلة التسمية أثناء إنشاء الأرقام التسلسلية. يرجى تغيير سلسلة التسمية للعنصر {0}." @@ -1223,7 +1223,7 @@ msgid "Accepted Qty in Stock UOM" msgstr "" #. Label of the qty (Float) field in DocType 'Purchase Receipt Item' -#: erpnext/public/js/controllers/transaction.js:2870 +#: erpnext/public/js/controllers/transaction.js:2875 #: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json msgid "Accepted Quantity" msgstr "كمية مقبولة" @@ -1259,7 +1259,7 @@ msgstr "مفتاح الوصول مطلوب لموفر الخدمة: {0}" msgid "According to CEFACT/ICG/2010/IC013 or CEFACT/ICG/2010/IC010" msgstr "وفقًا لـ CEFACT/ICG/2010/IC013 أو CEFACT/ICG/2010/IC010" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1281 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1290 msgid "According to the BOM {0}, the Item '{1}' is missing in the stock entry." msgstr "وفقًا لقائمة المواد {0}، فإن العنصر '{1}' مفقود في إدخال المخزون." @@ -1383,7 +1383,7 @@ msgid "Account Manager" msgstr "إدارة حساب المستخدم" #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1063 -#: erpnext/controllers/accounts_controller.py:2423 +#: erpnext/controllers/accounts_controller.py:2428 msgid "Account Missing" msgstr "الحساب مفقود" @@ -1623,7 +1623,7 @@ msgstr "تم تعطيل الحساب {0}." msgid "Account {0} is frozen" msgstr "الحساب {0} مجمد\\n
    \\nAccount {0} is frozen" -#: erpnext/controllers/accounts_controller.py:1498 +#: erpnext/controllers/accounts_controller.py:1503 msgid "Account {0} is invalid. Account Currency must be {1}" msgstr "الحساب {0} غير صحيح. يجب أن تكون عملة الحساب {1}" @@ -1659,7 +1659,7 @@ msgstr "الحساب: {0} لا يمكن تحديثه إلا من خلال معا msgid "Account: {0} is not permitted under Payment Entry" msgstr "الحساب: {0} غير مسموح به بموجب إدخال الدفع" -#: erpnext/controllers/accounts_controller.py:3307 +#: erpnext/controllers/accounts_controller.py:3312 msgid "Account: {0} with currency: {1} can not be selected" msgstr "الحساب: {0} مع العملة: {1} لا يمكن اختياره" @@ -1944,8 +1944,8 @@ msgstr "القيود المحاسبة" msgid "Accounting Entry for Asset" msgstr "المدخلات الحسابية للأصول" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2364 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2384 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2396 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2416 msgid "Accounting Entry for LCV in Stock Entry {0}" msgstr "" @@ -1957,20 +1957,20 @@ msgstr "" msgid "Accounting Entry for Service" msgstr "القيد المحاسبي للخدمة" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1046 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1067 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1085 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1106 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1127 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1155 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1267 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1532 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1554 -#: erpnext/controllers/stock_controller.py:773 -#: erpnext/controllers/stock_controller.py:790 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1056 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1077 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1095 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1116 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1137 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1165 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1277 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1542 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1564 +#: erpnext/controllers/stock_controller.py:787 +#: erpnext/controllers/stock_controller.py:804 #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:930 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2309 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2323 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2341 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2355 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:753 msgid "Accounting Entry for Stock" msgstr "القيود المحاسبية للمخزون" @@ -1979,7 +1979,7 @@ msgstr "القيود المحاسبية للمخزون" msgid "Accounting Entry for {0}" msgstr "القيد المحاسبي لـ {0}" -#: erpnext/controllers/accounts_controller.py:2464 +#: erpnext/controllers/accounts_controller.py:2469 msgid "Accounting Entry for {0}: {1} can only be made in currency: {2}" msgstr "المدخل المحاسبي ل {0}: {1} يمكن أن يكون فقط بالعملة {1}.\\n
    \\nAccounting Entry for {0}: {1} can only be made in currency: {2}" @@ -2176,7 +2176,7 @@ msgstr "إعدادات الحسابات" msgid "Accounts Setup" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1338 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1348 msgid "Accounts table cannot be blank." msgstr "جدول الحسابات لا يمكن أن يكون فارغا." @@ -2643,7 +2643,7 @@ msgstr "إضافة خصم" msgid "Add Employees" msgstr "إضافة موظفين" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:275 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:264 #: erpnext/selling/doctype/sales_order/sales_order.js:285 #: erpnext/stock/dashboard/item_dashboard.js:216 msgid "Add Item" @@ -2695,8 +2695,8 @@ msgstr "" msgid "Add Order Discount" msgstr "أضف خصم الطلب" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:300 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:435 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:289 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:424 msgid "Add Phantom Item" msgstr "إضافة عنصر وهمي" @@ -2773,8 +2773,8 @@ msgstr "" msgid "Add Stock" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:300 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:435 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:289 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:424 msgid "Add Sub Assembly" msgstr "" @@ -3378,7 +3378,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:306 +#: erpnext/controllers/accounts_controller.py:311 #: erpnext/setup/doctype/company/company.json msgid "Advance Payments" msgstr "دفعات مقدمة" @@ -3597,7 +3597,7 @@ msgstr "مقابل بند طلب مبيعات" msgid "Against Stock Entry" msgstr "ضد دخول الأسهم" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:339 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:349 msgid "Against Supplier Invoice {0}" msgstr "مقابل فاتورة المورد {0}" @@ -3928,11 +3928,11 @@ msgstr "تم بالفعل تحرير / إرجاع جميع العناصر" msgid "All items have already been received" msgstr "تم استلام جميع العناصر مسبقاً" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3696 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3728 msgid "All items have already been transferred for this Work Order." msgstr "جميع الإصناف تم نقلها لأمر العمل" -#: erpnext/public/js/controllers/transaction.js:2993 +#: erpnext/public/js/controllers/transaction.js:2998 msgid "All items in this document already have a linked Quality Inspection." msgstr "" @@ -4166,8 +4166,8 @@ msgstr "السماح باستهلاك المواد المتعددة" #. Valuation' #: erpnext/stock/doctype/item/item.json #: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json -#: erpnext/stock/doctype/stock_settings/stock_settings.py:217 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:229 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:222 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:234 msgid "Allow Negative Stock" msgstr "السماح بالقيم السالبة للمخزون" @@ -4787,7 +4787,7 @@ msgstr "" #: erpnext/manufacturing/doctype/bom_item/bom_item.json #: erpnext/manufacturing/doctype/work_order_additional_item/work_order_additional_item.json #: erpnext/manufacturing/doctype/work_order_item/work_order_item.json -#: erpnext/public/js/controllers/transaction.js:569 +#: erpnext/public/js/controllers/transaction.js:571 #: erpnext/selling/doctype/quotation/quotation.js:315 #: erpnext/selling/doctype/quotation_item/quotation_item.json #: erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -5533,7 +5533,7 @@ msgstr "" msgid "Are you sure you want to clear all demo data?" msgstr "هل أنت متأكد أنك تريد مسح كافة بيانات العرض التوضيحي؟" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:499 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:488 msgid "Are you sure you want to delete this Item?" msgstr "" @@ -5611,11 +5611,11 @@ msgstr "نظرًا لتمكين الحقل {0} ، يكون الحقل {1} إلز msgid "As the field {0} is enabled, the value of the field {1} should be more than 1." msgstr "أثناء تمكين الحقل {0} ، يجب أن تكون قيمة الحقل {1} أكثر من 1." -#: erpnext/stock/doctype/item/item.py:1094 +#: erpnext/stock/doctype/item/item.py:1104 msgid "As there are existing submitted transactions against item {0}, you can not change the value of {1}." msgstr "بما أن هناك معاملات مقدمة بالفعل مقابل العنصر {0}، فلا يمكنك تغيير قيمة {1}." -#: erpnext/stock/doctype/stock_settings/stock_settings.py:242 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:247 msgid "As there are reserved stock, you cannot disable {0}." msgstr "نظراً لوجود مخزون محجوز، لا يمكنك تعطيل {0}." @@ -5623,12 +5623,12 @@ msgstr "نظراً لوجود مخزون محجوز، لا يمكنك تعطيل msgid "As there are sufficient Sub Assembly Items, Work Order is not required for Warehouse {0}." msgstr "نظرًا لوجود عناصر تجميع فرعية كافية، فإن أمر العمل غير مطلوب للمستودع {0}." -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1850 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1849 msgid "As there are sufficient raw materials, Material Request is not required for Warehouse {0}." msgstr "نظرًا لوجود مواد خام كافية ، فإن طلب المواد ليس مطلوبًا للمستودع {0}." -#: erpnext/stock/doctype/stock_settings/stock_settings.py:216 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:228 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:221 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:233 msgid "As {0} is enabled, you can not enable {1}." msgstr "بما أن {0} مفعل، فلا يمكنك تفعيل {1}." @@ -6238,7 +6238,7 @@ msgstr "في الصف #{0}: الكمية المختارة {1} للصنف {2} أ msgid "At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} in the warehouse {4}." msgstr "في الصف #{0}: الكمية المختارة {1} للصنف {2} أكبر من المخزون المتاح {3} في المستودع {4}." -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1502 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1552 msgid "At Row {0}: In Serial and Batch Bundle {1} must have docstatus as 1 and not 0" msgstr "في الصف {0}: في حزمة البيانات التسلسلية والدفعية {1} ، يجب أن تكون حالة المستند 1 وليس 0" @@ -6254,7 +6254,7 @@ msgstr "يجب اختيار أصل واحد على الأقل." msgid "At least one invoice has to be selected." msgstr "يجب اختيار فاتورة واحدة على الأقل." -#: erpnext/controllers/sales_and_purchase_return.py:168 +#: erpnext/controllers/sales_and_purchase_return.py:186 msgid "At least one item should be entered with negative quantity in return document" msgstr "يجب إدخال عنصر واحد على الأقل بكمية سالبة في مستند الإرجاع" @@ -6271,7 +6271,7 @@ msgstr "يجب اختيار واحدة على الأقل من الوحدات ا msgid "At least one of the Selling or Buying must be selected" msgstr "يجب اختيار واحد على الأقل من خياري البيع أو الشراء" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:428 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:437 msgid "At least one raw material item must be present in the stock entry for the type {0}" msgstr "" @@ -6279,11 +6279,11 @@ msgstr "" msgid "At least one row is required for a financial report template" msgstr "يلزم وجود صف واحد على الأقل في نموذج التقرير المالي" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:993 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1002 msgid "At least one warehouse is mandatory" msgstr "يُشترط وجود مستودع واحد على الأقل" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:896 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:905 msgid "At row #{0}: the Difference Account must not be a Stock type account, please change the Account Type for the account {1} or select a different account" msgstr "في السطر #{0}: يجب ألا يكون حساب الفروقات حسابًا من نوع الأسهم، يُرجى تغيير نوع الحساب {1} أو تحديد حساب مختلف." @@ -6291,11 +6291,11 @@ msgstr "في السطر #{0}: يجب ألا يكون حساب الفروقات msgid "At row #{0}: the sequence id {1} cannot be less than previous row sequence id {2}" msgstr "في الصف # {0}: لا يمكن أن يكون معرف التسلسل {1} أقل من معرف تسلسل الصف السابق {2}" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:907 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:916 msgid "At row #{0}: you have selected the Difference Account {1}, which is a Cost of Goods Sold type account. Please select a different account" msgstr "في الصف #{0}: لقد اخترت حساب الفرق {1}، وهو حساب من نوع تكلفة البضائع المباعة. يرجى اختيار حساب مختلف." -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1250 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1300 msgid "At row {0}: Batch No is mandatory for Item {1}" msgstr "في الصف {0}: رقم الدفعة إلزامي للعنصر {1}" @@ -6303,15 +6303,15 @@ msgstr "في الصف {0}: رقم الدفعة إلزامي للعنصر {1}" msgid "At row {0}: Parent Row No cannot be set for item {1}" msgstr "في الصف {0}: لا يمكن تعيين رقم الصف الأصل للعنصر {1}" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1235 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1285 msgid "At row {0}: Qty is mandatory for the batch {1}" msgstr "في الصف {0}: الكمية إلزامية للدفعة {1}" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1242 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1292 msgid "At row {0}: Serial No is mandatory for Item {1}" msgstr "في الصف {0}: الرقم التسلسلي إلزامي للعنصر {1}" -#: erpnext/controllers/stock_controller.py:721 +#: erpnext/controllers/stock_controller.py:735 msgid "At row {0}: Serial and Batch Bundle {1} has already created. Please remove the values from the serial no or batch no fields." msgstr "في الصف {0}: تم إنشاء حزمة الرقم التسلسلي وحزمة الدفعة {1} مسبقًا. يُرجى حذف القيم من حقلي الرقم التسلسلي أو رقم الدفعة." @@ -6375,11 +6375,11 @@ msgstr "السمة اسم" msgid "Attribute Value" msgstr "السمة القيمة" -#: erpnext/stock/doctype/item/item.py:884 +#: erpnext/stock/doctype/item/item.py:894 msgid "Attribute Value {0} is not valid for the selected attribute {1}." msgstr "" -#: erpnext/stock/doctype/item/item.py:1030 +#: erpnext/stock/doctype/item/item.py:1040 msgid "Attribute table is mandatory" msgstr "جدول الخصائص إلزامي" @@ -6387,19 +6387,19 @@ msgstr "جدول الخصائص إلزامي" msgid "Attribute value: {0} must appear only once" msgstr "قيمة السمة: {0} يجب أن تظهر مرة واحدة فقط" -#: erpnext/stock/doctype/item/item.py:873 +#: erpnext/stock/doctype/item/item.py:883 msgid "Attribute {0} is disabled." msgstr "" -#: erpnext/stock/doctype/item/item.py:861 +#: erpnext/stock/doctype/item/item.py:871 msgid "Attribute {0} is not valid for the selected template." msgstr "" -#: erpnext/stock/doctype/item/item.py:1034 +#: erpnext/stock/doctype/item/item.py:1044 msgid "Attribute {0} selected multiple times in Attributes Table" msgstr "تم تحديد السمة {0} عدة مرات في جدول السمات\\n
    \\nAttribute {0} selected multiple times in Attributes Table" -#: erpnext/stock/doctype/item/item.py:962 +#: erpnext/stock/doctype/item/item.py:972 msgid "Attributes" msgstr "سمات" @@ -6824,7 +6824,7 @@ msgstr "" msgid "Available for use date is required" msgstr "مطلوب تاريخ متاح للاستخدام" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1243 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1252 msgid "Available quantity is {0}, you need {1}" msgstr "الكمية المتاحة هي {0} ، تحتاج إلى {1}" @@ -6887,7 +6887,7 @@ msgid "Avg Rate" msgstr "المعدل المتوسط" #: erpnext/stock/report/available_serial_no/available_serial_no.py:154 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:369 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:372 msgid "Avg Rate (Balance Stock)" msgstr "متوسط المعدل (رصيد المخزون)" @@ -7219,7 +7219,7 @@ msgstr "صنف الموقع الالكتروني بقائمة المواد" msgid "BOM Website Operation" msgstr "عملية الموقع الالكتروني بقائمة المواد" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2802 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2834 msgid "BOM and Finished Good Quantity is mandatory for Disassembly" msgstr "يُعدّ كل من قائمة المواد وكمية المنتج النهائي شرطًا أساسيًا لعملية التفكيك." @@ -7351,7 +7351,7 @@ msgstr "التوازن في العملة الأساسية" #: erpnext/stock/report/available_serial_no/available_serial_no.py:126 #: erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py:84 #: erpnext/stock/report/stock_balance/stock_balance.py:515 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:332 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:335 msgid "Balance Qty" msgstr "كمية الرصيد" @@ -7424,7 +7424,7 @@ msgstr "نوع التوازن" #: erpnext/stock/report/available_serial_no/available_serial_no.py:174 #: erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py:86 #: erpnext/stock/report/stock_balance/stock_balance.py:522 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:389 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:392 msgid "Balance Value" msgstr "قيمة الرصيد" @@ -8030,8 +8030,8 @@ msgstr "التسعير الاساسي استنادأ لوحدة القياس" #: erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py:34 #: erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py:80 #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:158 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:419 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:182 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:422 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:191 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:80 #: erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.js:19 #: erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py:32 @@ -8111,7 +8111,7 @@ msgstr "" #: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js:89 #: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py:115 -#: erpnext/public/js/controllers/transaction.js:2896 +#: erpnext/public/js/controllers/transaction.js:2901 #: erpnext/public/js/utils/barcode_scanner.js:286 #: erpnext/public/js/utils/serial_no_batch_selector.js:449 #: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -8142,11 +8142,11 @@ msgstr "" msgid "Batch No" msgstr "رقم دفعة" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1253 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1303 msgid "Batch No is mandatory" msgstr "رقم الدفعة إلزامي" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:3547 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:3597 msgid "Batch No {0} does not exists" msgstr "رقم الدفعة {0} غير موجود" @@ -8154,7 +8154,7 @@ msgstr "رقم الدفعة {0} غير موجود" msgid "Batch No {0} is linked with Item {1} which has serial no. Please scan serial no instead." msgstr "رقم الدفعة {0} مرتبط بالعنصر {1} الذي يحمل رقمًا تسلسليًا. يرجى مسح الرقم التسلسلي بدلاً من ذلك." -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:491 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:541 msgid "Batch No {0} is not present in the original {1} {2}, hence you can't return it against the {1} {2}" msgstr "رقم الدفعة {0} غير موجود في الدفعة الأصلية {1} {2}، لذا لا يمكنك إرجاعه مقابل الدفعة {1} {2}" @@ -8169,11 +8169,11 @@ msgstr "" msgid "Batch Nos" msgstr "أرقام الدفعات" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2075 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2125 msgid "Batch Nos are created successfully" msgstr "تم إنشاء أرقام الدفعات بنجاح" -#: erpnext/controllers/sales_and_purchase_return.py:1196 +#: erpnext/controllers/sales_and_purchase_return.py:1214 msgid "Batch Not Available for Return" msgstr "الدفعة غير متاحة للإرجاع" @@ -8242,16 +8242,16 @@ msgstr "" msgid "Batch {0} and Warehouse" msgstr "الدفعة {0} والمستودع" -#: erpnext/controllers/sales_and_purchase_return.py:1195 +#: erpnext/controllers/sales_and_purchase_return.py:1213 msgid "Batch {0} is not available in warehouse {1}" msgstr "الدفعة {0} غير متوفرة في المستودع {1}" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3880 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3912 #: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py:290 msgid "Batch {0} of Item {1} has expired." msgstr "الدفعة {0} للعنصر {1} انتهت صلاحيتها\\n
    \\nBatch {0} of Item {1} has expired." -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3886 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3918 msgid "Batch {0} of Item {1} is disabled." msgstr "تم تعطيل الدفعة {0} من الصنف {1}." @@ -8265,7 +8265,7 @@ msgid "Batch-Wise Balance History" msgstr "دفعة الحكيم التاريخ الرصيد" #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:164 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:194 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:203 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:86 msgid "Batchwise Valuation" msgstr "التقييم على أساس الدفعة" @@ -8287,7 +8287,7 @@ msgstr "" msgid "Beginning of the current subscription period" msgstr "بداية فترة الاشتراك الحالية" -#: erpnext/accounts/doctype/subscription/subscription.py:360 +#: erpnext/accounts/doctype/subscription/subscription.py:363 msgid "Below Subscription Plans are of different currency to the party default billing currency/Company currency: {0}" msgstr "تختلف عملات خطط الاشتراك أدناه عن عملة الفوترة الافتراضية للجهة/عملة الشركة: {0}" @@ -8434,7 +8434,7 @@ msgstr "تفاصيل عنوان الفوترة" msgid "Billing Address Name" msgstr "اسم عنوان تقديم الفواتير" -#: erpnext/controllers/accounts_controller.py:593 +#: erpnext/controllers/accounts_controller.py:598 msgid "Billing Address does not belong to the {0}" msgstr "عنوان الفوترة لا ينتمي إلى {0}" @@ -8511,7 +8511,7 @@ msgstr "عدد الفواتير الفوترة" msgid "Billing Interval Count cannot be less than 1" msgstr "لا يمكن أن يكون عدد فترات إعداد الفواتير أقل من 1" -#: erpnext/accounts/doctype/subscription/subscription.py:409 +#: erpnext/accounts/doctype/subscription/subscription.py:412 msgid "Billing Interval in Subscription Plan must be Month to follow calendar months" msgstr "يجب أن تكون فترة الفوترة في خطة الاشتراك شهرًا لمتابعة الأشهر التقويمية" @@ -8671,7 +8671,7 @@ msgid "Blanket Orders" msgstr "" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:109 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:271 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:269 msgid "Block Invoice" msgstr "حظر الفاتورة" @@ -8818,7 +8818,7 @@ msgstr "يجب أن يكون كل من حساب الدفع: {0} وحساب ال msgid "Both Receivable Account: {0} and Advance Account: {1} must be of same currency for company: {2}" msgstr "يجب أن يكون كل من حساب المستحقات: {0} وحساب السلفة: {1} من نفس العملة للشركة: {2}" -#: erpnext/accounts/doctype/subscription/subscription.py:379 +#: erpnext/accounts/doctype/subscription/subscription.py:382 msgid "Both Trial Period Start Date and Trial Period End Date must be set" msgstr "يجب تعيين كل من تاريخ بدء الفترة التجريبية وتاريخ انتهاء الفترة التجريبية" @@ -9560,19 +9560,19 @@ msgstr "لا يمكن التصفية بناءً على طريقة الدفع ، msgid "Can not filter based on Voucher No, if grouped by Voucher" msgstr "لا يمكن الفلتره علي اساس (رقم الأيصال)، إذا تم وضعه في مجموعة على اساس (ايصال)" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1397 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1407 #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2899 msgid "Can only make payment against unbilled {0}" msgstr "يمكن إجراء دفعة فقط مقابل فاتورة غير مدفوعة {0}" #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1510 -#: erpnext/controllers/accounts_controller.py:3216 +#: erpnext/controllers/accounts_controller.py:3221 #: erpnext/public/js/controllers/accounts.js:100 msgid "Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'" msgstr "لا يمكن الرجوع إلى الصف إلا إذا كان نوع الرسوم هو \"مبلغ الصف السابق\" أو \"إجمالي الصف السابق\"." #: erpnext/setup/doctype/company/company.py:210 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:183 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:188 msgid "Can't change the valuation method, as there are transactions against some items which do not have its own valuation method" msgstr "لا يمكن تغيير طريقة التقييم، حيث توجد معاملات على بعض البنود التي لا تملك طريقة تقييم خاصة بها." @@ -9621,7 +9621,7 @@ msgstr "لا يمكن حساب وقت الوصول حيث أن عنوان برن msgid "Cannot Change Inventory Account Setting" msgstr "لا يمكن تغيير إعدادات حساب المخزون" -#: erpnext/controllers/sales_and_purchase_return.py:438 +#: erpnext/controllers/sales_and_purchase_return.py:456 msgid "Cannot Create Return" msgstr "لا يمكن إنشاء إرجاع" @@ -9679,7 +9679,7 @@ msgstr "لا يمكن الإلغاء لأن معالجة المستندات ال msgid "Cannot cancel because submitted Stock Entry {0} exists" msgstr "لا يمكن الإلغاء لان هناك تدوينات مخزون مقدمة {0} موجوده" -#: erpnext/stock/stock_ledger.py:179 +#: erpnext/stock/stock_ledger.py:206 msgid "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet." msgstr "لا يمكن إلغاء العملية. لم تكتمل إعادة تقييم السلعة عند الإرسال بعد." @@ -9695,15 +9695,15 @@ msgstr "" msgid "Cannot cancel this document as it is linked with the submitted asset {asset_link}. Please cancel the asset to continue." msgstr "لا يمكن إلغاء هذا المستند لأنه مرتبط بالأصل المُرسَل {asset_link}. يُرجى إلغاء الأصل للمتابعة." -#: erpnext/stock/doctype/stock_entry/stock_entry.py:671 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:680 msgid "Cannot cancel transaction for Completed Work Order." msgstr "لا يمكن إلغاء المعاملة لأمر العمل المكتمل." -#: erpnext/stock/doctype/item/item.py:982 +#: erpnext/stock/doctype/item/item.py:992 msgid "Cannot change Attributes after stock transaction. Make a new Item and transfer stock to the new Item" msgstr "لا يمكن تغيير سمات بعد معاملة الأسهم. جعل عنصر جديد ونقل الأسهم إلى البند الجديد" -#: erpnext/stock/doctype/item/item.py:1119 +#: erpnext/stock/doctype/item/item.py:1129 msgid "Cannot change Item {0} from serialized to non-serialized because a Serial and Batch Bundle exists for it. Please delete or cancel the Serial and Batch Bundle first." msgstr "" @@ -9715,7 +9715,7 @@ msgstr "لا يمكن تغيير نوع المستند المرجعي." msgid "Cannot change Service Stop Date for item in row {0}" msgstr "لا يمكن تغيير تاريخ إيقاف الخدمة للعنصر الموجود في الصف {0}" -#: erpnext/stock/doctype/item/item.py:973 +#: erpnext/stock/doctype/item/item.py:983 msgid "Cannot change Variant properties after stock transaction. You will have to make a new Item to do this." msgstr "لا يمكن تغيير خصائص المتغير بعد معاملة المخزون. سيكون عليك عمل عنصر جديد للقيام بذلك." @@ -9723,7 +9723,7 @@ msgstr "لا يمكن تغيير خصائص المتغير بعد معاملة msgid "Cannot change company's default currency, because there are existing transactions. Transactions must be cancelled to change the default currency." msgstr "لا يمكن تغيير العملة الافتراضية للشركة، لأن هناك معاملات موجودة. يجب إلغاء المعاملات لتغيير العملة الافتراضية." -#: erpnext/projects/doctype/task/task.py:147 +#: erpnext/projects/doctype/task/task.py:148 msgid "Cannot complete task {0} as its dependant task {1} are not completed / cancelled." msgstr "لا يمكن إكمال المهمة {0} لأن المهمة التابعة لها {1} لم تكتمل / تم إلغاؤها." @@ -9760,7 +9760,7 @@ msgstr "لا يمكن إنشاء قائمة اختيار لأمر البيع {0} msgid "Cannot create accounting entries against disabled accounts: {0}" msgstr "لا يمكن إنشاء قيود محاسبية للحسابات المعطلة: {0}" -#: erpnext/controllers/sales_and_purchase_return.py:437 +#: erpnext/controllers/sales_and_purchase_return.py:455 msgid "Cannot create return for consolidated invoice {0}." msgstr "لا يمكن إنشاء إرجاع للفاتورة المجمعة {0}." @@ -9768,7 +9768,7 @@ msgstr "لا يمكن إنشاء إرجاع للفاتورة المجمعة {0}. msgid "Cannot deactivate or cancel BOM as it is linked with other BOMs" msgstr "لا يمكن تعطيل أو إلغاء قائمة المواد لانها مترابطة مع قوائم مواد اخرى" -#: erpnext/crm/doctype/opportunity/opportunity.py:282 +#: erpnext/crm/doctype/opportunity/opportunity.py:292 msgid "Cannot declare as Lost because an active Quotation exists." msgstr "" @@ -9785,7 +9785,7 @@ msgstr "لا يمكن حذف صف الربح/الخسارة في الصرف" msgid "Cannot delete Serial No {0}, as it is used in stock transactions" msgstr "لا يمكن حذف الرقم التسلسلي {0}، لانه يتم استخدامها في قيود المخزون" -#: erpnext/controllers/accounts_controller.py:3841 +#: erpnext/controllers/accounts_controller.py:3871 msgid "Cannot delete an item which has been ordered" msgstr "لا يمكن حذف عنصر تم طلبه" @@ -9798,7 +9798,7 @@ msgstr "" msgid "Cannot delete virtual DocType: {0}. Virtual DocTypes do not have database tables." msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:148 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:153 msgid "Cannot disable Serial and Batch No for Item, as there are existing records for serial / batch." msgstr "" @@ -9806,7 +9806,7 @@ msgstr "" msgid "Cannot disable perpetual inventory, as there are existing Stock Ledger Entries for the company {0}. Please cancel the stock transactions first and try again." msgstr "لا يمكن تعطيل الجرد الدائم، لوجود قيود دفترية للمخزون للشركة {0}. يرجى إلغاء معاملات المخزون أولاً ثم المحاولة مرة أخرى." -#: erpnext/stock/doctype/stock_settings/stock_settings.py:129 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:134 msgid "Cannot disable {0} as it may lead to incorrect stock valuation." msgstr "" @@ -9814,7 +9814,7 @@ msgstr "" msgid "Cannot disassemble more than produced quantity." msgstr "لا يمكن تفكيك كمية أكبر من الكمية المنتجة." -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1036 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1045 msgid "Cannot disassemble {0} qty against Stock Entry {1}. Only {2} qty available to disassemble." msgstr "" @@ -9843,11 +9843,11 @@ msgstr "لا يمكن العثور على المنتج أو المستودع ب msgid "Cannot find Item with this Barcode" msgstr "لا يمكن العثور على عنصر بهذا الرمز الشريطي" -#: erpnext/controllers/accounts_controller.py:3793 -msgid "Cannot find a default warehouse for item {0}. Please set one in the Item Master or in Stock Settings." -msgstr "تعذر العثور على مستودع افتراضي للصنف {0}. يرجى تحديد مستودع في بيانات الصنف الرئيسية أو في إعدادات المخزون." +#: erpnext/controllers/accounts_controller.py:3810 +msgid "Cannot find a default warehouse for item {0}. Please select one in the Update Items dialog, or set a default in the Item Master or in Stock Settings." +msgstr "" -#: erpnext/accounts/party.py:1108 +#: erpnext/accounts/party.py:1110 msgid "Cannot merge {0} '{1}' into '{2}' as both have existing accounting entries in different currencies for company '{3}'." msgstr "لا يمكن دمج {0} '{1}' في '{2}' حيث أن لكليهما قيود محاسبية موجودة بعملات مختلفة للشركة '{3}'." @@ -9867,12 +9867,12 @@ msgstr "لا يمكن إنتاج أكثر من {0} عنصرًا لـ {1}" msgid "Cannot receive from customer against negative outstanding" msgstr "" -#: erpnext/controllers/accounts_controller.py:3990 +#: erpnext/controllers/accounts_controller.py:4020 msgid "Cannot reduce quantity than ordered or purchased quantity" msgstr "لا يمكن تقليل الكمية عن الكمية المطلوبة أو المشتراة" #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1523 -#: erpnext/controllers/accounts_controller.py:3231 +#: erpnext/controllers/accounts_controller.py:3236 #: erpnext/public/js/controllers/accounts.js:117 msgid "Cannot refer row number greater than or equal to current row number for this Charge type" msgstr "لا يمكن أن يشير رقم الصف أكبر من أو يساوي رقم الصف الحالي لهذا النوع المسؤول" @@ -9889,14 +9889,14 @@ msgstr "تعذر استرداد رمز الرابط للتحديث. راجع س msgid "Cannot retrieve link token. Check Error Log for more information" msgstr "تعذر استرداد رمز الرابط. راجع سجل الأخطاء لمزيد من المعلومات." -#: erpnext/selling/doctype/customer/customer.py:378 +#: erpnext/selling/doctype/customer/customer.py:379 msgid "Cannot select a Group type Customer Group. Please select a non-group Customer Group." msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1516 #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1694 #: erpnext/accounts/doctype/payment_entry/payment_entry.py:1848 -#: erpnext/controllers/accounts_controller.py:3221 +#: erpnext/controllers/accounts_controller.py:3226 #: erpnext/public/js/controllers/accounts.js:109 #: erpnext/public/js/controllers/taxes_and_totals.js:570 msgid "Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row" @@ -9914,11 +9914,11 @@ msgstr "لا يمكن تحديد التخويل على أساس الخصم ل {0 msgid "Cannot set multiple Item Defaults for a company." msgstr "لا يمكن تعيين عدة عناصر افتراضية لأي شركة." -#: erpnext/controllers/accounts_controller.py:3956 +#: erpnext/controllers/accounts_controller.py:3986 msgid "Cannot set quantity less than delivered quantity." msgstr "لا يمكن ضبط كمية أقل من الكمية المسلمة." -#: erpnext/controllers/accounts_controller.py:3957 +#: erpnext/controllers/accounts_controller.py:3987 msgid "Cannot set quantity less than received quantity." msgstr "لا يمكن تعيين كمية أقل من الكمية المستلمة." @@ -9934,7 +9934,7 @@ msgstr "" msgid "Cannot submit Job Card {0} while it is On Hold. Please resume and complete the job before submission." msgstr "" -#: erpnext/controllers/accounts_controller.py:3984 +#: erpnext/controllers/accounts_controller.py:4014 msgid "Cannot update rate as item {0} is already ordered or purchased against this quotation" msgstr "" @@ -10115,7 +10115,7 @@ msgstr "التدفق النقدي من العمليات" msgid "Cash In Hand" msgstr "النقدية الحاضرة" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:329 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:339 msgid "Cash or Bank Account is mandatory for making payment entry" msgstr "الحساب النقدي أو البنكي مطلوب لعمل مدخل بيع
    Cash or Bank Account is mandatory for making payment entry" @@ -10349,7 +10349,7 @@ msgid "Channel Partner" msgstr "شريك القناة" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2277 -#: erpnext/controllers/accounts_controller.py:3284 +#: erpnext/controllers/accounts_controller.py:3289 msgid "Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount" msgstr "لا يمكن تضمين رسوم من النوع \"فعلي\" في الصف {0} في سعر السلعة أو المبلغ المدفوع" @@ -10543,7 +10543,7 @@ msgstr "عرض الشيك" #. Label of the reference_date (Date) field in DocType 'Payment Entry' #: erpnext/accounts/doctype/payment_entry/payment_entry.json -#: erpnext/public/js/controllers/transaction.js:2807 +#: erpnext/public/js/controllers/transaction.js:2812 msgid "Cheque/Reference Date" msgstr "تاريخ الصك / السند المرجع" @@ -10601,7 +10601,7 @@ msgstr "اسم الطفل" #. Label of the child_row_reference (Data) field in DocType 'Quality #. Inspection' -#: erpnext/public/js/controllers/transaction.js:2902 +#: erpnext/public/js/controllers/transaction.js:2907 #: erpnext/stock/doctype/quality_inspection/quality_inspection.json msgid "Child Row Reference" msgstr "مرجع صف الطفل" @@ -10610,7 +10610,7 @@ msgstr "مرجع صف الطفل" msgid "Child Table Not Allowed" msgstr "" -#: erpnext/projects/doctype/task/task.py:314 +#: erpnext/projects/doctype/task/task.py:332 msgid "Child Task exists for this Task. You can not delete this Task." msgstr "مهمة تابعة موجودة لهذه المهمة. لا يمكنك حذف هذه المهمة." @@ -10628,7 +10628,7 @@ msgstr "" msgid "Child warehouse exists for this warehouse. You can not delete this warehouse." msgstr "مستودع فرعي موجود لهذا المستودع. لا يمكنك حذف هذا المستودع.\\n
    \\nChild warehouse exists for this warehouse. You can not delete this warehouse." -#: erpnext/projects/doctype/task/task.py:262 +#: erpnext/projects/doctype/task/task.py:263 msgid "Circular Reference Error" msgstr "خطأ المرجع الدائري" @@ -10804,6 +10804,10 @@ msgstr "وثيقة مغلقة" msgid "Closed Documents" msgstr "وثائق مغلقة" +#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:147 +msgid "Closed Period" +msgstr "" + #: erpnext/manufacturing/doctype/work_order/work_order.py:2775 msgid "Closed Work Order can not be stopped or Re-opened" msgstr "لا يمكن إيقاف أمر العمل المغلق أو إعادة فتحه." @@ -10839,7 +10843,7 @@ msgstr "الإغلاق (الافتتاحي + الإجمالي)" msgid "Closing Account Head" msgstr "اقفال حساب المركز الرئيسي" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:133 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:136 msgid "Closing Account {0} must be of type Liability / Equity" msgstr "يجب ان يكون الحساب الختامي {0} من النوع متطلبات/الأسهم\\n
    \\nClosing Account {0} must be of type Liability / Equity" @@ -11558,10 +11562,10 @@ msgstr "شركات" #: erpnext/stock/report/stock_balance/stock_balance.js:8 #: erpnext/stock/report/stock_balance/stock_balance.py:576 #: erpnext/stock/report/stock_ledger/stock_ledger.js:8 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:442 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:445 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js:18 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.js:8 -#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js:8 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js:32 #: erpnext/stock/report/total_stock_summary/total_stock_summary.js:17 #: erpnext/stock/report/total_stock_summary/total_stock_summary.py:29 #: erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.js:8 @@ -11643,11 +11647,11 @@ msgstr "عرض عنوان الشركة" msgid "Company Address Name" msgstr "اسم عنوان الشركة" -#: erpnext/controllers/accounts_controller.py:4420 +#: erpnext/controllers/accounts_controller.py:4450 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:4408 +#: erpnext/controllers/accounts_controller.py:4438 msgid "Company Address is missing. You don't have permission to update it. Please contact your System Manager." msgstr "عنوان الشركة غير موجود. ليس لديك صلاحية لتحديثه. يرجى الاتصال بمدير النظام." @@ -11767,7 +11771,7 @@ msgstr "الشركة إلزامية" msgid "Company is mandatory for company account" msgstr "الشركة إلزامية لحساب الشركة" -#: erpnext/accounts/doctype/subscription/subscription.py:438 +#: erpnext/accounts/doctype/subscription/subscription.py:441 msgid "Company is mandatory for generating an invoice. Please set a default company in Global Defaults." msgstr "يُعدّ تحديد اسم الشركة أمراً إلزامياً لإصدار الفاتورة. يُرجى تحديد شركة افتراضية في الإعدادات الافتراضية العامة." @@ -11890,7 +11894,7 @@ msgstr "اكتمل بواسطة" msgid "Completed On" msgstr "اكتمل في" -#: erpnext/projects/doctype/task/task.py:187 +#: erpnext/projects/doctype/task/task.py:188 msgid "Completed On cannot be greater than Today" msgstr "لا يمكن أن يتجاوز تاريخ الإنجاز عدد الأيام" @@ -12043,7 +12047,7 @@ msgstr "" msgid "Configure Chart of Accounts" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:56 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:45 msgid "Configure Product Assembly" msgstr "تكوين تجميع المنتج" @@ -12345,7 +12349,7 @@ msgstr "الكمية المستهلكة من العنصر {0} تتجاوز ال msgid "Consumer Products" msgstr "المنتجات الاستهلاكية" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:209 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:218 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:101 msgid "Consumption Rate" msgstr "معدل الاستهلاك" @@ -12465,7 +12469,7 @@ msgstr "" msgid "Contact Person" msgstr "الشخص الذي يمكن الاتصال به" -#: erpnext/controllers/accounts_controller.py:605 +#: erpnext/controllers/accounts_controller.py:610 msgid "Contact Person does not belong to the {0}" msgstr "جهة الاتصال لا تنتمي إلى {0}" @@ -12633,7 +12637,7 @@ msgstr "" #: erpnext/manufacturing/doctype/bom_item/bom_item.json #: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json #: erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json -#: erpnext/public/js/utils.js:920 +#: erpnext/public/js/utils.js:923 #: erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json #: erpnext/stock/doctype/packed_item/packed_item.json #: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -12663,19 +12667,19 @@ msgstr "معدل التحويل" msgid "Conversion factor for default Unit of Measure must be 1 in row {0}" msgstr "معامل التحويل الافتراضي لوحدة القياس يجب أن يكون 1 في الصف {0}" -#: erpnext/controllers/stock_controller.py:163 +#: erpnext/controllers/stock_controller.py:177 msgid "Conversion factor for item {0} has been reset to 1.0 as the uom {1} is same as stock uom {2}." msgstr "تمت إعادة تعيين عامل التحويل للعنصر {0} إلى 1.0 لأن وحدة القياس {1} هي نفسها وحدة قياس المخزون {2}." -#: erpnext/controllers/accounts_controller.py:2999 +#: erpnext/controllers/accounts_controller.py:3004 msgid "Conversion rate cannot be 0" msgstr "لا يمكن أن يكون معدل التحويل 0" -#: erpnext/controllers/accounts_controller.py:3006 +#: erpnext/controllers/accounts_controller.py:3011 msgid "Conversion rate is 1.00, but document currency is different from company currency" msgstr "معدل التحويل هو 1.00، لكن عملة المستند تختلف عن عملة الشركة." -#: erpnext/controllers/accounts_controller.py:3002 +#: erpnext/controllers/accounts_controller.py:3007 msgid "Conversion rate must be 1.00 if document currency is same as company currency" msgstr "يجب أن يكون معدل التحويل 1.00 إذا كانت عملة المستند هي نفسها عملة الشركة" @@ -13029,7 +13033,7 @@ msgstr "يُعد مركز التكلفة جزءًا من تخصيص مركز ا msgid "Cost Center is required" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1498 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1508 #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:898 msgid "Cost Center is required in row {0} in Taxes table for type {1}" msgstr "مركز التكلفة مطلوب في الصف {0} في جدول الضرائب للنوع {1}\\n
    \\nCost Center is required in row {0} in Taxes table for type {1}" @@ -13112,7 +13116,7 @@ msgstr "تكلفة السلع والمواد المسلمة" msgid "Cost of Goods Sold" msgstr "تكلفة البضاعة المباعة" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:910 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:919 msgid "Cost of Goods Sold Account in Items Table" msgstr "حساب تكلفة البضائع المباعة في جدول الأصناف" @@ -13500,7 +13504,7 @@ msgstr "إنشاء إدخال الدفع" msgid "Create Payment Entry for Consolidated POS Invoices." msgstr "إنشاء إدخال دفع لفواتير نقاط البيع المجمعة." -#: erpnext/public/js/controllers/transaction.js:577 +#: erpnext/public/js/controllers/transaction.js:579 msgid "Create Payment Request" msgstr "" @@ -13604,7 +13608,7 @@ msgid "Create Service Item" msgstr "" #: erpnext/stock/dashboard/item_dashboard.js:283 -#: erpnext/stock/doctype/material_request/material_request.js:653 +#: erpnext/stock/doctype/material_request/material_request.js:649 msgid "Create Stock Entry" msgstr "إنشاء إدخال المخزون" @@ -13711,6 +13715,10 @@ msgstr "" msgid "Create Workstation" msgstr "إنشاء محطة عمل" +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:228 +msgid "Create a Stock Closing Entry for the entire company with To Date as {0} before submitting the Period Closing Voucher." +msgstr "" + #: banking/src/components/features/BankReconciliation/MatchAndReconcile.tsx:683 msgid "Create a journal entry for expenses, income or split transactions" msgstr "" @@ -13728,7 +13736,7 @@ msgstr "" msgid "Create a variant with the template image." msgstr "أنشئ نسخة بديلة باستخدام صورة القالب." -#: erpnext/stock/stock_ledger.py:2052 +#: erpnext/stock/stock_ledger.py:2095 msgid "Create an incoming stock transaction for the Item." msgstr "قم بإنشاء حركة مخزون واردة للصنف." @@ -13820,7 +13828,7 @@ msgstr "" msgid "Creating Purchase Order ..." msgstr "إنشاء أمر شراء ..." -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:729 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:727 #: erpnext/buying/doctype/purchase_order/purchase_order.js:506 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js:74 msgid "Creating Purchase Receipt ..." @@ -13863,7 +13871,7 @@ msgid "Creating {} out of {} {}" msgstr "إنشاء {} من {} {}" #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:141 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:165 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:174 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:46 msgid "Creation" msgstr "الخلق" @@ -13999,7 +14007,7 @@ msgstr "الائتمان أيام" msgid "Credit Limit" msgstr "الحد الائتماني" -#: erpnext/selling/doctype/customer/customer.py:657 +#: erpnext/selling/doctype/customer/customer.py:658 msgid "Credit Limit Crossed" msgstr "تم تجاوز الحد الائتماني" @@ -14035,7 +14043,7 @@ msgstr "أشهر الائتمان" #: 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:1223 -#: erpnext/controllers/sales_and_purchase_return.py:455 +#: erpnext/controllers/sales_and_purchase_return.py:473 #: erpnext/setup/setup_wizard/operations/install_fixtures.py:306 #: erpnext/stock/doctype/delivery_note/delivery_note.js:89 #: erpnext/stock/doctype/stock_entry/stock_entry.json @@ -14068,9 +14076,9 @@ msgstr "تم إنشاء ملاحظة الائتمان {0} تلقائيًا" #. Label of the credit_to (Link) field in DocType 'Purchase Invoice' #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:383 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:391 -#: erpnext/controllers/accounts_controller.py:2403 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:393 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:401 +#: erpnext/controllers/accounts_controller.py:2408 msgid "Credit To" msgstr "دائن الى" @@ -14079,16 +14087,16 @@ msgstr "دائن الى" msgid "Credit in Company Currency" msgstr "المدين في عملة الشركة" -#: erpnext/selling/doctype/customer/customer.py:623 -#: erpnext/selling/doctype/customer/customer.py:678 +#: erpnext/selling/doctype/customer/customer.py:624 +#: erpnext/selling/doctype/customer/customer.py:679 msgid "Credit limit has been crossed for customer {0} ({1}/{2})" msgstr "تم تجاوز حد الائتمان للعميل {0} ({1} / {2})" -#: erpnext/selling/doctype/customer/customer.py:405 +#: erpnext/selling/doctype/customer/customer.py:406 msgid "Credit limit is already defined for the Company {0}" msgstr "تم تحديد حد الائتمان بالفعل للشركة {0}" -#: erpnext/selling/doctype/customer/customer.py:677 +#: erpnext/selling/doctype/customer/customer.py:678 msgid "Credit limit reached for customer {0}" msgstr "تم بلوغ حد الائتمان للعميل {0}" @@ -14281,7 +14289,7 @@ msgstr "لا تدعم التقارير المالية المخصصة حاليً msgid "Currency for {0} must be {1}" msgstr "العملة ل {0} يجب أن تكون {1} \\n
    \\nCurrency for {0} must be {1}" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:140 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:143 msgid "Currency of the Closing Account must be {0}" msgstr "عملة الحساب الختامي يجب أن تكون {0}" @@ -15220,7 +15228,7 @@ msgid "Cycle/Second" msgstr "دورة/ثانية" #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:204 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:254 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:263 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:146 msgid "D - E" msgstr "د - هـ" @@ -15545,7 +15553,7 @@ msgstr "" #: 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:1226 -#: erpnext/controllers/sales_and_purchase_return.py:459 +#: erpnext/controllers/sales_and_purchase_return.py:477 #: erpnext/setup/setup_wizard/operations/install_fixtures.py:307 #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:45 #: erpnext/workspace_sidebar/invoicing.json @@ -15574,7 +15582,7 @@ msgstr "ستقوم مذكرة الخصم بتحديث المبلغ المستح #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1067 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1078 -#: erpnext/controllers/accounts_controller.py:2403 +#: erpnext/controllers/accounts_controller.py:2408 msgid "Debit To" msgstr "الخصم ل" @@ -15762,7 +15770,7 @@ msgstr "يجب أن تكون قائمة المواد الافتراضية ({0}) msgid "Default BOM for {0} not found" msgstr "فاتورة المواد ل {0} غير موجودة\\n
    \\nDefault BOM for {0} not found" -#: erpnext/controllers/accounts_controller.py:4028 +#: erpnext/controllers/accounts_controller.py:4058 msgid "Default BOM not found for FG Item {0}" msgstr "لم يتم العثور على قائمة مكونات افتراضية لعنصر المنتج النهائي {0}" @@ -16098,15 +16106,15 @@ msgstr "الإقليم الافتراضي" msgid "Default Unit of Measure" msgstr "وحدة القياس الافتراضية" -#: erpnext/stock/doctype/item/item.py:1396 +#: erpnext/stock/doctype/item/item.py:1406 msgid "Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You need to either cancel the linked documents or create a new Item." msgstr "لا يمكن تغيير وحدة القياس الافتراضية للعنصر {0} مباشرةً لأنك أجريتَ بالفعل بعض المعاملات بوحدة قياس أخرى. عليك إما إلغاء المستندات المرتبطة أو إنشاء عنصر جديد." -#: erpnext/stock/doctype/item/item.py:1379 +#: erpnext/stock/doctype/item/item.py:1389 msgid "Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM." msgstr "لا يمكن تغيير وحدة القياس الافتراضية للبند {0} مباشرة لأنك قمت بالفعل ببعض المعاملات (المعاملة) مع UOM أخرى. ستحتاج إلى إنشاء عنصر جديد لاستخدام واجهة مستخدم افتراضية مختلفة.\\n
    \\nDefault Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM." -#: erpnext/stock/doctype/item/item.py:1008 +#: erpnext/stock/doctype/item/item.py:1018 msgid "Default Unit of Measure for Variant '{0}' must be same as in Template '{1}'" msgstr "وحدة القياس الافتراضية للمتغير '{0}' يجب أن تكون كما في النمودج '{1}'" @@ -16505,7 +16513,7 @@ msgstr "تسليم" #: erpnext/manufacturing/doctype/master_production_schedule_item/master_production_schedule_item.json #: erpnext/manufacturing/doctype/sales_forecast_item/sales_forecast_item.json #: erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.py:1069 -#: erpnext/public/js/utils.js:913 +#: erpnext/public/js/utils.js:916 #: erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json #: erpnext/selling/doctype/sales_order/sales_order.js:632 #: erpnext/selling/doctype/sales_order/sales_order.js:1533 @@ -16761,7 +16769,7 @@ msgstr "رقم قسيمة SLE التابعة" msgid "Dependent Task" msgstr "مهمة تابعة" -#: erpnext/projects/doctype/task/task.py:180 +#: erpnext/projects/doctype/task/task.py:181 msgid "Dependent Task {0} is not a Template Task" msgstr "المهمة التابعة {0} ليست مهمة نموذجية" @@ -17054,7 +17062,7 @@ msgstr "ديزل" #: erpnext/public/js/bank_reconciliation_tool/number_card.js:30 #: erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py:130 #: erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py:35 -#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:35 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:41 msgid "Difference" msgstr "فرق" @@ -17080,11 +17088,11 @@ msgstr "الفرق ( المدين - الدائن )" msgid "Difference Account" msgstr "حساب الفرق" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:899 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:908 msgid "Difference Account in Items Table" msgstr "حساب الفرق في جدول البنود" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:888 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:897 msgid "Difference Account must be a Asset/Liability type account (Temporary Opening), since this Stock Entry is an Opening Entry" msgstr "" @@ -17306,7 +17314,7 @@ msgstr "لا يمكن استخدام المستودع المعطل {0} لهذه msgid "Disabled items cannot be selected in any transaction." msgstr "" -#: erpnext/controllers/accounts_controller.py:931 +#: erpnext/controllers/accounts_controller.py:936 msgid "Disabled pricing rules since this {} is an internal transfer" msgstr "" @@ -17315,7 +17323,7 @@ msgstr "" msgid "Disabled suppliers are hidden from selection in new transactions but remain in historical records" msgstr "" -#: erpnext/controllers/accounts_controller.py:945 +#: erpnext/controllers/accounts_controller.py:950 msgid "Disabled tax included prices since this {} is an internal transfer" msgstr "" @@ -17343,7 +17351,7 @@ msgstr "فكّك" msgid "Disassemble Order" msgstr "ترتيب التفكيك" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2744 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2776 msgid "Disassemble Qty cannot be less than or equal to 0." msgstr "" @@ -17858,7 +17866,7 @@ msgstr "عدم الاتصال" msgid "Do Not Explode" msgstr "ممنوع الانفجار" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:130 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:135 msgid "Do Not Use Batchwise Valuation" msgstr "" @@ -17989,7 +17997,7 @@ msgstr "" msgid "Documents Processed on each trigger. Queue Size should be between 5 and 100" msgstr "تتم معالجة المستندات عند كل عملية تشغيل. يجب أن يتراوح حجم قائمة الانتظار بين 5 و 100." -#: erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py:486 +#: erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py:491 msgid "Documents: {0} have deferred revenue/expense enabled for them. Cannot repost." msgstr "المستندات: {0} مُفعّلة لها خاصية الإيرادات/المصروفات المؤجلة. لا يمكن إعادة نشرها." @@ -18283,11 +18291,11 @@ msgstr "مشروع مكرر مع المهام" msgid "Duplicate Sales Invoices found" msgstr "تم العثور على فواتير مبيعات مكررة" -#: erpnext/stock/serial_batch_bundle.py:1482 +#: erpnext/stock/serial_batch_bundle.py:1568 msgid "Duplicate Serial Number Error" msgstr "خطأ في الرقم التسلسلي المكرر" -#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:81 +#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:123 msgid "Duplicate Stock Closing Entry" msgstr "إدخال إقفال المخزون المكرر" @@ -18433,7 +18441,7 @@ msgstr "أقدم عمر" msgid "Earnest Money" msgstr "العربون" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:544 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:533 msgid "Edit BOM" msgstr "تعديل قائمة المواد" @@ -18521,8 +18529,8 @@ msgstr "المؤهلات العلمية" msgid "Either 'Selling' or 'Buying' must be selected" msgstr "يجب اختيار إما \"بيع\" أو \"شراء\"." -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:309 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:460 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:298 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:449 msgid "Either Workstation or Workstation Type is mandatory" msgstr "يُعد اختيار محطة العمل أو نوع محطة العمل إلزاميًا." @@ -18876,7 +18884,7 @@ msgstr "" msgid "Ems(Pica)" msgstr "إيمز (بيكا)" -#: erpnext/public/js/controllers/transaction.js:2965 +#: erpnext/public/js/controllers/transaction.js:2970 msgid "Enable {0} on the Item master to proceed with {1} inspection." msgstr "" @@ -18908,7 +18916,7 @@ msgstr "تمكين جدولة موعد" msgid "Enable Auto Email" msgstr "تفعيل البريد الإلكتروني التلقائي" -#: erpnext/stock/doctype/item/item.py:1188 +#: erpnext/stock/doctype/item/item.py:1198 msgid "Enable Auto Re-Order" msgstr "تمكين إعادة الطلب التلقائي" @@ -19576,7 +19584,7 @@ msgstr "من المصنع" msgid "Example URL" msgstr "مثال على عنوان URL" -#: erpnext/stock/doctype/item/item.py:1100 +#: erpnext/stock/doctype/item/item.py:1110 msgid "Example of a linked document: {0}" msgstr "مثال على مستند مرتبط: {0}" @@ -19596,7 +19604,7 @@ msgstr "مثال: ABCD. #####. إذا تم ضبط المسلسل ولم يتم msgid "Example: If the transaction amount is 200, then this will be calculated as {} = {}" msgstr "" -#: erpnext/stock/stock_ledger.py:2334 +#: erpnext/stock/stock_ledger.py:2377 msgid "Example: Serial No {0} reserved in {1}." msgstr "مثال: الرقم التسلسلي {0} محجوز في {1}." @@ -19606,11 +19614,11 @@ msgstr "مثال: الرقم التسلسلي {0} محجوز في {1}." msgid "Exception Budget Approver Role" msgstr "دور الموافقة على الموازنة الاستثنائية" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1043 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1052 msgid "Excess Disassembly" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1339 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1348 msgid "Excess Material Transfer" msgstr "" @@ -19658,8 +19666,8 @@ msgstr "الربح أو الخسارة في الصرف" msgid "Exchange Gain/Loss" msgstr "أرباح / خسائر الناتجة عن صرف العملة" -#: erpnext/controllers/accounts_controller.py:1804 -#: erpnext/controllers/accounts_controller.py:1889 +#: erpnext/controllers/accounts_controller.py:1809 +#: erpnext/controllers/accounts_controller.py:1894 msgid "Exchange Gain/Loss amount has been booked through {0}" msgstr "تم تسجيل مبلغ الربح/الخسارة من خلال {0}" @@ -19903,7 +19911,7 @@ msgstr "يجب أن يكون تاريخ التسليم المتوقع بعد ت msgid "Expected End Date" msgstr "تاريخ الإنتهاء المتوقع" -#: erpnext/projects/doctype/task/task.py:114 +#: erpnext/projects/doctype/task/task.py:115 msgid "Expected End Date should be less than or equal to parent task's Expected End Date {0}." msgstr "يجب أن يكون تاريخ الانتهاء المتوقع أقل من أو يساوي تاريخ الانتهاء المتوقع للمهمة الأصلية {0}." @@ -19961,7 +19969,7 @@ msgstr "القيمة المتوقعة بعد حياة مفيدة" #: erpnext/accounts/doctype/cashier_closing/cashier_closing.json #: erpnext/accounts/doctype/ledger_merge/ledger_merge.json #: erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.json -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:601 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:611 #: erpnext/accounts/report/account_balance/account_balance.js:28 #: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js:89 #: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:192 @@ -19969,7 +19977,7 @@ msgstr "القيمة المتوقعة بعد حياة مفيدة" msgid "Expense" msgstr "نفقة" -#: erpnext/controllers/stock_controller.py:1067 +#: erpnext/controllers/stock_controller.py:1081 msgid "Expense / Difference account ({0}) must be a 'Profit or Loss' account" msgstr "حساب نفقات / قروق ({0}) يجب ان يكون حساب ارباح و خسائر" @@ -20017,7 +20025,7 @@ msgstr "حساب نفقات / قروق ({0}) يجب ان يكون حساب ار msgid "Expense Account" msgstr "حساب النفقات" -#: erpnext/controllers/stock_controller.py:1047 +#: erpnext/controllers/stock_controller.py:1061 msgid "Expense Account Missing" msgstr "حساب المصاريف مفقود" @@ -20032,13 +20040,13 @@ msgstr "" msgid "Expense Head" msgstr "عنوان المصروف" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:495 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:519 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:539 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:505 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:529 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:549 msgid "Expense Head Changed" msgstr "تغيير رأس المصاريف" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:597 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:607 msgid "Expense account is mandatory for item {0}" msgstr "اجباري حساب النفقات للصنف {0}" @@ -20070,7 +20078,7 @@ msgstr "" msgid "Expenses Added To Stock Contra Account" msgstr "" -#: erpnext/controllers/stock_controller.py:920 +#: erpnext/controllers/stock_controller.py:934 msgid "Expenses Added To Stock for Item {0}" msgstr "" @@ -20223,7 +20231,7 @@ msgid "FIFO Stock Queue (qty, rate)" msgstr "قائمة انتظار المخزون وفقًا لأسلوب FIFO (الكمية، السعر)" #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:179 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:229 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:238 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:121 msgid "FIFO/LIFO Queue" msgstr "قائمة انتظار FIFO/LIFO" @@ -20442,7 +20450,7 @@ msgid "Fetching Sales Orders..." msgstr "جلب طلبات المبيعات..." #: erpnext/accounts/doctype/dunning/dunning.js:135 -#: erpnext/public/js/controllers/transaction.js:1617 +#: erpnext/public/js/controllers/transaction.js:1619 msgid "Fetching exchange rates ..." msgstr "جلب أسعار الصرف ..." @@ -20729,7 +20737,7 @@ msgstr "تم الانتهاء من المنتج بنجاح." #. Service Item' #. Label of the fg_item (Link) field in DocType 'Subcontracting Order Service #. Item' -#: erpnext/public/js/utils.js:939 +#: erpnext/public/js/utils.js:965 #: erpnext/subcontracting/doctype/subcontracting_inward_order_service_item/subcontracting_inward_order_service_item.json #: erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json msgid "Finished Good Item" @@ -20742,7 +20750,7 @@ msgstr "منتج نهائي جيد" msgid "Finished Good Item Code" msgstr "انتهى رمز السلعة جيدة" -#: erpnext/public/js/utils.js:957 +#: erpnext/public/js/utils.js:983 msgid "Finished Good Item Qty" msgstr "الكمية من المنتج النهائي" @@ -20755,15 +20763,15 @@ msgstr "الكمية من المنتج النهائي" msgid "Finished Good Item Quantity" msgstr "المنتج النهائي الجيد الكمية" -#: erpnext/controllers/accounts_controller.py:4014 +#: erpnext/controllers/accounts_controller.py:4044 msgid "Finished Good Item is not specified for service item {0}" msgstr "لم يتم تحديد المنتج النهائي لعنصر الخدمة {0}" -#: erpnext/controllers/accounts_controller.py:4031 +#: erpnext/controllers/accounts_controller.py:4061 msgid "Finished Good Item {0} Qty can not be zero" msgstr "المنتج النهائي {0} لا يمكن أن تكون الكمية صفرًا" -#: erpnext/controllers/accounts_controller.py:4025 +#: erpnext/controllers/accounts_controller.py:4055 msgid "Finished Good Item {0} must be a sub-contracted item" msgstr "يجب أن يكون المنتج النهائي {0} منتجًا تم التعاقد عليه من الباطن" @@ -20850,11 +20858,11 @@ msgstr "مستودع البضائع الجاهزة" msgid "Finished Goods based Operating Cost" msgstr "تكلفة التشغيل بناءً على المنتجات النهائية" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2070 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2102 msgid "Finished Item {0} does not match with Work Order {1}" msgstr "المنتج النهائي {0} لا يتطابق مع أمر العمل {1}" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1060 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1069 msgid "Finished good quantity being consumed ({0} in stock UOM) must equal the quantity to disassemble ({1}). Do not change the UOM, conversion factor or quantity of the finished good row." msgstr "" @@ -21103,7 +21111,7 @@ msgstr "اتبع التقويم الأشهر" msgid "Following Material Requests have been raised automatically based on Item's re-order level" msgstr "تم رفع طلبات المواد التالية تلقائيا بناء على مستوى اعادة الطلب للبنود" -#: erpnext/selling/doctype/customer/customer.py:966 +#: erpnext/selling/doctype/customer/customer.py:967 msgid "Following fields are mandatory to create address:" msgstr "الحقول التالية إلزامية لإنشاء العنوان:" @@ -21160,7 +21168,7 @@ msgstr "للشركة" msgid "For Item" msgstr "للمنتج" -#: erpnext/controllers/stock_controller.py:1769 +#: erpnext/controllers/stock_controller.py:1783 msgid "For Item {0} cannot be received more than {1} qty against the {2} {3}" msgstr "" @@ -21195,7 +21203,7 @@ msgstr "لائحة الأسعار" msgid "For Production" msgstr "للإنتاج" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1010 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1019 msgid "For Quantity (Manufactured Qty) is mandatory" msgstr "" @@ -21205,7 +21213,7 @@ msgstr "" msgid "For Raw Materials" msgstr "للمواد الخام" -#: erpnext/controllers/accounts_controller.py:1469 +#: erpnext/controllers/accounts_controller.py:1474 msgid "For Return Invoices with Stock effect, '0' qty Items are not allowed. Following rows are affected: {0}" msgstr "بالنسبة لفواتير الإرجاع ذات تأثير المخزون، لا يُسمح بوجود عناصر بكمية '0'. تتأثر الصفوف التالية: {0}" @@ -21306,7 +21314,7 @@ msgstr "" msgid "For projected and forecast quantities, the system will consider all child warehouses under the selected parent warehouse." msgstr "بالنسبة للكميات المتوقعة والمتنبأ بها، سيأخذ النظام في الاعتبار جميع المستودعات الفرعية التابعة للمستودع الرئيسي المحدد." -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2102 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2134 msgid "For quantity {0} should not be greater than allowed quantity {1}" msgstr "" @@ -21320,7 +21328,7 @@ msgstr "للرجوع إليها" msgid "For row {0} in {1}. To include {2} in Item rate, rows {3} must also be included" msgstr "بالنسبة للصف {0} في {1}، يجب تضمين الصف {2} في سعر الصنف. لإضافة الصف {3} إلى سعر الصنف، يجب أيضًا إضافة الصف {3}." -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1729 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1728 msgid "For row {0}: Enter Planned Qty" msgstr "بالنسبة إلى الصف {0}: أدخل الكمية المخطط لها" @@ -21339,20 +21347,20 @@ msgstr "بالنسبة لشرط "تطبيق القاعدة على أخرى& msgid "For the convenience of customers, these codes can be used in print formats like Invoices and Delivery Notes" msgstr "لتسهيل الأمر على العملاء، يمكن استخدام هذه الرموز في نماذج الطباعة مثل الفواتير وإشعارات التسليم." -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1270 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1279 msgid "For the item {0}, the consumed quantity should be {1} according to the BOM {2}." msgstr "" -#: erpnext/public/js/controllers/transaction.js:1427 +#: erpnext/public/js/controllers/transaction.js:1429 msgctxt "Clear payment terms template and/or payment schedule when due date is changed" msgid "For the new {0} to take effect, would you like to clear the current {1}?" msgstr "لكي يسري مفعول {0} الجديد، هل ترغب في مسح {1}الحالي؟" -#: erpnext/controllers/stock_controller.py:488 +#: erpnext/controllers/stock_controller.py:502 msgid "For the {0}, no stock is available for the return in the warehouse {1}." msgstr "بالنسبة لـ {0}، لا يوجد مخزون متاح للإرجاع في المستودع {1}." -#: erpnext/controllers/sales_and_purchase_return.py:1247 +#: erpnext/controllers/sales_and_purchase_return.py:1265 msgid "For the {0}, the quantity is required to make the return entry" msgstr "بالنسبة لـ {0}، الكمية مطلوبة لإجراء قيد الإرجاع" @@ -21961,7 +21969,7 @@ msgstr "المدفوعات المستقبلية" msgid "Future date is not allowed" msgstr "التاريخ المستقبلي غير مسموح به" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:269 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:278 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:161 msgid "G - D" msgstr "جي - دي" @@ -22509,7 +22517,7 @@ msgstr "البضائع في العبور" msgid "Goods Transferred" msgstr "نقل البضائع" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2671 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2703 msgid "Goods are already received against the outward entry {0}" msgstr "تم استلام البضائع بالفعل مقابل الإدخال الخارجي {0}" @@ -22808,7 +22816,7 @@ msgstr "عقدة المجموعة" msgid "Group Same Items" msgstr "تجميع العناصر المتشابهة" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:158 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:163 msgid "Group Warehouses cannot be used in transactions. Please change the value of {0}" msgstr "لا يمكن استخدام مستودعات المجموعة في المعاملات. يرجى تغيير قيمة {0}" @@ -22871,7 +22879,7 @@ msgstr "مجموعات" msgid "Growth View" msgstr "منظور النمو" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:279 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:288 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:171 msgid "H - F" msgstr "H - F" @@ -23140,7 +23148,7 @@ msgstr "يساعدك ذلك على توزيع الميزانية/الهدف عل msgid "Here are the error logs for the aforementioned failed depreciation entries: {0}" msgstr "فيما يلي سجلات الأخطاء الخاصة بإدخالات الإهلاك الفاشلة المذكورة أعلاه: {0}" -#: erpnext/stock/stock_ledger.py:2037 +#: erpnext/stock/stock_ledger.py:2080 msgid "Here are the options to proceed:" msgstr "فيما يلي الخيارات المتاحة للمتابعة:" @@ -23389,12 +23397,12 @@ msgstr "هندردويت (المملكة المتحدة)" msgid "Hundredweight (US)" msgstr "وزن المئة (أمريكي)" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:294 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:303 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:186 msgid "I - J" msgstr "أنا - ي" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:304 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:313 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:196 msgid "I - K" msgstr "أنا - ك" @@ -23796,7 +23804,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:2047 +#: erpnext/stock/stock_ledger.py:2090 msgid "If not, you can Cancel / Submit this entry" msgstr "وإلا يمكنك إلغاء / إرسال هذا الإدخال" @@ -23842,7 +23850,7 @@ msgstr "إذا نتج عن قائمة المواد مواد خردة، فيجب msgid "If the account is frozen, entries are allowed to restricted users." msgstr "إذا الحساب مجمد، يسمح بالدخول إلى المستخدمين المحددين." -#: erpnext/stock/stock_ledger.py:2040 +#: erpnext/stock/stock_ledger.py:2083 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 "إذا كان العنصر يتعامل كعنصر سعر تقييم صفري في هذا الإدخال ، فالرجاء تمكين "السماح بمعدل تقييم صفري" في جدول العناصر {0}." @@ -23943,7 +23951,7 @@ msgstr "إذا كنت ترغب في مطابقة معاملات محددة مع msgid "If you still want to proceed, please disable '{0}' checkbox." msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1855 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1854 msgid "If you still want to proceed, please enable {0}." msgstr "إذا كنت لا تزال ترغب في المتابعة، يرجى تفعيل {0}." @@ -24283,7 +24291,7 @@ msgstr "في الانتاج" #: erpnext/stock/report/available_serial_no/available_serial_no.py:112 #: erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py:82 #: erpnext/stock/report/stock_balance/stock_balance.py:543 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:318 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:321 msgid "In Qty" msgstr "كمية قادمة" @@ -24301,11 +24309,11 @@ msgstr "في الأوراق المالية" msgid "In Transit" msgstr "في مرحلة انتقالية" -#: erpnext/stock/doctype/material_request/material_request.js:652 +#: erpnext/stock/doctype/material_request/material_request.js:648 msgid "In Transit Transfer" msgstr "النقل أثناء العبور" -#: erpnext/stock/doctype/material_request/material_request.js:621 +#: erpnext/stock/doctype/material_request/material_request.js:617 msgid "In Transit Warehouse" msgstr "مستودع النقل" @@ -24726,8 +24734,8 @@ msgstr "دفعة واردة" #: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json #: erpnext/stock/report/available_serial_no/available_serial_no.py:146 #: erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py:167 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:361 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:204 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:364 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:213 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:96 msgid "Incoming Rate" msgstr "معدل الواردة" @@ -24766,7 +24774,7 @@ msgstr "تسجيل دخول غير صحيح (مجموعة) إلى مستودع msgid "Incorrect Company" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1277 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1286 msgid "Incorrect Component Quantity" msgstr "كمية المكونات غير صحيحة" @@ -24816,7 +24824,7 @@ msgstr "نوع المعاملة غير صحيح" #: erpnext/stock/doctype/pick_list/pick_list.py:192 #: erpnext/stock/doctype/pick_list/pick_list.py:216 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:161 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:166 msgid "Incorrect Warehouse" msgstr "مستودع غير صحيح" @@ -24980,14 +24988,14 @@ msgstr "بدأت" msgid "Inspected By" msgstr "تفتيش من قبل" -#: erpnext/controllers/stock_controller.py:1663 +#: erpnext/controllers/stock_controller.py:1677 #: erpnext/manufacturing/doctype/job_card/job_card.py:834 msgid "Inspection Rejected" msgstr "تم رفض التفتيش" #. Label of the inspection_required (Check) field in DocType 'Stock Entry' -#: erpnext/controllers/stock_controller.py:1633 -#: erpnext/controllers/stock_controller.py:1635 +#: erpnext/controllers/stock_controller.py:1647 +#: erpnext/controllers/stock_controller.py:1649 #: erpnext/stock/doctype/stock_entry/stock_entry.json msgid "Inspection Required" msgstr "التفتيش مطلوب" @@ -25004,7 +25012,7 @@ msgstr "التفتيش المطلوبة قبل تسليم" msgid "Inspection Required before Purchase" msgstr "التفتيش المطلوبة قبل الشراء" -#: erpnext/controllers/stock_controller.py:1648 +#: erpnext/controllers/stock_controller.py:1662 #: erpnext/manufacturing/doctype/job_card/job_card.py:815 msgid "Inspection Submission" msgstr "طلب فحص" @@ -25074,11 +25082,11 @@ msgstr "تعليمات" msgid "Insufficient Capacity" msgstr "سعة غير كافية" -#: erpnext/controllers/accounts_controller.py:3910 -#: erpnext/controllers/accounts_controller.py:3932 -#: erpnext/controllers/accounts_controller.py:4450 -#: erpnext/controllers/accounts_controller.py:4456 -#: erpnext/controllers/accounts_controller.py:4478 +#: erpnext/controllers/accounts_controller.py:3940 +#: erpnext/controllers/accounts_controller.py:3962 +#: erpnext/controllers/accounts_controller.py:4480 +#: erpnext/controllers/accounts_controller.py:4486 +#: erpnext/controllers/accounts_controller.py:4508 msgid "Insufficient Permissions" msgstr "أذونات غير كافية" @@ -25086,13 +25094,13 @@ msgstr "أذونات غير كافية" #: erpnext/stock/doctype/pick_list/pick_list.py:150 #: erpnext/stock/doctype/pick_list/pick_list.py:168 #: erpnext/stock/doctype/pick_list/pick_list.py:1123 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1247 -#: erpnext/stock/serial_batch_bundle.py:1225 erpnext/stock/stock_ledger.py:1728 -#: erpnext/stock/stock_ledger.py:2225 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1256 +#: erpnext/stock/serial_batch_bundle.py:1311 erpnext/stock/stock_ledger.py:1771 +#: erpnext/stock/stock_ledger.py:2268 msgid "Insufficient Stock" msgstr "المالية غير كافية" -#: erpnext/stock/stock_ledger.py:2240 +#: erpnext/stock/stock_ledger.py:2283 msgid "Insufficient Stock for Batch" msgstr "المخزون غير كافٍ للدفعة" @@ -25247,7 +25255,7 @@ msgstr "داخلي" msgid "Internal Customer Accounting" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:264 +#: erpnext/selling/doctype/customer/customer.py:265 msgid "Internal Customer for company {0} already exists" msgstr "يوجد بالفعل عميل داخلي للشركة {0}" @@ -25255,7 +25263,7 @@ msgstr "يوجد بالفعل عميل داخلي للشركة {0}" msgid "Internal Purchase Order" msgstr "أمر شراء داخلي" -#: erpnext/controllers/accounts_controller.py:831 +#: erpnext/controllers/accounts_controller.py:836 msgid "Internal Sale or Delivery Reference missing." msgstr "رقم مرجع البيع أو التسليم الداخلي مفقود." @@ -25263,7 +25271,7 @@ msgstr "رقم مرجع البيع أو التسليم الداخلي مفقود msgid "Internal Sales Order" msgstr "أمر بيع داخلي" -#: erpnext/controllers/accounts_controller.py:833 +#: erpnext/controllers/accounts_controller.py:838 msgid "Internal Sales Reference Missing" msgstr "رقم مرجع المبيعات الداخلي مفقود" @@ -25294,7 +25302,7 @@ msgstr "يوجد بالفعل مورد داخلي لشركة {0}" msgid "Internal Transfer" msgstr "نقل داخلي" -#: erpnext/controllers/accounts_controller.py:842 +#: erpnext/controllers/accounts_controller.py:847 msgid "Internal Transfer Reference Missing" msgstr "رقم مرجع التحويل الداخلي مفقود" @@ -25318,7 +25326,7 @@ msgstr "سجل العمل الداخلي" msgid "Internal notes about this customer. Not visible on transactions or the portal." msgstr "" -#: erpnext/controllers/stock_controller.py:1730 +#: erpnext/controllers/stock_controller.py:1744 msgid "Internal transfers can only be done in company's default currency" msgstr "لا يمكن إجراء التحويلات الداخلية إلا بالعملة الافتراضية للشركة" @@ -25332,14 +25340,14 @@ msgstr "النشر عبر الإنترنت" msgid "Interval should be between 1 to 59 MInutes" msgstr "يجب أن تكون الفترة الزمنية بين 1 و 59 دقيقة" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:384 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:392 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:394 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:402 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1073 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1083 #: erpnext/assets/doctype/asset_category/asset_category.py:69 #: erpnext/assets/doctype/asset_category/asset_category.py:97 -#: erpnext/controllers/accounts_controller.py:3245 -#: erpnext/controllers/accounts_controller.py:3253 +#: erpnext/controllers/accounts_controller.py:3250 +#: erpnext/controllers/accounts_controller.py:3258 msgid "Invalid Account" msgstr "حساب غير صالح" @@ -25364,7 +25372,7 @@ msgstr "خاصية غير صالحة" msgid "Invalid Attribute Values" msgstr "" -#: erpnext/controllers/accounts_controller.py:645 +#: erpnext/controllers/accounts_controller.py:650 msgid "Invalid Auto Repeat Date" msgstr "تاريخ التكرار التلقائي غير صالح" @@ -25377,7 +25385,7 @@ msgstr "" msgid "Invalid Barcode. There is no Item attached to this barcode." msgstr "الباركود غير صالح. لا يوجد عنصر مرفق بهذا الرمز الشريطي." -#: erpnext/public/js/controllers/transaction.js:3186 +#: erpnext/public/js/controllers/transaction.js:3191 msgid "Invalid Blanket Order for the selected Customer and Item" msgstr "طلب فارغ غير صالح للعميل والعنصر المحدد" @@ -25399,11 +25407,11 @@ msgstr "شركة غير صالحة للمعاملات بين الشركات." #: erpnext/assets/doctype/asset/asset.py:365 #: erpnext/assets/doctype/asset/asset.py:372 -#: erpnext/controllers/accounts_controller.py:3268 +#: erpnext/controllers/accounts_controller.py:3273 msgid "Invalid Cost Center" msgstr "مركز تكلفة غير صالح" -#: erpnext/selling/doctype/customer/customer.py:379 +#: erpnext/selling/doctype/customer/customer.py:380 msgid "Invalid Customer Group" msgstr "" @@ -25411,12 +25419,12 @@ msgstr "" msgid "Invalid Delivery Date" msgstr "تاريخ تسليم غير صالح" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1099 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1108 msgid "Invalid Disassembly Item" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1065 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1114 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1074 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1123 msgid "Invalid Disassembly Quantity" msgstr "" @@ -25444,8 +25452,8 @@ msgstr "" msgid "Invalid File Type" msgstr "" -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:329 -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:334 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:374 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:379 msgid "Invalid Formula" msgstr "صيغة غير صالحة" @@ -25458,7 +25466,7 @@ msgstr "تجميع غير صالح" msgid "Invalid Item" msgstr "عنصر غير صالح" -#: erpnext/stock/doctype/item/item.py:1534 +#: erpnext/stock/doctype/item/item.py:1544 msgid "Invalid Item Defaults" msgstr "القيم الافتراضية للعناصر غير صالحة" @@ -25514,12 +25522,12 @@ msgstr "تكوين فقدان العملية غير صالح" msgid "Invalid Purchase Invoice" msgstr "فاتورة شراء غير صالحة" -#: erpnext/controllers/accounts_controller.py:3952 -#: erpnext/controllers/accounts_controller.py:3966 +#: erpnext/controllers/accounts_controller.py:3982 +#: erpnext/controllers/accounts_controller.py:3996 msgid "Invalid Qty" msgstr "كمية غير صالحة" -#: erpnext/controllers/accounts_controller.py:1487 +#: erpnext/controllers/accounts_controller.py:1492 msgid "Invalid Quantity" msgstr "كمية غير صحيحة" @@ -25527,6 +25535,10 @@ msgstr "كمية غير صحيحة" msgid "Invalid Query" msgstr "استعلام غير صالح" +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:325 +msgid "Invalid Reading" +msgstr "" + #: erpnext/selling/page/point_of_sale/pos_past_order_summary.js:202 msgid "Invalid Return" msgstr "إرجاع غير صالح" @@ -25544,12 +25556,12 @@ msgstr "جدول غير صالح" msgid "Invalid Selling Price" msgstr "سعر البيع غير صالح" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2145 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2177 msgid "Invalid Serial and Batch Bundle" msgstr "رقم تسلسلي وحزمة دفعات غير صالحة" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1366 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1388 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1375 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1397 msgid "Invalid Source and Target Warehouse" msgstr "مصدر ومستودع هدف غير صالحين" @@ -25852,6 +25864,10 @@ msgstr "الفواتير والمحاسبة" msgid "Invoice can't be made for zero billing hour" msgstr "لا يمكن إجراء الفاتورة لمدة صفر ساعة" +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1933 +msgid "Invoice is not blocked. Block the invoice to change the release date." +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 @@ -26563,7 +26579,7 @@ msgstr "تاريخ الإصدار" msgid "It can take upto few hours for accurate stock values to be visible after merging items." msgstr "قد يستغرق الأمر بضع ساعات حتى تظهر قيم المخزون الدقيقة بعد دمج العناصر." -#: erpnext/public/js/controllers/transaction.js:2564 +#: erpnext/public/js/controllers/transaction.js:2569 msgid "It is needed to fetch Item Details." msgstr "هناك حاجة لجلب تفاصيل البند." @@ -26641,8 +26657,8 @@ msgstr "نص مائل للمجاميع الفرعية أو الملاحظات" #: erpnext/manufacturing/report/process_loss_report/process_loss_report.js:15 #: erpnext/manufacturing/report/process_loss_report/process_loss_report.py:74 #: erpnext/manufacturing/workspace/manufacturing/manufacturing.json -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:253 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:404 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:242 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:393 #: erpnext/public/js/purchase_trends_filters.js:48 #: erpnext/public/js/purchase_trends_filters.js:63 #: erpnext/public/js/sales_trends_filters.js:23 @@ -26689,7 +26705,7 @@ msgstr "نص مائل للمجاميع الفرعية أو الملاحظات" #: erpnext/stock/report/stock_analytics/stock_analytics.js:15 #: erpnext/stock/report/stock_analytics/stock_analytics.py:43 #: erpnext/stock/report/stock_balance/stock_balance.py:470 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:288 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:291 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js:27 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:51 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.js:28 @@ -26943,10 +26959,10 @@ msgstr "سلة التسوق" #: erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py:86 #: erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py:119 #: erpnext/projects/doctype/timesheet/timesheet.js:216 -#: erpnext/public/js/controllers/transaction.js:2858 +#: erpnext/public/js/controllers/transaction.js:2863 #: erpnext/public/js/stock_reservation.js:112 #: erpnext/public/js/stock_reservation.js:318 erpnext/public/js/utils.js:596 -#: erpnext/public/js/utils.js:753 +#: erpnext/public/js/utils.js:754 #: erpnext/public/js/utils/serial_no_batch_selector.js:96 #: erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.json #: erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json @@ -27009,7 +27025,7 @@ msgstr "سلة التسوق" #: erpnext/stock/report/stock_ageing/stock_ageing.py:177 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:105 #: erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py:25 -#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:26 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:32 #: erpnext/subcontracting/doctype/subcontracting_inward_order_item/subcontracting_inward_order_item.json #: erpnext/subcontracting/doctype/subcontracting_inward_order_received_item/subcontracting_inward_order_received_item.json #: erpnext/subcontracting/doctype/subcontracting_inward_order_secondary_item/subcontracting_inward_order_secondary_item.json @@ -27039,7 +27055,7 @@ msgstr "رمز المنتج > مجموعة المنتجات > العلامة ا msgid "Item Code cannot be changed for Serial No." msgstr "لا يمكن تغيير رمز السلعة للرقم التسلسلي" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:451 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:461 msgid "Item Code required at Row No {0}" msgstr "رمز العنصر المطلوب في الصف رقم {0}\\n
    \\nItem Code required at Row No {0}" @@ -27212,7 +27228,7 @@ msgstr "بيانات الصنف" #: erpnext/stock/report/stock_balance/stock_balance.js:32 #: erpnext/stock/report/stock_balance/stock_balance.py:478 #: erpnext/stock/report/stock_ledger/stock_ledger.js:71 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:346 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:349 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.js:39 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:114 #: erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.js:33 @@ -27430,8 +27446,8 @@ msgstr "مادة المصنع" #: erpnext/manufacturing/report/production_planning_report/production_planning_report.py:371 #: erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py:92 #: erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py:138 -#: erpnext/public/js/controllers/transaction.js:2864 -#: erpnext/public/js/utils.js:849 +#: erpnext/public/js/controllers/transaction.js:2869 +#: erpnext/public/js/utils.js:852 #: erpnext/selling/doctype/quotation_item/quotation_item.json #: erpnext/selling/doctype/sales_order/sales_order.js:1286 #: erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -27474,10 +27490,10 @@ msgstr "مادة المصنع" #: erpnext/stock/report/stock_ageing/stock_ageing.py:183 #: erpnext/stock/report/stock_analytics/stock_analytics.py:45 #: erpnext/stock/report/stock_balance/stock_balance.py:476 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:294 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:297 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:111 #: erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py:31 -#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:32 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:38 #: erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py:98 #: erpnext/subcontracting/doctype/subcontracting_inward_order_item/subcontracting_inward_order_item.json #: erpnext/subcontracting/doctype/subcontracting_inward_order_service_item/subcontracting_inward_order_service_item.json @@ -27839,15 +27855,15 @@ msgstr "المنتج والمستودع" msgid "Item and Warranty Details" msgstr "البند والضمان تفاصيل" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3859 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3891 msgid "Item for row {0} does not match Material Request" msgstr "عنصر الصف {0} لا يتطابق مع طلب المواد" -#: erpnext/stock/doctype/item/item.py:895 +#: erpnext/stock/doctype/item/item.py:905 msgid "Item has variants." msgstr "البند لديه متغيرات." -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:455 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:444 msgid "Item is mandatory in Raw Materials table." msgstr "هذا العنصر إلزامي في جدول المواد الخام." @@ -27869,11 +27885,11 @@ msgstr "اسم السلعة" msgid "Item operation" msgstr "عملية الصنف" -#: erpnext/controllers/accounts_controller.py:4006 +#: erpnext/controllers/accounts_controller.py:4036 msgid "Item qty can not be updated as raw materials are already processed." msgstr "لا يمكن تحديث كمية الصنف لأن المواد الخام قد تمت معالجتها بالفعل." -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1520 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1531 msgid "Item rate has been updated to zero as Allow Zero Valuation Rate is checked for item {0}" msgstr "تم تحديث سعر السلعة إلى الصفر حيث تم تحديد خيار \"السماح بسعر تقييم صفري\" للسلعة {0}" @@ -27896,7 +27912,7 @@ msgstr "يتم إعادة حساب معدل تقييم السلعة مع الأ msgid "Item valuation reposting in progress. Report might show incorrect item valuation." msgstr "جارٍ إعادة نشر تقييم الأصناف. قد يُظهر التقرير تقييمًا غير صحيح للأصناف." -#: erpnext/stock/doctype/item/item.py:1052 +#: erpnext/stock/doctype/item/item.py:1062 msgid "Item variant {0} exists with same attributes" msgstr "متغير العنصر {0} موجود بنفس السمات\\n
    \\nItem variant {0} exists with same attributes" @@ -27922,6 +27938,7 @@ msgstr "لا يمكن طلب أكثر من {0} من المنتج {1} ضمن طل #: erpnext/assets/doctype/asset/asset.py:347 #: erpnext/stock/doctype/item/item.py:687 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:102 msgid "Item {0} does not exist" msgstr "العنصر {0} غير موجود\\n
    \\nItem {0} does not exist" @@ -27929,7 +27946,7 @@ msgstr "العنصر {0} غير موجود\\n
    \\nItem {0} does not exist" msgid "Item {0} does not exist in the system or has expired" msgstr "الصنف{0} غير موجود في النظام أو انتهت صلاحيته" -#: erpnext/controllers/stock_controller.py:602 +#: erpnext/controllers/stock_controller.py:616 msgid "Item {0} does not exist." msgstr "العنصر {0} غير موجود\\n
    \\nItem {0} does not exist." @@ -27937,7 +27954,7 @@ msgstr "العنصر {0} غير موجود\\n
    \\nItem {0} does not exist." msgid "Item {0} entered multiple times." msgstr "تم إدخال العنصر {0} عدة مرات." -#: erpnext/controllers/sales_and_purchase_return.py:221 +#: erpnext/controllers/sales_and_purchase_return.py:239 msgid "Item {0} has already been returned" msgstr "تمت إرجاع الصنف{0} من قبل" @@ -27953,11 +27970,11 @@ msgstr "العنصر {0} ليس له رقم تسلسلي. يتم تسليم ال msgid "Item {0} has no changes in delivered quantity. Please unselect the row if you do not wish to update its quantity." msgstr "" -#: erpnext/stock/doctype/item/item.py:1250 +#: erpnext/stock/doctype/item/item.py:1260 msgid "Item {0} has reached its end of life on {1}" msgstr "الصنف{0} قد وصل إلى نهاية عمره في {1}" -#: erpnext/stock/stock_ledger.py:117 +#: erpnext/stock/stock_ledger.py:144 msgid "Item {0} ignored since it is not a stock item" msgstr "تم تجاهل الصنف {0} لأنه ليس بند مخزون" @@ -27965,11 +27982,11 @@ msgstr "تم تجاهل الصنف {0} لأنه ليس بند مخزون" msgid "Item {0} is already reserved/delivered against Sales Order {1}." msgstr "تم حجز/تسليم المنتج {0} بالفعل بموجب أمر البيع {1}." -#: erpnext/stock/doctype/item/item.py:1270 +#: erpnext/stock/doctype/item/item.py:1280 msgid "Item {0} is cancelled" msgstr "تم إلغاء العنصر {0}\\n
    \\nItem {0} is cancelled" -#: erpnext/stock/doctype/item/item.py:1254 +#: erpnext/stock/doctype/item/item.py:1264 msgid "Item {0} is disabled" msgstr "تم تعطيل البند {0}" @@ -27981,7 +27998,7 @@ msgstr "" msgid "Item {0} is not a serialized Item" msgstr "البند {0} ليس بند لديه رقم تسلسلي" -#: erpnext/stock/doctype/item/item.py:1262 +#: erpnext/stock/doctype/item/item.py:1272 msgid "Item {0} is not a stock Item" msgstr "العنصر {0} ليس عنصر مخزون\\n
    \\nItem {0} is not a stock Item" @@ -27993,7 +28010,7 @@ msgstr "العنصر {0} ليس عنصرًا متعاقدًا عليه من ال msgid "Item {0} is not a template item." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2583 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2615 msgid "Item {0} is not active or end of life has been reached" msgstr "البند {0} غير نشط أو تم التوصل إلى نهاية الحياة" @@ -28013,7 +28030,7 @@ msgstr "" msgid "Item {0} must be a non-stock item" msgstr "الصنف {0} يجب ألا يكون صنف مخزن
    Item {0} must be a non-stock item" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1883 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1913 msgid "Item {0} not found in 'Raw Materials Supplied' table in {1} {2}" msgstr "العنصر {0} غير موجود في جدول \"المواد الخام الموردة\" في {1} {2}" @@ -28099,7 +28116,7 @@ msgstr "كتالوج العناصر" msgid "Items Filter" msgstr "تصفية الاصناف" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1691 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1690 #: erpnext/selling/doctype/sales_order/sales_order.js:1719 msgid "Items Required" msgstr "العناصر المطلوبة" @@ -28123,11 +28140,11 @@ msgstr "اصناف يمكن طلبه" msgid "Items and Pricing" msgstr "السلع والتسعيرات" -#: erpnext/controllers/accounts_controller.py:4264 +#: erpnext/controllers/accounts_controller.py:4294 msgid "Items cannot be updated as Subcontracting Inward Order(s) exist against this Subcontracted Sales Order." msgstr "لا يمكن تحديث العناصر لوجود أوامر واردة من الباطن مرتبطة بأمر البيع هذا." -#: erpnext/controllers/accounts_controller.py:4257 +#: erpnext/controllers/accounts_controller.py:4287 msgid "Items cannot be updated as Subcontracting Order is created against the Purchase Order {0}." msgstr "لا يمكن تحديث العناصر لأن أمر التعاقد من الباطن يتم إنشاؤه مقابل أمر الشراء {0}." @@ -28139,7 +28156,7 @@ msgstr "عناصر لطلب المواد الخام" msgid "Items not found." msgstr "لم يتم العثور على العناصر." -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1516 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1527 msgid "Items rate has been updated to zero as Allow Zero Valuation Rate is checked for the following items: {0}" msgstr "تم تحديث سعر الأصناف إلى الصفر حيث تم تحديد خيار \"السماح بسعر تقييم صفري\" للأصناف التالية: {0}" @@ -28149,7 +28166,7 @@ msgstr "تم تحديث سعر الأصناف إلى الصفر حيث تم تح msgid "Items to Be Repost" msgstr "عناصر سيتم إعادة نشرها" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1690 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1689 msgid "Items to Manufacture are required to pull the Raw Materials associated with it." msgstr "العناصر المطلوب تصنيعها لسحب المواد الخام المرتبطة بها." @@ -28169,7 +28186,7 @@ msgstr "العناصر المراد حجزها" msgid "Items under this warehouse will be suggested" msgstr "وسيتم اقتراح العناصر الموجودة تحت هذا المستودع" -#: erpnext/controllers/stock_controller.py:207 +#: erpnext/controllers/stock_controller.py:221 msgid "Items {0} do not exist in the Item master." msgstr "العناصر {0} غير موجودة في قائمة العناصر الرئيسية." @@ -28652,7 +28669,7 @@ msgstr "فاتورة المورد بتكلفة الشحن" #. Name of a DocType #. Label of a Link in the Stock Workspace #. Label of a Workspace Sidebar Item -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:671 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:669 #: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:88 #: erpnext/stock/workspace/stock/stock.json @@ -29119,7 +29136,7 @@ msgstr "رقم الرخصة" msgid "License Plate" msgstr "" -#: erpnext/controllers/status_updater.py:512 +#: erpnext/controllers/status_updater.py:513 msgid "Limit Crossed" msgstr "الحدود تجاوزت" @@ -29201,7 +29218,7 @@ msgstr "الفواتير المرتبطة" msgid "Linked Location" msgstr "الموقع المرتبط" -#: erpnext/stock/doctype/item/item.py:1104 +#: erpnext/stock/doctype/item/item.py:1114 msgid "Linked with submitted documents" msgstr "مرتبط بالوثائق المقدمة" @@ -29482,7 +29499,7 @@ msgstr "نقاط الولاء: {0}" #: erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.json #: erpnext/accounts/doctype/loyalty_program/loyalty_program.json #: erpnext/accounts/doctype/pos_invoice/pos_invoice.json -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1225 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1234 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/selling/doctype/customer/customer.json #: erpnext/selling/page/point_of_sale/pos_item_cart.js:963 @@ -29960,11 +29977,11 @@ msgstr "إلزامي لحساب الربح والخسارة" msgid "Mandatory Missing" msgstr "إلزامي مفقود" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:634 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:644 msgid "Mandatory Purchase Order" msgstr "أمر شراء إلزامي" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:656 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:666 msgid "Mandatory Purchase Receipt" msgstr "إيصال الشراء الإلزامي" @@ -30039,8 +30056,8 @@ msgstr "لا يمكن إنشاء الإدخال اليدوي! قم بتعطيل #: erpnext/stock/doctype/material_request_item/material_request_item.json #: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json #: erpnext/stock/doctype/stock_entry/stock_entry.json -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1625 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1641 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1668 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1684 #: erpnext/stock/doctype/stock_entry_type/stock_entry_type.json #: erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json #: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -30190,7 +30207,7 @@ msgstr "تاريخ التصنيع" msgid "Manufacturing Manager" msgstr "مدير التصنيع" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2941 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2973 msgid "Manufacturing Quantity is mandatory" msgstr "" @@ -30267,7 +30284,7 @@ msgstr "رسم خرائط طلبات الشراء الداخلية للتعاق msgid "Mapping Subcontracting Order ..." msgstr "تحديد ترتيب التعاقد من الباطن ..." -#: erpnext/public/js/utils.js:1084 +#: erpnext/public/js/utils.js:1110 msgid "Mapping {0} ..." msgstr "رسم الخرائط {0}..." @@ -30470,7 +30487,7 @@ msgstr "اهلاك المواد" #. Option for the 'Purpose' (Select) field in DocType 'Stock Entry Type' #: erpnext/setup/setup_wizard/operations/install_fixtures.py:117 #: erpnext/stock/doctype/stock_entry/stock_entry.json -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1626 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1669 #: erpnext/stock/doctype/stock_entry_type/stock_entry_type.json msgid "Material Consumption for Manufacture" msgstr "اهلاك المواد للتصنيع" @@ -30899,11 +30916,11 @@ msgstr "الحد الأقصى لمبلغ الدفع" msgid "Maximum Producible Items" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:4475 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:4507 msgid "Maximum Samples - {0} can be retained for Batch {1} and Item {2}." msgstr "الحد الأقصى للعينات - {0} يمكن الاحتفاظ بالدفعة {1} والبند {2}." -#: erpnext/stock/doctype/stock_entry/stock_entry.py:4466 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:4498 msgid "Maximum Samples - {0} have already been retained for Batch {1} and Item {2} in Batch {3}." msgstr "الحد الأقصى للعينات - {0} تم الاحتفاظ به مسبقا للدفعة {1} و العنصر {2} في الدفعة {3}." @@ -30964,7 +30981,7 @@ msgstr "ميغا جول" msgid "Megawatt" msgstr "ميغاواط" -#: erpnext/stock/stock_ledger.py:2053 +#: erpnext/stock/stock_ledger.py:2096 msgid "Mention Valuation Rate in the Item master." msgstr "اذكر معدل التقييم في مدير السلعة." @@ -30999,7 +31016,7 @@ msgstr "دمج التقدم" msgid "Merge similar Account Heads" msgstr "" -#: erpnext/public/js/utils.js:1116 +#: erpnext/public/js/utils.js:1142 msgid "Merge taxes from multiple documents" msgstr "دمج الضرائب من وثائق متعددة" @@ -31354,7 +31371,7 @@ msgstr "مفتقد" #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:97 #: erpnext/accounts/doctype/pos_profile/pos_profile.py:200 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:593 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:603 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2484 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3100 #: erpnext/assets/doctype/asset_category/asset_category.py:116 @@ -31386,15 +31403,15 @@ msgstr "فلاتر مفقودة" msgid "Missing Finance Book" msgstr "كتاب التمويل المفقود" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2080 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2112 msgid "Missing Finished Good" msgstr "مفقود، تم الانتهاء منه، جيد" -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:314 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:359 msgid "Missing Formula" msgstr "الصيغة المفقودة" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1284 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1293 msgid "Missing Item" msgstr "العنصر المفقود" @@ -31676,7 +31693,7 @@ msgstr "" msgid "Multiple Accounts (Journal Template)" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:453 +#: erpnext/selling/doctype/customer/customer.py:454 msgid "Multiple Loyalty Programs found for Customer {}. Please select manually." msgstr "تم العثور على عدة برامج ولاء للعميل {}. يرجى الاختيار يدويًا." @@ -31702,11 +31719,11 @@ msgstr "متغيرات متعددة" msgid "Multiple company fields available: {0}. Please select manually." msgstr "" -#: erpnext/controllers/accounts_controller.py:1333 +#: erpnext/controllers/accounts_controller.py:1338 msgid "Multiple fiscal years exist for the date {0}. Please set company in Fiscal Year" msgstr "يوجد سنوات مالية متعددة لنفس التاريخ {0}. الرجاء تحديد الشركة لهذه السنة المالية\\n
    \\nMultiple fiscal years exist for the date {0}. Please set company in Fiscal Year" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2087 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2119 msgid "Multiple items cannot be marked as finished item" msgstr "لا يمكن وضع علامة \"منتج نهائي\" على عدة عناصر" @@ -31857,8 +31874,8 @@ msgstr "الكمية السلبية غير مسموح بها\\n
    \\nnegative Q msgid "Negative Stock" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1674 -#: erpnext/stock/serial_batch_bundle.py:1548 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1724 +#: erpnext/stock/serial_batch_bundle.py:1634 msgid "Negative Stock Error" msgstr "خطأ في المخزون السالب" @@ -32168,7 +32185,7 @@ msgstr "الوزن الصافي" msgid "Net Weight UOM" msgstr "الوزن الصافي لوحدة القياس" -#: erpnext/controllers/accounts_controller.py:1693 +#: erpnext/controllers/accounts_controller.py:1698 msgid "Net total calculation precision loss" msgstr "صافي إجمالي فقدان دقة الحساب" @@ -32347,7 +32364,7 @@ msgstr "اسم المخزن الجديد" msgid "New Workplace" msgstr "مكان العمل الجديد" -#: erpnext/selling/doctype/customer/customer.py:418 +#: erpnext/selling/doctype/customer/customer.py:419 msgid "New credit limit is less than current outstanding amount for the customer. Credit limit has to be atleast {0}" msgstr "" @@ -32357,7 +32374,7 @@ msgstr "" msgid "New invoices will be generated as per schedule even if current invoices are unpaid or past due date" msgstr "سيتم إنشاء فواتير جديدة وفقًا للجدول الزمني حتى إذا كانت الفواتير الحالية غير مدفوعة أو تجاوز تاريخ الاستحقاق" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:261 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:259 msgid "New release date should be in the future" msgstr "يجب أن يكون تاريخ الإصدار الجديد في المستقبل" @@ -32475,10 +32492,10 @@ msgstr "لم يتم العثور على أي فواتير مستحقة لهذا msgid "No POS Profile found. Please create a New POS Profile first" msgstr "لم يتم العثور على ملف تعريف نقطة البيع. يرجى إنشاء ملف تعريف نقطة بيع جديد أولاً" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1583 -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1643 -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1657 -#: erpnext/stock/doctype/item/item.py:1495 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1593 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1653 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1667 +#: erpnext/stock/doctype/item/item.py:1505 msgid "No Permission" msgstr "لا يوجد تصريح" @@ -32495,7 +32512,7 @@ msgstr "" msgid "No Selection" msgstr "لا يوجد اختيار" -#: erpnext/controllers/sales_and_purchase_return.py:975 +#: erpnext/controllers/sales_and_purchase_return.py:993 msgid "No Serial / Batches are available for return" msgstr "لا تتوفر أرقام تسلسلية/دفعات للإرجاع" @@ -32593,7 +32610,7 @@ msgstr "" msgid "No billing email found for customer: {0}" msgstr "لم يتم العثور على بريد إلكتروني للفواتير خاص بالعميل: {0}" -#: banking/src/components/features/BankReconciliation/CompanySelector.tsx:66 +#: banking/src/components/features/BankReconciliation/CompanySelector.tsx:79 msgid "No company found." msgstr "" @@ -33064,6 +33081,10 @@ msgstr "ليس في الأسهم" msgid "Not permitted to make Purchase Orders" msgstr "غير مسموح له بتقديم طلبات شراء" +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:94 +msgid "Not permitted to update Serial No" +msgstr "" + #: erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js:21 msgid "Note: Automatic log deletion only applies to logs of type Update Cost" msgstr "" @@ -33086,7 +33107,7 @@ msgstr "ملاحظة: إذا كنت ترغب في استخدام المنتج ا msgid "Note: Item {0} added multiple times" msgstr "ملاحظة: تمت إضافة العنصر {0} عدة مرات" -#: erpnext/controllers/accounts_controller.py:731 +#: erpnext/controllers/accounts_controller.py:736 msgid "Note: Payment Entry will not be created since 'Cash or Bank Account' was not specified" msgstr "ملاحظة : لن يتم إنشاء تدوين المدفوعات نظرا لأن \" حساب النقد او المصرف\" لم يتم تحديده" @@ -33586,7 +33607,7 @@ msgstr "يجب أن يكون أحد خياري الإيداع أو السحب ف msgid "Only one operation can have 'Is Final Finished Good' checked when 'Track Semi Finished Goods' is enabled." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1640 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1683 msgid "Only one {0} entry can be created against the Work Order {1}" msgstr "لا يمكن إنشاء سوى إدخال واحد {0} مقابل أمر العمل {1}" @@ -33864,7 +33885,7 @@ msgstr "فتح الفاتورة البند" msgid "Opening Invoice Tool" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1712 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1722 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2044 msgid "Opening Invoice has rounding adjustment of {0}.

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

    Or, '{3}' can be enabled to not post any rounding adjustment." msgstr "" @@ -34516,7 +34537,7 @@ msgstr "أونصة/غالون (الولايات المتحدة)" #: erpnext/stock/report/available_serial_no/available_serial_no.py:119 #: erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py:83 #: erpnext/stock/report/stock_balance/stock_balance.py:551 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:325 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:328 msgid "Out Qty" msgstr "كمية خارجة" @@ -34573,7 +34594,7 @@ msgstr "الدفعة الصادرة" #. Label of the outgoing_rate (Currency) field in DocType 'Stock Ledger Entry' #: erpnext/stock/doctype/serial_and_batch_entry/serial_and_batch_entry.json #: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json -#: erpnext/stock/report/stock_ledger/stock_ledger.py:379 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:382 msgid "Outgoing Rate" msgstr "أسعار المنتهية ولايته" @@ -34690,11 +34711,11 @@ msgstr "" msgid "Over Picking Allowance (%)" msgstr "بدل الإفراط في الانتقاء (%)" -#: erpnext/controllers/stock_controller.py:1900 +#: erpnext/controllers/stock_controller.py:1914 msgid "Over Receipt" msgstr "إيصال زائد" -#: erpnext/controllers/status_updater.py:517 +#: erpnext/controllers/status_updater.py:518 msgid "Over Receipt/Delivery of {0} {1} ignored for item {2} because you have {3} role." msgstr "تم تجاهل استلام/تسليم {0} {1} للعنصر {2} لأن لديك الدور {3} ." @@ -34711,11 +34732,11 @@ msgstr "بدل التحويل الزائد (%)" msgid "Over Withheld" msgstr "مبالغ محجوزة" -#: erpnext/controllers/status_updater.py:519 +#: erpnext/controllers/status_updater.py:520 msgid "Overbilling of {0} {1} ignored for item {2} because you have {3} role." msgstr "تم تجاهل الفوترة الزائدة لـ {0} {1} للعنصر {2} لأن لديك الدور {3} ." -#: erpnext/controllers/accounts_controller.py:2211 +#: erpnext/controllers/accounts_controller.py:2216 msgid "Overbilling of {} ignored because you have {} role." msgstr "تم تجاهل الفوترة الزائدة لـ {} لأن لديك دور {} ." @@ -34752,11 +34773,11 @@ msgstr "الأيام المتأخرة" msgid "Overdue Limit" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:707 +#: erpnext/selling/doctype/customer/customer.py:708 msgid "Overdue Limit Crossed" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:702 +#: erpnext/selling/doctype/customer/customer.py:703 msgid "Overdue Limit crossed for customer {0}. Overdue amount {1} exceeds the allowed limit {2}." msgstr "" @@ -35228,7 +35249,7 @@ msgstr "عنصر معبأ" msgid "Packed Items" msgstr "عناصر معبأة" -#: erpnext/controllers/stock_controller.py:1734 +#: erpnext/controllers/stock_controller.py:1748 msgid "Packed Items cannot be transferred internally" msgstr "لا يمكن نقل العناصر المعبأة داخلياً" @@ -35375,7 +35396,7 @@ msgstr "" msgid "Paid To Account Type" msgstr "نوع الحساب المدفوع" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:334 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:344 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1204 msgid "Paid amount + Write Off Amount can not be greater than Grand Total" msgstr "المبلغ المدفوع + المبلغ المشطوب لا يمكن ان يكون أكبر من المجموع الكلي\\n
    \\nPaid amount + Write Off Amount can not be greater than Grand Total" @@ -35555,11 +35576,11 @@ msgstr "مجموعة موردي الآباء" msgid "Parent Task" msgstr "المهمة الرئيسية" -#: erpnext/projects/doctype/task/task.py:170 +#: erpnext/projects/doctype/task/task.py:171 msgid "Parent Task {0} is not a Template Task" msgstr "المهمة الأصلية {0} ليست مهمة نموذجية" -#: erpnext/projects/doctype/task/task.py:193 +#: erpnext/projects/doctype/task/task.py:194 msgid "Parent Task {0} must be a Group Task" msgstr "يجب أن تكون المهمة الرئيسية {0} مهمة جماعية" @@ -35884,7 +35905,7 @@ msgstr "" msgid "Party Account No. (Bank Statement)" msgstr "رقم حساب الطرف (كشف حساب بنكي)" -#: erpnext/controllers/accounts_controller.py:2495 +#: erpnext/controllers/accounts_controller.py:2500 msgid "Party Account {0} currency ({1}) and document currency ({2}) should be same" msgstr "يجب أن تكون عملة حساب الطرف {0} ({1}) وعملة المستند ({2}) متطابقتين." @@ -36388,7 +36409,7 @@ msgstr "تم تعديل تدوين مدفوعات بعد سحبه. يرجى سح msgid "Payment Entry is already created" msgstr "تدوين المدفوعات تم انشاؤه بالفعل" -#: erpnext/controllers/accounts_controller.py:1644 +#: erpnext/controllers/accounts_controller.py:1649 msgid "Payment Entry {0} is linked against Order {1}, check if it should be pulled as advance in this invoice." msgstr "تم ربط إدخال الدفعة {0} بالطلب {1}، تحقق مما إذا كان يجب سحبه كدفعة مقدمة في هذه الفاتورة." @@ -36672,7 +36693,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:2777 +#: erpnext/controllers/accounts_controller.py:2782 #: erpnext/selling/doctype/quotation/quotation.json #: erpnext/selling/doctype/sales_order/sales_order.json msgid "Payment Schedule" @@ -36682,7 +36703,7 @@ msgstr "جدول الدفع" msgid "Payment Schedule based Payment Requests cannot be created because a Payment Entry already exists for this document." msgstr "" -#: erpnext/public/js/controllers/transaction.js:532 +#: erpnext/public/js/controllers/transaction.js:534 msgid "Payment Schedules" msgstr "" @@ -36704,7 +36725,7 @@ msgstr "" #: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1217 #: erpnext/accounts/report/gross_profit/gross_profit.py:451 #: erpnext/accounts/workspace/invoicing/invoicing.json -#: erpnext/public/js/controllers/transaction.js:547 +#: erpnext/public/js/controllers/transaction.js:549 #: erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py:30 #: erpnext/workspace_sidebar/accounts_setup.json msgid "Payment Term" @@ -37157,11 +37178,11 @@ msgstr "قيد إقفال الفترة الحالية" msgid "Period Closing Voucher" msgstr "قيد إغلاق الفترة" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:509 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:627 msgid "Period Closing Voucher {0} GL Entry Cancellation Failed" msgstr "قسيمة إغلاق الفترة {0} فشل إلغاء قيد دفتر الأستاذ العام" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:488 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:606 msgid "Period Closing Voucher {0} GL Entry Processing Failed" msgstr "فشل معالجة قيد دفتر الأستاذ العام {0} قسيمة إغلاق الفترة" @@ -37181,7 +37202,7 @@ msgstr "تفاصيل الفترة" msgid "Period End Date" msgstr "تاريخ انتهاء الفترة" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:78 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:81 msgid "Period End Date cannot be greater than Fiscal Year End Date" msgstr "لا يمكن أن يكون تاريخ نهاية الفترة أكبر من تاريخ نهاية السنة المالية" @@ -37223,11 +37244,11 @@ msgstr "إعدادات الفترة" msgid "Period Start Date" msgstr "تاريخ بداية الفترة" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:75 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:78 msgid "Period Start Date cannot be greater than Period End Date" msgstr "لا يمكن أن يكون تاريخ بدء الفترة أكبر من تاريخ انتهاء الفترة" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:72 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:75 msgid "Period Start Date must be {0}" msgstr "يجب أن يكون تاريخ بدء الفترة {0}" @@ -37329,11 +37350,11 @@ msgid "Phantom BOM cannot be created for stock item {0}." msgstr "" #: erpnext/manufacturing/doctype/bom/bom_item_preview.html:16 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:340 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:329 msgid "Phantom Item" msgstr "عنصر شبح" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:449 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:438 msgid "Phantom Item is mandatory" msgstr "العنصر الوهمي إلزامي" @@ -37805,7 +37826,7 @@ msgstr "" msgid "Please add {1} role to user {0}." msgstr "يرجى إضافة الدور {1} إلى المستخدم {0}." -#: erpnext/controllers/stock_controller.py:1911 +#: erpnext/controllers/stock_controller.py:1925 msgid "Please adjust the qty or edit {0} to proceed." msgstr "يرجى تعديل الكمية أو تحرير {0} للمتابعة." @@ -37831,7 +37852,7 @@ msgstr "يرجى إلغاء المعاملة ذات الصلة." msgid "Please capitalize this asset before submitting." msgstr "يرجى كتابة هذا الأصل بأحرف كبيرة قبل الإرسال." -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:978 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:988 msgid "Please check Multi Currency option to allow accounts with other currency" msgstr "يرجى اختيار الخيار عملات متعددة للسماح بحسابات مع عملة أخرى" @@ -37883,7 +37904,7 @@ msgstr "" msgid "Please configure accounts for the Bank Entry rule." msgstr "" -#: erpnext/selling/doctype/customer/customer.py:649 +#: erpnext/selling/doctype/customer/customer.py:650 msgid "Please contact any of the following users to extend the credit limits for {0}: {1}" msgstr "يرجى الاتصال بأي من المستخدمين التاليين لتمديد حدود الائتمان لـ {0}: {1}" @@ -37891,7 +37912,7 @@ msgstr "يرجى الاتصال بأي من المستخدمين التاليي msgid "Please contact any of the following users to {} this transaction." msgstr "" -#: erpnext/selling/doctype/customer/customer.py:642 +#: erpnext/selling/doctype/customer/customer.py:643 msgid "Please contact your administrator to extend the credit limits for {0}." msgstr "يرجى الاتصال بمسؤول النظام لتمديد حدود الائتمان لـ {0}." @@ -37911,7 +37932,7 @@ msgstr "يرجى إنشاء قسائم تكلفة الشحن مقابل الفو msgid "Please create a new Accounting Dimension if required." msgstr "يرجى إنشاء بُعد محاسبي جديد إذا لزم الأمر." -#: erpnext/controllers/accounts_controller.py:832 +#: erpnext/controllers/accounts_controller.py:837 msgid "Please create purchase from internal sale or delivery document itself" msgstr "يرجى إنشاء عملية شراء من مستند البيع أو التسليم الداخلي نفسه" @@ -37959,11 +37980,11 @@ msgstr "يرجى تفعيل {0} في {1}." msgid "Please enable {} in {} to allow same item in multiple rows" msgstr "يرجى تفعيل {} في {} للسماح بظهور العنصر نفسه في صفوف متعددة" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:381 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:391 msgid "Please ensure that the {0} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account." msgstr "يرجى التأكد من أن الحساب {0} هو حساب في الميزانية العمومية. يمكنك تغيير الحساب الرئيسي إلى حساب في الميزانية العمومية أو اختيار حساب مختلف." -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:389 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:399 msgid "Please ensure that the {0} account {1} is a Payable account. You can change the account type to Payable or select a different account." msgstr "يرجى التأكد من أن الحساب {0} {1} هو حساب قابل للدفع. يمكنك تغيير نوع الحساب إلى قابل للدفع أو اختيار حساب آخر." @@ -37975,7 +37996,7 @@ msgstr "يرجى التأكد من أن حساب {} هو حساب في المي msgid "Please ensure {} account {} is a Receivable account." msgstr "يرجى التأكد من أن حساب {} هو حساب مستحق القبض." -#: erpnext/stock/doctype/stock_entry/stock_entry.py:874 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:883 msgid "Please enter Difference Account or set default Stock Adjustment Account for company {0}" msgstr "الرجاء إدخال حساب الفرق أو تعيين حساب تسوية المخزون الافتراضي للشركة {0}" @@ -38013,7 +38034,7 @@ msgstr "الرجاء إدخال حساب النفقات\\n
    \\nPlease enter Ex msgid "Please enter Item Code to get Batch Number" msgstr "الرجاء إدخال رمز العنصر للحصول على رقم الدفعة\\n
    \\nPlease enter Item Code to get Batch Number" -#: erpnext/public/js/controllers/transaction.js:3043 +#: erpnext/public/js/controllers/transaction.js:3048 msgid "Please enter Item Code to get batch no" msgstr "الرجاء إدخال كود البند للحصول على رقم الدفعة" @@ -38041,7 +38062,7 @@ msgstr "الرجاء إدخال إيصال الشراء أولا\\n
    \\nPlease msgid "Please enter Receipt Document" msgstr "الرجاء إدخال مستند الاستلام\\n
    \\nPlease enter Receipt Document" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1042 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1052 msgid "Please enter Reference date" msgstr "الرجاء إدخال تاريخ المرجع\\n
    \\nPlease enter Reference date" @@ -38065,16 +38086,16 @@ msgstr "يرجى إدخال معلومات طرد الشحنة" msgid "Please enter Warehouse and Date" msgstr "الرجاء إدخال المستودع والتاريخ" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:660 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:670 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1335 msgid "Please enter Write Off Account" msgstr "الرجاء إدخال حساب الشطب" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:670 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:680 msgid "Please enter a valid Write Off Account" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:681 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:691 msgid "Please enter a valid Write Off Cost Center" msgstr "" @@ -38094,7 +38115,7 @@ msgstr "يرجى إدخال تاريخ تسليم واحد على الأقل و msgid "Please enter company name first" msgstr "الرجاء إدخال اسم الشركة اولاً" -#: erpnext/controllers/accounts_controller.py:2996 +#: erpnext/controllers/accounts_controller.py:3001 msgid "Please enter default currency in Company Master" msgstr "الرجاء إدخال العملة الافتراضية في شركة الرئيسية" @@ -38369,11 +38390,11 @@ msgstr "الرجاء تحديد تاريخ البدء وتاريخ الانته msgid "Please select Stock Asset Account" msgstr "الرجاء تحديد حساب أصول الأسهم" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2006 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2036 msgid "Please select Subcontracting Order instead of Purchase Order {0}" msgstr "يرجى اختيار أمر التعاقد من الباطن بدلاً من أمر الشراء {0}" -#: erpnext/controllers/accounts_controller.py:2852 +#: erpnext/controllers/accounts_controller.py:2857 msgid "Please select Unrealized Profit / Loss account or add default Unrealized Profit / Loss account account for company {0}" msgstr "يرجى تحديد حساب الأرباح/الخسائر غير المحققة أو إضافة حساب الأرباح/الخسائر غير المحققة الافتراضي للشركة {0}" @@ -38390,7 +38411,7 @@ msgstr "الرجاء اختيار الشركة" #: erpnext/manufacturing/doctype/bom/bom.js:734 #: erpnext/manufacturing/doctype/bom/bom.py:279 #: erpnext/public/js/controllers/accounts.js:274 -#: erpnext/public/js/controllers/transaction.js:3342 +#: erpnext/public/js/controllers/transaction.js:3347 msgid "Please select a Company first." msgstr "الرجاء تحديد شركة أولاً." @@ -38491,6 +38512,10 @@ msgstr "" msgid "Please select a value for {0} quotation_to {1}" msgstr "يرجى اختيار قيمة ل {0} عرض مسعر إلى {1}" +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js:9 +msgid "Please select a warehouse first." +msgstr "" + #: erpnext/assets/doctype/asset_repair/asset_repair.js:203 msgid "Please select an item code before setting the warehouse." msgstr "يرجى تحديد رمز المنتج قبل تحديد المستودع." @@ -38515,7 +38540,7 @@ msgstr "يرجى تحديد صف واحد على الأقل لإصلاحه" msgid "Please select at least one row with difference value" msgstr "يرجى تحديد صف واحد على الأقل بقيمة مختلفة" -#: erpnext/public/js/controllers/transaction.js:584 +#: erpnext/public/js/controllers/transaction.js:586 msgid "Please select at least one schedule." msgstr "" @@ -38527,7 +38552,7 @@ msgstr "" msgid "Please select atleast one operation to create Job Card" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1722 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1732 msgid "Please select correct account" msgstr "يرجى اختيارالحساب الصحيح" @@ -38615,7 +38640,7 @@ msgstr "الرجاء اختيار يوم العطلة الاسبوعي" msgid "Please select {0} first" msgstr "الرجاء تحديد {0} أولا\\n
    \\nPlease select {0} first" -#: erpnext/public/js/controllers/transaction.js:150 +#: erpnext/public/js/controllers/transaction.js:152 msgid "Please set 'Apply Additional Discount On'" msgstr "يرجى تحديد 'تطبيق خصم إضافي على'" @@ -38687,7 +38712,7 @@ msgstr "" msgid "Please set Fixed Asset Account in Asset Category {0}" msgstr "يرجى تعيين حساب الأصول الثابتة في فئة الأصول {0}" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:590 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:600 msgid "Please set Fixed Asset Account in {} against {}." msgstr "" @@ -38733,7 +38758,7 @@ msgstr "يرجى تحديد قائمة العطلات الافتراضية لل msgid "Please set a default Holiday List for Employee {0} or Company {1}" msgstr "يرجى تعيين قائمة العطل الافتراضية للموظف {0} أو الشركة {1}\\n
    \\nPlease set a default Holiday List for Employee {0} or Company {1}" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1146 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1156 msgid "Please set account in Warehouse {0}" msgstr "يرجى تعيين الحساب في مستودع {0}" @@ -38746,7 +38771,7 @@ msgstr "يرجى تحديد الطلب الفعلي أو توقعات المبي msgid "Please set an Address on the Company '%s'" msgstr "" -#: erpnext/controllers/stock_controller.py:1042 +#: erpnext/controllers/stock_controller.py:1056 msgid "Please set an Expense Account in the Items table" msgstr "يرجى تحديد حساب مصروفات في جدول البنود" @@ -38790,11 +38815,11 @@ msgstr "يرجى تعيين حساب المصروفات الافتراضي في msgid "Please set default UOM in Stock Settings" msgstr "يرجى تعيين الافتراضي UOM في إعدادات الأسهم" -#: erpnext/controllers/stock_controller.py:821 +#: erpnext/controllers/stock_controller.py:835 msgid "Please set default cost of goods sold account in company {0} for booking rounding gain and loss during stock transfer" msgstr "يرجى تحديد حساب تكلفة البضائع المباعة الافتراضي في الشركة {0} لتسجيل مكاسب وخسائر التقريب أثناء نقل المخزون" -#: erpnext/controllers/stock_controller.py:272 +#: erpnext/controllers/stock_controller.py:286 msgid "Please set default inventory account for item {0}, or their item group or brand." msgstr "يرجى تعيين حساب المخزون الافتراضي للعنصر {0}، أو مجموعة العناصر أو العلامة التجارية الخاصة به." @@ -38807,7 +38832,7 @@ msgstr "يرجى تعيين {0} الافتراضي للشركة {1}" msgid "Please set filter based on Item or Warehouse" msgstr "يرجى ضبط الفلتر على أساس البند أو المخزن" -#: erpnext/controllers/accounts_controller.py:2411 +#: erpnext/controllers/accounts_controller.py:2416 msgid "Please set one of the following:" msgstr "يرجى تحديد أحد الخيارات التالية:" @@ -38815,7 +38840,7 @@ msgstr "يرجى تحديد أحد الخيارات التالية:" msgid "Please set opening number of booked depreciations" msgstr "يرجى تحديد عدد الإهلاكات المحجوزة في بداية الفترة" -#: erpnext/public/js/controllers/transaction.js:2707 +#: erpnext/public/js/controllers/transaction.js:2712 msgid "Please set recurring after saving" msgstr "يرجى تحديد (تكرار) بعد الحفظ" @@ -38871,7 +38896,7 @@ msgid "Please set {0} in BOM Creator {1}" msgstr "يرجى ضبط {0} في مُنشئ قائمة المواد {1}" #: erpnext/controllers/buying_controller.py:345 -#: erpnext/controllers/stock_controller.py:912 +#: erpnext/controllers/stock_controller.py:926 msgid "Please set {0} in Company {1} or in the Item Defaults of Item {2}" msgstr "" @@ -38879,7 +38904,7 @@ msgstr "" msgid "Please set {0} in Company {1} to account for Exchange Gain / Loss" msgstr "يرجى تعيين {0} في الشركة {1} لحساب مكاسب/خسائر الصرف" -#: erpnext/controllers/accounts_controller.py:613 +#: erpnext/controllers/accounts_controller.py:618 msgid "Please set {0} to {1}, the same account that was used in the original invoice {2}." msgstr "يرجى تعيين {0} إلى {1}، وهو نفس الحساب الذي تم استخدامه في الفاتورة الأصلية {2}." @@ -38896,12 +38921,12 @@ msgid "Please specify Company" msgstr "يرجى تحديد شركة" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.js:120 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:430 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:428 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:636 msgid "Please specify Company to proceed" msgstr "الرجاء تحديد الشركة للمضى قدما\\n
    \\nPlease specify Company to proceed" -#: erpnext/controllers/accounts_controller.py:3227 +#: erpnext/controllers/accounts_controller.py:3232 #: erpnext/public/js/controllers/accounts.js:114 msgid "Please specify a valid Row ID for row {0} in table {1}" msgstr "يرجى تحديد هوية الصف صالحة لصف {0} في الجدول {1}" @@ -39141,7 +39166,7 @@ msgstr "" #: erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py:86 #: erpnext/stock/report/serial_no_ledger/serial_no_ledger.py:25 #: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:155 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:155 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:164 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:36 #: erpnext/templates/form_grid/bank_reconciliation_grid.html:6 msgid "Posting Date" @@ -39158,7 +39183,7 @@ msgstr "" msgid "Posting Date inheritance for exchange gain / loss" msgstr "" -#: erpnext/public/js/controllers/transaction.js:1137 +#: erpnext/public/js/controllers/transaction.js:1139 msgid "Posting Date will change to today's date as Edit Posting Date and Time is unchecked. Are you sure want to proceed?" msgstr "سيتم تغيير تاريخ النشر إلى تاريخ اليوم لأن خيار \"تعديل تاريخ ووقت النشر\" غير مُفعّل. هل أنت متأكد من رغبتك في المتابعة؟" @@ -39215,13 +39240,13 @@ msgstr "تاريخ ووقت النشر" #: erpnext/stock/report/serial_no_ledger/serial_no_ledger.js:63 #: erpnext/stock/report/serial_no_ledger/serial_no_ledger.py:26 #: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:156 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:160 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:169 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:41 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json msgid "Posting Time" msgstr "نشر التوقيت" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2891 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2923 msgid "Posting date and posting time is mandatory" msgstr "" @@ -39429,7 +39454,7 @@ msgstr "" msgid "Previous Work Experience" msgstr "خبرة العمل السابق" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:109 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:112 msgid "Previous Year is not closed, please close it first" msgstr "لم يتم إغلاق ملف السنة السابقة، يرجى إغلاقه أولاً." @@ -40552,7 +40577,7 @@ msgstr "الربحية" msgid "Profitability Analysis" msgstr "تحليل الربحية" -#: erpnext/projects/doctype/task/task.py:156 +#: erpnext/projects/doctype/task/task.py:157 #, python-format msgid "Progress % for a task cannot be more than 100." msgstr "لا يمكن أن تتجاوز نسبة التقدم في مهمة ما 100%." @@ -41130,11 +41155,19 @@ msgstr "" msgid "Purchase Invoice Trends" msgstr "اتجهات فاتورة الشراء" +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:331 +msgid "Purchase Invoice can be held after submitting." +msgstr "" + #: erpnext/assets/doctype/asset/asset.py:340 msgid "Purchase Invoice cannot be made against an existing asset {0}" msgstr "لا يمكن إجراء فاتورة الشراء مقابل أصل موجود {0}" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1999 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1943 +msgid "Purchase Invoice without any outstanding amount cannot be held." +msgstr "" + +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:2033 msgid "Purchase Invoices" msgstr "فواتير الشراء" @@ -41263,11 +41296,11 @@ msgstr "لم يتم استلام طلبات الشراء في الوقت الم msgid "Purchase Order Pricing Rule" msgstr "قاعدة تسعير أمر الشراء" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:630 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:640 msgid "Purchase Order Required" msgstr "أمر الشراء مطلوب" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:625 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:635 msgid "Purchase Order Required for item {}" msgstr "" @@ -41293,7 +41326,7 @@ msgstr "عدد طلب الشراء مطلوب للبند\\n
    \\nPurchase Order msgid "Purchase Order {0} created" msgstr "تم إنشاء أمر الشراء {0}" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:690 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:700 msgid "Purchase Order {0} is not submitted" msgstr "طلب الشراء {0} يجب أن يعتمد\\n
    \\nPurchase Order {0} is not submitted" @@ -41327,7 +41360,7 @@ msgstr "أوامر الشراء إلى الفاتورة" msgid "Purchase Orders to Receive" msgstr "أوامر الشراء لتلقي" -#: erpnext/controllers/accounts_controller.py:2043 +#: erpnext/controllers/accounts_controller.py:2048 msgid "Purchase Orders {0} are un-linked" msgstr "أوامر الشراء {0} غير مرتبطة" @@ -41352,8 +41385,8 @@ msgstr "قائمة أسعار الشراء" #. Label of a Workspace Sidebar Item #: erpnext/accounts/doctype/accounts_settings/accounts_settings.js:62 #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:181 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:647 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:657 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:645 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:655 #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js:49 #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json #: erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py:244 @@ -41413,11 +41446,11 @@ msgstr "شراء السلعة استلام الموردة" msgid "Purchase Receipt No" msgstr "لا شراء استلام" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:652 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:662 msgid "Purchase Receipt Required" msgstr "إيصال استلام المشتريات مطلوب" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:647 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:657 msgid "Purchase Receipt Required for item {}" msgstr "" @@ -41445,7 +41478,7 @@ msgstr "" msgid "Purchase Receipt {0} created." msgstr "تم إنشاء إيصال الشراء {0} ." -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:697 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:707 msgid "Purchase Receipt {0} is not submitted" msgstr "إيصال استلام المشتريات {0} لم يتم تقديمه" @@ -41571,7 +41604,7 @@ msgstr "المشتريات" msgid "Purpose" msgstr "غرض" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:691 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:700 msgid "Purpose must be one of {0}" msgstr "" @@ -41671,12 +41704,12 @@ msgstr "" #: erpnext/manufacturing/doctype/work_order_item/work_order_item.json #: erpnext/manufacturing/doctype/workstation/workstation_job_card.html:28 #: erpnext/manufacturing/report/bom_explorer/bom_explorer.py:69 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:254 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:352 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:417 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:517 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:243 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:341 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:406 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:506 #: erpnext/public/js/stock_reservation.js:134 -#: erpnext/public/js/stock_reservation.js:336 erpnext/public/js/utils.js:887 +#: erpnext/public/js/stock_reservation.js:336 erpnext/public/js/utils.js:890 #: erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json #: erpnext/selling/doctype/product_bundle_item/product_bundle_item.json #: erpnext/selling/doctype/sales_order/sales_order.js:398 @@ -41764,7 +41797,7 @@ msgstr "الكمية بعد إتمام العملية" #: erpnext/stock/doctype/stock_closing_balance/stock_closing_balance.json #: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:169 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:199 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:208 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:91 msgid "Qty Change" msgstr "تغيير الكمية" @@ -42078,7 +42111,7 @@ msgstr "فحص الجودة" msgid "Quality Inspection Analysis" msgstr "تحليل فحص الجودة" -#: erpnext/public/js/controllers/transaction.js:2964 +#: erpnext/public/js/controllers/transaction.js:2969 msgid "Quality Inspection Not Configured" msgstr "" @@ -42157,7 +42190,7 @@ msgstr "" msgid "Quality Inspection {0} is rejected for the item: {1}" msgstr "" -#: erpnext/public/js/controllers/transaction.js:431 +#: erpnext/public/js/controllers/transaction.js:433 #: erpnext/stock/doctype/stock_entry/stock_entry.js:212 msgid "Quality Inspection(s)" msgstr "فحص الجودة" @@ -42753,7 +42786,7 @@ msgstr "التي أثارها (بريد إلكتروني)" #: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json #: erpnext/manufacturing/doctype/work_order_additional_item/work_order_additional_item.json #: erpnext/manufacturing/doctype/work_order_item/work_order_item.json -#: erpnext/public/js/utils.js:897 +#: erpnext/public/js/utils.js:900 #: erpnext/selling/doctype/product_bundle_item/product_bundle_item.json #: erpnext/selling/doctype/quotation_item/quotation_item.json #: erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -42936,7 +42969,7 @@ msgstr "المعدل الذي يتم تحويل العملة إلى عملة ا msgid "Rate at which this tax is applied" msgstr "السعر الذي يتم فيه تطبيق هذه الضريبة" -#: erpnext/controllers/accounts_controller.py:4132 +#: erpnext/controllers/accounts_controller.py:4162 msgid "Rate of '{}' items cannot be changed" msgstr "" @@ -43080,7 +43113,7 @@ msgstr "مستودع المواد الخام" #: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/manufacturing/doctype/production_plan/production_plan.json #: erpnext/manufacturing/doctype/workstation/workstation.js:462 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:398 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:387 msgid "Raw Materials" msgstr "مواد أولية" @@ -43105,7 +43138,7 @@ msgstr "المواد الخام المستهلكة" msgid "Raw Materials Consumption" msgstr "استهلاك المواد الخام" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:431 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:440 msgid "Raw Materials Missing" msgstr "" @@ -43252,7 +43285,7 @@ msgid "Real Estate" msgstr "العقارات" #. Label of the hold_comment (Small Text) field in DocType 'Purchase Invoice' -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:285 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:283 #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json msgid "Reason For Putting On Hold" msgstr "سبب لوضع في الانتظار" @@ -43807,11 +43840,11 @@ msgstr "" msgid "Reference #" msgstr "مرجع #" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1040 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1050 msgid "Reference #{0} dated {1}" msgstr "المرجع # {0} بتاريخ {1}" -#: erpnext/public/js/controllers/transaction.js:2820 +#: erpnext/public/js/controllers/transaction.js:2825 msgid "Reference Date for Early Payment Discount" msgstr "تاريخ مرجعي لخصم الدفع المبكر" @@ -44091,15 +44124,15 @@ msgstr "علاقة" #. Label of the release_date (Date) field in DocType 'Purchase Invoice' #. Label of the release_date (Date) field in DocType 'Supplier' -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:277 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:321 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:275 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:320 #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/buying/doctype/supplier/supplier.json #: erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.py:1079 msgid "Release Date" msgstr "تاريخ النشر" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:325 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:335 msgid "Release date must be in the future" msgstr "يجب أن يكون تاريخ الإصدار في المستقبل" @@ -44548,7 +44581,7 @@ msgid "Reposting cannot be started when status is {0}." msgstr "" #: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:227 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:338 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:347 msgid "Reposting entries created: {0}" msgstr "إعادة نشر المشاركات التي تم إنشاؤها: {0}" @@ -44613,7 +44646,7 @@ msgstr "تاريخ الاستحقاق" msgid "Reqd Qty (BOM)" msgstr "الكمية المطلوبة (قائمة المواد)" -#: erpnext/public/js/utils.js:913 +#: erpnext/public/js/utils.js:916 msgid "Reqd by date" msgstr "مطلوب بالتاريخ" @@ -44930,7 +44963,7 @@ msgstr "مخصص للتجميع الفرعي" msgid "Reserved" msgstr "محجوز" -#: erpnext/controllers/stock_controller.py:1491 +#: erpnext/controllers/stock_controller.py:1505 msgid "Reserved Batch Conflict" msgstr "تعارض الدُفعات المحجوزة" @@ -45004,7 +45037,7 @@ msgstr "الكمية المحجوزة" msgid "Reserved Quantity for Production" msgstr "الكمية المحجوزة للإنتاج" -#: erpnext/stock/stock_ledger.py:2340 +#: erpnext/stock/stock_ledger.py:2383 msgid "Reserved Serial No." msgstr "رقم تسلسلي محجوز" @@ -45022,13 +45055,13 @@ msgstr "رقم تسلسلي محجوز" #: erpnext/stock/report/reserved_stock/reserved_stock.json #: erpnext/stock/report/stock_balance/stock_balance.py:569 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:205 -#: erpnext/stock/stock_ledger.py:2324 +#: erpnext/stock/stock_ledger.py:2367 #: 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:2412 msgid "Reserved Stock for Batch" msgstr "المخزون المحجوز للدفعة" @@ -45408,6 +45441,10 @@ msgstr "مكونات الإرجاع" msgid "Return Issued" msgstr "تم إصدار الإرجاع" +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:328 +msgid "Return Purchase Invoice cannot be held." +msgstr "" + #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:329 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js:127 msgid "Return Qty" @@ -45944,8 +45981,8 @@ msgstr "مخصص خسائر التقريب" msgid "Rounding Loss Allowance should be between 0 and 1" msgstr "يجب أن يكون بدل خسائر التقريب بين 0 و 1" -#: erpnext/controllers/stock_controller.py:833 -#: erpnext/controllers/stock_controller.py:848 +#: erpnext/controllers/stock_controller.py:847 +#: erpnext/controllers/stock_controller.py:862 msgid "Rounding gain/loss Entry for Stock Transfer" msgstr "قيد تقريب الربح/الخسارة لنقل الأسهم" @@ -45968,7 +46005,7 @@ msgstr "التوجيه" msgid "Routing Name" msgstr "اسم التوجيه" -#: erpnext/controllers/sales_and_purchase_return.py:225 +#: erpnext/controllers/sales_and_purchase_return.py:243 msgid "Row # {0}: Cannot return more than {1} for Item {2}" msgstr "الصف # {0}: لا يمكن الارجاع أكثر من {1} للبند {2}" @@ -46006,11 +46043,11 @@ msgstr "الصف رقم {0} (جدول الدفع): يجب أن يكون المب msgid "Row #{0}: A reorder entry already exists for warehouse {1} with reorder type {2}." msgstr "الصف #{0}: يوجد بالفعل إدخال إعادة طلب للمستودع {1} بنوع إعادة الطلب {2}." -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:333 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:378 msgid "Row #{0}: Acceptance Criteria Formula is incorrect." msgstr "الصف #{0}: صيغة معايير القبول غير صحيحة." -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:313 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:358 msgid "Row #{0}: Acceptance Criteria Formula is required." msgstr "الصف #{0}: صيغة معايير القبول مطلوبة." @@ -46023,7 +46060,7 @@ msgstr "الصف #{0}: لا يمكن أن يكون المستودع المقبو msgid "Row #{0}: Accepted Warehouse is mandatory for the accepted Item {1}" msgstr "الصف #{0}: المستودع المقبول إلزامي للصنف المقبول {1}" -#: erpnext/controllers/accounts_controller.py:1321 +#: erpnext/controllers/accounts_controller.py:1326 msgid "Row #{0}: Account {1} does not belong to company {2}" msgstr "الصف # {0}: الحساب {1} لا ينتمي إلى الشركة {2}" @@ -46088,27 +46125,27 @@ msgstr "الصف #{0}: لا يمكن إلغاء إدخال المخزون هذا msgid "Row #{0}: Cannot create entry with different taxable AND withholding document links." msgstr "الصف #{0}: لا يمكن إنشاء إدخال بروابط مستندات مختلفة للضرائب والحجز." -#: erpnext/controllers/accounts_controller.py:3834 +#: erpnext/controllers/accounts_controller.py:3864 msgid "Row #{0}: Cannot delete item {1} which has already been billed." msgstr "الصف # {0}: لا يمكن حذف العنصر {1} الذي تم تحرير فاتورة به بالفعل." -#: erpnext/controllers/accounts_controller.py:3808 +#: erpnext/controllers/accounts_controller.py:3838 msgid "Row #{0}: Cannot delete item {1} which has already been delivered" msgstr "الصف # {0}: لا يمكن حذف العنصر {1} الذي تم تسليمه بالفعل" -#: erpnext/controllers/accounts_controller.py:3827 +#: erpnext/controllers/accounts_controller.py:3857 msgid "Row #{0}: Cannot delete item {1} which has already been received" msgstr "الصف # {0}: لا يمكن حذف العنصر {1} الذي تم استلامه بالفعل" -#: erpnext/controllers/accounts_controller.py:3814 +#: erpnext/controllers/accounts_controller.py:3844 msgid "Row #{0}: Cannot delete item {1} which has work order assigned to it." msgstr "الصف # {0}: لا يمكن حذف العنصر {1} الذي تم تعيين ترتيب العمل إليه." -#: erpnext/controllers/accounts_controller.py:3820 +#: erpnext/controllers/accounts_controller.py:3850 msgid "Row #{0}: Cannot delete item {1} which is already ordered against this Sales Order." msgstr "الصف #{0}: لا يمكن حذف العنصر {1} الذي تم طلبه بالفعل مقابل أمر البيع هذا." -#: erpnext/controllers/accounts_controller.py:4142 +#: erpnext/controllers/accounts_controller.py:4172 msgid "Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}." msgstr "الصف #{0}: لا يمكن تحديد السعر إذا كان المبلغ المطلوب دفعه أكبر من المبلغ الخاص بالعنصر {1}." @@ -46116,7 +46153,7 @@ msgstr "الصف #{0}: لا يمكن تحديد السعر إذا كان الم msgid "Row #{0}: Cannot transfer more than Required Qty {1} for Item {2} against Job Card {3}" msgstr "الصف #{0}: لا يمكن نقل أكثر من الكمية المطلوبة {1} للعنصر {2} مقابل بطاقة العمل {3}" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1329 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1338 msgid "Row #{0}: Cannot transfer {1} {2} of Item {3}. Maximum transferable quantity is {4} {2}." msgstr "" @@ -46211,7 +46248,7 @@ msgstr "الصف # {0}: إدخال مكرر في المراجع {1} {2}" msgid "Row #{0}: Expected Delivery Date cannot be before Purchase Order Date" msgstr "الصف # {0}: تاريخ التسليم المتوقع لا يمكن أن يكون قبل تاريخ أمر الشراء" -#: erpnext/controllers/stock_controller.py:1044 +#: erpnext/controllers/stock_controller.py:1058 msgid "Row #{0}: Expense Account not set for the Item {1}. {2}" msgstr "الصف #{0}: لم يتم تعيين حساب المصروفات للعنصر {1}. {2}" @@ -46238,7 +46275,7 @@ msgstr "" msgid "Row #{0}: Finished Good Item {1} must be a sub-contracted item" msgstr "الصف #{0}: يجب أن يكون المنتج النهائي {1} منتجًا تم التعاقد عليه من الباطن" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:647 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:656 msgid "Row #{0}: Finished Good must be {1}" msgstr "الصف #{0}: يجب أن يكون المنتج النهائي {1}" @@ -46275,7 +46312,7 @@ msgstr "الصف #{0}: حقلا \"من وقت\" و\"إلى وقت\" مطلوبا msgid "Row #{0}: Item added" msgstr "الصف # {0}: تمت إضافة العنصر" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1937 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1967 msgid "Row #{0}: Item {1} cannot be transferred more than {2} against {3} {4}" msgstr "الصف #{0}: لا يمكن نقل العنصر {1} إلى أكثر من {2} مقابل {3} {4}" @@ -46291,7 +46328,7 @@ msgstr "الصف #{0}: تم اختيار العنصر {1} ، يرجى حجز ا msgid "Row #{0}: Item {1} has no stock in warehouse {2}." msgstr "الصف #{0}: العنصر {1} ليس لديه مخزون في المستودع {2}." -#: erpnext/controllers/stock_controller.py:189 +#: erpnext/controllers/stock_controller.py:203 msgid "Row #{0}: Item {1} has zero rate but '{2}' is not enabled." msgstr "" @@ -46320,7 +46357,7 @@ msgstr "الصف #{0}: العنصر {1} ليس عنصر خدمة" msgid "Row #{0}: Item {1} is not a stock item" msgstr "الصف #{0}: العنصر {1} ليس عنصرًا متوفرًا في المخزون" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1095 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1104 msgid "Row #{0}: Item {1} is not part of the source manufacture entry and cannot be added to this disassembly." msgstr "" @@ -46332,7 +46369,7 @@ msgstr "" msgid "Row #{0}: Item {1} mismatch. Changing of item code is not permitted." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1104 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1113 msgid "Row #{0}: Item {1} quantity ({2} in stock UOM) does not match the quantity derived from the source ({3}). Do not change the UOM, conversion factor or quantity of disassembly rows." msgstr "" @@ -46360,7 +46397,7 @@ msgstr "الصف #{0}: الصف {1} فقط متاح للحجز للعنصر {2}" msgid "Row #{0}: Opening Accumulated Depreciation must be less than or equal to {1}" msgstr "الصف #{0}: يجب أن يكون الاستهلاك المتراكم الافتتاحي أقل من أو يساوي {1}" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1159 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1168 msgid "Row #{0}: Operation {1} is not completed for {2} qty of finished goods in Work Order {3}. Please update operation status via Job Card {4}." msgstr "" @@ -46389,7 +46426,7 @@ msgstr "الصف #{0}: الرجاء تحديد مستودع التجميع ال msgid "Row #{0}: Please set reorder quantity" msgstr "الصف # {0}: يرجى تعيين إعادة ترتيب الكمية\\n
    \\nRow #{0}: Please set reorder quantity" -#: erpnext/controllers/accounts_controller.py:636 +#: erpnext/controllers/accounts_controller.py:641 msgid "Row #{0}: Please update deferred revenue/expense account in item row or default account in company master" msgstr "الصف #{0}: يرجى تحديث حساب الإيرادات/المصروفات المؤجلة في صف البند أو الحساب الافتراضي في بيانات الشركة الرئيسية" @@ -46411,15 +46448,15 @@ msgstr "الصف #{0}: يجب أن تكون الكمية عددًا موجبًا msgid "Row #{0}: Qty should be less than or equal to Available Qty to Reserve (Actual Qty - Reserved Qty) {1} for Iem {2} against Batch {3} in Warehouse {4}." msgstr "" -#: erpnext/controllers/stock_controller.py:1629 +#: erpnext/controllers/stock_controller.py:1643 msgid "Row #{0}: Quality Inspection is required for Item {1}" msgstr "الصف #{0}: يلزم فحص الجودة للعنصر {1}" -#: erpnext/controllers/stock_controller.py:1644 +#: erpnext/controllers/stock_controller.py:1658 msgid "Row #{0}: Quality Inspection {1} is not submitted for the item: {2}" msgstr "الصف #{0}: لم يتم تقديم فحص الجودة {1} للعنصر: {2}" -#: erpnext/controllers/stock_controller.py:1659 +#: erpnext/controllers/stock_controller.py:1673 msgid "Row #{0}: Quality Inspection {1} was rejected for item {2}" msgstr "الصف #{0}: تم رفض فحص الجودة {1} للعنصر {2}" @@ -46427,10 +46464,14 @@ msgstr "الصف #{0}: تم رفض فحص الجودة {1} للعنصر {2}" msgid "Row #{0}: Quantity cannot be a non-positive number. Please increase the quantity or remove the Item {1}" msgstr "الصف #{0}: لا يمكن أن تكون الكمية عددًا غير موجب. يُرجى زيادة الكمية أو إزالة العنصر {1}" -#: erpnext/controllers/accounts_controller.py:1484 +#: erpnext/controllers/accounts_controller.py:1489 msgid "Row #{0}: Quantity for Item {1} cannot be zero." msgstr "الصف # {0}: كمية البند {1} لا يمكن أن يكون صفرا" +#: erpnext/crm/doctype/opportunity/opportunity.py:152 +msgid "Row #{0}: Quantity must be greater than 0 for Item {1}" +msgstr "" + #: erpnext/controllers/subcontracting_inward_controller.py:537 msgid "Row #{0}: Quantity of Item {1} cannot be more than {2} {3} against Subcontracting Inward Order {4}" msgstr "الصف #{0}: لا يمكن أن تتجاوز كمية الصنف {1} الكمية {2} {3} في طلب الشراء الداخلي للتعاقد من الباطن {4}" @@ -46439,13 +46480,17 @@ msgstr "الصف #{0}: لا يمكن أن تتجاوز كمية الصنف {1} msgid "Row #{0}: Quantity to reserve for the Item {1} should be greater than 0." msgstr "الصف #{0}: يجب أن تكون الكمية المراد حجزها للعنصر {1} أكبر من 0." -#: erpnext/controllers/accounts_controller.py:899 -#: erpnext/controllers/accounts_controller.py:911 +#: erpnext/controllers/accounts_controller.py:904 +#: erpnext/controllers/accounts_controller.py:916 #: erpnext/utilities/transaction_base.py:172 #: erpnext/utilities/transaction_base.py:178 msgid "Row #{0}: Rate must be same as {1}: {2} ({3} / {4})" msgstr "الصف #{0}: يجب أن يكون المعدل هو نفسه {1}: {2} ({3} / {4})" +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:316 +msgid "Row #{0}: Reading {1} {2} is not a valid number in the {3} number format. Use {4} as the decimal separator." +msgstr "" + #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1251 msgid "Row #{0}: Reference Document Type must be one of Purchase Order, Purchase Invoice or Journal Entry" msgstr "الصف {0} : نوع المستند المرجع يجب أن يكون واحدة من طلب شراء ,فاتورة شراء أو قيد يومبة\\n
    \\nRow #{0}: Reference Document Type must be one of Purchase Order, Purchase Invoice or Journal Entry" @@ -46496,7 +46541,7 @@ msgstr "الصف #{0}: معدل البيع للصنف {1} أقل من {2} الخ msgid "Row #{0}: Sequence ID must be {1} or {2} for Operation {3}." msgstr "الصف #{0}: يجب أن يكون معرف التسلسل {1} أو {2} للعملية {3}." -#: erpnext/controllers/stock_controller.py:344 +#: erpnext/controllers/stock_controller.py:358 msgid "Row #{0}: Serial No {1} does not belong to Batch {2}" msgstr "الصف # {0}: الرقم التسلسلي {1} لا ينتمي إلى الدُفعة {2}" @@ -46512,15 +46557,15 @@ msgstr "الصف #{0}: تم تحديد الرقم التسلسلي {1} بالف msgid "Row #{0}: Serial No(s) {1} are not a part of the linked Subcontracting Inward Order. Please select valid Serial No(s)." msgstr "الصف #{0}: الأرقام التسلسلية {1} ليست جزءًا من أمر التوريد الداخلي المرتبط بالتعاقد من الباطن. يرجى تحديد رقم تسلسلي صحيح." -#: erpnext/controllers/accounts_controller.py:664 +#: erpnext/controllers/accounts_controller.py:669 msgid "Row #{0}: Service End Date cannot be before Invoice Posting Date" msgstr "الصف # {0}: لا يمكن أن يكون تاريخ انتهاء الخدمة قبل تاريخ ترحيل الفاتورة" -#: erpnext/controllers/accounts_controller.py:658 +#: erpnext/controllers/accounts_controller.py:663 msgid "Row #{0}: Service Start Date cannot be greater than Service End Date" msgstr "الصف # {0}: لا يمكن أن يكون تاريخ بدء الخدمة أكبر من تاريخ انتهاء الخدمة" -#: erpnext/controllers/accounts_controller.py:652 +#: erpnext/controllers/accounts_controller.py:657 msgid "Row #{0}: Service Start and End Date is required for deferred accounting" msgstr "الصف # {0}: مطلوب بداية وتاريخ انتهاء الخدمة للمحاسبة المؤجلة" @@ -46544,11 +46589,11 @@ msgstr "الصف #{0}: لا يمكن أن يكون مستودع المصدر {1} msgid "Row #{0}: Source Warehouse {1} for item {2} must be same as Source Warehouse {3} in the Work Order." msgstr "الصف #{0}: يجب أن يكون مستودع المصدر {1} للعنصر {2} هو نفسه مستودع المصدر {3} في أمر العمل." -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1363 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1372 msgid "Row #{0}: Source and Target Warehouse cannot be the same for Material Transfer" msgstr "الصف #{0}: لا يمكن أن يكون مستودع المصدر ومستودع الهدف متطابقين لنقل المواد" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1385 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1394 msgid "Row #{0}: Source, Target Warehouse and Inventory Dimensions cannot be the exact same for Material Transfer" msgstr "الصف #{0}: لا يمكن أن تكون أبعاد المستودع المصدر والمستودع الهدف والمخزون متطابقة تمامًا في عملية نقل المواد." @@ -46556,7 +46601,7 @@ msgstr "الصف #{0}: لا يمكن أن تكون أبعاد المستودع msgid "Row #{0}: Start Time must be before End Time" msgstr "الصف #{0}: يجب أن يكون وقت البدء قبل وقت الانتهاء" -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:213 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:217 msgid "Row #{0}: Status is mandatory" msgstr "الصف #{0}: الحالة إلزامية" @@ -46601,7 +46646,7 @@ msgstr "الصف #{0}: كمية المخزون {1} ({2}) للصنف {3} لا ي msgid "Row #{0}: Target Warehouse must be same as Customer Warehouse {1} from the linked Subcontracting Inward Order" msgstr "الصف #{0}: يجب أن يكون المستودع المستهدف هو نفسه مستودع العميل {1} من أمر الشراء الداخلي المرتبط بالتعاقد من الباطن" -#: erpnext/controllers/stock_controller.py:357 +#: erpnext/controllers/stock_controller.py:371 msgid "Row #{0}: The batch {1} has already expired." msgstr "الصف رقم {0}: انتهت صلاحية الدفعة {1} بالفعل." @@ -46621,7 +46666,7 @@ msgstr "الصف #{0}: لا يمكن أن يكون إجمالي عدد الإه msgid "Row #{0}: Total Number of Depreciations must be greater than zero" msgstr "الصف #{0}: يجب أن يكون إجمالي عدد الاستهلاكات أكبر من الصفر" -#: erpnext/controllers/stock_controller.py:141 +#: erpnext/controllers/stock_controller.py:155 msgid "Row #{0}: Warehouse {1} does not match with the warehouse {2} in Serial and Batch Bundle {3}." msgstr "" @@ -46649,11 +46694,11 @@ msgstr "" msgid "Row #{0}: {1} can not be negative for item {2}" msgstr "الصف # {0}: {1} لا يمكن أن يكون سالبا للبند {2}" -#: erpnext/controllers/stock_controller.py:1308 +#: erpnext/controllers/stock_controller.py:1322 msgid "Row #{0}: {1} is mandatory for the Inventory Dimension {2}." msgstr "" -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:326 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:371 msgid "Row #{0}: {1} is not a valid reading field. Please refer to the field description." msgstr "الصف #{0}: {1} ليس حقل قراءة صالحًا. يُرجى مراجعة وصف الحقل." @@ -46665,7 +46710,7 @@ msgstr "الصف رقم {0}: {1} مطلوب لإنشاء فواتير الافت msgid "Row #{0}: {1} of {2} should be {3}. Please update the {1} or select a different account." msgstr "الصف #{0}: {1} من {2} يجب أن يكون {3}. يرجى تحديث {1} أو اختيار حساب آخر." -#: erpnext/controllers/accounts_controller.py:3949 +#: erpnext/controllers/accounts_controller.py:3979 msgid "Row #{0}:Quantity for Item {1} cannot be zero." msgstr "" @@ -46766,11 +46811,11 @@ msgstr "" msgid "Row #{}: {} {} does not exist." msgstr "" -#: erpnext/stock/doctype/item/item.py:1527 +#: erpnext/stock/doctype/item/item.py:1537 msgid "Row #{}: {} {} doesn't belong to Company {}. Please select valid {}." msgstr "الصف رقم {}: {} {} لا ينتمي إلى الشركة {}. يرجى اختيار {} صحيح." -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:440 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:450 msgid "Row No {0}: Warehouse is required. Please set a Default Warehouse for Item {1} and Company {2}" msgstr "رقم الصف {0}: مطلوب تحديد مستودع. يُرجى تحديد مستودع افتراضي للصنف {1} والشركة {2}" @@ -46782,7 +46827,7 @@ msgstr "الصف {0}: العملية مطلوبة مقابل عنصر الماد msgid "Row {0} picked quantity is less than the required quantity, additional {1} {2} required." msgstr "الكمية المختارة من الصف {0} أقل من الكمية المطلوبة، يلزم كمية إضافية {1} {2} ." -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1961 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1991 msgid "Row {0}# Item {1} not found in 'Raw Materials Supplied' table in {2} {3}" msgstr "الصف {0}# العنصر {1} غير موجود في جدول \"المواد الخام الموردة\" في {2} {3}" @@ -46814,7 +46859,7 @@ msgstr "الصف {0}: يجب أن يكون المبلغ المخصص {1} أقل msgid "Row {0}: Allocated amount {1} must be less than or equal to remaining payment amount {2}" msgstr "الصف {0}: يجب أن يكون المبلغ المخصص {1} أقل من أو يساوي مبلغ الدفعة المتبقية {2}" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1620 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1663 msgid "Row {0}: As {1} is enabled, raw materials cannot be added to {2} entry. Use {3} entry to consume raw materials." msgstr "الصف {0}: بما أن {1} مُفعّل، فلا يمكن إضافة المواد الخام إلى المدخل {2} . استخدم المدخل {3} لاستهلاك المواد الخام." @@ -46822,7 +46867,7 @@ msgstr "الصف {0}: بما أن {1} مُفعّل، فلا يمكن إضافة msgid "Row {0}: Bill of Materials not found for the Item {1}" msgstr "صف {0}: من مواد مشروع القانون لم يتم العثور على هذا البند {1}" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:936 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:946 msgid "Row {0}: Both Debit and Credit values cannot be zero" msgstr "الصف {0}: لا يمكن أن تكون قيمتا المدين والدائن صفرًا" @@ -46834,7 +46879,7 @@ msgstr "" msgid "Row {0}: Conversion Factor is mandatory" msgstr "الصف {0}: معامل التحويل إلزامي" -#: erpnext/controllers/accounts_controller.py:3265 +#: erpnext/controllers/accounts_controller.py:3270 msgid "Row {0}: Cost Center {1} does not belong to Company {2}" msgstr "الصف {0}: مركز التكلفة {1} لا ينتمي إلى الشركة {2}" @@ -46862,7 +46907,7 @@ msgstr "الصف {0}: لا يمكن أن يكون مستودع التسليم ({ msgid "Row {0}: Delivery Warehouse cannot be same as Customer Warehouse for Item {1}." msgstr "الصف {0}: لا يمكن أن يكون مستودع التسليم هو نفسه مستودع العميل بالنسبة للعنصر {1}." -#: erpnext/controllers/accounts_controller.py:2765 +#: erpnext/controllers/accounts_controller.py:2770 msgid "Row {0}: Due Date in the Payment Terms table cannot be before Posting Date" msgstr "الصف {0}: لا يمكن أن يكون تاريخ الاستحقاق في جدول شروط الدفع قبل تاريخ الترحيل" @@ -46870,7 +46915,7 @@ msgstr "الصف {0}: لا يمكن أن يكون تاريخ الاستحقاق msgid "Row {0}: Either Delivery Note Item or Packed Item reference is mandatory." msgstr "الصف {0}: يجب أن يكون مرجع عنصر إشعار التسليم أو العنصر المعبأ إلزاميًا." -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1027 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1037 #: erpnext/controllers/taxes_and_totals.py:1382 msgid "Row {0}: Exchange Rate is mandatory" msgstr "الصف {0}: سعر صرف إلزامي" @@ -46887,15 +46932,15 @@ msgstr "الصف {0}: يجب أن تكون القيمة المتوقعة بعد msgid "Row {0}: Expense Account {1} is linked to company {2}. Please select an account belonging to company {3}." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:530 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:540 msgid "Row {0}: Expense Head changed to {1} as no Purchase Receipt is created against Item {2}." msgstr "الصف {0}: تم تغيير رأس المصروفات إلى {1} حيث لم يتم إنشاء إيصال شراء مقابل العنصر {2}." -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:487 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:497 msgid "Row {0}: Expense Head changed to {1} because account {2} is not linked to warehouse {3} or it is not the default inventory account" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:512 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:522 msgid "Row {0}: Expense Head changed to {1} because expense is booked against this account in Purchase Receipt {2}" msgstr "الصف {0}: تم تغيير بند المصروفات إلى {1} لأن المصروفات مسجلة مقابل هذا الحساب في إيصال الشراء {2}" @@ -46912,7 +46957,7 @@ msgstr "صف {0}: (من الوقت) و (إلى وقت) تكون إلزامية." msgid "Row {0}: From Time and To Time of {1} is overlapping with {2}" msgstr "الصف {0}: من وقت إلى وقت {1} يتداخل مع {2}" -#: erpnext/controllers/stock_controller.py:1725 +#: erpnext/controllers/stock_controller.py:1739 msgid "Row {0}: From Warehouse is mandatory for internal transfers" msgstr "الصف {0}: من المستودع إلزامي للتحويلات الداخلية" @@ -47024,7 +47069,7 @@ msgstr "الصف {0}: فاتورة الشراء {1} ليس لها أي تأثي msgid "Row {0}: Qty cannot be greater than {1} for the Item {2}." msgstr "الصف {0}: لا يمكن أن تكون الكمية أكبر من {1} للعنصر {2}." -#: erpnext/stock/doctype/stock_entry/stock_entry.py:738 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:747 msgid "Row {0}: Qty in Stock UOM can not be zero." msgstr "الصف {0}: لا يمكن أن تكون الكمية في المخزون بوحدة القياس صفرًا." @@ -47036,7 +47081,7 @@ msgstr "الصف {0}: يجب أن تكون الكمية أكبر من 0." msgid "Row {0}: Quantity cannot be negative." msgstr "الصف {0}: لا يمكن أن تكون الكمية سالبة." -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1233 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1242 msgid "Row {0}: Quantity not available for {4} in warehouse {1} at posting time of the entry ({2} {3})" msgstr "" @@ -47044,7 +47089,7 @@ msgstr "" msgid "Row {0}: Sales Invoice {1} is already created for {2}" msgstr "الصف {0}: تم إنشاء فاتورة المبيعات {1} بالفعل لـ {2}" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:344 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:353 msgid "Row {0}: Serial/Batch has been reset to values linked with Work Order {1} because the previously selected serial/batch does not belong to this Work Order." msgstr "" @@ -47052,11 +47097,11 @@ msgstr "" msgid "Row {0}: Shift cannot be changed since the depreciation has already been processed" msgstr "الصف {0}: لا يمكن تغيير المناوبة لأن عملية الإهلاك قد تمت بالفعل" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1974 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2004 msgid "Row {0}: Subcontracted Item is mandatory for the raw material {1}" msgstr "الصف {0}: العنصر المتعاقد عليه من الباطن إلزامي للمادة الخام {1}" -#: erpnext/controllers/stock_controller.py:1716 +#: erpnext/controllers/stock_controller.py:1730 msgid "Row {0}: Target Warehouse is mandatory for internal transfers" msgstr "الصف {0}: المستودع المستهدف إلزامي للتحويلات الداخلية" @@ -47068,11 +47113,11 @@ msgstr "الصف {0}: المهمة {1} لا تنتمي إلى المشروع {2} msgid "Row {0}: The entire expense amount for account {1} in {2} has already been allocated." msgstr "الصف {0}: تم تخصيص مبلغ المصروفات بالكامل للحساب {1} في {2} بالفعل." -#: erpnext/stock/doctype/stock_entry/stock_entry.py:784 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:793 msgid "Row {0}: The item {1}, quantity must be positive number" msgstr "" -#: erpnext/controllers/accounts_controller.py:3242 +#: erpnext/controllers/accounts_controller.py:3247 msgid "Row {0}: The {3} Account {1} does not belong to the company {2}" msgstr "الصف {0}: الحساب {3} {1} لا ينتمي إلى الشركة {2}" @@ -47080,11 +47125,11 @@ msgstr "الصف {0}: الحساب {3} {1} لا ينتمي إلى الشركة { msgid "Row {0}: To set {1} periodicity, difference between from and to date must be greater than or equal to {2}" msgstr "الصف {0}: لتعيين دورية {1} ، يجب أن يكون الفرق بين تاريخي البداية والنهاية أكبر من أو يساوي {2}" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3954 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3986 msgid "Row {0}: Transferred quantity cannot be greater than the requested quantity." msgstr "الصف {0}: لا يمكن أن تكون الكمية المنقولة أكبر من الكمية المطلوبة." -#: erpnext/stock/doctype/stock_entry/stock_entry.py:732 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:741 msgid "Row {0}: UOM Conversion Factor is mandatory" msgstr "الصف {0}: عامل تحويل UOM إلزامي\\n
    \\nRow {0}: UOM Conversion Factor is mandatory" @@ -47105,7 +47150,7 @@ msgstr "" msgid "Row {0}: Workstation or Workstation Type is mandatory for an operation {1}" msgstr "الصف {0}: محطة العمل أو نوع محطة العمل إلزامي للعملية {1}" -#: erpnext/controllers/accounts_controller.py:1203 +#: erpnext/controllers/accounts_controller.py:1208 msgid "Row {0}: user has not applied the rule {1} on the item {2}" msgstr "الصف {0}: لم يطبق المستخدم القاعدة {1} على العنصر {2}" @@ -47117,7 +47162,7 @@ msgstr "الصف {0}: {1} تم تقديم طلب بالفعل للحساب في msgid "Row {0}: {1} must be greater than 0" msgstr "الصف {0}: يجب أن يكون {1} أكبر من 0" -#: erpnext/controllers/accounts_controller.py:809 +#: erpnext/controllers/accounts_controller.py:814 msgid "Row {0}: {1} {2} cannot be same as {3} (Party Account) {4}" msgstr "الصف {0}: {1} {2} لا يمكن أن يكون هو نفسه {3} (حساب الطرفية) {4}" @@ -47163,7 +47208,7 @@ msgstr "تمت إزالة الصفوف في {0}" msgid "Rows with Same Account heads will be merged on Ledger" msgstr "سيتم دمج الصفوف التي تحتوي على نفس رؤوس الحسابات في دفتر الأستاذ" -#: erpnext/controllers/accounts_controller.py:2776 +#: erpnext/controllers/accounts_controller.py:2781 msgid "Rows with duplicate due dates in other rows were found: {0}" msgstr "تم العثور على صفوف ذات تواريخ استحقاق مكررة في صفوف أخرى: {0}" @@ -47171,7 +47216,7 @@ msgstr "تم العثور على صفوف ذات تواريخ استحقاق م msgid "Rows: {0} have 'Payment Entry' as reference_type. This should not be set manually." msgstr "الصفوف: {0} تحتوي على \"إدخال الدفع\" كنوع مرجعي. لا ينبغي تعيين هذا يدويًا." -#: erpnext/controllers/accounts_controller.py:302 +#: erpnext/controllers/accounts_controller.py:307 msgid "Rows: {0} in {1} section are Invalid. Reference Name should point to a valid Payment Entry or Journal Entry." msgstr "الصفوف: {0} في القسم {1} غير صالحة. يجب أن يشير اسم المرجع إلى قيد دفع أو قيد يومية صالح." @@ -47294,7 +47339,7 @@ msgstr "تم الوفاء باتفاقية مستوى الخدمة (SLA)" msgid "SLA Paused On" msgstr "تم إيقاف اتفاقية مستوى الخدمة مؤقتًا" -#: erpnext/public/js/utils.js:1277 +#: erpnext/public/js/utils.js:1303 msgid "SLA is on hold since {0}" msgstr "اتفاقية مستوى الخدمة معلقة منذ {0}" @@ -47384,7 +47429,7 @@ msgstr "طريقة تحصيل الراتب" #: erpnext/accounts/doctype/tax_category/tax_category_dashboard.py:9 #: erpnext/accounts/doctype/tax_rule/tax_rule.json #: erpnext/crm/doctype/opportunity/opportunity.js:288 -#: erpnext/crm/doctype/opportunity/opportunity.py:158 +#: erpnext/crm/doctype/opportunity/opportunity.py:168 #: erpnext/projects/doctype/project/project_dashboard.py:15 #: erpnext/regional/report/vat_audit_report/vat_audit_report.py:145 #: erpnext/selling/doctype/quotation/quotation.json @@ -48248,12 +48293,12 @@ msgstr "مستودع الاحتفاظ بالعينات" #. Label of the sample_size (Float) field in DocType 'Quality Inspection' #: erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py:93 -#: erpnext/public/js/controllers/transaction.js:2877 +#: erpnext/public/js/controllers/transaction.js:2882 #: erpnext/stock/doctype/quality_inspection/quality_inspection.json msgid "Sample Size" msgstr "حجم العينة" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:4457 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:4489 msgid "Sample quantity {0} cannot be more than received quantity {1}" msgstr "كمية العينة {0} لا يمكن أن تكون أكثر من الكمية المستلمة {1}" @@ -48358,7 +48403,7 @@ msgstr "الكمية الممسوحة ضوئياً" msgid "Schedule Date" msgstr "جدول التسجيل" -#: erpnext/public/js/controllers/transaction.js:541 +#: erpnext/public/js/controllers/transaction.js:543 msgid "Schedule Name" msgstr "" @@ -48540,7 +48585,7 @@ msgstr "البحث عن طريق معرف الفاتورة أو اسم العم msgid "Search by item code, serial number or barcode" msgstr "ابحث باستخدام رمز المنتج أو الرقم التسلسلي أو الرمز الشريطي" -#: banking/src/components/features/BankReconciliation/CompanySelector.tsx:64 +#: banking/src/components/features/BankReconciliation/CompanySelector.tsx:77 msgid "Search company..." msgstr "" @@ -48772,7 +48817,7 @@ msgstr "اختيار العناصر" msgid "Select Items based on Delivery Date" msgstr "حدد العناصر بناءً على تاريخ التسليم" -#: erpnext/public/js/controllers/transaction.js:2912 +#: erpnext/public/js/controllers/transaction.js:2917 msgid "Select Items for Quality Inspection" msgstr "اختيار الأصناف لفحص الجودة" @@ -48797,12 +48842,12 @@ msgstr "اختر المنتجات حتى تاريخ التسليم" msgid "Select Job Worker Address" msgstr "حدد عنوان العامل" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1222 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1231 #: erpnext/selling/page/point_of_sale/pos_item_cart.js:966 msgid "Select Loyalty Program" msgstr "اختر برنامج الولاء" -#: erpnext/public/js/controllers/transaction.js:527 +#: erpnext/public/js/controllers/transaction.js:529 msgid "Select Payment Schedule" msgstr "" @@ -48957,7 +49002,7 @@ msgstr "حدد اسم الشركة الأول." msgid "Select date" msgstr "" -#: erpnext/controllers/accounts_controller.py:3017 +#: erpnext/controllers/accounts_controller.py:3022 msgid "Select finance book for the item {0} at row {1}" msgstr "حدد دفتر تمويل للعنصر {0} في الصف {1}" @@ -49159,7 +49204,7 @@ msgstr "معدل البيع" #: erpnext/selling/doctype/selling_settings/selling_settings.json #: erpnext/selling/workspace/selling/selling.json #: erpnext/setup/workspace/erpnext_settings/erpnext_settings.json -#: erpnext/stock/doctype/stock_settings/stock_settings.py:260 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:265 #: erpnext/workspace_sidebar/erpnext_settings.json msgid "Selling Settings" msgstr "إعدادات البيع" @@ -49217,7 +49262,7 @@ msgid "Send Emails to Suppliers" msgstr "إرسال رسائل البريد الإلكتروني إلى الموردين" #. Label of the send_sms (Button) field in DocType 'SMS Center' -#: erpnext/public/js/controllers/transaction.js:727 +#: erpnext/public/js/controllers/transaction.js:729 #: erpnext/selling/doctype/sms_center/sms_center.json msgid "Send SMS" msgstr "SMS أرسل رسالة" @@ -49364,7 +49409,7 @@ msgstr "" #: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js:74 #: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py:114 -#: erpnext/public/js/controllers/transaction.js:2890 +#: erpnext/public/js/controllers/transaction.js:2895 #: erpnext/public/js/utils/serial_no_batch_selector.js:432 #: erpnext/selling/doctype/installation_note_item/installation_note_item.json #: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -49384,7 +49429,7 @@ msgstr "" #: erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.py:450 #: erpnext/stock/report/serial_no_ledger/serial_no_ledger.js:38 #: erpnext/stock/report/serial_no_ledger/serial_no_ledger.py:61 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:427 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:430 #: erpnext/stock/workspace/stock/stock.json #: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json #: erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json @@ -49408,7 +49453,7 @@ msgstr "رقم المسلسل / الدفعة" msgid "Serial No Already Assigned" msgstr "تم تخصيص الرقم التسلسلي مسبقاً" -#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:33 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:39 msgid "Serial No Count" msgstr "المسلسل لا عد" @@ -49425,7 +49470,7 @@ msgstr "دفتر الأستاذ ذو الرقم التسلسلي" msgid "Serial No Range" msgstr "نطاق الأرقام التسلسلية" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2752 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2802 msgid "Serial No Reserved" msgstr "الرقم التسلسلي محجوز" @@ -49482,7 +49527,7 @@ msgstr "" msgid "Serial No and Batch Traceability" msgstr "إمكانية تتبع الرقم التسلسلي والدفعة" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1245 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1295 msgid "Serial No is mandatory" msgstr "الرقم التسلسلي إلزامي" @@ -49490,6 +49535,10 @@ msgstr "الرقم التسلسلي إلزامي" msgid "Serial No is mandatory for Item {0}" msgstr "رقم المسلسل إلزامي القطعة ل {0}" +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:111 +msgid "Serial No status sync has been queued. Reload the report after a few minutes." +msgstr "" + #: erpnext/public/js/utils/serial_no_batch_selector.js:603 msgid "Serial No {0} already exists" msgstr "الرقم التسلسلي {0} موجود بالفعل" @@ -49511,7 +49560,7 @@ msgstr "الرقم المتسلسل {0} لا ينتمي إلى البند {1}\\n msgid "Serial No {0} does not exist" msgstr "الرقم المتسلسل {0} غير موجود\\n
    \\nSerial No {0} does not exist" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:3541 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:3591 msgid "Serial No {0} does not exists" msgstr "الرقم التسلسلي {0} غير موجود" @@ -49527,7 +49576,7 @@ msgstr "تمت إضافة الرقم التسلسلي {0} بالفعل" msgid "Serial No {0} is already assigned to customer {1}. Can only be returned against the customer {1}" msgstr "الرقم التسلسلي {0} مُخصص بالفعل للعميل {1}. لا يمكن إرجاعه إلا للعميل {1}." -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:484 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:534 msgid "Serial No {0} is not present in the {1} {2}, hence you can't return it against the {1} {2}" msgstr "الرقم التسلسلي {0} غير موجود في {1} {2}، لذا لا يمكنك إرجاعه إلى {1} {2}" @@ -49565,11 +49614,11 @@ msgstr "الأرقام التسلسلية / أرقام الدفعات" msgid "Serial Nos / Batches" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2024 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2074 msgid "Serial Nos are created successfully" msgstr "تم إنشاء الأرقام التسلسلية بنجاح" -#: erpnext/stock/stock_ledger.py:2330 +#: erpnext/stock/stock_ledger.py:2373 msgid "Serial Nos are reserved in Stock Reservation Entries, you need to unreserve them before proceeding." msgstr "يتم حجز الأرقام التسلسلية في إدخالات حجز المخزون، لذا عليك إلغاء حجزها قبل المتابعة." @@ -49643,26 +49692,26 @@ msgstr "التسلسل والدفعة" #: erpnext/stock/report/available_serial_no/available_serial_no.py:188 #: erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.py:31 #: erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py:80 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:411 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:188 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:414 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:197 #: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json #: erpnext/workspace_sidebar/stock.json msgid "Serial and Batch Bundle" msgstr "حزمة التسلسل والدفعة" -#: erpnext/stock/doctype/item/item.py:1122 +#: erpnext/stock/doctype/item/item.py:1132 msgid "Serial and Batch Bundle Exists" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2253 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2303 msgid "Serial and Batch Bundle created" msgstr "تم إنشاء حزمة التسلسل والدفعة" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2349 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2399 msgid "Serial and Batch Bundle updated" msgstr "تم تحديث حزمة التسلسل والدفعة" -#: erpnext/controllers/stock_controller.py:237 +#: erpnext/controllers/stock_controller.py:251 msgid "Serial and Batch Bundle {0} is already used in {1} {2}." msgstr "تم استخدام حزمة Serial and Batch {0} بالفعل في {1} {2}." @@ -49670,7 +49719,7 @@ msgstr "تم استخدام حزمة Serial and Batch {0} بالفعل في {1} msgid "Serial and Batch Bundle {0} is not submitted" msgstr "لم يتم إرسال حزمة البيانات التسلسلية والدفعية {0}" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2323 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2373 msgid "Serial and Batch Bundle {0} is submitted and its entries cannot be modified." msgstr "" @@ -49926,12 +49975,12 @@ msgid "Service Stop Date" msgstr "تاريخ توقف الخدمة" #: erpnext/accounts/deferred_revenue.py:45 -#: erpnext/public/js/controllers/transaction.js:1799 +#: erpnext/public/js/controllers/transaction.js:1804 msgid "Service Stop Date cannot be after Service End Date" msgstr "لا يمكن أن يكون تاريخ إيقاف الخدمة بعد تاريخ انتهاء الخدمة" #: erpnext/accounts/deferred_revenue.py:42 -#: erpnext/public/js/controllers/transaction.js:1796 +#: erpnext/public/js/controllers/transaction.js:1801 msgid "Service Stop Date cannot be before Service Start Date" msgstr "لا يمكن أن يكون تاريخ إيقاف الخدمة قبل تاريخ بدء الخدمة" @@ -49955,7 +50004,7 @@ msgstr "تعيين السلف والتخصيص (FIFO)" #. Label of the set_basic_rate_manually (Check) field in DocType 'Stock Entry #. Detail' -#: erpnext/stock/doctype/stock_entry/stock_entry.py:411 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:420 #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json msgid "Set Basic Rate Manually" msgstr "قم بتعيين السعر الأساسي يدويًا" @@ -50006,11 +50055,11 @@ msgstr "تعيين مجموعة من الحكمة الإغلاق الميزان msgid "Set Landed Cost Based on Purchase Invoice Rate" msgstr "تحديد تكلفة الشحن بناءً على سعر فاتورة الشراء" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1234 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1243 msgid "Set Loyalty Program" msgstr "برنامج الولاء" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:315 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:314 msgid "Set New Release Date" msgstr "تعيين تاريخ الإصدار الجديد" @@ -50543,7 +50592,7 @@ msgstr "الشحن العنوان الاسم" msgid "Shipping Address Template" msgstr "نموذج عنوان الشحن" -#: erpnext/controllers/accounts_controller.py:595 +#: erpnext/controllers/accounts_controller.py:600 msgid "Shipping Address does not belong to the {0}" msgstr "عنوان الشحن لا ينتمي إلى {0}" @@ -50728,7 +50777,7 @@ msgstr "إظهار المبلغ التراكمي" msgid "Show Dimension Wise Stock" msgstr "عرض المخزون حسب الأبعاد" -#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js:29 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js:53 msgid "Show Disabled Items" msgstr "عرض العناصر المعطلة" @@ -51019,7 +51068,7 @@ msgstr "" msgid "Simultaneous" msgstr "متزامن" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:862 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:871 msgid "Since there is a process loss of {0} units for the finished good {1}, you should reduce the quantity by {0} units for the finished good {1} in the Items Table." msgstr "بما أن هناك خسارة في العملية قدرها {0} وحدة للمنتج النهائي {1}، فيجب عليك تقليل الكمية بمقدار {0} وحدة للمنتج النهائي {1} في جدول العناصر." @@ -51131,7 +51180,7 @@ msgstr "يباع بواسطة" msgid "Solvency Ratios" msgstr "نسب الملاءة المالية" -#: erpnext/controllers/accounts_controller.py:4400 +#: erpnext/controllers/accounts_controller.py:4430 msgid "Some required Company details are missing. You don't have permission to update them. Please contact your System Manager." msgstr "بعض بيانات الشركة المطلوبة مفقودة. ليس لديك صلاحية لتحديثها. يرجى الاتصال بمدير النظام." @@ -51204,11 +51253,11 @@ msgstr "" msgid "Source Stock Entry (Manufacture)" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1024 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1033 msgid "Source Stock Entry {0} belongs to Work Order {1}, not {2}. Please use a manufacture entry from the same Work Order." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2724 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2756 msgid "Source Stock Entry {0} has no finished goods quantity" msgstr "" @@ -51274,7 +51323,7 @@ msgstr "يجب أن يكون مستودع المصدر {0} هو نفسه مست msgid "Source and Target Location cannot be same" msgstr "لا يمكن أن يكون المصدر و الموقع الهدف نفسه" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:990 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:999 msgid "Source and target warehouse cannot be same for row {0}" msgstr "" @@ -51287,9 +51336,9 @@ msgstr "ويجب أن تكون مصدر ومستودع الهدف مختلفة" msgid "Source of Funds (Liabilities)" msgstr "(مصدر الأموال (الخصوم" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:957 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:973 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:980 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:966 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:982 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:989 msgid "Source warehouse is mandatory for row {0}" msgstr "" @@ -51662,7 +51711,7 @@ msgstr "يجب إلغاء الحالة أو إكمالها" msgid "Status must be one of {0}" msgstr "يجب أن تكون حالة واحدة من {0}" -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:279 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:283 msgid "Status set to rejected as there are one or more rejected readings." msgstr "تم تعيين الحالة إلى مرفوض لوجود قراءة واحدة أو أكثر مرفوضة." @@ -51692,8 +51741,8 @@ msgstr "المخازن" #: erpnext/accounts/doctype/account/account.json #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:96 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:158 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1406 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1445 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1416 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1455 #: erpnext/accounts/report/account_balance/account_balance.js:58 msgid "Stock Adjustment" msgstr "تسوية المخزون" @@ -51779,11 +51828,27 @@ msgstr "رصيد المخزون الختامي" msgid "Stock Closing Entry" msgstr "قيد إغلاق المخزون" -#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:80 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:239 +msgid "Stock Closing Entry In Progress" +msgstr "" + +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:257 +msgid "Stock Closing Entry Outdated" +msgstr "" + +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:231 +msgid "Stock Closing Entry Required" +msgstr "" + +#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:122 msgid "Stock Closing Entry {0} already exists for the selected date range" msgstr "تم بالفعل إدخال إغلاق المخزون {0} لنطاق التاريخ المحدد" -#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:101 +#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:144 +msgid "Stock Closing Entry {0} belongs to a closed accounting period. Cancel the Period Closing Voucher {1} first." +msgstr "" + +#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:159 msgid "Stock Closing Entry {0} has been queued for processing, system will take sometime to complete it." msgstr "" @@ -51800,7 +51865,7 @@ msgstr "سجل إغلاق المخزون" msgid "Stock Details" msgstr "تفاصيل المخزون" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1201 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1210 msgid "Stock Entries already created for Work Order {0}: {1}" msgstr "تم إنشاء إدخالات المخزون بالفعل لأمر العمل {0}: {1}" @@ -51868,7 +51933,7 @@ msgstr "الأسهم الدخول {0} خلق" msgid "Stock Entry {0} has created" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1325 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1335 msgid "Stock Entry {0} is not submitted" msgstr "الحركة المخزنية {0} غير مسجلة" @@ -51889,6 +51954,10 @@ msgstr "" msgid "Stock Expenses" msgstr "مصاريف المخزون" +#: erpnext/stock/stock_ledger.py:80 +msgid "Stock Frozen" +msgstr "" + #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:37 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:60 msgid "Stock In Hand" @@ -51922,7 +51991,7 @@ msgstr "يتم إعادة ترحيل قيود دفتر الأستاذ العام #. Name of a DocType #: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:113 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:149 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:158 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:30 msgid "Stock Ledger Entry" msgstr "حركة سجل المخزن" @@ -52046,7 +52115,7 @@ msgstr "كمية المخزون المتوقعة" #: erpnext/stock/doctype/material_request_item/material_request_item.json #: erpnext/stock/report/item_where_used/item_where_used.py:76 #: erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py:34 -#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:34 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:40 msgid "Stock Qty" msgstr "الأسهم الكمية" @@ -52137,9 +52206,9 @@ msgstr "إعدادات إعادة نشر المخزون" #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1706 #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1723 #: erpnext/stock/doctype/stock_settings/stock_settings.json -#: erpnext/stock/doctype/stock_settings/stock_settings.py:217 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:229 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:243 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:222 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:234 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:248 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:182 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:195 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:207 @@ -52153,7 +52222,7 @@ msgid "Stock Reservation Entries Cancelled" msgstr "تم إلغاء إدخالات حجز المخزون" #: erpnext/controllers/subcontracting_inward_controller.py:1037 -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:2262 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:2261 #: erpnext/manufacturing/doctype/work_order/work_order.py:2416 #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1784 msgid "Stock Reservation Entries Created" @@ -52332,7 +52401,7 @@ msgstr "قيود المخزون" #: erpnext/stock/report/item_where_used/item_where_used.py:82 #: erpnext/stock/report/reserved_stock/reserved_stock.py:110 #: erpnext/stock/report/stock_balance/stock_balance.py:508 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:296 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:299 #: erpnext/subcontracting/doctype/subcontracting_inward_order_item/subcontracting_inward_order_item.json #: erpnext/subcontracting/doctype/subcontracting_inward_order_received_item/subcontracting_inward_order_received_item.json #: erpnext/subcontracting/doctype/subcontracting_inward_order_secondary_item/subcontracting_inward_order_secondary_item.json @@ -52358,7 +52427,7 @@ msgstr "عدم وجود حجز على الأسهم" msgid "Stock Uom" msgstr "وحدة قياس السهم" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:758 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:768 msgid "Stock Update Not Allowed" msgstr "" @@ -52433,6 +52502,10 @@ msgstr "التحقق من صحة المخزون" msgid "Stock Value" msgstr "قيمة المخزون" +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:186 +msgid "Stock Value Mismatch" +msgstr "" + #. Label of a chart in the Stock Workspace #: erpnext/stock/workspace/stock/stock.json msgid "Stock Value by Item Group" @@ -52474,7 +52547,7 @@ msgstr "لا يمكن تحديث المخزون بناءً على إشعارات msgid "Stock cannot be updated because the invoice contains a drop shipping item. Please disable 'Update Stock' or remove the drop shipping item." msgstr "لا يمكن تحديث المخزون لأن الفاتورة تحتوي على منتج يتم شحنه مباشرة من المورد. يرجى تعطيل خيار \"تحديث المخزون\" أو إزالة المنتج الذي يتم شحنه مباشرة من المورد." -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:755 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:765 msgid "Stock cannot be updated for Purchase Invoice {0} because a Purchase Receipt {1} has already been created for this transaction. Please disable the 'Update Stock' checkbox in the Purchase Invoice and save the invoice." msgstr "" @@ -52507,12 +52580,20 @@ msgstr "الكمية المتوفرة من المنتج ذي الرمز {0} غي msgid "Stock transactions before {0} are frozen" msgstr "يتم تجميد المعاملات المخزنية قبل {0}" +#: erpnext/stock/stock_ledger.py:74 +msgid "Stock transactions dated on or before {0} are frozen because the period is closed and the Stock Closing Entry {1} has been generated. To make changes, cancel the Period Closing Voucher first." +msgstr "" + #. Description of the 'Freeze stocks older than (days)' (Int) field in DocType #. 'Stock Settings' #: erpnext/stock/doctype/stock_settings/stock_settings.json msgid "Stock transactions that are older than the mentioned days cannot be modified." msgstr "لا يمكن تعديل معاملات الأسهم التي مضى عليها أكثر من الأيام المذكورة." +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:254 +msgid "Stock transactions were created or modified after the Stock Closing Entry {0} was generated. Regenerate it before submitting the Period Closing Voucher." +msgstr "" + #. Description of the 'Auto reserve Stock for Sales Order on Purchase' (Check) #. field in DocType 'Stock Settings' #: erpnext/stock/doctype/stock_settings/stock_settings.json @@ -52565,7 +52646,7 @@ msgstr "المجمعات الفرعية" msgid "Sub Assemblies & Raw Materials" msgstr "التجميعات الفرعية والمواد الخام" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:340 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:329 msgid "Sub Assembly Item" msgstr "عناصر التجميع الفرعي" @@ -52581,7 +52662,7 @@ msgstr "رمز عنصر التجميع الفرعي" msgid "Sub Assembly Item Reference" msgstr "مرجع عناصر التجميع الفرعي" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:449 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:438 msgid "Sub Assembly Item is mandatory" msgstr "عنصر التجميع الفرعي إلزامي" @@ -53036,11 +53117,11 @@ msgstr "اشتراك" msgid "Subscription End Date" msgstr "تاريخ انتهاء الاشتراك" -#: erpnext/accounts/doctype/subscription/subscription.py:406 +#: erpnext/accounts/doctype/subscription/subscription.py:409 msgid "Subscription End Date is mandatory to follow calendar months" msgstr "تاريخ انتهاء الاشتراك إلزامي لمتابعة الأشهر التقويمية" -#: erpnext/accounts/doctype/subscription/subscription.py:396 +#: erpnext/accounts/doctype/subscription/subscription.py:399 msgid "Subscription End Date must be after {0} as per the subscription plan" msgstr "يجب أن يكون تاريخ انتهاء الاشتراك بعد {0} وفقًا لخطة الاشتراك" @@ -53100,7 +53181,7 @@ msgstr "إعدادات الاشتراك" msgid "Subscription Start Date" msgstr "تاريخ بدء الاشتراك" -#: erpnext/accounts/doctype/subscription/subscription.py:774 +#: erpnext/accounts/doctype/subscription/subscription.py:782 msgid "Subscription for Future dates cannot be processed." msgstr "لا يمكن معالجة الاشتراكات للتواريخ المستقبلية." @@ -53498,7 +53579,7 @@ msgstr "المورد فاتورة التسجيل" msgid "Supplier Invoice No" msgstr "رقم فاتورة المورد" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1841 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1851 msgid "Supplier Invoice No exists in Purchase Invoice {0}" msgstr "المورد فاتورة لا يوجد في شراء الفاتورة {0}" @@ -53850,6 +53931,10 @@ msgstr "" msgid "Sync Now" msgstr "مزامنة الآن" +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js:6 +msgid "Sync Serial No Status" +msgstr "" + #: erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js:36 msgid "Sync Started" msgstr "بدأت عملية المزامنة" @@ -53889,7 +53974,7 @@ msgstr "" msgid "System will fetch all the entries if limit value is zero." msgstr "سيقوم النظام بجلب كل الإدخالات إذا كانت قيمة الحد صفرا." -#: erpnext/controllers/accounts_controller.py:2256 +#: erpnext/controllers/accounts_controller.py:2261 msgid "System will not check over billing since amount for Item {0} in {1} is zero" msgstr "لن يتحقق النظام من الفواتير الزائدة لأن مبلغ العنصر {0} في {1} يساوي صفرًا" @@ -53912,7 +53997,7 @@ msgstr "" msgid "TDS Computation Summary" msgstr "ملخص حساب TDS" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1599 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1609 msgid "TDS Deducted" msgstr "تم خصم ضريبة الدخل المقتطعة" @@ -54099,9 +54184,9 @@ msgstr "تم إعداد مستودع Target لبعض المنتجات، لكن msgid "Target Warehouse {0} must be same as Delivery Warehouse {1} in the Subcontracting Inward Order Item." msgstr "يجب أن يكون المستودع المستهدف {0} هو نفسه مستودع التسليم {1} في بند أمر التوريد الداخلي للتعاقد من الباطن." -#: erpnext/stock/doctype/stock_entry/stock_entry.py:963 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:969 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:984 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:972 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:978 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:993 msgid "Target warehouse is mandatory for row {0}" msgstr "" @@ -55097,7 +55182,7 @@ msgstr "" msgid "The BOM which will be replaced" msgstr "وBOM التي سيتم استبدالها" -#: erpnext/stock/serial_batch_bundle.py:1545 +#: erpnext/stock/serial_batch_bundle.py:1631 msgid "The Batch {0} has negative batch quantity {1}. To fix this, go to the batch and click on Recalculate Batch Qty. If the issue still persists, create an inward entry." msgstr "تحتوي الدفعة {0} على كمية سالبة {1}. لحل هذه المشكلة، انتقل إلى الدفعة وانقر على \"إعادة حساب كمية الدفعة\". إذا استمرت المشكلة، فأنشئ إدخالًا داخليًا." @@ -55117,11 +55202,11 @@ msgstr "يجب أن يحتوي نوع المستند {0} على حقل الحا msgid "The Excluded Fee is bigger than the Deposit it is deducted from." msgstr "الرسوم المستثناة أكبر من مبلغ الوديعة التي يتم خصمها منه." -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:188 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:306 msgid "The GL Entries and closing balances will be processed in the background, it can take a few minutes." msgstr "ستتم معالجة قيود دفتر الأستاذ العام والأرصدة الختامية في الخلفية، وقد يستغرق ذلك بضع دقائق." -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:461 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:579 msgid "The GL Entries will be cancelled in the background, it can take a few minutes." msgstr "سيتم إلغاء إدخالات دفتر الأستاذ العام في الخلفية، وقد يستغرق ذلك بضع دقائق." @@ -55141,7 +55226,7 @@ msgstr "قد يكون مصطلح الدفع في الصف {0} مكررا." msgid "The Pick List having Stock Reservation Entries cannot be updated. If you need to make changes, we recommend canceling the existing Stock Reservation Entries before updating the Pick List." msgstr "لا يمكن تحديث قائمة الاختيار التي تحتوي على إدخالات حجز المخزون. إذا كنت ترغب في إجراء تغييرات، فننصحك بإلغاء إدخالات حجز المخزون الحالية قبل تحديث قائمة الاختيار." -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3176 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3208 msgid "The Process Loss Qty has reset as per job cards Process Loss Qty" msgstr "تمت إعادة ضبط كمية الفاقد في العملية وفقًا لبطاقات العمل." @@ -55153,14 +55238,18 @@ msgstr "يرتبط مندوب المبيعات بـ {0}" msgid "The Serial No at Row #{0}: {1} is not available in warehouse {2}." msgstr "الرقم التسلسلي في الصف #{0}: {1} غير متوفر في المستودع {2}." -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2749 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2799 msgid "The Serial No {0} is reserved against the {1} {2} and cannot be used for any other transaction." msgstr "الرقم التسلسلي {0} محجوز مقابل {1} {2} ولا يمكن استخدامه لأي معاملة أخرى." -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2142 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2174 msgid "The Serial and Batch Bundle {0} is not valid for this transaction. The 'Type of Transaction' should be 'Outward' instead of 'Inward' in Serial and Batch Bundle {0}" msgstr "حزمة البيانات التسلسلية والدفعية {0} غير صالحة لهذه المعاملة. يجب أن يكون \"نوع المعاملة\" \"خارجي\" بدلاً من \"داخلي\" في حزمة البيانات التسلسلية والدفعية {0}" +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:236 +msgid "The Stock Closing Entry for {0} is not completed yet. Wait for it to complete before submitting the Period Closing Voucher." +msgstr "" + #: erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.js:17 msgid "The Stock Entry of type 'Manufacture' is known as backflush. Raw materials being consumed to manufacture finished goods is known as backflushing.

    When creating Manufacture Entry, raw-material items are backflushed based on BOM of production item. If you want raw-material items to be backflushed based on Material Transfer entry made against that Work Order instead, then you can set it under this field." msgstr "يُعرف إدخال المخزون من نوع "التصنيع" باسم التدفق الرجعي. تُعرف المواد الخام التي يتم استهلاكها لتصنيع السلع التامة الصنع بالتدفق العكسي.

    عند إنشاء إدخال التصنيع ، يتم إجراء مسح تلقائي لعناصر المواد الخام استنادًا إلى قائمة مكونات الصنف الخاصة بصنف الإنتاج. إذا كنت تريد إعادة تسريح أصناف المواد الخام استنادًا إلى إدخال نقل المواد الذي تم إجراؤه مقابل طلب العمل هذا بدلاً من ذلك ، فيمكنك تعيينه ضمن هذا الحقل." @@ -55197,10 +55286,14 @@ msgstr "" msgid "The bank account is not a company account. Please select a company account" msgstr "" -#: erpnext/controllers/stock_controller.py:1482 +#: erpnext/controllers/stock_controller.py:1496 msgid "The batch {0} is reserved for {1} in the warehouse {2} and the remaining quantity is not enough to cover the reservations. So, cannot proceed with the {3} {4}." msgstr "" +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:179 +msgid "The closing balance {0} of the Stock Asset accounts does not match the closing value {1} of the Stock Balance report as on {2}. Resolve the difference using the Stock Ledger Variance report before closing the period." +msgstr "" + #: erpnext/regional/report/vat_audit_report/vat_audit_report.py:43 msgid "The company {0} is not in South Africa. VAT Audit Report is only available for companies in South Africa." msgstr "" @@ -55303,11 +55396,11 @@ msgstr "فشلت الأصول التالية في تسجيل قيود الإهل msgid "The following batches are expired, please restock them:
    {0}" msgstr "" -#: erpnext/controllers/accounts_controller.py:446 +#: erpnext/controllers/accounts_controller.py:451 msgid "The following cancelled repost entries exist for {0}:

    {1}

    Kindly delete these entries before continuing." msgstr "" -#: erpnext/stock/doctype/item/item.py:949 +#: erpnext/stock/doctype/item/item.py:959 msgid "The following deleted attributes exist in Variants but not in the Template. You can either delete the Variants or keep the attribute(s) in template." msgstr "توجد السمات المحذوفة التالية في المتغيرات ولكن ليس في القالب. يمكنك إما حذف المتغيرات أو الاحتفاظ بالسمة (السمات) في القالب." @@ -55417,7 +55510,7 @@ msgstr "" msgid "The original invoice should be consolidated before or along with the return invoice." msgstr "ينبغي تجميع الفاتورة الأصلية قبل أو مع فاتورة الإرجاع." -#: erpnext/controllers/accounts_controller.py:224 +#: erpnext/controllers/accounts_controller.py:229 msgid "The outstanding amount {0} in {1} is lesser than {2}. Updating the outstanding to this invoice." msgstr "" @@ -55468,7 +55561,7 @@ msgstr "" msgid "The reference number of the transaction" msgstr "" -#: erpnext/public/js/utils.js:985 +#: erpnext/public/js/utils.js:1011 msgid "The reserved stock will be released when you update items. Are you certain you wish to proceed?" msgstr "سيتم تحرير المخزون المحجوز عند تحديث العناصر. هل أنت متأكد من رغبتك في المتابعة؟" @@ -55521,7 +55614,7 @@ msgstr "الأسهم موجودة بالفعل" msgid "The shares don't exist with the {0}" msgstr "الأسهم غير موجودة مع {0}" -#: erpnext/stock/stock_ledger.py:839 +#: erpnext/stock/stock_ledger.py:866 msgid "The stock for the item {0} in the {1} warehouse was negative on the {2}. You should create a positive entry {3} before the date {4} and time {5} to post the correct valuation rate. For more details, please read the documentation." msgstr "كان رصيد الصنف {0} في المستودع {1} سالبًا في {2}. يجب عليك إنشاء قيد موجب {3} قبل التاريخ {4} والوقت {5} لتسجيل معدل التقييم الصحيح. لمزيد من التفاصيل، يُرجى قراءة الوثائق ." @@ -55623,7 +55716,7 @@ msgstr "" msgid "The {0} ({1}) must be equal to {2} ({3})" msgstr "يجب أن يكون {0} ({1}) مساويًا لـ {2} ({3})" -#: erpnext/public/js/controllers/transaction.js:3382 +#: erpnext/public/js/controllers/transaction.js:3387 msgid "The {0} contains Unit Price Items." msgstr "يحتوي {0} على عناصر سعر الوحدة." @@ -55724,7 +55817,7 @@ msgstr "لم يتم العثور على دفعة بالمقابلة مع {0}: {1 msgid "There is one unreconciled transaction before {0}." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2079 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2111 msgid "There must be atleast 1 Finished Good in this Stock Entry" msgstr "يجب أن يكون هناك منتج نهائي واحد على الأقل في هذا الإدخال المخزوني." @@ -55836,7 +55929,7 @@ msgstr "" msgid "This covers all scorecards tied to this Setup" msgstr "وهذا يغطي جميع بطاقات الأداء مرتبطة بهذا الإعداد" -#: erpnext/controllers/status_updater.py:501 +#: erpnext/controllers/status_updater.py:502 msgid "This document is over limit by {0} {1} for item {4}. Are you making another {3} against the same {2}?" msgstr "هذه الوثيقة هي على حد كتبها {0} {1} لمادة {4}. وجعل لكم آخر {3} ضد نفسه {2}؟" @@ -55939,7 +56032,7 @@ msgstr "هذا يعتمد على المعاملات ضد هذا الشخص ال msgid "This is considered dangerous from accounting point of view." msgstr "يُعتبر هذا الأمر خطيراً من وجهة نظر المحاسبة." -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:536 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:546 msgid "This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice" msgstr "يتم إجراء ذلك للتعامل مع محاسبة الحالات التي يتم فيها إنشاء إيصال الشراء بعد فاتورة الشراء" @@ -56141,6 +56234,10 @@ msgstr "" msgid "This will restrict user access to other employee records" msgstr "سيؤدي هذا إلى تقييد وصول المستخدم لسجلات الموظفين الأخرى" +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js:16 +msgid "This will update the warehouse and status of Serial Nos counted in {0} to match the stock ledger. Continue?" +msgstr "" + #: erpnext/controllers/selling_controller.py:886 msgid "This {} will be treated as material transfer." msgstr "سيتم التعامل مع هذا {} على أنه نقل مواد." @@ -56367,7 +56464,7 @@ msgstr "على فاتورة" msgid "To Currency" msgstr "إلى العملات" -#: erpnext/controllers/accounts_controller.py:645 +#: erpnext/controllers/accounts_controller.py:650 #: erpnext/setup/doctype/holiday_list/holiday_list.py:121 msgid "To Date cannot be before From Date" msgstr "(الى تاريخ) لا يمكن ان يكون قبل (من تاريخ)" @@ -56594,15 +56691,15 @@ msgstr "لإضافة عمليات، حدد خانة الاختيار \"مع ال msgid "To add subcontracted Item's raw materials if include exploded items is disabled." msgstr "لإضافة المواد الخام للعنصر المتعاقد عليه من الباطن في حالة تعطيل خيار تضمين العناصر المفككة." -#: erpnext/controllers/status_updater.py:494 +#: erpnext/controllers/status_updater.py:495 msgid "To allow over billing, update \"Over Billing Allowance\" in Accounts Settings or the Item." msgstr "للسماح بزيادة الفواتير ، حدّث "Over Billing Allowance" في إعدادات الحسابات أو العنصر." -#: erpnext/controllers/status_updater.py:488 +#: erpnext/controllers/status_updater.py:489 msgid "To allow over ordering, update \"Over Order Allowance\" in Buying Settings." msgstr "" -#: erpnext/controllers/status_updater.py:490 +#: erpnext/controllers/status_updater.py:491 msgid "To allow over receipt / delivery, update \"Over Receipt/Delivery Allowance\" in Stock Settings or the Item." msgstr "للسماح بوصول الاستلام / التسليم ، قم بتحديث "الإفراط في الاستلام / بدل التسليم" في إعدادات المخزون أو العنصر." @@ -56639,7 +56736,7 @@ msgid "To include sub-assembly costs and secondary items in Finished Goods on a msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2268 -#: erpnext/controllers/accounts_controller.py:3275 +#: erpnext/controllers/accounts_controller.py:3280 msgid "To include tax in row {0} in Item rate, taxes in rows {1} must also be included" msgstr "ل تشمل الضريبة في الصف {0} في معدل الإغلاق ، {1} ويجب أيضا تضمين الضرائب في الصفوف" @@ -56663,11 +56760,11 @@ msgstr "" msgid "To still proceed with editing this Attribute Value, enable {0} in Item Variant Settings." msgstr "للاستمرار في تعديل قيمة السمة هذه ، قم بتمكين {0} في إعدادات متغير العنصر." -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:627 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:637 msgid "To submit the invoice without purchase order please set {0} as {1} in {2}" msgstr "لإرسال الفاتورة بدون أمر شراء، يرجى تعيين {0} كـ {1} في {2}" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:649 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:659 msgid "To submit the invoice without purchase receipt please set {0} as {1} in {2}" msgstr "لإرسال الفاتورة بدون إيصال الشراء، يرجى تعيين {0} كـ {1} في {2}" @@ -57049,7 +57146,7 @@ msgstr "مجموع الخصم" msgid "Total Debit Transactions" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:942 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:952 msgid "Total Debit must be equal to Total Credit. The difference is {0}" msgstr "يجب أن يكون إجمالي الخصم يساوي إجمالي الائتمان ." @@ -57274,7 +57371,7 @@ msgstr "إجمالي المبلغ المستحق" msgid "Total Paid Amount" msgstr "إجمالي المبلغ المدفوع" -#: erpnext/controllers/accounts_controller.py:2830 +#: erpnext/controllers/accounts_controller.py:2835 msgid "Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total" msgstr "يجب أن يكون إجمالي مبلغ الدفع في جدول الدفع مساويا للمجموع الكبير / المستدير" @@ -57576,7 +57673,7 @@ msgstr "إجمالي وقت العمل على محطة العمل (بالساع msgid "Total allocated percentage for sales team should be 100" msgstr "مجموع النسبة المئوية المخصصة ل فريق المبيعات يجب أن يكون 100" -#: erpnext/selling/doctype/customer/customer.py:198 +#: erpnext/selling/doctype/customer/customer.py:199 msgid "Total contribution percentage should be equal to 100" msgstr "يجب أن تكون نسبة المساهمة الإجمالية مساوية 100" @@ -58165,7 +58262,7 @@ msgstr "" msgid "Trial Period End Date" msgstr "تاريخ انتهاء الفترة التجريبية" -#: erpnext/accounts/doctype/subscription/subscription.py:376 +#: erpnext/accounts/doctype/subscription/subscription.py:379 msgid "Trial Period End Date Cannot be before Trial Period Start Date" msgstr "لا يمكن أن يكون تاريخ انتهاء الفترة التجريبية قبل تاريخ بدء الفترة التجريبية" @@ -58174,7 +58271,7 @@ msgstr "لا يمكن أن يكون تاريخ انتهاء الفترة الت msgid "Trial Period Start Date" msgstr "فترة بداية الفترة التجريبية" -#: erpnext/accounts/doctype/subscription/subscription.py:382 +#: erpnext/accounts/doctype/subscription/subscription.py:385 msgid "Trial Period Start date cannot be after Subscription Start Date" msgstr "لا يمكن أن يكون تاريخ بدء الفترة التجريبية بعد تاريخ بدء الاشتراك" @@ -58366,7 +58463,7 @@ msgstr "إعدادات ضريبة القيمة المضافة في الإمار #: erpnext/manufacturing/doctype/workstation/workstation.js:480 #: erpnext/manufacturing/report/bom_explorer/bom_explorer.py:70 #: erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py:110 -#: erpnext/public/js/stock_analytics.js:94 erpnext/public/js/utils.js:858 +#: erpnext/public/js/stock_analytics.js:94 erpnext/public/js/utils.js:861 #: erpnext/quality_management/doctype/quality_goal_objective/quality_goal_objective.json #: erpnext/quality_management/doctype/quality_review_objective/quality_review_objective.json #: erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json @@ -58480,7 +58577,7 @@ msgstr "" msgid "UOM Name" msgstr "اسم وحدة القايس" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:4379 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:4411 msgid "UOM conversion factor required for UOM: {0} in Item: {1}" msgstr "معامل تحويل وحدة القياس المطلوب لوحدة القياس: {0} في العنصر: {1}" @@ -58664,7 +58761,7 @@ msgstr "وحدة" msgid "Unit Of Measure" msgstr "" -#: erpnext/controllers/accounts_controller.py:4132 +#: erpnext/controllers/accounts_controller.py:4162 msgid "Unit Price" msgstr "سعر الوحدة" @@ -59028,7 +59125,7 @@ msgstr "تحديث المخزون الحالي" #: erpnext/buying/doctype/purchase_order/purchase_order.js:324 #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.js:43 -#: erpnext/public/js/utils.js:964 +#: erpnext/public/js/utils.js:990 #: erpnext/selling/doctype/quotation/quotation.js:136 #: erpnext/selling/doctype/sales_order/sales_order.js:90 #: erpnext/selling/doctype/sales_order/sales_order.js:946 @@ -59041,7 +59138,7 @@ msgstr "تحديث العناصر" #. Invoice' #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json -#: erpnext/controllers/accounts_controller.py:217 +#: erpnext/controllers/accounts_controller.py:222 msgid "Update Outstanding for Self" msgstr "تحديث رائع للذات" @@ -59126,7 +59223,7 @@ msgstr "تم تحديث صف (صفوف) التقرير المالي {0} باسم msgid "Updating Costing and Billing fields against this Project..." msgstr "تحديث حقول التكاليف والفواتير لهذا المشروع..." -#: erpnext/stock/doctype/item/item.py:1511 +#: erpnext/stock/doctype/item/item.py:1521 msgid "Updating Variants..." msgstr "جارٍ تحديث المتغيرات ..." @@ -59744,11 +59841,11 @@ msgstr "سعر التقييم" msgid "Valuation Rate (In / Out)" msgstr "معدل التقييم (داخل / خارج)" -#: erpnext/stock/stock_ledger.py:2056 +#: erpnext/stock/stock_ledger.py:2099 msgid "Valuation Rate Missing" msgstr "معدل التقييم مفقود" -#: erpnext/stock/stock_ledger.py:2034 +#: erpnext/stock/stock_ledger.py:2077 msgid "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}." msgstr "معدل التقييم للعنصر {0} ، مطلوب لإجراء إدخالات محاسبية لـ {1} {2}." @@ -59780,7 +59877,7 @@ msgid "Valuation rate for the item as per Sales Invoice (Only for Internal Trans msgstr "معدل تقييم السلعة وفقًا لفاتورة المبيعات (للتحويلات الداخلية فقط)" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2292 -#: erpnext/controllers/accounts_controller.py:3299 +#: erpnext/controllers/accounts_controller.py:3304 msgid "Valuation type charges can not be marked as Inclusive" msgstr "لا يمكن تحديد رسوم نوع التقييم على أنها شاملة" @@ -59916,7 +60013,7 @@ msgstr "التباين ({})" msgid "Variant" msgstr "مختلف" -#: erpnext/stock/doctype/item/item.py:964 +#: erpnext/stock/doctype/item/item.py:974 msgid "Variant Attribute Error" msgstr "خطأ في سمة المتغير" @@ -59935,7 +60032,7 @@ msgstr "المتغير BOM" msgid "Variant Based On" msgstr "البديل القائم على" -#: erpnext/stock/doctype/item/item.py:992 +#: erpnext/stock/doctype/item/item.py:1002 msgid "Variant Based On cannot be changed" msgstr "لا يمكن تغيير المتغير بناءً على" @@ -59953,7 +60050,7 @@ msgstr "الحقل البديل" msgid "Variant Item" msgstr "عنصر متغير" -#: erpnext/stock/doctype/item/item.py:962 +#: erpnext/stock/doctype/item/item.py:972 msgid "Variant Items" msgstr "العناصر المتغيرة" @@ -60276,7 +60373,7 @@ msgstr "" #: erpnext/stock/report/available_serial_no/available_serial_no.js:56 #: erpnext/stock/report/available_serial_no/available_serial_no.py:196 #: erpnext/stock/report/stock_ledger/stock_ledger.js:97 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:404 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:407 msgid "Voucher #" msgstr "سند #" @@ -60375,12 +60472,12 @@ msgstr "" #: erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py:112 #: erpnext/stock/report/serial_no_ledger/serial_no_ledger.py:34 #: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:159 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:176 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:185 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:74 msgid "Voucher No" msgstr "رقم السند" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1485 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1535 msgid "Voucher No is mandatory" msgstr "رقم القسيمة إلزامي" @@ -60449,8 +60546,8 @@ msgstr "نوع القسيمة الفرعي" #: erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.py:486 #: erpnext/stock/report/serial_no_ledger/serial_no_ledger.py:28 #: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:157 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:402 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:170 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:405 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:179 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:68 msgid "Voucher Type" msgstr "نوع السند" @@ -60658,6 +60755,7 @@ msgid "Warehouse {0} does not belong to company {1}" msgstr "مستودع {0} لا تنتمي إلى شركة {1}" #: erpnext/stock/doctype/warehouse/warehouse.py:288 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:99 msgid "Warehouse {0} does not exist" msgstr "المستودع {0} غير موجود" @@ -60665,11 +60763,11 @@ msgstr "المستودع {0} غير موجود" msgid "Warehouse {0} is not allowed for Sales Order {1}, it should be {2}" msgstr "لا يُسمح باستخدام المستودع {0} في أمر البيع {1}، بل يجب أن يكون {2}" -#: erpnext/controllers/stock_controller.py:861 +#: erpnext/controllers/stock_controller.py:875 msgid "Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}." msgstr "المستودع {0} غير مرتبط بأي حساب، يرجى ذكر الحساب في سجل المستودع أو تعيين حساب المخزون الافتراضي في الشركة {1}." -#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:20 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:26 msgid "Warehouse: {0} does not belong to {1}" msgstr "المستودع: {0} لا ينتمي إلى {1}" @@ -60778,7 +60876,7 @@ msgstr "" msgid "Warning - Row {0}: Billing Hours are more than Actual Hours" msgstr "تحذير - الصف {0}: ساعات الفوترة أكثر من الساعات الفعلية" -#: erpnext/stock/stock_ledger.py:849 +#: erpnext/stock/stock_ledger.py:876 msgid "Warning on Negative Stock" msgstr "تحذير بشأن الأسهم السلبية" @@ -60790,11 +60888,11 @@ msgstr "" msgid "Warning: Account changed for warehouse" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1331 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1341 msgid "Warning: Another {0} # {1} exists against stock entry {2}" msgstr "تحذير: {0} أخر # {1} موجود في مدخل المخزن {2}\\n
    \\nWarning: Another {0} # {1} exists against stock entry {2}" -#: erpnext/stock/doctype/material_request/material_request.js:709 +#: erpnext/stock/doctype/material_request/material_request.js:705 msgid "Warning: Material Requested Qty is less than Minimum Order Qty" msgstr "تحذير : كمية المواد المطلوبة هي أقل من الحد الأدنى للطلب الكمية" @@ -60892,7 +60990,7 @@ msgstr "الطول الموجي بالكيلومترات" msgid "Wavelength In Megametres" msgstr "الطول الموجي بالميغامتر" -#: erpnext/controllers/accounts_controller.py:212 +#: erpnext/controllers/accounts_controller.py:217 msgid "We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, uncheck the '{2}' checkbox." msgstr "" @@ -61114,7 +61212,7 @@ msgstr "" msgid "When enabled, transactions with this supplier will be blocked based on the Hold Type below" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:408 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:417 msgid "When there are multiple finished goods ({0}) in a Repack stock entry, the basic rate for all finished goods must be set manually. To set rate manually, enable the checkbox 'Set Basic Rate Manually' in the respective finished good row." msgstr "" @@ -61353,7 +61451,7 @@ msgstr "المواد المستهلكة في أمر العمل" msgid "Work Order Item" msgstr "بند أمر العمل" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1027 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1036 msgid "Work Order Mismatch" msgstr "" @@ -61415,11 +61513,11 @@ msgstr "أمر العمل لم يتم إنشاؤه" msgid "Work Order {0} created" msgstr "تم إنشاء أمر العمل {0}" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2740 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2772 msgid "Work Order {0} has no produced qty" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1151 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1160 msgid "Work Order {0}: Job Card not found for the operation {1}" msgstr "" @@ -61747,7 +61845,7 @@ msgstr "تاريخ البدء أو تاريخ الانتهاء العام يتد msgid "You are importing data for the code list:" msgstr "أنت بصدد استيراد بيانات لقائمة الرموز:" -#: erpnext/controllers/accounts_controller.py:3929 +#: erpnext/controllers/accounts_controller.py:3959 msgid "You are not allowed to update as per the conditions set in {} Workflow." msgstr "" @@ -61763,6 +61861,10 @@ msgstr "أنت غير مخول بإجراء/تعديل معاملات المخز msgid "You are not authorized to set Frozen value" msgstr ".أنت غير مخول لتغيير القيم المجمدة" +#: erpnext/projects/doctype/task/task.py:317 +msgid "You are not permitted to create a Task for Project {0}" +msgstr "" + #: erpnext/stock/doctype/pick_list/pick_list.py:546 msgid "You are picking more than required quantity for the item {0}. Check if there is any other pick list created for the sales order {1}." msgstr "أنت تختار كمية أكبر من الكمية المطلوبة للصنف {0}. تحقق مما إذا كانت هناك أي قائمة اختيار أخرى تم إنشاؤها لطلب البيع {1}." @@ -61820,7 +61922,7 @@ msgstr "يمكنك تعيينه كاسم للآلة أو نوع العملية. msgid "You can set up the rule to split the transaction across multiple accounts." msgstr "" -#: erpnext/controllers/accounts_controller.py:233 +#: erpnext/controllers/accounts_controller.py:238 msgid "You can use {0} to reconcile against {1} later." msgstr "" @@ -61852,7 +61954,7 @@ msgstr "" msgid "You cannot create/amend any accounting entries till this date." msgstr "لا يمكنك إنشاء/تعديل أي قيود محاسبية حتى هذا التاريخ." -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:951 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:961 msgid "You cannot credit and debit same account at the same time" msgstr "لا يمكن إعطاء الحساب قيمة مدين وقيمة دائن في نفس الوقت" @@ -61880,7 +61982,7 @@ msgstr "لا يمكنك استرداد أكثر من {0}." msgid "You cannot repost item valuation before {}" msgstr "لا يمكنك إعادة نشر تقييم العنصر قبل {}" -#: erpnext/accounts/doctype/subscription/subscription.py:758 +#: erpnext/accounts/doctype/subscription/subscription.py:766 msgid "You cannot restart a Subscription that is not cancelled." msgstr "لا يمكنك إعادة تشغيل اشتراك غير ملغى." @@ -61892,7 +61994,7 @@ msgstr "" msgid "You cannot submit the order without payment." msgstr "لا يمكنك تقديم الطلب بدون دفع." -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:116 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:119 msgid "You cannot {0} this document because another Period Closing Entry {1} exists after {2}" msgstr "" @@ -61909,7 +62011,7 @@ msgstr "" msgid "You do not have permission to import bank transactions" msgstr "" -#: erpnext/controllers/accounts_controller.py:3907 +#: erpnext/controllers/accounts_controller.py:3937 msgid "You do not have permissions to {} items in a {}." msgstr "" @@ -61921,11 +62023,11 @@ msgstr "ليس لديك ما يكفي من نقاط الولاء لاستردا msgid "You don't have enough points to redeem." msgstr "ليس لديك ما يكفي من النقاط لاستردادها." -#: erpnext/controllers/accounts_controller.py:4475 +#: erpnext/controllers/accounts_controller.py:4505 msgid "You don't have permission to create a Company Address. Please contact your System Manager." msgstr "" -#: erpnext/controllers/accounts_controller.py:4455 +#: erpnext/controllers/accounts_controller.py:4485 msgid "You don't have permission to update Company details. Please contact your System Manager." msgstr "" @@ -61933,7 +62035,7 @@ msgstr "" msgid "You don't have permission to update Received Qty DocField for item {0}" msgstr "" -#: erpnext/controllers/accounts_controller.py:4449 +#: erpnext/controllers/accounts_controller.py:4479 msgid "You don't have permission to update this document. Please contact your System Manager." msgstr "" @@ -61941,7 +62043,7 @@ msgstr "" msgid "You had {} errors while creating opening invoices. Check {} for more details" msgstr "" -#: erpnext/public/js/utils.js:1064 +#: erpnext/public/js/utils.js:1090 msgid "You have already selected items from {0} {1}" msgstr "لقد حددت العناصر من {0} {1}" @@ -61949,7 +62051,7 @@ msgstr "لقد حددت العناصر من {0} {1}" msgid "You have been invited to collaborate on the project {0}." msgstr "لقد تمت دعوتك للمشاركة في المشروع {0}." -#: erpnext/stock/doctype/stock_settings/stock_settings.py:255 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:260 msgid "You have enabled {0} and {1} in {2}. This can lead to prices from the default price list being inserted in the transaction price list." msgstr "لقد قمت بتفعيل {0} و {1} في {2}. قد يؤدي هذا إلى إدراج أسعار من قائمة الأسعار الافتراضية في قائمة أسعار المعاملة." @@ -61969,7 +62071,7 @@ msgstr "" msgid "You have not performed any reconciliations in this session yet." msgstr "" -#: erpnext/stock/doctype/item/item.py:1187 +#: erpnext/stock/doctype/item/item.py:1197 msgid "You have to enable auto re-order in Stock Settings to maintain re-order levels." msgstr "يجب عليك تمكين الطلب التلقائي في إعدادات الأسهم للحفاظ على مستويات إعادة الطلب." @@ -61989,7 +62091,7 @@ msgstr "يجب عليك تحديد عميل قبل إضافة عنصر." msgid "You need to cancel POS Closing Entry {} to be able to cancel this document." msgstr "" -#: erpnext/controllers/accounts_controller.py:3250 +#: erpnext/controllers/accounts_controller.py:3255 msgid "You selected the account group {1} as {2} Account in row {0}. Please select a single account." msgstr "لقد اخترت مجموعة الحسابات {1} كحساب {2} في الصف {0}. يرجى اختيار حساب واحد." @@ -62049,7 +62151,7 @@ msgstr "رصيد صفري" msgid "Zero Rated" msgstr "معدل صفري" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:738 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:747 msgid "Zero quantity" msgstr "الكمية صفر" @@ -62075,7 +62177,7 @@ msgstr "[هام] [ERPNext] إعادة ترتيب الأخطاء تلقائيًا msgid "`Allow Negative rates for Items`" msgstr "السماح بأسعار سلبية للعناصر" -#: erpnext/stock/stock_ledger.py:2048 +#: erpnext/stock/stock_ledger.py:2091 msgid "after" msgstr "بعد" @@ -62095,7 +62197,7 @@ msgstr "كعنوان" msgid "as a percentage of finished item quantity" msgstr "كنسبة مئوية من كمية المنتج النهائي" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1655 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1705 msgid "as of {0}" msgstr "" @@ -62115,7 +62217,7 @@ msgstr "بواسطة {}" msgid "cannot be greater than 100" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:341 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:351 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1158 msgid "dated {0}" msgstr "مؤرخة {0}" @@ -62267,7 +62369,7 @@ msgstr "" msgid "per hour" msgstr "كل ساعة" -#: erpnext/stock/stock_ledger.py:2049 +#: erpnext/stock/stock_ledger.py:2092 msgid "performing either one below:" msgstr "أداء أحد الخيارين التاليين:" @@ -62339,12 +62441,12 @@ msgstr "رمل" msgid "sold" msgstr "تم البيع" -#: erpnext/accounts/doctype/subscription/subscription.py:734 +#: erpnext/accounts/doctype/subscription/subscription.py:742 msgid "subscription is already cancelled." msgstr "تم إلغاء الاشتراك بالفعل." -#: erpnext/controllers/status_updater.py:504 -#: erpnext/controllers/status_updater.py:523 +#: erpnext/controllers/status_updater.py:505 +#: erpnext/controllers/status_updater.py:524 msgid "target_ref_field" msgstr "حقل مرجع الهدف" @@ -62411,7 +62513,7 @@ msgstr "عبر أداة تحديث قائمة المواد" msgid "you must select Capital Work in Progress Account in accounts table" msgstr "" -#: erpnext/controllers/accounts_controller.py:1313 +#: erpnext/controllers/accounts_controller.py:1318 msgid "{0} '{1}' is disabled" msgstr "{0} '{1}' معطل" @@ -62427,7 +62529,7 @@ msgstr "{0} ({1}) لا يمكن أن يكون أكبر من الكمية الم msgid "{0} {1} has submitted Assets. Remove Item {2} from table to continue." msgstr "قام كل من {0} و و{1}و بإرسال الأصول. للمتابعة، قم بإزالة العنصر و{2}و من الجدول." -#: erpnext/controllers/accounts_controller.py:2410 +#: erpnext/controllers/accounts_controller.py:2415 msgid "{0} Account not found against Customer {1}." msgstr "{0} لم يتم العثور على حساب مقابل العميل {1}." @@ -62487,19 +62589,19 @@ msgstr "الحساب {0} ليس من النوع {1}" msgid "{0} account not found while submitting purchase receipt" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1071 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1081 msgid "{0} against Bill {1} dated {2}" msgstr "{0} مقابل الفاتورة {1} بتاريخ {2}" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1080 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1090 msgid "{0} against Purchase Order {1}" msgstr "{0} مقابل أمر الشراء {1}" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1047 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1057 msgid "{0} against Sales Invoice {1}" msgstr "{0} مقابل فاتورة المبيعات {1}" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1054 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1064 msgid "{0} against Sales Order {1}" msgstr "{0} مقابل طلب مبيعات {1}" @@ -62565,7 +62667,7 @@ msgstr "{0} لديه حاليا {1} بطاقة أداء بطاقة المورد msgid "{0} does not belong to Company {1}" msgstr "{0} لا تنتمي إلى شركة {1}" -#: erpnext/controllers/accounts_controller.py:372 +#: erpnext/controllers/accounts_controller.py:377 msgid "{0} does not belong to the Company {1}." msgstr "لا ينتمي {0} إلى الشركة {1}." @@ -62607,7 +62709,7 @@ msgstr "{0} تم التقديم بنجاح" msgid "{0} hours" msgstr "{0} ساعات" -#: erpnext/controllers/accounts_controller.py:2770 +#: erpnext/controllers/accounts_controller.py:2775 msgid "{0} in row {1}" msgstr "{0} في الحقل {1}" @@ -62637,7 +62739,7 @@ msgstr "" msgid "{0} is already running for {1}" msgstr "{0} قيد التشغيل بالفعل لـ {1}" -#: erpnext/controllers/accounts_controller.py:194 +#: erpnext/controllers/accounts_controller.py:199 msgid "{0} is blocked so this transaction cannot proceed" msgstr "تم حظر {0} حتى لا تتم متابعة هذه المعاملة" @@ -62666,15 +62768,15 @@ msgstr "{0} إلزامي للحساب {1}" msgid "{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}" msgstr "{0} إلزامي. ربما لم يتم إنشاء سجل صرف العملات من {1} إلى {2}" -#: erpnext/controllers/accounts_controller.py:3207 +#: erpnext/controllers/accounts_controller.py:3212 msgid "{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}." msgstr "{0} إلزامي. ربما لم يتم إنشاء سجل سعر صرف العملة ل{1} إلى {2}." -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1879 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1929 msgid "{0} is not a CSV file." msgstr "" -#: erpnext/selling/doctype/customer/customer.py:244 +#: erpnext/selling/doctype/customer/customer.py:245 msgid "{0} is not a company bank account" msgstr "{0} ليس حسابًا مصرفيًا للشركة" @@ -62682,7 +62784,7 @@ msgstr "{0} ليس حسابًا مصرفيًا للشركة" msgid "{0} is not a group node. Please select a group node as parent cost center" msgstr "{0} ليست عقدة مجموعة. يرجى تحديد عقدة المجموعة كمركز تكلفة الأصل" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:790 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:799 msgid "{0} is not a stock Item" msgstr "{0} ليس من نوع المخزون" @@ -62754,7 +62856,7 @@ msgstr "" msgid "{0} languages are marked as default languages. Please select only one of them." msgstr "" -#: erpnext/controllers/sales_and_purchase_return.py:218 +#: erpnext/controllers/sales_and_purchase_return.py:236 msgid "{0} must be negative in return document" msgstr "{0} يجب أن يكون سالبة في وثيقة الارجاع" @@ -62774,7 +62876,7 @@ msgstr "{0} المعلمة غير صالحة" msgid "{0} payment entries can not be filtered by {1}" msgstr "{0} لا يمكن فلترة المدفوعات المدخلة {1}" -#: erpnext/controllers/stock_controller.py:1903 +#: erpnext/controllers/stock_controller.py:1917 msgid "{0} qty of Item {1} is being received into Warehouse {2} with capacity {3}." msgstr "يتم استلام كمية {0} من الصنف {1} في المستودع {2} بسعة {3}." @@ -62803,16 +62905,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 "يلزم {0} وحدة من {1} في {2} مع بُعد المخزون: {3} على {4} {5} لـ {6} لإكمال المعاملة." -#: erpnext/stock/stock_ledger.py:1701 erpnext/stock/stock_ledger.py:2216 -#: erpnext/stock/stock_ledger.py:2230 +#: erpnext/stock/stock_ledger.py:1744 erpnext/stock/stock_ledger.py:2259 +#: erpnext/stock/stock_ledger.py:2273 msgid "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction." msgstr "{0} وحدات من {1} لازمة ل {2} في {3} {4} ل {5} لإكمال هذه المعاملة." -#: erpnext/stock/stock_ledger.py:2317 erpnext/stock/stock_ledger.py:2362 +#: erpnext/stock/stock_ledger.py:2360 erpnext/stock/stock_ledger.py:2405 msgid "{0} units of {1} needed in {2} on {3} {4} to complete this transaction." msgstr "{0} وحدة من {1} مطلوبة في {2} على {3} {4} لإكمال هذه المعاملة." -#: erpnext/stock/stock_ledger.py:1695 +#: erpnext/stock/stock_ledger.py:1738 msgid "{0} units of {1} needed in {2} to complete this transaction." msgstr "{0} وحدات من {1} لازمة في {2} لإكمال هذه المعاملة." @@ -62904,6 +63006,14 @@ msgstr "{0} {1} مرتبط بالفعل بالرمز المشترك {2}." msgid "{0} {1} is associated with {2}, but Party Account is {3}" msgstr "{0} {1} مرتبط ب {2}، ولكن حساب الطرف هو {3}" +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:911 +msgid "{0} {1} is blocked and on hold until {2}." +msgstr "" + +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:915 +msgid "{0} {1} is blocked." +msgstr "" + #: erpnext/controllers/selling_controller.py:494 #: erpnext/controllers/subcontracting_controller.py:1174 msgid "{0} {1} is cancelled or closed" @@ -62958,7 +63068,7 @@ msgstr "{0} {1} معلق" msgid "{0} {1} must be submitted" msgstr "{0} {1} يجب أن يتم اعتماده\\n
    \\n{0} {1} must be submitted" -#: erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py:501 +#: erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py:506 msgid "{0} {1} not allowed to be reposted. You can enable it by adding it '{2}' table in {3}." msgstr "" @@ -62993,7 +63103,7 @@ msgstr "{0} {1}: الحساب {2} غير فعال \\n
    \\n{0} {1}: Account {2} msgid "{0} {1}: Accounting Entry for {2} can only be made in currency: {3}" msgstr "{0} {1}: قيد محاسبي ل {2} يمكن ان يتم فقط بالعملة : {3}" -#: erpnext/controllers/stock_controller.py:1073 +#: erpnext/controllers/stock_controller.py:1087 msgid "{0} {1}: Cost Center is mandatory for Item {2}" msgstr "{0} {1}: مركز التكلفة إلزامي للبند {2}" @@ -63038,7 +63148,7 @@ msgstr "" msgid "{0}% of total invoice value will be given as discount." msgstr "" -#: erpnext/projects/doctype/task/task.py:130 +#: erpnext/projects/doctype/task/task.py:131 msgid "{0}'s {1} cannot be after {2}'s Expected End Date." msgstr "" @@ -63075,7 +63185,7 @@ msgstr "" msgid "{0}: select the typed value {1} from the list or clear it" msgstr "" -#: erpnext/controllers/accounts_controller.py:562 +#: erpnext/controllers/accounts_controller.py:567 msgid "{0}: {1} does not belong to the Company: {2}" msgstr "{0}: {1} لا ينتمي إلى الشركة: {2}" @@ -63103,11 +63213,11 @@ msgstr "{doctype} {name} تم إلغائه أو مغلق." msgid "{field_label} is mandatory for sub-contracted {doctype}." msgstr "{field_label} إلزامي للمقاولين من الباطن {doctype}." -#: erpnext/controllers/stock_controller.py:2369 +#: erpnext/controllers/stock_controller.py:2383 msgid "{item_name}'s Sample Size ({sample_size}) cannot be greater than the Accepted Quantity ({accepted_quantity})" msgstr "لا يمكن أن يكون حجم العينة {item_name}({sample_size}) أكبر من الكمية المقبولة ({accepted_quantity})" -#: erpnext/controllers/stock_controller.py:2132 +#: erpnext/controllers/stock_controller.py:2146 msgid "{ref_doctype} {ref_name} status is {status}." msgstr "{ref_doctype} {ref_name} الحالة {status}." diff --git a/erpnext/locale/bg.po b/erpnext/locale/bg.po index 0e69d727d33..3316ce1c0e4 100644 --- a/erpnext/locale/bg.po +++ b/erpnext/locale/bg.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: hello@frappe.io\n" -"POT-Creation-Date: 2026-08-02 10:09+0000\n" -"PO-Revision-Date: 2026-08-03 08:59\n" +"POT-Creation-Date: 2026-08-09 09:47+0000\n" +"PO-Revision-Date: 2026-08-09 11:01\n" "Last-Translator: hello@frappe.io\n" "Language-Team: Bulgarian\n" "MIME-Version: 1.0\n" @@ -18,7 +18,7 @@ msgstr "" "X-Crowdin-File-ID: 169\n" "Language: bg_BG\n" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1657 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1707 msgid "\n" "\t\t\tThe Batch {0} of an item {1} has negative stock in the warehouse {2}{3}.\n" "\t\t\tPlease add a stock quantity of {4} to proceed with this entry.\n" @@ -40,7 +40,7 @@ msgstr "" msgid " Amount" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:133 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:122 msgid " BOM" msgstr "" @@ -59,7 +59,7 @@ msgstr "" msgid " Is Subcontracted" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:215 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:204 msgid " Item" msgstr "" @@ -68,8 +68,8 @@ msgstr "" msgid " Name" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:163 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:204 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:152 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:193 msgid " Phantom Item" msgstr "" @@ -77,7 +77,7 @@ msgstr "" msgid " Rate" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:141 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:130 msgid " Raw Material" msgstr "" @@ -86,8 +86,8 @@ msgstr "" msgid " Skip Material Transfer" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:152 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:193 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:141 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:182 msgid " Sub Assembly" msgstr "" @@ -272,7 +272,7 @@ msgstr "" msgid "% of materials delivered against this Sales Order" msgstr "" -#: erpnext/controllers/accounts_controller.py:2414 +#: erpnext/controllers/accounts_controller.py:2419 msgid "'Account' in the Accounting section of Customer {0}" msgstr "" @@ -288,11 +288,11 @@ msgstr "" msgid "'Days Since Last Order' must be greater than or equal to zero" msgstr "" -#: erpnext/controllers/accounts_controller.py:2419 +#: erpnext/controllers/accounts_controller.py:2424 msgid "'Default {0} Account' in Company {1}" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1235 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1245 msgid "'Entries' cannot be empty" msgstr "" @@ -310,17 +310,17 @@ msgstr "" msgid "'Has Serial No' can not be 'Yes' for non-stock item" msgstr "" -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:147 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:151 msgid "'Inspection Required before Delivery' has disabled for the item {0}, no need to create the QI" msgstr "" -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:138 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:142 msgid "'Inspection Required before Purchase' has disabled for the item {0}, no need to create the QI" msgstr "" -#: erpnext/stock/report/stock_ledger/stock_ledger.py:685 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:726 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:831 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:688 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:781 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:913 msgid "'Opening'" msgstr "" @@ -360,17 +360,17 @@ msgid "'{0}' should be in company currency {1}." msgstr "" #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:174 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:214 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:223 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:106 msgid "(A) Qty After Transaction" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:219 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:228 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:111 msgid "(B) Expected Qty After Transaction" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:234 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:243 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:126 msgid "(C) Total Qty in Queue" msgstr "" @@ -380,7 +380,7 @@ msgid "(C) Total qty in queue" msgstr "" #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:194 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:244 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:253 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:136 msgid "(D) Balance Stock Value" msgstr "" @@ -391,12 +391,12 @@ msgid "(Daily Yield * No of Units Produced) / 100" msgstr "" #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:199 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:249 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:258 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:141 msgid "(E) Balance Stock Value in Queue" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:259 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:268 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:151 msgid "(F) Change in Stock Value" msgstr "" @@ -405,7 +405,7 @@ msgstr "" msgid "(Forecast)" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:264 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:273 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:156 msgid "(G) Sum of Change in Stock Value" msgstr "" @@ -416,7 +416,7 @@ msgstr "" msgid "(Good Units Produced / Total Units Produced) × 100" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:274 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:283 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:166 msgid "(H) Change in Stock Value (FIFO Queue)" msgstr "" @@ -431,17 +431,17 @@ msgstr "" msgid "(Hour Rate / 60) * Actual Operation Time" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:284 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:293 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:176 msgid "(I) Valuation Rate" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:289 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:298 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:181 msgid "(J) Valuation Rate as per FIFO" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:299 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:308 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:191 msgid "(K) Valuation = Value (D) ÷ Qty (A)" msgstr "" @@ -776,7 +776,7 @@ msgstr "" msgid "
  • Clearance date must be after cheque date for row(s): {0}
  • " msgstr "" -#: erpnext/controllers/accounts_controller.py:2297 +#: erpnext/controllers/accounts_controller.py:2302 msgid "
  • Item {0} in row(s) {1} billed more than {2}
  • " msgstr "" @@ -793,7 +793,7 @@ msgstr "" msgid "
  • {}
  • " msgstr "" -#: erpnext/controllers/accounts_controller.py:2294 +#: erpnext/controllers/accounts_controller.py:2299 msgid "

    Cannot overbill for the following Items:

    " msgstr "" @@ -837,7 +837,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:2306 +#: erpnext/controllers/accounts_controller.py:2311 msgid "

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

    " msgstr "" @@ -948,18 +948,18 @@ msgid "\n" "
    \n\n\n\n\n\n\n" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:224 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:233 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:116 msgid "A - B" msgstr "" #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:189 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:239 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:248 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:131 msgid "A - C" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:365 +#: erpnext/selling/doctype/customer/customer.py:366 msgid "A Customer Group exists with same name please change the Customer name or rename the Customer Group" msgstr "" @@ -993,7 +993,7 @@ msgstr "" msgid "A Reconciliation Job {0} is running for the same filters. Cannot reconcile now" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1773 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1783 msgid "A Reverse Journal Entry {0} already exists for this Journal Entry." msgstr "" @@ -1034,7 +1034,7 @@ msgstr "" msgid "A logical Warehouse against which stock entries are made." msgstr "" -#: erpnext/stock/serial_batch_bundle.py:1479 +#: erpnext/stock/serial_batch_bundle.py:1565 msgid "A naming series conflict occurred while creating serial numbers. Please change the naming series for the item {0}." msgstr "" @@ -1218,7 +1218,7 @@ msgid "Accepted Qty in Stock UOM" msgstr "" #. Label of the qty (Float) field in DocType 'Purchase Receipt Item' -#: erpnext/public/js/controllers/transaction.js:2870 +#: erpnext/public/js/controllers/transaction.js:2875 #: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json msgid "Accepted Quantity" msgstr "" @@ -1254,7 +1254,7 @@ msgstr "" msgid "According to CEFACT/ICG/2010/IC013 or CEFACT/ICG/2010/IC010" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1281 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1290 msgid "According to the BOM {0}, the Item '{1}' is missing in the stock entry." msgstr "" @@ -1378,7 +1378,7 @@ msgid "Account Manager" msgstr "" #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1063 -#: erpnext/controllers/accounts_controller.py:2423 +#: erpnext/controllers/accounts_controller.py:2428 msgid "Account Missing" msgstr "" @@ -1618,7 +1618,7 @@ msgstr "" msgid "Account {0} is frozen" msgstr "" -#: erpnext/controllers/accounts_controller.py:1498 +#: erpnext/controllers/accounts_controller.py:1503 msgid "Account {0} is invalid. Account Currency must be {1}" msgstr "" @@ -1654,7 +1654,7 @@ msgstr "" msgid "Account: {0} is not permitted under Payment Entry" msgstr "" -#: erpnext/controllers/accounts_controller.py:3307 +#: erpnext/controllers/accounts_controller.py:3312 msgid "Account: {0} with currency: {1} can not be selected" msgstr "" @@ -1939,8 +1939,8 @@ msgstr "" msgid "Accounting Entry for Asset" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2364 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2384 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2396 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2416 msgid "Accounting Entry for LCV in Stock Entry {0}" msgstr "" @@ -1952,20 +1952,20 @@ msgstr "" msgid "Accounting Entry for Service" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1046 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1067 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1085 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1106 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1127 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1155 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1267 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1532 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1554 -#: erpnext/controllers/stock_controller.py:773 -#: erpnext/controllers/stock_controller.py:790 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1056 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1077 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1095 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1116 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1137 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1165 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1277 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1542 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1564 +#: erpnext/controllers/stock_controller.py:787 +#: erpnext/controllers/stock_controller.py:804 #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:930 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2309 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2323 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2341 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2355 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:753 msgid "Accounting Entry for Stock" msgstr "" @@ -1974,7 +1974,7 @@ msgstr "" msgid "Accounting Entry for {0}" msgstr "" -#: erpnext/controllers/accounts_controller.py:2464 +#: erpnext/controllers/accounts_controller.py:2469 msgid "Accounting Entry for {0}: {1} can only be made in currency: {2}" msgstr "" @@ -2171,7 +2171,7 @@ msgstr "" msgid "Accounts Setup" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1338 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1348 msgid "Accounts table cannot be blank." msgstr "" @@ -2638,7 +2638,7 @@ msgstr "" msgid "Add Employees" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:275 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:264 #: erpnext/selling/doctype/sales_order/sales_order.js:285 #: erpnext/stock/dashboard/item_dashboard.js:216 msgid "Add Item" @@ -2690,8 +2690,8 @@ msgstr "" msgid "Add Order Discount" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:300 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:435 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:289 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:424 msgid "Add Phantom Item" msgstr "" @@ -2768,8 +2768,8 @@ msgstr "" msgid "Add Stock" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:300 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:435 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:289 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:424 msgid "Add Sub Assembly" msgstr "" @@ -3369,7 +3369,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:306 +#: erpnext/controllers/accounts_controller.py:311 #: erpnext/setup/doctype/company/company.json msgid "Advance Payments" msgstr "" @@ -3588,7 +3588,7 @@ msgstr "" msgid "Against Stock Entry" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:339 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:349 msgid "Against Supplier Invoice {0}" msgstr "" @@ -3919,11 +3919,11 @@ msgstr "" msgid "All items have already been received" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3696 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3728 msgid "All items have already been transferred for this Work Order." msgstr "" -#: erpnext/public/js/controllers/transaction.js:2993 +#: erpnext/public/js/controllers/transaction.js:2998 msgid "All items in this document already have a linked Quality Inspection." msgstr "" @@ -4157,8 +4157,8 @@ msgstr "" #. Valuation' #: erpnext/stock/doctype/item/item.json #: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json -#: erpnext/stock/doctype/stock_settings/stock_settings.py:217 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:229 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:222 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:234 msgid "Allow Negative Stock" msgstr "" @@ -4778,7 +4778,7 @@ msgstr "" #: erpnext/manufacturing/doctype/bom_item/bom_item.json #: erpnext/manufacturing/doctype/work_order_additional_item/work_order_additional_item.json #: erpnext/manufacturing/doctype/work_order_item/work_order_item.json -#: erpnext/public/js/controllers/transaction.js:569 +#: erpnext/public/js/controllers/transaction.js:571 #: erpnext/selling/doctype/quotation/quotation.js:315 #: erpnext/selling/doctype/quotation_item/quotation_item.json #: erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -5524,7 +5524,7 @@ msgstr "" msgid "Are you sure you want to clear all demo data?" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:499 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:488 msgid "Are you sure you want to delete this Item?" msgstr "" @@ -5602,11 +5602,11 @@ msgstr "" msgid "As the field {0} is enabled, the value of the field {1} should be more than 1." msgstr "" -#: erpnext/stock/doctype/item/item.py:1094 +#: erpnext/stock/doctype/item/item.py:1104 msgid "As there are existing submitted transactions against item {0}, you can not change the value of {1}." msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:242 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:247 msgid "As there are reserved stock, you cannot disable {0}." msgstr "" @@ -5614,12 +5614,12 @@ msgstr "" msgid "As there are sufficient Sub Assembly Items, Work Order is not required for Warehouse {0}." msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1850 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1849 msgid "As there are sufficient raw materials, Material Request is not required for Warehouse {0}." msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:216 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:228 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:221 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:233 msgid "As {0} is enabled, you can not enable {1}." msgstr "" @@ -6229,7 +6229,7 @@ msgstr "" msgid "At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} in the warehouse {4}." msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1502 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1552 msgid "At Row {0}: In Serial and Batch Bundle {1} must have docstatus as 1 and not 0" msgstr "" @@ -6245,7 +6245,7 @@ msgstr "" msgid "At least one invoice has to be selected." msgstr "" -#: erpnext/controllers/sales_and_purchase_return.py:168 +#: erpnext/controllers/sales_and_purchase_return.py:186 msgid "At least one item should be entered with negative quantity in return document" msgstr "" @@ -6262,7 +6262,7 @@ msgstr "" msgid "At least one of the Selling or Buying must be selected" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:428 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:437 msgid "At least one raw material item must be present in the stock entry for the type {0}" msgstr "" @@ -6270,11 +6270,11 @@ msgstr "" msgid "At least one row is required for a financial report template" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:993 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1002 msgid "At least one warehouse is mandatory" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:896 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:905 msgid "At row #{0}: the Difference Account must not be a Stock type account, please change the Account Type for the account {1} or select a different account" msgstr "" @@ -6282,11 +6282,11 @@ msgstr "" msgid "At row #{0}: the sequence id {1} cannot be less than previous row sequence id {2}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:907 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:916 msgid "At row #{0}: you have selected the Difference Account {1}, which is a Cost of Goods Sold type account. Please select a different account" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1250 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1300 msgid "At row {0}: Batch No is mandatory for Item {1}" msgstr "" @@ -6294,15 +6294,15 @@ msgstr "" msgid "At row {0}: Parent Row No cannot be set for item {1}" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1235 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1285 msgid "At row {0}: Qty is mandatory for the batch {1}" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1242 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1292 msgid "At row {0}: Serial No is mandatory for Item {1}" msgstr "" -#: erpnext/controllers/stock_controller.py:721 +#: erpnext/controllers/stock_controller.py:735 msgid "At row {0}: Serial and Batch Bundle {1} has already created. Please remove the values from the serial no or batch no fields." msgstr "" @@ -6366,11 +6366,11 @@ msgstr "" msgid "Attribute Value" msgstr "" -#: erpnext/stock/doctype/item/item.py:884 +#: erpnext/stock/doctype/item/item.py:894 msgid "Attribute Value {0} is not valid for the selected attribute {1}." msgstr "" -#: erpnext/stock/doctype/item/item.py:1030 +#: erpnext/stock/doctype/item/item.py:1040 msgid "Attribute table is mandatory" msgstr "" @@ -6378,19 +6378,19 @@ msgstr "" msgid "Attribute value: {0} must appear only once" msgstr "" -#: erpnext/stock/doctype/item/item.py:873 +#: erpnext/stock/doctype/item/item.py:883 msgid "Attribute {0} is disabled." msgstr "" -#: erpnext/stock/doctype/item/item.py:861 +#: erpnext/stock/doctype/item/item.py:871 msgid "Attribute {0} is not valid for the selected template." msgstr "" -#: erpnext/stock/doctype/item/item.py:1034 +#: erpnext/stock/doctype/item/item.py:1044 msgid "Attribute {0} selected multiple times in Attributes Table" msgstr "" -#: erpnext/stock/doctype/item/item.py:962 +#: erpnext/stock/doctype/item/item.py:972 msgid "Attributes" msgstr "" @@ -6815,7 +6815,7 @@ msgstr "" msgid "Available for use date is required" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1243 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1252 msgid "Available quantity is {0}, you need {1}" msgstr "" @@ -6878,7 +6878,7 @@ msgid "Avg Rate" msgstr "" #: erpnext/stock/report/available_serial_no/available_serial_no.py:154 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:369 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:372 msgid "Avg Rate (Balance Stock)" msgstr "" @@ -7210,7 +7210,7 @@ msgstr "" msgid "BOM Website Operation" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2802 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2834 msgid "BOM and Finished Good Quantity is mandatory for Disassembly" msgstr "" @@ -7342,7 +7342,7 @@ msgstr "" #: erpnext/stock/report/available_serial_no/available_serial_no.py:126 #: erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py:84 #: erpnext/stock/report/stock_balance/stock_balance.py:515 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:332 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:335 msgid "Balance Qty" msgstr "" @@ -7415,7 +7415,7 @@ msgstr "" #: erpnext/stock/report/available_serial_no/available_serial_no.py:174 #: erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py:86 #: erpnext/stock/report/stock_balance/stock_balance.py:522 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:389 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:392 msgid "Balance Value" msgstr "" @@ -8021,8 +8021,8 @@ msgstr "" #: erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py:34 #: erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py:80 #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:158 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:419 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:182 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:422 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:191 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:80 #: erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.js:19 #: erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py:32 @@ -8102,7 +8102,7 @@ msgstr "" #: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js:89 #: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py:115 -#: erpnext/public/js/controllers/transaction.js:2896 +#: erpnext/public/js/controllers/transaction.js:2901 #: erpnext/public/js/utils/barcode_scanner.js:286 #: erpnext/public/js/utils/serial_no_batch_selector.js:449 #: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -8133,11 +8133,11 @@ msgstr "" msgid "Batch No" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1253 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1303 msgid "Batch No is mandatory" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:3547 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:3597 msgid "Batch No {0} does not exists" msgstr "" @@ -8145,7 +8145,7 @@ msgstr "" msgid "Batch No {0} is linked with Item {1} which has serial no. Please scan serial no instead." msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:491 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:541 msgid "Batch No {0} is not present in the original {1} {2}, hence you can't return it against the {1} {2}" msgstr "" @@ -8160,11 +8160,11 @@ msgstr "" msgid "Batch Nos" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2075 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2125 msgid "Batch Nos are created successfully" msgstr "" -#: erpnext/controllers/sales_and_purchase_return.py:1196 +#: erpnext/controllers/sales_and_purchase_return.py:1214 msgid "Batch Not Available for Return" msgstr "" @@ -8233,16 +8233,16 @@ msgstr "" msgid "Batch {0} and Warehouse" msgstr "" -#: erpnext/controllers/sales_and_purchase_return.py:1195 +#: erpnext/controllers/sales_and_purchase_return.py:1213 msgid "Batch {0} is not available in warehouse {1}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3880 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3912 #: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py:290 msgid "Batch {0} of Item {1} has expired." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3886 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3918 msgid "Batch {0} of Item {1} is disabled." msgstr "" @@ -8256,7 +8256,7 @@ msgid "Batch-Wise Balance History" msgstr "" #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:164 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:194 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:203 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:86 msgid "Batchwise Valuation" msgstr "" @@ -8278,7 +8278,7 @@ msgstr "" msgid "Beginning of the current subscription period" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:360 +#: erpnext/accounts/doctype/subscription/subscription.py:363 msgid "Below Subscription Plans are of different currency to the party default billing currency/Company currency: {0}" msgstr "" @@ -8425,7 +8425,7 @@ msgstr "" msgid "Billing Address Name" msgstr "" -#: erpnext/controllers/accounts_controller.py:593 +#: erpnext/controllers/accounts_controller.py:598 msgid "Billing Address does not belong to the {0}" msgstr "" @@ -8502,7 +8502,7 @@ msgstr "" msgid "Billing Interval Count cannot be less than 1" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:409 +#: erpnext/accounts/doctype/subscription/subscription.py:412 msgid "Billing Interval in Subscription Plan must be Month to follow calendar months" msgstr "" @@ -8662,7 +8662,7 @@ msgid "Blanket Orders" msgstr "" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:109 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:271 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:269 msgid "Block Invoice" msgstr "" @@ -8809,7 +8809,7 @@ msgstr "" msgid "Both Receivable Account: {0} and Advance Account: {1} must be of same currency for company: {2}" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:379 +#: erpnext/accounts/doctype/subscription/subscription.py:382 msgid "Both Trial Period Start Date and Trial Period End Date must be set" msgstr "" @@ -9551,19 +9551,19 @@ msgstr "" msgid "Can not filter based on Voucher No, if grouped by Voucher" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1397 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1407 #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2899 msgid "Can only make payment against unbilled {0}" msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1510 -#: erpnext/controllers/accounts_controller.py:3216 +#: erpnext/controllers/accounts_controller.py:3221 #: erpnext/public/js/controllers/accounts.js:100 msgid "Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'" msgstr "" #: erpnext/setup/doctype/company/company.py:210 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:183 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:188 msgid "Can't change the valuation method, as there are transactions against some items which do not have its own valuation method" msgstr "" @@ -9612,7 +9612,7 @@ msgstr "" msgid "Cannot Change Inventory Account Setting" msgstr "" -#: erpnext/controllers/sales_and_purchase_return.py:438 +#: erpnext/controllers/sales_and_purchase_return.py:456 msgid "Cannot Create Return" msgstr "" @@ -9670,7 +9670,7 @@ msgstr "" msgid "Cannot cancel because submitted Stock Entry {0} exists" msgstr "" -#: erpnext/stock/stock_ledger.py:179 +#: erpnext/stock/stock_ledger.py:206 msgid "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet." msgstr "" @@ -9686,15 +9686,15 @@ msgstr "" msgid "Cannot cancel this document as it is linked with the submitted asset {asset_link}. Please cancel the asset to continue." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:671 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:680 msgid "Cannot cancel transaction for Completed Work Order." msgstr "" -#: erpnext/stock/doctype/item/item.py:982 +#: erpnext/stock/doctype/item/item.py:992 msgid "Cannot change Attributes after stock transaction. Make a new Item and transfer stock to the new Item" msgstr "" -#: erpnext/stock/doctype/item/item.py:1119 +#: erpnext/stock/doctype/item/item.py:1129 msgid "Cannot change Item {0} from serialized to non-serialized because a Serial and Batch Bundle exists for it. Please delete or cancel the Serial and Batch Bundle first." msgstr "" @@ -9706,7 +9706,7 @@ msgstr "" msgid "Cannot change Service Stop Date for item in row {0}" msgstr "" -#: erpnext/stock/doctype/item/item.py:973 +#: erpnext/stock/doctype/item/item.py:983 msgid "Cannot change Variant properties after stock transaction. You will have to make a new Item to do this." msgstr "" @@ -9714,7 +9714,7 @@ msgstr "" msgid "Cannot change company's default currency, because there are existing transactions. Transactions must be cancelled to change the default currency." msgstr "" -#: erpnext/projects/doctype/task/task.py:147 +#: erpnext/projects/doctype/task/task.py:148 msgid "Cannot complete task {0} as its dependant task {1} are not completed / cancelled." msgstr "" @@ -9751,7 +9751,7 @@ msgstr "" msgid "Cannot create accounting entries against disabled accounts: {0}" msgstr "" -#: erpnext/controllers/sales_and_purchase_return.py:437 +#: erpnext/controllers/sales_and_purchase_return.py:455 msgid "Cannot create return for consolidated invoice {0}." msgstr "" @@ -9759,7 +9759,7 @@ msgstr "" msgid "Cannot deactivate or cancel BOM as it is linked with other BOMs" msgstr "" -#: erpnext/crm/doctype/opportunity/opportunity.py:282 +#: erpnext/crm/doctype/opportunity/opportunity.py:292 msgid "Cannot declare as Lost because an active Quotation exists." msgstr "" @@ -9776,7 +9776,7 @@ msgstr "" msgid "Cannot delete Serial No {0}, as it is used in stock transactions" msgstr "" -#: erpnext/controllers/accounts_controller.py:3841 +#: erpnext/controllers/accounts_controller.py:3871 msgid "Cannot delete an item which has been ordered" msgstr "" @@ -9789,7 +9789,7 @@ msgstr "" msgid "Cannot delete virtual DocType: {0}. Virtual DocTypes do not have database tables." msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:148 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:153 msgid "Cannot disable Serial and Batch No for Item, as there are existing records for serial / batch." msgstr "" @@ -9797,7 +9797,7 @@ msgstr "" msgid "Cannot disable perpetual inventory, as there are existing Stock Ledger Entries for the company {0}. Please cancel the stock transactions first and try again." msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:129 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:134 msgid "Cannot disable {0} as it may lead to incorrect stock valuation." msgstr "" @@ -9805,7 +9805,7 @@ msgstr "" msgid "Cannot disassemble more than produced quantity." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1036 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1045 msgid "Cannot disassemble {0} qty against Stock Entry {1}. Only {2} qty available to disassemble." msgstr "" @@ -9834,11 +9834,11 @@ msgstr "" msgid "Cannot find Item with this Barcode" msgstr "" -#: erpnext/controllers/accounts_controller.py:3793 -msgid "Cannot find a default warehouse for item {0}. Please set one in the Item Master or in Stock Settings." +#: erpnext/controllers/accounts_controller.py:3810 +msgid "Cannot find a default warehouse for item {0}. Please select one in the Update Items dialog, or set a default in the Item Master or in Stock Settings." msgstr "" -#: erpnext/accounts/party.py:1108 +#: erpnext/accounts/party.py:1110 msgid "Cannot merge {0} '{1}' into '{2}' as both have existing accounting entries in different currencies for company '{3}'." msgstr "" @@ -9858,12 +9858,12 @@ msgstr "" msgid "Cannot receive from customer against negative outstanding" msgstr "" -#: erpnext/controllers/accounts_controller.py:3990 +#: erpnext/controllers/accounts_controller.py:4020 msgid "Cannot reduce quantity than ordered or purchased quantity" msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1523 -#: erpnext/controllers/accounts_controller.py:3231 +#: erpnext/controllers/accounts_controller.py:3236 #: erpnext/public/js/controllers/accounts.js:117 msgid "Cannot refer row number greater than or equal to current row number for this Charge type" msgstr "" @@ -9880,14 +9880,14 @@ msgstr "" msgid "Cannot retrieve link token. Check Error Log for more information" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:378 +#: erpnext/selling/doctype/customer/customer.py:379 msgid "Cannot select a Group type Customer Group. Please select a non-group Customer Group." msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1516 #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1694 #: erpnext/accounts/doctype/payment_entry/payment_entry.py:1848 -#: erpnext/controllers/accounts_controller.py:3221 +#: erpnext/controllers/accounts_controller.py:3226 #: erpnext/public/js/controllers/accounts.js:109 #: erpnext/public/js/controllers/taxes_and_totals.js:570 msgid "Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row" @@ -9905,11 +9905,11 @@ msgstr "" msgid "Cannot set multiple Item Defaults for a company." msgstr "" -#: erpnext/controllers/accounts_controller.py:3956 +#: erpnext/controllers/accounts_controller.py:3986 msgid "Cannot set quantity less than delivered quantity." msgstr "" -#: erpnext/controllers/accounts_controller.py:3957 +#: erpnext/controllers/accounts_controller.py:3987 msgid "Cannot set quantity less than received quantity." msgstr "" @@ -9925,7 +9925,7 @@ msgstr "" msgid "Cannot submit Job Card {0} while it is On Hold. Please resume and complete the job before submission." msgstr "" -#: erpnext/controllers/accounts_controller.py:3984 +#: erpnext/controllers/accounts_controller.py:4014 msgid "Cannot update rate as item {0} is already ordered or purchased against this quotation" msgstr "" @@ -10106,7 +10106,7 @@ msgstr "" msgid "Cash In Hand" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:329 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:339 msgid "Cash or Bank Account is mandatory for making payment entry" msgstr "" @@ -10340,7 +10340,7 @@ msgid "Channel Partner" msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2277 -#: erpnext/controllers/accounts_controller.py:3284 +#: erpnext/controllers/accounts_controller.py:3289 msgid "Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount" msgstr "" @@ -10534,7 +10534,7 @@ msgstr "" #. Label of the reference_date (Date) field in DocType 'Payment Entry' #: erpnext/accounts/doctype/payment_entry/payment_entry.json -#: erpnext/public/js/controllers/transaction.js:2807 +#: erpnext/public/js/controllers/transaction.js:2812 msgid "Cheque/Reference Date" msgstr "" @@ -10592,7 +10592,7 @@ msgstr "" #. Label of the child_row_reference (Data) field in DocType 'Quality #. Inspection' -#: erpnext/public/js/controllers/transaction.js:2902 +#: erpnext/public/js/controllers/transaction.js:2907 #: erpnext/stock/doctype/quality_inspection/quality_inspection.json msgid "Child Row Reference" msgstr "" @@ -10601,7 +10601,7 @@ msgstr "" msgid "Child Table Not Allowed" msgstr "" -#: erpnext/projects/doctype/task/task.py:314 +#: erpnext/projects/doctype/task/task.py:332 msgid "Child Task exists for this Task. You can not delete this Task." msgstr "" @@ -10619,7 +10619,7 @@ msgstr "" msgid "Child warehouse exists for this warehouse. You can not delete this warehouse." msgstr "" -#: erpnext/projects/doctype/task/task.py:262 +#: erpnext/projects/doctype/task/task.py:263 msgid "Circular Reference Error" msgstr "" @@ -10795,6 +10795,10 @@ msgstr "" msgid "Closed Documents" msgstr "" +#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:147 +msgid "Closed Period" +msgstr "" + #: erpnext/manufacturing/doctype/work_order/work_order.py:2775 msgid "Closed Work Order can not be stopped or Re-opened" msgstr "" @@ -10830,7 +10834,7 @@ msgstr "" msgid "Closing Account Head" msgstr "" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:133 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:136 msgid "Closing Account {0} must be of type Liability / Equity" msgstr "" @@ -11549,10 +11553,10 @@ msgstr "" #: erpnext/stock/report/stock_balance/stock_balance.js:8 #: erpnext/stock/report/stock_balance/stock_balance.py:576 #: erpnext/stock/report/stock_ledger/stock_ledger.js:8 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:442 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:445 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js:18 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.js:8 -#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js:8 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js:32 #: erpnext/stock/report/total_stock_summary/total_stock_summary.js:17 #: erpnext/stock/report/total_stock_summary/total_stock_summary.py:29 #: erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.js:8 @@ -11634,11 +11638,11 @@ msgstr "" msgid "Company Address Name" msgstr "" -#: erpnext/controllers/accounts_controller.py:4420 +#: erpnext/controllers/accounts_controller.py:4450 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:4408 +#: erpnext/controllers/accounts_controller.py:4438 msgid "Company Address is missing. You don't have permission to update it. Please contact your System Manager." msgstr "" @@ -11758,7 +11762,7 @@ msgstr "" msgid "Company is mandatory for company account" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:438 +#: erpnext/accounts/doctype/subscription/subscription.py:441 msgid "Company is mandatory for generating an invoice. Please set a default company in Global Defaults." msgstr "" @@ -11881,7 +11885,7 @@ msgstr "" msgid "Completed On" msgstr "" -#: erpnext/projects/doctype/task/task.py:187 +#: erpnext/projects/doctype/task/task.py:188 msgid "Completed On cannot be greater than Today" msgstr "" @@ -12034,7 +12038,7 @@ msgstr "" msgid "Configure Chart of Accounts" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:56 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:45 msgid "Configure Product Assembly" msgstr "" @@ -12336,7 +12340,7 @@ msgstr "" msgid "Consumer Products" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:209 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:218 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:101 msgid "Consumption Rate" msgstr "" @@ -12456,7 +12460,7 @@ msgstr "" msgid "Contact Person" msgstr "" -#: erpnext/controllers/accounts_controller.py:605 +#: erpnext/controllers/accounts_controller.py:610 msgid "Contact Person does not belong to the {0}" msgstr "" @@ -12624,7 +12628,7 @@ msgstr "" #: erpnext/manufacturing/doctype/bom_item/bom_item.json #: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json #: erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json -#: erpnext/public/js/utils.js:920 +#: erpnext/public/js/utils.js:923 #: erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json #: erpnext/stock/doctype/packed_item/packed_item.json #: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -12654,19 +12658,19 @@ msgstr "" msgid "Conversion factor for default Unit of Measure must be 1 in row {0}" msgstr "" -#: erpnext/controllers/stock_controller.py:163 +#: erpnext/controllers/stock_controller.py:177 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:2999 +#: erpnext/controllers/accounts_controller.py:3004 msgid "Conversion rate cannot be 0" msgstr "" -#: erpnext/controllers/accounts_controller.py:3006 +#: erpnext/controllers/accounts_controller.py:3011 msgid "Conversion rate is 1.00, but document currency is different from company currency" msgstr "" -#: erpnext/controllers/accounts_controller.py:3002 +#: erpnext/controllers/accounts_controller.py:3007 msgid "Conversion rate must be 1.00 if document currency is same as company currency" msgstr "" @@ -13020,7 +13024,7 @@ msgstr "" msgid "Cost Center is required" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1498 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1508 #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:898 msgid "Cost Center is required in row {0} in Taxes table for type {1}" msgstr "" @@ -13103,7 +13107,7 @@ msgstr "" msgid "Cost of Goods Sold" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:910 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:919 msgid "Cost of Goods Sold Account in Items Table" msgstr "" @@ -13491,7 +13495,7 @@ msgstr "" msgid "Create Payment Entry for Consolidated POS Invoices." msgstr "" -#: erpnext/public/js/controllers/transaction.js:577 +#: erpnext/public/js/controllers/transaction.js:579 msgid "Create Payment Request" msgstr "" @@ -13595,7 +13599,7 @@ msgid "Create Service Item" msgstr "" #: erpnext/stock/dashboard/item_dashboard.js:283 -#: erpnext/stock/doctype/material_request/material_request.js:653 +#: erpnext/stock/doctype/material_request/material_request.js:649 msgid "Create Stock Entry" msgstr "" @@ -13702,6 +13706,10 @@ msgstr "" msgid "Create Workstation" msgstr "" +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:228 +msgid "Create a Stock Closing Entry for the entire company with To Date as {0} before submitting the Period Closing Voucher." +msgstr "" + #: banking/src/components/features/BankReconciliation/MatchAndReconcile.tsx:683 msgid "Create a journal entry for expenses, income or split transactions" msgstr "" @@ -13719,7 +13727,7 @@ msgstr "" msgid "Create a variant with the template image." msgstr "" -#: erpnext/stock/stock_ledger.py:2052 +#: erpnext/stock/stock_ledger.py:2095 msgid "Create an incoming stock transaction for the Item." msgstr "" @@ -13811,7 +13819,7 @@ msgstr "" msgid "Creating Purchase Order ..." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:729 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:727 #: erpnext/buying/doctype/purchase_order/purchase_order.js:506 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js:74 msgid "Creating Purchase Receipt ..." @@ -13854,7 +13862,7 @@ msgid "Creating {} out of {} {}" msgstr "" #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:141 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:165 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:174 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:46 msgid "Creation" msgstr "" @@ -13990,7 +13998,7 @@ msgstr "" msgid "Credit Limit" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:657 +#: erpnext/selling/doctype/customer/customer.py:658 msgid "Credit Limit Crossed" msgstr "" @@ -14026,7 +14034,7 @@ msgstr "" #: 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:1223 -#: erpnext/controllers/sales_and_purchase_return.py:455 +#: erpnext/controllers/sales_and_purchase_return.py:473 #: erpnext/setup/setup_wizard/operations/install_fixtures.py:306 #: erpnext/stock/doctype/delivery_note/delivery_note.js:89 #: erpnext/stock/doctype/stock_entry/stock_entry.json @@ -14059,9 +14067,9 @@ msgstr "" #. Label of the credit_to (Link) field in DocType 'Purchase Invoice' #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:383 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:391 -#: erpnext/controllers/accounts_controller.py:2403 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:393 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:401 +#: erpnext/controllers/accounts_controller.py:2408 msgid "Credit To" msgstr "" @@ -14070,16 +14078,16 @@ msgstr "" msgid "Credit in Company Currency" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:623 -#: erpnext/selling/doctype/customer/customer.py:678 +#: erpnext/selling/doctype/customer/customer.py:624 +#: erpnext/selling/doctype/customer/customer.py:679 msgid "Credit limit has been crossed for customer {0} ({1}/{2})" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:405 +#: erpnext/selling/doctype/customer/customer.py:406 msgid "Credit limit is already defined for the Company {0}" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:677 +#: erpnext/selling/doctype/customer/customer.py:678 msgid "Credit limit reached for customer {0}" msgstr "" @@ -14272,7 +14280,7 @@ msgstr "" msgid "Currency for {0} must be {1}" msgstr "" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:140 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:143 msgid "Currency of the Closing Account must be {0}" msgstr "" @@ -15211,7 +15219,7 @@ msgid "Cycle/Second" msgstr "" #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:204 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:254 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:263 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:146 msgid "D - E" msgstr "" @@ -15536,7 +15544,7 @@ msgstr "" #: 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:1226 -#: erpnext/controllers/sales_and_purchase_return.py:459 +#: erpnext/controllers/sales_and_purchase_return.py:477 #: erpnext/setup/setup_wizard/operations/install_fixtures.py:307 #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:45 #: erpnext/workspace_sidebar/invoicing.json @@ -15565,7 +15573,7 @@ msgstr "" #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1067 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1078 -#: erpnext/controllers/accounts_controller.py:2403 +#: erpnext/controllers/accounts_controller.py:2408 msgid "Debit To" msgstr "" @@ -15753,7 +15761,7 @@ msgstr "" msgid "Default BOM for {0} not found" msgstr "" -#: erpnext/controllers/accounts_controller.py:4028 +#: erpnext/controllers/accounts_controller.py:4058 msgid "Default BOM not found for FG Item {0}" msgstr "" @@ -16089,15 +16097,15 @@ msgstr "" msgid "Default Unit of Measure" msgstr "" -#: erpnext/stock/doctype/item/item.py:1396 +#: erpnext/stock/doctype/item/item.py:1406 msgid "Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You need to either cancel the linked documents or create a new Item." msgstr "" -#: erpnext/stock/doctype/item/item.py:1379 +#: erpnext/stock/doctype/item/item.py:1389 msgid "Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM." msgstr "" -#: erpnext/stock/doctype/item/item.py:1008 +#: erpnext/stock/doctype/item/item.py:1018 msgid "Default Unit of Measure for Variant '{0}' must be same as in Template '{1}'" msgstr "" @@ -16496,7 +16504,7 @@ msgstr "" #: erpnext/manufacturing/doctype/master_production_schedule_item/master_production_schedule_item.json #: erpnext/manufacturing/doctype/sales_forecast_item/sales_forecast_item.json #: erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.py:1069 -#: erpnext/public/js/utils.js:913 +#: erpnext/public/js/utils.js:916 #: erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json #: erpnext/selling/doctype/sales_order/sales_order.js:632 #: erpnext/selling/doctype/sales_order/sales_order.js:1533 @@ -16752,7 +16760,7 @@ msgstr "" msgid "Dependent Task" msgstr "" -#: erpnext/projects/doctype/task/task.py:180 +#: erpnext/projects/doctype/task/task.py:181 msgid "Dependent Task {0} is not a Template Task" msgstr "" @@ -17045,7 +17053,7 @@ msgstr "" #: erpnext/public/js/bank_reconciliation_tool/number_card.js:30 #: erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py:130 #: erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py:35 -#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:35 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:41 msgid "Difference" msgstr "" @@ -17071,11 +17079,11 @@ msgstr "" msgid "Difference Account" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:899 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:908 msgid "Difference Account in Items Table" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:888 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:897 msgid "Difference Account must be a Asset/Liability type account (Temporary Opening), since this Stock Entry is an Opening Entry" msgstr "" @@ -17297,7 +17305,7 @@ msgstr "" msgid "Disabled items cannot be selected in any transaction." msgstr "" -#: erpnext/controllers/accounts_controller.py:931 +#: erpnext/controllers/accounts_controller.py:936 msgid "Disabled pricing rules since this {} is an internal transfer" msgstr "" @@ -17306,7 +17314,7 @@ msgstr "" msgid "Disabled suppliers are hidden from selection in new transactions but remain in historical records" msgstr "" -#: erpnext/controllers/accounts_controller.py:945 +#: erpnext/controllers/accounts_controller.py:950 msgid "Disabled tax included prices since this {} is an internal transfer" msgstr "" @@ -17334,7 +17342,7 @@ msgstr "" msgid "Disassemble Order" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2744 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2776 msgid "Disassemble Qty cannot be less than or equal to 0." msgstr "" @@ -17849,7 +17857,7 @@ msgstr "" msgid "Do Not Explode" msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:130 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:135 msgid "Do Not Use Batchwise Valuation" msgstr "" @@ -17980,7 +17988,7 @@ msgstr "" msgid "Documents Processed on each trigger. Queue Size should be between 5 and 100" msgstr "" -#: erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py:486 +#: erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py:491 msgid "Documents: {0} have deferred revenue/expense enabled for them. Cannot repost." msgstr "" @@ -18274,11 +18282,11 @@ msgstr "" msgid "Duplicate Sales Invoices found" msgstr "" -#: erpnext/stock/serial_batch_bundle.py:1482 +#: erpnext/stock/serial_batch_bundle.py:1568 msgid "Duplicate Serial Number Error" msgstr "" -#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:81 +#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:123 msgid "Duplicate Stock Closing Entry" msgstr "" @@ -18424,7 +18432,7 @@ msgstr "" msgid "Earnest Money" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:544 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:533 msgid "Edit BOM" msgstr "" @@ -18512,8 +18520,8 @@ msgstr "" msgid "Either 'Selling' or 'Buying' must be selected" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:309 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:460 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:298 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:449 msgid "Either Workstation or Workstation Type is mandatory" msgstr "" @@ -18867,7 +18875,7 @@ msgstr "" msgid "Ems(Pica)" msgstr "" -#: erpnext/public/js/controllers/transaction.js:2965 +#: erpnext/public/js/controllers/transaction.js:2970 msgid "Enable {0} on the Item master to proceed with {1} inspection." msgstr "" @@ -18899,7 +18907,7 @@ msgstr "" msgid "Enable Auto Email" msgstr "" -#: erpnext/stock/doctype/item/item.py:1188 +#: erpnext/stock/doctype/item/item.py:1198 msgid "Enable Auto Re-Order" msgstr "" @@ -19566,7 +19574,7 @@ msgstr "" msgid "Example URL" msgstr "" -#: erpnext/stock/doctype/item/item.py:1100 +#: erpnext/stock/doctype/item/item.py:1110 msgid "Example of a linked document: {0}" msgstr "" @@ -19585,7 +19593,7 @@ msgstr "" msgid "Example: If the transaction amount is 200, then this will be calculated as {} = {}" msgstr "" -#: erpnext/stock/stock_ledger.py:2334 +#: erpnext/stock/stock_ledger.py:2377 msgid "Example: Serial No {0} reserved in {1}." msgstr "" @@ -19595,11 +19603,11 @@ msgstr "" msgid "Exception Budget Approver Role" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1043 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1052 msgid "Excess Disassembly" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1339 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1348 msgid "Excess Material Transfer" msgstr "" @@ -19647,8 +19655,8 @@ msgstr "" msgid "Exchange Gain/Loss" msgstr "" -#: erpnext/controllers/accounts_controller.py:1804 -#: erpnext/controllers/accounts_controller.py:1889 +#: erpnext/controllers/accounts_controller.py:1809 +#: erpnext/controllers/accounts_controller.py:1894 msgid "Exchange Gain/Loss amount has been booked through {0}" msgstr "" @@ -19892,7 +19900,7 @@ msgstr "" msgid "Expected End Date" msgstr "" -#: erpnext/projects/doctype/task/task.py:114 +#: erpnext/projects/doctype/task/task.py:115 msgid "Expected End Date should be less than or equal to parent task's Expected End Date {0}." msgstr "" @@ -19950,7 +19958,7 @@ msgstr "" #: erpnext/accounts/doctype/cashier_closing/cashier_closing.json #: erpnext/accounts/doctype/ledger_merge/ledger_merge.json #: erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.json -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:601 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:611 #: erpnext/accounts/report/account_balance/account_balance.js:28 #: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js:89 #: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:192 @@ -19958,7 +19966,7 @@ msgstr "" msgid "Expense" msgstr "" -#: erpnext/controllers/stock_controller.py:1067 +#: erpnext/controllers/stock_controller.py:1081 msgid "Expense / Difference account ({0}) must be a 'Profit or Loss' account" msgstr "" @@ -20006,7 +20014,7 @@ msgstr "" msgid "Expense Account" msgstr "" -#: erpnext/controllers/stock_controller.py:1047 +#: erpnext/controllers/stock_controller.py:1061 msgid "Expense Account Missing" msgstr "" @@ -20021,13 +20029,13 @@ msgstr "" msgid "Expense Head" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:495 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:519 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:539 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:505 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:529 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:549 msgid "Expense Head Changed" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:597 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:607 msgid "Expense account is mandatory for item {0}" msgstr "" @@ -20059,7 +20067,7 @@ msgstr "" msgid "Expenses Added To Stock Contra Account" msgstr "" -#: erpnext/controllers/stock_controller.py:920 +#: erpnext/controllers/stock_controller.py:934 msgid "Expenses Added To Stock for Item {0}" msgstr "" @@ -20212,7 +20220,7 @@ msgid "FIFO Stock Queue (qty, rate)" msgstr "" #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:179 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:229 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:238 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:121 msgid "FIFO/LIFO Queue" msgstr "" @@ -20431,7 +20439,7 @@ msgid "Fetching Sales Orders..." msgstr "" #: erpnext/accounts/doctype/dunning/dunning.js:135 -#: erpnext/public/js/controllers/transaction.js:1617 +#: erpnext/public/js/controllers/transaction.js:1619 msgid "Fetching exchange rates ..." msgstr "" @@ -20718,7 +20726,7 @@ msgstr "" #. Service Item' #. Label of the fg_item (Link) field in DocType 'Subcontracting Order Service #. Item' -#: erpnext/public/js/utils.js:939 +#: erpnext/public/js/utils.js:965 #: erpnext/subcontracting/doctype/subcontracting_inward_order_service_item/subcontracting_inward_order_service_item.json #: erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json msgid "Finished Good Item" @@ -20731,7 +20739,7 @@ msgstr "" msgid "Finished Good Item Code" msgstr "" -#: erpnext/public/js/utils.js:957 +#: erpnext/public/js/utils.js:983 msgid "Finished Good Item Qty" msgstr "" @@ -20744,15 +20752,15 @@ msgstr "" msgid "Finished Good Item Quantity" msgstr "" -#: erpnext/controllers/accounts_controller.py:4014 +#: erpnext/controllers/accounts_controller.py:4044 msgid "Finished Good Item is not specified for service item {0}" msgstr "" -#: erpnext/controllers/accounts_controller.py:4031 +#: erpnext/controllers/accounts_controller.py:4061 msgid "Finished Good Item {0} Qty can not be zero" msgstr "" -#: erpnext/controllers/accounts_controller.py:4025 +#: erpnext/controllers/accounts_controller.py:4055 msgid "Finished Good Item {0} must be a sub-contracted item" msgstr "" @@ -20839,11 +20847,11 @@ msgstr "" msgid "Finished Goods based Operating Cost" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2070 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2102 msgid "Finished Item {0} does not match with Work Order {1}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1060 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1069 msgid "Finished good quantity being consumed ({0} in stock UOM) must equal the quantity to disassemble ({1}). Do not change the UOM, conversion factor or quantity of the finished good row." msgstr "" @@ -21092,7 +21100,7 @@ msgstr "" msgid "Following Material Requests have been raised automatically based on Item's re-order level" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:966 +#: erpnext/selling/doctype/customer/customer.py:967 msgid "Following fields are mandatory to create address:" msgstr "" @@ -21149,7 +21157,7 @@ msgstr "" msgid "For Item" msgstr "" -#: erpnext/controllers/stock_controller.py:1769 +#: erpnext/controllers/stock_controller.py:1783 msgid "For Item {0} cannot be received more than {1} qty against the {2} {3}" msgstr "" @@ -21184,7 +21192,7 @@ msgstr "" msgid "For Production" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1010 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1019 msgid "For Quantity (Manufactured Qty) is mandatory" msgstr "" @@ -21194,7 +21202,7 @@ msgstr "" msgid "For Raw Materials" msgstr "" -#: erpnext/controllers/accounts_controller.py:1469 +#: erpnext/controllers/accounts_controller.py:1474 msgid "For Return Invoices with Stock effect, '0' qty Items are not allowed. Following rows are affected: {0}" msgstr "" @@ -21295,7 +21303,7 @@ msgstr "" msgid "For projected and forecast quantities, the system will consider all child warehouses under the selected parent warehouse." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2102 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2134 msgid "For quantity {0} should not be greater than allowed quantity {1}" msgstr "" @@ -21309,7 +21317,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:1729 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1728 msgid "For row {0}: Enter Planned Qty" msgstr "" @@ -21328,20 +21336,20 @@ msgstr "" msgid "For the convenience of customers, these codes can be used in print formats like Invoices and Delivery Notes" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1270 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1279 msgid "For the item {0}, the consumed quantity should be {1} according to the BOM {2}." msgstr "" -#: erpnext/public/js/controllers/transaction.js:1427 +#: erpnext/public/js/controllers/transaction.js:1429 msgctxt "Clear payment terms template and/or payment schedule when due date is changed" msgid "For the new {0} to take effect, would you like to clear the current {1}?" msgstr "" -#: erpnext/controllers/stock_controller.py:488 +#: erpnext/controllers/stock_controller.py:502 msgid "For the {0}, no stock is available for the return in the warehouse {1}." msgstr "" -#: erpnext/controllers/sales_and_purchase_return.py:1247 +#: erpnext/controllers/sales_and_purchase_return.py:1265 msgid "For the {0}, the quantity is required to make the return entry" msgstr "" @@ -21950,7 +21958,7 @@ msgstr "" msgid "Future date is not allowed" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:269 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:278 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:161 msgid "G - D" msgstr "" @@ -22498,7 +22506,7 @@ msgstr "" msgid "Goods Transferred" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2671 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2703 msgid "Goods are already received against the outward entry {0}" msgstr "" @@ -22797,7 +22805,7 @@ msgstr "" msgid "Group Same Items" msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:158 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:163 msgid "Group Warehouses cannot be used in transactions. Please change the value of {0}" msgstr "" @@ -22860,7 +22868,7 @@ msgstr "" msgid "Growth View" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:279 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:288 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:171 msgid "H - F" msgstr "" @@ -23129,7 +23137,7 @@ msgstr "" msgid "Here are the error logs for the aforementioned failed depreciation entries: {0}" msgstr "" -#: erpnext/stock/stock_ledger.py:2037 +#: erpnext/stock/stock_ledger.py:2080 msgid "Here are the options to proceed:" msgstr "" @@ -23378,12 +23386,12 @@ msgstr "" msgid "Hundredweight (US)" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:294 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:303 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:186 msgid "I - J" msgstr "" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:304 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:313 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:196 msgid "I - K" msgstr "" @@ -23784,7 +23792,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:2047 +#: erpnext/stock/stock_ledger.py:2090 msgid "If not, you can Cancel / Submit this entry" msgstr "" @@ -23830,7 +23838,7 @@ msgstr "" msgid "If the account is frozen, entries are allowed to restricted users." msgstr "" -#: erpnext/stock/stock_ledger.py:2040 +#: erpnext/stock/stock_ledger.py:2083 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 "" @@ -23931,7 +23939,7 @@ msgstr "" msgid "If you still want to proceed, please disable '{0}' checkbox." msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1855 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1854 msgid "If you still want to proceed, please enable {0}." msgstr "" @@ -24271,7 +24279,7 @@ msgstr "" #: erpnext/stock/report/available_serial_no/available_serial_no.py:112 #: erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py:82 #: erpnext/stock/report/stock_balance/stock_balance.py:543 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:318 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:321 msgid "In Qty" msgstr "" @@ -24289,11 +24297,11 @@ msgstr "" msgid "In Transit" msgstr "" -#: erpnext/stock/doctype/material_request/material_request.js:652 +#: erpnext/stock/doctype/material_request/material_request.js:648 msgid "In Transit Transfer" msgstr "" -#: erpnext/stock/doctype/material_request/material_request.js:621 +#: erpnext/stock/doctype/material_request/material_request.js:617 msgid "In Transit Warehouse" msgstr "" @@ -24714,8 +24722,8 @@ msgstr "" #: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json #: erpnext/stock/report/available_serial_no/available_serial_no.py:146 #: erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py:167 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:361 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:204 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:364 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:213 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:96 msgid "Incoming Rate" msgstr "" @@ -24754,7 +24762,7 @@ msgstr "" msgid "Incorrect Company" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1277 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1286 msgid "Incorrect Component Quantity" msgstr "" @@ -24804,7 +24812,7 @@ msgstr "" #: erpnext/stock/doctype/pick_list/pick_list.py:192 #: erpnext/stock/doctype/pick_list/pick_list.py:216 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:161 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:166 msgid "Incorrect Warehouse" msgstr "" @@ -24968,14 +24976,14 @@ msgstr "" msgid "Inspected By" msgstr "" -#: erpnext/controllers/stock_controller.py:1663 +#: erpnext/controllers/stock_controller.py:1677 #: erpnext/manufacturing/doctype/job_card/job_card.py:834 msgid "Inspection Rejected" msgstr "" #. Label of the inspection_required (Check) field in DocType 'Stock Entry' -#: erpnext/controllers/stock_controller.py:1633 -#: erpnext/controllers/stock_controller.py:1635 +#: erpnext/controllers/stock_controller.py:1647 +#: erpnext/controllers/stock_controller.py:1649 #: erpnext/stock/doctype/stock_entry/stock_entry.json msgid "Inspection Required" msgstr "" @@ -24992,7 +25000,7 @@ msgstr "" msgid "Inspection Required before Purchase" msgstr "" -#: erpnext/controllers/stock_controller.py:1648 +#: erpnext/controllers/stock_controller.py:1662 #: erpnext/manufacturing/doctype/job_card/job_card.py:815 msgid "Inspection Submission" msgstr "" @@ -25062,11 +25070,11 @@ msgstr "" msgid "Insufficient Capacity" msgstr "" -#: erpnext/controllers/accounts_controller.py:3910 -#: erpnext/controllers/accounts_controller.py:3932 -#: erpnext/controllers/accounts_controller.py:4450 -#: erpnext/controllers/accounts_controller.py:4456 -#: erpnext/controllers/accounts_controller.py:4478 +#: erpnext/controllers/accounts_controller.py:3940 +#: erpnext/controllers/accounts_controller.py:3962 +#: erpnext/controllers/accounts_controller.py:4480 +#: erpnext/controllers/accounts_controller.py:4486 +#: erpnext/controllers/accounts_controller.py:4508 msgid "Insufficient Permissions" msgstr "" @@ -25074,13 +25082,13 @@ msgstr "" #: erpnext/stock/doctype/pick_list/pick_list.py:150 #: erpnext/stock/doctype/pick_list/pick_list.py:168 #: erpnext/stock/doctype/pick_list/pick_list.py:1123 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1247 -#: erpnext/stock/serial_batch_bundle.py:1225 erpnext/stock/stock_ledger.py:1728 -#: erpnext/stock/stock_ledger.py:2225 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1256 +#: erpnext/stock/serial_batch_bundle.py:1311 erpnext/stock/stock_ledger.py:1771 +#: erpnext/stock/stock_ledger.py:2268 msgid "Insufficient Stock" msgstr "" -#: erpnext/stock/stock_ledger.py:2240 +#: erpnext/stock/stock_ledger.py:2283 msgid "Insufficient Stock for Batch" msgstr "" @@ -25235,7 +25243,7 @@ msgstr "" msgid "Internal Customer Accounting" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:264 +#: erpnext/selling/doctype/customer/customer.py:265 msgid "Internal Customer for company {0} already exists" msgstr "" @@ -25243,7 +25251,7 @@ msgstr "" msgid "Internal Purchase Order" msgstr "" -#: erpnext/controllers/accounts_controller.py:831 +#: erpnext/controllers/accounts_controller.py:836 msgid "Internal Sale or Delivery Reference missing." msgstr "" @@ -25251,7 +25259,7 @@ msgstr "" msgid "Internal Sales Order" msgstr "" -#: erpnext/controllers/accounts_controller.py:833 +#: erpnext/controllers/accounts_controller.py:838 msgid "Internal Sales Reference Missing" msgstr "" @@ -25282,7 +25290,7 @@ msgstr "" msgid "Internal Transfer" msgstr "" -#: erpnext/controllers/accounts_controller.py:842 +#: erpnext/controllers/accounts_controller.py:847 msgid "Internal Transfer Reference Missing" msgstr "" @@ -25306,7 +25314,7 @@ msgstr "" msgid "Internal notes about this customer. Not visible on transactions or the portal." msgstr "" -#: erpnext/controllers/stock_controller.py:1730 +#: erpnext/controllers/stock_controller.py:1744 msgid "Internal transfers can only be done in company's default currency" msgstr "" @@ -25320,14 +25328,14 @@ msgstr "" msgid "Interval should be between 1 to 59 MInutes" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:384 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:392 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:394 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:402 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1073 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1083 #: erpnext/assets/doctype/asset_category/asset_category.py:69 #: erpnext/assets/doctype/asset_category/asset_category.py:97 -#: erpnext/controllers/accounts_controller.py:3245 -#: erpnext/controllers/accounts_controller.py:3253 +#: erpnext/controllers/accounts_controller.py:3250 +#: erpnext/controllers/accounts_controller.py:3258 msgid "Invalid Account" msgstr "" @@ -25352,7 +25360,7 @@ msgstr "" msgid "Invalid Attribute Values" msgstr "" -#: erpnext/controllers/accounts_controller.py:645 +#: erpnext/controllers/accounts_controller.py:650 msgid "Invalid Auto Repeat Date" msgstr "" @@ -25365,7 +25373,7 @@ msgstr "" msgid "Invalid Barcode. There is no Item attached to this barcode." msgstr "" -#: erpnext/public/js/controllers/transaction.js:3186 +#: erpnext/public/js/controllers/transaction.js:3191 msgid "Invalid Blanket Order for the selected Customer and Item" msgstr "" @@ -25387,11 +25395,11 @@ msgstr "" #: erpnext/assets/doctype/asset/asset.py:365 #: erpnext/assets/doctype/asset/asset.py:372 -#: erpnext/controllers/accounts_controller.py:3268 +#: erpnext/controllers/accounts_controller.py:3273 msgid "Invalid Cost Center" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:379 +#: erpnext/selling/doctype/customer/customer.py:380 msgid "Invalid Customer Group" msgstr "" @@ -25399,12 +25407,12 @@ msgstr "" msgid "Invalid Delivery Date" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1099 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1108 msgid "Invalid Disassembly Item" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1065 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1114 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1074 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1123 msgid "Invalid Disassembly Quantity" msgstr "" @@ -25432,8 +25440,8 @@ msgstr "" msgid "Invalid File Type" msgstr "" -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:329 -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:334 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:374 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:379 msgid "Invalid Formula" msgstr "" @@ -25446,7 +25454,7 @@ msgstr "" msgid "Invalid Item" msgstr "" -#: erpnext/stock/doctype/item/item.py:1534 +#: erpnext/stock/doctype/item/item.py:1544 msgid "Invalid Item Defaults" msgstr "" @@ -25502,12 +25510,12 @@ msgstr "" msgid "Invalid Purchase Invoice" msgstr "" -#: erpnext/controllers/accounts_controller.py:3952 -#: erpnext/controllers/accounts_controller.py:3966 +#: erpnext/controllers/accounts_controller.py:3982 +#: erpnext/controllers/accounts_controller.py:3996 msgid "Invalid Qty" msgstr "" -#: erpnext/controllers/accounts_controller.py:1487 +#: erpnext/controllers/accounts_controller.py:1492 msgid "Invalid Quantity" msgstr "" @@ -25515,6 +25523,10 @@ msgstr "" msgid "Invalid Query" msgstr "" +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:325 +msgid "Invalid Reading" +msgstr "" + #: erpnext/selling/page/point_of_sale/pos_past_order_summary.js:202 msgid "Invalid Return" msgstr "" @@ -25532,12 +25544,12 @@ msgstr "" msgid "Invalid Selling Price" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2145 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2177 msgid "Invalid Serial and Batch Bundle" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1366 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1388 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1375 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1397 msgid "Invalid Source and Target Warehouse" msgstr "" @@ -25840,6 +25852,10 @@ msgstr "" msgid "Invoice can't be made for zero billing hour" msgstr "" +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1933 +msgid "Invoice is not blocked. Block the invoice to change the release date." +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 @@ -26551,7 +26567,7 @@ msgstr "" msgid "It can take upto few hours for accurate stock values to be visible after merging items." msgstr "" -#: erpnext/public/js/controllers/transaction.js:2564 +#: erpnext/public/js/controllers/transaction.js:2569 msgid "It is needed to fetch Item Details." msgstr "" @@ -26629,8 +26645,8 @@ msgstr "" #: erpnext/manufacturing/report/process_loss_report/process_loss_report.js:15 #: erpnext/manufacturing/report/process_loss_report/process_loss_report.py:74 #: erpnext/manufacturing/workspace/manufacturing/manufacturing.json -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:253 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:404 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:242 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:393 #: erpnext/public/js/purchase_trends_filters.js:48 #: erpnext/public/js/purchase_trends_filters.js:63 #: erpnext/public/js/sales_trends_filters.js:23 @@ -26677,7 +26693,7 @@ msgstr "" #: erpnext/stock/report/stock_analytics/stock_analytics.js:15 #: erpnext/stock/report/stock_analytics/stock_analytics.py:43 #: erpnext/stock/report/stock_balance/stock_balance.py:470 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:288 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:291 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js:27 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:51 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.js:28 @@ -26931,10 +26947,10 @@ msgstr "" #: erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py:86 #: erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py:119 #: erpnext/projects/doctype/timesheet/timesheet.js:216 -#: erpnext/public/js/controllers/transaction.js:2858 +#: erpnext/public/js/controllers/transaction.js:2863 #: erpnext/public/js/stock_reservation.js:112 #: erpnext/public/js/stock_reservation.js:318 erpnext/public/js/utils.js:596 -#: erpnext/public/js/utils.js:753 +#: erpnext/public/js/utils.js:754 #: erpnext/public/js/utils/serial_no_batch_selector.js:96 #: erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.json #: erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json @@ -26997,7 +27013,7 @@ msgstr "" #: erpnext/stock/report/stock_ageing/stock_ageing.py:177 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:105 #: erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py:25 -#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:26 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:32 #: erpnext/subcontracting/doctype/subcontracting_inward_order_item/subcontracting_inward_order_item.json #: erpnext/subcontracting/doctype/subcontracting_inward_order_received_item/subcontracting_inward_order_received_item.json #: erpnext/subcontracting/doctype/subcontracting_inward_order_secondary_item/subcontracting_inward_order_secondary_item.json @@ -27027,7 +27043,7 @@ msgstr "" msgid "Item Code cannot be changed for Serial No." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:451 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:461 msgid "Item Code required at Row No {0}" msgstr "" @@ -27200,7 +27216,7 @@ msgstr "" #: erpnext/stock/report/stock_balance/stock_balance.js:32 #: erpnext/stock/report/stock_balance/stock_balance.py:478 #: erpnext/stock/report/stock_ledger/stock_ledger.js:71 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:346 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:349 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.js:39 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:114 #: erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.js:33 @@ -27418,8 +27434,8 @@ msgstr "" #: erpnext/manufacturing/report/production_planning_report/production_planning_report.py:371 #: erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py:92 #: erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py:138 -#: erpnext/public/js/controllers/transaction.js:2864 -#: erpnext/public/js/utils.js:849 +#: erpnext/public/js/controllers/transaction.js:2869 +#: erpnext/public/js/utils.js:852 #: erpnext/selling/doctype/quotation_item/quotation_item.json #: erpnext/selling/doctype/sales_order/sales_order.js:1286 #: erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -27462,10 +27478,10 @@ msgstr "" #: erpnext/stock/report/stock_ageing/stock_ageing.py:183 #: erpnext/stock/report/stock_analytics/stock_analytics.py:45 #: erpnext/stock/report/stock_balance/stock_balance.py:476 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:294 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:297 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:111 #: erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py:31 -#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:32 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:38 #: erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py:98 #: erpnext/subcontracting/doctype/subcontracting_inward_order_item/subcontracting_inward_order_item.json #: erpnext/subcontracting/doctype/subcontracting_inward_order_service_item/subcontracting_inward_order_service_item.json @@ -27827,15 +27843,15 @@ msgstr "" msgid "Item and Warranty Details" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3859 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3891 msgid "Item for row {0} does not match Material Request" msgstr "" -#: erpnext/stock/doctype/item/item.py:895 +#: erpnext/stock/doctype/item/item.py:905 msgid "Item has variants." msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:455 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:444 msgid "Item is mandatory in Raw Materials table." msgstr "" @@ -27857,11 +27873,11 @@ msgstr "" msgid "Item operation" msgstr "" -#: erpnext/controllers/accounts_controller.py:4006 +#: erpnext/controllers/accounts_controller.py:4036 msgid "Item qty can not be updated as raw materials are already processed." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1520 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1531 msgid "Item rate has been updated to zero as Allow Zero Valuation Rate is checked for item {0}" msgstr "" @@ -27884,7 +27900,7 @@ msgstr "" msgid "Item valuation reposting in progress. Report might show incorrect item valuation." msgstr "" -#: erpnext/stock/doctype/item/item.py:1052 +#: erpnext/stock/doctype/item/item.py:1062 msgid "Item variant {0} exists with same attributes" msgstr "" @@ -27910,6 +27926,7 @@ msgstr "" #: erpnext/assets/doctype/asset/asset.py:347 #: erpnext/stock/doctype/item/item.py:687 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:102 msgid "Item {0} does not exist" msgstr "" @@ -27917,7 +27934,7 @@ msgstr "" msgid "Item {0} does not exist in the system or has expired" msgstr "" -#: erpnext/controllers/stock_controller.py:602 +#: erpnext/controllers/stock_controller.py:616 msgid "Item {0} does not exist." msgstr "" @@ -27925,7 +27942,7 @@ msgstr "" msgid "Item {0} entered multiple times." msgstr "" -#: erpnext/controllers/sales_and_purchase_return.py:221 +#: erpnext/controllers/sales_and_purchase_return.py:239 msgid "Item {0} has already been returned" msgstr "" @@ -27941,11 +27958,11 @@ msgstr "" msgid "Item {0} has no changes in delivered quantity. Please unselect the row if you do not wish to update its quantity." msgstr "" -#: erpnext/stock/doctype/item/item.py:1250 +#: erpnext/stock/doctype/item/item.py:1260 msgid "Item {0} has reached its end of life on {1}" msgstr "" -#: erpnext/stock/stock_ledger.py:117 +#: erpnext/stock/stock_ledger.py:144 msgid "Item {0} ignored since it is not a stock item" msgstr "" @@ -27953,11 +27970,11 @@ msgstr "" msgid "Item {0} is already reserved/delivered against Sales Order {1}." msgstr "" -#: erpnext/stock/doctype/item/item.py:1270 +#: erpnext/stock/doctype/item/item.py:1280 msgid "Item {0} is cancelled" msgstr "" -#: erpnext/stock/doctype/item/item.py:1254 +#: erpnext/stock/doctype/item/item.py:1264 msgid "Item {0} is disabled" msgstr "" @@ -27969,7 +27986,7 @@ msgstr "" msgid "Item {0} is not a serialized Item" msgstr "" -#: erpnext/stock/doctype/item/item.py:1262 +#: erpnext/stock/doctype/item/item.py:1272 msgid "Item {0} is not a stock Item" msgstr "" @@ -27981,7 +27998,7 @@ msgstr "" msgid "Item {0} is not a template item." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2583 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2615 msgid "Item {0} is not active or end of life has been reached" msgstr "" @@ -28001,7 +28018,7 @@ msgstr "" msgid "Item {0} must be a non-stock item" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1883 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1913 msgid "Item {0} not found in 'Raw Materials Supplied' table in {1} {2}" msgstr "" @@ -28087,7 +28104,7 @@ msgstr "" msgid "Items Filter" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1691 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1690 #: erpnext/selling/doctype/sales_order/sales_order.js:1719 msgid "Items Required" msgstr "" @@ -28111,11 +28128,11 @@ msgstr "" msgid "Items and Pricing" msgstr "" -#: erpnext/controllers/accounts_controller.py:4264 +#: erpnext/controllers/accounts_controller.py:4294 msgid "Items cannot be updated as Subcontracting Inward Order(s) exist against this Subcontracted Sales Order." msgstr "" -#: erpnext/controllers/accounts_controller.py:4257 +#: erpnext/controllers/accounts_controller.py:4287 msgid "Items cannot be updated as Subcontracting Order is created against the Purchase Order {0}." msgstr "" @@ -28127,7 +28144,7 @@ msgstr "" msgid "Items not found." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1516 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1527 msgid "Items rate has been updated to zero as Allow Zero Valuation Rate is checked for the following items: {0}" msgstr "" @@ -28137,7 +28154,7 @@ msgstr "" msgid "Items to Be Repost" msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1690 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1689 msgid "Items to Manufacture are required to pull the Raw Materials associated with it." msgstr "" @@ -28157,7 +28174,7 @@ msgstr "" msgid "Items under this warehouse will be suggested" msgstr "" -#: erpnext/controllers/stock_controller.py:207 +#: erpnext/controllers/stock_controller.py:221 msgid "Items {0} do not exist in the Item master." msgstr "" @@ -28640,7 +28657,7 @@ msgstr "" #. Name of a DocType #. Label of a Link in the Stock Workspace #. Label of a Workspace Sidebar Item -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:671 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:669 #: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:88 #: erpnext/stock/workspace/stock/stock.json @@ -29106,7 +29123,7 @@ msgstr "" msgid "License Plate" msgstr "" -#: erpnext/controllers/status_updater.py:512 +#: erpnext/controllers/status_updater.py:513 msgid "Limit Crossed" msgstr "" @@ -29188,7 +29205,7 @@ msgstr "" msgid "Linked Location" msgstr "" -#: erpnext/stock/doctype/item/item.py:1104 +#: erpnext/stock/doctype/item/item.py:1114 msgid "Linked with submitted documents" msgstr "" @@ -29469,7 +29486,7 @@ msgstr "" #: erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.json #: erpnext/accounts/doctype/loyalty_program/loyalty_program.json #: erpnext/accounts/doctype/pos_invoice/pos_invoice.json -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1225 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1234 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json #: erpnext/selling/doctype/customer/customer.json #: erpnext/selling/page/point_of_sale/pos_item_cart.js:963 @@ -29947,11 +29964,11 @@ msgstr "" msgid "Mandatory Missing" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:634 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:644 msgid "Mandatory Purchase Order" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:656 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:666 msgid "Mandatory Purchase Receipt" msgstr "" @@ -30026,8 +30043,8 @@ msgstr "" #: erpnext/stock/doctype/material_request_item/material_request_item.json #: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json #: erpnext/stock/doctype/stock_entry/stock_entry.json -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1625 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1641 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1668 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1684 #: erpnext/stock/doctype/stock_entry_type/stock_entry_type.json #: erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json #: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -30177,7 +30194,7 @@ msgstr "" msgid "Manufacturing Manager" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2941 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2973 msgid "Manufacturing Quantity is mandatory" msgstr "" @@ -30254,7 +30271,7 @@ msgstr "" msgid "Mapping Subcontracting Order ..." msgstr "" -#: erpnext/public/js/utils.js:1084 +#: erpnext/public/js/utils.js:1110 msgid "Mapping {0} ..." msgstr "" @@ -30457,7 +30474,7 @@ msgstr "" #. Option for the 'Purpose' (Select) field in DocType 'Stock Entry Type' #: erpnext/setup/setup_wizard/operations/install_fixtures.py:117 #: erpnext/stock/doctype/stock_entry/stock_entry.json -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1626 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1669 #: erpnext/stock/doctype/stock_entry_type/stock_entry_type.json msgid "Material Consumption for Manufacture" msgstr "" @@ -30886,11 +30903,11 @@ msgstr "" msgid "Maximum Producible Items" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:4475 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:4507 msgid "Maximum Samples - {0} can be retained for Batch {1} and Item {2}." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:4466 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:4498 msgid "Maximum Samples - {0} have already been retained for Batch {1} and Item {2} in Batch {3}." msgstr "" @@ -30951,7 +30968,7 @@ msgstr "" msgid "Megawatt" msgstr "" -#: erpnext/stock/stock_ledger.py:2053 +#: erpnext/stock/stock_ledger.py:2096 msgid "Mention Valuation Rate in the Item master." msgstr "" @@ -30986,7 +31003,7 @@ msgstr "" msgid "Merge similar Account Heads" msgstr "" -#: erpnext/public/js/utils.js:1116 +#: erpnext/public/js/utils.js:1142 msgid "Merge taxes from multiple documents" msgstr "" @@ -31341,7 +31358,7 @@ msgstr "" #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:97 #: erpnext/accounts/doctype/pos_profile/pos_profile.py:200 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:593 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:603 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2484 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3100 #: erpnext/assets/doctype/asset_category/asset_category.py:116 @@ -31373,15 +31390,15 @@ msgstr "" msgid "Missing Finance Book" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2080 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2112 msgid "Missing Finished Good" msgstr "" -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:314 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:359 msgid "Missing Formula" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1284 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1293 msgid "Missing Item" msgstr "" @@ -31663,7 +31680,7 @@ msgstr "" msgid "Multiple Accounts (Journal Template)" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:453 +#: erpnext/selling/doctype/customer/customer.py:454 msgid "Multiple Loyalty Programs found for Customer {}. Please select manually." msgstr "" @@ -31689,11 +31706,11 @@ msgstr "" msgid "Multiple company fields available: {0}. Please select manually." msgstr "" -#: erpnext/controllers/accounts_controller.py:1333 +#: erpnext/controllers/accounts_controller.py:1338 msgid "Multiple fiscal years exist for the date {0}. Please set company in Fiscal Year" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2087 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2119 msgid "Multiple items cannot be marked as finished item" msgstr "" @@ -31844,8 +31861,8 @@ msgstr "" msgid "Negative Stock" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1674 -#: erpnext/stock/serial_batch_bundle.py:1548 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1724 +#: erpnext/stock/serial_batch_bundle.py:1634 msgid "Negative Stock Error" msgstr "" @@ -32155,7 +32172,7 @@ msgstr "" msgid "Net Weight UOM" msgstr "" -#: erpnext/controllers/accounts_controller.py:1693 +#: erpnext/controllers/accounts_controller.py:1698 msgid "Net total calculation precision loss" msgstr "" @@ -32334,7 +32351,7 @@ msgstr "" msgid "New Workplace" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:418 +#: erpnext/selling/doctype/customer/customer.py:419 msgid "New credit limit is less than current outstanding amount for the customer. Credit limit has to be atleast {0}" msgstr "" @@ -32344,7 +32361,7 @@ msgstr "" msgid "New invoices will be generated as per schedule even if current invoices are unpaid or past due date" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:261 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:259 msgid "New release date should be in the future" msgstr "" @@ -32462,10 +32479,10 @@ msgstr "" msgid "No POS Profile found. Please create a New POS Profile first" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1583 -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1643 -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1657 -#: erpnext/stock/doctype/item/item.py:1495 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1593 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1653 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1667 +#: erpnext/stock/doctype/item/item.py:1505 msgid "No Permission" msgstr "" @@ -32482,7 +32499,7 @@ msgstr "" msgid "No Selection" msgstr "" -#: erpnext/controllers/sales_and_purchase_return.py:975 +#: erpnext/controllers/sales_and_purchase_return.py:993 msgid "No Serial / Batches are available for return" msgstr "" @@ -32580,7 +32597,7 @@ msgstr "" msgid "No billing email found for customer: {0}" msgstr "" -#: banking/src/components/features/BankReconciliation/CompanySelector.tsx:66 +#: banking/src/components/features/BankReconciliation/CompanySelector.tsx:79 msgid "No company found." msgstr "" @@ -33051,6 +33068,10 @@ msgstr "" msgid "Not permitted to make Purchase Orders" msgstr "" +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:94 +msgid "Not permitted to update Serial No" +msgstr "" + #: erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js:21 msgid "Note: Automatic log deletion only applies to logs of type Update Cost" msgstr "" @@ -33073,7 +33094,7 @@ msgstr "" msgid "Note: Item {0} added multiple times" msgstr "" -#: erpnext/controllers/accounts_controller.py:731 +#: erpnext/controllers/accounts_controller.py:736 msgid "Note: Payment Entry will not be created since 'Cash or Bank Account' was not specified" msgstr "" @@ -33573,7 +33594,7 @@ msgstr "" msgid "Only one operation can have 'Is Final Finished Good' checked when 'Track Semi Finished Goods' is enabled." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1640 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1683 msgid "Only one {0} entry can be created against the Work Order {1}" msgstr "" @@ -33850,7 +33871,7 @@ msgstr "" msgid "Opening Invoice Tool" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1712 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1722 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2044 msgid "Opening Invoice has rounding adjustment of {0}.

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

    Or, '{3}' can be enabled to not post any rounding adjustment." msgstr "" @@ -34502,7 +34523,7 @@ msgstr "" #: erpnext/stock/report/available_serial_no/available_serial_no.py:119 #: erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py:83 #: erpnext/stock/report/stock_balance/stock_balance.py:551 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:325 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:328 msgid "Out Qty" msgstr "" @@ -34559,7 +34580,7 @@ msgstr "" #. Label of the outgoing_rate (Currency) field in DocType 'Stock Ledger Entry' #: erpnext/stock/doctype/serial_and_batch_entry/serial_and_batch_entry.json #: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json -#: erpnext/stock/report/stock_ledger/stock_ledger.py:379 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:382 msgid "Outgoing Rate" msgstr "" @@ -34676,11 +34697,11 @@ msgstr "" msgid "Over Picking Allowance (%)" msgstr "" -#: erpnext/controllers/stock_controller.py:1900 +#: erpnext/controllers/stock_controller.py:1914 msgid "Over Receipt" msgstr "" -#: erpnext/controllers/status_updater.py:517 +#: erpnext/controllers/status_updater.py:518 msgid "Over Receipt/Delivery of {0} {1} ignored for item {2} because you have {3} role." msgstr "" @@ -34697,11 +34718,11 @@ msgstr "" msgid "Over Withheld" msgstr "" -#: erpnext/controllers/status_updater.py:519 +#: erpnext/controllers/status_updater.py:520 msgid "Overbilling of {0} {1} ignored for item {2} because you have {3} role." msgstr "" -#: erpnext/controllers/accounts_controller.py:2211 +#: erpnext/controllers/accounts_controller.py:2216 msgid "Overbilling of {} ignored because you have {} role." msgstr "" @@ -34738,11 +34759,11 @@ msgstr "" msgid "Overdue Limit" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:707 +#: erpnext/selling/doctype/customer/customer.py:708 msgid "Overdue Limit Crossed" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:702 +#: erpnext/selling/doctype/customer/customer.py:703 msgid "Overdue Limit crossed for customer {0}. Overdue amount {1} exceeds the allowed limit {2}." msgstr "" @@ -35214,7 +35235,7 @@ msgstr "" msgid "Packed Items" msgstr "" -#: erpnext/controllers/stock_controller.py:1734 +#: erpnext/controllers/stock_controller.py:1748 msgid "Packed Items cannot be transferred internally" msgstr "" @@ -35361,7 +35382,7 @@ msgstr "" msgid "Paid To Account Type" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:334 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:344 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1204 msgid "Paid amount + Write Off Amount can not be greater than Grand Total" msgstr "" @@ -35541,11 +35562,11 @@ msgstr "" msgid "Parent Task" msgstr "" -#: erpnext/projects/doctype/task/task.py:170 +#: erpnext/projects/doctype/task/task.py:171 msgid "Parent Task {0} is not a Template Task" msgstr "" -#: erpnext/projects/doctype/task/task.py:193 +#: erpnext/projects/doctype/task/task.py:194 msgid "Parent Task {0} must be a Group Task" msgstr "" @@ -35870,7 +35891,7 @@ msgstr "" msgid "Party Account No. (Bank Statement)" msgstr "" -#: erpnext/controllers/accounts_controller.py:2495 +#: erpnext/controllers/accounts_controller.py:2500 msgid "Party Account {0} currency ({1}) and document currency ({2}) should be same" msgstr "" @@ -36374,7 +36395,7 @@ msgstr "" msgid "Payment Entry is already created" msgstr "" -#: erpnext/controllers/accounts_controller.py:1644 +#: erpnext/controllers/accounts_controller.py:1649 msgid "Payment Entry {0} is linked against Order {1}, check if it should be pulled as advance in this invoice." msgstr "" @@ -36658,7 +36679,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:2777 +#: erpnext/controllers/accounts_controller.py:2782 #: erpnext/selling/doctype/quotation/quotation.json #: erpnext/selling/doctype/sales_order/sales_order.json msgid "Payment Schedule" @@ -36668,7 +36689,7 @@ msgstr "" msgid "Payment Schedule based Payment Requests cannot be created because a Payment Entry already exists for this document." msgstr "" -#: erpnext/public/js/controllers/transaction.js:532 +#: erpnext/public/js/controllers/transaction.js:534 msgid "Payment Schedules" msgstr "" @@ -36690,7 +36711,7 @@ msgstr "" #: erpnext/accounts/report/accounts_receivable/accounts_receivable.py:1217 #: erpnext/accounts/report/gross_profit/gross_profit.py:451 #: erpnext/accounts/workspace/invoicing/invoicing.json -#: erpnext/public/js/controllers/transaction.js:547 +#: erpnext/public/js/controllers/transaction.js:549 #: erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py:30 #: erpnext/workspace_sidebar/accounts_setup.json msgid "Payment Term" @@ -37142,11 +37163,11 @@ msgstr "" msgid "Period Closing Voucher" msgstr "" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:509 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:627 msgid "Period Closing Voucher {0} GL Entry Cancellation Failed" msgstr "" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:488 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:606 msgid "Period Closing Voucher {0} GL Entry Processing Failed" msgstr "" @@ -37166,7 +37187,7 @@ msgstr "" msgid "Period End Date" msgstr "" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:78 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:81 msgid "Period End Date cannot be greater than Fiscal Year End Date" msgstr "" @@ -37208,11 +37229,11 @@ msgstr "" msgid "Period Start Date" msgstr "" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:75 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:78 msgid "Period Start Date cannot be greater than Period End Date" msgstr "" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:72 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:75 msgid "Period Start Date must be {0}" msgstr "" @@ -37314,11 +37335,11 @@ msgid "Phantom BOM cannot be created for stock item {0}." msgstr "" #: erpnext/manufacturing/doctype/bom/bom_item_preview.html:16 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:340 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:329 msgid "Phantom Item" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:449 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:438 msgid "Phantom Item is mandatory" msgstr "" @@ -37790,7 +37811,7 @@ msgstr "" msgid "Please add {1} role to user {0}." msgstr "" -#: erpnext/controllers/stock_controller.py:1911 +#: erpnext/controllers/stock_controller.py:1925 msgid "Please adjust the qty or edit {0} to proceed." msgstr "" @@ -37816,7 +37837,7 @@ msgstr "" msgid "Please capitalize this asset before submitting." msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:978 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:988 msgid "Please check Multi Currency option to allow accounts with other currency" msgstr "" @@ -37868,7 +37889,7 @@ msgstr "" msgid "Please configure accounts for the Bank Entry rule." msgstr "" -#: erpnext/selling/doctype/customer/customer.py:649 +#: erpnext/selling/doctype/customer/customer.py:650 msgid "Please contact any of the following users to extend the credit limits for {0}: {1}" msgstr "" @@ -37876,7 +37897,7 @@ msgstr "" msgid "Please contact any of the following users to {} this transaction." msgstr "" -#: erpnext/selling/doctype/customer/customer.py:642 +#: erpnext/selling/doctype/customer/customer.py:643 msgid "Please contact your administrator to extend the credit limits for {0}." msgstr "" @@ -37896,7 +37917,7 @@ msgstr "" msgid "Please create a new Accounting Dimension if required." msgstr "" -#: erpnext/controllers/accounts_controller.py:832 +#: erpnext/controllers/accounts_controller.py:837 msgid "Please create purchase from internal sale or delivery document itself" msgstr "" @@ -37944,11 +37965,11 @@ msgstr "" msgid "Please enable {} in {} to allow same item in multiple rows" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:381 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:391 msgid "Please ensure that the {0} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:389 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:399 msgid "Please ensure that the {0} account {1} is a Payable account. You can change the account type to Payable or select a different account." msgstr "" @@ -37960,7 +37981,7 @@ msgstr "" msgid "Please ensure {} account {} is a Receivable account." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:874 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:883 msgid "Please enter Difference Account or set default Stock Adjustment Account for company {0}" msgstr "" @@ -37998,7 +38019,7 @@ msgstr "" msgid "Please enter Item Code to get Batch Number" msgstr "" -#: erpnext/public/js/controllers/transaction.js:3043 +#: erpnext/public/js/controllers/transaction.js:3048 msgid "Please enter Item Code to get batch no" msgstr "" @@ -38026,7 +38047,7 @@ msgstr "" msgid "Please enter Receipt Document" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1042 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1052 msgid "Please enter Reference date" msgstr "" @@ -38050,16 +38071,16 @@ msgstr "" msgid "Please enter Warehouse and Date" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:660 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:670 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1335 msgid "Please enter Write Off Account" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:670 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:680 msgid "Please enter a valid Write Off Account" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:681 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:691 msgid "Please enter a valid Write Off Cost Center" msgstr "" @@ -38079,7 +38100,7 @@ msgstr "" msgid "Please enter company name first" msgstr "" -#: erpnext/controllers/accounts_controller.py:2996 +#: erpnext/controllers/accounts_controller.py:3001 msgid "Please enter default currency in Company Master" msgstr "" @@ -38354,11 +38375,11 @@ msgstr "" msgid "Please select Stock Asset Account" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2006 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2036 msgid "Please select Subcontracting Order instead of Purchase Order {0}" msgstr "" -#: erpnext/controllers/accounts_controller.py:2852 +#: erpnext/controllers/accounts_controller.py:2857 msgid "Please select Unrealized Profit / Loss account or add default Unrealized Profit / Loss account account for company {0}" msgstr "" @@ -38375,7 +38396,7 @@ msgstr "" #: erpnext/manufacturing/doctype/bom/bom.js:734 #: erpnext/manufacturing/doctype/bom/bom.py:279 #: erpnext/public/js/controllers/accounts.js:274 -#: erpnext/public/js/controllers/transaction.js:3342 +#: erpnext/public/js/controllers/transaction.js:3347 msgid "Please select a Company first." msgstr "" @@ -38476,6 +38497,10 @@ msgstr "" msgid "Please select a value for {0} quotation_to {1}" msgstr "" +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js:9 +msgid "Please select a warehouse first." +msgstr "" + #: erpnext/assets/doctype/asset_repair/asset_repair.js:203 msgid "Please select an item code before setting the warehouse." msgstr "" @@ -38500,7 +38525,7 @@ msgstr "" msgid "Please select at least one row with difference value" msgstr "" -#: erpnext/public/js/controllers/transaction.js:584 +#: erpnext/public/js/controllers/transaction.js:586 msgid "Please select at least one schedule." msgstr "" @@ -38512,7 +38537,7 @@ msgstr "" msgid "Please select atleast one operation to create Job Card" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1722 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1732 msgid "Please select correct account" msgstr "" @@ -38600,7 +38625,7 @@ msgstr "" msgid "Please select {0} first" msgstr "" -#: erpnext/public/js/controllers/transaction.js:150 +#: erpnext/public/js/controllers/transaction.js:152 msgid "Please set 'Apply Additional Discount On'" msgstr "" @@ -38672,7 +38697,7 @@ msgstr "" msgid "Please set Fixed Asset Account in Asset Category {0}" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:590 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:600 msgid "Please set Fixed Asset Account in {} against {}." msgstr "" @@ -38718,7 +38743,7 @@ msgstr "" msgid "Please set a default Holiday List for Employee {0} or Company {1}" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1146 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1156 msgid "Please set account in Warehouse {0}" msgstr "" @@ -38731,7 +38756,7 @@ msgstr "" msgid "Please set an Address on the Company '%s'" msgstr "" -#: erpnext/controllers/stock_controller.py:1042 +#: erpnext/controllers/stock_controller.py:1056 msgid "Please set an Expense Account in the Items table" msgstr "" @@ -38775,11 +38800,11 @@ msgstr "" msgid "Please set default UOM in Stock Settings" msgstr "" -#: erpnext/controllers/stock_controller.py:821 +#: erpnext/controllers/stock_controller.py:835 msgid "Please set default cost of goods sold account in company {0} for booking rounding gain and loss during stock transfer" msgstr "" -#: erpnext/controllers/stock_controller.py:272 +#: erpnext/controllers/stock_controller.py:286 msgid "Please set default inventory account for item {0}, or their item group or brand." msgstr "" @@ -38792,7 +38817,7 @@ msgstr "" msgid "Please set filter based on Item or Warehouse" msgstr "" -#: erpnext/controllers/accounts_controller.py:2411 +#: erpnext/controllers/accounts_controller.py:2416 msgid "Please set one of the following:" msgstr "" @@ -38800,7 +38825,7 @@ msgstr "" msgid "Please set opening number of booked depreciations" msgstr "" -#: erpnext/public/js/controllers/transaction.js:2707 +#: erpnext/public/js/controllers/transaction.js:2712 msgid "Please set recurring after saving" msgstr "" @@ -38856,7 +38881,7 @@ msgid "Please set {0} in BOM Creator {1}" msgstr "" #: erpnext/controllers/buying_controller.py:345 -#: erpnext/controllers/stock_controller.py:912 +#: erpnext/controllers/stock_controller.py:926 msgid "Please set {0} in Company {1} or in the Item Defaults of Item {2}" msgstr "" @@ -38864,7 +38889,7 @@ msgstr "" msgid "Please set {0} in Company {1} to account for Exchange Gain / Loss" msgstr "" -#: erpnext/controllers/accounts_controller.py:613 +#: erpnext/controllers/accounts_controller.py:618 msgid "Please set {0} to {1}, the same account that was used in the original invoice {2}." msgstr "" @@ -38881,12 +38906,12 @@ msgid "Please specify Company" msgstr "" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.js:120 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:430 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:428 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:636 msgid "Please specify Company to proceed" msgstr "" -#: erpnext/controllers/accounts_controller.py:3227 +#: erpnext/controllers/accounts_controller.py:3232 #: erpnext/public/js/controllers/accounts.js:114 msgid "Please specify a valid Row ID for row {0} in table {1}" msgstr "" @@ -39126,7 +39151,7 @@ msgstr "" #: erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py:86 #: erpnext/stock/report/serial_no_ledger/serial_no_ledger.py:25 #: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:155 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:155 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:164 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:36 #: erpnext/templates/form_grid/bank_reconciliation_grid.html:6 msgid "Posting Date" @@ -39143,7 +39168,7 @@ msgstr "" msgid "Posting Date inheritance for exchange gain / loss" msgstr "" -#: erpnext/public/js/controllers/transaction.js:1137 +#: erpnext/public/js/controllers/transaction.js:1139 msgid "Posting Date will change to today's date as Edit Posting Date and Time is unchecked. Are you sure want to proceed?" msgstr "" @@ -39200,13 +39225,13 @@ msgstr "" #: erpnext/stock/report/serial_no_ledger/serial_no_ledger.js:63 #: erpnext/stock/report/serial_no_ledger/serial_no_ledger.py:26 #: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:156 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:160 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:169 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:41 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json msgid "Posting Time" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2891 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2923 msgid "Posting date and posting time is mandatory" msgstr "" @@ -39414,7 +39439,7 @@ msgstr "" msgid "Previous Work Experience" msgstr "" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:109 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:112 msgid "Previous Year is not closed, please close it first" msgstr "" @@ -40537,7 +40562,7 @@ msgstr "" msgid "Profitability Analysis" msgstr "" -#: erpnext/projects/doctype/task/task.py:156 +#: erpnext/projects/doctype/task/task.py:157 #, python-format msgid "Progress % for a task cannot be more than 100." msgstr "" @@ -41115,11 +41140,19 @@ msgstr "" msgid "Purchase Invoice Trends" msgstr "" +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:331 +msgid "Purchase Invoice can be held after submitting." +msgstr "" + #: erpnext/assets/doctype/asset/asset.py:340 msgid "Purchase Invoice cannot be made against an existing asset {0}" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1999 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1943 +msgid "Purchase Invoice without any outstanding amount cannot be held." +msgstr "" + +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:2033 msgid "Purchase Invoices" msgstr "" @@ -41248,11 +41281,11 @@ msgstr "" msgid "Purchase Order Pricing Rule" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:630 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:640 msgid "Purchase Order Required" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:625 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:635 msgid "Purchase Order Required for item {}" msgstr "" @@ -41278,7 +41311,7 @@ msgstr "" msgid "Purchase Order {0} created" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:690 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:700 msgid "Purchase Order {0} is not submitted" msgstr "" @@ -41312,7 +41345,7 @@ msgstr "" msgid "Purchase Orders to Receive" msgstr "" -#: erpnext/controllers/accounts_controller.py:2043 +#: erpnext/controllers/accounts_controller.py:2048 msgid "Purchase Orders {0} are un-linked" msgstr "" @@ -41337,8 +41370,8 @@ msgstr "" #. Label of a Workspace Sidebar Item #: erpnext/accounts/doctype/accounts_settings/accounts_settings.js:62 #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:181 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:647 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:657 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:645 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:655 #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js:49 #: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json #: erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py:244 @@ -41398,11 +41431,11 @@ msgstr "" msgid "Purchase Receipt No" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:652 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:662 msgid "Purchase Receipt Required" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:647 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:657 msgid "Purchase Receipt Required for item {}" msgstr "" @@ -41430,7 +41463,7 @@ msgstr "" msgid "Purchase Receipt {0} created." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:697 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:707 msgid "Purchase Receipt {0} is not submitted" msgstr "" @@ -41556,7 +41589,7 @@ msgstr "" msgid "Purpose" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:691 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:700 msgid "Purpose must be one of {0}" msgstr "" @@ -41656,12 +41689,12 @@ msgstr "" #: erpnext/manufacturing/doctype/work_order_item/work_order_item.json #: erpnext/manufacturing/doctype/workstation/workstation_job_card.html:28 #: erpnext/manufacturing/report/bom_explorer/bom_explorer.py:69 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:254 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:352 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:417 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:517 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:243 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:341 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:406 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:506 #: erpnext/public/js/stock_reservation.js:134 -#: erpnext/public/js/stock_reservation.js:336 erpnext/public/js/utils.js:887 +#: erpnext/public/js/stock_reservation.js:336 erpnext/public/js/utils.js:890 #: erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json #: erpnext/selling/doctype/product_bundle_item/product_bundle_item.json #: erpnext/selling/doctype/sales_order/sales_order.js:398 @@ -41749,7 +41782,7 @@ msgstr "" #: erpnext/stock/doctype/stock_closing_balance/stock_closing_balance.json #: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:169 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:199 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:208 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:91 msgid "Qty Change" msgstr "" @@ -42063,7 +42096,7 @@ msgstr "" msgid "Quality Inspection Analysis" msgstr "" -#: erpnext/public/js/controllers/transaction.js:2964 +#: erpnext/public/js/controllers/transaction.js:2969 msgid "Quality Inspection Not Configured" msgstr "" @@ -42142,7 +42175,7 @@ msgstr "" msgid "Quality Inspection {0} is rejected for the item: {1}" msgstr "" -#: erpnext/public/js/controllers/transaction.js:431 +#: erpnext/public/js/controllers/transaction.js:433 #: erpnext/stock/doctype/stock_entry/stock_entry.js:212 msgid "Quality Inspection(s)" msgstr "" @@ -42738,7 +42771,7 @@ msgstr "" #: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json #: erpnext/manufacturing/doctype/work_order_additional_item/work_order_additional_item.json #: erpnext/manufacturing/doctype/work_order_item/work_order_item.json -#: erpnext/public/js/utils.js:897 +#: erpnext/public/js/utils.js:900 #: erpnext/selling/doctype/product_bundle_item/product_bundle_item.json #: erpnext/selling/doctype/quotation_item/quotation_item.json #: erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -42921,7 +42954,7 @@ msgstr "" msgid "Rate at which this tax is applied" msgstr "" -#: erpnext/controllers/accounts_controller.py:4132 +#: erpnext/controllers/accounts_controller.py:4162 msgid "Rate of '{}' items cannot be changed" msgstr "" @@ -43065,7 +43098,7 @@ msgstr "" #: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/manufacturing/doctype/production_plan/production_plan.json #: erpnext/manufacturing/doctype/workstation/workstation.js:462 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:398 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:387 msgid "Raw Materials" msgstr "" @@ -43090,7 +43123,7 @@ msgstr "" msgid "Raw Materials Consumption" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:431 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:440 msgid "Raw Materials Missing" msgstr "" @@ -43237,7 +43270,7 @@ msgid "Real Estate" msgstr "" #. Label of the hold_comment (Small Text) field in DocType 'Purchase Invoice' -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:285 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:283 #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json msgid "Reason For Putting On Hold" msgstr "" @@ -43792,11 +43825,11 @@ msgstr "" msgid "Reference #" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1040 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1050 msgid "Reference #{0} dated {1}" msgstr "" -#: erpnext/public/js/controllers/transaction.js:2820 +#: erpnext/public/js/controllers/transaction.js:2825 msgid "Reference Date for Early Payment Discount" msgstr "" @@ -44076,15 +44109,15 @@ msgstr "" #. Label of the release_date (Date) field in DocType 'Purchase Invoice' #. Label of the release_date (Date) field in DocType 'Supplier' -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:277 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:321 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:275 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:320 #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/buying/doctype/supplier/supplier.json #: erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.py:1079 msgid "Release Date" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:325 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:335 msgid "Release date must be in the future" msgstr "" @@ -44532,7 +44565,7 @@ msgid "Reposting cannot be started when status is {0}." msgstr "" #: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:227 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:338 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:347 msgid "Reposting entries created: {0}" msgstr "" @@ -44597,7 +44630,7 @@ msgstr "" msgid "Reqd Qty (BOM)" msgstr "" -#: erpnext/public/js/utils.js:913 +#: erpnext/public/js/utils.js:916 msgid "Reqd by date" msgstr "" @@ -44914,7 +44947,7 @@ msgstr "" msgid "Reserved" msgstr "" -#: erpnext/controllers/stock_controller.py:1491 +#: erpnext/controllers/stock_controller.py:1505 msgid "Reserved Batch Conflict" msgstr "" @@ -44988,7 +45021,7 @@ msgstr "" msgid "Reserved Quantity for Production" msgstr "" -#: erpnext/stock/stock_ledger.py:2340 +#: erpnext/stock/stock_ledger.py:2383 msgid "Reserved Serial No." msgstr "" @@ -45006,13 +45039,13 @@ msgstr "" #: erpnext/stock/report/reserved_stock/reserved_stock.json #: erpnext/stock/report/stock_balance/stock_balance.py:569 #: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:205 -#: erpnext/stock/stock_ledger.py:2324 +#: erpnext/stock/stock_ledger.py:2367 #: 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:2412 msgid "Reserved Stock for Batch" msgstr "" @@ -45392,6 +45425,10 @@ msgstr "" msgid "Return Issued" msgstr "" +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:328 +msgid "Return Purchase Invoice cannot be held." +msgstr "" + #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:329 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js:127 msgid "Return Qty" @@ -45928,8 +45965,8 @@ msgstr "" msgid "Rounding Loss Allowance should be between 0 and 1" msgstr "" -#: erpnext/controllers/stock_controller.py:833 -#: erpnext/controllers/stock_controller.py:848 +#: erpnext/controllers/stock_controller.py:847 +#: erpnext/controllers/stock_controller.py:862 msgid "Rounding gain/loss Entry for Stock Transfer" msgstr "" @@ -45952,7 +45989,7 @@ msgstr "" msgid "Routing Name" msgstr "" -#: erpnext/controllers/sales_and_purchase_return.py:225 +#: erpnext/controllers/sales_and_purchase_return.py:243 msgid "Row # {0}: Cannot return more than {1} for Item {2}" msgstr "" @@ -45990,11 +46027,11 @@ msgstr "" msgid "Row #{0}: A reorder entry already exists for warehouse {1} with reorder type {2}." msgstr "" -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:333 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:378 msgid "Row #{0}: Acceptance Criteria Formula is incorrect." msgstr "" -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:313 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:358 msgid "Row #{0}: Acceptance Criteria Formula is required." msgstr "" @@ -46007,7 +46044,7 @@ msgstr "" msgid "Row #{0}: Accepted Warehouse is mandatory for the accepted Item {1}" msgstr "" -#: erpnext/controllers/accounts_controller.py:1321 +#: erpnext/controllers/accounts_controller.py:1326 msgid "Row #{0}: Account {1} does not belong to company {2}" msgstr "" @@ -46072,27 +46109,27 @@ msgstr "" msgid "Row #{0}: Cannot create entry with different taxable AND withholding document links." msgstr "" -#: erpnext/controllers/accounts_controller.py:3834 +#: erpnext/controllers/accounts_controller.py:3864 msgid "Row #{0}: Cannot delete item {1} which has already been billed." msgstr "" -#: erpnext/controllers/accounts_controller.py:3808 +#: erpnext/controllers/accounts_controller.py:3838 msgid "Row #{0}: Cannot delete item {1} which has already been delivered" msgstr "" -#: erpnext/controllers/accounts_controller.py:3827 +#: erpnext/controllers/accounts_controller.py:3857 msgid "Row #{0}: Cannot delete item {1} which has already been received" msgstr "" -#: erpnext/controllers/accounts_controller.py:3814 +#: erpnext/controllers/accounts_controller.py:3844 msgid "Row #{0}: Cannot delete item {1} which has work order assigned to it." msgstr "" -#: erpnext/controllers/accounts_controller.py:3820 +#: erpnext/controllers/accounts_controller.py:3850 msgid "Row #{0}: Cannot delete item {1} which is already ordered against this Sales Order." msgstr "" -#: erpnext/controllers/accounts_controller.py:4142 +#: erpnext/controllers/accounts_controller.py:4172 msgid "Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}." msgstr "" @@ -46100,7 +46137,7 @@ msgstr "" msgid "Row #{0}: Cannot transfer more than Required Qty {1} for Item {2} against Job Card {3}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1329 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1338 msgid "Row #{0}: Cannot transfer {1} {2} of Item {3}. Maximum transferable quantity is {4} {2}." msgstr "" @@ -46195,7 +46232,7 @@ msgstr "" msgid "Row #{0}: Expected Delivery Date cannot be before Purchase Order Date" msgstr "" -#: erpnext/controllers/stock_controller.py:1044 +#: erpnext/controllers/stock_controller.py:1058 msgid "Row #{0}: Expense Account not set for the Item {1}. {2}" msgstr "" @@ -46222,7 +46259,7 @@ msgstr "" msgid "Row #{0}: Finished Good Item {1} must be a sub-contracted item" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:647 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:656 msgid "Row #{0}: Finished Good must be {1}" msgstr "" @@ -46259,7 +46296,7 @@ msgstr "" msgid "Row #{0}: Item added" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1937 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1967 msgid "Row #{0}: Item {1} cannot be transferred more than {2} against {3} {4}" msgstr "" @@ -46275,7 +46312,7 @@ msgstr "" msgid "Row #{0}: Item {1} has no stock in warehouse {2}." msgstr "" -#: erpnext/controllers/stock_controller.py:189 +#: erpnext/controllers/stock_controller.py:203 msgid "Row #{0}: Item {1} has zero rate but '{2}' is not enabled." msgstr "" @@ -46304,7 +46341,7 @@ msgstr "" msgid "Row #{0}: Item {1} is not a stock item" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1095 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1104 msgid "Row #{0}: Item {1} is not part of the source manufacture entry and cannot be added to this disassembly." msgstr "" @@ -46316,7 +46353,7 @@ msgstr "" msgid "Row #{0}: Item {1} mismatch. Changing of item code is not permitted." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1104 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1113 msgid "Row #{0}: Item {1} quantity ({2} in stock UOM) does not match the quantity derived from the source ({3}). Do not change the UOM, conversion factor or quantity of disassembly rows." msgstr "" @@ -46344,7 +46381,7 @@ msgstr "" msgid "Row #{0}: Opening Accumulated Depreciation must be less than or equal to {1}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1159 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1168 msgid "Row #{0}: Operation {1} is not completed for {2} qty of finished goods in Work Order {3}. Please update operation status via Job Card {4}." msgstr "" @@ -46373,7 +46410,7 @@ msgstr "" msgid "Row #{0}: Please set reorder quantity" msgstr "" -#: erpnext/controllers/accounts_controller.py:636 +#: erpnext/controllers/accounts_controller.py:641 msgid "Row #{0}: Please update deferred revenue/expense account in item row or default account in company master" msgstr "" @@ -46395,15 +46432,15 @@ msgstr "" msgid "Row #{0}: Qty should be less than or equal to Available Qty to Reserve (Actual Qty - Reserved Qty) {1} for Iem {2} against Batch {3} in Warehouse {4}." msgstr "" -#: erpnext/controllers/stock_controller.py:1629 +#: erpnext/controllers/stock_controller.py:1643 msgid "Row #{0}: Quality Inspection is required for Item {1}" msgstr "" -#: erpnext/controllers/stock_controller.py:1644 +#: erpnext/controllers/stock_controller.py:1658 msgid "Row #{0}: Quality Inspection {1} is not submitted for the item: {2}" msgstr "" -#: erpnext/controllers/stock_controller.py:1659 +#: erpnext/controllers/stock_controller.py:1673 msgid "Row #{0}: Quality Inspection {1} was rejected for item {2}" msgstr "" @@ -46411,10 +46448,14 @@ 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:1484 +#: erpnext/controllers/accounts_controller.py:1489 msgid "Row #{0}: Quantity for Item {1} cannot be zero." msgstr "" +#: erpnext/crm/doctype/opportunity/opportunity.py:152 +msgid "Row #{0}: Quantity must be greater than 0 for Item {1}" +msgstr "" + #: erpnext/controllers/subcontracting_inward_controller.py:537 msgid "Row #{0}: Quantity of Item {1} cannot be more than {2} {3} against Subcontracting Inward Order {4}" msgstr "" @@ -46423,13 +46464,17 @@ msgstr "" msgid "Row #{0}: Quantity to reserve for the Item {1} should be greater than 0." msgstr "" -#: erpnext/controllers/accounts_controller.py:899 -#: erpnext/controllers/accounts_controller.py:911 +#: erpnext/controllers/accounts_controller.py:904 +#: erpnext/controllers/accounts_controller.py:916 #: erpnext/utilities/transaction_base.py:172 #: erpnext/utilities/transaction_base.py:178 msgid "Row #{0}: Rate must be same as {1}: {2} ({3} / {4})" msgstr "" +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:316 +msgid "Row #{0}: Reading {1} {2} is not a valid number in the {3} number format. Use {4} as the decimal separator." +msgstr "" + #: erpnext/accounts/doctype/payment_entry/payment_entry.js:1251 msgid "Row #{0}: Reference Document Type must be one of Purchase Order, Purchase Invoice or Journal Entry" msgstr "" @@ -46477,7 +46522,7 @@ msgstr "" msgid "Row #{0}: Sequence ID must be {1} or {2} for Operation {3}." msgstr "" -#: erpnext/controllers/stock_controller.py:344 +#: erpnext/controllers/stock_controller.py:358 msgid "Row #{0}: Serial No {1} does not belong to Batch {2}" msgstr "" @@ -46493,15 +46538,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:664 +#: erpnext/controllers/accounts_controller.py:669 msgid "Row #{0}: Service End Date cannot be before Invoice Posting Date" msgstr "" -#: erpnext/controllers/accounts_controller.py:658 +#: erpnext/controllers/accounts_controller.py:663 msgid "Row #{0}: Service Start Date cannot be greater than Service End Date" msgstr "" -#: erpnext/controllers/accounts_controller.py:652 +#: erpnext/controllers/accounts_controller.py:657 msgid "Row #{0}: Service Start and End Date is required for deferred accounting" msgstr "" @@ -46525,11 +46570,11 @@ msgstr "" msgid "Row #{0}: Source Warehouse {1} for item {2} must be same as Source Warehouse {3} in the Work Order." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1363 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1372 msgid "Row #{0}: Source and Target Warehouse cannot be the same for Material Transfer" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1385 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1394 msgid "Row #{0}: Source, Target Warehouse and Inventory Dimensions cannot be the exact same for Material Transfer" msgstr "" @@ -46537,7 +46582,7 @@ msgstr "" msgid "Row #{0}: Start Time must be before End Time" msgstr "" -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:213 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:217 msgid "Row #{0}: Status is mandatory" msgstr "" @@ -46582,7 +46627,7 @@ msgstr "" msgid "Row #{0}: Target Warehouse must be same as Customer Warehouse {1} from the linked Subcontracting Inward Order" msgstr "" -#: erpnext/controllers/stock_controller.py:357 +#: erpnext/controllers/stock_controller.py:371 msgid "Row #{0}: The batch {1} has already expired." msgstr "" @@ -46602,7 +46647,7 @@ msgstr "" msgid "Row #{0}: Total Number of Depreciations must be greater than zero" msgstr "" -#: erpnext/controllers/stock_controller.py:141 +#: erpnext/controllers/stock_controller.py:155 msgid "Row #{0}: Warehouse {1} does not match with the warehouse {2} in Serial and Batch Bundle {3}." msgstr "" @@ -46630,11 +46675,11 @@ msgstr "" msgid "Row #{0}: {1} can not be negative for item {2}" msgstr "" -#: erpnext/controllers/stock_controller.py:1308 +#: erpnext/controllers/stock_controller.py:1322 msgid "Row #{0}: {1} is mandatory for the Inventory Dimension {2}." msgstr "" -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:326 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:371 msgid "Row #{0}: {1} is not a valid reading field. Please refer to the field description." msgstr "" @@ -46646,7 +46691,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:3949 +#: erpnext/controllers/accounts_controller.py:3979 msgid "Row #{0}:Quantity for Item {1} cannot be zero." msgstr "" @@ -46747,11 +46792,11 @@ msgstr "" msgid "Row #{}: {} {} does not exist." msgstr "" -#: erpnext/stock/doctype/item/item.py:1527 +#: erpnext/stock/doctype/item/item.py:1537 msgid "Row #{}: {} {} doesn't belong to Company {}. Please select valid {}." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:440 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:450 msgid "Row No {0}: Warehouse is required. Please set a Default Warehouse for Item {1} and Company {2}" msgstr "" @@ -46763,7 +46808,7 @@ msgstr "" msgid "Row {0} picked quantity is less than the required quantity, additional {1} {2} required." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1961 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1991 msgid "Row {0}# Item {1} not found in 'Raw Materials Supplied' table in {2} {3}" msgstr "" @@ -46795,7 +46840,7 @@ msgstr "" msgid "Row {0}: Allocated amount {1} must be less than or equal to remaining payment amount {2}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1620 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1663 msgid "Row {0}: As {1} is enabled, raw materials cannot be added to {2} entry. Use {3} entry to consume raw materials." msgstr "" @@ -46803,7 +46848,7 @@ msgstr "" msgid "Row {0}: Bill of Materials not found for the Item {1}" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:936 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:946 msgid "Row {0}: Both Debit and Credit values cannot be zero" msgstr "" @@ -46815,7 +46860,7 @@ msgstr "" msgid "Row {0}: Conversion Factor is mandatory" msgstr "" -#: erpnext/controllers/accounts_controller.py:3265 +#: erpnext/controllers/accounts_controller.py:3270 msgid "Row {0}: Cost Center {1} does not belong to Company {2}" msgstr "" @@ -46843,7 +46888,7 @@ msgstr "" msgid "Row {0}: Delivery Warehouse cannot be same as Customer Warehouse for Item {1}." msgstr "" -#: erpnext/controllers/accounts_controller.py:2765 +#: erpnext/controllers/accounts_controller.py:2770 msgid "Row {0}: Due Date in the Payment Terms table cannot be before Posting Date" msgstr "" @@ -46851,7 +46896,7 @@ msgstr "" msgid "Row {0}: Either Delivery Note Item or Packed Item reference is mandatory." msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1027 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1037 #: erpnext/controllers/taxes_and_totals.py:1382 msgid "Row {0}: Exchange Rate is mandatory" msgstr "" @@ -46868,15 +46913,15 @@ msgstr "" msgid "Row {0}: Expense Account {1} is linked to company {2}. Please select an account belonging to company {3}." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:530 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:540 msgid "Row {0}: Expense Head changed to {1} as no Purchase Receipt is created against Item {2}." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:487 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:497 msgid "Row {0}: Expense Head changed to {1} because account {2} is not linked to warehouse {3} or it is not the default inventory account" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:512 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:522 msgid "Row {0}: Expense Head changed to {1} because expense is booked against this account in Purchase Receipt {2}" msgstr "" @@ -46893,7 +46938,7 @@ msgstr "" msgid "Row {0}: From Time and To Time of {1} is overlapping with {2}" msgstr "" -#: erpnext/controllers/stock_controller.py:1725 +#: erpnext/controllers/stock_controller.py:1739 msgid "Row {0}: From Warehouse is mandatory for internal transfers" msgstr "" @@ -47005,7 +47050,7 @@ msgstr "" msgid "Row {0}: Qty cannot be greater than {1} for the Item {2}." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:738 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:747 msgid "Row {0}: Qty in Stock UOM can not be zero." msgstr "" @@ -47017,7 +47062,7 @@ msgstr "" msgid "Row {0}: Quantity cannot be negative." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1233 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1242 msgid "Row {0}: Quantity not available for {4} in warehouse {1} at posting time of the entry ({2} {3})" msgstr "" @@ -47025,7 +47070,7 @@ msgstr "" msgid "Row {0}: Sales Invoice {1} is already created for {2}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:344 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:353 msgid "Row {0}: Serial/Batch has been reset to values linked with Work Order {1} because the previously selected serial/batch does not belong to this Work Order." msgstr "" @@ -47033,11 +47078,11 @@ msgstr "" msgid "Row {0}: Shift cannot be changed since the depreciation has already been processed" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1974 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2004 msgid "Row {0}: Subcontracted Item is mandatory for the raw material {1}" msgstr "" -#: erpnext/controllers/stock_controller.py:1716 +#: erpnext/controllers/stock_controller.py:1730 msgid "Row {0}: Target Warehouse is mandatory for internal transfers" msgstr "" @@ -47049,11 +47094,11 @@ msgstr "" msgid "Row {0}: The entire expense amount for account {1} in {2} has already been allocated." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:784 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:793 msgid "Row {0}: The item {1}, quantity must be positive number" msgstr "" -#: erpnext/controllers/accounts_controller.py:3242 +#: erpnext/controllers/accounts_controller.py:3247 msgid "Row {0}: The {3} Account {1} does not belong to the company {2}" msgstr "" @@ -47061,11 +47106,11 @@ msgstr "" msgid "Row {0}: To set {1} periodicity, difference between from and to date must be greater than or equal to {2}" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3954 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3986 msgid "Row {0}: Transferred quantity cannot be greater than the requested quantity." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:732 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:741 msgid "Row {0}: UOM Conversion Factor is mandatory" msgstr "" @@ -47086,7 +47131,7 @@ msgstr "" msgid "Row {0}: Workstation or Workstation Type is mandatory for an operation {1}" msgstr "" -#: erpnext/controllers/accounts_controller.py:1203 +#: erpnext/controllers/accounts_controller.py:1208 msgid "Row {0}: user has not applied the rule {1} on the item {2}" msgstr "" @@ -47098,7 +47143,7 @@ msgstr "" msgid "Row {0}: {1} must be greater than 0" msgstr "" -#: erpnext/controllers/accounts_controller.py:809 +#: erpnext/controllers/accounts_controller.py:814 msgid "Row {0}: {1} {2} cannot be same as {3} (Party Account) {4}" msgstr "" @@ -47144,7 +47189,7 @@ msgstr "" msgid "Rows with Same Account heads will be merged on Ledger" msgstr "" -#: erpnext/controllers/accounts_controller.py:2776 +#: erpnext/controllers/accounts_controller.py:2781 msgid "Rows with duplicate due dates in other rows were found: {0}" msgstr "" @@ -47152,7 +47197,7 @@ msgstr "" msgid "Rows: {0} have 'Payment Entry' as reference_type. This should not be set manually." msgstr "" -#: erpnext/controllers/accounts_controller.py:302 +#: erpnext/controllers/accounts_controller.py:307 msgid "Rows: {0} in {1} section are Invalid. Reference Name should point to a valid Payment Entry or Journal Entry." msgstr "" @@ -47275,7 +47320,7 @@ msgstr "" msgid "SLA Paused On" msgstr "" -#: erpnext/public/js/utils.js:1277 +#: erpnext/public/js/utils.js:1303 msgid "SLA is on hold since {0}" msgstr "" @@ -47365,7 +47410,7 @@ msgstr "" #: erpnext/accounts/doctype/tax_category/tax_category_dashboard.py:9 #: erpnext/accounts/doctype/tax_rule/tax_rule.json #: erpnext/crm/doctype/opportunity/opportunity.js:288 -#: erpnext/crm/doctype/opportunity/opportunity.py:158 +#: erpnext/crm/doctype/opportunity/opportunity.py:168 #: erpnext/projects/doctype/project/project_dashboard.py:15 #: erpnext/regional/report/vat_audit_report/vat_audit_report.py:145 #: erpnext/selling/doctype/quotation/quotation.json @@ -48229,12 +48274,12 @@ msgstr "" #. Label of the sample_size (Float) field in DocType 'Quality Inspection' #: erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py:93 -#: erpnext/public/js/controllers/transaction.js:2877 +#: erpnext/public/js/controllers/transaction.js:2882 #: erpnext/stock/doctype/quality_inspection/quality_inspection.json msgid "Sample Size" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:4457 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:4489 msgid "Sample quantity {0} cannot be more than received quantity {1}" msgstr "" @@ -48339,7 +48384,7 @@ msgstr "" msgid "Schedule Date" msgstr "" -#: erpnext/public/js/controllers/transaction.js:541 +#: erpnext/public/js/controllers/transaction.js:543 msgid "Schedule Name" msgstr "" @@ -48519,7 +48564,7 @@ msgstr "" msgid "Search by item code, serial number or barcode" msgstr "" -#: banking/src/components/features/BankReconciliation/CompanySelector.tsx:64 +#: banking/src/components/features/BankReconciliation/CompanySelector.tsx:77 msgid "Search company..." msgstr "" @@ -48751,7 +48796,7 @@ msgstr "" msgid "Select Items based on Delivery Date" msgstr "" -#: erpnext/public/js/controllers/transaction.js:2912 +#: erpnext/public/js/controllers/transaction.js:2917 msgid "Select Items for Quality Inspection" msgstr "" @@ -48776,12 +48821,12 @@ msgstr "" msgid "Select Job Worker Address" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1222 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1231 #: erpnext/selling/page/point_of_sale/pos_item_cart.js:966 msgid "Select Loyalty Program" msgstr "" -#: erpnext/public/js/controllers/transaction.js:527 +#: erpnext/public/js/controllers/transaction.js:529 msgid "Select Payment Schedule" msgstr "" @@ -48936,7 +48981,7 @@ msgstr "" msgid "Select date" msgstr "" -#: erpnext/controllers/accounts_controller.py:3017 +#: erpnext/controllers/accounts_controller.py:3022 msgid "Select finance book for the item {0} at row {1}" msgstr "" @@ -49138,7 +49183,7 @@ msgstr "" #: erpnext/selling/doctype/selling_settings/selling_settings.json #: erpnext/selling/workspace/selling/selling.json #: erpnext/setup/workspace/erpnext_settings/erpnext_settings.json -#: erpnext/stock/doctype/stock_settings/stock_settings.py:260 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:265 #: erpnext/workspace_sidebar/erpnext_settings.json msgid "Selling Settings" msgstr "" @@ -49196,7 +49241,7 @@ msgid "Send Emails to Suppliers" msgstr "" #. Label of the send_sms (Button) field in DocType 'SMS Center' -#: erpnext/public/js/controllers/transaction.js:727 +#: erpnext/public/js/controllers/transaction.js:729 #: erpnext/selling/doctype/sms_center/sms_center.json msgid "Send SMS" msgstr "" @@ -49343,7 +49388,7 @@ msgstr "" #: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js:74 #: erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py:114 -#: erpnext/public/js/controllers/transaction.js:2890 +#: erpnext/public/js/controllers/transaction.js:2895 #: erpnext/public/js/utils/serial_no_batch_selector.js:432 #: erpnext/selling/doctype/installation_note_item/installation_note_item.json #: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -49363,7 +49408,7 @@ msgstr "" #: erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.py:450 #: erpnext/stock/report/serial_no_ledger/serial_no_ledger.js:38 #: erpnext/stock/report/serial_no_ledger/serial_no_ledger.py:61 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:427 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:430 #: erpnext/stock/workspace/stock/stock.json #: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json #: erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json @@ -49387,7 +49432,7 @@ msgstr "" msgid "Serial No Already Assigned" msgstr "" -#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:33 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:39 msgid "Serial No Count" msgstr "" @@ -49404,7 +49449,7 @@ msgstr "" msgid "Serial No Range" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2752 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2802 msgid "Serial No Reserved" msgstr "" @@ -49461,7 +49506,7 @@ msgstr "" msgid "Serial No and Batch Traceability" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1245 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1295 msgid "Serial No is mandatory" msgstr "" @@ -49469,6 +49514,10 @@ msgstr "" msgid "Serial No is mandatory for Item {0}" msgstr "" +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:111 +msgid "Serial No status sync has been queued. Reload the report after a few minutes." +msgstr "" + #: erpnext/public/js/utils/serial_no_batch_selector.js:603 msgid "Serial No {0} already exists" msgstr "" @@ -49490,7 +49539,7 @@ msgstr "" msgid "Serial No {0} does not exist" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:3541 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:3591 msgid "Serial No {0} does not exists" msgstr "" @@ -49506,7 +49555,7 @@ msgstr "" msgid "Serial No {0} is already assigned to customer {1}. Can only be returned against the customer {1}" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:484 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:534 msgid "Serial No {0} is not present in the {1} {2}, hence you can't return it against the {1} {2}" msgstr "" @@ -49544,11 +49593,11 @@ msgstr "" msgid "Serial Nos / Batches" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2024 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2074 msgid "Serial Nos are created successfully" msgstr "" -#: erpnext/stock/stock_ledger.py:2330 +#: erpnext/stock/stock_ledger.py:2373 msgid "Serial Nos are reserved in Stock Reservation Entries, you need to unreserve them before proceeding." msgstr "" @@ -49622,26 +49671,26 @@ msgstr "" #: erpnext/stock/report/available_serial_no/available_serial_no.py:188 #: erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.py:31 #: erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py:80 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:411 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:188 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:414 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:197 #: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json #: erpnext/workspace_sidebar/stock.json msgid "Serial and Batch Bundle" msgstr "" -#: erpnext/stock/doctype/item/item.py:1122 +#: erpnext/stock/doctype/item/item.py:1132 msgid "Serial and Batch Bundle Exists" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2253 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2303 msgid "Serial and Batch Bundle created" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2349 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2399 msgid "Serial and Batch Bundle updated" msgstr "" -#: erpnext/controllers/stock_controller.py:237 +#: erpnext/controllers/stock_controller.py:251 msgid "Serial and Batch Bundle {0} is already used in {1} {2}." msgstr "" @@ -49649,7 +49698,7 @@ msgstr "" msgid "Serial and Batch Bundle {0} is not submitted" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2323 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2373 msgid "Serial and Batch Bundle {0} is submitted and its entries cannot be modified." msgstr "" @@ -49905,12 +49954,12 @@ msgid "Service Stop Date" msgstr "" #: erpnext/accounts/deferred_revenue.py:45 -#: erpnext/public/js/controllers/transaction.js:1799 +#: erpnext/public/js/controllers/transaction.js:1804 msgid "Service Stop Date cannot be after Service End Date" msgstr "" #: erpnext/accounts/deferred_revenue.py:42 -#: erpnext/public/js/controllers/transaction.js:1796 +#: erpnext/public/js/controllers/transaction.js:1801 msgid "Service Stop Date cannot be before Service Start Date" msgstr "" @@ -49934,7 +49983,7 @@ msgstr "" #. Label of the set_basic_rate_manually (Check) field in DocType 'Stock Entry #. Detail' -#: erpnext/stock/doctype/stock_entry/stock_entry.py:411 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:420 #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json msgid "Set Basic Rate Manually" msgstr "" @@ -49985,11 +50034,11 @@ msgstr "" msgid "Set Landed Cost Based on Purchase Invoice Rate" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1234 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1243 msgid "Set Loyalty Program" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:315 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:314 msgid "Set New Release Date" msgstr "" @@ -50522,7 +50571,7 @@ msgstr "" msgid "Shipping Address Template" msgstr "" -#: erpnext/controllers/accounts_controller.py:595 +#: erpnext/controllers/accounts_controller.py:600 msgid "Shipping Address does not belong to the {0}" msgstr "" @@ -50707,7 +50756,7 @@ msgstr "" msgid "Show Dimension Wise Stock" msgstr "" -#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js:29 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js:53 msgid "Show Disabled Items" msgstr "" @@ -50998,7 +51047,7 @@ msgstr "" msgid "Simultaneous" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:862 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:871 msgid "Since there is a process loss of {0} units for the finished good {1}, you should reduce the quantity by {0} units for the finished good {1} in the Items Table." msgstr "" @@ -51110,7 +51159,7 @@ msgstr "" msgid "Solvency Ratios" msgstr "" -#: erpnext/controllers/accounts_controller.py:4400 +#: erpnext/controllers/accounts_controller.py:4430 msgid "Some required Company details are missing. You don't have permission to update them. Please contact your System Manager." msgstr "" @@ -51183,11 +51232,11 @@ msgstr "" msgid "Source Stock Entry (Manufacture)" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1024 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1033 msgid "Source Stock Entry {0} belongs to Work Order {1}, not {2}. Please use a manufacture entry from the same Work Order." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2724 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2756 msgid "Source Stock Entry {0} has no finished goods quantity" msgstr "" @@ -51253,7 +51302,7 @@ msgstr "" msgid "Source and Target Location cannot be same" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:990 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:999 msgid "Source and target warehouse cannot be same for row {0}" msgstr "" @@ -51266,9 +51315,9 @@ msgstr "" msgid "Source of Funds (Liabilities)" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:957 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:973 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:980 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:966 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:982 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:989 msgid "Source warehouse is mandatory for row {0}" msgstr "" @@ -51641,7 +51690,7 @@ msgstr "" msgid "Status must be one of {0}" msgstr "" -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:279 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:283 msgid "Status set to rejected as there are one or more rejected readings." msgstr "" @@ -51671,8 +51720,8 @@ msgstr "" #: erpnext/accounts/doctype/account/account.json #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:96 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:158 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1406 -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1445 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1416 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1455 #: erpnext/accounts/report/account_balance/account_balance.js:58 msgid "Stock Adjustment" msgstr "" @@ -51758,11 +51807,27 @@ msgstr "" msgid "Stock Closing Entry" msgstr "" -#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:80 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:239 +msgid "Stock Closing Entry In Progress" +msgstr "" + +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:257 +msgid "Stock Closing Entry Outdated" +msgstr "" + +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:231 +msgid "Stock Closing Entry Required" +msgstr "" + +#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:122 msgid "Stock Closing Entry {0} already exists for the selected date range" msgstr "" -#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:101 +#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:144 +msgid "Stock Closing Entry {0} belongs to a closed accounting period. Cancel the Period Closing Voucher {1} first." +msgstr "" + +#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:159 msgid "Stock Closing Entry {0} has been queued for processing, system will take sometime to complete it." msgstr "" @@ -51779,7 +51844,7 @@ msgstr "" msgid "Stock Details" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1201 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1210 msgid "Stock Entries already created for Work Order {0}: {1}" msgstr "" @@ -51847,7 +51912,7 @@ msgstr "" msgid "Stock Entry {0} has created" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1325 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1335 msgid "Stock Entry {0} is not submitted" msgstr "" @@ -51868,6 +51933,10 @@ msgstr "" msgid "Stock Expenses" msgstr "" +#: erpnext/stock/stock_ledger.py:80 +msgid "Stock Frozen" +msgstr "" + #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:37 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:60 msgid "Stock In Hand" @@ -51901,7 +51970,7 @@ msgstr "" #. Name of a DocType #: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:113 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:149 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:158 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:30 msgid "Stock Ledger Entry" msgstr "" @@ -52025,7 +52094,7 @@ msgstr "" #: erpnext/stock/doctype/material_request_item/material_request_item.json #: erpnext/stock/report/item_where_used/item_where_used.py:76 #: erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py:34 -#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:34 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:40 msgid "Stock Qty" msgstr "" @@ -52116,9 +52185,9 @@ msgstr "" #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1706 #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1723 #: erpnext/stock/doctype/stock_settings/stock_settings.json -#: erpnext/stock/doctype/stock_settings/stock_settings.py:217 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:229 -#: erpnext/stock/doctype/stock_settings/stock_settings.py:243 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:222 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:234 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:248 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:182 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:195 #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js:207 @@ -52132,7 +52201,7 @@ msgid "Stock Reservation Entries Cancelled" msgstr "" #: erpnext/controllers/subcontracting_inward_controller.py:1037 -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:2262 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:2261 #: erpnext/manufacturing/doctype/work_order/work_order.py:2416 #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1784 msgid "Stock Reservation Entries Created" @@ -52311,7 +52380,7 @@ msgstr "" #: erpnext/stock/report/item_where_used/item_where_used.py:82 #: erpnext/stock/report/reserved_stock/reserved_stock.py:110 #: erpnext/stock/report/stock_balance/stock_balance.py:508 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:296 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:299 #: erpnext/subcontracting/doctype/subcontracting_inward_order_item/subcontracting_inward_order_item.json #: erpnext/subcontracting/doctype/subcontracting_inward_order_received_item/subcontracting_inward_order_received_item.json #: erpnext/subcontracting/doctype/subcontracting_inward_order_secondary_item/subcontracting_inward_order_secondary_item.json @@ -52337,7 +52406,7 @@ msgstr "" msgid "Stock Uom" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:758 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:768 msgid "Stock Update Not Allowed" msgstr "" @@ -52412,6 +52481,10 @@ msgstr "" msgid "Stock Value" msgstr "" +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:186 +msgid "Stock Value Mismatch" +msgstr "" + #. Label of a chart in the Stock Workspace #: erpnext/stock/workspace/stock/stock.json msgid "Stock Value by Item Group" @@ -52453,7 +52526,7 @@ msgstr "" msgid "Stock cannot be updated because the invoice contains a drop shipping item. Please disable 'Update Stock' or remove the drop shipping item." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:755 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:765 msgid "Stock cannot be updated for Purchase Invoice {0} because a Purchase Receipt {1} has already been created for this transaction. Please disable the 'Update Stock' checkbox in the Purchase Invoice and save the invoice." msgstr "" @@ -52486,12 +52559,20 @@ msgstr "" msgid "Stock transactions before {0} are frozen" msgstr "" +#: erpnext/stock/stock_ledger.py:74 +msgid "Stock transactions dated on or before {0} are frozen because the period is closed and the Stock Closing Entry {1} has been generated. To make changes, cancel the Period Closing Voucher first." +msgstr "" + #. Description of the 'Freeze stocks older than (days)' (Int) field in DocType #. 'Stock Settings' #: erpnext/stock/doctype/stock_settings/stock_settings.json msgid "Stock transactions that are older than the mentioned days cannot be modified." msgstr "" +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:254 +msgid "Stock transactions were created or modified after the Stock Closing Entry {0} was generated. Regenerate it before submitting the Period Closing Voucher." +msgstr "" + #. Description of the 'Auto reserve Stock for Sales Order on Purchase' (Check) #. field in DocType 'Stock Settings' #: erpnext/stock/doctype/stock_settings/stock_settings.json @@ -52544,7 +52625,7 @@ msgstr "" msgid "Sub Assemblies & Raw Materials" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:340 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:329 msgid "Sub Assembly Item" msgstr "" @@ -52560,7 +52641,7 @@ msgstr "" msgid "Sub Assembly Item Reference" msgstr "" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:449 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:438 msgid "Sub Assembly Item is mandatory" msgstr "" @@ -53015,11 +53096,11 @@ msgstr "" msgid "Subscription End Date" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:406 +#: erpnext/accounts/doctype/subscription/subscription.py:409 msgid "Subscription End Date is mandatory to follow calendar months" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:396 +#: erpnext/accounts/doctype/subscription/subscription.py:399 msgid "Subscription End Date must be after {0} as per the subscription plan" msgstr "" @@ -53079,7 +53160,7 @@ msgstr "" msgid "Subscription Start Date" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:774 +#: erpnext/accounts/doctype/subscription/subscription.py:782 msgid "Subscription for Future dates cannot be processed." msgstr "" @@ -53477,7 +53558,7 @@ msgstr "" msgid "Supplier Invoice No" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1841 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1851 msgid "Supplier Invoice No exists in Purchase Invoice {0}" msgstr "" @@ -53829,6 +53910,10 @@ msgstr "" msgid "Sync Now" msgstr "" +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js:6 +msgid "Sync Serial No Status" +msgstr "" + #: erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js:36 msgid "Sync Started" msgstr "" @@ -53868,7 +53953,7 @@ msgstr "" msgid "System will fetch all the entries if limit value is zero." msgstr "" -#: erpnext/controllers/accounts_controller.py:2256 +#: erpnext/controllers/accounts_controller.py:2261 msgid "System will not check over billing since amount for Item {0} in {1} is zero" msgstr "" @@ -53891,7 +53976,7 @@ msgstr "" msgid "TDS Computation Summary" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1599 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:1609 msgid "TDS Deducted" msgstr "" @@ -54078,9 +54163,9 @@ msgstr "" msgid "Target Warehouse {0} must be same as Delivery Warehouse {1} in the Subcontracting Inward Order Item." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:963 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:969 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:984 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:972 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:978 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:993 msgid "Target warehouse is mandatory for row {0}" msgstr "" @@ -55075,7 +55160,7 @@ msgstr "" msgid "The BOM which will be replaced" msgstr "" -#: erpnext/stock/serial_batch_bundle.py:1545 +#: erpnext/stock/serial_batch_bundle.py:1631 msgid "The Batch {0} has negative batch quantity {1}. To fix this, go to the batch and click on Recalculate Batch Qty. If the issue still persists, create an inward entry." msgstr "" @@ -55095,11 +55180,11 @@ msgstr "" msgid "The Excluded Fee is bigger than the Deposit it is deducted from." msgstr "" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:188 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:306 msgid "The GL Entries and closing balances will be processed in the background, it can take a few minutes." msgstr "" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:461 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:579 msgid "The GL Entries will be cancelled in the background, it can take a few minutes." msgstr "" @@ -55119,7 +55204,7 @@ msgstr "" msgid "The Pick List having Stock Reservation Entries cannot be updated. If you need to make changes, we recommend canceling the existing Stock Reservation Entries before updating the Pick List." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:3176 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:3208 msgid "The Process Loss Qty has reset as per job cards Process Loss Qty" msgstr "" @@ -55131,14 +55216,18 @@ msgstr "" msgid "The Serial No at Row #{0}: {1} is not available in warehouse {2}." msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2749 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2799 msgid "The Serial No {0} is reserved against the {1} {2} and cannot be used for any other transaction." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2142 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2174 msgid "The Serial and Batch Bundle {0} is not valid for this transaction. The 'Type of Transaction' should be 'Outward' instead of 'Inward' in Serial and Batch Bundle {0}" msgstr "" +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:236 +msgid "The Stock Closing Entry for {0} is not completed yet. Wait for it to complete before submitting the Period Closing Voucher." +msgstr "" + #: erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.js:17 msgid "The Stock Entry of type 'Manufacture' is known as backflush. Raw materials being consumed to manufacture finished goods is known as backflushing.

    When creating Manufacture Entry, raw-material items are backflushed based on BOM of production item. If you want raw-material items to be backflushed based on Material Transfer entry made against that Work Order instead, then you can set it under this field." msgstr "" @@ -55175,10 +55264,14 @@ msgstr "" msgid "The bank account is not a company account. Please select a company account" msgstr "" -#: erpnext/controllers/stock_controller.py:1482 +#: erpnext/controllers/stock_controller.py:1496 msgid "The batch {0} is reserved for {1} in the warehouse {2} and the remaining quantity is not enough to cover the reservations. So, cannot proceed with the {3} {4}." msgstr "" +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:179 +msgid "The closing balance {0} of the Stock Asset accounts does not match the closing value {1} of the Stock Balance report as on {2}. Resolve the difference using the Stock Ledger Variance report before closing the period." +msgstr "" + #: erpnext/regional/report/vat_audit_report/vat_audit_report.py:43 msgid "The company {0} is not in South Africa. VAT Audit Report is only available for companies in South Africa." msgstr "" @@ -55281,11 +55374,11 @@ msgstr "" msgid "The following batches are expired, please restock them:
    {0}" msgstr "" -#: erpnext/controllers/accounts_controller.py:446 +#: erpnext/controllers/accounts_controller.py:451 msgid "The following cancelled repost entries exist for {0}:

    {1}

    Kindly delete these entries before continuing." msgstr "" -#: erpnext/stock/doctype/item/item.py:949 +#: erpnext/stock/doctype/item/item.py:959 msgid "The following deleted attributes exist in Variants but not in the Template. You can either delete the Variants or keep the attribute(s) in template." msgstr "" @@ -55395,7 +55488,7 @@ msgstr "" msgid "The original invoice should be consolidated before or along with the return invoice." msgstr "" -#: erpnext/controllers/accounts_controller.py:224 +#: erpnext/controllers/accounts_controller.py:229 msgid "The outstanding amount {0} in {1} is lesser than {2}. Updating the outstanding to this invoice." msgstr "" @@ -55446,7 +55539,7 @@ msgstr "" msgid "The reference number of the transaction" msgstr "" -#: erpnext/public/js/utils.js:985 +#: erpnext/public/js/utils.js:1011 msgid "The reserved stock will be released when you update items. Are you certain you wish to proceed?" msgstr "" @@ -55499,7 +55592,7 @@ msgstr "" msgid "The shares don't exist with the {0}" msgstr "" -#: erpnext/stock/stock_ledger.py:839 +#: erpnext/stock/stock_ledger.py:866 msgid "The stock for the item {0} in the {1} warehouse was negative on the {2}. You should create a positive entry {3} before the date {4} and time {5} to post the correct valuation rate. For more details, please read the
    documentation." msgstr "" @@ -55601,7 +55694,7 @@ msgstr "" msgid "The {0} ({1}) must be equal to {2} ({3})" msgstr "" -#: erpnext/public/js/controllers/transaction.js:3382 +#: erpnext/public/js/controllers/transaction.js:3387 msgid "The {0} contains Unit Price Items." msgstr "" @@ -55702,7 +55795,7 @@ msgstr "" msgid "There is one unreconciled transaction before {0}." msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2079 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2111 msgid "There must be atleast 1 Finished Good in this Stock Entry" msgstr "" @@ -55814,7 +55907,7 @@ msgstr "" msgid "This covers all scorecards tied to this Setup" msgstr "" -#: erpnext/controllers/status_updater.py:501 +#: erpnext/controllers/status_updater.py:502 msgid "This document is over limit by {0} {1} for item {4}. Are you making another {3} against the same {2}?" msgstr "" @@ -55917,7 +56010,7 @@ msgstr "" msgid "This is considered dangerous from accounting point of view." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:536 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:546 msgid "This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice" msgstr "" @@ -56119,6 +56212,10 @@ msgstr "" msgid "This will restrict user access to other employee records" msgstr "" +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js:16 +msgid "This will update the warehouse and status of Serial Nos counted in {0} to match the stock ledger. Continue?" +msgstr "" + #: erpnext/controllers/selling_controller.py:886 msgid "This {} will be treated as material transfer." msgstr "" @@ -56345,7 +56442,7 @@ msgstr "" msgid "To Currency" msgstr "" -#: erpnext/controllers/accounts_controller.py:645 +#: erpnext/controllers/accounts_controller.py:650 #: erpnext/setup/doctype/holiday_list/holiday_list.py:121 msgid "To Date cannot be before From Date" msgstr "" @@ -56572,15 +56669,15 @@ msgstr "" msgid "To add subcontracted Item's raw materials if include exploded items is disabled." msgstr "" -#: erpnext/controllers/status_updater.py:494 +#: erpnext/controllers/status_updater.py:495 msgid "To allow over billing, update \"Over Billing Allowance\" in Accounts Settings or the Item." msgstr "" -#: erpnext/controllers/status_updater.py:488 +#: erpnext/controllers/status_updater.py:489 msgid "To allow over ordering, update \"Over Order Allowance\" in Buying Settings." msgstr "" -#: erpnext/controllers/status_updater.py:490 +#: erpnext/controllers/status_updater.py:491 msgid "To allow over receipt / delivery, update \"Over Receipt/Delivery Allowance\" in Stock Settings or the Item." msgstr "" @@ -56617,7 +56714,7 @@ msgid "To include sub-assembly costs and secondary items in Finished Goods on a msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2268 -#: erpnext/controllers/accounts_controller.py:3275 +#: erpnext/controllers/accounts_controller.py:3280 msgid "To include tax in row {0} in Item rate, taxes in rows {1} must also be included" msgstr "" @@ -56641,11 +56738,11 @@ msgstr "" msgid "To still proceed with editing this Attribute Value, enable {0} in Item Variant Settings." msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:627 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:637 msgid "To submit the invoice without purchase order please set {0} as {1} in {2}" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:649 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:659 msgid "To submit the invoice without purchase receipt please set {0} as {1} in {2}" msgstr "" @@ -57027,7 +57124,7 @@ msgstr "" msgid "Total Debit Transactions" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:942 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:952 msgid "Total Debit must be equal to Total Credit. The difference is {0}" msgstr "" @@ -57252,7 +57349,7 @@ msgstr "" msgid "Total Paid Amount" msgstr "" -#: erpnext/controllers/accounts_controller.py:2830 +#: erpnext/controllers/accounts_controller.py:2835 msgid "Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total" msgstr "" @@ -57554,7 +57651,7 @@ msgstr "" msgid "Total allocated percentage for sales team should be 100" msgstr "" -#: erpnext/selling/doctype/customer/customer.py:198 +#: erpnext/selling/doctype/customer/customer.py:199 msgid "Total contribution percentage should be equal to 100" msgstr "" @@ -58143,7 +58240,7 @@ msgstr "" msgid "Trial Period End Date" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:376 +#: erpnext/accounts/doctype/subscription/subscription.py:379 msgid "Trial Period End Date Cannot be before Trial Period Start Date" msgstr "" @@ -58152,7 +58249,7 @@ msgstr "" msgid "Trial Period Start Date" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:382 +#: erpnext/accounts/doctype/subscription/subscription.py:385 msgid "Trial Period Start date cannot be after Subscription Start Date" msgstr "" @@ -58344,7 +58441,7 @@ msgstr "" #: erpnext/manufacturing/doctype/workstation/workstation.js:480 #: erpnext/manufacturing/report/bom_explorer/bom_explorer.py:70 #: erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py:110 -#: erpnext/public/js/stock_analytics.js:94 erpnext/public/js/utils.js:858 +#: erpnext/public/js/stock_analytics.js:94 erpnext/public/js/utils.js:861 #: erpnext/quality_management/doctype/quality_goal_objective/quality_goal_objective.json #: erpnext/quality_management/doctype/quality_review_objective/quality_review_objective.json #: erpnext/selling/doctype/delivery_schedule_item/delivery_schedule_item.json @@ -58458,7 +58555,7 @@ msgstr "" msgid "UOM Name" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:4379 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:4411 msgid "UOM conversion factor required for UOM: {0} in Item: {1}" msgstr "" @@ -58642,7 +58739,7 @@ msgstr "" msgid "Unit Of Measure" msgstr "" -#: erpnext/controllers/accounts_controller.py:4132 +#: erpnext/controllers/accounts_controller.py:4162 msgid "Unit Price" msgstr "" @@ -59006,7 +59103,7 @@ msgstr "" #: erpnext/buying/doctype/purchase_order/purchase_order.js:324 #: erpnext/buying/doctype/supplier_quotation/supplier_quotation.js:43 -#: erpnext/public/js/utils.js:964 +#: erpnext/public/js/utils.js:990 #: erpnext/selling/doctype/quotation/quotation.js:136 #: erpnext/selling/doctype/sales_order/sales_order.js:90 #: erpnext/selling/doctype/sales_order/sales_order.js:946 @@ -59019,7 +59116,7 @@ msgstr "" #. Invoice' #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json #: erpnext/accounts/doctype/sales_invoice/sales_invoice.json -#: erpnext/controllers/accounts_controller.py:217 +#: erpnext/controllers/accounts_controller.py:222 msgid "Update Outstanding for Self" msgstr "" @@ -59104,7 +59201,7 @@ msgstr "" msgid "Updating Costing and Billing fields against this Project..." msgstr "" -#: erpnext/stock/doctype/item/item.py:1511 +#: erpnext/stock/doctype/item/item.py:1521 msgid "Updating Variants..." msgstr "" @@ -59722,11 +59819,11 @@ msgstr "" msgid "Valuation Rate (In / Out)" msgstr "" -#: erpnext/stock/stock_ledger.py:2056 +#: erpnext/stock/stock_ledger.py:2099 msgid "Valuation Rate Missing" msgstr "" -#: erpnext/stock/stock_ledger.py:2034 +#: erpnext/stock/stock_ledger.py:2077 msgid "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}." msgstr "" @@ -59758,7 +59855,7 @@ msgid "Valuation rate for the item as per Sales Invoice (Only for Internal Trans msgstr "" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2292 -#: erpnext/controllers/accounts_controller.py:3299 +#: erpnext/controllers/accounts_controller.py:3304 msgid "Valuation type charges can not be marked as Inclusive" msgstr "" @@ -59894,7 +59991,7 @@ msgstr "" msgid "Variant" msgstr "" -#: erpnext/stock/doctype/item/item.py:964 +#: erpnext/stock/doctype/item/item.py:974 msgid "Variant Attribute Error" msgstr "" @@ -59913,7 +60010,7 @@ msgstr "" msgid "Variant Based On" msgstr "" -#: erpnext/stock/doctype/item/item.py:992 +#: erpnext/stock/doctype/item/item.py:1002 msgid "Variant Based On cannot be changed" msgstr "" @@ -59931,7 +60028,7 @@ msgstr "" msgid "Variant Item" msgstr "" -#: erpnext/stock/doctype/item/item.py:962 +#: erpnext/stock/doctype/item/item.py:972 msgid "Variant Items" msgstr "" @@ -60254,7 +60351,7 @@ msgstr "" #: erpnext/stock/report/available_serial_no/available_serial_no.js:56 #: erpnext/stock/report/available_serial_no/available_serial_no.py:196 #: erpnext/stock/report/stock_ledger/stock_ledger.js:97 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:404 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:407 msgid "Voucher #" msgstr "" @@ -60353,12 +60450,12 @@ msgstr "" #: erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py:112 #: erpnext/stock/report/serial_no_ledger/serial_no_ledger.py:34 #: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:159 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:176 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:185 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:74 msgid "Voucher No" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1485 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1535 msgid "Voucher No is mandatory" msgstr "" @@ -60427,8 +60524,8 @@ msgstr "" #: erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.py:486 #: erpnext/stock/report/serial_no_ledger/serial_no_ledger.py:28 #: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py:157 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:402 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:170 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:405 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:179 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:68 msgid "Voucher Type" msgstr "" @@ -60636,6 +60733,7 @@ msgid "Warehouse {0} does not belong to company {1}" msgstr "" #: erpnext/stock/doctype/warehouse/warehouse.py:288 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:99 msgid "Warehouse {0} does not exist" msgstr "" @@ -60643,11 +60741,11 @@ msgstr "" msgid "Warehouse {0} is not allowed for Sales Order {1}, it should be {2}" msgstr "" -#: erpnext/controllers/stock_controller.py:861 +#: erpnext/controllers/stock_controller.py:875 msgid "Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}." msgstr "" -#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:20 +#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:26 msgid "Warehouse: {0} does not belong to {1}" msgstr "" @@ -60756,7 +60854,7 @@ msgstr "" msgid "Warning - Row {0}: Billing Hours are more than Actual Hours" msgstr "" -#: erpnext/stock/stock_ledger.py:849 +#: erpnext/stock/stock_ledger.py:876 msgid "Warning on Negative Stock" msgstr "" @@ -60768,11 +60866,11 @@ msgstr "" msgid "Warning: Account changed for warehouse" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1331 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1341 msgid "Warning: Another {0} # {1} exists against stock entry {2}" msgstr "" -#: erpnext/stock/doctype/material_request/material_request.js:709 +#: erpnext/stock/doctype/material_request/material_request.js:705 msgid "Warning: Material Requested Qty is less than Minimum Order Qty" msgstr "" @@ -60870,7 +60968,7 @@ msgstr "" msgid "Wavelength In Megametres" msgstr "" -#: erpnext/controllers/accounts_controller.py:212 +#: erpnext/controllers/accounts_controller.py:217 msgid "We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, uncheck the '{2}' checkbox." msgstr "" @@ -61092,7 +61190,7 @@ msgstr "" msgid "When enabled, transactions with this supplier will be blocked based on the Hold Type below" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:408 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:417 msgid "When there are multiple finished goods ({0}) in a Repack stock entry, the basic rate for all finished goods must be set manually. To set rate manually, enable the checkbox 'Set Basic Rate Manually' in the respective finished good row." msgstr "" @@ -61331,7 +61429,7 @@ msgstr "" msgid "Work Order Item" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1027 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1036 msgid "Work Order Mismatch" msgstr "" @@ -61393,11 +61491,11 @@ msgstr "" msgid "Work Order {0} created" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2740 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2772 msgid "Work Order {0} has no produced qty" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1151 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1160 msgid "Work Order {0}: Job Card not found for the operation {1}" msgstr "" @@ -61725,7 +61823,7 @@ msgstr "" msgid "You are importing data for the code list:" msgstr "" -#: erpnext/controllers/accounts_controller.py:3929 +#: erpnext/controllers/accounts_controller.py:3959 msgid "You are not allowed to update as per the conditions set in {} Workflow." msgstr "" @@ -61741,6 +61839,10 @@ msgstr "" msgid "You are not authorized to set Frozen value" msgstr "" +#: erpnext/projects/doctype/task/task.py:317 +msgid "You are not permitted to create a Task for Project {0}" +msgstr "" + #: erpnext/stock/doctype/pick_list/pick_list.py:546 msgid "You are picking more than required quantity for the item {0}. Check if there is any other pick list created for the sales order {1}." msgstr "" @@ -61798,7 +61900,7 @@ msgstr "" msgid "You can set up the rule to split the transaction across multiple accounts." msgstr "" -#: erpnext/controllers/accounts_controller.py:233 +#: erpnext/controllers/accounts_controller.py:238 msgid "You can use {0} to reconcile against {1} later." msgstr "" @@ -61830,7 +61932,7 @@ msgstr "" msgid "You cannot create/amend any accounting entries till this date." msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:951 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:961 msgid "You cannot credit and debit same account at the same time" msgstr "" @@ -61858,7 +61960,7 @@ msgstr "" msgid "You cannot repost item valuation before {}" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:758 +#: erpnext/accounts/doctype/subscription/subscription.py:766 msgid "You cannot restart a Subscription that is not cancelled." msgstr "" @@ -61870,7 +61972,7 @@ msgstr "" msgid "You cannot submit the order without payment." msgstr "" -#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:116 +#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:119 msgid "You cannot {0} this document because another Period Closing Entry {1} exists after {2}" msgstr "" @@ -61887,7 +61989,7 @@ msgstr "" msgid "You do not have permission to import bank transactions" msgstr "" -#: erpnext/controllers/accounts_controller.py:3907 +#: erpnext/controllers/accounts_controller.py:3937 msgid "You do not have permissions to {} items in a {}." msgstr "" @@ -61899,11 +62001,11 @@ msgstr "" msgid "You don't have enough points to redeem." msgstr "" -#: erpnext/controllers/accounts_controller.py:4475 +#: erpnext/controllers/accounts_controller.py:4505 msgid "You don't have permission to create a Company Address. Please contact your System Manager." msgstr "" -#: erpnext/controllers/accounts_controller.py:4455 +#: erpnext/controllers/accounts_controller.py:4485 msgid "You don't have permission to update Company details. Please contact your System Manager." msgstr "" @@ -61911,7 +62013,7 @@ msgstr "" msgid "You don't have permission to update Received Qty DocField for item {0}" msgstr "" -#: erpnext/controllers/accounts_controller.py:4449 +#: erpnext/controllers/accounts_controller.py:4479 msgid "You don't have permission to update this document. Please contact your System Manager." msgstr "" @@ -61919,7 +62021,7 @@ msgstr "" msgid "You had {} errors while creating opening invoices. Check {} for more details" msgstr "" -#: erpnext/public/js/utils.js:1064 +#: erpnext/public/js/utils.js:1090 msgid "You have already selected items from {0} {1}" msgstr "" @@ -61927,7 +62029,7 @@ msgstr "" msgid "You have been invited to collaborate on the project {0}." msgstr "" -#: erpnext/stock/doctype/stock_settings/stock_settings.py:255 +#: erpnext/stock/doctype/stock_settings/stock_settings.py:260 msgid "You have enabled {0} and {1} in {2}. This can lead to prices from the default price list being inserted in the transaction price list." msgstr "" @@ -61947,7 +62049,7 @@ msgstr "" msgid "You have not performed any reconciliations in this session yet." msgstr "" -#: erpnext/stock/doctype/item/item.py:1187 +#: erpnext/stock/doctype/item/item.py:1197 msgid "You have to enable auto re-order in Stock Settings to maintain re-order levels." msgstr "" @@ -61967,7 +62069,7 @@ msgstr "" msgid "You need to cancel POS Closing Entry {} to be able to cancel this document." msgstr "" -#: erpnext/controllers/accounts_controller.py:3250 +#: erpnext/controllers/accounts_controller.py:3255 msgid "You selected the account group {1} as {2} Account in row {0}. Please select a single account." msgstr "" @@ -62027,7 +62129,7 @@ msgstr "" msgid "Zero Rated" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:738 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:747 msgid "Zero quantity" msgstr "" @@ -62053,7 +62155,7 @@ msgstr "" msgid "`Allow Negative rates for Items`" msgstr "" -#: erpnext/stock/stock_ledger.py:2048 +#: erpnext/stock/stock_ledger.py:2091 msgid "after" msgstr "" @@ -62073,7 +62175,7 @@ msgstr "" msgid "as a percentage of finished item quantity" msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1655 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1705 msgid "as of {0}" msgstr "" @@ -62093,7 +62195,7 @@ msgstr "" msgid "cannot be greater than 100" msgstr "" -#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:341 +#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:351 #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1158 msgid "dated {0}" msgstr "" @@ -62245,7 +62347,7 @@ msgstr "" msgid "per hour" msgstr "" -#: erpnext/stock/stock_ledger.py:2049 +#: erpnext/stock/stock_ledger.py:2092 msgid "performing either one below:" msgstr "" @@ -62317,12 +62419,12 @@ msgstr "" msgid "sold" msgstr "" -#: erpnext/accounts/doctype/subscription/subscription.py:734 +#: erpnext/accounts/doctype/subscription/subscription.py:742 msgid "subscription is already cancelled." msgstr "" -#: erpnext/controllers/status_updater.py:504 -#: erpnext/controllers/status_updater.py:523 +#: erpnext/controllers/status_updater.py:505 +#: erpnext/controllers/status_updater.py:524 msgid "target_ref_field" msgstr "" @@ -62389,7 +62491,7 @@ msgstr "" msgid "you must select Capital Work in Progress Account in accounts table" msgstr "" -#: erpnext/controllers/accounts_controller.py:1313 +#: erpnext/controllers/accounts_controller.py:1318 msgid "{0} '{1}' is disabled" msgstr "" @@ -62405,7 +62507,7 @@ msgstr "" msgid "{0} {1} has submitted Assets. Remove Item {2} from table to continue." msgstr "" -#: erpnext/controllers/accounts_controller.py:2410 +#: erpnext/controllers/accounts_controller.py:2415 msgid "{0} Account not found against Customer {1}." msgstr "" @@ -62465,19 +62567,19 @@ msgstr "" msgid "{0} account not found while submitting purchase receipt" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1071 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1081 msgid "{0} against Bill {1} dated {2}" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1080 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1090 msgid "{0} against Purchase Order {1}" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1047 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1057 msgid "{0} against Sales Invoice {1}" msgstr "" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1054 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1064 msgid "{0} against Sales Order {1}" msgstr "" @@ -62543,7 +62645,7 @@ msgstr "" msgid "{0} does not belong to Company {1}" msgstr "" -#: erpnext/controllers/accounts_controller.py:372 +#: erpnext/controllers/accounts_controller.py:377 msgid "{0} does not belong to the Company {1}." msgstr "" @@ -62585,7 +62687,7 @@ msgstr "" msgid "{0} hours" msgstr "" -#: erpnext/controllers/accounts_controller.py:2770 +#: erpnext/controllers/accounts_controller.py:2775 msgid "{0} in row {1}" msgstr "" @@ -62615,7 +62717,7 @@ msgstr "" msgid "{0} is already running for {1}" msgstr "" -#: erpnext/controllers/accounts_controller.py:194 +#: erpnext/controllers/accounts_controller.py:199 msgid "{0} is blocked so this transaction cannot proceed" msgstr "" @@ -62644,15 +62746,15 @@ msgstr "" msgid "{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}" msgstr "" -#: erpnext/controllers/accounts_controller.py:3207 +#: erpnext/controllers/accounts_controller.py:3212 msgid "{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}." msgstr "" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1879 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1929 msgid "{0} is not a CSV file." msgstr "" -#: erpnext/selling/doctype/customer/customer.py:244 +#: erpnext/selling/doctype/customer/customer.py:245 msgid "{0} is not a company bank account" msgstr "" @@ -62660,7 +62762,7 @@ msgstr "" msgid "{0} is not a group node. Please select a group node as parent cost center" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:790 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:799 msgid "{0} is not a stock Item" msgstr "" @@ -62732,7 +62834,7 @@ msgstr "" msgid "{0} languages are marked as default languages. Please select only one of them." msgstr "" -#: erpnext/controllers/sales_and_purchase_return.py:218 +#: erpnext/controllers/sales_and_purchase_return.py:236 msgid "{0} must be negative in return document" msgstr "" @@ -62752,7 +62854,7 @@ msgstr "" msgid "{0} payment entries can not be filtered by {1}" msgstr "" -#: erpnext/controllers/stock_controller.py:1903 +#: erpnext/controllers/stock_controller.py:1917 msgid "{0} qty of Item {1} is being received into Warehouse {2} with capacity {3}." msgstr "" @@ -62781,16 +62883,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:1701 erpnext/stock/stock_ledger.py:2216 -#: erpnext/stock/stock_ledger.py:2230 +#: erpnext/stock/stock_ledger.py:1744 erpnext/stock/stock_ledger.py:2259 +#: erpnext/stock/stock_ledger.py:2273 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:2360 erpnext/stock/stock_ledger.py:2405 msgid "{0} units of {1} needed in {2} on {3} {4} to complete this transaction." msgstr "" -#: erpnext/stock/stock_ledger.py:1695 +#: erpnext/stock/stock_ledger.py:1738 msgid "{0} units of {1} needed in {2} to complete this transaction." msgstr "" @@ -62882,6 +62984,14 @@ msgstr "" msgid "{0} {1} is associated with {2}, but Party Account is {3}" msgstr "" +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:911 +msgid "{0} {1} is blocked and on hold until {2}." +msgstr "" + +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:915 +msgid "{0} {1} is blocked." +msgstr "" + #: erpnext/controllers/selling_controller.py:494 #: erpnext/controllers/subcontracting_controller.py:1174 msgid "{0} {1} is cancelled or closed" @@ -62936,7 +63046,7 @@ msgstr "" msgid "{0} {1} must be submitted" msgstr "" -#: erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py:501 +#: erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py:506 msgid "{0} {1} not allowed to be reposted. You can enable it by adding it '{2}' table in {3}." msgstr "" @@ -62971,7 +63081,7 @@ msgstr "" msgid "{0} {1}: Accounting Entry for {2} can only be made in currency: {3}" msgstr "" -#: erpnext/controllers/stock_controller.py:1073 +#: erpnext/controllers/stock_controller.py:1087 msgid "{0} {1}: Cost Center is mandatory for Item {2}" msgstr "" @@ -63016,7 +63126,7 @@ msgstr "" msgid "{0}% of total invoice value will be given as discount." msgstr "" -#: erpnext/projects/doctype/task/task.py:130 +#: erpnext/projects/doctype/task/task.py:131 msgid "{0}'s {1} cannot be after {2}'s Expected End Date." msgstr "" @@ -63053,7 +63163,7 @@ msgstr "" msgid "{0}: select the typed value {1} from the list or clear it" msgstr "" -#: erpnext/controllers/accounts_controller.py:562 +#: erpnext/controllers/accounts_controller.py:567 msgid "{0}: {1} does not belong to the Company: {2}" msgstr "" @@ -63081,11 +63191,11 @@ msgstr "" msgid "{field_label} is mandatory for sub-contracted {doctype}." msgstr "" -#: erpnext/controllers/stock_controller.py:2369 +#: erpnext/controllers/stock_controller.py:2383 msgid "{item_name}'s Sample Size ({sample_size}) cannot be greater than the Accepted Quantity ({accepted_quantity})" msgstr "" -#: erpnext/controllers/stock_controller.py:2132 +#: erpnext/controllers/stock_controller.py:2146 msgid "{ref_doctype} {ref_name} status is {status}." msgstr "" diff --git a/erpnext/locale/bs.po b/erpnext/locale/bs.po index 3bcbae9fb06..d517b982210 100644 --- a/erpnext/locale/bs.po +++ b/erpnext/locale/bs.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: hello@frappe.io\n" -"POT-Creation-Date: 2026-08-02 10:09+0000\n" -"PO-Revision-Date: 2026-08-03 09:01\n" +"POT-Creation-Date: 2026-08-09 09:47+0000\n" +"PO-Revision-Date: 2026-08-10 11:05\n" "Last-Translator: hello@frappe.io\n" "Language-Team: Bosnian\n" "MIME-Version: 1.0\n" @@ -18,7 +18,7 @@ msgstr "" "X-Crowdin-File-ID: 169\n" "Language: bs_BA\n" -#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1657 +#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1707 msgid "\n" "\t\t\tThe Batch {0} of an item {1} has negative stock in the warehouse {2}{3}.\n" "\t\t\tPlease add a stock quantity of {4} to proceed with this entry.\n" @@ -45,7 +45,7 @@ msgstr " Adresa" msgid " Amount" msgstr "Iznos" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:133 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:122 msgid " BOM" msgstr " Sastavnica" @@ -64,7 +64,7 @@ msgstr " Je Podređena Tabela" msgid " Is Subcontracted" msgstr " Je Podugovjereno" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:215 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:204 msgid " Item" msgstr " Artikal" @@ -73,8 +73,8 @@ msgstr " Artikal" msgid " Name" msgstr " Naziv" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:163 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:204 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:152 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:193 msgid " Phantom Item" msgstr " Viritualni Artikal" @@ -82,7 +82,7 @@ msgstr " Viritualni Artikal" msgid " Rate" msgstr " Cjena" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:141 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:130 msgid " Raw Material" msgstr " Sirovina" @@ -91,8 +91,8 @@ msgstr " Sirovina" msgid " Skip Material Transfer" msgstr " Preskoči Prijenos Materijala" -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:152 -#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:193 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:141 +#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:182 msgid " Sub Assembly" msgstr " Podsklop" @@ -152,7 +152,7 @@ msgstr "% Završeno Metoda" #: erpnext/projects/doctype/project/project.py:226 msgid "% Complete must be between 0 and 100" -msgstr "" +msgstr "% dovršenosti mora biti između 0 i 100" #. Label of the percent_complete (Percent) field in DocType 'Project' #: erpnext/projects/doctype/project/project.json @@ -277,7 +277,7 @@ msgstr "% materijala isporučenih prema ovoj Listi Odabira" msgid "% of materials delivered against this Sales Order" msgstr "% materijala dostavljenog naspram ovog Prodajnog Naloga" -#: erpnext/controllers/accounts_controller.py:2414 +#: erpnext/controllers/accounts_controller.py:2419 msgid "'Account' in the Accounting section of Customer {0}" msgstr "'Račun' u sekciji Knjigovodstvo Klijenta {0}" @@ -293,11 +293,11 @@ msgstr "'Na Osnovu' i 'Grupiraj Po' ne mogu biti isti" msgid "'Days Since Last Order' must be greater than or equal to zero" msgstr "'Dana od posljednje narudžbe' mora biti veći ili jednako nuli" -#: erpnext/controllers/accounts_controller.py:2419 +#: erpnext/controllers/accounts_controller.py:2424 msgid "'Default {0} Account' in Company {1}" msgstr "'Standard {0} račun' u {1}" -#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1235 +#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1245 msgid "'Entries' cannot be empty" msgstr "Polje 'Unosi' ne može biti prazno" @@ -315,17 +315,17 @@ msgstr "'Od datuma' mora biti nakon 'Do datuma'" msgid "'Has Serial No' can not be 'Yes' for non-stock item" msgstr "'Ima Serijski Broj' ne može biti 'Da' za artikal koji nije na zalihama" -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:147 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:151 msgid "'Inspection Required before Delivery' has disabled for the item {0}, no need to create the QI" -msgstr "'Kontrola Obavezna prije Dostave' je onemogućena za artikal {0}, nema potrebe za kreiranjem Kontrole Kvaliteta" +msgstr "'Kontrola Obavezna prije Dostave' je onemogućena za artikal {0}, nema potrebe za izradom Kontrole Kvaliteta" -#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:138 +#: erpnext/stock/doctype/quality_inspection/quality_inspection.py:142 msgid "'Inspection Required before Purchase' has disabled for the item {0}, no need to create the QI" -msgstr "'Potrebna kontrola prije kupovine' je onemogućena za artikal {0}, nema potrebe za kreiranjem kvaliteta kontrole" +msgstr "'Potrebna kontrola prije kupovine' je onemogućena za artikal {0}, nema potrebe za izradom kvaliteta kontrole" -#: erpnext/stock/report/stock_ledger/stock_ledger.py:685 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:726 -#: erpnext/stock/report/stock_ledger/stock_ledger.py:831 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:688 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:781 +#: erpnext/stock/report/stock_ledger/stock_ledger.py:913 msgid "'Opening'" msgstr "'Početno'" @@ -349,7 +349,7 @@ msgstr "'Ažuriraj Zalihe' ne može se provjeriti za prodaju osnovne Imovine" #: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py:112 msgid "'Verification Link Expiry Duration' must be between 15 to 60 minutes." -msgstr "" +msgstr "'Trajanje Važenja Verifikacijskog Linka' mora biti između 15 i 60 minuta." #: erpnext/accounts/doctype/bank_account/bank_account.py:79 msgid "'{0}' account is already used by {1}. Use another account." @@ -365,17 +365,17 @@ msgid "'{0}' should be in company currency {1}." msgstr "'{0}' bi trebao biti u valuti {1}." #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:174 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:214 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:223 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:106 msgid "(A) Qty After Transaction" msgstr "(A) Količina Nakon Transakcije" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:219 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:228 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:111 msgid "(B) Expected Qty After Transaction" msgstr "(B) Očekivana Količina Nakon Transakcije" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:234 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:243 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:126 msgid "(C) Total Qty in Queue" msgstr "(C) Ukupna Količina u Redu" @@ -385,7 +385,7 @@ msgid "(C) Total qty in queue" msgstr "(C) Ukupna Količina u Redu" #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:194 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:244 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:253 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:136 msgid "(D) Balance Stock Value" msgstr "(D) Bilansna Vrijednost Zaliha" @@ -396,12 +396,12 @@ msgid "(Daily Yield * No of Units Produced) / 100" msgstr "(Dnevna Proizvodnja * Broj Proizvedenih Jedinica) / 100" #: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:199 -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:249 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:258 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:141 msgid "(E) Balance Stock Value in Queue" msgstr "(E) Bilansna Vrijednost Zaliha u Redu" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:259 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:268 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:151 msgid "(F) Change in Stock Value" msgstr "(F) Promjena Vrijednosti Zaliha" @@ -410,7 +410,7 @@ msgstr "(F) Promjena Vrijednosti Zaliha" msgid "(Forecast)" msgstr "(Prognoza)" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:264 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:273 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:156 msgid "(G) Sum of Change in Stock Value" msgstr "(G) Suma Promjene Vrijednosti Zaliha" @@ -421,7 +421,7 @@ msgstr "(G) Suma Promjene Vrijednosti Zaliha" msgid "(Good Units Produced / Total Units Produced) × 100" msgstr "(Proizvedene Jedinice / Ukupno Proizvedenih Jedinica) × 100" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:274 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:283 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:166 msgid "(H) Change in Stock Value (FIFO Queue)" msgstr "(H) Promjena Vrijednosti Zaliha (FIFO)" @@ -436,17 +436,17 @@ msgstr "(H) Stopa Vrednovanja" msgid "(Hour Rate / 60) * Actual Operation Time" msgstr "(Satnica / 60) * Stvarno Vrijeme Radnje" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:284 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:293 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:176 msgid "(I) Valuation Rate" msgstr "(I) Stopa Vrednovanja" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:289 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:298 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:181 msgid "(J) Valuation Rate as per FIFO" msgstr "(J) Stopa Vrednovanja prema FIFO" -#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:299 +#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:308 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:191 msgid "(K) Valuation = Value (D) ÷ Qty (A)" msgstr "(K) Vrijednovanje = Vrijednost (D) ÷ Količina (A)" @@ -779,9 +779,9 @@ msgstr "

    Primjer Predloška Ugovora

    \n\n" "-Važi do: {{ end_date }}\n" "\n\n" "

    Kako dobiti imena polja

    \n\n" -"

    Nazivi polja koje možete koristiti u svom predlošku ugovora su polja u ugovoru za koje izradi predložak. Možete saznati polja bilo kojeg dokumenta putem Podešavanja > Prilagodi prikaz obrasca i odabir vrste dokumenta (npr. Ugovor)

    \n\n" +"

    Nazivi polja koje možete koristiti u svom predlošku ugovora su polja u ugovoru za koje izradi predložak. Možete saznati polja bilo kojeg dokumenta putem Podešavanja > Prilagodi prikaz obrasca i odabir tipa dokumenta (npr. Ugovor)

    \n\n" "

    Predložak

    \n\n" -"

    Predložci se kompajliraju koristeći Jinja Templating Language. Da saznate više o Jinji, pročitajte ovu dokumentaciju.

    " +"

    Predložci se kompajliraju koristeći Jinja Templating Language. Da saznate više o Jinji, pročitaj ovu dokumentaciju.

    " #. Content of the 'Terms and Conditions Help' (HTML) field in DocType 'Terms #. and Conditions' @@ -826,7 +826,7 @@ msgstr "