Начальное наполнение

This commit is contained in:
parent 6f312b6ddf
commit 5c9f46b479
116 changed files with 111204 additions and 0 deletions

7
__init__.py Normal file
View File

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
from . import models
from . import report
from . import wizard

56
__manifest__.py Normal file
View File

@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Resellers',
'category': 'Website/Website',
'summary': 'Publish your resellers/partners and forward leads to them',
'version': '1.2',
'description': """
This module allows to publish your resellers/partners on your website and to forward incoming leads/opportunities to them.
**Publish a partner**
To publish a partner, set a *Level* in their contact form (in the Partner Assignment section) and click the *Publish* button.
**Forward leads**
Forwarding leads can be done for one or several leads at a time. The action is available in the *Assigned Partner* section of the lead/opportunity form view and in the *Action* menu of the list view.
The automatic assignment is figured from the weight of partner levels and the geolocalization. Partners get leads that are located around them.
""",
'depends': ['base_geolocalize', 'crm', 'account',
'website_partner', 'website_google_map', 'portal'],
'data': [
'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',
'views/res_partner_views.xml',
'views/res_partner_activation_views.xml',
'views/res_partner_grade_views.xml',
'views/crm_lead_views.xml',
'views/website_crm_partner_assign_templates.xml',
'views/partner_assign_menus.xml',
'report/crm_partner_report_view.xml',
'views/snippets.xml',
],
'demo': [
'data/res_partner_demo.xml',
'data/crm_lead_demo.xml',
'data/res_partner_grade_demo.xml',
],
'installable': True,
'assets': {
'web.assets_frontend': [
'website_crm_partner_assign/static/src/**/*',
],
},
'license': 'LGPL-3',
}

4
controllers/__init__.py Normal file
View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main

354
controllers/main.py Normal file
View File

