From b35ad10c51bcc85e3f24cb3492f5548a0b421f6d Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Thu, 12 Feb 2026 15:17:27 +0700 Subject: [PATCH] feat: Add new scripts for Odoo MO/QC cleanup, custom addon Git operations, and module upgrades, and improve module path resolution in existing scripts. --- delete_old_mo_qc.py | 129 +++++++++++++++++++++++++++++++ move_stock_remote.py | 2 + pull_all_addons.sh | 30 ++++++++ unlink_bom_operations.py | 2 + upgrade_custom_addons.py | 160 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 323 insertions(+) create mode 100644 delete_old_mo_qc.py create mode 100644 pull_all_addons.sh create mode 100644 upgrade_custom_addons.py diff --git a/delete_old_mo_qc.py b/delete_old_mo_qc.py new file mode 100644 index 0000000..f495899 --- /dev/null +++ b/delete_old_mo_qc.py @@ -0,0 +1,129 @@ +import sys +import os +import time +from datetime import datetime, timedelta + +sys.path.append(os.getcwd()) + +# Try to import odoo, if not found, assume we are in odoo-bin shell or need path +try: + import odoo + from odoo import api, SUPERUSER_ID + from odoo.tools import config +except ImportError as e: + # Check if due to missing dependency like passlib + if "No module named" in str(e): + print("!" * 80) + print(f"ERROR: Missing dependency: {e}") + print("Please run this script using the Python executable that runs Odoo.") + print("Example: /path/to/venv/bin/python delete_old_mo_qc.py") + print("!" * 80) + sys.exit(1) + + current_dir = os.path.dirname(os.path.abspath(__file__)) + + # Try adding current_dir/odoo to path if not already covered by os.getcwd() fix above + # But usually os.getcwd() should cover it if running from community/ folder. + if os.path.exists(os.path.join(current_dir, 'odoo')): + sys.path.append(os.path.join(current_dir, 'odoo')) + else: + sys.path.append(current_dir) + + import odoo + from odoo import api, SUPERUSER_ID + from odoo.tools import config + +def delete_old_records(): + print("Starting Delete Old MO/QC Script...") + + import argparse + parser = argparse.ArgumentParser(description='Delete MOs and Quality Checks older than 1 day.') + parser.add_argument('db_name', help='Database name') + parser.add_argument('-c', '--config', help='Path to odoo.conf', default=None) + + args = parser.parse_args() + + db_name = args.db_name + + # Load config if provided or default exists + conf_path = args.config + if conf_path: + if os.path.exists(conf_path): + config.parse_config(['-c', conf_path]) + else: + print(f"WARNING: Config file '{conf_path}' not found.") + elif os.path.exists('odoo.conf'): + config.parse_config(['-c', 'odoo.conf']) + + print(f"Connecting to database: {db_name}") + + try: + registry = odoo.registry(db_name) + except AttributeError: + from odoo.modules.registry import Registry + registry = Registry.new(db_name) + + with registry.cursor() as cr: + env = api.Environment(cr, SUPERUSER_ID, {}) + + # Calculate Cutoff Date + cutoff_date = datetime.now() - timedelta(days=1) + print(f"Cutoff Date: {cutoff_date}") + + # 1. Find Old Quality Checks (NOT passed/failed) + print("\nSearching for old Quality Checks...") + # quality_state can be 'none', 'pass', 'fail' + domain_qc = [('create_date', '<', cutoff_date), ('quality_state', '=', 'none')] + qcs = env['quality.check'].search(domain_qc) + print(f"Found {len(qcs)} Quality Checks strictly older than 1 day (and not done).") + + # 2. Find Old Manufacturing Orders (NOT done) + print("\nSearching for old Manufacturing Orders...") + # state can be 'draft', 'confirmed', 'progress', 'to_close', 'done', 'cancel' + # We delete anything NOT done (and maybe not cancel if user wants clean up, but user said 'skip done') + # Safer to skip 'cancel' too if we want pure cleanup of abandoned work? User said "skip done". + # But 'cancel' is safe to delete usually. Let's stick to != 'done'. + start_domain_mo = [('create_date', '<', cutoff_date), ('state', '!=', 'done')] + mos = env['mrp.production'].search(start_domain_mo) + print(f"Found {len(mos)} Manufacturing Orders strictly older than 1 day (and not done).") + + if not qcs and not mos: + print("\nNothing to delete.") + return + + print("\n---------------------------------------------------") + print("Dry run complete. No changes have been committed yet.") + print("WARNING: This will PERMANENTLY DELETE the following records:") + print(f" - {len(qcs)} Quality Checks") + print(f" - {len(mos)} Manufacturing Orders") + + user_input = input("Do you want to PROCEED with DELETION and COMMIT changes? (yes/no): ") + + if user_input.lower() == 'yes': + try: + if qcs: + print(f"Deleting {len(qcs)} Quality Checks...") + qcs.unlink() + print(" Success.") + + if mos: + print(f"Deleting {len(mos)} Manufacturing Orders...") + # MO deletions might be blocked if state is 'done'. + # We attempt unlink, if it fails, Odoo raises UserError. + mos.unlink() + print(" Success.") + + cr.commit() + print("Changes COMMITTED.") + + except Exception as e: + cr.rollback() + print(f"ERROR during deletion: {e}") + print("Transaction ROLLED BACK. No changes persisted.") + else: + cr.rollback() + print("Operation CANCELLED (Rolled back).") + print("Done.") + +if __name__ == '__main__': + delete_old_records() diff --git a/move_stock_remote.py b/move_stock_remote.py index 1e2e3a4..c205831 100644 --- a/move_stock_remote.py +++ b/move_stock_remote.py @@ -2,6 +2,8 @@ import sys import os import time +sys.path.append(os.getcwd()) + # Try to import odoo, if not found, assume we are in odoo-bin shell or need path try: import odoo diff --git a/pull_all_addons.sh b/pull_all_addons.sh new file mode 100644 index 0000000..c237474 --- /dev/null +++ b/pull_all_addons.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Define the customaddons directory relative to the script location +CUSTOM_ADDONS_DIR="$(dirname "$0")/customaddons" + +if [ ! -d "$CUSTOM_ADDONS_DIR" ]; then + echo "Error: customaddons directory not found at $CUSTOM_ADDONS_DIR" + exit 1 +fi + +echo "Starting git pull for all modules in $CUSTOM_ADDONS_DIR..." + +# Iterate over each subdirectory in customaddons +for dir in "$CUSTOM_ADDONS_DIR"/*; do + if [ -d "$dir" ]; then + # Check if it's a git repository + if [ -d "$dir/.git" ]; then + echo "--------------------------------------------------" + echo "Pulling updates for $(basename "$dir")..." + (cd "$dir" && git pull) + else + # Optional: Uncomment the line below if you want to be notified about non-git folders + # echo "Skipping $(basename "$dir") - Not a git repository" + : + fi + fi +done + +echo "--------------------------------------------------" +echo "All done." diff --git a/unlink_bom_operations.py b/unlink_bom_operations.py index d1cc6d3..37d4bea 100644 --- a/unlink_bom_operations.py +++ b/unlink_bom_operations.py @@ -2,6 +2,8 @@ import sys import os import time +sys.path.append(os.getcwd()) + # Try to import odoo, if not found, assume we are in odoo-bin shell or need path try: import odoo diff --git a/upgrade_custom_addons.py b/upgrade_custom_addons.py new file mode 100644 index 0000000..2d26d3a --- /dev/null +++ b/upgrade_custom_addons.py @@ -0,0 +1,160 @@ +import os +import sys +import argparse +import subprocess +import configparser +import psycopg2 + +def get_custom_addons_modules(addons_path): + """ + Scans the given addons path and returns a list of directory names + that contain a __manifest__.py file. + """ + modules = [] + if not os.path.exists(addons_path): + print(f"Error: Addons path '{addons_path}' does not exist.") + return [] + + for item in os.listdir(addons_path): + item_path = os.path.join(addons_path, item) + if os.path.isdir(item_path): + manifest_path = os.path.join(item_path, '__manifest__.py') + if os.path.exists(manifest_path): + modules.append(item) + return modules + +def get_db_params(config_path): + """ + Parses the Odoo config file to extract database connection parameters. + """ + config = configparser.ConfigParser() + try: + config.read(config_path) + except Exception as e: + print(f"Error reading config file: {e}") + return {} + + db_params = {} + if 'options' in config: + params = config['options'] + db_params['host'] = params.get('db_host', 'localhost') + db_params['port'] = params.get('db_port', '5432') + db_params['user'] = params.get('db_user', 'odoo') + db_params['password'] = params.get('db_password', '') + + return db_params + +def get_installed_modules(modules_list, db_name, db_params): + """ + Connects to the database and filters the given list of modules specifically + for those that are installed (state='installed'). + """ + installed_modules = [] + + conn_params = { + 'dbname': db_name, + 'user': db_params.get('user', 'odoo'), + 'password': db_params.get('password', ''), + 'host': db_params.get('host', 'localhost'), + 'port': db_params.get('port', '5432') + } + + # Filter out empty or None values to use defaults or avoid errors + conn_params = {k: v for k, v in conn_params.items() if v} + + try: + conn = psycopg2.connect(**conn_params) + cur = conn.cursor() + + # Check if modules are installed + if not modules_list: + return [] + + query = "SELECT name FROM ir_module_module WHERE state = 'installed' AND name IN %s" + cur.execute(query, (tuple(modules_list),)) + rows = cur.fetchall() + + installed_modules = [row[0] for row in rows] + + cur.close() + conn.close() + except psycopg2.Error as e: + print(f"Database error: {e}") + print("Ensure psycopg2 matches the server configuration.") + return [] + except Exception as e: + print(f"Error checking installed modules: {e}") + return [] + + return installed_modules + +def main(): + parser = argparse.ArgumentParser(description="Upgrade installed modules in customaddons for a specific database.") + parser.add_argument('-d', '--database', required=True, help="Database name to upgrade modules on.") + parser.add_argument('-c', '--config', required=True, help="Path to Odoo configuration file.") + parser.add_argument('--odoo-bin', default='./odoo/odoo-bin', help="Path to odoo-bin executable (default: ./odoo/odoo-bin)") + + args = parser.parse_args() + + # Determine customaddons path relative to this script + script_dir = os.path.dirname(os.path.abspath(__file__)) + custom_addons_path = os.path.join(script_dir, 'customaddons') + + print(f"Scanning for modules in {custom_addons_path}...") + modules = get_custom_addons_modules(custom_addons_path) + + if not modules: + print("No modules found in customaddons.") + sys.exit(0) + + print(f"Found {len(modules)} local modules.") + + # Get DB params from config + db_params = get_db_params(args.config) + + print(f"Checking installed status in database '{args.database}'...") + installed_modules = get_installed_modules(modules, args.database, db_params) + + if not installed_modules: + print("None of the local modules are installed in the specified database.") + sys.exit(0) + + print(f"Found {len(installed_modules)} installed modules to upgrade.") + + # Construct the Odoo command + modules_str = ','.join(installed_modules) + + # Check if odoo-bin exists + if not os.path.exists(args.odoo_bin) and not os.path.exists(os.path.abspath(args.odoo_bin)): + print(f"Error: odoo-bin not found at {args.odoo_bin}") + sys.exit(1) + + cmd = [ + sys.executable, + args.odoo_bin, + '-c', args.config, + '-d', args.database, + '-u', modules_str, + '--stop-after-init' + ] + + print("--------------------------------------------------") + print(f"Starting upgrade for database '{args.database}'...") + print(f"Modules to upgrade: {modules_str}") + print(f"Command: {' '.join(cmd)}") + print("--------------------------------------------------") + + try: + subprocess.run(cmd, check=True) + print("--------------------------------------------------") + print("Upgrade completed successfully.") + except subprocess.CalledProcessError as e: + print("--------------------------------------------------") + print(f"Error: Upgrade failed with exit code {e.returncode}") + sys.exit(e.returncode) + except Exception as e: + print(f"An unexpected error occurred: {e}") + sys.exit(1) + +if __name__ == "__main__": + main()