From 3751379f1e9a4c215fb6eb898b4ccc67659b9ace Mon Sep 17 00:00:00 2001 From: stephanchrst Date: Tue, 10 May 2022 21:51:50 +0700 Subject: initial commit 2 --- addons/crm/wizard/__init__.py | 7 + addons/crm/wizard/crm_lead_lost.py | 14 ++ addons/crm/wizard/crm_lead_lost_views.xml | 27 ++++ addons/crm/wizard/crm_lead_to_opportunity.py | 170 +++++++++++++++++++++ addons/crm/wizard/crm_lead_to_opportunity_mass.py | 110 +++++++++++++ .../wizard/crm_lead_to_opportunity_mass_views.xml | 63 ++++++++ .../crm/wizard/crm_lead_to_opportunity_views.xml | 53 +++++++ addons/crm/wizard/crm_merge_opportunities.py | 56 +++++++ .../crm/wizard/crm_merge_opportunities_views.xml | 45 ++++++ 9 files changed, 545 insertions(+) create mode 100644 addons/crm/wizard/__init__.py create mode 100644 addons/crm/wizard/crm_lead_lost.py create mode 100644 addons/crm/wizard/crm_lead_lost_views.xml create mode 100644 addons/crm/wizard/crm_lead_to_opportunity.py create mode 100644 addons/crm/wizard/crm_lead_to_opportunity_mass.py create mode 100644 addons/crm/wizard/crm_lead_to_opportunity_mass_views.xml create mode 100644 addons/crm/wizard/crm_lead_to_opportunity_views.xml create mode 100644 addons/crm/wizard/crm_merge_opportunities.py create mode 100644 addons/crm/wizard/crm_merge_opportunities_views.xml (limited to 'addons/crm/wizard') diff --git a/addons/crm/wizard/__init__.py b/addons/crm/wizard/__init__.py new file mode 100644 index 00000000..e424c548 --- /dev/null +++ b/addons/crm/wizard/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import crm_lead_lost +from . import crm_lead_to_opportunity +from . import crm_lead_to_opportunity_mass +from . import crm_merge_opportunities diff --git a/addons/crm/wizard/crm_lead_lost.py b/addons/crm/wizard/crm_lead_lost.py new file mode 100644 index 00000000..8f64a405 --- /dev/null +++ b/addons/crm/wizard/crm_lead_lost.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +from odoo import api, fields, models + + +class CrmLeadLost(models.TransientModel): + _name = 'crm.lead.lost' + _description = 'Get Lost Reason' + + lost_reason_id = fields.Many2one('crm.lost.reason', 'Lost Reason') + + def action_lost_reason_apply(self): + leads = self.env['crm.lead'].browse(self.env.context.get('active_ids')) + return leads.action_set_lost(lost_reason=self.lost_reason_id.id) diff --git a/addons/crm/wizard/crm_lead_lost_views.xml b/addons/crm/wizard/crm_lead_lost_views.xml new file mode 100644 index 00000000..2128db8a --- /dev/null +++ b/addons/crm/wizard/crm_lead_lost_views.xml @@ -0,0 +1,27 @@ + + + + crm.lead.lost.form + crm.lead.lost + +
+ + + +
+
+
+
+
+ + + Lost Reason + ir.actions.act_window + crm.lead.lost + form + + new + +
diff --git a/addons/crm/wizard/crm_lead_to_opportunity.py b/addons/crm/wizard/crm_lead_to_opportunity.py new file mode 100644 index 00000000..a3db5d2a --- /dev/null +++ b/addons/crm/wizard/crm_lead_to_opportunity.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models +from odoo.exceptions import UserError +from odoo.tools.translate import _ + + +class Lead2OpportunityPartner(models.TransientModel): + _name = 'crm.lead2opportunity.partner' + _description = 'Convert Lead to Opportunity (not in mass)' + + @api.model + def default_get(self, fields): + + """ Allow support of active_id / active_model instead of jut default_lead_id + to ease window action definitions, and be backward compatible. """ + result = super(Lead2OpportunityPartner, self).default_get(fields) + + if not result.get('lead_id') and self.env.context.get('active_id'): + result['lead_id'] = self.env.context.get('active_id') + return result + + name = fields.Selection([ + ('convert', 'Convert to opportunity'), + ('merge', 'Merge with existing opportunities') + ], 'Conversion Action', compute='_compute_name', readonly=False, store=True, compute_sudo=False) + action = fields.Selection([ + ('create', 'Create a new customer'), + ('exist', 'Link to an existing customer'), + ('nothing', 'Do not link to a customer') + ], string='Related Customer', compute='_compute_action', readonly=False, store=True, compute_sudo=False) + lead_id = fields.Many2one('crm.lead', 'Associated Lead', required=True) + duplicated_lead_ids = fields.Many2many( + 'crm.lead', string='Opportunities', context={'active_test': False}, + compute='_compute_duplicated_lead_ids', readonly=False, store=True, compute_sudo=False) + partner_id = fields.Many2one( + 'res.partner', 'Customer', + compute='_compute_partner_id', readonly=False, store=True, compute_sudo=False) + user_id = fields.Many2one( + 'res.users', 'Salesperson', + compute='_compute_user_id', readonly=False, store=True, compute_sudo=False) + team_id = fields.Many2one( + 'crm.team', 'Sales Team', + compute='_compute_team_id', readonly=False, store=True, compute_sudo=False) + force_assignment = fields.Boolean( + 'Force assignment', default=True, + help='If checked, forces salesman to be updated on updated opportunities even if already set.') + + @api.depends('duplicated_lead_ids') + def _compute_name(self): + for convert in self: + if not convert.name: + convert.name = 'merge' if convert.duplicated_lead_ids and len(convert.duplicated_lead_ids) >= 2 else 'convert' + + @api.depends('lead_id') + def _compute_action(self): + for convert in self: + if not convert.lead_id: + convert.action = 'nothing' + else: + partner = convert.lead_id._find_matching_partner() + if partner: + convert.action = 'exist' + elif convert.lead_id.contact_name: + convert.action = 'create' + else: + convert.action = 'nothing' + + @api.depends('lead_id', 'partner_id') + def _compute_duplicated_lead_ids(self): + for convert in self: + if not convert.lead_id: + convert.duplicated_lead_ids = False + continue + convert.duplicated_lead_ids = self.env['crm.lead']._get_lead_duplicates( + convert.partner_id, + convert.lead_id.partner_id.email if convert.lead_id.partner_id.email else convert.lead_id.email_from, + include_lost=True).ids + + @api.depends('action', 'lead_id') + def _compute_partner_id(self): + for convert in self: + if convert.action == 'exist': + convert.partner_id = convert.lead_id._find_matching_partner() + else: + convert.partner_id = False + + @api.depends('lead_id') + def _compute_user_id(self): + for convert in self: + convert.user_id = convert.lead_id.user_id if convert.lead_id.user_id else False + + @api.depends('user_id') + def _compute_team_id(self): + """ When changing the user, also set a team_id or restrict team id + to the ones user_id is member of. """ + for convert in self: + # setting user as void should not trigger a new team computation + if not convert.user_id: + continue + user = convert.user_id + if convert.team_id and user in convert.team_id.member_ids | convert.team_id.user_id: + continue + team_domain = [] + team = self.env['crm.team']._get_default_team_id(user_id=user.id, domain=team_domain) + convert.team_id = team.id + + @api.model + def view_init(self, fields): + # JEM TDE FIXME: clean that brol + """ Check some preconditions before the wizard executes. """ + for lead in self.env['crm.lead'].browse(self._context.get('active_ids', [])): + if lead.probability == 100: + raise UserError(_("Closed/Dead leads cannot be converted into opportunities.")) + return False + + def action_apply(self): + if self.name == 'merge': + result_opportunity = self._action_merge() + else: + result_opportunity = self._action_convert() + + return result_opportunity.redirect_lead_opportunity_view() + + def _action_merge(self): + to_merge = self.duplicated_lead_ids + result_opportunity = to_merge.merge_opportunity(auto_unlink=False) + result_opportunity.action_unarchive() + + if result_opportunity.type == "lead": + self._convert_and_allocate(result_opportunity, [self.user_id.id], team_id=self.team_id.id) + else: + if not result_opportunity.user_id or self.force_assignment: + result_opportunity.write({ + 'user_id': self.user_id.id, + 'team_id': self.team_id.id, + }) + (to_merge - result_opportunity).unlink() + return result_opportunity + + def _action_convert(self): + """ """ + result_opportunities = self.env['crm.lead'].browse(self._context.get('active_ids', [])) + self._convert_and_allocate(result_opportunities, [self.user_id.id], team_id=self.team_id.id) + return result_opportunities[0] + + def _convert_and_allocate(self, leads, user_ids, team_id=False): + self.ensure_one() + + for lead in leads: + if lead.active and self.action != 'nothing': + self._convert_handle_partner( + lead, self.action, self.partner_id.id or lead.partner_id.id) + + lead.convert_opportunity(lead.partner_id.id, [], False) + + leads_to_allocate = leads + if not self.force_assignment: + leads_to_allocate = leads_to_allocate.filtered(lambda lead: not lead.user_id) + + if user_ids: + leads_to_allocate.handle_salesmen_assignment(user_ids, team_id=team_id) + + def _convert_handle_partner(self, lead, action, partner_id): + # used to propagate user_id (salesman) on created partners during conversion + lead.with_context(default_user_id=self.user_id.id).handle_partner_assignment( + force_partner_id=partner_id, + create_missing=(action == 'create') + ) diff --git a/addons/crm/wizard/crm_lead_to_opportunity_mass.py b/addons/crm/wizard/crm_lead_to_opportunity_mass.py new file mode 100644 index 00000000..9deb2929 --- /dev/null +++ b/addons/crm/wizard/crm_lead_to_opportunity_mass.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class Lead2OpportunityMassConvert(models.TransientModel): + _name = 'crm.lead2opportunity.partner.mass' + _description = 'Convert Lead to Opportunity (in mass)' + _inherit = 'crm.lead2opportunity.partner' + + lead_id = fields.Many2one(required=False) + lead_tomerge_ids = fields.Many2many( + 'crm.lead', 'crm_convert_lead_mass_lead_rel', + string='Active Leads', context={'active_test': False}, + default=lambda self: self.env.context.get('active_ids', []), + ) + user_ids = fields.Many2many('res.users', string='Salespersons') + deduplicate = fields.Boolean('Apply deduplication', default=True, help='Merge with existing leads/opportunities of each partner') + action = fields.Selection(selection_add=[ + ('each_exist_or_create', 'Use existing partner or create'), + ], string='Related Customer', ondelete={ + 'each_exist_or_create': lambda recs: recs.write({'action': 'exist'}), + }) + force_assignment = fields.Boolean(default=False) + + @api.depends('duplicated_lead_ids') + def _compute_name(self): + for convert in self: + convert.name = 'convert' + + @api.depends('lead_tomerge_ids') + def _compute_action(self): + for convert in self: + convert.action = 'each_exist_or_create' + + @api.depends('lead_tomerge_ids') + def _compute_partner_id(self): + for convert in self: + convert.partner_id = False + + @api.depends('user_ids') + def _compute_team_id(self): + """ When changing the user, also set a team_id or restrict team id + to the ones user_id is member of. """ + for convert in self: + # setting user as void should not trigger a new team computation + if not convert.user_id and not convert.user_ids and convert.team_id: + continue + user = convert.user_id or convert.user_ids and convert.user_ids[0] or self.env.user + if convert.team_id and user in convert.team_id.member_ids | convert.team_id.user_id: + continue + team_domain = [] + team = self.env['crm.team']._get_default_team_id(user_id=user.id, domain=team_domain) + convert.team_id = team.id + + @api.depends('lead_tomerge_ids') + def _compute_duplicated_lead_ids(self): + for convert in self: + duplicated = self.env['crm.lead'] + for lead in convert.lead_tomerge_ids: + duplicated_leads = self.env['crm.lead']._get_lead_duplicates( + partner=lead.partner_id, + email=lead.partner_id and lead.partner_id.email or lead.email_from, + include_lost=False) + if len(duplicated_leads) > 1: + duplicated += lead + convert.duplicated_lead_ids = duplicated.ids + + def _convert_and_allocate(self, leads, user_ids, team_id=False): + """ When "massively" (more than one at a time) converting leads to + opportunities, check the salesteam_id and salesmen_ids and update + the values before calling super. + """ + self.ensure_one() + salesmen_ids = [] + if self.user_ids: + salesmen_ids = self.user_ids.ids + return super(Lead2OpportunityMassConvert, self)._convert_and_allocate(leads, salesmen_ids, team_id=team_id) + + def action_mass_convert(self): + self.ensure_one() + if self.name == 'convert' and self.deduplicate: + # TDE CLEANME: still using active_ids from context + active_ids = self._context.get('active_ids', []) + merged_lead_ids = set() + remaining_lead_ids = set() + for lead in self.lead_tomerge_ids: + if lead not in merged_lead_ids: + duplicated_leads = self.env['crm.lead']._get_lead_duplicates( + partner=lead.partner_id, + email=lead.partner_id.email or lead.email_from, + include_lost=False + ) + if len(duplicated_leads) > 1: + lead = duplicated_leads.merge_opportunity() + merged_lead_ids.update(duplicated_leads.ids) + remaining_lead_ids.add(lead.id) + # rebuild list of lead IDS to convert, following given order + final_ids = [lead_id for lead_id in active_ids if lead_id not in merged_lead_ids] + final_ids += [lead_id for lead_id in remaining_lead_ids if lead_id not in final_ids] + + self = self.with_context(active_ids=final_ids) # only update active_ids when there are set + return self.action_apply() + + def _convert_handle_partner(self, lead, action, partner_id): + if self.action == 'each_exist_or_create': + partner_id = lead._find_matching_partner(email_only=True).id + action = 'create' + return super(Lead2OpportunityMassConvert, self)._convert_handle_partner(lead, action, partner_id) diff --git a/addons/crm/wizard/crm_lead_to_opportunity_mass_views.xml b/addons/crm/wizard/crm_lead_to_opportunity_mass_views.xml new file mode 100644 index 00000000..8fc3e1a9 --- /dev/null +++ b/addons/crm/wizard/crm_lead_to_opportunity_mass_views.xml @@ -0,0 +1,63 @@ + + + + crm.lead2opportunity.partner.mass.form + crm.lead2opportunity.partner.mass + +
+ + + + + + + + + + + +
+
+ + + Convert to opportunities + crm.lead2opportunity.partner.mass + form + + new + {} + + list + +
diff --git a/addons/crm/wizard/crm_lead_to_opportunity_views.xml b/addons/crm/wizard/crm_lead_to_opportunity_views.xml new file mode 100644 index 00000000..2a379c19 --- /dev/null +++ b/addons/crm/wizard/crm_lead_to_opportunity_views.xml @@ -0,0 +1,53 @@ + + + + crm.lead2opportunity.partner.form + crm.lead2opportunity.partner + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + Convert to opportunity + ir.actions.act_window + crm.lead2opportunity.partner + form + + new + +
diff --git a/addons/crm/wizard/crm_merge_opportunities.py b/addons/crm/wizard/crm_merge_opportunities.py new file mode 100644 index 00000000..4c95d7db --- /dev/null +++ b/addons/crm/wizard/crm_merge_opportunities.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class MergeOpportunity(models.TransientModel): + """ + Merge opportunities together. + If we're talking about opportunities, it's just because it makes more sense + to merge opps than leads, because the leads are more ephemeral objects. + But since opportunities are leads, it's also possible to merge leads + together (resulting in a new lead), or leads and opps together (resulting + in a new opp). + """ + + _name = 'crm.merge.opportunity' + _description = 'Merge Opportunities' + + @api.model + def default_get(self, fields): + """ Use active_ids from the context to fetch the leads/opps to merge. + In order to get merged, these leads/opps can't be in 'Dead' or 'Closed' + """ + record_ids = self._context.get('active_ids') + result = super(MergeOpportunity, self).default_get(fields) + + if record_ids: + if 'opportunity_ids' in fields: + opp_ids = self.env['crm.lead'].browse(record_ids).filtered(lambda opp: opp.probability < 100).ids + result['opportunity_ids'] = [(6, 0, opp_ids)] + + return result + + opportunity_ids = fields.Many2many('crm.lead', 'merge_opportunity_rel', 'merge_id', 'opportunity_id', string='Leads/Opportunities') + user_id = fields.Many2one('res.users', 'Salesperson', index=True) + team_id = fields.Many2one( + 'crm.team', 'Sales Team', index=True, + compute='_compute_team_id', readonly=False, store=True) + + def action_merge(self): + self.ensure_one() + merge_opportunity = self.opportunity_ids.merge_opportunity(self.user_id.id, self.team_id.id) + return merge_opportunity.redirect_lead_opportunity_view() + + @api.depends('user_id') + def _compute_team_id(self): + """ When changing the user, also set a team_id or restrict team id + to the ones user_id is member of. """ + for wizard in self: + if wizard.user_id: + user_in_team = False + if wizard.team_id: + user_in_team = wizard.env['crm.team'].search_count([('id', '=', wizard.team_id.id), '|', ('user_id', '=', wizard.user_id.id), ('member_ids', '=', wizard.user_id.id)]) + if not user_in_team: + wizard.team_id = wizard.env['crm.team'].search(['|', ('user_id', '=', wizard.user_id.id), ('member_ids', '=', wizard.user_id.id)], limit=1) diff --git a/addons/crm/wizard/crm_merge_opportunities_views.xml b/addons/crm/wizard/crm_merge_opportunities_views.xml new file mode 100644 index 00000000..4d34d43a --- /dev/null +++ b/addons/crm/wizard/crm_merge_opportunities_views.xml @@ -0,0 +1,45 @@ + + + + + crm.merge.opportunity.form + crm.merge.opportunity + +
+ + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + Merge + crm.merge.opportunity + form + new + + list + + +
-- cgit v1.2.3