update utility
This commit is contained in:
parent
4938a55ad7
commit
f12b605180
68
cli/git.py
68
cli/git.py
@ -1,11 +1,71 @@
|
|||||||
# cli/module.py
|
# cli/module.py
|
||||||
import tqdm
|
import argparse
|
||||||
from services.git.handler import GitHandler
|
from services.git.handler import GitHandler
|
||||||
import lib.color_log as color_log
|
import lib.color_log as color_log
|
||||||
|
|
||||||
def setup_cli(subparsers):
|
def setup_cli(subparsers):
|
||||||
pass
|
git_parser = subparsers.add_parser('git', help='Git operations for Odoo instances')
|
||||||
|
git_subparsers = git_parser.add_subparsers(dest='git_command', help='Git commands')
|
||||||
|
|
||||||
|
# Clone command
|
||||||
|
clone_parser = git_subparsers.add_parser('clone', help='Clone repository for an instance')
|
||||||
|
clone_parser.add_argument('instance_name', help='Name of the instance to clone repository for')
|
||||||
|
clone_parser.add_argument('--branch', help='Branch to clone (optional)')
|
||||||
|
|
||||||
|
# Pull command
|
||||||
|
pull_parser = git_subparsers.add_parser('pull', help='Pull updates for an instance')
|
||||||
|
pull_parser.add_argument('instance_name', help='Name of the instance to pull updates for')
|
||||||
|
pull_parser.add_argument('--branch', help='Branch to pull from (optional)')
|
||||||
|
pull_parser.add_argument('--force', action='store_true', help='Force pull with hard reset (discards local changes)')
|
||||||
|
|
||||||
|
# Get commit command
|
||||||
|
commit_parser = git_subparsers.add_parser('commit', help='Get current commit hash')
|
||||||
|
commit_parser.add_argument('instance_name', help='Name of the instance to get commit for')
|
||||||
|
|
||||||
|
git_parser.set_defaults(func=git)
|
||||||
|
|
||||||
def git(args):
|
def git(args):
|
||||||
git_handler = GitHandler(args.repo_url, args.local_path, args.branch)
|
git_handler = GitHandler(config_path="utility/config/settings.yaml")
|
||||||
git_handler.pull_updates()
|
|
||||||
|
if args.git_command == 'clone':
|
||||||
|
try:
|
||||||
|
success = git_handler.clone_or_open_repo(
|
||||||
|
instance_name=args.instance_name,
|
||||||
|
branch=args.branch
|
||||||
|
)
|
||||||
|
if success:
|
||||||
|
color_log.Show("OK", f"Successfully cloned repository for {args.instance_name}")
|
||||||
|
else:
|
||||||
|
color_log.Show("FAILED", f"Failed to clone repository for {args.instance_name}")
|
||||||
|
except Exception as e:
|
||||||
|
color_log.Show("FAILED", f"Error cloning repository: {str(e)}")
|
||||||
|
|
||||||
|
elif args.git_command == 'pull':
|
||||||
|
try:
|
||||||
|
success = git_handler.pull_updates(
|
||||||
|
instance_name=args.instance_name,
|
||||||
|
branch=args.branch,
|
||||||
|
force=args.force
|
||||||
|
)
|
||||||
|
if success:
|
||||||
|
if args.force:
|
||||||
|
color_log.Show("OK", f"Successfully force pulled updates for {args.instance_name}")
|
||||||
|
else:
|
||||||
|
color_log.Show("OK", f"Successfully pulled updates for {args.instance_name}")
|
||||||
|
else:
|
||||||
|
color_log.Show("FAILED", f"Failed to pull updates for {args.instance_name}")
|
||||||
|
except Exception as e:
|
||||||
|
color_log.Show("FAILED", f"Error pulling updates: {str(e)}")
|
||||||
|
|
||||||
|
elif args.git_command == 'commit':
|
||||||
|
try:
|
||||||
|
commit_hash = git_handler.get_current_commit()
|
||||||
|
if commit_hash:
|
||||||
|
color_log.Show("INFO", f"Current commit for {args.instance_name}: {commit_hash}")
|
||||||
|
else:
|
||||||
|
color_log.Show("WARNING", f"No commit found for {args.instance_name}")
|
||||||
|
except Exception as e:
|
||||||
|
color_log.Show("FAILED", f"Error getting commit: {str(e)}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
color_log.Show("ERROR", "Please specify a valid git command (clone/pull/commit)")
|
||||||
|
2
main.py
2
main.py
@ -2,6 +2,7 @@
|
|||||||
import argparse
|
import argparse
|
||||||
from cli.service import setup_cli as setup_service_cli
|
from cli.service import setup_cli as setup_service_cli
|
||||||
from cli.module import setup_cli as setup_module_cli
|
from cli.module import setup_cli as setup_module_cli
|
||||||
|
from cli.git import setup_cli as setup_git_cli
|
||||||
|
|
||||||
|
|
||||||
def setup_cli():
|
def setup_cli():
|
||||||
@ -12,6 +13,7 @@ def setup_cli():
|
|||||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||||
setup_service_cli(subparsers)
|
setup_service_cli(subparsers)
|
||||||
setup_module_cli(subparsers)
|
setup_module_cli(subparsers)
|
||||||
|
setup_git_cli(subparsers)
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
@ -4,30 +4,96 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from ..services import config as Config
|
from ..services import config as Config
|
||||||
|
from ..lib import color_log
|
||||||
|
|
||||||
# Parse arguments properly
|
def run_command(cmd, description):
|
||||||
parser = argparse.ArgumentParser(description="Uninstall module for each instance")
|
"""Run a command and handle its output."""
|
||||||
parser.add_argument(
|
try:
|
||||||
"action", help="Action to perform", choices=["uninstall", "install", "upgrade"]
|
color_log.Show("INFO", f"Executing: {description}")
|
||||||
)
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
parser.add_argument("config_path", help="Path to the config file")
|
if result.stdout:
|
||||||
args = parser.parse_args()
|
print(result.stdout.strip())
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr.strip())
|
||||||
|
return result.returncode == 0
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
color_log.Show("FAILED", f"Error executing {description}: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
# Load the configuration
|
def update_instance(instance_name, action, force_pull=False):
|
||||||
print(f"Config path: {args.config_path}")
|
"""Update a single instance with git pull, module update, and service restart."""
|
||||||
config = Config.Config(config_path=args.config_path)
|
# 1. Pull latest code
|
||||||
|
pull_cmd = ["python", "utility/main.py", "git", "pull", instance_name]
|
||||||
|
if force_pull:
|
||||||
|
pull_cmd.append("--force")
|
||||||
|
if not run_command(pull_cmd, f"Pulling latest code for {instance_name}"):
|
||||||
|
color_log.Show("WARNING", f"Skipping module update for {instance_name} due to pull failure")
|
||||||
|
return False
|
||||||
|
|
||||||
instances = config.get_instances()
|
# 2. Update modules
|
||||||
|
module_cmd = ["python", "utility/main.py", "module", action, instance_name]
|
||||||
|
if not run_command(module_cmd, f"Updating modules for {instance_name}"):
|
||||||
|
color_log.Show("WARNING", f"Module update failed for {instance_name}")
|
||||||
|
return False
|
||||||
|
|
||||||
# Ensure instances is a valid list
|
# 3. Restart service
|
||||||
if not isinstance(instances, list):
|
restart_cmd = ["python", "utility/main.py", "service", "restart", instance_name]
|
||||||
print("Error: instances is not a valid list.")
|
if not run_command(restart_cmd, f"Restarting service for {instance_name}"):
|
||||||
sys.exit(1)
|
color_log.Show("WARNING", f"Service restart failed for {instance_name}")
|
||||||
|
return False
|
||||||
|
|
||||||
# Loop over each instance and run the uninstall command
|
return True
|
||||||
for instance in instances:
|
|
||||||
if "name" in instance:
|
def main():
|
||||||
cmd = ["python", "utility/main.py", "module", args.action, instance["name"]]
|
# Parse arguments
|
||||||
subprocess.run(cmd)
|
parser = argparse.ArgumentParser(description="Update modules for all instances")
|
||||||
else:
|
parser.add_argument(
|
||||||
print(f"Warning: Instance missing 'name' field. Skipping.")
|
"action",
|
||||||
|
help="Action to perform",
|
||||||
|
choices=["uninstall", "install", "upgrade"]
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"config_path",
|
||||||
|
help="Path to the config file"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--force-pull",
|
||||||
|
action="store_true",
|
||||||
|
help="Force pull with hard reset (discards local changes)"
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Load the configuration
|
||||||
|
color_log.Show("INFO", f"Loading configuration from {args.config_path}")
|
||||||
|
config = Config.Config(config_path=args.config_path)
|
||||||
|
|
||||||
|
# Get instances
|
||||||
|
instances = config.get_instances()
|
||||||
|
if not isinstance(instances, list):
|
||||||
|
color_log.Show("FAILED", "Error: instances is not a valid list.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Process each instance
|
||||||
|
success_count = 0
|
||||||
|
for instance in instances:
|
||||||
|
if "name" not in instance:
|
||||||
|
color_log.Show("WARNING", f"Instance missing 'name' field. Skipping.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
instance_name = instance["name"]
|
||||||
|
color_log.Show("INFO", f"\nProcessing instance: {instance_name}")
|
||||||
|
|
||||||
|
if update_instance(instance_name, args.action, args.force_pull):
|
||||||
|
success_count += 1
|
||||||
|
color_log.Show("OK", f"Successfully updated {instance_name}")
|
||||||
|
else:
|
||||||
|
color_log.Show("FAILED", f"Failed to update {instance_name}")
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
color_log.Show("INFO", f"\nUpdate Summary:")
|
||||||
|
color_log.Show("INFO", f"Total instances: {len(instances)}")
|
||||||
|
color_log.Show("INFO", f"Successful updates: {success_count}")
|
||||||
|
color_log.Show("INFO", f"Failed updates: {len(instances) - success_count}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import yaml
|
import yaml
|
||||||
class Config:
|
class Config:
|
||||||
@ -19,3 +18,20 @@ class Config:
|
|||||||
"""Return the list of Odoo instances."""
|
"""Return the list of Odoo instances."""
|
||||||
return self.settings.get("odoo_instances", [])
|
return self.settings.get("odoo_instances", [])
|
||||||
|
|
||||||
|
def get_instance(self, name):
|
||||||
|
"""
|
||||||
|
Get a single instance configuration by name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): Name of the instance to retrieve
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Instance configuration if found, None otherwise
|
||||||
|
"""
|
||||||
|
instances = self.get_instances()
|
||||||
|
for instance in instances:
|
||||||
|
if instance.get("name") == name:
|
||||||
|
return instance
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
@ -3,32 +3,84 @@ import os
|
|||||||
from services.odoo.connection import OdooConnection
|
from services.odoo.connection import OdooConnection
|
||||||
import lib.color_log as color_log
|
import lib.color_log as color_log
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
|
||||||
class GitHandler:
|
class GitHandler:
|
||||||
def __init__(self, config_path="config/settings.yaml"):
|
def __init__(self, config_path="config/settings.yaml"):
|
||||||
self.config = OdooConnection(config_path)
|
self.config = OdooConnection(config_path)
|
||||||
|
self.local_path = None # Will be set based on instance configuration
|
||||||
|
|
||||||
def _execute_command(self, cmd, instance_name):
|
def _execute_command(self, cmd, instance_name):
|
||||||
"""Execute a shell command and handle errors."""
|
"""Execute a shell command and handle errors."""
|
||||||
try:
|
try:
|
||||||
color_log.Show("INFO", f"Executing command: {cmd}")
|
color_log.Show("INFO", f"Executing command: {cmd}")
|
||||||
subprocess.run(cmd, shell=True, check=True, capture_output=True, text=True)
|
result = subprocess.run(
|
||||||
|
cmd, shell=True, check=True, capture_output=True, text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Print the output if there is any
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout.strip())
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr.strip())
|
||||||
|
|
||||||
color_log.Show("OK", f"Command executed successfully for {instance_name}")
|
color_log.Show("OK", f"Command executed successfully for {instance_name}")
|
||||||
|
return result.stdout
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
|
# Print the error output
|
||||||
|
if e.stdout:
|
||||||
|
print(e.stdout.strip())
|
||||||
|
if e.stderr:
|
||||||
|
print(e.stderr.strip())
|
||||||
|
|
||||||
color_log.Show(
|
color_log.Show(
|
||||||
"FAILED",
|
"FAILED",
|
||||||
f"Error performing git operation for {instance_name}: {e}",
|
f"Error performing git operation for {instance_name}: {e}",
|
||||||
)
|
)
|
||||||
|
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def _get_auth_url(self, repo_url, instance):
|
||||||
|
"""Add authentication to repository URL if credentials are provided."""
|
||||||
|
if not repo_url:
|
||||||
|
return repo_url
|
||||||
|
|
||||||
|
git_config = instance.get("git", {})
|
||||||
|
username = git_config.get("username")
|
||||||
|
password = git_config.get("password")
|
||||||
|
|
||||||
|
if username and password:
|
||||||
|
parsed_url = urlparse(repo_url)
|
||||||
|
# Replace the URL with authenticated version
|
||||||
|
auth_url = f"{parsed_url.scheme}://{username}:{password}@{parsed_url.netloc}{parsed_url.path}"
|
||||||
|
return auth_url
|
||||||
|
return repo_url
|
||||||
|
|
||||||
|
def _get_remote_command(self, instance, cmd):
|
||||||
|
"""Generate SSH command for remote execution."""
|
||||||
|
host = instance["host"]
|
||||||
|
ssh_settings = instance.get("ssh", {})
|
||||||
|
ssh_user = ssh_settings.get("user", "root")
|
||||||
|
ssh_key_path = ssh_settings.get("key_path")
|
||||||
|
|
||||||
|
local_host = host in ["localhost", "127.0.0.1"]
|
||||||
|
|
||||||
|
if not local_host:
|
||||||
|
if not ssh_key_path:
|
||||||
|
return f"ssh -t {ssh_user}@{host} 'sudo -s bash -c \"{cmd}\"'"
|
||||||
|
else:
|
||||||
|
return f"ssh -i {ssh_key_path} {ssh_user}@{host} 'sudo -s bash -c \"{cmd}\"'"
|
||||||
|
return cmd
|
||||||
|
|
||||||
def clone_or_open_repo(self, instance_name=None, repo_url=None, branch=None):
|
def clone_or_open_repo(self, instance_name=None, repo_url=None, branch=None):
|
||||||
"""Clone or open repository with SSH support"""
|
"""Clone or open repository with SSH support"""
|
||||||
try:
|
try:
|
||||||
if not instance_name:
|
if not instance_name:
|
||||||
# Local operation
|
# Local operation
|
||||||
if not os.path.exists(self.local_path):
|
if not os.path.exists(self.local_path):
|
||||||
cmd = f"git clone -b {branch or 'main'} {repo_url} {self.local_path}"
|
cmd = (
|
||||||
|
f"git clone -b {branch or 'main'} {repo_url} {self.local_path}"
|
||||||
|
)
|
||||||
self._execute_command(cmd, "local")
|
self._execute_command(cmd, "local")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -37,36 +89,101 @@ class GitHandler:
|
|||||||
if not instance:
|
if not instance:
|
||||||
raise ValueError(f"Instance {instance_name} not found")
|
raise ValueError(f"Instance {instance_name} not found")
|
||||||
|
|
||||||
|
# Set local_path from instance configuration
|
||||||
|
self.local_path = instance.get("git", {}).get("local_path")
|
||||||
|
if not self.local_path:
|
||||||
|
raise ValueError(
|
||||||
|
f"No local_path configured for instance {instance_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get repository URL from instance configuration if not provided
|
||||||
|
if not repo_url:
|
||||||
|
repo_url = instance.get("git", {}).get("repo_url")
|
||||||
|
if not repo_url:
|
||||||
|
raise ValueError(
|
||||||
|
f"No repository URL configured for instance {instance_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add authentication to repository URL
|
||||||
|
auth_url = self._get_auth_url(repo_url, instance)
|
||||||
|
|
||||||
# Check if repo exists
|
# Check if repo exists
|
||||||
check_cmd = f"test -d {self.local_path}/.git && echo 'exists' || echo 'not exists'"
|
check_cmd = (
|
||||||
result = self._execute_command(check_cmd, instance_name)
|
f"test -d {self.local_path}/.git && echo 'exists' || echo 'not exists'"
|
||||||
|
)
|
||||||
|
remote_check_cmd = self._get_remote_command(instance, check_cmd)
|
||||||
|
result = self._execute_command(remote_check_cmd, instance_name)
|
||||||
|
|
||||||
if "not exists" in result:
|
if "not exists" in result:
|
||||||
# Clone repository
|
# Clone repository with authentication
|
||||||
cmd = self._get_command(instance, "clone", repo_url, branch)
|
clone_cmd = (
|
||||||
self._execute_command(cmd, instance_name)
|
f"git clone -b {branch or 'main'} {auth_url} {self.local_path}"
|
||||||
|
)
|
||||||
|
remote_clone_cmd = self._get_remote_command(instance, clone_cmd)
|
||||||
|
self._execute_command(remote_clone_cmd, instance_name)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
color_log.Show("FAILED", f"Error in clone_or_open_repo: {e}")
|
color_log.Show("FAILED", f"Error in clone_or_open_repo: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def pull_updates(self, instance_name=None, branch=None):
|
def pull_updates(self, instance_name=None, branch=None, force=False):
|
||||||
"""Pull updates with SSH support"""
|
"""Pull updates with SSH support
|
||||||
|
|
||||||
|
Args:
|
||||||
|
instance_name (str, optional): Name of the instance
|
||||||
|
branch (str, optional): Branch to pull from
|
||||||
|
force (bool, optional): If True, will perform hard reset before pulling
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
if not instance_name:
|
if not instance_name:
|
||||||
# Local operation
|
# Local operation
|
||||||
|
if not self.local_path:
|
||||||
|
raise ValueError("local_path not set for local operation")
|
||||||
cmd = f"git --git-dir={self.local_path}/.git --work-tree={self.local_path} pull origin {branch or 'main'}"
|
cmd = f"git --git-dir={self.local_path}/.git --work-tree={self.local_path} pull origin {branch or 'main'}"
|
||||||
self._execute_command(cmd, "local")
|
self._execute_command(cmd, "local")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Remote operation
|
# Remote operation
|
||||||
instance = self.config.get_instance(instance_name)
|
instance = self.config.get_instance(instance_name)
|
||||||
|
branch = instance.get("git", {}).get("branch") or branch
|
||||||
if not instance:
|
if not instance:
|
||||||
raise ValueError(f"Instance {instance_name} not found")
|
raise ValueError(f"Instance {instance_name} not found")
|
||||||
|
|
||||||
cmd = self._get_command(instance, "pull", branch=branch)
|
# Set local_path from instance configuration
|
||||||
self._execute_command(cmd, instance_name)
|
self.local_path = instance.get("git", {}).get("local_path")
|
||||||
|
if not self.local_path:
|
||||||
|
raise ValueError(
|
||||||
|
f"No local_path configured for instance {instance_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get repository URL and add authentication
|
||||||
|
repo_url = instance.get("git", {}).get("repo_url")
|
||||||
|
auth_url = self._get_auth_url(repo_url, instance)
|
||||||
|
|
||||||
|
# Configure git to use credentials and set remote URL
|
||||||
|
git_commands = [
|
||||||
|
f"cd {self.local_path}",
|
||||||
|
f"git config --local credential.helper store",
|
||||||
|
f"git remote set-url origin {auth_url}",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add force reset commands if force is True
|
||||||
|
if force:
|
||||||
|
git_commands.extend(
|
||||||
|
[
|
||||||
|
"git fetch origin",
|
||||||
|
f"git reset --hard origin/{branch or 'main'}",
|
||||||
|
"git clean -fd", # Remove untracked files and directories
|
||||||
|
]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
git_commands.append(f"git pull origin {branch or 'main'}")
|
||||||
|
|
||||||
|
# Combine commands and execute remotely
|
||||||
|
combined_cmd = " && ".join(git_commands)
|
||||||
|
remote_cmd = self._get_remote_command(instance, combined_cmd)
|
||||||
|
self._execute_command(remote_cmd, instance_name)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -75,6 +192,7 @@ class GitHandler:
|
|||||||
|
|
||||||
def get_current_commit(self):
|
def get_current_commit(self):
|
||||||
return self.repo.head.commit.hexsha if self.repo else None
|
return self.repo.head.commit.hexsha if self.repo else None
|
||||||
|
|
||||||
def _get_command(self, instance, action, repo_url=None, branch=None):
|
def _get_command(self, instance, action, repo_url=None, branch=None):
|
||||||
"""
|
"""
|
||||||
Generate the appropriate git command based on instance type and action.
|
Generate the appropriate git command based on instance type and action.
|
||||||
@ -113,5 +231,3 @@ class GitHandler:
|
|||||||
cmd = f"ssh -i {ssh_key_path} {ssh_user}@{host} 'sudo {cmd}'"
|
cmd = f"ssh -i {ssh_key_path} {ssh_user}@{host} 'sudo {cmd}'"
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
|
|
@ -79,7 +79,9 @@ class OdooConnection:
|
|||||||
def get_instances(self):
|
def get_instances(self):
|
||||||
"""Return the list of configured instances."""
|
"""Return the list of configured instances."""
|
||||||
return self.instances
|
return self.instances
|
||||||
|
def get_instance(self,instance_name):
|
||||||
|
"""Return the instance configuration."""
|
||||||
|
return self.config.get_instance(instance_name)
|
||||||
def execute(self, instance_name, model, method, *args, **kwargs):
|
def execute(self, instance_name, model, method, *args, **kwargs):
|
||||||
"""Execute a method on a model for a specific instance."""
|
"""Execute a method on a model for a specific instance."""
|
||||||
odoo = self.get_connection(instance_name)
|
odoo = self.get_connection(instance_name)
|
||||||
|
Loading…
Reference in New Issue
Block a user