wip
This commit is contained in:
parent
c8bbeac919
commit
7783b04e73
33 changed files with 550 additions and 95 deletions
2
chatgpt_assistant/__init__.py
Normal file
2
chatgpt_assistant/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
from . import controllers
|
||||||
20
chatgpt_assistant/__manifest__.py
Normal file
20
chatgpt_assistant/__manifest__.py
Normal 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',
|
||||||
|
}
|
||||||
1
chatgpt_assistant/controllers/__init__.py
Normal file
1
chatgpt_assistant/controllers/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import main
|
||||||
23
chatgpt_assistant/controllers/main.py
Normal file
23
chatgpt_assistant/controllers/main.py
Normal 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']}
|
||||||
28
chatgpt_assistant/static/src/js/discuss_extension.js
Normal file
28
chatgpt_assistant/static/src/js/discuss_extension.js
Normal 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.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
8
chatgpt_assistant/views/assets.xml
Normal file
8
chatgpt_assistant/views/assets.xml
Normal 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>
|
||||||
2
odoo_unifi_manager/__init__.py
Normal file
2
odoo_unifi_manager/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
from . import models
|
||||||
|
from . import wizard
|
||||||
19
odoo_unifi_manager/__manifest__.py
Normal file
19
odoo_unifi_manager/__manifest__.py
Normal 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,
|
||||||
|
}
|
||||||
2
odoo_unifi_manager/models/__init__.py
Normal file
2
odoo_unifi_manager/models/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
from . import firewall_rule
|
||||||
|
from . import firewall_rule_template
|
||||||
43
odoo_unifi_manager/models/firewall_rule.py
Normal file
43
odoo_unifi_manager/models/firewall_rule.py
Normal 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)"
|
||||||
|
)
|
||||||
42
odoo_unifi_manager/models/firewall_rule_template.py
Normal file
42
odoo_unifi_manager/models/firewall_rule_template.py
Normal 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)"
|
||||||
|
)
|
||||||
4
odoo_unifi_manager/security/ir.model.access.csv
Normal file
4
odoo_unifi_manager/security/ir.model.access.csv
Normal 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
|
||||||
|
23
odoo_unifi_manager/views/firewall_rule_template_views.xml
Normal file
23
odoo_unifi_manager/views/firewall_rule_template_views.xml
Normal 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>
|
||||||
16
odoo_unifi_manager/views/firewall_rule_views.xml
Normal file
16
odoo_unifi_manager/views/firewall_rule_views.xml
Normal 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>
|
||||||
32
odoo_unifi_manager/views/firewall_rule_wizard_action.xml
Normal file
32
odoo_unifi_manager/views/firewall_rule_wizard_action.xml
Normal 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>
|
||||||
5
odoo_unifi_manager/views/unify_menu.xml
Normal file
5
odoo_unifi_manager/views/unify_menu.xml
Normal 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>
|
||||||
1
odoo_unifi_manager/wizard/__init__.py
Normal file
1
odoo_unifi_manager/wizard/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import firewall_rule_wizard
|
||||||
85
odoo_unifi_manager/wizard/firewall_rule_wizard.py
Normal file
85
odoo_unifi_manager/wizard/firewall_rule_wizard.py
Normal 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).")
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
|
from . import controlers
|
||||||
from . import models
|
from . import models
|
||||||
from . import wizard
|
from . import wizard
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
'data/queue_job_group.xml',
|
'data/queue_job_group.xml',
|
||||||
'views/res_config_settings_view.xml',
|
'views/res_config_settings_view.xml',
|
||||||
'views/res_partner_view.xml',
|
'views/res_partner_view.xml',
|
||||||
|
'views/sale_analysis_templates.xml',
|
||||||
'wizard/partner_purchase_analysis_wizard_view.xml',
|
'wizard/partner_purchase_analysis_wizard_view.xml',
|
||||||
],
|
],
|
||||||
'external_dependencies': {
|
'external_dependencies': {
|
||||||
|
|
|
||||||
1
openai_partner_purchase_analysis/controlers/__init__.py
Normal file
1
openai_partner_purchase_analysis/controlers/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import sale_analysis_controller
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -5,6 +5,23 @@ class ResPartner(models.Model):
|
||||||
|
|
||||||
sale_analysis = fields.Html("Sale Analysis", help="Analysis of the partner's sales.")
|
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_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):
|
def action_open_purchase_analysis(self):
|
||||||
"""Ouvre le wizard d'analyse des achats pour le client actuel."""
|
"""Ouvre le wizard d'analyse des achats pour le client actuel."""
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@
|
||||||
<notebook position="inside">
|
<notebook position="inside">
|
||||||
<page string="Sales Analysis">
|
<page string="Sales Analysis">
|
||||||
<group>
|
<group>
|
||||||
<field name="sale_analysis" widget="html" placeholder="No analysis available"/>
|
<!-- Affichage direct du champ HTML -->
|
||||||
<field name="sale_analysis_date" placeholder="Date of analysis"/>
|
<field name="sale_analysis_iframe" widget="html" readonly="1"/>
|
||||||
</group>
|
</group>
|
||||||
</page>
|
</page>
|
||||||
</notebook>
|
</notebook>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<odoo>
|
||||||
|
<template id="template_sale_analysis" name="Sale Analysis Template">
|
||||||
|
<t t-raw="content"/>
|
||||||
|
</template>
|
||||||
|
</odoo>
|
||||||
17
repos_module_linker/__manifest__.py
Normal file
17
repos_module_linker/__manifest__.py
Normal 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',
|
||||||
|
}
|
||||||
1
repos_module_linker/models/__init__.py
Normal file
1
repos_module_linker/models/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import ir_module_extension
|
||||||
75
repos_module_linker/models/ir_module_extension.py
Normal file
75
repos_module_linker/models/ir_module_extension.py
Normal 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}.")
|
||||||
2
repos_module_linker/security/ir.model.access.csv
Normal file
2
repos_module_linker/security/ir.model.access.csv
Normal 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
|
||||||
|
15
repos_module_linker/views/module_filter_view.xml
Normal file
15
repos_module_linker/views/module_filter_view.xml
Normal 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>
|
||||||
17
repos_module_linker/views/module_menu.xml
Normal file
17
repos_module_linker/views/module_menu.xml
Normal 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>
|
||||||
26
repos_module_linker/views/module_view.xml
Normal file
26
repos_module_linker/views/module_view.xml
Normal 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>
|
||||||
Loading…
Reference in a new issue