[IMP] crm: better handle team deletion

PURPOSE

Make crm.team deletion effect explicit on main models (excluding wizards
or reports). Purpose is to ensure deleting a crm team has known side
effets.

SPECIFICATIONS

M2O on crm.team set to CASCADE

  * crm.lead.scoring.frequency: as we remove the team no need to keep the
    PLS data -> change to "cascade";
  * crm.team.member: when removing a team, remove its members as it prevents
    from unlinking the team;

M2O on crm.team set to SET NULL

  * account.move: void team_id field but track it to keep history;
  * crm.lead: void team_id field as it will be taken back into assign
    process (either manual or automatic), and will therefore be managed
    by other sales persons. In case of archived / lost leads setting to
    void has no real issue;
  * crm.stage: void team_id and set stage as shared to avoid loosing stage
    information for existing leads in that stage;
  * res.partner: void team_id taking care of this customer as this field
    is mainly informative anyway;
  * crm.iap.lead.mining.request: void team_id as it only impacts generated
    leads. Those will be created with a void team which is ok;
  * crm.reveal.rule: void team_id as it only impacts generated leads. Those
    will be created with a void team which is ok;
  * event.lead.rule: void lead_sales_team_id as this impact generated leads.
    Those will be created with a void team which is ok;
  * pos.config and pos.oser: void crm_team_id field. It is optional as it is
    not a core field of this model. Pos configurations and orders are still
    valid without team assigned on it;
  * sale.order: void team_id field but track it to keep history;
  * website: void salesteam_id field;

Prevent unlink when having more than 5 SO not canceled. If more than 5 active
SOs in the team, we consider this team to be actively used. 5 is some random
guess based on "user testing", aka more than testing CRM feature and less
than use it in real life use cases.

LINKS

