diff --git a/westech_r2/__pycache__/hooks.cpython-312.pyc b/westech_r2/__pycache__/hooks.cpython-312.pyc index 9a036e6..0e87f95 100644 Binary files a/westech_r2/__pycache__/hooks.cpython-312.pyc and b/westech_r2/__pycache__/hooks.cpython-312.pyc differ diff --git a/westech_r2/api/__init__.py b/westech_r2/_archive_2level/api/__init__.py similarity index 100% rename from westech_r2/api/__init__.py rename to westech_r2/_archive_2level/api/__init__.py diff --git a/westech_r2/api/cor_generator.py b/westech_r2/_archive_2level/api/cor_generator.py similarity index 100% rename from westech_r2/api/cor_generator.py rename to westech_r2/_archive_2level/api/cor_generator.py diff --git a/westech_r2/api/ebay_pricing.py b/westech_r2/_archive_2level/api/ebay_pricing.py similarity index 100% rename from westech_r2/api/ebay_pricing.py rename to westech_r2/_archive_2level/api/ebay_pricing.py diff --git a/westech_r2/api/install_ssh.py b/westech_r2/_archive_2level/api/install_ssh.py similarity index 100% rename from westech_r2/api/install_ssh.py rename to westech_r2/_archive_2level/api/install_ssh.py diff --git a/westech_r2/api/optimize_routes.py b/westech_r2/_archive_2level/api/optimize_routes.py similarity index 100% rename from westech_r2/api/optimize_routes.py rename to westech_r2/_archive_2level/api/optimize_routes.py diff --git a/westech_r2/api/qa.py b/westech_r2/_archive_2level/api/qa.py similarity index 100% rename from westech_r2/api/qa.py rename to westech_r2/_archive_2level/api/qa.py diff --git a/westech_r2/_archive_2level/api/receiving_api.py b/westech_r2/_archive_2level/api/receiving_api.py new file mode 100644 index 0000000..68a6d75 --- /dev/null +++ b/westech_r2/_archive_2level/api/receiving_api.py @@ -0,0 +1,499 @@ +import json +import frappe +from frappe.utils import today, getdate, add_days +from datetime import timedelta + +@frappe.whitelist() +def get_pickups(date=None): + """Fetch scheduled pickups with optional date filter. + Returns pickups, calendar (next 30 days), and weekly chart data.""" + filters = [] + if date: + filters.append(["Scheduled Pickup", "pickup_date", "=", date]) + + fields = [ + "name", "pickup_date", "pickup_type", "status", "truck", "stop_order", + "customer_number", "company_name", + "contact_name", "contact_phone", "contact_email", + "address_line", "city", "state", "zip_code", + "latitude", "longitude", + "estimated_items", "estimated_weight", "load_contents", + "num_labels", "data_status", "red_r2", + "notes", "legacy_notes", "needs_aor", "needs_cod", + ] + + pickups = frappe.get_list("Scheduled Pickup", + fields=fields, + filters=filters if filters else None, + order_by="pickup_date asc, stop_order asc", + limit_page_length=500, + ) + + # Build calendar data (next 30 days) + from_date = getdate(today()) + to_date = add_days(from_date, 30) + + all_pickups = frappe.get_list("Scheduled Pickup", + fields=["pickup_date"], + filters=[["Scheduled Pickup", "pickup_date", ">=", str(from_date)], + ["Scheduled Pickup", "pickup_date", "<=", str(to_date)]], + limit_page_length=500, + ) + + pickup_counts = {} + for p in all_pickups: + d = p.get("pickup_date", "") + if d: + pickup_counts[d] = pickup_counts.get(d, 0) + 1 + + calendar = [] + for i in range(30): + d = add_days(from_date, i) + ds = str(d) + calendar.append({"date": ds, "count": pickup_counts.get(ds, 0)}) + + # Build weekly chart data (last 12 weeks) + weekly = [] + for i in range(11, -1, -1): + week_start = add_days(from_date, -(from_date.weekday() + 7 * i)) + week_end = add_days(week_start, 6) + count = 0 + for d_str, c in pickup_counts.items(): + try: + d = getdate(d_str) + if week_start <= d <= week_end: + count += c + except (ValueError, TypeError): + pass + weekly.append({"label": week_start.strftime("%m/%d"), "count": count}) + + return { + "pickups": pickups, + "calendar": calendar, + "weekly": weekly, + } + + +@frappe.whitelist() +def auto_route(date=None): + """Auto-assign pickups to trucks based on capacity and proximity.""" + if not date: + date = today() + + pickups = frappe.get_list("Scheduled Pickup", + filters={"pickup_date": date}, + fields=["name", "company_name", "estimated_items", "estimated_weight", + "latitude", "longitude", "pickup_type"], + limit_page_length=200, + ) + + if not pickups: + return {"success": True, "assigned": 0} + + trucks = ["Truck 1", "Truck 2", "Truck 3"] + sorted_p = sorted(pickups, key=lambda p: (float(p.get("latitude") or 0), float(p.get("longitude") or 0))) + n = len(sorted_p) + assigned = 0 + + for i, p in enumerate(sorted_p): + if p.get("pickup_type") == "Drop-off": + truck = "" + else: + truck = trucks[i % 3] if n <= 3 else trucks[min(i * 3 // n, 2)] + + doc = frappe.get_doc("Scheduled Pickup", p["name"]) + doc.truck = truck + doc.status = "Routed" if truck else "Scheduled" + doc.stop_order = i + 1 + doc.save() + assigned += 1 + + frappe.db.commit() + return {"success": True, "assigned": assigned} + + +@frappe.whitelist() +def get_checkins(): + """Fetch completed check-ins — returns Loads with their Pallets.""" + loads = frappe.get_list("Load", + fields=["name", "load_number", "incoming_date", "customer", "customer_name", + "total_devices", "total_weight", "data_status", "red_r2"], + order_by="incoming_date desc", + limit_page_length=100, + ) + + for load in loads: + pallets = frappe.get_list("Pallet", + filters={"load": load.name}, + fields=["name", "pallet_number", "received_date", "inbound_weight", + "total_items", "data_status", "red_r2", "description", "status"], + limit_page_length=50, + ) + load["pallets"] = pallets + load["pallet_count"] = len(pallets) + + return {"checkins": loads} + + +@frappe.whitelist() +def checkin_load(pickup_name, received_date, actual_pallets, total_weight, load_contents, data_status=None, red_r2=None): + """Check in a load: create Load + Pallets, mark pickup Complete.""" + # Get the pickup + pickup = frappe.get_doc("Scheduled Pickup", pickup_name) + + if not actual_pallets or int(actual_pallets) < 1: + frappe.throw("Actual pallet count must be at least 1") + + actual_pallets = int(actual_pallets) + + # Resolve customer - customer_number on pickup is a Link to Customer + customer_id = pickup.customer_number + if not customer_id or not frappe.db.exists("Customer", customer_id): + frappe.throw("Customer {} not found. Please verify the customer on the pickup.".format(customer_id)) + + # Generate Load name: MMDDYYYY-CustomerNumber format + from datetime import datetime + try: + dt = datetime.strptime(received_date, "%Y-%m-%d") + date_part = dt.strftime("%m%d%Y") + except (ValueError, TypeError): + date_part = received_date.replace("-", "") + + cust_num = customer_id + load_name = "{}-{}".format(date_part, cust_num) + + # Make unique if name already exists + base_name = load_name + counter = 1 + while frappe.db.exists("Load", load_name): + load_name = "{}-{}".format(base_name, counter) + counter += 1 + + # Create Load + load = frappe.get_doc({ + "doctype": "Load", + "name": load_name, + "load_number": load_name, + "incoming_date": received_date, + "customer": customer_id, + "customer_name": pickup.company_name or "", + "customer_number": customer_id, + "data_status": data_status or pickup.data_status or "", + "red_r2": red_r2 or pickup.red_r2 or "", + "total_weight": float(total_weight) if total_weight else 0, + "total_devices": actual_pallets, + "material_items": [{ + "material_type": "# Of Pallets", + "total_count": actual_pallets, + "weight": float(total_weight) if total_weight else 0, + "initial_data_status": data_status or pickup.data_status or "D0", + }], + }) + load.insert() + frappe.db.commit() + + # Create Pallets — autoname=pallet_number, so set name=pallet_number + for i in range(actual_pallets): + pallet_num = "{}-P{}".format(load_name, i + 1) + pallet = frappe.get_doc({ + "doctype": "Pallet", + "name": pallet_num, + "pallet_number": pallet_num, + "received_date": received_date, + "load": load.name, + "company_name": pickup.company_name or "", + "inbound_weight": str(round(float(total_weight) / actual_pallets, 1)) if total_weight and actual_pallets else "", + "description": load_contents or "", + "data_status": data_status or pickup.data_status or "", + "red_r2": red_r2 or pickup.red_r2 or "", + "contact_name": pickup.contact_name or "", + "contact_number": pickup.contact_phone or "", + "contact_email": pickup.contact_email or "", + "address_line": (pickup.address_line or "") + ((", " + pickup.city) if pickup.city else "") + ((", " + pickup.state) if pickup.state else ""), + "needs_aor": pickup.needs_aor or 0, + "needs_cod": pickup.needs_cod or 0, + "notes": pickup.notes or "", + "pickup": pickup.pickup_type or "", + "status": "Received", + }) + pallet.insert() + + frappe.db.commit() + + # Update pickup status + pickup.status = "Complete" + pickup.save() + frappe.db.commit() + + return { + "success": True, + "load": load.name, + "pallets_created": actual_pallets, + } + + +@frappe.whitelist() +def get_pickup_details(pickup_name): + """Get full details of a Scheduled Pickup for the check-in form.""" + pickup = frappe.get_doc("Scheduled Pickup", pickup_name) + return { + "name": pickup.name, + "pickup_date": pickup.pickup_date, + "pickup_type": pickup.pickup_type, + "customer_number": pickup.customer_number, + "company_name": pickup.company_name, + "contact_name": pickup.contact_name, + "contact_phone": pickup.contact_phone, + "contact_email": pickup.contact_email, + "address_line": pickup.address_line, + "city": pickup.city, + "state": pickup.state, + "zip_code": pickup.zip_code, + "estimated_items": pickup.estimated_items, + "estimated_weight": pickup.estimated_weight, + "data_status": pickup.data_status, + "red_r2": pickup.red_r2, + "needs_aor": pickup.needs_aor, + "needs_cod": pickup.needs_cod, + "notes": pickup.notes, + } + + +@frappe.whitelist() +def cor_report(): + """Generate Certificate of Recycling report.""" + pickups = frappe.get_list("Scheduled Pickup", + filters={"status": "Complete"}, + fields=["name", "pickup_date", "company_name", "customer_number", + "estimated_items", "estimated_weight", "load_contents", + "data_status", "red_r2", "needs_aor", "needs_cod"], + order_by="pickup_date desc", + limit_page_length=200, + ) + + html = "CoR Report" + html += "" + html += "

Certificate of Recycling (CoR) Report

" + html += "

Generated: " + frappe.utils.now() + "

" + html += "

Total completed loads: " + str(len(pickups)) + "

" + + if pickups: + html += "" + html += "" + for p in pickups: + html += "" + html += "" + html += "" + html += "" + html += "" + html += "" + html += "" + html += "" + html += "" + html += "
DateCustomerItemsWeightContentsData StatusRED/R2AoRCoD
" + str(p.get("pickup_date", "")) + "" + str(p.get("company_name", "")) + "" + str(p.get("estimated_items", "")) + "" + str(p.get("estimated_weight", "")) + "" + str(p.get("load_contents", "")) + "" + str(p.get("data_status", "")) + "" + str(p.get("red_r2", "")) + "" + ("✓" if p.get("needs_aor") else "") + "" + ("✓" if p.get("needs_cod") else "") + "
" + else: + html += "

No completed loads found.

