From b2233a44d1c37afd63ae691616a50a75aee30ac3 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 4 Sep 2026 14:32:28 +0530 Subject: [PATCH] revert(manufacturing): remove material coverage changes (#58714) --- .../doctype/job_card/test_job_card.py | 83 ++--------- .../work_order/services/material_coverage.py | 22 --- .../work_order/services/required_items.py | 25 ++-- .../doctype/work_order/services/status.py | 13 +- .../doctype/work_order/test_work_order.py | 130 +----------------- erpnext/patches.txt | 1 - .../repair_work_order_material_transfer.py | 66 --------- .../stock/doctype/material_request/mapper.py | 12 +- .../stock_entry/services/material_transfer.py | 114 --------------- 9 files changed, 40 insertions(+), 426 deletions(-) delete mode 100644 erpnext/manufacturing/doctype/work_order/services/material_coverage.py delete mode 100644 erpnext/patches/v16_0/repair_work_order_material_transfer.py diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index f3021611a68..414f8e67b25 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -327,8 +327,8 @@ class TestJobCard(ERPNextTestSuite): job_card.reload() - self.assertEqual(transfer_entry_1.fg_completed_qty, 0) - self.assertEqual(job_card.transferred_qty, 0) + self.assertEqual(transfer_entry_1.fg_completed_qty, 2) + self.assertEqual(job_card.transferred_qty, 2) # transfer second RM transfer_entry_2 = make_stock_entry_from_jc(job_card_name) @@ -336,61 +336,9 @@ class TestJobCard(ERPNextTestSuite): transfer_entry_2.insert() transfer_entry_2.submit() - self.assertEqual(transfer_entry_2.fg_completed_qty, 2) - job_card.reload() - self.assertEqual(job_card.transferred_qty, 2) - - def test_job_card_partial_material_transfer_qty(self): - 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}) - transfer_entry = make_stock_entry_from_jc(job_card.name) - for row in transfer_entry.items: - row.qty /= 2 - transfer_entry.submit() - - job_card.reload() - self.assertEqual(transfer_entry.fg_completed_qty, 1) - self.assertEqual(job_card.transferred_qty, 1) - - def test_material_request_stock_entry_uses_job_card_coverage(self): - from erpnext.stock.doctype.material_request.mapper import make_stock_entry - - self.transfer_material_against = "Job Card" - self.source_warehouse = "Stores - _TC" - job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name}) - mr = make_material_request(job_card.name) - mr.schedule_date = today() - for row in mr.items: - row.qty = flt(row.qty) / 2 - row.stock_qty = flt(row.stock_qty) / 2 - mr.submit() - - stock_entry = make_stock_entry(mr.name) - self.assertEqual(stock_entry.fg_completed_qty, job_card.for_quantity / 2) - - selected_row = mr.items[0] - try: - frappe.flags.selected_children = {"items": [selected_row.name]} - selected_stock_entry = make_stock_entry(mr.name) - finally: - frappe.flags.selected_children = None - - self.assertEqual( - [row.job_card_item for row in selected_stock_entry.items], [selected_row.job_card_item] - ) - self.assertEqual(selected_stock_entry.fg_completed_qty, 0) - - for row in mr.items: - transferred_qty = flt(row.stock_qty) / 2 - frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", transferred_qty) - frappe.db.set_value(row.doctype, row.name, "ordered_qty", transferred_qty) - mr.reload() - - repeated_stock_entry = make_stock_entry(mr.name) - self.assertEqual(repeated_stock_entry.fg_completed_qty, job_card.for_quantity / 4) + # 'For Quantity' here will be 0 since + # transfer was made for 2 fg qty in first transfer Stock Entry + self.assertEqual(transfer_entry_2.fg_completed_qty, 0) @ERPNextTestSuite.change_settings("Manufacturing Settings", {"job_card_excess_transfer": 1}) def test_job_card_excess_material_transfer(self): @@ -1054,7 +1002,6 @@ class TestJobCard(ERPNextTestSuite): self.assertEqual(ste.job_card, job_card_name) self.assertEqual(ste.from_bom, 1.0) self.assertEqual(ste.bom_no, work_order.bom_no) - self.assertEqual(ste.fg_completed_qty, frappe.get_value("Job Card", job_card_name, "for_quantity")) def test_job_card_material_transfer_via_pick_list(self): from erpnext.stock.doctype.material_request.mapper import create_pick_list @@ -2605,19 +2552,9 @@ class TestJobCard(ERPNextTestSuite): self.assertEqual(flt(fg_row.qty), 3.0) me_b.submit() - def test_semi_fg_job_card_transfer_keeps_completed_qty(self): - work_order = self.make_semi_fg_work_order("JC Transfer", skip_material_transfer=False) - job_card = self.get_semi_fg_job_card(work_order, "JC Transfer Op A") - - transfer_entry = make_stock_entry_from_jc(job_card.name) - transfer_entry.submit() - - job_card.reload() - self.assertEqual(transfer_entry.fg_completed_qty, job_card.for_quantity) - self.assertEqual(job_card.transferred_qty, job_card.for_quantity) - - def make_semi_fg_work_order(self, prefix, qty=5, skip_material_transfer=True): - """Create a two-operation semi-finished-goods Work Order with pre-seeded stock.""" + 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 @@ -2650,7 +2587,7 @@ class TestJobCard(ERPNextTestSuite): "time_in_mins": 60, "source_warehouse": warehouse, "fg_warehouse": warehouse, - "skip_material_transfer": skip_material_transfer, + "skip_material_transfer": 1, } operation2 = { "operation": f"{prefix} Op B", @@ -2662,7 +2599,7 @@ class TestJobCard(ERPNextTestSuite): "time_in_mins": 60, "source_warehouse": warehouse, "fg_warehouse": warehouse, - "skip_material_transfer": skip_material_transfer, + "skip_material_transfer": 1, } make_workstation(operation1) make_operation(operation1) diff --git a/erpnext/manufacturing/doctype/work_order/services/material_coverage.py b/erpnext/manufacturing/doctype/work_order/services/material_coverage.py deleted file mode 100644 index 8363e0c1284..00000000000 --- a/erpnext/manufacturing/doctype/work_order/services/material_coverage.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -from collections.abc import Mapping - -from frappe.utils import flt - - -def get_minimum_material_coverage_fraction( - required_qty: Mapping[str, float], transferred_qty: Mapping[str, float], precision: int -) -> float: - """Return the least-covered component ratio at the configured quantity precision.""" - coverage = [] - for item_code, required in required_qty.items(): - transferred = flt(transferred_qty.get(item_code)) - # Stored values can differ after the digits that the user can enter or see. - if flt(transferred, precision) == flt(required, precision): - coverage.append(1.0) - else: - coverage.append(transferred / required) - - return min(coverage, default=0.0) diff --git a/erpnext/manufacturing/doctype/work_order/services/required_items.py b/erpnext/manufacturing/doctype/work_order/services/required_items.py index 544c779e938..8ce58ff0143 100644 --- a/erpnext/manufacturing/doctype/work_order/services/required_items.py +++ b/erpnext/manufacturing/doctype/work_order/services/required_items.py @@ -15,9 +15,6 @@ from pypika import functions as fn from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict from erpnext.manufacturing.doctype.work_order.mapper import check_if_scrap_warehouse_mandatory -from erpnext.manufacturing.doctype.work_order.services.material_coverage import ( - get_minimum_material_coverage_fraction, -) from erpnext.manufacturing.doctype.work_order.services.reservation import ( WorkOrderStockReservation, get_consumed_qty, @@ -180,15 +177,22 @@ class RequiredItemsService: self.recompute_material_transferred_for_manufacturing(transferred_items) def recompute_material_transferred_for_manufacturing(self, transferred_items): - """Set transferred quantity from the raw materials that have actually moved.""" + """Set material_transferred_for_manufacturing based on actual item-level transfers, not fg_completed_qty.""" # Job Card transfers use the minimum completed quantity across operations. if self.doc.operations and self.doc.transfer_material_against == "Job Card": return - claimed_qty = StatusService(self.doc).get_transferred_or_manufactured_qty( + # When fg_completed_qty > 0 (direct stock entries, excess transfer), preserve the + # SUM(fg_completed_qty) approach so excess-transfer tracking works correctly. + sum_fg_completed_qty = StatusService(self.doc).get_transferred_or_manufactured_qty( "Material Transfer for Manufacture", "material_transferred_for_manufacturing" ) + if sum_fg_completed_qty: + self.doc.db_set("material_transferred_for_manufacturing", sum_fg_completed_qty) + return + # Pick list flow sets fg_completed_qty=0; use min-fraction of actual item transfers + # so partial availability does not prematurely mark the work order as fully transferred. required_by_item = {} for row in self.doc.required_items: if not row.include_item_in_manufacturing or flt(row.required_qty) <= 0: @@ -198,13 +202,12 @@ class RequiredItemsService: if not required_by_item: return - min_fraction = get_minimum_material_coverage_fraction( - required_by_item, - transferred_items, - self.doc.precision("required_qty", "required_items"), + min_fraction = min( + flt(transferred_items.get(item_code) or 0) / required_qty + for item_code, required_qty in required_by_item.items() ) - covered_qty = min_fraction * flt(self.doc.qty) - material_transferred = min(covered_qty, max(flt(self.doc.qty), claimed_qty)) + min_fraction = min(min_fraction, 1.0) + material_transferred = min_fraction * flt(self.doc.qty) self.doc.db_set("material_transferred_for_manufacturing", material_transferred) def update_returned_qty(self): diff --git a/erpnext/manufacturing/doctype/work_order/services/status.py b/erpnext/manufacturing/doctype/work_order/services/status.py index b2df261df68..e538c34d01a 100644 --- a/erpnext/manufacturing/doctype/work_order/services/status.py +++ b/erpnext/manufacturing/doctype/work_order/services/status.py @@ -142,9 +142,19 @@ class StatusService: return status def _has_transferred_material(self): - """True if any raw material was transferred against this work order.""" + """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) @@ -155,6 +165,7 @@ class StatusService: & (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 diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index b24d8a4b024..ba6f5ee165a 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -1479,11 +1479,9 @@ class TestWorkOrder(ERPNextTestSuite): del transfer_entry.get("items")[0] # transfer only one RM transfer_entry.submit() - # One required item is still missing, so no finished-good quantity is covered yet. + # WO's "Material Transferred for Mfg" shows all is transferred, one RM is pending work_order.reload() - self.assertEqual(transfer_entry.fg_completed_qty, 0) - self.assertEqual(work_order.material_transferred_for_manufacturing, 0) - self.assertEqual(work_order.status, "In Process") + self.assertEqual(work_order.material_transferred_for_manufacturing, 1) self.assertEqual(work_order.required_items[0].transferred_qty, 0) self.assertEqual(work_order.required_items[1].transferred_qty, 2) @@ -1503,39 +1501,6 @@ class TestWorkOrder(ERPNextTestSuite): self.assertEqual(work_order.required_items[0].transferred_qty, 1) self.assertEqual(work_order.required_items[1].transferred_qty, 2) - def test_material_transfer_claim_follows_actual_coverage(self): - work_order = make_wo_order_test_record(planned_start_date=now(), qty=4) - test_stock_entry.make_stock_entry( - item_code="_Test Item", target="_Test Warehouse - _TC", qty=10, basic_rate=5000.0 - ) - test_stock_entry.make_stock_entry( - item_code="_Test Item Home Desktop 100", - target="_Test Warehouse - _TC", - qty=20, - basic_rate=1000.0, - ) - - transfer_entry = frappe.get_doc( - make_stock_entry(work_order.name, "Material Transfer for Manufacture", 4) - ) - for row in transfer_entry.items: - if row.item_code == "_Test Item": - row.qty = 1 - transfer_entry.submit() - - work_order.reload() - self.assertEqual(transfer_entry.fg_completed_qty, 1) - self.assertEqual(work_order.material_transferred_for_manufacturing, 1) - - remainder_entry = frappe.get_doc( - make_stock_entry(work_order.name, "Material Transfer for Manufacture", 3) - ) - remainder_entry.submit() - - work_order.reload() - self.assertEqual(remainder_entry.fg_completed_qty, 3) - self.assertEqual(work_order.material_transferred_for_manufacturing, 4) - def test_material_transferred_min_fraction_on_partial_pick_list(self): """Pick-list flow (fg_completed_qty = 0): 'Material Transferred for Manufacturing' must reflect the least-transferred required item (the bottleneck), instead of being @@ -1598,97 +1563,6 @@ class TestWorkOrder(ERPNextTestSuite): work_order.reload() self.assertEqual(work_order.material_transferred_for_manufacturing, 2.0) - def test_material_transferred_ignores_hidden_precision_difference(self): - work_order = make_wo_order_test_record(planned_start_date=now(), qty=2) - test_stock_entry.make_stock_entry( - item_code="_Test Item", target="_Test Warehouse - _TC", qty=10, basic_rate=5000.0 - ) - test_stock_entry.make_stock_entry( - item_code="_Test Item Home Desktop 100", - target="_Test Warehouse - _TC", - qty=10, - basic_rate=1000.0, - ) - - precision = work_order.precision("required_qty", "required_items") - hidden_difference = 4 / (10 ** (precision + 1)) - row = work_order.required_items[0] - row.db_set("required_qty", flt(row.required_qty) + hidden_difference, update_modified=False) - work_order.reload() - required_qty = {row.item_code: flt(row.required_qty) for row in work_order.required_items} - - transfer_entry = frappe.get_doc( - make_stock_entry(work_order.name, "Material Transfer for Manufacture", 0) - ) - for item in transfer_entry.items: - item.qty = flt(required_qty[item.item_code], precision) - item.transfer_qty = item.qty - transfer_entry.submit() - - work_order.reload() - self.assertEqual( - flt(work_order.required_items[0].required_qty, precision), - flt(work_order.required_items[0].transferred_qty, precision), - ) - self.assertEqual(work_order.material_transferred_for_manufacturing, work_order.qty) - - def test_repair_material_transfer_precision_patch(self): - from erpnext.patches.v16_0.repair_work_order_material_transfer import ( - execute, - get_precision_affected_work_orders, - ) - - precision = frappe.get_precision("Work Order Item", "required_qty") - hidden_difference = 4 / (10 ** (precision + 1)) - work_order = make_wo_order_test_record(planned_start_date=now(), qty=2) - for index, row in enumerate(work_order.required_items): - required_qty = flt(row.required_qty) + (hidden_difference if index == 0 else 0) - row.db_set( - { - "required_qty": required_qty, - "transferred_qty": flt(required_qty, precision), - }, - update_modified=False, - ) - work_order.db_set("material_transferred_for_manufacturing", 1.99, update_modified=False) - - partial_work_order = make_wo_order_test_record(planned_start_date=now(), qty=2) - for row in partial_work_order.required_items: - row.db_set("transferred_qty", row.required_qty, update_modified=False) - partial_row = partial_work_order.required_items[0] - partial_row.db_set( - "transferred_qty", - flt(partial_row.required_qty, precision) - (1 / (10**precision)), - update_modified=False, - ) - partial_work_order.db_set("material_transferred_for_manufacturing", 1.99, update_modified=False) - - terminal_work_orders = [] - for status in ("Stopped", "Closed", "Completed"): - terminal_work_order = make_wo_order_test_record(planned_start_date=now(), qty=2) - for row in terminal_work_order.required_items: - row.db_set("transferred_qty", row.required_qty, update_modified=False) - terminal_work_order.db_set( - {"material_transferred_for_manufacturing": 1.99, "status": status}, - update_modified=False, - ) - terminal_work_orders.append(terminal_work_order) - - updates = get_precision_affected_work_orders() - self.assertIn(work_order.name, updates) - self.assertNotIn(partial_work_order.name, updates) - for terminal_work_order in terminal_work_orders: - self.assertNotIn(terminal_work_order.name, updates) - - execute() - work_order.reload() - partial_work_order.reload() - self.assertEqual(work_order.material_transferred_for_manufacturing, work_order.qty) - self.assertEqual(partial_work_order.material_transferred_for_manufacturing, 1.99) - for terminal_work_order in terminal_work_orders: - terminal_work_order.reload() - self.assertEqual(terminal_work_order.material_transferred_for_manufacturing, 1.99) - def _make_shared_alternative_transfer(self): work_order = make_wo_order_test_record(planned_start_date=now(), qty=2) test_stock_entry.make_stock_entry( diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 9e994f08745..b2320a6f455 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -514,7 +514,6 @@ erpnext.patches.v16_0.rename_italy_customer_name_fields erpnext.patches.v16_0.recalculate_purchase_receipt_billing_status erpnext.patches.v16_0.recalculate_mixed_purchase_receipt_billing_status erpnext.patches.v16_0.add_currency_to_blanket_orders -erpnext.patches.v16_0.repair_work_order_material_transfer erpnext.patches.v16_0.remove_frappe_crm_custom_fields erpnext.patches.v16_0.add_batch_split_stock_entry_type erpnext.patches.v16_0.add_transaction_roles_to_sms_settings diff --git a/erpnext/patches/v16_0/repair_work_order_material_transfer.py b/erpnext/patches/v16_0/repair_work_order_material_transfer.py deleted file mode 100644 index 94458b34466..00000000000 --- a/erpnext/patches/v16_0/repair_work_order_material_transfer.py +++ /dev/null @@ -1,66 +0,0 @@ -import frappe -from frappe.utils import flt -from pypika import functions as fn - -from erpnext.manufacturing.doctype.work_order.services.material_coverage import ( - get_minimum_material_coverage_fraction, -) - - -def execute(): - updates = get_precision_affected_work_orders() - frappe.db.bulk_update("Work Order", updates, update_modified=False) - - -def get_precision_affected_work_orders(): - """Return Work Orders whose components cover the plan at quantity precision.""" - work_orders = {} - for row in _get_candidate_rows(): - work_order = work_orders.setdefault( - row.work_order, - {"qty": flt(row.qty), "required_qty": {}, "transferred_qty": {}}, - ) - item_code = row.item_code - work_order["required_qty"][item_code] = work_order["required_qty"].get(item_code, 0.0) + flt( - row.required_qty - ) - work_order["transferred_qty"][item_code] = max( - work_order["transferred_qty"].get(item_code, 0.0), flt(row.transferred_qty) - ) - - precision = frappe.get_precision("Work Order Item", "required_qty") - return { - name: {"material_transferred_for_manufacturing": values["qty"]} - for name, values in work_orders.items() - if get_minimum_material_coverage_fraction( - values["required_qty"], values["transferred_qty"], precision - ) - >= 1.0 - } - - -def _get_candidate_rows(): - work_order = frappe.qb.DocType("Work Order") - required_item = frappe.qb.DocType("Work Order Item") - return ( - frappe.qb.from_(work_order) - .inner_join(required_item) - .on(required_item.parent == work_order.name) - .select( - work_order.name.as_("work_order"), - work_order.qty, - required_item.item_code, - required_item.required_qty, - required_item.transferred_qty, - ) - .where( - (work_order.docstatus == 1) - & (work_order.status.notin(["Stopped", "Closed", "Completed"])) - & (fn.Coalesce(work_order.skip_transfer, 0) == 0) - & (fn.Coalesce(work_order.track_semi_finished_goods, 0) == 0) - & (fn.Coalesce(work_order.material_transferred_for_manufacturing, 0) < work_order.qty) - & (fn.Coalesce(work_order.transfer_material_against, "") != "Job Card") - & (required_item.include_item_in_manufacturing == 1) - & (required_item.required_qty > 0) - ) - ).run(as_dict=True) diff --git a/erpnext/stock/doctype/material_request/mapper.py b/erpnext/stock/doctype/material_request/mapper.py index 2dbab79b9d7..70eb23b2740 100644 --- a/erpnext/stock/doctype/material_request/mapper.py +++ b/erpnext/stock/doctype/material_request/mapper.py @@ -398,16 +398,8 @@ def make_stock_entry(source_name: str, target_doc: str | dict | Document | None target.bom_no = work_order_details.bom_no target.use_multi_level_bom = work_order_details.use_multi_level_bom target.from_bom = 1 - if not source.job_card: - # not fg-qty-driven, mirrors the Pick List -> Stock Entry transfer for this Work Order - target.fg_completed_qty = 0 - - if source.job_card: - from erpnext.stock.doctype.stock_entry.services.material_transfer import ( - MaterialTransferForManufactureStockEntry, - ) - - MaterialTransferForManufactureStockEntry(target).cap_completed_qty_to_material_coverage() + # not fg-qty-driven, mirrors the Pick List -> Stock Entry transfer for this Work Order + target.fg_completed_qty = 0 doclist = get_mapped_doc( "Material Request", diff --git a/erpnext/stock/doctype/stock_entry/services/material_transfer.py b/erpnext/stock/doctype/stock_entry/services/material_transfer.py index 8012393faaa..11933cf4c9d 100644 --- a/erpnext/stock/doctype/stock_entry/services/material_transfer.py +++ b/erpnext/stock/doctype/stock_entry/services/material_transfer.py @@ -3,10 +3,6 @@ from frappe import _ from frappe.query_builder.functions import Sum from frappe.utils import cstr, flt -from erpnext.manufacturing.doctype.work_order.services.material_coverage import ( - get_minimum_material_coverage_fraction, -) - from .manufacturing import _check_bom_component_qty, get_bom_items from .stock_entry_base import BaseStockEntry @@ -183,118 +179,8 @@ class MaterialTransferForManufactureStockEntry(BaseMaterialTransferStockEntry): def validate(self): self.validate_warehouse() self.validate_component_and_quantities() - self._cap_completed_qty_to_material_coverage() self.validate_same_source_target_warehouse() - def _cap_completed_qty_to_material_coverage(self): - if not self._should_cap_completed_qty(): - return - # Keep an excessive claim intact so the Work Order allowance check can reject it. - if not self._is_overproduction_allowed(flt(self.wo_doc.qty)): - return - - self.cap_completed_qty_to_material_coverage() - - def cap_completed_qty_to_material_coverage(self): - required_qty, transferred_qty, target_qty, precision = self._get_material_coverage_data() - if not required_qty: - return - - covered_before = self._get_covered_qty(required_qty, transferred_qty, target_qty, precision) - for row in self.doc.items: - if self.doc.job_card: - material_reference = row.job_card_item - transferred = flt(row.qty) - else: - material_reference = row.original_item or row.item_code - transferred = flt(row.qty) * flt(row.conversion_factor or 1) - - if material_reference in required_qty and (self.doc.job_card or row.s_warehouse): - transferred_qty[material_reference] += transferred - - covered_after = self._get_covered_qty(required_qty, transferred_qty, target_qty, precision) - covered_by_entry = flt(max(covered_after - covered_before, 0), self.doc.precision("fg_completed_qty")) - self.doc.fg_completed_qty = min(flt(self.doc.fg_completed_qty), covered_by_entry) - - def _should_cap_completed_qty(self): - if self.doc.get("_action") != "submit": - return False - if not self.wo_doc or not self.doc.fg_completed_qty: - return False - if self.doc.is_return or self.doc.is_additional_transfer_entry: - return False - if self.doc.job_card: - return True - return not (self.wo_doc.operations and self.wo_doc.transfer_material_against == "Job Card") - - def _get_material_coverage_data(self): - if self.doc.job_card: - return self._get_job_card_material_qty() - return self._get_work_order_material_qty() - - def _get_job_card_material_qty(self): - job_card = frappe.get_doc("Job Card", self.doc.job_card) - required_qty = {} - transferred_qty = {} - for row in job_card.items: - if flt(row.required_qty) <= 0: - continue - required_qty[row.name] = flt(row.required_qty) - transferred_qty[row.name] = flt(row.transferred_qty) - - return ( - required_qty, - transferred_qty, - self._get_job_card_target_qty(job_card), - job_card.precision("required_qty", "items"), - ) - - def _get_job_card_target_qty(self, job_card): - required_by_item = {} - for row in job_card.items: - required_by_item[row.item_code] = required_by_item.get(row.item_code, 0.0) + flt(row.required_qty) - - work_order_required_by_item = {} - for row in self.wo_doc.required_items: - if not (job_card.operation == row.operation or job_card.operation_row_id == row.operation_row_id): - continue - work_order_required_by_item[row.item_code] = work_order_required_by_item.get( - row.item_code, 0.0 - ) + flt(row.required_qty) - - target_qty = [ - item_required * flt(self.wo_doc.qty) / work_order_required_by_item[item_code] - for item_code, item_required in required_by_item.items() - if work_order_required_by_item.get(item_code) - ] - return min(target_qty) if target_qty else job_card.for_quantity - - def _get_work_order_material_qty(self): - required_qty = {} - transferred_qty = {} - for row in self.wo_doc.required_items: - if not row.include_item_in_manufacturing or flt(row.required_qty) <= 0: - continue - required_qty[row.item_code] = required_qty.get(row.item_code, 0.0) + flt(row.required_qty) - # Duplicate required-item rows each hold the aggregate transferred quantity. - transferred_qty[row.item_code] = max( - transferred_qty.get(row.item_code, 0.0), flt(row.transferred_qty) - ) - return ( - required_qty, - transferred_qty, - self.wo_doc.qty, - self.wo_doc.precision("required_qty", "required_items"), - ) - - def _get_covered_qty(self, required_qty, transferred_qty, target_qty, precision): - min_fraction = get_minimum_material_coverage_fraction( - required_qty, - transferred_qty, - precision, - ) - return min_fraction * flt(target_qty) - def validate_component_and_quantities(self): if self.doc.fg_completed_qty: if frappe.db.get_single_value("Manufacturing Settings", "validate_components_quantities_per_bom"):