diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/website_crm_partner_assign/models | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/website_crm_partner_assign/models')
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 |
