mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-30 23:18:02 +00:00
fix(manufacturing): preserve job card transfer quantity (#58382)
This commit is contained in:
@@ -327,8 +327,8 @@ class TestJobCard(ERPNextTestSuite):
|
|||||||
|
|
||||||
job_card.reload()
|
job_card.reload()
|
||||||
|
|
||||||
self.assertEqual(transfer_entry_1.fg_completed_qty, 2)
|
self.assertEqual(transfer_entry_1.fg_completed_qty, 0)
|
||||||
self.assertEqual(job_card.transferred_qty, 2)
|
self.assertEqual(job_card.transferred_qty, 0)
|
||||||
|
|
||||||
# transfer second RM
|
# transfer second RM
|
||||||
transfer_entry_2 = make_stock_entry_from_jc(job_card_name)
|
transfer_entry_2 = make_stock_entry_from_jc(job_card_name)
|
||||||
@@ -336,9 +336,24 @@ class TestJobCard(ERPNextTestSuite):
|
|||||||
transfer_entry_2.insert()
|
transfer_entry_2.insert()
|
||||||
transfer_entry_2.submit()
|
transfer_entry_2.submit()
|
||||||
|
|
||||||
# 'For Quantity' here will be 0 since
|
self.assertEqual(transfer_entry_2.fg_completed_qty, 2)
|
||||||
# transfer was made for 2 fg qty in first transfer Stock Entry
|
job_card.reload()
|
||||||
self.assertEqual(transfer_entry_2.fg_completed_qty, 0)
|
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)
|
||||||
|
|
||||||
@ERPNextTestSuite.change_settings("Manufacturing Settings", {"job_card_excess_transfer": 1})
|
@ERPNextTestSuite.change_settings("Manufacturing Settings", {"job_card_excess_transfer": 1})
|
||||||
def test_job_card_excess_material_transfer(self):
|
def test_job_card_excess_material_transfer(self):
|
||||||
@@ -2408,9 +2423,19 @@ class TestJobCard(ERPNextTestSuite):
|
|||||||
self.assertEqual(flt(fg_row.qty), 3.0)
|
self.assertEqual(flt(fg_row.qty), 3.0)
|
||||||
me_b.submit()
|
me_b.submit()
|
||||||
|
|
||||||
def make_semi_fg_work_order(self, prefix, qty=5):
|
def test_semi_fg_job_card_transfer_keeps_completed_qty(self):
|
||||||
"""Two-operation semi FG work order: Op A makes the SFG from RM 1, final Op B
|
work_order = self.make_semi_fg_work_order("JC Transfer", skip_material_transfer=False)
|
||||||
consumes it. Both operations skip material transfer; stock is pre-seeded."""
|
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."""
|
||||||
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
|
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
|
||||||
from erpnext.stock.doctype.item.test_item import make_item
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
|
||||||
@@ -2443,7 +2468,7 @@ class TestJobCard(ERPNextTestSuite):
|
|||||||
"time_in_mins": 60,
|
"time_in_mins": 60,
|
||||||
"source_warehouse": warehouse,
|
"source_warehouse": warehouse,
|
||||||
"fg_warehouse": warehouse,
|
"fg_warehouse": warehouse,
|
||||||
"skip_material_transfer": 1,
|
"skip_material_transfer": skip_material_transfer,
|
||||||
}
|
}
|
||||||
operation2 = {
|
operation2 = {
|
||||||
"operation": f"{prefix} Op B",
|
"operation": f"{prefix} Op B",
|
||||||
@@ -2455,7 +2480,7 @@ class TestJobCard(ERPNextTestSuite):
|
|||||||
"time_in_mins": 60,
|
"time_in_mins": 60,
|
||||||
"source_warehouse": warehouse,
|
"source_warehouse": warehouse,
|
||||||
"fg_warehouse": warehouse,
|
"fg_warehouse": warehouse,
|
||||||
"skip_material_transfer": 1,
|
"skip_material_transfer": skip_material_transfer,
|
||||||
}
|
}
|
||||||
make_workstation(operation1)
|
make_workstation(operation1)
|
||||||
make_operation(operation1)
|
make_operation(operation1)
|
||||||
|
|||||||
@@ -193,17 +193,23 @@ class MaterialTransferForManufactureStockEntry(BaseMaterialTransferStockEntry):
|
|||||||
if not self._is_overproduction_allowed(flt(self.wo_doc.qty)):
|
if not self._is_overproduction_allowed(flt(self.wo_doc.qty)):
|
||||||
return
|
return
|
||||||
|
|
||||||
required_qty, transferred_qty = self._get_work_order_material_qty()
|
required_qty, transferred_qty, target_qty, precision = self._get_material_coverage_data()
|
||||||
if not required_qty:
|
if not required_qty:
|
||||||
return
|
return
|
||||||
|
|
||||||
covered_before = self._get_covered_work_order_qty(required_qty, transferred_qty)
|
covered_before = self._get_covered_qty(required_qty, transferred_qty, target_qty, precision)
|
||||||
for row in self.doc.items:
|
for row in self.doc.items:
|
||||||
item_code = row.original_item or row.item_code
|
if self.doc.job_card:
|
||||||
if row.s_warehouse and item_code in required_qty:
|
material_reference = row.job_card_item
|
||||||
transferred_qty[item_code] += flt(row.qty) * flt(row.conversion_factor or 1)
|
transferred = flt(row.qty)
|
||||||
|
else:
|
||||||
|
material_reference = row.original_item or row.item_code
|
||||||
|
transferred = flt(row.qty) * flt(row.conversion_factor or 1)
|
||||||
|
|
||||||
covered_after = self._get_covered_work_order_qty(required_qty, transferred_qty)
|
if row.s_warehouse and material_reference in required_qty:
|
||||||
|
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"))
|
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)
|
self.doc.fg_completed_qty = min(flt(self.doc.fg_completed_qty), covered_by_entry)
|
||||||
|
|
||||||
@@ -214,8 +220,52 @@ class MaterialTransferForManufactureStockEntry(BaseMaterialTransferStockEntry):
|
|||||||
return False
|
return False
|
||||||
if self.doc.is_return or self.doc.is_additional_transfer_entry:
|
if self.doc.is_return or self.doc.is_additional_transfer_entry:
|
||||||
return False
|
return False
|
||||||
|
if self.doc.job_card:
|
||||||
|
return True
|
||||||
return not (self.wo_doc.operations and self.wo_doc.transfer_material_against == "Job Card")
|
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):
|
def _get_work_order_material_qty(self):
|
||||||
required_qty = {}
|
required_qty = {}
|
||||||
transferred_qty = {}
|
transferred_qty = {}
|
||||||
@@ -227,15 +277,20 @@ class MaterialTransferForManufactureStockEntry(BaseMaterialTransferStockEntry):
|
|||||||
transferred_qty[row.item_code] = max(
|
transferred_qty[row.item_code] = max(
|
||||||
transferred_qty.get(row.item_code, 0.0), flt(row.transferred_qty)
|
transferred_qty.get(row.item_code, 0.0), flt(row.transferred_qty)
|
||||||
)
|
)
|
||||||
return required_qty, transferred_qty
|
return (
|
||||||
|
required_qty,
|
||||||
|
transferred_qty,
|
||||||
|
self.wo_doc.qty,
|
||||||
|
self.wo_doc.precision("required_qty", "required_items"),
|
||||||
|
)
|
||||||
|
|
||||||
def _get_covered_work_order_qty(self, required_qty, transferred_qty):
|
def _get_covered_qty(self, required_qty, transferred_qty, target_qty, precision):
|
||||||
min_fraction = get_minimum_material_coverage_fraction(
|
min_fraction = get_minimum_material_coverage_fraction(
|
||||||
required_qty,
|
required_qty,
|
||||||
transferred_qty,
|
transferred_qty,
|
||||||
self.wo_doc.precision("required_qty", "required_items"),
|
precision,
|
||||||
)
|
)
|
||||||
return min_fraction * flt(self.wo_doc.qty)
|
return min_fraction * flt(target_qty)
|
||||||
|
|
||||||
def validate_component_and_quantities(self):
|
def validate_component_and_quantities(self):
|
||||||
if self.doc.fg_completed_qty:
|
if self.doc.fg_completed_qty:
|
||||||
|
|||||||
Reference in New Issue
Block a user