feat: populate company/place/customer_name from MSFS extraction DB
- New script: scripts/populate_asset_company_place.py maps connect_id → account name (company/customer_name) + city (place) + address via extraction DB (msdyn_customerasset + account join) - 5,743/7,488 assets now have company + customer_name populated - 5,647/7,488 assets now have place (city) populated - Fix: SELECT COALESCE(c.name, a.customer_name) so stored customer_name isn't overridden to NULL when customer_id FK is missing - Bump SW v4→v5, version v3.1→v3.2 to force cache refresh
This commit is contained in:
@@ -1040,6 +1040,7 @@ def list_assets(
|
||||
q: Optional[str] = Query(None),
|
||||
no_dex_days: Optional[int] = Query(None, ge=1),
|
||||
has_gps: Optional[bool] = Query(None),
|
||||
branch: Optional[str] = Query(None, description="Filter by branch/territory name"),
|
||||
limit: int = Query(100, ge=1, le=2000),
|
||||
offset: int = Query(0, ge=0),
|
||||
):
|
||||
@@ -1087,10 +1088,25 @@ def list_assets(
|
||||
conditions.append("a.latitude IS NOT NULL AND a.longitude IS NOT NULL")
|
||||
else:
|
||||
conditions.append("(a.latitude IS NULL OR a.longitude IS NULL)")
|
||||
if branch:
|
||||
cache = _build_asset_branch_cache()
|
||||
# Find all connect_id prefixes that map to this branch name
|
||||
matching_prefixes = [
|
||||
pfx for pfx, br in cache.items() if br == branch
|
||||
]
|
||||
if matching_prefixes:
|
||||
placeholders = ",".join(["?"] * len(matching_prefixes))
|
||||
conditions.append(
|
||||
f"SUBSTR(a.connect_id, 1, 8) IN ({placeholders})"
|
||||
)
|
||||
params.extend(matching_prefixes)
|
||||
else:
|
||||
# Branch name not found — return empty result
|
||||
conditions.append("1=0")
|
||||
|
||||
where = " AND ".join(conditions)
|
||||
from_clause = "FROM assets a LEFT JOIN customers c ON a.customer_id = c.id"
|
||||
sql = f"SELECT a.*, c.name AS customer_name {from_clause}"
|
||||
sql = f"SELECT a.*, COALESCE(c.name, a.customer_name) AS customer_name {from_clause}"
|
||||
if where:
|
||||
sql += f" WHERE {where}"
|
||||
sql += " ORDER BY a.created_at DESC LIMIT ? OFFSET ?"
|
||||
@@ -1121,6 +1137,44 @@ def search_by_machine_id(machine_id: str = Query(...)):
|
||||
return row_to_dict(row)
|
||||
|
||||
|
||||
# ── Task 5b: GET /api/branches ─────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.get("/api/branches")
|
||||
def list_branches():
|
||||
"""Return distinct branch/territory names that assets belong to.
|
||||
|
||||
Uses the connect_id prefix → branch mapping from the extraction DB.
|
||||
Returns sorted list of {name, machine_count} objects.
|
||||
"""
|
||||
cache = _build_asset_branch_cache()
|
||||
# Count how many assets per branch
|
||||
conn = get_db()
|
||||
prefix_counts: dict[str, int] = {}
|
||||
try:
|
||||
cur = conn.execute(
|
||||
"SELECT SUBSTR(connect_id, 1, 8) AS prefix, COUNT(*) AS cnt "
|
||||
"FROM assets WHERE connect_id IS NOT NULL AND connect_id != '' "
|
||||
"GROUP BY prefix"
|
||||
)
|
||||
for r in cur.fetchall():
|
||||
prefix_counts[r[0]] = r[1]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
# Aggregate by branch name
|
||||
branch_map: dict[str, int] = {}
|
||||
for prefix, machine_count in prefix_counts.items():
|
||||
branch = cache.get(prefix, prefix)
|
||||
branch_map[branch] = branch_map.get(branch, 0) + machine_count
|
||||
|
||||
branches = sorted(
|
||||
[{"name": k, "machine_count": v} for k, v in branch_map.items()],
|
||||
key=lambda b: -b["machine_count"], # type: ignore[arg-type]
|
||||
)
|
||||
return {"branches": branches, "total": len(branches)}
|
||||
|
||||
|
||||
@app.get("/api/assets/{asset_id}")
|
||||
def get_asset(asset_id: int):
|
||||
conn = get_db()
|
||||
@@ -2595,6 +2649,71 @@ def _get_extraction_db() -> sqlite3.Connection | None:
|
||||
return conn
|
||||
|
||||
|
||||
# ── Asset Branch Cache (connect_id prefix → branch/territory name) ────────
|
||||
_BRANCH_CACHE: dict[str, str] | None = None
|
||||
|
||||
|
||||
def _build_asset_branch_cache() -> dict[str, str]:
|
||||
"""Build a mapping from connect_id prefix (first 8 chars) to branch name.
|
||||
|
||||
Queries the extraction DB for each customer asset, linking it through
|
||||
its account to work order territories (msdyn_serviceterritory!name).
|
||||
Returns {connect_id_prefix: branch_name}.
|
||||
"""
|
||||
global _BRANCH_CACHE
|
||||
if _BRANCH_CACHE is not None:
|
||||
return _BRANCH_CACHE
|
||||
|
||||
result: dict[str, str] = {}
|
||||
conn = _get_extraction_db()
|
||||
if not conn:
|
||||
print("⚠️ Extraction DB not available — branch cache will be empty")
|
||||
_BRANCH_CACHE = result
|
||||
return result
|
||||
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
# For each customer asset (connect_id + account), find the most common
|
||||
# work order territory for that account.
|
||||
cur.execute("""
|
||||
SELECT
|
||||
SUBSTR(ca.hsl_connectid, 1, 8) AS prefix,
|
||||
wo."msdyn_serviceterritory!name" AS territory,
|
||||
COUNT(*) AS cnt
|
||||
FROM msdyn_customerasset ca
|
||||
LEFT JOIN account a ON ca."msdyn_account!id" = a.accountid
|
||||
LEFT JOIN msdyn_workorder wo ON a.accountid = wo."msdyn_serviceaccount!id"
|
||||
WHERE ca.hsl_connectid IS NOT NULL AND ca.hsl_connectid != ''
|
||||
AND wo."msdyn_serviceterritory!name" IS NOT NULL
|
||||
AND wo."msdyn_serviceterritory!name" != ''
|
||||
GROUP BY prefix, wo."msdyn_serviceterritory!name"
|
||||
ORDER BY cnt DESC
|
||||
""")
|
||||
# Take the most frequent territory per prefix
|
||||
prefix_map: dict[str, list[tuple[str, int]]] = {}
|
||||
for r in cur.fetchall():
|
||||
prefix = r[0]
|
||||
territory = r[1]
|
||||
cnt = r[2]
|
||||
if prefix not in prefix_map:
|
||||
prefix_map[prefix] = []
|
||||
prefix_map[prefix].append((territory, cnt))
|
||||
|
||||
for prefix, territories in prefix_map.items():
|
||||
# Sort by count descending, pick the top one
|
||||
territories.sort(key=lambda x: -x[1])
|
||||
result[prefix] = territories[0][0]
|
||||
|
||||
print(f"📡 Built branch cache: {len(result)} connect_id prefixes mapped")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to build branch cache: {e}")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
_BRANCH_CACHE = result
|
||||
return result
|
||||
|
||||
|
||||
# ── TSP Solver ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user