diff --git a/customer_itch_cycle/__manifest__.py b/customer_itch_cycle/__manifest__.py
index 1f489f3..88f0256 100644
--- a/customer_itch_cycle/__manifest__.py
+++ b/customer_itch_cycle/__manifest__.py
@@ -1,10 +1,10 @@
-
{
'name': 'Customer Itch Cycle Management',
'version': '1.0',
'depends': [
'base',
- 'sale_stock'
+ 'sale_stock',
+ 'web'
],
'author': 'Benoit Vézina',
'category': 'Sales Management',
@@ -17,8 +17,15 @@
'views/res_partner_view.xml',
'views/product_category_view.xml',
'security/ir.model.access.csv'
-
],
+ 'assets': {
+ 'web.assets_backend': [
+ 'customer_itch_cycle/static/src/js/cycle_product_partner_list_view.js',
+ ],
+ 'web.assets_backend_xml': [
+ 'customer_itch_cycle/static/src/xml/process_history_button.xml',
+ ],
+ },
'installable': True,
'application': False,
}
diff --git a/customer_itch_cycle/models/itch_cycle_product_partner.py b/customer_itch_cycle/models/itch_cycle_product_partner.py
index d443659..0004478 100644
--- a/customer_itch_cycle/models/itch_cycle_product_partner.py
+++ b/customer_itch_cycle/models/itch_cycle_product_partner.py
@@ -1,4 +1,5 @@
from datetime import datetime, timedelta
+
import numpy as np
from odoo import models, fields, api
from odoo.exceptions import UserError
@@ -11,167 +12,193 @@ class ItchCycleProductPartner(models.Model):
partner_id = fields.Many2one(
comodel_name='res.partner',
string="Client",
+ help="Client associé à ce cycle",
required=True
)
product_id = fields.Many2one(
comodel_name='product.product',
string="Produit",
+ help="Produit associé à ce cycle",
required=True
)
- quantity_total_ordered = fields.Float(
- string="Quantité totale commandée",
- compute="_compute_quantity_total_ordered",
- store=True
- )
-
- quantity_of_orders = fields.Integer(
- string="Nombre de commandes",
- compute="_compute_quantity_of_orders",
- store=True
- )
-
- min_quantity_ordered = fields.Float(
- string="Quantité minimale commandée",
- compute="_compute_min_quantity_ordered",
- store=True
- )
-
- max_quantity_ordered = fields.Float(
- string="Quantité maximale commandée",
- compute="_compute_max_quantity_ordered",
- store=True
- )
-
- mean_quantity_ordered = fields.Float(
- string="Quantité moyenne commandée",
- compute="_compute_mean_quantity_ordered",
- store=True
- )
-
- average_cycle = fields.Integer(
- string="Cycle moyen (jours)",
- compute="_compute_average_cycle",
- store=True
- )
-
- manualy_set_cycle = fields.Integer(
- string="Cycle moyen (jours) défini manuellement"
- )
-
- manualy_set_qty = fields.Float(
- string="Quantité défini manuellement"
- )
-
- next_expected_date = fields.Date(
- string="Prochaine vente prévue",
- compute="_compute_next_expected_date",
- store=True
- )
-
- manualy_set_next_date = fields.Date(
- string="Prochaine vente prévue défini manuellement"
- )
-
- calc_next_date = fields.Date(
- string="Prochaine vente prévue calculée",
- compute="_compute_next_expected_date",
- store=True
- )
-
- calc_average_cycle = fields.Integer(
- string="Cycle moyen (jours) calculé",
- compute="_compute_itch_cycle_duration",
- store=True
- )
-
- calc_average_qty = fields.Float(
- string="Quantité moyenne calculée",
- compute="_compute_mean_quantity_ordered",
- store=True
- )
-
- @api.depends('manualy_set_cycle','average_cycle')
- def _compute_itch_cycle_duration(self):
- for record in self:
- if record.manualy_set_cycle:
- record.calc_average_cycle = record.manualy_set_cycle
- else:
- record.calc_average_cycle = record.average_cycle
-
- @api.depends('manualy_set_qty','max_quantity_ordered')
- def _compute_mean_quantity_ordered(self):
- for record in self:
- if record.manualy_set_qty:
- record.calc_average_qty = record.manualy_set_qty
- else:
- record.calc_average_qty = record.max_quantity
-
- 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',
+ help="Lignes de commande",
string="Lignes de commande"
)
- next_follow_up_date = fields.Date(
+ # Quantités: La plus part des champs sont informatifs et calculés automatiquement
+
+ quantity_total_ordered = fields.Float(
+ string="Quantité totale commandée",
+ compute="_compute_sale_order_line_related_fields",
+ help="Quantité totale commandée par le client pour ce produit",
+ store=True
+ )
+
+ quantity_of_orders = fields.Integer(
+ string="Nombre de commandes",
+ compute="_compute_sale_order_line_related_fields",
+ help="Nombre de commandes passées par le client pour ce produit",
+ store=True
+ )
+
+ quantity_min_ordered = fields.Float(
+ string="Quantité minimale commandée",
+ compute="_compute_sale_order_line_related_fields",
+ help="Quantité minimale commandée par le client pour ce produit",
+ store=True
+ )
+
+ quantity_max_ordered = fields.Float(
+ string="Quantité maximale commandée",
+ compute="_compute_sale_order_line_related_fields",
+ help="Quantité maximale commandée par le client pour ce produit",
+ store=True
+ )
+
+ quantity_mean_ordered = fields.Float(
+ string="Quantité moyenne commandée",
+ compute="_compute_sale_order_line_related_fields",
+ help="Quantité moyenne commandée par le client pour ce produit",
+ store=True
+ )
+
+ quantity_manual_override = fields.Float(
+ string="Quantité défini manuellement",
+ help="Quantité défini manuellement"
+ )
+
+ quantity_planned = fields.Float(
+ string="Quantité prévue",
+ help="Quantité prévue pour la prochaine commande",
+ compute="_compute_quantity_planned",
+ store=True
+ )
+
+ # Cycles: La plupart des champs sont informatifs et calculés automatiquement
+
+ cycle_duration_calculated = fields.Integer(
+ string="Cycle moyen (jours) calculé",
+ compute="_compute_average_cycle",
+ help="Cycle moyen calculé en jours",
+ store=True
+ )
+
+ cycle_duration_override = fields.Integer(
+ string="Cycle moyen (jours) défini manuellement",
+ help="Cycle moyen défini manuellement en jours"
+ )
+
+ cycle_duration = fields.Integer(
+ string="Cycle moyen (jours)",
+ compute="_compute_itch_cycle_duration",
+ help="Cycle moyen en jours",
+ store=True
+ )
+
+ # Dates:
+
+ date_expected_evaluated = fields.Date(
+ string="Prochaine vente prévue par calcul",
+ compute="_compute_next_expected_date",
+ store=True
+ )
+
+ date_next_override = fields.Date(
+ string="Prochaine vente prévue défini manuellement",
+ help="Prochaine vente prévue défini manuellement"
+ )
+
+ date_expected = fields.Date(
+ string="Prochaine vente prévue",
+ compute="_compute_next_expected_date",
+ help="Prochaine vente prévue calculée",
+ store=True
+ )
+
+ date_next_follow_up = fields.Date(
string="Prochaine date de suivi",
compute="_compute_next_follow_up_date",
+ help="Prochaine date de suivi",
store=True,
readonly=False
)
- last_purchase_date = fields.Date(
+ date_last_purchase = fields.Date(
string="Date du dernier achat",
compute="_compute_last_purchase_date",
+ help="Date du dernier achat",
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
+ prediction_status = fields.Selection(
+ selection=[
+ ('on_time', 'À l’heure'),
+ ('delayed', 'Retardée'),
+ ('archived', 'Archivée')
+ ],
+ string="Statut de la prédiction",
+ help="Statut de la prédiction",
+ compute="_compute_prediction_status"
)
- @api.depends('average_cycle')
+ @api.depends('cycle_duration_override', 'cycle_duration_calculated')
def _compute_itch_cycle_duration(self):
- """Calcule automatiquement la durée du cycle."""
+ """
+ Calcule automatiquement la durée du cycle.
+ Si un cycle est défini manuellement, il est utilisé à la place.
+ Sinon, le cycle moyen calculé est utilisé.
+ """
for record in self:
- record.itch_cycle_duration = record.average_cycle or 0
+ if record.cycle_duration_override:
+ record.cycle_duration = record.cycle_duration_override
+ else:
+ record.cycle_duration = record.cycle_duration_calculated
+
+ @api.depends('quantity_manual_override', 'quantity_max_ordered')
+ def _compute_mean_quantity_ordered(self):
+ """
+ Calcule la quantité de la prochaine commande.
+ Si une quantité est définie manuellement, elle est utilisée sinon la quantité maximale est utilisée.
+ """
+ for record in self:
+ if record.quantity_manual_override:
+ record.calc_average_qty = record.quantity_manual_override
+ else:
+ record.calc_average_qty = record.quantity_max_ordered
@api.depends('sale_order_line_ids')
def _compute_last_purchase_date(self):
- """Met automatiquement à jour la date du dernier achat."""
+ """
+ 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
+ record.date_last_purchase = last_order[0].date_order if last_order else None
- @api.depends('next_expected_date')
+ @api.depends('date_expected')
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.
- """
+ 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:
+ if record.date_expected:
# 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)
+ record.date_next_follow_up = record.date_expected - timedelta(days=7)
else:
- record.next_follow_up_date = None
+ record.date_next_follow_up = None
@api.depends('sale_order_line_ids', 'product_id.categ_id')
def _compute_average_cycle(self):
+ """
+ Calcule le cycle moyen en jours.
+ """
for record in self:
product_category = record.product_id.categ_id
if product_category.seasonal_factor and product_category.season_months:
@@ -188,23 +215,31 @@ class ItchCycleProductPartner(models.Model):
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
+ record.cycle_duration_calculated = int(np.mean(intervals)) if intervals else 0
else:
- record.average_cycle = 0
+ record.cycle_duration_calculated = 0
- @api.depends('average_cycle')
+ @api.depends('cycle_duration')
def _compute_next_expected_date(self):
+ """
+ Calcule automatiquement la prochaine date de vente
+ """
for record in self:
- if record.average_cycle and record.sale_order_line_ids:
+ if record.cycle_duration 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)
+ record.date_expected_evaluated = fields.Date.from_string(last_date) + timedelta(days=record.cycle_duration)
else:
- record.next_expected_date = None
+ record.date_expected_evaluated = None
def _compute_prediction_status(self):
+ """
+ Calcule le statut de la prédiction.
+ """
today = fields.Date.context_today(self)
for record in self:
- if record.next_expected_date and record.next_expected_date < today:
+ if record.prediction_status == 'archived':
+ record.prediction_status = 'archived'
+ elif record.date_expected and record.date_expected < today:
record.prediction_status = 'delayed'
else:
record.prediction_status = 'on_time'
@@ -221,7 +256,6 @@ class ItchCycleProductPartner(models.Model):
('order_id.state', 'in', ['sale', 'done']), # Commandes confirmées ou terminées
('qty_delivered', '>', 0), # Quantité livrée supérieure à 0
('product_id', '!=', False) # Exclure les lignes sans produit
-# ('order_id.partner_id', '=', 5024659), # un client pour test
])
# Carte pour regrouper par couple (client, produit)
@@ -262,8 +296,8 @@ class ItchCycleProductPartner(models.Model):
'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é.")
+ 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:
@@ -274,26 +308,42 @@ class ItchCycleProductPartner(models.Model):
return True
@api.depends('sale_order_line_ids')
- def _compute_quantity_total_ordered(self):
+ def _compute_sale_order_line_related_fields(self):
+ """
+ Regroupe tous les calculs relatifs aux `sale.order.line`.
+ Calcule les statistiques principales : quantité totale, moyenne, minimum, maximum, etc.
+ """
for record in self:
- record.quantity_total_ordered = sum(record.sale_order_line_ids.mapped('product_uom_qty'))
+ # Récupérer toutes les quantités des lignes de commande
+ quantities = record.sale_order_line_ids.mapped('product_uom_qty')
- @api.depends('sale_order_line_ids')
- def _compute_quantity_of_orders(self):
- for record in self:
+ # Quantité totale
+ record.quantity_total_ordered = sum(quantities)
+
+ # Nombre de commandes (lignes individuelles)
record.quantity_of_orders = len(record.sale_order_line_ids)
- @api.depends('sale_order_line_ids')
- def _compute_min_quantity_ordered(self):
- for record in self:
- record.min_quantity_ordered = min(record.sale_order_line_ids.mapped('product_uom_qty'))
+ # Quantité minimale, maximale et moyenne
+ if quantities:
+ record.quantity_min_ordered = min(quantities)
+ record.quantity_max_ordered = max(quantities)
+ record.quantity_mean_ordered = np.mean(quantities)
+ else:
+ record.quantity_min_ordered = 0
+ record.quantity_max_ordered = 0
+ record.quantity_mean_ordered = 0
- @api.depends('sale_order_line_ids')
- def _compute_max_quantity_ordered(self):
- for record in self:
- record.max_quantity_ordered = max(record.sale_order_line_ids.mapped('product_uom_qty'))
+ # Dernière date d'achat (commande la plus récente)
+ last_order = record.sale_order_line_ids.mapped('order_id').sorted(key=lambda o: o.date_order, reverse=True)
+ record.date_last_purchase = last_order[0].date_order if last_order else None
- @api.depends('sale_order_line_ids')
- def _compute_mean_quantity_ordered(self):
+ @api.depends('quantity_manual_override', 'quantity_mean_ordered')
+ def _compute_quantity_planned(self):
+ """
+ Calcule la quantité prévue pour la prochaine commande.
+ """
for record in self:
- record.mean_quantity_ordered = np.mean(record.sale_order_line_ids.mapped('product_uom_qty'))
+ if record.quantity_manual_override:
+ record.quantity_planned = record.quantity_manual_override
+ else:
+ record.quantity_planned = record.quantity_mean_ordered
\ No newline at end of file
diff --git a/customer_itch_cycle/static/src/js/cycle_product_partner_list_view.js b/customer_itch_cycle/static/src/js/cycle_product_partner_list_view.js
new file mode 100644
index 0000000..eabe13c
--- /dev/null
+++ b/customer_itch_cycle/static/src/js/cycle_product_partner_list_view.js
@@ -0,0 +1,35 @@
+/** @odoo-module **/
+
+import { registry } from "@web/core/registry";
+import { listView } from "@web/views/list/list_view";
+import { ListController } from "@web/views/list/list_controller";
+
+class CycleProductPartnerListController extends ListController {
+ setup() {
+ super.setup();
+ this.orm = this.env.services.orm; // Service ORM pour effectuer des appels RPC
+ }
+
+ async ProcessHistoryButton() {
+ try {
+ // Appel RPC via le service ORM
+ await this.orm.call("itch.cycle.product.partner", "populate_from_past_orders", []);
+ this.displayNotification({
+ type: "success",
+ message: "L'action a été exécutée avec succès.",
+ });
+ } catch (error) {
+ this.displayNotification({
+ type: "danger",
+ message: `Une erreur s'est produite : ${error.message}`,
+ });
+ }
+ }
+}
+
+// Enregistrement de la vue personnalisée
+registry.category("views").add("process_history_button_in_tree", {
+ ...listView,
+ Controller: CycleProductPartnerListController,
+ buttonTemplate: "customer_itch_cycle.button_in_tree",
+});
\ No newline at end of file
diff --git a/customer_itch_cycle/static/src/xml/process_history_button.xml b/customer_itch_cycle/static/src/xml/process_history_button.xml
new file mode 100644
index 0000000..5768fdb
--- /dev/null
+++ b/customer_itch_cycle/static/src/xml/process_history_button.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
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 6696f36..da80ddf 100644
--- a/customer_itch_cycle/views/itch_cycle_product_partner_view.xml
+++ b/customer_itch_cycle/views/itch_cycle_product_partner_view.xml
@@ -1,16 +1,17 @@
-
+
itch.cycle.product.partner.tree
itch.cycle.product.partner
-
+
-
-
-
-
-
+
+
+
+
+
+
@@ -22,7 +23,7 @@
-
- Itch Cycles Produits
+
+ Product Cycle Management
itch.cycle.product.partner
tree,form
-
+
+ action="action_cycle_product_partner"/>
\ No newline at end of file
diff --git a/customer_itch_cycle/views/res_partner_view.xml b/customer_itch_cycle/views/res_partner_view.xml
index 5cd3564..cbac8b0 100644
--- a/customer_itch_cycle/views/res_partner_view.xml
+++ b/customer_itch_cycle/views/res_partner_view.xml
@@ -17,12 +17,10 @@
-
-
-
-
-
-
+
+
+
+