itch work?

This commit is contained in:
Benoît Vézina 2025-01-02 08:06:56 -05:00
parent d644841f99
commit 010752b01f
22 changed files with 1517 additions and 338 deletions

View file

@ -1,2 +1,2 @@
from . import models
from . import wizards

View file

@ -1,24 +1,37 @@
{
'name': 'Customer Itch Cycle Management',
'version': '1.0',
'name': 'Customer Itch Cycle',
'version': '17.0.1.0.0',
'category': 'Sales',
'summary': 'Manage customer product cycles and predict future sales',
'description': """
This module helps track and manage customer purchasing cycles:
* Track product purchase cycles per customer
* Predict next purchase dates
* Generate opportunities based on predictions
* Monitor delayed purchases
""",
'author': 'DurPro',
'website': 'https://www.durpro.com',
'depends': [
'base',
'sale_stock',
'web'
'sale',
'sale_management',
'crm',
'sale_crm',
'base_automation',
],
'author': 'Benoit Vézina',
'category': 'Sales Management',
'summary': 'Manage customer itch cycles by product for proactive sales engagement.',
'website': 'https://www.bemade.org',
'description': "Manage customer itch cycles by product for proactive sales engagement.",
'license': 'AGPL-3',
'data': [
'security/ir.model.access.csv',
'data/month_data.xml',
'wizards/apply_to_child_categories.xml',
'views/itch_cycle_product_partner_view.xml',
'views/res_partner_view.xml',
'views/product_category_view.xml',
'views/itch_cycle_actions.xml',
'views/itch_cycle_menu.xml',
'security/ir.model.access.csv'
'views/crm_lead_view.xml',
'views/product_category_view.xml',
'data/ir_cron_data.xml',
# 'data/crm_automation_data.xml',
],
'assets': {
'web.assets_backend': [
@ -27,5 +40,6 @@
],
},
'installable': True,
'application': False,
'application': True,
'license': 'LGPL-3',
}

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="action_reprocess_all_itch_cycles" model="ir.actions.server">
<field name="name">Reprocess All Cycles</field>
<field name="model_id" ref="model_itch_cycle_product_partner"/>
<field name="binding_model_id" ref="model_itch_cycle_product_partner"/>
<field name="binding_view_types">list,form</field>
<field name="state">code</field>
<field name="code">
env['itch.cycle.product.partner'].populate_from_past_orders()
</field>
</record>
</odoo>

View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Automated Task for Creating Opportunities -->
<record id="ir_cron_create_opportunities" model="ir.cron">
<field name="name">Sales Cycles: Create Opportunities</field>
<field name="model_id" ref="model_itch_cycle_product_partner"/>
<field name="state">code</field>
<field name="code">model._cron_create_opportunities()</field>
<field name="user_id" ref="base.user_root"/>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False"/>
<field name="priority">5</field>
<field name="active" eval="True"/>
</record>
<!-- Automated Task for Processing History -->
<record id="ir_cron_process_history" model="ir.cron">
<field name="name">Sales Cycles: Process History</field>
<field name="model_id" ref="model_itch_cycle_product_partner"/>
<field name="state">code</field>
<field name="code">model.populate_from_past_orders()</field>
<field name="user_id" ref="base.user_root"/>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False"/>
<field name="priority">10</field>
<field name="active" eval="True"/>
</record>
</data>
</odoo>

