This commit is contained in:
xtremxpert 2025-02-05 13:14:42 -05:00
parent 45c1bdf440
commit b1a625a0e4
45 changed files with 1911 additions and 0 deletions

View file

@ -0,0 +1 @@
from . import models

View file

@ -0,0 +1,46 @@
{
'name': 'Interactive AI Voice Assistant',
'version': '17.0.1.0.0',
'category': 'Productivity/Communication',
'summary': 'Natural Language Voice Interaction for Odoo using Open WebUI',
'sequence': 1,
'author': 'Benoît Vézina',
'website': 'https://bemade.org',
'maintainer': 'it@bemade.org',
'license': 'LGPL-3',
'description': """
Interactive AI Voice Assistant for Odoo
=====================================
This module enables natural language voice interaction with Odoo directly in the Discuss interface:
- Voice input processing using Open WebUI models
- Interactive AI chat assistant
- Smart context-aware responses
- Draft mode for new records creation
""",
'depends': [
'base',
'web',
'mail',
],
'data': [
'security/ir.model.access.csv',
'views/ai_assistant_views.xml',
'views/ai_config_views.xml',
],
'assets': {
'web.assets_backend': [
'interactive_discuss_ai/static/src/js/ai_assistant_chat.js',
'interactive_discuss_ai/static/src/xml/ai_assistant_chat.xml',
],
},
'demo': [],
'installable': True,
'application': True,
'auto_install': False,
'external_dependencies': {
'python': [
'requests',
],
},
}

View file

@ -0,0 +1,2 @@
from . import ai_assistant
from . import ai_config

View file

@ -0,0 +1,101 @@
from odoo import models, fields, api, _
from odoo.exceptions import UserError
class AIAssistantChannel(models.Model):
_name = 'ai.assistant.channel'
_description = 'AI Assistant Channel'
_inherit = ['mail.thread']
name = fields.Char(default="Assistant AI", readonly=True, tracking=True)
company_id = fields.Many2one(
'res.company',
string='Company',
required=True,
default=lambda self: self.env.company,
tracking=True
)
active = fields.Boolean(default=True, tracking=True)
user_id = fields.Many2one(
'res.users',
string='User',
required=True,
default=lambda self: self.env.user,
tracking=True
)
message_ids = fields.One2many(
'mail.message',
'res_id',
string='Messages',
domain=lambda self: [('model', '=', self._name)],
tracking=True
)
@api.model_create_multi
def create(self, vals_list):
records = super().create(vals_list)
for record in records:
# Create a dedicated discussion channel for the assistant
channel = self.env['mail.channel'].create({
'name': _('Assistant AI - %s', record.company_id.name),
'channel_type': 'chat',
'channel_partner_ids': [(4, record.user_id.partner_id.id)],
})
return records
def _get_conversation_history(self, limit=10):
"""Get recent conversation history"""
self.ensure_one()
messages = self.env['mail.message'].search([
('model', '=', self._name),
('res_id', '=', self.id)
], limit=limit, order='id desc')
history = []
for msg in reversed(messages):
role = "assistant" if msg.author_id == self.user_id.partner_id else "user"
history.append({
"role": role,
"content": msg.body
})
return history
def process_voice_message(self, voice_input):
"""Process voice input and respond"""
self.ensure_one()
if not voice_input:
raise UserError(_('No voice input provided'))
# Get conversation history
conversation_history = self._get_conversation_history()
# Use AI service to generate response
ai_service = self.env['ai.service'].with_company(self.company_id)
response = ai_service.generate_response(voice_input, conversation_history)
# Post response as message
self.message_post(
body=response,
message_type='comment',
subtype_xmlid='mail.mt_comment'
)
return True
@api.model
def get_assistant_for_user(self, user_id=None):
"""Get or create an assistant for the user in their company"""
if not user_id:
user_id = self.env.user.id
assistant = self.search([
('user_id', '=', user_id),
('company_id', '=', self.env.company.id),
('active', '=', True)
], limit=1)
if not assistant:
assistant = self.create({
'user_id': user_id,
'company_id': self.env.company.id
})
return assistant

View file

@ -0,0 +1,61 @@
from odoo import models, fields, api
from odoo.exceptions import UserError
class AIConfig(models.Model):
_name = 'ai.config'
_description = 'AI Configuration'
_inherit = ['mail.thread']
name = fields.Char(string='Name', required=True, default='Default Config', tracking=True)
company_id = fields.Many2one(
'res.company',
string='Company',
required=True,
default=lambda self: self.env.company,
tracking=True
)
openwebui_url = fields.Char(
string='Open WebUI URL',
required=True,
default='http://localhost:8080',
tracking=True
)
model_name = fields.Char(
string='Model Name',
required=True,
help='Nom du modèle à utiliser dans Open WebUI',
tracking=True
)
system_prompt = fields.Text(
string='System Prompt',
default="""Tu es un assistant Odoo expert qui aide les utilisateurs à effectuer des actions
dans le système. Analyse leurs demandes et propose des actions concrètes.""",
tracking=True
)
temperature = fields.Float(
string='Temperature',
default=0.7,
help='Contrôle la créativité des réponses (0.0 - 1.0)',
tracking=True
)
max_tokens = fields.Integer(
string='Max Tokens',
default=2000,
tracking=True
)
active = fields.Boolean(default=True, tracking=True)
_sql_constraints = [
('company_uniq', 'unique(company_id, active)',
'Une seule configuration active par compagnie est autorisée!')
]
@api.model
def get_default_config(self):
"""Récupère la configuration active pour la compagnie actuelle"""
config = self.search([
('active', '=', True),
('company_id', '=', self.env.company.id)
], limit=1)
if not config:
raise UserError('Aucune configuration AI active trouvée pour votre compagnie')

View file

