diff --git a/addons/base_geolocalize/models/res_partner.py b/addons/base_geolocalize/models/res_partner.py index 0367c3e07e2..95a396522fd 100644 --- a/addons/base_geolocalize/models/res_partner.py +++ b/addons/base_geolocalize/models/res_partner.py @@ -1,4 +1,4 @@ -from odoo import api, fields, models +from odoo import api, fields, models, _ class ResPartner(models.Model): @@ -18,6 +18,7 @@ class ResPartner(models.Model): def geo_localize(self): # We need country names in English below + partners_not_geo_localized = self.env['res.partner'] for partner in self.with_context(lang='en_US'): result = self._geo_localize(partner.street, partner.zip, @@ -31,4 +32,15 @@ class ResPartner(models.Model): 'partner_longitude': result[1], 'date_localization': fields.Date.context_today(partner) }) + else: + partners_not_geo_localized |= partner + if partners_not_geo_localized: + self.env['bus.bus'].sendone( + (self.env.cr.dbname, 'res.partner', self.env.user.partner_id.id), + {'type': 'simple_notification', + 'title': _("Warning"), + 'message': _('No address found for %(partner_names)s.', + partner_names=', '.join(partners_not_geo_localized.mapped('name')) + ) + }) return True diff --git a/addons/website_crm_partner_assign/__manifest__.py b/addons/website_crm_partner_assign/__manifest__.py index aec9cf1600c..0bd6dbba40c 100644 --- a/addons/website_crm_partner_assign/__manifest__.py +++ b/addons/website_crm_partner_assign/__manifest__.py @@ -27,6 +27,8 @@ The automatic assignment is figured from the weight of partner levels and the ge 'data/crm_lead_merge_template.xml', 'data/crm_tag_data.xml', 'data/mail_template_data.xml', + 'data/res_partner_activation_data.xml', + 'data/res_partner_grade_data.xml', 'security/ir.model.access.csv', 'security/ir_rule.xml', 'wizard/crm_forward_to_partner_view.xml', diff --git a/addons/website_crm_partner_assign/data/res_partner_activation_data.xml b/addons/website_crm_partner_assign/data/res_partner_activation_data.xml new file mode 100644 index 00000000000..c75cfe10494 --- /dev/null +++ b/addons/website_crm_partner_assign/data/res_partner_activation_data.xml @@ -0,0 +1,15 @@ + + + + Fully Operational + 3 + + + Ramp-up + 2 + + + First Contact + 1 + + diff --git a/addons/website_crm_partner_assign/data/res_partner_demo.xml b/addons/website_crm_partner_assign/data/res_partner_demo.xml index dde9e124af6..1cf8ecd345e 100644 --- a/addons/website_crm_partner_assign/data/res_partner_demo.xml +++ b/addons/website_crm_partner_assign/data/res_partner_demo.xml @@ -1,70 +1,44 @@ - - Platinum - 4 - - - Gold - 3 - - - Silver - 2 - - - Bronze - 1 - - - + 10 - + 10 - + 10 - + 10 - + 10 - + 10 - + 10 - + 10 - - 10 - - - - - 10 - - - + 10 diff --git a/addons/website_crm_partner_assign/data/res_partner_grade_data.xml b/addons/website_crm_partner_assign/data/res_partner_grade_data.xml new file mode 100644 index 00000000000..86b774a91e0 --- /dev/null +++ b/addons/website_crm_partner_assign/data/res_partner_grade_data.xml @@ -0,0 +1,15 @@ + + + + Gold + 3 + + + Silver + 2 + + + Bronze + 1 + + diff --git a/addons/website_crm_partner_assign/data/res_partner_grade_demo.xml b/addons/website_crm_partner_assign/data/res_partner_grade_demo.xml index c12607f0cbf..c04c1b22036 100644 --- a/addons/website_crm_partner_assign/data/res_partner_grade_demo.xml +++ b/addons/website_crm_partner_assign/data/res_partner_grade_demo.xml @@ -1,21 +1,14 @@ - + - + - + - - - - - - 10 - diff --git a/addons/website_crm_partner_assign/models/crm_lead.py b/addons/website_crm_partner_assign/models/crm_lead.py index 4903950eff3..0376f7b9d4a 100644 --- a/addons/website_crm_partner_assign/models/crm_lead.py +++ b/addons/website_crm_partner_assign/models/crm_lead.py @@ -51,7 +51,21 @@ class CrmLead(models.Model): leads.write({'user_id': salesman_id}) def action_assign_partner(self): - return self.assign_partner(partner_id=False) + """ While assigning a partner, geo-localization is performed only for leads having country + set (see method 'assign_geo_localize' and 'search_geo_partner'). So for leads that does not + have country set, we show the notification, and for the rest, we geo-localize them. + """ + leads_with_country = self.filtered(lambda lead: lead.country_id) + leads_without_country = self - leads_with_country + if leads_without_country: + self.env['bus.bus'].sendone( + (self.env.cr.dbname, 'res.partner', self.env.user.partner_id.id), + {'type': 'simple_notification', + 'title': _("Warning"), + 'message': _('There is no country set in addresses for %(lead_names)s.', + lead_names=', '.join(leads_without_country.mapped('name'))) + }) + return leads_with_country.assign_partner(partner_id=False) def assign_partner(self, partner_id=False): partner_dict = {} diff --git a/addons/website_crm_partner_assign/models/res_partner.py b/addons/website_crm_partner_assign/models/res_partner.py index 8790859f0c5..f43ca2b6bf6 100644 --- a/addons/website_crm_partner_assign/models/res_partner.py +++ b/addons/website_crm_partner_assign/models/res_partner.py @@ -2,12 +2,13 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models +from odoo.osv import expression from odoo.addons.http_routing.models.ir_http import slug class ResPartnerGrade(models.Model): _name = 'res.partner.grade' - _order = 'sequence' + _order = 'sequence desc' _inherit = ['website.published.mixin'] _description = 'Partner Grade' @@ -28,7 +29,7 @@ class ResPartnerGrade(models.Model): class ResPartnerActivation(models.Model): _name = 'res.partner.activation' - _order = 'sequence' + _order = 'sequence desc' _description = 'Partner Activation' sequence = fields.Integer('Sequence') @@ -38,6 +39,18 @@ class ResPartnerActivation(models.Model): class ResPartner(models.Model): _inherit = "res.partner" + @api.model + def default_get(self, fields_list): + default_vals = super().default_get(fields_list) + if self.env.context.get('partner_set_default_grade_activation'): + # sets the lowest grade and activation if no default values given, mainly useful while + # creating assigned partner on the fly (to make it visible in same m2o again) + if 'grade_id' in fields_list and not default_vals.get('grade_id'): + default_vals['grade_id'] = self.env['res.partner.grade'].search([], order='sequence', limit=1).id + if 'activation' in fields_list and not default_vals.get('activation'): + default_vals['activation'] = self.env['res.partner.activation'].search([], order='sequence', limit=1).id + return default_vals + partner_weight = fields.Integer( 'Level Weight', compute='_compute_partner_weight', readonly=False, store=True, tracking=True, @@ -67,3 +80,20 @@ class ResPartner(models.Model): def _compute_partner_weight(self): for partner in self: partner.partner_weight = partner.grade_id.partner_weight if partner.grade_id else 0 + + def _compute_opportunity_count(self): + super()._compute_opportunity_count() + assign_counts = {} + if self.ids: + opportunity_data = self.env['crm.lead'].with_context(active_test=False).read_group( + [('partner_assigned_id', 'in', self.ids)], + ['partner_assigned_id'], ['partner_assigned_id'] + ) + assign_counts = {datum['partner_assigned_id'][0]: datum['partner_assigned_id_count'] for datum in opportunity_data} + for partner in self: + partner.opportunity_count += assign_counts.get(partner.id, 0) + + def action_view_opportunity(self): + action = super().action_view_opportunity() + action['domain'] = expression.OR([action.get('domain', []), [('partner_assigned_id', '=', self.id)]]) + return action diff --git a/addons/website_crm_partner_assign/static/src/js/crm_partner_assign.js b/addons/website_crm_partner_assign/static/src/js/crm_partner_assign.js index e43d1430579..2f3974807ba 100644 --- a/addons/website_crm_partner_assign/static/src/js/crm_partner_assign.js +++ b/addons/website_crm_partner_assign/static/src/js/crm_partner_assign.js @@ -1,6 +1,7 @@ odoo.define('crm.partner_assign', function (require) { 'use strict'; +const {_t} = require('web.core'); var publicWidget = require('web.public.widget'); var time = require('web.time'); @@ -15,6 +16,7 @@ publicWidget.registry.crmPartnerAssign = publicWidget.Widget.extend({ 'click .new_opp_confirm': '_onNewOppConfirm', 'click .edit_opp_confirm': '_onEditOppConfirm', 'change .edit_opp_form .next_activity': '_onChangeNextActivity', + 'change #new-opp-dialog .contact_name': '_onChangeContactName', 'click div.input-group.date[data-target-input="nearest"]': '_onCalendarInputGroupClick', }, @@ -229,6 +231,17 @@ publicWidget.registry.crmPartnerAssign = publicWidget.Widget.extend({ ev.stopPropagation(); this._buttonExec($(ev.currentTarget), this._editOpportunity); }, + /** + * @private + * @param {Event} ev + */ + _onChangeContactName: function (ev) { + const contactName = ev.currentTarget.value.trim(); + let titleEl = this.el.querySelector('.title'); + if (!titleEl.value.trim()) { + titleEl.value = contactName ? _.str.sprintf(_t("%s's Opportunity"), contactName) : ''; + } + }, /** * @private * @param {Event} ev @@ -259,12 +272,6 @@ publicWidget.registry.crmPartnerAssign = publicWidget.Widget.extend({ down: 'fa fa-chevron-down', }, }; - // in some screen sizes, the automatic position opens the picker below input, - // but because #next_activity_div is the last element, we always open the - // picker on top the input to prevent extra scroll - if ($calendarInputGroup.is('#next_activity_div')) { - calendarOptions.widgetPositioning = {vertical: 'top'}; - } $calendarInputGroup.datetimepicker(calendarOptions); }, diff --git a/addons/website_crm_partner_assign/views/crm_lead_views.xml b/addons/website_crm_partner_assign/views/crm_lead_views.xml index 4645c09afda..cf8ed757853 100644 --- a/addons/website_crm_partner_assign/views/crm_lead_views.xml +++ b/addons/website_crm_partner_assign/views/crm_lead_views.xml @@ -21,7 +21,7 @@ - + + + + crm.lead.view.kanban.inherit.website.crm.partner.assign + crm.lead + + + + + + + + + + + + + + + + + + diff --git a/addons/website_crm_partner_assign/views/res_partner_views.xml b/addons/website_crm_partner_assign/views/res_partner_views.xml index 61555b7c5c7..17b18fbc62c 100644 --- a/addons/website_crm_partner_assign/views/res_partner_views.xml +++ b/addons/website_crm_partner_assign/views/res_partner_views.xml @@ -34,6 +34,13 @@ Partner Activations res.partner.activation tree,form + + + Create a Partner Activation + + Those are used to know where your Partners stand in your onboarding process. + + - Partner Level + Partner Levels res.partner.grade + + + Create a Partner Level + + Partner Levels allow you to rank your Partners based on their performances. + + - - + + - - + + diff --git a/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml b/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml index 796dae42fff..72b4fdbf243 100644 --- a/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml +++ b/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml @@ -254,7 +254,7 @@ - Create New + Create Opportunity @@ -273,7 +273,7 @@ - Title + Opportunity @@ -569,31 +569,29 @@ Customer - + - + - - - + - + - - - + - + - - - + Address - + @@ -602,7 +600,7 @@ - + @@ -684,28 +682,35 @@ - Next Activity - - - - - + + + Next Activity + + + + + + + + + Next Activity Date + + + + + + + + + + + Details Next Activity - - - - Next Activity Date - - - - - - - - - +
+ Create a Partner Activation +
+ Those are used to know where your Partners stand in your onboarding process. +
+ Create a Partner Level +
+ Partner Levels allow you to rank your Partners based on their performances. +