This commit is contained in:
gsinghpal
2026-02-25 09:40:41 -05:00
parent 0e1aebe60b
commit e71bc503f9
69 changed files with 7537 additions and 82 deletions

View File

@@ -0,0 +1,191 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Demo Pool Cleanup Script
Removes the Studio-created x_demo_pool_tracking model and all related
database artifacts (tables, ir.model entries, fields, views, menus, etc.).
WARNING: Only run this AFTER verifying the import_demo_pool.py migration
was successful and all data is correct in Loaner Products.
Run via Odoo shell:
docker exec -i odoo-mobility-app odoo shell -d mobility < cleanup_demo_pool.py
Copyright 2024-2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
"""
import logging
_logger = logging.getLogger(__name__)
DEMO_POOL_MODELS = [
'x_demo_pool_tracking',
'x_demo_pool_tracking_line_4a032',
'x_demo_pool_tracking_line_629cc',
'x_demo_pool_tracking_line_b4ec9',
'x_demo_pool_tracking_stage',
'x_demo_pool_tracking_tag',
]
DEMO_POOL_TABLES = DEMO_POOL_MODELS + [
'x_demo_pool_tracking_tag_rel',
]
PRODUCT_TEMPLATE_STUDIO_FIELDS_TO_REMOVE = [
'x_studio_many2one_field_V8rnk',
]
def run_cleanup(env):
cr = env.cr
print(f"\n{'='*60}")
print("Demo Pool Cleanup")
print(f"{'='*60}")
print()
# =====================================================================
# Step 1: Remove ir.ui.menu entries referencing demo pool actions
# =====================================================================
print("[1/7] Removing menu items...")
cr.execute("""
DELETE FROM ir_ui_menu
WHERE action IN (
SELECT CONCAT('ir.actions.act_window,', id)
FROM ir_act_window
WHERE res_model IN %s
)
""", (tuple(DEMO_POOL_MODELS),))
print(f" Removed {cr.rowcount} menu items")
# =====================================================================
# Step 2: Remove ir.actions.act_window entries
# =====================================================================
print("[2/7] Removing window actions...")
cr.execute("""
DELETE FROM ir_act_window WHERE res_model IN %s
""", (tuple(DEMO_POOL_MODELS),))
print(f" Removed {cr.rowcount} window actions")
# =====================================================================
# Step 3: Remove ir.ui.view entries
# =====================================================================
print("[3/7] Removing views...")
cr.execute("""
DELETE FROM ir_ui_view WHERE model IN %s
""", (tuple(DEMO_POOL_MODELS),))
print(f" Removed {cr.rowcount} views")
# =====================================================================
# Step 4: Remove ir.model.access entries
# =====================================================================
print("[4/7] Removing access rules...")
cr.execute("""
SELECT id FROM ir_model WHERE model IN %s
""", (tuple(DEMO_POOL_MODELS),))
model_ids = [row[0] for row in cr.fetchall()]
if model_ids:
cr.execute("""
DELETE FROM ir_model_access WHERE model_id IN %s
""", (tuple(model_ids),))
print(f" Removed {cr.rowcount} access rules")
else:
print(" No model IDs found (already cleaned?)")
# =====================================================================
# Step 5: Remove ir.model.fields and ir.model entries
# =====================================================================
print("[5/7] Removing field definitions and model registry entries...")
if model_ids:
cr.execute("""
DELETE FROM ir_model_fields_selection
WHERE field_id IN (
SELECT id FROM ir_model_fields WHERE model_id IN %s
)
""", (tuple(model_ids),))
print(f" Removed {cr.rowcount} field selection values")
cr.execute("""
DELETE FROM ir_model_fields WHERE model_id IN %s
""", (tuple(model_ids),))
print(f" Removed {cr.rowcount} field definitions")
cr.execute("""
DELETE FROM ir_model_constraint WHERE model IN %s
""", (tuple(model_ids),))
print(f" Removed {cr.rowcount} constraints")
cr.execute("""
DELETE FROM ir_model_relation WHERE model IN %s
""", (tuple(model_ids),))
print(f" Removed {cr.rowcount} relations")
cr.execute("""
DELETE FROM ir_model WHERE id IN %s
""", (tuple(model_ids),))
print(f" Removed {cr.rowcount} model registry entries")
# =====================================================================
# Step 6: Remove Studio field on product.template that linked to demo pool
# =====================================================================
print("[6/7] Removing demo pool Studio fields from product.template...")
for field_name in PRODUCT_TEMPLATE_STUDIO_FIELDS_TO_REMOVE:
cr.execute("""
SELECT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'product_template' AND column_name = %s
)
""", (field_name,))
exists = cr.fetchone()[0]
if exists:
cr.execute("""
DELETE FROM ir_model_fields_selection
WHERE field_id IN (
SELECT id FROM ir_model_fields
WHERE model = 'product.template' AND name = %s
)
""", (field_name,))
cr.execute("""
DELETE FROM ir_model_fields
WHERE model = 'product.template' AND name = %s
""", (field_name,))
cr.execute(f'ALTER TABLE product_template DROP COLUMN IF EXISTS "{field_name}"')
print(f" Removed {field_name} from product.template")
else:
print(f" {field_name} not found on product.template (already removed)")
# =====================================================================
# Step 7: Drop the actual database tables
# =====================================================================
print("[7/7] Dropping demo pool tables...")
for table in DEMO_POOL_TABLES:
cr.execute(f"DROP TABLE IF EXISTS \"{table}\" CASCADE")
print(f" Dropped {table}")
cr.commit()
print(f"\n{'='*60}")
print("Cleanup Complete")
print(f"{'='*60}")
print()
print("The following have been removed:")
print(" - x_demo_pool_tracking model and all line/stage/tag tables")
print(" - All related views, menus, actions, and access rules")
print(" - x_studio_many2one_field_V8rnk from product.template")
print()
print("NOT touched:")
print(" - x_studio_adp_price, x_studio_adp_sku on product.template")
print(" - x_studio_msrp, x_studio_product_description_long on product.template")
print(" - x_studio_product_details on product.template")
print()
print("Restart the Odoo server to clear the model registry cache.")
print(f"{'='*60}\n")
run_cleanup(env)

View File

@@ -0,0 +1,277 @@
#!/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)