Merge pull request #33524 from rohitwaghchaure/revamp-process-loss-feature

refactor: revamp process loss feature & added tab breaks
This commit is contained in:
rohitwaghchaure 2023-01-04 18:37:32 +05:30 committed by GitHub
commit f6da6ece0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 312 additions and 255 deletions

View File

@ -4,7 +4,7 @@
frappe.provide("erpnext.bom"); frappe.provide("erpnext.bom");
frappe.ui.form.on("BOM", { frappe.ui.form.on("BOM", {
setup: function(frm) { setup(frm) {
frm.custom_make_buttons = { frm.custom_make_buttons = {
'Work Order': 'Work Order', 'Work Order': 'Work Order',
'Quality Inspection': 'Quality Inspection' 'Quality Inspection': 'Quality Inspection'
@ -65,11 +65,11 @@ frappe.ui.form.on("BOM", {
}); });
}, },
onload_post_render: function(frm) { onload_post_render(frm) {
frm.get_field("items").grid.set_multiple_add("item_code", "qty"); frm.get_field("items").grid.set_multiple_add("item_code", "qty");
}, },
refresh: function(frm) { refresh(frm) {
frm.toggle_enable("item", frm.doc.__islocal); frm.toggle_enable("item", frm.doc.__islocal);
frm.set_indicator_formatter('item_code', frm.set_indicator_formatter('item_code',
@ -152,7 +152,7 @@ frappe.ui.form.on("BOM", {
} }
}, },
make_work_order: function(frm) { make_work_order(frm) {
frm.events.setup_variant_prompt(frm, "Work Order", (frm, item, data, variant_items) => { frm.events.setup_variant_prompt(frm, "Work Order", (frm, item, data, variant_items) => {
frappe.call({ frappe.call({
method: "erpnext.manufacturing.doctype.work_order.work_order.make_work_order", method: "erpnext.manufacturing.doctype.work_order.work_order.make_work_order",
@ -164,7 +164,7 @@ frappe.ui.form.on("BOM", {
variant_items: variant_items variant_items: variant_items
}, },
freeze: true, freeze: true,
callback: function(r) { callback(r) {
if(r.message) { if(r.message) {
let doc = frappe.model.sync(r.message)[0]; let doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name); frappe.set_route("Form", doc.doctype, doc.name);
@ -174,7 +174,7 @@ frappe.ui.form.on("BOM", {
}); });
}, },
make_variant_bom: function(frm) { make_variant_bom(frm) {
frm.events.setup_variant_prompt(frm, "Variant BOM", (frm, item, data, variant_items) => { frm.events.setup_variant_prompt(frm, "Variant BOM", (frm, item, data, variant_items) => {
frappe.call({ frappe.call({
method: "erpnext.manufacturing.doctype.bom.bom.make_variant_bom", method: "erpnext.manufacturing.doctype.bom.bom.make_variant_bom",
@ -185,7 +185,7 @@ frappe.ui.form.on("BOM", {
variant_items: variant_items variant_items: variant_items
}, },
freeze: true, freeze: true,
callback: function(r) { callback(r) {
if(r.message) { if(r.message) {
let doc = frappe.model.sync(r.message)[0]; let doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name); frappe.set_route("Form", doc.doctype, doc.name);
@ -195,7 +195,7 @@ frappe.ui.form.on("BOM", {
}, true); }, true);
}, },
setup_variant_prompt: function(frm, title, callback, skip_qty_field) { setup_variant_prompt(frm, title, callback, skip_qty_field) {
const fields = []; const fields = [];
if (frm.doc.has_variants) { if (frm.doc.has_variants) {
@ -205,7 +205,7 @@ frappe.ui.form.on("BOM", {
fieldname: 'item', fieldname: 'item',
options: "Item", options: "Item",
reqd: 1, reqd: 1,
get_query: function() { get_query() {
return { return {
query: "erpnext.controllers.queries.item_query", query: "erpnext.controllers.queries.item_query",
filters: { filters: {
@ -273,7 +273,7 @@ frappe.ui.form.on("BOM", {
fieldtype: "Link", fieldtype: "Link",
in_list_view: 1, in_list_view: 1,
reqd: 1, reqd: 1,
get_query: function(data) { get_query(data) {
if (!data.item_code) { if (!data.item_code) {
frappe.throw(__("Select template item")); frappe.throw(__("Select template item"));
} }
@ -308,7 +308,7 @@ frappe.ui.form.on("BOM", {
], ],
in_place_edit: true, in_place_edit: true,
data: [], data: [],
get_data: function () { get_data () {
return []; return [];
}, },
}); });
@ -343,14 +343,14 @@ frappe.ui.form.on("BOM", {
} }
}, },
make_quality_inspection: function(frm) { make_quality_inspection(frm) {
frappe.model.open_mapped_doc({ frappe.model.open_mapped_doc({
method: "erpnext.stock.doctype.quality_inspection.quality_inspection.make_quality_inspection", method: "erpnext.stock.doctype.quality_inspection.quality_inspection.make_quality_inspection",
frm: frm frm: frm
}) })
}, },
update_cost: function(frm, save_doc=false) { update_cost(frm, save_doc=false) {
return frappe.call({ return frappe.call({
doc: frm.doc, doc: frm.doc,
method: "update_cost", method: "update_cost",
@ -360,26 +360,26 @@ frappe.ui.form.on("BOM", {
save: save_doc, save: save_doc,
from_child_bom: false from_child_bom: false
}, },
callback: function(r) { callback(r) {
refresh_field("items"); refresh_field("items");
if(!r.exc) frm.refresh_fields(); if(!r.exc) frm.refresh_fields();
} }
}); });
}, },
rm_cost_as_per: function(frm) { rm_cost_as_per(frm) {
if (in_list(["Valuation Rate", "Last Purchase Rate"], frm.doc.rm_cost_as_per)) { if (in_list(["Valuation Rate", "Last Purchase Rate"], frm.doc.rm_cost_as_per)) {
frm.set_value("plc_conversion_rate", 1.0); frm.set_value("plc_conversion_rate", 1.0);
} }
}, },
routing: function(frm) { routing(frm) {
if (frm.doc.routing) { if (frm.doc.routing) {
frappe.call({ frappe.call({
doc: frm.doc, doc: frm.doc,
method: "get_routing", method: "get_routing",
freeze: true, freeze: true,
callback: function(r) { callback(r) {
if (!r.exc) { if (!r.exc) {
frm.refresh_fields(); frm.refresh_fields();
erpnext.bom.calculate_op_cost(frm.doc); erpnext.bom.calculate_op_cost(frm.doc);
@ -388,6 +388,16 @@ frappe.ui.form.on("BOM", {
} }
}); });
} }
},
process_loss_percentage(frm) {
let qty = 0.0
if (frm.doc.process_loss_percentage) {
qty = (frm.doc.quantity * frm.doc.process_loss_percentage) / 100;
}
frm.set_value("process_loss_qty", qty);
frm.set_value("add_process_loss_cost_in_fg", qty ? 1: 0);
} }
}); });
@ -479,10 +489,6 @@ var get_bom_material_detail = function(doc, cdt, cdn, scrap_items) {
}, },
callback: function(r) { callback: function(r) {
d = locals[cdt][cdn]; d = locals[cdt][cdn];
if (d.is_process_loss) {
r.message.rate = 0;
r.message.base_rate = 0;
}
$.extend(d, r.message); $.extend(d, r.message);
refresh_field("items"); refresh_field("items");
@ -717,10 +723,6 @@ frappe.tour['BOM'] = [
frappe.ui.form.on("BOM Scrap Item", { frappe.ui.form.on("BOM Scrap Item", {
item_code(frm, cdt, cdn) { item_code(frm, cdt, cdn) {
const { item_code } = locals[cdt][cdn]; const { item_code } = locals[cdt][cdn];
if (item_code === frm.doc.item) {
locals[cdt][cdn].is_process_loss = 1;
trigger_process_loss_qty_prompt(frm, cdt, cdn, item_code);
}
}, },
}); });

View File

@ -6,6 +6,7 @@
"document_type": "Setup", "document_type": "Setup",
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"production_item_tab",
"item", "item",
"company", "company",
"item_name", "item_name",
@ -19,14 +20,15 @@
"quantity", "quantity",
"image", "image",
"currency_detail", "currency_detail",
"currency",
"conversion_rate",
"column_break_12",
"rm_cost_as_per", "rm_cost_as_per",
"buying_price_list", "buying_price_list",
"price_list_currency", "price_list_currency",
"plc_conversion_rate", "plc_conversion_rate",
"column_break_ivyw",
"currency",
"conversion_rate",
"section_break_21", "section_break_21",
"operations_section_section",
"with_operations", "with_operations",
"column_break_23", "column_break_23",
"transfer_material_against", "transfer_material_against",
@ -34,13 +36,14 @@
"operations_section", "operations_section",
"operations", "operations",
"materials_section", "materials_section",
"inspection_required",
"quality_inspection_template",
"column_break_31",
"section_break_33",
"items", "items",
"scrap_section", "scrap_section",
"scrap_items_section",
"scrap_items", "scrap_items",
"process_loss_section",
"process_loss_percentage",
"column_break_ssj2",
"process_loss_qty",
"costing", "costing",
"operating_cost", "operating_cost",
"raw_material_cost", "raw_material_cost",
@ -52,10 +55,14 @@
"column_break_26", "column_break_26",
"total_cost", "total_cost",
"base_total_cost", "base_total_cost",
"section_break_25", "more_info_tab",
"description", "description",
"column_break_27", "column_break_27",
"has_variants", "has_variants",
"quality_inspection_section_break",
"inspection_required",
"column_break_dxp7",
"quality_inspection_template",
"section_break0", "section_break0",
"exploded_items", "exploded_items",
"website_section", "website_section",
@ -68,7 +75,8 @@
"show_items", "show_items",
"show_operations", "show_operations",
"web_long_description", "web_long_description",
"amended_from" "amended_from",
"connections_tab"
], ],
"fields": [ "fields": [
{ {
@ -183,7 +191,7 @@
{ {
"fieldname": "currency_detail", "fieldname": "currency_detail",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Currency and Price List" "label": "Cost Configuration"
}, },
{ {
"fieldname": "company", "fieldname": "company",
@ -208,10 +216,6 @@
"precision": "9", "precision": "9",
"reqd": 1 "reqd": 1
}, },
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
},
{ {
"fieldname": "currency", "fieldname": "currency",
"fieldtype": "Link", "fieldtype": "Link",
@ -261,7 +265,7 @@
{ {
"fieldname": "materials_section", "fieldname": "materials_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Materials", "label": "Raw Materials",
"oldfieldtype": "Section Break" "oldfieldtype": "Section Break"
}, },
{ {
@ -276,18 +280,18 @@
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "scrap_section", "fieldname": "scrap_section",
"fieldtype": "Section Break", "fieldtype": "Tab Break",
"label": "Scrap" "label": "Scrap & Process Loss"
}, },
{ {
"fieldname": "scrap_items", "fieldname": "scrap_items",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Scrap Items", "label": "Items",
"options": "BOM Scrap Item" "options": "BOM Scrap Item"
}, },
{ {
"fieldname": "costing", "fieldname": "costing",
"fieldtype": "Section Break", "fieldtype": "Tab Break",
"label": "Costing", "label": "Costing",
"oldfieldtype": "Section Break" "oldfieldtype": "Section Break"
}, },
@ -379,10 +383,6 @@
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{
"fieldname": "section_break_25",
"fieldtype": "Section Break"
},
{ {
"fetch_from": "item.description", "fetch_from": "item.description",
"fieldname": "description", "fieldname": "description",
@ -478,8 +478,8 @@
}, },
{ {
"fieldname": "section_break_21", "fieldname": "section_break_21",
"fieldtype": "Section Break", "fieldtype": "Tab Break",
"label": "Operations" "label": "Operations & Materials"
}, },
{ {
"fieldname": "column_break_23", "fieldname": "column_break_23",
@ -511,6 +511,7 @@
"fetch_from": "item.has_variants", "fetch_from": "item.has_variants",
"fieldname": "has_variants", "fieldname": "has_variants",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 1,
"in_list_view": 1, "in_list_view": 1,
"label": "Has Variants", "label": "Has Variants",
"no_copy": 1, "no_copy": 1,
@ -518,13 +519,63 @@
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "column_break_31", "fieldname": "connections_tab",
"fieldtype": "Tab Break",
"label": "Connections",
"show_dashboard": 1
},
{
"fieldname": "operations_section_section",
"fieldtype": "Section Break",
"label": "Operations"
},
{
"fieldname": "process_loss_section",
"fieldtype": "Section Break",
"label": "Process Loss"
},
{
"fieldname": "process_loss_percentage",
"fieldtype": "Percent",
"label": "% Process Loss"
},
{
"fieldname": "process_loss_qty",
"fieldtype": "Float",
"label": "Process Loss Qty",
"read_only": 1
},
{
"fieldname": "column_break_ssj2",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"fieldname": "section_break_33", "fieldname": "more_info_tab",
"fieldtype": "Tab Break",
"label": "More Info"
},
{
"fieldname": "column_break_dxp7",
"fieldtype": "Column Break"
},
{
"fieldname": "quality_inspection_section_break",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hide_border": 1 "label": "Quality Inspection"
},
{
"fieldname": "production_item_tab",
"fieldtype": "Tab Break",
"label": "Production Item"
},
{
"fieldname": "column_break_ivyw",
"fieldtype": "Column Break"
},
{
"fieldname": "scrap_items_section",
"fieldtype": "Section Break",
"label": "Scrap Items"
} }
], ],
"icon": "fa fa-sitemap", "icon": "fa fa-sitemap",
@ -532,7 +583,7 @@
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-01-30 21:27:54.727298", "modified": "2023-01-03 18:42:27.732107",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM", "name": "BOM",

View File

@ -193,6 +193,7 @@ class BOM(WebsiteGenerator):
self.update_exploded_items(save=False) self.update_exploded_items(save=False)
self.update_stock_qty() self.update_stock_qty()
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate=False, save=False) 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_scrap_items()
def get_context(self, context): def get_context(self, context):
@ -233,6 +234,7 @@ class BOM(WebsiteGenerator):
"sequence_id", "sequence_id",
"operation", "operation",
"workstation", "workstation",
"workstation_type",
"description", "description",
"time_in_mins", "time_in_mins",
"batch_size", "batch_size",
@ -876,36 +878,19 @@ class BOM(WebsiteGenerator):
"""Get a complete tree representation preserving order of child items.""" """Get a complete tree representation preserving order of child items."""
return BOMTree(self.name) return BOMTree(self.name)
def set_process_loss_qty(self):
if self.process_loss_percentage:
self.process_loss_qty = flt(self.quantity) * flt(self.process_loss_percentage) / 100
def validate_scrap_items(self): def validate_scrap_items(self):
for item in self.scrap_items: must_be_whole_number = frappe.get_value("UOM", self.uom, "must_be_whole_number")
msg = ""
if item.item_code == self.item and not item.is_process_loss:
msg = _(
"Scrap/Loss Item: {0} should have Is Process Loss checked as it is the same as the item to be manufactured or repacked."
).format(frappe.bold(item.item_code))
elif item.item_code != self.item and item.is_process_loss:
msg = _(
"Scrap/Loss Item: {0} should not have Is Process Loss checked as it is different from the item to be manufactured or repacked"
).format(frappe.bold(item.item_code))
must_be_whole_number = frappe.get_value("UOM", item.stock_uom, "must_be_whole_number") if self.process_loss_percentage and self.process_loss_percentage > 100:
if item.is_process_loss and must_be_whole_number: frappe.throw(_("Process Loss Percentage cannot be greater than 100"))
msg = _(
"Item: {0} with Stock UOM: {1} cannot be a Scrap/Loss Item as {1} is a whole UOM."
).format(frappe.bold(item.item_code), frappe.bold(item.stock_uom))
if item.is_process_loss and (item.stock_qty >= self.quantity): if self.process_loss_qty and must_be_whole_number and self.process_loss_qty % 1 != 0:
msg = _("Scrap/Loss Item: {0} should have Qty less than finished goods Quantity.").format( 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."
frappe.bold(item.item_code) frappe.throw(msg, title=_("Invalid Process Loss Configuration"))
)
if item.is_process_loss and (item.rate > 0):
msg = _(
"Scrap/Loss Item: {0} should have Rate set to 0 because Is Process Loss is checked."
).format(frappe.bold(item.item_code))
if msg:
frappe.throw(msg, title=_("Note"))
def get_bom_item_rate(args, bom_doc): def get_bom_item_rate(args, bom_doc):
@ -1053,7 +1038,7 @@ def get_bom_items_as_dict(
query = query.format( query = query.format(
table="BOM Scrap Item", table="BOM Scrap Item",
where_conditions="", where_conditions="",
select_columns=", item.description, is_process_loss", select_columns=", item.description",
is_stock_item=is_stock_item, is_stock_item=is_stock_item,
qty_field="stock_qty", qty_field="stock_qty",
) )

View File

@ -384,36 +384,16 @@ class TestBOM(FrappeTestCase):
def test_bom_with_process_loss_item(self): def test_bom_with_process_loss_item(self):
fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items() fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items()
if not frappe.db.exists("BOM", f"BOM-{fg_item_non_whole.item_code}-001"):
bom_doc = create_bom_with_process_loss_item(
fg_item_non_whole, bom_item, scrap_qty=0.25, scrap_rate=0, fg_qty=1
)
bom_doc.submit()
bom_doc = create_bom_with_process_loss_item( bom_doc = create_bom_with_process_loss_item(
fg_item_non_whole, bom_item, scrap_qty=2, scrap_rate=0 fg_item_non_whole, bom_item, scrap_qty=2, scrap_rate=0, process_loss_percentage=110
) )
# PL Item qty can't be >= FG Item qty # PL can't be > 100
self.assertRaises(frappe.ValidationError, bom_doc.submit) self.assertRaises(frappe.ValidationError, bom_doc.submit)
bom_doc = create_bom_with_process_loss_item( bom_doc = create_bom_with_process_loss_item(fg_item_whole, bom_item, process_loss_percentage=20)
fg_item_non_whole, bom_item, scrap_qty=1, scrap_rate=100
)
# PL Item rate has to be 0
self.assertRaises(frappe.ValidationError, bom_doc.submit)
bom_doc = create_bom_with_process_loss_item(
fg_item_whole, bom_item, scrap_qty=0.25, scrap_rate=0
)
# Items with whole UOMs can't be PL Items # Items with whole UOMs can't be PL Items
self.assertRaises(frappe.ValidationError, bom_doc.submit) self.assertRaises(frappe.ValidationError, bom_doc.submit)
bom_doc = create_bom_with_process_loss_item(
fg_item_non_whole, bom_item, scrap_qty=0.25, scrap_rate=0, is_process_loss=0
)
# FG Items in Scrap/Loss Table should have Is Process Loss set
self.assertRaises(frappe.ValidationError, bom_doc.submit)
def test_bom_item_query(self): def test_bom_item_query(self):
query = partial( query = partial(
item_query, item_query,
@ -744,7 +724,7 @@ def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=Non
def create_bom_with_process_loss_item( def create_bom_with_process_loss_item(
fg_item, bom_item, scrap_qty, scrap_rate, fg_qty=2, is_process_loss=1 fg_item, bom_item, scrap_qty=0, scrap_rate=0, fg_qty=2, process_loss_percentage=0
): ):
bom_doc = frappe.new_doc("BOM") bom_doc = frappe.new_doc("BOM")
bom_doc.item = fg_item.item_code bom_doc.item = fg_item.item_code
@ -759,19 +739,22 @@ def create_bom_with_process_loss_item(
"rate": 100.0, "rate": 100.0,
}, },
) )
bom_doc.append(
"scrap_items", if scrap_qty:
{ bom_doc.append(
"item_code": fg_item.item_code, "scrap_items",
"qty": scrap_qty, {
"stock_qty": scrap_qty, "item_code": fg_item.item_code,
"uom": fg_item.stock_uom, "qty": scrap_qty,
"stock_uom": fg_item.stock_uom, "stock_qty": scrap_qty,
"rate": scrap_rate, "uom": fg_item.stock_uom,
"is_process_loss": is_process_loss, "stock_uom": fg_item.stock_uom,
}, "rate": scrap_rate,
) },
)
bom_doc.currency = "INR" bom_doc.currency = "INR"
bom_doc.process_loss_percentage = process_loss_percentage
return bom_doc return bom_doc

View File

@ -8,7 +8,6 @@
"item_code", "item_code",
"column_break_2", "column_break_2",
"item_name", "item_name",
"is_process_loss",
"quantity_and_rate", "quantity_and_rate",
"stock_qty", "stock_qty",
"rate", "rate",
@ -89,17 +88,11 @@
{ {
"fieldname": "column_break_2", "fieldname": "column_break_2",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "is_process_loss",
"fieldtype": "Check",
"label": "Is Process Loss"
} }
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-06-22 16:46:12.153311", "modified": "2023-01-03 14:19:28.460965",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM Scrap Item", "name": "BOM Scrap Item",
@ -108,5 +101,6 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -846,20 +846,20 @@ class TestWorkOrder(FrappeTestCase):
create_process_loss_bom_items, create_process_loss_bom_items,
) )
qty = 4 qty = 10
scrap_qty = 0.25 # bom item qty = 1, consider as 25% of FG scrap_qty = 0.25 # bom item qty = 1, consider as 25% of FG
source_warehouse = "Stores - _TC" source_warehouse = "Stores - _TC"
wip_warehouse = "_Test Warehouse - _TC" wip_warehouse = "_Test Warehouse - _TC"
fg_item_non_whole, _, bom_item = create_process_loss_bom_items() fg_item_non_whole, _, bom_item = create_process_loss_bom_items()
test_stock_entry.make_stock_entry( test_stock_entry.make_stock_entry(
item_code=bom_item.item_code, target=source_warehouse, qty=4, basic_rate=100 item_code=bom_item.item_code, target=source_warehouse, qty=qty, basic_rate=100
) )
bom_no = f"BOM-{fg_item_non_whole.item_code}-001" bom_no = f"BOM-{fg_item_non_whole.item_code}-001"
if not frappe.db.exists("BOM", bom_no): if not frappe.db.exists("BOM", bom_no):
bom_doc = create_bom_with_process_loss_item( bom_doc = create_bom_with_process_loss_item(
fg_item_non_whole, bom_item, scrap_qty=scrap_qty, scrap_rate=0, fg_qty=1, is_process_loss=1 fg_item_non_whole, bom_item, fg_qty=1, process_loss_percentage=10
) )
bom_doc.submit() bom_doc.submit()
@ -883,19 +883,12 @@ class TestWorkOrder(FrappeTestCase):
# Testing stock entry values # Testing stock entry values
items = se.get("items") items = se.get("items")
self.assertEqual(len(items), 3, "There should be 3 items including process loss.") self.assertEqual(len(items), 2, "There should be 3 items including process loss.")
fg_item = items[1]
source_item, fg_item, pl_item = items self.assertEqual(fg_item.qty, qty - 1)
self.assertEqual(se.process_loss_percentage, 10)
total_pl_qty = qty * scrap_qty self.assertEqual(se.process_loss_qty, 1)
actual_fg_qty = qty - total_pl_qty
self.assertEqual(pl_item.qty, total_pl_qty)
self.assertEqual(fg_item.qty, actual_fg_qty)
# Testing Work Order values
self.assertEqual(frappe.db.get_value("Work Order", wo.name, "produced_qty"), qty)
self.assertEqual(frappe.db.get_value("Work Order", wo.name, "process_loss_qty"), total_pl_qty)
@timeout(seconds=60) @timeout(seconds=60)
def test_job_card_scrap_item(self): def test_job_card_scrap_item(self):

View File

@ -14,13 +14,13 @@
"item_name", "item_name",
"image", "image",
"bom_no", "bom_no",
"sales_order",
"column_break1", "column_break1",
"company", "company",
"qty", "qty",
"material_transferred_for_manufacturing", "material_transferred_for_manufacturing",
"produced_qty", "produced_qty",
"process_loss_qty", "process_loss_qty",
"sales_order",
"project", "project",
"serial_no_and_batch_for_finished_good_section", "serial_no_and_batch_for_finished_good_section",
"has_serial_no", "has_serial_no",
@ -28,6 +28,7 @@
"column_break_17", "column_break_17",
"serial_no", "serial_no",
"batch_size", "batch_size",
"work_order_configuration",
"settings_section", "settings_section",
"allow_alternative_item", "allow_alternative_item",
"use_multi_level_bom", "use_multi_level_bom",
@ -42,7 +43,11 @@
"fg_warehouse", "fg_warehouse",
"scrap_warehouse", "scrap_warehouse",
"required_items_section", "required_items_section",
"materials_and_operations_tab",
"required_items", "required_items",
"operations_section",
"operations",
"transfer_material_against",
"time", "time",
"planned_start_date", "planned_start_date",
"planned_end_date", "planned_end_date",
@ -51,9 +56,6 @@
"actual_start_date", "actual_start_date",
"actual_end_date", "actual_end_date",
"lead_time", "lead_time",
"operations_section",
"transfer_material_against",
"operations",
"section_break_22", "section_break_22",
"planned_operating_cost", "planned_operating_cost",
"actual_operating_cost", "actual_operating_cost",
@ -72,12 +74,14 @@
"production_plan_item", "production_plan_item",
"production_plan_sub_assembly_item", "production_plan_sub_assembly_item",
"product_bundle_item", "product_bundle_item",
"amended_from" "amended_from",
"connections_tab"
], ],
"fields": [ "fields": [
{ {
"fieldname": "item", "fieldname": "item",
"fieldtype": "Section Break", "fieldtype": "Tab Break",
"label": "Production Item",
"options": "fa fa-gift" "options": "fa fa-gift"
}, },
{ {
@ -236,7 +240,7 @@
{ {
"fieldname": "warehouses", "fieldname": "warehouses",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Warehouses", "label": "Warehouse",
"options": "fa fa-building" "options": "fa fa-building"
}, },
{ {
@ -390,8 +394,8 @@
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "more_info", "fieldname": "more_info",
"fieldtype": "Section Break", "fieldtype": "Tab Break",
"label": "More Information", "label": "More Info",
"options": "fa fa-file-text" "options": "fa fa-file-text"
}, },
{ {
@ -474,8 +478,7 @@
}, },
{ {
"fieldname": "settings_section", "fieldname": "settings_section",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"label": "Settings"
}, },
{ {
"fieldname": "column_break_18", "fieldname": "column_break_18",
@ -568,6 +571,22 @@
"no_copy": 1, "no_copy": 1,
"non_negative": 1, "non_negative": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "connections_tab",
"fieldtype": "Tab Break",
"label": "Connections",
"show_dashboard": 1
},
{
"fieldname": "work_order_configuration",
"fieldtype": "Tab Break",
"label": "Configuration"
},
{
"fieldname": "materials_and_operations_tab",
"fieldtype": "Tab Break",
"label": "Materials & Operations"
} }
], ],
"icon": "fa fa-cogs", "icon": "fa fa-cogs",
@ -575,7 +594,7 @@
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-01-24 21:18:12.160114", "modified": "2023-01-03 14:16:35.427731",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Work Order", "name": "Work Order",

View File

@ -285,14 +285,7 @@ class WorkOrder(Document):
): ):
continue continue
qty = flt( qty = self.get_transferred_or_manufactured_qty(purpose)
frappe.db.sql(
"""select sum(fg_completed_qty)
from `tabStock Entry` where work_order=%s and docstatus=1
and purpose=%s""",
(self.name, purpose),
)[0][0]
)
completed_qty = self.qty + (allowance_percentage / 100 * self.qty) completed_qty = self.qty + (allowance_percentage / 100 * self.qty)
if qty > completed_qty: if qty > completed_qty:
@ -314,26 +307,27 @@ class WorkOrder(Document):
if self.production_plan: if self.production_plan:
self.update_production_plan_status() self.update_production_plan_status()
def set_process_loss_qty(self): def get_transferred_or_manufactured_qty(self, purpose):
process_loss_qty = flt( table = frappe.qb.DocType("Stock Entry")
frappe.db.sql( query = (
""" frappe.qb.from_(table)
SELECT sum(qty) FROM `tabStock Entry Detail` .select(Sum(table.fg_completed_qty))
WHERE .where((table.work_order == self.name) & (table.docstatus == 1) & (table.purpose == purpose))
is_process_loss=1
AND parent IN (
SELECT name FROM `tabStock Entry`
WHERE
work_order=%s
AND purpose='Manufacture'
AND docstatus=1
)
""",
(self.name,),
)[0][0]
) )
if process_loss_qty is not None:
self.db_set("process_loss_qty", process_loss_qty) return flt(query.run()[0][0])
def set_process_loss_qty(self):
table = frappe.qb.DocType("Stock Entry")
process_loss_qty = (
frappe.qb.from_(table)
.select(Sum(table.process_loss_qty))
.where(
(table.work_order == self.name) & (table.purpose == "Manufacture") & (table.docstatus == 1)
)
).run()[0][0]
self.db_set("process_loss_qty", flt(process_loss_qty))
def update_production_plan_status(self): def update_production_plan_status(self):
production_plan = frappe.get_doc("Production Plan", self.production_plan) production_plan = frappe.get_doc("Production Plan", self.production_plan)

View File

@ -7,7 +7,7 @@
"document_type": "Document", "document_type": "Document",
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"items_section", "stock_entry_details_tab",
"naming_series", "naming_series",
"stock_entry_type", "stock_entry_type",
"outgoing_stock_entry", "outgoing_stock_entry",
@ -26,15 +26,20 @@
"posting_time", "posting_time",
"set_posting_time", "set_posting_time",
"inspection_required", "inspection_required",
"from_bom",
"apply_putaway_rule", "apply_putaway_rule",
"sb1", "items_tab",
"bom_no", "bom_info_section",
"fg_completed_qty", "from_bom",
"cb1",
"use_multi_level_bom", "use_multi_level_bom",
"bom_no",
"cb1",
"fg_completed_qty",
"get_items", "get_items",
"section_break_12", "section_break_7qsm",
"process_loss_percentage",
"column_break_e92r",
"process_loss_qty",
"section_break_jwgn",
"from_warehouse", "from_warehouse",
"source_warehouse_address", "source_warehouse_address",
"source_address_display", "source_address_display",
@ -44,6 +49,7 @@
"target_address_display", "target_address_display",
"sb0", "sb0",
"scan_barcode", "scan_barcode",
"items_section",
"items", "items",
"get_stock_and_rate", "get_stock_and_rate",
"section_break_19", "section_break_19",
@ -54,6 +60,7 @@
"additional_costs_section", "additional_costs_section",
"additional_costs", "additional_costs",
"total_additional_costs", "total_additional_costs",
"supplier_info_tab",
"contact_section", "contact_section",
"supplier", "supplier",
"supplier_name", "supplier_name",
@ -61,7 +68,7 @@
"address_display", "address_display",
"accounting_dimensions_section", "accounting_dimensions_section",
"project", "project",
"dimension_col_break", "other_info_tab",
"printing_settings", "printing_settings",
"select_print_heading", "select_print_heading",
"print_settings_col_break", "print_settings_col_break",
@ -78,11 +85,6 @@
"is_return" "is_return"
], ],
"fields": [ "fields": [
{
"fieldname": "items_section",
"fieldtype": "Section Break",
"oldfieldtype": "Section Break"
},
{ {
"fieldname": "naming_series", "fieldname": "naming_series",
"fieldtype": "Select", "fieldtype": "Select",
@ -236,17 +238,12 @@
}, },
{ {
"default": "0", "default": "0",
"depends_on": "eval:in_list([\"Material Issue\", \"Material Transfer\", \"Manufacture\", \"Repack\", \t\t\t\t\t\"Send to Subcontractor\", \"Material Transfer for Manufacture\", \"Material Consumption for Manufacture\"], doc.purpose)", "depends_on": "eval:in_list([\"Material Issue\", \"Material Transfer\", \"Manufacture\", \"Repack\", \"Send to Subcontractor\", \"Material Transfer for Manufacture\", \"Material Consumption for Manufacture\"], doc.purpose)",
"fieldname": "from_bom", "fieldname": "from_bom",
"fieldtype": "Check", "fieldtype": "Check",
"label": "From BOM", "label": "From BOM",
"print_hide": 1 "print_hide": 1
}, },
{
"depends_on": "eval: doc.from_bom && (doc.purpose!==\"Sales Return\" && doc.purpose!==\"Purchase Return\")",
"fieldname": "sb1",
"fieldtype": "Section Break"
},
{ {
"depends_on": "from_bom", "depends_on": "from_bom",
"fieldname": "bom_no", "fieldname": "bom_no",
@ -285,10 +282,6 @@
"oldfieldtype": "Button", "oldfieldtype": "Button",
"print_hide": 1 "print_hide": 1
}, },
{
"fieldname": "section_break_12",
"fieldtype": "Section Break"
},
{ {
"description": "Sets 'Source Warehouse' in each row of the items table.", "description": "Sets 'Source Warehouse' in each row of the items table.",
"fieldname": "from_warehouse", "fieldname": "from_warehouse",
@ -411,7 +404,7 @@
"collapsible": 1, "collapsible": 1,
"collapsible_depends_on": "total_additional_costs", "collapsible_depends_on": "total_additional_costs",
"fieldname": "additional_costs_section", "fieldname": "additional_costs_section",
"fieldtype": "Section Break", "fieldtype": "Tab Break",
"label": "Additional Costs" "label": "Additional Costs"
}, },
{ {
@ -576,13 +569,9 @@
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "accounting_dimensions_section", "fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break", "fieldtype": "Tab Break",
"label": "Accounting Dimensions" "label": "Accounting Dimensions"
}, },
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{ {
"fieldname": "pick_list", "fieldname": "pick_list",
"fieldtype": "Link", "fieldtype": "Link",
@ -621,6 +610,66 @@
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "items_tab",
"fieldtype": "Tab Break",
"label": "Items"
},
{
"fieldname": "bom_info_section",
"fieldtype": "Section Break",
"label": "BOM Info"
},
{
"collapsible": 1,
"fieldname": "section_break_jwgn",
"fieldtype": "Section Break",
"label": "Default Warehouse"
},
{
"fieldname": "other_info_tab",
"fieldtype": "Tab Break",
"label": "Other Info"
},
{
"fieldname": "supplier_info_tab",
"fieldtype": "Tab Break",
"label": "Supplier Info"
},
{
"fieldname": "stock_entry_details_tab",
"fieldtype": "Tab Break",
"label": "Details",
"oldfieldtype": "Section Break"
},
{
"fieldname": "section_break_7qsm",
"fieldtype": "Section Break"
},
{
"depends_on": "process_loss_percentage",
"fieldname": "process_loss_qty",
"fieldtype": "Float",
"label": "Process Loss Qty",
"read_only": 1
},
{
"fieldname": "column_break_e92r",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.from_bom && doc.fg_completed_qty",
"fetch_from": "bom_no.process_loss_percentage",
"fetch_if_empty": 1,
"fieldname": "process_loss_percentage",
"fieldtype": "Percent",
"label": "% Process Loss"
},
{
"fieldname": "items_section",
"fieldtype": "Section Break",
"label": "Items"
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
@ -628,7 +677,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-10-07 14:39:51.943770", "modified": "2023-01-03 16:02:50.741816",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Entry", "name": "Stock Entry",

View File

@ -113,6 +113,7 @@ class StockEntry(StockController):
self.validate_warehouse() self.validate_warehouse()
self.validate_work_order() self.validate_work_order()
self.validate_bom() self.validate_bom()
self.set_process_loss_qty()
self.validate_purchase_order() self.validate_purchase_order()
self.validate_subcontracting_order() self.validate_subcontracting_order()
@ -123,7 +124,7 @@ class StockEntry(StockController):
self.validate_with_material_request() self.validate_with_material_request()
self.validate_batch() self.validate_batch()
self.validate_inspection() self.validate_inspection()
# self.validate_fg_completed_qty() self.validate_fg_completed_qty()
self.validate_difference_account() self.validate_difference_account()
self.set_job_card_data() self.set_job_card_data()
self.set_purpose_for_stock_entry() self.set_purpose_for_stock_entry()
@ -385,11 +386,20 @@ class StockEntry(StockController):
item_wise_qty = {} item_wise_qty = {}
if self.purpose == "Manufacture" and self.work_order: if self.purpose == "Manufacture" and self.work_order:
for d in self.items: for d in self.items:
if d.is_finished_item or d.is_process_loss: if d.is_finished_item:
item_wise_qty.setdefault(d.item_code, []).append(d.qty) item_wise_qty.setdefault(d.item_code, []).append(d.qty)
precision = frappe.get_precision("Stock Entry Detail", "qty")
for item_code, qty_list in item_wise_qty.items(): for item_code, qty_list in item_wise_qty.items():
total = flt(sum(qty_list), frappe.get_precision("Stock Entry Detail", "qty")) total = flt(sum(qty_list), precision)
if (self.fg_completed_qty - total) > 0:
self.process_loss_qty = flt(self.fg_completed_qty - total, precision)
self.process_loss_percentage = flt(self.process_loss_qty * 100 / self.fg_completed_qty)
if self.process_loss_qty:
total += flt(self.process_loss_qty, precision)
if self.fg_completed_qty != total: if self.fg_completed_qty != total:
frappe.throw( frappe.throw(
_("The finished product {0} quantity {1} and For Quantity {2} cannot be different").format( _("The finished product {0} quantity {1} and For Quantity {2} cannot be different").format(
@ -468,7 +478,7 @@ class StockEntry(StockController):
if self.purpose == "Manufacture": if self.purpose == "Manufacture":
if validate_for_manufacture: if validate_for_manufacture:
if d.is_finished_item or d.is_scrap_item or d.is_process_loss: if d.is_finished_item or d.is_scrap_item:
d.s_warehouse = None d.s_warehouse = None
if not d.t_warehouse: if not d.t_warehouse:
frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx)) frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx))
@ -645,9 +655,7 @@ class StockEntry(StockController):
outgoing_items_cost = self.set_rate_for_outgoing_items( outgoing_items_cost = self.set_rate_for_outgoing_items(
reset_outgoing_rate, raise_error_if_no_rate reset_outgoing_rate, raise_error_if_no_rate
) )
finished_item_qty = sum( finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item)
d.transfer_qty for d in self.items if d.is_finished_item or d.is_process_loss
)
# Set basic rate for incoming items # Set basic rate for incoming items
for d in self.get("items"): for d in self.get("items"):
@ -686,8 +694,6 @@ class StockEntry(StockController):
# do not round off basic rate to avoid precision loss # do not round off basic rate to avoid precision loss
d.basic_rate = flt(d.basic_rate) d.basic_rate = flt(d.basic_rate)
if d.is_process_loss:
d.basic_rate = flt(0.0)
d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount")) d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
def set_rate_for_outgoing_items(self, reset_outgoing_rate=True, raise_error_if_no_rate=True): def set_rate_for_outgoing_items(self, reset_outgoing_rate=True, raise_error_if_no_rate=True):
@ -1466,11 +1472,11 @@ class StockEntry(StockController):
# add finished goods item # add finished goods item
if self.purpose in ("Manufacture", "Repack"): if self.purpose in ("Manufacture", "Repack"):
self.set_process_loss_qty()
self.load_items_from_bom() self.load_items_from_bom()
self.set_scrap_items() self.set_scrap_items()
self.set_actual_qty() self.set_actual_qty()
self.update_items_for_process_loss()
self.validate_customer_provided_item() self.validate_customer_provided_item()
self.calculate_rate_and_amount(raise_error_if_no_rate=False) self.calculate_rate_and_amount(raise_error_if_no_rate=False)
@ -1483,6 +1489,20 @@ class StockEntry(StockController):
self.add_to_stock_entry_detail(scrap_item_dict, bom_no=self.bom_no) self.add_to_stock_entry_detail(scrap_item_dict, bom_no=self.bom_no)
def set_process_loss_qty(self):
if self.purpose not in ("Manufacture", "Repack"):
return
self.process_loss_qty = 0.0
self.process_loss_percentage = frappe.get_cached_value(
"BOM", self.bom_no, "process_loss_percentage"
)
if self.process_loss_percentage:
self.process_loss_qty = flt(
(flt(self.fg_completed_qty) * flt(self.process_loss_percentage)) / 100
)
def set_work_order_details(self): def set_work_order_details(self):
if not getattr(self, "pro_doc", None): if not getattr(self, "pro_doc", None):
self.pro_doc = frappe._dict() self.pro_doc = frappe._dict()
@ -1515,7 +1535,7 @@ class StockEntry(StockController):
args = { args = {
"to_warehouse": to_warehouse, "to_warehouse": to_warehouse,
"from_warehouse": "", "from_warehouse": "",
"qty": self.fg_completed_qty, "qty": flt(self.fg_completed_qty) - flt(self.process_loss_qty),
"item_name": item.item_name, "item_name": item.item_name,
"description": item.description, "description": item.description,
"stock_uom": item.stock_uom, "stock_uom": item.stock_uom,
@ -1963,7 +1983,6 @@ class StockEntry(StockController):
) )
se_child.is_finished_item = item_row.get("is_finished_item", 0) 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.is_scrap_item = item_row.get("is_scrap_item", 0)
se_child.is_process_loss = item_row.get("is_process_loss", 0)
se_child.po_detail = item_row.get("po_detail") se_child.po_detail = item_row.get("po_detail")
se_child.sco_rm_detail = item_row.get("sco_rm_detail") se_child.sco_rm_detail = item_row.get("sco_rm_detail")
@ -2210,31 +2229,6 @@ class StockEntry(StockController):
material_requests.append(material_request) material_requests.append(material_request)
frappe.db.set_value("Material Request", material_request, "transfer_status", status) frappe.db.set_value("Material Request", material_request, "transfer_status", status)
def update_items_for_process_loss(self):
process_loss_dict = {}
for d in self.get("items"):
if not d.is_process_loss:
continue
scrap_warehouse = frappe.db.get_single_value(
"Manufacturing Settings", "default_scrap_warehouse"
)
if scrap_warehouse is not None:
d.t_warehouse = scrap_warehouse
d.is_scrap_item = 0
if d.item_code not in process_loss_dict:
process_loss_dict[d.item_code] = [flt(0), flt(0)]
process_loss_dict[d.item_code][0] += flt(d.transfer_qty)
process_loss_dict[d.item_code][1] += flt(d.qty)
for d in self.get("items"):
if not d.is_finished_item or d.item_code not in process_loss_dict:
continue
# Assumption: 1 finished item has 1 row.
d.transfer_qty -= process_loss_dict[d.item_code][0]
d.qty -= process_loss_dict[d.item_code][1]
def set_serial_no_batch_for_finished_good(self): def set_serial_no_batch_for_finished_good(self):
serial_nos = [] serial_nos = []
if self.pro_doc.serial_no: if self.pro_doc.serial_no:

View File

@ -20,7 +20,6 @@
"is_finished_item", "is_finished_item",
"is_scrap_item", "is_scrap_item",
"quality_inspection", "quality_inspection",
"is_process_loss",
"subcontracted_item", "subcontracted_item",
"section_break_8", "section_break_8",
"description", "description",
@ -559,12 +558,6 @@
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{
"default": "0",
"fieldname": "is_process_loss",
"fieldtype": "Check",
"label": "Is Process Loss"
},
{ {
"default": "0", "default": "0",
"depends_on": "barcode", "depends_on": "barcode",
@ -578,7 +571,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-11-02 13:00:34.258828", "modified": "2023-01-03 14:51:16.575515",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Entry Detail", "name": "Stock Entry Detail",