@ -0,0 +1,62 @@
import requests
import json
from odoo import models, tools
from odoo.exceptions import UserError
class AIService(models.AbstractModel):
_name = 'ai.service'
_description = 'AI Service for Open WebUI Integration'
def _get_headers(self):
return {
'Content-Type': 'application/json',
}
def _call_openwebui_api(self, endpoint, payload):
"""Appelle l'API Open WebUI avec la configuration de la compagnie actuelle"""
config = self.env['ai.config'].with_company(self.env.company).get_default_config()
url = f"{config.openwebui_url}/api/v1/{endpoint}"
try:
response = requests.post(
url,
headers=self._get_headers(),
json=payload,
timeout=30
)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
raise UserError(f"Erreur de communication avec Open WebUI: {str(e)}")
def generate_response(self, user_input, conversation_history=None):
"""Génère une réponse en utilisant le modèle Open WebUI de la compagnie"""
config = self.env['ai.config'].with_company(self.env.company).get_default_config()
messages = []
if config.system_prompt:
messages.append({
"role": "system",
"content": config.system_prompt
})
# Ajouter l'historique de conversation si disponible
if conversation_history:
messages.extend(conversation_history)
# Ajouter l'entrée utilisateur actuelle
messages.append({
"role": "user",
"content": user_input
})
payload = {
"model": config.model_name,
"messages": messages,
"temperature": config.temperature,
"max_tokens": config.max_tokens,
"stream": False
}
response = self._call_openwebui_api('chat/completions', payload)
return response.get('choices', [{}])[0].get('message', {}).get('content', '')

View file

@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_ai_assistant_channel_user,ai.assistant.channel.user,interactive_discuss_ai.model_ai_assistant_channel,base.group_user,1,1,1,1
access_ai_config_user,ai.config.user,interactive_discuss_ai.model_ai_config,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_ai_assistant_channel_user ai.assistant.channel.user interactive_discuss_ai.model_ai_assistant_channel base.group_user 1 1 1 1
3 access_ai_config_user ai.config.user interactive_discuss_ai.model_ai_config base.group_user 1 1 1 1

View file

@ -0,0 +1,52 @@
/** @odoo-module **/
import { registerPatch } from '@mail/model/model_core';
import { attr } from '@mail/model/model_field';
import { clear } from '@mail/model/model_field_command';
registerPatch({
name: 'mail.composer',
fields: {
isVoiceRecording: attr({
default: false,
}),
},
recordMethods: {
async onClickVoiceRecord() {
if (!this.isVoiceRecording) {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this.mediaRecorder = new MediaRecorder(stream);
const audioChunks = [];
this.mediaRecorder.addEventListener('dataavailable', (event) => {
audioChunks.push(event.data);
});
this.mediaRecorder.addEventListener('stop', async () => {
const audioBlob = new Blob(audioChunks);
// Convert to base64 and send to backend
const reader = new FileReader();
reader.readAsDataURL(audioBlob);
reader.onloadend = async () => {
const base64data = reader.result;
await this.env.services.rpc({
model: 'ai.assistant.channel',
method: 'process_voice_message',
args: [this.thread.id, base64data],
});
};
});
this.mediaRecorder.start();
this.update({ isVoiceRecording: true });
} catch (err) {
console.error('Error accessing microphone:', err);
}
} else {
this.mediaRecorder.stop();
this.update({ isVoiceRecording: false });
}
},
},
});

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="mail.Composer" t-inherit-mode="extension">
<xpath expr="//div[hasclass('o-mail-Composer-buttons')]" position="inside">
<button class="btn btn-light"
t-on-click="onClickVoiceRecord"
t-att-class="{ 'btn-danger': isVoiceRecording }">
<i class="fa fa-microphone" t-att-class="{ 'fa-pulse': isVoiceRecording }"/>
</button>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- AI Assistant Channel Form View -->
<record id="view_ai_assistant_channel_form" model="ir.ui.view">
<field name="name">ai.assistant.channel.form</field>
<field name="model">ai.assistant.channel</field>
<field name="arch" type="xml">
<form string="AI Assistant Channel">
<sheet>
<div class="oe_button_box" name="button_box">
<button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button" options="{&quot;terminology&quot;: &quot;archive&quot;}"/>
</button>
</div>
<group>
<group>
<field name="name"/>
<field name="user_id"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
</group>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="message_ids"/>
</div>
</form>
</field>
</record>
<!-- AI Assistant Channel Tree View -->
<record id="view_ai_assistant_channel_tree" model="ir.ui.view">
<field name="name">ai.assistant.channel.tree</field>
<field name="model">ai.assistant.channel</field>
<field name="arch" type="xml">
<tree string="AI Assistant Channels">
<field name="name"/>
<field name="user_id"/>
<field name="company_id" groups="base.group_multi_company"/>
</tree>
</field>
</record>
<!-- AI Assistant Channel Search View -->
<record id="view_ai_assistant_channel_search" model="ir.ui.view">
<field name="name">ai.assistant.channel.search</field>
<field name="model">ai.assistant.channel</field>
<field name="arch" type="xml">
<search string="Search AI Assistant Channels">
<field name="name"/>
<field name="user_id"/>
<field name="company_id" groups="base.group_multi_company"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
</search>
</field>
</record>
<!-- AI Assistant Channel Action -->
<record id="action_ai_assistant_channel" model="ir.actions.act_window">
<field name="name">AI Assistant Channels</field>
<field name="res_model">ai.assistant.channel</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="view_ai_assistant_channel_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first AI Assistant Channel!
</p>
<p>
AI Assistant Channels allow you to interact with the AI assistant through messages.
</p>
</field>
</record>
<!-- Menu Items -->
<menuitem id="menu_ai_assistant"
name="AI Assistant"
web_icon="interactive_discuss_ai,static/description/icon.png"
sequence="10"/>
<menuitem id="menu_ai_assistant_channel"
name="Assistant Channels"
parent="menu_ai_assistant"
action="action_ai_assistant_channel"
sequence="10"/>
</odoo>

View file

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_ai_config_form" model="ir.ui.view">
<field name="name">ai.config.form</field>
<field name="model">ai.config</field>
<field name="arch" type="xml">
<form>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button"/>
</button>
</div>
<group>
<group>
<field name="name"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="openwebui_url"/>
<field name="model_name"/>
<field name="active"/>
</group>
<group>
<field name="temperature"/>
<field name="max_tokens"/>
</group>
</group>
<group string="System Prompt">
<field name="system_prompt" nolabel="1"/>
</group>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="message_ids"/>
</div>
</form>
</field>
</record>
<record id="view_ai_config_tree" model="ir.ui.view">
<field name="name">ai.config.tree</field>
<field name="model">ai.config</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="model_name"/>
<field name="openwebui_url"/>
<field name="active"/>
</tree>
</field>
</record>
<record id="view_ai_config_search" model="ir.ui.view">
<field name="name">ai.config.search</field>
<field name="model">ai.config</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="model_name"/>
<separator/>
<filter string="Archivé" name="inactive" domain="[('active', '=', False)]"/>
<group expand="0" string="Group By">
<filter string="Compagnie" name="company" domain="[]" context="{'group_by': 'company_id'}"/>
</group>
</search>
</field>
</record>
<record id="action_ai_config" model="ir.actions.act_window">
<field name="name">Configuration AI</field>
<field name="res_model">ai.config</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="view_ai_config_search"/>
<field name="context">{'search_default_active': 1}</field>
</record>
<menuitem id="menu_ai_config"
name="Configuration AI"
action="action_ai_config"
parent="base.menu_administration"
sequence="100"/>
</odoo>