View file

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Initialize months -->
<record id="month_01" model="itch.month">
<field name="name">January</field>
<field name="sequence">1</field>
<field name="number">1</field>
</record>
<record id="month_02" model="itch.month">
<field name="name">February</field>
<field name="sequence">2</field>
<field name="number">2</field>
</record>
<record id="month_03" model="itch.month">
<field name="name">March</field>
<field name="sequence">3</field>
<field name="number">3</field>
</record>
<record id="month_04" model="itch.month">
<field name="name">April</field>
<field name="sequence">4</field>
<field name="number">4</field>
</record>
<record id="month_05" model="itch.month">
<field name="name">May</field>
<field name="sequence">5</field>
<field name="number">5</field>
</record>
<record id="month_06" model="itch.month">
<field name="name">June</field>
<field name="sequence">6</field>
<field name="number">6</field>
</record>
<record id="month_07" model="itch.month">
<field name="name">July</field>
<field name="sequence">7</field>
<field name="number">7</field>
</record>
<record id="month_08" model="itch.month">
<field name="name">August</field>
<field name="sequence">8</field>
<field name="number">8</field>
</record>
<record id="month_09" model="itch.month">
<field name="name">September</field>
<field name="sequence">9</field>
<field name="number">9</field>
</record>
<record id="month_10" model="itch.month">
<field name="name">October</field>
<field name="sequence">10</field>
<field name="number">10</field>
</record>
<record id="month_11" model="itch.month">
<field name="name">November</field>
<field name="sequence">11</field>
<field name="number">11</field>
</record>
<record id="month_12" model="itch.month">
<field name="name">December</field>
<field name="sequence">12</field>
<field name="number">12</field>
</record>
</data>
</odoo>

View file

@ -2,3 +2,4 @@ from . import itch_cycle_product_partner
from . import sale_order_line
from . import product_category
from . import res_partner
from . import crm_lead

View file

@ -0,0 +1,42 @@
from odoo import models, fields
class CrmLead(models.Model):
"""
Extend CRM Lead model to add purchase cycle information.
Adds a link to the purchase cycle associated with the opportunity.
This allows tracking customer purchasing patterns and forecasting
future opportunities based on historical data.
"""
_inherit = 'crm.lead'
itch_cycle_id = fields.Many2one(
comodel_name='itch.cycle.product.partner',
string='Purchase Cycle',
ondelete='set null',
help="""
Linked purchase cycle for this opportunity.
Used to track the customer's purchasing patterns.
"""
)
date_last_purchase = fields.Date(
related='itch_cycle_id.date_last_purchase',
string='Last Purchase Date',
readonly=True
)
cycle_duration = fields.Integer(
related='itch_cycle_id.cycle_duration',
string='Cycle Duration (days)',
readonly=True
)
cycle_sales = fields.One2many(
comodel_name='sale.order.line',
related='itch_cycle_id.sale_order_line_ids',
string='Cycle Sales',
readonly=True
)

File diff suppressed because it is too large Load diff

View file

