fix: match Subcontracting Order service cost by purchase order item

calculate_service_costs paired the service_items and items child tables
by list index, which breaks if the tables are not index-aligned (e.g.
populate_items_table skips a service item with zero available qty),
assigning the wrong service cost or raising IndexError. Match by
purchase_order_item instead, and guard against division by zero qty.

Adds a regression test asserting service costs follow purchase_order_item
regardless of table ordering.
This commit is contained in:
Nabin Hait
2026-06-22 12:56:26 +05:30
parent bbb7384ea5
commit d68f7ea9d1
2 changed files with 41 additions and 2 deletions

View File

@@ -182,8 +182,15 @@ class SubcontractingOrder(SubcontractingController):
self.calculate_items_qty_and_amount()
def calculate_service_costs(self):
for idx, item in enumerate(self.get("service_items")):
self.items[idx].service_cost_per_qty = item.amount / self.items[idx].qty
# Match by purchase_order_item rather than list position: the service_items and items
# tables are not guaranteed to stay index-aligned (e.g. a skipped zero-qty service item).
service_amount_by_po_item = {
service_item.purchase_order_item: service_item.amount
for service_item in self.get("service_items")
}
for item in self.items:
service_amount = flt(service_amount_by_po_item.get(item.purchase_order_item))
item.service_cost_per_qty = service_amount / item.qty if item.qty else 0
def calculate_supplied_items_qty_and_amount(self):
for item in self.get("items"):

View File

@@ -138,6 +138,38 @@ class TestSubcontractingOrder(ERPNextTestSuite):
self.assertEqual(scr.items[0].process_loss_qty, 1)
self.assertEqual(scr.items[0].qty, 9)
def test_service_cost_is_matched_by_purchase_order_item(self):
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 7",
"qty": 10,
"rate": 100,
"fg_item": "Subcontracted Item SA7",
"fg_item_qty": 10,
},
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 1",
"qty": 10,
"rate": 200,
"fg_item": "Subcontracted Item SA1",
"fg_item_qty": 10,
},
]
sco = get_subcontracting_order(service_items=service_items)
expected = {item.purchase_order_item: item.service_cost_per_qty for item in sco.items}
# The two finished goods have distinct service costs, so a position-based pairing would swap them
self.assertEqual(len(set(expected.values())), 2)
# Service costs must follow purchase_order_item, not list position
sco.service_items.reverse()
sco.calculate_service_costs()
for item in sco.items:
self.assertEqual(item.service_cost_per_qty, expected[item.purchase_order_item])
def test_make_rm_stock_entry(self):
sco = get_subcontracting_order()
rm_items = get_rm_items(sco.supplied_items)