Skip to content

Commit

Permalink
feat: provision to return non consumed components against the work order
Browse files Browse the repository at this point in the history
(cherry picked from commit d59ed24)
  • Loading branch information
rohitwaghchaure authored and mergify[bot] committed Oct 10, 2022
1 parent 49cedca commit d0f3818
Show file tree
Hide file tree
Showing 9 changed files with 274 additions and 40 deletions.
72 changes: 72 additions & 0 deletions erpnext/manufacturing/doctype/work_order/test_work_order.py
Expand Up @@ -17,6 +17,7 @@
close_work_order,
make_job_card,
make_stock_entry,
make_stock_return_entry,
stop_unstop,
)
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
Expand Down Expand Up @@ -1408,6 +1409,77 @@ def test_backflushed_serial_no_batch_raw_materials_based_on_transferred(self):
)
self.assertEqual(manufacture_ste_doc2.items[1].qty, 1)

def test_non_consumed_material_return_against_work_order(self):
frappe.db.set_value(
"Manufacturing Settings",
None,
"backflush_raw_materials_based_on",
"Material Transferred for Manufacture",
)

item = make_item(
"Test FG Item To Test Return Case",
{
"is_stock_item": 1,
},
)

item_code = item.name
bom_doc = make_bom(
item=item_code,
source_warehouse="Stores - _TC",
raw_materials=["Test Batch MCC Keyboard", "Test Serial No BTT Headphone"],
)

# Create a work order
wo_doc = make_wo_order_test_record(production_item=item_code, qty=5)
wo_doc.save()

self.assertEqual(wo_doc.bom_no, bom_doc.name)

# Transfer material for manufacture
ste_doc = frappe.get_doc(make_stock_entry(wo_doc.name, "Material Transfer for Manufacture", 5))
for row in ste_doc.items:
row.qty += 2
row.transfer_qty += 2
nste_doc = test_stock_entry.make_stock_entry(
item_code=row.item_code, target="Stores - _TC", qty=row.qty, basic_rate=100
)

row.batch_no = nste_doc.items[0].batch_no
row.serial_no = nste_doc.items[0].serial_no

ste_doc.save()
ste_doc.submit()
ste_doc.load_from_db()

# Create a stock entry to manufacture the item
ste_doc = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 5))
for row in ste_doc.items:
if row.s_warehouse and not row.t_warehouse:
row.qty -= 2
row.transfer_qty -= 2

if row.serial_no:
serial_nos = get_serial_nos(row.serial_no)
row.serial_no = "\n".join(serial_nos[0:5])

ste_doc.save()
ste_doc.submit()

wo_doc.load_from_db()
for row in wo_doc.required_items:
self.assertEqual(row.transferred_qty, 7)
self.assertEqual(row.consumed_qty, 5)

self.assertEqual(wo_doc.status, "Completed")
return_ste_doc = make_stock_return_entry(wo_doc.name)
return_ste_doc.save()

self.assertTrue(return_ste_doc.is_return)
for row in return_ste_doc.items:
self.assertEqual(row.qty, 2)


def prepare_data_for_backflush_based_on_materials_transferred():
batch_item_doc = make_item(
Expand Down
36 changes: 35 additions & 1 deletion erpnext/manufacturing/doctype/work_order/work_order.js
Expand Up @@ -180,6 +180,37 @@ frappe.ui.form.on("Work Order", {
frm.trigger("make_bom");
});
}

frm.trigger("add_custom_button_to_return_components");
},

add_custom_button_to_return_components: function(frm) {
if (frm.doc.docstatus === 1 && in_list(["Closed", "Completed"], frm.doc.status)) {
let non_consumed_items = frm.doc.required_items.filter(d =>{
return flt(d.consumed_qty) < flt(d.transferred_qty - d.returned_qty)
});

if (non_consumed_items && non_consumed_items.length) {
frm.add_custom_button(__("Return Components"), function() {
frm.trigger("create_stock_return_entry");
}).addClass("btn-primary");
}
}
},

create_stock_return_entry: function(frm) {
frappe.call({
method: "erpnext.manufacturing.doctype.work_order.work_order.make_stock_return_entry",
args: {
"work_order": frm.doc.name,
},
callback: function(r) {
if(!r.exc) {
let doc = frappe.model.sync(r.message);
frappe.set_route("Form", doc[0].doctype, doc[0].name);
}
}
});
},

