Add Excel import script for Cantaloupe machine list
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
"""
|
||||
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
|
||||
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, created_at, updated_at, description)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'), ?)
|
||||
""", (machine_id, name, serial, make, model, cat,
|
||||
status, full_address, customer_id,
|
||||
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]))
|
||||
Reference in New Issue
Block a user