chore: rename type field to secondary_item_type (#55469)

(cherry picked from commit dbcfac839c)

# Conflicts:
#	erpnext/stock/doctype/stock_entry/stock_entry.py
#	erpnext/stock/doctype/stock_entry/stock_entry_handler/disassemble.py
#	erpnext/stock/doctype/stock_entry/stock_entry_handler/manufacturing.py
#	erpnext/stock/doctype/stock_entry/test_stock_entry.py
This commit is contained in:
Mihir Kandoi
2026-06-01 11:24:59 +05:30
committed by Mergify
parent ef2700bec6
commit 1c3d3632ad
30 changed files with 1793 additions and 82 deletions

View File

@@ -1441,7 +1441,7 @@ class StockController(AccountsController):
elif self.doctype == "Stock Entry" and row.t_warehouse:
qi_required = True # inward stock needs inspection
if row.get("type") or row.get("is_legacy_scrap_item"):
if row.get("secondary_item_type") or row.get("is_legacy_scrap_item"):
continue
if qi_required: # validate row only if inspection is required on item level

View File

@@ -160,7 +160,7 @@ class SubcontractingController(StockController):
).format(item.idx, get_link_to_form("Item", item.item_code))
)
if not item.get("type") and not item.get("is_legacy_scrap_item"):
if not item.get("secondary_item_type") and not item.get("is_legacy_scrap_item"):
if not is_sub_contracted_item:
frappe.throw(
_("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name)
@@ -1287,10 +1287,10 @@ class SubcontractingController(StockController):
total_amt = sum(
flt(item.amount)
for item in self.get("items")
if not item.get("type") and not item.get("is_legacy_scrap_item")
if not item.get("secondary_item_type") and not item.get("is_legacy_scrap_item")
)
for item in self.items:
if not item.get("type") and not item.get("is_legacy_scrap_item"):
if not item.get("secondary_item_type") and not item.get("is_legacy_scrap_item"):
item.additional_cost_per_qty = (
(item.amount * self.total_additional_costs) / total_amt
) / item.qty
@@ -1298,15 +1298,15 @@ class SubcontractingController(StockController):
total_qty = sum(
flt(item.qty)
for item in self.get("items")
if not item.get("type") and not item.get("is_legacy_scrap_item")
if not item.get("secondary_item_type") and not item.get("is_legacy_scrap_item")
)
additional_cost_per_qty = self.total_additional_costs / total_qty
for item in self.items:
if not item.get("type") and not item.get("is_legacy_scrap_item"):
if not item.get("secondary_item_type") and not item.get("is_legacy_scrap_item"):
item.additional_cost_per_qty = additional_cost_per_qty
else:
for item in self.items:
if not item.get("type") and not item.get("is_legacy_scrap_item"):
if not item.get("secondary_item_type") and not item.get("is_legacy_scrap_item"):
item.additional_cost_per_qty = 0
@frappe.whitelist()

View File

@@ -241,7 +241,7 @@ class SubcontractingInwardController:
item
for item in self.get("items")
if not item.is_finished_item
and not item.type
and not item.secondary_item_type
and not item.is_legacy_scrap_item
and frappe.get_cached_value("Item", item.item_code, "is_customer_provided_item")
]
@@ -372,7 +372,7 @@ class SubcontractingInwardController:
if self.purpose in ["Subcontracting Delivery", "Subcontracting Return", "Manufacture"]:
for item in self.items:
if (
item.is_finished_item or item.type or item.is_legacy_scrap_item
item.is_finished_item or item.secondary_item_type or item.is_legacy_scrap_item
) and item.valuation_rate == 0:
item.allow_zero_valuation_rate = 1
@@ -472,7 +472,7 @@ class SubcontractingInwardController:
self.validate_delivery_on_save()
else:
for item in self.items:
if not item.type and not item.is_legacy_scrap_item:
if not item.secondary_item_type and not item.is_legacy_scrap_item:
delivered_qty, returned_qty = frappe.get_value(
"Subcontracting Inward Order Item",
item.scio_detail,
@@ -543,7 +543,7 @@ class SubcontractingInwardController:
bold(
frappe.get_cached_value(
"Subcontracting Inward Order Item"
if not item.type and not item.is_legacy_scrap_item
if not item.secondary_item_type and not item.is_legacy_scrap_item
else "Subcontracting Inward Order Secondary Item",
item.scio_detail,
"stock_uom",
@@ -595,7 +595,7 @@ class SubcontractingInwardController:
)
for item in [item for item in self.items if not item.is_finished_item]:
if item.type or item.is_legacy_scrap_item:
if item.secondary_item_type or item.is_legacy_scrap_item:
scio_secondary_item = frappe.get_value(
"Subcontracting Inward Order Secondary Item",
{
@@ -655,7 +655,7 @@ class SubcontractingInwardController:
for item in self.items:
doctype = (
"Subcontracting Inward Order Item"
if not item.type and not item.is_legacy_scrap_item
if not item.secondary_item_type and not item.is_legacy_scrap_item
else "Subcontracting Inward Order Secondary Item"
)
qty_map[doctype][item.scio_detail] += (
@@ -780,7 +780,7 @@ class SubcontractingInwardController:
items = [
item
for item in self.items
if not item.is_finished_item and not item.type and not item.is_legacy_scrap_item
if not item.is_finished_item and not item.secondary_item_type and not item.is_legacy_scrap_item
]
item_code_wh = frappe._dict(
{
@@ -882,7 +882,9 @@ class SubcontractingInwardController:
def update_inward_order_secondary_items(self):
if (scio := self.subcontracting_inward_order) and self.purpose == "Manufacture":
secondary_items_list = [item for item in self.items if item.type or item.is_legacy_scrap_item]
secondary_items_list = [
item for item in self.items if item.secondary_item_type or item.is_legacy_scrap_item
]
secondary_items = defaultdict(float)
for item in secondary_items_list:
@@ -956,7 +958,7 @@ class SubcontractingInwardController:
stock_uom=secondary_item.stock_uom,
warehouse=secondary_item.t_warehouse,
produced_qty=secondary_item.transfer_qty,
type=secondary_item.type,
secondary_item_type=secondary_item.secondary_item_type,
delivered_qty=0,
reference_name=frappe.get_value(
"Work Order", self.work_order, "subcontracting_inward_order_item"

View File

@@ -337,14 +337,14 @@ class BOM(WebsiteGenerator):
if not item.qty:
frappe.throw(
_("Row #{0}: Quantity should be greater than 0 for {1} Item {2}").format(
item.idx, item.type, get_link_to_form("Item", item.item_code)
item.idx, item.secondary_item_type, get_link_to_form("Item", item.item_code)
)
)
if item.process_loss_per >= 100:
frappe.throw(
_("Row #{0}: Process Loss Percentage should be less than 100% for {1} Item {2}").format(
item.idx, item.type, get_link_to_form("Item", item.item_code)
item.idx, item.secondary_item_type, get_link_to_form("Item", item.item_code)
)
)
@@ -1276,7 +1276,9 @@ class BOM(WebsiteGenerator):
frappe.throw(msg, title=_("Invalid Process Loss Configuration"))
def has_scrap_items(self):
return any(d.get("type") == "Scrap" or d.get("is_legacy") for d in self.get("secondary_items"))
return any(
d.get("secondary_item_type") == "Scrap" or d.get("is_legacy") for d in self.get("secondary_items")
)
def get_bom_item_rate(args, bom_doc):
@@ -1444,7 +1446,7 @@ def get_bom_items_as_dict(
query = query.format(
table="BOM Secondary Item",
where_conditions=")",
select_columns=", item.description, bom_item.cost_allocation_per, bom_item.process_loss_per, bom_item.type, bom_item.name, bom_item.is_legacy",
select_columns=", item.description, bom_item.cost_allocation_per, bom_item.process_loss_per, bom_item.secondary_item_type, bom_item.name, bom_item.is_legacy",
is_stock_item=is_stock_item,
qty_field="stock_qty",
group_by_cond=group_by_cond,

View File

@@ -45,7 +45,7 @@
"stock_qty": 1.0,
"rate": 2000.0,
"stock_uom": "_Test UOM",
"type": "Scrap",
"secondary_item_type": "Scrap",
"is_legacy": 1
}
],

View File

@@ -6,7 +6,7 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"type",
"secondary_item_type",
"rate",
"column_break_gres",
"is_legacy",
@@ -35,7 +35,7 @@
"fields": [
{
"depends_on": "eval:!doc.is_legacy",
"fieldname": "type",
"fieldname": "secondary_item_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Type",
@@ -218,7 +218,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-03-11 12:12:29.208031",
"modified": "2026-06-01 10:00:00.000000",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Secondary Item",

View File

@@ -32,7 +32,7 @@ class BOMSecondaryItem(Document):
rate: DF.Currency
stock_qty: DF.Float
stock_uom: DF.Link | None
type: DF.Literal["", "Co-Product", "By-Product", "Scrap", "Additional Finished Good"]
secondary_item_type: DF.Literal["", "Co-Product", "By-Product", "Scrap", "Additional Finished Good"]
uom: DF.Link
# end: auto-generated types

View File

@@ -294,7 +294,7 @@ class JobCard(Document):
"stock_qty": values.qty,
"item_name": values.item_name,
"stock_uom": values.stock_uom,
"type": values.type,
"secondary_item_type": values.secondary_item_type,
"bom_secondary_item": values.name,
}
@@ -1531,7 +1531,7 @@ class JobCard(Document):
ste.stock_entry.pro_doc = frappe.get_doc("Work Order", self.work_order)
ste.stock_entry.set_secondary_items_from_job_card()
for row in ste.stock_entry.items:
if (row.type or row.is_legacy_scrap_item) and not row.t_warehouse:
if (row.secondary_item_type or row.is_legacy_scrap_item) and not row.t_warehouse:
row.t_warehouse = self.target_warehouse
if auto_submit:

View File

@@ -912,7 +912,7 @@ class TestJobCard(ERPNextTestSuite):
"qty": 1,
"process_loss_per": 10,
"cost_allocation_per": 5,
"type": "Scrap",
"secondary_item_type": "Scrap",
},
)
if submit:
@@ -995,7 +995,8 @@ class TestJobCard(ERPNextTestSuite):
},
)
job_card.append(
"secondary_items", {"item_code": scrap_extra.name, "stock_qty": 5, "type": "Co-Product"}
"secondary_items",
{"item_code": scrap_extra.name, "stock_qty": 5, "secondary_item_type": "Co-Product"},
)
job_card.submit()
@@ -1014,7 +1015,7 @@ class TestJobCard(ERPNextTestSuite):
self.assertEqual(manufacturing_entry.items[2].qty, 9)
self.assertEqual(flt(manufacturing_entry.items[2].basic_rate, 3), 5.556)
self.assertEqual(manufacturing_entry.items[3].item_code, scrap_extra.name)
self.assertEqual(manufacturing_entry.items[3].type, "Co-Product")
self.assertEqual(manufacturing_entry.items[3].secondary_item_type, "Co-Product")
self.assertEqual(manufacturing_entry.items[3].qty, 5)
self.assertEqual(manufacturing_entry.items[3].basic_rate, 0)
@@ -1059,7 +1060,9 @@ class TestJobCard(ERPNextTestSuite):
)
job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
job_card.append("secondary_items", {"item_code": "_Test Item", "stock_qty": 2, "type": "Scrap"})
job_card.append(
"secondary_items", {"item_code": "_Test Item", "stock_qty": 2, "secondary_item_type": "Scrap"}
)
job_card.append(
"time_logs",
{

View File

@@ -5,7 +5,7 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"type",
"secondary_item_type",
"description",
"column_break_3",
"item_code",
@@ -69,7 +69,7 @@
"read_only": 1
},
{
"fieldname": "type",
"fieldname": "secondary_item_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Type",
@@ -87,7 +87,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-03-06 13:51:00.492621",
"modified": "2026-06-01 10:00:00.000000",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card Secondary Item",

View File

@@ -22,7 +22,7 @@ class JobCardSecondaryItem(Document):
parenttype: DF.Data
stock_qty: DF.Float
stock_uom: DF.Link | None
type: DF.Literal["Co-Product", "By-Product", "Scrap", "Additional Finished Good"]
secondary_item_type: DF.Literal["Co-Product", "By-Product", "Scrap", "Additional Finished Good"]
# end: auto-generated types
pass

View File

@@ -2906,7 +2906,7 @@ def make_bom(**args):
bom.append(
"secondary_items",
{
"type": "Scrap",
"secondary_item_type": "Scrap",
"item_code": item,
"item_name": item,
"uom": item_doc.stock_uom,

View File

@@ -1092,7 +1092,7 @@ class TestWorkOrder(ERPNextTestSuite):
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
for row in stock_entry.items:
if row.type or row.is_legacy_scrap_item:
if row.secondary_item_type or row.is_legacy_scrap_item:
self.assertEqual(row.qty, 1)
# Partial Job Card 1 with qty 10
@@ -1104,7 +1104,7 @@ class TestWorkOrder(ERPNextTestSuite):
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
for row in stock_entry.items:
if row.type or row.is_legacy_scrap_item:
if row.secondary_item_type or row.is_legacy_scrap_item:
self.assertEqual(row.qty, 2)
# Partial Job Card 2 with qty 10
@@ -2191,7 +2191,7 @@ class TestWorkOrder(ERPNextTestSuite):
self.assertTrue(se_doc.additional_costs)
secondary_items = []
for item in se_doc.items:
if item.type or item.is_legacy_scrap_item:
if item.secondary_item_type or item.is_legacy_scrap_item:
secondary_items.append(item.item_code)
self.assertEqual(
@@ -2656,7 +2656,7 @@ class TestWorkOrder(ERPNextTestSuite):
# Secondary/Scrap item: should be taken from scrap warehouse in disassembly
scrap_row = next((i for i in stock_entry.items if i.item_code == scrap_item), None)
self.assertIsNotNone(scrap_row)
self.assertEqual(scrap_row.type, "Scrap")
self.assertEqual(scrap_row.secondary_item_type, "Scrap")
self.assertTrue(scrap_row.s_warehouse)
self.assertFalse(scrap_row.t_warehouse)
self.assertEqual(scrap_row.s_warehouse, wo.scrap_warehouse)

View File

@@ -483,3 +483,4 @@ erpnext.patches.v16_0.fix_titles
erpnext.patches.v16_0.set_not_applicable_on_german_item_tax_templates
erpnext.patches.v16_0.clear_procedures_from_receivable_report
erpnext.patches.v16_0.migrate_address_contact_custom_fields
erpnext.patches.v16_0.rename_secondary_item_type_field

View File

@@ -41,7 +41,7 @@ def insert_into_bom():
"conversion_factor": 1,
"qty": item.stock_qty,
"is_legacy": 1,
"type": "Scrap",
"secondary_item_type": "Scrap",
}
)
secondary_item.insert()
@@ -49,7 +49,14 @@ def insert_into_bom():
def insert_into_job_card():
fields = ["item_code", "item_name", "description", "stock_qty", "stock_uom"]
bulk_insert("Job Card", "Job Card Scrap Item", "Job Card Secondary Item", fields, ["type"], ["Scrap"])
bulk_insert(
"Job Card",
"Job Card Scrap Item",
"Job Card Secondary Item",
fields,
["secondary_item_type"],
["Scrap"],
)
def insert_into_subcontracting_inward():
@@ -67,7 +74,7 @@ def insert_into_subcontracting_inward():
"Subcontracting Inward Order Scrap Item",
"Subcontracting Inward Order Secondary Item",
fields,
["type"],
["secondary_item_type"],
["Scrap"],
)

View File

@@ -0,0 +1,18 @@
import frappe
from frappe.model.utils.rename_field import rename_field
def execute():
doctypes = [
"BOM Secondary Item",
"Job Card Secondary Item",
"Stock Entry Detail",
"Subcontracting Inward Order Secondary Item",
"Subcontracting Receipt Item",
]
for doctype in doctypes:
if not frappe.db.has_column(doctype, "type"):
continue
rename_field(doctype, "type", "secondary_item_type")

View File

@@ -1217,9 +1217,19 @@ class StockEntry(StockController, SubcontractingInwardController):
)
d.basic_rate = (outgoing_items_cost * (cost_allocation_per / 100)) / d.transfer_qty
<<<<<<< HEAD
if not d.basic_rate and not d.allow_zero_valuation_rate:
if self.is_new():
raise_error_if_no_rate = False
=======
if self.bom_no:
d.basic_rate *= frappe.get_value("BOM", self.bom_no, "cost_allocation_per") / 100
elif d.secondary_item_type and d.bom_secondary_item:
cost_allocation_per = frappe.get_value(
"BOM Secondary Item", d.bom_secondary_item, "cost_allocation_per"
)
d.basic_rate = (outgoing_items_cost * (cost_allocation_per / 100)) / d.transfer_qty
>>>>>>> dbcfac839c (chore: rename type field to secondary_item_type (#55469))
d.basic_rate = get_valuation_rate(
d.item_code,
@@ -1372,6 +1382,75 @@ class StockEntry(StockController, SubcontractingInwardController):
return flt((outgoing_items_cost - scrap_items_cost) / finished_item_qty)
<<<<<<< HEAD
=======
def _get_rm_cost_for_manufacture(self, settings, finished_item_qty, outgoing_items_cost):
if settings.get_rm_cost_from_consumption_entry and self.work_order:
if frappe.db.exists(
"Stock Entry",
{
"docstatus": 1,
"work_order": self.work_order,
"purpose": "Material Consumption for Manufacture",
},
):
self._validate_no_raw_materials_in_manufacture_entry(settings)
self._validate_single_manufacture_entry()
return self._fetch_consumption_entry_cost()
elif not outgoing_items_cost:
bom_items = self.get_bom_raw_materials(finished_item_qty)
outgoing_items_cost = sum([flt(row.qty) * flt(row.rate) for row in bom_items.values()])
return outgoing_items_cost
def _validate_no_raw_materials_in_manufacture_entry(self, settings):
for item in self.items:
if not item.is_finished_item and not item.secondary_item_type and not item.is_legacy_scrap_item:
label = frappe.get_meta(settings.doctype).get_label("get_rm_cost_from_consumption_entry")
frappe.throw(
_(
"Row {0}: As {1} is enabled, raw materials cannot be added to {2} entry. Use {3} entry to consume raw materials."
).format(
item.idx,
frappe.bold(label),
frappe.bold(_("Manufacture")),
frappe.bold(_("Material Consumption for Manufacture")),
)
)
def _validate_single_manufacture_entry(self):
if frappe.db.exists(
"Stock Entry",
{
"docstatus": 1,
"work_order": self.work_order,
"purpose": "Manufacture",
"name": ("!=", self.name),
},
):
frappe.throw(
_("Only one {0} entry can be created against the Work Order {1}").format(
frappe.bold(_("Manufacture")), frappe.bold(self.work_order)
)
)
def _fetch_consumption_entry_cost(self):
SE = frappe.qb.DocType("Stock Entry")
SE_ITEM = frappe.qb.DocType("Stock Entry Detail")
return (
frappe.qb.from_(SE)
.left_join(SE_ITEM)
.on(SE.name == SE_ITEM.parent)
.select(Sum(SE_ITEM.valuation_rate * SE_ITEM.transfer_qty))
.where(
(SE.docstatus == 1)
& (SE.work_order == self.work_order)
& (SE.purpose == "Material Consumption for Manufacture")
)
).run()[0][0] or 0
>>>>>>> dbcfac839c (chore: rename type field to secondary_item_type (#55469))
def distribute_additional_costs(self):
# If no incoming items, set additional costs blank
if not any(d.item_code for d in self.items if d.t_warehouse):
@@ -1742,7 +1821,7 @@ class StockEntry(StockController, SubcontractingInwardController):
d.is_finished_item = 1
else:
d.is_finished_item = 0
d.type = ""
d.secondary_item_type = ""
def get_finished_item(self):
finished_item = None

View File

@@ -0,0 +1,529 @@
from collections import defaultdict
import frappe
from frappe import _
from frappe.query_builder.functions import Sum
from frappe.utils import flt
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
from erpnext.stock.utils import get_combine_datetime
from .base import BaseStockEntry
from .manufacturing import (
ceil_qty_if_uom_has_whole_number,
get_bom_items,
get_production_item_details,
get_secondary_items,
)
class DisassembleStockEntry(BaseStockEntry):
def validate(self):
self.validate_warehouse()
def validate_warehouse(self):
for row in self.doc.items:
if not row.s_warehouse and not row.t_warehouse:
frappe.throw(_("Source or Target Warehouse is required for item {0}").format(row.item_code))
def validate_fg_completed_qty(self):
if not self.doc.source_stock_entry:
return
from erpnext.manufacturing.doctype.work_order.work_order import get_disassembly_available_qty
available_qty = get_disassembly_available_qty(self.doc.source_stock_entry, self.doc.name)
if flt(self.doc.fg_completed_qty) > available_qty:
frappe.throw(
_(
"Cannot disassemble {0} qty against Stock Entry {1}. Only {2} qty available to disassemble."
).format(
self.doc.fg_completed_qty,
self.doc.source_stock_entry,
available_qty,
),
title=_("Excess Disassembly"),
)
def add_items(self):
"""
Priority:
1. From a specific Manufacture Stock Entry (exact reversal)
2. From Work Order Manufacture Stock Entries (averaged reversal)
3. From BOM (standalone disassembly)
"""
# Auto-set source_stock_entry if WO has exactly one manufacture entry
if not self.doc.get("source_stock_entry") and self.doc.work_order:
manufacture_entries = frappe.get_all(
"Stock Entry",
filters={
"work_order": self.doc.work_order,
"purpose": "Manufacture",
"docstatus": 1,
},
pluck="name",
)
if len(manufacture_entries) == 1:
self.doc.source_stock_entry = manufacture_entries[0]
if self.doc.get("source_stock_entry"):
return self._add_items_for_disassembly_from_stock_entry()
if self.doc.work_order:
return self._add_items_for_disassembly_from_work_order()
return self._add_items_for_disassembly_from_bom()
def _add_items_for_disassembly_from_stock_entry(self):
source_fg_qty = frappe.db.get_value("Stock Entry", self.doc.source_stock_entry, "fg_completed_qty")
if not source_fg_qty:
frappe.throw(
_("Source Stock Entry {0} has no finished goods quantity").format(self.doc.source_stock_entry)
)
disassemble_qty = flt(self.doc.fg_completed_qty)
scale_factor = disassemble_qty / flt(source_fg_qty)
self._append_disassembly_row_from_source(
disassemble_qty=disassemble_qty,
scale_factor=scale_factor,
)
def _add_items_for_disassembly_from_work_order(self):
wo_produced_qty = frappe.db.get_value("Work Order", self.doc.work_order, "produced_qty")
wo_produced_qty = flt(wo_produced_qty)
if wo_produced_qty <= 0:
frappe.throw(_("Work Order {0} has no produced qty").format(self.doc.work_order))
disassemble_qty = flt(self.doc.fg_completed_qty)
if disassemble_qty <= 0:
frappe.throw(_("Disassemble Qty cannot be less than or equal to 0."))
scale_factor = disassemble_qty / wo_produced_qty
self._append_disassembly_row_from_source(
disassemble_qty=disassemble_qty,
scale_factor=scale_factor,
)
def _append_disassembly_row_from_source(self, disassemble_qty, scale_factor):
for source_row in self.get_items_from_manufacture_stock_entry():
self._append_disassembly_item(source_row, disassemble_qty, scale_factor)
def _get_disassembly_warehouses(self, source_row, disassemble_qty, scale_factor):
if source_row.is_finished_item:
return disassemble_qty, self.doc.from_warehouse or source_row.t_warehouse, ""
elif source_row.s_warehouse:
return flt(source_row.qty * scale_factor), "", self.doc.to_warehouse or source_row.s_warehouse
else:
return flt(source_row.qty * scale_factor), source_row.t_warehouse, ""
def _build_disassembly_item_dict(self, source_row, qty, s_warehouse, t_warehouse):
return {
"item_code": source_row.item_code,
"item_name": source_row.item_name,
"description": source_row.description,
"stock_uom": source_row.stock_uom,
"uom": source_row.uom,
"conversion_factor": source_row.conversion_factor,
"basic_rate": source_row.basic_rate,
"qty": qty,
"s_warehouse": s_warehouse,
"t_warehouse": t_warehouse,
"is_finished_item": source_row.is_finished_item,
"secondary_item_type": source_row.secondary_item_type,
"is_legacy_scrap_item": source_row.is_legacy_scrap_item,
"bom_secondary_item": source_row.bom_secondary_item,
"bom_no": source_row.bom_no,
"use_serial_batch_fields": 1 if (source_row.batch_no or source_row.serial_no) else 0,
}
def _append_disassembly_item(self, source_row, disassemble_qty, scale_factor):
qty, s_warehouse, t_warehouse = self._get_disassembly_warehouses(
source_row, disassemble_qty, scale_factor
)
item = self._build_disassembly_item_dict(source_row, qty, s_warehouse, t_warehouse)
if self.doc.source_stock_entry:
item.update({"against_stock_entry": self.doc.source_stock_entry, "ste_detail": source_row.name})
self.doc.append("items", item)
def _add_items_for_disassembly_from_bom(self):
if not self.doc.bom_no or not self.doc.fg_completed_qty:
frappe.throw(_("BOM and Finished Good Quantity is mandatory for Disassembly"))
self.add_raw_materials()
self.add_secondary_items()
self.add_finished_goods()
def add_raw_materials(self):
# Raw materials will be available after disassembly in target warehouse
items = get_bom_items(self.doc.bom_no, self.doc.use_multi_level_bom)
for row in items:
row["t_warehouse"] = self.doc.to_warehouse
row["from_warehouse"] = ""
row["is_finished_item"] = 0
row["qty"] = flt(row["qty"]) * flt(self.doc.fg_completed_qty)
row["uom"] = row.get("uom") or row.get("stock_uom")
self.doc.append("items", row)
def add_secondary_items(self):
# Secondary items will be removed from source warehouse
secondary_items = get_secondary_items(self.doc.bom_no, self.doc.work_order)
for row in secondary_items:
item_args = {}
fields = [
"item_code",
"item_name",
"uom",
"stock_uom",
"conversion_factor",
"item_group",
"description",
"secondary_item_type",
]
for field in fields:
item_args[field] = row.get(field)
item_args["is_legacy_scrap_item"] = row.get("is_legacy")
item_args["s_warehouse"] = self.doc.from_warehouse
item_args["uom"] = item_args.get("uom") or item_args.get("stock_uom")
item_args["bom_secondary_item"] = row.get("name")
row.qty = row.qty * self.doc.fg_completed_qty
if row.get("process_loss_per"):
row.qty -= flt(row.qty * row.get("process_loss_per") / 100)
item_args["qty"] = ceil_qty_if_uom_has_whole_number(row.qty, item_args["uom"])
self.doc.append("items", item_args)
def add_finished_goods(self):
item_details = get_production_item_details(self.doc.work_order, self.doc.bom_no)
item_details.update(
{
"conversion_factor": 1,
"uom": item_details.stock_uom,
"qty": self.doc.fg_completed_qty,
"t_warehouse": None,
"s_warehouse": self.doc.from_warehouse,
"is_finished_item": 1,
}
)
item_details["item_code"] = item_details["name"]
del item_details["name"]
self.doc.append("items", item_details)
def get_items_from_manufacture_stock_entry(self):
SE = frappe.qb.DocType("Stock Entry")
SED = frappe.qb.DocType("Stock Entry Detail")
query = frappe.qb.from_(SED).join(SE).on(SED.parent == SE.name).where(SE.docstatus == 1)
common_fields = [
SED.item_code,
SED.item_name,
SED.description,
SED.stock_uom,
SED.uom,
SED.basic_rate,
SED.conversion_factor,
SED.is_finished_item,
SED.secondary_item_type,
SED.is_legacy_scrap_item,
SED.bom_secondary_item,
SED.batch_no,
SED.serial_no,
SED.use_serial_batch_fields,
SED.s_warehouse,
SED.t_warehouse,
SED.bom_no,
]
if self.doc.source_stock_entry:
return (
query.select(SED.name, SED.qty, SED.transfer_qty, *common_fields)
.where(SE.name == self.doc.source_stock_entry)
.orderby(SED.idx)
.run(as_dict=True)
)
return (
query.select(Sum(SED.qty).as_("qty"), Sum(SED.transfer_qty).as_("transfer_qty"), *common_fields)
.where(SE.purpose == "Manufacture")
.where(SE.work_order == self.doc.work_order)
.groupby(SED.item_code)
.orderby(SED.idx)
.run(as_dict=True)
)
def on_submit(self):
self.set_serial_batch_for_disassembly()
self.update_disassembled_order()
def on_cancel(self):
self.update_disassembled_order()
def set_serial_batch_for_disassembly(self):
if self.doc.get("source_stock_entry"):
self._set_serial_batch_for_disassembly_from_stock_entry()
else:
self._set_serial_batch_for_disassembly_from_available_materials()
def _set_serial_batch_for_disassembly_from_stock_entry(self):
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
get_voucher_wise_serial_batch_from_bundle,
)
source_fg_qty = flt(
frappe.db.get_value("Stock Entry", self.doc.source_stock_entry, "fg_completed_qty")
)
scale_factor = flt(self.doc.fg_completed_qty) / source_fg_qty if source_fg_qty else 0
bundle_data = get_voucher_wise_serial_batch_from_bundle(voucher_no=[self.doc.source_stock_entry])
source_rows_by_name = {r.name: r for r in self.get_items_from_manufacture_stock_entry()}
for row in self.doc.items:
if not row.ste_detail:
continue
source_row = source_rows_by_name.get(row.ste_detail)
if source_row:
self._apply_bundle_to_disassembly_row(row, source_row, bundle_data, scale_factor)
def _apply_bundle_to_disassembly_row(self, row, source_row, bundle_data, scale_factor):
source_warehouse = source_row.s_warehouse or source_row.t_warehouse
key = (source_row.item_code, source_warehouse, self.doc.source_stock_entry)
source_bundle = bundle_data.get(key, {})
batches = self._extract_batches(source_row, source_bundle, row, scale_factor)
serial_nos = self._extract_serial_nos(source_row, source_bundle, row)
self._set_serial_batch_bundle_for_disassembly_row(row, serial_nos, batches)
def _extract_batches(self, source_row, source_bundle, row, scale_factor):
batches = defaultdict(float)
if source_bundle.get("batch_nos"):
self._allocate_batches(batches, source_bundle["batch_nos"], row.transfer_qty, scale_factor)
elif source_row.batch_no:
batches[source_row.batch_no] = row.transfer_qty
return batches
def _allocate_batches(self, batches, batch_nos, transfer_qty, scale_factor):
qty_remaining = transfer_qty
for batch_no, batch_qty in batch_nos.items():
if qty_remaining <= 0:
break
alloc = min(abs(flt(batch_qty)) * scale_factor, qty_remaining)
batches[batch_no] = alloc
qty_remaining -= alloc
def _extract_serial_nos(self, source_row, source_bundle, row):
if source_bundle.get("serial_nos"):
return get_serial_nos(source_bundle["serial_nos"])[: int(row.transfer_qty)]
elif source_row.serial_no:
return get_serial_nos(source_row.serial_no)[: int(row.transfer_qty)]
return []
def _set_serial_batch_for_disassembly_from_available_materials(self):
available_materials = get_available_materials(self.doc.work_order, self.doc)
for row in self.doc.items:
warehouse = row.s_warehouse or row.t_warehouse
materials = available_materials.get((row.item_code, warehouse))
if materials:
self._apply_available_material_bundle(row, materials)
def _apply_available_material_bundle(self, row, materials):
batches = self._collect_available_batches(materials.batch_details, row.transfer_qty)
serial_nos = materials.serial_nos[: int(row.transfer_qty)] if materials.serial_nos else []
self._set_serial_batch_bundle_for_disassembly_row(row, serial_nos, batches)
def _collect_available_batches(self, batch_details, transfer_qty):
batches, qty = defaultdict(float), transfer_qty
for batch_no, batch_qty in batch_details.items():
if qty <= 0:
break
batch_qty = abs(batch_qty)
if batch_qty <= qty:
batches[batch_no], qty = batch_qty, qty - batch_qty
else:
batches[batch_no], qty = qty, 0
return batches
def _set_serial_batch_bundle_for_disassembly_row(self, row, serial_nos, batches):
if not serial_nos and not batches:
return
warehouse = row.s_warehouse or row.t_warehouse
bundle_doc = SerialBatchCreation(
{
"item_code": row.item_code,
"warehouse": warehouse,
"posting_datetime": get_combine_datetime(self.doc.posting_date, self.doc.posting_time),
"voucher_type": self.doc.doctype,
"voucher_no": self.doc.name,
"voucher_detail_no": row.name,
"qty": row.transfer_qty,
"type_of_transaction": "Inward" if row.t_warehouse else "Outward",
"company": self.doc.company,
"do_not_submit": True,
}
).make_serial_and_batch_bundle(serial_nos=serial_nos, batch_nos=batches)
row.serial_and_batch_bundle = bundle_doc.name
row.use_serial_batch_fields = 0
def update_disassembled_order(self):
if not self.doc.work_order:
return
if self.doc.fg_completed_qty:
pro_doc = frappe.get_doc("Work Order", self.doc.work_order)
pro_doc.run_method(
"update_disassembled_qty", self.doc.fg_completed_qty, self.doc._action == "cancel"
)
def get_available_materials(work_order, stock_entry_doc=None) -> dict:
data = get_stock_entry_data(work_order, stock_entry_doc=stock_entry_doc)
available_materials = {}
for row in data:
key = _get_material_key(row, stock_entry_doc)
if key not in available_materials:
available_materials[key] = frappe._dict(
{"item_details": row, "batch_details": defaultdict(float), "qty": 0, "serial_nos": []}
)
_update_material_qty(available_materials[key], row, stock_entry_doc)
return available_materials
def _get_material_key(row, stock_entry_doc):
if stock_entry_doc and stock_entry_doc.purpose == "Disassemble":
return (row.item_code, row.s_warehouse or row.warehouse)
if row.purpose != "Material Transfer for Manufacture":
return (row.item_code, row.s_warehouse)
return (row.item_code, row.warehouse)
def _update_material_qty(item_data, row, stock_entry_doc):
is_inward = row.purpose == "Material Transfer for Manufacture" or (
stock_entry_doc and stock_entry_doc.purpose == "Disassemble" and row.purpose == "Manufacture"
)
if is_inward:
_add_inward_material_qty(item_data, row)
else:
_deduct_consumed_material_qty(item_data, row)
def _add_inward_material_qty(item_data, row):
item_data.qty += row.qty
if row.batch_no:
item_data.batch_details[row.batch_no] += row.qty
elif row.batch_nos:
for batch_no, qty in row.batch_nos.items():
item_data.batch_details[batch_no] += qty
_extend_serial_nos_from_row(item_data, row)
def _extend_serial_nos_from_row(item_data, row):
sn = row.serial_no or row.serial_nos
if sn:
item_data.serial_nos.extend(get_serial_nos(sn))
item_data.serial_nos.sort()
def _deduct_consumed_material_qty(item_data, row):
item_data.qty -= row.qty
if row.batch_no:
item_data.batch_details[row.batch_no] -= row.qty
elif row.batch_nos:
for batch_no, qty in row.batch_nos.items():
item_data.batch_details[batch_no] += qty
_remove_serial_nos_from_available(item_data, row)
def _remove_serial_nos_from_available(item_data, row):
sn = row.serial_no or row.serial_nos
if not sn:
return
for serial_no in get_serial_nos(sn):
if serial_no in item_data.serial_nos:
item_data.serial_nos.remove(serial_no)
def get_stock_entry_data(work_order, stock_entry_doc=None):
data = _run_stock_entry_query(work_order, stock_entry_doc)
if not data:
return []
_enrich_with_bundle_data(data, stock_entry_doc)
return data
def _run_stock_entry_query(work_order, stock_entry_doc):
se = frappe.qb.DocType("Stock Entry")
sed = frappe.qb.DocType("Stock Entry Detail")
query = _build_stock_entry_base_query(se, sed, work_order)
query = _apply_stock_entry_purpose_filter(query, se, sed, stock_entry_doc)
return query.run(as_dict=1)
def _build_stock_entry_base_query(se, sed, work_order):
return (
frappe.qb.from_(se)
.from_(sed)
.select(
sed.item_name,
sed.original_item,
sed.item_code,
sed.qty,
sed.t_warehouse.as_("warehouse"),
sed.s_warehouse.as_("s_warehouse"),
sed.description,
sed.stock_uom,
sed.expense_account,
sed.cost_center,
sed.serial_and_batch_bundle,
sed.batch_no,
sed.serial_no,
se.purpose,
se.name,
)
.where((se.name == sed.parent) & (se.work_order == work_order) & (se.docstatus == 1))
.orderby(se.creation, sed.item_code, sed.idx)
)
def _apply_stock_entry_purpose_filter(query, se, sed, stock_entry_doc):
if stock_entry_doc and stock_entry_doc.purpose == "Disassemble":
query = query.where(se.purpose.isin(["Disassemble", "Manufacture"]))
return query.where(se.name != stock_entry_doc.name)
query = query.where(
se.purpose.isin(
["Manufacture", "Material Consumption for Manufacture", "Material Transfer for Manufacture"]
)
)
return query.where(sed.s_warehouse.isnotnull())
def _enrich_with_bundle_data(data, stock_entry_doc):
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
get_voucher_wise_serial_batch_from_bundle,
)
voucher_nos = [row.get("name") for row in data if row.get("name")]
if not voucher_nos:
return
bundle_data = get_voucher_wise_serial_batch_from_bundle(voucher_no=voucher_nos)
for row in data:
key = _get_bundle_key(row, stock_entry_doc)
if bundle_data.get(key):
row.update(bundle_data.get(key))
def _get_bundle_key(row, stock_entry_doc):
if stock_entry_doc and stock_entry_doc.purpose == "Disassemble":
return (row.item_code, row.s_warehouse or row.warehouse, row.name)
if row.purpose != "Material Transfer for Manufacture":
return (row.item_code, row.s_warehouse, row.name)
return (row.item_code, row.warehouse, row.name)

File diff suppressed because it is too large Load Diff

View File

@@ -909,7 +909,14 @@ class TestStockEntry(ERPNextTestSuite):
if d.s_warehouse:
rm_cost += d.amount
fg_cost = next(filter(lambda x: x.item_code == "_Test FG Item", s.get("items"))).amount
<<<<<<< HEAD
secondary_item_cost = next(filter(lambda x: x.type or x.is_legacy_scrap_item, s.get("items"))).amount
=======
secondary_item_cost = next(
filter(lambda x: x.secondary_item_type or x.is_legacy_scrap_item, s.get("items"))
).amount
>>>>>>> dbcfac839c (chore: rename type field to secondary_item_type (#55469))
self.assertEqual(fg_cost, flt(rm_cost - secondary_item_cost, 2))
# When Stock Entry has only FG + Scrap
@@ -1027,7 +1034,7 @@ class TestStockEntry(ERPNextTestSuite):
basic_rate=row.basic_rate or 100,
)
if row.type or row.is_legacy_scrap_item:
if row.secondary_item_type or row.is_legacy_scrap_item:
row.item_code = secondary_item
row.uom = frappe.db.get_value("Item", secondary_item, "stock_uom")
row.stock_uom = frappe.db.get_value("Item", secondary_item, "stock_uom")
@@ -1035,10 +1042,16 @@ class TestStockEntry(ERPNextTestSuite):
stock_entry.inspection_required = 1
stock_entry.save()
self.assertTrue([row.item_code for row in stock_entry.items if row.type or row.is_legacy_scrap_item])
self.assertTrue(
[
row.item_code
for row in stock_entry.items
if row.secondary_item_type or row.is_legacy_scrap_item
]
)
for row in stock_entry.items:
if not row.type and not row.is_legacy_scrap_item:
if not row.secondary_item_type and not row.is_legacy_scrap_item:
qc = frappe.get_doc(
{
"doctype": "Quality Inspection",
@@ -1058,7 +1071,7 @@ class TestStockEntry(ERPNextTestSuite):
stock_entry.reload()
stock_entry.submit()
for row in stock_entry.items:
if row.type or row.is_legacy_scrap_item:
if row.secondary_item_type or row.is_legacy_scrap_item:
self.assertFalse(row.quality_inspection)
else:
self.assertTrue(row.quality_inspection)

View File

@@ -19,7 +19,7 @@
"col_break2",
"is_finished_item",
"is_legacy_scrap_item",
"type",
"secondary_item_type",
"quality_inspection",
"subcontracted_item",
"against_fg",
@@ -559,7 +559,7 @@
},
{
"default": "0",
"depends_on": "eval:!doc.is_legacy_scrap_item && !doc.type",
"depends_on": "eval:!doc.is_legacy_scrap_item && !doc.secondary_item_type",
"fieldname": "is_finished_item",
"fieldtype": "Check",
"label": "Is Finished Item",
@@ -653,7 +653,7 @@
},
{
"depends_on": "eval:parent.purpose == \"Manufacture\" && doc.t_warehouse && !doc.is_finished_item && !doc.is_legacy_scrap_item",
"fieldname": "type",
"fieldname": "secondary_item_type",
"fieldtype": "Select",
"label": "Type",
"options": "\nCo-Product\nBy-Product\nScrap\nAdditional Finished Good"
@@ -679,7 +679,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-04-27 11:40:38.294196",
"modified": "2026-06-01 10:00:00.000000",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Detail",

View File

@@ -67,7 +67,7 @@ class StockEntryDetail(Document):
t_warehouse: DF.Link | None
transfer_qty: DF.Float
transferred_qty: DF.Float
type: DF.Literal["", "Co-Product", "By-Product", "Scrap", "Additional Finished Good"]
secondary_item_type: DF.Literal["", "Co-Product", "By-Product", "Scrap", "Additional Finished Good"]
uom: DF.Link
use_serial_batch_fields: DF.Check
valuation_rate: DF.Currency

View File

@@ -492,7 +492,7 @@ class SubcontractingInwardOrder(SubcontractingController):
"from_warehouse": secondary_item.warehouse,
"stock_uom": secondary_item.stock_uom,
"scio_detail": secondary_item.name,
"type": secondary_item.type,
"secondary_item_type": secondary_item.secondary_item_type,
}
}

View File

@@ -327,7 +327,7 @@ class IntegrationTestSubcontractingInwardOrder(ERPNextTestSuite):
def test_secondary_items_delivery(self):
new_bom = frappe.copy_doc(frappe.get_doc("BOM", "BOM-Basic FG Item-001"))
new_bom.secondary_items.append(
frappe.new_doc("BOM Secondary Item", item_code="Basic RM 2", qty=1, type="Scrap")
frappe.new_doc("BOM Secondary Item", item_code="Basic RM 2", qty=1, secondary_item_type="Scrap")
)
new_bom.submit()
sc_bom = frappe.get_doc("Subcontracting BOM", "SB-0001")

View File

@@ -7,7 +7,7 @@
"engine": "InnoDB",
"field_order": [
"column_break_rptg",
"type",
"secondary_item_type",
"reference_name",
"column_break_jkzt",
"item_code",
@@ -97,7 +97,7 @@
"fieldtype": "Column Break"
},
{
"fieldname": "type",
"fieldname": "secondary_item_type",
"fieldtype": "Select",
"label": "Type",
"no_copy": 1,
@@ -114,7 +114,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-02-27 15:15:40.009957",
"modified": "2026-06-01 10:00:00.000000",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Inward Order Secondary Item",

View File

@@ -23,7 +23,7 @@ class SubcontractingInwardOrderSecondaryItem(Document):
produced_qty: DF.Float
reference_name: DF.Data
stock_uom: DF.Link
type: DF.Literal["Co-Product", "By-Product", "Scrap", "Additional Finished Good"]
secondary_item_type: DF.Literal["Co-Product", "By-Product", "Scrap", "Additional Finished Good"]
warehouse: DF.Link
# end: auto-generated types

View File

@@ -419,7 +419,7 @@ class SubcontractingReceipt(SubcontractingController):
self.append(
"items",
{
"type": secondary_item.type,
"secondary_item_type": secondary_item.secondary_item_type,
"is_legacy_scrap_item": secondary_item.is_legacy,
"reference_name": item.name,
"item_code": secondary_item.item_code,
@@ -447,7 +447,7 @@ class SubcontractingReceipt(SubcontractingController):
def remove_secondary_items(self):
for item in list(self.items):
if item.type or item.is_legacy_scrap_item:
if item.secondary_item_type or item.is_legacy_scrap_item:
self.remove(item)
else:
item.secondary_items_cost_per_qty = 0
@@ -507,7 +507,7 @@ class SubcontractingReceipt(SubcontractingController):
secondary_items_cost_map = {}
for item in self.get("items") or []:
if item.type or item.is_legacy_scrap_item:
if item.secondary_item_type or item.is_legacy_scrap_item:
qty = (
flt(item.qty)
if item.is_legacy_scrap_item
@@ -522,7 +522,7 @@ class SubcontractingReceipt(SubcontractingController):
total_qty = total_amount = 0
for item in self.get("items") or []:
if not item.type and not item.is_legacy_scrap_item:
if not item.secondary_item_type and not item.is_legacy_scrap_item:
if item.qty:
if item.name in rm_cost_map:
item.rm_supp_cost = rm_cost_map[item.name]
@@ -566,7 +566,7 @@ class SubcontractingReceipt(SubcontractingController):
def validate_secondary_items(self):
for item in self.items:
if item.type or item.is_legacy_scrap_item:
if item.secondary_item_type or item.is_legacy_scrap_item:
if not item.qty:
frappe.throw(
_("Row #{0}: Secondary Item Qty cannot be zero").format(item.idx),

View File

@@ -1221,7 +1221,7 @@ class TestSubcontractingReceipt(ERPNextTestSuite):
scr.get_secondary_items()
scr_secondary_items = set(
[item.item_code for item in scr.items if item.type or item.is_legacy_scrap_item]
[item.item_code for item in scr.items if item.secondary_item_type or item.is_legacy_scrap_item]
)
self.assertEqual(len(scr.items), 3) # 1 FG Item + 2 Scrap Items
self.assertEqual(scr_secondary_items, set(secondary_items))

View File

@@ -9,7 +9,7 @@
"field_order": [
"item_code",
"is_legacy_scrap_item",
"type",
"secondary_item_type",
"column_break_2",
"item_name",
"section_break_4",
@@ -162,12 +162,12 @@
"label": "Accepted Qty",
"no_copy": 1,
"print_width": "100px",
"read_only_depends_on": "eval:doc.type || doc.is_legacy_scrap_item",
"read_only_depends_on": "eval:doc.secondary_item_type || doc.is_legacy_scrap_item",
"width": "100px"
},
{
"columns": 1,
"depends_on": "eval:!parent.is_return && !doc.type && !doc.is_legacy_scrap_item",
"depends_on": "eval:!parent.is_return && !doc.secondary_item_type && !doc.is_legacy_scrap_item",
"fieldname": "rejected_qty",
"fieldtype": "Float",
"in_list_view": 1,
@@ -175,7 +175,7 @@
"no_copy": 1,
"print_hide": 1,
"print_width": "100px",
"read_only_depends_on": "eval:doc.type || doc.is_legacy_scrap_item",
"read_only_depends_on": "eval:doc.secondary_item_type || doc.is_legacy_scrap_item",
"width": "100px"
},
{
@@ -234,7 +234,7 @@
},
{
"default": "0",
"depends_on": "eval:!doc.type && !doc.is_legacy_scrap_item",
"depends_on": "eval:!doc.secondary_item_type && !doc.is_legacy_scrap_item",
"fieldname": "rm_cost_per_qty",
"fieldtype": "Currency",
"label": "Raw Material Cost Per Qty",
@@ -244,7 +244,7 @@
},
{
"default": "0",
"depends_on": "eval:!doc.type && !doc.is_legacy_scrap_item",
"depends_on": "eval:!doc.secondary_item_type && !doc.is_legacy_scrap_item",
"fieldname": "service_cost_per_qty",
"fieldtype": "Currency",
"label": "Service Cost Per Qty",
@@ -254,7 +254,7 @@
},
{
"default": "0",
"depends_on": "eval:!doc.type && !doc.is_legacy_scrap_item",
"depends_on": "eval:!doc.secondary_item_type && !doc.is_legacy_scrap_item",
"fieldname": "additional_cost_per_qty",
"fieldtype": "Currency",
"label": "Additional Cost Per Qty",
@@ -278,7 +278,7 @@
"width": "100px"
},
{
"depends_on": "eval: !parent.is_return && !doc.type && !doc.is_legacy_scrap_item",
"depends_on": "eval: !parent.is_return && !doc.secondary_item_type && !doc.is_legacy_scrap_item",
"fieldname": "rejected_warehouse",
"fieldtype": "Link",
"ignore_user_permissions": 1,
@@ -290,7 +290,7 @@
"width": "100px"
},
{
"depends_on": "eval:!doc.__islocal && !doc.type && !doc.is_legacy_scrap_item",
"depends_on": "eval:!doc.__islocal && !doc.secondary_item_type && !doc.is_legacy_scrap_item",
"fieldname": "quality_inspection",
"fieldtype": "Link",
"label": "Quality Inspection",
@@ -372,7 +372,7 @@
"no_copy": 1,
"options": "BOM",
"print_hide": 1,
"read_only_depends_on": "eval:doc.type || doc.is_legacy_scrap_item"
"read_only_depends_on": "eval:doc.secondary_item_type || doc.is_legacy_scrap_item"
},
{
"fetch_from": "item_code.brand",
@@ -499,7 +499,7 @@
"print_hide": 1
},
{
"depends_on": "eval:(doc.use_serial_batch_fields === 0 || doc.docstatus === 1) && !doc.type && !doc.is_legacy_scrap_item",
"depends_on": "eval:(doc.use_serial_batch_fields === 0 || doc.docstatus === 1) && !doc.secondary_item_type && !doc.is_legacy_scrap_item",
"fieldname": "rejected_serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Rejected Serial and Batch Bundle",
@@ -564,7 +564,7 @@
"label": "Add Serial / Batch Bundle"
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 0 && !doc.type && !doc.is_legacy_scrap_item",
"depends_on": "eval:doc.use_serial_batch_fields === 0 && !doc.secondary_item_type && !doc.is_legacy_scrap_item",
"fieldname": "add_serial_batch_for_rejected_qty",
"fieldtype": "Button",
"label": "Add Serial / Batch No (Rejected Qty)"
@@ -578,7 +578,7 @@
"search_index": 1
},
{
"depends_on": "eval:!doc.type && !doc.is_legacy_scrap_item",
"depends_on": "eval:!doc.secondary_item_type && !doc.is_legacy_scrap_item",
"fieldname": "landed_cost_voucher_amount",
"fieldtype": "Currency",
"label": "Landed Cost Voucher Amount",
@@ -596,7 +596,7 @@
"options": "Account"
},
{
"fieldname": "type",
"fieldname": "secondary_item_type",
"fieldtype": "Select",
"label": "Type",
"no_copy": 1,
@@ -606,7 +606,7 @@
},
{
"default": "0",
"depends_on": "eval:!doc.type && !doc.is_legacy_scrap_item",
"depends_on": "eval:!doc.secondary_item_type && !doc.is_legacy_scrap_item",
"fieldname": "secondary_items_cost_per_qty",
"fieldtype": "Currency",
"label": "Secondary Items Cost Per Qty",
@@ -635,7 +635,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2026-03-09 15:11:16.977539",
"modified": "2026-06-01 10:00:00.000000",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Receipt Item",

View File

@@ -62,7 +62,7 @@ class SubcontractingReceiptItem(Document):
subcontracting_order: DF.Link | None
subcontracting_order_item: DF.Data | None
subcontracting_receipt_item: DF.Data | None
type: DF.Literal["", "Co-Product", "By-Product", "Scrap", "Additional Finished Good"]
secondary_item_type: DF.Literal["", "Co-Product", "By-Product", "Scrap", "Additional Finished Good"]
use_serial_batch_fields: DF.Check
warehouse: DF.Link | None
# end: auto-generated types