[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:
@@ -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):
|
||||
|
||||
@@ -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.')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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.")
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
))
|
||||
|
||||
@@ -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)]")
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user