summaryrefslogtreecommitdiff
path: root/addons/website_crm_partner_assign/models
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/website_crm_partner_assign/models
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/website_crm_partner_assign/models')
-rw-r--r--addons/website_crm_partner_assign/models/__init__.py6
-rw-r--r--addons/website_crm_partner_assign/models/crm_lead.py305
-rw-r--r--addons/website_crm_partner_assign/models/res_partner.py69
-rw-r--r--addons/website_crm_partner_assign/models/website.py14
4 files changed, 394 insertions, 0 deletions
diff --git a/addons/website_crm_partner_assign/models/__init__.py b/addons/website_crm_partner_assign/models/__init__.py
new file mode 100644
index 00000000..f5fb173f
--- /dev/null
+++ b/addons/website_crm_partner_assign/models/__init__.py
@@ -0,0 +1,6 @@
+# -*- 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 website
diff --git a/addons/website_crm_partner_assign/models/crm_lead.py b/addons/website_crm_partner_assign/models/crm_lead.py
new file mode 100644
index 00000000..c77d2701
--- /dev/null
+++ b/addons/website_crm_partner_assign/models/crm_lead.py
@@ -0,0 +1,305 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import random
+
+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=(16, 5))
+ partner_longitude = fields.Float('Geo Longitude', digits=(16, 5))
+ 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=True)
+ 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")
+
+ def _merge_data(self, fields):
+ fields += ['partner_latitude', 'partner_longitude', 'partner_assigned_id', 'date_partner_assign']
+ return super(CrmLead, self)._merge_data(fields)
+
+ @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 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):
+ return self.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(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 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']])
+
+ total_weight = 0
+ toassign = []
+ for partner in partner_ids:
+ total_weight += partner.partner_weight
+ toassign.append((partner.id, total_weight))
+
+ random.shuffle(toassign) # avoid always giving the leads to the first ones in db natural order!
+ nearest_weight = random.randint(0, total_weight)
+ for partner_id, weight in toassign:
+ if nearest_weight <= weight:
+ res_partner_ids[lead.id] = partner_id
+ break
+ return res_partner_ids
+
+ def partner_interested(self, comment=False):
+ message = _('<p>I am interested by this lead.</p>')
+ if comment:
+ message += '<p>%s</p>' % html_escape(comment)
+ for lead in self:
+ lead.message_post(body=message)
+ lead.sudo().convert_opportunity(lead.partner_id.id) # sudo required to convert partner data
+
+ def partner_desinterested(self, comment=False, contacted=False, spam=False):
+ if contacted:
+ message = '<p>%s</p>' % _('I am not interested by this lead. I contacted the lead.')
+ else:
+ message = '<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 += '<p>%s</p>' % html_escape(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.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):
+ """ Instead of the classic form view, redirect to the online document for
+ portal users or if force_website=True in the context. """
+ 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)
+ user = self.env['res.users'].sudo().browse(access_uid)
+ record = self.with_user(user)
+ if user.share or self.env.context.get('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)
diff --git a/addons/website_crm_partner_assign/models/res_partner.py b/addons/website_crm_partner_assign/models/res_partner.py
new file mode 100644
index 00000000..8790859f
--- /dev/null
+++ b/addons/website_crm_partner_assign/models/res_partner.py
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, 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
+
+
+class ResPartnerActivation(models.Model):
+ _name = 'res.partner.activation'
+ _order = 'sequence'
+ _description = 'Partner Activation'
+
+ sequence = fields.Integer('Sequence')
+ name = fields.Char('Name', required=True)
+
+
+class ResPartner(models.Model):
+ _inherit = "res.partner"
+
+ 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=True, 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_count = fields.Integer(compute='_compute_implemented_partner_count', store=True)
+
+ @api.depends('implemented_partner_ids', 'implemented_partner_ids.website_published', 'implemented_partner_ids.active')
+ def _compute_implemented_partner_count(self):
+ for partner in self:
+ partner.implemented_count = len(partner.implemented_partner_ids.filtered('website_published'))
+
+ @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
diff --git a/addons/website_crm_partner_assign/models/website.py b/addons/website_crm_partner_assign/models/website.py
new file mode 100644
index 00000000..5910fb51
--- /dev/null
+++ b/addons/website_crm_partner_assign/models/website.py
@@ -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