mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-09 09:55:08 +00:00
@@ -1435,7 +1435,7 @@ class StockController(AccountsController):
|
||||
elif self.doctype == "Stock Entry" and row.t_warehouse:
|
||||
qi_required = True # inward stock needs inspection
|
||||
|
||||
if row.get("is_scrap_item"):
|
||||
if row.get("type") or row.get("is_legacy_scrap_item"):
|
||||
continue
|
||||
|
||||
if qi_required: # validate row only if inspection is required on item level
|
||||
|
||||
@@ -160,7 +160,7 @@ class SubcontractingController(StockController):
|
||||
).format(item.idx, get_link_to_form("Item", item.item_code))
|
||||
)
|
||||
|
||||
if not item.get("is_scrap_item"):
|
||||
if not item.get("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)
|
||||
@@ -206,7 +206,7 @@ class SubcontractingController(StockController):
|
||||
).format(item.idx, item.item_name)
|
||||
)
|
||||
|
||||
if self.doctype != "Subcontracting Inward Order":
|
||||
if self.doctype not in ["Subcontracting Inward Order", "Subcontracting Receipt"]:
|
||||
item.amount = item.qty * item.rate
|
||||
|
||||
if item.bom:
|
||||
@@ -238,7 +238,7 @@ class SubcontractingController(StockController):
|
||||
and self._doc_before_save
|
||||
):
|
||||
for row in self._doc_before_save.get("items"):
|
||||
item_dict[row.name] = (row.item_code, row.qty + (row.get("rejected_qty") or 0))
|
||||
item_dict[row.name] = (row.item_code, row.received_qty)
|
||||
|
||||
return item_dict
|
||||
|
||||
@@ -264,7 +264,7 @@ class SubcontractingController(StockController):
|
||||
self.__reference_name.append(row.name)
|
||||
if (row.name not in item_dict) or (
|
||||
row.item_code,
|
||||
row.qty + (row.get("rejected_qty") or 0),
|
||||
row.received_qty,
|
||||
) != item_dict[row.name]:
|
||||
self.__changed_name.append(row.name)
|
||||
|
||||
@@ -962,7 +962,7 @@ class SubcontractingController(StockController):
|
||||
):
|
||||
qty = (
|
||||
flt(bom_item.qty_consumed_per_unit)
|
||||
* flt(row.qty + (row.get("rejected_qty") or 0))
|
||||
* flt(row.get("received_qty") or (row.qty + (row.get("rejected_qty") or 0)))
|
||||
* row.conversion_factor
|
||||
)
|
||||
bom_item.main_item_code = row.item_code
|
||||
@@ -1285,22 +1285,28 @@ class SubcontractingController(StockController):
|
||||
if self.total_additional_costs:
|
||||
if self.distribute_additional_costs_based_on == "Amount":
|
||||
total_amt = sum(
|
||||
flt(item.amount) for item in self.get("items") if not item.get("is_scrap_item")
|
||||
flt(item.amount)
|
||||
for item in self.get("items")
|
||||
if not item.get("type") and not item.get("is_legacy_scrap_item")
|
||||
)
|
||||
for item in self.items:
|
||||
if not item.get("is_scrap_item"):
|
||||
if not item.get("type") and not item.get("is_legacy_scrap_item"):
|
||||
item.additional_cost_per_qty = (
|
||||
(item.amount * self.total_additional_costs) / total_amt
|
||||
) / item.qty
|
||||
else:
|
||||
total_qty = sum(flt(item.qty) for item in self.get("items") if not item.get("is_scrap_item"))
|
||||
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")
|
||||
)
|
||||
additional_cost_per_qty = self.total_additional_costs / total_qty
|
||||
for item in self.items:
|
||||
if not item.get("is_scrap_item"):
|
||||
if not item.get("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("is_scrap_item"):
|
||||
if not item.get("type") and not item.get("is_legacy_scrap_item"):
|
||||
item.additional_cost_per_qty = 0
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe import _, bold
|
||||
from frappe.query_builder import Case
|
||||
@@ -18,7 +20,7 @@ class SubcontractingInwardController:
|
||||
def on_submit_subcontracting_inward(self):
|
||||
self.update_inward_order_item()
|
||||
self.update_inward_order_received_items()
|
||||
self.update_inward_order_scrap_items()
|
||||
self.update_inward_order_secondary_items()
|
||||
self.create_stock_reservation_entries_for_inward()
|
||||
self.update_inward_order_status()
|
||||
|
||||
@@ -28,7 +30,7 @@ class SubcontractingInwardController:
|
||||
self.validate_delivery()
|
||||
self.validate_receive_from_customer_cancel()
|
||||
self.update_inward_order_received_items()
|
||||
self.update_inward_order_scrap_items()
|
||||
self.update_inward_order_secondary_items()
|
||||
self.remove_reference_for_additional_items()
|
||||
self.update_inward_order_status()
|
||||
|
||||
@@ -239,7 +241,8 @@ class SubcontractingInwardController:
|
||||
item
|
||||
for item in self.get("items")
|
||||
if not item.is_finished_item
|
||||
and not item.is_scrap_item
|
||||
and not item.type
|
||||
and not item.is_legacy_scrap_item
|
||||
and frappe.get_cached_value("Item", item.item_code, "is_customer_provided_item")
|
||||
]
|
||||
|
||||
@@ -368,7 +371,9 @@ class SubcontractingInwardController:
|
||||
if self.subcontracting_inward_order:
|
||||
if self.purpose in ["Subcontracting Delivery", "Subcontracting Return", "Manufacture"]:
|
||||
for item in self.items:
|
||||
if (item.is_finished_item or item.is_scrap_item) and item.valuation_rate == 0:
|
||||
if (
|
||||
item.is_finished_item or item.type or item.is_legacy_scrap_item
|
||||
) and item.valuation_rate == 0:
|
||||
item.allow_zero_valuation_rate = 1
|
||||
|
||||
def validate_warehouse_(self):
|
||||
@@ -467,7 +472,7 @@ class SubcontractingInwardController:
|
||||
self.validate_delivery_on_save()
|
||||
else:
|
||||
for item in self.items:
|
||||
if not item.is_scrap_item:
|
||||
if not item.type and not item.is_legacy_scrap_item:
|
||||
delivered_qty, returned_qty = frappe.get_value(
|
||||
"Subcontracting Inward Order Item",
|
||||
item.scio_detail,
|
||||
@@ -519,7 +524,7 @@ class SubcontractingInwardController:
|
||||
if max_allowed_qty:
|
||||
max_allowed_qty = max_allowed_qty[0]
|
||||
else:
|
||||
table = frappe.qb.DocType("Subcontracting Inward Order Scrap Item")
|
||||
table = frappe.qb.DocType("Subcontracting Inward Order Secondary Item")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select((table.produced_qty - table.delivered_qty).as_("max_allowed_qty"))
|
||||
@@ -538,8 +543,8 @@ class SubcontractingInwardController:
|
||||
bold(
|
||||
frappe.get_cached_value(
|
||||
"Subcontracting Inward Order Item"
|
||||
if not item.is_scrap_item
|
||||
else "Subcontracting Inward Order Scrap Item",
|
||||
if not item.type and not item.is_legacy_scrap_item
|
||||
else "Subcontracting Inward Order Secondary Item",
|
||||
item.scio_detail,
|
||||
"stock_uom",
|
||||
)
|
||||
@@ -590,9 +595,9 @@ class SubcontractingInwardController:
|
||||
)
|
||||
|
||||
for item in [item for item in self.items if not item.is_finished_item]:
|
||||
if item.is_scrap_item:
|
||||
scio_scrap_item = frappe.get_value(
|
||||
"Subcontracting Inward Order Scrap Item",
|
||||
if item.type or item.is_legacy_scrap_item:
|
||||
scio_secondary_item = frappe.get_value(
|
||||
"Subcontracting Inward Order Secondary Item",
|
||||
{
|
||||
"docstatus": 1,
|
||||
"item_code": item.item_code,
|
||||
@@ -603,12 +608,13 @@ class SubcontractingInwardController:
|
||||
as_dict=True,
|
||||
)
|
||||
if (
|
||||
scio_scrap_item
|
||||
and scio_scrap_item.delivered_qty > scio_scrap_item.produced_qty - item.transfer_qty
|
||||
scio_secondary_item
|
||||
and scio_secondary_item.delivered_qty
|
||||
> scio_secondary_item.produced_qty - item.transfer_qty
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Cannot cancel this Manufacturing Stock Entry as quantity of Scrap Item {1} produced cannot be less than quantity delivered."
|
||||
"Row #{0}: Cannot cancel this Manufacturing Stock Entry as quantity of Secondary Item {1} produced cannot be less than quantity delivered."
|
||||
).format(item.idx, get_link_to_form("Item", item.item_code))
|
||||
)
|
||||
else:
|
||||
@@ -648,8 +654,8 @@ class SubcontractingInwardController:
|
||||
for item in self.items:
|
||||
doctype = (
|
||||
"Subcontracting Inward Order Item"
|
||||
if not item.is_scrap_item
|
||||
else "Subcontracting Inward Order Scrap Item"
|
||||
if not item.type and not item.is_legacy_scrap_item
|
||||
else "Subcontracting Inward Order Secondary Item"
|
||||
)
|
||||
frappe.db.set_value(
|
||||
doctype,
|
||||
@@ -763,7 +769,11 @@ class SubcontractingInwardController:
|
||||
customer_warehouse = frappe.get_cached_value(
|
||||
"Subcontracting Inward Order", self.subcontracting_inward_order, "customer_warehouse"
|
||||
)
|
||||
items = [item for item in self.items if not item.is_finished_item and not item.is_scrap_item]
|
||||
items = [
|
||||
item
|
||||
for item in self.items
|
||||
if not item.is_finished_item and not item.type and not item.is_legacy_scrap_item
|
||||
]
|
||||
item_code_wh = frappe._dict(
|
||||
{
|
||||
(
|
||||
@@ -860,24 +870,24 @@ class SubcontractingInwardController:
|
||||
doc.insert()
|
||||
doc.submit()
|
||||
|
||||
def update_inward_order_scrap_items(self):
|
||||
def update_inward_order_secondary_items(self):
|
||||
if (scio := self.subcontracting_inward_order) and self.purpose == "Manufacture":
|
||||
scrap_items_list = [item for item in self.items if item.is_scrap_item]
|
||||
scrap_items = frappe._dict(
|
||||
{
|
||||
(item.item_code, item.t_warehouse): item.transfer_qty
|
||||
if self._action == "submit"
|
||||
else -item.transfer_qty
|
||||
for item in scrap_items_list
|
||||
}
|
||||
)
|
||||
if scrap_items:
|
||||
item_codes, warehouses = zip(*list(scrap_items.keys()), strict=True)
|
||||
secondary_items_list = [item for item in self.items if item.type or item.is_legacy_scrap_item]
|
||||
|
||||
secondary_items = defaultdict(float)
|
||||
for item in secondary_items_list:
|
||||
secondary_items[(item.item_code, item.t_warehouse)] += (
|
||||
item.transfer_qty if self._action == "submit" else -item.transfer_qty
|
||||
)
|
||||
secondary_items = frappe._dict(secondary_items)
|
||||
|
||||
if secondary_items:
|
||||
item_codes, warehouses = zip(*list(secondary_items.keys()), strict=True)
|
||||
item_codes = list(item_codes)
|
||||
warehouses = list(warehouses)
|
||||
|
||||
result = frappe.get_all(
|
||||
"Subcontracting Inward Order Scrap Item",
|
||||
"Subcontracting Inward Order Secondary Item",
|
||||
filters={
|
||||
"item_code": ["in", item_codes],
|
||||
"warehouse": ["in", warehouses],
|
||||
@@ -890,7 +900,7 @@ class SubcontractingInwardController:
|
||||
)
|
||||
|
||||
if result:
|
||||
scrap_item_dict = frappe._dict(
|
||||
secondary_items_dict = frappe._dict(
|
||||
{
|
||||
(d.item_code, d.warehouse): frappe._dict(
|
||||
{"name": d.name, "produced_qty": d.produced_qty}
|
||||
@@ -900,40 +910,45 @@ class SubcontractingInwardController:
|
||||
)
|
||||
deleted_docs = []
|
||||
case_expr = Case()
|
||||
table = frappe.qb.DocType("Subcontracting Inward Order Scrap Item")
|
||||
for key, value in scrap_item_dict.items():
|
||||
if self._action == "cancel" and value.produced_qty - abs(scrap_items.get(key)) == 0:
|
||||
table = frappe.qb.DocType("Subcontracting Inward Order Secondary Item")
|
||||
for key, value in secondary_items_dict.items():
|
||||
if (
|
||||
self._action == "cancel"
|
||||
and value.produced_qty - abs(secondary_items.get(key)) == 0
|
||||
):
|
||||
deleted_docs.append(value.name)
|
||||
frappe.delete_doc("Subcontracting Inward Order Scrap Item", value.name)
|
||||
frappe.delete_doc("Subcontracting Inward Order Secondary Item", value.name)
|
||||
else:
|
||||
case_expr = case_expr.when(
|
||||
table.name == value.name, value.produced_qty + scrap_items.get(key)
|
||||
table.name == value.name, value.produced_qty + secondary_items.get(key)
|
||||
)
|
||||
|
||||
if final_list := list(
|
||||
set([v.name for v in scrap_item_dict.values()]) - set(deleted_docs)
|
||||
set([v.name for v in secondary_items_dict.values()]) - set(deleted_docs)
|
||||
):
|
||||
frappe.qb.update(table).set(table.produced_qty, case_expr).where(
|
||||
(table.name.isin(final_list)) & (table.docstatus == 1)
|
||||
).run()
|
||||
|
||||
fg_item_code = next(fg for fg in self.items if fg.is_finished_item).item_code
|
||||
for scrap_item in [
|
||||
for secondary_item in [
|
||||
item
|
||||
for item in scrap_items_list
|
||||
for item in secondary_items_list
|
||||
if (item.item_code, item.t_warehouse) not in [(d.item_code, d.warehouse) for d in result]
|
||||
]:
|
||||
doc = frappe.new_doc(
|
||||
"Subcontracting Inward Order Scrap Item",
|
||||
"Subcontracting Inward Order Secondary Item",
|
||||
parent=scio,
|
||||
parenttype="Subcontracting Inward Order",
|
||||
parentfield="scrap_items",
|
||||
idx=frappe.db.count("Subcontracting Inward Order Scrap Item", {"parent": scio}) + 1,
|
||||
item_code=scrap_item.item_code,
|
||||
parentfield="secondary_items",
|
||||
idx=frappe.db.count("Subcontracting Inward Order Secondary Item", {"parent": scio})
|
||||
+ 1,
|
||||
item_code=secondary_item.item_code,
|
||||
fg_item_code=fg_item_code,
|
||||
stock_uom=scrap_item.stock_uom,
|
||||
warehouse=scrap_item.t_warehouse,
|
||||
produced_qty=scrap_item.transfer_qty,
|
||||
stock_uom=secondary_item.stock_uom,
|
||||
warehouse=secondary_item.t_warehouse,
|
||||
produced_qty=secondary_item.transfer_qty,
|
||||
type=secondary_item.type,
|
||||
delivered_qty=0,
|
||||
reference_name=frappe.get_value(
|
||||
"Work Order", self.work_order, "subcontracting_inward_order_item"
|
||||
@@ -965,7 +980,7 @@ class SubcontractingInwardController:
|
||||
and (
|
||||
not frappe.db.exists("Subcontracting Inward Order Received Item", item.scio_detail)
|
||||
and not frappe.db.exists("Subcontracting Inward Order Item", item.scio_detail)
|
||||
and not frappe.db.exists("Subcontracting Inward Order Scrap Item", item.scio_detail)
|
||||
and not frappe.db.exists("Subcontracting Inward Order Secondary Item", item.scio_detail)
|
||||
)
|
||||
]
|
||||
for item in items:
|
||||
|
||||
@@ -501,8 +501,8 @@ class TestSubcontractingController(ERPNextTestSuite):
|
||||
scr1.items[0].qty = 2
|
||||
add_second_row_in_scr(scr1)
|
||||
scr1.flags.ignore_mandatory = True
|
||||
scr1.save()
|
||||
scr1.set_missing_values()
|
||||
scr1.save()
|
||||
scr1.submit()
|
||||
|
||||
for _key, value in get_supplied_items(scr1).items():
|
||||
@@ -513,8 +513,8 @@ class TestSubcontractingController(ERPNextTestSuite):
|
||||
scr2.items[0].qty = 2
|
||||
add_second_row_in_scr(scr2)
|
||||
scr2.flags.ignore_mandatory = True
|
||||
scr2.save()
|
||||
scr2.set_missing_values()
|
||||
scr2.save()
|
||||
scr2.submit()
|
||||
|
||||
for _key, value in get_supplied_items(scr2).items():
|
||||
@@ -523,8 +523,8 @@ class TestSubcontractingController(ERPNextTestSuite):
|
||||
scr3 = make_subcontracting_receipt(sco.name)
|
||||
scr3.items[0].qty = 2
|
||||
scr3.flags.ignore_mandatory = True
|
||||
scr3.save()
|
||||
scr3.set_missing_values()
|
||||
scr3.save()
|
||||
scr3.submit()
|
||||
|
||||
for _key, value in get_supplied_items(scr3).items():
|
||||
@@ -1164,6 +1164,54 @@ class TestSubcontractingController(ERPNextTestSuite):
|
||||
|
||||
self.assertEqual([item.rm_item_code for item in sco.supplied_items], expected)
|
||||
|
||||
def test_co_by_product(self):
|
||||
frappe.set_value("UOM", "Nos", "must_be_whole_number", 0)
|
||||
|
||||
fg_item = make_item("FG Item", properties={"is_stock_item": 1, "is_sub_contracted_item": 1}).name
|
||||
rm_item = make_item("RM Item", properties={"is_stock_item": 1}).name
|
||||
scrap_item = make_item("Scrap Item", properties={"is_stock_item": 1}).name
|
||||
make_bom(
|
||||
item=fg_item, raw_materials=[rm_item], scrap_items=[scrap_item], process_loss_percentage=10
|
||||
).name
|
||||
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 11",
|
||||
"qty": 5,
|
||||
"rate": 100,
|
||||
"fg_item": fg_item,
|
||||
"fg_item_qty": 5,
|
||||
},
|
||||
]
|
||||
sco = get_subcontracting_order(service_items=service_items)
|
||||
rm_items = get_rm_items(sco.supplied_items)
|
||||
itemwise_details = make_stock_in_entry(rm_items=rm_items)
|
||||
make_stock_transfer_entry(
|
||||
sco_no=sco.name,
|
||||
rm_items=rm_items,
|
||||
itemwise_details=copy.deepcopy(itemwise_details),
|
||||
)
|
||||
|
||||
scr1 = make_subcontracting_receipt(sco.name)
|
||||
scr1.get_secondary_items()
|
||||
scr1.save()
|
||||
|
||||
self.assertEqual(scr1.items[0].received_qty, 5)
|
||||
self.assertEqual(scr1.items[0].process_loss_qty, 0.5)
|
||||
self.assertEqual(scr1.items[0].qty, 4.5)
|
||||
self.assertEqual(scr1.items[0].rate, 200)
|
||||
self.assertEqual(scr1.items[0].amount, 900)
|
||||
|
||||
self.assertEqual(scr1.items[1].item_code, scrap_item)
|
||||
self.assertEqual(scr1.items[1].received_qty, 5)
|
||||
self.assertEqual(scr1.items[1].process_loss_qty, 0.5)
|
||||
self.assertEqual(scr1.items[1].qty, 4.5)
|
||||
self.assertEqual(flt(scr1.items[1].rate, 3), 11.111)
|
||||
self.assertEqual(scr1.items[1].amount, 50)
|
||||
|
||||
frappe.set_value("UOM", "Nos", "must_be_whole_number", 1)
|
||||
|
||||
|
||||
def add_second_row_in_scr(scr):
|
||||
item_dict = {}
|
||||
|
||||
@@ -620,10 +620,10 @@ erpnext.bom.BomController = class BomController extends erpnext.TransactionContr
|
||||
}
|
||||
|
||||
item_code(doc, cdt, cdn) {
|
||||
var scrap_items = false;
|
||||
let secondary_items = false;
|
||||
var child = locals[cdt][cdn];
|
||||
if (child.doctype == "BOM Scrap Item") {
|
||||
scrap_items = true;
|
||||
if (child.doctype == "BOM Secondary Item") {
|
||||
secondary_items = true;
|
||||
}
|
||||
|
||||
if (child.bom_no) {
|
||||
@@ -634,7 +634,7 @@ erpnext.bom.BomController = class BomController extends erpnext.TransactionContr
|
||||
child.do_not_explode = 1;
|
||||
}
|
||||
|
||||
get_bom_material_detail(doc, cdt, cdn, scrap_items);
|
||||
get_bom_material_detail(doc, cdt, cdn, secondary_items);
|
||||
}
|
||||
|
||||
buying_price_list(doc) {
|
||||
@@ -683,7 +683,7 @@ cur_frm.cscript.is_default = function (doc) {
|
||||
if (doc.is_default) cur_frm.set_value("is_active", 1);
|
||||
};
|
||||
|
||||
var get_bom_material_detail = function (doc, cdt, cdn, scrap_items) {
|
||||
var get_bom_material_detail = function (doc, cdt, cdn, secondary_items) {
|
||||
if (!doc.company) {
|
||||
frappe.throw({ message: __("Please select a Company first."), title: __("Mandatory") });
|
||||
}
|
||||
@@ -697,7 +697,6 @@ var get_bom_material_detail = function (doc, cdt, cdn, scrap_items) {
|
||||
company: doc.company,
|
||||
item_code: d.item_code,
|
||||
bom_no: d.bom_no != null ? d.bom_no : "",
|
||||
scrap_items: scrap_items,
|
||||
qty: d.qty,
|
||||
stock_qty: d.stock_qty,
|
||||
include_item_in_manufacturing: d.include_item_in_manufacturing,
|
||||
@@ -706,15 +705,15 @@ var get_bom_material_detail = function (doc, cdt, cdn, scrap_items) {
|
||||
conversion_factor: d.conversion_factor,
|
||||
sourced_by_supplier: d.sourced_by_supplier,
|
||||
do_not_explode: d.do_not_explode,
|
||||
fetch_rate: !secondary_items,
|
||||
},
|
||||
callback: function (r) {
|
||||
$.extend(d, r.message);
|
||||
refresh_field("items");
|
||||
refresh_field("scrap_items");
|
||||
refresh_field("secondary_items");
|
||||
|
||||
doc = locals[doc.doctype][doc.name];
|
||||
erpnext.bom.calculate_rm_cost(doc);
|
||||
erpnext.bom.calculate_scrap_materials_cost(doc);
|
||||
erpnext.bom.calculate_total(doc);
|
||||
},
|
||||
freeze: true,
|
||||
@@ -724,20 +723,18 @@ var get_bom_material_detail = function (doc, cdt, cdn, scrap_items) {
|
||||
|
||||
cur_frm.cscript.qty = function (doc) {
|
||||
erpnext.bom.calculate_rm_cost(doc);
|
||||
erpnext.bom.calculate_scrap_materials_cost(doc);
|
||||
erpnext.bom.calculate_total(doc);
|
||||
};
|
||||
|
||||
cur_frm.cscript.rate = function (doc, cdt, cdn) {
|
||||
var d = locals[cdt][cdn];
|
||||
const is_scrap_item = cdt == "BOM Scrap Item";
|
||||
const is_secondary_item = cdt == "BOM Secondary Item";
|
||||
|
||||
if (d.bom_no) {
|
||||
frappe.msgprint(__("You cannot change the rate if BOM is mentioned against any Item."));
|
||||
get_bom_material_detail(doc, cdt, cdn, is_scrap_item);
|
||||
get_bom_material_detail(doc, cdt, cdn, is_secondary_item);
|
||||
} else {
|
||||
erpnext.bom.calculate_rm_cost(doc);
|
||||
erpnext.bom.calculate_scrap_materials_cost(doc);
|
||||
erpnext.bom.calculate_total(doc);
|
||||
}
|
||||
};
|
||||
@@ -745,7 +742,6 @@ cur_frm.cscript.rate = function (doc, cdt, cdn) {
|
||||
erpnext.bom.update_cost = function (doc) {
|
||||
erpnext.bom.calculate_op_cost(doc);
|
||||
erpnext.bom.calculate_rm_cost(doc);
|
||||
erpnext.bom.calculate_scrap_materials_cost(doc);
|
||||
erpnext.bom.calculate_total(doc);
|
||||
};
|
||||
|
||||
@@ -804,34 +800,11 @@ erpnext.bom.calculate_rm_cost = function (doc) {
|
||||
cur_frm.set_value("base_raw_material_cost", base_total_rm_cost);
|
||||
};
|
||||
|
||||
// sm : scrap material
|
||||
erpnext.bom.calculate_scrap_materials_cost = function (doc) {
|
||||
var sm = doc.scrap_items || [];
|
||||
var total_sm_cost = 0;
|
||||
var base_total_sm_cost = 0;
|
||||
|
||||
for (var i = 0; i < sm.length; i++) {
|
||||
var base_rate = flt(sm[i].rate) * flt(doc.conversion_rate);
|
||||
var amount = flt(sm[i].rate) * flt(sm[i].stock_qty);
|
||||
var base_amount = amount * flt(doc.conversion_rate);
|
||||
|
||||
frappe.model.set_value("BOM Scrap Item", sm[i].name, "base_rate", base_rate);
|
||||
frappe.model.set_value("BOM Scrap Item", sm[i].name, "amount", amount);
|
||||
frappe.model.set_value("BOM Scrap Item", sm[i].name, "base_amount", base_amount);
|
||||
|
||||
total_sm_cost += amount;
|
||||
base_total_sm_cost += base_amount;
|
||||
}
|
||||
|
||||
cur_frm.set_value("scrap_material_cost", total_sm_cost);
|
||||
cur_frm.set_value("base_scrap_material_cost", base_total_sm_cost);
|
||||
};
|
||||
|
||||
// Calculate Total Cost
|
||||
erpnext.bom.calculate_total = function (doc) {
|
||||
var total_cost = flt(doc.operating_cost) + flt(doc.raw_material_cost) - flt(doc.scrap_material_cost);
|
||||
var total_cost = flt(doc.operating_cost) + flt(doc.raw_material_cost) - flt(doc.secondary_items_cost);
|
||||
var base_total_cost =
|
||||
flt(doc.base_operating_cost) + flt(doc.base_raw_material_cost) - flt(doc.base_scrap_material_cost);
|
||||
flt(doc.base_operating_cost) + flt(doc.base_raw_material_cost) - flt(doc.base_secondary_items_cost);
|
||||
|
||||
cur_frm.set_value("total_cost", total_cost);
|
||||
cur_frm.set_value("base_total_cost", base_total_cost);
|
||||
@@ -986,7 +959,7 @@ frappe.tour["BOM"] = [
|
||||
},
|
||||
];
|
||||
|
||||
frappe.ui.form.on("BOM Scrap Item", {
|
||||
frappe.ui.form.on("BOM Secondary Item", {
|
||||
item_code(frm, cdt, cdn) {
|
||||
const { item_code } = locals[cdt][cdn];
|
||||
},
|
||||
@@ -1007,7 +980,7 @@ function trigger_process_loss_qty_prompt(frm, cdt, cdn, item_code) {
|
||||
const row = locals[cdt][cdn];
|
||||
row.stock_qty = (frm.doc.quantity * data.percent) / 100;
|
||||
row.qty = row.stock_qty / (row.conversion_factor || 1);
|
||||
refresh_field("scrap_items");
|
||||
refresh_field("secondary_items");
|
||||
},
|
||||
__("Set Process Loss Item Quantity"),
|
||||
__("Set Quantity")
|
||||
|
||||
@@ -16,6 +16,14 @@
|
||||
"allow_alternative_item",
|
||||
"set_rate_of_sub_assembly_item_based_on_bom",
|
||||
"is_phantom_bom",
|
||||
"cost_allocation_section",
|
||||
"cost_allocation_per",
|
||||
"column_break_srby",
|
||||
"cost_allocation",
|
||||
"process_loss_section",
|
||||
"process_loss_percentage",
|
||||
"column_break_ssj2",
|
||||
"process_loss_qty",
|
||||
"currency_detail",
|
||||
"rm_cost_as_per",
|
||||
"buying_price_list",
|
||||
@@ -38,21 +46,16 @@
|
||||
"operations",
|
||||
"materials_section",
|
||||
"items",
|
||||
"scrap_section",
|
||||
"scrap_items_section",
|
||||
"scrap_items",
|
||||
"process_loss_section",
|
||||
"process_loss_percentage",
|
||||
"column_break_ssj2",
|
||||
"process_loss_qty",
|
||||
"secondary_items_tab",
|
||||
"secondary_items",
|
||||
"costing",
|
||||
"operating_cost",
|
||||
"raw_material_cost",
|
||||
"scrap_material_cost",
|
||||
"secondary_items_cost",
|
||||
"cb1",
|
||||
"base_operating_cost",
|
||||
"base_raw_material_cost",
|
||||
"base_scrap_material_cost",
|
||||
"base_secondary_items_cost",
|
||||
"column_break_26",
|
||||
"total_cost",
|
||||
"base_total_cost",
|
||||
@@ -298,19 +301,6 @@
|
||||
"options": "BOM Item",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"depends_on": "eval:!doc.is_phantom_bom",
|
||||
"fieldname": "scrap_section",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Scrap & Process Loss"
|
||||
},
|
||||
{
|
||||
"fieldname": "scrap_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Scrap Items",
|
||||
"options": "BOM Scrap Item"
|
||||
},
|
||||
{
|
||||
"fieldname": "costing",
|
||||
"fieldtype": "Tab Break",
|
||||
@@ -332,15 +322,6 @@
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_phantom_bom",
|
||||
"fieldname": "scrap_material_cost",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Scrap Material Cost",
|
||||
"options": "currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "cb1",
|
||||
"fieldtype": "Column Break"
|
||||
@@ -362,15 +343,6 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_phantom_bom",
|
||||
"fieldname": "base_scrap_material_cost",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Scrap Material Cost(Company Currency)",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "total_cost",
|
||||
"fieldtype": "Currency",
|
||||
@@ -602,12 +574,6 @@
|
||||
"fieldname": "column_break_ivyw",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "scrap_items_section",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1,
|
||||
"label": "Scrap Items"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "fg_based_operating_cost",
|
||||
@@ -706,6 +672,59 @@
|
||||
"fieldname": "quality_inspection_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Quality Inspection"
|
||||
},
|
||||
{
|
||||
"fieldname": "secondary_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Secondary Items",
|
||||
"options": "BOM Secondary Item"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_phantom_bom",
|
||||
"fieldname": "secondary_items_cost",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Secondary Items Cost",
|
||||
"options": "currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_phantom_bom",
|
||||
"fieldname": "base_secondary_items_cost",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Secondary Items Cost (Company Currency)",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "secondary_items_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Secondary Items"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_allocation_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Cost Allocation"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_srby",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_allocation",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Cost Allocation",
|
||||
"non_negative": 1,
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "100",
|
||||
"fieldname": "cost_allocation_per",
|
||||
"fieldtype": "Percent",
|
||||
"label": "% Cost Allocation",
|
||||
"non_negative": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-sitemap",
|
||||
@@ -713,7 +732,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-06 17:23:15.255301",
|
||||
"modified": "2026-02-26 14:13:34.040181",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM",
|
||||
|
||||
@@ -113,19 +113,21 @@ class BOM(WebsiteGenerator):
|
||||
from erpnext.manufacturing.doctype.bom_explosion_item.bom_explosion_item import BOMExplosionItem
|
||||
from erpnext.manufacturing.doctype.bom_item.bom_item import BOMItem
|
||||
from erpnext.manufacturing.doctype.bom_operation.bom_operation import BOMOperation
|
||||
from erpnext.manufacturing.doctype.bom_scrap_item.bom_scrap_item import BOMScrapItem
|
||||
from erpnext.manufacturing.doctype.bom_secondary_item.bom_secondary_item import BOMSecondaryItem
|
||||
|
||||
allow_alternative_item: DF.Check
|
||||
amended_from: DF.Link | None
|
||||
base_operating_cost: DF.Currency
|
||||
base_raw_material_cost: DF.Currency
|
||||
base_scrap_material_cost: DF.Currency
|
||||
base_secondary_items_cost: DF.Currency
|
||||
base_total_cost: DF.Currency
|
||||
bom_creator: DF.Link | None
|
||||
bom_creator_item: DF.Data | None
|
||||
buying_price_list: DF.Link | None
|
||||
company: DF.Link
|
||||
conversion_rate: DF.Float
|
||||
cost_allocation: DF.Currency
|
||||
cost_allocation_per: DF.Percent
|
||||
currency: DF.Link
|
||||
default_source_warehouse: DF.Link | None
|
||||
default_target_warehouse: DF.Link | None
|
||||
@@ -155,8 +157,8 @@ class BOM(WebsiteGenerator):
|
||||
rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List"]
|
||||
route: DF.SmallText | None
|
||||
routing: DF.Link | None
|
||||
scrap_items: DF.Table[BOMScrapItem]
|
||||
scrap_material_cost: DF.Currency
|
||||
secondary_items: DF.Table[BOMSecondaryItem]
|
||||
secondary_items_cost: DF.Currency
|
||||
set_rate_of_sub_assembly_item_based_on_bom: DF.Check
|
||||
show_in_website: DF.Check
|
||||
show_items: DF.Check
|
||||
@@ -284,7 +286,7 @@ class BOM(WebsiteGenerator):
|
||||
self.set_plc_conversion_rate()
|
||||
self.validate_uom_is_interger()
|
||||
self.set_bom_material_details()
|
||||
self.set_bom_scrap_items_detail()
|
||||
self.set_secondary_items_details()
|
||||
self.validate_materials()
|
||||
self.validate_transfer_against()
|
||||
self.set_routing_operations()
|
||||
@@ -294,9 +296,12 @@ class BOM(WebsiteGenerator):
|
||||
self.update_stock_qty()
|
||||
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate=False, save=False)
|
||||
self.set_process_loss_qty()
|
||||
self.validate_scrap_items()
|
||||
self.validate_uoms()
|
||||
self.set_default_uom()
|
||||
self.validate_semi_finished_goods()
|
||||
self.validate_secondary_items()
|
||||
self.set_fg_cost_allocation()
|
||||
self.validate_total_cost_allocation()
|
||||
|
||||
if self.docstatus == 1:
|
||||
self.validate_raw_materials_of_operation()
|
||||
@@ -326,6 +331,22 @@ class BOM(WebsiteGenerator):
|
||||
),
|
||||
)
|
||||
|
||||
def validate_secondary_items(self):
|
||||
for item in self.secondary_items:
|
||||
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)
|
||||
)
|
||||
)
|
||||
|
||||
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)
|
||||
)
|
||||
)
|
||||
|
||||
def validate_raw_materials_of_operation(self):
|
||||
if not self.track_semi_finished_goods or not self.operations:
|
||||
return
|
||||
@@ -401,6 +422,24 @@ class BOM(WebsiteGenerator):
|
||||
doc = frappe.get_doc("BOM Creator", self.bom_creator)
|
||||
doc.set_status(save=True)
|
||||
|
||||
def set_fg_cost_allocation(self):
|
||||
total_secondary_items_per = 0
|
||||
for item in self.secondary_items:
|
||||
total_secondary_items_per += item.cost_allocation_per
|
||||
|
||||
if self.cost_allocation_per == 100 and total_secondary_items_per:
|
||||
self.cost_allocation_per -= total_secondary_items_per
|
||||
|
||||
self.cost_allocation = self.raw_material_cost * (self.cost_allocation_per / 100)
|
||||
|
||||
def validate_total_cost_allocation(self):
|
||||
total_cost_allocation_per = self.cost_allocation_per
|
||||
for item in self.secondary_items:
|
||||
total_cost_allocation_per += item.cost_allocation_per
|
||||
|
||||
if total_cost_allocation_per != 100:
|
||||
frappe.throw(_("Cost allocation between finished goods and secondary items should equal 100%"))
|
||||
|
||||
def on_update_after_submit(self):
|
||||
self.validate_bom_links()
|
||||
self.manage_default_bom()
|
||||
@@ -462,6 +501,7 @@ class BOM(WebsiteGenerator):
|
||||
"conversion_factor": item.conversion_factor,
|
||||
"sourced_by_supplier": item.sourced_by_supplier,
|
||||
"do_not_explode": item.do_not_explode,
|
||||
"fetch_rate": True,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -469,13 +509,13 @@ class BOM(WebsiteGenerator):
|
||||
if not item.get(r):
|
||||
item.set(r, ret[r])
|
||||
|
||||
def set_bom_scrap_items_detail(self):
|
||||
for item in self.get("scrap_items"):
|
||||
def set_secondary_items_details(self):
|
||||
for item in self.get("secondary_items"):
|
||||
args = {
|
||||
"item_code": item.item_code,
|
||||
"company": self.company,
|
||||
"scrap_items": True,
|
||||
"bom_no": "",
|
||||
"uom": item.uom,
|
||||
"fetch_rate": False,
|
||||
}
|
||||
ret = self.get_bom_material_detail(args)
|
||||
for key, value in ret.items():
|
||||
@@ -495,7 +535,7 @@ class BOM(WebsiteGenerator):
|
||||
|
||||
item = self.get_item_det(args["item_code"])
|
||||
|
||||
args["bom_no"] = args["bom_no"] or item and cstr(item["default_bom"]) or ""
|
||||
args["bom_no"] = args.get("bom_no") or item and cstr(item["default_bom"]) or ""
|
||||
args["transfer_for_manufacture"] = (
|
||||
cstr(args.get("include_item_in_manufacturing", ""))
|
||||
or item
|
||||
@@ -504,7 +544,7 @@ class BOM(WebsiteGenerator):
|
||||
)
|
||||
args.update(item)
|
||||
|
||||
rate = self.get_rm_rate(args)
|
||||
rate = self.get_rm_rate(args) if args.get("fetch_rate") else 0
|
||||
ret_item = {
|
||||
"item_name": item and args["item_name"] or "",
|
||||
"description": item and args["description"] or "",
|
||||
@@ -546,9 +586,7 @@ class BOM(WebsiteGenerator):
|
||||
if not self.rm_cost_as_per:
|
||||
self.rm_cost_as_per = "Valuation Rate"
|
||||
|
||||
if arg.get("scrap_items"):
|
||||
rate = get_valuation_rate(arg)
|
||||
elif arg:
|
||||
if arg:
|
||||
# Customer Provided parts and Supplier sourced parts will have zero rate
|
||||
if not frappe.db.get_value("Item", arg["item_code"], "is_customer_provided_item") and not arg.get(
|
||||
"sourced_by_supplier"
|
||||
@@ -688,7 +726,7 @@ class BOM(WebsiteGenerator):
|
||||
)
|
||||
|
||||
def update_stock_qty(self):
|
||||
for m in self.get("items"):
|
||||
for m in self.get("items") + self.get("secondary_items"):
|
||||
if not m.conversion_factor:
|
||||
m.conversion_factor = flt(get_conversion_factor(m.item_code, m.uom)["conversion_factor"])
|
||||
if m.uom and m.qty:
|
||||
@@ -889,16 +927,16 @@ class BOM(WebsiteGenerator):
|
||||
"""Calculate bom totals"""
|
||||
self.calculate_op_cost(update_hour_rate)
|
||||
self.calculate_rm_cost(save=save_updates)
|
||||
self.calculate_sm_cost(save=save_updates)
|
||||
self.calculate_secondary_items_costs(save=save_updates)
|
||||
if save_updates:
|
||||
# not via doc event, table is not regenerated and needs updation
|
||||
self.calculate_exploded_cost()
|
||||
|
||||
old_cost = self.total_cost
|
||||
|
||||
self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost
|
||||
self.total_cost = self.operating_cost + self.raw_material_cost - self.secondary_items_cost
|
||||
self.base_total_cost = (
|
||||
self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost
|
||||
self.base_operating_cost + self.base_raw_material_cost - self.base_secondary_items_cost
|
||||
)
|
||||
|
||||
if self.total_cost != old_cost:
|
||||
@@ -997,29 +1035,24 @@ class BOM(WebsiteGenerator):
|
||||
self.raw_material_cost = total_rm_cost
|
||||
self.base_raw_material_cost = base_total_rm_cost
|
||||
|
||||
def calculate_sm_cost(self, save=False):
|
||||
def calculate_secondary_items_costs(self, save=False):
|
||||
"""Fetch RM rate as per today's valuation rate and calculate totals"""
|
||||
total_sm_cost = 0
|
||||
base_total_sm_cost = 0
|
||||
precision = self.precision("raw_material_cost")
|
||||
|
||||
for d in self.get("scrap_items"):
|
||||
d.base_rate = flt(d.rate, d.precision("rate")) * flt(
|
||||
self.conversion_rate, self.precision("conversion_rate")
|
||||
)
|
||||
d.amount = flt(
|
||||
flt(d.rate, d.precision("rate")) * flt(d.stock_qty, d.precision("stock_qty")),
|
||||
d.precision("amount"),
|
||||
)
|
||||
d.base_amount = flt(d.amount, d.precision("amount")) * flt(
|
||||
self.conversion_rate, self.precision("conversion_rate")
|
||||
)
|
||||
total_sm_cost += d.amount
|
||||
base_total_sm_cost += d.base_amount
|
||||
if save:
|
||||
d.db_update()
|
||||
for d in self.get("secondary_items"):
|
||||
if not d.is_legacy:
|
||||
d.cost = flt(self.raw_material_cost * (d.cost_allocation_per / 100), precision)
|
||||
d.base_cost = flt(d.cost * self.conversion_rate, precision)
|
||||
|
||||
self.scrap_material_cost = total_sm_cost
|
||||
self.base_scrap_material_cost = base_total_sm_cost
|
||||
total_sm_cost += d.cost
|
||||
base_total_sm_cost += d.base_cost
|
||||
if save:
|
||||
d.db_update()
|
||||
|
||||
self.secondary_items_cost = total_sm_cost
|
||||
self.base_secondary_items_cost = base_total_sm_cost
|
||||
|
||||
def calculate_exploded_cost(self):
|
||||
"Set exploded row cost from it's parent BOM."
|
||||
@@ -1221,16 +1254,29 @@ class BOM(WebsiteGenerator):
|
||||
if self.process_loss_percentage:
|
||||
self.process_loss_qty = flt(self.quantity) * flt(self.process_loss_percentage) / 100
|
||||
|
||||
def validate_scrap_items(self):
|
||||
must_be_whole_number = frappe.get_value("UOM", self.uom, "must_be_whole_number")
|
||||
for item in self.secondary_items:
|
||||
item.process_loss_qty = flt(
|
||||
item.stock_qty * (item.process_loss_per / 100), self.precision("quantity")
|
||||
)
|
||||
|
||||
if self.process_loss_percentage and self.process_loss_percentage > 100:
|
||||
def validate_uoms(self):
|
||||
self.validate_uom(self.item, self.uom, self.process_loss_percentage, self.process_loss_qty)
|
||||
for item in self.secondary_items:
|
||||
self.validate_uom(item.item_code, item.stock_uom, item.process_loss_per, item.process_loss_qty)
|
||||
|
||||
def validate_uom(self, item_code, uom, process_loss_per, process_loss_qty):
|
||||
must_be_whole_number = frappe.get_value("UOM", uom, "must_be_whole_number")
|
||||
|
||||
if process_loss_per and process_loss_per > 100:
|
||||
frappe.throw(_("Process Loss Percentage cannot be greater than 100"))
|
||||
|
||||
if self.process_loss_qty and must_be_whole_number and self.process_loss_qty % 1 != 0:
|
||||
msg = f"Item: {frappe.bold(self.item)} with Stock UOM: {frappe.bold(self.uom)} can't have fractional process loss qty as UOM {frappe.bold(self.uom)} is a whole Number."
|
||||
if process_loss_qty and must_be_whole_number and process_loss_qty % 1 != 0:
|
||||
msg = f"Item: {frappe.bold(item_code)} with Stock UOM: {frappe.bold(uom)} can't have fractional process loss qty as UOM {frappe.bold(uom)} is a whole Number."
|
||||
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"))
|
||||
|
||||
|
||||
def get_bom_item_rate(args, bom_doc):
|
||||
if bom_doc.rm_cost_as_per == "Valuation Rate":
|
||||
@@ -1332,7 +1378,7 @@ def get_bom_items_as_dict(
|
||||
company,
|
||||
qty=1,
|
||||
fetch_exploded=1,
|
||||
fetch_scrap_items=0,
|
||||
fetch_secondary_items=0,
|
||||
include_non_stock_items=False,
|
||||
fetch_qty_in_stock_uom=True,
|
||||
):
|
||||
@@ -1343,7 +1389,7 @@ def get_bom_items_as_dict(
|
||||
fetch_exploded = 0
|
||||
group_by_cond = "group by item_code, operation_row_id, stock_uom"
|
||||
|
||||
if fetch_scrap_items:
|
||||
if fetch_secondary_items:
|
||||
fetch_exploded = 0
|
||||
group_by_cond = "group by item_code"
|
||||
|
||||
@@ -1355,8 +1401,6 @@ def get_bom_items_as_dict(
|
||||
sum(bom_item.{qty_field}/ifnull(bom.quantity, 1)) * %(qty)s as qty,
|
||||
item.image,
|
||||
bom.project,
|
||||
bom_item.rate,
|
||||
sum(bom_item.{qty_field}/ifnull(bom.quantity, 1)) * bom_item.rate * %(qty)s as amount,
|
||||
item.stock_uom,
|
||||
item.item_group,
|
||||
item.allow_alternative_item,
|
||||
@@ -1388,17 +1432,18 @@ def get_bom_items_as_dict(
|
||||
group_by_cond=group_by_cond,
|
||||
select_columns=""", bom_item.source_warehouse, bom_item.operation,
|
||||
bom_item.include_item_in_manufacturing, bom_item.description, bom_item.rate, bom_item.sourced_by_supplier,
|
||||
sum(bom_item.stock_qty/ifnull(bom.quantity, 1)) * bom_item.rate * %(qty)s as amount,
|
||||
(Select idx from `tabBOM Item` where item_code = bom_item.item_code and parent = %(parent)s limit 1) as idx""",
|
||||
)
|
||||
|
||||
items = frappe.db.sql(
|
||||
query, {"parent": bom, "qty": qty, "bom": bom, "company": company}, as_dict=True
|
||||
)
|
||||
elif fetch_scrap_items:
|
||||
elif fetch_secondary_items:
|
||||
query = query.format(
|
||||
table="BOM Scrap Item",
|
||||
table="BOM Secondary Item",
|
||||
where_conditions=")",
|
||||
select_columns=", item.description",
|
||||
select_columns=", item.description, bom_item.cost_allocation_per, bom_item.process_loss_per, bom_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,
|
||||
@@ -1411,8 +1456,9 @@ def get_bom_items_as_dict(
|
||||
where_conditions="or bom_item.is_phantom_item)",
|
||||
is_stock_item=is_stock_item,
|
||||
qty_field="stock_qty" if fetch_qty_in_stock_uom else "qty",
|
||||
select_columns=""", bom_item.uom, bom_item.conversion_factor, bom_item.source_warehouse,
|
||||
select_columns=""", bom_item.rate, bom_item.uom, bom_item.conversion_factor, bom_item.source_warehouse,
|
||||
bom_item.operation, bom_item.include_item_in_manufacturing, bom_item.sourced_by_supplier,
|
||||
sum(bom_item.stock_qty/ifnull(bom.quantity, 1)) * bom_item.rate * %(qty)s as amount,
|
||||
bom_item.description, bom_item.base_rate as rate, bom_item.operation_row_id, bom_item.is_phantom_item , bom_item.bom_no """,
|
||||
group_by_cond=group_by_cond,
|
||||
)
|
||||
@@ -1432,7 +1478,7 @@ def get_bom_items_as_dict(
|
||||
company,
|
||||
qty=item.get("qty"),
|
||||
fetch_exploded=fetch_exploded,
|
||||
fetch_scrap_items=fetch_scrap_items,
|
||||
fetch_secondary_items=fetch_secondary_items,
|
||||
include_non_stock_items=include_non_stock_items,
|
||||
fetch_qty_in_stock_uom=fetch_qty_in_stock_uom,
|
||||
)
|
||||
@@ -1482,7 +1528,7 @@ def validate_bom_no(item, bom_no):
|
||||
for d in bom.items:
|
||||
if d.item_code.lower() == item.lower():
|
||||
rm_item_exists = True
|
||||
for d in bom.scrap_items:
|
||||
for d in bom.secondary_items:
|
||||
if d.item_code.lower() == item.lower():
|
||||
rm_item_exists = True
|
||||
if (
|
||||
@@ -1773,7 +1819,7 @@ def get_bom_diff(bom1, bom2):
|
||||
identifiers = {
|
||||
"operations": "operation",
|
||||
"items": "item_code",
|
||||
"scrap_items": "item_code",
|
||||
"secondary_items": "item_code",
|
||||
"exploded_items": "item_code",
|
||||
}
|
||||
|
||||
@@ -1919,9 +1965,9 @@ def get_op_cost_from_sub_assemblies(bom_no, op_cost=0):
|
||||
return op_cost
|
||||
|
||||
|
||||
def get_scrap_items_from_sub_assemblies(bom_no, company, qty, scrap_items=None):
|
||||
if not scrap_items:
|
||||
scrap_items = {}
|
||||
def get_secondary_items_from_sub_assemblies(bom_no, company, qty, secondary_items=None):
|
||||
if not secondary_items:
|
||||
secondary_items = {}
|
||||
|
||||
bom_items = frappe.get_all(
|
||||
"BOM Item",
|
||||
@@ -1935,9 +1981,9 @@ def get_scrap_items_from_sub_assemblies(bom_no, company, qty, scrap_items=None):
|
||||
continue
|
||||
|
||||
qty = flt(row.qty) * flt(qty)
|
||||
items = get_bom_items_as_dict(row.bom_no, company, qty=qty, fetch_exploded=0, fetch_scrap_items=1)
|
||||
scrap_items.update(items)
|
||||
items = get_bom_items_as_dict(row.bom_no, company, qty=qty, fetch_exploded=0, fetch_secondary_items=1)
|
||||
secondary_items.update(items)
|
||||
|
||||
get_scrap_items_from_sub_assemblies(row.bom_no, company, qty, scrap_items)
|
||||
get_secondary_items_from_sub_assemblies(row.bom_no, company, qty, secondary_items)
|
||||
|
||||
return scrap_items
|
||||
return secondary_items
|
||||
|
||||
@@ -895,7 +895,7 @@ def create_bom_with_process_loss_item(
|
||||
|
||||
if scrap_qty:
|
||||
bom_doc.append(
|
||||
"scrap_items",
|
||||
"secondary_items",
|
||||
{
|
||||
"item_code": fg_item.item_code,
|
||||
"qty": scrap_qty,
|
||||
|
||||
@@ -36,15 +36,17 @@
|
||||
"quantity": 1.0
|
||||
},
|
||||
{
|
||||
"scrap_items":[
|
||||
"secondary_items":[
|
||||
{
|
||||
"amount": 2000.0,
|
||||
"doctype": "BOM Scrap Item",
|
||||
"doctype": "BOM Secondary Item",
|
||||
"item_code": "_Test Item Home Desktop 100",
|
||||
"parentfield": "scrap_items",
|
||||
"parentfield": "secondary_items",
|
||||
"stock_qty": 1.0,
|
||||
"rate": 2000.0,
|
||||
"stock_uom": "_Test UOM"
|
||||
"stock_uom": "_Test UOM",
|
||||
"type": "Scrap",
|
||||
"is_legacy": 1
|
||||
}
|
||||
],
|
||||
"items": [
|
||||
|
||||
@@ -356,7 +356,6 @@ class BOMCreator(Document):
|
||||
{
|
||||
"bom_no": bom_no,
|
||||
"allow_alternative_item": 1,
|
||||
"allow_scrap_items": not item.get("is_phantom_item"),
|
||||
"include_item_in_manufacturing": 1,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2016-09-26 02:19:21.642081",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"item_code",
|
||||
"column_break_2",
|
||||
"item_name",
|
||||
"quantity_and_rate",
|
||||
"stock_qty",
|
||||
"rate",
|
||||
"amount",
|
||||
"column_break_6",
|
||||
"stock_uom",
|
||||
"base_rate",
|
||||
"base_amount"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Item Code",
|
||||
"options": "Item",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "item_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Item Name"
|
||||
},
|
||||
{
|
||||
"fieldname": "quantity_and_rate",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Quantity and Rate"
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Qty",
|
||||
"non_negative": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Rate",
|
||||
"non_negative": 1,
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_6",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock UOM",
|
||||
"options": "UOM",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "base_rate",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Basic Rate (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "base_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Basic Amount (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-31 16:21:44.047007",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM Scrap Item",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2026-02-25 12:44:21.760154",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"type",
|
||||
"rate",
|
||||
"column_break_gres",
|
||||
"is_legacy",
|
||||
"section_break_sbnk",
|
||||
"item_code",
|
||||
"item_name",
|
||||
"uom",
|
||||
"column_break_atlf",
|
||||
"qty",
|
||||
"stock_uom",
|
||||
"conversion_factor",
|
||||
"stock_qty",
|
||||
"section_break_yith",
|
||||
"image",
|
||||
"description",
|
||||
"column_break_wsra",
|
||||
"image_nygv",
|
||||
"section_break_ielf",
|
||||
"cost_allocation_per",
|
||||
"process_loss_per",
|
||||
"column_break_gtbl",
|
||||
"cost",
|
||||
"base_cost",
|
||||
"process_loss_qty"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"depends_on": "eval:!doc.is_legacy",
|
||||
"fieldname": "type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Type",
|
||||
"mandatory_depends_on": "eval:!doc.is_legacy",
|
||||
"options": "\nCo-Product\nBy-Product\nScrap\nAdditional Finished Good"
|
||||
},
|
||||
{
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Item Code",
|
||||
"options": "Item",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.item_name",
|
||||
"fieldname": "item_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Item Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "cost",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Cost",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"options": "currency",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_sbnk",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.stock_uom",
|
||||
"fieldname": "stock_uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock UOM",
|
||||
"options": "UOM",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_atlf",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "uom",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "UOM",
|
||||
"options": "UOM",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "conversion_factor",
|
||||
"fieldtype": "Float",
|
||||
"label": "Conversion Factor",
|
||||
"non_negative": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_legacy",
|
||||
"fieldname": "section_break_ielf",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_gtbl",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_yith",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.image",
|
||||
"fieldname": "image",
|
||||
"fieldtype": "Attach Image",
|
||||
"hidden": 1,
|
||||
"label": "Image",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_wsra",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Stock Qty",
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Qty",
|
||||
"non_negative": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "cost_allocation_per",
|
||||
"fieldtype": "Percent",
|
||||
"label": "Cost Allocation %",
|
||||
"non_negative": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "process_loss_per",
|
||||
"fieldtype": "Percent",
|
||||
"label": "Process Loss %",
|
||||
"non_negative": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Description"
|
||||
},
|
||||
{
|
||||
"depends_on": "image",
|
||||
"fieldname": "image_nygv",
|
||||
"fieldtype": "Image",
|
||||
"options": "image",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "base_cost",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Base Cost (Company Currency)",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_gres",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "is_legacy",
|
||||
"fieldname": "is_legacy",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Legacy",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.is_legacy",
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rate",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "process_loss_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Process Loss Qty",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-03-11 12:12:29.208031",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM Secondary Item",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"rows_threshold_for_grid_search": 20,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class BOMScrapItem(Document):
|
||||
class BOMSecondaryItem(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
@@ -14,17 +14,26 @@ class BOMScrapItem(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
amount: DF.Currency
|
||||
base_amount: DF.Currency
|
||||
base_rate: DF.Currency
|
||||
base_cost: DF.Currency
|
||||
conversion_factor: DF.Float
|
||||
cost: DF.Currency
|
||||
cost_allocation_per: DF.Percent
|
||||
description: DF.TextEditor | None
|
||||
image: DF.AttachImage | None
|
||||
is_legacy: DF.Check
|
||||
item_code: DF.Link
|
||||
item_name: DF.Data | None
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
process_loss_per: DF.Percent
|
||||
process_loss_qty: DF.Float
|
||||
qty: DF.Float
|
||||
rate: DF.Currency
|
||||
stock_qty: DF.Float
|
||||
stock_uom: DF.Link | None
|
||||
type: DF.Literal["", "Co-Product", "By-Product", "Scrap", "Additional Finished Good"]
|
||||
uom: DF.Link
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -23,7 +23,7 @@ frappe.ui.form.on("Job Card", {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("item_code", "scrap_items", () => {
|
||||
frm.set_query("item_code", "secondary_items", () => {
|
||||
return {
|
||||
filters: {
|
||||
disabled: 0,
|
||||
@@ -104,7 +104,7 @@ frappe.ui.form.on("Job Card", {
|
||||
frm.doc.docstatus === 1 &&
|
||||
!frm.doc.is_subcontracted &&
|
||||
(frm.doc.skip_material_transfer || frm.doc.transferred_qty > 0) &&
|
||||
flt(frm.doc.for_quantity) + flt(frm.doc.process_loss_qty) > flt(frm.doc.manufactured_qty)
|
||||
flt(frm.doc.manufactured_qty) + flt(frm.doc.process_loss_qty) < flt(frm.doc.for_quantity)
|
||||
) {
|
||||
frm.add_custom_button(__("Make Stock Entry"), () => {
|
||||
frappe.confirm(
|
||||
@@ -278,8 +278,6 @@ frappe.ui.form.on("Job Card", {
|
||||
frm.trigger("complete_job_card");
|
||||
});
|
||||
}
|
||||
|
||||
frm.trigger("make_dashboard");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,8 +59,8 @@
|
||||
"time_logs",
|
||||
"section_break_21",
|
||||
"sub_operations",
|
||||
"scrap_items_section",
|
||||
"scrap_items",
|
||||
"secondary_items_section",
|
||||
"secondary_items",
|
||||
"corrective_operation_section",
|
||||
"for_job_card",
|
||||
"is_corrective_job_card",
|
||||
@@ -406,20 +406,6 @@
|
||||
"options": "Batch",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "scrap_items_section",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Scrap Items"
|
||||
},
|
||||
{
|
||||
"fieldname": "scrap_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Scrap Items",
|
||||
"no_copy": 1,
|
||||
"options": "Job Card Scrap Item",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "operation.quality_inspection_template",
|
||||
"fieldname": "quality_inspection_template",
|
||||
@@ -623,12 +609,26 @@
|
||||
{
|
||||
"fieldname": "column_break_xhzg",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "secondary_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Secondary Items",
|
||||
"no_copy": 1,
|
||||
"options": "Job Card Secondary Item",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "secondary_items_section",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Secondary Items"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-06 18:27:03.178783",
|
||||
"modified": "2026-02-26 15:13:56.767070",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card",
|
||||
|
||||
@@ -71,7 +71,9 @@ class JobCard(Document):
|
||||
from erpnext.manufacturing.doctype.job_card_scheduled_time.job_card_scheduled_time import (
|
||||
JobCardScheduledTime,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.job_card_scrap_item.job_card_scrap_item import JobCardScrapItem
|
||||
from erpnext.manufacturing.doctype.job_card_secondary_item.job_card_secondary_item import (
|
||||
JobCardSecondaryItem,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.job_card_time_log.job_card_time_log import JobCardTimeLog
|
||||
|
||||
actual_end_date: DF.Datetime | None
|
||||
@@ -110,7 +112,7 @@ class JobCard(Document):
|
||||
remarks: DF.SmallText | None
|
||||
requested_qty: DF.Float
|
||||
scheduled_time_logs: DF.Table[JobCardScheduledTime]
|
||||
scrap_items: DF.Table[JobCardScrapItem]
|
||||
secondary_items: DF.Table[JobCardSecondaryItem]
|
||||
semi_fg_bom: DF.Link | None
|
||||
sequence_id: DF.Int
|
||||
serial_and_batch_bundle: DF.Link | None
|
||||
@@ -199,6 +201,7 @@ class JobCard(Document):
|
||||
|
||||
def set_manufactured_qty(self):
|
||||
table_name = "Stock Entry"
|
||||
child_name = "Stock Entry Detail"
|
||||
if self.is_subcontracted:
|
||||
table_name = "Subcontracting Receipt Item"
|
||||
|
||||
@@ -208,8 +211,13 @@ class JobCard(Document):
|
||||
if self.is_subcontracted:
|
||||
query = query.select(Sum(table.qty))
|
||||
else:
|
||||
query = query.select(Sum(table.fg_completed_qty))
|
||||
query = query.where(table.purpose == "Manufacture")
|
||||
child = frappe.qb.DocType(child_name)
|
||||
query = (
|
||||
query.join(child)
|
||||
.on(table.name == child.parent)
|
||||
.select(Sum(child.transfer_qty))
|
||||
.where((table.purpose == "Manufacture") & (child.is_finished_item == 1))
|
||||
)
|
||||
|
||||
qty = query.run()[0][0] or 0.0
|
||||
self.manufactured_qty = flt(qty)
|
||||
@@ -267,25 +275,35 @@ class JobCard(Document):
|
||||
row.sub_operation = row.operation
|
||||
self.append("sub_operations", row)
|
||||
|
||||
def set_scrap_items(self):
|
||||
if not self.semi_fg_bom:
|
||||
def set_secondary_items(self):
|
||||
if not self.semi_fg_bom and not self.bom_no:
|
||||
return
|
||||
|
||||
items_dict = get_bom_items_as_dict(
|
||||
self.semi_fg_bom, self.company, qty=self.for_quantity, fetch_exploded=0, fetch_scrap_items=1
|
||||
self.semi_fg_bom or self.bom_no,
|
||||
self.company,
|
||||
qty=self.for_quantity,
|
||||
fetch_exploded=0,
|
||||
fetch_secondary_items=1,
|
||||
)
|
||||
for item_code, values in items_dict.items():
|
||||
values = frappe._dict(values)
|
||||
secondary_item = {
|
||||
"item_code": item_code,
|
||||
"stock_qty": values.qty,
|
||||
"item_name": values.item_name,
|
||||
"stock_uom": values.stock_uom,
|
||||
"type": values.type,
|
||||
"bom_secondary_item": values.name,
|
||||
}
|
||||
|
||||
self.append(
|
||||
"scrap_items",
|
||||
{
|
||||
"item_code": item_code,
|
||||
"stock_qty": values.qty,
|
||||
"item_name": values.item_name,
|
||||
"stock_uom": values.stock_uom,
|
||||
},
|
||||
)
|
||||
if not values.is_legacy:
|
||||
secondary_item["stock_qty"] -= flt(
|
||||
secondary_item["stock_qty"] * (values.process_loss_per / 100),
|
||||
self.precision("for_quantity"),
|
||||
)
|
||||
|
||||
self.append("secondary_items", secondary_item)
|
||||
|
||||
def validate_time_logs(self, save=False):
|
||||
self.total_time_in_mins = 0.0
|
||||
@@ -1181,7 +1199,7 @@ class JobCard(Document):
|
||||
def set_status(self, update_status=False):
|
||||
self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0]
|
||||
if self.finished_good and self.docstatus == 1:
|
||||
if self.manufactured_qty >= self.for_quantity:
|
||||
if (self.manufactured_qty + self.process_loss_qty) >= self.for_quantity:
|
||||
self.status = "Completed"
|
||||
elif self.transferred_qty > 0 or self.skip_material_transfer:
|
||||
self.status = "Work In Progress"
|
||||
@@ -1456,12 +1474,24 @@ class JobCard(Document):
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_stock_entry_for_semi_fg_item(self, auto_submit=False):
|
||||
def make_stock_entry_for_semi_fg_item(self, auto_submit: bool = False):
|
||||
def get_consumed_process_loss():
|
||||
table = frappe.qb.DocType("Stock Entry")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(Sum(table.process_loss_qty))
|
||||
.where(
|
||||
(table.purpose == "Manufacture") & (table.job_card == self.name) & (table.docstatus == 1)
|
||||
)
|
||||
)
|
||||
return query.run()[0][0] or 0
|
||||
|
||||
from erpnext.stock.doctype.stock_entry_type.stock_entry_type import ManufactureEntry
|
||||
|
||||
ste = ManufactureEntry(
|
||||
{
|
||||
"for_quantity": self.for_quantity - self.manufactured_qty,
|
||||
"process_loss_qty": max(self.process_loss_qty - get_consumed_process_loss(), 0),
|
||||
"job_card": self.name,
|
||||
"skip_material_transfer": self.skip_material_transfer,
|
||||
"backflush_from_wip_warehouse": self.backflush_from_wip_warehouse,
|
||||
@@ -1481,9 +1511,10 @@ class JobCard(Document):
|
||||
wo_doc = frappe.get_doc("Work Order", self.work_order)
|
||||
add_additional_cost(ste.stock_entry, wo_doc, self)
|
||||
|
||||
ste.stock_entry.set_scrap_items()
|
||||
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.is_scrap_item and not row.t_warehouse:
|
||||
if (row.type or row.is_legacy_scrap_item) and not row.t_warehouse:
|
||||
row.t_warehouse = self.target_warehouse
|
||||
|
||||
if auto_submit:
|
||||
|
||||
@@ -882,6 +882,193 @@ class TestJobCard(ERPNextTestSuite):
|
||||
s = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 6))
|
||||
self.assertEqual(s.additional_costs[0].amount, 8)
|
||||
|
||||
def test_co_by_product_for_sfg_flow(self):
|
||||
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
|
||||
|
||||
frappe.db.set_value("UOM", "Nos", "must_be_whole_number", 0)
|
||||
|
||||
def create_bom(raw_material, finished_good, scrap_item, submit=True):
|
||||
bom = frappe.new_doc("BOM")
|
||||
bom.company = "_Test Company"
|
||||
bom.item = finished_good
|
||||
bom.quantity = 1
|
||||
bom.append("items", {"item_code": raw_material, "qty": 1})
|
||||
bom.append(
|
||||
"secondary_items",
|
||||
{
|
||||
"item_code": scrap_item,
|
||||
"qty": 1,
|
||||
"process_loss_per": 10,
|
||||
"cost_allocation_per": 5,
|
||||
"type": "Scrap",
|
||||
},
|
||||
)
|
||||
if submit:
|
||||
bom.insert()
|
||||
bom.submit()
|
||||
|
||||
return bom
|
||||
|
||||
rm1 = create_item("RM 1")
|
||||
scrap1 = create_item("Scrap 1")
|
||||
sfg = create_item("SFG 1")
|
||||
sfg_bom = create_bom(rm1.name, sfg.name, scrap1.name)
|
||||
|
||||
rm2 = create_item("RM 2")
|
||||
fg1 = create_item("FG 1")
|
||||
scrap2 = create_item("Scrap 2")
|
||||
scrap_extra = create_item("Scrap Extra")
|
||||
fg_bom = create_bom(rm2.name, fg1.name, scrap2.name, submit=False)
|
||||
fg_bom.with_operations = 1
|
||||
fg_bom.track_semi_finished_goods = 1
|
||||
|
||||
operation1 = {
|
||||
"operation": "Test Operation A",
|
||||
"workstation": "_Test Workstation A",
|
||||
"finished_good": sfg.name,
|
||||
"bom_no": sfg_bom.name,
|
||||
"finished_good_qty": 1,
|
||||
"sequence_id": 1,
|
||||
"time_in_mins": 30,
|
||||
}
|
||||
operation2 = {
|
||||
"operation": "Test Operation B",
|
||||
"workstation": "_Test Workstation A",
|
||||
"finished_good": fg1.name,
|
||||
"bom_no": fg_bom.name,
|
||||
"finished_good_qty": 1,
|
||||
"is_final_finished_good": 1,
|
||||
"sequence_id": 2,
|
||||
"time_in_mins": 30,
|
||||
}
|
||||
|
||||
make_workstation(operation1)
|
||||
make_operation(operation1)
|
||||
make_operation(operation2)
|
||||
|
||||
fg_bom.append("operations", operation1)
|
||||
fg_bom.append("operations", operation2)
|
||||
fg_bom.append("items", {"item_code": sfg.name, "qty": 1, "uom": "Nos", "operation_row_id": 2})
|
||||
fg_bom.insert()
|
||||
fg_bom.save()
|
||||
fg_bom.submit()
|
||||
|
||||
work_order = make_wo_order_test_record(
|
||||
item=fg1.name,
|
||||
qty=10,
|
||||
source_warehouse="Stores - _TC",
|
||||
fg_warehouse="Finished Goods - _TC",
|
||||
bom_no=fg_bom.name,
|
||||
skip_transfer=1,
|
||||
do_not_save=True,
|
||||
)
|
||||
|
||||
work_order.operations[0].time_in_mins = 60
|
||||
work_order.operations[1].time_in_mins = 60
|
||||
work_order.save()
|
||||
work_order.submit()
|
||||
|
||||
job_card = frappe.get_doc(
|
||||
"Job Card",
|
||||
frappe.db.get_value(
|
||||
"Job Card", {"work_order": work_order.name, "operation": "Test Operation A"}, "name"
|
||||
),
|
||||
)
|
||||
job_card.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": "2009-01-01 12:06:25",
|
||||
"to_time": "2009-01-01 12:37:25",
|
||||
"completed_qty": job_card.for_quantity,
|
||||
},
|
||||
)
|
||||
job_card.append(
|
||||
"secondary_items", {"item_code": scrap_extra.name, "stock_qty": 5, "type": "Co-Product"}
|
||||
)
|
||||
job_card.submit()
|
||||
|
||||
for row in sfg_bom.items:
|
||||
make_stock_entry(
|
||||
item_code=row.item_code,
|
||||
target="Stores - _TC",
|
||||
qty=10,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
manufacturing_entry = frappe.get_doc(job_card.make_stock_entry_for_semi_fg_item())
|
||||
manufacturing_entry.submit()
|
||||
|
||||
self.assertEqual(manufacturing_entry.items[2].item_code, scrap1.name)
|
||||
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].qty, 5)
|
||||
self.assertEqual(manufacturing_entry.items[3].basic_rate, 0)
|
||||
|
||||
job_card = frappe.get_doc(
|
||||
"Job Card",
|
||||
frappe.db.get_value(
|
||||
"Job Card", {"work_order": work_order.name, "operation": "Test Operation B"}, "name"
|
||||
),
|
||||
)
|
||||
job_card.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": "2009-02-01 12:06:25",
|
||||
"to_time": "2009-02-01 12:37:25",
|
||||
"completed_qty": job_card.for_quantity,
|
||||
},
|
||||
)
|
||||
job_card.submit()
|
||||
|
||||
for row in fg_bom.items:
|
||||
make_stock_entry(
|
||||
item_code=row.item_code,
|
||||
target="Stores - _TC",
|
||||
qty=10,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
manufacturing_entry = frappe.get_doc(job_card.make_stock_entry_for_semi_fg_item())
|
||||
manufacturing_entry.submit()
|
||||
|
||||
self.assertEqual(manufacturing_entry.items[2].item_code, scrap2.name)
|
||||
self.assertEqual(manufacturing_entry.items[2].qty, 9)
|
||||
self.assertEqual(flt(manufacturing_entry.items[2].basic_rate, 3), 5.556)
|
||||
|
||||
def test_secondary_items_without_sfg(self):
|
||||
for row in frappe.get_doc("BOM", self.work_order.bom_no).items:
|
||||
make_stock_entry(
|
||||
item_code=row.item_code,
|
||||
target="_Test Warehouse - _TC",
|
||||
qty=10,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
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(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": "2009-01-01 12:06:25",
|
||||
"to_time": "2009-01-01 12:37:25",
|
||||
"completed_qty": job_card.for_quantity,
|
||||
},
|
||||
)
|
||||
job_card.save()
|
||||
job_card.submit()
|
||||
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
make_stock_entry as make_stock_entry_for_wo,
|
||||
)
|
||||
|
||||
s = frappe.get_doc(make_stock_entry_for_wo(self.work_order.name, "Manufacture"))
|
||||
s.submit()
|
||||
|
||||
self.assertEqual(s.items[3].item_code, "_Test Item")
|
||||
self.assertEqual(s.items[3].transfer_qty, 2)
|
||||
|
||||
|
||||
def create_bom_with_multiple_operations():
|
||||
"Create a BOM with multiple operations and Material Transfer against Job Card"
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"type",
|
||||
"description",
|
||||
"column_break_3",
|
||||
"item_code",
|
||||
"item_name",
|
||||
"column_break_3",
|
||||
"description",
|
||||
"bom_secondary_item",
|
||||
"quantity_and_rate",
|
||||
"stock_qty",
|
||||
"column_break_6",
|
||||
@@ -19,7 +21,7 @@
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Scrap Item Code",
|
||||
"label": "Secondary Item Code",
|
||||
"options": "Item",
|
||||
"reqd": 1
|
||||
},
|
||||
@@ -28,7 +30,7 @@
|
||||
"fieldname": "item_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Scrap Item Name"
|
||||
"label": "Secondary Item Name"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
@@ -65,20 +67,36 @@
|
||||
"label": "Stock UOM",
|
||||
"options": "UOM",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Type",
|
||||
"options": "Co-Product\nBy-Product\nScrap\nAdditional Finished Good",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "bom_secondary_item",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "BOM Secondary Item Reference",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-29 13:09:57.323835",
|
||||
"modified": "2026-03-06 13:51:00.492621",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card Scrap Item",
|
||||
"name": "Job Card Secondary Item",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class JobCardScrapItem(Document):
|
||||
class JobCardSecondaryItem(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
@@ -13,6 +13,7 @@ class JobCardScrapItem(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
bom_secondary_item: DF.Data | None
|
||||
description: DF.SmallText | None
|
||||
item_code: DF.Link
|
||||
item_name: DF.Data | None
|
||||
@@ -21,6 +22,7 @@ class JobCardScrapItem(Document):
|
||||
parenttype: DF.Data
|
||||
stock_qty: DF.Float
|
||||
stock_uom: DF.Link | None
|
||||
type: DF.Literal["Co-Product", "By-Product", "Scrap", "Additional Finished Good"]
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -36,7 +36,7 @@
|
||||
"capacity_planning_for_days",
|
||||
"mins_between_operations",
|
||||
"other_settings_section",
|
||||
"set_op_cost_and_scrap_from_sub_assemblies",
|
||||
"set_op_cost_and_secondary_items_from_sub_assemblies",
|
||||
"column_break_23",
|
||||
"make_serial_no_batch_from_work_order"
|
||||
],
|
||||
@@ -202,13 +202,6 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Validate Components and Quantities Per BOM"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "To include sub-assembly costs and scrap items in Finished Goods on a work order without using a job card, when the 'Use Multi-Level BOM' option is enabled.",
|
||||
"fieldname": "set_op_cost_and_scrap_from_sub_assemblies",
|
||||
"fieldtype": "Check",
|
||||
"label": "Set Operating Cost / Scrap Items From Sub-assemblies"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enabling this checkbox will force each Job Card Time Log to have From Time and To Time",
|
||||
@@ -237,6 +230,13 @@
|
||||
"fieldname": "allow_editing_of_items_and_quantities_in_work_order",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Editing of Items and Quantities in Work Order"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "To include sub-assembly costs and secondary items in Finished Goods on a work order without using a job card, when the 'Use Multi-Level BOM' option is enabled.",
|
||||
"fieldname": "set_op_cost_and_secondary_items_from_sub_assemblies",
|
||||
"fieldtype": "Check",
|
||||
"label": "Set Operating Cost / Secondary Items From Sub-assemblies"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 0,
|
||||
@@ -244,7 +244,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-03-16 13:28:20.714576",
|
||||
"modified": "2026-03-20 13:28:20.714576",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Manufacturing Settings",
|
||||
|
||||
@@ -32,7 +32,7 @@ class ManufacturingSettings(Document):
|
||||
mins_between_operations: DF.Int
|
||||
overproduction_percentage_for_sales_order: DF.Percent
|
||||
overproduction_percentage_for_work_order: DF.Percent
|
||||
set_op_cost_and_scrap_from_sub_assemblies: DF.Check
|
||||
set_op_cost_and_secondary_items_from_sub_assemblies: DF.Check
|
||||
transfer_extra_materials_percentage: DF.Percent
|
||||
update_bom_costs_automatically: DF.Check
|
||||
validate_components_quantities_per_bom: DF.Check
|
||||
|
||||
@@ -2875,6 +2875,7 @@ def make_bom(**args):
|
||||
"company": args.company or "_Test Company",
|
||||
"routing": args.routing,
|
||||
"with_operations": args.with_operations or 0,
|
||||
"process_loss_percentage": args.process_loss_percentage or 0,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2896,6 +2897,23 @@ def make_bom(**args):
|
||||
},
|
||||
)
|
||||
|
||||
if args.scrap_items:
|
||||
for item in args.scrap_items:
|
||||
item_doc = frappe.get_doc("Item", item)
|
||||
bom.append(
|
||||
"secondary_items",
|
||||
{
|
||||
"type": "Scrap",
|
||||
"item_code": item,
|
||||
"item_name": item,
|
||||
"uom": item_doc.stock_uom,
|
||||
"stock_uom": item_doc.stock_uom,
|
||||
"qty": args.scrap_qty or 1,
|
||||
"cost_allocation_per": args.scrap_cost_allocation_per or 10,
|
||||
"process_loss_per": args.scrap_process_loss_per or 10,
|
||||
},
|
||||
)
|
||||
|
||||
if not args.do_not_save:
|
||||
bom.insert(ignore_permissions=True)
|
||||
|
||||
|
||||
@@ -329,7 +329,7 @@ class TestWorkOrder(ERPNextTestSuite):
|
||||
cint(bin1_on_stop_production.projected_qty) + 1, cint(self.bin1_at_start.projected_qty)
|
||||
)
|
||||
|
||||
def test_scrap_material_qty(self):
|
||||
def test_secondary_material_qty(self):
|
||||
wo_order = make_wo_order_test_record(planned_start_date=now(), qty=2)
|
||||
|
||||
# add raw materials to stores
|
||||
@@ -354,15 +354,15 @@ class TestWorkOrder(ERPNextTestSuite):
|
||||
"Work Order", wo_order.name, ["scrap_warehouse", "qty", "produced_qty", "bom_no"], as_dict=1
|
||||
)
|
||||
|
||||
scrap_item_details = get_scrap_item_details(wo_order_details.bom_no)
|
||||
secondary_item_details = get_secondary_item_details(wo_order_details.bom_no)
|
||||
|
||||
self.assertEqual(wo_order_details.produced_qty, 2)
|
||||
|
||||
for item in s.items:
|
||||
if item.bom_no and item.item_code in scrap_item_details:
|
||||
if item.bom_no and item.item_code in secondary_item_details:
|
||||
self.assertEqual(wo_order_details.scrap_warehouse, item.t_warehouse)
|
||||
self.assertEqual(
|
||||
flt(wo_order_details.qty) * flt(scrap_item_details[item.item_code]), item.qty
|
||||
flt(wo_order_details.qty) * flt(secondary_item_details[item.item_code]), item.qty
|
||||
)
|
||||
|
||||
def test_allow_overproduction(self):
|
||||
@@ -1015,7 +1015,7 @@ class TestWorkOrder(ERPNextTestSuite):
|
||||
self.assertEqual(wo.status, "Completed")
|
||||
|
||||
@timeout(seconds=60)
|
||||
def test_job_card_scrap_item(self):
|
||||
def test_job_card_secondary_item(self):
|
||||
items = [
|
||||
"Test FG Item for Scrap Item Test",
|
||||
"Test RM Item 1 for Scrap Item Test",
|
||||
@@ -1074,7 +1074,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.is_scrap_item:
|
||||
if row.type or row.is_legacy_scrap_item:
|
||||
self.assertEqual(row.qty, 1)
|
||||
|
||||
# Partial Job Card 1 with qty 10
|
||||
@@ -1086,7 +1086,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.is_scrap_item:
|
||||
if row.type or row.is_legacy_scrap_item:
|
||||
self.assertEqual(row.qty, 2)
|
||||
|
||||
# Partial Job Card 2 with qty 10
|
||||
@@ -2134,10 +2134,12 @@ class TestWorkOrder(ERPNextTestSuite):
|
||||
for row in se_doc.additional_costs:
|
||||
self.assertEqual(row.expense_account, operating_cost_account)
|
||||
|
||||
def test_op_cost_and_scrap_based_on_sub_assemblies(self):
|
||||
def test_set_op_cost_and_secondary_items_from_sub_assemblies(self):
|
||||
# Make Sub Assembly BOM 1
|
||||
|
||||
frappe.db.set_single_value("Manufacturing Settings", "set_op_cost_and_scrap_from_sub_assemblies", 1)
|
||||
frappe.db.set_single_value(
|
||||
"Manufacturing Settings", "set_op_cost_and_secondary_items_from_sub_assemblies", 1
|
||||
)
|
||||
|
||||
items = {
|
||||
"Test Final FG Item": 0,
|
||||
@@ -2169,16 +2171,20 @@ class TestWorkOrder(ERPNextTestSuite):
|
||||
se_doc.save()
|
||||
|
||||
self.assertTrue(se_doc.additional_costs)
|
||||
scrap_items = []
|
||||
secondary_items = []
|
||||
for item in se_doc.items:
|
||||
if item.is_scrap_item:
|
||||
scrap_items.append(item.item_code)
|
||||
if item.type or item.is_legacy_scrap_item:
|
||||
secondary_items.append(item.item_code)
|
||||
|
||||
self.assertEqual(sorted(scrap_items), sorted(["Test Final Scrap Item 1", "Test Final Scrap Item 2"]))
|
||||
self.assertEqual(
|
||||
sorted(secondary_items), sorted(["Test Final Scrap Item 1", "Test Final Scrap Item 2"])
|
||||
)
|
||||
for row in se_doc.additional_costs:
|
||||
self.assertEqual(row.amount, 3000)
|
||||
|
||||
frappe.db.set_single_value("Manufacturing Settings", "set_op_cost_and_scrap_from_sub_assemblies", 0)
|
||||
frappe.db.set_single_value(
|
||||
"Manufacturing Settings", "set_op_cost_and_secondary_items_from_sub_assemblies", 0
|
||||
)
|
||||
|
||||
@ERPNextTestSuite.change_settings(
|
||||
"Manufacturing Settings", {"material_consumption": 1, "get_rm_cost_from_consumption_entry": 1}
|
||||
@@ -3951,7 +3957,7 @@ def prepare_boms_for_sub_assembly_test():
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
bom.append("scrap_items", {"item_code": "Test Final Scrap Item 1", "qty": 1})
|
||||
bom.append("secondary_items", {"item_code": "Test Final Scrap Item 1", "qty": 1, "is_legacy": 1})
|
||||
|
||||
bom.submit()
|
||||
|
||||
@@ -3964,7 +3970,7 @@ def prepare_boms_for_sub_assembly_test():
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
bom.append("scrap_items", {"item_code": "Test Final Scrap Item 2", "qty": 1})
|
||||
bom.append("secondary_items", {"item_code": "Test Final Scrap Item 2", "qty": 1, "is_legacy": 1})
|
||||
|
||||
bom.submit()
|
||||
|
||||
@@ -4159,7 +4165,7 @@ def update_job_card(job_card, jc_qty=None, days=None):
|
||||
employee = frappe.db.get_value("Employee", {"status": "Active"}, "name")
|
||||
job_card_doc = frappe.get_doc("Job Card", job_card)
|
||||
job_card_doc.set(
|
||||
"scrap_items",
|
||||
"secondary_items",
|
||||
[
|
||||
{"item_code": "Test RM Item 1 for Scrap Item Test", "stock_qty": 2},
|
||||
{"item_code": "Test RM Item 2 for Scrap Item Test", "stock_qty": 2},
|
||||
@@ -4199,17 +4205,17 @@ def update_job_card(job_card, jc_qty=None, days=None):
|
||||
job_card_doc.submit()
|
||||
|
||||
|
||||
def get_scrap_item_details(bom_no):
|
||||
scrap_items = {}
|
||||
def get_secondary_item_details(bom_no):
|
||||
secondary_items = {}
|
||||
for item in frappe.db.sql(
|
||||
"""select item_code, stock_qty from `tabBOM Scrap Item`
|
||||
"""select item_code, stock_qty from `tabBOM Secondary Item`
|
||||
where parent = %s""",
|
||||
bom_no,
|
||||
as_dict=1,
|
||||
):
|
||||
scrap_items[item.item_code] = item.stock_qty
|
||||
secondary_items[item.item_code] = item.stock_qty
|
||||
|
||||
return scrap_items
|
||||
return secondary_items
|
||||
|
||||
|
||||
def allow_overproduction(fieldname, percentage):
|
||||
|
||||
@@ -387,6 +387,7 @@ frappe.ui.form.on("Work Order", {
|
||||
args: {
|
||||
work_order: frm.doc.name,
|
||||
operations: selected_rows,
|
||||
parent_bom: frm.doc.bom_no,
|
||||
},
|
||||
callback: function () {
|
||||
frm.reload_doc();
|
||||
|
||||
@@ -2356,7 +2356,7 @@ def check_if_scrap_warehouse_mandatory(bom_no):
|
||||
if bom_no:
|
||||
bom = frappe.get_doc("BOM", bom_no)
|
||||
|
||||
if len(bom.scrap_items) > 0:
|
||||
if bom.has_scrap_items():
|
||||
res["set_scrap_wh_mandatory"] = True
|
||||
|
||||
return res
|
||||
@@ -2420,6 +2420,7 @@ def make_stock_entry(
|
||||
stock_entry.set_stock_entry_type()
|
||||
stock_entry.is_additional_transfer_entry = is_additional_transfer_entry
|
||||
stock_entry.get_items()
|
||||
stock_entry.set_secondary_items_from_job_card()
|
||||
|
||||
if purpose != "Disassemble":
|
||||
stock_entry.set_serial_no_batch_for_finished_good()
|
||||
@@ -2478,14 +2479,14 @@ def query_sales_order(doctype, txt, searchfield, start, page_len, filters) -> li
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_job_card(work_order, operations):
|
||||
def make_job_card(work_order: str, operations: str | list, parent_bom: str | None = None):
|
||||
if isinstance(operations, str):
|
||||
operations = json.loads(operations)
|
||||
|
||||
work_order = frappe.get_doc("Work Order", work_order)
|
||||
for row in operations:
|
||||
row = frappe._dict(row)
|
||||
row.update(get_operation_details(row.name, work_order))
|
||||
row.update(get_operation_details(row.name, work_order, parent_bom))
|
||||
|
||||
validate_operation_data(row)
|
||||
qty = row.get("qty")
|
||||
@@ -2495,7 +2496,7 @@ def make_job_card(work_order, operations):
|
||||
create_job_card(work_order, row, auto_create=True)
|
||||
|
||||
|
||||
def get_operation_details(name, work_order):
|
||||
def get_operation_details(name, work_order, parent_bom):
|
||||
for row in work_order.operations:
|
||||
if row.name == name:
|
||||
return {
|
||||
@@ -2505,7 +2506,7 @@ def get_operation_details(name, work_order):
|
||||
"fg_warehouse": row.fg_warehouse,
|
||||
"wip_warehouse": row.wip_warehouse,
|
||||
"finished_good": row.finished_good,
|
||||
"bom_no": row.get("bom_no"),
|
||||
"bom_no": row.get("bom_no") or parent_bom,
|
||||
"is_subcontracted": row.get("is_subcontracted"),
|
||||
}
|
||||
|
||||
@@ -2640,8 +2641,9 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create
|
||||
work_order.transfer_material_against == "Job Card" and not work_order.skip_transfer
|
||||
):
|
||||
doc.get_required_items()
|
||||
if work_order.track_semi_finished_goods:
|
||||
doc.set_scrap_items()
|
||||
|
||||
if work_order.track_semi_finished_goods:
|
||||
doc.set_secondary_items()
|
||||
|
||||
if auto_create:
|
||||
doc.flags.ignore_mandatory = True
|
||||
|
||||
@@ -472,3 +472,4 @@ erpnext.patches.v16_0.update_order_qty_and_requested_qty_based_on_mr_and_po
|
||||
erpnext.patches.v16_0.enable_serial_batch_setting
|
||||
erpnext.patches.v16_0.update_requested_qty_packed_item
|
||||
erpnext.patches.v16_0.remove_payables_receivables_workspace
|
||||
erpnext.patches.v16_0.co_by_product_patch
|
||||
|
||||
104
erpnext/patches/v16_0/co_by_product_patch.py
Normal file
104
erpnext/patches/v16_0/co_by_product_patch.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe.model.utils.rename_field import rename_field
|
||||
|
||||
|
||||
def execute():
|
||||
copy_doctypes()
|
||||
rename_fields()
|
||||
|
||||
|
||||
def copy_doctypes():
|
||||
previous = frappe.db.auto_commit_on_many_writes
|
||||
frappe.db.auto_commit_on_many_writes = True
|
||||
try:
|
||||
insert_into_bom()
|
||||
insert_into_job_card()
|
||||
if frappe.db.has_table("Subcontracting Inward Order Scrap Item"):
|
||||
insert_into_subcontracting_inward()
|
||||
finally:
|
||||
frappe.db.auto_commit_on_many_writes = previous
|
||||
|
||||
|
||||
def insert_into_bom():
|
||||
fields = ["item_code", "item_name", "stock_uom", "stock_qty", "rate"]
|
||||
data = frappe.get_all("BOM Scrap Item", {"docstatus": ("<", 2)}, ["parent", *fields])
|
||||
grouped_data = defaultdict(list)
|
||||
for item in data:
|
||||
grouped_data[item.parent].append(item)
|
||||
|
||||
for parent, items in grouped_data.items():
|
||||
bom = frappe.get_doc("BOM", parent)
|
||||
for item in items:
|
||||
secondary_item = frappe.new_doc(
|
||||
"BOM Secondary Item", parent_doc=bom, parentfield="secondary_items"
|
||||
)
|
||||
secondary_item.update({field: item[field] for field in fields})
|
||||
secondary_item.update(
|
||||
{
|
||||
"uom": item.stock_uom,
|
||||
"conversion_factor": 1,
|
||||
"qty": item.stock_qty,
|
||||
"is_legacy": 1,
|
||||
"type": "Scrap",
|
||||
}
|
||||
)
|
||||
secondary_item.insert()
|
||||
|
||||
|
||||
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"])
|
||||
|
||||
|
||||
def insert_into_subcontracting_inward():
|
||||
fields = [
|
||||
"item_code",
|
||||
"fg_item_code",
|
||||
"stock_uom",
|
||||
"warehouse",
|
||||
"reference_name",
|
||||
"produced_qty",
|
||||
"delivered_qty",
|
||||
]
|
||||
bulk_insert(
|
||||
"Subcontracting Inward Order",
|
||||
"Subcontracting Inward Order Scrap Item",
|
||||
"Subcontracting Inward Order Secondary Item",
|
||||
fields,
|
||||
["type"],
|
||||
["Scrap"],
|
||||
)
|
||||
|
||||
|
||||
def bulk_insert(parent_doctype, old_doctype, new_doctype, old_fields, new_fields, new_values):
|
||||
data = frappe.get_all(old_doctype, {"docstatus": ("<", 2)}, ["parent", *old_fields])
|
||||
grouped_data = defaultdict(list)
|
||||
|
||||
for item in data:
|
||||
grouped_data[item.parent].append(item)
|
||||
|
||||
for parent, items in grouped_data.items():
|
||||
parent_doc = frappe.get_doc(parent_doctype, parent)
|
||||
for item in items:
|
||||
secondary_item = frappe.new_doc(new_doctype, parent_doc=parent_doc, parentfield="secondary_items")
|
||||
secondary_item.update({old_field: item[old_field] for old_field in old_fields})
|
||||
secondary_item.update(
|
||||
{new_field: new_value for new_field, new_value in zip(new_fields, new_values, strict=True)}
|
||||
)
|
||||
secondary_item.insert()
|
||||
|
||||
|
||||
def rename_fields():
|
||||
rename_field("BOM", "scrap_material_cost", "secondary_items_cost")
|
||||
rename_field("BOM", "base_scrap_material_cost", "base_secondary_items_cost")
|
||||
rename_field("Stock Entry Detail", "is_scrap_item", "is_legacy_scrap_item")
|
||||
rename_field(
|
||||
"Manufacturing Settings",
|
||||
"set_op_cost_and_scrap_from_sub_assemblies",
|
||||
"set_op_cost_and_secondary_items_from_sub_assemblies",
|
||||
)
|
||||
rename_field("Selling Settings", "deliver_scrap_items", "deliver_secondary_items")
|
||||
rename_field("Subcontracting Receipt Item", "is_scrap_item", "is_legacy_scrap_item")
|
||||
rename_field("Subcontracting Receipt Item", "scrap_cost_per_qty", "secondary_items_cost_per_qty")
|
||||
@@ -1855,7 +1855,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
"base_operating_cost",
|
||||
"base_raw_material_cost",
|
||||
"base_total_cost",
|
||||
"base_scrap_material_cost",
|
||||
"base_secondary_items_cost",
|
||||
"base_totals_section",
|
||||
],
|
||||
company_currency
|
||||
@@ -1873,7 +1873,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
"paid_amount",
|
||||
"write_off_amount",
|
||||
"operating_cost",
|
||||
"scrap_material_cost",
|
||||
"secondary_items_cost",
|
||||
"raw_material_cost",
|
||||
"total_cost",
|
||||
"totals_section",
|
||||
@@ -1919,7 +1919,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
"base_operating_cost",
|
||||
"base_raw_material_cost",
|
||||
"base_total_cost",
|
||||
"base_scrap_material_cost",
|
||||
"base_secondary_items_cost",
|
||||
"base_rounding_adjustment",
|
||||
],
|
||||
this.frm.doc.currency != company_currency
|
||||
@@ -1984,11 +1984,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
});
|
||||
}
|
||||
|
||||
if (this.frm.doc.scrap_items && this.frm.doc.scrap_items.length > 0) {
|
||||
this.frm.set_currency_labels(["rate", "amount"], this.frm.doc.currency, "scrap_items");
|
||||
this.frm.set_currency_labels(["base_rate", "base_amount"], company_currency, "scrap_items");
|
||||
if (this.frm.doc.secondary_items && this.frm.doc.secondary_items.length > 0) {
|
||||
this.frm.set_currency_labels(["rate", "amount"], this.frm.doc.currency, "secondary_items");
|
||||
this.frm.set_currency_labels(["base_rate", "base_amount"], company_currency, "secondary_items");
|
||||
|
||||
var item_grid = this.frm.fields_dict["scrap_items"].grid;
|
||||
var item_grid = this.frm.fields_dict["secondary_items"].grid;
|
||||
$.each(["base_rate", "base_amount"], function (i, fname) {
|
||||
if (frappe.meta.get_docfield(item_grid.doctype, fname))
|
||||
item_grid.set_column_disp(fname, me.frm.doc.currency != company_currency);
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"section_break_zwh6",
|
||||
"allow_delivery_of_overproduced_qty",
|
||||
"column_break_mla9",
|
||||
"deliver_scrap_items"
|
||||
"deliver_secondary_items"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -260,13 +260,6 @@
|
||||
"fieldname": "column_break_mla9",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If enabled, the Scrap Item generated against a Finished Good will also be added in the Stock Entry when delivering that Finished Good.",
|
||||
"fieldname": "deliver_scrap_items",
|
||||
"fieldtype": "Check",
|
||||
"label": "Deliver Scrap Items"
|
||||
},
|
||||
{
|
||||
"fieldname": "item_price_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
@@ -320,6 +313,13 @@
|
||||
"fieldname": "enable_utm",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable UTM"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If enabled, the Secondary Items generated against a Finished Good will also be added in the Stock Entry when delivering that Finished Good.",
|
||||
"fieldname": "deliver_secondary_items",
|
||||
"fieldtype": "Check",
|
||||
"label": "Deliver Secondary Items"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
|
||||
@@ -41,7 +41,7 @@ class SellingSettings(Document):
|
||||
blanket_order_allowance: DF.Float
|
||||
cust_master_name: DF.Literal["Customer Name", "Naming Series", "Auto Name"]
|
||||
customer_group: DF.Link | None
|
||||
deliver_scrap_items: DF.Check
|
||||
deliver_secondary_items: DF.Check
|
||||
dn_required: DF.Literal["No", "Yes"]
|
||||
dont_reserve_sales_order_qty_on_sales_return: DF.Check
|
||||
editable_bundle_item_rates: DF.Check
|
||||
|
||||
@@ -820,7 +820,7 @@ class Company(NestedSet):
|
||||
boms = frappe.db.sql_list("select name from tabBOM where company=%s", self.name)
|
||||
if boms:
|
||||
frappe.db.sql("delete from tabBOM where company=%s", self.name)
|
||||
for dt in ("BOM Operation", "BOM Item", "BOM Scrap Item", "BOM Explosion Item"):
|
||||
for dt in ("BOM Operation", "BOM Item", "BOM Secondary Item", "BOM Explosion Item"):
|
||||
frappe.db.sql(
|
||||
"delete from `tab{}` where parent in ({})".format(dt, ", ".join(["%s"] * len(boms))),
|
||||
tuple(boms),
|
||||
|
||||
@@ -1334,13 +1334,13 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
|
||||
}
|
||||
|
||||
fg_completed_qty() {
|
||||
this.get_items();
|
||||
if (!this.frm.doc.job_card) {
|
||||
this.get_items();
|
||||
}
|
||||
}
|
||||
|
||||
get_items() {
|
||||
var me = this;
|
||||
if (!this.frm.doc.fg_completed_qty || !this.frm.doc.bom_no)
|
||||
frappe.throw(__("BOM and Manufacturing Quantity are required"));
|
||||
|
||||
if (this.frm.doc.work_order || this.frm.doc.bom_no) {
|
||||
// if work order / bom is mentioned, get items
|
||||
|
||||
@@ -31,7 +31,7 @@ from erpnext.manufacturing.doctype.bom.bom import (
|
||||
add_additional_cost,
|
||||
get_bom_items_as_dict,
|
||||
get_op_cost_from_sub_assemblies,
|
||||
get_scrap_items_from_sub_assemblies,
|
||||
get_secondary_items_from_sub_assemblies,
|
||||
validate_bom_no,
|
||||
)
|
||||
from erpnext.setup.doctype.brand.brand import get_brand_defaults
|
||||
@@ -245,7 +245,7 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
self.validate_company_in_accounting_dimension()
|
||||
|
||||
if self.purpose in ("Manufacture", "Repack"):
|
||||
self.mark_finished_and_scrap_items()
|
||||
self.mark_finished_and_secondary_items()
|
||||
if not self.job_card:
|
||||
self.validate_finished_goods()
|
||||
else:
|
||||
@@ -272,7 +272,7 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
self.validate_component_and_quantities()
|
||||
|
||||
if self.get("purpose") != "Manufacture":
|
||||
# ignore scrap item wh difference and empty source/target wh
|
||||
# ignore other item wh difference and empty source/target wh
|
||||
# in Manufacture Entry
|
||||
self.reset_default_field_value("from_warehouse", "items", "s_warehouse")
|
||||
self.reset_default_field_value("to_warehouse", "items", "t_warehouse")
|
||||
@@ -656,7 +656,7 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
item.expense_account = frappe.get_value("Company", self.company, "default_expense_account")
|
||||
|
||||
def validate_fg_completed_qty(self):
|
||||
if self.purpose != "Manufacture":
|
||||
if self.purpose != "Manufacture" or not self.from_bom:
|
||||
return
|
||||
|
||||
fg_qty = defaultdict(float)
|
||||
@@ -789,7 +789,7 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
|
||||
if self.purpose == "Manufacture":
|
||||
if has_bom:
|
||||
if d.is_finished_item or d.is_scrap_item:
|
||||
if d.is_finished_item or d.type or d.is_legacy_scrap_item:
|
||||
d.s_warehouse = None
|
||||
if not d.t_warehouse:
|
||||
frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx))
|
||||
@@ -1093,11 +1093,10 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
|
||||
def set_basic_rate(self, reset_outgoing_rate=True, raise_error_if_no_rate=True):
|
||||
"""
|
||||
Set rate for outgoing, scrapped and finished items
|
||||
Set rate for outgoing, secondary and finished items
|
||||
"""
|
||||
# Set rate for outgoing items
|
||||
outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate, raise_error_if_no_rate)
|
||||
finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item)
|
||||
|
||||
items = []
|
||||
# Set basic rate for incoming items
|
||||
@@ -1111,11 +1110,19 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
elif d.is_finished_item:
|
||||
if self.purpose == "Manufacture":
|
||||
d.basic_rate = self.get_basic_rate_for_manufactured_item(
|
||||
finished_item_qty, outgoing_items_cost
|
||||
d.transfer_qty, outgoing_items_cost
|
||||
)
|
||||
elif self.purpose == "Repack":
|
||||
d.basic_rate = self.get_basic_rate_for_repacked_items(d.transfer_qty, outgoing_items_cost)
|
||||
|
||||
if self.bom_no:
|
||||
d.basic_rate *= frappe.get_value("BOM", self.bom_no, "cost_allocation_per") / 100
|
||||
elif d.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
|
||||
|
||||
if not d.basic_rate and not d.allow_zero_valuation_rate:
|
||||
if self.is_new():
|
||||
raise_error_if_no_rate = False
|
||||
@@ -1198,7 +1205,7 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
|
||||
def get_basic_rate_for_manufactured_item(self, finished_item_qty, outgoing_items_cost=0) -> float:
|
||||
settings = frappe.get_single("Manufacturing Settings")
|
||||
scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item])
|
||||
scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_legacy_scrap_item])
|
||||
|
||||
if settings.material_consumption:
|
||||
if settings.get_rm_cost_from_consumption_entry and self.work_order:
|
||||
@@ -1212,7 +1219,7 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
},
|
||||
):
|
||||
for item in self.items:
|
||||
if not item.is_finished_item and not item.is_scrap_item:
|
||||
if not item.is_finished_item and not item.type and not item.is_legacy_scrap_item:
|
||||
label = frappe.get_meta(settings.doctype).get_label(
|
||||
"get_rm_cost_from_consumption_entry"
|
||||
)
|
||||
@@ -1614,7 +1621,7 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
order,
|
||||
)
|
||||
|
||||
def mark_finished_and_scrap_items(self):
|
||||
def mark_finished_and_secondary_items(self):
|
||||
if self.purpose != "Repack" and any(
|
||||
[d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]
|
||||
):
|
||||
@@ -1631,11 +1638,9 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
if d.t_warehouse and not d.s_warehouse:
|
||||
if self.purpose == "Repack" or d.item_code == finished_item:
|
||||
d.is_finished_item = 1
|
||||
else:
|
||||
d.is_scrap_item = 1
|
||||
else:
|
||||
d.is_finished_item = 0
|
||||
d.is_scrap_item = 0
|
||||
d.type = ""
|
||||
|
||||
def get_finished_item(self):
|
||||
finished_item = None
|
||||
@@ -2434,7 +2439,7 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
self.load_items_from_bom()
|
||||
|
||||
self.set_serial_batch_from_reserved_entry()
|
||||
self.set_scrap_items()
|
||||
self.set_secondary_items()
|
||||
self.set_actual_qty()
|
||||
self.validate_customer_provided_item()
|
||||
self.calculate_rate_and_amount(raise_error_if_no_rate=False)
|
||||
@@ -2579,14 +2584,21 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
def set_scrap_items(self):
|
||||
if self.purpose != "Send to Subcontractor" and self.purpose in ["Manufacture", "Repack"]:
|
||||
scrap_item_dict = self.get_bom_scrap_material(self.fg_completed_qty)
|
||||
for item in scrap_item_dict.values():
|
||||
if self.pro_doc and self.pro_doc.scrap_warehouse:
|
||||
item["to_warehouse"] = self.pro_doc.scrap_warehouse
|
||||
def set_secondary_items(self):
|
||||
if self.purpose in ["Manufacture", "Repack"]:
|
||||
secondary_items_dict = self.get_secondary_items(self.fg_completed_qty)
|
||||
for item in secondary_items_dict.values():
|
||||
if self.pro_doc and item.type:
|
||||
if self.pro_doc.scrap_warehouse and item.type == "Scrap":
|
||||
item["to_warehouse"] = self.pro_doc.scrap_warehouse
|
||||
|
||||
self.add_to_stock_entry_detail(scrap_item_dict, bom_no=self.bom_no)
|
||||
if item.process_loss_per:
|
||||
item["qty"] -= flt(
|
||||
item["qty"] * (item.process_loss_per / 100),
|
||||
self.precision("fg_completed_qty"),
|
||||
)
|
||||
|
||||
self.add_to_stock_entry_detail(secondary_items_dict, bom_no=self.bom_no)
|
||||
|
||||
def set_process_loss_qty(self):
|
||||
if self.purpose not in ("Manufacture", "Repack"):
|
||||
@@ -2600,7 +2612,7 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
fields=[{"MAX": "process_loss_qty", "as": "process_loss_qty"}],
|
||||
)
|
||||
|
||||
if data and data[0].process_loss_qty is not None:
|
||||
if data and data[0].process_loss_qty:
|
||||
process_loss_qty = data[0].process_loss_qty
|
||||
if flt(self.process_loss_qty, precision) != flt(process_loss_qty, precision):
|
||||
self.process_loss_qty = flt(process_loss_qty, precision)
|
||||
@@ -2632,7 +2644,7 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
if not self.pro_doc:
|
||||
self.pro_doc = frappe.get_doc("Work Order", self.work_order)
|
||||
|
||||
if self.pro_doc:
|
||||
if self.pro_doc and not self.pro_doc.track_semi_finished_goods:
|
||||
self.bom_no = self.pro_doc.bom_no
|
||||
else:
|
||||
# invalid work order
|
||||
@@ -2774,54 +2786,59 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
|
||||
return item_dict
|
||||
|
||||
def get_bom_scrap_material(self, qty):
|
||||
def get_secondary_items(self, qty):
|
||||
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
|
||||
|
||||
if (
|
||||
frappe.db.get_single_value("Manufacturing Settings", "set_op_cost_and_scrap_from_sub_assemblies")
|
||||
frappe.db.get_single_value(
|
||||
"Manufacturing Settings", "set_op_cost_and_secondary_items_from_sub_assemblies"
|
||||
)
|
||||
and self.work_order
|
||||
and frappe.get_cached_value("Work Order", self.work_order, "use_multi_level_bom")
|
||||
):
|
||||
item_dict = get_scrap_items_from_sub_assemblies(self.bom_no, self.company, qty)
|
||||
item_dict = get_secondary_items_from_sub_assemblies(self.bom_no, self.company, qty)
|
||||
else:
|
||||
# item dict = { item_code: {qty, description, stock_uom} }
|
||||
item_dict = (
|
||||
get_bom_items_as_dict(
|
||||
self.bom_no, self.company, qty=qty, fetch_exploded=0, fetch_scrap_items=1
|
||||
self.bom_no, self.company, qty=qty, fetch_exploded=0, fetch_secondary_items=1
|
||||
)
|
||||
or {}
|
||||
)
|
||||
|
||||
for item in item_dict.values():
|
||||
item.from_warehouse = ""
|
||||
item.is_scrap_item = 1
|
||||
|
||||
for row in self.get_scrap_items_from_job_card():
|
||||
if row.stock_qty <= 0:
|
||||
continue
|
||||
|
||||
item_row = item_dict.get(row.item_code)
|
||||
if not item_row:
|
||||
item_row = frappe._dict({})
|
||||
|
||||
item_row.update(
|
||||
{
|
||||
"uom": row.stock_uom,
|
||||
"from_warehouse": "",
|
||||
"qty": row.stock_qty + flt(item_row.stock_qty),
|
||||
"converison_factor": 1,
|
||||
"is_scrap_item": 1,
|
||||
"item_name": row.item_name,
|
||||
"description": row.description,
|
||||
"allow_zero_valuation_rate": 1,
|
||||
}
|
||||
)
|
||||
|
||||
item_dict[row.item_code] = item_row
|
||||
|
||||
return item_dict
|
||||
|
||||
def get_scrap_items_from_job_card(self):
|
||||
def set_secondary_items_from_job_card(self):
|
||||
if self.purpose not in ["Manufacture", "Repack"]:
|
||||
return
|
||||
|
||||
item_dict = {}
|
||||
for row in self.get_secondary_items_from_job_card():
|
||||
if row.stock_qty <= 0:
|
||||
continue
|
||||
|
||||
item_dict[row.item_code] = frappe._dict(
|
||||
{
|
||||
"uom": row.stock_uom,
|
||||
"from_warehouse": "",
|
||||
"qty": row.stock_qty,
|
||||
"conversion_factor": 1,
|
||||
"type": row.type,
|
||||
"item_name": row.item_name,
|
||||
"description": row.description,
|
||||
"bom_secondary_item": row.bom_secondary_item,
|
||||
}
|
||||
)
|
||||
|
||||
for item in item_dict.values():
|
||||
item.from_warehouse = ""
|
||||
|
||||
self.add_to_stock_entry_detail(item_dict)
|
||||
|
||||
def get_secondary_items_from_job_card(self):
|
||||
if not hasattr(self, "pro_doc"):
|
||||
self.pro_doc = None
|
||||
|
||||
@@ -2832,70 +2849,78 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
return []
|
||||
|
||||
job_card = frappe.qb.DocType("Job Card")
|
||||
job_card_scrap_item = frappe.qb.DocType("Job Card Scrap Item")
|
||||
job_card_secondary_item = frappe.qb.DocType("Job Card Secondary Item")
|
||||
|
||||
scrap_items = (
|
||||
other = (
|
||||
frappe.qb.from_(job_card)
|
||||
.select(
|
||||
Sum(job_card_scrap_item.stock_qty).as_("stock_qty"),
|
||||
job_card_scrap_item.item_code,
|
||||
job_card_scrap_item.item_name,
|
||||
job_card_scrap_item.description,
|
||||
job_card_scrap_item.stock_uom,
|
||||
Sum(job_card_secondary_item.stock_qty).as_("stock_qty"),
|
||||
job_card_secondary_item.item_code,
|
||||
job_card_secondary_item.item_name,
|
||||
job_card_secondary_item.description,
|
||||
job_card_secondary_item.stock_uom,
|
||||
job_card_secondary_item.type,
|
||||
job_card_secondary_item.bom_secondary_item,
|
||||
)
|
||||
.join(job_card_scrap_item)
|
||||
.on(job_card_scrap_item.parent == job_card.name)
|
||||
.join(job_card_secondary_item)
|
||||
.on(job_card_secondary_item.parent == job_card.name)
|
||||
.where(
|
||||
(job_card_scrap_item.item_code.isnotnull())
|
||||
(job_card_secondary_item.item_code.isnotnull())
|
||||
& (job_card.work_order == self.work_order)
|
||||
& (job_card.docstatus == 1)
|
||||
)
|
||||
.groupby(job_card_scrap_item.item_code)
|
||||
.groupby(job_card_secondary_item.item_code, job_card_secondary_item.type)
|
||||
.orderby(job_card_secondary_item.idx)
|
||||
)
|
||||
|
||||
if self.job_card:
|
||||
scrap_items = scrap_items.where(job_card.name == self.job_card)
|
||||
other = other.where(job_card.name == self.job_card)
|
||||
|
||||
scrap_items = scrap_items.run(as_dict=1)
|
||||
other = other.run(as_dict=1)
|
||||
|
||||
if self.job_card:
|
||||
pending_qty = flt(self.fg_completed_qty)
|
||||
else:
|
||||
pending_qty = flt(self.get_completed_job_card_qty()) - flt(self.pro_doc.produced_qty)
|
||||
|
||||
used_scrap_items = self.get_used_scrap_items()
|
||||
for row in scrap_items:
|
||||
row.stock_qty -= flt(used_scrap_items.get(row.item_code))
|
||||
used_secondary_items = self.get_used_secondary_items()
|
||||
for row in other:
|
||||
row.stock_qty -= flt(used_secondary_items.get(row.item_code))
|
||||
row.stock_qty = (row.stock_qty) * flt(self.fg_completed_qty) / flt(pending_qty)
|
||||
|
||||
if used_scrap_items.get(row.item_code):
|
||||
used_scrap_items[row.item_code] -= row.stock_qty
|
||||
if used_secondary_items.get(row.item_code):
|
||||
used_secondary_items[row.item_code] -= row.stock_qty
|
||||
|
||||
if cint(frappe.get_cached_value("UOM", row.stock_uom, "must_be_whole_number")):
|
||||
row.stock_qty = frappe.utils.ceil(row.stock_qty)
|
||||
|
||||
return scrap_items
|
||||
return other
|
||||
|
||||
def get_completed_job_card_qty(self):
|
||||
return flt(min([d.completed_qty for d in self.pro_doc.operations]))
|
||||
|
||||
def get_used_scrap_items(self):
|
||||
used_scrap_items = defaultdict(float)
|
||||
data = frappe.get_all(
|
||||
"Stock Entry",
|
||||
fields=["`tabStock Entry Detail`.`item_code`", "`tabStock Entry Detail`.`qty`"],
|
||||
filters=[
|
||||
["Stock Entry", "work_order", "=", self.work_order],
|
||||
["Stock Entry Detail", "is_scrap_item", "=", 1],
|
||||
["Stock Entry", "docstatus", "=", 1],
|
||||
["Stock Entry", "purpose", "in", ["Repack", "Manufacture"]],
|
||||
],
|
||||
)
|
||||
def get_used_secondary_items(self):
|
||||
used_secondary_items = defaultdict(float)
|
||||
|
||||
StockEntry = frappe.qb.DocType("Stock Entry")
|
||||
StockEntryDetail = frappe.qb.DocType("Stock Entry Detail")
|
||||
data = (
|
||||
frappe.qb.from_(StockEntry)
|
||||
.inner_join(StockEntryDetail)
|
||||
.on(StockEntryDetail.parent == StockEntry.name)
|
||||
.select(StockEntryDetail.item_code, StockEntryDetail.qty)
|
||||
.where(
|
||||
(StockEntry.work_order == self.work_order)
|
||||
& ((StockEntryDetail.type.isnotnull()) | (StockEntryDetail.is_legacy_scrap_item == 1))
|
||||
& (StockEntry.docstatus == 1)
|
||||
& (StockEntry.purpose.isin(["Repack", "Manufacture"]))
|
||||
)
|
||||
).run(as_dict=1)
|
||||
|
||||
for row in data:
|
||||
used_scrap_items[row.item_code] += row.qty
|
||||
used_secondary_items[row.item_code] += row.qty
|
||||
|
||||
return used_scrap_items
|
||||
return used_secondary_items
|
||||
|
||||
def get_unconsumed_raw_materials(self):
|
||||
wo = frappe.get_doc("Work Order", self.work_order)
|
||||
@@ -3187,7 +3212,12 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
item_row = item_dict[d]
|
||||
|
||||
child_qty = flt(item_row["qty"], precision)
|
||||
if not self.is_return and child_qty <= 0 and not item_row.get("is_scrap_item"):
|
||||
if (
|
||||
not self.is_return
|
||||
and child_qty <= 0
|
||||
and not item_row.get("type")
|
||||
and not item_row.get("is_legacy_scrap_item")
|
||||
):
|
||||
if self.purpose not in ["Receive from Customer", "Send to Subcontractor"]:
|
||||
continue
|
||||
|
||||
@@ -3205,11 +3235,13 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
item_row, company=self.company
|
||||
)
|
||||
se_child.is_finished_item = item_row.get("is_finished_item", 0)
|
||||
se_child.is_scrap_item = item_row.get("is_scrap_item", 0)
|
||||
se_child.po_detail = item_row.get("po_detail")
|
||||
se_child.sco_rm_detail = item_row.get("sco_rm_detail")
|
||||
se_child.scio_detail = item_row.get("scio_detail")
|
||||
se_child.sample_quantity = item_row.get("sample_quantity", 0)
|
||||
se_child.type = item_row.get("type")
|
||||
se_child.is_legacy_scrap_item = item_row.get("is_legacy")
|
||||
se_child.bom_secondary_item = item_row.get("name") or item_row.get("bom_secondary_item")
|
||||
|
||||
for field in [
|
||||
self.subcontract_data.rm_detail_field,
|
||||
@@ -3686,7 +3718,7 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None):
|
||||
if (
|
||||
bom_no
|
||||
and frappe.db.get_single_value(
|
||||
"Manufacturing Settings", "set_op_cost_and_scrap_from_sub_assemblies"
|
||||
"Manufacturing Settings", "set_op_cost_and_secondary_items_from_sub_assemblies"
|
||||
)
|
||||
and frappe.get_cached_value("Work Order", work_order.name, "use_multi_level_bom")
|
||||
):
|
||||
|
||||
@@ -909,8 +909,8 @@ 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
|
||||
scrap_cost = next(filter(lambda x: x.is_scrap_item, s.get("items"))).amount
|
||||
self.assertEqual(fg_cost, flt(rm_cost - scrap_cost, 2))
|
||||
secondary_item_cost = next(filter(lambda x: x.type or x.is_legacy_scrap_item, s.get("items"))).amount
|
||||
self.assertEqual(fg_cost, flt(rm_cost - secondary_item_cost, 2))
|
||||
|
||||
# When Stock Entry has only FG + Scrap
|
||||
s.items.pop(0)
|
||||
@@ -989,15 +989,15 @@ class TestStockEntry(ERPNextTestSuite):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, ste.submit)
|
||||
|
||||
def test_quality_check_for_scrap_item(self):
|
||||
def test_quality_check_for_secondary_item(self):
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
make_stock_entry as _make_stock_entry,
|
||||
)
|
||||
|
||||
scrap_item = "_Test Scrap Item 1"
|
||||
make_item(scrap_item, {"is_stock_item": 1, "is_purchase_item": 0})
|
||||
secondary_item = "_Test Scrap Item 1"
|
||||
make_item(secondary_item, {"is_stock_item": 1, "is_purchase_item": 0})
|
||||
|
||||
bom_name = frappe.db.get_value("BOM Scrap Item", {"docstatus": 1}, "parent")
|
||||
bom_name = frappe.db.get_value("BOM Secondary Item", {"docstatus": 1}, "parent")
|
||||
production_item = frappe.db.get_value("BOM", bom_name, "item")
|
||||
|
||||
work_order = frappe.new_doc("Work Order")
|
||||
@@ -1027,18 +1027,18 @@ class TestStockEntry(ERPNextTestSuite):
|
||||
basic_rate=row.basic_rate or 100,
|
||||
)
|
||||
|
||||
if row.is_scrap_item:
|
||||
row.item_code = scrap_item
|
||||
row.uom = frappe.db.get_value("Item", scrap_item, "stock_uom")
|
||||
row.stock_uom = frappe.db.get_value("Item", scrap_item, "stock_uom")
|
||||
if row.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")
|
||||
|
||||
stock_entry.inspection_required = 1
|
||||
stock_entry.save()
|
||||
|
||||
self.assertTrue([row.item_code for row in stock_entry.items if row.is_scrap_item])
|
||||
self.assertTrue([row.item_code for row in stock_entry.items if row.type or row.is_legacy_scrap_item])
|
||||
|
||||
for row in stock_entry.items:
|
||||
if not row.is_scrap_item:
|
||||
if not row.type and not row.is_legacy_scrap_item:
|
||||
qc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Quality Inspection",
|
||||
@@ -1058,7 +1058,7 @@ class TestStockEntry(ERPNextTestSuite):
|
||||
stock_entry.reload()
|
||||
stock_entry.submit()
|
||||
for row in stock_entry.items:
|
||||
if row.is_scrap_item:
|
||||
if row.type or row.is_legacy_scrap_item:
|
||||
self.assertFalse(row.quality_inspection)
|
||||
else:
|
||||
self.assertTrue(row.quality_inspection)
|
||||
@@ -2464,6 +2464,35 @@ class TestStockEntry(ERPNextTestSuite):
|
||||
# delete naming rule
|
||||
frappe.delete_doc("Document Naming Rule", qc_naming_rule.name)
|
||||
|
||||
def test_co_by_product(self):
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
|
||||
frappe.set_value("UOM", "Nos", "must_be_whole_number", 0)
|
||||
|
||||
fg_item = make_item("FG Item", properties={"is_stock_item": 1}).name
|
||||
rm_item = make_item("RM Item", properties={"is_stock_item": 1}).name
|
||||
scrap_item = make_item("Scrap Item", properties={"is_stock_item": 1}).name
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
make_stock_entry(item_code=rm_item, target=warehouse, qty=5, rate=10, purpose="Material Receipt")
|
||||
|
||||
bom_no = make_bom(
|
||||
item=fg_item, raw_materials=[rm_item], scrap_items=[scrap_item], process_loss_percentage=10
|
||||
).name
|
||||
se = make_stock_entry(item_code=fg_item, qty=5, purpose="Manufacture", do_not_save=True)
|
||||
se.from_bom = 1
|
||||
se.bom_no = bom_no
|
||||
se.fg_completed_qty = 5
|
||||
se.from_warehouse = warehouse
|
||||
se.to_warehouse = "_Test Warehouse 1 - _TC"
|
||||
se.get_items()
|
||||
se.save()
|
||||
se.reload()
|
||||
|
||||
self.assertEqual(se.items[1].qty, 4.5)
|
||||
self.assertEqual(se.items[1].amount, 45)
|
||||
self.assertEqual(se.items[2].qty, 4.5)
|
||||
self.assertEqual(se.items[2].amount, 5)
|
||||
|
||||
|
||||
def make_serialized_item(self, **args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"item_name",
|
||||
"col_break2",
|
||||
"is_finished_item",
|
||||
"is_scrap_item",
|
||||
"is_legacy_scrap_item",
|
||||
"type",
|
||||
"quality_inspection",
|
||||
"subcontracted_item",
|
||||
"against_fg",
|
||||
@@ -81,7 +82,8 @@
|
||||
"putaway_rule",
|
||||
"column_break_51",
|
||||
"reference_purchase_receipt",
|
||||
"job_card_item"
|
||||
"job_card_item",
|
||||
"bom_secondary_item"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -558,12 +560,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_scrap_item",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Scrap Item"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:!doc.is_legacy_scrap_item && !doc.type",
|
||||
"fieldname": "is_finished_item",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Finished Item",
|
||||
@@ -654,6 +651,28 @@
|
||||
"no_copy": 1,
|
||||
"options": "Subcontracting Inward Order Item",
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.purpose == \"Manufacture\" && doc.t_warehouse && !doc.is_finished_item && !doc.is_legacy_scrap_item",
|
||||
"fieldname": "type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Type",
|
||||
"options": "\nCo-Product\nBy-Product\nScrap\nAdditional Finished Good"
|
||||
},
|
||||
{
|
||||
"fieldname": "bom_secondary_item",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "BOM Secondary Item",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "is_legacy_scrap_item",
|
||||
"fieldname": "is_legacy_scrap_item",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Legacy Scrap Item",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
|
||||
@@ -26,6 +26,7 @@ class StockEntryDetail(Document):
|
||||
basic_rate: DF.Currency
|
||||
batch_no: DF.Link | None
|
||||
bom_no: DF.Link | None
|
||||
bom_secondary_item: DF.Data | None
|
||||
conversion_factor: DF.Float
|
||||
cost_center: DF.Link | None
|
||||
customer_provided_item_cost: DF.Currency
|
||||
@@ -34,7 +35,7 @@ class StockEntryDetail(Document):
|
||||
has_item_scanned: DF.Check
|
||||
image: DF.Attach | None
|
||||
is_finished_item: DF.Check
|
||||
is_scrap_item: DF.Check
|
||||
is_legacy_scrap_item: DF.Check
|
||||
item_code: DF.Link
|
||||
item_group: DF.Data | None
|
||||
item_name: DF.Data | None
|
||||
@@ -66,6 +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"]
|
||||
uom: DF.Link
|
||||
use_serial_batch_fields: DF.Check
|
||||
valuation_rate: DF.Currency
|
||||
|
||||
@@ -75,13 +75,18 @@ class ManufactureEntry:
|
||||
self.stock_entry = frappe.new_doc("Stock Entry")
|
||||
self.stock_entry.purpose = self.purpose
|
||||
self.stock_entry.company = self.company
|
||||
self.stock_entry.from_bom = 1
|
||||
self.stock_entry.bom_no = self.bom_no
|
||||
self.stock_entry.use_multi_level_bom = 1
|
||||
|
||||
if self.bom_no:
|
||||
self.stock_entry.from_bom = 1
|
||||
self.stock_entry.bom_no = self.bom_no
|
||||
self.stock_entry.use_multi_level_bom = 1
|
||||
|
||||
self.stock_entry.fg_completed_qty = self.for_quantity
|
||||
self.stock_entry.process_loss_qty = self.process_loss_qty
|
||||
self.stock_entry.project = self.project
|
||||
self.stock_entry.job_card = self.job_card
|
||||
self.stock_entry.set_stock_entry_type()
|
||||
self.stock_entry.work_order = self.work_order
|
||||
|
||||
self.prepare_source_warehouse()
|
||||
self.add_raw_materials()
|
||||
@@ -303,7 +308,7 @@ class ManufactureEntry:
|
||||
args = {
|
||||
"to_warehouse": self.fg_warehouse,
|
||||
"from_warehouse": "",
|
||||
"qty": self.for_quantity,
|
||||
"qty": self.for_quantity - self.process_loss_qty,
|
||||
"item_name": item.item_name,
|
||||
"description": item.description,
|
||||
"stock_uom": item.stock_uom,
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"raw_materials_received_section",
|
||||
"received_items",
|
||||
"scrap_items_generated_section",
|
||||
"scrap_items",
|
||||
"secondary_items",
|
||||
"service_items_section",
|
||||
"service_items",
|
||||
"tab_other_info",
|
||||
@@ -252,17 +252,10 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "scrap_items",
|
||||
"depends_on": "secondary_items",
|
||||
"fieldname": "scrap_items_generated_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Scrap Items Generated"
|
||||
},
|
||||
{
|
||||
"fieldname": "scrap_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Scrap Items",
|
||||
"no_copy": 1,
|
||||
"options": "Subcontracting Inward Order Scrap Item"
|
||||
"label": "Secondary Items Generated"
|
||||
},
|
||||
{
|
||||
"fieldname": "per_returned",
|
||||
@@ -300,13 +293,20 @@
|
||||
"label": "Customer Currency",
|
||||
"options": "Currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "secondary_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Secondary Items",
|
||||
"no_copy": 1,
|
||||
"options": "Subcontracting Inward Order Secondary Item"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-09 15:52:55.781346",
|
||||
"modified": "2026-02-26 17:16:21.697846",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Inward Order",
|
||||
|
||||
@@ -25,8 +25,8 @@ class SubcontractingInwardOrder(SubcontractingController):
|
||||
from erpnext.subcontracting.doctype.subcontracting_inward_order_received_item.subcontracting_inward_order_received_item import (
|
||||
SubcontractingInwardOrderReceivedItem,
|
||||
)
|
||||
from erpnext.subcontracting.doctype.subcontracting_inward_order_scrap_item.subcontracting_inward_order_scrap_item import (
|
||||
SubcontractingInwardOrderScrapItem,
|
||||
from erpnext.subcontracting.doctype.subcontracting_inward_order_secondary_item.subcontracting_inward_order_secondary_item import (
|
||||
SubcontractingInwardOrderSecondaryItem,
|
||||
)
|
||||
from erpnext.subcontracting.doctype.subcontracting_inward_order_service_item.subcontracting_inward_order_service_item import (
|
||||
SubcontractingInwardOrderServiceItem,
|
||||
@@ -48,7 +48,7 @@ class SubcontractingInwardOrder(SubcontractingController):
|
||||
per_returned: DF.Percent
|
||||
received_items: DF.Table[SubcontractingInwardOrderReceivedItem]
|
||||
sales_order: DF.Link
|
||||
scrap_items: DF.Table[SubcontractingInwardOrderScrapItem]
|
||||
secondary_items: DF.Table[SubcontractingInwardOrderSecondaryItem]
|
||||
service_items: DF.Table[SubcontractingInwardOrderServiceItem]
|
||||
set_delivery_warehouse: DF.Link | None
|
||||
status: DF.Literal[
|
||||
@@ -474,23 +474,25 @@ class SubcontractingInwardOrder(SubcontractingController):
|
||||
stock_entry.add_to_stock_entry_detail(items_dict)
|
||||
|
||||
if (
|
||||
frappe.get_single_value("Selling Settings", "deliver_scrap_items")
|
||||
and self.scrap_items
|
||||
frappe.get_single_value("Selling Settings", "deliver_secondary_items")
|
||||
and self.secondary_items
|
||||
and scio_details
|
||||
):
|
||||
scrap_items = [
|
||||
scrap_item for scrap_item in self.scrap_items if scrap_item.reference_name in scio_details
|
||||
secondary_items = [
|
||||
secondary_item
|
||||
for secondary_item in self.secondary_items
|
||||
if secondary_item.reference_name in scio_details
|
||||
]
|
||||
for scrap_item in scrap_items:
|
||||
qty = scrap_item.produced_qty - scrap_item.delivered_qty
|
||||
for secondary_item in secondary_items:
|
||||
qty = secondary_item.produced_qty - secondary_item.delivered_qty
|
||||
if qty > 0:
|
||||
items_dict = {
|
||||
scrap_item.item_code: {
|
||||
"qty": scrap_item.produced_qty - scrap_item.delivered_qty,
|
||||
"from_warehouse": scrap_item.warehouse,
|
||||
"stock_uom": scrap_item.stock_uom,
|
||||
"scio_detail": scrap_item.name,
|
||||
"is_scrap_item": 1,
|
||||
secondary_item.item_code: {
|
||||
"qty": secondary_item.produced_qty - secondary_item.delivered_qty,
|
||||
"from_warehouse": secondary_item.warehouse,
|
||||
"stock_uom": secondary_item.stock_uom,
|
||||
"scio_detail": secondary_item.name,
|
||||
"type": secondary_item.type,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -323,10 +323,12 @@ class IntegrationTestSubcontractingInwardOrder(ERPNextTestSuite):
|
||||
delivery.items[0].qty = 6
|
||||
self.assertRaises(frappe.ValidationError, delivery.submit)
|
||||
|
||||
@ERPNextTestSuite.change_settings("Selling Settings", {"deliver_scrap_items": 1})
|
||||
@ERPNextTestSuite.change_settings("Selling Settings", {"deliver_secondary_items": 1})
|
||||
def test_secondary_items_delivery(self):
|
||||
new_bom = frappe.copy_doc(frappe.get_doc("BOM", "BOM-Basic FG Item-001"))
|
||||
new_bom.scrap_items.append(frappe.new_doc("BOM Scrap Item", item_code="Basic RM 2", qty=1))
|
||||
new_bom.secondary_items.append(
|
||||
frappe.new_doc("BOM Secondary Item", item_code="Basic RM 2", qty=1, type="Scrap")
|
||||
)
|
||||
new_bom.submit()
|
||||
sc_bom = frappe.get_doc("Subcontracting BOM", "SB-0001")
|
||||
sc_bom.finished_good_bom = new_bom.name
|
||||
@@ -343,12 +345,12 @@ class IntegrationTestSubcontractingInwardOrder(ERPNextTestSuite):
|
||||
frappe.new_doc("Stock Entry").update(make_stock_entry_from_wo(wo.name, "Manufacture")).submit()
|
||||
|
||||
scio.reload()
|
||||
self.assertEqual(scio.scrap_items[0].item_code, "Basic RM 2")
|
||||
self.assertEqual(scio.secondary_items[0].item_code, "Basic RM 2")
|
||||
|
||||
delivery = frappe.new_doc("Stock Entry").update(scio.make_subcontracting_delivery())
|
||||
self.assertEqual(delivery.items[-1].item_code, "Basic RM 2")
|
||||
|
||||
frappe.db.set_single_value("Selling Settings", "deliver_scrap_items", 0)
|
||||
frappe.db.set_single_value("Selling Settings", "deliver_secondary_items", 0)
|
||||
delivery = frappe.new_doc("Stock Entry").update(scio.make_subcontracting_delivery())
|
||||
self.assertNotEqual(delivery.items[-1].item_code, "Basic RM 2")
|
||||
|
||||
|
||||
@@ -6,13 +6,15 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"column_break_rptg",
|
||||
"type",
|
||||
"reference_name",
|
||||
"column_break_jkzt",
|
||||
"item_code",
|
||||
"fg_item_code",
|
||||
"column_break_hoxe",
|
||||
"stock_uom",
|
||||
"warehouse",
|
||||
"column_break_rptg",
|
||||
"reference_name",
|
||||
"section_break_gqk9",
|
||||
"produced_qty",
|
||||
"column_break_n4xc",
|
||||
@@ -93,16 +95,29 @@
|
||||
{
|
||||
"fieldname": "column_break_n4xc",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Type",
|
||||
"no_copy": 1,
|
||||
"options": "Co-Product\nBy-Product\nScrap\nAdditional Finished Good",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_jkzt",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-14 10:28:30.192350",
|
||||
"modified": "2026-02-27 15:15:40.009957",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Inward Order Scrap Item",
|
||||
"name": "Subcontracting Inward Order Secondary Item",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
@@ -5,7 +5,7 @@
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class SubcontractingInwardOrderScrapItem(Document):
|
||||
class SubcontractingInwardOrderSecondaryItem(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
@@ -23,6 +23,7 @@ class SubcontractingInwardOrderScrapItem(Document):
|
||||
produced_qty: DF.Float
|
||||
reference_name: DF.Data
|
||||
stock_uom: DF.Link
|
||||
type: DF.Literal["Co-Product", "By-Product", "Scrap", "Additional Finished Good"]
|
||||
warehouse: DF.Link
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -439,6 +439,13 @@ def get_mapped_subcontracting_receipt(source_name, target_doc=None, items=None):
|
||||
target.purchase_order = source_parent.purchase_order
|
||||
target.purchase_order_item = source.purchase_order_item
|
||||
target.qty = items.get(source.name) or (flt(source.qty) - flt(source.received_qty))
|
||||
target.received_qty = target.qty
|
||||
if process_loss_per := frappe.get_value("BOM", source.bom, "process_loss_percentage"):
|
||||
target.process_loss_qty = flt(
|
||||
target.qty * (process_loss_per / 100), target.precision("process_loss_qty")
|
||||
)
|
||||
target.qty -= target.process_loss_qty
|
||||
|
||||
target.amount = (flt(source.qty) - flt(source.received_qty)) * flt(source.rate)
|
||||
|
||||
items = {item["name"]: item["qty"] for item in items} if items else {}
|
||||
|
||||
@@ -425,7 +425,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-03 12:29:45.156101",
|
||||
"modified": "2026-02-27 23:03:36.436504",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Order Item",
|
||||
|
||||
@@ -174,6 +174,7 @@ frappe.ui.form.on("Subcontracting Receipt", {
|
||||
|
||||
frm.trigger("setup_quality_inspection");
|
||||
frm.trigger("set_route_options_for_new_doc");
|
||||
frm.set_df_property("items", "cannot_add_rows", true);
|
||||
},
|
||||
|
||||
set_warehouse: (frm) => {
|
||||
@@ -184,15 +185,15 @@ frappe.ui.form.on("Subcontracting Receipt", {
|
||||
set_warehouse_in_children(frm.doc.items, "rejected_warehouse", frm.doc.rejected_warehouse);
|
||||
},
|
||||
|
||||
get_scrap_items: (frm) => {
|
||||
get_secondary_items: (frm) => {
|
||||
frappe.call({
|
||||
doc: frm.doc,
|
||||
method: "get_scrap_items",
|
||||
method: "get_secondary_items",
|
||||
args: {
|
||||
recalculate_rate: true,
|
||||
},
|
||||
freeze: true,
|
||||
freeze_message: __("Getting Scrap Items"),
|
||||
freeze_message: __("Getting Secondary Items"),
|
||||
callback: (r) => {
|
||||
if (!r.exc) {
|
||||
frm.refresh();
|
||||
@@ -422,11 +423,19 @@ frappe.ui.form.on("Subcontracting Receipt Item", {
|
||||
set_missing_values(frm);
|
||||
},
|
||||
|
||||
rejected_qty(frm) {
|
||||
set_missing_values(frm);
|
||||
},
|
||||
|
||||
process_loss_qty(frm) {
|
||||
set_missing_values(frm);
|
||||
},
|
||||
|
||||
rate(frm) {
|
||||
set_missing_values(frm);
|
||||
},
|
||||
|
||||
items_delete: (frm) => {
|
||||
items_delete(frm) {
|
||||
set_missing_values(frm);
|
||||
},
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
"col_break_warehouse",
|
||||
"supplier_warehouse",
|
||||
"items_section",
|
||||
"get_scrap_items",
|
||||
"items",
|
||||
"get_secondary_items",
|
||||
"section_break0",
|
||||
"total_qty",
|
||||
"column_break_27",
|
||||
@@ -631,13 +631,6 @@
|
||||
"label": "Edit Posting Date and Time",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: (!doc.__islocal && doc.docstatus == 0)",
|
||||
"fieldname": "get_scrap_items",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Scrap Items",
|
||||
"options": "get_scrap_items"
|
||||
},
|
||||
{
|
||||
"fieldname": "supplier_delivery_note",
|
||||
"fieldtype": "Data",
|
||||
@@ -674,12 +667,19 @@
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Connections",
|
||||
"show_dashboard": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: (!doc.__islocal && doc.docstatus == 0)",
|
||||
"fieldname": "get_secondary_items",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Secondary Items",
|
||||
"options": "get_secondary_items"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-08 21:43:27.065640",
|
||||
"modified": "2026-02-27 17:59:44.107193",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Receipt",
|
||||
|
||||
@@ -144,12 +144,12 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
super().validate()
|
||||
|
||||
if self.is_new() and self.get("_action") == "save" and not frappe.in_test:
|
||||
self.get_scrap_items()
|
||||
self.get_secondary_items()
|
||||
|
||||
self.set_missing_values()
|
||||
|
||||
if self.get("_action") == "submit":
|
||||
self.validate_scrap_items()
|
||||
self.validate_secondary_items()
|
||||
self.validate_accepted_warehouse()
|
||||
self.validate_rejected_warehouse()
|
||||
|
||||
@@ -343,39 +343,66 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
self.update_rate_for_supplied_items()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_scrap_items(self, recalculate_rate=False):
|
||||
self.remove_scrap_items()
|
||||
def get_secondary_items(self, recalculate_rate: bool | None = False):
|
||||
self.remove_secondary_items()
|
||||
|
||||
for item in list(self.items):
|
||||
if item.bom:
|
||||
bom = frappe.get_doc("BOM", item.bom)
|
||||
for scrap_item in bom.scrap_items:
|
||||
qty = flt(item.qty) * (flt(scrap_item.stock_qty) / flt(bom.quantity))
|
||||
rate = (
|
||||
get_valuation_rate(
|
||||
scrap_item.item_code,
|
||||
self.set_warehouse,
|
||||
self.doctype,
|
||||
self.name,
|
||||
currency=erpnext.get_company_currency(self.company),
|
||||
company=self.company,
|
||||
)
|
||||
or scrap_item.rate
|
||||
for secondary_item in bom.secondary_items:
|
||||
per_unit = secondary_item.stock_qty / bom.quantity
|
||||
received_qty = flt(item.received_qty * per_unit, item.precision("received_qty"))
|
||||
qty = flt(
|
||||
item.received_qty * (per_unit - (secondary_item.process_loss_qty / bom.quantity)),
|
||||
item.precision("qty"),
|
||||
)
|
||||
if not secondary_item.is_legacy:
|
||||
lcv_cost_per_qty = (
|
||||
flt(item.landed_cost_voucher_amount) / flt(item.qty) if flt(item.qty) else 0.0
|
||||
)
|
||||
fg_item_cost = (
|
||||
flt(item.rm_cost_per_qty)
|
||||
+ flt(item.secondary_items_cost_per_qty)
|
||||
+ flt(item.additional_cost_per_qty)
|
||||
+ flt(lcv_cost_per_qty)
|
||||
+ flt(item.service_cost_per_qty)
|
||||
) * flt(item.received_qty)
|
||||
rate = (
|
||||
(item.amount if self.is_new() else fg_item_cost)
|
||||
* (secondary_item.cost_allocation_per / 100)
|
||||
) / qty
|
||||
else:
|
||||
rate = (
|
||||
get_valuation_rate(
|
||||
secondary_item.item_code,
|
||||
self.set_warehouse,
|
||||
self.doctype,
|
||||
self.name,
|
||||
currency=erpnext.get_company_currency(self.company),
|
||||
company=self.company,
|
||||
)
|
||||
or secondary_item.rate
|
||||
)
|
||||
|
||||
self.append(
|
||||
"items",
|
||||
{
|
||||
"is_scrap_item": 1,
|
||||
"type": secondary_item.type,
|
||||
"is_legacy_scrap_item": secondary_item.is_legacy,
|
||||
"reference_name": item.name,
|
||||
"item_code": scrap_item.item_code,
|
||||
"item_name": scrap_item.item_name,
|
||||
"qty": qty,
|
||||
"stock_uom": scrap_item.stock_uom,
|
||||
"item_code": secondary_item.item_code,
|
||||
"item_name": secondary_item.item_name,
|
||||
"qty": received_qty
|
||||
if not secondary_item.is_legacy
|
||||
else flt(item.qty) * (flt(secondary_item.stock_qty) / flt(bom.quantity)),
|
||||
"received_qty": received_qty,
|
||||
"process_loss_qty": received_qty - qty,
|
||||
"stock_uom": secondary_item.stock_uom,
|
||||
"rate": rate,
|
||||
"rm_cost_per_qty": 0,
|
||||
"service_cost_per_qty": 0,
|
||||
"additional_cost_per_qty": 0,
|
||||
"scrap_cost_per_qty": 0,
|
||||
"secondary_items_cost_per_qty": 0,
|
||||
"amount": qty * rate,
|
||||
"warehouse": self.set_warehouse,
|
||||
"rejected_warehouse": self.rejected_warehouse,
|
||||
@@ -386,15 +413,12 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
self.calculate_additional_costs()
|
||||
self.calculate_items_qty_and_amount()
|
||||
|
||||
def remove_scrap_items(self, recalculate_rate=False):
|
||||
def remove_secondary_items(self):
|
||||
for item in list(self.items):
|
||||
if item.is_scrap_item:
|
||||
if item.type or item.is_legacy_scrap_item:
|
||||
self.remove(item)
|
||||
else:
|
||||
item.scrap_cost_per_qty = 0
|
||||
|
||||
if recalculate_rate:
|
||||
self.calculate_items_qty_and_amount()
|
||||
item.secondary_items_cost_per_qty = 0
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_missing_values(self):
|
||||
@@ -449,30 +473,35 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
else:
|
||||
rm_cost_map[item.reference_name] = item.amount
|
||||
|
||||
scrap_cost_map = {}
|
||||
secondary_items_cost_map = {}
|
||||
for item in self.get("items") or []:
|
||||
if item.is_scrap_item:
|
||||
item.amount = flt(item.qty) * flt(item.rate)
|
||||
if item.type or item.is_legacy_scrap_item:
|
||||
qty = (
|
||||
flt(item.qty)
|
||||
if item.is_legacy_scrap_item
|
||||
else (flt(item.received_qty) - flt(item.process_loss_qty))
|
||||
)
|
||||
item.amount = qty * flt(item.rate)
|
||||
|
||||
if item.reference_name in scrap_cost_map:
|
||||
scrap_cost_map[item.reference_name] += item.amount
|
||||
if item.reference_name in secondary_items_cost_map:
|
||||
secondary_items_cost_map[item.reference_name] += item.amount
|
||||
else:
|
||||
scrap_cost_map[item.reference_name] = item.amount
|
||||
secondary_items_cost_map[item.reference_name] = item.amount
|
||||
|
||||
total_qty = total_amount = 0
|
||||
for item in self.get("items") or []:
|
||||
if not item.is_scrap_item:
|
||||
if not 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]
|
||||
item.rm_cost_per_qty = item.rm_supp_cost / item.qty
|
||||
item.rm_cost_per_qty = item.rm_supp_cost / (item.received_qty or item.qty)
|
||||
rm_cost_map.pop(item.name)
|
||||
|
||||
if item.name in scrap_cost_map:
|
||||
item.scrap_cost_per_qty = scrap_cost_map[item.name] / item.qty
|
||||
scrap_cost_map.pop(item.name)
|
||||
if item.name in secondary_items_cost_map:
|
||||
item.secondary_items_cost_per_qty = secondary_items_cost_map[item.name] / item.qty
|
||||
secondary_items_cost_map.pop(item.name)
|
||||
else:
|
||||
item.scrap_cost_per_qty = 0
|
||||
item.secondary_items_cost_per_qty = 0
|
||||
|
||||
lcv_cost_per_qty = 0.0
|
||||
if item.landed_cost_voucher_amount:
|
||||
@@ -483,36 +512,44 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
+ flt(item.service_cost_per_qty)
|
||||
+ flt(item.additional_cost_per_qty)
|
||||
+ flt(lcv_cost_per_qty)
|
||||
- flt(item.scrap_cost_per_qty)
|
||||
)
|
||||
|
||||
item.received_qty = flt(item.qty) + flt(item.rejected_qty)
|
||||
item.amount = flt(item.qty) * flt(item.rate)
|
||||
if item.bom:
|
||||
item.received_qty = flt(item.qty) + flt(item.rejected_qty) + flt(item.process_loss_qty)
|
||||
item.amount = (
|
||||
flt(item.received_qty)
|
||||
* flt(item.rate)
|
||||
* (frappe.get_value("BOM", item.bom, "cost_allocation_per") / 100)
|
||||
)
|
||||
item.rate = item.amount / (item.qty or item.rejected_qty)
|
||||
else:
|
||||
item.qty = flt(item.received_qty) - flt(item.process_loss_qty)
|
||||
item.amount = flt(item.qty) * flt(item.rate)
|
||||
|
||||
total_qty += flt(item.qty)
|
||||
total_qty += flt(item.qty) + flt(item.rejected_qty)
|
||||
total_amount += item.amount
|
||||
else:
|
||||
self.total_qty = total_qty
|
||||
self.total = total_amount
|
||||
|
||||
def validate_scrap_items(self):
|
||||
def validate_secondary_items(self):
|
||||
for item in self.items:
|
||||
if item.is_scrap_item:
|
||||
if item.type or item.is_legacy_scrap_item:
|
||||
if not item.qty:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Scrap Item Qty cannot be zero").format(item.idx),
|
||||
_("Row #{0}: Secondary Item Qty cannot be zero").format(item.idx),
|
||||
)
|
||||
|
||||
if item.rejected_qty:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Rejected Qty cannot be set for Scrap Item {1}.").format(
|
||||
_("Row #{0}: Rejected Qty cannot be set for Secondary Item {1}.").format(
|
||||
item.idx, frappe.bold(item.item_code)
|
||||
),
|
||||
)
|
||||
|
||||
if not item.reference_name:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Finished Good reference is mandatory for Scrap Item {1}.").format(
|
||||
_("Row #{0}: Finished Good reference is mandatory for Secondary Item {1}.").format(
|
||||
item.idx, frappe.bold(item.item_code)
|
||||
),
|
||||
)
|
||||
|
||||
@@ -597,6 +597,7 @@ class TestSubcontractingReceipt(ERPNextTestSuite):
|
||||
|
||||
scr.items[0].qty = 6 # Accepted Qty
|
||||
scr.items[0].rejected_qty = 4
|
||||
scr.set_missing_values()
|
||||
scr.save()
|
||||
|
||||
# consumed_qty should be (accepted_qty * qty_consumed_per_unit) = (6 * 1) = 6
|
||||
@@ -1154,7 +1155,7 @@ class TestSubcontractingReceipt(ERPNextTestSuite):
|
||||
# ValidationError should not be raised as `Inspection Required before Purchase` is disabled
|
||||
scr2.submit()
|
||||
|
||||
def test_scrap_items_for_subcontracting_receipt(self):
|
||||
def test_secondary_items_for_subcontracting_receipt(self):
|
||||
set_backflush_based_on("BOM")
|
||||
|
||||
fg_item = "Subcontracted Item SA1"
|
||||
@@ -1166,9 +1167,9 @@ class TestSubcontractingReceipt(ERPNextTestSuite):
|
||||
]
|
||||
|
||||
# Create Scrap Items
|
||||
scrap_item_1 = make_item(properties={"is_stock_item": 1, "valuation_rate": 10}).name
|
||||
scrap_item_2 = make_item(properties={"is_stock_item": 1, "valuation_rate": 20}).name
|
||||
scrap_items = [scrap_item_1, scrap_item_2]
|
||||
secondary_item_1 = make_item(properties={"is_stock_item": 1, "valuation_rate": 10}).name
|
||||
secondary_item_2 = make_item(properties={"is_stock_item": 1, "valuation_rate": 20}).name
|
||||
secondary_items = [secondary_item_1, secondary_item_2]
|
||||
|
||||
service_items = [
|
||||
{
|
||||
@@ -1187,13 +1188,14 @@ class TestSubcontractingReceipt(ERPNextTestSuite):
|
||||
)
|
||||
for idx, item in enumerate(bom.items):
|
||||
item.qty = 1 * (idx + 1)
|
||||
for idx, item in enumerate(scrap_items):
|
||||
for idx, item in enumerate(secondary_items):
|
||||
bom.append(
|
||||
"scrap_items",
|
||||
"secondary_items",
|
||||
{
|
||||
"item_code": item,
|
||||
"stock_qty": 1 * (idx + 1),
|
||||
"rate": 10 * (idx + 1),
|
||||
"is_legacy": 1,
|
||||
},
|
||||
)
|
||||
bom.save()
|
||||
@@ -1216,12 +1218,13 @@ class TestSubcontractingReceipt(ERPNextTestSuite):
|
||||
# Create Subcontracting Receipt
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.save()
|
||||
scr.get_scrap_items()
|
||||
scr.get_secondary_items()
|
||||
|
||||
# Test - 1: Scrap Items should be fetched from BOM in items table with `is_scrap_item` = 1
|
||||
scr_scrap_items = set([item.item_code for item in scr.items if item.is_scrap_item])
|
||||
scr_secondary_items = set(
|
||||
[item.item_code for item in scr.items if item.type or item.is_legacy_scrap_item]
|
||||
)
|
||||
self.assertEqual(len(scr.items), 3) # 1 FG Item + 2 Scrap Items
|
||||
self.assertEqual(scr_scrap_items, set(scrap_items))
|
||||
self.assertEqual(scr_secondary_items, set(secondary_items))
|
||||
|
||||
scr.submit()
|
||||
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"item_code",
|
||||
"is_legacy_scrap_item",
|
||||
"type",
|
||||
"column_break_2",
|
||||
"item_name",
|
||||
"is_scrap_item",
|
||||
"section_break_4",
|
||||
"description",
|
||||
"brand",
|
||||
@@ -22,6 +23,7 @@
|
||||
"qty",
|
||||
"rejected_qty",
|
||||
"returned_qty",
|
||||
"process_loss_qty",
|
||||
"col_break2",
|
||||
"stock_uom",
|
||||
"conversion_factor",
|
||||
@@ -33,7 +35,7 @@
|
||||
"rm_cost_per_qty",
|
||||
"service_cost_per_qty",
|
||||
"additional_cost_per_qty",
|
||||
"scrap_cost_per_qty",
|
||||
"secondary_items_cost_per_qty",
|
||||
"rm_supp_cost",
|
||||
"warehouse_and_reference",
|
||||
"warehouse",
|
||||
@@ -144,7 +146,7 @@
|
||||
"default": "0",
|
||||
"fieldname": "received_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Received Quantity",
|
||||
"label": "Qty (As per BOM)",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"print_width": "100px",
|
||||
@@ -157,22 +159,23 @@
|
||||
"fieldname": "qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Accepted Quantity",
|
||||
"label": "Accepted Qty",
|
||||
"no_copy": 1,
|
||||
"print_width": "100px",
|
||||
"read_only_depends_on": "eval:doc.type || doc.is_legacy_scrap_item",
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"depends_on": "eval: !parent.is_return",
|
||||
"depends_on": "eval:!parent.is_return && !doc.type && !doc.is_legacy_scrap_item",
|
||||
"fieldname": "rejected_qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Rejected Quantity",
|
||||
"label": "Rejected Qty",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"print_width": "100px",
|
||||
"read_only_depends_on": "eval: doc.is_scrap_item",
|
||||
"read_only_depends_on": "eval:doc.type || doc.is_legacy_scrap_item",
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
@@ -181,6 +184,7 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.stock_uom",
|
||||
"fieldname": "stock_uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock UOM",
|
||||
@@ -230,7 +234,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: !doc.is_scrap_item",
|
||||
"depends_on": "eval:!doc.type && !doc.is_legacy_scrap_item",
|
||||
"fieldname": "rm_cost_per_qty",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Raw Material Cost Per Qty",
|
||||
@@ -240,7 +244,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: !doc.is_scrap_item",
|
||||
"depends_on": "eval:!doc.type && !doc.is_legacy_scrap_item",
|
||||
"fieldname": "service_cost_per_qty",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Service Cost Per Qty",
|
||||
@@ -250,7 +254,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: !doc.is_scrap_item",
|
||||
"depends_on": "eval:!doc.type && !doc.is_legacy_scrap_item",
|
||||
"fieldname": "additional_cost_per_qty",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Additional Cost Per Qty",
|
||||
@@ -274,7 +278,7 @@
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: !parent.is_return",
|
||||
"depends_on": "eval: !parent.is_return && !doc.type && !doc.is_legacy_scrap_item",
|
||||
"fieldname": "rejected_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
@@ -283,11 +287,10 @@
|
||||
"options": "Warehouse",
|
||||
"print_hide": 1,
|
||||
"print_width": "100px",
|
||||
"read_only_depends_on": "eval: doc.is_scrap_item",
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"depends_on": "eval:!doc.__islocal && !doc.type && !doc.is_legacy_scrap_item",
|
||||
"fieldname": "quality_inspection",
|
||||
"fieldtype": "Link",
|
||||
"label": "Quality Inspection",
|
||||
@@ -369,7 +372,7 @@
|
||||
"no_copy": 1,
|
||||
"options": "BOM",
|
||||
"print_hide": 1,
|
||||
"read_only_depends_on": "eval: doc.is_scrap_item"
|
||||
"read_only_depends_on": "eval:doc.type || doc.is_legacy_scrap_item"
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.brand",
|
||||
@@ -496,7 +499,7 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||
"depends_on": "eval:(doc.use_serial_batch_fields === 0 || doc.docstatus === 1) && !doc.type && !doc.is_legacy_scrap_item",
|
||||
"fieldname": "rejected_serial_and_batch_bundle",
|
||||
"fieldtype": "Link",
|
||||
"label": "Rejected Serial and Batch Bundle",
|
||||
@@ -504,26 +507,6 @@
|
||||
"options": "Serial and Batch Bundle",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: !doc.bom",
|
||||
"fieldname": "is_scrap_item",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Scrap Item",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only_depends_on": "eval: doc.bom"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: !doc.is_scrap_item",
|
||||
"fieldname": "scrap_cost_per_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Scrap Cost Per Qty",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Data",
|
||||
@@ -553,6 +536,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.bom",
|
||||
"fieldname": "include_exploded_items",
|
||||
"fieldtype": "Check",
|
||||
"label": "Include Exploded Items",
|
||||
@@ -580,7 +564,7 @@
|
||||
"label": "Add Serial / Batch Bundle"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0",
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0 && !doc.type && !doc.is_legacy_scrap_item",
|
||||
"fieldname": "add_serial_batch_for_rejected_qty",
|
||||
"fieldtype": "Button",
|
||||
"label": "Add Serial / Batch No (Rejected Qty)"
|
||||
@@ -594,6 +578,7 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.type && !doc.is_legacy_scrap_item",
|
||||
"fieldname": "landed_cost_voucher_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Landed Cost Voucher Amount",
|
||||
@@ -609,13 +594,48 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Service Expense Account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Type",
|
||||
"no_copy": 1,
|
||||
"options": "\nCo-Product\nBy-Product\nScrap\nAdditional Finished Good",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:!doc.type && !doc.is_legacy_scrap_item",
|
||||
"fieldname": "secondary_items_cost_per_qty",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Secondary Items Cost Per Qty",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "is_legacy_scrap_item",
|
||||
"fieldname": "is_legacy_scrap_item",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Legacy Scrap Item",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "process_loss_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Process Loss Qty",
|
||||
"non_negative": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-09-26 12:00:38.877638",
|
||||
"modified": "2026-03-09 15:11:16.977539",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Receipt Item",
|
||||
|
||||
@@ -25,7 +25,7 @@ class SubcontractingReceiptItem(Document):
|
||||
expense_account: DF.Link | None
|
||||
image: DF.Attach | None
|
||||
include_exploded_items: DF.Check
|
||||
is_scrap_item: DF.Check
|
||||
is_legacy_scrap_item: DF.Check
|
||||
item_code: DF.Link
|
||||
item_name: DF.Data | None
|
||||
job_card: DF.Link | None
|
||||
@@ -36,6 +36,7 @@ class SubcontractingReceiptItem(Document):
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
process_loss_qty: DF.Float
|
||||
project: DF.Link | None
|
||||
purchase_order: DF.Link | None
|
||||
purchase_order_item: DF.Data | None
|
||||
@@ -52,7 +53,7 @@ class SubcontractingReceiptItem(Document):
|
||||
rm_cost_per_qty: DF.Currency
|
||||
rm_supp_cost: DF.Currency
|
||||
schedule_date: DF.Date | None
|
||||
scrap_cost_per_qty: DF.Float
|
||||
secondary_items_cost_per_qty: DF.Currency
|
||||
serial_and_batch_bundle: DF.Link | None
|
||||
serial_no: DF.SmallText | None
|
||||
service_cost_per_qty: DF.Currency
|
||||
@@ -61,6 +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"]
|
||||
use_serial_batch_fields: DF.Check
|
||||
warehouse: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
Reference in New Issue
Block a user