This commit is contained in:
xtremxpert 2024-12-02 15:12:08 -05:00
parent c8bbeac919
commit 7783b04e73
33 changed files with 550 additions and 95 deletions

View file

@ -0,0 +1,2 @@
from . import controllers

View file

@ -0,0 +1,20 @@
{
'name': 'ChatGPT Assistant for Discuss',
'version': '1.0',
'author': 'Bemade Inc.',
'category': 'Tools',
'summary': 'Integrate ChatGPT into Odoo Discuss',
'depends': ['mail'],
'data': [
# 'views/assets.xml',
],
'assets': {
'web.assets_backend': [
# '/chatgpt_assistant/static/src/js/discuss_extension.js',
],
},
'installable': True,
'application': False,
'license': 'LGPL-3',
}

View file

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

View file

@ -0,0 +1,23 @@
from odoo import http
from odoo.http import request
import openai
import pandas as pd
class DiscussChatGPT(http.Controller):
@http.route('/discuss/chatgpt', type='json', auth='user')
def chatgpt_respond(self, message, attachment_id=None):
openai.api_key = request.env['ir.config_parameter'].sudo().get_param('chatgpt.api_key')
if attachment_id:
attachment = request.env['ir.attachment'].sudo().browse(attachment_id)
if attachment.mimetype == 'text/csv':
data = pd.read_csv(attachment._full_path(attachment.store_fname))
processed_data = data.describe().to_string()
return {'response': f"Analyse des données : \n{processed_data}"}
response = openai.ChatCompletion.create(
model="gpt-4",
messages=[{"role": "user", "content": message}]
)
return {'response': response['choices'][0]['message']['content']}

View file

@ -0,0 +1,28 @@
odoo.define('chatgpt_assistant.discuss_extension', [], function (Discuss, utils) {
"use strict";
const { patch } = utils;
patch(Discuss.prototype, 'chatgpt_assistant.discuss_extension', {
async _onChatGPTSendMessage(event) {
const message = this.inputValue || '';
if (!message.trim()) {
return;
}
try {
const result = await this.env.services.rpc({
route: '/discuss/chatgpt',
params: { message },
});
if (result.response) {
this._insertMessage(result.response);
}
} catch (error) {
console.error('Erreur lors de l\'appel à ChatGPT:', error);
this._insertMessage('Erreur : Impossible de contacter ChatGPT.');
}
},
});
});

View file

@ -0,0 +1,8 @@
<odoo>
<template id="assets_backend" name="chatgpt assistant assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/chatgpt_assistant/static/src/js/discuss_extension.js"></script>
</xpath>
</template>
</odoo>

View file

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

View file

@ -0,0 +1,19 @@
{
'name': 'Odoo Unifi Manager',
'version': '1.0',
'author': 'Bemade Inc.',
'website': 'https://www.bemade.org',
'license': 'LGPL-3',
'depends': [
'base'
],
'data': [
'views/firewall_rule_wizard_action.xml',
'views/firewall_rule_views.xml',
'views/firewall_rule_template_views.xml',
# 'views/unify_menu.xml',
'security/ir.model.access.csv'
],
'installable': True,
'application': True,
}

View file

@ -0,0 +1,2 @@
from . import firewall_rule
from . import firewall_rule_template

View file

@ -0,0 +1,43 @@
from odoo import models, fields, api
class FirewallRule(models.Model):
_name = 'unifi.firewall.rule'
_description = 'Unifi Firewall Rule'
name = fields.Char(
string='Rule Name',
required=True
)
action = fields.Selection(
selection=[
('accept', 'Accept'),
('drop', 'Drop')
],
string='Action',
required=True
)
src_ip = fields.Char(
string='Source IP',
help="Source IP address (can be left empty)"
)
dst_ip = fields.Char(
string='Destination IP',
help="Destination IP address (can be left empty)"
)
protocol = fields.Selection(
selection=[
('tcp', 'TCP'),
('udp', 'UDP')
],
string='Protocol',
required=True
)
port = fields.Char(
string='Port',
help="Port or port range (e.g., 80 or 8000-9000)"
)

View file

