"""Fill in an asset's vendor from the catalog model it already points at. An asset that carries a model but no vendor is showing a blank the database can answer: the model records its own vendor, and both sides reference the same vendors table. The detail pages fall back to it at display time, but the record itself stays empty, so the edit form shows an empty vendor box and any report reading vendorid directly still sees nothing. This writes it down. ONLY fills rows where the asset's vendor is NULL and the model names one. It never overwrites a vendor somebody chose, and it never guesses: if the model has no vendor either, the row is left alone. DELIBERATELY NOT DONE HERE: the asset's TYPE. modeltypes is the catalog-wide list covering every kind of asset - it holds "Access Point", "Camera" and "Desktop PC" alongside the machine entries - so it is a different taxonomy from machinetypes, and mapping one onto the other by name would mistype whatever does not match. Only about two thirds of the names overlap. Dry run by default; nothing is written without --commit. python scripts/backfill_vendor_from_model.py python scripts/backfill_vendor_from_model.py --commit python scripts/backfill_vendor_from_model.py --only machines --commit """ import argparse import os import sys sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) # Each asset table that carries both a model link and its own vendor, with the # column that identifies a row in reports and logs. TARGETS = [ ('machines', 'machineid', 'machinename'), ('computers', 'computerid', 'computername'), ('printers', 'printerid', 'printername'), ('networkdevices', 'networkdeviceid', 'hostname'), ] def resolve(connection, table, idcol, labelcol): """Rows that would change, newest table columns tolerated.""" from sqlalchemy import text columns = {c['name'] for c in __import__('sqlalchemy').inspect(connection).get_columns(table)} if 'modelnumberid' not in columns or 'vendorid' not in columns: return None, [] label = labelcol if labelcol in columns else idcol rows = connection.execute(text(f""" SELECT a.{idcol} AS assetid, a.{label} AS label, m.modelnumber AS modelnumber, m.vendorid AS vendorid, v.vendor AS vendorname FROM {table} a JOIN models m ON a.modelnumberid = m.modelnumberid JOIN vendors v ON m.vendorid = v.vendorid WHERE a.vendorid IS NULL AND m.vendorid IS NOT NULL ORDER BY a.{idcol} """)).fetchall() return label, rows def main(): parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument('--commit', action='store_true', help='write the changes (default is a dry run)') parser.add_argument('--only', metavar='TABLE', help='restrict to one table, e.g. machines') parser.add_argument('--limit', type=int, default=20, help='rows to list per table in the preview (default 20)') args = parser.parse_args() # FLASK_ENV is deliberately NOT forced. The app reads it from .env, which on # a server already says production; overriding it here demanded a SECRET_KEY # the environment had no reason to provide and the script could not run. from sqlalchemy import text from shopdb import create_app from shopdb.extensions import db app = create_app() with app.app_context(): connection = db.session.connection() total = 0 for table, idcol, labelcol in TARGETS: if args.only and args.only != table: continue label, rows = resolve(connection, table, idcol, labelcol) if label is None: print(f'{table}: no model or vendor column here, skipped') continue print(f'\n{table}: {len(rows)} row(s) would get a vendor') for row in rows[:args.limit]: print(f' {row.assetid:>7} {str(row.label)[:28]:<28} ' f'model {row.modelnumber[:22]:<22} -> {row.vendorname}') if len(rows) > args.limit: print(f' ... and {len(rows) - args.limit} more') total += len(rows) if args.commit and rows: connection.execute(text(f""" UPDATE {table} a JOIN models m ON a.modelnumberid = m.modelnumberid SET a.vendorid = m.vendorid WHERE a.vendorid IS NULL AND m.vendorid IS NOT NULL """)) if args.commit: db.session.commit() print(f'\nCommitted. {total} row(s) updated.') else: print(f'\nDRY RUN - nothing written. {total} row(s) would change.') print('Re-run with --commit to apply.') return 0 if __name__ == '__main__': sys.exit(main())