mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-15 07:28:39 +00:00
fix(work order): exclude corrective transfers from item-level transferred qty (#58091)
This commit is contained in:
@@ -678,6 +678,93 @@ class TestJobCard(ERPNextTestSuite):
|
||||
self.assertEqual(corrective_job_card.get_onload("backflush_raw_materials_based_on"), "BOM")
|
||||
self.assertEqual(corrective_job_card.get_onload("transfer_material_against"), "Job Card")
|
||||
|
||||
def test_corrective_job_card_transfer_excluded_from_item_transferred_qty(self):
|
||||
from erpnext.manufacturing.doctype.work_order.mapper import (
|
||||
make_stock_entry as make_stock_entry_for_wo,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.work_order.mapper import (
|
||||
make_stock_return_entry,
|
||||
)
|
||||
|
||||
wo = make_wo_order_test_record(
|
||||
item="_Test FG Item 2",
|
||||
qty=4,
|
||||
transfer_material_against="Work Order",
|
||||
source_warehouse=self.source_warehouse,
|
||||
)
|
||||
self.generate_required_stock(wo)
|
||||
|
||||
transfer = frappe.get_doc(
|
||||
make_stock_entry_for_wo(wo.name, "Material Transfer for Manufacture", qty=2)
|
||||
)
|
||||
transfer.fg_completed_qty = 0
|
||||
transfer.submit()
|
||||
|
||||
job_card = frappe.get_last_doc("Job Card", {"work_order": wo.name})
|
||||
job_card.append(
|
||||
"time_logs",
|
||||
{"from_time": now(), "to_time": add_to_date(now(), hours=1), "completed_qty": 4},
|
||||
)
|
||||
job_card.submit()
|
||||
|
||||
corrective_operation = frappe.get_doc(
|
||||
doctype="Operation", is_corrective_operation=1, name=frappe.generate_hash()
|
||||
).insert()
|
||||
corrective_job_card = make_corrective_job_card(
|
||||
job_card.name, operation=corrective_operation.name, for_operation=job_card.operation
|
||||
)
|
||||
corrective_job_card.for_quantity = 1
|
||||
corrective_item = create_item(f"Corrective Item {frappe.generate_hash(length=8)}")
|
||||
corrective_source_warehouse = wo.required_items[0].source_warehouse
|
||||
make_stock_entry(
|
||||
item_code=corrective_item.name,
|
||||
target=corrective_source_warehouse,
|
||||
qty=1,
|
||||
basic_rate=100,
|
||||
)
|
||||
for row in wo.required_items:
|
||||
corrective_job_card.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": row.item_code,
|
||||
"source_warehouse": row.source_warehouse,
|
||||
"uom": frappe.db.get_value("Item", row.item_code, "stock_uom"),
|
||||
"required_qty": flt(row.required_qty) / 4,
|
||||
},
|
||||
)
|
||||
corrective_job_card.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": corrective_item.name,
|
||||
"source_warehouse": corrective_source_warehouse,
|
||||
"uom": corrective_item.stock_uom,
|
||||
"required_qty": 1,
|
||||
},
|
||||
)
|
||||
corrective_job_card.insert()
|
||||
|
||||
corrective_transfer = make_stock_entry_from_jc(corrective_job_card.name)
|
||||
corrective_transfer.submit()
|
||||
|
||||
wo.reload()
|
||||
self.assertNotIn(corrective_item.name, [row.item_code for row in wo.required_items])
|
||||
for row in wo.required_items:
|
||||
self.assertEqual(flt(row.transferred_qty), flt(row.required_qty) / 2)
|
||||
|
||||
stock_return = make_stock_return_entry(wo.name)
|
||||
stock_return.company = wo.company
|
||||
returned_by_item = {
|
||||
row.item_code: flt(row.transfer_qty) for row in stock_return.items if row.item_code
|
||||
}
|
||||
self.assertEqual(returned_by_item[corrective_item.name], 1)
|
||||
for row in wo.required_items:
|
||||
self.assertGreater(returned_by_item[row.item_code], flt(row.transferred_qty))
|
||||
|
||||
stock_return.submit()
|
||||
wo.reload()
|
||||
for row in wo.required_items:
|
||||
self.assertEqual(flt(row.returned_qty), flt(row.transferred_qty))
|
||||
|
||||
@ERPNextTestSuite.change_settings(
|
||||
"Manufacturing Settings",
|
||||
{
|
||||
|
||||
@@ -293,10 +293,13 @@ class RequiredItemsService:
|
||||
def _material_transfer_qty_by_item(self, is_return):
|
||||
ste = frappe.qb.DocType("Stock Entry")
|
||||
ste_child = frappe.qb.DocType("Stock Entry Detail")
|
||||
job_card = frappe.qb.DocType("Job Card")
|
||||
query = (
|
||||
frappe.qb.from_(ste)
|
||||
.inner_join(ste_child)
|
||||
.on(ste_child.parent == ste.name)
|
||||
.left_join(job_card)
|
||||
.on(ste.job_card == job_card.name)
|
||||
# original_item becomes the output dict key below, so it must stay coherent per row: the
|
||||
# same item_code can be transferred both for itself (original_item NULL) and as a substitute
|
||||
# for another required item (original_item set). Max() over a single item_code group could
|
||||
@@ -309,14 +312,30 @@ class RequiredItemsService:
|
||||
fn.Sum(ste_child.transfer_qty).as_("qty"),
|
||||
)
|
||||
.where(self._material_transfer_filter(ste, is_return))
|
||||
.where(fn.Coalesce(job_card.is_corrective_job_card, 0) == 0)
|
||||
.groupby(ste_child.item_code, ste_child.original_item)
|
||||
)
|
||||
qty_by_item = frappe._dict()
|
||||
for d in query.run(as_dict=1) or []:
|
||||
key = d.original_item or d.item_code
|
||||
qty_by_item[key] = (qty_by_item.get(key) or 0.0) + flt(d.qty)
|
||||
|
||||
if is_return:
|
||||
return self._cap_returned_qty_to_transferred(qty_by_item)
|
||||
|
||||
return qty_by_item
|
||||
|
||||
def _cap_returned_qty_to_transferred(self, returned_qty_by_item):
|
||||
# Work Order returns combine regular and corrective stock without a Job Card link.
|
||||
# Cap each return at the regular transfer total so corrective quantities stay neutral.
|
||||
transferred_qty_by_item = self._material_transfer_qty_by_item(is_return=0)
|
||||
return frappe._dict(
|
||||
{
|
||||
item_code: min(flt(returned_qty), flt(transferred_qty_by_item.get(item_code)))
|
||||
for item_code, returned_qty in returned_qty_by_item.items()
|
||||
}
|
||||
)
|
||||
|
||||
def _material_transfer_filter(self, ste, is_return):
|
||||
return (
|
||||
(ste.docstatus == 1)
|
||||
@@ -359,6 +378,11 @@ class RequiredItemsService:
|
||||
if stock_entry.purpose != "Material Transfer for Manufacture":
|
||||
return
|
||||
|
||||
if stock_entry.job_card and frappe.get_cached_value(
|
||||
"Job Card", stock_entry.job_card, "is_corrective_job_card"
|
||||
):
|
||||
return
|
||||
|
||||
additional_items = self._additional_items_by_code(stock_entry)
|
||||
self.doc.flags.ignore_validate_update_after_submit = True
|
||||
for rows in additional_items.values():
|
||||
|
||||
Reference in New Issue
Block a user