fix: don't consider phantom item in additional costs in stock entry

This commit is contained in:
Smit Vora
2025-12-22 11:04:21 +05:30
parent 5ae7805429
commit 6e8a6582a0
2 changed files with 188 additions and 0 deletions

View File

@@ -1492,6 +1492,10 @@ def add_non_stock_items_cost(stock_entry, work_order, expense_account, job_card=
items = {}
for d in bom.get(table):
# Phantom item is exploded, so its cost is considered via its components
if d.is_phantom_item:
continue
items.setdefault(d.item_code, d.amount)
non_stock_items = frappe.get_all(

View File

@@ -3271,6 +3271,190 @@ class TestWorkOrder(IntegrationTestCase):
)
frappe.db.set_single_value("Stock Settings", "auto_reserve_serial_and_batch", original_auto_reserve)
def test_phantom_bom_item_not_in_additional_cost(self):
"""Test that phantom BOMs are not added to additional costs,
but regular non-stock items in the FG BOM are added."""
# Create items:
# - FG Item (stock item)
# - Phantom sub-assembly (non-stock item to be phantom)
# - Phantom RM (stock item - component of phantom BOM)
# - Packing Material (non-stock item - directly in FG BOM)
# - Regular RM (stock item - directly in FG BOM)
fg_item = make_item(
"Test FG Item For Phantom Non Stock",
{"is_stock_item": 1, "valuation_rate": 100},
).name
phantom_item = make_item(
"Test Phantom Sub Assembly Non Stock",
{"is_stock_item": 0, "valuation_rate": 0},
).name
phantom_rm = make_item(
"Test Phantom RM Item",
{"is_stock_item": 1, "valuation_rate": 200},
).name
packing_material = make_item(
"Test Packing Material Non Stock",
{"is_stock_item": 0, "valuation_rate": 150},
).name
regular_rm = make_item(
"Test Regular RM Stock Item",
{"is_stock_item": 1, "valuation_rate": 100},
).name
# Create price list entries for non-stock items
price_list = "_Test Price List India"
for item_code, rate in [
(phantom_item, 500),
(phantom_rm, 200),
(packing_material, 150),
]:
if not frappe.db.get_value("Item Price", {"item_code": item_code, "price_list": price_list}):
frappe.get_doc(
{
"doctype": "Item Price",
"item_code": item_code,
"price_list_rate": rate,
"price_list": price_list,
}
).insert(ignore_permissions=True)
# Create Phantom BOM (for the phantom sub-assembly)
phantom_bom = frappe.get_doc(
{
"doctype": "BOM",
"item": phantom_item,
"is_default": 1,
"is_active": 1,
"is_phantom_bom": 1, # Mark as phantom BOM
"currency": "INR",
"quantity": 1,
"company": "_Test Company",
"rm_cost_as_per": "Price List",
"buying_price_list": price_list,
}
)
phantom_bom.append(
"items",
{
"item_code": phantom_rm,
"qty": 1,
"rate": 200,
},
)
phantom_bom.insert()
phantom_bom.submit()
# Create FG BOM with phantom item, packing material, and regular RM
fg_bom = frappe.get_doc(
{
"doctype": "BOM",
"item": fg_item,
"is_default": 1,
"is_active": 1,
"currency": "INR",
"quantity": 1,
"company": "_Test Company",
"rm_cost_as_per": "Price List",
"buying_price_list": price_list,
}
)
# Add phantom item (will be marked as is_phantom_item based on is_phantom_bom)
fg_bom.append(
"items",
{
"item_code": phantom_item,
"qty": 1,
"rate": 200,
"bom_no": phantom_bom.name,
},
)
# Add packing material (non-stock, directly in FG BOM)
fg_bom.append(
"items",
{
"item_code": packing_material,
"qty": 1,
"rate": 150,
},
)
# Add regular RM (stock item)
fg_bom.append(
"items",
{
"item_code": regular_rm,
"qty": 1,
"rate": 100,
},
)
fg_bom.insert()
fg_bom.submit()
# Ensure stock for regular RM
test_stock_entry.make_stock_entry(
item_code=regular_rm,
target="_Test Warehouse - _TC",
qty=10,
basic_rate=100,
)
# Create work order
wo = make_wo_order_test_record(
production_item=fg_item,
bom_no=fg_bom.name,
qty=1,
source_warehouse="_Test Warehouse - _TC",
)
# Transfer materials
se_transfer = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 1))
se_transfer.insert()
se_transfer.submit()
# Manufacture
se_manufacture = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 1))
se_manufacture.insert()
# Verify additional costs
self.assertTrue(se_manufacture.additional_costs, "Additional costs should not be empty")
total_additional_cost = sum(row.amount for row in se_manufacture.additional_costs)
self.assertEqual(
total_additional_cost,
150, # only packing material; phantom RM excluded
f"Additional cost should be 150 (packing material only), got {total_additional_cost}",
)
self.assertEqual(
se_manufacture.total_outgoing_value,
300, # 100 (regular RM) + 200 (phantom RM)
f"Total outgoing value should be 300, got {se_manufacture.total_outgoing_value}",
)
self.assertEqual(
se_manufacture.total_incoming_value,
450, # 300 (RM total) + 150 (packing material)
f"Total incoming value should be 450, got {se_manufacture.total_incoming_value}",
)
# Clean up
se_manufacture.submit()
se_manufacture.cancel()
se_transfer.cancel()
wo.reload()
wo.cancel()
fg_bom.cancel()
phantom_bom.cancel()
def test_phantom_bom_explosion(self):
from erpnext.manufacturing.doctype.bom.test_bom import create_tree_for_phantom_bom_tests