diff --git a/import_machines.py b/import_machines.py new file mode 100644 index 0000000..8f26b0f --- /dev/null +++ b/import_machines.py @@ -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 +""" + +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 ') + sys.exit(1) + sys.exit(main(sys.argv[1]))