[MERGE][IMP] website_crm_partner_assign: globally improve module usability

PURPOSE

Light Usability improvements for Reseller module.

SPECIFICATIONS

- Adding some demo data for the menu items Partner Level and Activations to
  make sure that user's aren't completely stuck until they find the menu items.
      -> Partner Level : Bronze, Silver, Gold
      -> Partner Activations : First Contact, Ramp-up, Fully Operational

- Adding action helper for menu items Partner Level and Partner Activations.
      -> Partner Level :
          Create a Partner Level
          Partner Levels allow you to rank up your Partners based on their
          performances.
     -> Partner Activations :
         Create a Partner Activation
         Those are used to know where your Partners stand in your onboarding
         process.

- Changing name of the menu item "Partner Level" to "Partner Levels".

- Changing Partner Levels and Activation in res.partner form selection to
   standard many2one.

- In the partner form changing the location of geolocation to below the groups.

- In kanban view displaying the record to which the partner is assigned, if
  there is no assigned partners it will show nothing.

- Change the "Title" label to "Opportunity" in portal, because people think it's
   a title as "Doctor" or "Mister".
- In portal while creating new Opportunity the "Opportunity" field will
   auto-filled as "contact name's opportunity" when contact name is entered.

- The "opportunities" stat button (res.partner) will show opportunities where
  the partner is either customer or assignated partner.

- From crm.lead form letting users to create new Assigned Partner on the fly.
  But since the domain requires some basic configs, a new partner won't appear
  in  the next searches to prevent this, when a new partner is created through
  here,  we will add it the lowest activation and level possible.

LINKS
PR #67524
Task 2443894

