Начальное наполнение
This commit is contained in:
parent
6f312b6ddf
commit
5c9f46b479
7
__init__.py
Normal file
7
__init__.py
Normal 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
56
__manifest__.py
Normal 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
4
controllers/__init__.py
Normal 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
354
controllers/main.py
Normal 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
33
data/crm_lead_demo.xml
Normal 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>
|
27
data/crm_lead_merge_template.xml
Normal file
27
data/crm_lead_merge_template.xml
Normal 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
17
data/crm_tag_data.xml
Normal 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
102
data/mail_template_data.xml
Normal 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" & "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&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>
|
15
data/res_partner_activation_data.xml
Normal file
15
data/res_partner_activation_data.xml
Normal 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
81
data/res_partner_demo.xml
Normal 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>
|
15
data/res_partner_grade_data.xml
Normal file
15
data/res_partner_grade_data.xml
Normal 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>
|
14
data/res_partner_grade_demo.xml
Normal file
14
data/res_partner_grade_demo.xml
Normal 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
1381
i18n/af.po
Normal file
File diff suppressed because it is too large
Load Diff
1377
i18n/am.po
Normal file
1377
i18n/am.po
Normal file
File diff suppressed because it is too large
Load Diff
1541
i18n/ar.po
Normal file
1541
i18n/ar.po
Normal file
File diff suppressed because it is too large
Load Diff
1382
i18n/az.po
Normal file
1382
i18n/az.po
Normal file
File diff suppressed because it is too large
Load Diff
1443
i18n/bg.po
Normal file
1443
i18n/bg.po
Normal file
File diff suppressed because it is too large
Load Diff
1382
i18n/bs.po
Normal file
1382
i18n/bs.po
Normal file
File diff suppressed because it is too large
Load Diff
1465
i18n/ca.po
Normal file
1465
i18n/ca.po
Normal file
File diff suppressed because it is too large
Load Diff
1449
i18n/cs.po
Normal file
1449
i18n/cs.po
Normal file
File diff suppressed because it is too large
Load Diff
1445
i18n/da.po
Normal file
1445
i18n/da.po
Normal file
File diff suppressed because it is too large
Load Diff
1550
i18n/de.po
Normal file
1550
i18n/de.po
Normal file
File diff suppressed because it is too large
Load Diff
1383
i18n/el.po
Normal file
1383
i18n/el.po
Normal file
File diff suppressed because it is too large
Load Diff
1379
i18n/en_AU.po
Normal file
1379
i18n/en_AU.po
Normal file
File diff suppressed because it is too large
Load Diff
1391
i18n/en_GB.po
Normal file
1391
i18n/en_GB.po
Normal file
File diff suppressed because it is too large
Load Diff
1546
i18n/es.po
Normal file
1546
i18n/es.po
Normal file
File diff suppressed because it is too large
Load Diff
1551
i18n/es_419.po
Normal file
1551
i18n/es_419.po
Normal file
File diff suppressed because it is too large
Load Diff
1391
i18n/es_BO.po
Normal file
1391
i18n/es_BO.po
Normal file
File diff suppressed because it is too large
Load Diff
1390
i18n/es_CL.po
Normal file
1390
i18n/es_CL.po
Normal file
File diff suppressed because it is too large
Load Diff
1394
i18n/es_CO.po
Normal file
1394
i18n/es_CO.po
Normal file
File diff suppressed because it is too large
Load Diff
1390
i18n/es_CR.po
Normal file
1390
i18n/es_CR.po
Normal file
File diff suppressed because it is too large
Load Diff
1380
i18n/es_DO.po
Normal file
1380
i18n/es_DO.po
Normal file
File diff suppressed because it is too large
Load Diff
1395
i18n/es_EC.po
Normal file
1395
i18n/es_EC.po
Normal file
File diff suppressed because it is too large
Load Diff
1392
i18n/es_PE.po
Normal file
1392
i18n/es_PE.po
Normal file
File diff suppressed because it is too large
Load Diff
1390
i18n/es_PY.po
Normal file
1390
i18n/es_PY.po
Normal file
File diff suppressed because it is too large
Load Diff
1390
i18n/es_VE.po
Normal file
1390
i18n/es_VE.po
Normal file
File diff suppressed because it is too large
Load Diff
1557
i18n/et.po
Normal file
1557
i18n/et.po
Normal file
File diff suppressed because it is too large
Load Diff
1390
i18n/eu.po
Normal file
1390
i18n/eu.po
Normal file
File diff suppressed because it is too large
Load Diff
1433
i18n/fa.po
Normal file
1433
i18n/fa.po
Normal file
File diff suppressed because it is too large
Load Diff
1560
i18n/fi.po
Normal file
1560
i18n/fi.po
Normal file
File diff suppressed because it is too large
Load Diff
1548
i18n/fr.po
Normal file
1548
i18n/fr.po
Normal file
File diff suppressed because it is too large
Load Diff
1390
i18n/fr_BE.po
Normal file
1390
i18n/fr_BE.po
Normal file
File diff suppressed because it is too large
Load Diff
1390
i18n/fr_CA.po
Normal file
1390
i18n/fr_CA.po
Normal file
File diff suppressed because it is too large
Load Diff
1390
i18n/gl.po
Normal file
1390
i18n/gl.po
Normal file
File diff suppressed because it is too large
Load Diff
1381
i18n/gu.po
Normal file
1381
i18n/gu.po
Normal file
File diff suppressed because it is too large
Load Diff
1445
i18n/he.po
Normal file
1445
i18n/he.po
Normal file
File diff suppressed because it is too large
Load Diff
1379
i18n/hi.po
Normal file
1379
i18n/hi.po
Normal file
File diff suppressed because it is too large
Load Diff
1391
i18n/hr.po
Normal file
1391
i18n/hr.po
Normal file
File diff suppressed because it is too large
Load Diff
1438
i18n/hu.po
Normal file
1438
i18n/hu.po
Normal file
File diff suppressed because it is too large
Load Diff
1379
i18n/hy.po
Normal file
1379
i18n/hy.po
Normal file
File diff suppressed because it is too large
Load Diff
1544
i18n/id.po
Normal file
1544
i18n/id.po
Normal file
File diff suppressed because it is too large
Load Diff
1377
i18n/is.po
Normal file
1377
i18n/is.po
Normal file
File diff suppressed because it is too large
Load Diff
1546
i18n/it.po
Normal file
1546
i18n/it.po
Normal file
File diff suppressed because it is too large
Load Diff
1533
i18n/ja.po
Normal file
1533
i18n/ja.po
Normal file
File diff suppressed because it is too large
Load Diff
1379
i18n/ka.po
Normal file
1379
i18n/ka.po
Normal file
File diff suppressed because it is too large
Load Diff
1390
i18n/kab.po
Normal file
1390
i18n/kab.po
Normal file
File diff suppressed because it is too large
Load Diff
1390
i18n/kk.po
Normal file
1390
i18n/kk.po
Normal file
File diff suppressed because it is too large
Load Diff
1382
i18n/km.po
Normal file
1382
i18n/km.po
Normal file
File diff suppressed because it is too large
Load Diff
1531
i18n/ko.po
Normal file
1531
i18n/ko.po
Normal file
File diff suppressed because it is too large
Load Diff
1377
i18n/lb.po
Normal file
1377
i18n/lb.po
Normal file
File diff suppressed because it is too large
Load Diff
1379
i18n/lo.po
Normal file
1379
i18n/lo.po
Normal file
File diff suppressed because it is too large
Load Diff
1453
i18n/lt.po
Normal file
1453
i18n/lt.po
Normal file
File diff suppressed because it is too large
Load Diff
1433
i18n/lv.po
Normal file
1433
i18n/lv.po
Normal file
File diff suppressed because it is too large
Load Diff
1392
i18n/mk.po
Normal file
1392
i18n/mk.po
Normal file
File diff suppressed because it is too large
Load Diff
1388
i18n/mn.po
Normal file
1388
i18n/mn.po
Normal file
File diff suppressed because it is too large
Load Diff
1392
i18n/nb.po
Normal file
1392
i18n/nb.po
Normal file
File diff suppressed because it is too large
Load Diff
1547
i18n/nl.po
Normal file
1547
i18n/nl.po
Normal file
File diff suppressed because it is too large
Load Diff
1449
i18n/pl.po
Normal file
1449
i18n/pl.po
Normal file
File diff suppressed because it is too large
Load Diff
1431
i18n/pt.po
Normal file
1431
i18n/pt.po
Normal file
File diff suppressed because it is too large
Load Diff
1545
i18n/pt_BR.po
Normal file
1545
i18n/pt_BR.po
Normal file
File diff suppressed because it is too large
Load Diff
1386
i18n/ro.po
Normal file
1386
i18n/ro.po
Normal file
File diff suppressed because it is too large
Load Diff
1551
i18n/ru.po
Normal file
1551
i18n/ru.po
Normal file
File diff suppressed because it is too large
Load Diff
1441
i18n/sk.po
Normal file
1441
i18n/sk.po
Normal file
File diff suppressed because it is too large
Load Diff
1438
i18n/sl.po
Normal file
1438
i18n/sl.po
Normal file
File diff suppressed because it is too large
Load Diff
1390
i18n/sq.po
Normal file
1390
i18n/sq.po
Normal file
File diff suppressed because it is too large
Load Diff
1460
i18n/sr.po
Normal file
1460
i18n/sr.po
Normal file
File diff suppressed because it is too large
Load Diff
1383
i18n/sr@latin.po
Normal file
1383
i18n/sr@latin.po
Normal file
File diff suppressed because it is too large
Load Diff
1462
i18n/sv.po
Normal file
1462
i18n/sv.po
Normal file
File diff suppressed because it is too large
Load Diff
1390
i18n/ta.po
Normal file
1390
i18n/ta.po
Normal file
File diff suppressed because it is too large
Load Diff
1539
i18n/th.po
Normal file
1539
i18n/th.po
Normal file
File diff suppressed because it is too large
Load Diff
1460
i18n/tr.po
Normal file
1460
i18n/tr.po
Normal file
File diff suppressed because it is too large
Load Diff
1447
i18n/uk.po
Normal file
1447
i18n/uk.po
Normal file
File diff suppressed because it is too large
Load Diff
1438
i18n/vi.po
Normal file
1438
i18n/vi.po
Normal file
File diff suppressed because it is too large
Load Diff
1423
i18n/website_crm_partner_assign.pot
Normal file
1423
i18n/website_crm_partner_assign.pot
Normal file
File diff suppressed because it is too large
Load Diff
1533
i18n/zh_CN.po
Normal file
1533
i18n/zh_CN.po
Normal file
File diff suppressed because it is too large
Load Diff
1379
i18n/zh_HK.po
Normal file
1379
i18n/zh_HK.po
Normal file
File diff suppressed because it is too large
Load Diff
1517
i18n/zh_TW.po
Normal file
1517
i18n/zh_TW.po
Normal file
File diff suppressed because it is too large
Load Diff
8
models/__init__.py
Normal file
8
models/__init__.py
Normal 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
321
models/crm_lead.py
Normal 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
85
models/res_partner.py
Normal 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
|
14
models/res_partner_activation.py
Normal file
14
models/res_partner_activation.py
Normal 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)
|
26
models/res_partner_grade.py
Normal file
26
models/res_partner_grade.py
Normal 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
14
models/website.py
Normal 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
4
report/__init__.py
Normal 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
|
58
report/crm_partner_report.py
Normal file
58
report/crm_partner_report.py
Normal 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
|
||||
)
|
63
report/crm_partner_report_view.xml
Normal file
63
report/crm_partner_report_view.xml
Normal 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>
|
15
security/ir.model.access.csv
Normal file
15
security/ir.model.access.csv
Normal 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
|
|
39
security/ir_rule.xml
Normal file
39
security/ir_rule.xml
Normal 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
BIN
static/description/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
1
static/description/icon.svg
Normal file
1
static/description/icon.svg
Normal 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
Loading…
x
Reference in New Issue
Block a user