Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b6dc0ef
feat: add provision for process loss in manufac
18alantom Jun 22, 2021
984c97e
feat: add is process loss autoset and validation
18alantom Jun 23, 2021
3df8d0c
fix: add warehouse and unset is scrap for process loss items
18alantom Jun 23, 2021
7433b97
refactor: shift auto entry of is process loss check, update validations
18alantom Jun 24, 2021
8ecb146
test: add bom tests for process loss val, add se test for qty calc
18alantom Jun 25, 2021
cdf253a
fix: add more validations, remove source wh req for pl item
18alantom Jun 25, 2021
55acb2e
fix: sider
18alantom Jun 25, 2021
47a4a3d
refactor: polyfill ??
18alantom Jun 25, 2021
23ef51a
fix: sider
18alantom Jun 25, 2021
ad73d3f
refactor: validation error message formatting
18alantom Aug 9, 2021
2670adc
test: check manufacture completion qty in se and wo
18alantom Aug 10, 2021
cc177f3
fix: wo tests, sider, account for pl in se validation
18alantom Aug 10, 2021
7fb0817
fix: reword error messages, fix test values
18alantom Aug 17, 2021
b58853e
feat: add procss_loss_qty field in work order
18alantom Aug 24, 2021
8f73d58
feat: process loss report, fix set pl query condition
18alantom Aug 24, 2021
c3ce3f9
fix: remove spurious function 'toggle_operations'
18alantom Aug 24, 2021
aff9aee
Merge branch 'develop' into feat-bom-process-loss-fp
18alantom Aug 24, 2021
95a2565
fix: correct value in test
18alantom Aug 24, 2021
c7e11c8
fix: get filters to work
18alantom Aug 25, 2021
e20d219
Merge branch 'develop' into feat-bom-process-loss-fp
18alantom Aug 26, 2021
b389b8c
fix: prevent over riding scrap table values, name kwargs, set currency
18alantom Aug 26, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions erpnext/manufacturing/doctype/bom/bom.js
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,11 @@ var get_bom_material_detail = function(doc, cdt, cdn, scrap_items) {
},
callback: function(r) {
d = locals[cdt][cdn];
if (d.is_process_loss) {
r.message.rate = 0;
r.message.base_rate = 0;
}

$.extend(d, r.message);
refresh_field("items");
refresh_field("scrap_items");
Expand Down Expand Up @@ -655,3 +660,35 @@ frappe.ui.form.on("BOM", "with_operations", function(frm) {
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")
);
}
36 changes: 33 additions & 3 deletions erpnext/manufacturing/doctype/bom/bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ def validate(self):
self.update_stock_qty()
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False)
self.set_bom_level()
self.validate_scrap_items()

def get_context(self, context):
context.parents = [{'name': 'boms', 'title': _('All BOMs') }]
Expand Down Expand Up @@ -230,7 +231,7 @@ def set_bom_scrap_items_detail(self):
}
ret = self.get_bom_material_detail(args)
for key, value in ret.items():
if not item.get(key):
if item.get(key) is None:
item.set(key, value)

@frappe.whitelist()
Expand Down Expand Up @@ -705,6 +706,32 @@ def set_bom_level(self, update=False):
if update:
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):
if bom_doc.rm_cost_as_per == 'Valuation Rate':
rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1)
Expand Down Expand Up @@ -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)
elif fetch_scrap_items:
query = query.format(table="BOM Scrap Item", where_conditions="",
select_columns=", bom_item.idx, item.description", is_stock_item=is_stock_item, qty_field="stock_qty")
query = query.format(
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)
else:
Expand Down
76 changes: 74 additions & 2 deletions erpnext/manufacturing/doctype/bom/test_bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,12 +280,41 @@ def test_generated_variant_bom(self):
self.assertEqual(reqd_item.qty, created_item.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()

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})
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"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})

def level_order_traversal(node):
traversal = []
Expand Down Expand Up @@ -332,6 +361,7 @@ def dfs(tree, node):
bom = frappe.get_doc(doctype="BOM", item=bom_item_code)
for child_item in child_items.keys():
bom.append("items", {"item_code": prefix + child_item})
bom.currency = "INR"
bom.insert()
bom.submit()

Expand All @@ -353,3 +383,45 @@ def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=Non

for warehouse in warehouse_list:
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)
Loading