mirror of
https://github.com/Ladebeze66/odoo_toolkit.git
synced 2025-12-13 09:06:52 +01:00
firstcommit
This commit is contained in:
commit
c999ab12a9
58
README.md
Normal file
58
README.md
Normal file
@ -0,0 +1,58 @@
|
||||
# Gestionnaire de Tickets Odoo Simplifié
|
||||
|
||||
Ce projet est une version simplifiée et optimisée du gestionnaire de tickets Odoo. Il permet d'interagir avec une instance Odoo pour gérer des tickets de projet.
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
1. **Afficher la liste des modèles** - Affiche tous les modèles disponibles dans l'instance Odoo.
|
||||
2. **Afficher les champs d'un modèle** - Affiche tous les champs d'un modèle donné.
|
||||
3. **Exporter les informations des champs en JSON** - Exporte la structure des champs d'un modèle en format JSON.
|
||||
4. **Exporter les tickets d'un project_id par étape** - Exporte tous les tickets d'un project_id, classés par étape (stage_id).
|
||||
|
||||
## Structure du projet
|
||||
|
||||
- `main.py` - Point d'entrée principal du programme
|
||||
- `menu_principal.py` - Gestion du menu principal
|
||||
- `menu_handlers.py` - Gestionnaires d'actions pour chaque option du menu
|
||||
- `ticket_manager.py` - Classe principale pour la gestion des tickets et modèles
|
||||
- `odoo_connection.py` - Gestion de la connexion à l'instance Odoo
|
||||
- `data_filter.py` - Fonctions pour filtrer et nettoyer les données des tickets
|
||||
- `utils.py` - Fonctions utilitaires diverses
|
||||
- `config.py` - Configuration de l'application (connexion Odoo, chemins d'export, etc.)
|
||||
|
||||
## Prérequis
|
||||
|
||||
- Python 3.6 ou supérieur
|
||||
- Package `odoorpc` pour la connexion à Odoo
|
||||
- Package `bs4` (BeautifulSoup) pour le nettoyage des données HTML
|
||||
|
||||
## Installation
|
||||
|
||||
1. Installer les dépendances :
|
||||
```
|
||||
pip install odoorpc bs4
|
||||
```
|
||||
|
||||
2. Configurer les variables d'environnement (ou modifier `config.py`) :
|
||||
- `ODOO_HOST` : Hôte de l'instance Odoo
|
||||
- `ODOO_DB` : Nom de la base de données Odoo
|
||||
- `ODOO_USER` : Nom d'utilisateur Odoo
|
||||
- `ODOO_PASSWORD` : Mot de passe Odoo
|
||||
|
||||
## Utilisation
|
||||
|
||||
1. Exécuter le programme :
|
||||
```
|
||||
python main.py
|
||||
```
|
||||
|
||||
2. Suivre les instructions du menu pour utiliser les différentes fonctionnalités.
|
||||
|
||||
## Exemple d'utilisation
|
||||
|
||||
### Exporter les tickets d'un projet par étape
|
||||
|
||||
1. Sélectionner l'option 4 dans le menu
|
||||
2. Entrer l'ID du projet (par exemple, "5")
|
||||
3. Confirmer l'action
|
||||
4. Les tickets seront exportés dans le répertoire `exported_tickets/project_5_NomDuProjet/`, classés par étape
|
||||
0
README.md:Zone.Identifier
Normal file
0
README.md:Zone.Identifier
Normal file
13
config.py
Normal file
13
config.py
Normal file
@ -0,0 +1,13 @@
|
||||
import os
|
||||
|
||||
# Configuration Odoo
|
||||
ODOO_HOST = os.getenv('ODOO_HOST', 'odoo.cbao.fr')
|
||||
ODOO_DB = os.getenv('ODOO_DB', 'production_cbao')
|
||||
ODOO_USER = os.getenv('ODOO_USER', 'fernand@cbao.fr')
|
||||
ODOO_PASSWORD = os.getenv('ODOO_PASSWORD', 'Lestat66!')
|
||||
|
||||
# Configuration export
|
||||
EXPORT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "exported_tickets")
|
||||
|
||||
# Créer le répertoire d'export s'il n'existe pas
|
||||
os.makedirs(EXPORT_DIR, exist_ok=True)
|
||||
0
config.py:Zone.Identifier
Normal file
0
config.py:Zone.Identifier
Normal file
39
data_filter.py
Normal file
39
data_filter.py
Normal file
@ -0,0 +1,39 @@
|
||||
import re
|
||||
import html
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
def clean_html(content):
|
||||
"""Nettoie le contenu HTML en supprimant les balises."""
|
||||
if not content:
|
||||
return ""
|
||||
soup = BeautifulSoup(content, 'html.parser')
|
||||
return soup.get_text(separator='\n', strip=True)
|
||||
|
||||
def filter_ticket_data(ticket_data):
|
||||
"""Filtre les données d'un ticket pour ne garder que les informations essentielles."""
|
||||
# Créer un nouveau dictionnaire pour le ticket filtré
|
||||
filtered_ticket = {
|
||||
"ID du Ticket": ticket_data["ID du Ticket"],
|
||||
"Nom": ticket_data["Nom"],
|
||||
"Code": ticket_data.get("Code", "N/A"),
|
||||
"Date Limite": ticket_data["Date Limite"],
|
||||
"Champs Simples": ticket_data["Champs Simples"], # Conserver tous les champs simples
|
||||
"Champs Relationnels": ticket_data["Champs Relationnels"], # Conserver tous les champs relationnels
|
||||
"Discussions": [] # Initialiser la liste des discussions
|
||||
}
|
||||
|
||||
# Nettoyer le champ description dans Champs Simples
|
||||
if "description" in filtered_ticket["Champs Simples"]:
|
||||
filtered_ticket["Champs Simples"]["description"] = clean_html(filtered_ticket["Champs Simples"]["description"])
|
||||
|
||||
# Garder uniquement les discussions nécessaires
|
||||
for msg in ticket_data["Discussions"]:
|
||||
filtered_ticket["Discussions"].append({
|
||||
"ID Message": msg["ID Message"],
|
||||
"Sujet": msg["Sujet"],
|
||||
"Contenu": clean_html(msg["Contenu"]), # Nettoyage du contenu HTML
|
||||
"Auteur": msg["Auteur"],
|
||||
"Date": msg["Date"]
|
||||
})
|
||||
|
||||
return filtered_ticket
|
||||
0
data_filter.py:Zone.Identifier
Normal file
0
data_filter.py:Zone.Identifier
Normal file
8
main.py
Normal file
8
main.py
Normal file
@ -0,0 +1,8 @@
|
||||
from menu_principal import run_menu
|
||||
|
||||
def main():
|
||||
|
||||
run_menu()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
0
main.py:Zone.Identifier
Normal file
0
main.py:Zone.Identifier
Normal file
129
menu_handlers.py
Normal file
129
menu_handlers.py
Normal file
@ -0,0 +1,129 @@
|
||||
from ticket_manager import TicketManager
|
||||
|
||||
# Initialisation de l'objet
|
||||
ticket_manager = TicketManager()
|
||||
|
||||
def handle_list_models():
|
||||
"""Gère l'affichage de la liste des modèles"""
|
||||
ticket_manager.list_models()
|
||||
|
||||
|
||||
def handle_list_model_fields():
|
||||
"""Gère l'affichage des champs d'un modèle"""
|
||||
model_name = input("\nEntrez le nom du modèle: ")
|
||||
if not model_name:
|
||||
print("Aucun nom de modèle fourni.")
|
||||
return
|
||||
ticket_manager.list_model_fields(model_name)
|
||||
|
||||
|
||||
def handle_export_model_fields_to_json():
|
||||
"""Gère l'exportation des informations des champs d'un modèle en JSON"""
|
||||
model_name = input("\nEntrez le nom du modèle: ")
|
||||
if not model_name:
|
||||
print("Aucun nom de modèle fourni.")
|
||||
return
|
||||
filename = input("Entrez le nom du fichier pour l'exportation: ")
|
||||
if not filename:
|
||||
print("Aucun nom de fichier fourni.")
|
||||
return
|
||||
ticket_manager.export_model_fields_to_json(model_name, filename)
|
||||
|
||||
|
||||
def handle_project_tickets_by_stage():
|
||||
"""Gère l'exportation des tickets d'un projet par étape"""
|
||||
# Récupérer la liste des projets disponibles
|
||||
projects = ticket_manager.get_available_projects()
|
||||
if not projects:
|
||||
print("Aucun projet disponible. Impossible de continuer.")
|
||||
return
|
||||
|
||||
# Demander à l'utilisateur de choisir un projet
|
||||
project_id_input = input("\nEntrez l'ID du projet (ou 'q' pour quitter): ")
|
||||
if project_id_input.lower() == 'q':
|
||||
return
|
||||
|
||||
try:
|
||||
project_id = int(project_id_input)
|
||||
if project_id not in projects:
|
||||
print(f"Aucun projet trouvé avec l'ID: {project_id}")
|
||||
return
|
||||
except ValueError:
|
||||
print("L'ID du projet doit être un nombre entier.")
|
||||
return
|
||||
|
||||
# Récupérer les étapes (stage_id) du projet
|
||||
print(f"\nRécupération des étapes du projet: {projects[project_id]} (ID: {project_id})")
|
||||
stages = ticket_manager.get_project_stages(project_id)
|
||||
|
||||
if not stages:
|
||||
print("Aucune étape trouvée pour ce projet. Impossible de continuer.")
|
||||
return
|
||||
|
||||
# Afficher les étapes disponibles
|
||||
print("\nÉtapes disponibles:")
|
||||
for stage_id, stage_name in stages.items():
|
||||
print(f"ID: {stage_id} - {stage_name}")
|
||||
|
||||
# Demander à l'utilisateur s'il veut sélectionner ou exclure des étapes
|
||||
selection_mode = input("\nSouhaitez-vous:\n1. Exporter toutes les étapes\n2. Sélectionner des étapes spécifiques par ID\n3. Exclure certaines étapes par ID\nVotre choix (1/2/3): ")
|
||||
|
||||
selected_stage_ids = None
|
||||
|
||||
if selection_mode == '2':
|
||||
# Sélectionner des étapes spécifiques par ID
|
||||
stage_id_input = input("Entrez les IDs des étapes à inclure (séparés par des virgules): ")
|
||||
try:
|
||||
selected_stage_ids = [int(x.strip()) for x in stage_id_input.split(',') if x.strip()]
|
||||
# Vérifier que les IDs existent dans les étapes disponibles
|
||||
valid_ids = [stage_id for stage_id in selected_stage_ids if stage_id in stages]
|
||||
invalid_ids = [stage_id for stage_id in selected_stage_ids if stage_id not in stages]
|
||||
|
||||
selected_stage_ids = valid_ids
|
||||
|
||||
if invalid_ids:
|
||||
print(f"Attention: Les IDs suivants ne sont pas valides et seront ignorés: {', '.join(map(str, invalid_ids))}")
|
||||
|
||||
if not selected_stage_ids:
|
||||
print("Aucun ID d'étape valide sélectionné. Exportation annulée.")
|
||||
return
|
||||
|
||||
print(f"Étapes sélectionnées: {', '.join([f'{stages[stage_id]} (ID: {stage_id})' for stage_id in selected_stage_ids])}")
|
||||
except ValueError:
|
||||
print("Erreur dans la sélection des étapes. Format attendu: 1,2,3,...")
|
||||
return
|
||||
|
||||
elif selection_mode == '3':
|
||||
# Exclure certaines étapes par ID
|
||||
stage_id_input = input("Entrez les IDs des étapes à exclure (séparés par des virgules): ")
|
||||
try:
|
||||
excluded_stage_ids = [int(x.strip()) for x in stage_id_input.split(',') if x.strip()]
|
||||
# Vérifier que les IDs existent dans les étapes disponibles
|
||||
valid_excluded_ids = [stage_id for stage_id in excluded_stage_ids if stage_id in stages]
|
||||
invalid_ids = [stage_id for stage_id in excluded_stage_ids if stage_id not in stages]
|
||||
|
||||
excluded_stage_ids = valid_excluded_ids
|
||||
|
||||
if invalid_ids:
|
||||
print(f"Attention: Les IDs suivants ne sont pas valides et seront ignorés: {', '.join(map(str, invalid_ids))}")
|
||||
|
||||
# Sélectionner toutes les étapes sauf les exclues
|
||||
selected_stage_ids = [stage_id for stage_id in stages.keys() if stage_id not in excluded_stage_ids]
|
||||
|
||||
if not selected_stage_ids:
|
||||
print("Toutes les étapes ont été exclues. Exportation annulée.")
|
||||
return
|
||||
|
||||
print(f"Étapes sélectionnées: {', '.join([f'{stages[stage_id]} (ID: {stage_id})' for stage_id in selected_stage_ids])}")
|
||||
except ValueError:
|
||||
print("Erreur dans la sélection des étapes. Format attendu: 1,2,3,...")
|
||||
return
|
||||
|
||||
# Confirmer l'action
|
||||
confirmation = input(f"\nVoulez-vous exporter les tickets du projet {projects[project_id]}? (o/n): ")
|
||||
if confirmation.lower() != 'o':
|
||||
print("Exportation annulée.")
|
||||
return
|
||||
|
||||
# Exporter les tickets
|
||||
ticket_manager.export_tickets_by_project_and_stage(project_id, selected_stage_ids)
|
||||
0
menu_handlers.py:Zone.Identifier
Normal file
0
menu_handlers.py:Zone.Identifier
Normal file
36
menu_principal.py
Normal file
36
menu_principal.py
Normal file
@ -0,0 +1,36 @@
|
||||
from menu_handlers import (
|
||||
handle_list_models,
|
||||
handle_list_model_fields,
|
||||
handle_export_model_fields_to_json,
|
||||
handle_project_tickets_by_stage
|
||||
)
|
||||
|
||||
def display_main_menu():
|
||||
"""Affiche le menu principal de l'application"""
|
||||
print("\n==== GESTIONNAIRE DE TICKETS ODOO ====")
|
||||
print("1. Afficher la liste des modèles")
|
||||
print("2. Afficher les champs d'un modèle")
|
||||
print("3. Exporter les informations des champs d'un modèle en JSON")
|
||||
print("4. Exporter les tickets d'un project_id par étape")
|
||||
print("5. Quitter")
|
||||
return input("\nChoisissez une option (1-5): ")
|
||||
|
||||
|
||||
def run_menu():
|
||||
"""Exécute la boucle du menu principal"""
|
||||
while True:
|
||||
choice = display_main_menu()
|
||||
|
||||
if choice == '1':
|
||||
handle_list_models()
|
||||
elif choice == '2':
|
||||
handle_list_model_fields()
|
||||
elif choice == '3':
|
||||
handle_export_model_fields_to_json()
|
||||
elif choice == '4':
|
||||
handle_project_tickets_by_stage()
|
||||
elif choice == '5':
|
||||
print("Au revoir!")
|
||||
break
|
||||
else:
|
||||
print("Option invalide. Veuillez choisir entre 1 et 5.")
|
||||
0
menu_principal.py:Zone.Identifier
Normal file
0
menu_principal.py:Zone.Identifier
Normal file
33
odoo_connection.py
Normal file
33
odoo_connection.py
Normal file
@ -0,0 +1,33 @@
|
||||
import odoorpc
|
||||
from config import ODOO_HOST, ODOO_DB, ODOO_USER, ODOO_PASSWORD
|
||||
|
||||
class OdooConnection:
|
||||
"""Gère la connexion à l'instance Odoo"""
|
||||
|
||||
def __init__(self):
|
||||
self.odoo = None
|
||||
self.connected = False
|
||||
|
||||
def connect(self):
|
||||
"""Établit la connexion à Odoo"""
|
||||
try:
|
||||
self.odoo = odoorpc.ODOO(ODOO_HOST, port=443, protocol='jsonrpc+ssl')
|
||||
print(f"Connexion réussie à {ODOO_HOST}")
|
||||
|
||||
self.odoo.login(ODOO_DB, ODOO_USER, ODOO_PASSWORD)
|
||||
print(f"Authentifié en tant que {ODOO_USER}")
|
||||
|
||||
self.connected = True
|
||||
return True
|
||||
except odoorpc.error.RPCError as e:
|
||||
print(f"Erreur RPC Odoo : {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Erreur inattendue : {e}")
|
||||
return False
|
||||
|
||||
def get_odoo_instance(self):
|
||||
"""Retourne l'instance Odoo connectée"""
|
||||
if not self.connected:
|
||||
self.connect()
|
||||
return self.odoo
|
||||
0
odoo_connection.py:Zone.Identifier
Normal file
0
odoo_connection.py:Zone.Identifier
Normal file
131
ticket_manager.py
Normal file
131
ticket_manager.py
Normal file
@ -0,0 +1,131 @@
|
||||
from odoo_connection import OdooConnection
|
||||
import os
|
||||
import json
|
||||
from utils import save_json, ensure_export_directory
|
||||
from config import EXPORT_DIR
|
||||
from data_filter import filter_ticket_data
|
||||
|
||||
class TicketManager:
|
||||
"""Gestionnaire de tickets simplifié avec seulement les fonctionnalités essentielles"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialise le gestionnaire de tickets"""
|
||||
self.conn = OdooConnection()
|
||||
self.odoo = self.conn.get_odoo_instance()
|
||||
self.model_name = "project.task"
|
||||
|
||||
def _check_connection(self):
|
||||
"""Vérifie la connexion Odoo"""
|
||||
if self.odoo is None:
|
||||
try:
|
||||
self.conn = OdooConnection()
|
||||
self.odoo = self.conn.get_odoo_instance()
|
||||
except Exception as e:
|
||||
print(f"Erreur de connexion: {e}")
|
||||
self.odoo = None
|
||||
return self.odoo is not None
|
||||
|
||||
def _safe_execute(self, model, method, *args):
|
||||
"""Exécute une méthode Odoo de manière sécurisée"""
|
||||
if not self._check_connection():
|
||||
return None
|
||||
try:
|
||||
return self.odoo.execute(model, method, *args)
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de {method} sur {model}: {e}")
|
||||
return None
|
||||
|
||||
def list_models(self):
|
||||
"""Affiche la liste des modèles disponibles dans Odoo"""
|
||||
models = self._safe_execute('ir.model', 'search_read', [], ['model', 'name'])
|
||||
if not models:
|
||||
print("Aucun modèle disponible.")
|
||||
return []
|
||||
|
||||
print("\nListe des modèles disponibles:")
|
||||
for model in models:
|
||||
print(f"Modèle: {model['name']} (ID: {model['model']})")
|
||||
return models
|
||||
|
||||
def list_model_fields(self, model_name):
|
||||
"""Affiche les champs d'un modèle donné"""
|
||||
fields_info = self._safe_execute(model_name, 'fields_get')
|
||||
if not fields_info:
|
||||
print(f"Aucun champ trouvé pour le modèle {model_name}.")
|
||||
return []
|
||||
|
||||
print(f"\nChamps du modèle {model_name}:")
|
||||
for field_name, field_data in fields_info.items():
|
||||
print(f"Champ: {field_name} - Type: {field_data['type']}")
|
||||
return fields_info
|
||||
|
||||
def get_project_tickets_summary(self, project_id):
|
||||
"""Récupère un résumé des tickets d'un projet pour permettre la sélection"""
|
||||
domain = [('project_id', '=', project_id)]
|
||||
ticket_ids = self._safe_execute(self.model_name, 'search', domain, 0, 200)
|
||||
|
||||
if not ticket_ids:
|
||||
print(f"Aucun ticket trouvé pour le projet ID: {project_id}")
|
||||
return []
|
||||
|
||||
fields_to_read = ['id', 'name', 'code', 'stage_id', 'date_deadline']
|
||||
tickets_data = self._safe_execute(self.model_name, 'read', ticket_ids, fields_to_read)
|
||||
|
||||
if not tickets_data:
|
||||
print("Erreur lors de la récupération des données des tickets.")
|
||||
return []
|
||||
|
||||
summary_tickets = []
|
||||
for ticket in tickets_data:
|
||||
stage_name = "Non défini"
|
||||
if ticket.get('stage_id'):
|
||||
stage_name = ticket['stage_id'][1] if isinstance(ticket['stage_id'], list) and len(ticket['stage_id']) > 1 else str(ticket['stage_id'])
|
||||
|
||||
summary_tickets.append({
|
||||
'id': ticket['id'],
|
||||
'name': ticket['name'],
|
||||
'code': ticket.get('code', 'N/A'),
|
||||
'stage_id': ticket.get('stage_id', [0, "Non défini"]),
|
||||
'stage_name': stage_name,
|
||||
'date_deadline': ticket.get('date_deadline', 'Non défini')
|
||||
})
|
||||
|
||||
return summary_tickets
|
||||
|
||||
def export_tickets_by_project_and_stage(self, project_id, selected_stage_ids=None):
|
||||
"""Exporte les tickets d'un projet classés par étape"""
|
||||
project_data = self._safe_execute('project.project', 'search_read', [('id', '=', project_id)], ['id', 'name'])
|
||||
if not project_data:
|
||||
print(f"Projet ID {project_id} introuvable")
|
||||
return
|
||||
|
||||
project_name = project_data[0]['name']
|
||||
domain = [('project_id', '=', project_id)]
|
||||
if selected_stage_ids:
|
||||
domain.append(('stage_id', 'in', selected_stage_ids))
|
||||
|
||||
ticket_ids = self._safe_execute(self.model_name, 'search', domain, 0, 1000)
|
||||
if not ticket_ids:
|
||||
print("Aucun ticket trouvé")
|
||||
return
|
||||
|
||||
tickets = [self.get_ticket_by_id(ticket_id) for ticket_id in ticket_ids]
|
||||
|
||||
tickets_by_stage = {}
|
||||
for ticket in tickets:
|
||||
stage_id = ticket["Champs Relationnels"].get("stage_id", [0, "Non classé"])[0]
|
||||
stage_name = ticket["Champs Relationnels"].get("stage_id", [0, "Non classé"])[1]
|
||||
key = f"{stage_id}_{stage_name}"
|
||||
|
||||
if key not in tickets_by_stage:
|
||||
tickets_by_stage[key] = []
|
||||
tickets_by_stage[key].append(ticket)
|
||||
|
||||
project_dir = ensure_export_directory(f"project_{project_id}_{project_name.replace(' ', '_')}")
|
||||
|
||||
for stage_key, stage_tickets in tickets_by_stage.items():
|
||||
stage_dir = os.path.join(project_dir, stage_key)
|
||||
os.makedirs(stage_dir, exist_ok=True)
|
||||
save_json(os.path.join(stage_dir, "all_tickets.json"), stage_tickets)
|
||||
|
||||
print(f"Exportation terminée dans {project_dir}/")
|
||||
0
ticket_manager.py:Zone.Identifier
Normal file
0
ticket_manager.py:Zone.Identifier
Normal file
38
utils.py
Normal file
38
utils.py
Normal file
@ -0,0 +1,38 @@
|
||||
import os
|
||||
import json
|
||||
from config import EXPORT_DIR
|
||||
|
||||
def ensure_export_directory(subdir=None):
|
||||
"""Assure que le répertoire d'export existe"""
|
||||
if subdir:
|
||||
directory = os.path.join(EXPORT_DIR, subdir)
|
||||
else:
|
||||
directory = EXPORT_DIR
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
return directory
|
||||
|
||||
def save_json(filename, data):
|
||||
"""Sauvegarde des données au format JSON"""
|
||||
try:
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=4)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de la sauvegarde du fichier {filename}: {e}")
|
||||
return False
|
||||
|
||||
def get_user_choice(prompt, options):
|
||||
"""Obtient un choix utilisateur parmi une liste d'options"""
|
||||
while True:
|
||||
print(prompt)
|
||||
for i, option in enumerate(options, 1):
|
||||
print(f"{i}. {option}")
|
||||
|
||||
try:
|
||||
choice = int(input("Votre choix: "))
|
||||
if 1 <= choice <= len(options):
|
||||
return choice
|
||||
else:
|
||||
print("Choix invalide. Veuillez réessayer.")
|
||||
except ValueError:
|
||||
print("Veuillez entrer un nombre valide.")
|
||||
0
utils.py:Zone.Identifier
Normal file
0
utils.py:Zone.Identifier
Normal file
Loading…
x
Reference in New Issue
Block a user