View file

@ -0,0 +1,90 @@
# Odoo Proxmox Manager
This module allows you to manage your Proxmox servers and virtual machines directly from Odoo 17.0.
## Features
- Manage multiple Proxmox servers and clusters
- Monitor server status and resources
- View and manage virtual machines
- Start, stop, restart, suspend, and resume VMs
- Group servers into clusters for better organization
- Multi-company support
- Role-based access control
## Installation
### Prerequisites
1. Odoo 17.0
2. Python package requirements:
```
proxmoxer
requests
```
### Steps
1. Clone this repository into your Odoo addons directory
2. Install the required Python packages:
```bash
pip install proxmoxer requests
```
3. Update your Odoo addons list
4. Install the module through Odoo's Apps menu
## Configuration
1. Create an API Token in your Proxmox server:
- Log in to your Proxmox web interface
- Go to Datacenter -> Permissions -> API Tokens
- Create a new token and note down the Token ID and Secret
2. In Odoo:
- Go to the Proxmox menu
- Create a new cluster (optional)
- Create a new server:
- Enter the server hostname
- Enter your API token information
- Test the connection
## Usage
### Managing Servers
1. Navigate to Proxmox -> Servers
2. Create or select a server
3. Click "Sync VMs" to synchronize virtual machines
4. View server status and resource information
### Managing Virtual Machines
1. Navigate to Proxmox -> Virtual Machines
2. View all VMs across your servers
3. Use the action buttons to:
- Start VMs
- Stop VMs
- Restart VMs
- Suspend VMs
- Resume VMs
### Managing Clusters
1. Navigate to Proxmox -> Clusters
2. Create a new cluster
3. Add servers to the cluster
4. Use "Sync All Servers" to update all servers in the cluster
## Security
The module includes two user groups:
- Proxmox User: Can view servers and VMs
- Proxmox Manager: Can manage servers and VMs
## Support
For bugs or feature requests, please create an issue in the repository.
## License
LGPL-3

View file

@ -0,0 +1,3 @@
from . import models
from . import controllers
from . import wizard

View file

@ -0,0 +1,35 @@
{
'name': 'Proxmox Manager',
'version': '17.0.1.0.0',
'category': 'Administration',
'summary': 'Manage Proxmox servers and clusters from Odoo',
'sequence': 1,
'author': 'DurPro',
'website': 'https://www.durpro.com',
'license': 'LGPL-3',
'icon': '/odoo_proxmox_manager/static/description/icon.png',
'depends': [
'base',
'web',
'mail'
],
'data': [
'security/proxmox_security.xml',
'security/ir.model.access.csv',
'views/proxmox_server_views.xml',
'views/proxmox_cluster_views.xml',
'views/proxmox_vm_views.xml',
'views/proxmox_menus.xml',
],
'assets': {
'web.assets_backend': [
'odoo_proxmox_manager/static/src/components/**/*',
'odoo_proxmox_manager/static/src/js/dashboard_view.js',
'odoo_proxmox_manager/static/src/scss/dashboard.scss',
'odoo_proxmox_manager/static/src/xml/dashboard.xml',
],
},
'application': True,
'installable': True,
'auto_install': False
}

View file

@ -0,0 +1 @@
from . import main

View file

@ -0,0 +1,9 @@
from odoo import http
from odoo.http import request
class ProxmoxController(http.Controller):
@http.route('/proxmox/dashboard/data', type='json', auth='user')
def get_dashboard_data(self):
"""Get dashboard data for the Proxmox overview"""
return request.env['proxmox.server'].get_dashboard_data()

View file

@ -0,0 +1,3 @@
from . import proxmox_server
from . import proxmox_cluster
from . import proxmox_vm

View file

@ -0,0 +1,37 @@
from odoo import models, fields, api
class ProxmoxCluster(models.Model):
_name = 'proxmox.cluster'
_description = 'Proxmox Cluster'
_order = 'name'
_inherit = ['mail.thread']
name = fields.Char(string='Name', required=True, tracking=True)
description = fields.Text(string='Description', tracking=True)
company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company)
server_ids = fields.One2many('proxmox.server', 'cluster_id', string='Servers')
server_count = fields.Integer(string='Server Count', compute='_compute_server_count')
total_vms = fields.Integer(string='Total VMs', compute='_compute_total_vms')
active_servers = fields.Integer(string='Active Servers', compute='_compute_active_servers')
@api.depends('server_ids')
def _compute_server_count(self):
for cluster in self:
cluster.server_count = len(cluster.server_ids)
@api.depends('server_ids.vm_ids')
def _compute_total_vms(self):
for cluster in self:
cluster.total_vms = sum(server.vm_count for server in cluster.server_ids)
@api.depends('server_ids.state')
def _compute_active_servers(self):
for cluster in self:
cluster.active_servers = len(cluster.server_ids.filtered(lambda s: s.state == 'online'))
def action_sync_all_servers(self):
"""Synchronize all servers in the cluster"""
self.ensure_one()
for server in self.server_ids:
server.action_sync_vms()
return True

View file

