mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-09 23:39:28 +00:00
revert(manufacturing): remove material coverage changes (#58714)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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",
|
||||
|
||||
@@ -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"):
|
||||
|
||||
Reference in New Issue
Block a user