@ -1,15 +1,124 @@
from odoo import models, fields, api
from odoo.exceptions import ValidationError
import logging
_logger = logging.getLogger(__name__)
class Month(models.Model):
"""
Model to represent months for seasonal tracking.
This allows for better organization and selection of active months
for seasonal product categories.
"""
_name = 'itch.month'
_description = 'Month for Seasonal Tracking'
_order = 'sequence'
name = fields.Char(string='Name', required=True, translate=True)
sequence = fields.Integer(string='Sequence', required=True)
number = fields.Integer(string='Month Number', required=True)
_sql_constraints = [
('unique_number', 'unique(number)', 'Month number must be unique!'),
('number_range', 'CHECK(number >= 1 AND number <= 12)', 'Month number must be between 1 and 12!')
]
from odoo import models, fields
class ProductCategory(models.Model):
"""
Extends the product category model to add seasonal behavior functionality.
This is used in the sales cycle calculation to better predict next sales
by taking into account seasonal patterns.
"""
_inherit = 'product.category'
seasonal_factor = fields.Boolean(
string="Catégorie saisonnière",
default=False
is_cycle_tracked = fields.Boolean(
string="Track Sales Cycle",
help="If enabled, products in this category will be tracked by the sales cycle system",
default=True,
)
season_months = fields.Char(
string="Mois actifs",
help="Mois pendant lesquels cette catégorie est active (ex: '3,4,5' pour mars-mai)"
temp_is_cycle_tracked = fields.Boolean(
string="Temporary Track Sales Cycle",
help="Technical field to track changes in is_cycle_tracked",
default=True,
)
@api.onchange('is_cycle_tracked')
def _onchange_is_cycle_tracked(self):
"""Store the new value temporarily and restore the original value"""
if self.id and self.is_cycle_tracked != self.temp_is_cycle_tracked:
self.temp_is_cycle_tracked = self.is_cycle_tracked
self.is_cycle_tracked = not self.is_cycle_tracked
return {
'warning': {
'title': 'Confirmation Required',
'message': 'Please use the "Apply Changes" button to change tracking status.'
}
}
def action_apply_cycle_tracked(self):
"""Open wizard to apply cycle tracked changes"""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': 'Apply to Child Categories',
'res_model': 'itch.apply.to.child.categories',
'view_mode': 'form',
'target': 'new',
'context': {
'default_category_id': self.id,
'default_new_value': self.temp_is_cycle_tracked,
}
}
def write(self, vals):
"""
Override write to handle changes in is_cycle_tracked field.
If a category is no longer tracked, we archive all related cycles.
"""
if 'is_cycle_tracked' in vals and not vals['is_cycle_tracked']:
# Find all cycles for products in this category
cycles = self.env['itch.cycle.product.partner'].search([
('product_id.categ_id', 'in', self.ids)
])
if cycles:
# Archive the cycles
cycles.write({
'active': False,
'notes': f'{fields.Date.today()}: Archived automatically - Category no longer tracked'
})
# Log the action
_logger.info(
f"Archived {len(cycles)} cycles due to category {self.name} "
f"being marked as not tracked"
)
return super().write(vals)
seasonal_factor = fields.Boolean(
string="Seasonal Category",
help="Enable this if the products in this category have seasonal sales patterns",
default=False,
)
season_months = fields.Many2many(
'itch.month',
string="Active Months",
help="Select the months when this category is typically active",
)
@api.constrains('season_months')
def _check_season_months(self):
"""
Validate the selection of season_months field.
Rules:
- Cannot be empty if seasonal_factor is True
"""
for record in self:
if record.seasonal_factor and not record.season_months:
raise ValidationError("Active months must be specified for seasonal categories")

View file

@ -1,43 +1,53 @@
from odoo import models, fields, api, _
from odoo.exceptions import UserError
from datetime import datetime
from odoo import api, fields, models
class ResPartner(models.Model):
"""
Extends the partner model to add sales cycle tracking functionality.
"""
_inherit = 'res.partner'
itch_cycle_product_ids = fields.One2many(
comodel_name='itch.cycle.product.partner',
inverse_name='partner_id',
string="Itch Cycles Products"
string="Product Sales Cycles",
help="List of all product sales cycles associated with this customer",
)
reorder_cycle_product_ids = fields.One2many(
comodel_name='itch.cycle.product.partner',
inverse_name='partner_id',
string="Reorder Cycles Products",
domain=[('quantity_of_orders', '>', 1)]
string="Active Sales Cycles",
domain=[('quantity_of_orders', '>', 1)],
help="List of product sales cycles with established patterns",
)
itch_next_delay = fields.Date(
string="Prochaine Date de Suivi (Itch-Cycle Min)",
# compute="_compute_itch_next_delay",
store=True
string="Next Follow-up Date",
compute="_compute_itch_next_delay",
store=True,
help="Earliest follow-up date",
)
@api.depends('itch_cycle_product_ids.next_follow_up_date')
@api.depends(
'itch_cycle_product_ids',
'itch_cycle_product_ids.date_next_follow_up'
)
def _compute_itch_next_delay(self):
"""
Calcule la date de suivi minimale parmi tous les cycles de produits associés.
"""
Compute the earliest follow-up date.
The date is calculated from all associated product cycles.
"""
for partner in self:
follow_up_dates = partner.reorder_cycle_product_ids.mapped('next_follow_up_date')
if follow_up_dates:
partner.itch_next_delay = min(follow_up_dates)
else:
partner.itch_next_delay = False
cycles = partner.reorder_cycle_product_ids
dates = cycles.mapped('date_next_follow_up')
partner.itch_next_delay = min(dates) if 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.
Initialize or update product cycles.
The cycles are created from historical order data.
"""
self.env['itch.cycle.product.partner'].populate_from_past_orders()
self.env['itch.cycle.product.partner'].populate_from_past_orders()

View file

@ -1,43 +1,51 @@
from odoo import models, fields, api
class SaleOrderLine(models.Model):
"""
Extends sale order line to add itch cycle tracking.
"""
_inherit = 'sale.order.line'
itch_cycle_id = fields.Many2one(
comodel_name='itch.cycle.product.partner',
string="Cycle Produit/Partenaire"
)
string="Cycle")
stock_move_ids = fields.One2many(
comodel_name='stock.move',
inverse_name='sale_line_id',
string="Mouvements de stock"
)
string="Movements")
sale_date = fields.Datetime(
string="Date de vente",
string="Date",
related='order_id.date_order',
store=True
)
store=True)
@api.model_create_multi
def create(self, vals_list):
# Appeler le super pour créer les lignes
"""
Create sale order lines and associate them with their itch cycle.
If the product is cycle tracked and no existing cycle exists for the
product/partner combination, a new cycle is created.
"""
lines = super().create(vals_list)
# Associer les lignes nouvellement créées avec leur cycle
for line in lines:
if line.product_id and line.order_id:
if (line.product_id and line.order_id and
line.product_id.categ_id.is_cycle_tracked):
partner_id = line.order_id.partner_id.id
itch_cycle = self.env['itch.cycle.product.partner'].search([
('partner_id', '=', partner_id),
('product_id', '=', line.product_id.id)
], limit=1)
if not itch_cycle:
itch_cycle = self.env['itch.cycle.product.partner'].create({
cycle_vals = {
'partner_id': partner_id,
'product_id': line.product_id.id,
})
}
itch_cycle = self.env['itch.cycle.product.partner'].create(
cycle_vals)
line.itch_cycle_id = itch_cycle.id
return lines
return lines