Signed-off-by: awa-odoo <awa-odoo@users.noreply.github.com>
This commit is contained in:
Odoo's Mergebot
2021-10-19 17:58:51 +02:00
committed by GitHub
12 changed files with 197 additions and 94 deletions
+13 -1
View File
@@ -1,4 +1,4 @@
from odoo import api, fields, models
from odoo import api, fields, models, _
class ResPartner(models.Model):
@@ -18,6 +18,7 @@ class ResPartner(models.Model):
def geo_localize(self):
# We need country names in English below
partners_not_geo_localized = self.env['res.partner']
for partner in self.with_context(lang='en_US'):
result = self._geo_localize(partner.street,
partner.zip,
@@ -31,4 +32,15 @@ class ResPartner(models.Model):
'partner_longitude': result[1],
'date_localization': fields.Date.context_today(partner)
})
else:
partners_not_geo_localized |= partner
if partners_not_geo_localized:
self.env['bus.bus'].sendone(
(self.env.cr.dbname, 'res.partner', self.env.user.partner_id.id),
{'type': 'simple_notification',
'title': _("Warning"),
'message': _('No address found for %(partner_names)s.',
partner_names=', '.join(partners_not_geo_localized.mapped('name'))
)
})
return True
@@ -27,6 +27,8 @@ The automatic assignment is figured from the weight of partner levels and the ge
'data/crm_lead_merge_template.xml',
'data/crm_tag_data.xml',
'data/mail_template_data.xml',
'data/res_partner_activation_data.xml',
'data/res_partner_grade_data.xml',
'security/ir.model.access.csv',
'security/ir_rule.xml',
'wizard/crm_forward_to_partner_view.xml',
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="res_partner_activation_data_fully_operational" model="res.partner.activation">
<field name="name">Fully Operational</field>
<field name="sequence">3</field>
</record>
<record id="res_partner_activation_data_ramp_up" model="res.partner.activation">
<field name="name">Ramp-up</field>
<field name="sequence">2</field>
</record>
<record id="res_partner_activation_data_first_contact" model="res.partner.activation">
<field name="name">First Contact</field>
<field name="sequence">1</field>
</record>
</odoo>
@@ -1,70 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="res_partner_grade_platinium" model="res.partner.grade">
<field name="name">Platinum</field>
<field name="sequence">4</field>
</record>
<record id="res_partner_grade_gold" model="res.partner.grade">
<field name="name">Gold</field>
<field name="sequence">3</field>
</record>
<record id="res_partner_grade_silver" model="res.partner.grade">
<field name="name">Silver</field>
<field name="sequence">2</field>
</record>
<record id="res_partner_grade_bronze" model="res.partner.grade">
<field name="name">Bronze</field>
<field name="sequence">1</field>
</record>
<record id="base.res_partner_3" model="res.partner">
<field name="grade_id" ref="res_partner_grade_bronze"/>
<field name="grade_id" ref="res_partner_grade_data_bronze"/>
<field name="partner_weight">10</field>
</record>
<record model="res.partner" id="base.res_partner_2">
<field name="assigned_partner_id" ref="base.res_partner_3"/>
</record>
<record id="base.res_partner_4" model="res.partner">
<field name="grade_id" ref="res_partner_grade_bronze"/>
<field name="grade_id" ref="res_partner_grade_data_bronze"/>
<field name="partner_weight">10</field>
</record>
<record id="base.res_partner_12" model="res.partner">
<field name="grade_id" ref="res_partner_grade_bronze"/>
<field name="grade_id" ref="res_partner_grade_data_bronze"/>
<field name="partner_weight">10</field>
</record>
<record id="base.res_partner_3" model="res.partner">
<field name="grade_id" ref="res_partner_grade_silver"/>
<field name="grade_id" ref="res_partner_grade_data_silver"/>
<field name="partner_weight">10</field>
</record>
<record id="base.res_partner_12" model="res.partner">
<field name="grade_id" ref="res_partner_grade_silver"/>
<field name="grade_id" ref="res_partner_grade_data_silver"/>
<field name="partner_weight">10</field>
</record>
<record id="base.res_partner_12" model="res.partner">
<field name="grade_id" ref="res_partner_grade_silver"/>
<field name="grade_id" ref="res_partner_grade_data_silver"/>
<field name="partner_weight">10</field>
</record>
<record id="base.res_partner_10" model="res.partner">
<field name="grade_id" ref="res_partner_grade_gold"/>
<field name="grade_id" ref="res_partner_grade_data_gold"/>
<field name="partner_weight">10</field>
</record>
<record id="base.res_partner_18" model="res.partner">
<field name="grade_id" ref="res_partner_grade_gold"/>
<field name="grade_id" ref="res_partner_grade_data_gold"/>
<field name="partner_weight">10</field>
</record>
<record id="base.res_partner_1" model="res.partner">
<field name="grade_id" ref="res_partner_grade_gold"/>
<field name="partner_weight">10</field>
</record>
<record id="base.res_partner_12" model="res.partner">
<field name="grade_id" ref="res_partner_grade_platinium"/>
<field name="partner_weight">10</field>
</record>
<record id="base.res_partner_4" model="res.partner">
<field name="grade_id" ref="res_partner_grade_platinium"/>
<field name="grade_id" ref="res_partner_grade_data_gold"/>
<field name="partner_weight">10</field>
</record>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="res_partner_grade_data_gold" model="res.partner.grade">
<field name="name">Gold</field>
<field name="sequence">3</field>
</record>
<record id="res_partner_grade_data_silver" model="res.partner.grade">
<field name="name">Silver</field>
<field name="sequence">2</field>
</record>
<record id="res_partner_grade_data_bronze" model="res.partner.grade">
<field name="name">Bronze</field>
<field name="sequence">1</field>
</record>
</odoo>
@@ -1,21 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data noupdate="1">
<record id="website_crm_partner_assign.res_partner_grade_platinium" model="res.partner.grade">
<record id="website_crm_partner_assign.res_partner_grade_data_gold" model="res.partner.grade">
<field name="is_published" eval="True" />
</record>
<record id="website_crm_partner_assign.res_partner_grade_gold" model="res.partner.grade">
<record id="website_crm_partner_assign.res_partner_grade_data_silver" model="res.partner.grade">
<field name="is_published" eval="True" />
</record>
<record id="website_crm_partner_assign.res_partner_grade_silver" model="res.partner.grade">
<record id="website_crm_partner_assign.res_partner_grade_data_bronze" model="res.partner.grade">
<field name="is_published" eval="True" />
</record>
<record id="website_crm_partner_assign.res_partner_grade_bronze" model="res.partner.grade">
<field name="is_published" eval="True" />
</record>
<record id="base.partner_demo_portal" model="res.partner">
<field name="grade_id" ref="website_crm_partner_assign.res_partner_grade_platinium"/>
<field name="partner_weight">10</field>
</record>
</data>
</odoo>
@@ -51,7 +51,21 @@ class CrmLead(models.Model):
leads.write({'user_id': salesman_id})
def action_assign_partner(self):
return self.assign_partner(partner_id=False)
""" While assigning a partner, geo-localization is performed only for leads having country
set (see method 'assign_geo_localize' and 'search_geo_partner'). So for leads that does not
have country set, we show the notification, and for the rest, we geo-localize them.
"""
leads_with_country = self.filtered(lambda lead: lead.country_id)
leads_without_country = self - leads_with_country
if leads_without_country:
self.env['bus.bus'].sendone(
(self.env.cr.dbname, 'res.partner', self.env.user.partner_id.id),
{'type': 'simple_notification',
'title': _("Warning"),
'message': _('There is no country set in addresses for %(lead_names)s.',
lead_names=', '.join(leads_without_country.mapped('name')))
})
return leads_with_country.assign_partner(partner_id=False)
def assign_partner(self, partner_id=False):
partner_dict = {}
@@ -2,12 +2,13 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo.osv import expression
from odoo.addons.http_routing.models.ir_http import slug
class ResPartnerGrade(models.Model):
_name = 'res.partner.grade'
_order = 'sequence'
_order = 'sequence desc'
_inherit = ['website.published.mixin']
_description = 'Partner Grade'
@@ -28,7 +29,7 @@ class ResPartnerGrade(models.Model):
class ResPartnerActivation(models.Model):
_name = 'res.partner.activation'
_order = 'sequence'
_order = 'sequence desc'
_description = 'Partner Activation'
sequence = fields.Integer('Sequence')
@@ -38,6 +39,18 @@ class ResPartnerActivation(models.Model):
class ResPartner(models.Model):
_inherit = "res.partner"
@api.model
def default_get(self, fields_list):
default_vals = super().default_get(fields_list)
if self.env.context.get('partner_set_default_grade_activation'):
# sets the lowest grade and activation if no default values given, mainly useful while
# creating assigned partner on the fly (to make it visible in same m2o again)
if 'grade_id' in fields_list and not default_vals.get('grade_id'):
default_vals['grade_id'] = self.env['res.partner.grade'].search([], order='sequence', limit=1).id
if 'activation' in fields_list and not default_vals.get('activation'):
default_vals['activation'] = self.env['res.partner.activation'].search([], order='sequence', limit=1).id
return default_vals
partner_weight = fields.Integer(
'Level Weight', compute='_compute_partner_weight',
readonly=False, store=True, tracking=True,
@@ -67,3 +80,20 @@ class ResPartner(models.Model):
def _compute_partner_weight(self):
for partner in self:
partner.partner_weight = partner.grade_id.partner_weight if partner.grade_id else 0
def _compute_opportunity_count(self):
super()._compute_opportunity_count()
assign_counts = {}
if self.ids:
opportunity_data = self.env['crm.lead'].with_context(active_test=False).read_group(
[('partner_assigned_id', 'in', self.ids)],
['partner_assigned_id'], ['partner_assigned_id']
)
assign_counts = {datum['partner_assigned_id'][0]: datum['partner_assigned_id_count'] for datum in opportunity_data}
for partner in self:
partner.opportunity_count += assign_counts.get(partner.id, 0)
def action_view_opportunity(self):
action = super().action_view_opportunity()
action['domain'] = expression.OR([action.get('domain', []), [('partner_assigned_id', '=', self.id)]])
return action
@@ -1,6 +1,7 @@
odoo.define('crm.partner_assign', function (require) {
'use strict';
const {_t} = require('web.core');
var publicWidget = require('web.public.widget');
var time = require('web.time');
@@ -15,6 +16,7 @@ publicWidget.registry.crmPartnerAssign = publicWidget.Widget.extend({
'click .new_opp_confirm': '_onNewOppConfirm',
'click .edit_opp_confirm': '_onEditOppConfirm',
'change .edit_opp_form .next_activity': '_onChangeNextActivity',
'change #new-opp-dialog .contact_name': '_onChangeContactName',
'click div.input-group.date[data-target-input="nearest"]': '_onCalendarInputGroupClick',
},
@@ -229,6 +231,17 @@ publicWidget.registry.crmPartnerAssign = publicWidget.Widget.extend({
ev.stopPropagation();
this._buttonExec($(ev.currentTarget), this._editOpportunity);
},
/**
* @private
* @param {Event} ev
*/
_onChangeContactName: function (ev) {
const contactName = ev.currentTarget.value.trim();
let titleEl = this.el.querySelector('.title');
if (!titleEl.value.trim()) {
titleEl.value = contactName ? _.str.sprintf(_t("%s's Opportunity"), contactName) : '';
}
},
/**
* @private
* @param {Event} ev
@@ -259,12 +272,6 @@ publicWidget.registry.crmPartnerAssign = publicWidget.Widget.extend({
down: 'fa fa-chevron-down',
},
};
// in some screen sizes, the automatic position opens the picker below input,
// but because #next_activity_div is the last element, we always open the
// picker on top the input to prevent extra scroll
if ($calendarInputGroup.is('#next_activity_div')) {
calendarOptions.widgetPositioning = {vertical: 'top'};
}
$calendarInputGroup.datetimepicker(calendarOptions);
},
@@ -21,7 +21,7 @@
</div>
<button string="Automatic Assignment" name="action_assign_partner" type="object" class="btn-link"/>
<field name="partner_assigned_id" domain="[('grade_id','!=',False)]"/>
<field name="partner_assigned_id" domain="[('grade_id','!=',False)]" context="{'partner_set_default_grade_activation': 1}"/>
<button string="Send Email" type="action" class="btn-link"
attrs="{'invisible':[('partner_assigned_id','=',False)]}"
name="%(crm_lead_forward_to_partner_act)d"
@@ -136,4 +136,26 @@
</xpath>
</field>
</record>
<record id="crm_lead_view_kanban" model="ir.ui.view">
<field name="name">crm.lead.view.kanban.inherit.website.crm.partner.assign</field>
<field name="model">crm.lead</field>
<field name="inherit_id" ref="crm.crm_case_kanban_view_leads"/>
<field name="arch" type="xml">
<field name="user_id" position="after">
<field name="partner_assigned_id"/>
</field>
<xpath expr="//span[@t-esc='record.partner_id.value']" position="replace">
<t t-if="record.partner_id.value or record.partner_assigned_id.value">
<span class="o_text_overflow">
<t t-esc="record.partner_id.value"/>
<t t-if="record.partner_assigned_id.value">
<i class="fa fa-arrow-circle-right" aria-label="Arrow icon" title="Arrow"/>
<t t-esc="record.partner_assigned_id.value"/>
</t>
</span>
</t>
</xpath>
</field>
</record>
</odoo>
@@ -34,6 +34,13 @@
<field name="name">Partner Activations</field>
<field name="res_model">res.partner.activation</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a Partner Activation
</p><p>
Those are used to know where your Partners stand in your onboarding process.
</p>
</field>
</record>
<menuitem id="res_partner_activation_config_mi" parent="crm_menu_resellers" action="res_partner_activation_act"
@@ -90,9 +97,16 @@
</record>
<record id="res_partner_grade_action" model="ir.actions.act_window">
<field name="name">Partner Level</field>
<field name="name">Partner Levels</field>
<field name="res_model">res.partner.grade</field>
<field name="search_view_id" ref="res_partner_grade_view_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a Partner Level
</p><p>
Partner Levels allow you to rank your Partners based on their performances.
</p>
</field>
</record>
<menuitem action="res_partner_grade_action" id="menu_res_partner_grade_action"
@@ -130,12 +144,12 @@
<field name="inherit_id" ref="base_geolocalize.view_crm_partner_geo_form"/>
<field name="arch" type="xml">
<data>
<xpath expr="//page[@name='geo_location']" position="inside">
<group>
<xpath expr="//page[@name='geo_location']/group" position="before">
<group name="group_partner_activation_review">
<group>
<separator string="Partner Activation" colspan="2"/>
<field name="grade_id" options="{'no_open': True, 'no_create': True}"/>
<field name="activation" options="{'no_open': True, 'no_create': True}"/>
<field name="grade_id" options="{'no_open': True}"/>
<field name="activation" options="{'no_open': True}"/>
<field name="partner_weight"/>
</group>
<group>
@@ -254,7 +254,7 @@
<div class="form-inline ml-lg-4" t-if="request.env.user.partner_id.grade_id or request.env.user.commercial_partner_id.grade_id">
<button class="btn btn-success btn-sm" name='new_opp' data-toggle="modal" data-target=".modal_new_opp" title="Add an opportunity" aria-label="Add an opportunity">
<i class="fa fa-plus"/> Create New
Create Opportunity
</button>
</div>
</t>
@@ -273,7 +273,7 @@
<input type='text' name="contact_name" class="form-control contact_name"/>
</div>
<div class="form-group">
<label class="col-form-label h4dd" for="title">Title</label>
<label class="col-form-label h4dd" for="title">Opportunity</label>
<input type='text' name="title" class="form-control title"/>
</div>
<div class="form-group">
@@ -569,31 +569,29 @@
<div class="row mb-3" t-if="opportunity.partner_name or opportunity.email_from or opportunity.contact_name">
<strong class="col-12 col-sm-3">Customer</strong>
<div class="col">
<div>
<div t-if="opportunity.partner_name or opportunity.contact_name">
<span class="fa fa-user fa-fw" role="img" aria-label="User" title="User"/>
<strong class="font-italic" t-if="opportunity.partner_name" itemprop="name" t-field="opportunity.partner_name" />
<strong class="font-italic" t-else="" itemprop="name" t-field="opportunity.contact_name" />
</div>
<div>
<div t-if="opportunity.phone">
<span class="fa fa-phone fa-fw" role="img" aria-label="Phone" title="Phone"/>
<span t-if="opportunity.phone" itemprop="telephone" t-field="opportunity.phone"/>
<span t-else="" class="text-muted"> - </span>
<span itemprop="telephone" t-field="opportunity.phone"/>
</div>
<div>
<div t-if="opportunity.mobile">
<span class="fa fa-mobile fa-fw" role="img" aria-label="Mobile" title="Mobile"/>
<span t-if="opportunity.mobile" itemprop="mobile" t-field="opportunity.mobile"/>
<span t-else="" class="text-muted"> - </span>
<span itemprop="mobile" t-field="opportunity.mobile"/>
</div>
<div>
<div t-if="opportunity.email_from">
<span class="fa fa-envelope fa-fw" role="img" aria-label="Email" title="Email"/>
<span t-if="opportunity.email_from" itemprop="email" t-field="opportunity.email_from"/>
<span t-else="" class="text-muted"> - </span>
<span itemprop="email" t-field="opportunity.email_from"/>
</div>
</div>
</div>
<div class="row">
<strong class="col-12 col-sm-3">Address</strong>
<address class="col d-flex align-items-baseline mb-0">
<address t-if="opportunity.street or opportunity.street2 or opportunity.city or opportunity.zip or opportunity.state_id or opportunity.country_id"
class="col d-flex align-items-baseline mb-0">
<span class="fa fa-map-marker fa-fw" role="img" aria-label="Address" title="Address"/>
<div>
<div t-if="opportunity.street"><span t-field="opportunity.street"/></div>
@@ -602,7 +600,7 @@
<div t-if="opportunity.city or opportunity.zip">
<span t-field="opportunity.city"/> <span t-field="opportunity.zip"/>
</div>
<div t-if="opportunity.state_id or country_id">
<div t-if="opportunity.state_id or opportunity.country_id">
<span t-field="opportunity.state_id"/> <span t-field="opportunity.country_id"/>
</div>
</div>
@@ -684,28 +682,35 @@
</div>
</div>
<div class="form-group">
<label class="col-form-label" for="next_activity">Next Activity</label>
<select class="form-control next_activity" name="next_activity">
<t t-foreach="activity_types" t-as="activity_type">
<option t-att-data="activity_type.id" t-att-selected="activity_type.id == user_activity.activity_type_id.id" t-att-name="activity_type.name" t-att-delay_count="activity_type.delay_count" t-att-delay_unit="activity_type.delay_unit" t-att-summary="activity_type.summary"><t t-esc="activity_type.name"/></option>
</t>
</select>
<div class="row">
<div class="col-md-5">
<label class="col-form-label" for="next_activity">Next Activity</label>
<select class="form-control next_activity" name="next_activity">
<t t-foreach="activity_types" t-as="activity_type">
<option t-att-data="activity_type.id" t-att-selected="activity_type.id == user_activity.activity_type_id.id"
t-att-name="activity_type.name" t-att-delay_count="activity_type.delay_count"
t-att-delay_unit="activity_type.delay_unit" t-att-summary="activity_type.summary">
<t t-esc="activity_type.name"/></option>
</t>
</select>
</div>
<div class="col-md-7">
<label class="col-form-label" for="activity_date_deadline">Next Activity Date</label>
<div class="input-group date" id="next_activity_div" data-target-input="nearest">
<t t-set='date_formatted'><t t-options='{"widget": "date"}' t-esc="user_activity.date_deadline"/></t>
<input type="text" name="activity_date_deadline" data-target="#next_activity_div" t-att-value="date_formatted" class="form-control activity_date_deadline datetimepicker-input" t-att-name="prefix"/>
<div class="input-group-append" data-target="#next_activity_div" data-toggle="datetimepicker">
<span class="input-group-text">
<span class="fa fa-calendar" role="img" aria-label="Calendar" title="Calendar"></span>
</span>
</div>
</div>
</div>
</div>
</div>
<div class="form-group">
<label class="col-form-label" for="activity_summary">Details Next Activity</label>
<textarea rows="3" name="activity_summary" class="form-control activity_summary"><t t-esc="user_activity.summary"/></textarea>
</div>
<div class="form-group">
<label class="col-form-label" for="activity_date_deadline">Next Activity Date</label>
<div class="input-group date" id="next_activity_div" data-target-input="nearest">
<t t-set='date_formatted'><t t-options='{"widget": "date"}' t-esc="user_activity.date_deadline"/></t>
<input type="text" name="activity_date_deadline" data-target="#next_activity_div" t-att-value="date_formatted" class="form-control activity_date_deadline datetimepicker-input" t-att-name="prefix"/>
<div class="input-group-append" data-target="#next_activity_div" data-toggle="datetimepicker">
<span class="input-group-text">
<span class="fa fa-calendar" role="img" aria-label="Calendar" title="Calendar"></span>
</span>
</div>
</div>
<textarea rows="6" name="activity_summary" class="form-control activity_summary"><t t-esc="user_activity.summary"/></textarea>
</div>
</main>
<footer class="modal-footer">