This commit is contained in:
Benoît Vézina 2024-12-19 10:16:30 -05:00
parent f3a7072314
commit b06e9c4569
6 changed files with 284 additions and 178 deletions

View file

@ -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,
}

View file

@ -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', 'À lheure'),
('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', 'À lheure'),
('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

View file

@ -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",
});

View file

@ -0,0 +1,5 @@
<template id="process_history_button_in_tree" name="Button in Tree">
<t t-call="web.Button" class="btn-primary" t-on-click="ProcessHistoryButton">
<t t-esc="__('Mettre à jour depuis commandes passées')"/>
</t>
</template>

View file

@ -1,16 +1,17 @@
<odoo>
<record id="view_itch_cycle_product_partner_tree" model="ir.ui.view">
<record id="view_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>
<tree js_class="cycle_product_partner_list">
<field name="partner_id"/>
<field name="product_id"/>
<field name="itch_cycle_duration"/>
<field name="average_cycle"/>
<field name="last_purchase_date"/>
<field name="next_expected_date"/>
<field name="next_follow_up_date"/>
<field name="quantity_planned"/>
<field name="cycle_duration"/>
<field name="cycle_duration_calculated"/>
<field name="date_last_purchase"/>
<field name="date_expected"/>
<field name="date_next_follow_up"/>
<field name="prediction_status"/>
</tree>
</field>
@ -22,7 +23,7 @@
<field name="arch" type="xml">
<form>
<header>
<field name="prediction_status" widget="statusbar" options="{'on_time': 'green', 'delayed': 'red'}"/>
<field name="prediction_status" widget="statusbar" options="{'on_time': 'green', 'delayed': 'red', 'archived': 'gray'}"/>
</header>
<sheet>
<group>
@ -30,22 +31,31 @@
<field name="partner_id"/>
<field name="product_id"/>
<group string="Surchage manuelle">
<field name="manualy_set_cycle"/>
<field name="manualy_set_qty"/>
<field name="manualy_set_next_date"/>
<field name="cycle_duration_override"/>
<field name="quantity_manual_override"/>
<field name="date_next_override"/>
</group>
<group string="Statut">
<field name="date_expected" readonly="1"/>
<field name="cycle_duration" readonly="1"/>
<field name="quantity_planned" readonly="1"/>
<field name="prediction_status" readonly="1"/>
</group>
</group>
<group string="Statistiques">
<field name="itch_cycle_duration" readonly="1"/>
<field name="average_cycle" readonly="1"/>
<field name="last_purchase_date" readonly="1"/>
<field name="next_expected_date" readonly="1"/>
<field name="next_follow_up_date"/>
<field name="quantity_of_orders" readonly="1"/>
<field name="quantity_total_ordered" readonly="1"/>
<field name="min_quantity_ordered" readonly="1"/>
<field name="max_quantity_ordered" readonly="1"/>
<field name="mean_quantity_ordered" readonly="1"/>
<group string="Statistiques de commandes">
<field name="quantity_of_orders" readonly="1"/>
<field name="quantity_total_ordered" readonly="1"/>
<field name="quantity_min_ordered" readonly="1"/>
<field name="quantity_max_ordered" readonly="1"/>
<field name="quantity_mean_ordered" readonly="1"/>
</group>
<group string="Statistiques de cycle">
<field name="date_last_purchase" readonly="1"/>
<field name="date_next_follow_up" readonly="1"/>
<field name="cycle_duration_calculated" readonly="1"/>
<field name="date_expected_evaluated" readonly="1"/>
</group>
</group>
</group>
<notebook>
@ -67,16 +77,17 @@
</field>
</record>
<record id="action_itch_cycle_product_partner" model="ir.actions.act_window">
<field name="name">Itch Cycles Produits</field>
<record id="action_cycle_product_partner" model="ir.actions.act_window">
<field name="name">Product Cycle Management</field>
<field name="res_model">itch.cycle.product.partner</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_itch_cycle_product_partner_tree"/>
<field name="view_id" ref="view_cycle_product_partner_tree"/>
</record>
<menuitem id="menu_itch_cycle_product_root"
name="Itch Cycle"
sequence="10"
name="Product Cycle"
sequence="5"
parent="sale.menu_sale_report"
action="action_itch_cycle_product_partner"/>
action="action_cycle_product_partner"/>
</odoo>

View file

@ -17,12 +17,10 @@
<field name="reorder_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"/>
<field name="min_quantity_ordered"/>
<field name="mean_quantity_ordered"/>
<field name="max_quantity_ordered"/>
<field name="date_last_purchase"/>
<field name="cycle_duration"/>
<field name="date_next_follow_up"/>
<field name="quantity_planned"/>
<field name="quantity_of_orders"/>
<field name="quantity_total_ordered"/>
</tree>