View file

@ -1,2 +1,5 @@
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
access_itch_cycle_product_partner,itch.cycle.product.partner access,model_itch_cycle_product_partner,base.group_user,1,1,1,1
access_itch_month_user,itch.month access user,model_itch_month,base.group_user,1,0,0,0
access_itch_month_manager,itch.month access manager,model_itch_month,sales_team.group_sale_manager,1,1,1,1
access_itch_apply_to_child_categories,itch.apply.to.child.categories access,model_itch_apply_to_child_categories,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_itch_cycle_product_partner itch.cycle.product.partner access model_itch_cycle_product_partner base.group_user 1 1 1 1
3 access_itch_month_user itch.month access user model_itch_month base.group_user 1 0 0 0
4 access_itch_month_manager itch.month access manager model_itch_month sales_team.group_sale_manager 1 1 1 1
5 access_itch_apply_to_child_categories itch.apply.to.child.categories access model_itch_apply_to_child_categories base.group_user 1 1 1 1

View file

@ -5,6 +5,10 @@ import { listView } from "@web/views/list/list_view";
import { ListController } from "@web/views/list/list_controller";
import { useService } from "@web/core/utils/hooks";
/**
* Custom List Controller for Product/Partner Cycle Management
* Extends standard list view to add history processing functionality
*/
class CycleProductPartnerListController extends ListController {
setup() {
super.setup();
@ -12,22 +16,41 @@ class CycleProductPartnerListController extends ListController {
this.notification = useService("notification");
}
/**
* Process historical sales data to update cycles
* Shows notifications for processing status
*/
async ProcessHistoryButton() {
try {
await this.orm.call("itch.cycle.product.partner", "populate_from_past_orders", []);
this.notification.add("L'action a été exécutée avec succès.", {
type: "success",
this.notification.add("Starting history processing...", {
type: "info",
sticky: false,
});
const result = await this.orm.call("itch.cycle.product.partner", "populate_from_past_orders", []);
if (result && result.tag === 'display_notifications' && result.params.notifications) {
for (const notif of result.params.notifications) {
this.notification.add(notif.params.message, {
type: notif.params.type,
sticky: notif.params.sticky,
});
await new Promise(resolve => setTimeout(resolve, 300));
}
}
await this.model.load();
} catch (error) {
this.notification.add(`Une erreur s'est produite : ${error.message}`, {
this.notification.add(`An error occurred: ${error.message}`, {
type: "danger",
});
}
}
}
// Register the custom list view for cycle product partner
registry.category("views").add("cycle_product_partner_list", {
...listView,
Controller: CycleProductPartnerListController,
buttonTemplate: "customer_itch_cycle.process_history_button_in_tree",
});
buttonTemplate: "customer_itch_cycle.ListButtons",
});

View file

@ -1,8 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="customer_itch_cycle.process_history_button_in_tree" name="Button in Tree">
<button class="btn btn-primary" t-on-click="ProcessHistoryButton">
Mettre à jour depuis commandes passées
</button>
<t t-name="customer_itch_cycle.ListButtons" name="ProcessHistoryButton">
<div>
<button class="btn btn-primary" t-on-click="ProcessHistoryButton">
Update from Sales History
</button>
</div>
</t>
</templates>

View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- Add itch cycle field to CRM lead form -->
<record id="view_crm_lead_form_inherit_itch_cycle" model="ir.ui.view">
<field name="name">crm.lead.form.inherit.itch.cycle</field>
<field name="model">crm.lead</field>
<field name="inherit_id" ref="crm.crm_lead_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_id']" position="after">
<field name="itch_cycle_id" widget="many2one"/>
</xpath>
</field>
</record>
<!-- Add itch cycle tab to CRM lead form -->
<record id="view_crm_lead_form_inherit_itch_cycle_tab" model="ir.ui.view">
<field name="name">crm.lead.form.inherit.itch.cycle.tab</field>
<field name="model">crm.lead</field>
<field name="inherit_id" ref="crm.crm_lead_view_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="Purchase Cycle">
<field name="itch_cycle_id" readonly="1"/>
</page>
<page string="Opportunities">
<tree>
<field name="expected_revenue"/>
<field name="probability"/>
<field name="date_deadline"/>
<field name="stage_id"/>
<field name="tag_ids"/>
</tree>
<field name="description" widget="html"/>
</page>
</xpath>
</field>
</record>
<!-- Add itch cycle filter to CRM lead search -->
<record id="view_crm_lead_search_inherit_itch_cycle" model="ir.ui.view">
<field name="name">crm.lead.search.inherit.itch.cycle</field>
<field name="model">crm.lead</field>
<field name="inherit_id" ref="crm.view_crm_case_opportunities_filter"/>
<field name="arch" type="xml">
<xpath expr="//filter[@name='assigned_to_me']" position="after">
<filter string="Purchase Cycle" name="itch_cycle"
domain="[('itch_cycle_id','!=',False)]"/>
</xpath>
</field>
</record>
</odoo>

View file

@ -6,6 +6,8 @@
<field name="res_model">itch.cycle.product.partner</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_cycle_product_partner_tree"/>
<field name="domain">[('state', '!=', 'new'), ('active', '=', True)]
</field>
</record>
</odoo>

View file

@ -1,19 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="action_itch_cycle_product_partner" model="ir.actions.act_window">
<field name="name">Cycles Produit/Partenaire</field>
<field name="res_model">itch.cycle.product.partner</field>
<field name="view_mode">tree,form</field>
<field name="display_name">name_get</field>
</record>
<record id="action_create_batch_opportunities" model="ir.actions.server">
<field name="name">Create Opportunities</field>
<field name="model_id" ref="customer_itch_cycle.model_itch_cycle_product_partner"/>
<field name="state">code</field>
<field name="code">records.action_create_batch_opportunities()</field>
<field name="binding_model_id" ref="customer_itch_cycle.model_itch_cycle_product_partner"/>
<field name="binding_type">action</field>
<field name="binding_view_types">tree,form</field>
</record>
<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 js_class="cycle_product_partner_list">
<tree js_class="cycle_product_partner_list" decoration-bf="state == 'delayed'" decoration-it="state == 'archived'">
<field name="partner_id" widget="many2one_avatar"/>
<field name="partner_city"/>
<field name="partner_country_id" widget="country_flag" optional="hide"/>
<field name="product_id" widget="many2one_avatar"/>
<field name="product_categ_id" optional="hide"/>
<field name="quantity_planned" sum="Total prévu"/>
<field name="quantity_of_orders" sum="Total commandé"/>
<field name="cycle_duration" widget="duration"/>
<field name="date_last_purchase" optional="hide"/>
<field name="date_expected" optional="show" string="Date attendue"/>
<field name="date_next_follow_up" optional="show" string="Prochain suivi"/>
<field name="state" widget="badge" options="{'classes': {'new': 'bg-info', 'on_time': 'bg-success', 'delayed': 'bg-danger', 'archived': 'bg-secondary'}}"/>
<field name="active" invisible="1"/>
</tree>
</field>
</record>
<record id="view_itch_cycle_product_partner_search" model="ir.ui.view">
<field name="name">itch.cycle.product.partner.search</field>
<field name="model">itch.cycle.product.partner</field>
<field name="arch" type="xml">
<search>
<field name="partner_id"/>
<field name="product_id"/>
<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 name="state"/>
<separator/>
<filter string="Archivé" name="inactive" domain="[('active', '=', False)]"/>
<separator/>
<filter string="Suivi cette semaine" name="follow_up_this_week"
domain="[('date_next_follow_up', '>=', context_today().strftime('%Y-%m-%d')),
('date_next_follow_up', '&lt;=', (context_today() + datetime.timedelta(days=7)).strftime('%Y-%m-%d'))]"/>
<filter string="Commande attendue cette semaine" name="expected_this_week"
domain="[('date_expected', '>=', context_today().strftime('%Y-%m-%d')),
('date_expected', '&lt;=', (context_today() + datetime.timedelta(days=7)).strftime('%Y-%m-%d'))]"/>
<group expand="0" string="Grouper par">
<filter string="État" name="group_by_state" context="{'group_by': 'state'}"/>
<filter string="Partenaire" name="group_by_partner" context="{'group_by': 'partner_id'}"/>
<filter string="Ville" name="group_by_city" context="{'group_by': 'partner_id:city'}"/>
<filter string="Pays" name="group_by_country" context="{'group_by': 'partner_id:country_id'}"/>
<filter string="Produit" name="group_by_product" context="{'group_by': 'product_id'}"/>
<filter string="Catégorie de produit" name="group_by_product_categ" context="{'group_by': 'product_id:categ_id'}"/>
<filter string="Date de suivi" name="group_by_follow_up" context="{'group_by': 'date_next_follow_up:week'}"/>
<filter string="Date attendue" name="group_by_expected" context="{'group_by': 'date_expected:week'}"/>
</group>
</search>
</field>
</record>
@ -23,44 +76,86 @@
<field name="arch" type="xml">
<form>
<header>
<field name="prediction_status" widget="statusbar" options="{'on_time': 'green', 'delayed': 'red', 'archived': 'gray'}"/>
<field name="state" widget="statusbar" statusbar_visible="new,on_time,delayed,archived"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<widget name="web_ribbon" title="Archivé" bg_color="bg-secondary" invisible="active"/>
<button name="toggle_active"
type="object"
class="oe_stat_button"
icon="fa-archive"
help="Archiver/Désarchiver cet enregistrement">
<field name="active" widget="boolean_button" options="{'terminology': 'archive'}"/>
</button>
<button name="action_create_opportunity"
type="object"
string="Créer une opportunité"
class="oe_stat_button"
icon="fa-plus"
help="Créer une nouvelle opportunité pour ce partenaire et ce produit"/>
<button name="action_view_latest_opportunity"
type="object"
string="Dernière opportunité"
class="oe_stat_button"
icon="fa-eye"
help="Afficher la dernière opportunité créée pour ce partenaire et ce produit"/>
</div>
<group>
<group string="Informations principales">
<field name="partner_id"/>
<field name="product_id"/>
<group string="Surchage manuelle">
<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 string="Partenaire" col="2">
<field name="partner_id"
context="{'form_view_ref': 'base.view_partner_form'}"
options="{'always_reload': true}"
help="Partenaire associé à ce cycle"/>
<field name="partner_email" help="Adresse e-mail principale du partenaire"/>
<field name="partner_phone" help="Téléphone principal du partenaire"/>
<field name="partner_mobile" help="Téléphone mobile du partenaire"/>
<field name="partner_city" help="Ville du partenaire"/>
<field name="partner_country_id" help="Pays du partenaire"/>
</group>
<group string="Statistiques">
<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 string="Produit" col="2">
<field name="product_id"
context="{'form_view_ref': 'product.product_normal_form_view'}"
options="{'always_reload': true}"
help="Produit associé à ce cycle"/>
<field name="product_categ_id" help="Catégorie de produit"/>
<field name="product_type" help="Type de produit (stockable, consommable, service)"/>
<field name="product_lst_price" widget="monetary" help="Prix de vente du produit"/>
<field name="product_qty_available" help="Quantité disponible en stock"/>
</group>
</group>
<group>
<group string="Remplacement manuel" col="2">
<field name="cycle_duration_override" help="Remplacement manuel de la durée du cycle"/>
<field name="quantity_manual_override" help="Remplacement manuel de la quantité prévue"/>
<field name="date_expected_override" help="Remplacement manuel de la date attendue"/>
</group>
<group string="Statut" col="2">
<field name="date_expected" readonly="1" widget="date" help="Date attendue calculée"/>
<field name="cycle_duration" readonly="1" widget="duration" help="Durée du cycle calculée"/>
<field name="quantity_planned" readonly="1" widget="float_time" help="Quantité prévue calculée"/>
<field name="state" readonly="1" widget="badge" help="Statut actuel du cycle"/>
<field name="active" invisible="1"/>
</group>
</group>
<group>
<group string="Statistiques de commande" col="2">
<field name="quantity_of_orders" readonly="1" help="Nombre total de commandes"/>
<field name="quantity_total_ordered" readonly="1" help="Quantité totale commandée"/>
<field name="quantity_min_ordered" readonly="1" help="Quantité minimale commandée"/>
<field name="quantity_max_ordered" readonly="1" help="Quantité maximale commandée"/>
<field name="quantity_mean_ordered" readonly="1" help="Quantité moyenne commandée"/>
</group>
<group string="Statistiques de cycle" col="2">
<field name="date_last_purchase" readonly="1" widget="date" help="Date de la dernière commande"/>
<field name="date_next_follow_up" widget="date" options="{'datepicker': {'minDate': context_today().strftime('%Y-%m-%d')}}" help="Date du prochain suivi"/>
<field name="cycle_duration_calculated" readonly="1" widget="duration" help="Durée du cycle calculée"/>
<field name="date_expected_evaluated" readonly="1" widget="date" help="Date attendue évaluée"/>
</group>
</group>
<notebook>
<page string="Lignes de commandes associées">
<field name="sale_order_line_ids">
<page string="Lignes de commande associées">
<field name="sale_order_line_ids" readonly="1">
<tree editable="bottom">
<field name="order_id"/>
<field name="product_id"/>
@ -71,9 +166,26 @@
</tree>
</field>
</page>
<page string="Opportunités associées">
<field name="opportunity_ids" readonly="1">
<tree>
<field name="name"/>
<field name="partner_id"/>
<field name="expected_revenue"/>
<field name="probability"/>
<field name="stage_id"/>
<field name="create_date"/>
</tree>
</field>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="activity_ids"/>
<field name="message_ids"/>
</div>
</form>
</field>
</record>
</odoo>
</odoo>

View file

@ -1,21 +1,30 @@
<odoo>
<record id="view_product_category_form" model="ir.ui.view">
<field name="name">product.category.form</field>
<field name="model">product.category</field>
<field name="inherit_id" ref="product.product_category_form_view"/>
<field name="arch" type="xml">
<form string="Catégorie de produit">
<sheet>
<xpath expr="//form[1]/sheet[1]" position="inside">
<group string="Sales Cycle">
<group>
<field name="name"/>
<field name="parent_id"/>
<field name="is_cycle_tracked"/>
<field name="temp_is_cycle_tracked" invisible="1"/>
<button name="action_apply_cycle_tracked"
string="Apply Changes"
type="object"
class="btn btn-primary"
invisible="is_cycle_tracked == temp_is_cycle_tracked"/>
<field name="seasonal_factor"
help="Factor used to adjust sales predictions based on seasonal patterns"/>
<field name="season_months"
widget="many2many_tags"
options="{'no_create': True}"
help="Months where this product category has peak sales"
invisible="not seasonal_factor"
required="seasonal_factor"/>
</group>
<group string="Saisonnalité">
<field name="seasonal_factor"/>
<field name="season_months"/>
</group>
</sheet>
</form>
</group>
</xpath>
</field>
</record>
</odoo>

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_partner_form_inherit_itch_cycle" model="ir.ui.view">
<field name="name">res.partner.form.itch.cycle</field>
@ -6,27 +6,84 @@
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<notebook position="inside">
<page string="Itch Cycle" name="itch_cycle">
<button name="action_populate_itch_cycles"
string="Traiter l'historique des cycles"
type="object"
class="btn-primary"/>
<group string="Itch Cycle Information">
<field name="itch_next_delay" readonly="1"/>
</group>
<field name="reorder_cycle_product_ids" readonly="1">
<page string="Product Cycles" name="product_cycles">
<field name="itch_cycle_product_ids">
<tree>
<field name="product_id"/>
<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"/>
<field name="quantity_mean_ordered"/>
<field name="date_expected"/>
<field name="active"/>
</tree>
</field>
</page>
</notebook>
</field>
</record>
<record id="view_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>
<header>
<button name="create_opportunity"
string="Create Opportunity"
type="object"
class="oe_highlight"
invisible="not active"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="action_view_opportunities"
type="object"
class="oe_stat_button"
icon="fa-star">
<field name="opportunity_count" widget="statinfo" string="Opportunities"/>
</button>
</div>
<group>
<group>
<field name="partner_id"/>
<field name="product_id"/>
<field name="active"/>
</group>
<group>
<field name="date_expected"/>
<field name="cycle_duration"/>
<field name="quantity_mean_ordered"/>
<field name="quantity_total_ordered"/>
</group>
</group>
<notebook>
<page string="Opportunities" name="opportunities">
<field name="opportunity_ids">
<tree>
<field name="name"/>
<field name="date_deadline"/>
<field name="expected_revenue"/>
<field name="stage_id"/>
</tree>
</field>
</page>
<page string="Sales History" name="sales_history">
<field name="sale_order_line_ids">
<tree>
<field name="order_id"/>
<field name="product_uom_qty"/>
<field name="price_unit"/>
<field name="price_subtotal"/>
<field name="state"/>
</tree>
</field>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" groups="base.group_user"/>
<field name="activity_ids"/>
<field name="message_ids"/>
</div>
</form>
</field>
</record>
</odoo>

View file

@ -0,0 +1 @@
from . import apply_to_child_categories

View file

@ -0,0 +1,48 @@
from odoo import models, fields, api
class ApplyToChildCategories(models.TransientModel):
"""
Wizard to apply cycle tracking settings to child categories
This wizard is shown when changing is_cycle_tracked on a product category
"""
_name = 'itch.apply.to.child.categories'
_description = 'Apply Settings to Child Categories'
category_id = fields.Many2one('product.category', string='Category', required=True)
new_value = fields.Boolean(string='New Tracking Value')
apply_to_children = fields.Boolean(
string='Apply to Child Categories',
help='If checked, the cycle tracking settings will be applied to all child categories',
default=True
)
child_count = fields.Integer(
string='Number of Child Categories',
compute='_compute_child_count'
)
@api.depends('category_id')
def _compute_child_count(self):
"""Compute the number of child categories that would be affected"""
for wizard in self:
wizard.child_count = self.env['product.category'].search_count([
('id', 'child_of', wizard.category_id.id),
('id', '!=', wizard.category_id.id)
])
def apply_settings(self):
"""Apply the settings to the selected categories"""
self.ensure_one()
if self.apply_to_children:
categories = self.env['product.category'].search([
('id', 'child_of', self.category_id.id)
])
else:
categories = self.category_id
# Apply the settings
values = {
'is_cycle_tracked': self.new_value,
'temp_is_cycle_tracked': self.new_value,
}
categories.write(values)
return {'type': 'ir.actions.act_window_close'}

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_apply_to_child_categories_form" model="ir.ui.view">
<field name="name">itch.apply.to.child.categories.form</field>
<field name="model">itch.apply.to.child.categories</field>
<field name="arch" type="xml">
<form string="Apply to Child Categories">
<group>
<field name="category_id" invisible="1"/>
<field name="child_count"/>
<field name="apply_to_children"/>
</group>
<footer>
<button string="Apply" name="apply_settings" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
</footer>
</form>
</field>
</record>
</odoo>