Files
canteen-asset-tracker/import_machines.py
T
shawn e8a918fc7b feat: add is_disney column for reliable Disney/Non-Disney filtering
- Added is_disney INTEGER DEFAULT 0 column to assets table
- Backfilled 996 Disney assets based on D- customer prefix
- Server: is_disney in AssetCreate/AssetUpdate models, INSERT/UPDATE
- Server: disney_filter query param now uses is_disney column (no JOIN needed)
- import_cantaloupe.py: sets is_disney=1 when customer starts with D-
- import_machines.py: sets is_disney=1 when customer starts with D-
- disney_classify.py: sets is_disney=1 when classifying
- Frontend: new Location Type dropdown with Disney/Non-Disney toggle
  (replaces the __disney__/__non_disney__ park dropdown pseudo-options)
2026-05-22 18:34:13 -04:00

172 lines
6.5 KiB
Python

"""
Import machines from Cantaloupe Excel export into assets.db.
Maps Excel columns to DB fields, creates/finds customers and locations.
Usage: python3 import_machines.py <excel_path>
"""
import openpyxl, sqlite3, sys, re, os
from datetime import datetime
DB = '/home/oplabs/projects/canteen-asset-tracker/assets.db'
def norm(s):
"""Normalize a string for comparison."""
if s is None:
return ''
return str(s).strip().lower()
def _sanitize_machine_id(mid):
if not mid:
return ''
return str(mid).strip()[:100]
def main(xlsx_path):
if not os.path.exists(xlsx_path):
print(f'ERROR: File not found: {xlsx_path}')
return 1
wb = openpyxl.load_workbook(xlsx_path, data_only=True)
ws = wb['Machine List']
# Read all rows
rows = []
for row in ws.iter_rows(min_row=2, values_only=True):
if row[2] is None:
continue
rows.append(row)
print(f'Loaded {len(rows)} machines from Excel')
conn = sqlite3.connect(DB)
conn.execute("PRAGMA journal_mode=WAL")
# Pre-load existing customers
existing_customers = {}
for r in conn.execute("SELECT id, name FROM customers").fetchall():
existing_customers[norm(r[1])] = r[0]
# Pre-load existing assets by machine_id for upsert
existing_assets = {}
for r in conn.execute("SELECT id, machine_id FROM assets WHERE machine_id IS NOT NULL").fetchall():
existing_assets[_sanitize_machine_id(r[1])] = r[0]
# Stats
stats = {'created': 0, 'updated': 0, 'skipped': 0, 'errors': 0}
# Build customer cache - keep a set of customer names we've already seen in this import
customer_cache = set()
# Process in a single transaction
for idx, row in enumerate(rows):
try:
machine_id = _sanitize_machine_id(str(int(row[2])))
location_str = str(row[1]) if row[1] else '' # "Company - Address"
customer_name = str(row[21]) if row[21] else '' # Customer
is_disney = 1 if customer_name.strip().startswith('D-') else 0
address = str(row[6]) if row[6] else ''
city = str(row[5]) if row[5] else ''
state = str(row[33]) if row[33] else ''
postal = str(row[34]) if row[34] else ''
serial = str(row[37]) if row[37] else ''
make = str(row[39]) if row[39] else 'Unknown'
model = str(row[40]) if row[40] else 'Unknown'
cls = str(row[38]) if row[38] else 'Other' # Class → category
branch = str(row[51]) if row[51] else ''
device = str(row[0]) if row[0] else ''
# Asset name: use location string if available, else customer + address
name = location_str if location_str else (customer_name + ' - ' + address if address else machine_id)
name = name.strip()[:200]
# Build full address
full_address_parts = [p for p in [address, city, state, postal] if p]
full_address = ', '.join(full_address_parts) if full_address_parts else ''
full_address = full_address[:200]
# Category: map from class
category_map = {
'snack': 'Snack', 'drink': 'Drink', 'cold drink': 'Drink',
'combo': 'Combo', 'food': 'Food', 'other': 'Other',
'unknown': 'Other',
}
cat = category_map.get(cls.strip().lower(), cls.capitalize() if cls else 'Other')
# Status: always active for import (Cantaloupe RPC status is telemetry, not condition)
status = 'active'
# ----- Customer resolution -----
customer_id = None
if customer_name:
key = norm(customer_name)
if key in existing_customers:
customer_id = existing_customers[key]
else:
# Create new customer
c = conn.execute(
"INSERT INTO customers (name) VALUES (?)",
(customer_name.strip()[:100],)
)
customer_id = c.lastrowid
existing_customers[key] = customer_id
print(f' Created customer: {customer_name.strip()[:60]}')
# ----- Upsert logic -----
if machine_id in existing_assets:
aid = existing_assets[machine_id]
# Update existing - preserve disney_park, GPS, keys, badges
conn.execute("""
UPDATE assets SET
name = ?,
serial_number = ?,
make = ?,
model = ?,
category = ?,
address = ?,
customer_id = COALESCE(?, customer_id),
description = ?,
updated_at = datetime('now')
WHERE id = ?
""", (name, serial, make, model, cat, full_address,
customer_id,
device + ' | Branch: ' + branch if branch else device,
aid))
stats['updated'] += 1
else:
# Insert new
cursor = conn.execute("""
INSERT INTO assets
(machine_id, name, serial_number, make, model, category,
status, address, customer_id, is_disney, created_at, updated_at, description)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'), ?)
""", (machine_id, name, serial, make, model, cat,
status, full_address, customer_id, is_disney,
device + ' | Branch: ' + branch if branch else device))
aid = cursor.lastrowid
existing_assets[machine_id] = aid
stats['created'] += 1
except Exception as e:
print(f'ERROR row {idx+2} (machine_id={row[2]}): {e}')
stats['errors'] += 1
conn.commit()
conn.close()
print()
print(f'=== Import Complete ===')
print(f' Created: {stats["created"]} new assets')
print(f' Updated: {stats["updated"]} existing assets')
print(f' Skipped: {stats["skipped"]}')
print(f' Errors: {stats["errors"]}')
print(f' Total rows: {len(rows)}')
return 0 if stats['errors'] == 0 else 1
if __name__ == '__main__':
if len(sys.argv) < 2:
print('Usage: python3 import_machines.py <excel.xlsx>')
sys.exit(1)
sys.exit(main(sys.argv[1]))