@ -0,0 +1,42 @@
from odoo import models, fields
class FirewallRuleTemplate(models.Model):
_name = 'unifi.firewall.rule.template'
_description = 'Firewall Rule Template'
name = fields.Char(
string='Template Name',
required=True
)
action = fields.Selection(
selection=[
('accept', 'Accept'),
('drop', 'Drop')
],
string='Action',
required=True
)
src_ip = fields.Char(
string='Source IP',
help="Source IP address (can be left empty)"
)
dst_ip = fields.Char(
string='Destination IP',
help="Destination IP address (can be left empty)"
)
protocol = fields.Selection(
selection=[
('tcp', 'TCP'),
('udp', 'UDP')
],
string='Protocol',
required=True)
port = fields.Char(
string='Port',
help="Port or port range (e.g., 80 or 8000-9000)"
)

View file

@ -0,0 +1,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
odoo_unifi_manager.access_unifi_firewall_rule,access_unifi_firewall_rule,odoo_unifi_manager.model_unifi_firewall_rule,base.group_user,1,0,0,0
odoo_unifi_manager.access_unifi_firewall_rule_template,access_unifi_firewall_rule_template,odoo_unifi_manager.model_unifi_firewall_rule_template,base.group_user,1,0,0,0
odoo_unifi_manager.access_unifi_firewall_rule_wizard,access_unifi_firewall_rule_wizard,odoo_unifi_manager.model_unifi_firewall_rule_wizard,base.group_user,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 odoo_unifi_manager.access_unifi_firewall_rule access_unifi_firewall_rule odoo_unifi_manager.model_unifi_firewall_rule base.group_user 1 0 0 0
3 odoo_unifi_manager.access_unifi_firewall_rule_template access_unifi_firewall_rule_template odoo_unifi_manager.model_unifi_firewall_rule_template base.group_user 1 0 0 0
4 odoo_unifi_manager.access_unifi_firewall_rule_wizard access_unifi_firewall_rule_wizard odoo_unifi_manager.model_unifi_firewall_rule_wizard base.group_user 1 0 0 0

View file

@ -0,0 +1,23 @@
<odoo>
<record id="view_firewall_rule_template_form" model="ir.ui.view">
<field name="name">firewall.rule.template.form</field>
<field name="model">unifi.firewall.rule.template</field>
<field name="arch" type="xml">
<form string="Firewall Rule Template">
<sheet>
<group>
<field name="name"/>
<field name="action"/>
<field name="src_ip"/>
<field name="dst_ip"/>
<field name="protocol"/>
<field name="port"/>
</group>
<footer>
<button name="%(action_open_firewall_rule_wizard)d" type="action" string="Create Rule" class="btn-primary"/>
</footer>
</sheet>
</form>
</field>
</record>
</odoo>

View file

@ -0,0 +1,16 @@
<odoo>
<record id="view_firewall_rule_tree" model="ir.ui.view">
<field name="name">firewall.rule.tree</field>
<field name="model">unifi.firewall.rule</field>
<field name="arch" type="xml">
<tree string="Firewall Rules">
<field name="name"/>
<field name="action"/>
<field name="src_ip"/>
<field name="dst_ip"/>
<field name="protocol"/>
<field name="port"/>
</tree>
</field>
</record>
</odoo>

View file

@ -0,0 +1,32 @@
<odoo>
<record id="action_open_firewall_rule_wizard" model="ir.actions.act_window">
<field name="name">Create Firewall Rule</field>
<field name="res_model">unifi.firewall.rule.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="context">{'default_template_id': active_id}</field>
</record>
<!-- Action pour afficher les Firewall Rules -->
<record id="action_firewall_rules" model="ir.actions.act_window">
<field name="name">Firewall Rules</field>
<field name="res_model">unifi.firewall.rule</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Configure and manage your firewall rules here.
</p>
</field>
</record>
<!-- Action pour afficher les Templates -->
<record id="action_firewall_templates" model="ir.actions.act_window">
<field name="name">Firewall Templates</field>
<field name="res_model">unifi.firewall.rule.template</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create and manage reusable firewall templates here.
</p>
</field>
</record>
</odoo>

View file

@ -0,0 +1,5 @@
<odoo>
<menuitem id="menu_unifi_root" name="Unifi" sequence="10" app="True" />
<menuitem id="menu_unifi_firewall_rules" name="Firewall Rules" parent="menu_unifi_root" action="action_firewall_rules" />
<menuitem id="menu_unifi_firewall_templates" name="Templates" parent="menu_unifi_root" action="action_firewall_templates" />
</odoo>

View file

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

View file

