feat: provision to add scrap item in job card (#27483)
This commit is contained in:
parent
6e7945fbb7
commit
c5a77f60ed
@ -38,6 +38,8 @@
|
|||||||
"total_time_in_mins",
|
"total_time_in_mins",
|
||||||
"section_break_8",
|
"section_break_8",
|
||||||
"items",
|
"items",
|
||||||
|
"scrap_items_section",
|
||||||
|
"scrap_items",
|
||||||
"corrective_operation_section",
|
"corrective_operation_section",
|
||||||
"for_job_card",
|
"for_job_card",
|
||||||
"is_corrective_job_card",
|
"is_corrective_job_card",
|
||||||
@ -392,11 +394,24 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Batch No",
|
"label": "Batch No",
|
||||||
"options": "Batch"
|
"options": "Batch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "scrap_items_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Scrap Items"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "scrap_items",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Scrap Items",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "Job Card Scrap Item",
|
||||||
|
"print_hide": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-09-13 21:34:15.177928",
|
"modified": "2021-09-14 00:38:46.873105",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Job Card",
|
"name": "Job Card",
|
||||||
|
@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2021-09-14 00:30:28.533884",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"item_code",
|
||||||
|
"item_name",
|
||||||
|
"column_break_3",
|
||||||
|
"description",
|
||||||
|
"quantity_and_rate",
|
||||||
|
"stock_qty",
|
||||||
|
"column_break_6",
|
||||||
|
"stock_uom"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "item_code",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Scrap Item Code",
|
||||||
|
"options": "Item",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "item_code.item_name",
|
||||||
|
"fieldname": "item_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Scrap Item Name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_3",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "item_code.description",
|
||||||
|
"fieldname": "description",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Description",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "quantity_and_rate",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Quantity and Rate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "stock_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Qty",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_6",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "item_code.stock_uom",
|
||||||
|
"fieldname": "stock_uom",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Stock UOM",
|
||||||
|
"options": "UOM",
|
||||||
|
"read_only": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-09-14 01:20:48.588052",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Manufacturing",
|
||||||
|
"name": "Job Card Scrap Item",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"quick_entry": 1,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class JobCardScrapItem(Document):
|
||||||
|
pass
|
@ -404,6 +404,7 @@ def make_bom(**args):
|
|||||||
'uom': item_doc.stock_uom,
|
'uom': item_doc.stock_uom,
|
||||||
'stock_uom': item_doc.stock_uom,
|
'stock_uom': item_doc.stock_uom,
|
||||||
'rate': item_doc.valuation_rate or args.rate,
|
'rate': item_doc.valuation_rate or args.rate,
|
||||||
|
'source_warehouse': args.source_warehouse
|
||||||
})
|
})
|
||||||
|
|
||||||
if not args.do_not_save:
|
if not args.do_not_save:
|
||||||
|
@ -16,7 +16,7 @@ from erpnext.manufacturing.doctype.work_order.work_order import (
|
|||||||
stop_unstop,
|
stop_unstop,
|
||||||
)
|
)
|
||||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||||
from erpnext.stock.doctype.item.test_item import make_item
|
from erpnext.stock.doctype.item.test_item import create_item, make_item
|
||||||
from erpnext.stock.doctype.stock_entry import test_stock_entry
|
from erpnext.stock.doctype.stock_entry import test_stock_entry
|
||||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||||
from erpnext.stock.utils import get_bin
|
from erpnext.stock.utils import get_bin
|
||||||
@ -768,6 +768,60 @@ class TestWorkOrder(unittest.TestCase):
|
|||||||
total_pl_qty
|
total_pl_qty
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_job_card_scrap_item(self):
|
||||||
|
items = ['Test FG Item for Scrap Item Test', 'Test RM Item 1 for Scrap Item Test',
|
||||||
|
'Test RM Item 2 for Scrap Item Test']
|
||||||
|
|
||||||
|
company = '_Test Company with perpetual inventory'
|
||||||
|
for item_code in items:
|
||||||
|
create_item(item_code = item_code, is_stock_item = 1,
|
||||||
|
is_purchase_item=1, opening_stock=100, valuation_rate=10, company=company, warehouse='Stores - TCP1')
|
||||||
|
|
||||||
|
item = 'Test FG Item for Scrap Item Test'
|
||||||
|
raw_materials = ['Test RM Item 1 for Scrap Item Test', 'Test RM Item 2 for Scrap Item Test']
|
||||||
|
if not frappe.db.get_value('BOM', {'item': item}):
|
||||||
|
bom = make_bom(item=item, source_warehouse='Stores - TCP1', raw_materials=raw_materials, do_not_save=True)
|
||||||
|
bom.with_operations = 1
|
||||||
|
bom.append('operations', {
|
||||||
|
'operation': '_Test Operation 1',
|
||||||
|
'workstation': '_Test Workstation 1',
|
||||||
|
'hour_rate': 20,
|
||||||
|
'time_in_mins': 60
|
||||||
|
})
|
||||||
|
|
||||||
|
bom.submit()
|
||||||
|
|
||||||
|
wo_order = make_wo_order_test_record(item=item, company=company, planned_start_date=now(), qty=20, skip_transfer=1)
|
||||||
|
job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name}, 'name')
|
||||||
|
update_job_card(job_card)
|
||||||
|
|
||||||
|
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
|
||||||
|
for row in stock_entry.items:
|
||||||
|
if row.is_scrap_item:
|
||||||
|
self.assertEqual(row.qty, 1)
|
||||||
|
|
||||||
|
def update_job_card(job_card):
|
||||||
|
job_card_doc = frappe.get_doc('Job Card', job_card)
|
||||||
|
job_card_doc.set('scrap_items', [
|
||||||
|
{
|
||||||
|
'item_code': 'Test RM Item 1 for Scrap Item Test',
|
||||||
|
'stock_qty': 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'item_code': 'Test RM Item 2 for Scrap Item Test',
|
||||||
|
'stock_qty': 2
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
job_card_doc.append('time_logs', {
|
||||||
|
'from_time': now(),
|
||||||
|
'time_in_mins': 60,
|
||||||
|
'completed_qty': job_card_doc.for_quantity
|
||||||
|
})
|
||||||
|
|
||||||
|
job_card_doc.submit()
|
||||||
|
|
||||||
|
|
||||||
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`
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
@ -684,7 +685,7 @@ class StockEntry(StockController):
|
|||||||
|
|
||||||
def validate_bom(self):
|
def validate_bom(self):
|
||||||
for d in self.get('items'):
|
for d in self.get('items'):
|
||||||
if d.bom_no and (d.t_warehouse != getattr(self, "pro_doc", frappe._dict()).scrap_warehouse):
|
if d.bom_no and d.is_finished_item:
|
||||||
item_code = d.original_item or d.item_code
|
item_code = d.original_item or d.item_code
|
||||||
validate_bom_no(item_code, d.bom_no)
|
validate_bom_no(item_code, d.bom_no)
|
||||||
|
|
||||||
@ -1191,13 +1192,88 @@ class StockEntry(StockController):
|
|||||||
|
|
||||||
# item dict = { item_code: {qty, description, stock_uom} }
|
# item dict = { item_code: {qty, description, stock_uom} }
|
||||||
item_dict = get_bom_items_as_dict(self.bom_no, self.company, qty=qty,
|
item_dict = get_bom_items_as_dict(self.bom_no, self.company, qty=qty,
|
||||||
fetch_exploded = 0, fetch_scrap_items = 1)
|
fetch_exploded = 0, fetch_scrap_items = 1) or {}
|
||||||
|
|
||||||
for item in itervalues(item_dict):
|
for item in itervalues(item_dict):
|
||||||
item.from_warehouse = ""
|
item.from_warehouse = ""
|
||||||
item.is_scrap_item = 1
|
item.is_scrap_item = 1
|
||||||
|
|
||||||
|
for row in self.get_scrap_items_from_job_card():
|
||||||
|
if row.stock_qty <= 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
item_row = item_dict.get(row.item_code)
|
||||||
|
if not item_row:
|
||||||
|
item_row = frappe._dict({})
|
||||||
|
|
||||||
|
item_row.update({
|
||||||
|
'uom': row.stock_uom,
|
||||||
|
'from_warehouse': '',
|
||||||
|
'qty': row.stock_qty + flt(item_row.stock_qty),
|
||||||
|
'converison_factor': 1,
|
||||||
|
'is_scrap_item': 1,
|
||||||
|
'item_name': row.item_name,
|
||||||
|
'description': row.description,
|
||||||
|
'allow_zero_valuation_rate': 1
|
||||||
|
})
|
||||||
|
|
||||||
|
item_dict[row.item_code] = item_row
|
||||||
|
|
||||||
return item_dict
|
return item_dict
|
||||||
|
|
||||||
|
def get_scrap_items_from_job_card(self):
|
||||||
|
if not self.pro_doc:
|
||||||
|
self.set_work_order_details()
|
||||||
|
|
||||||
|
scrap_items = frappe.db.sql('''
|
||||||
|
SELECT
|
||||||
|
JCSI.item_code, JCSI.item_name, SUM(JCSI.stock_qty) as stock_qty, JCSI.stock_uom, JCSI.description
|
||||||
|
FROM
|
||||||
|
`tabJob Card` JC, `tabJob Card Scrap Item` JCSI
|
||||||
|
WHERE
|
||||||
|
JCSI.parent = JC.name AND JC.docstatus = 1
|
||||||
|
AND JCSI.item_code IS NOT NULL AND JC.work_order = %s
|
||||||
|
GROUP BY
|
||||||
|
JCSI.item_code
|
||||||
|
''', self.work_order, as_dict=1)
|
||||||
|
|
||||||
|
pending_qty = flt(self.pro_doc.qty) - flt(self.pro_doc.produced_qty)
|
||||||
|
if pending_qty <=0:
|
||||||
|
return []
|
||||||
|
|
||||||
|
used_scrap_items = self.get_used_scrap_items()
|
||||||
|
for row in scrap_items:
|
||||||
|
row.stock_qty -= flt(used_scrap_items.get(row.item_code))
|
||||||
|
row.stock_qty = (row.stock_qty) * flt(self.fg_completed_qty) / flt(pending_qty)
|
||||||
|
|
||||||
|
if used_scrap_items.get(row.item_code):
|
||||||
|
used_scrap_items[row.item_code] -= row.stock_qty
|
||||||
|
|
||||||
|
if cint(frappe.get_cached_value('UOM', row.stock_uom, 'must_be_whole_number')):
|
||||||
|
row.stock_qty = frappe.utils.ceil(row.stock_qty)
|
||||||
|
|
||||||
|
return scrap_items
|
||||||
|
|
||||||
|
def get_used_scrap_items(self):
|
||||||
|
used_scrap_items = defaultdict(float)
|
||||||
|
data = frappe.get_all(
|
||||||
|
'Stock Entry',
|
||||||
|
fields = [
|
||||||
|
'`tabStock Entry Detail`.`item_code`', '`tabStock Entry Detail`.`qty`'
|
||||||
|
],
|
||||||
|
filters = [
|
||||||
|
['Stock Entry', 'work_order', '=', self.work_order],
|
||||||
|
['Stock Entry Detail', 'is_scrap_item', '=', 1],
|
||||||
|
['Stock Entry', 'docstatus', '=', 1],
|
||||||
|
['Stock Entry', 'purpose', 'in', ['Repack', 'Manufacture']]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
for row in data:
|
||||||
|
used_scrap_items[row.item_code] += row.qty
|
||||||
|
|
||||||
|
return used_scrap_items
|
||||||
|
|
||||||
def get_unconsumed_raw_materials(self):
|
def get_unconsumed_raw_materials(self):
|
||||||
wo = frappe.get_doc("Work Order", self.work_order)
|
wo = frappe.get_doc("Work Order", self.work_order)
|
||||||
wo_items = frappe.get_all('Work Order Item',
|
wo_items = frappe.get_all('Work Order Item',
|
||||||
@ -1417,8 +1493,8 @@ class StockEntry(StockController):
|
|||||||
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)
|
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",
|
||||||
"expense_account", "description", "item_name", "serial_no", "batch_no"]:
|
"description", "item_name", "serial_no", "batch_no", "allow_zero_valuation_rate"]:
|
||||||
if item_dict[d].get(field):
|
if item_dict[d].get(field):
|
||||||
se_child.set(field, item_dict[d].get(field))
|
se_child.set(field, item_dict[d].get(field))
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user