make_job_card: function(frm) {
Expand Down Expand Up @@ -517,15 +548,18 @@ frappe.ui.form.on("Work Order Operation", {
erpnext.work_order = {
set_custom_buttons: function(frm) {
var doc = frm.doc;
if (doc.docstatus === 1 && doc.status != "Closed") {

if (doc.status !== "Closed") {
frm.add_custom_button(__('Close'), function() {
frappe.confirm(__("Once the Work Order is Closed. It can't be resumed."),
() => {
erpnext.work_order.change_work_order_status(frm, "Closed");
}
);
}, __("Status"));
}

if (doc.docstatus === 1 && !in_list(["Closed", "Completed"], doc.status)) {
if (doc.status != 'Stopped' && doc.status != 'Completed') {
frm.add_custom_button(__('Stop'), function() {
erpnext.work_order.change_work_order_status(frm, "Stopped");
Expand Down
93 changes: 78 additions & 15 deletions erpnext/manufacturing/doctype/work_order/work_order.py
Expand Up @@ -20,6 +20,7 @@
nowdate,
time_diff_in_hours,
)
from pypika import functions as fn

from erpnext.manufacturing.doctype.bom.bom import (
get_bom_item_rate,
Expand Down Expand Up @@ -859,6 +860,7 @@ def update_required_items(self):
if self.docstatus == 1:
# calculate transferred qty based on submitted stock entries
self.update_transferred_qty_for_required_items()
self.update_returned_qty()

# update in bin
self.update_reserved_qty_for_production()
Expand Down Expand Up @@ -930,23 +932,62 @@ def set_required_items(self, reset_only_qty=False):
self.set_available_qty()

def update_transferred_qty_for_required_items(self):
"""update transferred qty from submitted stock entries for that item against
the work order"""
ste = frappe.qb.DocType("Stock Entry")
ste_child = frappe.qb.DocType("Stock Entry Detail")

query = (
frappe.qb.from_(ste)
.inner_join(ste_child)
.on((ste_child.parent == ste.name))
.select(
ste_child.item_code,
ste_child.original_item,
fn.Sum(ste_child.qty).as_("qty"),
)
.where(
(ste.docstatus == 1)
& (ste.work_order == self.name)
& (ste.purpose == "Material Transfer for Manufacture")
& (ste.is_return == 0)
)
.groupby(ste_child.item_code)
)

for d in self.required_items:
transferred_qty = frappe.db.sql(
"""select sum(qty)
from `tabStock Entry` entry, `tabStock Entry Detail` detail
where
entry.work_order = %(name)s
and entry.purpose = 'Material Transfer for Manufacture'
and entry.docstatus = 1
and detail.parent = entry.name
and (detail.item_code = %(item)s or detail.original_item = %(item)s)""",
{"name": self.name, "item": d.item_code},
)[0][0]
data = query.run(as_dict=1) or []
transferred_items = frappe._dict({d.original_item or d.item_code: d.qty for d in data})

for row in self.required_items:
row.db_set(
"transferred_qty", (transferred_items.get(row.item_code) or 0.0), update_modified=False
)

def update_returned_qty(self):
ste = frappe.qb.DocType("Stock Entry")
ste_child = frappe.qb.DocType("Stock Entry Detail")

query = (
frappe.qb.from_(ste)
.inner_join(ste_child)
.on((ste_child.parent == ste.name))
.select(
ste_child.item_code,
ste_child.original_item,
fn.Sum(ste_child.qty).as_("qty"),
)
.where(
(ste.docstatus == 1)
& (ste.work_order == self.name)
& (ste.purpose == "Material Transfer for Manufacture")
& (ste.is_return == 1)
)
.groupby(ste_child.item_code)
)

d.db_set("transferred_qty", flt(transferred_qty), update_modified=False)
data = query.run(as_dict=1) or []
returned_dict = frappe._dict({d.original_item or d.item_code: d.qty for d in data})

for row in self.required_items:
row.db_set("returned_qty", (returned_dict.get(row.item_code) or 0.0), update_modified=False)

def update_consumed_qty_for_required_items(self):
"""
Expand Down Expand Up @@ -1470,3 +1511,25 @@ def get_reserved_qty_for_production(item_code: str, warehouse: str) -> float:
)
)
).run()[0][0] or 0.0


@frappe.whitelist()
def make_stock_return_entry(work_order):
from erpnext.stock.doctype.stock_entry.stock_entry import get_available_materials

non_consumed_items = get_available_materials(work_order)
if not non_consumed_items:
return

wo_doc = frappe.get_cached_doc("Work Order", work_order)

stock_entry = frappe.new_doc("Stock Entry")
stock_entry.from_bom = 1
stock_entry.is_return = 1
stock_entry.work_order = work_order
stock_entry.purpose = "Material Transfer for Manufacture"
stock_entry.bom_no = wo_doc.bom_no
stock_entry.add_transfered_raw_materials_in_items()
stock_entry.set_stock_entry_type()

return stock_entry
Expand Up @@ -20,6 +20,7 @@
"column_break_11",
"transferred_qty",
"consumed_qty",
"returned_qty",
"available_qty_at_source_warehouse",
"available_qty_at_wip_warehouse"
],
Expand Down Expand Up @@ -97,6 +98,7 @@
"fieldtype": "Column Break"
},
{
"columns": 1,
"depends_on": "eval:!parent.skip_transfer",
"fieldname": "consumed_qty",
"fieldtype": "Float",
Expand Down Expand Up @@ -127,11 +129,19 @@
"fieldtype": "Currency",
"label": "Amount",
"read_only": 1
},
{
"columns": 1,
"fieldname": "returned_qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Returned Qty ",
"read_only": 1
}
],
"istable": 1,
"links": [],
"modified": "2020-04-13 18:46:32.966416",
"modified": "2022-09-28 10:50:43.512562",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order Item",
Expand All @@ -140,5 +150,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
Expand Up @@ -50,7 +50,7 @@ frappe.query_reports["Work Order Consumed Materials"] = {
label: __("Status"),
fieldname: "status",
fieldtype: "Select",
options: ["In Process", "Completed", "Stopped"]
options: ["", "In Process", "Completed", "Stopped"]
},
{
label: __("Excess Materials Consumed"),
Expand Down
@@ -1,6 +1,8 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt

from collections import defaultdict

import frappe
from frappe import _

Expand All @@ -18,7 +20,11 @@ def get_data(report_filters):
filters = get_filter_condition(report_filters)

wo_items = {}
for d in frappe.get_all("Work Order", filters=filters, fields=fields):

work_orders = frappe.get_all("Work Order", filters=filters, fields=fields)
returned_materials = get_returned_materials(work_orders)

for d in work_orders:
d.extra_consumed_qty = 0.0
if d.consumed_qty and d.consumed_qty > d.required_qty:
d.extra_consumed_qty = d.consumed_qty - d.required_qty
Expand All @@ -39,6 +45,28 @@ def get_data(report_filters):
return data


def get_returned_materials(work_orders):
raw_materials_qty = defaultdict(float)

raw_materials = frappe.get_all(
"Stock Entry",
fields=["`tabStock Entry Detail`.`item_code`", "`tabStock Entry Detail`.`qty`"],
filters=[
["Stock Entry", "is_return", "=", 1],
["Stock Entry Detail", "docstatus", "=", 1],
["Stock Entry", "work_order", "in", [d.name for d in work_orders]],
],
)

for d in raw_materials:
raw_materials_qty[d.item_code] += d.qty

for row in work_orders:
row.returned_qty = 0.0
if raw_materials_qty.get(row.raw_material_item_code):
row.returned_qty = raw_materials_qty.get(row.raw_material_item_code)


def get_fields():
return [
"`tabWork Order Item`.`parent`",
Expand All @@ -65,7 +93,7 @@ def get_filter_condition(report_filters):
for field in ["name", "production_item", "company", "status"]:
value = report_filters.get(field)
if value:
key = f"`{field}`"
key = f"{field}"
filters.update({key: value})

return filters
Expand Down Expand Up @@ -112,4 +140,10 @@ def get_columns():
"fieldtype": "Float",
"width": 100,
},
{
"label": _("Returned Qty"),
"fieldname": "returned_qty",
"fieldtype": "Float",
"width": 100,
},
]

0 comments on commit d0f3818

Please sign in to comment.