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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user