" + + html += "" + frappe.local.response["type"] = "html" + frappe.local.response["page_content"] = html + + +@frappe.whitelist() +def print_route_sheet(date=None): + """Generate printable route sheet.""" + if not date: + date = today() + + filters = {"pickup_date": date} if date else {} + pickups = frappe.get_list("Scheduled Pickup", + filters=filters, + fields=["name", "pickup_date", "pickup_type", "status", "truck", "stop_order", + "company_name", "contact_name", "contact_phone", "contact_email", + "address_line", "city", "state", "zip_code", + "estimated_items", "estimated_weight", "load_contents", + "data_status", "red_r2", "needs_aor", "needs_cod", "notes"], + order_by="truck asc, stop_order asc", + limit_page_length=200, + ) + + trucks = {} + unassigned = [] + for p in pickups: + t = p.get("truck", "") + if t and t != "Unassigned": + trucks.setdefault(t, []).append(p) + else: + unassigned.append(p) + + html = "Route Sheet" + html += "" + html += "

Route Sheet — " + str(date or "Today") + "

" + + for truck_name, stops in sorted(trucks.items()): + html += '
🚛 ' + truck_name + " — " + str(len(stops)) + " stops
" + html += "" + html += "" + for i, s in enumerate(stops, 1): + addr = str(s.get("address_line", "")) + ", " + str(s.get("city", "")) + ", " + str(s.get("state", "")) + " " + str(s.get("zip_code", "")) + html += "" + html += "" + html += "" + html += "" + html += "" + html += "" + html += "" + html += "
#CustomerAddressContactItemsWeightDataRED/R2AoRCoDNotes
" + str(i) + "" + str(s.get("company_name", "")) + "" + addr + "" + str(s.get("contact_name", "")) + "
" + str(s.get("contact_phone", "")) + "
" + str(s.get("estimated_items", "")) + "" + str(s.get("estimated_weight", "")) + "" + str(s.get("data_status", "")) + "" + str(s.get("red_r2", "")) + "" + ("✓" if s.get("needs_aor") else "") + "" + ("✓" if s.get("needs_cod") else "") + "" + str(s.get("notes", "")) + "
" + + if unassigned: + html += "

Unassigned

" + for s in unassigned: + html += "" + html += "" + html += "" + html += "
CustomerAddressNotes
" + str(s.get("company_name", "")) + "" + str(s.get("address_line", "")) + "" + str(s.get("notes", "")) + "
" + + html += "" + frappe.local.response["type"] = "html" + frappe.local.response["page_content"] = html + + +@frappe.whitelist() +def print_green_sheet(date=None): + """Generate Green Sheet printout for each pallet. + Shows customer info, service level banner, driver instructions, RED LINE instructions.""" + if not date: + date = today() + + # Get completed loads for this date + loads = frappe.get_list("Load", + filters={"incoming_date": date}, + fields=["name", "load_number", "customer", "customer_name", + "incoming_date", "total_weight", "data_status", "red_r2"], + order_by="name asc", + limit_page_length=200, + ) + + html = "Green Sheets" + html += "" + + for load in loads: + pallets = frappe.get_list("Pallet", + filters={"load": load.name}, + fields=["name", "pallet_number", "inbound_weight", "total_items", + "data_status", "red_r2", "description", "needs_aor", "needs_cod", "notes", "status"], + limit_page_length=50, + ) + + for pallet in pallets: + html += '
' + html += '
🟢 GREEN SHEET — Data-Bearing Equipment Tracking
' + html += '
Pallet # ' + str(pallet.get("pallet_number", "")) + ' | Load # ' + str(load.get("name", "")) + ' | ' + str(load.get("incoming_date", "")) + '
' + + # Customer block + html += '
' + service_level = "" + if pallet.get("red_r2"): + service_level = pallet.get("red_r2", "") + html += '(' + str(load.get("customer", "")) + ') — ' + str(service_level) + ' — ' + str(load.get("customer_name", "")) + '' + html += '
' + + # Service level banner (only for RED/NIST) + rr = pallet.get("red_r2", "") + if rr and rr != "Neither": + html += '
SERVICE LEVEL: ' + str(rr).upper() + '
' + + # Driver instructions (notes) + if pallet.get("notes"): + html += '
Driver Instructions:
' + str(pallet.get("notes", "")) + '
' + + # RED LINE instructions (for RED/NIST) + if rr and rr not in ("", "Neither", "R2"): + html += '
⚠ RED LINE INSTRUCTIONS
All data-bearing equipment must be tracked. Destruction method per customer specification.
' + + # Pallet details table + html += '' + html += '' + html += '' + html += '' + html += '' + aor_cor = "" + if pallet.get("needs_aor"): aor_cor += "✓ AoR " + if pallet.get("needs_cod"): aor_cor += "✓ CoD" + html += '' + html += '
Pallet DesignationData Status
' + str(pallet.get("status", "Received")) + '' + str(pallet.get("data_status", "")) + '
Inbound WeightTotal Items
' + str(pallet.get("inbound_weight", "")) + '' + str(pallet.get("total_items", "")) + '
AoR/CoRContents
' + (aor_cor or "None") + '' + str(pallet.get("description", "")) + '
' + + # Material tracking (hand-write on paper) + html += '' + for _ in range(4): + html += '' + html += '
Material%WeightSign OffDate
 
' + + # R2 warning + html += '
⚠ R2 REQUIREMENT: This pallet contains data-bearing equipment. All devices must be tracked through erasure with 5% verification audit.
' + + # Signatures + html += '' + html += '
Received ByInspected ByVerified By
   
' + + # Footer + html += '' + html += '
' + + html += "" + frappe.local.response["type"] = "html" + frappe.local.response["page_content"] = html + + +@frappe.whitelist() +def print_labels(date=None): + """Generate printable labels.""" + if not date: + date = today() + + filters = {"pickup_date": date} if date else {} + pickups = frappe.get_list("Scheduled Pickup", + filters=filters, + fields=["name", "company_name", "pickup_date", "num_labels", "data_status", "red_r2"], + limit_page_length=200, + ) + + html = "Labels" + html += "" + + for p in pickups: + n = p.get("num_labels") or 1 + for _ in range(n): + html += '
' + html += '
' + str(p.get("company_name", "")) + '
' + html += '
' + str(p.get("pickup_date", "")) + '
' + html += '
' + str(p.get("data_status", "")) + " | " + str(p.get("red_r2", "")) + '
' + html += '
' + + html += "" + frappe.local.response["type"] = "html" + frappe.local.response["page_content"] = html \ No newline at end of file diff --git a/westech_r2/api/sales.py b/westech_r2/_archive_2level/api/sales.py similarity index 100% rename from westech_r2/api/sales.py rename to westech_r2/_archive_2level/api/sales.py diff --git a/westech_r2/api/scoring.py b/westech_r2/_archive_2level/api/scoring.py similarity index 100% rename from westech_r2/api/scoring.py rename to westech_r2/_archive_2level/api/scoring.py diff --git a/westech_r2/api/serial_hooks.py b/westech_r2/_archive_2level/api/serial_hooks.py similarity index 100% rename from westech_r2/api/serial_hooks.py rename to westech_r2/_archive_2level/api/serial_hooks.py diff --git a/westech_r2/api/service_invoice.py b/westech_r2/_archive_2level/api/service_invoice.py similarity index 100% rename from westech_r2/api/service_invoice.py rename to westech_r2/_archive_2level/api/service_invoice.py diff --git a/westech_r2/doctype/__init__.py b/westech_r2/_archive_2level/doctype/__init__.py similarity index 100% rename from westech_r2/doctype/__init__.py rename to westech_r2/_archive_2level/doctype/__init__.py diff --git a/westech_r2/doctype/customer_interaction/__init__.py b/westech_r2/_archive_2level/doctype/customer_interaction/__init__.py similarity index 100% rename from westech_r2/doctype/customer_interaction/__init__.py rename to westech_r2/_archive_2level/doctype/customer_interaction/__init__.py diff --git a/westech_r2/doctype/customer_interaction/customer_interaction.js b/westech_r2/_archive_2level/doctype/customer_interaction/customer_interaction.js similarity index 100% rename from westech_r2/doctype/customer_interaction/customer_interaction.js rename to westech_r2/_archive_2level/doctype/customer_interaction/customer_interaction.js diff --git a/westech_r2/doctype/customer_interaction/customer_interaction.json b/westech_r2/_archive_2level/doctype/customer_interaction/customer_interaction.json similarity index 100% rename from westech_r2/doctype/customer_interaction/customer_interaction.json rename to westech_r2/_archive_2level/doctype/customer_interaction/customer_interaction.json diff --git a/westech_r2/doctype/customer_interaction/customer_interaction.py b/westech_r2/_archive_2level/doctype/customer_interaction/customer_interaction.py similarity index 100% rename from westech_r2/doctype/customer_interaction/customer_interaction.py rename to westech_r2/_archive_2level/doctype/customer_interaction/customer_interaction.py diff --git a/westech_r2/doctype/customer_interaction/test_customer_interaction.py b/westech_r2/_archive_2level/doctype/customer_interaction/test_customer_interaction.py similarity index 100% rename from westech_r2/doctype/customer_interaction/test_customer_interaction.py rename to westech_r2/_archive_2level/doctype/customer_interaction/test_customer_interaction.py diff --git a/westech_r2/doctype/load/__init__.py b/westech_r2/_archive_2level/doctype/load/__init__.py similarity index 100% rename from westech_r2/doctype/load/__init__.py rename to westech_r2/_archive_2level/doctype/load/__init__.py diff --git a/westech_r2/doctype/load/load.js b/westech_r2/_archive_2level/doctype/load/load.js similarity index 100% rename from westech_r2/doctype/load/load.js rename to westech_r2/_archive_2level/doctype/load/load.js diff --git a/westech_r2/doctype/load/load.py b/westech_r2/_archive_2level/doctype/load/load.py similarity index 100% rename from westech_r2/doctype/load/load.py rename to westech_r2/_archive_2level/doctype/load/load.py diff --git a/westech_r2/doctype/pallet/__init__.py b/westech_r2/_archive_2level/doctype/pallet/__init__.py similarity index 100% rename from westech_r2/doctype/pallet/__init__.py rename to westech_r2/_archive_2level/doctype/pallet/__init__.py diff --git a/westech_r2/doctype/pallet/pallet.js b/westech_r2/_archive_2level/doctype/pallet/pallet.js similarity index 100% rename from westech_r2/doctype/pallet/pallet.js rename to westech_r2/_archive_2level/doctype/pallet/pallet.js diff --git a/westech_r2/doctype/pallet/pallet.py b/westech_r2/_archive_2level/doctype/pallet/pallet.py similarity index 100% rename from westech_r2/doctype/pallet/pallet.py rename to westech_r2/_archive_2level/doctype/pallet/pallet.py diff --git a/westech_r2/doctype/scheduled_pickup/__init__.py b/westech_r2/_archive_2level/doctype/scheduled_pickup/__init__.py similarity index 100% rename from westech_r2/doctype/scheduled_pickup/__init__.py rename to westech_r2/_archive_2level/doctype/scheduled_pickup/__init__.py diff --git a/westech_r2/doctype/scheduled_pickup/scheduled_pickup.js b/westech_r2/_archive_2level/doctype/scheduled_pickup/scheduled_pickup.js similarity index 100% rename from westech_r2/doctype/scheduled_pickup/scheduled_pickup.js rename to westech_r2/_archive_2level/doctype/scheduled_pickup/scheduled_pickup.js diff --git a/westech_r2/doctype/scheduled_pickup/scheduled_pickup.py b/westech_r2/_archive_2level/doctype/scheduled_pickup/scheduled_pickup.py similarity index 100% rename from westech_r2/doctype/scheduled_pickup/scheduled_pickup.py rename to westech_r2/_archive_2level/doctype/scheduled_pickup/scheduled_pickup.py diff --git a/westech_r2/westech_r2/hooks.py b/westech_r2/_archive_2level/hooks.py.3level similarity index 90% rename from westech_r2/westech_r2/hooks.py rename to westech_r2/_archive_2level/hooks.py.3level index d47bed1..47b76a8 100644 --- a/westech_r2/westech_r2/hooks.py +++ b/westech_r2/_archive_2level/hooks.py.3level @@ -25,7 +25,7 @@ doc_events = { "before_save": "westech_r2.doctype.scheduled_pickup.scheduled_pickup.set_title", }, "Serial No": { - "validate": "westech_r2.api.serial_hooks.validate_hardware_tests", + "validate": "westech_r2.westech_r2.api.serial_hooks.validate_hardware_tests", }, "Load": { "before_save": "westech_r2.doctype.load.load.calculate_totals", diff --git a/westech_r2/westech_r2/modules.txt b/westech_r2/_archive_2level/modules.txt.3level similarity index 100% rename from westech_r2/westech_r2/modules.txt rename to westech_r2/_archive_2level/modules.txt.3level diff --git a/westech_r2/page/__init__.py b/westech_r2/_archive_2level/page/__init__.py similarity index 100% rename from westech_r2/page/__init__.py rename to westech_r2/_archive_2level/page/__init__.py diff --git a/westech_r2/page/customer_intake/__init__.py b/westech_r2/_archive_2level/page/customer_intake/__init__.py similarity index 100% rename from westech_r2/page/customer_intake/__init__.py rename to westech_r2/_archive_2level/page/customer_intake/__init__.py diff --git a/westech_r2/page/customer_intake/customer-intake.json b/westech_r2/_archive_2level/page/customer_intake/customer-intake.json similarity index 100% rename from westech_r2/page/customer_intake/customer-intake.json rename to westech_r2/_archive_2level/page/customer_intake/customer-intake.json diff --git a/westech_r2/page/customer_intake/customer_intake.html b/westech_r2/_archive_2level/page/customer_intake/customer_intake.html similarity index 100% rename from westech_r2/page/customer_intake/customer_intake.html rename to westech_r2/_archive_2level/page/customer_intake/customer_intake.html diff --git a/westech_r2/page/customer_intake/customer_intake.js b/westech_r2/_archive_2level/page/customer_intake/customer_intake.js similarity index 100% rename from westech_r2/page/customer_intake/customer_intake.js rename to westech_r2/_archive_2level/page/customer_intake/customer_intake.js diff --git a/westech_r2/page/customer_intake/customer_intake.py b/westech_r2/_archive_2level/page/customer_intake/customer_intake.py similarity index 100% rename from westech_r2/page/customer_intake/customer_intake.py rename to westech_r2/_archive_2level/page/customer_intake/customer_intake.py diff --git a/westech_r2/page/customer_records/__init__.py b/westech_r2/_archive_2level/page/customer_records/__init__.py similarity index 100% rename from westech_r2/page/customer_records/__init__.py rename to westech_r2/_archive_2level/page/customer_records/__init__.py diff --git a/westech_r2/page/customer_records/customer-records.json b/westech_r2/_archive_2level/page/customer_records/customer-records.json similarity index 100% rename from westech_r2/page/customer_records/customer-records.json rename to westech_r2/_archive_2level/page/customer_records/customer-records.json diff --git a/westech_r2/page/customer_records/customer_records.html b/westech_r2/_archive_2level/page/customer_records/customer_records.html similarity index 100% rename from westech_r2/page/customer_records/customer_records.html rename to westech_r2/_archive_2level/page/customer_records/customer_records.html diff --git a/westech_r2/page/customer_records/customer_records.js b/westech_r2/_archive_2level/page/customer_records/customer_records.js similarity index 100% rename from westech_r2/page/customer_records/customer_records.js rename to westech_r2/_archive_2level/page/customer_records/customer_records.js diff --git a/westech_r2/page/customer_records/customer_records.json b/westech_r2/_archive_2level/page/customer_records/customer_records.json similarity index 100% rename from westech_r2/page/customer_records/customer_records.json rename to westech_r2/_archive_2level/page/customer_records/customer_records.json diff --git a/westech_r2/page/customer_records/customer_records.py b/westech_r2/_archive_2level/page/customer_records/customer_records.py similarity index 100% rename from westech_r2/page/customer_records/customer_records.py rename to westech_r2/_archive_2level/page/customer_records/customer_records.py diff --git a/westech_r2/page/ebay-pricing/__init__.py b/westech_r2/_archive_2level/page/ebay-pricing/__init__.py similarity index 100% rename from westech_r2/page/ebay-pricing/__init__.py rename to westech_r2/_archive_2level/page/ebay-pricing/__init__.py diff --git a/westech_r2/page/ebay-pricing/ebay-pricing.css b/westech_r2/_archive_2level/page/ebay-pricing/ebay-pricing.css similarity index 100% rename from westech_r2/page/ebay-pricing/ebay-pricing.css rename to westech_r2/_archive_2level/page/ebay-pricing/ebay-pricing.css diff --git a/westech_r2/page/ebay-pricing/ebay-pricing.html b/westech_r2/_archive_2level/page/ebay-pricing/ebay-pricing.html similarity index 100% rename from westech_r2/page/ebay-pricing/ebay-pricing.html rename to westech_r2/_archive_2level/page/ebay-pricing/ebay-pricing.html diff --git a/westech_r2/page/ebay-pricing/ebay-pricing.js b/westech_r2/_archive_2level/page/ebay-pricing/ebay-pricing.js similarity index 100% rename from westech_r2/page/ebay-pricing/ebay-pricing.js rename to westech_r2/_archive_2level/page/ebay-pricing/ebay-pricing.js diff --git a/westech_r2/page/ebay-pricing/ebay-pricing.json b/westech_r2/_archive_2level/page/ebay-pricing/ebay-pricing.json similarity index 100% rename from westech_r2/page/ebay-pricing/ebay-pricing.json rename to westech_r2/_archive_2level/page/ebay-pricing/ebay-pricing.json diff --git a/westech_r2/page/ebay-pricing/ebay-pricing.py b/westech_r2/_archive_2level/page/ebay-pricing/ebay-pricing.py similarity index 100% rename from westech_r2/page/ebay-pricing/ebay-pricing.py rename to westech_r2/_archive_2level/page/ebay-pricing/ebay-pricing.py diff --git a/westech_r2/page/ebay-pricing/ebay_pricing.css b/westech_r2/_archive_2level/page/ebay-pricing/ebay_pricing.css similarity index 100% rename from westech_r2/page/ebay-pricing/ebay_pricing.css rename to westech_r2/_archive_2level/page/ebay-pricing/ebay_pricing.css diff --git a/westech_r2/page/ebay-pricing/ebay_pricing.html b/westech_r2/_archive_2level/page/ebay-pricing/ebay_pricing.html similarity index 100% rename from westech_r2/page/ebay-pricing/ebay_pricing.html rename to westech_r2/_archive_2level/page/ebay-pricing/ebay_pricing.html diff --git a/westech_r2/page/ebay-pricing/ebay_pricing.js b/westech_r2/_archive_2level/page/ebay-pricing/ebay_pricing.js similarity index 100% rename from westech_r2/page/ebay-pricing/ebay_pricing.js rename to westech_r2/_archive_2level/page/ebay-pricing/ebay_pricing.js diff --git a/westech_r2/page/ebay-pricing/ebay_pricing.json b/westech_r2/_archive_2level/page/ebay-pricing/ebay_pricing.json similarity index 100% rename from westech_r2/page/ebay-pricing/ebay_pricing.json rename to westech_r2/_archive_2level/page/ebay-pricing/ebay_pricing.json diff --git a/westech_r2/page/ebay-pricing/ebay_pricing.py b/westech_r2/_archive_2level/page/ebay-pricing/ebay_pricing.py similarity index 100% rename from westech_r2/page/ebay-pricing/ebay_pricing.py rename to westech_r2/_archive_2level/page/ebay-pricing/ebay_pricing.py diff --git a/westech_r2/page/eim_portal/__init__.py b/westech_r2/_archive_2level/page/eim_portal/__init__.py similarity index 100% rename from westech_r2/page/eim_portal/__init__.py rename to westech_r2/_archive_2level/page/eim_portal/__init__.py diff --git a/westech_r2/page/eim_portal/eim-portal.css b/westech_r2/_archive_2level/page/eim_portal/eim-portal.css similarity index 100% rename from westech_r2/page/eim_portal/eim-portal.css rename to westech_r2/_archive_2level/page/eim_portal/eim-portal.css diff --git a/westech_r2/page/eim_portal/eim-portal.js b/westech_r2/_archive_2level/page/eim_portal/eim-portal.js similarity index 100% rename from westech_r2/page/eim_portal/eim-portal.js rename to westech_r2/_archive_2level/page/eim_portal/eim-portal.js diff --git a/westech_r2/page/eim_portal/eim-portal.json b/westech_r2/_archive_2level/page/eim_portal/eim-portal.json similarity index 100% rename from westech_r2/page/eim_portal/eim-portal.json rename to westech_r2/_archive_2level/page/eim_portal/eim-portal.json diff --git a/westech_r2/page/eim_portal/eim-portal.py b/westech_r2/_archive_2level/page/eim_portal/eim-portal.py similarity index 100% rename from westech_r2/page/eim_portal/eim-portal.py rename to westech_r2/_archive_2level/page/eim_portal/eim-portal.py diff --git a/westech_r2/page/eim_portal/eim_portal.js b/westech_r2/_archive_2level/page/eim_portal/eim_portal.js similarity index 100% rename from westech_r2/page/eim_portal/eim_portal.js rename to westech_r2/_archive_2level/page/eim_portal/eim_portal.js diff --git a/westech_r2/page/eim_portal/eim_portal.json b/westech_r2/_archive_2level/page/eim_portal/eim_portal.json similarity index 100% rename from westech_r2/page/eim_portal/eim_portal.json rename to westech_r2/_archive_2level/page/eim_portal/eim_portal.json diff --git a/westech_r2/page/intake/__init__.py b/westech_r2/_archive_2level/page/intake/__init__.py similarity index 100% rename from westech_r2/page/intake/__init__.py rename to westech_r2/_archive_2level/page/intake/__init__.py diff --git a/westech_r2/page/intake/intake.css b/westech_r2/_archive_2level/page/intake/intake.css similarity index 100% rename from westech_r2/page/intake/intake.css rename to westech_r2/_archive_2level/page/intake/intake.css diff --git a/westech_r2/page/intake/intake.js b/westech_r2/_archive_2level/page/intake/intake.js similarity index 100% rename from westech_r2/page/intake/intake.js rename to westech_r2/_archive_2level/page/intake/intake.js diff --git a/westech_r2/page/intake/intake.json b/westech_r2/_archive_2level/page/intake/intake.json similarity index 100% rename from westech_r2/page/intake/intake.json rename to westech_r2/_archive_2level/page/intake/intake.json diff --git a/westech_r2/page/intake/intake.py b/westech_r2/_archive_2level/page/intake/intake.py similarity index 100% rename from westech_r2/page/intake/intake.py rename to westech_r2/_archive_2level/page/intake/intake.py diff --git a/westech_r2/page/load_detail/__init__.py b/westech_r2/_archive_2level/page/load_detail/__init__.py similarity index 100% rename from westech_r2/page/load_detail/__init__.py rename to westech_r2/_archive_2level/page/load_detail/__init__.py diff --git a/westech_r2/page/load_detail/load-detail.css b/westech_r2/_archive_2level/page/load_detail/load-detail.css similarity index 100% rename from westech_r2/page/load_detail/load-detail.css rename to westech_r2/_archive_2level/page/load_detail/load-detail.css diff --git a/westech_r2/page/load_detail/load-detail.html b/westech_r2/_archive_2level/page/load_detail/load-detail.html similarity index 100% rename from westech_r2/page/load_detail/load-detail.html rename to westech_r2/_archive_2level/page/load_detail/load-detail.html diff --git a/westech_r2/page/load_detail/load-detail.js b/westech_r2/_archive_2level/page/load_detail/load-detail.js similarity index 100% rename from westech_r2/page/load_detail/load-detail.js rename to westech_r2/_archive_2level/page/load_detail/load-detail.js diff --git a/westech_r2/page/load_detail/load-detail.json b/westech_r2/_archive_2level/page/load_detail/load-detail.json similarity index 100% rename from westech_r2/page/load_detail/load-detail.json rename to westech_r2/_archive_2level/page/load_detail/load-detail.json diff --git a/westech_r2/page/load_detail/load-detail.py b/westech_r2/_archive_2level/page/load_detail/load-detail.py similarity index 100% rename from westech_r2/page/load_detail/load-detail.py rename to westech_r2/_archive_2level/page/load_detail/load-detail.py diff --git a/westech_r2/page/load_detail/load_detail.css b/westech_r2/_archive_2level/page/load_detail/load_detail.css similarity index 100% rename from westech_r2/page/load_detail/load_detail.css rename to westech_r2/_archive_2level/page/load_detail/load_detail.css diff --git a/westech_r2/page/load_detail/load_detail.html b/westech_r2/_archive_2level/page/load_detail/load_detail.html similarity index 100% rename from westech_r2/page/load_detail/load_detail.html rename to westech_r2/_archive_2level/page/load_detail/load_detail.html diff --git a/westech_r2/page/load_detail/load_detail.js b/westech_r2/_archive_2level/page/load_detail/load_detail.js similarity index 100% rename from westech_r2/page/load_detail/load_detail.js rename to westech_r2/_archive_2level/page/load_detail/load_detail.js diff --git a/westech_r2/page/load_detail/load_detail.json b/westech_r2/_archive_2level/page/load_detail/load_detail.json similarity index 100% rename from westech_r2/page/load_detail/load_detail.json rename to westech_r2/_archive_2level/page/load_detail/load_detail.json diff --git a/westech_r2/page/load_detail/load_detail.py b/westech_r2/_archive_2level/page/load_detail/load_detail.py similarity index 100% rename from westech_r2/page/load_detail/load_detail.py rename to westech_r2/_archive_2level/page/load_detail/load_detail.py diff --git a/westech_r2/page/load_update/__init__.py b/westech_r2/_archive_2level/page/load_update/__init__.py similarity index 100% rename from westech_r2/page/load_update/__init__.py rename to westech_r2/_archive_2level/page/load_update/__init__.py diff --git a/westech_r2/page/load_update/load-update.css b/westech_r2/_archive_2level/page/load_update/load-update.css similarity index 100% rename from westech_r2/page/load_update/load-update.css rename to westech_r2/_archive_2level/page/load_update/load-update.css diff --git a/westech_r2/page/load_update/load-update.html b/westech_r2/_archive_2level/page/load_update/load-update.html similarity index 100% rename from westech_r2/page/load_update/load-update.html rename to westech_r2/_archive_2level/page/load_update/load-update.html diff --git a/westech_r2/page/load_update/load-update.js b/westech_r2/_archive_2level/page/load_update/load-update.js similarity index 100% rename from westech_r2/page/load_update/load-update.js rename to westech_r2/_archive_2level/page/load_update/load-update.js diff --git a/westech_r2/page/load_update/load-update.json b/westech_r2/_archive_2level/page/load_update/load-update.json similarity index 100% rename from westech_r2/page/load_update/load-update.json rename to westech_r2/_archive_2level/page/load_update/load-update.json diff --git a/westech_r2/page/load_update/load-update.py b/westech_r2/_archive_2level/page/load_update/load-update.py similarity index 100% rename from westech_r2/page/load_update/load-update.py rename to westech_r2/_archive_2level/page/load_update/load-update.py diff --git a/westech_r2/page/load_update/load_update.css b/westech_r2/_archive_2level/page/load_update/load_update.css similarity index 100% rename from westech_r2/page/load_update/load_update.css rename to westech_r2/_archive_2level/page/load_update/load_update.css diff --git a/westech_r2/page/load_update/load_update.html b/westech_r2/_archive_2level/page/load_update/load_update.html similarity index 100% rename from westech_r2/page/load_update/load_update.html rename to westech_r2/_archive_2level/page/load_update/load_update.html diff --git a/westech_r2/page/load_update/load_update.js b/westech_r2/_archive_2level/page/load_update/load_update.js similarity index 100% rename from westech_r2/page/load_update/load_update.js rename to westech_r2/_archive_2level/page/load_update/load_update.js diff --git a/westech_r2/page/load_update/load_update.json b/westech_r2/_archive_2level/page/load_update/load_update.json similarity index 100% rename from westech_r2/page/load_update/load_update.json rename to westech_r2/_archive_2level/page/load_update/load_update.json diff --git a/westech_r2/page/load_update/load_update.py b/westech_r2/_archive_2level/page/load_update/load_update.py similarity index 100% rename from westech_r2/page/load_update/load_update.py rename to westech_r2/_archive_2level/page/load_update/load_update.py diff --git a/westech_r2/page/pallet_list/__init__.py b/westech_r2/_archive_2level/page/pallet_list/__init__.py similarity index 100% rename from westech_r2/page/pallet_list/__init__.py rename to westech_r2/_archive_2level/page/pallet_list/__init__.py diff --git a/westech_r2/page/pallet_list/pallet_list.html b/westech_r2/_archive_2level/page/pallet_list/pallet_list.html similarity index 100% rename from westech_r2/page/pallet_list/pallet_list.html rename to westech_r2/_archive_2level/page/pallet_list/pallet_list.html diff --git a/westech_r2/page/pallet_list/pallet_list.js b/westech_r2/_archive_2level/page/pallet_list/pallet_list.js similarity index 100% rename from westech_r2/page/pallet_list/pallet_list.js rename to westech_r2/_archive_2level/page/pallet_list/pallet_list.js diff --git a/westech_r2/page/pallet_list/pallet_list.json b/westech_r2/_archive_2level/page/pallet_list/pallet_list.json similarity index 100% rename from westech_r2/page/pallet_list/pallet_list.json rename to westech_r2/_archive_2level/page/pallet_list/pallet_list.json diff --git a/westech_r2/page/pallet_list/pallet_list.py b/westech_r2/_archive_2level/page/pallet_list/pallet_list.py similarity index 100% rename from westech_r2/page/pallet_list/pallet_list.py rename to westech_r2/_archive_2level/page/pallet_list/pallet_list.py diff --git a/westech_r2/page/r2_tracking/__init__.py b/westech_r2/_archive_2level/page/r2_tracking/__init__.py similarity index 100% rename from westech_r2/page/r2_tracking/__init__.py rename to westech_r2/_archive_2level/page/r2_tracking/__init__.py diff --git a/westech_r2/page/r2_tracking/r2-tracking.css b/westech_r2/_archive_2level/page/r2_tracking/r2-tracking.css similarity index 100% rename from westech_r2/page/r2_tracking/r2-tracking.css rename to westech_r2/_archive_2level/page/r2_tracking/r2-tracking.css diff --git a/westech_r2/page/r2_tracking/r2-tracking.js b/westech_r2/_archive_2level/page/r2_tracking/r2-tracking.js similarity index 100% rename from westech_r2/page/r2_tracking/r2-tracking.js rename to westech_r2/_archive_2level/page/r2_tracking/r2-tracking.js diff --git a/westech_r2/page/r2_tracking/r2-tracking.json b/westech_r2/_archive_2level/page/r2_tracking/r2-tracking.json similarity index 100% rename from westech_r2/page/r2_tracking/r2-tracking.json rename to westech_r2/_archive_2level/page/r2_tracking/r2-tracking.json diff --git a/westech_r2/page/r2_tracking/r2-tracking.py b/westech_r2/_archive_2level/page/r2_tracking/r2-tracking.py similarity index 100% rename from westech_r2/page/r2_tracking/r2-tracking.py rename to westech_r2/_archive_2level/page/r2_tracking/r2-tracking.py diff --git a/westech_r2/page/r2_tracking/r2_tracking.js b/westech_r2/_archive_2level/page/r2_tracking/r2_tracking.js similarity index 100% rename from westech_r2/page/r2_tracking/r2_tracking.js rename to westech_r2/_archive_2level/page/r2_tracking/r2_tracking.js diff --git a/westech_r2/page/r2_tracking/r2_tracking.json b/westech_r2/_archive_2level/page/r2_tracking/r2_tracking.json similarity index 100% rename from westech_r2/page/r2_tracking/r2_tracking.json rename to westech_r2/_archive_2level/page/r2_tracking/r2_tracking.json diff --git a/westech_r2/page/receiving/__init__.py b/westech_r2/_archive_2level/page/receiving/__init__.py similarity index 100% rename from westech_r2/page/receiving/__init__.py rename to westech_r2/_archive_2level/page/receiving/__init__.py diff --git a/westech_r2/_archive_2level/page/receiving/receiving.js b/westech_r2/_archive_2level/page/receiving/receiving.js new file mode 100644 index 0000000..2be2b5a --- /dev/null +++ b/westech_r2/_archive_2level/page/receiving/receiving.js @@ -0,0 +1,642 @@ +frappe.pages['receiving'].on_page_load = function(wrapper) { + var page = frappe.ui.make_app_page({ + parent: wrapper, + title: 'Receiving', + single_column: true + }); + + // Inline HTML + $(wrapper).find('.layout-main-section').html(` +
+
+
+

🚛 Receiving

+

Schedule pickups, manage routes, and check in loads.

+
+
+ +
+
+
+
+
+
📅 Pickup Calendar — Next 30 Days
+
Loading...
+
+
+
+
+
+
+
Scheduled Pickups
+
+ + + +
+
+
+
+ + + +
DateWeekdayTypeCustomerContactAddressEst. ItemsDataRED/R2StatusNotesTruckAoRCoD
Loading...
+
+
+
+
+ +
+
+
+
+
🚛 Truck 1
+
🚛 Truck 2
+
🚛 Truck 3
+
+
📋 Unassigned
+
+
+
+
+
Recent Check-ins
+
+ + + +
DateCustomerLoad #PalletsWeightContentsData StatusRED/R2
Loading...
+
+
+ +
+
+
+ + `); + + // Prevent Frappe router from intercepting tab clicks + // Tab switching — direct DOM to avoid Frappe router intercepting clicks + $("#receiving-tabs").on("click", ".stage-tab", function() { + var target = $(this).data("target"); + $("#receiving-tabs li, .stage-tab").removeClass("active"); + $(this).addClass("active").closest("li").addClass("active"); + $(".tab-pane").removeClass("active"); + $(target).addClass("active"); + }); + // ── Stage A: Link Controls ── + var customer_control = null; + + function setupCustomerLink() { + customer_control = frappe.ui.form.make_control({ + parent: $("#sp-customer-control"), + df: { + fieldtype: "Link", + fieldname: "customer_number", + options: "Customer", + label: "Customer", + reqd: 1, + placeholder: "Search by name, number, or address...", + onchange: function() { + var val = customer_control.get_value(); + if (val) fetchCustomerDetails(val); + else clearCustomerFields(); + } + }, + only_input: true, + }); + customer_control.refresh(); + $("#sp-customer-control .control-input").css("margin", "0"); + $("#sp-customer-control .help-box").remove(); + } + + // New Customer button + $(document).on("click", "#btn-new-customer", function() { + frappe.call({ + method: "frappe.client.insert", + args: { doc: { doctype: "Customer", customer_type: "Company", customer_group: "All Customer Groups", territory: "United States" } }, + callback: function(r) { + if (r.message) { + frappe.set_route("Form", "Customer", r.message.name); + } + } + }); + }); + + function fetchCustomerDetails(customer_name) { + frappe.call({ + method: "frappe.client.get", + args: { doctype: "Customer", name: customer_name }, + callback: function(r) { + if (!r.message) return; + var c = r.message; + $("#sp-company_name").val(c.customer_name || ""); + $("#sp-contact_name").val(c.contact_name || ""); + $("#sp-contact_phone").val(c.contact_phone || ""); + $("#sp-contact_email").val(c.contact_email || ""); + + frappe.call({ + method: "frappe.client.get_list", + args: { + doctype: "Address", + filters: [["Dynamic Link", "link_name", "=", customer_name]], + fields: ["address_line1", "city", "state", "pincode"], + limit_page_length: 1 + }, + callback: function(ra) { + if (ra.message && ra.message.length) { + var a = ra.message[0]; + $("#sp-address_line").val(a.address_line1 || ""); + $("#sp-city").val(a.city || ""); + $("#sp-state").val(a.state || "AZ"); + $("#sp-zip_code").val(a.pincode || ""); + } + } + }); + + frappe.call({ + method: "frappe.client.get_list", + args: { + doctype: "Contact", + filters: [["Dynamic Link", "link_name", "=", customer_name]], + fields: ["first_name", "last_name", "email_id", "phone", "mobile_no"], + limit_page_length: 1 + }, + callback: function(rc) { + if (rc.message && rc.message.length) { + var ct = rc.message[0]; + if (!$("#sp-contact_name").val()) { + $("#sp-contact_name").val((ct.first_name || "") + " " + (ct.last_name || "")); + } + if (!$("#sp-contact_phone").val()) { + $("#sp-contact_phone").val(ct.phone || ct.mobile_no || ""); + } + if (!$("#sp-contact_email").val()) { + $("#sp-contact_email").val(ct.email_id || ""); + } + } + } + }); + } + }); + } + + function clearCustomerFields() { + $("#sp-company_name, #sp-contact_name, #sp-contact_phone, #sp-contact_email, #sp-address_line, #sp-city, #sp-state, #sp-zip_code").val(""); + $("#sp-state").val("AZ"); + } + + // ── Stage A: Load Pickups ── + function loadPickups() { + var dateFilter = $("#pickup-date-filter").val(); + frappe.call({ + method: "westech_r2.api.receiving_api.get_pickups", + args: { date: dateFilter || undefined }, + callback: function(r) { + if (r.message) { + renderPickupTable(r.message.pickups || []); + renderCalendar(r.message.calendar || []); + } + } + }); + } + + function renderPickupTable(pickups) { + var tbody = $("#pickup-tbody"); + $("#pickup-count-label").text("(" + pickups.length + ")"); + if (!pickups.length) { + tbody.html('No pickups found'); + return; + } + tbody.html(pickups.map(function(p) { + var dt = p.pickup_date ? new Date(p.pickup_date + "T00:00:00") : null; + var dn = dt ? dayName(dt) : ""; + return '' + + '' + esc(p.pickup_date || "") + '' + + '' + dn + '' + + '' + esc(p.pickup_type || "") + '' + + '' + esc(p.company_name || p.customer_number || "") + '' + + '' + esc(p.contact_name || "") + '' + + '' + esc((p.address_line || "") + (p.city ? ", " + p.city : "")) + '' + + '' + (p.estimated_items || "—") + '' + + '' + esc(p.data_status || "—") + '' + + '' + esc(p.red_r2 || "—") + '' + + '' + esc(p.status || "") + '' + + '' + esc(p.notes || "") + '' + + '' + esc(p.truck || "") + '' + + '' + (p.needs_aor ? "✓" : "") + '' + + '' + (p.needs_cod ? "✓" : "") + ''; + }).join("")); + } + + function renderCalendar(calendar) { + var el = $("#pickup-calendar"); + if (!calendar.length) { el.html('
No data
'); return; } + var todayStr = frappe.datetime.nowdate(); + var html = '
'; + calendar.forEach(function(d) { + var cls = "cal-day"; + if (d.count > 0) cls += " has-pickups"; + if (d.date === todayStr) cls += " today"; + var label = d.date.substring(5); + html += '
' + label + '
'; + }); + html += '
'; + el.html(html); + } + + $(document).on("click", ".cal-day.has-pickups", function() { + $("#pickup-date-filter").val($(this).data("date")); + loadPickups(); + }); + + // ── Stage A: New Pickup ── + $("#btn-new-pickup").on("click", function() { + $("#new-pickup-form").show(); + $("#sp-pickup_date").val(frappe.datetime.nowdate()); + setupCustomerLink(); + }); + + $("#btn-cancel-pickup").on("click", function() { + $("#new-pickup-form").hide(); + }); + + $("#pickup-form").on("submit", function(e) { + e.preventDefault(); + var customerVal = customer_control ? customer_control.get_value() : ""; + if (!customerVal) { frappe.msgprint("Select a customer"); return; } + + var doc = { + doctype: "Scheduled Pickup", + pickup_date: $("#sp-pickup_date").val(), + pickup_type: $("#sp-pickup_type").val(), + customer_number: customerVal, + company_name: $("#sp-company_name").val(), + contact_name: $("#sp-contact_name").val(), + contact_phone: $("#sp-contact_phone").val(), + contact_email: $("#sp-contact_email").val(), + address_line: $("#sp-address_line").val(), + city: $("#sp-city").val(), + state: $("#sp-state").val(), + zip_code: $("#sp-zip_code").val(), + estimated_items: parseInt($("#sp-estimated_items").val()) || 0, + estimated_weight: $("#sp-estimated_weight").val(), + load_contents: $("#sp-load_contents").val(), + data_status: $("#sp-data_status").val(), + red_r2: $("#sp-red_r2").val(), + needs_aor: $("#sp-needs_aor").is(":checked") ? 1 : 0, + needs_cod: $("#sp-needs_cod").is(":checked") ? 1 : 0, + notes: $("#sp-notes").val(), + status: "Scheduled" + }; + frappe.call({ + method: "frappe.client.insert", + args: { doc: doc }, + callback: function(r) { + if (r.message) { + frappe.show_alert({ message: "Pickup scheduled", indicator: "green" }); + $("#new-pickup-form").hide(); + loadPickups(); + } + } + }); + }); + + $("#pickup-date-filter").on("change", loadPickups); + $("#btn-clear-date").on("click", function() { + $("#pickup-date-filter").val(""); + loadPickups(); + }); + + // ── Stage B: Routing ── + function loadRoutes() { + var date = $("#route-date").val() || frappe.datetime.nowdate(); + $("#route-date").val(date); + frappe.call({ + method: "westech_r2.api.receiving_api.get_pickups", + args: { date: date }, + callback: function(r) { + if (r.message) renderRouteColumns(r.message.pickups || []); + } + }); + } + + function renderRouteColumns(pickups) { + var trucks = { "Truck 1": [], "Truck 2": [], "Truck 3": [], "Unassigned": [] }; + pickups.forEach(function(p) { + var t = p.truck || ""; + if (t && trucks[t]) trucks[t].push(p); + else trucks["Unassigned"].push(p); + }); + ["Truck 1", "Truck 2", "Truck 3"].forEach(function(t) { + var key = t.toLowerCase().replace(/ /g, ""); + $("#" + key + "-count").text("(" + trucks[t].length + " stops)"); + $("#" + key + "-stops").html(trucks[t].map(function(p, i) { return stopCard(p, i + 1); }).join("")); + }); + $("#unassigned-count").text("(" + trucks["Unassigned"].length + ")"); + $("#unassigned-stops").html(trucks["Unassigned"].map(function(p) { return stopCard(p, 0); }).join("")); + } + + function stopCard(p, order) { + var h = '
'; + if (order) h += '
Stop #' + order + '
'; + h += '
' + esc(p.company_name || p.customer_number || "Unknown") + '
'; + h += '
' + esc((p.address_line || "") + (p.city ? ", " + p.city : "")) + '
'; + h += '
'; + if (p.estimated_items) h += '' + p.estimated_items + ' items'; + if (p.data_status) h += '' + esc(p.data_status) + ''; + if (p.red_r2) h += '' + esc(p.red_r2) + ''; + if (p.needs_aor) h += 'AoR'; + if (p.needs_cod) h += 'CoD'; + h += '
'; + return h; + } + + $("#btn-load-routes").on("click", loadRoutes); + + $("#btn-auto-route").on("click", function() { + var date = $("#route-date").val(); + if (!date) { frappe.msgprint("Select a date first"); return; } + frappe.call({ + method: "westech_r2.api.receiving_api.auto_route", + args: { date: date }, + callback: function(r) { + if (r.message && r.message.success) { + frappe.show_alert({ message: "Routes optimized", indicator: "green" }); + loadRoutes(); + } + } + }); + }); + + $("#btn-route-sheet").on("click", function() { + var date = $("#route-date").val() || frappe.datetime.nowdate(); + window.open("/api/method/westech_r2.api.receiving_api.print_route_sheet?date=" + date, "_blank"); + }); + + $("#btn-green-sheet").on("click", function() { + var date = $("#route-date").val() || frappe.datetime.nowdate(); + window.open("/api/method/westech_r2.api.receiving_api.print_green_sheet?date=" + date, "_blank"); + }); + + $("#btn-labels").on("click", function() { + var date = $("#route-date").val() || frappe.datetime.nowdate(); + window.open("/api/method/westech_r2.api.receiving_api.print_labels?date=" + date, "_blank"); + }); + + // ── Stage C: Check-in ── + var checkin_pickup_control = null; + var current_pickup_details = null; + + function loadCheckins() { + frappe.call({ + method: "westech_r2.api.receiving_api.get_checkins", + callback: function(r) { + if (r.message) renderCheckinTable(r.message.checkins || []); + } + }); + } + + function renderCheckinTable(checkins) { + var tbody = $("#checkin-tbody"); + if (!checkins.length) { + tbody.html('No check-ins yet'); + return; + } + tbody.html(checkins.map(function(c) { + var palletInfo = (c.pallets || []).map(function(p) { return esc(p.pallet_number || p.name); }).join(", "); + return '' + + '' + esc(c.incoming_date || "") + '' + + '' + esc(c.customer_name || c.customer || "") + '' + + '' + esc(c.name || "") + '' + + '' + (c.pallet_count || 0) + '' + + '' + (c.total_weight || "—") + '' + + '' + esc(c.data_status || "") + '' + + '' + esc(c.red_r2 || "—") + '' + + '' + palletInfo + ''; + }).join("")); + } + + $("#btn-new-checkin").on("click", function() { + $("#checkin-form").show(); + $("#ci-received_date").val(frappe.datetime.nowdate()); + $("#ci-pickup-details").hide(); + current_pickup_details = null; + checkin_pickup_control = frappe.ui.form.make_control({ + parent: $("#ci-pickup-control"), + df: { + fieldtype: "Link", + fieldname: "pickup_ref", + options: "Scheduled Pickup", + label: "Scheduled Pickup", + reqd: 1, + placeholder: "Search pickup...", + get_query: function() { + return { + filters: [ + ["Scheduled Pickup", "status", "in", ["Scheduled", "Routed", "In Progress"]] + ] + }; + }, + onchange: function() { + var val = checkin_pickup_control ? checkin_pickup_control.get_value() : ""; + if (val) loadPickupDetails(val); + else { + $("#ci-pickup-details").hide(); + current_pickup_details = null; + } + } + }, + only_input: true, + }); + checkin_pickup_control.refresh(); + $("#ci-pickup-control .control-input").css("margin", "0"); + $("#ci-pickup-control .help-box").remove(); + }); + + function loadPickupDetails(pickup_name) { + frappe.call({ + method: "westech_r2.api.receiving_api.get_pickup_details", + args: { pickup_name: pickup_name }, + callback: function(r) { + if (!r.message) return; + current_pickup_details = r.message; + var d = r.message; + + $("#ci-pickup-details").show(); + $("#ci-customer-info").text((d.company_name || d.customer_number || "Unknown") + (d.red_r2 ? " — " + d.red_r2 : "")); + $("#ci-address-info").text((d.address_line || "") + (d.city ? ", " + d.city : "") + (d.state ? ", " + d.state : "") + (d.zip_code ? " " + d.zip_code : "")); + $("#ci-contact-info").text((d.contact_name || "") + (d.contact_phone ? " • " + d.contact_phone : "") + (d.contact_email ? " • " + d.contact_email : "")); + + // Special handling for RED/NIST + if (d.red_r2 && d.red_r2 !== "Neither" && d.red_r2 !== "") { + $("#ci-special-handling").show().html("⚠ " + esc(d.red_r2) + " — Special handling required" + (d.needs_aor ? " • AoR ✓" : "") + (d.needs_cod ? " • CoD ✓" : "")); + } else { + $("#ci-special-handling").hide(); + } + + // Notes + if (d.notes) { + $("#ci-pickup-notes").show().html("Notes: " + esc(d.notes)); + } else { + $("#ci-pickup-notes").hide(); + } + + // Pre-fill check-in fields from pickup + $("#ci-actual_pallets").val(d.estimated_items || 1); + $("#ci-data_status").val(d.data_status || ""); + $("#ci-red_r2").val(d.red_r2 || ""); + } + }); + } + + $("#btn-cancel-checkin").on("click", function() { + $("#checkin-form").hide(); + }); + + $("#checkin-form-inner").on("submit", function(e) { + e.preventDefault(); + var pickupName = checkin_pickup_control ? checkin_pickup_control.get_value() : ""; + if (!pickupName) { frappe.msgprint("Select a pickup"); return; } + + var actualPallets = parseInt($("#ci-actual_pallets").val()) || 0; + if (actualPallets < 1) { frappe.msgprint("Enter at least 1 pallet"); return; } + + frappe.confirm( + "Check in " + actualPallets + " pallet(s) for this load?", + function() { + frappe.call({ + method: "westech_r2.api.receiving_api.checkin_load", + args: { + pickup_name: pickupName, + received_date: $("#ci-received_date").val(), + actual_pallets: actualPallets, + total_weight: $("#ci-total_weight").val(), + load_contents: $("#ci-load_contents").val(), + data_status: $("#ci-data_status").val(), + red_r2: $("#ci-red_r2").val() + }, + callback: function(r) { + if (r.message && r.message.success) { + frappe.show_alert({ + message: "Load checked in! Created " + r.message.pallets_created + " pallet(s) in Load " + r.message.load, + indicator: "green" + }); + $("#checkin-form").hide(); + loadCheckins(); + loadPickups(); + } + } + }); + } + ); + }); + + $("#btn-cor-report").on("click", function() { + window.open("/api/method/westech_r2.api.receiving_api.cor_report", "_blank"); + }); + + // ── Helpers ── + function esc(s) { return s ? String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """) : ""; } + function dayName(d) { return ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][d.getDay()]; } + + // ── Init ── + loadPickups(); + loadCheckins(); +}; \ No newline at end of file diff --git a/westech_r2/page/receiving/receiving.js.bak b/westech_r2/_archive_2level/page/receiving/receiving.js.bak similarity index 100% rename from westech_r2/page/receiving/receiving.js.bak rename to westech_r2/_archive_2level/page/receiving/receiving.js.bak diff --git a/westech_r2/page/receiving/receiving.js b/westech_r2/_archive_2level/page/receiving/receiving.js.bak2 similarity index 65% rename from westech_r2/page/receiving/receiving.js rename to westech_r2/_archive_2level/page/receiving/receiving.js.bak2 index 3d83536..d6d514b 100644 --- a/westech_r2/page/receiving/receiving.js +++ b/westech_r2/_archive_2level/page/receiving/receiving.js.bak2 @@ -5,7 +5,7 @@ frappe.pages['receiving'].on_page_load = function(wrapper) { single_column: true }); - // Inline HTML — same pattern as intake.js + // Inline HTML $(wrapper).find('.layout-main-section').html(`
@@ -15,9 +15,9 @@ frappe.pages['receiving'].on_page_load = function(wrapper) {
@@ -59,8 +59,14 @@ frappe.pages['receiving'].on_page_load = function(wrapper) {
📅 Pickup Info
-
-
+
+ +
+
+ +
+
+
@@ -79,10 +85,9 @@ frappe.pages['receiving'].on_page_load = function(wrapper) {
-
+
-
-
+
@@ -106,8 +111,8 @@ frappe.pages['receiving'].on_page_load = function(wrapper) {
Recent Check-ins
- - + +
DateCustomerTypeActual PalletsActual WeightLoad ContentsData StatusRED/R2Status
Loading...
DateCustomerLoad #PalletsWeightContentsData StatusRED/R2
Loading...
@@ -117,15 +122,31 @@ frappe.pages['receiving'].on_page_load = function(wrapper) {
-
-
-
+
+
📅 Pickup Reference
+
+ +
+
+
⚖️ Actual Load
+
+
+
+
+
+
📦 Contents & Classification
+
+
+
+
-
-
-
-
-
+
@@ -142,27 +163,18 @@ frappe.pages['receiving'].on_page_load = function(wrapper) { .stop-card .stop-meta { display: flex; gap: 8px; margin-top: 4px; font-size: 11px; } .stop-card .stop-meta span { background: #D6E4F0; color: #2F5496; padding: 1px 6px; border-radius: 3px; } .stop-card.dragging { opacity: 0.5; } - .truck-column { min-height: 200px; } - #pickup-calendar { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; font-size: 12px; } - .cal-day { text-align: center; padding: 6px 4px; border-radius: 6px; } - .cal-day.has-pickups { background: #D6E4F0; cursor: pointer; } - .cal-day.today { background: #2F5496; color: #fff; } - .cal-day .day-num { font-weight: 600; } - .cal-day .day-count { font-size: 10px; font-weight: 700; } + .cal-day { display: inline-block; width: 36px; height: 36px; line-height: 36px; text-align: center; margin: 1px; border-radius: 4px; font-size: 12px; cursor: pointer; } + .cal-day:hover { background: #D6E4F0; } + .cal-day.has-pickups { background: #2F5496; color: #fff; font-weight: 700; } + .cal-day.today { border: 2px solid #C62828; } `); - - // ── Stage Tabs ── - $("#receiving-tabs a").on("click", function(e) { + // Prevent Frappe router from intercepting tab clicks + $("#receiving-tabs").on("click", "a[data-toggle=tab]", function(e) { e.preventDefault(); $(this).tab("show"); - var stage = $(this).attr("href").replace("#stage-", ""); - if (stage === "a") loadPickups(); - if (stage === "b") loadRoutes(); - if (stage === "c") loadCheckins(); }); - // ── Stage A: Link Controls ── var customer_control = null; @@ -175,7 +187,7 @@ frappe.pages['receiving'].on_page_load = function(wrapper) { options: "Customer", label: "Customer", reqd: 1, - placeholder: "Search customer...", + placeholder: "Search by name, number, or address...", onchange: function() { var val = customer_control.get_value(); if (val) fetchCustomerDetails(val); @@ -189,6 +201,19 @@ frappe.pages['receiving'].on_page_load = function(wrapper) { $("#sp-customer-control .help-box").remove(); } + // New Customer button + $(document).on("click", "#btn-new-customer", function() { + frappe.call({ + method: "frappe.client.insert", + args: { doc: { doctype: "Customer", customer_type: "Company", customer_group: "All Customer Groups", territory: "United States" } }, + callback: function(r) { + if (r.message) { + frappe.set_route("Form", "Customer", r.message.name); + } + } + }); + }); + function fetchCustomerDetails(customer_name) { frappe.call({ method: "frappe.client.get", @@ -200,8 +225,6 @@ frappe.pages['receiving'].on_page_load = function(wrapper) { $("#sp-contact_name").val(c.contact_name || ""); $("#sp-contact_phone").val(c.contact_phone || ""); $("#sp-contact_email").val(c.contact_email || ""); - $("#sp-legacy_notes").val(c.legacy_notes || ""); - $("#sp-hours_of_operation").val(c.hours_of_operation || ""); frappe.call({ method: "frappe.client.get_list", @@ -250,7 +273,7 @@ frappe.pages['receiving'].on_page_load = function(wrapper) { } function clearCustomerFields() { - $("#sp-company_name, #sp-contact_name, #sp-contact_phone, #sp-contact_email, #sp-address_line, #sp-city, #sp-state, #sp-zip_code, #sp-legacy_notes, #sp-hours_of_operation").val(""); + $("#sp-company_name, #sp-contact_name, #sp-contact_phone, #sp-contact_email, #sp-address_line, #sp-city, #sp-state, #sp-zip_code").val(""); $("#sp-state").val("AZ"); } @@ -259,13 +282,11 @@ frappe.pages['receiving'].on_page_load = function(wrapper) { var dateFilter = $("#pickup-date-filter").val(); frappe.call({ method: "westech_r2.api.receiving_api.get_pickups", - args: { date: dateFilter }, + args: { date: dateFilter || undefined }, callback: function(r) { if (r.message) { renderPickupTable(r.message.pickups || []); renderCalendar(r.message.calendar || []); - // weekly chart removed - $("#pickup-count-label").text((r.message.pickups || []).length + " pickups"); } } }); @@ -273,63 +294,53 @@ frappe.pages['receiving'].on_page_load = function(wrapper) { function renderPickupTable(pickups) { var tbody = $("#pickup-tbody"); + $("#pickup-count-label").text("(" + pickups.length + ")"); if (!pickups.length) { - tbody.html('No scheduled pickups'); + tbody.html('No pickups found'); return; } - var statusColors = { "Scheduled": "#2196F3", "Routed": "#009688", "In Progress": "#FF9800", "Complete": "#4CAF50", "Cancelled": "#F44336" }; - var h = ""; - pickups.forEach(function(p) { - var st = p.status || "Scheduled"; - var sc = statusColors[st] || "#999"; - var weekday = p.pickup_date ? dayName(new Date(p.pickup_date + "T12:00:00")) : ""; - var typeBadge = p.pickup_type === "Drop-off" - ? 'Drop-off' - : 'Pickup'; - h += ''; - h += '' + esc(p.pickup_date || "") + ''; - h += '' + weekday + ''; - h += '' + typeBadge + ''; - h += '' + esc(p.company_name || p.customer_number || "") + ''; - h += '' + esc((p.contact_name || "") + (p.contact_phone ? " • " + p.contact_phone : "")) + ''; - h += '' + esc((p.address_line || "") + (p.city ? ", " + p.city : "")) + ''; - h += '' + (p.estimated_items || "—") + ''; - h += '' + esc(p.data_status || "—") + ''; - h += '' + esc(p.red_r2 || "—") + ''; - h += '' + esc(st) + ''; - h += '' + esc(p.notes || "") + ''; - h += '' + esc(p.truck || "—") + ''; - h += '' + (p.needs_aor ? "✓" : "") + ''; - h += '' + (p.needs_cod ? "✓" : "") + ''; - h += ''; - }); - tbody.html(h); + tbody.html(pickups.map(function(p) { + var dt = p.pickup_date ? new Date(p.pickup_date + "T00:00:00") : null; + var dn = dt ? dayName(dt) : ""; + return '' + + '' + esc(p.pickup_date || "") + '' + + '' + dn + '' + + '' + esc(p.pickup_type || "") + '' + + '' + esc(p.company_name || p.customer_number || "") + '' + + '' + esc(p.contact_name || "") + '' + + '' + esc((p.address_line || "") + (p.city ? ", " + p.city : "")) + '' + + '' + (p.estimated_items || "—") + '' + + '' + esc(p.data_status || "—") + '' + + '' + esc(p.red_r2 || "—") + '' + + '' + esc(p.status || "") + '' + + '' + esc(p.notes || "") + '' + + '' + esc(p.truck || "") + '' + + '' + (p.needs_aor ? "✓" : "") + '' + + '' + (p.needs_cod ? "✓" : "") + ''; + }).join("")); } - function renderCalendar(days) { + function renderCalendar(calendar) { var el = $("#pickup-calendar"); - if (!days || !days.length) { el.html('
No upcoming pickups
'); return; } - var today = frappe.datetime.nowdate(); - var h = '
'; - ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].forEach(function(d) { - h += '
' + d + '
'; + if (!calendar.length) { el.html('
No data
'); return; } + var todayStr = frappe.datetime.nowdate(); + var html = '
'; + calendar.forEach(function(d) { + var cls = "cal-day"; + if (d.count > 0) cls += " has-pickups"; + if (d.date === todayStr) cls += " today"; + var label = d.date.substring(5); + html += '
' + label + '
'; }); - var first = new Date(days[0].date + "T12:00:00"); - for (var i = 0; i < first.getDay(); i++) h += '
'; - days.forEach(function(d) { - var isToday = d.date === today; - var hasCount = d.count > 0; - var bg = isToday ? "cal-day today" : (hasCount ? "cal-day has-pickups" : "cal-day"); - var onclick = hasCount ? "onclick=$('#pickup-date-filter').val('" + d.date + "');loadPickups();" : ""; - h += '
'; - h += '
' + d.date.split("-")[2] + '
'; - if (hasCount) h += '
' + d.count + '
'; - h += '
'; - }); - h += '
'; - el.html(h); + html += '
'; + el.html(html); } + $(document).on("click", ".cal-day.has-pickups", function() { + $("#pickup-date-filter").val($(this).data("date")); + loadPickups(); + }); + // ── Stage A: New Pickup ── $("#btn-new-pickup").on("click", function() { $("#new-pickup-form").show(); @@ -339,16 +350,18 @@ frappe.pages['receiving'].on_page_load = function(wrapper) { $("#btn-cancel-pickup").on("click", function() { $("#new-pickup-form").hide(); - if (customer_control) customer_control.set_value(""); }); $("#pickup-form").on("submit", function(e) { e.preventDefault(); + var customerVal = customer_control ? customer_control.get_value() : ""; + if (!customerVal) { frappe.msgprint("Select a customer"); return; } + var doc = { doctype: "Scheduled Pickup", pickup_date: $("#sp-pickup_date").val(), pickup_type: $("#sp-pickup_type").val(), - customer_number: customer_control ? customer_control.get_value() : "", + customer_number: customerVal, company_name: $("#sp-company_name").val(), contact_name: $("#sp-contact_name").val(), contact_phone: $("#sp-contact_phone").val(), @@ -365,7 +378,6 @@ frappe.pages['receiving'].on_page_load = function(wrapper) { needs_aor: $("#sp-needs_aor").is(":checked") ? 1 : 0, needs_cod: $("#sp-needs_cod").is(":checked") ? 1 : 0, notes: $("#sp-notes").val(), - legacy_notes: $("#sp-legacy_notes").val(), status: "Scheduled" }; frappe.call({ @@ -412,7 +424,7 @@ frappe.pages['receiving'].on_page_load = function(wrapper) { $("#" + key + "-count").text("(" + trucks[t].length + " stops)"); $("#" + key + "-stops").html(trucks[t].map(function(p, i) { return stopCard(p, i + 1); }).join("")); }); - $("#unassigned-count").text("(" + trucks["Unassigned"].length + " stops)"); + $("#unassigned-count").text("(" + trucks["Unassigned"].length + ")"); $("#unassigned-stops").html(trucks["Unassigned"].map(function(p) { return stopCard(p, 0); }).join("")); } @@ -465,6 +477,7 @@ frappe.pages['receiving'].on_page_load = function(wrapper) { // ── Stage C: Check-in ── var checkin_pickup_control = null; + var current_pickup_details = null; function loadCheckins() { frappe.call({ @@ -478,25 +491,28 @@ frappe.pages['receiving'].on_page_load = function(wrapper) { function renderCheckinTable(checkins) { var tbody = $("#checkin-tbody"); if (!checkins.length) { - tbody.html('No check-ins yet'); + tbody.html('No check-ins yet'); return; } tbody.html(checkins.map(function(c) { - return '' + esc(c.pickup_date || "") + '' + - '' + esc(c.company_name || "") + '' + - '' + esc(c.pickup_type || "") + '' + - '' + (c.estimated_items || "—") + '' + - '' + (c.estimated_weight || "—") + '' + - '' + esc(c.load_contents || "") + '' + - '' + esc(c.data_status || "—") + '' + + var palletInfo = (c.pallets || []).map(function(p) { return esc(p.pallet_number || p.name); }).join(", "); + return '' + + '' + esc(c.incoming_date || "") + '' + + '' + esc(c.customer_name || c.customer || "") + '' + + '' + esc(c.name || "") + '' + + '' + (c.pallet_count || 0) + '' + + '' + (c.total_weight || "—") + '' + + '' + esc(c.data_status || "") + '' + '' + esc(c.red_r2 || "—") + '' + - '' + esc(c.status || "") + ''; + '' + palletInfo + ''; }).join("")); } $("#btn-new-checkin").on("click", function() { $("#checkin-form").show(); $("#ci-received_date").val(frappe.datetime.nowdate()); + $("#ci-pickup-details").hide(); + current_pickup_details = null; checkin_pickup_control = frappe.ui.form.make_control({ parent: $("#ci-pickup-control"), df: { @@ -512,6 +528,14 @@ frappe.pages['receiving'].on_page_load = function(wrapper) { ["Scheduled Pickup", "status", "in", ["Scheduled", "Routed", "In Progress"]] ] }; + }, + onchange: function() { + var val = checkin_pickup_control ? checkin_pickup_control.get_value() : ""; + if (val) loadPickupDetails(val); + else { + $("#ci-pickup-details").hide(); + current_pickup_details = null; + } } }, only_input: true, @@ -521,6 +545,42 @@ frappe.pages['receiving'].on_page_load = function(wrapper) { $("#ci-pickup-control .help-box").remove(); }); + function loadPickupDetails(pickup_name) { + frappe.call({ + method: "westech_r2.api.receiving_api.get_pickup_details", + args: { pickup_name: pickup_name }, + callback: function(r) { + if (!r.message) return; + current_pickup_details = r.message; + var d = r.message; + + $("#ci-pickup-details").show(); + $("#ci-customer-info").text((d.company_name || d.customer_number || "Unknown") + (d.red_r2 ? " — " + d.red_r2 : "")); + $("#ci-address-info").text((d.address_line || "") + (d.city ? ", " + d.city : "") + (d.state ? ", " + d.state : "") + (d.zip_code ? " " + d.zip_code : "")); + $("#ci-contact-info").text((d.contact_name || "") + (d.contact_phone ? " • " + d.contact_phone : "") + (d.contact_email ? " • " + d.contact_email : "")); + + // Special handling for RED/NIST + if (d.red_r2 && d.red_r2 !== "Neither" && d.red_r2 !== "") { + $("#ci-special-handling").show().html("⚠ " + esc(d.red_r2) + " — Special handling required" + (d.needs_aor ? " • AoR ✓" : "") + (d.needs_cod ? " • CoD ✓" : "")); + } else { + $("#ci-special-handling").hide(); + } + + // Notes + if (d.notes) { + $("#ci-pickup-notes").show().html("Notes: " + esc(d.notes)); + } else { + $("#ci-pickup-notes").hide(); + } + + // Pre-fill check-in fields from pickup + $("#ci-actual_pallets").val(d.estimated_items || 1); + $("#ci-data_status").val(d.data_status || ""); + $("#ci-red_r2").val(d.red_r2 || ""); + } + }); + } + $("#btn-cancel-checkin").on("click", function() { $("#checkin-form").hide(); }); @@ -530,27 +590,37 @@ frappe.pages['receiving'].on_page_load = function(wrapper) { var pickupName = checkin_pickup_control ? checkin_pickup_control.get_value() : ""; if (!pickupName) { frappe.msgprint("Select a pickup"); return; } - var update = {}; - update.status = "Complete"; - if ($("#ci-actual_pallets").val()) update.estimated_items = parseInt($("#ci-actual_pallets").val()); - if ($("#ci-actual_weight").val()) update.estimated_weight = $("#ci-actual_weight").val(); - if ($("#ci-load_contents").val()) update.load_contents = $("#ci-load_contents").val(); + var actualPallets = parseInt($("#ci-actual_pallets").val()) || 0; + if (actualPallets < 1) { frappe.msgprint("Enter at least 1 pallet"); return; } - frappe.call({ - method: "frappe.client.set_value", - args: { - doctype: "Scheduled Pickup", - name: pickupName, - fieldname: update - }, - callback: function(r) { - if (r.message) { - frappe.show_alert({ message: "Load checked in", indicator: "green" }); - $("#checkin-form").hide(); - loadCheckins(); - } + frappe.confirm( + "Check in " + actualPallets + " pallet(s) for this load?", + function() { + frappe.call({ + method: "westech_r2.api.receiving_api.checkin_load", + args: { + pickup_name: pickupName, + received_date: $("#ci-received_date").val(), + actual_pallets: actualPallets, + total_weight: $("#ci-total_weight").val(), + load_contents: $("#ci-load_contents").val(), + data_status: $("#ci-data_status").val(), + red_r2: $("#ci-red_r2").val() + }, + callback: function(r) { + if (r.message && r.message.success) { + frappe.show_alert({ + message: "Load checked in! Created " + r.message.pallets_created + " pallet(s) in Load " + r.message.load, + indicator: "green" + }); + $("#checkin-form").hide(); + loadCheckins(); + loadPickups(); + } + } + }); } - }); + ); }); $("#btn-cor-report").on("click", function() { @@ -563,4 +633,5 @@ frappe.pages['receiving'].on_page_load = function(wrapper) { // ── Init ── loadPickups(); -}; + loadCheckins(); +}; \ No newline at end of file diff --git a/westech_r2/page/receiving/receiving.json b/westech_r2/_archive_2level/page/receiving/receiving.json similarity index 100% rename from westech_r2/page/receiving/receiving.json rename to westech_r2/_archive_2level/page/receiving/receiving.json diff --git a/westech_r2/page/receiving/receiving.py b/westech_r2/_archive_2level/page/receiving/receiving.py similarity index 100% rename from westech_r2/page/receiving/receiving.py rename to westech_r2/_archive_2level/page/receiving/receiving.py diff --git a/westech_r2/page/route_planner/__init__.py b/westech_r2/_archive_2level/page/route_planner/__init__.py similarity index 100% rename from westech_r2/page/route_planner/__init__.py rename to westech_r2/_archive_2level/page/route_planner/__init__.py diff --git a/westech_r2/page/route_planner/route-planner.html b/westech_r2/_archive_2level/page/route_planner/route-planner.html similarity index 100% rename from westech_r2/page/route_planner/route-planner.html rename to westech_r2/_archive_2level/page/route_planner/route-planner.html diff --git a/westech_r2/page/route_planner/route-planner.js b/westech_r2/_archive_2level/page/route_planner/route-planner.js similarity index 100% rename from westech_r2/page/route_planner/route-planner.js rename to westech_r2/_archive_2level/page/route_planner/route-planner.js diff --git a/westech_r2/page/route_planner/route-planner.json b/westech_r2/_archive_2level/page/route_planner/route-planner.json similarity index 100% rename from westech_r2/page/route_planner/route-planner.json rename to westech_r2/_archive_2level/page/route_planner/route-planner.json diff --git a/westech_r2/page/route_planner/route-planner.py b/westech_r2/_archive_2level/page/route_planner/route-planner.py similarity index 100% rename from westech_r2/page/route_planner/route-planner.py rename to westech_r2/_archive_2level/page/route_planner/route-planner.py diff --git a/westech_r2/page/route_planner/route_planner.html b/westech_r2/_archive_2level/page/route_planner/route_planner.html similarity index 100% rename from westech_r2/page/route_planner/route_planner.html rename to westech_r2/_archive_2level/page/route_planner/route_planner.html diff --git a/westech_r2/page/route_planner/route_planner.js b/westech_r2/_archive_2level/page/route_planner/route_planner.js similarity index 100% rename from westech_r2/page/route_planner/route_planner.js rename to westech_r2/_archive_2level/page/route_planner/route_planner.js diff --git a/westech_r2/page/route_planner/route_planner.json b/westech_r2/_archive_2level/page/route_planner/route_planner.json similarity index 100% rename from westech_r2/page/route_planner/route_planner.json rename to westech_r2/_archive_2level/page/route_planner/route_planner.json diff --git a/westech_r2/page/route_planner/route_planner.py b/westech_r2/_archive_2level/page/route_planner/route_planner.py similarity index 100% rename from westech_r2/page/route_planner/route_planner.py rename to westech_r2/_archive_2level/page/route_planner/route_planner.py diff --git a/westech_r2/page/wes-ai/__init__.py b/westech_r2/_archive_2level/page/wes-ai/__init__.py similarity index 100% rename from westech_r2/page/wes-ai/__init__.py rename to westech_r2/_archive_2level/page/wes-ai/__init__.py diff --git a/westech_r2/page/wes-ai/wes-ai.css b/westech_r2/_archive_2level/page/wes-ai/wes-ai.css similarity index 100% rename from westech_r2/page/wes-ai/wes-ai.css rename to westech_r2/_archive_2level/page/wes-ai/wes-ai.css diff --git a/westech_r2/page/wes-ai/wes-ai.js b/westech_r2/_archive_2level/page/wes-ai/wes-ai.js similarity index 100% rename from westech_r2/page/wes-ai/wes-ai.js rename to westech_r2/_archive_2level/page/wes-ai/wes-ai.js diff --git a/westech_r2/page/wes-ai/wes-ai.json b/westech_r2/_archive_2level/page/wes-ai/wes-ai.json similarity index 100% rename from westech_r2/page/wes-ai/wes-ai.json rename to westech_r2/_archive_2level/page/wes-ai/wes-ai.json diff --git a/westech_r2/page/wes-ai/wes-ai.py b/westech_r2/_archive_2level/page/wes-ai/wes-ai.py similarity index 100% rename from westech_r2/page/wes-ai/wes-ai.py rename to westech_r2/_archive_2level/page/wes-ai/wes-ai.py diff --git a/westech_r2/page/wes-ai/wes_ai.js b/westech_r2/_archive_2level/page/wes-ai/wes_ai.js similarity index 100% rename from westech_r2/page/wes-ai/wes_ai.js rename to westech_r2/_archive_2level/page/wes-ai/wes_ai.js diff --git a/westech_r2/api/__pycache__/__init__.cpython-312.pyc b/westech_r2/api/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 5bef39a..0000000 Binary files a/westech_r2/api/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/westech_r2/api/__pycache__/receiving_api.cpython-312.pyc b/westech_r2/api/__pycache__/receiving_api.cpython-312.pyc deleted file mode 100644 index 8541d68..0000000 Binary files a/westech_r2/api/__pycache__/receiving_api.cpython-312.pyc and /dev/null differ diff --git a/westech_r2/api/__pycache__/sales.cpython-312.pyc b/westech_r2/api/__pycache__/sales.cpython-312.pyc deleted file mode 100644 index 57d58b9..0000000 Binary files a/westech_r2/api/__pycache__/sales.cpython-312.pyc and /dev/null differ diff --git a/westech_r2/api/receiving_api.py b/westech_r2/api/receiving_api.py deleted file mode 100644 index 07b203f..0000000 --- a/westech_r2/api/receiving_api.py +++ /dev/null @@ -1,322 +0,0 @@ -import json -import frappe -from frappe.utils import today, getdate, add_days -from datetime import timedelta - -@frappe.whitelist() -def get_pickups(date=None): - """Fetch scheduled pickups with optional date filter. - Returns pickups, calendar (next 30 days), and weekly chart data.""" - filters = [] - if date: - filters.append(["Scheduled Pickup", "pickup_date", "=", date]) - - fields = [ - "name", "pickup_date", "pickup_type", "status", "truck", "stop_order", - "customer_number", "company_name", - "contact_name", "contact_phone", "contact_email", - "address_line", "city", "state", "zip_code", - "latitude", "longitude", - "estimated_items", "estimated_weight", "load_contents", - "num_labels", "data_status", "red_r2", - "notes", "legacy_notes", "needs_aor", "needs_cod", - ] - - pickups = frappe.get_list("Scheduled Pickup", - fields=fields, - filters=filters if filters else None, - order_by="pickup_date asc, stop_order asc", - limit_page_length=500, - ) - - # Build calendar data (next 30 days) - from_date = getdate(today()) - to_date = add_days(from_date, 30) - - all_pickups = frappe.get_list("Scheduled Pickup", - fields=["pickup_date"], - filters=[["Scheduled Pickup", "pickup_date", ">=", str(from_date)], - ["Scheduled Pickup", "pickup_date", "<=", str(to_date)]], - limit_page_length=500, - ) - - pickup_counts = {} - for p in all_pickups: - d = p.get("pickup_date", "") - if d: - pickup_counts[d] = pickup_counts.get(d, 0) + 1 - - calendar = [] - for i in range(30): - d = add_days(from_date, i) - ds = str(d) - calendar.append({"date": ds, "count": pickup_counts.get(ds, 0)}) - - # Build weekly chart data (last 12 weeks) - weekly = [] - for i in range(11, -1, -1): - week_start = add_days(from_date, -(from_date.weekday() + 7 * i)) - week_end = add_days(week_start, 6) - count = 0 - for d_str, c in pickup_counts.items(): - try: - d = getdate(d_str) - if week_start <= d <= week_end: - count += c - except (ValueError, TypeError): - pass - weekly.append({"label": week_start.strftime("%m/%d"), "count": count}) - - return { - "pickups": pickups, - "calendar": calendar, - "weekly": weekly, - } - - -@frappe.whitelist() -def auto_route(date=None): - """Auto-assign pickups to trucks based on capacity and proximity.""" - if not date: - date = today() - - pickups = frappe.get_list("Scheduled Pickup", - filters={"pickup_date": date}, - fields=["name", "company_name", "estimated_items", "estimated_weight", - "latitude", "longitude", "pickup_type"], - limit_page_length=200, - ) - - if not pickups: - return {"success": True, "assigned": 0} - - trucks = ["Truck 1", "Truck 2", "Truck 3"] - sorted_p = sorted(pickups, key=lambda p: (float(p.get("latitude") or 0), float(p.get("longitude") or 0))) - n = len(sorted_p) - assigned = 0 - - for i, p in enumerate(sorted_p): - if p.get("pickup_type") == "Drop-off": - truck = "" - else: - truck = trucks[i % 3] if n <= 3 else trucks[min(i * 3 // n, 2)] - - doc = frappe.get_doc("Scheduled Pickup", p["name"]) - doc.truck = truck - doc.status = "Routed" if truck else "Scheduled" - doc.stop_order = i + 1 - doc.save() - assigned += 1 - - frappe.db.commit() - return {"success": True, "assigned": assigned} - - -@frappe.whitelist() -def get_checkins(): - """Fetch completed check-ins.""" - checkins = frappe.get_list("Scheduled Pickup", - filters={"status": ["in", ["Complete", "In Progress"]]}, - fields=["name", "pickup_date", "pickup_type", "status", - "company_name", "customer_number", - "estimated_items", "estimated_weight", "load_contents", - "data_status", "red_r2", "notes"], - order_by="pickup_date desc", - limit_page_length=100, - ) - return {"checkins": checkins} - - -@frappe.whitelist() -def cor_report(): - """Generate Certificate of Recycling report.""" - pickups = frappe.get_list("Scheduled Pickup", - filters={"status": "Complete"}, - fields=["name", "pickup_date", "company_name", "customer_number", - "estimated_items", "estimated_weight", "load_contents", - "data_status", "red_r2", "needs_aor", "needs_cod"], - order_by="pickup_date desc", - limit_page_length=200, - ) - - html = "CoR Report" - html += "" - html += "

Certificate of Recycling (CoR) Report

" - html += "

Generated: " + frappe.utils.now() + "

" - html += "

Total completed loads: " + str(len(pickups)) + "

" - - if pickups: - html += "" - html += "" - for p in pickups: - html += "" - html += "" - html += "" - html += "" - html += "" - html += "" - html += "" - html += "" - html += "" - html += "
DateCustomerItemsWeightContentsData StatusRED/R2AoRCoD
" + str(p.get("pickup_date", "")) + "" + str(p.get("company_name", "")) + "" + str(p.get("estimated_items", "")) + "" + str(p.get("estimated_weight", "")) + "" + str(p.get("load_contents", "")) + "" + str(p.get("data_status", "")) + "" + str(p.get("red_r2", "")) + "" + ("✓" if p.get("needs_aor") else "") + "" + ("✓" if p.get("needs_cod") else "") + "
" - else: - html += "

No completed loads found.

" - - html += "" - frappe.local.response["type"] = "html" - frappe.local.response["page_content"] = html - - -@frappe.whitelist() -def print_route_sheet(date=None): - """Generate printable route sheet.""" - if not date: - date = today() - - filters = {"pickup_date": date} if date else {} - pickups = frappe.get_list("Scheduled Pickup", - filters=filters, - fields=["name", "pickup_date", "pickup_type", "status", "truck", "stop_order", - "company_name", "contact_name", "contact_phone", "contact_email", - "address_line", "city", "state", "zip_code", - "estimated_items", "estimated_weight", "load_contents", - "data_status", "red_r2", "needs_aor", "needs_cod", "notes"], - order_by="truck asc, stop_order asc", - limit_page_length=200, - ) - - trucks = {} - unassigned = [] - for p in pickups: - t = p.get("truck", "") - if t and t != "Unassigned": - trucks.setdefault(t, []).append(p) - else: - unassigned.append(p) - - html = "Route Sheet" - html += "" - html += "

Route Sheet — " + str(date or "Today") + "

" - - for truck_name, stops in sorted(trucks.items()): - html += '
🚛 ' + truck_name + " — " + str(len(stops)) + " stops
" - html += "" - html += "" - for i, s in enumerate(stops, 1): - addr = str(s.get("address_line", "")) + ", " + str(s.get("city", "")) + ", " + str(s.get("state", "")) + " " + str(s.get("zip_code", "")) - html += "" - html += "" - html += "" - html += "" - html += "" - html += "" - html += "" - html += "
#CustomerAddressContactItemsWeightDataRED/R2AoRCoDNotes
" + str(i) + "" + str(s.get("company_name", "")) + "" + addr + "" + str(s.get("contact_name", "")) + "
" + str(s.get("contact_phone", "")) + "
" + str(s.get("estimated_items", "")) + "" + str(s.get("estimated_weight", "")) + "" + str(s.get("data_status", "")) + "" + str(s.get("red_r2", "")) + "" + ("✓" if s.get("needs_aor") else "") + "" + ("✓" if s.get("needs_cod") else "") + "" + str(s.get("notes", "")) + "
" - - if unassigned: - html += "

Unassigned

" - for s in unassigned: - html += "" - html += "" - html += "" - html += "
CustomerAddressNotes
" + str(s.get("company_name", "")) + "" + str(s.get("address_line", "")) + "" + str(s.get("notes", "")) + "
" - - html += "" - frappe.local.response["type"] = "html" - frappe.local.response["page_content"] = html - - -@frappe.whitelist() -def print_green_sheet(date=None): - """Generate Green Sheet printout. - ⚠️ TODO: Green Sheet template needs to be filled in with the actual form layout.""" - if not date: - date = today() - - filters = {"pickup_date": date} if date else {} - pickups = frappe.get_list("Scheduled Pickup", - filters=filters, - fields=["name", "pickup_date", "pickup_type", "company_name", - "contact_name", "contact_phone", "address_line", "city", "state", "zip_code", - "estimated_items", "estimated_weight", "load_contents", - "data_status", "red_r2", "needs_aor", "needs_cod", "notes"], - order_by="truck asc, stop_order asc", - limit_page_length=200, - ) - - html = "Green Sheet" - html += "" - html += "

🟢 Green Sheet — " + str(date or "Today") + "

" - html += '

⚠️ This is a PLACEHOLDER template. Replace with actual Green Sheet layout once the form spec is provided.

' - - for p in pickups: - html += '
' - fields = [ - ("Customer", p.get("company_name", "")), - ("Contact", p.get("contact_name", "")), - ("Phone", p.get("contact_phone", "")), - ("Address", str(p.get("address_line", "")) + ", " + str(p.get("city", "")) + ", " + str(p.get("state", "")) + " " + str(p.get("zip_code", ""))), - ("Est. Items", p.get("estimated_items", "")), - ("Est. Weight", p.get("estimated_weight", "")), - ("Load Contents", p.get("load_contents", "")), - ("Data Status", p.get("data_status", "")), - ("RED/R2", p.get("red_r2", "")), - ("Needs AoR", "✓" if p.get("needs_aor") else ""), - ("Needs CoD", "✓" if p.get("needs_cod") else ""), - ("Notes", p.get("notes", "")), - ] - for label, val in fields: - html += '
' + label + ':
' + str(val) + '
' - html += '
' - - html += "" - frappe.local.response["type"] = "html" - frappe.local.response["page_content"] = html - - -@frappe.whitelist() -def print_labels(date=None): - """Generate printable labels.""" - if not date: - date = today() - - filters = {"pickup_date": date} if date else {} - pickups = frappe.get_list("Scheduled Pickup", - filters=filters, - fields=["name", "company_name", "pickup_date", "num_labels", "data_status", "red_r2"], - limit_page_length=200, - ) - - html = "Labels" - html += "" - - for p in pickups: - n = p.get("num_labels") or 1 - for _ in range(n): - html += '
' - html += '
' + str(p.get("company_name", "")) + '
' - html += '
' + str(p.get("pickup_date", "")) + '
' - html += '
' + str(p.get("data_status", "")) + " | " + str(p.get("red_r2", "")) + '
' - html += '
' - - html += "" - frappe.local.response["type"] = "html" - frappe.local.response["page_content"] = html diff --git a/westech_r2/hooks.py b/westech_r2/hooks.py index d47bed1..9001763 100644 --- a/westech_r2/hooks.py +++ b/westech_r2/hooks.py @@ -19,16 +19,16 @@ required_apps = ["erpnext"] # DocType event hooks doc_events = { "Pallet": { - "before_save": "westech_r2.doctype.pallet.pallet.update_serial_nos", + "before_save": "westech_r2.westech_r2.doctype.pallet.pallet.update_serial_nos", }, "Scheduled Pickup": { - "before_save": "westech_r2.doctype.scheduled_pickup.scheduled_pickup.set_title", + "before_save": "westech_r2.westech_r2.doctype.scheduled_pickup.scheduled_pickup.set_title", }, "Serial No": { - "validate": "westech_r2.api.serial_hooks.validate_hardware_tests", + "validate": "westech_r2.westech_r2.api.serial_hooks.validate_hardware_tests", }, "Load": { - "before_save": "westech_r2.doctype.load.load.calculate_totals", + "before_save": "westech_r2.westech_r2.doctype.load.load.calculate_totals", }, } diff --git a/westech_r2/westech_r2/api/__init__.py b/westech_r2/westech_r2/api/__init__.py index 99e389c..8b13789 100644 --- a/westech_r2/westech_r2/api/__init__.py +++ b/westech_r2/westech_r2/api/__init__.py @@ -1,3 +1 @@ -from westech_r2.api import sales -from westech_r2.api import receiving_api diff --git a/westech_r2/westech_r2/api/customer_records_api.py b/westech_r2/westech_r2/api/customer_records_api.py new file mode 100644 index 0000000..a9254aa --- /dev/null +++ b/westech_r2/westech_r2/api/customer_records_api.py @@ -0,0 +1,366 @@ +import frappe +import json + + +@frappe.whitelist() +def get_records(start=0, page_length=5000): + """Fetch all Customer records for the modify records form.""" + records = frappe.get_all( + "Customer", + fields=[ + "name", + "customer_name", + "customer_number", + "primary_address", + "customer_primary_address", + "mobile_no", + "email_id", + "contact_persons", + "hours_of_operation", + "legacy_notes", + "contacted_date", + "follow_up_date", + "last_pu_date", + "legacy_record_number", + "do_not_call", + "prospect_status", + "customer_type", + "customer_group", + "territory", + "disabled", + ], + order_by="customer_name asc", + limit_page_length=int(page_length), + limit_start=int(start), + ) + + # Enrich with address and contact details + for r in records: + addr_name = r.get("customer_primary_address") + if addr_name and frappe.db.exists("Address", addr_name): + addr = frappe.db.get_value( + "Address", addr_name, + ["address_line1", "city", "state", "pincode", "phone"], + as_dict=True, + ) + if addr: + r["address_line1"] = addr.address_line1 or "" + r["city"] = addr.city or "" + r["state"] = addr.state or "" + r["zip"] = addr.pincode or "" + r["address_phone"] = addr.phone or "" + else: + r["address_line1"] = "" + r["city"] = "" + r["state"] = "" + r["zip"] = "" + r["address_phone"] = "" + else: + r["address_line1"] = "" + r["city"] = "" + r["state"] = "" + r["zip"] = "" + r["address_phone"] = "" + + # Get contact details + contacts = frappe.get_all( + "Dynamic Link", + filters={"link_doctype": "Customer", "link_name": r["name"], "parenttype": "Contact"}, + fields=["parent"], + ) + contact_lines = [] + for c in contacts: + contact = frappe.db.get_value( + "Contact", c.parent, ["first_name", "last_name", "email_id", "phone", "mobile_no"], + as_dict=True, + ) + if contact: + cname = f"{contact.first_name or ''} {contact.last_name or ''}".strip() + email = contact.email_id or "" + phone = contact.phone or "" + mobile = contact.mobile_no or "" + parts = [cname] + if email: + parts.append(f"- {email}") + if phone: + parts.append(f"- {phone}") + elif mobile: + parts.append(f"- {mobile}") + contact_lines.append(" ".join(parts)) + r["contact_details"] = "\n".join(contact_lines) if contact_lines else r.get("contact_persons", "") + + return records + + +@frappe.whitelist() +def get_record(name): + """Fetch a single Customer record by name.""" + r = frappe.get_doc("Customer", name) + result = { + "name": r.name, + "customer_name": r.customer_name or "", + "customer_number": r.customer_number or "", + "primary_address": r.primary_address or "", + "mobile_no": r.mobile_no or "", + "email_id": r.email_id or "", + "contact_persons": r.contact_persons or "", + "hours_of_operation": r.hours_of_operation or "", + "legacy_notes": r.legacy_notes or "", + "contacted_date": str(r.contacted_date) if r.contacted_date else "", + "follow_up_date": str(r.follow_up_date) if r.follow_up_date else "", + "last_pu_date": str(r.last_pu_date) if r.last_pu_date else "", + "legacy_record_number": r.legacy_record_number or "", + "do_not_call": r.do_not_call or 0, + "prospect_status": r.prospect_status or "", + "customer_type": r.customer_type or "", + "customer_group": r.customer_group or "", + "territory": r.territory or "", + "disabled": r.disabled or 0, + } + + # Get address + addr_name = r.customer_primary_address + if addr_name and frappe.db.exists("Address", addr_name): + addr = frappe.db.get_value( + "Address", addr_name, + ["address_line1", "city", "state", "pincode", "phone"], + as_dict=True, + ) + if addr: + result["address_line1"] = addr.address_line1 or "" + result["city"] = addr.city or "" + result["state"] = addr.state or "" + result["zip"] = addr.pincode or "" + result["address_phone"] = addr.phone or "" + else: + result["address_line1"] = "" + result["city"] = "" + result["state"] = "" + result["zip"] = "" + result["address_phone"] = "" + else: + result["address_line1"] = "" + result["city"] = "" + result["state"] = "" + result["zip"] = "" + result["address_phone"] = "" + + # Get contacts + contacts = frappe.get_all( + "Dynamic Link", + filters={"link_doctype": "Customer", "link_name": r.name, "parenttype": "Contact"}, + fields=["parent"], + ) + contact_lines = [] + for c in contacts: + contact = frappe.db.get_value( + "Contact", c.parent, ["first_name", "last_name", "email_id", "phone", "mobile_no"], + as_dict=True, + ) + if contact: + cname = f"{contact.first_name or ''} {contact.last_name or ''}".strip() + email = contact.email_id or "" + phone = contact.phone or "" + mobile = contact.mobile_no or "" + parts = [cname] + if email: + parts.append(f"- {email}") + if phone: + parts.append(f"- {phone}") + elif mobile: + parts.append(f"- {mobile}") + contact_lines.append(" ".join(parts)) + result["contact_details"] = "\n".join(contact_lines) if contact_lines else r.contact_persons or "" + + return result + + +@frappe.whitelist() +def save_record(data): + """Update an existing Customer record.""" + data = frappe.parse_json(data) + name = data.get("name") + if not name: + return {"status": "error", "message": "No record name provided"} + + doc = frappe.get_doc("Customer", name) + + # Update basic fields + field_map = { + "customer_name": "customer_name", + "email_id": "email_id", + "mobile_no": "mobile_no", + "contact_persons": "contact_persons", + "hours_of_operation": "hours_of_operation", + "legacy_notes": "legacy_notes", + "prospect_status": "prospect_status", + } + for src_key, doc_key in field_map.items(): + if src_key in data: + setattr(doc, doc_key, data[src_key]) + + if "contacted_date" in data: + doc.contacted_date = data["contacted_date"] or None + if "follow_up_date" in data: + doc.follow_up_date = data["follow_up_date"] or None + if "last_pu_date" in data: + doc.last_pu_date = data["last_pu_date"] or None + if "do_not_call" in data: + doc.do_not_call = 1 if data["do_not_call"] else 0 + + # Update primary address if it exists + addr_name = doc.customer_primary_address + if addr_name and frappe.db.exists("Address", addr_name): + addr = frappe.get_doc("Address", addr_name) + if "address_line1" in data: + addr.address_line1 = data["address_line1"] + if "city" in data: + addr.city = data["city"] + if "zip" in data: + addr.pincode = data["zip"] + if "address_phone" in data: + addr.phone = data["address_phone"] + addr.save(ignore_permissions=True) + + doc.save(ignore_permissions=True) + frappe.db.commit() + + return {"status": "ok", "message": f"Saved {name}"} + + +@frappe.whitelist() +def delete_record(name): + """Delete a Customer record.""" + if not name: + return {"status": "error", "message": "No record name provided"} + + frappe.delete_doc("Customer", name, ignore_permissions=True) + frappe.db.commit() + return {"status": "ok", "message": f"Deleted {name}"} + + +@frappe.whitelist() +def search_records(field=None, value=None): + """Search Customer records by field.""" + if not value: + return [] + + like_val = f"%{value}%" + + or_filters = [ + ["Customer", "customer_name", "like", like_val], + ["Customer", "customer_number", "like", like_val], + ["Customer", "primary_address", "like", like_val], + ["Customer", "email_id", "like", like_val], + ["Customer", "mobile_no", "like", like_val], + ["Customer", "contact_persons", "like", like_val], + ["Customer", "legacy_notes", "like", like_val], + ["Customer", "hours_of_operation", "like", like_val], + ["Customer", "legacy_record_number", "like", like_val], + ] + + records = frappe.get_all( + "Customer", + or_filters=or_filters, + fields=[ + "name", "customer_name", "customer_number", "primary_address", + "customer_primary_address", "mobile_no", "email_id", "contact_persons", + "hours_of_operation", "legacy_notes", "contacted_date", "follow_up_date", + "last_pu_date", "legacy_record_number", "do_not_call", "prospect_status", + "customer_type", "customer_group", "territory", "disabled", + ], + order_by="customer_name asc", + limit_page_length=100, + ) + + # Also search addresses for city/zip matches + addr_matches = frappe.get_all( + "Address", + or_filters=[ + ["Address", "city", "like", like_val], + ["Address", "pincode", "like", like_val], + ["Address", "address_line1", "like", like_val], + ], + fields=["name", "address_line1", "city", "pincode"], + limit_page_length=100, + ) + + existing_names = {r["name"] for r in records} + for addr in addr_matches: + link = frappe.get_all( + "Dynamic Link", + filters={"parent": addr.name, "parenttype": "Address", "link_doctype": "Customer"}, + fields=["link_name"], + ) + for l in link: + if l.link_name not in existing_names: + cust = frappe.get_all( + "Customer", + filters={"name": l.link_name}, + fields=[ + "name", "customer_name", "customer_number", "primary_address", + "customer_primary_address", "mobile_no", "email_id", "contact_persons", + "hours_of_operation", "legacy_notes", "contacted_date", "follow_up_date", + "last_pu_date", "legacy_record_number", "do_not_call", "prospect_status", + "customer_type", "customer_group", "territory", "disabled", + ], + ) + if cust: + records.append(cust[0]) + existing_names.add(l.link_name) + + # Enrich with address details + for r in records: + addr_name = r.get("customer_primary_address") + if addr_name and frappe.db.exists("Address", addr_name): + addr = frappe.db.get_value( + "Address", addr_name, + ["address_line1", "city", "state", "pincode", "phone"], + as_dict=True, + ) + if addr: + r["address_line1"] = addr.address_line1 or "" + r["city"] = addr.city or "" + r["state"] = addr.state or "" + r["zip"] = addr.pincode or "" + r["address_phone"] = addr.phone or "" + else: + r["address_line1"] = "" + r["city"] = "" + r["state"] = "" + r["zip"] = "" + r["address_phone"] = "" + else: + r["address_line1"] = "" + r["city"] = "" + r["state"] = "" + r["zip"] = "" + r["address_phone"] = "" + + contacts = frappe.get_all( + "Dynamic Link", + filters={"link_doctype": "Customer", "link_name": r["name"], "parenttype": "Contact"}, + fields=["parent"], + ) + contact_lines = [] + for c in contacts: + contact = frappe.db.get_value( + "Contact", c.parent, ["first_name", "last_name", "email_id", "phone", "mobile_no"], + as_dict=True, + ) + if contact: + cname = f"{contact.first_name or ''} {contact.last_name or ''}".strip() + email = contact.email_id or "" + phone = contact.phone or "" + mobile = contact.mobile_no or "" + parts = [cname] + if email: + parts.append(f"- {email}") + if phone: + parts.append(f"- {phone}") + elif mobile: + parts.append(f"- {mobile}") + contact_lines.append(" ".join(parts)) + r["contact_details"] = "\n".join(contact_lines) if contact_lines else r.get("contact_persons", "") + + return records \ No newline at end of file diff --git a/westech_r2/westech_r2/api/receiving_api.py b/westech_r2/westech_r2/api/receiving_api.py index 07b203f..68a6d75 100644 --- a/westech_r2/westech_r2/api/receiving_api.py +++ b/westech_r2/westech_r2/api/receiving_api.py @@ -114,17 +114,149 @@ def auto_route(date=None): @frappe.whitelist() def get_checkins(): - """Fetch completed check-ins.""" - checkins = frappe.get_list("Scheduled Pickup", - filters={"status": ["in", ["Complete", "In Progress"]]}, - fields=["name", "pickup_date", "pickup_type", "status", - "company_name", "customer_number", - "estimated_items", "estimated_weight", "load_contents", - "data_status", "red_r2", "notes"], - order_by="pickup_date desc", + """Fetch completed check-ins — returns Loads with their Pallets.""" + loads = frappe.get_list("Load", + fields=["name", "load_number", "incoming_date", "customer", "customer_name", + "total_devices", "total_weight", "data_status", "red_r2"], + order_by="incoming_date desc", limit_page_length=100, ) - return {"checkins": checkins} + + for load in loads: + pallets = frappe.get_list("Pallet", + filters={"load": load.name}, + fields=["name", "pallet_number", "received_date", "inbound_weight", + "total_items", "data_status", "red_r2", "description", "status"], + limit_page_length=50, + ) + load["pallets"] = pallets + load["pallet_count"] = len(pallets) + + return {"checkins": loads} + + +@frappe.whitelist() +def checkin_load(pickup_name, received_date, actual_pallets, total_weight, load_contents, data_status=None, red_r2=None): + """Check in a load: create Load + Pallets, mark pickup Complete.""" + # Get the pickup + pickup = frappe.get_doc("Scheduled Pickup", pickup_name) + + if not actual_pallets or int(actual_pallets) < 1: + frappe.throw("Actual pallet count must be at least 1") + + actual_pallets = int(actual_pallets) + + # Resolve customer - customer_number on pickup is a Link to Customer + customer_id = pickup.customer_number + if not customer_id or not frappe.db.exists("Customer", customer_id): + frappe.throw("Customer {} not found. Please verify the customer on the pickup.".format(customer_id)) + + # Generate Load name: MMDDYYYY-CustomerNumber format + from datetime import datetime + try: + dt = datetime.strptime(received_date, "%Y-%m-%d") + date_part = dt.strftime("%m%d%Y") + except (ValueError, TypeError): + date_part = received_date.replace("-", "") + + cust_num = customer_id + load_name = "{}-{}".format(date_part, cust_num) + + # Make unique if name already exists + base_name = load_name + counter = 1 + while frappe.db.exists("Load", load_name): + load_name = "{}-{}".format(base_name, counter) + counter += 1 + + # Create Load + load = frappe.get_doc({ + "doctype": "Load", + "name": load_name, + "load_number": load_name, + "incoming_date": received_date, + "customer": customer_id, + "customer_name": pickup.company_name or "", + "customer_number": customer_id, + "data_status": data_status or pickup.data_status or "", + "red_r2": red_r2 or pickup.red_r2 or "", + "total_weight": float(total_weight) if total_weight else 0, + "total_devices": actual_pallets, + "material_items": [{ + "material_type": "# Of Pallets", + "total_count": actual_pallets, + "weight": float(total_weight) if total_weight else 0, + "initial_data_status": data_status or pickup.data_status or "D0", + }], + }) + load.insert() + frappe.db.commit() + + # Create Pallets — autoname=pallet_number, so set name=pallet_number + for i in range(actual_pallets): + pallet_num = "{}-P{}".format(load_name, i + 1) + pallet = frappe.get_doc({ + "doctype": "Pallet", + "name": pallet_num, + "pallet_number": pallet_num, + "received_date": received_date, + "load": load.name, + "company_name": pickup.company_name or "", + "inbound_weight": str(round(float(total_weight) / actual_pallets, 1)) if total_weight and actual_pallets else "", + "description": load_contents or "", + "data_status": data_status or pickup.data_status or "", + "red_r2": red_r2 or pickup.red_r2 or "", + "contact_name": pickup.contact_name or "", + "contact_number": pickup.contact_phone or "", + "contact_email": pickup.contact_email or "", + "address_line": (pickup.address_line or "") + ((", " + pickup.city) if pickup.city else "") + ((", " + pickup.state) if pickup.state else ""), + "needs_aor": pickup.needs_aor or 0, + "needs_cod": pickup.needs_cod or 0, + "notes": pickup.notes or "", + "pickup": pickup.pickup_type or "", + "status": "Received", + }) + pallet.insert() + + frappe.db.commit() + + # Update pickup status + pickup.status = "Complete" + pickup.save() + frappe.db.commit() + + return { + "success": True, + "load": load.name, + "pallets_created": actual_pallets, + } + + +@frappe.whitelist() +def get_pickup_details(pickup_name): + """Get full details of a Scheduled Pickup for the check-in form.""" + pickup = frappe.get_doc("Scheduled Pickup", pickup_name) + return { + "name": pickup.name, + "pickup_date": pickup.pickup_date, + "pickup_type": pickup.pickup_type, + "customer_number": pickup.customer_number, + "company_name": pickup.company_name, + "contact_name": pickup.contact_name, + "contact_phone": pickup.contact_phone, + "contact_email": pickup.contact_email, + "address_line": pickup.address_line, + "city": pickup.city, + "state": pickup.state, + "zip_code": pickup.zip_code, + "estimated_items": pickup.estimated_items, + "estimated_weight": pickup.estimated_weight, + "data_status": pickup.data_status, + "red_r2": pickup.red_r2, + "needs_aor": pickup.needs_aor, + "needs_cod": pickup.needs_cod, + "notes": pickup.notes, + } @frappe.whitelist() @@ -236,52 +368,97 @@ def print_route_sheet(date=None): @frappe.whitelist() def print_green_sheet(date=None): - """Generate Green Sheet printout. - ⚠️ TODO: Green Sheet template needs to be filled in with the actual form layout.""" + """Generate Green Sheet printout for each pallet. + Shows customer info, service level banner, driver instructions, RED LINE instructions.""" if not date: date = today() - filters = {"pickup_date": date} if date else {} - pickups = frappe.get_list("Scheduled Pickup", - filters=filters, - fields=["name", "pickup_date", "pickup_type", "company_name", - "contact_name", "contact_phone", "address_line", "city", "state", "zip_code", - "estimated_items", "estimated_weight", "load_contents", - "data_status", "red_r2", "needs_aor", "needs_cod", "notes"], - order_by="truck asc, stop_order asc", + # Get completed loads for this date + loads = frappe.get_list("Load", + filters={"incoming_date": date}, + fields=["name", "load_number", "customer", "customer_name", + "incoming_date", "total_weight", "data_status", "red_r2"], + order_by="name asc", limit_page_length=200, ) - html = "Green Sheet" + html = "Green Sheets" html += "" - html += "

🟢 Green Sheet — " + str(date or "Today") + "

" - html += '

⚠️ This is a PLACEHOLDER template. Replace with actual Green Sheet layout once the form spec is provided.

' + html += ".green-sheet{border:2px solid #2E7D32;border-radius:6px;padding:16px;margin:20px 0;page-break-after:always;}" + html += ".gs-title{color:#2E7D32;font-size:18px;font-weight:700;border-bottom:2px solid #2E7D32;padding-bottom:4px;}" + html += ".gs-customer{background:#E8F5E9;border:1px solid #66BB6A;border-radius:4px;padding:8px;margin:8px 0;}" + html += ".gs-service-banner{background:#C62828;color:#fff;font-size:16px;font-weight:700;text-align:center;padding:8px;border-radius:4px;margin:8px 0;}" + html += ".gs-driver{background:#F5F5F5;border:1px solid #999;border-radius:4px;padding:8px;margin:8px 0;}" + html += ".gs-redline{background:#FFCDD2;border:2px solid #C62828;border-radius:4px;padding:8px;margin:8px 0;}" + html += ".gs-r2warning{background:#FFF9C4;border:1px solid #F9A825;border-radius:4px;padding:8px;margin:8px 0;font-size:12px;}" + html += "table{border-collapse:collapse;width:100%;margin:8px 0;}" + html += "th,td{border:1px solid #999;padding:6px;text-align:left;font-size:12px;}" + html += "th{background:#2E7D32;color:#fff;}" + html += ".gs-footer{margin-top:12px;font-size:11px;color:#666;border-top:1px solid #ccc;padding-top:8px;}" + html += "@media print{body{margin:10px;}.green-sheet{page-break-after:always;}}" - for p in pickups: - html += '
' - fields = [ - ("Customer", p.get("company_name", "")), - ("Contact", p.get("contact_name", "")), - ("Phone", p.get("contact_phone", "")), - ("Address", str(p.get("address_line", "")) + ", " + str(p.get("city", "")) + ", " + str(p.get("state", "")) + " " + str(p.get("zip_code", ""))), - ("Est. Items", p.get("estimated_items", "")), - ("Est. Weight", p.get("estimated_weight", "")), - ("Load Contents", p.get("load_contents", "")), - ("Data Status", p.get("data_status", "")), - ("RED/R2", p.get("red_r2", "")), - ("Needs AoR", "✓" if p.get("needs_aor") else ""), - ("Needs CoD", "✓" if p.get("needs_cod") else ""), - ("Notes", p.get("notes", "")), - ] - for label, val in fields: - html += '
' + label + ':
' + str(val) + '
' - html += '
' + for load in loads: + pallets = frappe.get_list("Pallet", + filters={"load": load.name}, + fields=["name", "pallet_number", "inbound_weight", "total_items", + "data_status", "red_r2", "description", "needs_aor", "needs_cod", "notes", "status"], + limit_page_length=50, + ) + + for pallet in pallets: + html += '
' + html += '
🟢 GREEN SHEET — Data-Bearing Equipment Tracking
' + html += '
Pallet # ' + str(pallet.get("pallet_number", "")) + ' | Load # ' + str(load.get("name", "")) + ' | ' + str(load.get("incoming_date", "")) + '
' + + # Customer block + html += '
' + service_level = "" + if pallet.get("red_r2"): + service_level = pallet.get("red_r2", "") + html += '(' + str(load.get("customer", "")) + ') — ' + str(service_level) + ' — ' + str(load.get("customer_name", "")) + '' + html += '
' + + # Service level banner (only for RED/NIST) + rr = pallet.get("red_r2", "") + if rr and rr != "Neither": + html += '
SERVICE LEVEL: ' + str(rr).upper() + '
' + + # Driver instructions (notes) + if pallet.get("notes"): + html += '
Driver Instructions:
' + str(pallet.get("notes", "")) + '
' + + # RED LINE instructions (for RED/NIST) + if rr and rr not in ("", "Neither", "R2"): + html += '
⚠ RED LINE INSTRUCTIONS
All data-bearing equipment must be tracked. Destruction method per customer specification.
' + + # Pallet details table + html += '' + html += '' + html += '' + html += '' + html += '' + aor_cor = "" + if pallet.get("needs_aor"): aor_cor += "✓ AoR " + if pallet.get("needs_cod"): aor_cor += "✓ CoD" + html += '' + html += '
Pallet DesignationData Status
' + str(pallet.get("status", "Received")) + '' + str(pallet.get("data_status", "")) + '
Inbound WeightTotal Items
' + str(pallet.get("inbound_weight", "")) + '' + str(pallet.get("total_items", "")) + '
AoR/CoRContents
' + (aor_cor or "None") + '' + str(pallet.get("description", "")) + '
' + + # Material tracking (hand-write on paper) + html += '' + for _ in range(4): + html += '' + html += '
Material%WeightSign OffDate
 
' + + # R2 warning + html += '
⚠ R2 REQUIREMENT: This pallet contains data-bearing equipment. All devices must be tracked through erasure with 5% verification audit.
' + + # Signatures + html += '' + html += '
Received ByInspected ByVerified By
   
' + + # Footer + html += '' + html += '
' html += "" frappe.local.response["type"] = "html" @@ -319,4 +496,4 @@ def print_labels(date=None): html += "" frappe.local.response["type"] = "html" - frappe.local.response["page_content"] = html + frappe.local.response["page_content"] = html \ No newline at end of file diff --git a/westech_r2/westech_r2/doctype/customer_interaction/customer_interaction.json b/westech_r2/westech_r2/doctype/customer_interaction/customer_interaction.json index e1c711d..3d3a2af 100644 --- a/westech_r2/westech_r2/doctype/customer_interaction/customer_interaction.json +++ b/westech_r2/westech_r2/doctype/customer_interaction/customer_interaction.json @@ -129,13 +129,47 @@ "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2026-05-22 11:58:31.649154", + "modified": "2026-05-25 11:33:19.259739", "modified_by": "Administrator", "module": "Westech R2", "name": "Customer Interaction", "naming_rule": "Autoincrement", "owner": "Administrator", - "permissions": [], + "permissions": [ + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock User", + "share": 1, + "write": 1 + } + ], "row_format": "Dynamic", "rows_threshold_for_grid_search": 20, "sort_field": "modified", diff --git a/westech_r2/westech_r2/doctype/pallet/pallet.js b/westech_r2/westech_r2/doctype/pallet/pallet.js index 4a7c269..0312518 100644 --- a/westech_r2/westech_r2/doctype/pallet/pallet.js +++ b/westech_r2/westech_r2/doctype/pallet/pallet.js @@ -17,7 +17,7 @@ frappe.ui.form.on('Pallet', { frappe.msgprint('Please save the Pallet first'); return; } - var url = '/api/method/westech_r2.api.cor_generator.generate_cor?pallet_number=' + encodeURIComponent(frm.doc.pallet_number); + var url = '/api/method/westech_r2.westech_r2.api.cor_generator.generate_cor?pallet_number=' + encodeURIComponent(frm.doc.pallet_number); window.open(url, '_blank'); }, __('Actions')); }, diff --git a/westech_r2/westech_r2/doctype/scheduled_pickup/scheduled_pickup.js b/westech_r2/westech_r2/doctype/scheduled_pickup/scheduled_pickup.js index ecfd2df..f5ad1a5 100644 --- a/westech_r2/westech_r2/doctype/scheduled_pickup/scheduled_pickup.js +++ b/westech_r2/westech_r2/doctype/scheduled_pickup/scheduled_pickup.js @@ -7,17 +7,18 @@ frappe.ui.form.on('Scheduled Pickup', { } frappe.call({ method: 'frappe.client.get', - args: {doctype: 'Supplier', name: customer}, + args: {doctype: 'Customer', name: customer}, callback: function(r) { if (!r.message) return; - var s = r.message; - if (!frm.doc.company_name && s.supplier_name) { - frm.set_value('company_name', s.supplier_name); + var c = r.message; + if (!frm.doc.company_name && c.customer_name) { + frm.set_value('company_name', c.customer_name); } - if (s.supplier_primary_contact) { + // Get primary contact + if (c.customer_primary_contact) { frappe.call({ method: 'frappe.client.get', - args: {doctype: 'Contact', name: s.supplier_primary_contact}, + args: {doctype: 'Contact', name: c.customer_primary_contact}, callback: function(cr) { if (!cr.message) return; var ct = cr.message; @@ -28,10 +29,11 @@ frappe.ui.form.on('Scheduled Pickup', { } }); } - if (s.supplier_primary_address) { + // Get primary address + if (c.customer_primary_address) { frappe.call({ method: 'frappe.client.get', - args: {doctype: 'Address', name: s.supplier_primary_address}, + args: {doctype: 'Address', name: c.customer_primary_address}, callback: function(ar) { if (!ar.message) return; var a = ar.message; @@ -42,11 +44,6 @@ frappe.ui.form.on('Scheduled Pickup', { } }); } - if (s.geocoded && s.latitude && s.longitude) { - frm.set_value('latitude', s.latitude); - frm.set_value('longitude', s.longitude); - frm.set_value('geocoded', 1); - } } }); } diff --git a/westech_r2/westech_r2/page/customer_records/customer-records.json b/westech_r2/westech_r2/page/customer_records/customer-records.json deleted file mode 100644 index 1fbb98a..0000000 --- a/westech_r2/westech_r2/page/customer_records/customer-records.json +++ /dev/null @@ -1 +0,0 @@ -{"content": null,"creation": "2026-05-20 22:00:00.000000","docstatus": 0,"doctype": "Page","idx": 0,"modified": "2026-05-20 22:00:00.000000","modified_by": "Administrator","module": "Westech R2","name": "customer-records","owner": "Administrator","page_name": "customer-records","roles": [{"doctype": "Has Role","idx": 1,"name": "a80mopj93i","parent": "customer-records","parentfield": "roles","parenttype": "Page","role": "All"}],"script": null,"standard": "Yes","style": null,"system_page": 0,"title": "Customer Records"} diff --git a/westech_r2/westech_r2/page/customer_records/customer-records.py b/westech_r2/westech_r2/page/customer_records/customer-records.py new file mode 100644 index 0000000..693d738 --- /dev/null +++ b/westech_r2/westech_r2/page/customer_records/customer-records.py @@ -0,0 +1,416 @@ +import frappe +import json + +@frappe.whitelist() +def get_records(start=0, page_length=1000): + """Fetch all Customer records for the modify records form.""" + records = frappe.get_all( + "Customer", + fields=[ + "name", + "customer_name", + "customer_number", + "primary_address", + "customer_primary_address", + "mobile_no", + "email_id", + "contact_persons", + "hours_of_operation", + "legacy_notes", + "contacted_date", + "follow_up_date", + "last_pu_date", + "legacy_record_number", + "do_not_call", + "prospect_status", + "customer_type", + "customer_group", + "territory", + "disabled", + ], + order_by="customer_name asc", + limit_page_length=int(page_length), + limit_start=int(start), + ) + + # Enrich with address and contact details + for r in records: + # Get address details — primary_address is display string, + # customer_primary_address is the Link to the Address doc + addr_name = r.get("customer_primary_address") or r.get("primary_address") + # Try to find address via Dynamic Link if primary_address is display string + if addr_name and not frappe.db.exists("Address", addr_name): + # primary_address was a display string; find via dynamic link + addr_links = frappe.get_all( + "Dynamic Link", + filters={"link_doctype": "Customer", "link_name": r["name"], "parenttype": "Address"}, + fields=["parent"], + limit=1, + ) + addr_name = addr_links[0].parent if addr_links else None + + if addr_name and frappe.db.exists("Address", addr_name): + addr = frappe.db.get_value( + "Address", addr_name, + ["address_line1", "city", "state", "pincode", "phone"], + as_dict=True, + ) + if addr: + r["address_line1"] = addr.address_line1 or "" + r["city"] = addr.city or "" + r["state"] = addr.state or "" + r["zip"] = addr.pincode or "" + r["address_phone"] = addr.phone or "" + else: + r["address_line1"] = "" + r["city"] = "" + r["state"] = "" + r["zip"] = "" + r["address_phone"] = "" + else: + r["address_line1"] = "" + r["city"] = "" + r["state"] = "" + r["zip"] = "" + r["address_phone"] = "" + + # Get contact details from linked contacts + contacts = frappe.get_all( + "Dynamic Link", + filters={"link_doctype": "Customer", "link_name": r["name"], "parenttype": "Contact"}, + fields=["parent"], + ) + contact_lines = [] + for c in contacts: + contact = frappe.db.get_value( + "Contact", c.parent, ["first_name", "last_name", "email_id", "phone", "mobile_no"], + as_dict=True, + ) + if contact: + name = f"{contact.first_name or ''} {contact.last_name or ''}".strip() + email = contact.email_id or "" + phone = contact.phone or "" + mobile = contact.mobile_no or "" + parts = [name] + if email: + parts.append(f"- {email}") + if phone: + parts.append(f"- {phone}") + elif mobile: + parts.append(f"- {mobile}") + contact_lines.append(" ".join(parts)) + r["contact_details"] = "\n".join(contact_lines) if contact_lines else r.get("contact_persons", "") + + return records + + +@frappe.whitelist() +def get_record(name): + """Fetch a single Customer record by name.""" + r = frappe.get_doc("Customer", name) + result = { + "name": r.name, + "customer_name": r.customer_name or "", + "customer_number": r.customer_number or "", + "primary_address": r.primary_address or "", + "mobile_no": r.mobile_no or "", + "email_id": r.email_id or "", + "contact_persons": r.contact_persons or "", + "hours_of_operation": r.hours_of_operation or "", + "legacy_notes": r.legacy_notes or "", + "contacted_date": str(r.contacted_date) if r.contacted_date else "", + "follow_up_date": str(r.follow_up_date) if r.follow_up_date else "", + "last_pu_date": str(r.last_pu_date) if r.last_pu_date else "", + "legacy_record_number": r.legacy_record_number or "", + "do_not_call": r.do_not_call or 0, + "prospect_status": r.prospect_status or "", + "customer_type": r.customer_type or "", + "customer_group": r.customer_group or "", + "territory": r.territory or "", + "disabled": r.disabled or 0, + } + + # Get address — primary_address on Customer is the display string; + # customer_primary_address is the actual Link to the Address doc + addr_name = r.customer_primary_address + if addr_name: + addr = frappe.db.get_value( + "Address", addr_name, + ["address_line1", "city", "state", "pincode", "phone"], + as_dict=True, + ) + if addr: + result["address_line1"] = addr.address_line1 or "" + result["city"] = addr.city or "" + result["state"] = addr.state or "" + result["zip"] = addr.pincode or "" + result["address_phone"] = addr.phone or "" + else: + result["address_line1"] = "" + result["city"] = "" + result["state"] = "" + result["zip"] = "" + result["address_phone"] = "" + else: + result["address_line1"] = "" + result["city"] = "" + result["state"] = "" + result["zip"] = "" + result["address_phone"] = "" + + # Get contacts + contacts = frappe.get_all( + "Dynamic Link", + filters={"link_doctype": "Customer", "link_name": r.name, "parenttype": "Contact"}, + fields=["parent"], + ) + contact_lines = [] + for c in contacts: + contact = frappe.db.get_value( + "Contact", c.parent, ["first_name", "last_name", "email_id", "phone", "mobile_no"], + as_dict=True, + ) + if contact: + name = f"{contact.first_name or ''} {contact.last_name or ''}".strip() + email = contact.email_id or "" + phone = contact.phone or "" + mobile = contact.mobile_no or "" + parts = [name] + if email: + parts.append(f"- {email}") + if phone: + parts.append(f"- {phone}") + elif mobile: + parts.append(f"- {mobile}") + contact_lines.append(" ".join(parts)) + result["contact_details"] = "\n".join(contact_lines) if contact_lines else r.contact_persons or "" + + return result + + +@frappe.whitelist() +def save_record(data): + """Update an existing Customer record.""" + data = frappe.parse_json(data) + name = data.get("name") + if not name: + return {"status": "error", "message": "No record name provided"} + + doc = frappe.get_doc("Customer", name) + + # Update basic fields + if "customer_name" in data: + doc.customer_name = data["customer_name"] + if "email_id" in data: + doc.email_id = data["email_id"] + if "mobile_no" in data: + doc.mobile_no = data["mobile_no"] + if "contact_persons" in data: + doc.contact_persons = data["contact_persons"] + if "hours_of_operation" in data: + doc.hours_of_operation = data["hours_of_operation"] + if "legacy_notes" in data: + doc.legacy_notes = data["legacy_notes"] + if "contacted_date" in data: + doc.contacted_date = data["contacted_date"] or None + if "follow_up_date" in data: + doc.follow_up_date = data["follow_up_date"] or None + if "last_pu_date" in data: + doc.last_pu_date = data["last_pu_date"] or None + if "prospect_status" in data: + doc.prospect_status = data["prospect_status"] + if "do_not_call" in data: + doc.do_not_call = 1 if data["do_not_call"] else 0 + + # Update primary address if it exists + addr_name = doc.customer_primary_address + if addr_name: + addr = frappe.get_doc("Address", addr_name) + if "address_line1" in data: + addr.address_line1 = data["address_line1"] + if "city" in data: + addr.city = data["city"] + if "zip" in data: + addr.pincode = data["zip"] + if "address_phone" in data: + addr.phone = data["address_phone"] + addr.save(ignore_permissions=True) + + doc.save(ignore_permissions=True) + frappe.db.commit() + + return {"status": "ok", "message": f"Saved {name}"} + + +@frappe.whitelist() +def delete_record(name): + """Delete a Customer record.""" + if not name: + return {"status": "error", "message": "No record name provided"} + + frappe.delete_doc("Customer", name, ignore_permissions=True) + frappe.db.commit() + return {"status": "ok", "message": f"Deleted {name}"} + + +@frappe.whitelist() +def search_records(field=None, value=None): + """Search Customer records by field.""" + if not value: + return [] + + value_lower = value.lower() + filters = [] + + # Map UI field names to database fields + field_map = { + "address": "primary_address", + "company_name": "customer_name", + "contact_person": "contact_persons", + "phone": "mobile_no", + "email": "email_id", + "city": None, # Need join + "zip": None, # Need join + "record_number": "customer_number", + } + + # Search across multiple fields + or_filters = [] + like_val = f"%{value}%" + + or_filters.append(["Customer", "customer_name", "like", like_val]) + or_filters.append(["Customer", "customer_number", "like", like_val]) + or_filters.append(["Customer", "primary_address", "like", like_val]) + or_filters.append(["Customer", "email_id", "like", like_val]) + or_filters.append(["Customer", "mobile_no", "like", like_val]) + or_filters.append(["Customer", "contact_persons", "like", like_val]) + or_filters.append(["Customer", "legacy_notes", "like", like_val]) + or_filters.append(["Customer", "hours_of_operation", "like", like_val]) + or_filters.append(["Customer", "legacy_record_number", "like", like_val]) + + records = frappe.get_all( + "Customer", + or_filters=or_filters, + fields=[ + "name", + "customer_name", + "customer_number", + "primary_address", + "mobile_no", + "email_id", + "contact_persons", + "hours_of_operation", + "legacy_notes", + "contacted_date", + "follow_up_date", + "last_pu_date", + "legacy_record_number", + "do_not_call", + "prospect_status", + "customer_type", + "customer_group", + "territory", + "disabled", + ], + order_by="customer_name asc", + limit_page_length=100, + ) + + # Also search addresses for city/zip matches + addr_matches = frappe.get_all( + "Address", + or_filters=[ + ["Address", "city", "like", like_val], + ["Address", "pincode", "like", like_val], + ["Address", "address_line1", "like", like_val], + ], + fields=["name", "address_line1", "city", "pincode"], + limit_page_length=100, + ) + + # Get customer names linked to matching addresses + existing_names = {r["name"] for r in records} + for addr in addr_matches: + # Find Customer linked to this address + link = frappe.get_all( + "Dynamic Link", + filters={"parent": addr.name, "parenttype": "Address", "link_doctype": "Customer"}, + fields=["link_name"], + ) + for l in link: + if l.link_name not in existing_names: + cust = frappe.get_all( + "Customer", + filters={"name": l.link_name}, + fields=[ + "name", "customer_name", "customer_number", "primary_address", + "mobile_no", "email_id", "contact_persons", "hours_of_operation", + "legacy_notes", "contacted_date", "follow_up_date", "last_pu_date", + "legacy_record_number", "do_not_call", "prospect_status", + "customer_type", "customer_group", "territory", "disabled", + ], + ) + if cust: + records.append(cust[0]) + existing_names.add(l.link_name) + + # Enrich with address details (same as get_records) + for r in records: + if r.get("primary_address"): + addr = frappe.db.get_value( + "Address", r["primary_address"], + ["address_line1", "city", "state", "pincode", "phone"], + as_dict=True, + ) + if addr: + r["address_line1"] = addr.address_line1 or "" + r["city"] = addr.city or "" + r["state"] = addr.state or "" + r["zip"] = addr.pincode or "" + r["address_phone"] = addr.phone or "" + else: + r["address_line1"] = "" + r["city"] = "" + r["state"] = "" + r["zip"] = "" + r["address_phone"] = "" + else: + r["address_line1"] = "" + r["city"] = "" + r["state"] = "" + r["zip"] = "" + r["address_phone"] = "" + + contacts = frappe.get_all( + "Dynamic Link", + filters={"link_doctype": "Customer", "link_name": r["name"], "parenttype": "Contact"}, + fields=["parent"], + ) + contact_lines = [] + for c in contacts: + contact = frappe.db.get_value( + "Contact", c.parent, ["first_name", "last_name", "email_id", "phone", "mobile_no"], + as_dict=True, + ) + if contact: + cname = f"{contact.first_name or ''} {contact.last_name or ''}".strip() + email = contact.email_id or "" + phone = contact.phone or "" + mobile = contact.mobile_no or "" + parts = [cname] + if email: + parts.append(f"- {email}") + if phone: + parts.append(f"- {phone}") + elif mobile: + parts.append(f"- {mobile}") + contact_lines.append(" ".join(parts)) + r["contact_details"] = "\n".join(contact_lines) if contact_lines else r.get("contact_persons", "") + + return records + + +@frappe.whitelist() +def get_total_count(): + """Return total number of Customer records.""" + return frappe.db.count("Customer", {"disabled": 0}) \ No newline at end of file diff --git a/westech_r2/westech_r2/page/customer_records/customer_records.html b/westech_r2/westech_r2/page/customer_records/customer_records.html index fd9d8f2..e928169 100644 --- a/westech_r2/westech_r2/page/customer_records/customer_records.html +++ b/westech_r2/westech_r2/page/customer_records/customer_records.html @@ -1,217 +1,310 @@ -
-

Modify Records

+
+
+

Modify Records

+
0 of 0 - - - - + + + + +
-
-
- - -
-
- - -
+
+
+ +
+ + +
+
+ + +
-
- - -
-
- - -
+
+ + +
+
+ + +
-
- - -
-
- - -
+
+ + +
-
- - -
-
- - -
+
+ + +
+
+ + +
-
- - -
-
- - -
+
+ + +
+
+ + +
-
- - -
-
- - -
+
+ + +
+
+ + +
-
- - -
-
- - -
+
+ + +
+
+ + +
-
- - -
+
+ + +
+
+ + +
-
- - -
- -
- - +
+ + +
-
+ +
+ +
+
\ No newline at end of file diff --git a/westech_r2/westech_r2/page/customer_records/customer_records.js b/westech_r2/westech_r2/page/customer_records/customer_records.js index 63de432..5bbffbe 100644 --- a/westech_r2/westech_r2/page/customer_records/customer_records.js +++ b/westech_r2/westech_r2/page/customer_records/customer_records.js @@ -5,80 +5,113 @@ frappe.pages["customer-records"].on_page_load = function(wrapper) { single_column: true }); - var content = frappe.render_template("customer-records", {}); + var content = frappe.render_template("customer_records", {}); $(page.body).append(content); var records = []; var currentIdx = -1; - var searchResults = []; function loadRecord(idx) { if (idx < 0 || idx >= records.length) return; currentIdx = idx; var r = records[idx]; + $("#current-index").text(idx + 1); $("#total-count").text(records.length); + + // Left column $("#record-number").val(r.name || ""); - $("#additional-numbers").val(r.additional_numbers || ""); - $("#field-star").val(r.field_star || ""); - $("#customer-address").val(r.customer_address || ""); - $("#any-letter").val(r.any_letter || ""); - $("#city").val(r.city || ""); - $("#field-e").val(r.field_e || ""); - $("#zip").val(r.zip || ""); - $("#company-name").val(r.company_name || ""); + $("#company-name").val(r.customer_name || ""); + $("#contact-persons").val(r.contact_details || r.contact_persons || ""); + $("#email-address").val(r.email_id || ""); + $("#contact-numbers").val(r.mobile_no || r.address_phone || ""); + $("#hours-operation").val(r.hours_of_operation || ""); + $("#prospect-status").val(r.prospect_status || ""); + $("#do-not-call").prop("checked", r.do_not_call ? true : false); + $("#comments").val(r.legacy_notes || ""); $("#contacted-date").val(r.contacted_date || ""); - $("#contact-persons").val(r.contact_persons || ""); $("#follow-up-date").val(r.follow_up_date || ""); - $("#email-address").val(r.email_address || ""); + + // Right column + $("#additional-numbers").val(r.customer_number || ""); + $("#customer-address").val(r.address_line1 || ""); + $("#city").val(r.city || ""); + $("#zip").val(r.zip || ""); $("#last-pu-date").val(r.last_pu_date || ""); - $("#contact-numbers").val(r.contact_numbers || ""); - $("#hours-operation").val(r.hours_operation || ""); - $("#comments").val(r.comments || ""); + + $("#status-msg").text("Record " + (idx + 1) + " of " + records.length); + } + + function clearForm() { + $("input:not([type=checkbox]), textarea").val(""); + $("#do-not-call").prop("checked", false); + $("#current-index").text(0); + $("#total-count").text(records.length); + $("#status-msg").text("New record"); + currentIdx = -1; } function fetchRecords() { frappe.call({ - method: "westech_r2.page.customer-records.customer-records.get_records", + method: "westech_r2.westech_r2.api.customer_records_api.get_records", + args: { start: 0, page_length: 500 }, + freeze: true, + freeze_message: "Loading customers...", callback: function(r) { if (r.message) { records = r.message; - if (records.length > 0) loadRecord(0); - else $("#current-index").text(0); - $("#total-count").text(records.length); + if (records.length > 0) { + loadRecord(0); + } else { + clearForm(); + $("#status-msg").text("No records found"); + } } } }); } function saveRecord() { - if (currentIdx < 0) return; + if (currentIdx < 0 || currentIdx >= records.length) { + frappe.msgprint("No record selected to save."); + return; + } var data = { name: $("#record-number").val(), - additional_numbers: $("#additional-numbers").val(), - field_star: $("#field-star").val(), - customer_address: $("#customer-address").val(), - any_letter: $("#any-letter").val(), - city: $("#city").val(), - field_e: $("#field-e").val(), - zip: $("#zip").val(), - company_name: $("#company-name").val(), - contacted_date: $("#contacted-date").val(), + customer_name: $("#company-name").val(), + customer_number: $("#additional-numbers").val(), + email_id: $("#email-address").val(), + mobile_no: $("#contact-numbers").val(), contact_persons: $("#contact-persons").val(), + hours_of_operation: $("#hours-operation").val(), + legacy_notes: $("#comments").val(), + contacted_date: $("#contacted-date").val(), follow_up_date: $("#follow-up-date").val(), - email_address: $("#email-address").val(), last_pu_date: $("#last-pu-date").val(), - contact_numbers: $("#contact-numbers").val(), - hours_operation: $("#hours-operation").val(), - comments: $("#comments").val() + prospect_status: $("#prospect-status").val(), + do_not_call: $("#do-not-call").is(":checked") ? 1 : 0, + address_line1: $("#customer-address").val(), + city: $("#city").val(), + zip: $("#zip").val() }; frappe.call({ - method: "westech_r2.page.customer-records.customer-records.save_record", + method: "westech_r2.westech_r2.api.customer_records_api.save_record", args: { data: JSON.stringify(data) }, + freeze: true, + freeze_message: "Saving...", callback: function(r) { if (r.message && r.message.status === "ok") { - frappe.show_alert("Saved successfully"); - records[currentIdx] = data; + frappe.show_alert({message: "Saved successfully", indicator: "green"}); + // Update local record + records[currentIdx].customer_name = data.customer_name; + records[currentIdx].customer_number = data.customer_number; + records[currentIdx].email_id = data.email_id; + records[currentIdx].mobile_no = data.mobile_no; + records[currentIdx].contact_persons = data.contact_persons; + records[currentIdx].hours_of_operation = data.hours_of_operation; + records[currentIdx].legacy_notes = data.legacy_notes; + } else { + frappe.show_alert({message: "Save failed: " + (r.message ? r.message.message : "Unknown error"), indicator: "red"}); } } }); @@ -86,63 +119,104 @@ frappe.pages["customer-records"].on_page_load = function(wrapper) { function deleteRecord() { if (currentIdx < 0) return; - frappe.confirm("Delete record #" + $("#record-number").val() + "?", function() { - frappe.call({ - method: "westech_r2.page.customer-records.customer-records.delete_record", - args: { name: $("#record-number").val() }, - callback: function(r) { - if (r.message && r.message.status === "ok") { - records.splice(currentIdx, 1); - if (records.length > 0) { - currentIdx = Math.min(currentIdx, records.length - 1); - loadRecord(currentIdx); - } else { - $("#current-index").text(0); - $("#total-count").text(0); - $("input, textarea").val(""); + var recName = $("#record-number").val(); + var recLabel = $("#company-name").val() || recName; + + frappe.confirm( + 'Delete customer "' + recLabel + '"?

This cannot be undone.', + function() { + frappe.call({ + method: "westech_r2.westech_r2.api.customer_records_api.delete_record", + args: { name: recName }, + freeze: true, + freeze_message: "Deleting...", + callback: function(r) { + if (r.message && r.message.status === "ok") { + records.splice(currentIdx, 1); + if (records.length > 0) { + currentIdx = Math.min(currentIdx, records.length - 1); + loadRecord(currentIdx); + } else { + clearForm(); + } + frappe.show_alert({message: "Deleted", indicator: "red"}); } - frappe.show_alert("Deleted"); } - } - }); - }); + }); + } + ); + } + + function newRecord() { + // Open a new Customer form in ERPNext + frappe.set_route("Form", "Customer", "New"); } function searchRecords() { var field = $("#search-field").val(); - var value = $("#search-value").val().toLowerCase(); + var value = $("#search-value").val().trim(); if (!value) { - searchResults = []; fetchRecords(); return; } frappe.call({ - method: "westech_r2.page.customer-records.customer-records.search_records", + method: "westech_r2.westech_r2.api.customer_records_api.search_records", args: { field: field, value: value }, + freeze: true, + freeze_message: "Searching...", callback: function(r) { if (r.message) { - searchResults = r.message; - records = searchResults; - if (records.length > 0) loadRecord(0); - else { - $("#current-index").text(0); - $("#total-count").text(records.length); - $("input, textarea").val(""); + records = r.message; + if (records.length > 0) { + loadRecord(0); + $("#status-msg").text("Found " + records.length + " record(s)"); + } else { + clearForm(); + $("#status-msg").text("No records found"); } } } }); } + function resetSearch() { + $("#search-value").val(""); + fetchRecords(); + } + + // Button bindings $("#btn-save").click(saveRecord); $("#btn-delete").click(deleteRecord); + $("#btn-new").click(newRecord); $("#btn-prev").click(function() { if (currentIdx > 0) loadRecord(currentIdx - 1); }); $("#btn-next").click(function() { if (currentIdx < records.length - 1) loadRecord(currentIdx + 1); }); $("#btn-first").click(function() { if (records.length > 0) loadRecord(0); }); $("#btn-last").click(function() { if (records.length > 0) loadRecord(records.length - 1); }); $("#btn-search").click(searchRecords); - $("#btn-reset").click(function() { $("#search-value").val(""); searchResults = []; fetchRecords(); }); + $("#btn-reset").click(resetSearch); $("#btn-print").click(function() { window.print(); }); + // Keyboard navigation + $(wrapper).on("keydown", function(e) { + if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA" || e.target.tagName === "SELECT") return; + if (e.key === "ArrowLeft" || e.key === "PageUp") { + e.preventDefault(); + if (currentIdx > 0) loadRecord(currentIdx - 1); + } else if (e.key === "ArrowRight" || e.key === "PageDown") { + e.preventDefault(); + if (currentIdx < records.length - 1) loadRecord(currentIdx + 1); + } else if (e.key === "Home") { + e.preventDefault(); + if (records.length > 0) loadRecord(0); + } else if (e.key === "End") { + e.preventDefault(); + if (records.length > 0) loadRecord(records.length - 1); + } else if (e.key === "Enter" && e.target.id === "search-value") { + e.preventDefault(); + searchRecords(); + } + }); + + // Load initial data fetchRecords(); -}; +}; \ No newline at end of file diff --git a/westech_r2/westech_r2/page/customer_records/customer_records.py b/westech_r2/westech_r2/page/customer_records/customer_records.py index 008840e..693d738 100644 --- a/westech_r2/westech_r2/page/customer_records/customer_records.py +++ b/westech_r2/westech_r2/page/customer_records/customer_records.py @@ -1,85 +1,416 @@ import frappe +import json @frappe.whitelist() -def get_records(): - # For now return Lead records mapped to form fields - leads = frappe.db.sql(""" - SELECT name, company_name, email_id, mobile_no, phone, address_line1, city, state, pincode, - title, status, industry, source - FROM tabLead - ORDER BY creation DESC - LIMIT 1000 - """, as_dict=True) - result = [] - for l in leads: - result.append({ - "name": l.name, - "additional_numbers": "", - "field_star": l.status or "", - "customer_address": l.address_line1 or "", - "any_letter": l.title or "", - "city": l.city or "", - "field_e": l.email_id or "", - "zip": l.pincode or "", - "company_name": l.company_name or "", - "contacted_date": "", - "contact_persons": l.title or "", - "follow_up_date": "", - "email_address": l.email_id or "", - "last_pu_date": "", - "contact_numbers": (l.phone or "") + "\n" + (l.mobile_no or ""), - "hours_operation": l.industry or "", - "comments": l.source or "" - }) +def get_records(start=0, page_length=1000): + """Fetch all Customer records for the modify records form.""" + records = frappe.get_all( + "Customer", + fields=[ + "name", + "customer_name", + "customer_number", + "primary_address", + "customer_primary_address", + "mobile_no", + "email_id", + "contact_persons", + "hours_of_operation", + "legacy_notes", + "contacted_date", + "follow_up_date", + "last_pu_date", + "legacy_record_number", + "do_not_call", + "prospect_status", + "customer_type", + "customer_group", + "territory", + "disabled", + ], + order_by="customer_name asc", + limit_page_length=int(page_length), + limit_start=int(start), + ) + + # Enrich with address and contact details + for r in records: + # Get address details — primary_address is display string, + # customer_primary_address is the Link to the Address doc + addr_name = r.get("customer_primary_address") or r.get("primary_address") + # Try to find address via Dynamic Link if primary_address is display string + if addr_name and not frappe.db.exists("Address", addr_name): + # primary_address was a display string; find via dynamic link + addr_links = frappe.get_all( + "Dynamic Link", + filters={"link_doctype": "Customer", "link_name": r["name"], "parenttype": "Address"}, + fields=["parent"], + limit=1, + ) + addr_name = addr_links[0].parent if addr_links else None + + if addr_name and frappe.db.exists("Address", addr_name): + addr = frappe.db.get_value( + "Address", addr_name, + ["address_line1", "city", "state", "pincode", "phone"], + as_dict=True, + ) + if addr: + r["address_line1"] = addr.address_line1 or "" + r["city"] = addr.city or "" + r["state"] = addr.state or "" + r["zip"] = addr.pincode or "" + r["address_phone"] = addr.phone or "" + else: + r["address_line1"] = "" + r["city"] = "" + r["state"] = "" + r["zip"] = "" + r["address_phone"] = "" + else: + r["address_line1"] = "" + r["city"] = "" + r["state"] = "" + r["zip"] = "" + r["address_phone"] = "" + + # Get contact details from linked contacts + contacts = frappe.get_all( + "Dynamic Link", + filters={"link_doctype": "Customer", "link_name": r["name"], "parenttype": "Contact"}, + fields=["parent"], + ) + contact_lines = [] + for c in contacts: + contact = frappe.db.get_value( + "Contact", c.parent, ["first_name", "last_name", "email_id", "phone", "mobile_no"], + as_dict=True, + ) + if contact: + name = f"{contact.first_name or ''} {contact.last_name or ''}".strip() + email = contact.email_id or "" + phone = contact.phone or "" + mobile = contact.mobile_no or "" + parts = [name] + if email: + parts.append(f"- {email}") + if phone: + parts.append(f"- {phone}") + elif mobile: + parts.append(f"- {mobile}") + contact_lines.append(" ".join(parts)) + r["contact_details"] = "\n".join(contact_lines) if contact_lines else r.get("contact_persons", "") + + return records + + +@frappe.whitelist() +def get_record(name): + """Fetch a single Customer record by name.""" + r = frappe.get_doc("Customer", name) + result = { + "name": r.name, + "customer_name": r.customer_name or "", + "customer_number": r.customer_number or "", + "primary_address": r.primary_address or "", + "mobile_no": r.mobile_no or "", + "email_id": r.email_id or "", + "contact_persons": r.contact_persons or "", + "hours_of_operation": r.hours_of_operation or "", + "legacy_notes": r.legacy_notes or "", + "contacted_date": str(r.contacted_date) if r.contacted_date else "", + "follow_up_date": str(r.follow_up_date) if r.follow_up_date else "", + "last_pu_date": str(r.last_pu_date) if r.last_pu_date else "", + "legacy_record_number": r.legacy_record_number or "", + "do_not_call": r.do_not_call or 0, + "prospect_status": r.prospect_status or "", + "customer_type": r.customer_type or "", + "customer_group": r.customer_group or "", + "territory": r.territory or "", + "disabled": r.disabled or 0, + } + + # Get address — primary_address on Customer is the display string; + # customer_primary_address is the actual Link to the Address doc + addr_name = r.customer_primary_address + if addr_name: + addr = frappe.db.get_value( + "Address", addr_name, + ["address_line1", "city", "state", "pincode", "phone"], + as_dict=True, + ) + if addr: + result["address_line1"] = addr.address_line1 or "" + result["city"] = addr.city or "" + result["state"] = addr.state or "" + result["zip"] = addr.pincode or "" + result["address_phone"] = addr.phone or "" + else: + result["address_line1"] = "" + result["city"] = "" + result["state"] = "" + result["zip"] = "" + result["address_phone"] = "" + else: + result["address_line1"] = "" + result["city"] = "" + result["state"] = "" + result["zip"] = "" + result["address_phone"] = "" + + # Get contacts + contacts = frappe.get_all( + "Dynamic Link", + filters={"link_doctype": "Customer", "link_name": r.name, "parenttype": "Contact"}, + fields=["parent"], + ) + contact_lines = [] + for c in contacts: + contact = frappe.db.get_value( + "Contact", c.parent, ["first_name", "last_name", "email_id", "phone", "mobile_no"], + as_dict=True, + ) + if contact: + name = f"{contact.first_name or ''} {contact.last_name or ''}".strip() + email = contact.email_id or "" + phone = contact.phone or "" + mobile = contact.mobile_no or "" + parts = [name] + if email: + parts.append(f"- {email}") + if phone: + parts.append(f"- {phone}") + elif mobile: + parts.append(f"- {mobile}") + contact_lines.append(" ".join(parts)) + result["contact_details"] = "\n".join(contact_lines) if contact_lines else r.contact_persons or "" + return result + @frappe.whitelist() def save_record(data): + """Update an existing Customer record.""" data = frappe.parse_json(data) - # For now just return OK - Lead update can be wired later - return {"status": "ok", "message": "Saved " + (data.get("name") or "")} + name = data.get("name") + if not name: + return {"status": "error", "message": "No record name provided"} + + doc = frappe.get_doc("Customer", name) + + # Update basic fields + if "customer_name" in data: + doc.customer_name = data["customer_name"] + if "email_id" in data: + doc.email_id = data["email_id"] + if "mobile_no" in data: + doc.mobile_no = data["mobile_no"] + if "contact_persons" in data: + doc.contact_persons = data["contact_persons"] + if "hours_of_operation" in data: + doc.hours_of_operation = data["hours_of_operation"] + if "legacy_notes" in data: + doc.legacy_notes = data["legacy_notes"] + if "contacted_date" in data: + doc.contacted_date = data["contacted_date"] or None + if "follow_up_date" in data: + doc.follow_up_date = data["follow_up_date"] or None + if "last_pu_date" in data: + doc.last_pu_date = data["last_pu_date"] or None + if "prospect_status" in data: + doc.prospect_status = data["prospect_status"] + if "do_not_call" in data: + doc.do_not_call = 1 if data["do_not_call"] else 0 + + # Update primary address if it exists + addr_name = doc.customer_primary_address + if addr_name: + addr = frappe.get_doc("Address", addr_name) + if "address_line1" in data: + addr.address_line1 = data["address_line1"] + if "city" in data: + addr.city = data["city"] + if "zip" in data: + addr.pincode = data["zip"] + if "address_phone" in data: + addr.phone = data["address_phone"] + addr.save(ignore_permissions=True) + + doc.save(ignore_permissions=True) + frappe.db.commit() + + return {"status": "ok", "message": f"Saved {name}"} + @frappe.whitelist() def delete_record(name): - # For now return OK - return {"status": "ok", "message": "Deleted " + name} + """Delete a Customer record.""" + if not name: + return {"status": "error", "message": "No record name provided"} + + frappe.delete_doc("Customer", name, ignore_permissions=True) + frappe.db.commit() + return {"status": "ok", "message": f"Deleted {name}"} + @frappe.whitelist() -def search_records(field, value): - leads = frappe.db.sql(""" - SELECT name, company_name, email_id, mobile_no, phone, address_line1, city, state, pincode, - title, status, industry, source - FROM tabLead - WHERE LOWER(company_name) LIKE %s - OR LOWER(title) LIKE %s - OR LOWER(address_line1) LIKE %s - OR LOWER(city) LIKE %s - OR LOWER(pincode) LIKE %s - OR LOWER(email_id) LIKE %s - OR LOWER(phone) LIKE %s - OR LOWER(mobile_no) LIKE %s - ORDER BY creation DESC - LIMIT 100 - """, tuple(["%" + value + "%"] * 8), as_dict=True) - result = [] - for l in leads: - result.append({ - "name": l.name, - "additional_numbers": "", - "field_star": l.status or "", - "customer_address": l.address_line1 or "", - "any_letter": l.title or "", - "city": l.city or "", - "field_e": l.email_id or "", - "zip": l.pincode or "", - "company_name": l.company_name or "", - "contacted_date": "", - "contact_persons": l.title or "", - "follow_up_date": "", - "email_address": l.email_id or "", - "last_pu_date": "", - "contact_numbers": (l.phone or "") + "\n" + (l.mobile_no or ""), - "hours_operation": l.industry or "", - "comments": l.source or "" - }) - return result +def search_records(field=None, value=None): + """Search Customer records by field.""" + if not value: + return [] + + value_lower = value.lower() + filters = [] + + # Map UI field names to database fields + field_map = { + "address": "primary_address", + "company_name": "customer_name", + "contact_person": "contact_persons", + "phone": "mobile_no", + "email": "email_id", + "city": None, # Need join + "zip": None, # Need join + "record_number": "customer_number", + } + + # Search across multiple fields + or_filters = [] + like_val = f"%{value}%" + + or_filters.append(["Customer", "customer_name", "like", like_val]) + or_filters.append(["Customer", "customer_number", "like", like_val]) + or_filters.append(["Customer", "primary_address", "like", like_val]) + or_filters.append(["Customer", "email_id", "like", like_val]) + or_filters.append(["Customer", "mobile_no", "like", like_val]) + or_filters.append(["Customer", "contact_persons", "like", like_val]) + or_filters.append(["Customer", "legacy_notes", "like", like_val]) + or_filters.append(["Customer", "hours_of_operation", "like", like_val]) + or_filters.append(["Customer", "legacy_record_number", "like", like_val]) + + records = frappe.get_all( + "Customer", + or_filters=or_filters, + fields=[ + "name", + "customer_name", + "customer_number", + "primary_address", + "mobile_no", + "email_id", + "contact_persons", + "hours_of_operation", + "legacy_notes", + "contacted_date", + "follow_up_date", + "last_pu_date", + "legacy_record_number", + "do_not_call", + "prospect_status", + "customer_type", + "customer_group", + "territory", + "disabled", + ], + order_by="customer_name asc", + limit_page_length=100, + ) + + # Also search addresses for city/zip matches + addr_matches = frappe.get_all( + "Address", + or_filters=[ + ["Address", "city", "like", like_val], + ["Address", "pincode", "like", like_val], + ["Address", "address_line1", "like", like_val], + ], + fields=["name", "address_line1", "city", "pincode"], + limit_page_length=100, + ) + + # Get customer names linked to matching addresses + existing_names = {r["name"] for r in records} + for addr in addr_matches: + # Find Customer linked to this address + link = frappe.get_all( + "Dynamic Link", + filters={"parent": addr.name, "parenttype": "Address", "link_doctype": "Customer"}, + fields=["link_name"], + ) + for l in link: + if l.link_name not in existing_names: + cust = frappe.get_all( + "Customer", + filters={"name": l.link_name}, + fields=[ + "name", "customer_name", "customer_number", "primary_address", + "mobile_no", "email_id", "contact_persons", "hours_of_operation", + "legacy_notes", "contacted_date", "follow_up_date", "last_pu_date", + "legacy_record_number", "do_not_call", "prospect_status", + "customer_type", "customer_group", "territory", "disabled", + ], + ) + if cust: + records.append(cust[0]) + existing_names.add(l.link_name) + + # Enrich with address details (same as get_records) + for r in records: + if r.get("primary_address"): + addr = frappe.db.get_value( + "Address", r["primary_address"], + ["address_line1", "city", "state", "pincode", "phone"], + as_dict=True, + ) + if addr: + r["address_line1"] = addr.address_line1 or "" + r["city"] = addr.city or "" + r["state"] = addr.state or "" + r["zip"] = addr.pincode or "" + r["address_phone"] = addr.phone or "" + else: + r["address_line1"] = "" + r["city"] = "" + r["state"] = "" + r["zip"] = "" + r["address_phone"] = "" + else: + r["address_line1"] = "" + r["city"] = "" + r["state"] = "" + r["zip"] = "" + r["address_phone"] = "" + + contacts = frappe.get_all( + "Dynamic Link", + filters={"link_doctype": "Customer", "link_name": r["name"], "parenttype": "Contact"}, + fields=["parent"], + ) + contact_lines = [] + for c in contacts: + contact = frappe.db.get_value( + "Contact", c.parent, ["first_name", "last_name", "email_id", "phone", "mobile_no"], + as_dict=True, + ) + if contact: + cname = f"{contact.first_name or ''} {contact.last_name or ''}".strip() + email = contact.email_id or "" + phone = contact.phone or "" + mobile = contact.mobile_no or "" + parts = [cname] + if email: + parts.append(f"- {email}") + if phone: + parts.append(f"- {phone}") + elif mobile: + parts.append(f"- {mobile}") + contact_lines.append(" ".join(parts)) + r["contact_details"] = "\n".join(contact_lines) if contact_lines else r.get("contact_persons", "") + + return records + + +@frappe.whitelist() +def get_total_count(): + """Return total number of Customer records.""" + return frappe.db.count("Customer", {"disabled": 0}) \ No newline at end of file diff --git a/westech_r2/westech_r2/page/ebay-pricing/ebay-pricing.js b/westech_r2/westech_r2/page/ebay-pricing/ebay-pricing.js index 1414078..9004536 100644 --- a/westech_r2/westech_r2/page/ebay-pricing/ebay-pricing.js +++ b/westech_r2/westech_r2/page/ebay-pricing/ebay-pricing.js @@ -134,7 +134,7 @@ frappe.pages['ebay-pricing'].on_page_load = function(wrapper) { function search_ebay(query) { frappe.call({ - method: 'westech_r2.api.ebay_pricing.search_model', + method: 'westech_r2.westech_r2.api.ebay_pricing.search_model', args: { query: query }, freeze: true, freeze_message: __('Searching eBay sold listings...'), @@ -151,7 +151,7 @@ frappe.pages['ebay-pricing'].on_page_load = function(wrapper) { function run_batch(size) { frappe.call({ - method: 'westech_r2.api.ebay_pricing.run_batch', + method: 'westech_r2.westech_r2.api.ebay_pricing.run_batch', args: { batch_size: size }, freeze: true, freeze_message: __('Running batch pricing...'), @@ -167,7 +167,7 @@ frappe.pages['ebay-pricing'].on_page_load = function(wrapper) { function apply_pricing(item_code) { frappe.call({ - method: 'westech_r2.api.ebay_pricing.batch_apply_pricing', + method: 'westech_r2.westech_r2.api.ebay_pricing.batch_apply_pricing', args: { item_code: item_code }, freeze: true, freeze_message: __('Applying pricing to Serial Nos...'), @@ -183,7 +183,7 @@ frappe.pages['ebay-pricing'].on_page_load = function(wrapper) { function apply_pricing_all() { frappe.call({ - method: 'westech_r2.api.ebay_pricing.batch_apply_pricing', + method: 'westech_r2.westech_r2.api.ebay_pricing.batch_apply_pricing', args: { batch_size: 1000 }, freeze: true, freeze_message: __('Applying pricing to all Serial Nos...'), @@ -258,7 +258,7 @@ frappe.pages['ebay-pricing'].on_page_load = function(wrapper) { function load_recent_pricing() { frappe.call({ - method: 'westech_r2.api.ebay_pricing.get_recent_pricing', + method: 'westech_r2.westech_r2.api.ebay_pricing.get_recent_pricing', args: { limit: 50 }, callback: function(r) { if (r.message) { diff --git a/westech_r2/westech_r2/page/intake/intake.js b/westech_r2/westech_r2/page/intake/intake.js index 92a2687..f7edadd 100644 --- a/westech_r2/westech_r2/page/intake/intake.js +++ b/westech_r2/westech_r2/page/intake/intake.js @@ -565,7 +565,7 @@ function generate_cor_report() { address_line: $('#address_line').val() || '', pallet_name: $('#intake-form-container').data('pallet-name') || '' }; - window.open('/api/method/westech_r2.api.cor_generator.generate_cor?' + window.open('/api/method/westech_r2.westech_r2.api.cor_generator.generate_cor?' + '&company_name=' + encodeURIComponent(args.company_name) + '&weights=' + encodeURIComponent(args.weights) + '&received_date=' + encodeURIComponent(args.received_date) diff --git a/westech_r2/westech_r2/page/receiving/receiving.js b/westech_r2/westech_r2/page/receiving/receiving.js index 3d83536..7ba5a19 100644 --- a/westech_r2/westech_r2/page/receiving/receiving.js +++ b/westech_r2/westech_r2/page/receiving/receiving.js @@ -5,19 +5,12 @@ frappe.pages['receiving'].on_page_load = function(wrapper) { single_column: true }); - // Inline HTML — same pattern as intake.js $(wrapper).find('.layout-main-section').html(`
-
-
-

🚛 Receiving

-

Schedule pickups, manage routes, and check in loads.

-
-
@@ -42,54 +35,13 @@ frappe.pages['receiving'].on_page_load = function(wrapper) {
- - + +
DateWeekdayTypeCustomerContactAddressEst. ItemsDataRED/R2StatusNotesTruckAoRCoD
Loading...
DateTypeCustomerContactAddressItemsDataRED/R2StatusNotes
Loading...
-
@@ -106,30 +58,11 @@ frappe.pages['receiving'].on_page_load = function(wrapper) {
Recent Check-ins
- - + +
DateCustomerTypeActual PalletsActual WeightLoad ContentsData StatusRED/R2Status
Loading...
DateCustomerLoad #PalletsWeightContentsData StatusRED/R2
Loading...
- @@ -141,131 +74,142 @@ frappe.pages['receiving'].on_page_load = function(wrapper) { .stop-card .stop-addr { color: #666; font-size: 12px; margin-top: 2px; } .stop-card .stop-meta { display: flex; gap: 8px; margin-top: 4px; font-size: 11px; } .stop-card .stop-meta span { background: #D6E4F0; color: #2F5496; padding: 1px 6px; border-radius: 3px; } - .stop-card.dragging { opacity: 0.5; } - .truck-column { min-height: 200px; } - #pickup-calendar { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; font-size: 12px; } - .cal-day { text-align: center; padding: 6px 4px; border-radius: 6px; } - .cal-day.has-pickups { background: #D6E4F0; cursor: pointer; } - .cal-day.today { background: #2F5496; color: #fff; } - .cal-day .day-num { font-weight: 600; } - .cal-day .day-count { font-size: 10px; font-weight: 700; } + .cal-day { display: inline-block; width: 36px; height: 36px; line-height: 36px; text-align: center; margin: 1px; border-radius: 4px; font-size: 12px; cursor: pointer; } + .cal-day:hover { background: #D6E4F0; } + .cal-day.has-pickups { background: #2F5496; color: #fff; font-weight: 700; } + .stage-tab { display: inline-block; padding: 10px 15px; font-size: 14px; color: #555; } .stage-tab:hover { color: #2F5496; } .stage-tab.active { color: #2F5496; font-weight: 600; border-bottom: 2px solid #2F5496; } + .cal-day.today { border: 2px solid #C62828; } + /* Tighten Frappe Section Break bars in dialogs by 2/3 */ + .modal-dialog .section-head { padding-top: 3px !important; padding-bottom: 3px !important; font-size: 12px !important; } + .modal-dialog .form-section { padding-top: 2px !important; padding-bottom: 2px !important; } + .modal-dialog .section-desc { font-size: 11px !important; } `); - - // ── Stage Tabs ── - $("#receiving-tabs a").on("click", function(e) { - e.preventDefault(); - $(this).tab("show"); - var stage = $(this).attr("href").replace("#stage-", ""); - if (stage === "a") loadPickups(); - if (stage === "b") loadRoutes(); - if (stage === "c") loadCheckins(); + // Tab switching + $("#receiving-tabs").on("click", ".stage-tab", function() { + var target = $(this).data("target"); + $("#receiving-tabs li, .stage-tab").removeClass("active"); + $(this).addClass("active").closest("li").addClass("active"); + $(".tab-pane").removeClass("active"); + $(target).addClass("active"); }); - // ── Stage A: Link Controls ── - var customer_control = null; - - function setupCustomerLink() { - customer_control = frappe.ui.form.make_control({ - parent: $("#sp-customer-control"), - df: { - fieldtype: "Link", - fieldname: "customer_number", - options: "Customer", - label: "Customer", - reqd: 1, - placeholder: "Search customer...", - onchange: function() { - var val = customer_control.get_value(); - if (val) fetchCustomerDetails(val); - else clearCustomerFields(); - } - }, - only_input: true, - }); - customer_control.refresh(); - $("#sp-customer-control .control-input").css("margin", "0"); - $("#sp-customer-control .help-box").remove(); - } - - function fetchCustomerDetails(customer_name) { - frappe.call({ - method: "frappe.client.get", - args: { doctype: "Customer", name: customer_name }, - callback: function(r) { - if (!r.message) return; - var c = r.message; - $("#sp-company_name").val(c.customer_name || ""); - $("#sp-contact_name").val(c.contact_name || ""); - $("#sp-contact_phone").val(c.contact_phone || ""); - $("#sp-contact_email").val(c.contact_email || ""); - $("#sp-legacy_notes").val(c.legacy_notes || ""); - $("#sp-hours_of_operation").val(c.hours_of_operation || ""); - + // ── Stage A: New Pickup DIALOG ── + $("#btn-new-pickup").on("click", function() { + var d = new frappe.ui.Dialog({ + title: "New Scheduled Pickup", + size: "large", + fields: [ + {fieldtype: "Section Break", label: "📅 Pickup Info"}, + {fieldname: "pickup_date", fieldtype: "Date", label: "Pickup Date", reqd: 1, default: frappe.datetime.nowdate()}, + {fieldname: "pickup_type", fieldtype: "Select", label: "Type", reqd: 1, options: "Pickup\nDrop-off", default: "Pickup"}, + {fieldname: "customer_number", fieldtype: "Link", label: "Customer", options: "Customer", reqd: 1}, + {fieldname: "company_name", fieldtype: "Data", label: "Company Name", read_only: 1}, + {fieldtype: "Column Break"}, + {fieldname: "contact_name", fieldtype: "Data", label: "Contact Name"}, + {fieldname: "contact_phone", fieldtype: "Data", label: "Contact Phone"}, + {fieldname: "contact_email", fieldtype: "Data", label: "Contact Email"}, + {fieldtype: "Section Break", label: "📍 Address"}, + {fieldname: "address_line", fieldtype: "Data", label: "Street Address"}, + {fieldname: "city", fieldtype: "Data", label: "City"}, + {fieldtype: "Column Break"}, + {fieldname: "state", fieldtype: "Data", label: "State", default: "AZ"}, + {fieldname: "zip_code", fieldtype: "Data", label: "ZIP"}, + {fieldname: "hours_of_operation", fieldtype: "Data", label: "Hours of Operation", placeholder: "e.g. Mon-Fri 8am-5pm"}, + {fieldtype: "Section Break", label: "📦 Load Info"}, + {fieldname: "estimated_items", fieldtype: "Int", label: "Estimated Items"}, + {fieldname: "estimated_weight", fieldtype: "Data", label: "Estimated Weight"}, + {fieldname: "load_contents", fieldtype: "Data", label: "Load Contents", placeholder: "Wire, Monitors, Laptops..."}, + {fieldtype: "Column Break"}, + {fieldname: "data_status", fieldtype: "Select", label: "Data Status", options: "\nD0\nD1\nND1\nND2\nND3\nND4"}, + {fieldname: "red_r2", fieldtype: "Select", label: "RED / R2", options: "\nRED\nNIST\nRed+NIST\nR2\nBoth\nNeither"}, + {fieldname: "needs_aor", fieldtype: "Check", label: "Needs AoR"}, + {fieldname: "needs_cod", fieldtype: "Check", label: "Needs CoD"}, + {fieldname: "notes", fieldtype: "Small Text", label: "Notes / Special Handling"}, + ], + primary_action_label: "Save Pickup", + primary_action: function(values) { + if (!values.customer_number) { frappe.msgprint("Select a customer"); return; } + values.doctype = "Scheduled Pickup"; + values.status = "Scheduled"; frappe.call({ - method: "frappe.client.get_list", - args: { - doctype: "Address", - filters: [["Dynamic Link", "link_name", "=", customer_name]], - fields: ["address_line1", "city", "state", "pincode"], - limit_page_length: 1 - }, - callback: function(ra) { - if (ra.message && ra.message.length) { - var a = ra.message[0]; - $("#sp-address_line").val(a.address_line1 || ""); - $("#sp-city").val(a.city || ""); - $("#sp-state").val(a.state || "AZ"); - $("#sp-zip_code").val(a.pincode || ""); - } - } - }); - - frappe.call({ - method: "frappe.client.get_list", - args: { - doctype: "Contact", - filters: [["Dynamic Link", "link_name", "=", customer_name]], - fields: ["first_name", "last_name", "email_id", "phone", "mobile_no"], - limit_page_length: 1 - }, - callback: function(rc) { - if (rc.message && rc.message.length) { - var ct = rc.message[0]; - if (!$("#sp-contact_name").val()) { - $("#sp-contact_name").val((ct.first_name || "") + " " + (ct.last_name || "")); - } - if (!$("#sp-contact_phone").val()) { - $("#sp-contact_phone").val(ct.phone || ct.mobile_no || ""); - } - if (!$("#sp-contact_email").val()) { - $("#sp-contact_email").val(ct.email_id || ""); - } + method: "frappe.client.insert", + args: { doc: values }, + callback: function(r) { + if (r.message) { + frappe.show_alert({message: "Pickup scheduled", indicator: "green"}); + d.hide(); + loadPickups(); } } }); } }); - } - function clearCustomerFields() { - $("#sp-company_name, #sp-contact_name, #sp-contact_phone, #sp-contact_email, #sp-address_line, #sp-city, #sp-state, #sp-zip_code, #sp-legacy_notes, #sp-hours_of_operation").val(""); - $("#sp-state").val("AZ"); - } + // Auto-fill customer details on selection + d.fields_dict.customer_number.$input.on("change", function() { + var val = d.get_value("customer_number"); + if (!val) return; + frappe.call({ + method: "frappe.client.get", + args: { doctype: "Customer", name: val }, + callback: function(r) { + if (!r.message) return; + var c = r.message; + d.set_value("company_name", c.customer_name || ""); + frappe.call({ + method: "frappe.client.get_list", + args: { + doctype: "Address", + filters: [["Dynamic Link", "link_name", "=", val]], + fields: ["name", "address_line1", "city", "state", "pincode"], + limit_page_length: 1 + }, + callback: function(ra) { + if (ra.message && ra.message.length) { + var a = ra.message[0]; + d.set_value("address_line", a.address_line1 || ""); + d.set_value("city", a.city || ""); + d.set_value("state", a.state || "AZ"); + d.set_value("zip_code", a.pincode || ""); + } + } + }); + frappe.call({ + method: "frappe.client.get_list", + args: { + doctype: "Contact", + filters: [["Dynamic Link", "link_name", "=", val]], + fields: ["first_name", "last_name", "email_id", "phone", "mobile_no"], + limit_page_length: 1 + }, + callback: function(rc) { + if (rc.message && rc.message.length) { + var ct = rc.message[0]; + d.set_value("contact_name", (ct.first_name || "") + " " + (ct.last_name || "")); + d.set_value("contact_phone", ct.phone || ct.mobile_no || ""); + d.set_value("contact_email", ct.email_id || ""); + } + } + }); + } + }); + }); + + d.show(); + }); // ── Stage A: Load Pickups ── function loadPickups() { var dateFilter = $("#pickup-date-filter").val(); frappe.call({ - method: "westech_r2.api.receiving_api.get_pickups", - args: { date: dateFilter }, + method: "westech_r2.westech_r2.api.receiving_api.get_pickups", + args: { date: dateFilter || undefined }, callback: function(r) { if (r.message) { renderPickupTable(r.message.pickups || []); renderCalendar(r.message.calendar || []); - // weekly chart removed - $("#pickup-count-label").text((r.message.pickups || []).length + " pickups"); } } }); @@ -273,112 +217,45 @@ frappe.pages['receiving'].on_page_load = function(wrapper) { function renderPickupTable(pickups) { var tbody = $("#pickup-tbody"); + $("#pickup-count-label").text("(" + pickups.length + ")"); if (!pickups.length) { - tbody.html('No scheduled pickups'); + tbody.html('No pickups found'); return; } - var statusColors = { "Scheduled": "#2196F3", "Routed": "#009688", "In Progress": "#FF9800", "Complete": "#4CAF50", "Cancelled": "#F44336" }; - var h = ""; - pickups.forEach(function(p) { - var st = p.status || "Scheduled"; - var sc = statusColors[st] || "#999"; - var weekday = p.pickup_date ? dayName(new Date(p.pickup_date + "T12:00:00")) : ""; - var typeBadge = p.pickup_type === "Drop-off" - ? 'Drop-off' - : 'Pickup'; - h += ''; - h += '' + esc(p.pickup_date || "") + ''; - h += '' + weekday + ''; - h += '' + typeBadge + ''; - h += '' + esc(p.company_name || p.customer_number || "") + ''; - h += '' + esc((p.contact_name || "") + (p.contact_phone ? " • " + p.contact_phone : "")) + ''; - h += '' + esc((p.address_line || "") + (p.city ? ", " + p.city : "")) + ''; - h += '' + (p.estimated_items || "—") + ''; - h += '' + esc(p.data_status || "—") + ''; - h += '' + esc(p.red_r2 || "—") + ''; - h += '' + esc(st) + ''; - h += '' + esc(p.notes || "") + ''; - h += '' + esc(p.truck || "—") + ''; - h += '' + (p.needs_aor ? "✓" : "") + ''; - h += '' + (p.needs_cod ? "✓" : "") + ''; - h += ''; - }); - tbody.html(h); + tbody.html(pickups.map(function(p) { + return '' + + '' + esc(p.pickup_date || "") + '' + + '' + esc(p.pickup_type || "") + '' + + '' + esc(p.company_name || p.customer_number || "") + '' + + '' + esc(p.contact_name || "") + '' + + '' + esc(p.address_line || "") + (p.city ? ", " + esc(p.city) : "") + '' + + '' + (p.estimated_items || "—") + '' + + '' + esc(p.data_status || "—") + '' + + '' + esc(p.red_r2 || "—") + '' + + '' + esc(p.status || "") + '' + + '' + esc(p.notes || "") + ''; + }).join("")); } - function renderCalendar(days) { + function renderCalendar(calendar) { var el = $("#pickup-calendar"); - if (!days || !days.length) { el.html('
No upcoming pickups
'); return; } - var today = frappe.datetime.nowdate(); - var h = '
'; - ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].forEach(function(d) { - h += '
' + d + '
'; + if (!calendar.length) { el.html('
No data
'); return; } + var todayStr = frappe.datetime.nowdate(); + var html = '
'; + calendar.forEach(function(d) { + var cls = "cal-day"; + if (d.count > 0) cls += " has-pickups"; + if (d.date === todayStr) cls += " today"; + var label = d.date.substring(5); + html += '
' + label + '
'; }); - var first = new Date(days[0].date + "T12:00:00"); - for (var i = 0; i < first.getDay(); i++) h += '
'; - days.forEach(function(d) { - var isToday = d.date === today; - var hasCount = d.count > 0; - var bg = isToday ? "cal-day today" : (hasCount ? "cal-day has-pickups" : "cal-day"); - var onclick = hasCount ? "onclick=$('#pickup-date-filter').val('" + d.date + "');loadPickups();" : ""; - h += '
'; - h += '
' + d.date.split("-")[2] + '
'; - if (hasCount) h += '
' + d.count + '
'; - h += '
'; - }); - h += '
'; - el.html(h); + html += '
'; + el.html(html); } - // ── Stage A: New Pickup ── - $("#btn-new-pickup").on("click", function() { - $("#new-pickup-form").show(); - $("#sp-pickup_date").val(frappe.datetime.nowdate()); - setupCustomerLink(); - }); - - $("#btn-cancel-pickup").on("click", function() { - $("#new-pickup-form").hide(); - if (customer_control) customer_control.set_value(""); - }); - - $("#pickup-form").on("submit", function(e) { - e.preventDefault(); - var doc = { - doctype: "Scheduled Pickup", - pickup_date: $("#sp-pickup_date").val(), - pickup_type: $("#sp-pickup_type").val(), - customer_number: customer_control ? customer_control.get_value() : "", - company_name: $("#sp-company_name").val(), - contact_name: $("#sp-contact_name").val(), - contact_phone: $("#sp-contact_phone").val(), - contact_email: $("#sp-contact_email").val(), - address_line: $("#sp-address_line").val(), - city: $("#sp-city").val(), - state: $("#sp-state").val(), - zip_code: $("#sp-zip_code").val(), - estimated_items: parseInt($("#sp-estimated_items").val()) || 0, - estimated_weight: $("#sp-estimated_weight").val(), - load_contents: $("#sp-load_contents").val(), - data_status: $("#sp-data_status").val(), - red_r2: $("#sp-red_r2").val(), - needs_aor: $("#sp-needs_aor").is(":checked") ? 1 : 0, - needs_cod: $("#sp-needs_cod").is(":checked") ? 1 : 0, - notes: $("#sp-notes").val(), - legacy_notes: $("#sp-legacy_notes").val(), - status: "Scheduled" - }; - frappe.call({ - method: "frappe.client.insert", - args: { doc: doc }, - callback: function(r) { - if (r.message) { - frappe.show_alert({ message: "Pickup scheduled", indicator: "green" }); - $("#new-pickup-form").hide(); - loadPickups(); - } - } - }); + $(document).on("click", ".cal-day.has-pickups", function() { + $("#pickup-date-filter").val($(this).data("date")); + loadPickups(); }); $("#pickup-date-filter").on("change", loadPickups); @@ -392,7 +269,7 @@ frappe.pages['receiving'].on_page_load = function(wrapper) { var date = $("#route-date").val() || frappe.datetime.nowdate(); $("#route-date").val(date); frappe.call({ - method: "westech_r2.api.receiving_api.get_pickups", + method: "westech_r2.westech_r2.api.receiving_api.get_pickups", args: { date: date }, callback: function(r) { if (r.message) renderRouteColumns(r.message.pickups || []); @@ -412,7 +289,7 @@ frappe.pages['receiving'].on_page_load = function(wrapper) { $("#" + key + "-count").text("(" + trucks[t].length + " stops)"); $("#" + key + "-stops").html(trucks[t].map(function(p, i) { return stopCard(p, i + 1); }).join("")); }); - $("#unassigned-count").text("(" + trucks["Unassigned"].length + " stops)"); + $("#unassigned-count").text("(" + trucks["Unassigned"].length + ")"); $("#unassigned-stops").html(trucks["Unassigned"].map(function(p) { return stopCard(p, 0); }).join("")); } @@ -432,43 +309,114 @@ frappe.pages['receiving'].on_page_load = function(wrapper) { } $("#btn-load-routes").on("click", loadRoutes); - $("#btn-auto-route").on("click", function() { var date = $("#route-date").val(); if (!date) { frappe.msgprint("Select a date first"); return; } frappe.call({ - method: "westech_r2.api.receiving_api.auto_route", + method: "westech_r2.westech_r2.api.receiving_api.auto_route", args: { date: date }, callback: function(r) { - if (r.message && r.message.success) { - frappe.show_alert({ message: "Routes optimized", indicator: "green" }); - loadRoutes(); - } + if (r.message && r.message.success) { frappe.show_alert({message: "Routes optimized", indicator: "green"}); loadRoutes(); } } }); }); - $("#btn-route-sheet").on("click", function() { var date = $("#route-date").val() || frappe.datetime.nowdate(); - window.open("/api/method/westech_r2.api.receiving_api.print_route_sheet?date=" + date, "_blank"); + window.open("/api/method/westech_r2.westech_r2.api.receiving_api.print_route_sheet?date=" + date, "_blank"); }); - $("#btn-green-sheet").on("click", function() { var date = $("#route-date").val() || frappe.datetime.nowdate(); - window.open("/api/method/westech_r2.api.receiving_api.print_green_sheet?date=" + date, "_blank"); + window.open("/api/method/westech_r2.westech_r2.api.receiving_api.print_green_sheet?date=" + date, "_blank"); }); - $("#btn-labels").on("click", function() { var date = $("#route-date").val() || frappe.datetime.nowdate(); - window.open("/api/method/westech_r2.api.receiving_api.print_labels?date=" + date, "_blank"); + window.open("/api/method/westech_r2.westech_r2.api.receiving_api.print_labels?date=" + date, "_blank"); }); - // ── Stage C: Check-in ── - var checkin_pickup_control = null; + // ── Stage C: Check-in DIALOG ── + $("#btn-new-checkin").on("click", function() { + var d = new frappe.ui.Dialog({ + title: "Load Check-in", + size: "large", + fields: [ + {fieldtype: "Section Break", label: "📅 Pickup Reference"}, + {fieldname: "pickup_ref", fieldtype: "Link", label: "Scheduled Pickup", options: "Scheduled Pickup", reqd: 1, + get_query: function() { return { filters: [["Scheduled Pickup", "status", "in", ["Scheduled", "Routed", "In Progress"]]] }; } + }, + {fieldname: "pickup_info", fieldtype: "HTML", label: "Pickup Details"}, + {fieldtype: "Section Break", label: "⚖️ Actual Load"}, + {fieldname: "received_date", fieldtype: "Date", label: "Received Date", reqd: 1, default: frappe.datetime.nowdate()}, + {fieldname: "actual_pallets", fieldtype: "Int", label: "Actual # of Pallets/Gaylords", reqd: 1, default: 1}, + {fieldname: "total_weight", fieldtype: "Data", label: "Total Weight (lbs)"}, + {fieldtype: "Column Break"}, + {fieldname: "load_contents", fieldtype: "Small Text", label: "Load Contents", placeholder: "e.g. 80% wire 20% tablets"}, + {fieldname: "data_status", fieldtype: "Select", label: "Data Status", options: "\nD0\nD1\nND1\nND2\nND3\nND4"}, + {fieldname: "red_r2", fieldtype: "Select", label: "RED / R2", options: "\nRED\nNIST\nRed+NIST\nR2\nBoth\nNeither"}, + ], + primary_action_label: "✓ Check In Load", + primary_action: function(values) { + if (values.actual_pallets < 1) { frappe.msgprint("Enter at least 1 pallet"); return; } + frappe.confirm("Check in " + values.actual_pallets + " pallet(s) for this load?", function() { + frappe.call({ + method: "westech_r2.westech_r2.api.receiving_api.checkin_load", + args: { + pickup_name: values.pickup_ref, + received_date: values.received_date, + actual_pallets: values.actual_pallets, + total_weight: values.total_weight, + load_contents: values.load_contents, + data_status: values.data_status, + red_r2: values.red_r2 + }, + callback: function(r) { + if (r.message && r.message.success) { + frappe.show_alert({message: "Load checked in! " + r.message.pallets_created + " pallet(s) in Load " + r.message.load, indicator: "green"}); + d.hide(); + loadCheckins(); + loadPickups(); + } + } + }); + }); + } + }); + // Load pickup details on selection + d.fields_dict.pickup_ref.$input.on("change", function() { + var val = d.get_value("pickup_ref"); + if (!val) { d.fields_dict.pickup_info.html(""); return; } + frappe.call({ + method: "westech_r2.westech_r2.api.receiving_api.get_pickup_details", + args: { pickup_name: val }, + callback: function(r) { + if (!r.message) return; + var p = r.message; + var html = '
'; + html += '
' + esc(p.company_name || p.customer_number || "Unknown") + '
'; + html += '
' + esc((p.address_line || "") + (p.city ? ", " + p.city : "") + (p.state ? ", " + p.state : "") + (p.zip_code ? " " + p.zip_code : "")) + '
'; + html += '
' + esc((p.contact_name || "") + (p.contact_phone ? " • " + p.contact_phone : "") + (p.contact_email ? " • " + p.contact_email : "")) + '
'; + if (p.red_r2 && p.red_r2 !== "Neither" && p.red_r2 !== "") { + html += '
⚠ ' + esc(p.red_r2) + ' — Special handling required' + (p.needs_aor ? ' • AoR ✓' : '') + (p.needs_cod ? ' • CoD ✓' : '') + '
'; + } + if (p.notes) { + html += '
Notes: ' + esc(p.notes) + '
'; + } + html += '
'; + d.fields_dict.pickup_info.html(html); + d.set_value("actual_pallets", p.estimated_items || 1); + d.set_value("data_status", p.data_status || ""); + d.set_value("red_r2", p.red_r2 || ""); + } + }); + }); + + d.show(); + }); + + // ── Stage C: Load Checkins ── function loadCheckins() { frappe.call({ - method: "westech_r2.api.receiving_api.get_checkins", + method: "westech_r2.westech_r2.api.receiving_api.get_checkins", callback: function(r) { if (r.message) renderCheckinTable(r.message.checkins || []); } @@ -478,89 +426,30 @@ frappe.pages['receiving'].on_page_load = function(wrapper) { function renderCheckinTable(checkins) { var tbody = $("#checkin-tbody"); if (!checkins.length) { - tbody.html('No check-ins yet'); + tbody.html('No check-ins yet'); return; } tbody.html(checkins.map(function(c) { - return '' + esc(c.pickup_date || "") + '' + - '' + esc(c.company_name || "") + '' + - '' + esc(c.pickup_type || "") + '' + - '' + (c.estimated_items || "—") + '' + - '' + (c.estimated_weight || "—") + '' + - '' + esc(c.load_contents || "") + '' + - '' + esc(c.data_status || "—") + '' + + return '' + + '' + esc(c.incoming_date || "") + '' + + '' + esc(c.customer_name || c.customer || "") + '' + + '' + esc(c.name || "") + '' + + '' + (c.pallet_count || 0) + '' + + '' + (c.total_weight || "—") + '' + + '' + esc(c.data_status || "") + '' + '' + esc(c.red_r2 || "—") + '' + - '' + esc(c.status || "") + ''; + '' + ((c.pallets || []).map(function(p) { return esc(p.pallet_number || p.name); }).join(", ")) + ''; }).join("")); } - $("#btn-new-checkin").on("click", function() { - $("#checkin-form").show(); - $("#ci-received_date").val(frappe.datetime.nowdate()); - checkin_pickup_control = frappe.ui.form.make_control({ - parent: $("#ci-pickup-control"), - df: { - fieldtype: "Link", - fieldname: "pickup_ref", - options: "Scheduled Pickup", - label: "Scheduled Pickup", - reqd: 1, - placeholder: "Search pickup...", - get_query: function() { - return { - filters: [ - ["Scheduled Pickup", "status", "in", ["Scheduled", "Routed", "In Progress"]] - ] - }; - } - }, - only_input: true, - }); - checkin_pickup_control.refresh(); - $("#ci-pickup-control .control-input").css("margin", "0"); - $("#ci-pickup-control .help-box").remove(); - }); - - $("#btn-cancel-checkin").on("click", function() { - $("#checkin-form").hide(); - }); - - $("#checkin-form-inner").on("submit", function(e) { - e.preventDefault(); - var pickupName = checkin_pickup_control ? checkin_pickup_control.get_value() : ""; - if (!pickupName) { frappe.msgprint("Select a pickup"); return; } - - var update = {}; - update.status = "Complete"; - if ($("#ci-actual_pallets").val()) update.estimated_items = parseInt($("#ci-actual_pallets").val()); - if ($("#ci-actual_weight").val()) update.estimated_weight = $("#ci-actual_weight").val(); - if ($("#ci-load_contents").val()) update.load_contents = $("#ci-load_contents").val(); - - frappe.call({ - method: "frappe.client.set_value", - args: { - doctype: "Scheduled Pickup", - name: pickupName, - fieldname: update - }, - callback: function(r) { - if (r.message) { - frappe.show_alert({ message: "Load checked in", indicator: "green" }); - $("#checkin-form").hide(); - loadCheckins(); - } - } - }); - }); - $("#btn-cor-report").on("click", function() { - window.open("/api/method/westech_r2.api.receiving_api.cor_report", "_blank"); + window.open("/api/method/westech_r2.westech_r2.api.receiving_api.cor_report", "_blank"); }); // ── Helpers ── function esc(s) { return s ? String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """) : ""; } - function dayName(d) { return ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][d.getDay()]; } // ── Init ── loadPickups(); -}; + loadCheckins(); +}; \ No newline at end of file diff --git a/westech_r2/westech_r2/page/receiving/receiving.js.bak3 b/westech_r2/westech_r2/page/receiving/receiving.js.bak3 new file mode 100644 index 0000000..85f0f44 --- /dev/null +++ b/westech_r2/westech_r2/page/receiving/receiving.js.bak3 @@ -0,0 +1,668 @@ +frappe.pages['receiving'].on_page_load = function(wrapper) { + var page = frappe.ui.make_app_page({ + parent: wrapper, + title: 'Receiving', + single_column: true + }); + + // Inline HTML + $(wrapper).find('.layout-main-section').html(` +
+
+
+

🚛 Receiving

+

Schedule pickups, manage routes, and check in loads.

+
+
+ +
+
+
+
+
+
📅 Pickup Calendar — Next 30 Days
+
Loading...
+
+
+
+
+
+
+
Scheduled Pickups
+
+ + + +
+
+
+
+ + + +
DateWeekdayTypeCustomerContactAddressEst. ItemsDataRED/R2StatusNotesTruckAoRCoD
Loading...
+
+
+
+
+ +
+
+
+
+
🚛 Truck 1
+
🚛 Truck 2
+
🚛 Truck 3
+
+
📋 Unassigned
+
+
+
+
+
Recent Check-ins
+
+ + + +
DateCustomerLoad #PalletsWeightContentsData StatusRED/R2
Loading...
+
+
+ +
+
+
+ + `); + + // Prevent Frappe router from intercepting tab clicks + // Tab switching — direct DOM to avoid Frappe router intercepting clicks + $("#receiving-tabs").on("click", ".stage-tab", function() { + var target = $(this).data("target"); + $("#receiving-tabs li, .stage-tab").removeClass("active"); + $(this).addClass("active").closest("li").addClass("active"); + $(".tab-pane").removeClass("active"); + $(target).addClass("active"); + }); + // ── Stage A: Link Controls ── + var customer_control = null; + + function setupCustomerLink() { + customer_control = frappe.ui.form.make_control({ + parent: $("#sp-customer-control"), + df: { + fieldtype: "Link", + fieldname: "customer_number", + options: "Customer", + label: "Customer", + reqd: 1, + placeholder: "Search by name, number, or address...", + onchange: function() { + var val = customer_control.get_value(); + if (val) fetchCustomerDetails(val); + else clearCustomerFields(); + } + }, + only_input: true, + }); + customer_control.refresh(); + $("#sp-customer-control .control-input").css("margin", "0"); + $("#sp-customer-control .help-box").remove(); + } + + // New Customer button + $(document).on("click", "#btn-new-customer", function() { + var d = new frappe.ui.Dialog({ + title: "New Customer", + fields: [ + {label: "Customer Name", fieldname: "customer_name", fieldtype: "Data", reqd: 1}, + {label: "Customer Type", fieldname: "customer_type", fieldtype: "Select", options: "Company\nIndividual", default: "Company"}, + {label: "Phone", fieldname: "phone", fieldtype: "Data"}, + {label: "Email", fieldname: "email", fieldtype: "Data"}, + ], + primary_action_label: "Create", + primary_action: function(values) { + d.hide(); + frappe.call({ + method: "frappe.client.insert", + args: { + doc: { + doctype: "Customer", + customer_name: values.customer_name, + customer_type: values.customer_type, + customer_group: "All Customer Groups", + territory: "United States", + } + }, + callback: function(r) { + if (r.message) { + $("#sp-company_name").val(values.customer_name); + if (window.customerSearchWidget) { + window.customerSearchWidget.set_value(r.message.name); + } + frappe.show_alert("Customer " + values.customer_name + " created", 3); + } + } + }); + } + }); + d.show(); + }); + + function fetchCustomerDetails(customer_name) { + frappe.call({ + method: "frappe.client.get", + args: { doctype: "Customer", name: customer_name }, + callback: function(r) { + if (!r.message) return; + var c = r.message; + $("#sp-company_name").val(c.customer_name || ""); + $("#sp-contact_name").val(c.contact_name || ""); + $("#sp-contact_phone").val(c.contact_phone || ""); + $("#sp-contact_email").val(c.contact_email || ""); + + frappe.call({ + method: "frappe.client.get_list", + args: { + doctype: "Address", + filters: [["Dynamic Link", "link_name", "=", customer_name]], + fields: ["address_line1", "city", "state", "pincode"], + limit_page_length: 1 + }, + callback: function(ra) { + if (ra.message && ra.message.length) { + var a = ra.message[0]; + $("#sp-address_line").val(a.address_line1 || ""); + $("#sp-city").val(a.city || ""); + $("#sp-state").val(a.state || "AZ"); + $("#sp-zip_code").val(a.pincode || ""); + } + } + }); + + frappe.call({ + method: "frappe.client.get_list", + args: { + doctype: "Contact", + filters: [["Dynamic Link", "link_name", "=", customer_name]], + fields: ["first_name", "last_name", "email_id", "phone", "mobile_no"], + limit_page_length: 1 + }, + callback: function(rc) { + if (rc.message && rc.message.length) { + var ct = rc.message[0]; + if (!$("#sp-contact_name").val()) { + $("#sp-contact_name").val((ct.first_name || "") + " " + (ct.last_name || "")); + } + if (!$("#sp-contact_phone").val()) { + $("#sp-contact_phone").val(ct.phone || ct.mobile_no || ""); + } + if (!$("#sp-contact_email").val()) { + $("#sp-contact_email").val(ct.email_id || ""); + } + } + } + }); + } + }); + } + + function clearCustomerFields() { + $("#sp-company_name, #sp-contact_name, #sp-contact_phone, #sp-contact_email, #sp-address_line, #sp-city, #sp-state, #sp-zip_code").val(""); + $("#sp-state").val("AZ"); + } + + // ── Stage A: Load Pickups ── + function loadPickups() { + var dateFilter = $("#pickup-date-filter").val(); + frappe.call({ + method: "westech_r2.westech_r2.api.receiving_api.get_pickups", + args: { date: dateFilter || undefined }, + callback: function(r) { + if (r.message) { + renderPickupTable(r.message.pickups || []); + renderCalendar(r.message.calendar || []); + } + } + }); + } + + function renderPickupTable(pickups) { + var tbody = $("#pickup-tbody"); + $("#pickup-count-label").text("(" + pickups.length + ")"); + if (!pickups.length) { + tbody.html('No pickups found'); + return; + } + tbody.html(pickups.map(function(p) { + var dt = p.pickup_date ? new Date(p.pickup_date + "T00:00:00") : null; + var dn = dt ? dayName(dt) : ""; + return '' + + '' + esc(p.pickup_date || "") + '' + + '' + dn + '' + + '' + esc(p.pickup_type || "") + '' + + '' + esc(p.company_name || p.customer_number || "") + '' + + '' + esc(p.contact_name || "") + '' + + '' + esc((p.address_line || "") + (p.city ? ", " + p.city : "")) + '' + + '' + (p.estimated_items || "—") + '' + + '' + esc(p.data_status || "—") + '' + + '' + esc(p.red_r2 || "—") + '' + + '' + esc(p.status || "") + '' + + '' + esc(p.notes || "") + '' + + '' + esc(p.truck || "") + '' + + '' + (p.needs_aor ? "✓" : "") + '' + + '' + (p.needs_cod ? "✓" : "") + ''; + }).join("")); + } + + function renderCalendar(calendar) { + var el = $("#pickup-calendar"); + if (!calendar.length) { el.html('
No data
'); return; } + var todayStr = frappe.datetime.nowdate(); + var html = '
'; + calendar.forEach(function(d) { + var cls = "cal-day"; + if (d.count > 0) cls += " has-pickups"; + if (d.date === todayStr) cls += " today"; + var label = d.date.substring(5); + html += '
' + label + '
'; + }); + html += '
'; + el.html(html); + } + + $(document).on("click", ".cal-day.has-pickups", function() { + $("#pickup-date-filter").val($(this).data("date")); + loadPickups(); + }); + + // ── Stage A: New Pickup ── + $("#btn-new-pickup").on("click", function() { + $("#new-pickup-form").show(); + $("#sp-pickup_date").val(frappe.datetime.nowdate()); + setupCustomerLink(); + }); + + $("#btn-cancel-pickup").on("click", function() { + $("#new-pickup-form").hide(); + }); + + $("#pickup-form").on("submit", function(e) { + e.preventDefault(); + var customerVal = customer_control ? customer_control.get_value() : ""; + if (!customerVal) { frappe.msgprint("Select a customer"); return; } + + var doc = { + doctype: "Scheduled Pickup", + pickup_date: $("#sp-pickup_date").val(), + pickup_type: $("#sp-pickup_type").val(), + customer_number: customerVal, + company_name: $("#sp-company_name").val(), + contact_name: $("#sp-contact_name").val(), + contact_phone: $("#sp-contact_phone").val(), + contact_email: $("#sp-contact_email").val(), + address_line: $("#sp-address_line").val(), + city: $("#sp-city").val(), + state: $("#sp-state").val(), + zip_code: $("#sp-zip_code").val(), + estimated_items: parseInt($("#sp-estimated_items").val()) || 0, + estimated_weight: $("#sp-estimated_weight").val(), + load_contents: $("#sp-load_contents").val(), + data_status: $("#sp-data_status").val(), + red_r2: $("#sp-red_r2").val(), + needs_aor: $("#sp-needs_aor").is(":checked") ? 1 : 0, + needs_cod: $("#sp-needs_cod").is(":checked") ? 1 : 0, + notes: $("#sp-notes").val(), + status: "Scheduled" + }; + frappe.call({ + method: "frappe.client.insert", + args: { doc: doc }, + callback: function(r) { + if (r.message) { + frappe.show_alert({ message: "Pickup scheduled", indicator: "green" }); + $("#new-pickup-form").hide(); + loadPickups(); + } + } + }); + }); + + $("#pickup-date-filter").on("change", loadPickups); + $("#btn-clear-date").on("click", function() { + $("#pickup-date-filter").val(""); + loadPickups(); + }); + + // ── Stage B: Routing ── + function loadRoutes() { + var date = $("#route-date").val() || frappe.datetime.nowdate(); + $("#route-date").val(date); + frappe.call({ + method: "westech_r2.westech_r2.api.receiving_api.get_pickups", + args: { date: date }, + callback: function(r) { + if (r.message) renderRouteColumns(r.message.pickups || []); + } + }); + } + + function renderRouteColumns(pickups) { + var trucks = { "Truck 1": [], "Truck 2": [], "Truck 3": [], "Unassigned": [] }; + pickups.forEach(function(p) { + var t = p.truck || ""; + if (t && trucks[t]) trucks[t].push(p); + else trucks["Unassigned"].push(p); + }); + ["Truck 1", "Truck 2", "Truck 3"].forEach(function(t) { + var key = t.toLowerCase().replace(/ /g, ""); + $("#" + key + "-count").text("(" + trucks[t].length + " stops)"); + $("#" + key + "-stops").html(trucks[t].map(function(p, i) { return stopCard(p, i + 1); }).join("")); + }); + $("#unassigned-count").text("(" + trucks["Unassigned"].length + ")"); + $("#unassigned-stops").html(trucks["Unassigned"].map(function(p) { return stopCard(p, 0); }).join("")); + } + + function stopCard(p, order) { + var h = '
'; + if (order) h += '
Stop #' + order + '
'; + h += '
' + esc(p.company_name || p.customer_number || "Unknown") + '
'; + h += '
' + esc((p.address_line || "") + (p.city ? ", " + p.city : "")) + '
'; + h += '
'; + if (p.estimated_items) h += '' + p.estimated_items + ' items'; + if (p.data_status) h += '' + esc(p.data_status) + ''; + if (p.red_r2) h += '' + esc(p.red_r2) + ''; + if (p.needs_aor) h += 'AoR'; + if (p.needs_cod) h += 'CoD'; + h += '
'; + return h; + } + + $("#btn-load-routes").on("click", loadRoutes); + + $("#btn-auto-route").on("click", function() { + var date = $("#route-date").val(); + if (!date) { frappe.msgprint("Select a date first"); return; } + frappe.call({ + method: "westech_r2.westech_r2.api.receiving_api.auto_route", + args: { date: date }, + callback: function(r) { + if (r.message && r.message.success) { + frappe.show_alert({ message: "Routes optimized", indicator: "green" }); + loadRoutes(); + } + } + }); + }); + + $("#btn-route-sheet").on("click", function() { + var date = $("#route-date").val() || frappe.datetime.nowdate(); + window.open("/api/method/westech_r2.westech_r2.api.receiving_api.print_route_sheet?date=" + date, "_blank"); + }); + + $("#btn-green-sheet").on("click", function() { + var date = $("#route-date").val() || frappe.datetime.nowdate(); + window.open("/api/method/westech_r2.westech_r2.api.receiving_api.print_green_sheet?date=" + date, "_blank"); + }); + + $("#btn-labels").on("click", function() { + var date = $("#route-date").val() || frappe.datetime.nowdate(); + window.open("/api/method/westech_r2.westech_r2.api.receiving_api.print_labels?date=" + date, "_blank"); + }); + + // ── Stage C: Check-in ── + var checkin_pickup_control = null; + var current_pickup_details = null; + + function loadCheckins() { + frappe.call({ + method: "westech_r2.westech_r2.api.receiving_api.get_checkins", + callback: function(r) { + if (r.message) renderCheckinTable(r.message.checkins || []); + } + }); + } + + function renderCheckinTable(checkins) { + var tbody = $("#checkin-tbody"); + if (!checkins.length) { + tbody.html('No check-ins yet'); + return; + } + tbody.html(checkins.map(function(c) { + var palletInfo = (c.pallets || []).map(function(p) { return esc(p.pallet_number || p.name); }).join(", "); + return '' + + '' + esc(c.incoming_date || "") + '' + + '' + esc(c.customer_name || c.customer || "") + '' + + '' + esc(c.name || "") + '' + + '' + (c.pallet_count || 0) + '' + + '' + (c.total_weight || "—") + '' + + '' + esc(c.data_status || "") + '' + + '' + esc(c.red_r2 || "—") + '' + + '' + palletInfo + ''; + }).join("")); + } + + $("#btn-new-checkin").on("click", function() { + $("#checkin-form").show(); + $("#ci-received_date").val(frappe.datetime.nowdate()); + $("#ci-pickup-details").hide(); + current_pickup_details = null; + checkin_pickup_control = frappe.ui.form.make_control({ + parent: $("#ci-pickup-control"), + df: { + fieldtype: "Link", + fieldname: "pickup_ref", + options: "Scheduled Pickup", + label: "Scheduled Pickup", + reqd: 1, + placeholder: "Search pickup...", + get_query: function() { + return { + filters: [ + ["Scheduled Pickup", "status", "in", ["Scheduled", "Routed", "In Progress"]] + ] + }; + }, + onchange: function() { + var val = checkin_pickup_control ? checkin_pickup_control.get_value() : ""; + if (val) loadPickupDetails(val); + else { + $("#ci-pickup-details").hide(); + current_pickup_details = null; + } + } + }, + only_input: true, + }); + checkin_pickup_control.refresh(); + $("#ci-pickup-control .control-input").css("margin", "0"); + $("#ci-pickup-control .help-box").remove(); + }); + + function loadPickupDetails(pickup_name) { + frappe.call({ + method: "westech_r2.westech_r2.api.receiving_api.get_pickup_details", + args: { pickup_name: pickup_name }, + callback: function(r) { + if (!r.message) return; + current_pickup_details = r.message; + var d = r.message; + + $("#ci-pickup-details").show(); + $("#ci-customer-info").text((d.company_name || d.customer_number || "Unknown") + (d.red_r2 ? " — " + d.red_r2 : "")); + $("#ci-address-info").text((d.address_line || "") + (d.city ? ", " + d.city : "") + (d.state ? ", " + d.state : "") + (d.zip_code ? " " + d.zip_code : "")); + $("#ci-contact-info").text((d.contact_name || "") + (d.contact_phone ? " • " + d.contact_phone : "") + (d.contact_email ? " • " + d.contact_email : "")); + + // Special handling for RED/NIST + if (d.red_r2 && d.red_r2 !== "Neither" && d.red_r2 !== "") { + $("#ci-special-handling").show().html("⚠ " + esc(d.red_r2) + " — Special handling required" + (d.needs_aor ? " • AoR ✓" : "") + (d.needs_cod ? " • CoD ✓" : "")); + } else { + $("#ci-special-handling").hide(); + } + + // Notes + if (d.notes) { + $("#ci-pickup-notes").show().html("Notes: " + esc(d.notes)); + } else { + $("#ci-pickup-notes").hide(); + } + + // Pre-fill check-in fields from pickup + $("#ci-actual_pallets").val(d.estimated_items || 1); + $("#ci-data_status").val(d.data_status || ""); + $("#ci-red_r2").val(d.red_r2 || ""); + } + }); + } + + $("#btn-cancel-checkin").on("click", function() { + $("#checkin-form").hide(); + }); + + $("#checkin-form-inner").on("submit", function(e) { + e.preventDefault(); + var pickupName = checkin_pickup_control ? checkin_pickup_control.get_value() : ""; + if (!pickupName) { frappe.msgprint("Select a pickup"); return; } + + var actualPallets = parseInt($("#ci-actual_pallets").val()) || 0; + if (actualPallets < 1) { frappe.msgprint("Enter at least 1 pallet"); return; } + + frappe.confirm( + "Check in " + actualPallets + " pallet(s) for this load?", + function() { + frappe.call({ + method: "westech_r2.westech_r2.api.receiving_api.checkin_load", + args: { + pickup_name: pickupName, + received_date: $("#ci-received_date").val(), + actual_pallets: actualPallets, + total_weight: $("#ci-total_weight").val(), + load_contents: $("#ci-load_contents").val(), + data_status: $("#ci-data_status").val(), + red_r2: $("#ci-red_r2").val() + }, + callback: function(r) { + if (r.message && r.message.success) { + frappe.show_alert({ + message: "Load checked in! Created " + r.message.pallets_created + " pallet(s) in Load " + r.message.load, + indicator: "green" + }); + $("#checkin-form").hide(); + loadCheckins(); + loadPickups(); + } + } + }); + } + ); + }); + + $("#btn-cor-report").on("click", function() { + window.open("/api/method/westech_r2.westech_r2.api.receiving_api.cor_report", "_blank"); + }); + + // ── Helpers ── + function esc(s) { return s ? String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """) : ""; } + function dayName(d) { return ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][d.getDay()]; } + + // ── Init ── + loadPickups(); + loadCheckins(); +}; \ No newline at end of file diff --git a/westech_r2/westech_r2/page/route_planner/route-planner.js b/westech_r2/westech_r2/page/route_planner/route-planner.js index 9d8ff74..9a0168d 100644 --- a/westech_r2/westech_r2/page/route_planner/route-planner.js +++ b/westech_r2/westech_r2/page/route_planner/route-planner.js @@ -6,7 +6,7 @@ frappe.pages["route-planner"].on_page_load = function(wrapper) { }); frappe.call({ - method: "westech_r2.api.optimize_routes.get_scheduled_pickups", + method: "westech_r2.westech_r2.api.optimize_routes.get_scheduled_pickups", callback: function(r) { if (r.message) { renderRoutePlanner(page, r.message); @@ -26,7 +26,7 @@ frappe.pages["route-planner"].on_page_load = function(wrapper) { $("#optimize-btn").on("click", function() { frappe.call({ - method: "westech_r2.api.optimize_routes.optimize_routes", + method: "westech_r2.westech_r2.api.optimize_routes.optimize_routes", callback: function(r) { if (r.message) { frappe.show_alert({message: "Routes optimized", indicator: "green"}); diff --git a/westech_r2/westech_r2/workspace/westech/westech.json b/westech_r2/westech_r2/workspace/westech/westech.json new file mode 100644 index 0000000..85b59f4 --- /dev/null +++ b/westech_r2/westech_r2/workspace/westech/westech.json @@ -0,0 +1,136 @@ +{ + "charts": [], + "content": "[{\"id\": \"sc1\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Receiving Dashboard\", \"col\": 3}}, {\"id\": \"sc2\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Customer Manager\", \"col\": 3}}, {\"id\": \"sc3\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"New Pickup\", \"col\": 3}}, {\"id\": \"sc4\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"New Load\", \"col\": 3}}, {\"id\": \"sc5\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"New Customer\", \"col\": 3}}, {\"id\": \"sp1\", \"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"id\": \"hd1\", \"type\": \"header\", \"data\": {\"text\": \"Quick Links\", \"col\": 12}}, {\"id\": \"lk1\", \"type\": \"link\", \"data\": {\"link_name\": \"Scheduled Pickups\", \"col\": 4}}, {\"id\": \"lk2\", \"type\": \"link\", \"data\": {\"link_name\": \"Loads\", \"col\": 4}}, {\"id\": \"lk3\", \"type\": \"link\", \"data\": {\"link_name\": \"Pallets\", \"col\": 4}}, {\"id\": \"lk4\", \"type\": \"link\", \"data\": {\"link_name\": \"Customers\", \"col\": 4}}, {\"id\": \"lk5\", \"type\": \"link\", \"data\": {\"link_name\": \"Serial Nos\", \"col\": 4}}, {\"id\": \"lk6\", \"type\": \"link\", \"data\": {\"link_name\": \"Warehouses\", \"col\": 4}}, {\"id\": \"lk7\", \"type\": \"link\", \"data\": {\"link_name\": \"Stock Entry\", \"col\": 4}}]", + "creation": "2026-05-20 07:36:01.779902", + "custom_blocks": [], + "docstatus": 0, + "doctype": "Workspace", + "hide_custom": 0, + "icon": "stock", + "idx": 0, + "indicator_color": "green", + "is_hidden": 0, + "label": "Westech", + "links": [ + { + "hidden": 0, + "icon": "calendar", + "is_query_report": 0, + "label": "Scheduled Pickups", + "link_count": 0, + "link_to": "Scheduled Pickup", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "icon": "truck", + "is_query_report": 0, + "label": "Loads", + "link_count": 0, + "link_to": "Load", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "icon": "box", + "is_query_report": 0, + "label": "Pallets", + "link_count": 0, + "link_to": "Pallet", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "icon": "user", + "is_query_report": 0, + "label": "Customers", + "link_count": 0, + "link_to": "Customer", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "icon": "bar-code", + "is_query_report": 0, + "label": "Serial Nos", + "link_count": 0, + "link_to": "Serial No", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "icon": "warehouse", + "is_query_report": 0, + "label": "Warehouses", + "link_count": 0, + "link_to": "Warehouse", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "icon": "stock-entry", + "is_query_report": 0, + "label": "Stock Entry", + "link_count": 0, + "link_to": "Stock Entry", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + } + ], + "modified": "2026-05-25 15:52:23.527530", + "modified_by": "Administrator", + "module": "Westech R2", + "name": "Westech", + "number_cards": [], + "owner": "Administrator", + "public": 1, + "quick_lists": [], + "roles": [], + "sequence_id": 0.0, + "shortcuts": [ + { + "doc_view": "", + "label": "Receiving Dashboard", + "link_to": "receiving", + "type": "Page" + }, + { + "doc_view": "List", + "label": "Customer Manager", + "link_to": "Customer", + "type": "DocType" + }, + { + "doc_view": "New", + "label": "New Pickup", + "link_to": "Scheduled Pickup", + "type": "DocType" + }, + { + "doc_view": "New", + "label": "New Load", + "link_to": "Load", + "type": "DocType" + }, + { + "doc_view": "New", + "label": "New Customer", + "link_to": "Customer", + "type": "DocType" + } + ], + "title": "Westech" +} \ No newline at end of file