@ -0,0 +1,354 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
import werkzeug.urls
from collections import OrderedDict
from werkzeug.exceptions import NotFound
from odoo import fields
from odoo import http
from odoo.http import request
from odoo.addons.http_routing.models.ir_http import slug, unslug
from odoo.addons.website.models.ir_http import sitemap_qs2dom
from odoo.addons.portal.controllers.portal import CustomerPortal
from odoo.addons.website_partner.controllers.main import WebsitePartnerPage
from odoo.tools.translate import _
class WebsiteAccount(CustomerPortal):
def get_domain_my_lead(self, user):
return [
('partner_assigned_id', 'child_of', user.commercial_partner_id.id),
('type', '=', 'lead')
]
def get_domain_my_opp(self, user):
return [
('partner_assigned_id', 'child_of', user.commercial_partner_id.id),
('type', '=', 'opportunity')
]
def _prepare_home_portal_values(self, counters):
values = super()._prepare_home_portal_values(counters)
CrmLead = request.env['crm.lead']
if 'lead_count' in counters:
values['lead_count'] = (
CrmLead.search_count(self.get_domain_my_lead(request.env.user))
if CrmLead.check_access_rights('read', raise_exception=False)
else 0
)
if 'opp_count' in counters:
values['opp_count'] = (
CrmLead.search_count(self.get_domain_my_opp(request.env.user))
if CrmLead.check_access_rights('read', raise_exception=False)
else 0
)
return values
@http.route(['/my/leads', '/my/leads/page/<int:page>'], type='http', auth="user", website=True)
def portal_my_leads(self, page=1, date_begin=None, date_end=None, sortby=None, **kw):
values = self._prepare_portal_layout_values()
CrmLead = request.env['crm.lead']
domain = self.get_domain_my_lead(request.env.user)
searchbar_sortings = {
'date': {'label': _('Newest'), 'order': 'create_date desc'},
'name': {'label': _('Name'), 'order': 'name'},
'contact_name': {'label': _('Contact Name'), 'order': 'contact_name'},
}
# default sort by value
if not sortby:
sortby = 'date'
order = searchbar_sortings[sortby]['order']
if date_begin and date_end:
domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]
# pager
lead_count = CrmLead.search_count(domain)
pager = request.website.pager(
url="/my/leads",
url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby},
total=lead_count,
page=page,
step=self._items_per_page
)
# content according to pager and archive selected
leads = CrmLead.search(domain, order=order, limit=self._items_per_page, offset=pager['offset'])
values.update({
'date': date_begin,
'leads': leads,
'page_name': 'lead',
'default_url': '/my/leads',
'pager': pager,
'searchbar_sortings': searchbar_sortings,
'sortby': sortby,
})
return request.render("website_crm_partner_assign.portal_my_leads", values)
@http.route(['/my/opportunities', '/my/opportunities/page/<int:page>'], type='http', auth="user", website=True)
def portal_my_opportunities(self, page=1, date_begin=None, date_end=None, sortby=None, filterby=None, **kw):
values = self._prepare_portal_layout_values()
CrmLead = request.env['crm.lead']
domain = self.get_domain_my_opp(request.env.user)
today = fields.Date.today()
this_week_end_date = fields.Date.to_string(fields.Date.from_string(today) + datetime.timedelta(days=7))
searchbar_filters = {
'all': {'label': _('Active'), 'domain': []},
'today': {'label': _('Today Activities'), 'domain': [('activity_date_deadline', '=', today)]},
'week': {'label': _('This Week Activities'),
'domain': [('activity_date_deadline', '>=', today), ('activity_date_deadline', '<=', this_week_end_date)]},
'overdue': {'label': _('Overdue Activities'), 'domain': [('activity_date_deadline', '<', today)]},
'won': {'label': _('Won'), 'domain': [('stage_id.is_won', '=', True)]},
'lost': {'label': _('Lost'), 'domain': [('active', '=', False), ('probability', '=', 0)]},
}
searchbar_sortings = {
'date': {'label': _('Newest'), 'order': 'create_date desc'},
'name': {'label': _('Name'), 'order': 'name'},
'contact_name': {'label': _('Contact Name'), 'order': 'contact_name'},
'revenue': {'label': _('Expected Revenue'), 'order': 'expected_revenue desc'},
'probability': {'label': _('Probability'), 'order': 'probability desc'},
'stage': {'label': _('Stage'), 'order': 'stage_id'},
}
# default sort by value
if not sortby:
sortby = 'date'
order = searchbar_sortings[sortby]['order']
# default filter by value
if not filterby:
filterby = 'all'
domain += searchbar_filters[filterby]['domain']
if filterby == 'lost':
CrmLead = CrmLead.with_context(active_test=False)
if date_begin and date_end:
domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]
# pager
opp_count = CrmLead.search_count(domain)
pager = request.website.pager(
url="/my/opportunities",
url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby, 'filterby': filterby},
total=opp_count,
page=page,
step=self._items_per_page
)
# content according to pager
opportunities = CrmLead.search(domain, order=order, limit=self._items_per_page, offset=pager['offset'])
values.update({
'date': date_begin,
'opportunities': opportunities,
'page_name': 'opportunity',
'default_url': '/my/opportunities',
'pager': pager,
'searchbar_sortings': searchbar_sortings,
'sortby': sortby,
'searchbar_filters': OrderedDict(sorted(searchbar_filters.items())),
'filterby': filterby,
})
return request.render("website_crm_partner_assign.portal_my_opportunities", values)
@http.route(['''/my/lead/<model('crm.lead', "[('type','=', 'lead')]"):lead>'''], type='http', auth="user", website=True)
def portal_my_lead(self, lead, **kw):
if lead.type != 'lead':
raise NotFound()
return request.render("website_crm_partner_assign.portal_my_lead", {'lead': lead})
@http.route(['''/my/opportunity/<model('crm.lead', "[('type','=', 'opportunity')]"):opp>'''], type='http', auth="user", website=True)
def portal_my_opportunity(self, opp, **kw):
if opp.type != 'opportunity':
raise NotFound()
return request.render(
"website_crm_partner_assign.portal_my_opportunity", {
'opportunity': opp,
'user_activity': opp.sudo().activity_ids.filtered(lambda activity: activity.user_id == request.env.user)[:1],
'stages': request.env['crm.stage'].search([
('is_won', '!=', True), '|', ('team_id', '=', False), ('team_id', '=', opp.team_id.id)
], order='sequence desc, name desc, id desc'),
'activity_types': request.env['mail.activity.type'].sudo().search(['|', ('res_model', '=', opp._name), ('res_model', '=', False)]),
'states': request.env['res.country.state'].sudo().search([]),
'countries': request.env['res.country'].sudo().search([]),
})
class WebsiteCrmPartnerAssign(WebsitePartnerPage):
_references_per_page = 40
def sitemap_partners(env, rule, qs):
if not qs or qs.lower() in '/partners':
yield {'loc': '/partners'}
Grade = env['res.partner.grade']
dom = [('website_published', '=', True)]
dom += sitemap_qs2dom(qs=qs, route='/partners/grade/', field=Grade._rec_name)
for grade in env['res.partner.grade'].search(dom):
loc = '/partners/grade/%s' % slug(grade)
if not qs or qs.lower() in loc:
yield {'loc': loc}
partners_dom = [('is_company', '=', True), ('grade_id', '!=', False), ('website_published', '=', True),
('grade_id.website_published', '=', True), ('country_id', '!=', False)]
dom += sitemap_qs2dom(qs=qs, route='/partners/country/')
countries = env['res.partner'].sudo()._read_group(partners_dom, groupby=['country_id'])
for [country] in countries:
loc = '/partners/country/%s' % slug(country)
if not qs or qs.lower() in loc:
yield {'loc': loc}
@http.route([
'/partners',
'/partners/page/<int:page>',
'/partners/grade/<model("res.partner.grade"):grade>',
'/partners/grade/<model("res.partner.grade"):grade>/page/<int:page>',
'/partners/country/<model("res.country"):country>',
'/partners/country/<model("res.country"):country>/page/<int:page>',
'/partners/grade/<model("res.partner.grade"):grade>/country/<model("res.country"):country>',
'/partners/grade/<model("res.partner.grade"):grade>/country/<model("res.country"):country>/page/<int:page>',
], type='http', auth="public", website=True, sitemap=sitemap_partners)
def partners(self, country=None, grade=None, page=0, **post):
country_all = post.pop('country_all', False)
partner_obj = request.env['res.partner']
country_obj = request.env['res.country']
search = post.get('search', '')
base_partner_domain = [('is_company', '=', True), ('grade_id', '!=', False), ('website_published', '=', True)]
if not request.env['res.users'].has_group('website.group_website_restricted_editor'):
base_partner_domain += [('grade_id.website_published', '=', True)]
if search:
base_partner_domain += ['|', ('name', 'ilike', search), ('website_description', 'ilike', search)]
# Infer Country
if not country and not country_all:
if request.geoip.country_code:
country = country_obj.search([('code', '=', request.geoip.country_code)], limit=1)
# Group by country
country_domain = list(base_partner_domain)
if grade:
country_domain += [('grade_id', '=', grade.id)]
countries = partner_obj.sudo().read_group(
country_domain, ["id", "country_id"],
groupby="country_id", orderby="country_id")
# Fallback: Show all partners when country has no associates.
country_ids = [c['country_id'][0] for c in countries]
if country and country.id not in country_ids:
country = None
# Group by grade
grade_domain = list(base_partner_domain)
if country:
grade_domain += [('country_id', '=', country.id)]
grades = partner_obj.sudo().read_group(
grade_domain, ["id", "grade_id"],
groupby="grade_id")
grades_partners = partner_obj.sudo().search_count(grade_domain)
# flag active grade
for grade_dict in grades:
grade_dict['active'] = grade and grade_dict['grade_id'][0] == grade.id
grades.insert(0, {
'grade_id_count': grades_partners,
'grade_id': (0, _("All Categories")),
'active': bool(grade is None),
})
countries_partners = partner_obj.sudo().search_count(country_domain)
# flag active country
for country_dict in countries:
country_dict['active'] = country and country_dict['country_id'] and country_dict['country_id'][0] == country.id
countries.insert(0, {
'country_id_count': countries_partners,
'country_id': (0, _("All Countries")),
'active': bool(country is None),
})
# current search
if grade:
base_partner_domain += [('grade_id', '=', grade.id)]
if country:
base_partner_domain += [('country_id', '=', country.id)]
# format pager
if grade and not country:
url = '/partners/grade/' + slug(grade)
elif country and not grade:
url = '/partners/country/' + slug(country)
elif country and grade:
url = '/partners/grade/' + slug(grade) + '/country/' + slug(country)
else:
url = '/partners'
url_args = {}
if search:
url_args['search'] = search
if country_all:
url_args['country_all'] = True
partner_count = partner_obj.sudo().search_count(base_partner_domain)
pager = request.website.pager(
url=url, total=partner_count, page=page, step=self._references_per_page, scope=7,
url_args=url_args)
# search partners matching current search parameters
partner_ids = partner_obj.sudo().search(
base_partner_domain, order="grade_sequence ASC, implemented_partner_count DESC, complete_name ASC, id ASC",
offset=pager['offset'], limit=self._references_per_page)
partners = partner_ids.sudo()
google_map_partner_ids = ','.join(str(p.id) for p in partners)
google_maps_api_key = request.website.google_maps_api_key
values = {
'countries': countries,
'country_all': country_all,
'current_country': country,
'grades': grades,
'current_grade': grade,
'partners': partners,
'google_map_partner_ids': google_map_partner_ids,
'pager': pager,
'searches': post,
'search_path': "%s" % werkzeug.urls.url_encode(post),
'google_maps_api_key': google_maps_api_key,
}
return request.render("website_crm_partner_assign.index", values, status=partners and 200 or 404)
# Do not use semantic controller due to sudo()
@http.route()
def partners_detail(self, partner_id, **post):
current_slug = partner_id
_, partner_id = unslug(partner_id)
current_grade, current_country = None, None
grade_id = post.get('grade_id')
country_id = post.get('country_id')
if grade_id:
current_grade = request.env['res.partner.grade'].browse(int(grade_id)).exists()
if country_id:
current_country = request.env['res.country'].browse(int(country_id)).exists()
if partner_id:
partner = request.env['res.partner'].sudo().browse(partner_id)
is_website_restricted_editor = request.env['res.users'].has_group('website.group_website_restricted_editor')
if partner.exists() and (partner.website_published or is_website_restricted_editor):
if slug(partner) != current_slug:
return request.redirect('/partners/%s' % slug(partner))
values = {
'main_object': partner,
'partner': partner,
'current_grade': current_grade,
'current_country': current_country
}
return request.render("website_crm_partner_assign.partner", values)
raise request.not_found()

