[IMP] salesteam: Simplify sales team config and improved team dashboard

Reason:
  *Sales team configuration is too complex with fields you
   don't understand and that bring no added value
  *Remove the team type, every team should be able to handle
   a POS or a website for example
Contains:
  *Removal of the team_type, use_quotations, use_leads fields
  *Removal of the graph configuration fields
  *Adapting the filters that were based on the team_type
  *Clear distinction between the team dashboard behavior in crm
   from the one in sales
  *Cleansing of now dead code due to the removal of the previously
   mentionned fields

Task: #1830105
Enterprise PR: #3006
Closes: #28063
This commit is contained in:
qmo-odoo
2018-12-05 15:32:53 +00:00
parent e1f9499d26
commit 1da63f0ab0
52 changed files with 431 additions and 696 deletions
-5
View File
@@ -8,8 +8,3 @@ from . import wizard
from odoo import api, SUPERUSER_ID
def uninstall_hook(cr, registry):
env = api.Environment(cr, SUPERUSER_ID, {})
teams = env['crm.team'].search([('dashboard_graph_model', '=', 'crm.opportunity.report')])
teams.update({'dashboard_graph_model': None})
+1 -2
View File
@@ -54,6 +54,5 @@
'css': ['static/src/css/crm.css'],
'installable': True,
'application': True,
'auto_install': False,
'uninstall_hook': 'uninstall_hook',
'auto_install': False
}
+10 -8
View File
@@ -1,13 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="sales_team.team_sales_department" model="crm.team">
<field name="use_opportunities">True</field>
<field name="use_leads">True</field>
<field name="alias_name">sales</field>
<field name="dashboard_graph_model">crm.lead</field>
<field name="company_id" eval="False"/>
<field name="alias_user_id" ref="base.user_admin"/>
<record id="sales_team.salesteam_website_sales" model="crm.team">
<field name="use_opportunities" eval="False"/>
</record>
<record id="sales_team.pos_sales_team" model="crm.team">
<field name="use_opportunities" eval="False"/>
</record>
<record id="sales_team.ebay_sales_team" model="crm.team">
<field name="use_opportunities" eval="False"/>
</record>
<!--default alias for leads-->
@@ -29,5 +32,4 @@
<field name="name">Not enough stock</field>
</record>
</data>
</odoo>
+3 -8
View File
@@ -6,14 +6,9 @@
</record>
<record id="sales_team.team_sales_department" model="crm.team">
<field name="member_ids" eval="[(4, ref('base.user_demo'))]"/>
</record>
<record id="sales_team.crm_team_1" model="crm.team">
<field name="use_opportunities">True</field>
<field name="use_leads">True</field>
<field name="dashboard_graph_model">crm.lead</field>
<field name="member_ids" eval="[(4, ref('base.user_demo'))]"/>
<field name="alias_name">sales</field>
<field name="alias_user_id" ref="base.user_admin"/>
<field name="company_id" eval="False"/>
</record>
</data>
</odoo>
+1 -6
View File
@@ -58,12 +58,7 @@
<field name="default" eval="False"/>
<field name="parent_id" eval="ref('mt_lead_lost')"/>
<field name="relation_field">team_id</field>
</record>
<function model="crm.team" name="message_unsubscribe"
eval="[ref('sales_team.team_sales_department'),ref('sales_team.salesteam_website_sales')], [ref('base.partner_root')]"/>
<function model="crm.team" name="message_subscribe"
eval="[ref('sales_team.team_sales_department'),ref('sales_team.salesteam_website_sales')], [ref('base.partner_root')]"/>
</record>
<!--Definition of an email template with an empty body that will be used in opportunity mailing.
Used to give a basis for email recipients, name and to ease the definition of a further
+57 -135
View File
@@ -12,33 +12,24 @@ class Team(models.Model):
_name = 'crm.team'
_inherit = ['mail.alias.mixin', 'crm.team']
_description = 'Sales Channels'
use_opportunities = fields.Boolean('Manage a pipeline', default=True, help="Check this box to manage a presales process with opportunities.")
alias_id = fields.Many2one('mail.alias', string='Alias', ondelete="restrict", required=True, help="The email address associated with this channel. New emails received will automatically create new leads assigned to the channel.")
use_leads = fields.Boolean('Leads', help="Check this box to filter and qualify incoming requests as leads before converting them into opportunities and assigning them to a salesperson.")
use_opportunities = fields.Boolean('Pipeline', help="Check this box to manage a presales process with opportunities.")
alias_id = fields.Many2one('mail.alias', string='Alias', ondelete="restrict", required=True, help="The email address associated with this channel. New emails received will automatically create new leads assigned to the channel.")
unassigned_leads_count = fields.Integer(
compute='_compute_unassigned_leads_count',
string='Unassigned Leads', readonly=True)
string='Unassigned Leads')
opportunities_count = fields.Integer(
compute='_compute_opportunities',
string='Number of open opportunities', readonly=True)
string='Number of open opportunities')
overdue_opportunities_count = fields.Integer(
compute='_compute_overdue_opportunities',
string='Number of overdue opportunities')
opportunities_amount = fields.Integer(
compute='_compute_opportunities',
string='Opportunities Revenues', readonly=True)
dashboard_graph_model = fields.Selection(selection_add=[('crm.lead', 'Pipeline')])
dashboard_graph_period_pipeline = fields.Selection([
('week', 'Within a Week'),
('month', 'Within a Month'),
('year', 'Within a Year'),
], string='Expected to Close', help="The time period this channel's dashboard graph will consider.",
compute="_compute_dashboard_graph_period_pipeline", inverse="_inverse_dashboard_graph_period_pipeline")
dashboard_graph_group_pipeline = fields.Selection([
('day', 'Expected Closing Day'),
('week', 'Expected Closing Week'),
('month', 'Expected Closing Month'),
('user', 'Salesperson'),
('stage', 'Stage'),
], string='Grouping Method', default='day', help="How this channel's dashboard graph will group the results.")
string='Opportunities Revenues')
overdue_opportunities_amount = fields.Integer(
compute='_compute_overdue_opportunities',
string='Overdue Opportunities Revenues')
alias_user_id = fields.Many2one(domain=lambda self: [
('groups_id', 'in', self.env.ref('sales_team.group_sale_salesman_all_leads').id)])
@@ -58,20 +49,30 @@ class Team(models.Model):
('team_id', 'in', self.ids),
('probability', '<', 100),
('type', '=', 'opportunity'),
], ['planned_revenue', 'probability', 'team_id'], ['team_id'])
], ['planned_revenue', 'team_id'], ['team_id'])
counts = {datum['team_id'][0]: datum['team_id_count'] for datum in opportunity_data}
amounts = {datum['team_id'][0]: (datum['planned_revenue'] * datum['probability'] / 100) for datum in opportunity_data}
amounts = {datum['team_id'][0]: (datum['planned_revenue']) for datum in opportunity_data}
for team in self:
team.opportunities_count = counts.get(team.id, 0)
team.opportunities_amount = amounts.get(team.id, 0)
def _compute_dashboard_graph_period_pipeline(self):
for channel in self:
channel.dashboard_graph_period_pipeline = channel.dashboard_graph_period
def _inverse_dashboard_graph_period_pipeline(self):
for channel in self.filtered(lambda ch: ch.dashboard_graph_model == 'crm.lead'):
channel.dashboard_graph_period = channel.dashboard_graph_period_pipeline
def _compute_overdue_opportunities(self):
opportunity_data = self.env['crm.lead'].read_group([
('team_id', 'in', self.ids),
('probability', '<', 100),
('type', '=', 'opportunity'),
('date_deadline', '<', fields.Date.to_string(fields.Datetime.now()))
], ['planned_revenue', 'team_id'], ['team_id'])
counts = {datum['team_id'][0]: datum['team_id_count'] for datum in opportunity_data}
amounts = {datum['team_id'][0]: (datum['planned_revenue']) for datum in opportunity_data}
for team in self:
team.overdue_opportunities_count = counts.get(team.id, 0)
team.overdue_opportunities_amount = amounts.get(team.id, 0)
@api.onchange('use_opportunities')
def _onchange_opportunities(self):
if not self.use_opportunities:
self.alias_name = False
def get_alias_model_name(self, vals):
return 'crm.lead'
@@ -80,54 +81,14 @@ class Team(models.Model):
has_group_use_lead = self.env.user.has_group('crm.group_use_lead')
values = super(Team, self).get_alias_values()
values['alias_defaults'] = defaults = safe_eval(self.alias_defaults or "{}")
defaults['type'] = 'lead' if has_group_use_lead and self.use_leads else 'opportunity'
defaults['type'] = 'lead' if has_group_use_lead else 'opportunity'
defaults['team_id'] = self.id
return values
@api.onchange('use_leads', 'use_opportunities')
def _onchange_use_leads_opportunities(self):
if not self.use_leads and not self.use_opportunities:
self.alias_name = False
if not self.use_opportunities and self.use_leads:
self.use_leads = False
@api.onchange('team_type')
def _onchange_team_type(self):
if self.team_type == 'sales':
self.use_opportunities = True
self.use_leads = lambda self: self.user_has_groups('crm.group_use_lead')
self.dashboard_graph_model = 'crm.lead'
else:
self.use_opportunities = False
self.use_leads = False
return super(Team, self)._onchange_team_type()
@api.onchange('dashboard_graph_model')
def _onchange_dashboard_graph_model(self):
if self.dashboard_graph_model == 'crm.lead':
self.dashboard_graph_period_pipeline = self.dashboard_graph_period
self.dashboard_graph_group_pipeline = self.dashboard_graph_group
else:
self.dashboard_graph_period = self.dashboard_graph_period_pipeline
if not self.dashboard_graph_group:
self.dashboard_graph_group = self._fields['dashboard_graph_group'].default(self)
@api.onchange('dashboard_graph_group_pipeline')
def _onchange_dashboard_graph_group_pipeline(self):
if self.dashboard_graph_group_pipeline == 'stage':
self.dashboard_graph_group = False
else:
self.dashboard_graph_group = self.dashboard_graph_group_pipeline
@api.constrains('dashboard_graph_model', 'use_opportunities')
def _check_graph_model(self):
if not self.use_opportunities and self.dashboard_graph_model == 'crm.lead':
raise ValidationError(_("You have to enable the Pipeline on your Sales Team to be able to set it as a content for the graph"))
return values
@api.multi
def write(self, vals):
result = super(Team, self).write(vals)
if 'use_leads' in vals or 'alias_defaults' in vals:
if 'alias_defaults' in vals:
for team in self:
team.alias_id.write(team.get_alias_values())
return result
@@ -154,75 +115,36 @@ class Team(models.Model):
return action
def _compute_dashboard_button_name(self):
opportunity_teams = self.filtered('use_opportunities')
opportunity_teams.update({'dashboard_button_name': _("Pipeline")})
super(Team, self - opportunity_teams)._compute_dashboard_button_name()
super(Team,self)._compute_dashboard_button_name()
team_with_pipelines = self.filtered(lambda el: el.use_opportunities)
team_with_pipelines.update({'dashboard_button_name': _("Pipeline")})
def action_primary_channel_button(self):
if self.use_opportunities:
action = self.env.ref('crm.crm_case_form_view_salesteams_opportunity').read()[0]
return action
return super(Team, self).action_primary_channel_button()
def _graph_get_dates(self, today):
""" return a coherent start and end date for the dashboard graph according to the graph settings.
"""
if self.dashboard_graph_model == 'crm.lead':
if self.dashboard_graph_group == 'month':
start_date = today.replace(day=1)
elif self.dashboard_graph_group == 'week':
start_date = today - relativedelta(days=today.isocalendar()[2] - 1)
else:
start_date = today
if self.dashboard_graph_period == 'week':
end_date = today + relativedelta(weeks=1)
elif self.dashboard_graph_period == 'year':
end_date = today + relativedelta(years=1)
else:
end_date = today + relativedelta(months=1)
# we take the end of the preceding month/week/day if we group by month/week/day
# (to avoid having twice the same month/week/day from different years/month/week)
if self.dashboard_graph_group == 'month':
end_date = end_date.replace(day=1) - relativedelta(days=1)
elif self.dashboard_graph_group == 'week':
end_date -= relativedelta(days=end_date.isocalendar()[2])
else:
end_date -= relativedelta(days=1)
return [start_date, end_date]
return super(Team, self)._graph_get_dates(today)
def _get_graph(self):
graph_datas = super(Team, self)._get_graph()
if self.dashboard_graph_model == 'crm.lead' and self.dashboard_graph_group_pipeline == 'stage':
stage_ids = [d['label'] for d in graph_datas[0]['values'] if d['label'] is not None]
stage_data = self.env['crm.stage'].browse(stage_ids).read(['sequence', 'name'])
stage_data = {d['id']: {'name': d['name'], 'sequence': d['sequence']} for d in stage_data}
# use "Undefined" stage for unset stage records
stage_data[None] = {'name': _('Undefined'), 'sequence': -1}
graph_datas[0]['values'] = sorted(graph_datas[0]['values'], key=lambda el: stage_data[el['label']]['sequence'])
for gdata in graph_datas[0]['values']:
gdata['label'] = stage_data[gdata['label']]['name']
return graph_datas
return self.env.ref('crm.crm_case_form_view_salesteams_opportunity').read()[0]
return super(Team,self).action_primary_channel_button()
def _graph_get_model(self):
if self.use_opportunities:
return 'crm.lead'
return super(Team,self)._graph_get_model()
def _graph_date_column(self):
if self.dashboard_graph_model == 'crm.lead':
return 'date_deadline'
return super(Team, self)._graph_date_column()
def _graph_x_query(self):
if self.dashboard_graph_model == 'crm.lead' and self.dashboard_graph_group_pipeline == 'stage':
return 'stage_id'
return super(Team, self)._graph_x_query()
if self.use_opportunities:
return 'create_date'
return super(Team,self)._graph_date_column()
def _graph_y_query(self):
if self.dashboard_graph_model == 'crm.lead':
return 'SUM(expected_revenue)'
return super(Team, self)._graph_y_query()
if self.use_opportunities:
return 'count(*)'
return super(Team,self)._graph_y_query()
def _extra_sql_conditions(self):
if self.use_opportunities:
return "AND type LIKE 'opportunity'"
return super(Team,self)._extra_sql_conditions()
def _graph_title_and_key(self):
if self.dashboard_graph_model == 'crm.lead':
return ['', _('Pipeline: Expected Revenue')] # no more title
return super(Team, self)._graph_title_and_key()
if self.use_opportunities:
return ['', _('New Opportunities')] # no more title
return super(Team,self)._graph_title_and_key()
+2 -2
View File
@@ -114,7 +114,7 @@
<group>
<field name="user_id" domain="[('share', '=', False)]"
context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'sales_team.group_sale_salesman_all_leads'], 'team_id': team_id}"/>
<field name="team_id" widget="selection" domain="[('use_leads','=',True)]"/>
<field name="team_id" widget="selection"/>
<field name="type" invisible="1"/>
</group>
<group>
@@ -887,7 +887,7 @@ for rec in records:
<menuitem id="sales_team_menu_team_pipeline"
name="Team Pipelines"
parent="crm_menu_sales"
action="sales_team.crm_team_salesteams_act"
action="sales_team.crm_team_salesteams_pipelines_act"
sequence="4"/>
<menuitem id="res_partner_menu_customer"
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0"?>
<odoo>
<menuitem
id="crm_menu_sales"
name="Sales"
parent="crm_menu_root"
sequence="1"/>
<menuitem id="sales_team_menu_team_pipeline"
name="Team Pipelines"
parent="crm_menu_sales"
action="crm_team_salesteams_pipelines_act"
sequence="4"/>
<menuitem id="res_partner_menu_customer"
name="Customers"
parent="crm_menu_sales"
action="base.action_partner_form"
sequence="5"/>
<menuitem id="crm_menu_leads"
name="Leads"
parent="crm_menu_root"
groups="crm.group_use_lead"
sequence="5"/>
</odoo>
+51 -38
View File
@@ -59,13 +59,29 @@
</field>
</record>
<record id="crm_lead_action_team_overdue_opportunity" model="ir.actions.act_window">
<field name="name">Overdue Opportunities</field>
<field name="res_model">crm.lead</field>
<field name="view_mode">kanban,tree,graph,form,calendar,pivot</field>
<field name="domain">[('type','=','opportunity')]</field>
<field name="view_id" ref="crm.crm_case_kanban_view_leads"/>
<field name="search_view_id" ref="crm.view_crm_case_opportunities_filter"/>
<field name="context">{
'search_default_team_id': [active_id],
'search_default_overdue_opp': 1,
'default_team_id': active_id,
'default_type': 'opportunity',
'default_user_id': uid,
}
</field>
</record>
<record id="action_report_crm_lead_salesteam" model="ir.actions.act_window">
<field name="name">Leads Analysis</field>
<field name="res_model">crm.lead</field>
<field name="context">{"search_default_month":1}</field>
<field name="context">{'search_default_team_id': [active_id], 'time_ranges': {'field':'create_date', 'range':'last_30_days'}}</field>
<field name="view_mode">graph,pivot,tree,form</field>
<field name="domain">[('type', '=', 'lead'),('team_id', '=', active_id)]</field>
<field name="context">{'active_test': True}</field>
<field name="search_view_id" ref="crm.crm_opportunity_report_view_search"/>
<field name="help">Leads Analysis allows you to check different CRM related information like the treatment delays or number of leads per state. You can sort out your leads analysis by different groups to get accurate grained analysis.</field>
</record>
@@ -73,8 +89,8 @@
<field name="name">Pipeline Analysis</field>
<field name="res_model">crm.lead</field>
<field name="view_mode">graph,pivot,tree,form</field>
<field name="domain">[('type', '=', 'opportunity'), ('team_id', '=', active_id)]</field>
<field name="context">{'active_test': True}</field>
<field name="search_view_id" ref="crm.crm_opportunity_report_view_search"/>
<field name="context">{'search_default_team_id': [active_id], 'form_view_ref': 'crm.crm_case_form_view_oppor', 'tree_view_ref': 'crm.crm_case_tree_view_oppor','search_default_opportunity': True, 'time_ranges': {'field':'create_date', 'range':'last_365_days'}}</field>
<field name="help">Opportunities Analysis gives you an instant access to your opportunities with information such as the expected revenue, planned cost, missed deadlines or the number of interactions per opportunity. This report is mainly used by the sales manager in order to do the periodic review with the channels of the sales pipeline.</field>
</record>
@@ -85,20 +101,16 @@
<field name="priority">12</field>
<field name="arch" type="xml">
<xpath expr="//div[@name='options_active']" position="inside">
<div class="o_row">
<div name="opportunities" class="o_row" invisible="1">
<field name="use_opportunities"/>
<label for="use_opportunities"/>
<span groups="crm.group_use_lead" class="o_row" attrs="{'invisible': [('use_opportunities', '=', False)]}">
<field name="use_leads"/>
<label for="use_leads" string="Leads"/>
</span>
</div>
</xpath>
<xpath expr="//field[@name='user_id']" position="after">
<label for="alias_name" string="Email Alias"
attrs="{'invisible': [('use_leads', '=', False), ('use_opportunities', '=', False)]}"/>
attrs="{'invisible': [('use_opportunities', '=', False)]}"/>
<div class="oe_inline" name="alias_def"
attrs="{'invisible': [('use_leads', '=', False), ('use_opportunities', '=', False)]}">
attrs="{'invisible': [('use_opportunities', '=', False)]}">
<field name="alias_id" class="oe_read_only oe_inline"
string="Email Alias" required="0"/>
<div class="oe_edit_only oe_inline" name="edit_alias" style="display: inline;" >
@@ -106,23 +118,11 @@
</div>
</div>
<field name="alias_contact" class="oe_inline"
groups="base.group_no_one"
string="Accept Emails From"
attrs="{'invisible': [('use_leads', '=', False), ('use_opportunities', '=', False)]}"/>
attrs="{'invisible': [('use_opportunities', '=', False)]}"/>
<field name="alias_user_id" string="Assign to"/>
</xpath>
<field name="dashboard_graph_period" position="attributes">
<attribute name="attrs">{'invisible': [('dashboard_graph_model', '=', 'crm.lead')]}</attribute>
</field>
<field name="dashboard_graph_period" position="after">
<field name="dashboard_graph_period_pipeline" attrs="{'invisible': [('dashboard_graph_model', '!=', 'crm.lead')]}"/>
</field>
<field name="dashboard_graph_group" position="attributes">
<attribute name="attrs">{'invisible': [('dashboard_graph_model', '=', 'crm.lead')]}</attribute>
</field>
<field name="dashboard_graph_group" position="after">
<field name="dashboard_graph_group_pipeline" attrs="{'invisible': [('dashboard_graph_model', '!=', 'crm.lead')]}"/>
</field>
</field>
</record>
@@ -183,23 +183,24 @@
<field name="alias_id"/>
<field name="alias_name"/>
<field name="alias_domain"/>
<field name="use_leads"/>
<field name="use_opportunities"/>
</xpath>
<xpath expr="//div[hasclass('o_primary')]" position="after">
<div t-if="record.use_leads.raw_value and record.alias_name.value and record.alias_domain.value">
<div t-if="record.alias_name.value and record.alias_domain.value">
<small t-translation="off"><i class="fa fa-envelope-o" aria-label="Leads" title="Leads" role="img"></i>&amp;nbsp; <field name="alias_id"/></small>
</div>
</xpath>
<xpath expr="//t[@name='first_options']" position="after">
<div class="row" t-if="record.unassigned_leads_count.raw_value">
<a name="%(crm_case_form_view_salesteams_lead)d" type="action" context="{'search_default_unassigned': True}">
<field name="unassigned_leads_count"/>
<t t-if="record.unassigned_leads_count.raw_value == 1">Unassigned Lead</t>
<t t-else="">Unassigned Leads</t>
</a>
<div class="col-8">
<a name="%(crm_case_form_view_salesteams_lead)d" type="action" context="{'search_default_unassigned': True}">
<field name="unassigned_leads_count"/>
<t t-if="record.unassigned_leads_count.raw_value == 1">Unassigned Lead</t>
<t t-else="">Unassigned Leads</t>
</a>
</div>
</div>
<div class="row" t-if="record.opportunities_count.raw_value">
<div class="col-8">
@@ -209,14 +210,26 @@
<t t-else="">Open Opportunities</t>
</a>
</div>
<div class="col-4 text-right">
<div class="col-4 text-right text-truncate">
<field name="opportunities_amount" widget="monetary"/>
</div>
</div>
<div class="row" t-if="record.overdue_opportunities_count.raw_value">
<div class="col-8">
<a name="%(crm_lead_action_team_overdue_opportunity)d" type="action">
<field name="overdue_opportunities_count"/>
<t t-if="record.overdue_opportunities_count.raw_value == 1">Overdue Opportunity</t>
<t t-else="">Overdue Opportunities</t>
</a>
</div>
<div class="col-4 text-right text-truncate">
<field name="overdue_opportunities_amount" widget="monetary"/>
</div>
</div>
</xpath>
<xpath expr="//div[hasclass('o_kanban_manage_view')]" position="inside">
<div t-if="record.use_leads.raw_value" groups="crm.group_use_lead">
<div groups="crm.group_use_lead">
<a name="%(crm_case_form_view_salesteams_lead)d" type="action">
Leads
</a>
@@ -237,7 +250,7 @@
</xpath>
<xpath expr="//div[hasclass('o_kanban_manage_reports')]" position="inside">
<div t-if="record.use_leads.raw_value" groups="crm.group_use_lead">
<div groups="crm.group_use_lead">
<a name="%(action_report_crm_lead_salesteam)d" type="action">
Leads
</a>
@@ -247,9 +260,9 @@
Opportunities
</a>
</div>
<div class="mb4" t-if="record.use_opportunities.raw_value">
<div t-if="record.use_opportunities.raw_value">
<a name="%(crm.crm_activity_report_action_team)d" type="action">
Activities Analysis
Activities
</a>
</div>
</xpath>
@@ -466,7 +466,7 @@
</div>
</div>
<h2>Accounting</h2>
<div class="row mt16 o_settings_container">
<div class="row mt16 o_settings_container" id="accounting_section">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_right_pane">
<span class="o_form_label">Journal Entries</span>
+3 -5
View File
@@ -1,13 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="pos_sales_team" model="crm.team">
<field name="name">Point of Sale</field>
<field name="team_type" eval="'pos'"/>
<field name="dashboard_graph_model">sale.report</field>
<record id="sales_team.pos_sales_team" model="crm.team">
<field name="active" eval="True"/>
</record>
<record id="point_of_sale.pos_config_main" model="pos.config">
<field name="crm_team_id" ref="pos_sales_team"/>
<field name="crm_team_id" ref="sales_team.pos_sales_team"/>
</record>
</data>
</odoo>
+3 -98
View File
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
from datetime import datetime
import pytz
@@ -9,112 +10,16 @@ import pytz
class CrmTeam(models.Model):
_inherit = 'crm.team'
team_type = fields.Selection(selection_add=[('pos', 'Point of Sale')])
pos_config_ids = fields.One2many('pos.config', 'crm_team_id', string="Point of Sales")
pos_sessions_open_count = fields.Integer(string='Open POS Sessions', compute='_compute_pos_sessions_open_count')
pos_order_amount_total = fields.Float(string="Session Sale Amount", compute='_compute_pos_order_amount_total')
dashboard_graph_group_pos = fields.Selection([
('day', 'Day'),
('week', 'Week'),
('month', 'Month'),
('user', 'Salesperson'),
('pos', 'Point of Sale'),
], string='POS Grouping', default='day', help="How this channel's dashboard graph will group the results.")
@api.onchange('dashboard_graph_group_pos')
def _onchange_dashboard_graph_group_pos(self):
if self.dashboard_graph_group_pos == 'pos':
self.dashboard_graph_group = False
else:
self.dashboard_graph_group = self.dashboard_graph_group_pos
def _compute_pos_sessions_open_count(self):
for team in self.filtered(lambda t: t.team_type == 'pos'):
for team in self:
team.pos_sessions_open_count = self.env['pos.session'].search_count([('config_id.crm_team_id', '=', team.id), ('state', '=', 'opened')])
def _compute_pos_order_amount_total(self):
for team in self.filtered(lambda t: t.team_type == 'pos'):
for team in self:
team.pos_order_amount_total = sum(self.env['report.pos.order'].search(
[('session_id', 'in', team.pos_config_ids.mapped('session_ids').filtered(lambda s: s.state == 'opened').ids)]
).mapped('price_total'))
def _graph_data(self, start_date, end_date):
""" If the type of the sales team is point of sale ('pos'), the graph will display the sales data.
The override here is to get data from pos.order instead of sale.order.
"""
offset = datetime.now(pytz.timezone(self.env.user.tz or 'UTC')).utcoffset()
min_date = fields.Datetime.to_string(datetime.combine(start_date, datetime.min.time()) - offset)
max_date = fields.Datetime.to_string(datetime.combine(end_date, datetime.max.time()) - offset)
if self.team_type == 'pos':
result = []
if self.dashboard_graph_group_pos == 'pos':
order_data = self.env['report.pos.order'].read_group(
domain=[
('date', '>=', min_date),
('date', '<=', max_date),
('config_id', 'in', self.pos_config_ids.ids),
('state', 'in', ['paid', 'done', 'invoiced'])],
fields=['config_id', 'price_total'],
groupby=['config_id']
)
appended_config_ids = set()
for data_point in order_data:
result.append({'x_value': self.env['pos.config'].browse(data_point.get('config_id')[0]).name, 'y_value': data_point.get('price_total')})
appended_config_ids.add(data_point.get('config_id'))
for config_id in set(self.pos_config_ids.ids) - appended_config_ids:
result.append({'x_value': self.env['pos.config'].browse(config_id).name, 'y_value': 0})
elif self.dashboard_graph_group_pos == 'user':
order_data = self.env['report.pos.order'].read_group(
domain=[
('date', '>=', min_date),
('date', '<=', max_date),
('config_id', 'in', self.pos_config_ids.ids),
('state', 'in', ['paid', 'done', 'invoiced'])],
fields=['user_id', 'price_total'],
groupby=['user_id']
)
for data_point in order_data:
result.append({'x_value': data_point.get('user_id')[0], 'y_value': data_point.get('price_total')})
elif self.dashboard_graph_group_pos in ['day', 'week', 'month']:
# locale en_GB is used to be able to obtain the datetime from the string returned by read_group
# /!\ do not use en_US as it's not ISO-standard and does not match datetime's library
order_data = self.env['report.pos.order'].with_context(lang='en_GB').read_group(
domain=[
('date', '>=', min_date),
('date', '<=', max_date),
('config_id', 'in', self.pos_config_ids.ids),
('state', 'in', ['paid', 'done', 'invoiced'])],
fields=['date', 'price_total'],
groupby=['date:' + self.dashboard_graph_group_pos]
)
if self.dashboard_graph_group_pos == 'day':
for data_point in order_data:
result.append({'x_value': fields.Date.to_date(datetime.strptime(data_point.get('date:day'), "%d %b %Y")), 'y_value': data_point.get('price_total')})
elif self.dashboard_graph_group_pos == 'week':
for data_point in order_data:
result.append({'x_value': int(data_point.get('date:week')[1:3]), 'y_value': data_point.get('price_total')})
elif self.dashboard_graph_group_pos == 'month':
for data_point in order_data:
result.append({'x_value': fields.datetime.strptime(data_point.get('date:month'), "%B %Y").month, 'y_value': data_point.get('price_total')})
return result
return super(CrmTeam, self)._graph_data(start_date, end_date)
def _compute_dashboard_button_name(self):
pos_teams = self.filtered(lambda team: team.team_type == 'pos')
pos_teams.update({'dashboard_button_name': _("Dashboard")})
super(CrmTeam, self - pos_teams)._compute_dashboard_button_name()
def action_primary_channel_button(self):
if self.team_type == 'pos':
action = self.env.ref('point_of_sale.action_pos_config_kanban').read()[0]
action['context'] = {'search_default_crm_team_id': self.id}
return action
return super(CrmTeam, self).action_primary_channel_button()
def _graph_title_and_key(self):
if self.team_type == 'pos':
return ['', _('Sales: Untaxed Amount')] # no more title
return super(CrmTeam, self)._graph_title_and_key()
+10 -3
View File
@@ -7,7 +7,14 @@ from odoo import fields, models
class PosConfig(models.Model):
_inherit = 'pos.config'
def _get_default_pos_team(self):
try:
team = self.env.ref('sales_team.pos_sales_team')
return team if team.active else None
except ValueError:
return None
crm_team_id = fields.Many2one(
'crm.team', string="Sales Team", domain=[('team_type', '=', 'pos')],
default=lambda self: self.env['crm.team'].search([('team_type', '=', 'pos')], limit=1).id,
help="This Point of sale's sales will be related to this Sales Team.")
'crm.team', string="Sales Team",
help="This Point of sale's sales will be related to this Sales Team.",
default=_get_default_pos_team)
-1
View File
@@ -100,7 +100,6 @@ class SaleReport(models.Model):
u.factor,
config.crm_team_id
'''
current = '(SELECT %s FROM %s GROUP BY %s)' % (select_, from_, groupby_)
return '%s UNION ALL %s' % (res, current)
@@ -3,7 +3,6 @@
<record id="pos_sale_rule_pos_channel_pos_manager" model="ir.rule">
<field name="name">POS Sales Team</field>
<field name="model_id" ref="sales_team.model_crm_team"/>
<field name="domain_force">[('team_type', '=', 'pos')]</field>
<field name="groups" eval="[(4, ref('point_of_sale.group_pos_manager'))]"/>
</record>
</odoo>
+11 -7
View File
@@ -6,14 +6,18 @@
<field name="model">pos.config</field>
<field name="inherit_id" ref="point_of_sale.pos_config_view_form"/>
<field name="arch" type="xml">
<div id="company" position="after">
<div class="row mt16 o_settings_container" id="crm_team_id" groups="base.group_no_one">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_right_pane">
<label for="crm_team_id" class="col-lg-3"/>
<field name="crm_team_id"/>
<div id="accounting_section" position="after">
<h2>Sales Reporting</h2>
<div class="row mt16 o_settings_container">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_right_pane">
<span class="o_form_label">Sales Team</span>
<div class="text-muted">
Sales are reported to the following sales team
</div>
<field name="crm_team_id"/>
</div>
</div>
</div>
</div>
</div>
</field>
+1 -50
View File
@@ -1,22 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="crm_team_view_form_inherit_pos_sale" model="ir.ui.view">
<field name="name">crm.team.form</field>
<field name="model">crm.team</field>
<field name="inherit_id" ref="sales_team.crm_team_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='dashboard_graph_model']" position="attributes">
<attribute name="attrs">{'readonly': [('team_type', 'in', ['pos', 'ebay'])]}</attribute>
</xpath>
<xpath expr="//field[@name='dashboard_graph_group']" position="attributes">
<attribute name="attrs">{'invisible': ['|', ('dashboard_graph_model', '=', 'crm.lead'), ('team_type', '=', 'pos')]}</attribute>
</xpath>
<xpath expr="//field[@name='dashboard_graph_group']" position="after">
<field name="dashboard_graph_group_pos" string="Group by" attrs="{'invisible': [('team_type', '!=', 'pos')]}"/>
</xpath>
</field>
</record>
<record id="view_pos_session_search_inherit_pos_sale" model="ir.ui.view">
<field name="name">pos.session.search.view</field>
<field name="model">pos.session</field>
@@ -54,40 +37,8 @@
<field name="inherit_id" ref="sales_team.crm_team_salesteams_view_kanban"/>
<field name="arch" type="xml">
<data>
<xpath expr="//templates" position="before">
<field name="team_type"/>
</xpath>
<xpath expr="//div[@name='orders_to_invoice']" position="attributes">
<attribute name="attrs">{'invisible': [('team_type', '=', 'pos')]}</attribute>
</xpath>
<xpath expr="//div[@name='sales_report']" position="attributes">
<attribute name="attrs">{'invisible': [('team_type', '=', 'pos')]}</attribute>
</xpath>
<xpath expr="//div[@name='invoices_report']" position="attributes">
<attribute name="attrs">{'invisible': [('team_type', '=', 'pos')]}</attribute>
</xpath>
<xpath expr="//div[hasclass('o_kanban_manage_view')]" position="attributes">
<attribute name="attrs">{'invisible': [('team_type', '=', 'pos')]}</attribute>
</xpath>
<xpath expr="//div[hasclass('o_kanban_manage_new')]" position="attributes">
<attribute name="attrs">{'invisible': [('team_type', '=', 'pos')]}</attribute>
</xpath>
<xpath expr="//div[hasclass('o_kanban_manage_reports')]" position="inside">
<t t-if="record.team_type.raw_value == 'pos'">
<a name="%(point_of_sale.action_report_pos_order_all)d" type="action">
Sales
</a>
</t>
</xpath>
<xpath expr="//t[@name='first_options']" position="before">
<div class="row" t-if="record.team_type.raw_value == 'pos'">
<div class="row" t-if="record.pos_sessions_open_count.raw_value">
<div class="col-8">
<a name="%(pos_session_action_from_crm_team)d" type="action">
<field name="pos_sessions_open_count"/>
-11
View File
@@ -11,14 +11,3 @@ import odoo
from odoo import api, SUPERUSER_ID
def uninstall_hook(cr, registry):
def update_dashboard_graph_model(dbname):
db_registry = odoo.modules.registry.Registry.new(dbname)
with api.Environment.manage(), db_registry.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {})
if 'crm.team' in env:
recs = env['crm.team'].search([])
for rec in recs:
rec._onchange_team_type()
cr.after("commit", partial(update_dashboard_graph_model, cr.dbname))
-6
View File
@@ -1,12 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record model="crm.team" id="sales_team.team_sales_department">
<field name="use_quotations" eval="True"/>
<field name="use_invoices" eval="True"/>
<field name="dashboard_graph_model">sale.report</field>
</record>
<!-- Share Button in action menu -->
<record id="model_sale_order_action_share" model="ir.actions.server">
<field name="name">Share</field>
-8
View File
@@ -17,10 +17,7 @@
</record>
<record model="crm.team" id="sales_team.crm_team_1">
<field name="use_quotations" eval="True"/>
<field name="use_invoices" eval="True"/>
<field name="invoiced_target">40000</field>
<field name="dashboard_graph_model">sale.report</field>
</record>
<record id="sale_order_1" model="sale.order">
@@ -733,11 +730,6 @@ Thanks!</field>
<field name="confirmation_date" eval="datetime.now() - relativedelta(months=1)"/>
</record>
<function model="crm.team" name="message_unsubscribe"
eval="[ref('sales_team.team_sales_department'),ref('sales_team.salesteam_website_sales')], [ref('base.partner_root')]"/>
<function model="crm.team" name="message_subscribe"
eval="[ref('sales_team.team_sales_department'),ref('sales_team.salesteam_website_sales')], [ref('base.partner_root')]"/>
<record id="product_product_1_product_template" model="product.template">
<field name="name">Chair floor protection</field>
<field name="categ_id" ref="product.product_category_5"/>
+1 -1
View File
@@ -734,7 +734,7 @@ class SaleOrder(models.Model):
return report_pages
def has_to_be_signed(self, also_in_draft=False):
return (self.state == 'sent' or (self.state == 'draft' and also_in_draft)) and not self.is_expired and self.require_signature and not self.signature and self.team_id.team_type != 'website'
return (self.state == 'sent' or (self.state == 'draft' and also_in_draft)) and not self.is_expired and self.require_signature and not self.signature
def has_to_be_paid(self, also_in_draft=False):
transaction = self.get_portal_last_transaction()
+49 -78
View File
@@ -9,9 +9,6 @@ from odoo import api, fields, models, _
class CrmTeam(models.Model):
_inherit = 'crm.team'
use_quotations = fields.Boolean(string='Quotations', help="Check this box if you send quotations to your customers rather than confirming orders straight away. "
"This will add specific action buttons to your dashboard.")
use_invoices = fields.Boolean('Set Invoicing Target', help="Check this box to set an invoicing target for this Sales Team.")
invoiced = fields.Integer(
compute='_compute_invoiced',
string='Invoiced This Month', readonly=True,
@@ -31,36 +28,31 @@ class CrmTeam(models.Model):
sales_to_invoice_count = fields.Integer(
compute='_compute_sales_to_invoice',
string='Number of sales to invoice', readonly=True)
dashboard_graph_model = fields.Selection(selection_add=[
('sale.report', 'Sales'),
('account.invoice.report', 'Invoices'),
])
def _compute_quotations_to_invoice(self):
non_website_teams = self.filtered(lambda team: team.team_type != 'website')
if non_website_teams:
query = self.env['sale.order']._where_calc([
('team_id', 'in', non_website_teams.ids),
('state', 'in', ['draft', 'sent']),
])
self.env['sale.order']._apply_ir_rules(query, 'read')
_, where_clause, where_clause_args = query.get_sql()
select_query = """
SELECT team_id, count(*), sum(amount_total /
CASE COALESCE(currency_rate, 0)
WHEN 0 THEN 1.0
ELSE currency_rate
END
) as amount_total
FROM sale_order
WHERE %s
GROUP BY team_id
""" % where_clause
self.env.cr.execute(select_query, where_clause_args)
quotation_data = self.env.cr.dictfetchall()
for datum in quotation_data:
self.browse(datum['team_id']).quotations_amount = datum['amount_total']
self.browse(datum['team_id']).quotations_count = datum['count']
query = self.env['sale.order']._where_calc([
('team_id', 'in', self.ids),
('state', 'in', ['draft', 'sent']),
])
self.env['sale.order']._apply_ir_rules(query, 'read')
_, where_clause, where_clause_args = query.get_sql()
select_query = """
SELECT team_id, count(*), sum(amount_total /
CASE COALESCE(currency_rate, 0)
WHEN 0 THEN 1.0
ELSE currency_rate
END
) as amount_total
FROM sale_order
WHERE %s
GROUP BY team_id
""" % where_clause
self.env.cr.execute(select_query, where_clause_args)
quotation_data = self.env.cr.dictfetchall()
for datum in quotation_data:
self.browse(datum['team_id']).quotations_amount = datum['amount_total']
self.browse(datum['team_id']).quotations_count = datum['count']
@api.multi
def _compute_sales_to_invoice(self):
@@ -68,8 +60,9 @@ class CrmTeam(models.Model):
('team_id', 'in', self.ids),
('order_line.qty_to_invoice', '>', 0),
], ['team_id'], ['team_id'])
for datum in sale_order_data:
self.browse(datum['team_id'][0]).invoiced = datum['team_id_count']
data_map = {datum['team_id'][0]: datum['team_id_count'] for datum in sale_order_data }
for team in self:
team.sales_to_invoice_count = data_map.get(team.id,0.0)
@api.multi
def _compute_invoiced(self):
@@ -80,67 +73,45 @@ class CrmTeam(models.Model):
('date', '>=', date.today().replace(day=1)),
('type', 'in', ['out_invoice', 'out_refund']),
], ['amount_untaxed_signed', 'team_id'], ['team_id'])
for datum in invoice_data:
self.browse(datum['team_id'][0]).invoiced = datum['amount_untaxed_signed']
data_map = { datum['team_id'][0]: datum['amount_untaxed_signed'] for datum in invoice_data}
for team in self:
team.invoiced = data_map.get(team.id,0.0)
def _graph_get_model(self):
if self._context.get('in_sales_app'):
return 'sale.report'
return super(CrmTeam,self)._graph_get_model()
def _graph_date_column(self):
if self.dashboard_graph_model == 'sale.report':
if self._context.get('in_sales_app'):
return 'confirmation_date'
elif self.dashboard_graph_model == 'account.invoice.report':
return 'date'
return super(CrmTeam, self)._graph_date_column()
return super(CrmTeam,self)._graph_date_column()
def _graph_y_query(self):
if self.dashboard_graph_model == 'sale.report':
if self._context.get('in_sales_app'):
return 'SUM(price_subtotal)'
elif self.dashboard_graph_model == 'account.invoice.report':
return 'SUM(price_total)'
return super(CrmTeam, self)._graph_y_query()
return super(CrmTeam,self)._graph_y_query()
def _extra_sql_conditions(self):
if self.dashboard_graph_model == 'sale.report':
return "AND state in ('sale', 'done')"
elif self.dashboard_graph_model == 'account.invoice.report':
return "AND state in ('open', 'in_payment', 'paid')"
return super(CrmTeam, self)._extra_sql_conditions()
if self._context.get('in_sales_app'):
return "AND state in ('sale', 'done', 'pos_done')"
return super(CrmTeam,self)._extra_sql_conditions()
def _graph_title_and_key(self):
if self.dashboard_graph_model == 'sale.report':
if self._context.get('in_sales_app'):
return ['', _('Sales: Untaxed Total')] # no more title
elif self.dashboard_graph_model == 'account.invoice.report':
return ['', _('Invoices: Untaxed Total')]
return super(CrmTeam, self)._graph_title_and_key()
def _compute_dashboard_button_name(self):
quotation_teams = self.filtered('use_quotations')
quotation_teams.update({'dashboard_button_name': _("Quotations")})
(self - quotation_teams).update({'dashboard_button_name': _("Sales Orders")})
super(CrmTeam,self)._compute_dashboard_button_name()
if self._context.get('in_sales_app'):
self.update({'dashboard_button_name': _("Sales Analysis")})
def action_primary_channel_button(self):
if hasattr(self, 'use_opportunities') and self.use_opportunities:
return super(CrmTeam, self).action_primary_channel_button()
elif self.use_quotations:
action = self.env.ref('sale.action_quotations_salesteams').read()[0]
action['context'] = {'search_default_team_id': self.id}
return action
else:
action = self.env.ref('sale.action_orders_salesteams').read()[0]
action['context'] = {'search_default_team_id': self.id}
return action
@api.onchange('team_type')
def _onchange_team_type(self):
if self.team_type == 'sales':
self.use_quotations = True
self.use_invoices = True
if not self.dashboard_graph_model:
self.dashboard_graph_model = 'sale.report'
else:
self.use_quotations = False
self.use_invoices = False
self.dashboard_graph_model = 'sale.report'
return super(CrmTeam, self)._onchange_team_type()
if self._context.get('in_sales_app'):
return self.env.ref('sale.action_order_report_so_salesteam').read()[0]
return super(CrmTeam, self).action_primary_channel_button()
@api.multi
def update_invoiced_target(self, value):
return self.write({'invoiced_target': round(float(value or 0))})
+2 -2
View File
@@ -86,8 +86,8 @@ class TestAccessRights(TestCommonSaleNoChart):
})
self.assertIn(india_channel.id, self.env['crm.team'].search([]).ids, 'Sales manager should be able to create a Sales Team')
# Manager can edit a Sales Team
india_channel.sudo(self.user_manager).write({'dashboard_graph_group': 'week'})
self.assertEquals(india_channel.dashboard_graph_group, 'week', 'Sales manager should be able to edit a Sales Team')
india_channel.sudo(self.user_manager).write({'name': 'new_india'})
self.assertEquals(india_channel.name, 'new_india', 'Sales manager should be able to edit a Sales Team')
# Manager can delete a Sales Team
india_channel.sudo(self.user_manager).unlink()
self.assertNotIn(india_channel.id, self.env['crm.team'].search([]).ids, 'Sales manager should be able to delete a Sales Team')
+4 -4
View File
@@ -1239,8 +1239,8 @@
<field name="name">Sales Analysis</field>
<field name="res_model">sale.report</field>
<field name="view_mode">graph</field>
<field name="domain">[('state','not in',('draft','cancel')),('team_id', '=', active_id)]</field>
<field name="context">{'search_default_order_month':1}</field>
<field name="domain">[('state','not in',('draft','cancel'))]</field>
<field name="context">{'search_default_Sales': 1, 'time_ranges': {'field':'confirmation_date', 'range':'last_30_days'}, 'search_default_team_id': [active_id]}</field>
<field name="help">This report performs analysis on your sales orders. Analysis check your sales revenues and sort it by different group criteria (salesman, partner, product, etc.) Use this report to perform analysis on sales not having invoiced yet. If you want to analyse your turnover, you should use the Invoice Analysis report in the Accounting application.</field>
</record>
@@ -1248,8 +1248,8 @@
<field name="name">Invoices Analysis</field>
<field name="res_model">account.invoice.report</field>
<field name="view_mode">graph</field>
<field name="domain">[('team_id', '=', active_id),('state', 'not in', ['draft', 'cancel'])]</field>
<field name="context">{'search_default_month':1}</field>
<field name="domain">[('state', 'not in', ['draft', 'cancel'])]</field>
<field name="context">{'search_default_month':1, 'search_default_team_id': [active_id]}</field>
<field name="help">From this report, you can have an overview of the amount invoiced to your customer. The search tool can also be used to personalise your Invoices reports and so, match this analysis to your needs.</field>
</record>
</odoo>
+7 -23
View File
@@ -6,17 +6,8 @@
<field name="priority">9</field>
<field name="inherit_id" ref="sales_team.crm_team_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='options_active']" position="inside">
<div class="o_row" style="display:inherit">
<field name="use_quotations"/><label for="use_quotations"/>
</div>
</xpath>
<xpath expr="//group[@name='graph']" position="before">
<group name="invoicing" groups="account.group_account_invoice" attrs="{'invisible': [('team_type', 'not in', ['sales', 'website'])]}">
<field name="use_invoices" groups="base.group_no_one"/>
<field name="invoiced_target" attrs="{'invisible': [('use_invoices', '=', False)]}"/>
</group>
<xpath expr="//field[@name='user_id']" position="after">
<field name="invoiced_target"/>
</xpath>
</field>
</record>
@@ -58,8 +49,6 @@
<field name="arch" type="xml">
<data>
<xpath expr="//templates" position="before">
<field name="use_quotations"/>
<field name="use_invoices"/>
<field name="invoiced_target"/>
</xpath>
@@ -89,17 +78,17 @@
<xpath expr="//div[hasclass('bottom_block')]" position="inside">
<t groups="sales_team.group_sale_manager">
<t t-if="record.use_invoices.raw_value &amp;&amp; record.invoiced_target.raw_value" class="col-12 o_kanban_primary_bottom">
<t t-if="record.invoiced_target.raw_value" class="col-12 o_kanban_primary_bottom">
<field name="invoiced" widget="progressbar" title="Invoicing" options="{'current_value': 'invoiced', 'max_value': 'invoiced_target', 'editable': true, 'edit_max_value': true, 'on_change': 'update_invoiced_target'}"/>
</t>
<t t-if="record.use_invoices.raw_value &amp;&amp; !record.invoiced_target.raw_value" class="col-12 o_kanban_primary_bottom text-center">
<a href="#" class="sales_team_target_definition o_inline_link">Click to define a target</a>
<t t-if="!record.invoiced_target.raw_value" class="col-12 o_kanban_primary_bottom text-center">
<a href="#" class="sales_team_target_definition o_inline_link">Click to define an invoicing target</a>
</t>
</t>
</xpath>
<xpath expr="//div[hasclass('o_kanban_manage_view')]" position="inside">
<div t-if="record.use_quotations.raw_value">
<div>
<a name="%(action_quotations_salesteams)d" type="action" class="o_quotation_view_button">Quotations</a>
</div>
<div>
@@ -111,7 +100,7 @@
</xpath>
<xpath expr="//div[hasclass('o_kanban_manage_new')]" position="inside">
<div t-if="record.use_quotations.raw_value">
<div>
<a name="%(action_quotation_form)d" type="action">
Quotation
</a>
@@ -119,11 +108,6 @@
</xpath>
<xpath expr="//div[hasclass('o_kanban_manage_reports')]" position="inside">
<div t-if="record.use_quotations.raw_value">
<a name="%(action_order_report_quotation_salesteam)d" type="action">
Quotation
</a>
</div>
<div name="sales_report">
<a name="%(action_order_report_so_salesteam)d" type="action">
Sales
+7
View File
@@ -2,3 +2,10 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
from odoo import api, SUPERUSER_ID
def uninstall_hook(cr, registry):
env = api.Environment(cr, SUPERUSER_ID, {})
teams = env['crm.team'].search([('use_opportunities', '=', False)])
teams.write({'use_opportunities': True})
+2
View File
@@ -22,6 +22,8 @@ modules.
'views/partner_views.xml',
'views/sale_order_views.xml',
'views/crm_lead_views.xml',
'views/crm_team_views.xml'
],
'auto_install': True,
'uninstall_hook': 'uninstall_hook'
}
+34 -2
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, _
from odoo import models,fields, api, _
class CrmTeam(models.Model):
@@ -9,4 +9,36 @@ class CrmTeam(models.Model):
def _compute_dashboard_button_name(self):
super(CrmTeam, self)._compute_dashboard_button_name()
self.filtered(lambda team: team.use_opportunities and team.team_type in ('sales', 'website')).update({'dashboard_button_name': _("Pipeline")})
teams_with_opp = self.filtered(lambda team: team.use_opportunities)
if self._context.get('in_sales_app'):
teams_with_opp.update({'dashboard_button_name': _("Sales Analysis")})
def action_primary_channel_button(self):
if self._context.get('in_sales_app') and self.use_opportunities:
return self.env.ref('sale.action_order_report_so_salesteam').read()[0]
return super(CrmTeam,self).action_primary_channel_button()
def _graph_get_model(self):
if self.use_opportunities and self._context.get('in_sales_app') :
return 'sale.report'
return super(CrmTeam,self)._graph_get_model()
def _graph_date_column(self):
if self.use_opportunities and self._context.get('in_sales_app'):
return 'confirmation_date'
return super(CrmTeam,self)._graph_date_column()
def _graph_y_query(self):
if self.use_opportunities and self._context.get('in_sales_app'):
return 'SUM(price_subtotal)'
return super(CrmTeam,self)._graph_y_query()
def _graph_title_and_key(self):
if self.use_opportunities and self._context.get('in_sales_app'):
return ['', _('Sales: Untaxed Total')]
return super(CrmTeam,self)._graph_title_and_key()
def _extra_sql_conditions(self):
if self.use_opportunities and self._context.get('in_sales_app'):
return "AND state in ('sale', 'done', 'pos_done')"
return super(CrmTeam,self)._extra_sql_conditions()
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="crm_team_salesteams_view_form_in_sale_crm" model="ir.ui.view">
<field name="name">crm.team.form</field>
<field name="model">crm.team</field>
<field name="inherit_id" ref="crm.sales_team_form_view_in_crm"/>
<field name="arch" type="xml">
<data>
<xpath expr="//div[@name='opportunities']" position="attributes">
<attribute name="invisible" value="0"/>
</xpath>
</data>
</field>
</record>
<record id="sales_team.crm_team_salesteams_act" model="ir.actions.act_window">
<field name="context">{'in_sales_app': True}</field>
</record>
<record id="sales_team.crm_team_salesteams_pipelines_act" model="ir.actions.act_window">
<field name="domain">[('use_opportunities', '=', True)]</field>
</record>
</odoo>
+18 -1
View File
@@ -11,8 +11,25 @@
<field name="name">Website</field>
<field name="company_id" eval="False"/>
<field name="active" eval="False"/>
<field name="team_type" eval="'website'"/>
</record>
<record id="pos_sales_team" model="crm.team">
<field name="name">Point of Sale</field>
<field name="active" eval="False"/>
</record>
<record id="ebay_sales_team" model="crm.team">
<field name="name">eBay</field>
<field name="active" eval="False"/>
</record>
<function model="crm.team" name="message_unsubscribe"
eval="[
ref('sales_team.team_sales_department'),
ref('sales_team.salesteam_website_sales'),
ref('sales_team.ebay_sales_team'),
ref('sales_team.pos_sales_team')],
[ref('base.partner_root')]"/>
</data>
<record id="sales_team.group_sale_salesman" model="res.groups">
+2 -2
View File
@@ -4,7 +4,7 @@
<!-- rename for demo-data -->
<record model="crm.team" id="team_sales_department">
<field name="name">Europe</field>
<field name="dashboard_graph_group">week</field>
<field name="member_ids" eval="[(4, ref('base.user_demo'))]"/>
<field name="company_id" eval="False"/>
</record>
<record id="base.user_demo" model="res.users">
@@ -17,8 +17,8 @@
<record model="crm.team" id="crm_team_1">
<field name="name">America</field>
<field name="dashboard_graph_group">week</field>
<field name="company_id" eval="False"/>
<field name="member_ids" eval="[(4, ref('base.user_demo'))]"/>
</record>
</data>
</odoo>
+43 -109
View File
@@ -38,10 +38,11 @@ class CrmTeam(models.Model):
default_team_id.check_access_rule('read')
except AccessError:
return self.env['crm.team']
if self.env.context.get('default_type') != 'lead' or default_team_id.use_leads and default_team_id.active:
if self.env.context.get('default_type') != 'lead' and default_team_id.active:
team_id = default_team_id
return team_id
def _get_default_favorite_user_ids(self):
return [(6, 0, [self.env.uid])]
@@ -53,7 +54,7 @@ class CrmTeam(models.Model):
"res.currency", related='company_id.currency_id',
string="Currency", readonly=True)
user_id = fields.Many2one('res.users', string='Team Leader')
member_ids = fields.One2many('res.users', 'sale_team_id', string='Channel Members')
member_ids = fields.One2many('res.users', 'sale_team_id', string='Channel Members', domain= lambda self: [('groups_id', 'in', self.env.ref('base.group_user').id)], help="Add members to automatically assign their documents to this sales team. You can only be member of one team.")
favorite_user_ids = fields.Many2many(
'res.users', 'team_favorite_user_rel', 'team_id', 'user_id',
string='Favorite Members',
@@ -65,35 +66,11 @@ class CrmTeam(models.Model):
reply_to = fields.Char(string='Reply-To',
help="The email address put in the 'Reply-To' of all emails sent by Odoo about cases in this Sales Team")
color = fields.Integer(string='Color Index', help="The color of the channel")
team_type = fields.Selection([('sales', 'Sales'), ('website', 'Website')], string='Team Type', default='sales', required=True,
help="The type of this channel, it will define the resources this channel uses.")
dashboard_button_name = fields.Char(string="Dashboard Button", compute='_compute_dashboard_button_name')
dashboard_graph_data = fields.Text(compute='_compute_dashboard_graph')
dashboard_graph_type = fields.Selection([
('line', 'Line'),
('bar', 'Bar'),
], string='Type', compute='_compute_dashboard_graph', help='The type of graph this channel will display in the dashboard.')
dashboard_graph_model = fields.Selection([], string="Content", help='The graph this channel will display in the Dashboard.\n')
dashboard_graph_group = fields.Selection([
('day', 'Day'),
('week', 'Week'),
('month', 'Month'),
('user', 'Salesperson'),
], string='Group by', default='day', help="How this channel's dashboard graph will group the results.")
dashboard_graph_period = fields.Selection([
('week', 'Last Week'),
('month', 'Last Month'),
('year', 'Last Year'),
], string='Scale', default='month', help="The time period this channel's dashboard graph will consider.")
@api.depends('dashboard_graph_group', 'dashboard_graph_model', 'dashboard_graph_period')
def _compute_dashboard_graph(self):
for team in self.filtered('dashboard_graph_model'):
if team.dashboard_graph_group in (False, 'user') or team.dashboard_graph_period == 'week' and team.dashboard_graph_group != 'day' \
or team.dashboard_graph_period == 'month' and team.dashboard_graph_group != 'day':
team.dashboard_graph_type = 'bar'
else:
team.dashboard_graph_type = 'line'
for team in self:
team.dashboard_graph_data = json.dumps(team._get_graph())
def _compute_is_favorite(self):
@@ -107,45 +84,26 @@ class CrmTeam(models.Model):
(sudoed_self - to_fav).write({'favorite_user_ids': [(3, self.env.uid)]})
return True
def _graph_get_dates(self, today):
""" return a coherent start and end date for the dashboard graph according to the graph settings.
def _graph_get_model(self):
""" skeleton function defined here because it'll be called by crm and/or sale
"""
if self.dashboard_graph_period == 'week':
start_date = today - relativedelta(weeks=1)
elif self.dashboard_graph_period == 'year':
start_date = today - relativedelta(years=1)
else:
start_date = today - relativedelta(months=1)
# we take the start of the following month/week/day if we group by month/week/day
# (to avoid having twice the same month/week/day from different years/month/week)
if self.dashboard_graph_group == 'month':
start_date = date(start_date.year + start_date.month // 12, start_date.month % 12 + 1, 1)
# handle period=week, grouping=month for silly managers
if self.dashboard_graph_period == 'week':
start_date = today.replace(day=1)
elif self.dashboard_graph_group == 'week':
start_date += relativedelta(days=8 - start_date.isocalendar()[2])
# add a week to make sure no overlapping is possible in case of year period (will display max 52 weeks, avoid case of 53 weeks in a year)
if self.dashboard_graph_period == 'year':
start_date += relativedelta(weeks=1)
else:
start_date += relativedelta(days=1)
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_x_query(self):
if self.dashboard_graph_group == 'user':
return 'user_id'
elif self.dashboard_graph_group == 'week':
return 'EXTRACT(WEEK FROM %s)' % self._graph_date_column()
elif self.dashboard_graph_group == 'month':
return 'EXTRACT(MONTH FROM %s)' % self._graph_date_column()
else:
return 'DATE(%s)' % self._graph_date_column()
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)
@@ -162,7 +120,7 @@ class CrmTeam(models.Model):
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 either be dates, weeks, months or user_ids depending on the self.dashboard_graph_group value.
x_values should be weeks.
y_values are floats.
"""
query = """SELECT %(x_query)s as x_value, %(y_query)s as y_value
@@ -174,9 +132,8 @@ class CrmTeam(models.Model):
GROUP BY x_value;"""
# apply rules
if not self.dashboard_graph_model:
raise UserError(_('Undefined graph model for Sales Team: %s') % self.name)
GraphModel = self.env[self.dashboard_graph_model]
dashboard_graph_model = self._graph_get_model()
GraphModel = self.env[dashboard_graph_model]
graph_table = GraphModel._table
extra_conditions = self._extra_sql_conditions()
where_query = GraphModel._where_calc([])
@@ -195,6 +152,7 @@ class CrmTeam(models.Model):
'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()
@@ -217,51 +175,20 @@ class CrmTeam(models.Model):
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)
# line graphs and bar graphs require different labels
if self.dashboard_graph_type == 'line':
x_field = 'x'
y_field = 'y'
else:
x_field = 'label'
y_field = 'value'
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'
if self.dashboard_graph_group == 'day':
for day in range(0, (end_date - start_date).days + 1):
short_name = format_date(start_date + relativedelta(days=day), 'd MMM', locale=locale)
values.append({x_field: short_name, y_field: 0})
for data_item in graph_data:
index = (data_item.get('x_value') - start_date).days
values[index][y_field] = data_item.get('y_value')
weeks_in_start_year = int(date(start_date.year, 12, 28).isocalendar()[1]) # This date is always in the last week of ISO years
for week in range(0, (end_date.isocalendar()[1] - start_date.isocalendar()[1]) % weeks_in_start_year + 1):
short_name = get_week_name(start_date + relativedelta(days=7 * week), locale)
values.append({x_field: short_name, y_field: 0})
elif self.dashboard_graph_group == 'week':
weeks_in_start_year = int(date(start_date.year, 12, 28).isocalendar()[1]) # This date is always in the last week of ISO years
for week in range(0, (end_date.isocalendar()[1] - start_date.isocalendar()[1]) % weeks_in_start_year + 1):
short_name = get_week_name(start_date + relativedelta(days=7 * week), locale)
values.append({x_field: short_name, y_field: 0})
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')
elif self.dashboard_graph_group == 'month':
for month in range(0, (end_date.month - start_date.month) % 12 + 1):
short_name = format_date(start_date + relativedelta(months=month), 'MMM', locale=locale)
values.append({x_field: short_name, y_field: 0})
for data_item in graph_data:
index = int((data_item.get('x_value') - start_date.month) % 12)
values[index][y_field] = data_item.get('y_value')
elif self.dashboard_graph_group == 'user':
for data_item in graph_data:
values.append({x_field: self.env['res.users'].browse(data_item.get('x_value')).name or _('Not Defined'), y_field: data_item.get('y_value')})
else:
for data_item in graph_data:
values.append({x_field: data_item.get('x_value'), y_field: data_item.get('y_value')})
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'
@@ -279,11 +206,6 @@ class CrmTeam(models.Model):
"""
return False
def _onchange_team_type(self):
""" skeleton function defined here because it'll be called by crm and/or sale
"""
self.ensure_one()
@api.model
def create(self, values):
team = super(CrmTeam, self.with_context(mail_create_nosubscribe=True)).create(values)
@@ -298,6 +220,18 @@ class CrmTeam(models.Model):
self._add_members_to_favorites()
return res
@api.multi
def unlink(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)))
return super(CrmTeam,self).unlink()
def _add_members_to_favorites(self):
for team in self:
team.favorite_user_ids = [(4, member.id) for member in team.member_ids]
+1 -11
View File
@@ -9,14 +9,4 @@ class ResUsers(models.Model):
sale_team_id = fields.Many2one(
'crm.team', "User's Sales Team",
help='Sales Team the user is member of. Used to compute the members of a Sales Team through the inverse one2many')
@api.model
def create(self, vals):
# Assign the new user in the sales team if there's only one sales team of type `Sales`
user = super(ResUsers, self).create(vals)
if user.has_group('sales_team.group_sale_salesman') and not user.sale_team_id:
teams = self.env['crm.team'].search([('team_type', '=', 'sales')])
if len(teams.ids) == 1:
user.sale_team_id = teams.id
return user
help='Sales Team the user is member of. Used to compute the members of a Sales Team through the inverse one2many')
@@ -25,7 +25,6 @@
<record id="crm_rule_all_salesteam" model="ir.rule">
<field name="name">All Salesteam</field>
<field ref="sales_team.model_crm_team" name="model_id"/>
<field name="domain_force">[('team_type','in', ['sales', 'website'])]</field>
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman_all_leads'))]"/>
</record>
+19 -13
View File
@@ -8,8 +8,6 @@
<field name="model">crm.team</field>
<field name="arch" type="xml">
<search string="Salesteams Search">
<filter name="my_favorites" string="My Favorites" domain="[('favorite_user_ids', 'in', uid)]"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
<field name="name"/>
<field name="user_id"/>
@@ -22,11 +20,27 @@
<!-- Case Teams Action -->
<record id="crm_team_salesteams_act" model="ir.actions.act_window">
<field name="name">Sales Teams</field>
<field name="res_model">crm.team</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,form</field>
<field name="view_id" ref="crm_team_salesteams_search"/>
<field name="context">{'in_sales_app': True}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Define a new Sales Team
</p><p>
Use Sales Teams to organize your sales departments.
Each channel will work with a separate pipeline.
</p>
</field>
</record>
<record id="crm_team_salesteams_pipelines_act" model="ir.actions.act_window">
<field name="name">Team Pipelines</field>
<field name="res_model">crm.team</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,form</field>
<field name="context">{'search_default_my_favorites': 1}</field>
<field name="view_id" ref="crm_team_salesteams_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
@@ -56,11 +70,10 @@
<h1>
<field name="name" placeholder="Salesteam name..."/>
</h1>
<div name="options_active" attrs="{'invisible': ['&amp;', ('team_type', '!=', 'sales'), ('team_type', '!=', 'website')]}"/>
<div name="options_active" />
</div>
<group>
<group name="left">
<field name="team_type" groups="base.group_no_one"/>
<field name="user_id" domain="[('share', '=', False)]"/>
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
</group>
@@ -68,7 +81,7 @@
</group>
</group>
<notebook>
<page name="members" string="Team Members" attrs="{'invisible': [('team_type', 'in', ['pos', 'ebay'])]}">
<page name="members" string="Team Members" >
<field name="member_ids" widget="many2many" options="{'not_delete': True}">
<kanban quick_create="false" create="true" delete="true">
<field name="id"/>
@@ -88,13 +101,6 @@
</kanban>
</field>
</page>
<page string="Dashboard" name="dashboard">
<group string="Graph" name="graph">
<field name="dashboard_graph_model" attrs="{'readonly': [('team_type', 'in', ['pos', 'ebay'])]}"/>
<field name="dashboard_graph_period"/>
<field name="dashboard_graph_group"/>
</group>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
@@ -9,7 +9,6 @@
<field name="user_id"/>
<field name="member_ids"/>
<field name="color"/>
<field name="dashboard_graph_type"/>
<field name="currency_id"/>
<field name="is_favorite"/>
<templates>
@@ -25,10 +24,10 @@
</div>
<div class="container o_kanban_card_content">
<div class="row o_kanban_card_upper_content">
<div class="col-6 o_kanban_primary_left" name="to_replace_in_sale_crm">
<div class="col-5 o_kanban_primary_left" name="to_replace_in_sale_crm">
<button type="object" class="btn btn-primary" name="action_primary_channel_button"><field name="dashboard_button_name"/></button>
</div>
<div class="col-6 o_kanban_primary_right" style="padding-bottom:0;">
<div class="col-7 o_kanban_primary_right" style="padding-bottom:0;">
<t name="first_options"/>
<t name="second_options"/>
<t name="third_options"/>
@@ -68,17 +67,12 @@
<a type="edit">Settings</a>
</div>
</div>
<div class="row mb16">
<div role="menuitem" class="col-6">
<field name="is_favorite" widget="boolean_favorite" />
</div>
</div>
</div>
</div>
</t>
<t t-name="SalesTeamDashboardGraph">
<div t-if="record.dashboard_graph_data.raw_value" class="o_sales_team_kanban_graph_section">
<field name="dashboard_graph_data" widget="dashboard_graph" t-att-graph_type="record.dashboard_graph_type.raw_value"/>
<field name="dashboard_graph_data" widget="dashboard_graph" t-att-graph_type="'bar'"/>
</div>
</t>
</templates>
+2 -3
View File
@@ -22,10 +22,9 @@ class Website(models.Model):
_inherit = 'website'
def _get_crm_default_team_domain(self):
if self.env.user.has_group('crm.group_use_lead'):
return [('use_leads', '=', True)]
else:
if not self.env.user.has_group('crm.group_use_lead'):
return [('use_opportunities', '=', True)]
return []
crm_default_team_id = fields.Many2one(
'crm.team', string='Default Sales Teams',
@@ -7,10 +7,9 @@ class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
def _get_crm_default_team_domain(self):
if self.env.user.has_group('crm.group_use_lead'):
return [('use_leads', '=', True)]
else:
if not self.env.user.has_group('crm.group_use_lead'):
return [('use_opportunities', '=', True)]
return []
crm_default_team_id = fields.Many2one(
'crm.team', string='Default Sales Team', related='website_id.crm_default_team_id', readonly=False,
@@ -105,8 +105,6 @@ class TestPartnerLeadPortal(TestCrmCases):
# Sales Team of crm_salesman
self.team = self.env['crm.team'].with_context(mail_notrack=True).create({
'name': 'Test Team FOR THE WIN',
'use_leads': True,
'use_opportunities': True,
'member_ids': [(6, 0, [self.crm_salesman.id])],
})
+1 -5
View File
@@ -43,7 +43,6 @@ class WebsiteSaleBackend(WebsiteBackend):
report_product_lines = request.env['sale.report'].read_group(
domain=[
('website_id', '=', current_website.id),
('team_id.team_type', '=', 'website'),
('state', 'in', ['sale', 'done']),
('confirmation_date', '>=', date_from),
('confirmation_date', '<=', fields.Datetime.now())],
@@ -61,7 +60,7 @@ class WebsiteSaleBackend(WebsiteBackend):
# Sale-based results computation
sale_order_domain = [
('website_id', '=', current_website.id),
('team_id', 'in', request.env['crm.team'].search([('team_type', '=', 'website')]).ids),
('team_id', 'in', request.env['crm.team'].search([('website_ids', '!=', False)]).ids),
('date_order', '>=', fields.Datetime.to_string(datetime_from)),
('date_order', '<=', fields.Datetime.to_string(datetime_to))]
so_group_data = request.env['sale.order'].read_group(sale_order_domain, fields=['state'], groupby='state')
@@ -75,7 +74,6 @@ class WebsiteSaleBackend(WebsiteBackend):
report_price_lines = request.env['sale.report'].read_group(
domain=[
('website_id', '=', current_website.id),
('team_id.team_type', '=', 'website'),
('state', 'in', ['sale', 'done']),
('date', '>=', date_from),
('date', '<=', date_to)],
@@ -116,7 +114,6 @@ class WebsiteSaleBackend(WebsiteBackend):
sales_domain = [
('website_id', '=', current_website.id),
('team_id.team_type', '=', 'website'),
('state', 'in', ['sale', 'done']),
('confirmation_date', '>=', date_from),
('confirmation_date', '<=', fields.Datetime.now())
@@ -132,7 +129,6 @@ class WebsiteSaleBackend(WebsiteBackend):
return results
def _compute_sale_graph(self, date_from, date_to, sales_domain, previous=False):
days_between = (date_to - date_from).days
date_list = [(date_from + timedelta(days=x)) for x in range(0, days_between + 1)]
+1 -4
View File
@@ -27,11 +27,8 @@
<field name="html_class">oe_image_full</field>
</record>
<record model="crm.team" id="sales_team.salesteam_website_sales">
<record id="sales_team.salesteam_website_sales" model="crm.team">
<field name="active" eval="True"/>
<field name="dashboard_graph_model">sale.report</field>
<field name="dashboard_graph_period">week</field>
<field name="company_id" eval="False"/>
</record>
<record model="website" id="website.default_website">
+3 -7
View File
@@ -4,7 +4,8 @@
from datetime import datetime
from dateutil.relativedelta import relativedelta
from odoo import fields, models, _
from odoo import fields,api, models, _
from odoo.exceptions import UserError, ValidationError
class CrmTeam(models.Model):
@@ -22,7 +23,7 @@ class CrmTeam(models.Model):
# abandoned carts to recover are draft sales orders that have no order lines,
# a partner other than the public user, and created over an hour ago
# and the recovery mail was not yet sent
website_teams = self.filtered(lambda team: team.team_type == 'website')
website_teams = self.filtered(lambda team: team.website_ids)
if website_teams:
abandoned_carts_data = self.env['sale.order'].read_group([
('is_abandoned_cart', '=', True),
@@ -34,11 +35,6 @@ class CrmTeam(models.Model):
team.abandoned_carts_count = counts.get(team.id, 0)
team.abandoned_carts_amount = amounts.get(team.id, 0)
def _compute_dashboard_button_name(self):
website_teams = self.filtered(lambda team: team.team_type == 'website' and not team.use_quotations)
website_teams.update({'dashboard_button_name': _("Online Sales")})
super(CrmTeam, self - website_teams)._compute_dashboard_button_name()
def get_abandoned_carts(self):
self.ensure_one()
return {
+1 -1
View File
@@ -20,7 +20,7 @@ class Digest(models.Model):
('confirmation_date', '>=', start),
('confirmation_date', '<', end),
('state', 'not in', ['draft', 'cancel', 'sent']),
('team_id.team_type', '=', 'website'),
('website_id', '!=', False),
('company_id', '=', company.id)
])
record.kpi_website_sale_total_value = sum(confirmed_website_sales.mapped('amount_total'))
@@ -16,7 +16,7 @@ class ResConfigSettings(models.TransientModel):
return False
salesperson_id = fields.Many2one('res.users', related='website_id.salesperson_id', string='Salesperson', readonly=False)
salesteam_id = fields.Many2one('crm.team', related='website_id.salesteam_id', string='Sales Team', domain=[('team_type', '!=', 'pos')], readonly=False)
salesteam_id = fields.Many2one('crm.team', related='website_id.salesteam_id', string='Sales Team', readonly=False)
module_website_sale_delivery = fields.Boolean("eCommerce Shipping Costs")
# field used to have a nice radio in form view, resuming the 2 fields above
sale_delivery_settings = fields.Selection([
@@ -19,7 +19,6 @@ class ResPartner(models.Model):
if request and hasattr(request, 'website') and not is_public:
partner.last_website_so_id = SaleOrder.search([
('partner_id', '=', partner.id),
('team_id.team_type', '=', 'website'),
('website_id', '=', request.website.id),
('state', '=', 'draft'),
], order='write_date desc', limit=1)
+3 -3
View File
@@ -41,12 +41,12 @@ class SaleOrder(models.Model):
order.only_services = all(l.product_id.type in ('service', 'digital') for l in order.website_order_line)
@api.multi
@api.depends('team_id.team_type', 'date_order', 'order_line', 'state', 'partner_id')
@api.depends('website_id', 'date_order', 'order_line', 'state', 'partner_id')
def _compute_abandoned_cart(self):
abandoned_delay = self.website_id and self.website_id.cart_abandoned_delay or 1.0
abandoned_datetime = datetime.utcnow() - relativedelta(hours=abandoned_delay)
for order in self:
domain = order.date_order and order.date_order <= abandoned_datetime and order.team_id.team_type == 'website' and order.state == 'draft' and order.partner_id.id != self.env.ref('base.public_partner').id and order.order_line
domain = order.date_order and order.date_order <= abandoned_datetime and order.state == 'draft' and order.partner_id.id != self.env.ref('base.public_partner').id and order.order_line
order.is_abandoned_cart = bool(domain)
def _search_abandoned_cart(self, operator, value):
@@ -54,7 +54,7 @@ class SaleOrder(models.Model):
abandoned_datetime = fields.Datetime.to_string(datetime.utcnow() - relativedelta(hours=abandoned_delay))
abandoned_domain = expression.normalize_domain([
('date_order', '<=', abandoned_datetime),
('team_id.team_type', '=', 'website'),
('website_id', '!=', False),
('state', '=', 'draft'),
('partner_id', '!=', self.env.ref('base.public_partner').id),
('order_line', '!=', False)
+11 -1
View File
@@ -16,7 +16,17 @@ class Website(models.Model):
pricelist_id = fields.Many2one('product.pricelist', compute='_compute_pricelist_id', string='Default Pricelist')
currency_id = fields.Many2one('res.currency', related='pricelist_id.currency_id', related_sudo=False, string='Default Currency', readonly=False)
salesperson_id = fields.Many2one('res.users', string='Salesperson')
salesteam_id = fields.Many2one('crm.team', string='Sales Team')
def _get_default_website_team(self):
try:
team = self.env.ref('sales_team.salesteam_website_sales')
return team if team.active else None
except ValueError:
return None
salesteam_id = fields.Many2one('crm.team',
string='Sales Team',
default=_get_default_website_team)
pricelist_ids = fields.One2many('product.pricelist', compute="_compute_pricelist_ids",
string='Price list available for this Ecommerce/Website')
+1 -4
View File
@@ -7,11 +7,8 @@
<field name="inherit_id" ref="sales_team.crm_team_salesteams_view_kanban"/>
<field name="arch" type="xml">
<data>
<xpath expr="//templates" position="before">
<field name="team_type"/>
</xpath>
<xpath expr="//t[@name='third_options']" position="after">
<div class="row" t-if="record.team_type.raw_value == 'website' &amp;&amp; record.abandoned_carts_count.raw_value">
<div class="row" t-if="record.abandoned_carts_count.raw_value">
<div class="col-8">
<div>
<a name="get_abandoned_carts" type="object">
@@ -288,7 +288,7 @@
<div class="content-group">
<div class="row mt16">
<label class="o_light_label col-lg-3" string="Sales Team" for="salesteam_id"/>
<field name="salesteam_id" context="{'default_team_type': 'website'}"/>
<field name="salesteam_id" />
</div>
<div class="row">
<label class="o_light_label col-lg-3" for="salesperson_id"/>
@@ -8,12 +8,12 @@
<field name="arch" type="xml">
<xpath expr="//filter[@name='my_sale_orders_filter']" position="replace">
<filter string="Confirmed Orders" name="order_confirmed" domain="[('state', 'in', ('sale', 'done'))]"/>
<filter string="Unpaid Orders" name="order_unpaid" domain="[('state', '=', 'sent'), ('team_id.team_type', '=', 'website')]"/>
<filter string="Unpaid Orders" name="order_unpaid" domain="[('state', '=', 'sent'), ('website_id', '!=', False)]"/>
<filter string="Abandoned Carts" name="order_abandoned" domain="[('is_abandoned_cart', '=', True)]"/>
<separator/>
<filter string="Order Date" name="order_date" date="date_order"/>
<separator/>
<filter string="From Website" name="from_website" domain="[('team_id.team_type', '=', 'website')]"/>
<filter string="From Website" name="from_website" domain="[('website_id', '!=', False)]"/>
<separator/>
</xpath>
</field>
@@ -72,7 +72,7 @@
<field name="name">Unpaid Orders</field>
<field name="res_model">sale.order</field>
<field name="view_mode">tree,form,kanban,activity</field>
<field name="domain">[('state', '=', 'sent'), ('team_id.team_type', '=', 'website')]</field>
<field name="domain">[('state', '=', 'sent'), ('website_id', '!=', False)]</field>
<field name="context">{'show_sale': True, 'create': False}</field>
<field name="search_view_id" ref="view_sales_order_filter_ecommerce"/>
<field name="help" type="html">
@@ -119,7 +119,7 @@
<field name="name">Orders To Invoice</field>
<field name="res_model">sale.order</field>
<field name="view_mode">tree,form,kanban</field>
<field name="domain">[('state', 'in', ('sale', 'done')), ('invoice_status', '=', 'to invoice'), ('team_id.team_type', '=', 'website')]</field>
<field name="domain">[('state', 'in', ('sale', 'done')), ('invoice_status', '=', 'to invoice'), ('website_id', '!=', False)]</field>
<field name="context">{'show_sale': True, 'search_default_order_confirmed': 1, 'create': False}</field>
<field name="search_view_id" ref="view_sales_order_filter_ecommerce"/>
<field name="help" type="html">
@@ -147,7 +147,7 @@
<field name="res_model">sale.order</field>
<field name="view_mode">tree,kanban,form,activity</field>
<field name="view_type">form</field>
<field name="domain">[('state', '=', 'sent'), ('team_id.team_type', '=', 'website')]</field>
<field name="domain">[('state', '=', 'sent'), ('website_id', '!=', False)]</field>
<field name="context" eval="{'show_sale': True, 'create': False}"/>
<field name="view_id" ref="sale.view_quotation_tree"/>
<field name="search_view_id" ref="view_sales_order_filter_ecommerce_unpaid"/>
@@ -165,7 +165,7 @@
<field name="res_model">sale.order</field>
<field name="view_type">form</field>
<field name="view_mode">tree,kanban,form,activity</field>
<field name="domain">[('state', '=', 'draft'), ('order_line', '!=', False), ('partner_id', '!=', context.get('public_partner_id')), ('team_id.team_type', '=', 'website'), ('date_order', '&lt;=', (datetime.datetime.utcnow() - datetime.timedelta(hours=1)).strftime('%Y-%m-%d %H:%M:%S'))]</field>
<field name="domain">[('state', '=', 'draft'), ('order_line', '!=', False), ('partner_id', '!=', context.get('public_partner_id')), ('website_id', '!=', False), ('date_order', '&lt;=', (datetime.datetime.utcnow() - datetime.timedelta(hours=1)).strftime('%Y-%m-%d %H:%M:%S'))]</field>
<field name="context" eval="{'show_sale': True, 'create': False, 'public_partner_id': ref('base.public_partner'), 'search_default_recovery_email': True}"/>
<field name="view_id" ref="sale.view_quotation_tree"/>
<field name="search_view_id" ref="view_sales_order_filter_ecommerce_abondand"/>
@@ -54,7 +54,7 @@
<field name="name">Online Sales Analysis</field>
<field name="res_model">sale.report</field>
<field name="view_mode">pivot,graph</field>
<field name="domain">[('state','in',('sale', 'done')), ('team_id.team_type', '=', 'website')]</field>
<field name="domain">[('state','in',('sale', 'done')), ('website_id', '!=', False)]</field>
<field name="search_view_id" ref="sale_report_view_search_website"/>
<field name="help" type="html">
<p class="o_view_nocontent_empty_folder">
@@ -81,7 +81,7 @@
<field name="name">Sales</field>
<field name="res_model">sale.report</field>
<field name="view_mode">pivot,graph</field>
<field name="domain">[('team_id.team_type', '=', 'website')]</field>
<field name="domain">[('website_id', '!=', False)]</field>
<field name="search_view_id" ref="sale_report_view_search_website"/>
<field name="help" type="html">
<p class="o_view_nocontent_empty_folder">
@@ -16,7 +16,7 @@ class WebsiteSaleLinkTrackerBackend(WebsiteSaleBackend):
def fetch_utm_data(self, date_from, date_to):
sale_utm_domain = [
('team_id.team_type', '=', 'website'),
('website_id', '!=', False),
('state', 'in', ['sale', 'done']),
('confirmation_date', '>=', date_from),
('confirmation_date', '<=', date_to)