wip
This commit is contained in:
parent
45c1bdf440
commit
b1a625a0e4
45 changed files with 1911 additions and 0 deletions
1
interactive_discuss_ai/__init__.py
Normal file
1
interactive_discuss_ai/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import models
|
||||||
46
interactive_discuss_ai/__manifest__.py
Normal file
46
interactive_discuss_ai/__manifest__.py
Normal 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',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
2
interactive_discuss_ai/models/__init__.py
Normal file
2
interactive_discuss_ai/models/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
from . import ai_assistant
|
||||||
|
from . import ai_config
|
||||||
101
interactive_discuss_ai/models/ai_assistant.py
Normal file
101
interactive_discuss_ai/models/ai_assistant.py
Normal 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
|
||||||
61
interactive_discuss_ai/models/ai_config.py
Normal file
61
interactive_discuss_ai/models/ai_config.py
Normal 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')
|
||||||
62
interactive_discuss_ai/models/ai_service.py
Normal file
62
interactive_discuss_ai/models/ai_service.py
Normal 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', '')
|
||||||
3
interactive_discuss_ai/security/ir.model.access.csv
Normal file
3
interactive_discuss_ai/security/ir.model.access.csv
Normal 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
|
||||||
|
52
interactive_discuss_ai/static/src/js/ai_assistant_chat.js
Normal file
52
interactive_discuss_ai/static/src/js/ai_assistant_chat.js
Normal 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 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
12
interactive_discuss_ai/static/src/xml/ai_assistant_chat.xml
Normal file
12
interactive_discuss_ai/static/src/xml/ai_assistant_chat.xml
Normal 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>
|
||||||
86
interactive_discuss_ai/views/ai_assistant_views.xml
Normal file
86
interactive_discuss_ai/views/ai_assistant_views.xml
Normal 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="{"terminology": "archive"}"/>
|
||||||
|
</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>
|
||||||
83
interactive_discuss_ai/views/ai_config_views.xml
Normal file
83
interactive_discuss_ai/views/ai_config_views.xml
Normal 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>
|
||||||
90
odoo_proxmox_manager/README.md
Normal file
90
odoo_proxmox_manager/README.md
Normal 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
|
||||||
3
odoo_proxmox_manager/__init__.py
Normal file
3
odoo_proxmox_manager/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from . import models
|
||||||
|
from . import controllers
|
||||||
|
from . import wizard
|
||||||
35
odoo_proxmox_manager/__manifest__.py
Normal file
35
odoo_proxmox_manager/__manifest__.py
Normal 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
|
||||||
|
}
|
||||||
1
odoo_proxmox_manager/controllers/__init__.py
Normal file
1
odoo_proxmox_manager/controllers/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import main
|
||||||
9
odoo_proxmox_manager/controllers/main.py
Normal file
9
odoo_proxmox_manager/controllers/main.py
Normal 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()
|
||||||
3
odoo_proxmox_manager/models/__init__.py
Normal file
3
odoo_proxmox_manager/models/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from . import proxmox_server
|
||||||
|
from . import proxmox_cluster
|
||||||
|
from . import proxmox_vm
|
||||||
37
odoo_proxmox_manager/models/proxmox_cluster.py
Normal file
37
odoo_proxmox_manager/models/proxmox_cluster.py
Normal 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
|
||||||
164
odoo_proxmox_manager/models/proxmox_server.py
Normal file
164
odoo_proxmox_manager/models/proxmox_server.py
Normal 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,
|
||||||
|
}
|
||||||
87
odoo_proxmox_manager/models/proxmox_vm.py
Normal file
87
odoo_proxmox_manager/models/proxmox_vm.py
Normal 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
|
||||||
2
odoo_proxmox_manager/requirements.txt
Normal file
2
odoo_proxmox_manager/requirements.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
proxmoxer>=1.3.1
|
||||||
|
requests>=2.31.0
|
||||||
9
odoo_proxmox_manager/security/ir.model.access.csv
Normal file
9
odoo_proxmox_manager/security/ir.model.access.csv
Normal 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
|
||||||
|
39
odoo_proxmox_manager/security/proxmox_security.xml
Normal file
39
odoo_proxmox_manager/security/proxmox_security.xml
Normal 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>
|
||||||
BIN
odoo_proxmox_manager/static/description/icon.png
Normal file
BIN
odoo_proxmox_manager/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 833 B |
|
|
@ -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);
|
||||||
|
|
@ -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>
|
||||||
51
odoo_proxmox_manager/static/src/js/dashboard_view.js
Normal file
51
odoo_proxmox_manager/static/src/js/dashboard_view.js
Normal 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,
|
||||||
|
});
|
||||||
52
odoo_proxmox_manager/static/src/js/proxmox_server_list.js
Normal file
52
odoo_proxmox_manager/static/src/js/proxmox_server_list.js
Normal 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);
|
||||||
113
odoo_proxmox_manager/static/src/scss/dashboard.scss
Normal file
113
odoo_proxmox_manager/static/src/scss/dashboard.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
57
odoo_proxmox_manager/static/src/xml/dashboard.xml
Normal file
57
odoo_proxmox_manager/static/src/xml/dashboard.xml
Normal 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>
|
||||||
10
odoo_proxmox_manager/static/src/xml/proxmox_server_list.xml
Normal file
10
odoo_proxmox_manager/static/src/xml/proxmox_server_list.xml
Normal 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>
|
||||||
89
odoo_proxmox_manager/views/proxmox_cluster_views.xml
Normal file
89
odoo_proxmox_manager/views/proxmox_cluster_views.xml
Normal 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>
|
||||||
32
odoo_proxmox_manager/views/proxmox_menus.xml
Normal file
32
odoo_proxmox_manager/views/proxmox_menus.xml
Normal 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>
|
||||||
108
odoo_proxmox_manager/views/proxmox_server_views.xml
Normal file
108
odoo_proxmox_manager/views/proxmox_server_views.xml
Normal 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>
|
||||||
107
odoo_proxmox_manager/views/proxmox_vm_views.xml
Normal file
107
odoo_proxmox_manager/views/proxmox_vm_views.xml
Normal 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>
|
||||||
1
odoo_proxmox_manager/wizard/__init__.py
Normal file
1
odoo_proxmox_manager/wizard/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import proxmox_server_wizard
|
||||||
98
odoo_proxmox_manager/wizard/proxmox_server_wizard.py
Normal file
98
odoo_proxmox_manager/wizard/proxmox_server_wizard.py
Normal 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)}")
|
||||||
67
odoo_proxmox_manager/wizard/proxmox_server_wizard_views.xml
Normal file
67
odoo_proxmox_manager/wizard/proxmox_server_wizard_views.xml
Normal 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>
|
||||||
3
project_task_date_deadline_cascade_update/__init__.py
Normal file
3
project_task_date_deadline_cascade_update/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import models
|
||||||
19
project_task_date_deadline_cascade_update/__manifest__.py
Normal file
19
project_task_date_deadline_cascade_update/__manifest__.py
Normal 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,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import project_task
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import update_deadline_wizard
|
||||||
|
|
@ -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})
|
||||||
Loading…
Reference in a new issue