33
data/crm_lead_demo.xml Normal file
View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data noupdate="1">
<!-- Demo Leads -->
<record id="crm_case_partner_assign_1" model="crm.lead">
<field name="type">lead</field>
<field name="name">Specifications and price of your phones</field>
<field name="contact_name">Steve Martinez</field>
<field name="partner_name"></field>
<field name="partner_id" ref=""/>
<field name="function">Reseller</field>
<field name="country_id" ref="base.uk"/>
<field name="city">Edinburgh</field>
<field name="tag_ids" eval="[(6, 0, [ref('sales_team.categ_oppor1')])]"/>
<field name="priority">2</field>
<field name="team_id" ref="sales_team.crm_team_1"/>
<field name="user_id" ref=""/>
<field name="stage_id" ref="crm.stage_lead1"/>
<field name="description">Hi,
Please, can you give me more details about your phones, including their specifications and their prices.
Regards,
Steve</field>
<field eval="1" name="active"/>
<field name="partner_assigned_id" ref="base.partner_demo_portal"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="medium_id" ref="utm.utm_medium_email"/>
<field name="source_id" ref="utm.utm_source_newsletter"/>
</record>
</data>
</odoo>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="crm_lead_merge_summary_inherit_partner_assign" inherit_id="crm.crm_lead_merge_summary">
<xpath expr="//div[@name='company']" position="after">
<br t-if="lead.date_partner_assign or lead.partner_assigned_id" />
<div t-if="lead.partner_assigned_id">
Assigned Partner: <span t-field="lead.partner_assigned_id"/>
</div>
<div name="date_partner_assign" t-if="lead.date_partner_assign">
Partner Assignment Date: <span t-field="lead.date_partner_assign"/>
</div>
</xpath>
<xpath expr="//div[@name='address']" position="attributes">
<attribute name="t-if">
lead.street or lead.street2 or lead.zip or lead.city or lead.state_id or lead.country_id
or lead.partner_latitude or lead.partner_longitude
</attribute>
</xpath>
<xpath expr="//div[@name='address']/*[last()]" position="after">
<div name="partner_geolocation" t-if="lead.partner_latitude or lead.partner_longitude">
Geolocation: <t t-esc="lead.partner_latitude"/> latitude, <t t-esc="lead.partner_longitude"/> longitude
</div>
</xpath>
</template>
</odoo>

17
data/crm_tag_data.xml Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record model="crm.tag" id="tag_portal_lead_partner_unavailable">
<field name="name">No more partner available</field>
<field name="color">3</field>
</record>
<record model="crm.tag" id="tag_portal_lead_is_spam">
<field name="name">Spam</field>
<field name="color">3</field>
</record>
<record model="crm.tag" id="tag_portal_lead_own_opp">
<field name="name">Created by Partner</field>
<field name="color">4</field>
</record>
</data>
</odoo>

102
data/mail_template_data.xml Normal file
View File

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="0">
<!-- Technical template, keep updated -->
<record id="email_template_lead_forward_mail" model="mail.template">
<field name="name">Lead Forward: Send to partner</field>
<field name="model_id" ref="website_crm_partner_assign.model_crm_lead_forward_to_partner" />
<field name="subject">Fwd: Lead: {{ ctx['partner_id'].name }}</field>
<field name="email_from">{{ user.email_formatted }}</field>
<field name="email_to">{{ ctx['partner_id'].email_formatted }}</field>
<field name="description">Sent to partner when a lead has been assigned to him</field>
<field name="body_html" type="html">
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
<tbody>
<!-- HEADER -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="middle">
<span style="font-size: 10px;">Your leads</span><br/>
</td><td valign="middle" align="right" t-if="not user.company_id.uses_default_logo">
<img t-attf-src="/logo.png?company={{ user.company_id.id }}" style="padding: 0px; margin: 0px; height: auto; width: 80px;" t-att-alt="user.company_id.name"/>
</td></tr>
<tr><td colspan="2" style="text-align:center;">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin:16px 0px 16px 0px;"/>
</td></tr>
</table>
</td>
</tr>
<!-- CONTENT -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr>
<td valign="top" style="font-size: 13px;">
<div>
Hello,<br/>
We have been contacted by those prospects that are in your region. Thus, the following leads have been assigned to <t t-out="ctx['partner_id'].name or ''"></t>:<br/>
<ol>
<li t-foreach="ctx['partner_leads']" t-as="lead"><a t-att-href="lead['lead_link']" t-out="lead['lead_id'].name or 'Subject Undefined'">Subject Undefined</a>, <t t-out="lead['lead_id'].partner_name or lead['lead_id'].contact_name or 'Contact Name Undefined'">Contact Name Undefined</t>, <t t-out="lead['lead_id'].country_id and lead['lead_id'].country_id.name or 'Country Undefined'">Country Undefined</t>, <t t-out="lead['lead_id'].email_from or 'Email Undefined' or ''">Email Undefined</t>, <t t-out="lead['lead_id'].phone or ''">+1 650-123-4567</t> </li><br/>
</ol>
<t t-if="ctx.get('partner_in_portal')">
Please connect to your <a t-att-href="'%s?db=%s' % (object.get_base_url(), object.env.cr.dbname)">Partner Portal</a> to get details. On each lead are two buttons on the top left corner that you should press after having contacted the lead: "I'm interested" &amp; "I'm not interested".<br/>
</t>
<t t-else="">
You do not have yet a portal access to our database. Please contact
<t t-out="ctx['partner_id'].user_id and ctx['partner_id'].user_id.email and 'your account manager %s (%s)' % (ctx['partner_id'].user_id.name,ctx['partner_id'].user_id.email) or 'us'">us</t>.<br/>
</t>
The lead will be sent to another partner if you do not contact the lead before 20 days.<br/><br/>
Thank you,<br/>
<t t-out="ctx['partner_id'].user_id and ctx['partner_id'].user_id.signature or ''"></t>
<br/>
<t t-if="not ctx['partner_id'].user_id">
PS: It looks like you do not have an account manager assigned to you, please contact us.
</t>
</div>
</td>
</tr>
<tr>
<td style="text-align:center;">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
</td>
</tr>
</table>
</td>
</tr>
<!-- FOOTER -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; font-size: 11px; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="middle" align="left">
<t t-out="user.company_id.name or ''">YourCompany</t>
</td></tr>
<tr><td valign="middle" align="left" style="opacity: 0.7;">
<t t-out="user.company_id.phone or ''">+1 650-123-4567</t>
<t t-if="user.company_id.phone and (user.company_id.email or user.company_id.website)">|</t>
<a t-if="user.company_id.email" t-att-href="'mailto:%s' % user.company_id.email" style="text-decoration:none; color: #454748;" t-out="user.company_id.email or ''">info@yourcompany.com</a>
<t t-if="user.company_id.email and user.company_id.website">|</t>
<a t-if="user.company_id.website" t-att-href="'%s' % user.company_id.website" style="text-decoration:none; color: #454748;" t-out="user.company_id.website or ''">http://www.example.com</a>
</td></tr>
</table>
</td>
</tr>
</tbody>
</table>
</td></tr>
<!-- POWERED BY -->
<tr><td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: #F1F1F1; color: #454748; padding: 8px; border-collapse:separate;">
<tr><td style="text-align: center; font-size: 13px;">
Powered by <a target="_blank" href="https://www.odoo.com?utm_source=db&amp;utm_medium=website" style="color: #875A7B;">Odoo</a>
</td></tr>
</table>
</td></tr>
</table>
</field>
<field name="lang">{{ ctx['partner_id'].lang }}</field>
<field name="auto_delete" eval="True"/>
</record>
</data>
</odoo>