@ -0,0 +1,85 @@
from odoo import models, fields, api
from odoo.exceptions import ValidationError
class FirewallRuleWizard(models.TransientModel):
_name = 'unifi.firewall.rule.wizard'
_description = 'Wizard to Create Firewall Rule from Template'
template_id = fields.Many2one(
'unifi.firewall.rule.template',
string='Template',
required=True,
help="Select the template to create a firewall rule."
)
name = fields.Char(
string='Name of the new firewall rule',
required=True
)
action = fields.Selection(
selection=[
('accept', 'Accept'),
('drop', 'Drop')
],
string='Action',
required=True
)
src_ip = fields.Char(
string='Source IP',
help="Source IP address (can be left empty)"
)
dst_ip = fields.Char(
string='Destination IP',
help="Destination IP address (can be left empty)"
)
protocol = fields.Selection(
selection=[
('tcp', 'TCP'),
('udp', 'UDP')
],
string='Protocol',
required=True
)
port = fields.Char(
string='Port',
help="Port or port range (e.g., 80 or 8000-9000)"
)
def create_firewall_rule(self):
self.ensure_one()
template = self.template_id
self.env['unifi.firewall.rule'].create({
'name': self.name,
'action': template.action,
'src_ip': template.src_ip,
'dst_ip': template.dst_ip,
'protocol': template.protocol,
'port': template.port,
})
@api.onchange('template_id')
def _onchange_template_id(self):
"""Apply template values only if they are not empty."""
if self.template_id:
# Appliquer les valeurs du modèle uniquement si elles ne sont pas vides
if self.template_id.action:
self.action = self.template_id.action
if self.template_id.src_ip:
self.src_ip = self.template_id.src_ip
if self.template_id.dst_ip:
self.dst_ip = self.template_id.dst_ip
if self.template_id.protocol:
self.protocol = self.template_id.protocol
if self.template_id.port:
self.port = self.template_id.port
@api.constrains('port')
def _check_port(self):
if self.port:
if not self.port.isdigit() and '-' not in self.port:
raise ValidationError("Port must be a number or a valid range (e.g., 80 or 8000-9000).")

View file

@ -1,2 +1,3 @@
from . import controlers
from . import models
from . import wizard

View file