@ -0,0 +1,164 @@
from odoo import models, fields, api
from proxmoxer import ProxmoxAPI
import requests
import logging
_logger = logging.getLogger(__name__)
class ProxmoxServer(models.Model):
_name = 'proxmox.server'
_description = 'Proxmox Server'
_order = 'name'
_inherit = ['mail.thread']
name = fields.Char(string='Name', required=True, tracking=True)
hostname = fields.Char(string='Hostname', required=True, tracking=True)
port = fields.Integer(string='Port', default=8006, tracking=True)
cluster_id = fields.Many2one('proxmox.cluster', string='Cluster', tracking=True)
username = fields.Char(string='Username', required=True, tracking=True)
token_name = fields.Char(string='API Token Name', tracking=True)
token_value = fields.Char(string='API Token Value', tracking=True)
verify_ssl = fields.Boolean(string='Verify SSL', default=True, tracking=True)
company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company)
state = fields.Selection([
('offline', 'Offline'),
('online', 'Online')
], string='Status', default='offline', compute='_compute_state', store=True, tracking=True)
vm_ids = fields.One2many('proxmox.vm', 'server_id', string='Virtual Machines')
vm_count = fields.Integer(string='VM Count', compute='_compute_vm_count')
node_info = fields.Text(string='Node Information', compute='_compute_node_info')
def _get_proxmox_connection(self):
try:
if not self.token_name or not self.token_value:
raise ValueError("API Token is required")
return ProxmoxAPI(
self.hostname,
port=self.port,
user=self.username,
token_name=self.token_name,
token_value=self.token_value,
verify_ssl=self.verify_ssl
)
except Exception as e:
_logger.error(f"Failed to connect to Proxmox server {self.name}: {str(e)}")
return None
@api.depends('hostname', 'port', 'token_name', 'token_value')
def _compute_state(self):
for server in self:
try:
proxmox = server._get_proxmox_connection()
if proxmox:
# Test connection by getting version info
proxmox.version.get()
server.state = 'online'
else:
server.state = 'offline'
except:
server.state = 'offline'
@api.depends('vm_ids')
def _compute_vm_count(self):
for server in self:
server.vm_count = len(server.vm_ids)
def _compute_node_info(self):
for server in self:
try:
proxmox = server._get_proxmox_connection()
if proxmox:
node_info = proxmox.nodes(server.hostname).status.get()
server.node_info = str(node_info)
else:
server.node_info = "Unable to connect to server"
except Exception as e:
server.node_info = f"Error getting node info: {str(e)}"
def action_sync_vms(self):
"""Synchronize VMs from Proxmox server"""
self.ensure_one()
proxmox = self._get_proxmox_connection()
if not proxmox:
return False
try:
# Get all VMs from the server
vms = proxmox.nodes(self.hostname).qemu.get()
# Update or create VM records
for vm in vms:
vm_vals = {
'name': vm.get('name'),
'vmid': vm.get('vmid'),
'status': vm.get('status'),
'server_id': self.id,
}
existing_vm = self.env['proxmox.vm'].search([
('server_id', '=', self.id),
('vmid', '=', vm.get('vmid'))
])
if existing_vm:
existing_vm.write(vm_vals)
else:
self.env['proxmox.vm'].create(vm_vals)
return True
except Exception as e:
_logger.error(f"Failed to sync VMs for server {self.name}: {str(e)}")
return False
@api.model
def get_dashboard_data(self):
"""Get dashboard data for the Proxmox overview"""
servers = self.search([])
clusters = self.env['proxmox.cluster'].search([])
vms = self.env['proxmox.vm'].search([])
server_data = []
for server in servers:
try:
proxmox = server._get_proxmox_connection()
if proxmox:
node_info = proxmox.nodes(server.hostname).status.get()
memory_total = node_info.get('memory', {}).get('total', 0)
memory_used = node_info.get('memory', {}).get('used', 0)
memory_usage = round((memory_used / memory_total) * 100, 2) if memory_total else 0
cpu_usage = round(node_info.get('cpu', 0) * 100, 2)
server_data.append({
'id': server.id,
'name': server.name,
'state': server.state,
'vm_count': server.vm_count,
'memory_usage': memory_usage,
'cpu_usage': cpu_usage,
})
else:
server_data.append({
'id': server.id,
'name': server.name,
'state': 'offline',
'vm_count': server.vm_count,
'memory_usage': 0,
'cpu_usage': 0,
})
except Exception as e:
_logger.error(f"Error getting data for server {server.name}: {str(e)}")
server_data.append({
'id': server.id,
'name': server.name,
'state': 'offline',
'vm_count': server.vm_count,
'memory_usage': 0,
'cpu_usage': 0,
})
return {
'server_count': len(servers),
'cluster_count': len(clusters),
'vm_count': len(vms),
'servers': server_data,
}

View file

@ -0,0 +1,87 @@
from odoo import models, fields, api
class ProxmoxVM(models.Model):
_name = 'proxmox.vm'
_description = 'Proxmox Virtual Machine'
_order = 'name'
_inherit = ['mail.thread']
name = fields.Char(string='Name', required=True, tracking=True)
vmid = fields.Integer(string='VM ID', required=True, tracking=True)
server_id = fields.Many2one('proxmox.server', string='Server', required=True, tracking=True)
company_id = fields.Many2one('res.company', string='Company', required=True,
related='server_id.company_id', store=True, readonly=True)
status = fields.Selection([
('running', 'Running'),
('stopped', 'Stopped'),
('suspended', 'Suspended')
], string='Status', default='stopped', tracking=True)
memory = fields.Integer(string='Memory (MB)')
cpus = fields.Integer(string='vCPUs')
disk_size = fields.Float(string='Disk Size (GB)')
ip_address = fields.Char(string='IP Address')
def action_start(self):
"""Start the virtual machine"""
self.ensure_one()
proxmox = self.server_id._get_proxmox_connection()
if proxmox:
try:
proxmox.nodes(self.server_id.hostname).qemu(self.vmid).status.start.post()
self.status = 'running'
return True
except Exception as e:
return False
return False
def action_stop(self):
"""Stop the virtual machine"""
self.ensure_one()
proxmox = self.server_id._get_proxmox_connection()
if proxmox:
try:
proxmox.nodes(self.server_id.hostname).qemu(self.vmid).status.stop.post()
self.status = 'stopped'
return True
except Exception as e:
return False
return False
def action_restart(self):
"""Restart the virtual machine"""
self.ensure_one()
proxmox = self.server_id._get_proxmox_connection()
if proxmox:
try:
proxmox.nodes(self.server_id.hostname).qemu(self.vmid).status.reset.post()
self.status = 'running'
return True
except Exception as e:
return False
return False
def action_suspend(self):
"""Suspend the virtual machine"""
self.ensure_one()
proxmox = self.server_id._get_proxmox_connection()
if proxmox:
try:
proxmox.nodes(self.server_id.hostname).qemu(self.vmid).status.suspend.post()
self.status = 'suspended'
return True
except Exception as e:
return False
return False
def action_resume(self):
"""Resume the virtual machine"""
self.ensure_one()
proxmox = self.server_id._get_proxmox_connection()
if proxmox:
try:
proxmox.nodes(self.server_id.hostname).qemu(self.vmid).status.resume.post()
self.status = 'running'
return True
except Exception as e:
return False
return False

View file

@ -0,0 +1,2 @@
proxmoxer>=1.3.1
requests>=2.31.0

View file

