e8a918fc7b
- 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)
172 lines
6.5 KiB
Python
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]))
|