@ -21,6 +21,7 @@
'data/queue_job_group.xml',
'views/res_config_settings_view.xml',
'views/res_partner_view.xml',
'views/sale_analysis_templates.xml',
'wizard/partner_purchase_analysis_wizard_view.xml',
],
'external_dependencies': {

View file

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

View file

@ -0,0 +1,14 @@
from odoo import http
from odoo.http import request
class SaleAnalysisController(http.Controller):
@http.route('/sale_analysis/render/<int:partner_id>', type='http', auth='user', csrf=False)
def render_sale_analysis(self, partner_id, **kwargs):
# Récupérer le partenaire et son contenu d'analyse
partner = request.env['res.partner'].sudo().browse(partner_id)
if not partner:
return request.not_found()
# Générer le rendu HTML avec les scripts inclus
sale_analysis_content = partner.sale_analysis or "<p>No analysis available</p>"
return http.Response(sale_analysis_content, content_type='text/html', status=200)

View file

@ -1,93 +0,0 @@
from odoo import models, fields, api, _
from odoo.exceptions import UserError
import openai
try:
from odoo.addons.queue_job.job import job
except ImportError:
job = None # Si queue_job n'est pas disponible, job reste None
class PartnerPurchaseAnalysisWizard(models.TransientModel):
_name = 'partner.purchase.analysis.wizard'
_description = 'Wizard for Partner Purchase Analysis with OpenAI'
partner_id = fields.Many2one('res.partner', string="Customer", required=True, readonly=True)
date_start = fields.Date(string="Start Date")
date_end = fields.Date(string="End Date")
analysis_result = fields.Text(string="Analysis Result", readonly=True)
def start_analysis(self):
use_queue = self.env['ir.config_parameter'].sudo().get_param('my_module.use_queue_job')
if use_queue and job:
return self.with_delay().perform_analysis()
else:
return self.perform_analysis()
def perform_analysis(self):
"""Effectue l'analyse des ventes pour le partenaire sélectionné."""
selected_categories = self.env.company.product_categories_analyzed
if selected_categories:
category_ids = selected_categories.ids
domain = [
('order_id.partner_id', '=', self.partner_id.id),
('product_id.categ_id', 'child_of', category_ids),
]
else:
domain = [('order_id.partner_id', '=', self.partner_id.id)]
if self.date_start:
domain.append(('order_id.date_order', '>=', self.date_start))
if self.date_end:
domain.append(('order_id.date_order', '<=', self.date_end))
sale_order_lines = self.env['sale.order.line'].search(domain)
if not sale_order_lines:
raise UserError(_("No relevant purchase history found for this customer based on the selected filters."))
# Préparation des données pour l'API d'OpenAI
purchase_data = {}
for line in sale_order_lines:
category_name = line.product_id.categ_id.name or "Uncategorized"
if category_name not in purchase_data:
purchase_data[category_name] = {}
product_name = line.product_id.display_name
if product_name not in purchase_data[category_name]:
purchase_data[category_name][product_name] = []
purchase_data[category_name][product_name].append({
'date': line.order_id.date_order,
'quantity': line.product_uom_qty,
'unit_price': line.price_unit,
})
# Construire le prompt pour OpenAI
user_lang = self.env.user.lang or 'en_US'
user_lang_name = self.env['res.lang'].search([('code', '=', user_lang)], limit=1).name or "English"
purchase_details = f"Customer Purchase Analysis Grouped by Product Category and Product (Response in {user_lang_name}):\n\n"
for category, products in purchase_data.items():
purchase_details += f"Category: {category}\n"
for product, entries in products.items():
purchase_details += f" Product: {product}\n"
for entry in entries:
purchase_details += (
f" - Date: {entry['date']}, "
f"Qty: {entry['quantity']}, "
f"U.Price: {entry['unit_price']}\n"
)
purchase_details += "\n"
purchase_details += "\n"
try:
response = openai.ChatCompletion.create(
model="gpt-4",
messages=[
{"role": "system",
"content": f"Analyze the following customer purchase history. Identify trends, product category preferences, and any significant deviations. Respond in {user_lang_name}."},
{"role": "user", "content": purchase_details}
]
)
self.analysis_result = response.choices[0].message['content']
except Exception as e:
raise UserError(_("Failed to get response from OpenAI. Error: %s") % str(e))

View file

@ -5,6 +5,23 @@ class ResPartner(models.Model):
sale_analysis = fields.Html("Sale Analysis", help="Analysis of the partner's sales.")
sale_analysis_date = fields.Date("Sale Analysis Date", help="Date of the latest sale analysis.")
sale_analysis_url = fields.Char(string="Sale Analysis URL", compute="_compute_sale_analysis_url")
sale_analysis_iframe = fields.Html(string="Sale Analysis Iframe", compute="_compute_sale_analysis_iframe")
def _compute_sale_analysis_iframe(self):
for partner in self:
# Construction de l'iframe directement dans le champ HTML
partner.sale_analysis_iframe = f"""
<iframe
src="/sale_analysis/render/{partner.id}"
width="100%" height="600px" frameborder="0">
</iframe>
"""
def _compute_sale_analysis_url(self):
for partner in self:
partner.sale_analysis_url = f'/sale_analysis/render/{partner.id}'
def action_open_purchase_analysis(self):
"""Ouvre le wizard d'analyse des achats pour le client actuel."""

View file

@ -7,8 +7,8 @@
<notebook position="inside">
<page string="Sales Analysis">
<group>
<field name="sale_analysis" widget="html" placeholder="No analysis available"/>
<field name="sale_analysis_date" placeholder="Date of analysis"/>
<!-- Affichage direct du champ HTML -->
<field name="sale_analysis_iframe" widget="html" readonly="1"/>
</group>
</page>
</notebook>

View file

@ -0,0 +1,5 @@
<odoo>
<template id="template_sale_analysis" name="Sale Analysis Template">
<t t-raw="content"/>
</template>
</odoo>

View file

@ -0,0 +1,17 @@
{
'name': 'Extended Module Manager',
'version': '1.1',
'summary': 'Gérer les modules via liens symboliques directement depuis ir.modules',
'category': 'Tools',
'author': 'Benoît Vézina',
'depends': ['base'],
'data': [
# 'views/module_filter_view.xml',
# 'views/module_menu.xml',
'views/module_view.xml',
'security/ir.model.access.csv',
],
'installable': True,
'application': False,
'license': 'LGPL-3',
}

View file

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

View file

@ -0,0 +1,75 @@
import os
import logging
from odoo import models, fields, api
_logger = logging.getLogger(__name__)
class ModuleExtension(models.Model):
_inherit = 'ir.module.module'
state = fields.Selection(
selection_add=[
('linkable', 'Linkable'),
('linked', 'Linked'),
],
ondelete={
'linkable': 'set default',
'linked': 'cascade',
},
)
@staticmethod
def _get_base_paths():
"""Obtenir les chemins de base pour les dépôts et les addons"""
cwd = os.getcwd()
repos_path = os.path.join(cwd, '.repos')
addons_path = os.path.join(cwd, 'addons')
return repos_path, addons_path
@api.model
def scan_modules(self):
"""Scanner les dépôts dans .repos et mettre à jour les statuts des modules."""
repos_path, addons_path = self._get_base_paths()
if not os.path.exists(repos_path):
_logger.error(f"Le répertoire {repos_path} n'existe pas.")
return
existing_modules = self.search([]).mapped('name')
for repo in os.listdir(repos_path):
repo_path = os.path.join(repos_path, repo)
if os.path.isdir(repo_path):
for module in os.listdir(repo_path):
module_path = os.path.join(repo_path, module)
if os.path.isdir(module_path):
module_record = self.search([('name', '=', module)], limit=1)
if module_record:
if module_record.state == 'uninstalled':
module_record.write({'state': 'linkable'})
else:
self.create({
'name': module,
'state': 'linkable',
'repo_path': module_path,
})
def toggle_link(self):
"""Créer ou supprimer le lien symbolique pour un module."""
repos_path, addons_path = self._get_base_paths()
for module in self:
symlink_path = os.path.join(addons_path, module.name)
if module.state == 'linkable':
# Créer un lien symbolique et marquer comme 'linked'
if os.path.exists(module.repo_path):
os.symlink(module.repo_path, symlink_path)
module.write({'state': 'linked'})
_logger.info(f"Lien symbolique créé pour le module {module.name}.")
else:
_logger.error(f"Le chemin {module.repo_path} n'existe pas.")
elif module.state == 'linked':
# Supprimer le lien symbolique et repasser en 'linkable'
if os.path.islink(symlink_path):
os.unlink(symlink_path)
module.write({'state': 'linkable'})
_logger.info(f"Lien symbolique supprimé pour le module {module.name}.")

View file

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_module_manager,Access to Module Manager,base.model_ir_module_module,,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_module_manager Access to Module Manager base.model_ir_module_module 1 1 1 1

View file

@ -0,0 +1,15 @@
<odoo>
<record id="view_module_tree_filter_inherit" model="ir.ui.view">
<field name="name">ir.module.module.tree.filter.inherit</field>
<field name="model">ir.module.module</field>
<field name="inherit_id" ref="base.module_tree"/>
<field name="arch" type="xml">
<xpath expr="//filter[@name='state']" position="after">
<!-- Nouveau filtre pour Linked -->
<filter name="linked" string="Linked" domain="[('state', '=', 'linked')]"/>
<!-- Nouveau filtre pour Linkable -->
<filter name="linkable" string="Linkable" domain="[('state', '=', 'linkable')]"/>
</xpath>
</field>
</record>
</odoo>

View file

@ -0,0 +1,17 @@
<odoo>
<record id="menu_update_linkable_list" model="ir.actions.server">
<field name="name">Update Linkable List</field>
<field name="model_id" ref="base.model_ir_module_module"/>
<field name="state">code</field>
<field name="code">
action = env['ir.module.module'].scan_modules()
</field>
</record>
<record id="menu_apps_update_linkable_list" model="ir.ui.menu">
<field name="name">Update Linkable List</field>
<field name="parent_id" ref="base.menu_apps"/>
<field name="action" ref="menu_update_linkable_list"/>
<field name="sequence" eval="17"/>
</record>
</odoo>

View file

@ -0,0 +1,26 @@
<odoo>
<record id="view_module_tree_inherit" model="ir.ui.view">
<field name="name">ir.module.module.tree.inherit</field>
<field name="model">ir.module.module</field>
<field name="inherit_id" ref="base.module_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='state']" position="after">
<button string="Link/Unlink" type="object" name="toggle_link" attrs="{'invisible': [('state', 'not in', ['linkable', 'linked'])]}"/>
</xpath>
</field>
</record>
<record id="view_module_form_inherit" model="ir.ui.view">
<field name="name">ir.module.module.form.inherit</field>
<field name="model">ir.module.module</field>
<field name="inherit_id" ref="base.view_module_form"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='module_settings']" position="after">
<group string="Lien Symbolique">
<field name="state"/>
<button string="Link/Unlink" type="object" name="toggle_link" attrs="{'invisible': [('state', 'not in', ['linkable', 'linked'])]}"/>
</group>
</xpath>
</field>
</record>
</odoo>