Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
867ed3a
feat: add provision for process loss in manufac
18alantom Jun 22, 2021
025f4b2
feat: add is process loss autoset and validation
18alantom Jun 23, 2021
8032095
fix: add warehouse and unset is scrap for process loss items
18alantom Jun 23, 2021
697a8bc
refactor: shift auto entry of is process loss check, update validations
18alantom Jun 24, 2021
f34f0a4
test: add bom tests for process loss val, add se test for qty calc
18alantom Jun 25, 2021
e957c02
fix: add more validations, remove source wh req for pl item
18alantom Jun 25, 2021
69d5e2a
fix: sider
18alantom Jun 25, 2021
edb38ad
Merge branch 'version-13-hotfix' into feat-bom-process-loss
18alantom Jun 25, 2021
e81a7cf
refactor: polyfill ??
18alantom Jun 25, 2021
2d13d5e
Merge branch 'feat-bom-process-loss' of https://github.com/18alantom/…
18alantom Jun 25, 2021
7ed8c8c
fix: sider
18alantom Jun 25, 2021
37a886a
Merge branch 'version-13-hotfix' into feat-bom-process-loss
18alantom Jul 21, 2021
ce44e11
refactor: validation error message formatting
18alantom Aug 9, 2021
0992b2e
test: check manufacture completion qty in se and wo
18alantom Aug 10, 2021
1c23544
fix: wo tests, sider, account for pl in se validation
18alantom Aug 10, 2021
f8a4752
fix: reword error messages, fix test values
18alantom Aug 17, 2021
a14b93d
feat: add procss_loss_qty field in work order
18alantom Aug 24, 2021
795efcd
feat: process loss report, fix set pl query condition
18alantom Aug 24, 2021
b58496d
Merge branch 'version-13-hotfix' into feat-bom-process-loss
18alantom Aug 24, 2021
9e96861
fix: correct value in test
18alantom Aug 24, 2021
8c4d80a
Merge branch 'feat-bom-process-loss' of https://github.com/18alantom/…
18alantom Aug 24, 2021
c703ce1
fix: get filters to work
18alantom Aug 25, 2021
466ff46
Merge branch 'version-13-hotfix' into feat-bom-process-loss
18alantom Aug 26, 2021
f7f573b
fix: prevent over riding scrap table values, name kwargs, set currency
18alantom Aug 26, 2021
8db4418
Merge branch 'version-13-hotfix' into feat-bom-process-loss
marination 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")
);
}
38 changes: 35 additions & 3 deletions erpnext/manufacturing/doctype/bom/bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,11 @@ def validate(self):
self.validate_operations()
self.calculate_cost()
self.update_stock_qty()
self.validate_scrap_items()
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False)
self.set_bom_level()


def get_context(self, context):
context.parents = [{'name': 'boms', 'title': _('All BOMs') }]

Expand Down Expand Up @@ -230,7 +232,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 @@ -687,6 +689,33 @@ def validate_operations(self):
if not d.batch_size or d.batch_size <= 0:
d.batch_size = 1


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_tree_representation(self) -> BOMTree:
"""Get a complete tree representation preserving order of child items."""
return BOMTree(self.name)
Expand Down Expand Up @@ -822,8 +851,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
81 changes: 77 additions & 4 deletions erpnext/manufacturing/doctype/bom/test_bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,40 @@ def test_subcontractor_sourced_item(self):
supplied_items = sorted([d.rm_item_code for d in po.supplied_items])
self.assertEqual(bom_items, supplied_items)


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 test_bom_tree_representation(self):
bom_tree = {
"Assembly": {
Expand All @@ -248,13 +282,9 @@ def test_bom_tree_representation(self):
for reqd_item, created_item in zip(reqd_order, created_order):
self.assertEqual(reqd_item, created_item.item_code)


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 = []
q = deque()
Expand Down Expand Up @@ -300,6 +330,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 @@ -321,3 +352,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