diff --git a/setup/delete_records.py b/setup/delete_records.py index c91bebcc0..dcb409e67 100755 --- a/setup/delete_records.py +++ b/setup/delete_records.py @@ -67,6 +67,16 @@ parser.add_argument( action="store_true", help="Force delete cascade all records instead of archiving", ) +parser.add_argument( + "--soft", + action="store_true", + help="Just archive the records instead of deleting", +) +parser.add_argument( + "--refresh-cache", + action="store_true", + help="Refersh the related models cache", +) args = parser.parse_args() db_name = args.db_name @@ -85,18 +95,24 @@ color_log.Show(INFO, ("Available databases:", odoo.db.list())) try: odoo.login(db_name, USERNAME, PASSWORD) color_log.Show( - 0, - f"Connected to Odoo at {HOST}:{PORT}, Database: {db_name}, Model: {base_model}", + OK, f"Connected to {HOST}:{PORT}, DB: {db_name}, Model: {base_model}" ) +except odoorpc.error.RPCError as e: + color_log.Show(FAIL, f"Login failed: {e} (check credentials or database)") + exit(1) except Exception as e: - color_log.Show(FAIL, f"Fail to Connect to Odoo Server {e}") + color_log.Show(FAIL, f"Connection error: {e} (check host/port)") exit(1) # Convert DOMAIN from string to list try: domain_filter = ast.literal_eval(domain_str) -except Exception as e: - color_log.Show(FAIL, f"Invalid DOMAIN format: {e}") + if not isinstance(domain_filter, list) or not all( + isinstance(t, (tuple, list)) and len(t) == 3 for t in domain_filter + ): + raise ValueError("Domain must be a list of 3-tuples") +except (ValueError, SyntaxError) as e: + color_log.Show(FAIL, f"Invalid domain format: {e}") exit(1) @@ -136,6 +152,7 @@ def get_related_fields(db_name, base_model): if os.path.exists(cache_file): with open(cache_file, "r") as f: related_models = json.load(f) + color_log.Show(INFO, f"Loaded related models for {base_model} from cache.") return related_models color_log.Show(INFO, f"Fetching related models for {base_model} from Odoo...") @@ -145,7 +162,6 @@ def get_related_fields(db_name, base_model): all_model_ids = odoo.env["ir.model"].search([]) all_models = odoo.env["ir.model"].read(all_model_ids, ["model"]) model_names = [model["model"] for model in all_models] - # Use multiprocessing to fetch related fields with mp.Pool(processes=process_size) as pool: results = pool.starmap( @@ -169,6 +185,7 @@ def get_related_fields(db_name, base_model): return {} +# Experimental function def delete_records_cascade( record_ids, model_name, process_count, related_models, db_name ): @@ -211,7 +228,9 @@ def delete_records_cascade( ) process_count += 1 except odoorpc.error.RPCError as e: - color_log.Show(NOTICE, f"Access denied for model {related_model}: {e}") + color_log.Show( + NOTICE, f"Access denied for model {related_model}: {e}" + ) skipped_models_cache.add(related_model) # Add to cache break # Skip further processing for this model except Exception as e: @@ -244,6 +263,33 @@ def delete_records_cascade( color_log.Show(FAIL, f"Error deleting {model_name} records: {e}") +def unreference_records(record_id, model_name, process_count, related_models): + """Force delete a record and its related records.""" + for related_model, fields in related_models.items(): + related_model_obj = odoo.env[related_model] + for field in fields: + try: + # Find records in the related model that reference the record_id + related_ids = related_model_obj.search([(field, "=", record_id)]) + if related_ids: + # Unreference the record_id by setting the field to False + related_model_obj.browse(related_ids).write({field: False}) + color_log.Show( + OK, + f"Unreferenced {record_id} in {related_model} ({field}) for {len(related_ids)} records.", + ) + except odoorpc.error.RPCError as e: + color_log.Show( + NOTICE, + f"Access denied while unreferencing {record_id} in {related_model} ({field}): {e}", + ) + except Exception as e: + color_log.Show( + NOTICE, + f"Error while unreferencing {record_id} in {related_model} ({field}): {e}", + ) + + # Function to delete records in parallel def delete_record(batch, model_name, process_count, related_models): """Process a batch of records - archive or delete based on references.""" @@ -266,17 +312,35 @@ def delete_record(batch, model_name, process_count, related_models): color_log.Show( INFO, f"{process_count}: Archived {model_name} ID {record_id}" ) - else: + if (not is_referenced and not args.soft) or args.force: model.unlink([record_id]) deleted_count += 1 color_log.Show( INFO, f"{process_count}: Deleted {model_name} ID {record_id}" ) except Exception as e: - skipped_count += 1 - color_log.Show( - INFO, f"{process_count}: Skipped {model_name} ID {record_id}: {e}" - ) + if args.force: + try: + unreference_records( + record_id, model_name, process_count, related_models + ) + model.unlink([record_id]) + archived_count += 1 + color_log.Show( + INFO, + f"{process_count}: Force deleted {model_name} ID {record_id}", + ) + except Exception as e: + skipped_count += 1 + color_log.Show( + NOTICE, + f"{process_count}: Skipped {model_name} ID {record_id} after force delete attempt: {e}", + ) + else: + skipped_count += 1 + color_log.Show( + INFO, f"{process_count}: Skipped {model_name} ID {record_id}: {e}" + ) color_log.Show( OK, @@ -304,29 +368,16 @@ def main(): for i in range(0, len(record_ids), process_size) ] processes = [] - if args.force: - delete_records_cascade(record_ids, base_model, 0, related_models, db_name) - # for i, batch in enumerate(batch_list, start=1): - # process = mp.Process( - # target=delete_records_cascade, - # args=(batch, base_model, f"Process-{i}", related_models,db_name), - # ) - # processes.append(process) - # process.start() - - # for process in processes: - # process.join() - else: - for i, batch in enumerate(batch_list, start=1): - process = mp.Process( - target=delete_record, - args=(batch, base_model, f"Process-{i}", related_models), - ) - processes.append(process) - process.start() - - for process in processes: - process.join() + for i, batch in enumerate(batch_list, start=1): + # delete_record(batch, base_model, f"Process-{i}", related_models) + process = mp.Process( + target=delete_record, + args=(batch, base_model, f"Process-{i}", related_models), + ) + processes.append(process) + process.start() + for process in processes: + process.join() color_log.Show(OK, "Record deletion process completed.")