mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-26 14:31:58 +02:00
19.0 vanilla
This commit is contained in:
parent
79f83631d5
commit
73afc09215
6267 changed files with 1534193 additions and 1130106 deletions
|
|
@ -4,5 +4,4 @@
|
|||
from . import crm_team_member
|
||||
from . import crm_team
|
||||
from . import crm_tag
|
||||
from . import res_partner
|
||||
from . import res_users
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from random import randint
|
||||
|
|
@ -6,8 +5,8 @@ from random import randint
|
|||
from odoo import fields, models
|
||||
|
||||
|
||||
class Tag(models.Model):
|
||||
_name = "crm.tag"
|
||||
class CrmTag(models.Model):
|
||||
_name = 'crm.tag'
|
||||
_description = "CRM Tag"
|
||||
|
||||
def _get_default_color(self):
|
||||
|
|
@ -16,6 +15,7 @@ class Tag(models.Model):
|
|||
name = fields.Char('Tag Name', required=True, translate=True)
|
||||
color = fields.Integer('Color', default=_get_default_color)
|
||||
|
||||
_sql_constraints = [
|
||||
('name_uniq', 'unique (name)', "Tag name already exists !"),
|
||||
]
|
||||
_name_uniq = models.Constraint(
|
||||
'unique (name)',
|
||||
'Tag name already exists!',
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,26 +1,20 @@
|
|||
# -*- 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 import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.release import version
|
||||
|
||||
|
||||
class CrmTeam(models.Model):
|
||||
_name = "crm.team"
|
||||
_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):
|
||||
def _get_default_color(self):
|
||||
return random.randint(1, 11)
|
||||
|
||||
def _get_default_team_id(self, user_id=False, domain=False):
|
||||
""" 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.
|
||||
|
|
@ -36,15 +30,10 @@ class CrmTeam(models.Model):
|
|||
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);
|
||||
:param domain: optional domain to filter teams (like use_lead = True);
|
||||
"""
|
||||
if user_id is None:
|
||||
if not user_id:
|
||||
user = self.env.user
|
||||
else:
|
||||
user = self.env['res.users'].sudo().browse(user_id)
|
||||
|
|
@ -97,12 +86,11 @@ class CrmTeam(models.Model):
|
|||
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)
|
||||
'res.company', string='Company', index=True)
|
||||
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)
|
||||
user_id = fields.Many2one('res.users', string='Team Leader', check_company=True, domain=[('share', '!=', True)])
|
||||
# memberships
|
||||
is_membership_multi = fields.Boolean(
|
||||
'Multiple Memberships Allowed', compute='_compute_is_membership_multi',
|
||||
|
|
@ -124,7 +112,7 @@ class CrmTeam(models.Model):
|
|||
'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")
|
||||
color = fields.Integer(string='Color Index', help="The color of the channel", default=_get_default_color)
|
||||
favorite_user_ids = fields.Many2many(
|
||||
'res.users', 'team_favorite_user_rel', 'team_id', 'user_id',
|
||||
string='Favorite Members', default=_get_default_favorite_user_ids)
|
||||
|
|
@ -132,7 +120,19 @@ class CrmTeam(models.Model):
|
|||
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.constrains('company_id')
|
||||
def _constrains_company_members(self):
|
||||
for team in self.filtered('company_id'):
|
||||
invalid_members = team.crm_team_member_ids.filtered(
|
||||
lambda m: team.company_id not in m.user_id.company_ids
|
||||
)
|
||||
if invalid_members:
|
||||
raise UserError(_("The following team members are not allowed in company '%(company)s' of the Sales Team '%(team)s': %(users)s",
|
||||
company=team.company_id.display_name,
|
||||
team=team.name,
|
||||
users=", ".join(invalid_members.mapped('user_id.name'))
|
||||
))
|
||||
|
||||
@api.depends('sequence') # TDE FIXME: force compute in new mode
|
||||
def _compute_is_membership_multi(self):
|
||||
|
|
@ -170,23 +170,15 @@ class CrmTeam(models.Model):
|
|||
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
|
||||
('crm_team_id', '!=', team._origin.id if team.ids else False),
|
||||
('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).",
|
||||
if other_memberships:
|
||||
team.member_warning = _("%(user_names)s already in other 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)]
|
||||
|
|
@ -218,10 +210,6 @@ class CrmTeam(models.Model):
|
|||
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
|
||||
# ------------------------------------------------------------
|
||||
|
|
@ -232,13 +220,13 @@ class CrmTeam(models.Model):
|
|||
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'])
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
|
||||
if values.get('member_ids'):
|
||||
if vals.get('company_id'): # Force re-check of memberships constraint for this team
|
||||
self.crm_team_member_ids._constrains_membership()
|
||||
|
||||
if vals.get('member_ids'):
|
||||
self._add_members_to_favorites()
|
||||
return res
|
||||
|
||||
|
|
@ -247,7 +235,6 @@ class CrmTeam(models.Model):
|
|||
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:
|
||||
|
|
@ -269,128 +256,3 @@ class CrmTeam(models.Model):
|
|||
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}]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, exceptions, fields, models, _
|
||||
from odoo import _, api, exceptions, fields, models
|
||||
|
||||
|
||||
class CrmTeamMember(models.Model):
|
||||
|
|
@ -13,9 +12,10 @@ class CrmTeamMember(models.Model):
|
|||
_check_company_auto = True
|
||||
|
||||
crm_team_id = fields.Many2one(
|
||||
'crm.team', string='Sales Team', group_expand='_read_group_crm_team_id',
|
||||
'crm.team', string='Sales Team',
|
||||
group_expand='_read_group_expand_full', # Always display all the teams
|
||||
default=False, # TDE: temporary fix to activate depending computed fields
|
||||
check_company=True, index=True, ondelete="cascade", required=True)
|
||||
check_company=False, 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,
|
||||
|
|
@ -37,7 +37,6 @@ class CrmTeamMember(models.Model):
|
|||
name = fields.Char(string='Name', related='user_id.display_name', readonly=False)
|
||||
email = fields.Char(string='Email', related='user_id.email')
|
||||
phone = fields.Char(string='Phone', related='user_id.phone')
|
||||
mobile = fields.Char(string='Mobile', related='user_id.mobile')
|
||||
company_id = fields.Many2one('res.company', string='Company', related='user_id.company_id')
|
||||
|
||||
@api.constrains('crm_team_id', 'user_id', 'active')
|
||||
|
|
@ -76,6 +75,16 @@ class CrmTeamMember(models.Model):
|
|||
duplicates=", ".join("%s (%s)" % (m.user_id.name, m.crm_team_id.name) for m in duplicates)
|
||||
))
|
||||
|
||||
@api.constrains('crm_team_id', 'user_id')
|
||||
def _constrains_company_membership(self):
|
||||
for membership in self.filtered(lambda m: m.crm_team_id.company_id):
|
||||
if membership.crm_team_id.company_id not in membership.user_id.company_ids:
|
||||
raise exceptions.UserError(_("User '%(user)s' is not allowed in the company '%(company)s' of the Sales Team '%(team)s'.",
|
||||
user=membership.user_id.name,
|
||||
company=membership.crm_team_id.company_id.display_name,
|
||||
team=membership.crm_team_id.name
|
||||
))
|
||||
|
||||
@api.depends('crm_team_id', 'is_membership_multi', 'user_id')
|
||||
@api.depends_context('default_crm_team_id')
|
||||
def _compute_user_in_teams_ids(self):
|
||||
|
|
@ -130,7 +139,7 @@ class CrmTeamMember(models.Model):
|
|||
teams = user_mapping.get(member.user_id, self.env['crm.team'])
|
||||
remaining = teams - (member.crm_team_id | member._origin.crm_team_id)
|
||||
if remaining:
|
||||
member.member_warning = _("Adding %(user_name)s in this team would remove him/her from its current teams %(team_names)s.",
|
||||
member.member_warning = _("%(user_name)s already in other teams (%(team_names)s).",
|
||||
user_name=member.user_id.name,
|
||||
team_names=", ".join(remaining.mapped('name'))
|
||||
)
|
||||
|
|
@ -142,7 +151,7 @@ class CrmTeamMember(models.Model):
|
|||
# ------------------------------------------------------------
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, values_list):
|
||||
def create(self, vals_list):
|
||||
""" Specific behavior implemented on create
|
||||
|
||||
* mono membership mode: other user memberships are automatically
|
||||
|
|
@ -156,12 +165,12 @@ class CrmTeamMember(models.Model):
|
|||
"""
|
||||
is_membership_multi = self.env['ir.config_parameter'].sudo().get_param('sales_team.membership_multi', False)
|
||||
if not is_membership_multi:
|
||||
self._synchronize_memberships(values_list)
|
||||
self._synchronize_memberships(vals_list)
|
||||
return super(CrmTeamMember, self.with_context(
|
||||
mail_create_nosubscribe=True
|
||||
)).create(values_list)
|
||||
)).create(vals_list)
|
||||
|
||||
def write(self, values):
|
||||
def write(self, vals):
|
||||
""" Specific behavior about active. If you change user_id / team_id user
|
||||
get warnings in form view and a raise in constraint check. We support
|
||||
archive / activation of memberships that toggles other memberships. But
|
||||
|
|
@ -172,19 +181,12 @@ class CrmTeamMember(models.Model):
|
|||
modifying user_id or team_id is advanced and does not benefit from our
|
||||
support. """
|
||||
is_membership_multi = self.env['ir.config_parameter'].sudo().get_param('sales_team.membership_multi', False)
|
||||
if not is_membership_multi and values.get('active'):
|
||||
if not is_membership_multi and vals.get('active'):
|
||||
self._synchronize_memberships([
|
||||
dict(user_id=membership.user_id.id, crm_team_id=membership.crm_team_id.id)
|
||||
for membership in self
|
||||
])
|
||||
return super(CrmTeamMember, self).write(values)
|
||||
|
||||
@api.model
|
||||
def _read_group_crm_team_id(self, teams, domain, order):
|
||||
"""Read group customization in order to display all the teams in
|
||||
Kanban view, even if they are empty.
|
||||
"""
|
||||
return self.env['crm.team'].search([], order=order)
|
||||
return super().write(vals)
|
||||
|
||||
def _synchronize_memberships(self, user_team_ids):
|
||||
""" Synchronize memberships: archive other memberships.
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
team_id = fields.Many2one(
|
||||
'crm.team', 'Sales Team',
|
||||
help='If set, this Sales Team will be used for sales and assignments related to this partner')
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
|
@ -23,7 +22,18 @@ class ResUsers(models.Model):
|
|||
user.crm_team_ids = user.crm_team_member_ids.crm_team_id
|
||||
|
||||
def _search_crm_team_ids(self, operator, value):
|
||||
return [('crm_team_member_ids.crm_team_id', operator, value)]
|
||||
# Equivalent to `[('crm_team_member_ids.crm_team_id', operator, value)]`,
|
||||
# but we inline the ids directly to simplify final queries and improve performance,
|
||||
# as it's part of a few ir.rules.
|
||||
# If we're going to inject too many `ids`, we fall back on the default behavior
|
||||
# to avoid a performance regression.
|
||||
IN_MAX = 10_000
|
||||
domain = [('crm_team_member_ids.crm_team_id', operator, value)]
|
||||
user_ids = self.env['res.users'].with_context(active_test=False)._search(domain, limit=IN_MAX).get_result_ids()
|
||||
if len(user_ids) < IN_MAX:
|
||||
return [('id', 'in', user_ids)]
|
||||
|
||||
return domain
|
||||
|
||||
@api.depends('crm_team_member_ids.crm_team_id', 'crm_team_member_ids.create_date', 'crm_team_member_ids.active')
|
||||
def _compute_sale_team_id(self):
|
||||
|
|
@ -33,3 +43,7 @@ class ResUsers(models.Model):
|
|||
else:
|
||||
sorted_memberships = user.crm_team_member_ids # sorted by create date
|
||||
user.sale_team_id = sorted_memberships[0].crm_team_id if sorted_memberships else False
|
||||
|
||||
def action_archive(self):
|
||||
self.env['crm.team.member'].search([('user_id', 'in', self.ids)]).action_archive()
|
||||
return super().action_archive()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue