Merge pull request #27111 from 18alantom/feat-bom-process-loss-fp
feat: add provision for process loss in manufac (frontport #26151)
This commit is contained in:
commit
e11bfe7da4
@ -446,6 +446,11 @@ 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");
|
||||||
refresh_field("scrap_items");
|
refresh_field("scrap_items");
|
||||||
@ -655,3 +660,35 @@ frappe.ui.form.on("BOM", "with_operations", function(frm) {
|
|||||||
frm.set_value("operations", []);
|
frm.set_value("operations", []);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frappe.ui.form.on("BOM Scrap Item", {
|
||||||
|
item_code(frm, 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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function trigger_process_loss_qty_prompt(frm, cdt, cdn, item_code) {
|
||||||
|
frappe.prompt(
|
||||||
|
{
|
||||||
|
fieldname: "percent",
|
||||||
|
fieldtype: "Percent",
|
||||||
|
label: __("% Finished Item Quantity"),
|
||||||
|
description:
|
||||||
|
__("Set quantity of process loss item:") +
|
||||||
|
` ${item_code} ` +
|
||||||
|
__("as a percentage of finished item quantity"),
|
||||||
|
},
|
||||||
|
(data) => {
|
||||||
|
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");
|
||||||
|
},
|
||||||
|
__("Set Process Loss Item Quantity"),
|
||||||
|
__("Set Quantity")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -156,6 +156,7 @@ class BOM(WebsiteGenerator):
|
|||||||
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_bom_level()
|
self.set_bom_level()
|
||||||
|
self.validate_scrap_items()
|
||||||
|
|
||||||
def get_context(self, context):
|
def get_context(self, context):
|
||||||
context.parents = [{'name': 'boms', 'title': _('All BOMs') }]
|
context.parents = [{'name': 'boms', 'title': _('All BOMs') }]
|
||||||
@ -230,7 +231,7 @@ class BOM(WebsiteGenerator):
|
|||||||
}
|
}
|
||||||
ret = self.get_bom_material_detail(args)
|
ret = self.get_bom_material_detail(args)
|
||||||
for key, value in ret.items():
|
for key, value in ret.items():
|
||||||
if not item.get(key):
|
if item.get(key) is None:
|
||||||
item.set(key, value)
|
item.set(key, value)
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@ -705,6 +706,32 @@ class BOM(WebsiteGenerator):
|
|||||||
if update:
|
if update:
|
||||||
self.db_set("bom_level", self.bom_level)
|
self.db_set("bom_level", self.bom_level)
|
||||||
|
|
||||||
|
def validate_scrap_items(self):
|
||||||
|
for item in self.scrap_items:
|
||||||
|
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 item.is_process_loss and must_be_whole_number:
|
||||||
|
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):
|
||||||
|
msg = _("Scrap/Loss Item: {0} should have Qty less than finished goods Quantity.") \
|
||||||
|
.format(frappe.bold(item.item_code))
|
||||||
|
|
||||||
|
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):
|
||||||
if bom_doc.rm_cost_as_per == 'Valuation Rate':
|
if bom_doc.rm_cost_as_per == 'Valuation Rate':
|
||||||
rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1)
|
rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1)
|
||||||
@ -822,8 +849,11 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite
|
|||||||
|
|
||||||
items = frappe.db.sql(query, { "parent": bom, "qty": qty, "bom": bom, "company": company }, as_dict=True)
|
items = frappe.db.sql(query, { "parent": bom, "qty": qty, "bom": bom, "company": company }, as_dict=True)
|
||||||
elif fetch_scrap_items:
|
elif fetch_scrap_items:
|
||||||
query = query.format(table="BOM Scrap Item", where_conditions="",
|
query = query.format(
|
||||||
select_columns=", bom_item.idx, item.description", is_stock_item=is_stock_item, qty_field="stock_qty")
|
table="BOM Scrap Item", where_conditions="",
|
||||||
|
select_columns=", bom_item.idx, item.description, is_process_loss",
|
||||||
|
is_stock_item=is_stock_item, qty_field="stock_qty"
|
||||||
|
)
|
||||||
|
|
||||||
items = frappe.db.sql(query, { "qty": qty, "bom": bom, "company": company }, as_dict=True)
|
items = frappe.db.sql(query, { "qty": qty, "bom": bom, "company": company }, as_dict=True)
|
||||||
else:
|
else:
|
||||||
|
@ -280,13 +280,42 @@ class TestBOM(unittest.TestCase):
|
|||||||
self.assertEqual(reqd_item.qty, created_item.qty)
|
self.assertEqual(reqd_item.qty, created_item.qty)
|
||||||
self.assertEqual(reqd_item.exploded_qty, created_item.exploded_qty)
|
self.assertEqual(reqd_item.exploded_qty, created_item.exploded_qty)
|
||||||
|
|
||||||
|
def test_bom_with_process_loss_item(self):
|
||||||
|
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(
|
||||||
|
fg_item_non_whole, bom_item, scrap_qty=2, scrap_rate=0
|
||||||
|
)
|
||||||
|
# PL Item qty can't be >= FG Item qty
|
||||||
|
self.assertRaises(frappe.ValidationError, bom_doc.submit)
|
||||||
|
|
||||||
|
bom_doc = create_bom_with_process_loss_item(
|
||||||
|
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
|
||||||
|
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 get_default_bom(item_code="_Test FG Item 2"):
|
def get_default_bom(item_code="_Test FG Item 2"):
|
||||||
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
|
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def level_order_traversal(node):
|
def level_order_traversal(node):
|
||||||
traversal = []
|
traversal = []
|
||||||
q = deque()
|
q = deque()
|
||||||
@ -332,6 +361,7 @@ def create_nested_bom(tree, prefix="_Test bom "):
|
|||||||
bom = frappe.get_doc(doctype="BOM", item=bom_item_code)
|
bom = frappe.get_doc(doctype="BOM", item=bom_item_code)
|
||||||
for child_item in child_items.keys():
|
for child_item in child_items.keys():
|
||||||
bom.append("items", {"item_code": prefix + child_item})
|
bom.append("items", {"item_code": prefix + child_item})
|
||||||
|
bom.currency = "INR"
|
||||||
bom.insert()
|
bom.insert()
|
||||||
bom.submit()
|
bom.submit()
|
||||||
|
|
||||||
@ -353,3 +383,45 @@ def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=Non
|
|||||||
|
|
||||||
for warehouse in warehouse_list:
|
for warehouse in warehouse_list:
|
||||||
create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=qty, rate=rate)
|
create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=qty, rate=rate)
|
||||||
|
|
||||||
|
def create_bom_with_process_loss_item(
|
||||||
|
fg_item, bom_item, scrap_qty, scrap_rate, fg_qty=2, is_process_loss=1):
|
||||||
|
bom_doc = frappe.new_doc("BOM")
|
||||||
|
bom_doc.item = fg_item.item_code
|
||||||
|
bom_doc.quantity = fg_qty
|
||||||
|
bom_doc.append("items", {
|
||||||
|
"item_code": bom_item.item_code,
|
||||||
|
"qty": 1,
|
||||||
|
"uom": bom_item.stock_uom,
|
||||||
|
"stock_uom": bom_item.stock_uom,
|
||||||
|
"rate": 100.0
|
||||||
|
})
|
||||||
|
bom_doc.append("scrap_items", {
|
||||||
|
"item_code": fg_item.item_code,
|
||||||
|
"qty": scrap_qty,
|
||||||
|
"stock_qty": scrap_qty,
|
||||||
|
"uom": fg_item.stock_uom,
|
||||||
|
"stock_uom": fg_item.stock_uom,
|
||||||
|
"rate": scrap_rate,
|
||||||
|
"is_process_loss": is_process_loss
|
||||||
|
})
|
||||||
|
bom_doc.currency = "INR"
|
||||||
|
return bom_doc
|
||||||
|
|
||||||
|
def create_process_loss_bom_items():
|
||||||
|
item_list = [
|
||||||
|
("_Test Item - Non Whole UOM", "Kg"),
|
||||||
|
("_Test Item - Whole UOM", "Unit"),
|
||||||
|
("_Test PL BOM Item", "Unit")
|
||||||
|
]
|
||||||
|
return [create_process_loss_bom_item(it) for it in item_list]
|
||||||
|
|
||||||
|
def create_process_loss_bom_item(item_tuple):
|
||||||
|
item_code, stock_uom = item_tuple
|
||||||
|
if frappe.db.exists("Item", item_code) is None:
|
||||||
|
return make_item(
|
||||||
|
item_code,
|
||||||
|
{'stock_uom':stock_uom, 'valuation_rate':100}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return frappe.get_doc("Item", item_code)
|
||||||
|
@ -1,345 +1,112 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_guest_to_view": 0,
|
|
||||||
"allow_import": 0,
|
|
||||||
"allow_rename": 0,
|
|
||||||
"beta": 0,
|
|
||||||
"creation": "2016-09-26 02:19:21.642081",
|
"creation": "2016-09-26 02:19:21.642081",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "",
|
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"item_code",
|
||||||
|
"column_break_2",
|
||||||
|
"item_name",
|
||||||
|
"is_process_loss",
|
||||||
|
"quantity_and_rate",
|
||||||
|
"stock_qty",
|
||||||
|
"rate",
|
||||||
|
"amount",
|
||||||
|
"column_break_6",
|
||||||
|
"stock_uom",
|
||||||
|
"base_rate",
|
||||||
|
"base_amount"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "item_code",
|
"fieldname": "item_code",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Item Code",
|
"label": "Item Code",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Item",
|
"options": "Item",
|
||||||
"permlevel": 0,
|
"reqd": 1
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "item_name",
|
"fieldname": "item_name",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
"label": "Item Name"
|
||||||
"label": "Item Name",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "quantity_and_rate",
|
"fieldname": "quantity_and_rate",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"hidden": 0,
|
"label": "Quantity and Rate"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Quantity and Rate",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "stock_qty",
|
"fieldname": "stock_qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Qty",
|
"label": "Qty",
|
||||||
"length": 0,
|
"reqd": 1
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "rate",
|
"fieldname": "rate",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Rate",
|
"label": "Rate",
|
||||||
"length": 0,
|
"options": "currency"
|
||||||
"no_copy": 0,
|
|
||||||
"options": "currency",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "amount",
|
"fieldname": "amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Amount",
|
"label": "Amount",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "currency",
|
"options": "currency",
|
||||||
"permlevel": 0,
|
"read_only": 1
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 1,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "column_break_6",
|
"fieldname": "column_break_6",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "stock_uom",
|
"fieldname": "stock_uom",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Stock UOM",
|
"label": "Stock UOM",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "UOM",
|
"options": "UOM",
|
||||||
"permlevel": 0,
|
"read_only": 1
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 1,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "base_rate",
|
"fieldname": "base_rate",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Basic Rate (Company Currency)",
|
"label": "Basic Rate (Company Currency)",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Company:company:default_currency",
|
"options": "Company:company:default_currency",
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"print_hide_if_no_value": 0,
|
"read_only": 1
|
||||||
"read_only": 1,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "base_amount",
|
"fieldname": "base_amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Basic Amount (Company Currency)",
|
"label": "Basic Amount (Company Currency)",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Company:company:default_currency",
|
"options": "Company:company:default_currency",
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"print_hide_if_no_value": 0,
|
"read_only": 1
|
||||||
"read_only": 1,
|
},
|
||||||
"remember_last_selected_value": 0,
|
{
|
||||||
"report_hide": 0,
|
"fieldname": "column_break_2",
|
||||||
"reqd": 0,
|
"fieldtype": "Column Break"
|
||||||
"search_index": 0,
|
},
|
||||||
"set_only_once": 0,
|
{
|
||||||
"unique": 0
|
"default": "0",
|
||||||
|
"fieldname": "is_process_loss",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Process Loss"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
|
||||||
"hide_heading": 0,
|
|
||||||
"hide_toolbar": 0,
|
|
||||||
"idx": 0,
|
|
||||||
"image_view": 0,
|
|
||||||
"in_create": 0,
|
|
||||||
"is_submittable": 0,
|
|
||||||
"issingle": 0,
|
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"max_attachments": 0,
|
"links": [],
|
||||||
"modified": "2017-07-04 16:04:32.442287",
|
"modified": "2021-06-22 16:46:12.153311",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "BOM Scrap Item",
|
"name": "BOM Scrap Item",
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"show_name_in_global_search": 0,
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_changes": 1,
|
"track_changes": 1
|
||||||
"track_seen": 0
|
|
||||||
}
|
}
|
@ -690,6 +690,71 @@ class TestWorkOrder(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertRaises(frappe.ValidationError, make_stock_entry, wo.name, 'Material Transfer for Manufacture')
|
self.assertRaises(frappe.ValidationError, make_stock_entry, wo.name, 'Material Transfer for Manufacture')
|
||||||
|
|
||||||
|
def test_wo_completion_with_pl_bom(self):
|
||||||
|
from erpnext.manufacturing.doctype.bom.test_bom import create_process_loss_bom_items
|
||||||
|
from erpnext.manufacturing.doctype.bom.test_bom import create_bom_with_process_loss_item
|
||||||
|
|
||||||
|
qty = 4
|
||||||
|
scrap_qty = 0.25 # bom item qty = 1, consider as 25% of FG
|
||||||
|
source_warehouse = "Stores - _TC"
|
||||||
|
wip_warehouse = "_Test Warehouse - _TC"
|
||||||
|
fg_item_non_whole, _, bom_item = create_process_loss_bom_items()
|
||||||
|
|
||||||
|
test_stock_entry.make_stock_entry(item_code=bom_item.item_code,
|
||||||
|
target=source_warehouse, qty=4, basic_rate=100)
|
||||||
|
|
||||||
|
bom_no = f"BOM-{fg_item_non_whole.item_code}-001"
|
||||||
|
if not frappe.db.exists("BOM", bom_no):
|
||||||
|
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
|
||||||
|
)
|
||||||
|
bom_doc.submit()
|
||||||
|
|
||||||
|
wo = make_wo_order_test_record(
|
||||||
|
production_item=fg_item_non_whole.item_code,
|
||||||
|
bom_no=bom_no,
|
||||||
|
wip_warehouse=wip_warehouse,
|
||||||
|
qty=qty,
|
||||||
|
skip_transfer=1,
|
||||||
|
stock_uom=fg_item_non_whole.stock_uom,
|
||||||
|
)
|
||||||
|
|
||||||
|
se = frappe.get_doc(
|
||||||
|
make_stock_entry(wo.name, "Material Transfer for Manufacture", qty)
|
||||||
|
)
|
||||||
|
se.get("items")[0].s_warehouse = "Stores - _TC"
|
||||||
|
se.insert()
|
||||||
|
se.submit()
|
||||||
|
|
||||||
|
se = frappe.get_doc(
|
||||||
|
make_stock_entry(wo.name, "Manufacture", qty)
|
||||||
|
)
|
||||||
|
se.insert()
|
||||||
|
se.submit()
|
||||||
|
|
||||||
|
# Testing stock entry values
|
||||||
|
items = se.get("items")
|
||||||
|
self.assertEqual(len(items), 3, "There should be 3 items including process loss.")
|
||||||
|
|
||||||
|
source_item, fg_item, pl_item = items
|
||||||
|
|
||||||
|
total_pl_qty = qty * scrap_qty
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
def get_scrap_item_details(bom_no):
|
def get_scrap_item_details(bom_no):
|
||||||
scrap_items = {}
|
scrap_items = {}
|
||||||
for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item`
|
for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item`
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"qty",
|
"qty",
|
||||||
"material_transferred_for_manufacturing",
|
"material_transferred_for_manufacturing",
|
||||||
"produced_qty",
|
"produced_qty",
|
||||||
|
"process_loss_qty",
|
||||||
"sales_order",
|
"sales_order",
|
||||||
"project",
|
"project",
|
||||||
"serial_no_and_batch_for_finished_good_section",
|
"serial_no_and_batch_for_finished_good_section",
|
||||||
@ -64,16 +65,12 @@
|
|||||||
"description",
|
"description",
|
||||||
"stock_uom",
|
"stock_uom",
|
||||||
"column_break2",
|
"column_break2",
|
||||||
"references_section",
|
|
||||||
"material_request",
|
"material_request",
|
||||||
"material_request_item",
|
"material_request_item",
|
||||||
"sales_order_item",
|
"sales_order_item",
|
||||||
"column_break_61",
|
|
||||||
"production_plan",
|
"production_plan",
|
||||||
"production_plan_item",
|
"production_plan_item",
|
||||||
"production_plan_sub_assembly_item",
|
"production_plan_sub_assembly_item",
|
||||||
"parent_work_order",
|
|
||||||
"bom_level",
|
|
||||||
"product_bundle_item",
|
"product_bundle_item",
|
||||||
"amended_from"
|
"amended_from"
|
||||||
],
|
],
|
||||||
@ -553,20 +550,29 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "production_plan_sub_assembly_item",
|
"fieldname": "production_plan_sub_assembly_item",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Production Plan Sub-assembly Item",
|
"label": "Production Plan Sub-assembly Item",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval: doc.process_loss_qty",
|
||||||
|
"fieldname": "process_loss_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Process Loss Qty",
|
||||||
|
"no_copy": 1,
|
||||||
|
"non_negative": 1,
|
||||||
|
"read_only": 1
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-cogs",
|
"icon": "fa fa-cogs",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-06-28 16:19:14.902699",
|
"modified": "2021-08-24 15:14:03.844937",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Work Order",
|
"name": "Work Order",
|
||||||
|
@ -214,6 +214,7 @@ class WorkOrder(Document):
|
|||||||
self.meta.get_label(fieldname), qty, completed_qty, self.name), StockOverProductionError)
|
self.meta.get_label(fieldname), qty, completed_qty, self.name), StockOverProductionError)
|
||||||
|
|
||||||
self.db_set(fieldname, qty)
|
self.db_set(fieldname, qty)
|
||||||
|
self.set_process_loss_qty()
|
||||||
|
|
||||||
from erpnext.selling.doctype.sales_order.sales_order import update_produced_qty_in_so_item
|
from erpnext.selling.doctype.sales_order.sales_order import update_produced_qty_in_so_item
|
||||||
|
|
||||||
@ -223,6 +224,22 @@ 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):
|
||||||
|
process_loss_qty = flt(frappe.db.sql("""
|
||||||
|
SELECT sum(qty) FROM `tabStock Entry Detail`
|
||||||
|
WHERE
|
||||||
|
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)
|
||||||
|
|
||||||
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)
|
||||||
produced_qty = 0
|
produced_qty = 0
|
||||||
|
@ -272,7 +272,7 @@ 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:
|
if d.is_finished_item or d.is_process_loss:
|
||||||
item_wise_qty.setdefault(d.item_code, []).append(d.qty)
|
item_wise_qty.setdefault(d.item_code, []).append(d.qty)
|
||||||
|
|
||||||
for item_code, qty_list in iteritems(item_wise_qty):
|
for item_code, qty_list in iteritems(item_wise_qty):
|
||||||
@ -333,7 +333,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:
|
if d.is_finished_item or d.is_scrap_item or d.is_process_loss:
|
||||||
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))
|
||||||
@ -465,7 +465,7 @@ class StockEntry(StockController):
|
|||||||
"""
|
"""
|
||||||
# Set rate for outgoing items
|
# Set rate for outgoing items
|
||||||
outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate, raise_error_if_no_rate)
|
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)
|
finished_item_qty = sum(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'):
|
||||||
@ -486,6 +486,8 @@ class StockEntry(StockController):
|
|||||||
raise_error_if_no_rate=raise_error_if_no_rate)
|
raise_error_if_no_rate=raise_error_if_no_rate)
|
||||||
|
|
||||||
d.basic_rate = flt(d.basic_rate, d.precision("basic_rate"))
|
d.basic_rate = flt(d.basic_rate, d.precision("basic_rate"))
|
||||||
|
if d.is_process_loss:
|
||||||
|
d.basic_rate = flt(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):
|
||||||
@ -1043,6 +1045,7 @@ class StockEntry(StockController):
|
|||||||
|
|
||||||
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()
|
self.calculate_rate_and_amount()
|
||||||
|
|
||||||
@ -1400,6 +1403,7 @@ class StockEntry(StockController):
|
|||||||
get_default_cost_center(item_dict[d], company = self.company))
|
get_default_cost_center(item_dict[d], company = self.company))
|
||||||
se_child.is_finished_item = item_dict[d].get("is_finished_item", 0)
|
se_child.is_finished_item = item_dict[d].get("is_finished_item", 0)
|
||||||
se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0)
|
se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0)
|
||||||
|
se_child.is_process_loss = item_dict[d].get("is_process_loss", 0)
|
||||||
|
|
||||||
for field in ["idx", "po_detail", "original_item",
|
for field in ["idx", "po_detail", "original_item",
|
||||||
"expense_account", "description", "item_name", "serial_no", "batch_no"]:
|
"expense_account", "description", "item_name", "serial_no", "batch_no"]:
|
||||||
@ -1579,6 +1583,29 @@ 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):
|
||||||
args = {}
|
args = {}
|
||||||
if self.pro_doc.serial_no:
|
if self.pro_doc.serial_no:
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
QUnit.module('Stock');
|
||||||
|
|
||||||
|
QUnit.test("test manufacture from bom", function(assert) {
|
||||||
|
assert.expect(2);
|
||||||
|
let done = assert.async();
|
||||||
|
frappe.run_serially([
|
||||||
|
() => {
|
||||||
|
return frappe.tests.make("Stock Entry", [
|
||||||
|
{ purpose: "Manufacture" },
|
||||||
|
{ from_bom: 1 },
|
||||||
|
{ bom_no: "BOM-_Test Item - Non Whole UOM-001" },
|
||||||
|
{ fg_completed_qty: 2 }
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
() => cur_frm.save(),
|
||||||
|
() => frappe.click_button("Update Rate and Availability"),
|
||||||
|
() => {
|
||||||
|
assert.ok(cur_frm.doc.items[1] === 0.75, " Finished Item Qty correct");
|
||||||
|
assert.ok(cur_frm.doc.items[2] === 0.25, " Process Loss Item Qty correct");
|
||||||
|
},
|
||||||
|
() => frappe.tests.click_button('Submit'),
|
||||||
|
() => frappe.tests.click_button('Yes'),
|
||||||
|
() => frappe.timeout(0.3),
|
||||||
|
() => done()
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
@ -19,6 +19,7 @@
|
|||||||
"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",
|
||||||
@ -543,13 +544,19 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_process_loss",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Process Loss"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-06-21 16:03:18.834880",
|
"modified": "2021-06-22 16:47:11.268975",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Entry Detail",
|
"name": "Stock Entry Detail",
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
frappe.query_reports["Process Loss Report"] = {
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
label: __("Company"),
|
||||||
|
fieldname: "company",
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Company",
|
||||||
|
mandatory: true,
|
||||||
|
default: frappe.defaults.get_user_default("Company"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Item"),
|
||||||
|
fieldname: "item",
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Item",
|
||||||
|
mandatory: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Work Order"),
|
||||||
|
fieldname: "work_order",
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Work Order",
|
||||||
|
mandatory: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("From Date"),
|
||||||
|
fieldname: "from_date",
|
||||||
|
fieldtype: "Date",
|
||||||
|
mandatory: true,
|
||||||
|
default: frappe.datetime.year_start(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("To Date"),
|
||||||
|
fieldname: "to_date",
|
||||||
|
fieldtype: "Date",
|
||||||
|
mandatory: true,
|
||||||
|
default: frappe.datetime.get_today(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
};
|
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"add_total_row": 0,
|
||||||
|
"columns": [],
|
||||||
|
"creation": "2021-08-24 16:38:15.233395",
|
||||||
|
"disable_prepared_report": 0,
|
||||||
|
"disabled": 0,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Report",
|
||||||
|
"filters": [],
|
||||||
|
"idx": 0,
|
||||||
|
"is_standard": "Yes",
|
||||||
|
"modified": "2021-08-24 16:38:15.233395",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Stock",
|
||||||
|
"name": "Process Loss Report",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"prepared_report": 0,
|
||||||
|
"ref_doctype": "Work Order",
|
||||||
|
"report_name": "Process Loss Report",
|
||||||
|
"report_type": "Script Report",
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "Manufacturing User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Stock User"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
132
erpnext/stock/report/process_loss_report/process_loss_report.py
Normal file
132
erpnext/stock/report/process_loss_report/process_loss_report.py
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from typing import Dict, List, Tuple
|
||||||
|
|
||||||
|
Filters = frappe._dict
|
||||||
|
Row = frappe._dict
|
||||||
|
Data = List[Row]
|
||||||
|
Columns = List[Dict[str, str]]
|
||||||
|
QueryArgs = Dict[str, str]
|
||||||
|
|
||||||
|
def execute(filters: Filters) -> Tuple[Columns, Data]:
|
||||||
|
columns = get_columns()
|
||||||
|
data = get_data(filters)
|
||||||
|
return columns, data
|
||||||
|
|
||||||
|
def get_data(filters: Filters) -> Data:
|
||||||
|
query_args = get_query_args(filters)
|
||||||
|
data = run_query(query_args)
|
||||||
|
update_data_with_total_pl_value(data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_columns() -> Columns:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'label': 'Work Order',
|
||||||
|
'fieldname': 'name',
|
||||||
|
'fieldtype': 'Link',
|
||||||
|
'options': 'Work Order',
|
||||||
|
'width': '200'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': 'Item',
|
||||||
|
'fieldname': 'production_item',
|
||||||
|
'fieldtype': 'Link',
|
||||||
|
'options': 'Item',
|
||||||
|
'width': '100'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': 'Status',
|
||||||
|
'fieldname': 'status',
|
||||||
|
'fieldtype': 'Data',
|
||||||
|
'width': '100'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': 'Manufactured Qty',
|
||||||
|
'fieldname': 'produced_qty',
|
||||||
|
'fieldtype': 'Float',
|
||||||
|
'width': '150'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': 'Loss Qty',
|
||||||
|
'fieldname': 'process_loss_qty',
|
||||||
|
'fieldtype': 'Float',
|
||||||
|
'width': '150'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': 'Actual Manufactured Qty',
|
||||||
|
'fieldname': 'actual_produced_qty',
|
||||||
|
'fieldtype': 'Float',
|
||||||
|
'width': '150'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': 'Loss Value',
|
||||||
|
'fieldname': 'total_pl_value',
|
||||||
|
'fieldtype': 'Float',
|
||||||
|
'width': '150'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': 'FG Value',
|
||||||
|
'fieldname': 'total_fg_value',
|
||||||
|
'fieldtype': 'Float',
|
||||||
|
'width': '150'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': 'Raw Material Value',
|
||||||
|
'fieldname': 'total_rm_value',
|
||||||
|
'fieldtype': 'Float',
|
||||||
|
'width': '150'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_query_args(filters: Filters) -> QueryArgs:
|
||||||
|
query_args = {}
|
||||||
|
query_args.update(filters)
|
||||||
|
query_args.update(
|
||||||
|
get_filter_conditions(filters)
|
||||||
|
)
|
||||||
|
return query_args
|
||||||
|
|
||||||
|
def run_query(query_args: QueryArgs) -> Data:
|
||||||
|
return frappe.db.sql("""
|
||||||
|
SELECT
|
||||||
|
wo.name, wo.status, wo.production_item, wo.qty,
|
||||||
|
wo.produced_qty, wo.process_loss_qty,
|
||||||
|
(wo.produced_qty - wo.process_loss_qty) as actual_produced_qty,
|
||||||
|
sum(se.total_incoming_value) as total_fg_value,
|
||||||
|
sum(se.total_outgoing_value) as total_rm_value
|
||||||
|
FROM
|
||||||
|
`tabWork Order` wo INNER JOIN `tabStock Entry` se
|
||||||
|
ON wo.name=se.work_order
|
||||||
|
WHERE
|
||||||
|
process_loss_qty > 0
|
||||||
|
AND wo.company = %(company)s
|
||||||
|
AND se.docstatus = 1
|
||||||
|
AND se.posting_date BETWEEN %(from_date)s AND %(to_date)s
|
||||||
|
{item_filter}
|
||||||
|
{work_order_filter}
|
||||||
|
GROUP BY
|
||||||
|
se.work_order
|
||||||
|
""".format(**query_args), query_args, as_dict=1, debug=1)
|
||||||
|
|
||||||
|
def update_data_with_total_pl_value(data: Data) -> None:
|
||||||
|
for row in data:
|
||||||
|
value_per_unit_fg = row['total_fg_value'] / row['actual_produced_qty']
|
||||||
|
row['total_pl_value'] = row['process_loss_qty'] * value_per_unit_fg
|
||||||
|
|
||||||
|
def get_filter_conditions(filters: Filters) -> QueryArgs:
|
||||||
|
filter_conditions = dict(item_filter="", work_order_filter="")
|
||||||
|
if "item" in filters:
|
||||||
|
production_item = filters.get("item")
|
||||||
|
filter_conditions.update(
|
||||||
|
{"item_filter": f"AND wo.production_item='{production_item}'"}
|
||||||
|
)
|
||||||
|
if "work_order" in filters:
|
||||||
|
work_order_name = filters.get("work_order")
|
||||||
|
filter_conditions.update(
|
||||||
|
{"work_order_filter": f"AND wo.name='{work_order_name}'"}
|
||||||
|
)
|
||||||
|
return filter_conditions
|
||||||
|
|
Loading…
Reference in New Issue
Block a user