import frappe import re from frappe.utils import now, now_datetime def get_pricing_config(): if not frappe.db.exists("Pricing Score Config", "Pricing Score Config"): return None return frappe.get_doc("Pricing Score Config", "Pricing Score Config") def parse_ram(ram_str): if not ram_str: return 0 m = re.search(r'(\d+(?:\.\d+)?)', str(ram_str)) if m: return int(float(m.group(1))) return 0 def parse_cpu(cpu_str): if not cpu_str: return {"brand": "", "series": "", "gen": 0} cpu = str(cpu_str).lower() result = {"brand": "", "series": "", "gen": 0} if "intel" in cpu or "core" in cpu or "i3" in cpu or "i5" in cpu or "i7" in cpu or "i9" in cpu or "xeon" in cpu: result["brand"] = "intel" elif "ryzen" in cpu or "athlon" in cpu or "amd" in cpu: result["brand"] = "amd" elif "apple" in cpu or "m1" in cpu or "m2" in cpu or "m3" in cpu: result["brand"] = "apple" series_match = re.search(r'(i[3579])', cpu) if series_match: result["series"] = series_match.group(1) elif "xeon" in cpu: result["series"] = "xeon" elif "ryzen" in cpu: ryz_match = re.search(r'ryzen\s+(\d+)', cpu) if ryz_match: result["series"] = f"r{ryz_match.group(1)}" elif "m1" in cpu: result["series"] = "m1" elif "m2" in cpu: result["series"] = "m2" elif "m3" in cpu: result["series"] = "m3" gen_match = re.search(r'(?:i[3579]-|core\s+i[3579]\s+)(\d)', cpu) if gen_match: first_digit = int(gen_match.group(1)) if first_digit == 1: gen_match2 = re.search(r'(?:i[3579]-|core\s+i[3579]\s+)1(\d)', cpu) if gen_match2: result["gen"] = 10 + int(gen_match2.group(1)) else: result["gen"] = 1 else: result["gen"] = first_digit gen_match3 = re.search(r'(?:i[3579]-)(\d{4})', cpu) if gen_match3: model = gen_match3.group(1) first_digit = int(model[0]) if first_digit == 1: result["gen"] = 10 + int(model[1]) else: result["gen"] = first_digit return result @frappe.whitelist() def calculate_serial_score(serial_no): serial = frappe.get_doc("Serial No", serial_no) config = get_pricing_config() if not config: return {"error": "No Pricing Score Config found"} score = 0.0 details = [] cos_grade = serial.get("cosmetic_grade", "") if cos_grade: grade_match = re.search(r'[Cc](\d+)', str(cos_grade)) if grade_match: g = int(grade_match.group(1)) if g <= 2: return {"status": "scrap", "reason": f"Cosmetic C{g} - below minimum"} base = getattr(config, f"c{g}_base", 0) score += base details.append(f"Cosmetic C{g} = +{base}") cpu_info = parse_cpu(serial.get("processor", "")) if cpu_info["series"] in ("i7", "r7"): score += config.i7_bonus details.append(f"CPU {cpu_info['series'].upper()} = +{config.i7_bonus}") elif cpu_info["series"] in ("i9", "r9"): score += config.i9_bonus details.append(f"CPU {cpu_info['series'].upper()} = +{config.i9_bonus}") gen = cpu_info["gen"] gen_bonus = 0 if gen == 10: gen_bonus = config.gen_10_bonus elif gen == 11: gen_bonus = config.gen_11_bonus elif gen == 12: gen_bonus = config.gen_12_bonus elif gen == 13: gen_bonus = config.gen_13_bonus elif gen == 14: gen_bonus = config.gen_14_bonus if gen_bonus > 0: score += gen_bonus details.append(f"Gen {gen} = +{gen_bonus}") ram_gb = parse_ram(serial.get("ram", "")) if ram_gb >= 16: score += config.ram_16_bonus details.append(f"RAM {ram_gb}GB = +{config.ram_16_bonus}") if ram_gb >= 32: score += config.ram_32_bonus details.append(f"RAM {ram_gb}GB = +{config.ram_32_bonus}") if ram_gb > 32: details.append(f"WARNING: {ram_gb}GB exceeds laptop/desktop norm") if score >= config.medium_threshold: tier = "High" elif score >= config.low_threshold: tier = "Medium" else: tier = "Low" market = {"low": 0, "median": 0, "high": 0} age_days = 0 age_status = "unknown" if serial.item_code: item = frappe.get_doc("Item", serial.item_code) market = { "low": item.market_low or item.base_market_price or 0, "median": item.market_median or item.base_market_price or 0, "high": item.market_high or item.base_market_price or 0, } if item.market_last_priced: age_days = (now_datetime() - item.market_last_priced).days if age_days <= 30: age_status = "current" elif age_days <= 60: age_status = "stale" elif age_days <= 90: age_status = "aging" else: age_status = "expired" suggested_price = market["high"] if tier == "High" else market["median"] if tier == "Medium" else market["low"] serial.desirability_score = round(score, 1) serial.suggested_tier = tier serial.suggested_price = suggested_price serial.save() frappe.db.commit() return { "status": "ok", "serial_no": serial_no, "score": round(score, 1), "tier": tier, "details": details, "market_prices": market, "suggested_price": suggested_price, "age_days": age_days, "age_status": age_status, } @frappe.whitelist() def batch_calculate_scores(batch_size=100): serials = frappe.get_all("Serial No", filters={"cosmetic_grade": ["is", "set"]}, fields=["name"], limit=int(batch_size) ) results = {"updated": 0, "scrap": 0, "errors": 0} for s in serials: try: result = calculate_serial_score(s.name) if result.get("status") == "scrap": results["scrap"] += 1 else: results["updated"] += 1 except Exception as e: results["errors"] += 1 frappe.log_error(f"Score calc error for {s.name}: {e}") return results @frappe.whitelist() def get_sales_pricing_data(limit=50): """Get pricing data for Sales Manager page.""" config = get_pricing_config() if not config: return {"error": "No Pricing Score Config"} serials = frappe.get_all("Serial No", filters={"cosmetic_grade": ["is", "set"]}, fields=["name", "serial_no", "item_code", "item_name", "cosmetic_grade", "processor", "ram", "desirability_score", "suggested_tier", "suggested_price", "assigned_price", "pricing_status"], limit=int(limit), order_by="modified desc" ) results = [] for s in serials: age = {"days": 0, "status": "unknown", "color": "gray"} market = {"low": 0, "median": 0, "high": 0} if s.item_code: item = frappe.get_doc("Item", s.item_code) market = { "low": item.market_low or 0, "median": item.market_median or 0, "high": item.market_high or 0, } if item.market_last_priced: age_days = (now_datetime() - item.market_last_priced).days age["days"] = age_days if age_days <= 30: age["status"] = "current" age["color"] = "green" elif age_days <= 60: age["status"] = "stale" age["color"] = "yellow" elif age_days <= 90: age["status"] = "aging" age["color"] = "orange" else: age["status"] = "expired" age["color"] = "red" results.append({ "serial_no": s.name, "item_code": s.item_code, "item_name": s.item_name, "cosmetic_grade": s.cosmetic_grade, "processor": s.processor, "ram": s.ram, "score": s.desirability_score, "tier": s.suggested_tier, "market": market, "suggested_price": s.suggested_price, "assigned_price": s.assigned_price, "pricing_status": s.pricing_status, "age": age, }) return { "serials": results, "config": { "low_threshold": config.low_threshold, "medium_threshold": config.medium_threshold, } }