@ -0,0 +1,9 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_proxmox_server_user,proxmox.server.user,model_proxmox_server,group_proxmox_user,1,0,0,0
access_proxmox_server_manager,proxmox.server.manager,model_proxmox_server,group_proxmox_manager,1,1,1,1
access_proxmox_cluster_user,proxmox.cluster.user,model_proxmox_cluster,group_proxmox_user,1,0,0,0
access_proxmox_cluster_manager,proxmox.cluster.manager,model_proxmox_cluster,group_proxmox_manager,1,1,1,1
access_proxmox_vm_user,proxmox.vm.user,model_proxmox_vm,group_proxmox_user,1,1,0,0
access_proxmox_vm_manager,proxmox.vm.manager,model_proxmox_vm,group_proxmox_manager,1,1,1,1
access_proxmox_server_wizard,proxmox.server.wizard,model_proxmox_server_wizard,base.group_user,1,1,1,0
access_proxmox_server_wizard_node,proxmox.server.wizard.node,model_proxmox_server_wizard_node,base.group_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_proxmox_server_user proxmox.server.user model_proxmox_server group_proxmox_user 1 0 0 0
3 access_proxmox_server_manager proxmox.server.manager model_proxmox_server group_proxmox_manager 1 1 1 1
4 access_proxmox_cluster_user proxmox.cluster.user model_proxmox_cluster group_proxmox_user 1 0 0 0
5 access_proxmox_cluster_manager proxmox.cluster.manager model_proxmox_cluster group_proxmox_manager 1 1 1 1
6 access_proxmox_vm_user proxmox.vm.user model_proxmox_vm group_proxmox_user 1 1 0 0
7 access_proxmox_vm_manager proxmox.vm.manager model_proxmox_vm group_proxmox_manager 1 1 1 1
8 access_proxmox_server_wizard proxmox.server.wizard model_proxmox_server_wizard base.group_user 1 1 1 0
9 access_proxmox_server_wizard_node proxmox.server.wizard.node model_proxmox_server_wizard_node base.group_user 1 1 1 0

View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="module_proxmox_category" model="ir.module.category">
<field name="name">Proxmox Management</field>
<field name="description">Manage Proxmox servers and virtual machines</field>
<field name="sequence">20</field>
</record>
<record id="group_proxmox_user" model="res.groups">
<field name="name">User</field>
<field name="category_id" ref="module_proxmox_category"/>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
</record>
<record id="group_proxmox_manager" model="res.groups">
<field name="name">Manager</field>
<field name="category_id" ref="module_proxmox_category"/>
<field name="implied_ids" eval="[(4, ref('group_proxmox_user'))]"/>
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
</record>
</data>
<data noupdate="1">
<record id="proxmox_server_rule" model="ir.rule">
<field name="name">Proxmox Server Multi-Company</field>
<field name="model_id" ref="model_proxmox_server"/>
<field name="global" eval="True"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
<record id="proxmox_cluster_rule" model="ir.rule">
<field name="name">Proxmox Cluster Multi-Company</field>
<field name="model_id" ref="model_proxmox_cluster"/>
<field name="global" eval="True"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
</data>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 833 B

View file

@ -0,0 +1,32 @@
/** @odoo-module **/
import { ListController } from "@web/views/list/list_controller";
import { registry } from "@web/core/registry";
import { listView } from "@web/views/list/list_view";
class ProxmoxServerListController extends ListController {
setup() {
super.setup();
}
async onAddServer() {
await this.env.services.action.doAction({
type: 'ir.actions.act_window',
res_model: 'proxmox.server.wizard',
view_mode: 'form',
view_type: 'form',
views: [[false, 'form']],
target: 'new',
context: {},
});
}
}
ProxmoxServerListController.template = 'odoo_proxmox_manager.ProxmoxServerListView.Buttons';
export const proxmoxServerList = {
...listView,
Controller: ProxmoxServerListController,
};
registry.category("views").add("proxmox_server_list", proxmoxServerList);

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="odoo_proxmox_manager.ProxmoxServerListView.Buttons" t-inherit="web.ListView.Buttons" t-inherit-mode="primary" owl="1">
<xpath expr="//div[hasclass('o_list_buttons')]" position="inside">
<button type="button" class="btn btn-primary" t-on-click="onAddServer">
Ajouter un serveur
</button>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,51 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { Layout } from "@web/search/layout";
import { useService } from "@web/core/utils/hooks";
import { Component, onWillStart, useRef } from "@odoo/owl";
class ProxmoxDashboardController extends Component {
setup() {
this.actionService = useService("action");
this.orm = useService("orm");
this.serverData = {};
this.chartRefs = {};
onWillStart(async () => {
await this.fetchData();
});
}
async fetchData() {
const serverData = await this.orm.call(
'proxmox.server',
'get_dashboard_data',
[]
);
this.serverData = serverData;
this.render();
}
openServers() {
this.actionService.doAction('odoo_proxmox_manager.action_proxmox_server');
}
openClusters() {
this.actionService.doAction('odoo_proxmox_manager.action_proxmox_cluster');
}
openVMs() {
this.actionService.doAction('odoo_proxmox_manager.action_proxmox_vm');
}
}
ProxmoxDashboardController.template = 'odoo_proxmox_manager.DashboardView';
ProxmoxDashboardController.components = { Layout };
registry.category("actions").add("proxmox_dashboard_client_action", {
type: "ir.actions.client",
tag: "proxmox_dashboard",
target: "main",
Component: ProxmoxDashboardController,
});

View file

@ -0,0 +1,52 @@
/** @odoo-module **/
import { ListController } from "@web/views/list/list_controller";
import { registry } from "@web/core/registry";
import { listView } from "@web/views/list/list_view";
export class ProxmoxServerListController extends ListController {
setup() {
super.setup();
this.state = {};
}
/**
* @override
*/
async getLocalState() {
const state = await super.getLocalState();
return { ...state, ...this.state };
}
/**
* @override
*/
async setLocalState(state) {
if (state) {
this.state = { ...this.state, ...state };
await super.setLocalState(state);
}
}
async onAddServer() {
await this.env.services.action.doAction({
type: 'ir.actions.act_window',
res_model: 'proxmox.server.wizard',
view_mode: 'form',
views: [[false, 'form']],
target: 'new',
context: {},
}, {
onClose: () => this.actionService.restore(),
});
}
}
ProxmoxServerListController.template = 'odoo_proxmox_manager.ProxmoxServerListView';
export const ProxmoxServerListView = {
...listView,
Controller: ProxmoxServerListController,
};
registry.category("views").add("proxmox_server_list", ProxmoxServerListView);

