- Added a validation if invalid item code ia passed via data import/API, etc. - Used DB APIs instead of raw sql - Separated checks into separate functions - Added return types to functions - Better variable naming and removed redundant utils import in function
		
			
				
	
	
		
			151 lines
		
	
	
		
			4.3 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			151 lines
		
	
	
		
			4.3 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
 | |
| # License: GNU General Public License v3. See license.txt
 | |
| 
 | |
| 
 | |
| import json
 | |
| from typing import Dict
 | |
| 
 | |
| import frappe
 | |
| from frappe import _
 | |
| from frappe.utils import cint, cstr, flt, getdate
 | |
| 
 | |
| from erpnext.stock.doctype.item.item import get_last_purchase_details, validate_end_of_life
 | |
| 
 | |
| 
 | |
| def update_last_purchase_rate(doc, is_submit) -> None:
 | |
| 	"""updates last_purchase_rate in item table for each item"""
 | |
| 
 | |
| 	this_purchase_date = getdate(doc.get("posting_date") or doc.get("transaction_date"))
 | |
| 
 | |
| 	for d in doc.get("items"):
 | |
| 		# get last purchase details
 | |
| 		last_purchase_details = get_last_purchase_details(d.item_code, doc.name)
 | |
| 
 | |
| 		# compare last purchase date and this transaction's date
 | |
| 		last_purchase_rate = None
 | |
| 		if last_purchase_details and (
 | |
| 			doc.get("docstatus") == 2 or last_purchase_details.purchase_date > this_purchase_date
 | |
| 		):
 | |
| 			last_purchase_rate = last_purchase_details["base_net_rate"]
 | |
| 		elif is_submit == 1:
 | |
| 			# even if this transaction is the latest one, it should be submitted
 | |
| 			# for it to be considered for latest purchase rate
 | |
| 			if flt(d.conversion_factor):
 | |
| 				last_purchase_rate = flt(d.base_net_rate) / flt(d.conversion_factor)
 | |
| 			# Check if item code is present
 | |
| 			# Conversion factor should not be mandatory for non itemized items
 | |
| 			elif d.item_code:
 | |
| 				frappe.throw(_("UOM Conversion factor is required in row {0}").format(d.idx))
 | |
| 
 | |
| 		# update last purchsae rate
 | |
| 		frappe.db.set_value("Item", d.item_code, "last_purchase_rate", flt(last_purchase_rate))
 | |
| 
 | |
| 
 | |
| def validate_for_items(doc) -> None:
 | |
| 	items = []
 | |
| 	for d in doc.get("items"):
 | |
| 		if not d.qty:
 | |
| 			if doc.doctype == "Purchase Receipt" and d.rejected_qty:
 | |
| 				continue
 | |
| 			frappe.throw(_("Please enter quantity for Item {0}").format(d.item_code))
 | |
| 
 | |
| 		set_stock_levels(row=d)  # update with latest quantities
 | |
| 		item = validate_item_and_get_basic_data(row=d)
 | |
| 		validate_stock_item_warehouse(row=d, item=item)
 | |
| 		validate_end_of_life(d.item_code, item.end_of_life, item.disabled)
 | |
| 
 | |
| 		items.append(cstr(d.item_code))
 | |
| 
 | |
| 	if (
 | |
| 		items
 | |
| 		and len(items) != len(set(items))
 | |
| 		and not cint(frappe.db.get_single_value("Buying Settings", "allow_multiple_items") or 0)
 | |
| 	):
 | |
| 		frappe.throw(_("Same item cannot be entered multiple times."))
 | |
| 
 | |
| 
 | |
| def set_stock_levels(row) -> None:
 | |
| 	projected_qty = frappe.db.get_value(
 | |
| 		"Bin",
 | |
| 		{
 | |
| 			"item_code": row.item_code,
 | |
| 			"warehouse": row.warehouse,
 | |
| 		},
 | |
| 		"projected_qty",
 | |
| 	)
 | |
| 
 | |
| 	qty_data = {
 | |
| 		"projected_qty": flt(projected_qty),
 | |
| 		"ordered_qty": 0,
 | |
| 		"received_qty": 0,
 | |
| 	}
 | |
| 	if row.doctype in ("Purchase Receipt Item", "Purchase Invoice Item"):
 | |
| 		qty_data.pop("received_qty")
 | |
| 
 | |
| 	for field in qty_data:
 | |
| 		if row.meta.get_field(field):
 | |
| 			row.set(field, qty_data[field])
 | |
| 
 | |
| 
 | |
| def validate_item_and_get_basic_data(row) -> Dict:
 | |
| 	item = frappe.db.get_values(
 | |
| 		"Item",
 | |
| 		filters={"name": row.item_code},
 | |
| 		fieldname=["is_stock_item", "is_sub_contracted_item", "end_of_life", "disabled"],
 | |
| 		as_dict=1,
 | |
| 	)
 | |
| 	if not item:
 | |
| 		frappe.throw(_("Row #{0}: Item {1} does not exist").format(row.idx, frappe.bold(row.item_code)))
 | |
| 
 | |
| 	return item[0]
 | |
| 
 | |
| 
 | |
| def validate_stock_item_warehouse(row, item) -> None:
 | |
| 	if (
 | |
| 		item.is_stock_item == 1
 | |
| 		and row.qty
 | |
| 		and not row.warehouse
 | |
| 		and not row.get("delivered_by_supplier")
 | |
| 	):
 | |
| 		frappe.throw(
 | |
| 			_("Row #{1}: Warehouse is mandatory for stock Item {0}").format(
 | |
| 				frappe.bold(row.item_code), row.idx
 | |
| 			)
 | |
| 		)
 | |
| 
 | |
| 
 | |
| def check_on_hold_or_closed_status(doctype, docname) -> None:
 | |
| 	status = frappe.db.get_value(doctype, docname, "status")
 | |
| 
 | |
| 	if status in ("Closed", "On Hold"):
 | |
| 		frappe.throw(
 | |
| 			_("{0} {1} status is {2}").format(doctype, docname, status), frappe.InvalidStatusError
 | |
| 		)
 | |
| 
 | |
| 
 | |
| @frappe.whitelist()
 | |
| def get_linked_material_requests(items):
 | |
| 	items = json.loads(items)
 | |
| 	mr_list = []
 | |
| 	for item in items:
 | |
| 		material_request = frappe.db.sql(
 | |
| 			"""SELECT distinct mr.name AS mr_name,
 | |
| 				(mr_item.qty - mr_item.ordered_qty) AS qty,
 | |
| 				mr_item.item_code AS item_code,
 | |
| 				mr_item.name AS mr_item
 | |
| 			FROM `tabMaterial Request` mr, `tabMaterial Request Item` mr_item
 | |
| 			WHERE mr.name = mr_item.parent
 | |
| 				AND mr_item.item_code = %(item)s
 | |
| 				AND mr.material_request_type = 'Purchase'
 | |
| 				AND mr.per_ordered < 99.99
 | |
| 				AND mr.docstatus = 1
 | |
| 				AND mr.status != 'Stopped'
 | |
|                         ORDER BY mr_item.item_code ASC""",
 | |
| 			{"item": item},
 | |
| 			as_dict=1,
 | |
| 		)
 | |
| 		if material_request:
 | |
| 			mr_list.append(material_request)
 | |
| 
 | |
| 	return mr_list
 |