diff --git a/customer_itch_cycle/__init__.py b/customer_itch_cycle/__init__.py
new file mode 100644
index 0000000..a9e3372
--- /dev/null
+++ b/customer_itch_cycle/__init__.py
@@ -0,0 +1,2 @@
+
+from . import models
diff --git a/customer_itch_cycle/__manifest__.py b/customer_itch_cycle/__manifest__.py
new file mode 100644
index 0000000..f8417fe
--- /dev/null
+++ b/customer_itch_cycle/__manifest__.py
@@ -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,
+}
diff --git a/customer_itch_cycle/models/__init__.py b/customer_itch_cycle/models/__init__.py
new file mode 100644
index 0000000..8272bc5
--- /dev/null
+++ b/customer_itch_cycle/models/__init__.py
@@ -0,0 +1,3 @@
+
+from . import itch_cycle_product_partner
+from . import sale_order
diff --git a/customer_itch_cycle/models/itch_cycle_product_partner.py b/customer_itch_cycle/models/itch_cycle_product_partner.py
new file mode 100644
index 0000000..f5dbbfe
--- /dev/null
+++ b/customer_itch_cycle/models/itch_cycle_product_partner.py
@@ -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
diff --git a/customer_itch_cycle/models/sale_order.py b/customer_itch_cycle/models/sale_order.py
new file mode 100644
index 0000000..e0fc3a1
--- /dev/null
+++ b/customer_itch_cycle/models/sale_order.py
@@ -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."
+ ))
diff --git a/customer_itch_cycle/views/itch_cycle_product_partner_view.xml b/customer_itch_cycle/views/itch_cycle_product_partner_view.xml
new file mode 100644
index 0000000..c1b5b83
--- /dev/null
+++ b/customer_itch_cycle/views/itch_cycle_product_partner_view.xml
@@ -0,0 +1,44 @@
+
+
+
+ itch.cycle.product.partner.tree
+ itch_cycle_product_partner
+
+
+
+
+
+
+
+
+
+
+
+
+ itch.cycle.product.partner.form
+ itch_cycle_product_partner
+
+
+
+
+
+
+
+
+ Itch Cycles
+ itch_cycle_product_partner
+ tree,form
+
+
diff --git a/customer_itch_cycle/views/res_partner_view.xml b/customer_itch_cycle/views/res_partner_view.xml
new file mode 100644
index 0000000..f7285b5
--- /dev/null
+++ b/customer_itch_cycle/views/res_partner_view.xml
@@ -0,0 +1,23 @@
+
+
+
+ res.partner.form.itch.cycle
+ res.partner
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/openai_connector/__init__.py b/openai_connector/__init__.py
new file mode 100644
index 0000000..9a7e03e
--- /dev/null
+++ b/openai_connector/__init__.py
@@ -0,0 +1 @@
+from . import models
\ No newline at end of file
diff --git a/openai_connector/__manifest__.py b/openai_connector/__manifest__.py
new file mode 100644
index 0000000..3832c8f
--- /dev/null
+++ b/openai_connector/__manifest__.py
@@ -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,
+}
\ No newline at end of file
diff --git a/openai_connector/models/__init__.py b/openai_connector/models/__init__.py
new file mode 100644
index 0000000..3468e42
--- /dev/null
+++ b/openai_connector/models/__init__.py
@@ -0,0 +1,2 @@
+from . import res_config_settings
+from . import res_company
\ No newline at end of file
diff --git a/openai_connector/models/res_company.py b/openai_connector/models/res_company.py
new file mode 100644
index 0000000..0945476
--- /dev/null
+++ b/openai_connector/models/res_company.py
@@ -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-'")
+
\ No newline at end of file
diff --git a/openai_connector/models/res_config_settings.py b/openai_connector/models/res_config_settings.py
new file mode 100644
index 0000000..e8438f0
--- /dev/null
+++ b/openai_connector/models/res_config_settings.py
@@ -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,
+ },
+ }
\ No newline at end of file
diff --git a/openai_connector/security/ir.model.access.csv b/openai_connector/security/ir.model.access.csv
new file mode 100644
index 0000000..1d9caaa
--- /dev/null
+++ b/openai_connector/security/ir.model.access.csv
@@ -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
diff --git a/openai_connector/views/res_config_settings_views.xml b/openai_connector/views/res_config_settings_views.xml
new file mode 100644
index 0000000..3d4131e
--- /dev/null
+++ b/openai_connector/views/res_config_settings_views.xml
@@ -0,0 +1,46 @@
+
+
+
+ openai.connector.config.settings.form
+ res.config.settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/openai_partner_purchase_analysis/__init__.py b/openai_partner_purchase_analysis/__init__.py
new file mode 100644
index 0000000..c536983
--- /dev/null
+++ b/openai_partner_purchase_analysis/__init__.py
@@ -0,0 +1,2 @@
+from . import models
+from . import wizard
\ No newline at end of file
diff --git a/openai_partner_purchase_analysis/__manifest__.py b/openai_partner_purchase_analysis/__manifest__.py
new file mode 100644
index 0000000..684df61
--- /dev/null
+++ b/openai_partner_purchase_analysis/__manifest__.py
@@ -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,
+}
\ No newline at end of file
diff --git a/openai_partner_purchase_analysis/data/queue_job_group.xml b/openai_partner_purchase_analysis/data/queue_job_group.xml
new file mode 100644
index 0000000..a2ac0e3
--- /dev/null
+++ b/openai_partner_purchase_analysis/data/queue_job_group.xml
@@ -0,0 +1,7 @@
+
+
+
+ Use Queue Job for Asynchronous Processing
+
+
+
diff --git a/openai_partner_purchase_analysis/models/__init__.py b/openai_partner_purchase_analysis/models/__init__.py
new file mode 100644
index 0000000..fbeefd0
--- /dev/null
+++ b/openai_partner_purchase_analysis/models/__init__.py
@@ -0,0 +1,4 @@
+from . import res_partner
+from . import partner_purchase_analysis_wizard
+from . import res_config_settings
+from . import res_company
\ No newline at end of file
diff --git a/openai_partner_purchase_analysis/models/partner_purchase_analysis_wizard.py b/openai_partner_purchase_analysis/models/partner_purchase_analysis_wizard.py
new file mode 100644
index 0000000..b1a6faa
--- /dev/null
+++ b/openai_partner_purchase_analysis/models/partner_purchase_analysis_wizard.py
@@ -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))
\ No newline at end of file
diff --git a/openai_partner_purchase_analysis/models/res_company.py b/openai_partner_purchase_analysis/models/res_company.py
new file mode 100644
index 0000000..2582c6c
--- /dev/null
+++ b/openai_partner_purchase_analysis/models/res_company.py
@@ -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."
+ )
+
diff --git a/openai_partner_purchase_analysis/models/res_config_settings.py b/openai_partner_purchase_analysis/models/res_config_settings.py
new file mode 100644
index 0000000..b7b1aec
--- /dev/null
+++ b/openai_partner_purchase_analysis/models/res_config_settings.py
@@ -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)]})
\ No newline at end of file
diff --git a/openai_partner_purchase_analysis/models/res_partner.py b/openai_partner_purchase_analysis/models/res_partner.py
new file mode 100644
index 0000000..a37f8cf
--- /dev/null
+++ b/openai_partner_purchase_analysis/models/res_partner.py
@@ -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,
+ },
+ }
\ No newline at end of file
diff --git a/openai_partner_purchase_analysis/security/ir.model.access.csv b/openai_partner_purchase_analysis/security/ir.model.access.csv
new file mode 100644
index 0000000..fe746dd
--- /dev/null
+++ b/openai_partner_purchase_analysis/security/ir.model.access.csv
@@ -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
\ No newline at end of file
diff --git a/openai_partner_purchase_analysis/views/res_config_settings_view.xml b/openai_partner_purchase_analysis/views/res_config_settings_view.xml
new file mode 100644
index 0000000..d2b2269
--- /dev/null
+++ b/openai_partner_purchase_analysis/views/res_config_settings_view.xml
@@ -0,0 +1,19 @@
+
+
+ res.config.settings.view.form.inherit.partner.analysis
+ res.config.settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/openai_partner_purchase_analysis/views/res_partner_view.xml b/openai_partner_purchase_analysis/views/res_partner_view.xml
new file mode 100644
index 0000000..9fb4018
--- /dev/null
+++ b/openai_partner_purchase_analysis/views/res_partner_view.xml
@@ -0,0 +1,17 @@
+
+
+ res.partner.form.inherit.purchase.analysis
+ res.partner
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/openai_partner_purchase_analysis/wizard/__init__.py b/openai_partner_purchase_analysis/wizard/__init__.py
new file mode 100644
index 0000000..7e6aa79
--- /dev/null
+++ b/openai_partner_purchase_analysis/wizard/__init__.py
@@ -0,0 +1 @@
+from . import partner_purchase_analysis_wizard
\ No newline at end of file
diff --git a/openai_partner_purchase_analysis/wizard/partner_purchase_analysis_wizard.py b/openai_partner_purchase_analysis/wizard/partner_purchase_analysis_wizard.py
new file mode 100644
index 0000000..d4d8fcb
--- /dev/null
+++ b/openai_partner_purchase_analysis/wizard/partner_purchase_analysis_wizard.py
@@ -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
+ 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"
{cleaned_text.replace('\n', ' ')}
"
+
+ 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))
\ No newline at end of file
diff --git a/openai_partner_purchase_analysis/wizard/partner_purchase_analysis_wizard_view.xml b/openai_partner_purchase_analysis/wizard/partner_purchase_analysis_wizard_view.xml
new file mode 100644
index 0000000..75f0238
--- /dev/null
+++ b/openai_partner_purchase_analysis/wizard/partner_purchase_analysis_wizard_view.xml
@@ -0,0 +1,45 @@
+
+
+ partner.purchase.analysis.wizard.form
+ partner.purchase.analysis.wizard
+
+
+
+
+
+
+
+ Analyze Purchases
+
+ code
+ action
+
+
+ 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'),
+ }
+ }
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file