mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-23 12:06:43 +00:00
fix(manufacturing): cap transferred qty by actual material coverage
A Material Transfer for Manufacture entry could claim any For Quantity regardless of what its rows carry; the work order copied that claim into material_transferred_for_manufacturing on submit. Editing rows down after generating the entry marked the work order fully transferred, blocking further transfers and allowing manufacture entries without material. Cap SUM(fg_completed_qty) by the finished-good qty the transferred item quantities actually cover (the pick-list min-fraction rule). Status now treats any raw-material transfer as material movement, not only pick list or material request sourced entries, so a zero-coverage partial transfer still moves the work order to In Process.
This commit is contained in:
@@ -161,22 +161,28 @@ class RequiredItemsService:
|
||||
self.recompute_material_transferred_for_manufacturing(transferred_items)
|
||||
|
||||
def recompute_material_transferred_for_manufacturing(self, transferred_items):
|
||||
"""Set material_transferred_for_manufacturing based on actual item-level transfers, not fg_completed_qty."""
|
||||
"""Set material_transferred_for_manufacturing to the claimed SUM(fg_completed_qty),
|
||||
capped by the finished-good qty the transferred item quantities actually cover.
|
||||
"""
|
||||
# Job Card transfers use the minimum completed quantity across operations.
|
||||
if self.doc.operations and self.doc.transfer_material_against == "Job Card":
|
||||
return
|
||||
|
||||
# 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(
|
||||
claimed_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)
|
||||
covered_qty = self._transfer_covered_qty(transferred_items)
|
||||
|
||||
if covered_qty is None:
|
||||
if claimed_qty:
|
||||
self.doc.db_set("material_transferred_for_manufacturing", claimed_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.
|
||||
material_transferred = min(claimed_qty, covered_qty) if claimed_qty else covered_qty
|
||||
self.doc.db_set("material_transferred_for_manufacturing", material_transferred)
|
||||
|
||||
def _transfer_covered_qty(self, transferred_items):
|
||||
"""Finished-good qty covered by the transferred raw materials, None when unmeasurable."""
|
||||
required_by_item = {}
|
||||
for row in self.doc.required_items:
|
||||
if not row.include_item_in_manufacturing or flt(row.required_qty) <= 0:
|
||||
@@ -184,15 +190,14 @@ class RequiredItemsService:
|
||||
required_by_item[row.item_code] = required_by_item.get(row.item_code, 0.0) + flt(row.required_qty)
|
||||
|
||||
if not required_by_item:
|
||||
return
|
||||
return None
|
||||
|
||||
min_fraction = min(
|
||||
flt(transferred_items.get(item_code) or 0) / required_qty
|
||||
for item_code, required_qty in required_by_item.items()
|
||||
)
|
||||
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)
|
||||
covered_qty = min(min_fraction, 1.0) * flt(self.doc.qty)
|
||||
return flt(covered_qty, self.doc.precision("material_transferred_for_manufacturing"))
|
||||
|
||||
def update_returned_qty(self):
|
||||
returned_dict = self._material_transfer_qty_by_item(is_return=1)
|
||||
|
||||
@@ -88,9 +88,9 @@ class StatusService:
|
||||
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.
|
||||
# Refresh material_transferred_for_manufacturing before deciding status so the
|
||||
# item-level transfer coverage is reflected immediately, instead of only after
|
||||
# the next status update call.
|
||||
self.doc.refresh_material_transferred_for_manufacturing()
|
||||
|
||||
if self.doc.status != "Closed":
|
||||
@@ -144,19 +144,10 @@ class StatusService:
|
||||
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)."""
|
||||
"""True if any raw material was transferred against this work order, even when the
|
||||
covered qty leaves material_transferred_for_manufacturing at 0."""
|
||||
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)
|
||||
@@ -167,7 +158,6 @@ 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
|
||||
|
||||
Reference in New Issue
Block a user