View File

@ -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">1</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">3</field>
</record>
</odoo>

81
data/res_partner_demo.xml Normal file
View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="base.res_partner_3" model="res.partner">
<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_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_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_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_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_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_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_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_data_gold"/>
<field name="partner_weight">10</field>
</record>
<record model="res.partner" id="base.res_partner_10">
<field name="assigned_partner_id" ref="base.res_partner_4"/>
</record>
<record model="res.partner" id="base.res_partner_12">
<field name="assigned_partner_id" ref="base.res_partner_10"/>
</record>
<record model="res.partner" id="base.res_partner_4">
<field name="assigned_partner_id" ref="base.res_partner_4"/>
</record>
<record model="res.partner" id="base.res_partner_10">
<field name="assigned_partner_id" ref="base.res_partner_12"/>
</record>
<record model="res.partner" id="base.res_partner_3">
<field name="assigned_partner_id" ref="base.res_partner_3"/>
</record>
<record model="res.partner" id="base.res_partner_2">
<field name="assigned_partner_id" ref="base.res_partner_1"/>
</record>
<record model="res.partner" id="base.res_partner_4">
<field name="assigned_partner_id" ref="base.res_partner_18"/>
</record>
<record model="res.partner" id="base.res_partner_4">
<field name="assigned_partner_id" ref="base.res_partner_3"/>
</record>
<record model="res.partner" id="base.res_partner_1">
<field name="assigned_partner_id" ref="base.res_partner_12"/>
</record>
<record model="res.partner" id="base.res_partner_1">
<field name="assigned_partner_id" ref="base.res_partner_2"/>
</record>
<record model="res.partner" id="base.res_partner_1">
<field name="assigned_partner_id" ref="base.res_partner_2"/>
</record>
<record model="res.partner" id="base.res_partner_12">
<field name="assigned_partner_id" ref="base.res_partner_12"/>
</record>
</odoo>

View File

@ -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">1</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">3</field>
</record>
</odoo>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data noupdate="1">
<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_data_silver" model="res.partner.grade">
<field name="is_published" eval="True" />
</record>
<record id="website_crm_partner_assign.res_partner_grade_data_bronze" model="res.partner.grade">
<field name="is_published" eval="True" />
</record>
</data>
</odoo>

1381
i18n/af.po Normal file

File diff suppressed because it is too large Load Diff

1377
i18n/am.po Normal file

File diff suppressed because it is too large Load Diff

1541
i18n/ar.po Normal file

File diff suppressed because it is too large Load Diff

1382
i18n/az.po Normal file

File diff suppressed because it is too large Load Diff

1443
i18n/bg.po Normal file

File diff suppressed because it is too large Load Diff

1382
i18n/bs.po Normal file

File diff suppressed because it is too large Load Diff

1465
i18n/ca.po Normal file

File diff suppressed because it is too large Load Diff

1449
i18n/cs.po Normal file

File diff suppressed because it is too large Load Diff

1445
i18n/da.po Normal file

File diff suppressed because it is too large Load Diff

1550
i18n/de.po Normal file

File diff suppressed because it is too large Load Diff

1383
i18n/el.po Normal file

File diff suppressed because it is too large Load Diff

1379
i18n/en_AU.po Normal file

File diff suppressed because it is too large Load Diff

1391
i18n/en_GB.po Normal file

File diff suppressed because it is too large Load Diff

1546
i18n/es.po Normal file

File diff suppressed because it is too large Load Diff

1551
i18n/es_419.po Normal file

File diff suppressed because it is too large Load Diff

1391
i18n/es_BO.po Normal file

File diff suppressed because it is too large Load Diff

1390
i18n/es_CL.po Normal file

File diff suppressed because it is too large Load Diff

1394
i18n/es_CO.po Normal file

File diff suppressed because it is too large Load Diff

1390
i18n/es_CR.po Normal file

File diff suppressed because it is too large Load Diff

1380
i18n/es_DO.po Normal file

File diff suppressed because it is too large Load Diff

1395
i18n/es_EC.po Normal file

File diff suppressed because it is too large Load Diff

1392
i18n/es_PE.po Normal file

File diff suppressed because it is too large Load Diff

1390
i18n/es_PY.po Normal file

File diff suppressed because it is too large Load Diff

1390
i18n/es_VE.po Normal file

File diff suppressed because it is too large Load Diff

1557
i18n/et.po Normal file

File diff suppressed because it is too large Load Diff

1390
i18n/eu.po Normal file

File diff suppressed because it is too large Load Diff

1433
i18n/fa.po Normal file

File diff suppressed because it is too large Load Diff

1560
i18n/fi.po Normal file

File diff suppressed because it is too large Load Diff

1548
i18n/fr.po Normal file

File diff suppressed because it is too large Load Diff

1390
i18n/fr_BE.po Normal file

File diff suppressed because it is too large Load Diff

1390
i18n/fr_CA.po Normal file

File diff suppressed because it is too large Load Diff

1390
i18n/gl.po Normal file

File diff suppressed because it is too large Load Diff

1381
i18n/gu.po Normal file

File diff suppressed because it is too large Load Diff

1445
i18n/he.po Normal file

File diff suppressed because it is too large Load Diff

1379
i18n/hi.po Normal file

File diff suppressed because it is too large Load Diff

1391
i18n/hr.po Normal file

File diff suppressed because it is too large Load Diff

1438
i18n/hu.po Normal file

File diff suppressed because it is too large Load Diff

1379
i18n/hy.po Normal file

File diff suppressed because it is too large Load Diff

1544
i18n/id.po Normal file

File diff suppressed because it is too large Load Diff

1377
i18n/is.po Normal file

File diff suppressed because it is too large Load Diff

1546
i18n/it.po Normal file

File diff suppressed because it is too large Load Diff

1533
i18n/ja.po Normal file

File diff suppressed because it is too large Load Diff

1379
i18n/ka.po Normal file

File diff suppressed because it is too large Load Diff

1390
i18n/kab.po Normal file

File diff suppressed because it is too large Load Diff

1390
i18n/kk.po Normal file

File diff suppressed because it is too large Load Diff

1382
i18n/km.po Normal file

File diff suppressed because it is too large Load Diff

