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 wizard
|
||||
|
|
@ -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': {
|
||||
|
|
|
|||
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_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."""
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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