View file

@ -0,0 +1,113 @@
.proxmox-dashboard {
padding: 1rem;
.dashboard-header {
margin-bottom: 2rem;
h1 {
color: #2b2b2b;
font-size: 2rem;
}
}
.dashboard-stats {
display: flex;
gap: 1.5rem;
margin-bottom: 2rem;
.stat-card {
flex: 1;
background: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
cursor: pointer;
transition: transform 0.2s;
&:hover {
transform: translateY(-2px);
}
.stat-value {
font-size: 2.5rem;
font-weight: bold;
color: #2b2b2b;
margin-bottom: 0.5rem;
}
.stat-label {
color: #666;
font-size: 1rem;
}
}
}
.dashboard-servers {
h2 {
color: #2b2b2b;
margin-bottom: 1rem;
}
.server-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
.server-card {
background: white;
border-radius: 8px;
padding: 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
.server-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
.server-name {
font-weight: bold;
font-size: 1.1rem;
}
.server-status {
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.9rem;
&.status-online {
background: #e6f4ea;
color: #1e8e3e;
}
&.status-offline {
background: #fce8e6;
color: #d93025;
}
}
}
.server-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
.stat {
text-align: center;
.label {
display: block;
color: #666;
font-size: 0.9rem;
margin-bottom: 0.25rem;
}
.value {
font-weight: bold;
color: #2b2b2b;
}
}
}
}
}
}
}

View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="odoo_proxmox_manager.DashboardView" owl="1">
<Layout>
<div class="proxmox-dashboard">
<div class="dashboard-header">
<h1>Proxmox Dashboard</h1>
</div>
<div class="dashboard-stats">
<div class="stat-card" t-on-click="openServers">
<div class="stat-value"><t t-esc="serverData.server_count || 0"/></div>
<div class="stat-label">Servers</div>
</div>
<div class="stat-card" t-on-click="openClusters">
<div class="stat-value"><t t-esc="serverData.cluster_count || 0"/></div>
<div class="stat-label">Clusters</div>
</div>
<div class="stat-card" t-on-click="openVMs">
<div class="stat-value"><t t-esc="serverData.vm_count || 0"/></div>
<div class="stat-label">Virtual Machines</div>
</div>
</div>
<div class="dashboard-servers">
<h2>Server Status</h2>
<div class="server-list">
<t t-foreach="serverData.servers || []" t-as="server" t-key="server.id">
<div class="server-card">
<div class="server-header">
<span class="server-name"><t t-esc="server.name"/></span>
<span t-attf-class="server-status status-#{server.state}">
<t t-esc="server.state"/>
</span>
</div>
<div class="server-stats">
<div class="stat">
<span class="label">VMs:</span>
<span class="value"><t t-esc="server.vm_count"/></span>
</div>
<div class="stat">
<span class="label">Memory:</span>
<span class="value"><t t-esc="server.memory_usage"/>%</span>
</div>
<div class="stat">
<span class="label">CPU:</span>
<span class="value"><t t-esc="server.cpu_usage"/>%</span>
</div>
</div>
</div>
</t>
</div>
</div>
</div>
</Layout>
</t>
</templates>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="odoo_proxmox_manager.ProxmoxServerListView" t-inherit="web.ListView" t-inherit-mode="primary" owl="1">
<xpath expr="//div[hasclass('o_control_panel_main_buttons')]" position="inside">
<button type="button" class="btn btn-primary" t-on-click="onAddServer">
Ajouter un serveur
</button>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Cluster Tree View -->
<record id="view_proxmox_cluster_tree" model="ir.ui.view">
<field name="name">proxmox.cluster.tree</field>
<field name="model">proxmox.cluster</field>
<field name="arch" type="xml">
<tree string="Proxmox Clusters" create="False">
<field name="name"/>
<field name="server_count"/>
<field name="active_servers"/>
<field name="total_vms"/>
</tree>
</field>
</record>
<!-- Cluster Form View -->
<record id="view_proxmox_cluster_form" model="ir.ui.view">
<field name="name">proxmox.cluster.form</field>
<field name="model">proxmox.cluster</field>
<field name="arch" type="xml">
<form string="Proxmox Cluster">
<header>
<button name="action_sync_all_servers" string="Sync All Servers" type="object" class="oe_highlight"/>
</header>
<sheet>
<div class="oe_title">
<h1>
<field name="name" placeholder="Cluster Name"/>
</h1>
</div>
<group>
<group>
<field name="description"/>
</group>
<group>
<field name="server_count"/>
<field name="active_servers"/>
<field name="total_vms"/>
</group>
</group>
<notebook>
<page string="Servers">
<field name="server_ids">
<tree>
<field name="name"/>
<field name="hostname"/>
<field name="state"/>
<field name="vm_count"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Cluster Search View -->
<record id="view_proxmox_cluster_search" model="ir.ui.view">
<field name="name">proxmox.cluster.search</field>
<field name="model">proxmox.cluster</field>
<field name="arch" type="xml">
<search string="Search Proxmox Clusters">
<field name="name"/>
<field name="description"/>
</search>
</field>
</record>
<!-- Cluster Action Window -->
<record id="action_proxmox_cluster" model="ir.actions.act_window">
<field name="name">Proxmox Clusters</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">proxmox.cluster</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="view_proxmox_cluster_search"/>
<field name="context">{'form_view_ref': 'odoo_proxmox_manager.view_proxmox_cluster_form'}</field>
<field name="domain">[]</field>
<field name="limit">80</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first Proxmox cluster!
</p>
</field>
</record>
</data>
</odoo>

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Main Menu -->
<menuitem id="menu_proxmox_root"
name="Proxmox"
web_icon="odoo_proxmox_manager,static/description/icon.png"
sequence="100"
groups="base.group_system"/>
<!-- Clusters Menu -->
<menuitem id="menu_proxmox_clusters"
name="Clusters"
parent="menu_proxmox_root"
action="action_proxmox_cluster"
sequence="10"/>
<!-- Servers Menu -->
<menuitem id="menu_proxmox_servers"
name="Servers"
parent="menu_proxmox_root"
action="action_proxmox_server"
sequence="20"/>
<!-- Virtual Machines Menu -->
<menuitem id="menu_proxmox_vms"
name="Virtual Machines"
parent="menu_proxmox_root"
action="action_proxmox_vm"
sequence="30"/>
</data>
</odoo>

