wip: receiving dashboard, customer records, archive old 2-level paths, various fixes
This commit is contained in:
Binary file not shown.
@@ -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 = "<!DOCTYPE html><html><head><title>CoR Report</title>"
|
||||
html += "<style>body{font-family:Arial,sans-serif;margin:40px;}"
|
||||
html += "table{border-collapse:collapse;width:100%;}"
|
||||
html += "th,td{border:1px solid #ddd;padding:8px;text-align:left;font-size:13px;}"
|
||||
html += "th{background:#2F5496;color:#fff;}h1{color:#2F5496;}</style></head><body>"
|
||||
html += "<h1>Certificate of Recycling (CoR) Report</h1>"
|
||||
html += "<p>Generated: " + frappe.utils.now() + "</p>"
|
||||
html += "<p>Total completed loads: " + str(len(pickups)) + "</p>"
|
||||
|
||||
if pickups:
|
||||
html += "<table><tr><th>Date</th><th>Customer</th><th>Items</th><th>Weight</th>"
|
||||
html += "<th>Contents</th><th>Data Status</th><th>RED/R2</th><th>AoR</th><th>CoD</th></tr>"
|
||||
for p in pickups:
|
||||
html += "<tr><td>" + str(p.get("pickup_date", "")) + "</td>"
|
||||
html += "<td>" + str(p.get("company_name", "")) + "</td>"
|
||||
html += "<td>" + str(p.get("estimated_items", "")) + "</td>"
|
||||
html += "<td>" + str(p.get("estimated_weight", "")) + "</td>"
|
||||
html += "<td>" + str(p.get("load_contents", "")) + "</td>"
|
||||
html += "<td>" + str(p.get("data_status", "")) + "</td>"
|
||||
html += "<td>" + str(p.get("red_r2", "")) + "</td>"
|
||||
html += "<td>" + ("✓" if p.get("needs_aor") else "") + "</td>"
|
||||
html += "<td>" + ("✓" if p.get("needs_cod") else "") + "</td></tr>"
|
||||
html += "</table>"
|
||||
else:
|
||||
html += "<p>No completed loads found.</p>"
|
||||
|
||||
html += "</body></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 = "<!DOCTYPE html><html><head><title>Route Sheet</title>"
|
||||
html += "<style>body{font-family:Arial,sans-serif;margin:30px;}"
|
||||
html += "table{border-collapse:collapse;width:100%;margin-bottom:12px;}"
|
||||
html += "th,td{border:1px solid #999;padding:6px 10px;text-align:left;font-size:12px;}"
|
||||
html += "th{background:#2F5496;color:#fff;}h1{color:#2F5496;font-size:20px;}"
|
||||
html += ".truck-header{background:#f0f0f0;padding:8px;font-weight:700;font-size:14px;border:1px solid #ccc;}"
|
||||
html += "@media print{body{margin:10px;}}</style></head><body>"
|
||||
html += "<h1>Route Sheet — " + str(date or "Today") + "</h1>"
|
||||
|
||||
for truck_name, stops in sorted(trucks.items()):
|
||||
html += '<div class="truck-header">🚛 ' + truck_name + " — " + str(len(stops)) + " stops</div>"
|
||||
html += "<table><tr><th>#</th><th>Customer</th><th>Address</th><th>Contact</th>"
|
||||
html += "<th>Items</th><th>Weight</th><th>Data</th><th>RED/R2</th><th>AoR</th><th>CoD</th><th>Notes</th></tr>"
|
||||
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 += "<tr><td>" + str(i) + "</td><td>" + str(s.get("company_name", "")) + "</td>"
|
||||
html += "<td>" + addr + "</td><td>" + str(s.get("contact_name", "")) + "<br>" + str(s.get("contact_phone", "")) + "</td>"
|
||||
html += "<td>" + str(s.get("estimated_items", "")) + "</td><td>" + str(s.get("estimated_weight", "")) + "</td>"
|
||||
html += "<td>" + str(s.get("data_status", "")) + "</td><td>" + str(s.get("red_r2", "")) + "</td>"
|
||||
html += "<td>" + ("✓" if s.get("needs_aor") else "") + "</td>"
|
||||
html += "<td>" + ("✓" if s.get("needs_cod") else "") + "</td>"
|
||||
html += "<td>" + str(s.get("notes", "")) + "</td></tr>"
|
||||
html += "</table>"
|
||||
|
||||
if unassigned:
|
||||
html += "<h2>Unassigned</h2><table><tr><th>Customer</th><th>Address</th><th>Notes</th></tr>"
|
||||
for s in unassigned:
|
||||
html += "<tr><td>" + str(s.get("company_name", "")) + "</td>"
|
||||
html += "<td>" + str(s.get("address_line", "")) + "</td>"
|
||||
html += "<td>" + str(s.get("notes", "")) + "</td></tr>"
|
||||
html += "</table>"
|
||||
|
||||
html += "</body></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 = "<!DOCTYPE html><html><head><title>Green Sheets</title>"
|
||||
html += "<style>body{font-family:Arial,sans-serif;margin:30px;}"
|
||||
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;}}</style></head><body>"
|
||||
|
||||
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 += '<div class="green-sheet">'
|
||||
html += '<div class="gs-title">🟢 GREEN SHEET — Data-Bearing Equipment Tracking</div>'
|
||||
html += '<div style="text-align:right;font-size:12px;">Pallet # ' + str(pallet.get("pallet_number", "")) + ' | Load # ' + str(load.get("name", "")) + ' | ' + str(load.get("incoming_date", "")) + '</div>'
|
||||
|
||||
# Customer block
|
||||
html += '<div class="gs-customer">'
|
||||
service_level = ""
|
||||
if pallet.get("red_r2"):
|
||||
service_level = pallet.get("red_r2", "")
|
||||
html += '<strong>(' + str(load.get("customer", "")) + ') — ' + str(service_level) + ' — ' + str(load.get("customer_name", "")) + '</strong>'
|
||||
html += '</div>'
|
||||
|
||||
# Service level banner (only for RED/NIST)
|
||||
rr = pallet.get("red_r2", "")
|
||||
if rr and rr != "Neither":
|
||||
html += '<div class="gs-service-banner">SERVICE LEVEL: ' + str(rr).upper() + '</div>'
|
||||
|
||||
# Driver instructions (notes)
|
||||
if pallet.get("notes"):
|
||||
html += '<div class="gs-driver"><strong>Driver Instructions:</strong><br>' + str(pallet.get("notes", "")) + '</div>'
|
||||
|
||||
# RED LINE instructions (for RED/NIST)
|
||||
if rr and rr not in ("", "Neither", "R2"):
|
||||
html += '<div class="gs-redline"><strong>⚠ RED LINE INSTRUCTIONS</strong><br>All data-bearing equipment must be tracked. Destruction method per customer specification.</div>'
|
||||
|
||||
# Pallet details table
|
||||
html += '<table><tr><th>Pallet Designation</th><th>Data Status</th></tr>'
|
||||
html += '<tr><td>' + str(pallet.get("status", "Received")) + '</td><td>' + str(pallet.get("data_status", "")) + '</td></tr>'
|
||||
html += '<tr><th>Inbound Weight</th><th>Total Items</th></tr>'
|
||||
html += '<tr><td>' + str(pallet.get("inbound_weight", "")) + '</td><td>' + str(pallet.get("total_items", "")) + '</td></tr>'
|
||||
html += '<tr><th>AoR/CoR</th><th>Contents</th></tr>'
|
||||
aor_cor = ""
|
||||
if pallet.get("needs_aor"): aor_cor += "✓ AoR "
|
||||
if pallet.get("needs_cod"): aor_cor += "✓ CoD"
|
||||
html += '<tr><td>' + (aor_cor or "None") + '</td><td>' + str(pallet.get("description", "")) + '</td></tr>'
|
||||
html += '</table>'
|
||||
|
||||
# Material tracking (hand-write on paper)
|
||||
html += '<table><tr><th>Material</th><th>%</th><th>Weight</th><th>Sign Off</th><th>Date</th></tr>'
|
||||
for _ in range(4):
|
||||
html += '<tr><td> </td><td></td><td></td><td></td><td></td></tr>'
|
||||
html += '</table>'
|
||||
|
||||
# R2 warning
|
||||
html += '<div class="gs-r2warning">⚠ R2 REQUIREMENT: This pallet contains data-bearing equipment. All devices must be tracked through erasure with 5% verification audit.</div>'
|
||||
|
||||
# Signatures
|
||||
html += '<table><tr><th>Received By</th><th>Inspected By</th><th>Verified By</th></tr>'
|
||||
html += '<tr><td> </td><td> </td><td> </td></tr></table>'
|
||||
|
||||
# Footer
|
||||
html += '<div class="gs-footer">Westech Electronics • Green Sheet — Pallet # ' + str(pallet.get("pallet_number", "")) + ' • Printed ' + str(frappe.utils.nowdate()) + ' • KEEP WITH PALLET AT ALL TIMES</div>'
|
||||
html += '</div>'
|
||||
|
||||
html += "</body></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 = "<!DOCTYPE html><html><head><title>Labels</title>"
|
||||
html += "<style>body{font-family:Arial,sans-serif;margin:10px;}"
|
||||
html += ".label{border:2px solid #000;width:3in;height:1.5in;display:inline-block;margin:4px;padding:6px;font-size:11px;page-break-inside:avoid;}"
|
||||
html += ".label-co{font-weight:700;font-size:14px;}.label-date{font-size:10px;color:#666;}"
|
||||
html += ".label-status{font-size:10px;margin-top:2px;}"
|
||||
html += "@media print{body{margin:0;}}</style></head><body>"
|
||||
|
||||
for p in pickups:
|
||||
n = p.get("num_labels") or 1
|
||||
for _ in range(n):
|
||||
html += '<div class="label">'
|
||||
html += '<div class="label-co">' + str(p.get("company_name", "")) + '</div>'
|
||||
html += '<div class="label-date">' + str(p.get("pickup_date", "")) + '</div>'
|
||||
html += '<div class="label-status">' + str(p.get("data_status", "")) + " | " + str(p.get("red_r2", "")) + '</div>'
|
||||
html += '</div>'
|
||||
|
||||
html += "</body></html>"
|
||||
frappe.local.response["type"] = "html"
|
||||
frappe.local.response["page_content"] = html
|
||||
@@ -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",
|
||||
@@ -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(`
|
||||
<div class="receiving-station" style="padding: 20px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3 style="margin-top: 0; color: #2F5496;">🚛 Receiving</h3>
|
||||
<p class="text-muted">Schedule pickups, manage routes, and check in loads.</p>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="nav nav-tabs" role="tablist" id="receiving-tabs">
|
||||
<li role="presentation" class="active"><span class="stage-tab active" data-target="#stage-a" style="cursor:pointer">📋 Stage A — Schedule Pickup</span></li>
|
||||
<li role="presentation"><span class="stage-tab" data-target="#stage-b" style="cursor:pointer">🗺️ Stage B — Route & Dispatch</span></li>
|
||||
<li role="presentation"><span class="stage-tab" data-target="#stage-c" style="cursor:pointer">⚖️ Stage C — Load Check-in</span></li>
|
||||
</ul>
|
||||
<div class="tab-content" style="padding-top: 20px;">
|
||||
<div role="tabpanel" class="tab-pane active" id="stage-a">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">📅 Pickup Calendar — Next 30 Days</div>
|
||||
<div class="panel-body" id="pickup-calendar"><div class="text-muted text-center">Loading...</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="row">
|
||||
<div class="col-md-6"><strong>Scheduled Pickups</strong><span id="pickup-count-label" class="text-muted" style="margin-left: 8px;"></span></div>
|
||||
<div class="col-md-6 text-right">
|
||||
<button class="btn btn-primary btn-sm" id="btn-new-pickup">+ New Pickup</button>
|
||||
<input type="date" id="pickup-date-filter" class="form-control input-sm" style="display:inline-block;width:auto;vertical-align:middle;margin-left:8px;">
|
||||
<button class="btn btn-default btn-sm" id="btn-clear-date" style="margin-left:4px;">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body" style="padding: 0; overflow-x: auto;">
|
||||
<table class="table table-striped table-hover" id="pickup-table" style="font-size: 13px; margin-bottom: 0;">
|
||||
<thead><tr><th>Date</th><th>Weekday</th><th>Type</th><th>Customer</th><th>Contact</th><th>Address</th><th>Est. Items</th><th>Data</th><th>RED/R2</th><th>Status</th><th>Notes</th><th>Truck</th><th>AoR</th><th>CoD</th></tr></thead>
|
||||
<tbody id="pickup-tbody"><tr><td colspan="14" class="text-center text-muted">Loading...</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="new-pickup-form" style="display:none; margin-top: 16px;">
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">+ New Scheduled Pickup</div>
|
||||
<div class="panel-body">
|
||||
<form id="pickup-form">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<h5 style="color:#6f42c1;">📅 Pickup Info</h5>
|
||||
<div class="form-group"><label>Pickup Date <span class="text-danger">*</span></label><input type="date" id="sp-pickup_date" class="form-control" required></div>
|
||||
<div class="form-group"><label>Type <span class="text-danger">*</span></label><select id="sp-pickup_type" class="form-control" required><option value="Pickup">Pickup</option><option value="Drop-off">Drop-off</option></select></div>
|
||||
<div class="form-group">
|
||||
<label>Customer <span class="text-danger">*</span></label>
|
||||
<div class="input-group">
|
||||
<div id="sp-customer-control" style="flex:1;"></div>
|
||||
<span class="input-group-btn"><button class="btn btn-success" id="btn-new-customer" type="button" title="New Customer">+</button></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"><label>Company Name</label><input type="text" id="sp-company_name" class="form-control"></div>
|
||||
<div class="form-group"><label>Contact Name</label><input type="text" id="sp-contact_name" class="form-control"></div>
|
||||
<div class="form-group"><label>Contact Phone</label><input type="text" id="sp-contact_phone" class="form-control"></div>
|
||||
<div class="form-group"><label>Contact Email</label><input type="email" id="sp-contact_email" class="form-control"></div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h5 style="color:#6f42c1;">📍 Address</h5>
|
||||
<div class="form-group"><label>Street Address</label><input type="text" id="sp-address_line" class="form-control"></div>
|
||||
<div class="form-group"><label>City</label><input type="text" id="sp-city" class="form-control"></div>
|
||||
<div class="form-group"><label>State</label><input type="text" id="sp-state" class="form-control" value="AZ"></div>
|
||||
<div class="form-group"><label>ZIP</label><input type="text" id="sp-zip_code" class="form-control"></div>
|
||||
<div class="form-group"><label>Hours of Operation</label><input type="text" id="sp-hours_of_operation" class="form-control" placeholder="e.g. Mon-Fri 8am-5pm"></div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h5 style="color:#6f42c1;">📦 Load Info</h5>
|
||||
<div class="form-group"><label>Estimated Items</label><input type="number" id="sp-estimated_items" class="form-control"></div>
|
||||
<div class="form-group"><label>Estimated Weight</label><input type="text" id="sp-estimated_weight" class="form-control"></div>
|
||||
<div class="form-group"><label>Load Contents</label><input type="text" id="sp-load_contents" class="form-control" placeholder="Wire, Monitors, Laptops..."></div>
|
||||
<div class="form-group"><label>Data Status</label><select id="sp-data_status" class="form-control"><option value="">—</option><option value="D0">D0</option><option value="D1">D1</option><option value="ND1">ND1</option><option value="ND2">ND2</option><option value="ND3">ND3</option><option value="ND4">ND4</option></select></div>
|
||||
<div class="form-group"><label>RED / R2</label><select id="sp-red_r2" class="form-control"><option value="">—</option><option value="RED">RED</option><option value="NIST">NIST</option><option value="Red+NIST">Red+NIST</option><option value="R2">R2</option><option value="Both">Both</option><option value="Neither">Neither</option></select></div>
|
||||
<div class="form-group"><div class="checkbox"><label><input type="checkbox" id="sp-needs_aor"> <strong>Needs AoR</strong></label></div><div class="checkbox"><label><input type="checkbox" id="sp-needs_cod"> <strong>Needs CoD</strong></label></div></div>
|
||||
<div class="form-group"><label>Notes / Special Handling</label><textarea id="sp-notes" class="form-control" rows="3"></textarea></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="margin-top: 16px;"><div class="col-md-12"><button type="submit" class="btn btn-primary btn-lg">Save Pickup</button><button type="button" class="btn btn-default btn-lg" id="btn-cancel-pickup">Cancel</button></div></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="stage-b">
|
||||
<div class="row" style="margin-bottom: 16px;"><div class="col-md-12"><div class="btn-group"><input type="date" id="route-date" class="form-control" style="display:inline-block;width:auto;"><button class="btn btn-primary" id="btn-load-routes">Load Pickups</button><button class="btn btn-primary" id="btn-auto-route">🧮 Auto-Route</button><button class="btn btn-success" id="btn-route-sheet">🖨️ Route Sheet</button><button class="btn btn-success" id="btn-green-sheet">📄 Green Sheet</button><button class="btn btn-success" id="btn-labels">🏷️ Labels</button></div></div></div>
|
||||
<div class="row" id="route-columns">
|
||||
<div class="col-md-4"><div class="panel panel-default truck-column" data-truck="Truck 1"><div class="panel-heading">🚛 Truck 1 <span id="truck1-count" class="text-muted"></span></div><div class="panel-body truck-stops" id="truck1-stops"></div></div></div>
|
||||
<div class="col-md-4"><div class="panel panel-default truck-column" data-truck="Truck 2"><div class="panel-heading">🚛 Truck 2 <span id="truck2-count" class="text-muted"></span></div><div class="panel-body truck-stops" id="truck2-stops"></div></div></div>
|
||||
<div class="col-md-4"><div class="panel panel-default truck-column" data-truck="Truck 3"><div class="panel-heading">🚛 Truck 3 <span id="truck3-count" class="text-muted"></span></div><div class="panel-body truck-stops" id="truck3-stops"></div></div></div>
|
||||
</div>
|
||||
<div class="row" style="margin-top: 16px;"><div class="col-md-12"><div class="panel panel-default truck-column" data-truck=""><div class="panel-heading">📋 Unassigned <span id="unassigned-count" class="text-muted"></span></div><div class="panel-body truck-stops" id="unassigned-stops"></div></div></div></div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="stage-c">
|
||||
<div class="row" style="margin-bottom: 16px;"><div class="col-md-12"><button class="btn btn-primary" id="btn-new-checkin">+ Check In Load</button><button class="btn btn-success" id="btn-cor-report">📋 CoR Report</button></div></div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Recent Check-ins</div>
|
||||
<div class="panel-body" style="padding: 0; overflow-x: auto;">
|
||||
<table class="table table-striped table-hover" id="checkin-table" style="font-size: 13px; margin-bottom: 0;">
|
||||
<thead><tr><th>Date</th><th>Customer</th><th>Load #</th><th class="text-right">Pallets</th><th class="text-right">Weight</th><th>Contents</th><th>Data Status</th><th>RED/R2</th></tr></thead>
|
||||
<tbody id="checkin-tbody"><tr><td colspan="8" class="text-center text-muted">Loading...</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div id="checkin-form" style="display:none; margin-top: 16px;">
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">+ Load Check-in</div>
|
||||
<div class="panel-body">
|
||||
<form id="checkin-form-inner">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<h5 style="color:#6f42c1;">📅 Pickup Reference</h5>
|
||||
<div class="form-group"><label>Scheduled Pickup <span class="text-danger">*</span></label><div id="ci-pickup-control"></div></div>
|
||||
<div id="ci-pickup-details" style="display:none; background:#f8f9fa; border:1px solid #ddd; border-radius:4px; padding:10px; margin-bottom:10px;">
|
||||
<div id="ci-customer-info" style="font-weight:700;"></div>
|
||||
<div id="ci-address-info" style="font-size:12px; color:#666;"></div>
|
||||
<div id="ci-contact-info" style="font-size:12px; color:#666;"></div>
|
||||
<div id="ci-special-handling" style="display:none; margin-top:8px; padding:6px; background:#FFCDD2; border:1px solid #C62828; border-radius:4px; font-size:12px;"></div>
|
||||
<div id="ci-pickup-notes" style="display:none; margin-top:8px; padding:6px; background:#F5F5F5; border:1px solid #999; border-radius:4px; font-size:12px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h5 style="color:#6f42c1;">⚖️ Actual Load</h5>
|
||||
<div class="form-group"><label>Received Date <span class="text-danger">*</span></label><input type="date" id="ci-received_date" class="form-control" required></div>
|
||||
<div class="form-group"><label>Actual # of Pallets/Gaylords <span class="text-danger">*</span></label><input type="number" id="ci-actual_pallets" class="form-control" min="1" required></div>
|
||||
<div class="form-group"><label>Total Weight (lbs)</label><input type="text" id="ci-total_weight" class="form-control"></div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h5 style="color:#6f42c1;">📦 Contents & Classification</h5>
|
||||
<div class="form-group"><label>Load Contents</label><textarea id="ci-load_contents" class="form-control" rows="2" placeholder="e.g. 80% wire 20% tablets"></textarea></div>
|
||||
<div class="form-group"><label>Data Status</label><select id="ci-data_status" class="form-control"><option value="">—</option><option value="D0">D0</option><option value="D1">D1</option><option value="ND1">ND1</option><option value="ND2">ND2</option><option value="ND3">ND3</option><option value="ND4">ND4</option></select></div>
|
||||
<div class="form-group"><label>RED / R2</label><select id="ci-red_r2" class="form-control"><option value="">—</option><option value="RED">RED</option><option value="NIST">NIST</option><option value="Red+NIST">Red+NIST</option><option value="R2">R2</option><option value="Both">Both</option><option value="Neither">Neither</option></select></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="margin-top: 8px;"><div class="col-md-12"><button type="submit" class="btn btn-primary btn-lg">✓ Check In Load</button><button type="button" class="btn btn-default btn-lg" id="btn-cancel-checkin">Cancel</button></div></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.truck-stops { min-height: 60px; }
|
||||
.stop-card { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 6px; padding: 10px 12px; margin: 6px 0; cursor: grab; font-size: 13px; }
|
||||
.stop-card:hover { border-color: #2F5496; }
|
||||
.stop-card .stop-co { font-weight: 700; color: #2F5496; }
|
||||
.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; }
|
||||
.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; }
|
||||
</style>
|
||||
`);
|
||||
|
||||
// Prevent Frappe router from intercepting tab clicks
|
||||
// Tab switching — direct DOM to avoid Frappe router intercepting <a> 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('<tr><td colspan="14" class="text-center text-muted">No pickups found</td></tr>');
|
||||
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 '<tr style="cursor:pointer" data-pickup="' + esc(p.name) + '">' +
|
||||
'<td>' + esc(p.pickup_date || "") + '</td>' +
|
||||
'<td>' + dn + '</td>' +
|
||||
'<td>' + esc(p.pickup_type || "") + '</td>' +
|
||||
'<td><strong>' + esc(p.company_name || p.customer_number || "") + '</strong></td>' +
|
||||
'<td>' + esc(p.contact_name || "") + '</td>' +
|
||||
'<td style="max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc((p.address_line || "") + (p.city ? ", " + p.city : "")) + '</td>' +
|
||||
'<td class="text-right">' + (p.estimated_items || "—") + '</td>' +
|
||||
'<td>' + esc(p.data_status || "—") + '</td>' +
|
||||
'<td>' + esc(p.red_r2 || "—") + '</td>' +
|
||||
'<td>' + esc(p.status || "") + '</td>' +
|
||||
'<td style="max-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(p.notes || "") + '</td>' +
|
||||
'<td>' + esc(p.truck || "") + '</td>' +
|
||||
'<td>' + (p.needs_aor ? "✓" : "") + '</td>' +
|
||||
'<td>' + (p.needs_cod ? "✓" : "") + '</td></tr>';
|
||||
}).join(""));
|
||||
}
|
||||
|
||||
function renderCalendar(calendar) {
|
||||
var el = $("#pickup-calendar");
|
||||
if (!calendar.length) { el.html('<div class="text-muted text-center">No data</div>'); return; }
|
||||
var todayStr = frappe.datetime.nowdate();
|
||||
var html = '<div style="text-align:center;">';
|
||||
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 += '<div class="' + cls + '" data-date="' + d.date + '" title="' + d.count + ' pickup(s)">' + label + '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
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 = '<div class="stop-card" data-pickup="' + esc(p.name) + '">';
|
||||
if (order) h += '<div style="font-size:11px;color:#666;margin-bottom:2px">Stop #' + order + '</div>';
|
||||
h += '<div class="stop-co">' + esc(p.company_name || p.customer_number || "Unknown") + '</div>';
|
||||
h += '<div class="stop-addr">' + esc((p.address_line || "") + (p.city ? ", " + p.city : "")) + '</div>';
|
||||
h += '<div class="stop-meta">';
|
||||
if (p.estimated_items) h += '<span>' + p.estimated_items + ' items</span>';
|
||||
if (p.data_status) h += '<span>' + esc(p.data_status) + '</span>';
|
||||
if (p.red_r2) h += '<span>' + esc(p.red_r2) + '</span>';
|
||||
if (p.needs_aor) h += '<span>AoR</span>';
|
||||
if (p.needs_cod) h += '<span>CoD</span>';
|
||||
h += '</div></div>';
|
||||
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('<tr><td colspan="8" class="text-center text-muted">No check-ins yet</td></tr>');
|
||||
return;
|
||||
}
|
||||
tbody.html(checkins.map(function(c) {
|
||||
var palletInfo = (c.pallets || []).map(function(p) { return esc(p.pallet_number || p.name); }).join(", ");
|
||||
return '<tr>' +
|
||||
'<td>' + esc(c.incoming_date || "") + '</td>' +
|
||||
'<td><strong>' + esc(c.customer_name || c.customer || "") + '</strong></td>' +
|
||||
'<td>' + esc(c.name || "") + '</td>' +
|
||||
'<td class="text-right">' + (c.pallet_count || 0) + '</td>' +
|
||||
'<td class="text-right">' + (c.total_weight || "—") + '</td>' +
|
||||
'<td style="max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(c.data_status || "") + '</td>' +
|
||||
'<td>' + esc(c.red_r2 || "—") + '</td>' +
|
||||
'<td style="font-size:11px;color:#666;">' + palletInfo + '</td></tr>';
|
||||
}).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("<strong>⚠ " + esc(d.red_r2) + "</strong> — 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("<strong>Notes:</strong> " + 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, ">").replace(/"/g, """) : ""; }
|
||||
function dayName(d) { return ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][d.getDay()]; }
|
||||
|
||||
// ── Init ──
|
||||
loadPickups();
|
||||
loadCheckins();
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user