diff --git a/customer_itch_cycle/__manifest__.py b/customer_itch_cycle/__manifest__.py
index f8417fe..5c6d243 100644
--- a/customer_itch_cycle/__manifest__.py
+++ b/customer_itch_cycle/__manifest__.py
@@ -9,6 +9,9 @@
'data': [
'views/itch_cycle_product_partner_view.xml',
'views/res_partner_view.xml',
+ 'views/product_category_view.xml',
+ 'security/ir.model.access.csv'
+
],
'installable': True,
'application': False,
diff --git a/customer_itch_cycle/models/__init__.py b/customer_itch_cycle/models/__init__.py
index 8272bc5..5bc4a0e 100644
--- a/customer_itch_cycle/models/__init__.py
+++ b/customer_itch_cycle/models/__init__.py
@@ -1,3 +1,4 @@
-
from . import itch_cycle_product_partner
-from . import sale_order
+from . import sale_order_line
+from . import product_category
+from . import res_partner
diff --git a/customer_itch_cycle/models/itch_cycle_product_partner.py b/customer_itch_cycle/models/itch_cycle_product_partner.py
index f5dbbfe..426d0a2 100644
--- a/customer_itch_cycle/models/itch_cycle_product_partner.py
+++ b/customer_itch_cycle/models/itch_cycle_product_partner.py
@@ -1,33 +1,195 @@
-
+from datetime import datetime, timedelta
+import numpy as np
from odoo import models, fields, api
-from datetime import timedelta, datetime
+from odoo.exceptions import UserError
+
class ItchCycleProductPartner(models.Model):
- _name = 'itch_cycle_product_partner'
- _description = 'Itch Cycle by Product and Partner'
+ _name = "itch.cycle.product.partner"
+ _description = "Cycle Produit/Partenaire"
- 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)
+ partner_id = fields.Many2one(
+ comodel_name='res.partner',
+ string="Client",
+ required=True
+ )
- @api.depends('last_purchase_date', 'itch_cycle_duration')
- def _compute_next_follow_up_date(self):
+ product_id = fields.Many2one(
+ comodel_name='product.product',
+ string="Produit",
+ required=True
+ )
+
+ average_cycle = fields.Integer(
+ string="Cycle moyen (jours)",
+ compute="_compute_average_cycle",
+ store=True
+ )
+
+ next_expected_date = fields.Date(
+ string="Prochaine vente prévue",
+ compute="_compute_next_expected_date",
+ store=True
+ )
+
+ prediction_status = fields.Selection(
+ selection=[
+ ('on_time', 'À l’heure'),
+ ('delayed', 'Retardée')
+ ],
+ string="Statut de la prédiction",
+ compute="_compute_prediction_status"
+ )
+
+ sale_order_line_ids = fields.One2many(
+ comodel_name='sale.order.line',
+ inverse_name='itch_cycle_id',
+ string="Lignes de commande"
+ )
+
+ next_follow_up_date = fields.Date(
+ string="Prochaine date de suivi",
+ compute="_compute_next_follow_up_date",
+ store=True,
+ readonly=False
+ )
+
+ last_purchase_date = fields.Date(
+ string="Date du dernier achat",
+ compute="_compute_last_purchase_date",
+ store=True
+ )
+
+ itch_cycle_duration = fields.Integer(
+ string="Durée du cycle (jours)",
+ help="Représente la durée moyenne du cycle en jours",
+ compute="_compute_itch_cycle_duration",
+ store=True
+ )
+
+ @api.depends('average_cycle')
+ def _compute_itch_cycle_duration(self):
+ """Calcule automatiquement la durée du cycle."""
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)
+ record.itch_cycle_duration = record.average_cycle or 0
+
+ @api.depends('sale_order_line_ids')
+ def _compute_last_purchase_date(self):
+ """Met automatiquement à jour la date du dernier achat."""
+ for record in self:
+ # Rechercher la commande la plus récente associée
+ last_order = record.sale_order_line_ids.mapped('order_id').sorted(key=lambda o: o.date_order, reverse=True)
+ record.last_purchase_date = last_order[0].date_order if last_order else None
+
+ @api.depends('next_expected_date')
+ def _compute_next_follow_up_date(self):
+ """
+ Définit la logique pour calculer automatiquement la date de suivi.
+ Peut être basée sur `next_expected_date`, mais modifiable par l'utilisateur.
+ """
+ for record in self:
+ if record.next_expected_date:
+ # Exemple : une alerte de suivi une semaine avant la prochaine vente prévue
+ record.next_follow_up_date = record.next_expected_date - timedelta(days=7)
else:
- record.next_follow_up_date = False
+ record.next_follow_up_date = None
-class ResPartner(models.Model):
- _inherit = 'res.partner'
+ @api.depends('sale_order_line_ids', 'product_id.categ_id')
+ def _compute_average_cycle(self):
+ for record in self:
+ product_category = record.product_id.categ_id
+ if product_category.seasonal_factor and product_category.season_months:
+ # Filtrer les commandes par mois saisonniers de la catégorie
+ active_months = list(map(int, product_category.season_months.split(',')))
+ dates = sorted([
+ line.order_id.date_order
+ for line in record.sale_order_line_ids
+ if line.order_id.date_order.month in active_months
+ ])
+ else:
+ # Cycle habituel (non saisonnier)
+ dates = sorted(record.sale_order_line_ids.mapped('order_id.date_order'))
- 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)
+ if len(dates) > 1:
+ intervals = [(dates[i + 1] - dates[i]).days for i in range(len(dates) - 1)]
+ record.average_cycle = int(np.mean(intervals)) if intervals else 0
+ else:
+ record.average_cycle = 0
- @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
+ @api.depends('average_cycle')
+ def _compute_next_expected_date(self):
+ for record in self:
+ if record.average_cycle and record.sale_order_line_ids:
+ last_date = max(record.sale_order_line_ids.mapped('order_id.date_order'))
+ record.next_expected_date = fields.Date.from_string(last_date) + timedelta(days=record.average_cycle)
+ else:
+ record.next_expected_date = None
+
+ def _compute_prediction_status(self):
+ today = fields.Date.context_today(self)
+ for record in self:
+ if record.next_expected_date and record.next_expected_date < today:
+ record.prediction_status = 'delayed'
+ else:
+ record.prediction_status = 'on_time'
+
+ @api.model
+ def populate_from_past_orders(self):
+ """
+ Méthode pour traiter le passé :
+ Crée des enregistrements 'itch.cycle.product.partner'
+ basés sur les commandes de vente existantes.
+ """
+ # Récupérer toutes les lignes de commandes confirmées
+ sale_order_lines = self.env['sale.order.line'].search([
+ ('order_id.state', 'in', ['sale', 'done']) # Commandes confirmées ou terminées
+ ])
+
+ # Carte pour regrouper par couple (client, produit)
+ cycle_data = {}
+
+ for line in sale_order_lines:
+ key = (line.order_id.partner_id.id, line.product_id.id)
+ if key not in cycle_data:
+ cycle_data[key] = {
+ 'partner_id': line.order_id.partner_id.id,
+ 'product_id': line.product_id.id,
+ 'sale_order_line_ids': []
+ }
+ cycle_data[key]['sale_order_line_ids'].append(line.id) # Ajouter l'ID de la ligne
+
+ # Créer les enregistrements manquants dans Itch Cycle
+ for key, data in cycle_data.items():
+ partner_id, product_id = key
+
+ # Vérifier si l'enregistrement existe déjà
+ itch_cycle = self.search([('partner_id', '=', partner_id), ('product_id', '=', product_id)], limit=1)
+
+ if not itch_cycle:
+ # Calculer le cycle moyen basé sur les dates de commandes
+ order_dates = sorted([self.env['sale.order.line'].browse(line_id).order_id.date_order
+ for line_id in data['sale_order_line_ids']])
+ if len(order_dates) > 1:
+ intervals = [(order_dates[i + 1] - order_dates[i]).days for i in range(len(order_dates) - 1)]
+ average_cycle = sum(intervals) // len(intervals) if intervals else 0
+ else:
+ average_cycle = 0
+
+ # Créer l'enregistrement dans la table Itch Cycle
+ if product_id and partner_id:
+ self.create({
+ 'partner_id': partner_id,
+ 'product_id': product_id,
+ 'sale_order_line_ids': [(6, 0, data['sale_order_line_ids'])]
+ })
+ else:
+ #raise UserError("Impossible de créer un cycle sans client ou produit associé.")
+ print("Impossible de créer un cycle sans client ou produit associé.")
+ print('valeur de product_id', product_id)
+ print('valeur de partner_id', partner_id)
+ else:
+ # Mettre à jour les lignes de commande si nécessaire
+ itch_cycle.write({
+ 'sale_order_line_ids': [(6, 0, data['sale_order_line_ids'])], # Lier ou actualiser les lignes
+ })
+ return True
diff --git a/customer_itch_cycle/models/product_category.py b/customer_itch_cycle/models/product_category.py
new file mode 100644
index 0000000..0602138
--- /dev/null
+++ b/customer_itch_cycle/models/product_category.py
@@ -0,0 +1,15 @@
+
+from odoo import models, fields
+
+class ProductCategory(models.Model):
+ _inherit = 'product.category'
+
+ seasonal_factor = fields.Boolean(
+ string="Catégorie saisonnière",
+ default=False
+ )
+
+ season_months = fields.Char(
+ string="Mois actifs",
+ help="Mois pendant lesquels cette catégorie est active (ex: '3,4,5' pour mars-mai)"
+ )
diff --git a/customer_itch_cycle/models/res_partner.py b/customer_itch_cycle/models/res_partner.py
new file mode 100644
index 0000000..df98f40
--- /dev/null
+++ b/customer_itch_cycle/models/res_partner.py
@@ -0,0 +1,33 @@
+from odoo import models, fields, api, _
+from odoo.exceptions import UserError
+from datetime import datetime
+
+class ResPartner(models.Model):
+ _inherit = 'res.partner'
+
+ itch_cycle_product_ids = fields.One2many(
+ comodel_name='itch.cycle.product.partner',
+ inverse_name='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):
+ """
+ Calcule la date de suivi minimale parmi tous les cycles de produits associés.
+ """
+ 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
+
+ def action_populate_itch_cycles(self):
+ """
+ Action bouton qui appelle la méthode pour peupler les cycles depuis les données passées.
+ """
+ self.env['itch.cycle.product.partner'].populate_from_past_orders()
\ No newline at end of file
diff --git a/customer_itch_cycle/models/sale_order.py b/customer_itch_cycle/models/sale_order.py
deleted file mode 100644
index e0fc3a1..0000000
--- a/customer_itch_cycle/models/sale_order.py
+++ /dev/null
@@ -1,44 +0,0 @@
-
-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/models/sale_order_line.py b/customer_itch_cycle/models/sale_order_line.py
new file mode 100644
index 0000000..9ce7e98
--- /dev/null
+++ b/customer_itch_cycle/models/sale_order_line.py
@@ -0,0 +1,27 @@
+from odoo import models, fields, api
+
+class SaleOrderLine(models.Model):
+ _inherit = 'sale.order.line'
+
+ itch_cycle_id = fields.Many2one(
+ comodel_name='itch.cycle.product.partner',
+ string="Cycle Produit/Partenaire"
+ )
+
+ @api.model
+ def create(self, vals):
+ line = super(SaleOrderLine, self).create(vals)
+ # Associer au bon cycle produit/partenaire
+ if vals.get('product_id') and vals.get('order_id'):
+ partner_id = self.env['sale.order'].browse(vals['order_id']).partner_id.id
+ itch_cycle = self.env['itch.cycle.product.partner'].search([
+ ('partner_id', '=', partner_id),
+ ('product_id', '=', vals['product_id'])
+ ], limit=1)
+ if not itch_cycle:
+ itch_cycle = self.env['itch.cycle.product.partner'].create({
+ 'partner_id': partner_id,
+ 'product_id': vals['product_id'],
+ })
+ line.itch_cycle_id = itch_cycle.id
+ return line
\ No newline at end of file
diff --git a/customer_itch_cycle/security/ir.model.access.csv b/customer_itch_cycle/security/ir.model.access.csv
new file mode 100644
index 0000000..8d88f59
--- /dev/null
+++ b/customer_itch_cycle/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_itch_cycle_product_partner,itch.cycle.product.partner access,model_itch_cycle_product_partner,base.group_user,1,1,1,1
\ No newline at end of file
diff --git a/customer_itch_cycle/views/itch_cycle_menu.xml b/customer_itch_cycle/views/itch_cycle_menu.xml
new file mode 100644
index 0000000..e69de29
diff --git a/customer_itch_cycle/views/itch_cycle_product_partner_view.xml b/customer_itch_cycle/views/itch_cycle_product_partner_view.xml
index c1b5b83..17aab98 100644
--- a/customer_itch_cycle/views/itch_cycle_product_partner_view.xml
+++ b/customer_itch_cycle/views/itch_cycle_product_partner_view.xml
@@ -1,44 +1,71 @@
-
itch.cycle.product.partner.tree
- itch_cycle_product_partner
+ itch.cycle.product.partner
-
+
+
+
+
itch.cycle.product.partner.form
- itch_cycle_product_partner
+ itch.cycle.product.partner
-
-
- Itch Cycles
- itch_cycle_product_partner
+ Itch Cycles Produits
+ itch.cycle.product.partner
tree,form
+
-
+
+
+
\ No newline at end of file
diff --git a/customer_itch_cycle/views/product_category_view.xml b/customer_itch_cycle/views/product_category_view.xml
new file mode 100644
index 0000000..a903bf3
--- /dev/null
+++ b/customer_itch_cycle/views/product_category_view.xml
@@ -0,0 +1,21 @@
+
+
+
+ product.category.form
+ product.category
+
+
+
+
+
diff --git a/customer_itch_cycle/views/res_partner_view.xml b/customer_itch_cycle/views/res_partner_view.xml
index f7285b5..a233e96 100644
--- a/customer_itch_cycle/views/res_partner_view.xml
+++ b/customer_itch_cycle/views/res_partner_view.xml
@@ -5,19 +5,25 @@
res.partner
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/openai_partner_purchase_analysis/wizard/partner_purchase_analysis_wizard.py b/openai_partner_purchase_analysis/wizard/partner_purchase_analysis_wizard.py
index d4d8fcb..f68ed5f 100644
--- a/openai_partner_purchase_analysis/wizard/partner_purchase_analysis_wizard.py
+++ b/openai_partner_purchase_analysis/wizard/partner_purchase_analysis_wizard.py
@@ -57,6 +57,8 @@ class PartnerPurchaseAnalysisWizard(models.TransientModel):
if self.date_end:
domain.append(('order_id.date_order', '<=', self.date_end))
+ domain.append(('order_id.state', 'in', ['sale', 'done']))
+
sale_order_lines = self.env['sale.order.line'].search(domain)
if not sale_order_lines:
@@ -82,10 +84,14 @@ class PartnerPurchaseAnalysisWizard(models.TransientModel):
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."
+ "Analyze all the sale order lines of the following customer. Identify trends, "
+ "and any significant deviations. Respond in "
+ f"{user_lang_name}. Produce the analysis adding next plan purchase or lost purchase. You output all "
+ "in html format. Focus on missing product order and deviation from the average. "
+ "Be sure to list all products and categories in the analysis. Try to identify "
+ "the next purchase date for all product and warn if the customer is not buying "
+ "and identify recurring sale and product not sale. Put in the analysis if the product is not bought "
+ "in the last 12 months and show those product as lost sale with a value of the lost sales"
)
# Construction de `purchase_details` pour le contenu du prompt