Files
odoo_source/addons/sales_team/models/crm_team.py
T
mafo-odoo 6a0ab5f3c6 [FIX] sale,sales_team: set lead team when creating quotation
Steps to reproduce:
-Install sales and crm
-Apply multiteam option in crm settings
-Create a user with two teams A and B
-Create with this user a lead with team B
-From this lead create a quotation

Current behavior:
The quotation could have team A (the team of the lead is ignored)

Expected behavior:
The quotation always has team B

Problem when creating the quotation the team id is not propagated
in the context to the final function _get_default_team_id and in
this function the default context team is only check if no teams
have been found for the user, so the function always selects the
first team of the user. To solve the issue we propagate the team
id to the default function and we change it so that if the context
default team is in the user teams it is this one that is going to
be selected and no other.

opw-2830913

X-original-commit: 0b6c63f2be90928f5d6806ef489c369a1310be04
Part-of: odoo/odoo#94045
2022-06-20 16:51:15 +02:00

394 lines
18 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
import random
from babel.dates import format_date
from datetime import date
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.release import version
class CrmTeam(models.Model):
_name = "crm.team"
_inherit = ['mail.thread']
_description = "Sales Team"
_order = "sequence ASC, create_date DESC, id DESC"
_check_company_auto = True
def _get_default_team_id(self, user_id=None, domain=None):
""" Compute default team id for sales related documents. Note that this
method is not called by default_get as it takes some additional
parameters and is meant to be called by other default methods.
Heuristic (when multiple match: take from default context value or first
sequence ordered)
1- any of my teams (member OR responsible) matching domain, either from
context or based on _order;
2- any of my teams (member OR responsible), either from context or based
on _order;
3- default from context
4- any team matching my company and domain (based on company rule)
5- any team matching my company (based on company rule)
Note: ResPartner.team_id field is explicitly not taken into account. We
think this field causes a lot of noises compared to its added value.
Think notably: team not in responsible teams, team company not matching
responsible or lead company, asked domain not matching, ...
:param user_id: salesperson to target, fallback on env.uid;
:domain: optional domain to filter teams (like use_lead = True);
"""
if user_id is None:
user = self.env.user
else:
user = self.env['res.users'].sudo().browse(user_id)
default_team = self.env['crm.team'].browse(
self.env.context['default_team_id']
) if self.env.context.get('default_team_id') else self.env['crm.team']
valid_cids = [False] + [c for c in user.company_ids.ids if c in self.env.companies.ids]
# 1- find in user memberships - note that if current user in C1 searches
# for team belonging to a user in C1/C2 -> only results for C1 will be returned
team = self.env['crm.team']
teams = self.env['crm.team'].search([
('company_id', 'in', valid_cids),
'|', ('user_id', '=', user.id), ('member_ids', 'in', [user.id])
])
if teams and domain:
filtered_teams = teams.filtered_domain(domain)
if default_team and default_team in filtered_teams:
team = default_team
else:
team = filtered_teams[:1]
# 2- any of my teams
if not team:
if default_team and default_team in teams:
team = default_team
else:
team = teams[:1]
# 3- default: context
if not team and default_team:
team = default_team
if not team:
teams = self.env['crm.team'].search([('company_id', 'in', valid_cids)])
# 4- default: based on company rule, first one matching domain
if teams and domain:
team = teams.filtered_domain(domain)[:1]
# 5- default: based on company rule, first one
if not team:
team = teams[:1]
return team
def _get_default_favorite_user_ids(self):
return [(6, 0, [self.env.uid])]
# description
name = fields.Char('Sales Team', required=True, translate=True)
sequence = fields.Integer('Sequence', default=10)
active = fields.Boolean(default=True, help="If the active field is set to false, it will allow you to hide the Sales Team without removing it.")
company_id = fields.Many2one(
'res.company', string='Company', index=True,
default=lambda self: self.env.company)
currency_id = fields.Many2one(
"res.currency", string="Currency",
related='company_id.currency_id', readonly=True)
user_id = fields.Many2one('res.users', string='Team Leader', check_company=True)
# memberships
is_membership_multi = fields.Boolean(
'Multiple Memberships Allowed', compute='_compute_is_membership_multi',
help='If True, users may belong to several sales teams. Otherwise membership is limited to a single sales team.')
member_ids = fields.Many2many(
'res.users', string='Salespersons',
domain="['&', ('share', '=', False), ('company_ids', 'in', member_company_ids)]",
compute='_compute_member_ids', inverse='_inverse_member_ids', search='_search_member_ids',
help="Users assigned to this team.")
member_company_ids = fields.Many2many(
'res.company', compute='_compute_member_company_ids',
help='UX: Limit to team company or all if no company')
member_warning = fields.Text('Membership Issue Warning', compute='_compute_member_warning')
crm_team_member_ids = fields.One2many(
'crm.team.member', 'crm_team_id', string='Sales Team Members',
help="Add members to automatically assign their documents to this sales team.")
crm_team_member_all_ids = fields.One2many(
'crm.team.member', 'crm_team_id', string='Sales Team Members (incl. inactive)',
context={'active_test': False})
# UX options
color = fields.Integer(string='Color Index', help="The color of the channel")
favorite_user_ids = fields.Many2many(
'res.users', 'team_favorite_user_rel', 'team_id', 'user_id',
string='Favorite Members', default=_get_default_favorite_user_ids)
is_favorite = fields.Boolean(
string='Show on dashboard', compute='_compute_is_favorite', inverse='_inverse_is_favorite',
help="Favorite teams to display them in the dashboard and access them easily.")
dashboard_button_name = fields.Char(string="Dashboard Button", compute='_compute_dashboard_button_name')
dashboard_graph_data = fields.Text(compute='_compute_dashboard_graph')
@api.depends('sequence') # TDE FIXME: force compute in new mode
def _compute_is_membership_multi(self):
multi_enabled = self.env['ir.config_parameter'].sudo().get_param('sales_team.membership_multi', False)
self.is_membership_multi = multi_enabled
@api.depends('crm_team_member_ids.active')
def _compute_member_ids(self):
for team in self:
team.member_ids = team.crm_team_member_ids.user_id
def _inverse_member_ids(self):
for team in self:
# pre-save value to avoid having _compute_member_ids interfering
# while building membership status
memberships = team.crm_team_member_ids
users_current = team.member_ids
users_new = users_current - memberships.user_id
# add missing memberships
self.env['crm.team.member'].create([{'crm_team_id': team.id, 'user_id': user.id} for user in users_new])
# activate or deactivate other memberships depending on members
for membership in memberships:
membership.active = membership.user_id in users_current
@api.depends('is_membership_multi', 'member_ids')
def _compute_member_warning(self):
""" Display a warning message to warn user they are about to archive
other memberships. Only valid in mono-membership mode and take into
account only active memberships as we may keep several archived
memberships. """
self.member_warning = False
if all(team.is_membership_multi for team in self):
return
# done in a loop, but to be used in form view only -> not optimized
for team in self:
member_warning = False
other_memberships = self.env['crm.team.member'].search([
('crm_team_id', '!=', team.id if team.ids else False), # handle NewID
('user_id', 'in', team.member_ids.ids)
])
if other_memberships and len(other_memberships) == 1:
member_warning = _("Adding %(user_name)s in this team would remove him/her from its current team %(team_name)s.",
user_name=other_memberships.user_id.name,
team_name=other_memberships.crm_team_id.name
)
elif other_memberships:
member_warning = _("Adding %(user_names)s in this team would remove them from their current teams (%(team_names)s).",
user_names=", ".join(other_memberships.mapped('user_id.name')),
team_names=", ".join(other_memberships.mapped('crm_team_id.name'))
)
if member_warning:
team.member_warning = member_warning + " " + _("To add a Salesperson into multiple Teams, activate the Multi-Team option in settings.")
def _search_member_ids(self, operator, value):
return [('crm_team_member_ids.user_id', operator, value)]
@api.depends('company_id')
def _compute_member_company_ids(self):
""" Available companies for members. Either team company if set, either
any company if not set on team. """
all_companies = self.env['res.company'].search([])
for team in self:
team.member_company_ids = team.company_id or all_companies
def _compute_is_favorite(self):
for team in self:
team.is_favorite = self.env.user in team.favorite_user_ids
def _inverse_is_favorite(self):
sudoed_self = self.sudo()
to_fav = sudoed_self.filtered(lambda team: self.env.user not in team.favorite_user_ids)
to_fav.write({'favorite_user_ids': [(4, self.env.uid)]})
(sudoed_self - to_fav).write({'favorite_user_ids': [(3, self.env.uid)]})
return True
def _compute_dashboard_button_name(self):
""" Sets the adequate dashboard button name depending on the Sales Team's options
"""
for team in self:
team.dashboard_button_name = _("Big Pretty Button :)") # placeholder
def _compute_dashboard_graph(self):
for team in self:
team.dashboard_graph_data = json.dumps(team._get_dashboard_graph_data())
# ------------------------------------------------------------
# CRUD
# ------------------------------------------------------------
@api.model_create_multi
def create(self, vals_list):
teams = super(CrmTeam, self.with_context(mail_create_nosubscribe=True)).create(vals_list)
teams.filtered(lambda t: t.member_ids)._add_members_to_favorites()
return teams
def write(self, values):
res = super(CrmTeam, self).write(values)
# manually launch company sanity check
if values.get('company_id'):
self.crm_team_member_ids._check_company(fnames=['crm_team_id'])
if values.get('member_ids'):
self._add_members_to_favorites()
return res
@api.ondelete(at_uninstall=False)
def _unlink_except_default(self):
default_teams = [
self.env.ref('sales_team.salesteam_website_sales'),
self.env.ref('sales_team.pos_sales_team'),
self.env.ref('sales_team.ebay_sales_team')
]
for team in self:
if team in default_teams:
raise UserError(_('Cannot delete default team "%s"', team.name))
# ------------------------------------------------------------
# ACTIONS
# ------------------------------------------------------------
def action_primary_channel_button(self):
""" Skeleton function to be overloaded It will return the adequate action
depending on the Sales Team's options. """
return False
# ------------------------------------------------------------
# TOOLS
# ------------------------------------------------------------
def _add_members_to_favorites(self):
for team in self:
team.favorite_user_ids = [(4, member.id) for member in team.member_ids]
# ------------------------------------------------------------
# GRAPH
# ------------------------------------------------------------
def _graph_get_model(self):
""" skeleton function defined here because it'll be called by crm and/or sale
"""
raise UserError(_('Undefined graph model for Sales Team: %s', self.name))
def _graph_get_dates(self, today):
""" return a coherent start and end date for the dashboard graph covering a month period grouped by week.
"""
start_date = today - relativedelta(months=1)
# we take the start of the following week if we group by week
# (to avoid having twice the same week from different month)
start_date += relativedelta(days=8 - start_date.isocalendar()[2])
return [start_date, today]
def _graph_date_column(self):
return 'create_date'
def _graph_get_table(self, GraphModel):
return GraphModel._table
def _graph_x_query(self):
return 'EXTRACT(WEEK FROM %s)' % self._graph_date_column()
def _graph_y_query(self):
raise UserError(_('Undefined graph model for Sales Team: %s', self.name))
def _extra_sql_conditions(self):
return ''
def _graph_title_and_key(self):
""" Returns an array containing the appropriate graph title and key respectively.
The key is for lineCharts, to have the on-hover label.
"""
return ['', '']
def _graph_data(self, start_date, end_date):
""" return format should be an iterable of dicts that contain {'x_value': ..., 'y_value': ...}
x_values should be weeks.
y_values are floats.
"""
query = """SELECT %(x_query)s as x_value, %(y_query)s as y_value
FROM %(table)s
WHERE team_id = %(team_id)s
AND DATE(%(date_column)s) >= %(start_date)s
AND DATE(%(date_column)s) <= %(end_date)s
%(extra_conditions)s
GROUP BY x_value;"""
# apply rules
dashboard_graph_model = self._graph_get_model()
GraphModel = self.env[dashboard_graph_model]
graph_table = self._graph_get_table(GraphModel)
extra_conditions = self._extra_sql_conditions()
where_query = GraphModel._where_calc([])
GraphModel._apply_ir_rules(where_query, 'read')
from_clause, where_clause, where_clause_params = where_query.get_sql()
if where_clause:
extra_conditions += " AND " + where_clause
query = query % {
'x_query': self._graph_x_query(),
'y_query': self._graph_y_query(),
'table': graph_table,
'team_id': "%s",
'date_column': self._graph_date_column(),
'start_date': "%s",
'end_date': "%s",
'extra_conditions': extra_conditions
}
self._cr.execute(query, [self.id, start_date, end_date] + where_clause_params)
return self.env.cr.dictfetchall()
def _get_dashboard_graph_data(self):
def get_week_name(start_date, locale):
""" Generates a week name (string) from a datetime according to the locale:
E.g.: locale start_date (datetime) return string
"en_US" November 16th "16-22 Nov"
"en_US" December 28th "28 Dec-3 Jan"
"""
if (start_date + relativedelta(days=6)).month == start_date.month:
short_name_from = format_date(start_date, 'd', locale=locale)
else:
short_name_from = format_date(start_date, 'd MMM', locale=locale)
short_name_to = format_date(start_date + relativedelta(days=6), 'd MMM', locale=locale)
return short_name_from + '-' + short_name_to
self.ensure_one()
values = []
today = fields.Date.from_string(fields.Date.context_today(self))
start_date, end_date = self._graph_get_dates(today)
graph_data = self._graph_data(start_date, end_date)
x_field = 'label'
y_field = 'value'
# generate all required x_fields and update the y_values where we have data for them
locale = self._context.get('lang') or 'en_US'
weeks_in_start_year = int(date(start_date.year, 12, 28).isocalendar()[1]) # This date is always in the last week of ISO years
week_count = (end_date.isocalendar()[1] - start_date.isocalendar()[1]) % weeks_in_start_year + 1
for week in range(week_count):
short_name = get_week_name(start_date + relativedelta(days=7 * week), locale)
values.append({x_field: short_name, y_field: 0, 'type': 'future' if week + 1 == week_count else 'past'})
for data_item in graph_data:
index = int((data_item.get('x_value') - start_date.isocalendar()[1]) % weeks_in_start_year)
values[index][y_field] = data_item.get('y_value')
[graph_title, graph_key] = self._graph_title_and_key()
color = '#875A7B' if '+e' in version else '#7c7bad'
# If no actual data available, show some sample data
if not graph_data:
graph_key = _('Sample data')
for value in values:
value['type'] = 'o_sample_data'
# we use unrealistic values for the sample data
value['value'] = random.randint(0, 20)
return [{'values': values, 'area': True, 'title': graph_title, 'key': graph_key, 'color': color}]