new modules openai
This commit is contained in:
parent
e7fd3c3910
commit
4163232371
28 changed files with 771 additions and 0 deletions
2
customer_itch_cycle/__init__.py
Normal file
2
customer_itch_cycle/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
from . import models
|
||||
15
customer_itch_cycle/__manifest__.py
Normal file
15
customer_itch_cycle/__manifest__.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
{
|
||||
'name': 'Customer Itch Cycle Management',
|
||||
'version': '1.0',
|
||||
'depends': ['base', 'sale'],
|
||||
'author': 'Your Name',
|
||||
'category': 'Sales Management',
|
||||
'description': "Manage customer itch cycles by product for proactive sales engagement.",
|
||||
'data': [
|
||||
'views/itch_cycle_product_partner_view.xml',
|
||||
'views/res_partner_view.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
}
|
||||
3
customer_itch_cycle/models/__init__.py
Normal file
3
customer_itch_cycle/models/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
from . import itch_cycle_product_partner
|
||||
from . import sale_order
|
||||
33
customer_itch_cycle/models/itch_cycle_product_partner.py
Normal file
33
customer_itch_cycle/models/itch_cycle_product_partner.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
|
||||
from odoo import models, fields, api
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
class ItchCycleProductPartner(models.Model):
|
||||
_name = 'itch_cycle_product_partner'
|
||||
_description = 'Itch Cycle by Product and Partner'
|
||||
|
||||
partner_id = fields.Many2one('res.partner', string="Client", required=True, ondelete='cascade')
|
||||
product_id = fields.Many2one('product.product', string="Produit", required=True)
|
||||
last_purchase_date = fields.Date(string="Dernière Date d'Achat")
|
||||
itch_cycle_duration = fields.Integer(string="Durée du Itch-Cycle (jours)", default=0)
|
||||
next_follow_up_date = fields.Date(string="Prochaine Date de Suivi", compute="_compute_next_follow_up_date", store=True)
|
||||
|
||||
@api.depends('last_purchase_date', 'itch_cycle_duration')
|
||||
def _compute_next_follow_up_date(self):
|
||||
for record in self:
|
||||
if record.last_purchase_date and record.itch_cycle_duration > 0:
|
||||
record.next_follow_up_date = record.last_purchase_date + timedelta(days=record.itch_cycle_duration)
|
||||
else:
|
||||
record.next_follow_up_date = False
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
itch_cycle_product_ids = fields.One2many('itch_cycle_product_partner', 'partner_id', string="Itch Cycles Produits")
|
||||
itch_next_delay = fields.Date(string="Prochaine Date de Suivi (Itch-Cycle Min)", compute="_compute_itch_next_delay", store=True)
|
||||
|
||||
@api.depends('itch_cycle_product_ids.next_follow_up_date')
|
||||
def _compute_itch_next_delay(self):
|
||||
for partner in self:
|
||||
follow_up_dates = partner.itch_cycle_product_ids.mapped('next_follow_up_date')
|
||||
partner.itch_next_delay = min(follow_up_dates) if follow_up_dates else False
|
||||
44
customer_itch_cycle/models/sale_order.py
Normal file
44
customer_itch_cycle/models/sale_order.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
from datetime import datetime
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = 'sale.order'
|
||||
|
||||
def action_confirm(self):
|
||||
super(SaleOrder, self).action_confirm()
|
||||
for line in self.order_line:
|
||||
partner_id = self.partner_id
|
||||
product_id = line.product_id
|
||||
|
||||
itch_cycle_record = self.env['itch_cycle_product_partner'].search([
|
||||
('partner_id', '=', partner_id.id),
|
||||
('product_id', '=', product_id.id)
|
||||
], limit=1)
|
||||
|
||||
if itch_cycle_record:
|
||||
if itch_cycle_record.last_purchase_date:
|
||||
days_since_last_purchase = (datetime.now().date() - itch_cycle_record.last_purchase_date).days
|
||||
if itch_cycle_record.itch_cycle_duration == 0:
|
||||
raise UserError(_(
|
||||
f"Veuillez définir la durée du itch-cycle pour le produit '{product_id.name}' "
|
||||
f"pour le client '{partner_id.name}'.\n\n"
|
||||
f"Il y a eu {days_since_last_purchase} jours depuis le dernier achat."
|
||||
))
|
||||
else:
|
||||
itch_cycle_record.last_purchase_date = datetime.now().date()
|
||||
itch_cycle_record.itch_cycle_duration = days_since_last_purchase
|
||||
else:
|
||||
itch_cycle_record.last_purchase_date = datetime.now().date()
|
||||
else:
|
||||
new_record = self.env['itch_cycle_product_partner'].create({
|
||||
'partner_id': partner_id.id,
|
||||
'product_id': product_id.id,
|
||||
'last_purchase_date': datetime.now().date(),
|
||||
'itch_cycle_duration': 0
|
||||
})
|
||||
raise UserError(_(
|
||||
f"Il n'y a pas encore de itch-cycle défini pour le produit '{product_id.name}' "
|
||||
f"avec le client '{partner_id.name}'.\n\nVeuillez entrer une durée pour ce cycle."
|
||||
))
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
|
||||
<odoo>
|
||||
<record id="view_itch_cycle_product_partner_tree" model="ir.ui.view">
|
||||
<field name="name">itch.cycle.product.partner.tree</field>
|
||||
<field name="model">itch_cycle_product_partner</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="partner_id"/>
|
||||
<field name="product_id"/>
|
||||
<field name="last_purchase_date"/>
|
||||
<field name="itch_cycle_duration"/>
|
||||
<field name="next_follow_up_date"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_itch_cycle_product_partner_form" model="ir.ui.view">
|
||||
<field name="name">itch.cycle.product.partner.form</field>
|
||||
<field name="model">itch_cycle_product_partner</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="partner_id"/>
|
||||
<field name="product_id"/>
|
||||
<field name="last_purchase_date"/>
|
||||
<field name="itch_cycle_duration"/>
|
||||
<field name="next_follow_up_date"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_itch_cycle_product_partner" name="Itch Cycles"
|
||||
parent="sale.sale_order_menu"
|
||||
action="action_itch_cycle_product_partner"/>
|
||||
|
||||
<record id="action_itch_cycle_product_partner" model="ir.actions.act_window">
|
||||
<field name="name">Itch Cycles</field>
|
||||
<field name="res_model">itch_cycle_product_partner</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
</odoo>
|
||||
23
customer_itch_cycle/views/res_partner_view.xml
Normal file
23
customer_itch_cycle/views/res_partner_view.xml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
|
||||
<odoo>
|
||||
<record id="view_partner_form_inherit_itch_cycle" model="ir.ui.view">
|
||||
<field name="name">res.partner.form.itch.cycle</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<sheet position="before">
|
||||
<group string="Itch Cycle Information">
|
||||
<field name="itch_next_delay" readonly="1"/>
|
||||
<field name="itch_cycle_product_ids" readonly="1">
|
||||
<tree>
|
||||
<field name="product_id"/>
|
||||
<field name="last_purchase_date"/>
|
||||
<field name="itch_cycle_duration"/>
|
||||
<field name="next_follow_up_date"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
</sheet>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
1
openai_connector/__init__.py
Normal file
1
openai_connector/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
||||
22
openai_connector/__manifest__.py
Normal file
22
openai_connector/__manifest__.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
'name': 'OpenAI Connector',
|
||||
'version': '1.0',
|
||||
'category': 'Tools',
|
||||
'summary': 'Manage OpenAI connection settings',
|
||||
'license': 'AGPL-3',
|
||||
'description': '''
|
||||
This module allows the configuration of OpenAI API connection
|
||||
settings, including API key and Organization ID.
|
||||
''',
|
||||
'author': 'Bemade Inc.',
|
||||
'depends': ['base_setup'],
|
||||
'data': [
|
||||
'views/res_config_settings_views.xml',
|
||||
'security/ir.model.access.csv',
|
||||
],
|
||||
'external_dependencies': {
|
||||
'python': ['openai'],
|
||||
},
|
||||
'installable': True,
|
||||
'application': False,
|
||||
}
|
||||
2
openai_connector/models/__init__.py
Normal file
2
openai_connector/models/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from . import res_config_settings
|
||||
from . import res_company
|
||||
9
openai_connector/models/res_company.py
Normal file
9
openai_connector/models/res_company.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
from odoo import models, fields
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
api_key = fields.Char(string="API Key", help="API Key for OpenAI specific to this company. It should start with 'sk-'")
|
||||
organization = fields.Char(string="Organization ID", help="Organization ID for OpenAI specific to this company. It should start with 'org-'")
|
||||
|
||||
96
openai_connector/models/res_config_settings.py
Normal file
96
openai_connector/models/res_config_settings.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
import openai
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
api_key = fields.Char(
|
||||
string="API Key",
|
||||
config_parameter='openai_connector.api_key',
|
||||
help="API Key for OpenAI, used as a default if the company-specific key is not set. Format should start with 'sk-'"
|
||||
)
|
||||
organization = fields.Char(
|
||||
string="Organization ID",
|
||||
config_parameter='openai_connector.organization',
|
||||
help="Organization ID for OpenAI, used as a default if the company-specific ID is not set. Format should start with 'org-'"
|
||||
)
|
||||
|
||||
connection_status = fields.Char(
|
||||
string="Connection Status",
|
||||
compute='_compute_connection_status',
|
||||
help="Displays the current connection status with OpenAI."
|
||||
)
|
||||
|
||||
@api.depends('api_key', 'organization')
|
||||
def _compute_connection_status(self):
|
||||
for record in self:
|
||||
try:
|
||||
if record.api_key and record.organization: # Vérifie que les champs ne sont pas vides
|
||||
record._test_openai_connection()
|
||||
record.connection_status = "Connected"
|
||||
else:
|
||||
record.connection_status = "Disconnected"
|
||||
except Exception as e:
|
||||
record.connection_status = "Disconnected"
|
||||
_logger.error(f"OpenAI connection test failed: {str(e)}")
|
||||
|
||||
def set_values(self):
|
||||
super(ResConfigSettings, self).set_values()
|
||||
self.env['ir.config_parameter'].sudo().set_param(
|
||||
'openai_connector.api_key', self.api_key)
|
||||
self.env['ir.config_parameter'].sudo().set_param(
|
||||
'openai_connector.organization', self.organization)
|
||||
|
||||
# Test de connexion automatique lors de l'enregistrement si les champs sont remplis
|
||||
if self.api_key and self.organization:
|
||||
self._test_openai_connection()
|
||||
|
||||
@api.model
|
||||
def get_values(self):
|
||||
res = super(ResConfigSettings, self).get_values()
|
||||
res.update(
|
||||
api_key=self.env['ir.config_parameter'].sudo().get_param(
|
||||
'openai_connector.api_key', default=''),
|
||||
organization=self.env['ir.config_parameter'].sudo().get_param(
|
||||
'openai_connector.organization', default='')
|
||||
)
|
||||
return res
|
||||
|
||||
def _test_openai_connection(self):
|
||||
"""Method to test connection to OpenAI."""
|
||||
if not self.api_key or not self.organization:
|
||||
return # Ne fait rien si l'un des champs est vide
|
||||
|
||||
try:
|
||||
client = openai.OpenAI(organization=self.organization, api_key=self.api_key)
|
||||
client.models.list() # Test basique de la connexion
|
||||
except Exception as e:
|
||||
raise UserError(_("Failed to connect to OpenAI API. Please check the API Key and Organization ID.\nError: %s") % str(e))
|
||||
|
||||
def action_test_openai_connection(self):
|
||||
"""Action to test connection manually and display the result."""
|
||||
try:
|
||||
self._test_openai_connection()
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _("Connection Test Successful"),
|
||||
'message': _("The connection to OpenAI was successful."),
|
||||
'sticky': False,
|
||||
},
|
||||
}
|
||||
except UserError as e:
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _("Connection Test Failed"),
|
||||
'message': str(e),
|
||||
'sticky': True,
|
||||
},
|
||||
}
|
||||
2
openai_connector/security/ir.model.access.csv
Normal file
2
openai_connector/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_res_config_settings_openai,res.config.settings.openai,model_res_config_settings,base.group_system,1,1,1,1
|
||||
|
46
openai_connector/views/res_config_settings_views.xml
Normal file
46
openai_connector/views/res_config_settings_views.xml
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_openai_connector_config_settings_form" model="ir.ui.view">
|
||||
<field name="name">openai.connector.config.settings.form</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="priority" eval="30"/>
|
||||
<field name="inherit_id" ref="base_setup.res_config_settings_view_form"/>
|
||||
|
||||
<field name="arch" type="xml">
|
||||
<!-- Insertion sous les paramètres généraux -->
|
||||
<xpath expr="//block[@name='integration']" position="after">
|
||||
<block title="OpenAI Integration" name="openai_connector_settings">
|
||||
<!-- Champ pour l'API Key OpenAI -->
|
||||
<setting id="openai_api_key_setting"
|
||||
help="Enter the API Key for OpenAI. This will be used by default if the company-specific key is not set."
|
||||
company_dependent="1">
|
||||
<field name="api_key"/>
|
||||
</setting>
|
||||
|
||||
<!-- Champ pour l'Organization ID OpenAI -->
|
||||
<setting id="openai_organization_setting"
|
||||
help="Enter the Organization ID for OpenAI. This will be used by default if the company-specific ID is not set."
|
||||
company_dependent="1">
|
||||
<field name="organization"/>
|
||||
</setting>
|
||||
|
||||
<!-- Champ affichant le statut de connexion -->
|
||||
<setting id="openai_connection_status_setting"
|
||||
help="Displays the current connection status with OpenAI."
|
||||
company_dependent="1">
|
||||
<field name="connection_status" readonly="1"/>
|
||||
</setting>
|
||||
|
||||
<!-- Bouton pour tester la connexion -->
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_test_openai_connection"
|
||||
string="Tester la connexion"
|
||||
type="object"
|
||||
class="btn-primary oe_stat_button"
|
||||
icon="fa-check-circle"/>
|
||||
</div>
|
||||
</block>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
2
openai_partner_purchase_analysis/__init__.py
Normal file
2
openai_partner_purchase_analysis/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from . import models
|
||||
from . import wizard
|
||||
31
openai_partner_purchase_analysis/__manifest__.py
Normal file
31
openai_partner_purchase_analysis/__manifest__.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
'name': 'Partner Purchase Analysis with Optional OpenAI and Queue Job',
|
||||
'version': '1.0',
|
||||
'category': 'Tools',
|
||||
'summary': 'Manage OpenAI connection settings',
|
||||
'license': 'AGPL-3',
|
||||
'description': '''
|
||||
This module allows the configuration of OpenAI API connection
|
||||
settings, including API key and Organization ID.
|
||||
''',
|
||||
'author': 'Bemade Inc.',
|
||||
'depends': [
|
||||
'base',
|
||||
'sale',
|
||||
'product',
|
||||
'openai_connector',
|
||||
'sale_management',
|
||||
],
|
||||
'data': [
|
||||
'security/ir.model.access.csv', # Fichier de sécurité mis à jour
|
||||
'data/queue_job_group.xml',
|
||||
'views/res_config_settings_view.xml',
|
||||
'views/res_partner_view.xml',
|
||||
'wizard/partner_purchase_analysis_wizard_view.xml',
|
||||
],
|
||||
'external_dependencies': {
|
||||
'python': ['openai'],
|
||||
},
|
||||
'installable': True,
|
||||
'application': False,
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
<odoo>
|
||||
<record id="group_use_queue_job" model="res.groups">
|
||||
<field name="name">Use Queue Job for Asynchronous Processing</field>
|
||||
<field name="category_id" ref="base.module_category_tools"/>
|
||||
</record>
|
||||
</odoo>
|
||||
4
openai_partner_purchase_analysis/models/__init__.py
Normal file
4
openai_partner_purchase_analysis/models/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from . import res_partner
|
||||
from . import partner_purchase_analysis_wizard
|
||||
from . import res_config_settings
|
||||
from . import res_company
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
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))
|
||||
11
openai_partner_purchase_analysis/models/res_company.py
Normal file
11
openai_partner_purchase_analysis/models/res_company.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from odoo import models, fields
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
product_categories_analyzed = fields.Many2many(
|
||||
'product.category',
|
||||
string="Product Categories Analyzed",
|
||||
help="Select product categories to include in the purchase analysis. Only products in these categories and their subcategories will be analyzed."
|
||||
)
|
||||
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
from odoo import models, fields, api
|
||||
from odoo.modules.module import get_module_resource
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
use_queue_job = fields.Boolean(
|
||||
string="Use Queue Job for Asynchronous Processing",
|
||||
help="If enabled, the system will use queue jobs for background tasks. Requires the queue_job module.",
|
||||
default=False
|
||||
)
|
||||
product_categories_analyzed = fields.Many2many(
|
||||
related='company_id.product_categories_analyzed',
|
||||
comodel_name='product.category',
|
||||
string="Product Categories Analyzed",
|
||||
help="Select product categories to include in the purchase analysis."
|
||||
)
|
||||
|
||||
@api.model
|
||||
def get_values(self):
|
||||
res = super().get_values()
|
||||
res.update(
|
||||
use_queue_job=self.env['ir.config_parameter'].sudo().get_param('my_module.use_queue_job', default=False)
|
||||
)
|
||||
return res
|
||||
|
||||
def set_values(self):
|
||||
super().set_values()
|
||||
self.env['ir.config_parameter'].sudo().set_param('my_module.use_queue_job', self.use_queue_job)
|
||||
|
||||
@api.model
|
||||
def enable_queue_job_group(self):
|
||||
"""Enable the group if the queue_job module is installed"""
|
||||
group = self.env.ref('partner_purchase_analysis_with_openai_filtered.group_use_queue_job', raise_if_not_found=False)
|
||||
if group and get_module_resource('queue_job'):
|
||||
group.sudo().write({'users': [(4, self.env.user.id)]})
|
||||
20
openai_partner_purchase_analysis/models/res_partner.py
Normal file
20
openai_partner_purchase_analysis/models/res_partner.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
from odoo import models, fields
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
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.")
|
||||
|
||||
def action_open_purchase_analysis(self):
|
||||
"""Ouvre le wizard d'analyse des achats pour le client actuel."""
|
||||
return {
|
||||
'name': 'Purchase Analysis',
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'partner.purchase.analysis.wizard',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_partner_id': self.id,
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_partner_purchase_analysis_wizard_sales,partner.purchase.analysis.wizard access,model_partner_purchase_analysis_wizard,sales_team.group_sale_salesman,1,1,1,1
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<odoo>
|
||||
<record id="res_config_settings_view_form_inherit_partner_analysis" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.partner.analysis</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="openai_connector.view_openai_connector_config_settings_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//block[@name='openai_connector_settings']" position="inside">
|
||||
<div class="col-12" groups="openai_partner_purchase_analysis.group_use_queue_job">
|
||||
<label for="use_queue_job"/>
|
||||
<field name="use_queue_job"/>
|
||||
</div>
|
||||
<div class="col-12 mt16">
|
||||
<label for="product_categories_analyzed"/>
|
||||
<field name="product_categories_analyzed" widget="many2many_tags"/>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
17
openai_partner_purchase_analysis/views/res_partner_view.xml
Normal file
17
openai_partner_purchase_analysis/views/res_partner_view.xml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<odoo>
|
||||
<record id="view_partner_form_inherit_purchase_analysis" model="ir.ui.view">
|
||||
<field name="name">res.partner.form.inherit.purchase.analysis</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<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"/>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
1
openai_partner_purchase_analysis/wizard/__init__.py
Normal file
1
openai_partner_purchase_analysis/wizard/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import partner_purchase_analysis_wizard
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
from openai import OpenAI
|
||||
from datetime import datetime
|
||||
from bs4 import BeautifulSoup
|
||||
from markupsafe import escape
|
||||
|
||||
# Import conditionnel de queue_job pour gérer les files d'attente si elles sont disponibles
|
||||
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", required=True)
|
||||
date_end = fields.Date(string="End Date", default=fields.Date.today, required=True)
|
||||
|
||||
def start_analysis(self):
|
||||
"""Lance l'analyse en arrière-plan si `queue_job` est disponible, sinon exécute immédiatement."""
|
||||
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é."""
|
||||
|
||||
# Récupération de l'organisation et de la clé API OpenAI dans les paramètres
|
||||
organization = self.env['ir.config_parameter'].sudo().get_param('openai_connector.organization')
|
||||
api_key = self.env['ir.config_parameter'].sudo().get_param('openai_connector.api_key')
|
||||
|
||||
if not api_key or not organization:
|
||||
raise UserError(_("API Key or Organization ID for OpenAI is missing in settings."))
|
||||
|
||||
client = OpenAI(
|
||||
api_key=api_key,
|
||||
organization=organization
|
||||
)
|
||||
|
||||
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 le prompt 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,
|
||||
})
|
||||
|
||||
# Prompt défini en plusieurs lignes pour lisibilité
|
||||
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"
|
||||
|
||||
prompt = (
|
||||
"Analyze the following customer purchase history. Identify trends, product "
|
||||
"category preferences, and any significant deviations. Respond in "
|
||||
f"{user_lang_name}. Produce graph and table of the analysis. You output all "
|
||||
"in html format."
|
||||
)
|
||||
|
||||
# Construction de `purchase_details` pour le contenu du prompt
|
||||
purchase_details = f"Customer Purchase Analysis Grouped by Product Category and Product:\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"
|
||||
|
||||
# Appel à l'API OpenAI
|
||||
try:
|
||||
response = client.chat.completions.create(
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": prompt
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": purchase_details
|
||||
}
|
||||
],
|
||||
model="gpt-4o",
|
||||
)
|
||||
|
||||
# Supposons que `response` contient la réponse complète au format HTML
|
||||
response_text = response.choices[0].message.content
|
||||
|
||||
# Parse avec BeautifulSoup pour extraire le contenu du <body>
|
||||
soup = BeautifulSoup(response_text, "html.parser")
|
||||
body_content = soup.body
|
||||
|
||||
# Si body est présent, on utilise son contenu ; sinon, on utilise tout le texte
|
||||
cleaned_text = escape(body_content.get_text()) if body_content else escape(response_text)
|
||||
|
||||
# Ajouter des balises de base pour structurer le texte en HTML simple
|
||||
html_content = f"<div>{cleaned_text.replace('\n', '<br/>')}</div>"
|
||||
|
||||
self.partner_id.sale_analysis = body_content
|
||||
self.partner_id.sale_analysis_date = fields.Datetime.today()
|
||||
# self.sale_analysis = response.choices[0].message.content
|
||||
# self.sale_analysis_date = fields.Datetime.today()
|
||||
print(response.choices[0].message.content)
|
||||
except Exception as e:
|
||||
raise UserError(_("Failed to get response from OpenAI. Error: %s") % str(e))
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<odoo>
|
||||
<record id="view_partner_purchase_analysis_wizard_form" model="ir.ui.view">
|
||||
<field name="name">partner.purchase.analysis.wizard.form</field>
|
||||
<field name="model">partner.purchase.analysis.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Analyze Purchases with OpenAI">
|
||||
<group>
|
||||
<field name="partner_id" readonly="1"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
<button name="start_analysis" string="Run Analysis" type="object" class="btn-primary"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action serveur pour ouvrir le wizard d'analyse des achats -->
|
||||
<record id="action_partner_purchase_analysis" model="ir.actions.server">
|
||||
<field name="name">Analyze Purchases</field>
|
||||
<field name="model_id" ref="base.model_res_partner"/>
|
||||
<field name="state">code</field>
|
||||
<field name="binding_type">action</field>
|
||||
<field name="binding_model_id" ref="base.model_res_partner"/>
|
||||
<field name="code">
|
||||
action = {
|
||||
'name': 'Purchase Analysis',
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'partner.purchase.analysis.wizard',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_partner_id': env.context.get('active_id'),
|
||||
}
|
||||
}
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Ajouter l'action dans le menu "Actions" de res.partner -->
|
||||
<!-- <record id="action_partner_purchase_analysis_act_window" model="ir.actions.servers">-->
|
||||
<!-- <field name="name">Analyze Purchases</field>-->
|
||||
<!-- <field name="model">res.partner</field>-->
|
||||
<!-- <field name="key2">client_action_multi</field>-->
|
||||
<!-- <field name="value" eval="'ir.actions.server,' + str(ref('openai_partner_purchase_analysis.action_partner_purchase_analysis'))"/>-->
|
||||
<!-- </record>-->
|
||||
</odoo>
|
||||
Loading…
Reference in a new issue