#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Demo Pool to Loaner Products Migration Script Reads all records from the Studio-created x_demo_pool_tracking table and creates proper product.template records with x_fc_can_be_loaned=True, plus stock.lot serial numbers where applicable. Run via Odoo shell: docker exec -i odoo-mobility-app odoo shell -d mobility < import_demo_pool.py Or from the host with the script mounted: docker exec -i odoo-mobility-app odoo shell -d mobility \ < /mnt/extra-addons/fusion_claims/scripts/import_demo_pool.py Copyright 2024-2026 Nexa Systems Inc. License OPL-1 (Odoo Proprietary License v1.0) """ import json import logging _logger = logging.getLogger(__name__) EQUIPMENT_TYPE_MAP = { 'Type 1 Walker': 'type_1_walker', 'Type 2 MW': 'type_2_mw', 'Type 2 PW': 'type_2_pw', 'Type 2 Walker': 'type_2_walker', 'Type 3 MW': 'type_3_mw', 'Type 3 PW': 'type_3_pw', 'Type 3 Walker': 'type_3_walker', 'Type 4 MW': 'type_4_mw', 'Type 5 MW': 'type_5_mw', 'Ceiling Lift': 'ceiling_lift', 'Mobility Scooter': 'mobility_scooter', 'Patient Lift': 'patient_lift', 'Transport Wheelchair': 'transport_wheelchair', 'Wheelchair': 'standard_wheelchair', 'Standard Wheelchair': 'standard_wheelchair', 'Power Wheelchair': 'power_wheelchair', 'Cushion': 'cushion', 'Backrest': 'backrest', 'Stairlift': 'stairlift', 'Others': 'others', } WHEELCHAIR_CATEGORY_MAP = { 'Type 1': 'type_1', 'Type 2': 'type_2', 'Type 3': 'type_3', 'Type 4': 'type_4', 'Type 5': 'type_5', } LOCATION_MAP = { 'Warehouse': 'warehouse', 'Westin Brampton': 'westin_brampton', 'Mobility Etobicoke': 'mobility_etobicoke', 'Scarborough Storage': 'scarborough_storage', 'Client/Loaned': 'client_loaned', 'Rented Out': 'rented_out', } LISTING_TYPE_MAP = { 'Owned': 'owned', 'Borrowed': 'borrowed', } SKIP_SERIALS = {'na', 'n/a', 'update', 'updated', ''} def extract_name(json_val): """Extract English name from Odoo JSONB field.""" if not json_val: return '' if isinstance(json_val, dict): return json_val.get('en_US', '') or '' if isinstance(json_val, str): try: parsed = json.loads(json_val) if isinstance(parsed, dict): return parsed.get('en_US', '') or '' except (json.JSONDecodeError, TypeError): return json_val return str(json_val) def get_category_id(env, equipment_type_key): """Map equipment type to an appropriate product category.""" loaner_cat = env.ref('fusion_claims.product_category_loaner', raise_if_not_found=False) wheelchair_cat = env.ref('fusion_claims.product_category_loaner_wheelchair', raise_if_not_found=False) powerchair_cat = env.ref('fusion_claims.product_category_loaner_powerchair', raise_if_not_found=False) rollator_cat = env.ref('fusion_claims.product_category_loaner_rollator', raise_if_not_found=False) if not loaner_cat: return env['product.category'].search([], limit=1).id wheelchair_types = { 'type_2_mw', 'type_3_mw', 'type_4_mw', 'type_5_mw', 'type_2_walker', 'type_3_walker', 'type_1_walker', 'standard_wheelchair', 'transport_wheelchair', } powerchair_types = {'type_2_pw', 'type_3_pw', 'power_wheelchair'} rollator_types = set() if equipment_type_key in powerchair_types and powerchair_cat: return powerchair_cat.id if equipment_type_key in wheelchair_types and wheelchair_cat: return wheelchair_cat.id if equipment_type_key in rollator_types and rollator_cat: return rollator_cat.id return loaner_cat.id def fetch_accessories(cr, demo_pool_id): """Fetch accessory lines from x_demo_pool_tracking_line_b4ec9.""" cr.execute(""" SELECT x_name FROM x_demo_pool_tracking_line_b4ec9 WHERE x_demo_pool_tracking_id = %s ORDER BY x_studio_sequence, id """, (demo_pool_id,)) rows = cr.fetchall() accessories = [] for row in rows: name = extract_name(row[0]) if name: accessories.append(name) return accessories def run_import(env): cr = env.cr ProductTemplate = env['product.template'] StockLot = env['stock.lot'] company = env.company cr.execute(""" SELECT id, x_name, x_studio_equipment_type, x_studio_wheelchair_categorytype, x_studio_serial_number, x_studio_seat_width, x_studio_seat_depth, x_studio_seat_height, x_studio_where_is_it_located, x_studio_listing_type, x_studio_asset_, x_studio_package_information, x_active, x_studio_value, x_studio_notes FROM x_demo_pool_tracking ORDER BY id """) rows = cr.fetchall() columns = [ 'id', 'x_name', 'equipment_type', 'wheelchair_category', 'serial_number', 'seat_width', 'seat_depth', 'seat_height', 'location', 'listing_type', 'asset_number', 'package_info', 'active', 'value', 'notes', ] created_count = 0 serial_count = 0 skipped_serials = 0 errors = [] print(f"\n{'='*60}") print("Demo Pool to Loaner Products Migration") print(f"{'='*60}") print(f"Records found: {len(rows)}") print() for row in rows: record = dict(zip(columns, row)) demo_id = record['id'] try: name = extract_name(record['x_name']) if not name: errors.append(f"ID {demo_id}: empty name, skipped") continue equipment_type_raw = record['equipment_type'] or '' equipment_type_key = EQUIPMENT_TYPE_MAP.get(equipment_type_raw, '') wheelchair_cat_raw = record['wheelchair_category'] or '' wheelchair_cat_key = WHEELCHAIR_CATEGORY_MAP.get(wheelchair_cat_raw, '') location_raw = record['location'] or '' location_key = LOCATION_MAP.get(location_raw, '') listing_raw = record['listing_type'] or '' listing_key = LISTING_TYPE_MAP.get(listing_raw, '') seat_width = (record['seat_width'] or '').strip() seat_depth = (record['seat_depth'] or '').strip() seat_height = (record['seat_height'] or '').strip() asset_number = (record['asset_number'] or '').strip() package_info = (record['package_info'] or '').strip() accessories = fetch_accessories(cr, demo_id) if accessories: acc_text = '\n'.join(f'- {a}' for a in accessories) if package_info: package_info = f"{package_info}\n\nAccessories:\n{acc_text}" else: package_info = f"Accessories:\n{acc_text}" is_active = bool(record['active']) categ_id = get_category_id(env, equipment_type_key) product_vals = { 'name': name, 'type': 'consu', 'tracking': 'serial', 'sale_ok': False, 'purchase_ok': False, 'x_fc_can_be_loaned': True, 'x_fc_loaner_period_days': 7, 'x_fc_equipment_type': equipment_type_key or False, 'x_fc_wheelchair_category': wheelchair_cat_key or False, 'x_fc_seat_width': seat_width or False, 'x_fc_seat_depth': seat_depth or False, 'x_fc_seat_height': seat_height or False, 'x_fc_storage_location': location_key or False, 'x_fc_listing_type': listing_key or False, 'x_fc_asset_number': asset_number or False, 'x_fc_package_info': package_info or False, 'categ_id': categ_id, 'active': is_active, } product = ProductTemplate.with_context(active_test=False).create(product_vals) created_count += 1 serial_raw = (record['serial_number'] or '').strip() if serial_raw.lower() not in SKIP_SERIALS: try: product_product = product.product_variant_id if product_product: lot = StockLot.create({ 'name': serial_raw, 'product_id': product_product.id, 'company_id': company.id, }) serial_count += 1 except Exception as e: skipped_serials += 1 errors.append(f"ID {demo_id} ({name}): serial '{serial_raw}' failed: {e}") else: skipped_serials += 1 status = "ACTIVE" if is_active else "ARCHIVED" print(f" [{status}] {name} (demo #{demo_id}) -> product #{product.id}" f"{f' serial={serial_raw}' if serial_raw.lower() not in SKIP_SERIALS else ''}") except Exception as e: errors.append(f"ID {demo_id}: {e}") print(f" ERROR: ID {demo_id}: {e}") env.cr.commit() print(f"\n{'='*60}") print("Migration Summary") print(f"{'='*60}") print(f"Products created: {created_count}") print(f"Serials created: {serial_count}") print(f"Serials skipped: {skipped_serials}") print(f"Errors: {len(errors)}") if errors: print(f"\nErrors:") for err in errors: print(f" - {err}") print(f"\nDone. Verify in Fusion Claims > Loaner Management > Loaner Products") print(f"{'='*60}\n") run_import(env)