1531
i18n/ko.po Normal file

File diff suppressed because it is too large Load Diff

1377
i18n/lb.po Normal file

File diff suppressed because it is too large Load Diff

1379
i18n/lo.po Normal file

File diff suppressed because it is too large Load Diff

1453
i18n/lt.po Normal file

File diff suppressed because it is too large Load Diff

1433
i18n/lv.po Normal file

File diff suppressed because it is too large Load Diff

1392
i18n/mk.po Normal file

File diff suppressed because it is too large Load Diff

1388
i18n/mn.po Normal file

File diff suppressed because it is too large Load Diff

1392
i18n/nb.po Normal file

File diff suppressed because it is too large Load Diff

1547
i18n/nl.po Normal file

File diff suppressed because it is too large Load Diff

1449
i18n/pl.po Normal file

File diff suppressed because it is too large Load Diff

1431
i18n/pt.po Normal file

File diff suppressed because it is too large Load Diff

1545
i18n/pt_BR.po Normal file

File diff suppressed because it is too large Load Diff

1386
i18n/ro.po Normal file

File diff suppressed because it is too large Load Diff

1551
i18n/ru.po Normal file

File diff suppressed because it is too large Load Diff

1441
i18n/sk.po Normal file

File diff suppressed because it is too large Load Diff

1438
i18n/sl.po Normal file

File diff suppressed because it is too large Load Diff

1390
i18n/sq.po Normal file

File diff suppressed because it is too large Load Diff

1460
i18n/sr.po Normal file

File diff suppressed because it is too large Load Diff

1383
i18n/sr@latin.po Normal file

File diff suppressed because it is too large Load Diff

1462
i18n/sv.po Normal file

File diff suppressed because it is too large Load Diff

1390
i18n/ta.po Normal file

File diff suppressed because it is too large Load Diff

1539
i18n/th.po Normal file

File diff suppressed because it is too large Load Diff

1460
i18n/tr.po Normal file

File diff suppressed because it is too large Load Diff

1447
i18n/uk.po Normal file

File diff suppressed because it is too large Load Diff

1438
i18n/vi.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1533
i18n/zh_CN.po Normal file

File diff suppressed because it is too large Load Diff

1379
i18n/zh_HK.po Normal file

File diff suppressed because it is too large Load Diff

1517
i18n/zh_TW.po Normal file

File diff suppressed because it is too large Load Diff

8
models/__init__.py Normal file
View File

@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import crm_lead
from . import res_partner
from . import res_partner_activation
from . import res_partner_grade
from . import website

321
models/crm_lead.py Normal file
View File