View file

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Server Tree View -->
<record id="view_proxmox_server_tree" model="ir.ui.view">
<field name="name">proxmox.server.tree</field>
<field name="model">proxmox.server</field>
<field name="arch" type="xml">
<tree js_class="proxmox_server_list" string="Proxmox Servers">
<field name="name"/>
<field name="hostname"/>
<field name="cluster_id"/>
<field name="state"/>
<field name="vm_count"/>
</tree>
</field>
</record>
<!-- Server Form View -->
<record id="view_proxmox_server_form" model="ir.ui.view">
<field name="name">proxmox.server.form</field>
<field name="model">proxmox.server</field>
<field name="arch" type="xml">
<form string="Proxmox Server">
<header>
<button name="action_sync_vms" string="Sync VMs" type="object" class="oe_highlight"/>
</header>
<sheet>
<div class="oe_title">
<h1>
<field name="name" placeholder="Server Name"/>
</h1>
</div>
<group>
<group>
<field name="hostname"/>
<field name="port"/>
<field name="cluster_id"/>
<field name="state"/>
</group>
<group>
<field name="username"/>
<field name="token_name"/>
<field name="token_value" password="True"/>
<field name="verify_ssl"/>
</group>
</group>
<notebook>
<page string="Virtual Machines">
<field name="vm_ids">
<tree>
<field name="name"/>
<field name="vmid"/>
<field name="status"/>
<field name="memory"/>
<field name="cpus"/>
<field name="disk_size"/>
<field name="ip_address"/>
</tree>
</field>
</page>
<page string="Node Information">
<field name="node_info"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Server Search View -->
<record id="view_proxmox_server_search" model="ir.ui.view">
<field name="name">proxmox.server.search</field>
<field name="model">proxmox.server</field>
<field name="arch" type="xml">
<search string="Search Proxmox Servers">
<field name="name"/>
<field name="hostname"/>
<field name="cluster_id"/>
<separator/>
<filter string="Online" name="online" domain="[('state', '=', 'online')]"/>
<filter string="Offline" name="offline" domain="[('state', '=', 'offline')]"/>
<group expand="0" string="Group By">
<filter string="Cluster" name="cluster" context="{'group_by': 'cluster_id'}"/>
<filter string="Status" name="status" context="{'group_by': 'state'}"/>
</group>
</search>
</field>
</record>
<!-- Server Action Window -->
<record id="action_proxmox_server" model="ir.actions.act_window">
<field name="name">Proxmox Servers</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">proxmox.server</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="view_proxmox_server_search"/>
<field name="context">{'search_default_cluster': 1, 'form_view_ref': 'odoo_proxmox_manager.view_proxmox_server_form'}</field>
<field name="domain">[]</field>
<field name="limit">80</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first Proxmox server!
</p>
</field>
</record>
</data>
</odoo>

View file

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- VM Tree View -->
<record id="view_proxmox_vm_tree" model="ir.ui.view">
<field name="name">proxmox.vm.tree</field>
<field name="model">proxmox.vm</field>
<field name="arch" type="xml">
<tree string="Virtual Machines" create="False">
<field name="name"/>
<field name="vmid"/>
<field name="server_id"/>
<field name="status"/>
<field name="memory"/>
<field name="cpus"/>
<field name="disk_size"/>
<field name="ip_address"/>
</tree>
</field>
</record>
<!-- VM Form View -->
<record id="view_proxmox_vm_form" model="ir.ui.view">
<field name="name">proxmox.vm.form</field>
<field name="model">proxmox.vm</field>
<field name="arch" type="xml">
<form string="Virtual Machine">
<header>
<button name="action_start" string="Start" type="object"
invisible="status == 'running'"
class="oe_highlight"/>
<button name="action_stop" string="Stop" type="object"
invisible="status == 'stopped'"
class="btn-danger"/>
<button name="action_restart" string="Restart" type="object"
invisible="status == 'stopped'"
class="btn-warning"/>
<button name="action_suspend" string="Suspend" type="object"
invisible="status in ('suspended', 'stopped')"
class="btn-info"/>
<button name="action_resume" string="Resume" type="object"
invisible="status != 'suspended'"
class="oe_highlight"/>
<field name="status" widget="statusbar"/>
</header>
<sheet>
<div class="oe_title">
<h1>
<field name="name" placeholder="VM Name"/>
</h1>
</div>
<group>
<group>
<field name="vmid"/>
<field name="server_id"/>
<field name="ip_address"/>
</group>
<group>
<field name="memory"/>
<field name="cpus"/>
<field name="disk_size"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- VM Search View -->
<record id="view_proxmox_vm_search" model="ir.ui.view">
<field name="name">proxmox.vm.search</field>
<field name="model">proxmox.vm</field>
<field name="arch" type="xml">
<search string="Search Virtual Machines">
<field name="name"/>
<field name="vmid"/>
<field name="server_id"/>
<field name="ip_address"/>
<filter string="Running" name="running" domain="[('status', '=', 'running')]"/>
<filter string="Stopped" name="stopped" domain="[('status', '=', 'stopped')]"/>
<filter string="Suspended" name="suspended" domain="[('status', '=', 'suspended')]"/>
<group expand="0" string="Group By">
<filter string="Server" name="server" context="{'group_by': 'server_id'}"/>
<filter string="Status" name="status" context="{'group_by': 'status'}"/>
</group>
</search>
</field>
</record>
<!-- VM Action Window -->
<record id="action_proxmox_vm" model="ir.actions.act_window">
<field name="name">Virtual Machines</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">proxmox.vm</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="view_proxmox_vm_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No virtual machines found!
</p>
<p>
Virtual machines will be synchronized from your Proxmox servers.
</p>
</field>
</record>
</data>
</odoo>

View file

@ -0,0 +1 @@
from . import proxmox_server_wizard

View file