Task ID-2507750
COM PR odoo/odoo#70645
ENT PR odoo/enterprise#18266
This commit is contained in:
Thibault Delavallée
2021-06-23 08:36:18 +00:00
parent ca148375a8
commit c24ccef4fc
14 changed files with 43 additions and 13 deletions
+1 -1
View File
@@ -114,7 +114,7 @@ class Lead(models.Model):
team_id = fields.Many2one(
'crm.team', string='Sales Team', check_company=True, index=True, tracking=True,
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
compute='_compute_team_id', readonly=False, store=True)
compute='_compute_team_id', ondelete="set null", readonly=False, store=True)
company_id = fields.Many2one(
'res.company', string='Company', index=True,
compute='_compute_company_id', readonly=False, store=True)
@@ -10,7 +10,7 @@ class LeadScoringFrequency(models.Model):
value = fields.Char('Value')
won_count = fields.Float('Won Count', digits=(16, 1)) # Float because we add 0.1 to avoid zero Frequency issue
lost_count = fields.Float('Lost Count', digits=(16, 1)) # Float because we add 0.1 to avoid zero Frequency issue
team_id = fields.Many2one('crm.team', 'Sales Team')
team_id = fields.Many2one('crm.team', 'Sales Team', ondelete="cascade")
class FrequencyField(models.Model):
+1 -1
View File
@@ -36,7 +36,7 @@ class Stage(models.Model):
sequence = fields.Integer('Sequence', default=1, help="Used to order stages. Lower is better.")
is_won = fields.Boolean('Is Won Stage?')
requirements = fields.Text('Requirements', help="Enter here the internal requirements for this stage (ex: Offer sent to customer). It will appear as a tooltip over the stage's name.")
team_id = fields.Many2one('crm.team', string='Sales Team', ondelete='set null',
team_id = fields.Many2one('crm.team', string='Sales Team', ondelete="set null",
help='Specific team that uses this stage. Other teams will not be able to see or use this stage.')
fold = fields.Boolean('Folded in Pipeline',
help='This stage is folded in the kanban view when there are no records in that stage to display.')
+1 -1
View File
@@ -8,7 +8,7 @@ class Partner(models.Model):
_name = 'res.partner'
_inherit = 'res.partner'
team_id = fields.Many2one('crm.team', string='Sales Team')
team_id = fields.Many2one('crm.team', string='Sales Team', ondelete="set null")
opportunity_ids = fields.One2many('crm.lead', 'partner_id', string='Opportunities', domain=[('type', '=', 'opportunity')])
opportunity_count = fields.Integer("Opportunity", compute='_compute_opportunity_count')
@@ -48,7 +48,7 @@ class CRMLeadMiningRequest(models.Model):
lead_type = fields.Selection([('lead', 'Leads'), ('opportunity', 'Opportunities')], string='Type', required=True, default=_default_lead_type)
display_lead_label = fields.Char(compute='_compute_display_lead_label')
team_id = fields.Many2one(
'crm.team', string='Sales Team',
'crm.team', string='Sales Team', ondelete="set null",
domain="[('use_opportunities', '=', True)]", readonly=False, compute='_compute_team_id', store=True)
user_id = fields.Many2one('res.users', string='Salesperson', default=lambda self: self.env.user)
tag_ids = fields.Many2many('crm.tag', string='Tags')
@@ -52,7 +52,7 @@ class CRMRevealRule(models.Model):
lead_for = fields.Selection([('companies', 'Companies'), ('people', 'Companies and their Contacts')], string='Data Tracking', required=True, default='companies', help='Choose whether to track companies only or companies and their contacts')
lead_type = fields.Selection([('lead', 'Lead'), ('opportunity', 'Opportunity')], string='Type', required=True, default='opportunity')
suffix = fields.Char(string='Suffix', help='This will be appended in name of generated lead so you can identify lead/opportunity is generated with this rule')
team_id = fields.Many2one('crm.team', string='Sales Team')
team_id = fields.Many2one('crm.team', string='Sales Team', ondelete="set null")
tag_ids = fields.Many2many('crm.tag', string='Tags')
user_id = fields.Many2one('res.users', string='Salesperson')
priority = fields.Selection(crm_stage.AVAILABLE_PRIORITIES, string='Priority')
+3 -1
View File
@@ -105,7 +105,9 @@ class EventLeadRule(models.Model):
('lead', 'Lead'), ('opportunity', 'Opportunity')], string="Lead Type", required=True,
default=lambda self: 'lead' if self.env['res.users'].has_group('crm.group_use_lead') else 'opportunity',
help="Default lead type when this rule is applied.")
lead_sales_team_id = fields.Many2one('crm.team', string='Sales Team', help="Automatically assign the created leads to this Sales Team.")
lead_sales_team_id = fields.Many2one(
'crm.team', string='Sales Team', ondelete="set null",
help="Automatically assign the created leads to this Sales Team.")
lead_user_id = fields.Many2one('res.users', string='Salesperson', help="Automatically assign the created leads to this Salesperson.")
lead_tag_ids = fields.Many2many('crm.tag', string='Tags', help="Automatically add these tags to the created leads.")
+1 -1
View File
@@ -10,7 +10,7 @@ class PosConfig(models.Model):
crm_team_id = fields.Many2one(
'crm.team', string="Sales Team",
'crm.team', string="Sales Team", ondelete="set null",
help="This Point of sale's sales will be related to this Sales Team.")
@api.onchange('company_id')
+1 -1
View File
@@ -8,7 +8,7 @@ class PosOrder(models.Model):
_inherit = 'pos.order'
currency_rate = fields.Float(compute='_compute_currency_rate', store=True, digits=0, readonly=True)
crm_team_id = fields.Many2one('crm.team', string="Sales Team")
crm_team_id = fields.Many2one('crm.team', string="Sales Team", ondelete="set null")
@api.model
def _complete_values_from_session(self, session, values):
+1
View File
@@ -14,6 +14,7 @@ class AccountMove(models.Model):
team_id = fields.Many2one(
'crm.team', string='Sales Team', default=_get_invoice_default_sale_team,
ondelete="set null", tracking=True,
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
partner_shipping_id = fields.Many2one(
'res.partner',
+28 -2
View File
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, _
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class CrmTeam(models.Model):
@@ -26,7 +27,7 @@ class CrmTeam(models.Model):
sales_to_invoice_count = fields.Integer(
compute='_compute_sales_to_invoice',
string='Number of sales to invoice', readonly=True)
sale_order_count = fields.Integer(compute='_compute_sale_order_count', string='# Sale Orders')
def _compute_quotations_to_invoice(self):
query = self.env['sale.order']._where_calc([
@@ -96,6 +97,17 @@ class CrmTeam(models.Model):
for team in self:
team.invoiced = data_map.get(team.id, 0.0)
def _compute_sale_order_count(self):
data_map = {}
if self.ids:
sale_order_data = self.env['sale.order'].read_group([
('team_id', 'in', self.ids),
('state', '!=', 'cancel'),
], ['team_id'], ['team_id'])
data_map = {datum['team_id'][0]: datum['team_id_count'] for datum in sale_order_data}
for team in self:
team.sale_order_count = data_map.get(team.id, 0)
def _graph_get_model(self):
if self._context.get('in_sales_app'):
return 'sale.report'
@@ -133,3 +145,17 @@ class CrmTeam(models.Model):
def update_invoiced_target(self, value):
return self.write({'invoiced_target': round(float(value or 0))})
@api.ondelete(at_uninstall=False)
def _unlink_except_used_for_sales(self):
""" If more than 5 active SOs, we consider this team to be actively used.
5 is some random guess based on "user testing", aka more than testing
CRM feature and less than use it in real life use cases. """
SO_COUNT_TRIGGER = 5
for team in self:
if team.sale_order_count >= SO_COUNT_TRIGGER:
raise UserError(
_('Team %(team_name)s has %(sale_order_count)s active sale orders. Consider canceling them or archiving the team instead.',
team_name=team.name,
sale_order_count=team.sale_order_count
))
+1
View File
@@ -246,6 +246,7 @@ class SaleOrder(models.Model):
company_id = fields.Many2one('res.company', 'Company', required=True, index=True, default=lambda self: self.env.company)
team_id = fields.Many2one(
'crm.team', 'Sales Team',
ondelete="set null", tracking=True,
change_default=True, default=_get_default_team, check_company=True, # Unrequired company
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
+1 -1
View File
@@ -15,7 +15,7 @@ class CrmTeamMember(models.Model):
crm_team_id = fields.Many2one(
'crm.team', string='Sales Team',
default=False, # TDE: temporary fix to activate depending computed fields
check_company=True, index=True, required=True)
check_company=True, index=True, ondelete="cascade", required=True)
user_id = fields.Many2one(
'res.users', string='Salesperson', # TDE FIXME check responsible field
check_company=True, index=True, ondelete='cascade', required=True,
+1 -1
View File
@@ -29,7 +29,7 @@ class Website(models.Model):
return None
salesteam_id = fields.Many2one('crm.team',
string='Sales Team',
string='Sales Team', ondelete="set null",
default=_get_default_website_team)
pricelist_ids = fields.One2many('product.pricelist', compute="_compute_pricelist_ids",
string='Price list available for this Ecommerce/Website')