@ -0,0 +1,321 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import random
from markupsafe import Markup
from odoo import api, fields, models, _
from odoo.exceptions import AccessDenied, AccessError, UserError
from odoo.tools import html_escape
class CrmLead(models.Model):
_inherit = "crm.lead"
partner_latitude = fields.Float('Geo Latitude', digits=(10, 7))
partner_longitude = fields.Float('Geo Longitude', digits=(10, 7))
partner_assigned_id = fields.Many2one('res.partner', 'Assigned Partner', tracking=True, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", help="Partner this case has been forwarded/assigned to.", index='btree_not_null')
partner_declined_ids = fields.Many2many(
'res.partner',
'crm_lead_declined_partner',
'lead_id',
'partner_id',
string='Partner not interested')
date_partner_assign = fields.Date(
'Partner Assignment Date', compute='_compute_date_partner_assign',
copy=True, readonly=False, store=True,
help="Last date this case was forwarded/assigned to a partner")
@api.depends("partner_assigned_id")
def _compute_date_partner_assign(self):
for lead in self:
if not lead.partner_assigned_id:
lead.date_partner_assign = False
else:
lead.date_partner_assign = fields.Date.context_today(lead)
def _merge_get_fields(self):
fields_list = super(CrmLead, self)._merge_get_fields()
fields_list += ['partner_latitude', 'partner_longitude', 'partner_assigned_id', 'date_partner_assign']
return fields_list
def assign_salesman_of_assigned_partner(self):
salesmans_leads = {}
for lead in self:
if lead.active and lead.probability < 100:
if lead.partner_assigned_id and lead.partner_assigned_id.user_id != lead.user_id:
salesmans_leads.setdefault(lead.partner_assigned_id.user_id.id, []).append(lead.id)
for salesman_id, leads_ids in salesmans_leads.items():
leads = self.browse(leads_ids)
leads.write({'user_id': salesman_id})
def action_assign_partner(self):
""" 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.user.partner_id, 'simple_notification', {
'type': 'danger',
'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 = {}
res = False
if not partner_id:
partner_dict = self.search_geo_partner()
for lead in self:
if not partner_id:
partner_id = partner_dict.get(lead.id, False)
if not partner_id:
tag_to_add = self.env.ref('website_crm_partner_assign.tag_portal_lead_partner_unavailable', False)
if tag_to_add:
lead.write({'tag_ids': [(4, tag_to_add.id, False)]})
continue
lead.assign_geo_localize(lead.partner_latitude, lead.partner_longitude)
partner = self.env['res.partner'].browse(partner_id)
if partner.user_id:
lead._handle_salesmen_assignment(user_ids=partner.user_id.ids, team_id=partner.team_id.id)
lead.write({'partner_assigned_id': partner_id})
return res
def assign_geo_localize(self, latitude=False, longitude=False):
if latitude and longitude:
self.write({
'partner_latitude': latitude,
'partner_longitude': longitude
})
return True
# Don't pass context to browse()! We need country name in english below
for lead in self:
if lead.partner_latitude and lead.partner_longitude:
continue
if lead.country_id:
result = self.env['res.partner']._geo_localize(
lead.street, lead.zip, lead.city,
lead.state_id.name, lead.country_id.name
)
if result:
lead.write({
'partner_latitude': result[0],
'partner_longitude': result[1]
})
return True
def _prepare_customer_values(self, partner_name, is_company=False, parent_id=False):
res = super()._prepare_customer_values(partner_name, is_company=is_company, parent_id=parent_id)
res.update({
'partner_latitude': self.partner_latitude,
'partner_longitude': self.partner_longitude,
})
return res
def search_geo_partner(self):
Partner = self.env['res.partner']
res_partner_ids = {}
self.assign_geo_localize()
for lead in self:
partner_ids = []
if not lead.country_id:
continue
latitude = lead.partner_latitude
longitude = lead.partner_longitude
if latitude and longitude:
# 1. first way: in the same country, small area
partner_ids = Partner.search([
('partner_weight', '>', 0),
('partner_latitude', '>', latitude - 2), ('partner_latitude', '<', latitude + 2),
('partner_longitude', '>', longitude - 1.5), ('partner_longitude', '<', longitude + 1.5),
('country_id', '=', lead.country_id.id),
('id', 'not in', lead.partner_declined_ids.mapped('id')),
])
# 2. second way: in the same country, big area
if not partner_ids:
partner_ids = Partner.search([
('partner_weight', '>', 0),
('partner_latitude', '>', latitude - 4), ('partner_latitude', '<', latitude + 4),
('partner_longitude', '>', longitude - 3), ('partner_longitude', '<', longitude + 3),
('country_id', '=', lead.country_id.id),
('id', 'not in', lead.partner_declined_ids.mapped('id')),
])
# 3. third way: in the same country, extra large area
if not partner_ids:
partner_ids = Partner.search([
('partner_weight', '>', 0),
('partner_latitude', '>', latitude - 8), ('partner_latitude', '<', latitude + 8),
('partner_longitude', '>', longitude - 8), ('partner_longitude', '<', longitude + 8),
('country_id', '=', lead.country_id.id),
('id', 'not in', lead.partner_declined_ids.mapped('id')),
])
# 5. fifth way: anywhere in same country
if not partner_ids:
# still haven't found any, let's take all partners in the country!
partner_ids = Partner.search([
('partner_weight', '>', 0),
('country_id', '=', lead.country_id.id),
('id', 'not in', lead.partner_declined_ids.mapped('id')),
])
# 6. sixth way: closest partner whatsoever, just to have at least one result
if not partner_ids:
# warning: point() type takes (longitude, latitude) as parameters in this order!
self._cr.execute("""SELECT id, distance
FROM (select id, (point(partner_longitude, partner_latitude) <-> point(%s,%s)) AS distance FROM res_partner
WHERE active
AND partner_longitude is not null
AND partner_latitude is not null
AND partner_weight > 0
AND id not in (select partner_id from crm_lead_declined_partner where lead_id = %s)
) AS d
ORDER BY distance LIMIT 1""", (longitude, latitude, lead.id))
res = self._cr.dictfetchone()
if res:
partner_ids = Partner.browse([res['id']])
if partner_ids:
res_partner_ids[lead.id] = random.choices(
partner_ids.ids,
partner_ids.mapped('partner_weight'),
)[0]
return res_partner_ids
def partner_interested(self, comment=False):
message = Markup('<p>%s</p>') % _('I am interested by this lead.')
if comment:
message += Markup('<p>%s</p>') % comment
for lead in self:
lead.message_post(body=message)
lead.sudo().convert_opportunity(lead.partner_id) # sudo required to convert partner data
def partner_desinterested(self, comment=False, contacted=False, spam=False):
if contacted:
message = Markup('<p>%s</p>') % _('I am not interested by this lead. I contacted the lead.')
else:
message = Markup('<p>%s</p>') % _('I am not interested by this lead. I have not contacted the lead.')
partner_ids = self.env['res.partner'].search(
[('id', 'child_of', self.env.user.partner_id.commercial_partner_id.id)])
self.message_unsubscribe(partner_ids=partner_ids.ids)
if comment:
message += Markup('<p>%s</p>') % comment
self.message_post(body=message)
values = {
'partner_assigned_id': False,
}
if spam:
tag_spam = self.env.ref('website_crm_partner_assign.tag_portal_lead_is_spam', False)
if tag_spam and tag_spam not in self.tag_ids:
values['tag_ids'] = [(4, tag_spam.id, False)]
if partner_ids:
values['partner_declined_ids'] = [(4, p, 0) for p in partner_ids.ids]
self.sudo().write(values)
def update_lead_portal(self, values):
self.check_access_rights('write')
for lead in self:
lead_values = {
'expected_revenue': values['expected_revenue'],
'probability': values['probability'] or False,
'priority': values['priority'],
'date_deadline': values['date_deadline'] or False,
}
# As activities may belong to several users, only the current portal user activity
# will be modified by the portal form. If no activity exist we create a new one instead
# that we assign to the portal user.
user_activity = lead.sudo().activity_ids.filtered(lambda activity: activity.user_id == self.env.user)[:1]
if values['activity_date_deadline']:
if user_activity:
user_activity.sudo().write({
'activity_type_id': values['activity_type_id'],
'summary': values['activity_summary'],
'date_deadline': values['activity_date_deadline'],
})
else:
self.env['mail.activity'].sudo().create({
'res_model_id': self.env.ref('crm.model_crm_lead').id,
'res_id': lead.id,
'user_id': self.env.user.id,
'activity_type_id': values['activity_type_id'],
'summary': values['activity_summary'],
'date_deadline': values['activity_date_deadline'],
})
lead.write(lead_values)
def update_contact_details_from_portal(self, values):
self.check_access_rights('write')
fields = ['partner_name', 'phone', 'mobile', 'email_from', 'street', 'street2',
'city', 'zip', 'state_id', 'country_id']
if any([key not in fields for key in values]):
raise UserError(_("Not allowed to update the following field(s): %s.", ", ".join([key for key in values if not key in fields])))
return self.sudo().write(values)
@api.model
def create_opp_portal(self, values):
if not (self.env.user.partner_id.grade_id or self.env.user.commercial_partner_id.grade_id):
raise AccessDenied()
user = self.env.user
self = self.sudo()
if not (values['contact_name'] and values['description'] and values['title']):
return {
'errors': _('All fields are required!')
}
tag_own = self.env.ref('website_crm_partner_assign.tag_portal_lead_own_opp', False)
values = {
'contact_name': values['contact_name'],
'name': values['title'],
'description': values['description'],
'priority': '2',
'partner_assigned_id': user.commercial_partner_id.id,
}
if tag_own:
values['tag_ids'] = [(4, tag_own.id, False)]
lead = self.create(values)
lead.assign_salesman_of_assigned_partner()
lead.convert_opportunity(lead.partner_id)
return {
'id': lead.id
}
#
# DO NOT FORWARD PORT IN MASTER
# instead, crm.lead should implement portal.mixin
#
def _get_access_action(self, access_uid=None, force_website=False):
""" Instead of the classic form view, redirect to the online document for
portal users or if force_website=True. """
self.ensure_one()
user, record = self.env.user, self
if access_uid:
try:
record.check_access_rights('read')
record.check_access_rule("read")
except AccessError:
return super(CrmLead, self)._get_access_action(access_uid=access_uid, force_website=force_website)
user = self.env['res.users'].sudo().browse(access_uid)
record = self.with_user(user)
if user.share or force_website:
try:
record.check_access_rights('read')
record.check_access_rule('read')
except AccessError:
pass
else:
return {
'type': 'ir.actions.act_url',
'url': '/my/opportunity/%s' % record.id,
}
return super(CrmLead, self)._get_access_action(access_uid=access_uid, force_website=force_website)

85
models/res_partner.py Normal file
View File

@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo.osv import expression
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,
help="This should be a numerical value greater than 0 which will decide the contention for this partner to take this lead/opportunity.")
grade_id = fields.Many2one('res.partner.grade', 'Partner Level', tracking=True)
grade_sequence = fields.Integer(related='grade_id.sequence', readonly=True, store=True)
activation = fields.Many2one('res.partner.activation', 'Activation', index='btree_not_null', tracking=True)
date_partnership = fields.Date('Partnership Date')
date_review = fields.Date('Latest Partner Review')
date_review_next = fields.Date('Next Partner Review')
# customer implementation
assigned_partner_id = fields.Many2one(
'res.partner', 'Implemented by',
)
implemented_partner_ids = fields.One2many(
'res.partner', 'assigned_partner_id',
string='Implementation References',
)
implemented_partner_count = fields.Integer(compute='_compute_implemented_partner_count', store=True)
@api.depends('implemented_partner_ids.is_published', 'implemented_partner_ids.active')
def _compute_implemented_partner_count(self):
rg_result = self.env['res.partner']._read_group(
[('assigned_partner_id', 'in', self.ids),
('is_published', '=', True)],
['assigned_partner_id'],
['__count'],
)
rg_data = {assigned_partner.id: count for assigned_partner, count in rg_result}
for partner in self:
partner.implemented_partner_count = rg_data.get(partner.id, 0)
@api.depends('grade_id.partner_weight')
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()
opportunity_data = self.env['crm.lead'].with_context(active_test=False)._read_group(
[('partner_assigned_id', 'in', self.ids)],
['partner_assigned_id'], ['__count']
)
assign_counts = {partner_assigned.id: count for partner_assigned, count in opportunity_data}
for partner in self:
partner.opportunity_count += assign_counts.get(partner.id, 0)
def action_view_opportunity(self):
self.ensure_one() # especially here as we are doing an id, in, IDS domain
action = super().action_view_opportunity()
action_domain_origin = action.get('domain')
action_context_origin = action.get('context') or {}
action_domain_assign = [('partner_assigned_id', '=', self.id)]
if not action_domain_origin:
action['domain'] = action_domain_assign
return action
# perform searches independently as having OR with those leaves seems to
# be counter productive
Lead = self.env['crm.lead'].with_context(**action_context_origin)
ids_origin = Lead.search(action_domain_origin).ids
ids_new = Lead.search(action_domain_assign).ids
action['domain'] = [('id', 'in', sorted(list(set(ids_origin) | set(ids_new))))]
return action

View File

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class ResPartnerActivation(models.Model):
_name = 'res.partner.activation'
_order = 'sequence'
_description = 'Partner Activation'
sequence = fields.Integer('Sequence')
name = fields.Char('Name', required=True)
active = fields.Boolean(default=True)

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo.addons.http_routing.models.ir_http import slug
class ResPartnerGrade(models.Model):
_name = 'res.partner.grade'
_order = 'sequence'
_inherit = ['website.published.mixin']
_description = 'Partner Grade'
sequence = fields.Integer('Sequence')
active = fields.Boolean('Active', default=lambda *args: 1)
name = fields.Char('Level Name', translate=True)
partner_weight = fields.Integer('Level Weight', default=1,
help="Gives the probability to assign a lead to this partner. (0 means no assignment.)")
def _compute_website_url(self):
super(ResPartnerGrade, self)._compute_website_url()
for grade in self:
grade.website_url = "/partners/grade/%s" % (slug(grade))
def _default_is_published(self):
return True

14
models/website.py Normal file
View File

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, _
from odoo.addons.http_routing.models.ir_http import url_for
class Website(models.Model):
_inherit = "website"
def get_suggested_controllers(self):
suggested_controllers = super(Website, self).get_suggested_controllers()
suggested_controllers.append((_('Resellers'), url_for('/partners'), 'website_crm_partner_assign'))
return suggested_controllers

4
report/__init__.py Normal file
View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import crm_partner_report

View File

@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class CrmPartnerReportAssign(models.Model):
""" CRM Lead Report """
_name = "crm.partner.report.assign"
_auto = False
_description = "CRM Partnership Analysis"
partner_id = fields.Many2one('res.partner', 'Partner', required=False, readonly=True)
grade_id = fields.Many2one('res.partner.grade', 'Grade', readonly=True)
activation = fields.Many2one('res.partner.activation', 'Activation', index=True)
user_id = fields.Many2one('res.users', 'User', readonly=True)
date_review = fields.Date('Latest Partner Review')
date_partnership = fields.Date('Partnership Date')
country_id = fields.Many2one('res.country', 'Country', readonly=True)
team_id = fields.Many2one('crm.team', 'Sales Team', readonly=True)
nbr_opportunities = fields.Integer('# of Opportunity', readonly=True)
turnover = fields.Float('Turnover', readonly=True)
date = fields.Date('Invoice Account Date', readonly=True)
_depends = {
'account.invoice.report': ['invoice_date', 'partner_id', 'price_subtotal', 'state', 'move_type'],
'crm.lead': ['partner_assigned_id'],
'res.partner': ['activation', 'country_id', 'date_partnership', 'date_review',
'grade_id', 'parent_id', 'team_id', 'user_id'],
}
@property
def _table_query(self):
"""
CRM Lead Report
@param cr: the current row, from the database cursor
"""
return """
SELECT
COALESCE(2 * i.id, 2 * p.id + 1) AS id,
p.id as partner_id,
(SELECT country_id FROM res_partner a WHERE a.parent_id=p.id AND country_id is not null limit 1) as country_id,
p.grade_id,
p.activation,
p.date_review,
p.date_partnership,
p.user_id,
p.team_id,
(SELECT count(id) FROM crm_lead WHERE partner_assigned_id=p.id) AS nbr_opportunities,
i.price_subtotal as turnover,
i.invoice_date as date
FROM
res_partner p
left join ({account_invoice_report}) i
on (i.partner_id=p.id and i.move_type in ('out_invoice','out_refund') and i.state='posted')
""".format(
account_invoice_report=self.env['account.invoice.report']._table_query
)

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Opportunity tree view -->
<record id="view_report_crm_partner_assign_filter" model="ir.ui.view">
<field name="name">crm.partner.report.assign.select</field>
<field name="model">crm.partner.report.assign</field>
<field name="arch" type="xml">
<search string="Partner assigned Analysis">
<field name="team_id"/>
<field name="user_id"/>
<field name="grade_id"/>
<field name="activation"/>
<filter name="filter_date_partnership" date="date_partnership"/>
<filter name="filter_date_review" date="date_review"/>
<group expand="1" string="Group By">
<filter string="Salesperson" name="user"
context="{'group_by':'user_id'}" />
<filter string="Sales Team" name="sales_team"
context="{'group_by':'team_id'}"/>
<filter string="Partner" name="partner"
context="{'group_by':'partner_id'}" />
<separator/>
<filter string="Date Partnership" name="group_date_partnership"
context="{'group_by':'date_partnership'}" />
<filter string="Date Review" name="group_date_review"
context="{'group_by':'date_review'}" />
</group>
</search>
</field>
</record>
<record id="view_report_crm_partner_assign_graph" model="ir.ui.view">
<field name="name">crm.partner.assign.report.graph</field>
<field name="model">crm.partner.report.assign</field>
<field name="arch" type="xml">
<graph string="Opportunities Assignment Analysis" sample="1" disable_linking="1">
<field name="grade_id"/>
<field name="nbr_opportunities" type="measure"/>
<field name="turnover" type="measure"/>
</graph>
</field>
</record>
<!-- Leads by user and team Action -->
<record id="action_report_crm_partner_assign" model="ir.actions.act_window">
<field name="name">Partnership Analysis</field>
<field name="res_model">crm.partner.report.assign</field>
<field name="context">{'group_by_no_leaf':1,'group_by':[]}</field>
<field name="view_mode">graph</field>
<field name="domain">[('grade_id', '!=', False)]</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No data yet!
</p>
</field>
</record>
<menuitem name="Partnerships" id="menu_report_crm_partner_assign_tree"
parent="crm.crm_menu_report" action="action_report_crm_partner_assign" sequence="5"/>
</odoo>

View File

@ -0,0 +1,15 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_crm_partner_report,crm.partner.report.assign.all,model_crm_partner_report_assign,sales_team.group_sale_salesman,1,0,0,0
access_res_partner_grade,res.partner.grade,model_res_partner_grade,sales_team.group_sale_salesman,1,1,1,0
access_res_partner_grade_employee,res.partner.grade,model_res_partner_grade,base.group_user,1,0,0,0
access_res_partner_grade_portal,res.partner.grade,model_res_partner_grade,base.group_portal,1,0,0,0
access_res_partner_grade_public,res.partner.grade,model_res_partner_grade,base.group_public,1,0,0,0
access_res_partner_grade_manager,res.partner.grade.manager,model_res_partner_grade,sales_team.group_sale_manager,1,1,1,1
access_res_partner_activation_user,res.partner.activation.user,model_res_partner_activation,base.group_user,1,0,0,0
access_partner_activation_manager,res.partner.activation.manager,model_res_partner_activation,base.group_partner_manager,1,1,1,1
partner_access_crm_lead,crm.lead,crm.model_crm_lead,base.group_portal,1,1,0,0
partner_access_crm_stage,crm.stage,crm.model_crm_stage,base.group_portal,1,0,0,0
access_res_partner_grade_invoicing_payment_readonly,res.partner.grade,model_res_partner_grade,account.group_account_readonly,1,0,0,0
access_res_partner_grade_invoicing_payment,res.partner.grade,model_res_partner_grade,account.group_account_invoice,1,0,0,0
access_crm_lead_forward_to_partner,access.crm.lead.forward.to.partner,model_crm_lead_forward_to_partner,sales_team.group_sale_salesman,1,1,1,0
access_crm_lead_assignation,access.crm.lead.assignation,model_crm_lead_assignation,sales_team.group_sale_salesman,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_crm_partner_report crm.partner.report.assign.all model_crm_partner_report_assign sales_team.group_sale_salesman 1 0 0 0
3 access_res_partner_grade res.partner.grade model_res_partner_grade sales_team.group_sale_salesman 1 1 1 0
4 access_res_partner_grade_employee res.partner.grade model_res_partner_grade base.group_user 1 0 0 0
5 access_res_partner_grade_portal res.partner.grade model_res_partner_grade base.group_portal 1 0 0 0
6 access_res_partner_grade_public res.partner.grade model_res_partner_grade base.group_public 1 0 0 0
7 access_res_partner_grade_manager res.partner.grade.manager model_res_partner_grade sales_team.group_sale_manager 1 1 1 1
8 access_res_partner_activation_user res.partner.activation.user model_res_partner_activation base.group_user 1 0 0 0
9 access_partner_activation_manager res.partner.activation.manager model_res_partner_activation base.group_partner_manager 1 1 1 1
10 partner_access_crm_lead crm.lead crm.model_crm_lead base.group_portal 1 1 0 0
11 partner_access_crm_stage crm.stage crm.model_crm_stage base.group_portal 1 0 0 0
12 access_res_partner_grade_invoicing_payment_readonly res.partner.grade model_res_partner_grade account.group_account_readonly 1 0 0 0
13 access_res_partner_grade_invoicing_payment res.partner.grade model_res_partner_grade account.group_account_invoice 1 0 0 0
14 access_crm_lead_forward_to_partner access.crm.lead.forward.to.partner model_crm_lead_forward_to_partner sales_team.group_sale_salesman 1 1 1 0
15 access_crm_lead_assignation access.crm.lead.assignation model_crm_lead_assignation sales_team.group_sale_salesman 1 1 1 0

39
security/ir_rule.xml Normal file
View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<!-- CRM Lead portal -->
<record id="assigned_lead_portal_rule_1" model="ir.rule">
<field name="name">Portal Graded Partner: read and write assigned leads</field>
<field name="model_id" ref="crm.model_crm_lead"/>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
<field name="domain_force">[('partner_assigned_id','child_of',user.commercial_partner_id.id)]</field>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<!-- Partner Grade : portal and public -->
<record id="res_partner_grade_rule_portal_public" model="ir.rule">
<field name="name">Portal/Public user: read only website published</field>
<field name="model_id" ref="website_crm_partner_assign.model_res_partner_grade"/>
<field name="groups" eval="[(4, ref('base.group_portal')), (4, ref('base.group_public'))]"/>
<field name="domain_force">[('website_published','=', True)]</field>
<field name="perm_read" eval="True"/>
</record>
<record id="ir_rule_crm_partner_report_assign_all" model="ir.rule">
<field name="name">CRM partner assign report: All Assignations</field>
<field name="model_id" ref="model_crm_partner_report_assign"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman_all_leads'))]"/>
</record>
<record id="ir_rule_crm_partner_report_assign_salesman" model="ir.rule">
<field name="name">CRM partner assign report: Personal / Global Assignations</field>
<field name="model_id" ref="model_crm_partner_report_assign"/>
<field name="domain_force">['|', ('user_id', '=', user.id), ('user_id', '=', False)]</field>
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
</record>
</odoo>

BIN
static/description/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1 @@
<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M15.724 6.397C16.377 4.94 17.852 4 19.481 4h11.037c1.63 0 3.104.94 3.757 2.397L37.236 13H41.9c2.46 0 4.367 2.099 4.07 4.481l-3.106 25C42.613 44.49 40.866 46 38.793 46H11.207c-2.074 0-3.82-1.51-4.07-3.519l-3.107-25C3.734 15.1 5.64 13 8.1 13h4.663l2.961-6.603ZM32.917 13H17.082c0-.56.123-1.134.39-1.691l.956-2C19.102 7.9 20.551 7 22.144 7h5.711c1.593 0 3.042.9 3.716 2.308l.957 2c.266.558.39 1.132.39 1.692Z" fill="#088BF5"/><path fill-rule="evenodd" clip-rule="evenodd" d="M8.514 45.016a3.963 3.963 0 0 1-1.377-2.535l-3.107-25C3.734 15.1 5.64 13 8.1 13h4.663l2.961-6.603C16.377 4.94 17.852 4 19.481 4h11.037c1.63 0 3.104.94 3.757 2.397l2.59 5.777C35.5 28.256 23.848 41.405 8.515 45.016ZM17.082 13h15.835c0-.56-.123-1.134-.39-1.691l-.956-2C30.897 7.9 29.448 7 27.855 7h-5.711c-1.593 0-3.042.9-3.716 2.308l-.956 2a3.904 3.904 0 0 0-.39 1.692Z" fill="#2EBCFA"/><path d="M33.689 27.924c-.776 2.774-5.49 7.604-9.144 11.031-2.055 1.927-5.403 1.068-6.177-1.585-1.376-4.719-2.937-11.16-2.161-13.933 1.295-4.63 6.258-7.379 11.085-6.14 4.828 1.24 7.692 5.997 6.397 10.627Z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Some files were not shown because too many files have changed in this diff Show More