@ -0,0 +1,98 @@
from odoo import api, fields, models
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
class ProxmoxServerWizard(models.TransientModel):
_name = 'proxmox.server.wizard'
_description = 'Assistant de configuration serveur Proxmox'
name = fields.Char(string='Nom', required=True)
hostname = fields.Char(string='Nom d\'hôte/IP', required=True)
port = fields.Integer(string='Port', default=443)
username = fields.Char(string='Nom d\'utilisateur', required=True)
password = fields.Char(string='Mot de passe', required=True)
verify_ssl = fields.Boolean(string='Vérifier SSL', default=False)
def _get_proxmox_connection(self):
"""Établit une connexion à l'API Proxmox"""
base_url = f"https://{self.hostname}:{self.port}/api2/json"
try:
auth_response = requests.post(
f"{base_url}/access/ticket",
verify=self.verify_ssl,
data={
"username": self.username,
"password": self.password
}
)
auth_response.raise_for_status()
auth_data = auth_response.json()['data']
headers = {
'CSRFPreventionToken': auth_data['CSRFPreventionToken'],
'Cookie': f"PVEAuthCookie={auth_data['ticket']}"
}
return base_url, headers
except Exception as e:
raise ValueError(f"Erreur de connexion: {str(e)}")
def action_test_connection(self):
"""Teste la connexion et crée le serveur et le cluster si la connexion réussit"""
try:
base_url, headers = self._get_proxmox_connection()
# Vérifier si c'est un cluster
cluster_response = requests.get(
f"{base_url}/cluster/status",
headers=headers,
verify=self.verify_ssl
)
cluster_response.raise_for_status()
cluster_data = cluster_response.json()['data']
is_cluster = any(node['type'] == 'cluster' for node in cluster_data)
cluster = False
if is_cluster:
cluster_name = next((node['name'] for node in cluster_data if node['type'] == 'cluster'), 'Cluster Proxmox')
cluster = self.env['proxmox.cluster'].create({
'name': cluster_name,
})
# Créer le serveur principal
main_server = self.env['proxmox.server'].create({
'name': self.name,
'hostname': self.hostname,
'port': self.port,
'username': self.username,
'password': self.password,
'cluster_id': cluster.id if cluster else False,
})
# Créer les autres nœuds si c'est un cluster
if is_cluster:
nodes = [node for node in cluster_data if node['type'] == 'node' and node['name'] != self.hostname]
for node in nodes:
self.env['proxmox.server'].create({
'name': node['name'],
'hostname': node['name'],
'port': self.port,
'username': self.username,
'password': self.password,
'cluster_id': cluster.id,
})
return {
'type': 'ir.actions.act_window',
'res_model': 'proxmox.server',
'res_id': main_server.id,
'view_mode': 'form',
'view_type': 'form',
'views': [(False, 'form')],
'target': 'current',
'flags': {'mode': 'readonly'},
}
except Exception as e:
raise ValueError(f"Erreur lors du test de connexion: {str(e)}")

View file

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_proxmox_server_wizard_form" model="ir.ui.view">
<field name="name">proxmox.server.wizard.form</field>
<field name="model">proxmox.server.wizard</field>
<field name="arch" type="xml">
<form string="Configuration Serveur Proxmox">
<header>
<field name="state" widget="statusbar"/>
</header>
<sheet>
<group states="connect">
<group>
<field name="name" placeholder="Ex: Proxmox-1"/>
<field name="hostname" placeholder="Ex: proxmox.example.com"/>
<field name="port"/>
</group>
<group>
<field name="username" placeholder="Ex: root@pam"/>
<field name="password" password="True"/>
<field name="verify_ssl"/>
</group>
</group>
<group states="config" attrs="{'invisible': [('state', '!=', 'config')]}">
<group>
<field name="is_cluster"/>
<field name="cluster_name" attrs="{'invisible': [('is_cluster', '=', False)], 'required': [('is_cluster', '=', True)]}"/>
</group>
<group attrs="{'invisible': [('is_cluster', '=', False)]}">
<field name="other_nodes" nolabel="1" colspan="2">
<tree editable="bottom">
<field name="name"/>
<field name="hostname"/>
<field name="port"/>
<field name="username"/>
<field name="password" password="True"/>
</tree>
</field>
</group>
</group>
</sheet>
<footer>
<button name="action_test_connection" string="Tester la connexion" type="object"
class="btn-primary" states="connect"/>
<button name="action_create_server" string="Créer" type="object"
class="btn-primary" states="config"/>
<button string="Annuler" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_proxmox_server_wizard" model="ir.actions.act_window">
<field name="name">Ajouter un serveur Proxmox</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">proxmox.server.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<menuitem id="menu_proxmox_server_wizard"
name="Ajouter un serveur"
action="action_proxmox_server_wizard"
parent="menu_proxmox_server_root"
sequence="10"/>
</odoo>

View file

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import models

View file

@ -0,0 +1,19 @@
{
'name': 'Project Task Date Deadline Cascade Update',
'version': '17.0.0.1',
'category': 'Project',
'summary': 'Automatically update child task deadlines when parent task deadline changes.',
'description': """
This module ensures that when a parent task's deadline is updated, all child tasks' deadlines are automatically updated to match.
""",
'author': 'Your Name',
'license': 'AGPL-3',
'website': 'https://www.yourwebsite.com',
'depends': ['project'],
'data': [
'views/update_deadline_wizard_view.xml',
'wizard/update_deadline_wizard.xml',
],
'installable': True,
'auto_install': False,
}

View file

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import project_task

View file

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
from odoo.exceptions import UserError
from odoo import _
class ProjectTask(models.Model):
_inherit = 'project.task'
@api.model
def write(self, vals):
res = super(ProjectTask, self).write(vals)
if 'date_deadline' in vals:
for task in self:
if task.child_ids:
return {
'name': _('Update Deadline'),
'type': 'ir.actions.act_window',
'res_model': 'update.deadline.wizard',
'view_mode': 'form',
'target': 'new',
'context': {'active_ids': self.ids},
}
return res

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_update_deadline_wizard_form" model="ir.ui.view">
<field name="name">update.deadline.wizard.form</field>
<field name="model">update.deadline.wizard</field>
<field name="arch" type="xml">
<form string="Update Deadline">
<group>
<field name="apply_to_children"/>
</group>
<footer>
<button string="Apply" type="object" name="update_deadline" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_update_deadline_wizard" model="ir.actions.act_window">
<field name="name">Update Deadline</field>
<field name="res_model">update.deadline.wizard</field>
<field name="view_mode">form</field>
<field name="view_id" ref="view_update_deadline_wizard_form"/>
<field name="target">new</field>
</record>
</odoo>

View file

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import update_deadline_wizard

View file

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
class UpdateDeadlineWizard(models.TransientModel):
_name = 'update.deadline.wizard'
_description = 'Update Deadline Wizard'
apply_to_children = fields.Boolean(string='Apply to Child Tasks', default=False)
def update_deadline(self):
context = dict(self._context or {})
active_ids = context.get('active_ids', [])
tasks = self.env['project.task'].browse(active_ids)
for task in tasks:
if task.child_ids and self.apply_to_children:
task.child_ids.write({'date_deadline': task.date_deadline})