Initial commit: Crm packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:49 +02:00
commit 21a345b5b9
654 changed files with 418312 additions and 0 deletions

View file

@ -0,0 +1,8 @@
# -*- 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
from . import crm_lead_pls_update

View file

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, _
from odoo.tools.mail import is_html_empty
class CrmLeadLost(models.TransientModel):
_name = 'crm.lead.lost'
_description = 'Get Lost Reason'
lost_reason_id = fields.Many2one('crm.lost.reason', 'Lost Reason')
lost_feedback = fields.Html(
'Closing Note', sanitize=True
)
def action_lost_reason_apply(self):
self.ensure_one()
leads = self.env['crm.lead'].browse(self.env.context.get('active_ids'))
if not is_html_empty(self.lost_feedback):
leads._track_set_log_message(
'<div style="margin-bottom: 4px;"><p>%s:</p>%s<br /></div>' % (
_('Lost Comment'),
self.lost_feedback
)
)
res = leads.action_set_lost(lost_reason_id=self.lost_reason_id.id)
return res

View file

@ -0,0 +1,31 @@
<?xml version="1.0"?>
<odoo>
<record id="crm_lead_lost_view_form" model="ir.ui.view">
<field name="name">crm.lead.lost.form</field>
<field name="model">crm.lead.lost</field>
<field name="arch" type="xml">
<form string="Lost Reason">
<group>
<field name="lost_reason_id" options="{'no_create_edit': True}" />
</group>
<field name="lost_feedback" placeholder="What went wrong ?"/>
<footer>
<button name="action_lost_reason_apply" string="Submit" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
</footer>
</form>
</field>
</record>
<record id="crm_lead_lost_action" model="ir.actions.act_window">
<field name="name">Lost Reason</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">crm.lead.lost</field>
<field name="view_mode">form</field>
<field name="view_id" ref="crm_lead_lost_view_form"/>
<field name="target">new</field>
<field name="context">{
'dialog_size' : 'medium',
}</field>
</record>
</odoo>

View file

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class CrmUpdateProbabilities(models.TransientModel):
_name = 'crm.lead.pls.update'
_description = "Update the probabilities"
def _get_default_pls_start_date(self):
pls_start_date_config = self.env['ir.config_parameter'].sudo().get_param('crm.pls_start_date')
return fields.Date.to_date(pls_start_date_config)
def _get_default_pls_fields(self):
pls_fields_config = self.env['ir.config_parameter'].sudo().get_param('crm.pls_fields')
if pls_fields_config:
names = pls_fields_config.split(',')
fields = self.env['ir.model.fields'].search([('name', 'in', names), ('model', '=', 'crm.lead')])
return self.env['crm.lead.scoring.frequency.field'].search([('field_id', 'in', fields.ids)])
else:
return None
pls_start_date = fields.Date(required=True, default=_get_default_pls_start_date)
pls_fields = fields.Many2many('crm.lead.scoring.frequency.field', default=_get_default_pls_fields)
def action_update_crm_lead_probabilities(self):
if self.env.user._is_admin():
set_param = self.env['ir.config_parameter'].sudo().set_param
if self.pls_fields:
pls_fields_str = ','.join(self.pls_fields.mapped('field_id.name'))
set_param('crm.pls_fields', pls_fields_str)
else:
set_param('crm.pls_fields', "")
set_param('crm.pls_start_date', str(self.pls_start_date))
self.env['crm.lead'].sudo()._cron_update_automated_probabilities()

View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="crm_lead_pls_update_view_form" model="ir.ui.view">
<field name="name">crm.lead.pls.update.view.form</field>
<field name="model">crm.lead.pls.update</field>
<field name="arch" type="xml">
<form>
<p>
The success rate is computed based on the stage, but you can add more fields in the statistical analysis.
</p>
<p>
<field name="pls_fields" widget="many2many_tags" placeholder="Extra fields..."/>
</p>
<p>
Consider leads created as of the: <field name="pls_start_date"/>
</p>
<footer>
<button name="action_update_crm_lead_probabilities" type="object"
string="Confirm" class="btn-primary" data-hotkey="q"/>
<button special="cancel" data-hotkey="z" string="Cancel"/>
</footer>
</form>
</field>
</record>
<record id="crm_lead_pls_update_action" model="ir.actions.act_window">
<field name="name">Update Probabilities</field>
<field name="res_model">crm.lead.pls.update</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="view_id" ref="crm_lead_pls_update_view_form"/>
</record>
</odoo>

View file

@ -0,0 +1,167 @@
# -*- 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')
if result.get('lead_id'):
if self.env['crm.lead'].browse(result['lead_id']).probability == 100:
raise UserError(_("Closed/Dead leads cannot be converted into opportunities."))
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 = self.env['crm.team']._get_default_team_id(user_id=user.id, domain=None)
convert.team_id = team.id
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,
})
if self.lead_id != result_opportunity:
# Prevent unwanted cascade during unlinks, keeping other operations and overrides possible
self.write({'lead_id': result_opportunity})
(to_merge - result_opportunity).sudo().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, user_ids=False, team_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')
)

View file

@ -0,0 +1,109 @@
# -*- 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 = self.env['crm.team']._get_default_team_id(user_id=user.id, domain=None)
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)

View file

@ -0,0 +1,63 @@
<?xml version="1.0"?>
<odoo>
<record id="view_crm_lead2opportunity_partner_mass" model="ir.ui.view">
<field name="name">crm.lead2opportunity.partner.mass.form</field>
<field name="model">crm.lead2opportunity.partner.mass</field>
<field name="arch" type="xml">
<form string="Convert to Opportunity">
<field name="lead_tomerge_ids" invisible="1"/>
<separator string="Conversion Options"/>
<group>
<field name="name" class="oe_inline" widget="radio"/>
<field name="deduplicate" class="oe_inline"/>
</group>
<group string="Assign these opportunities to">
<field name="team_id" kanban_view_ref="%(sales_team.crm_team_view_kanban)s"/>
<field name="user_ids" widget="many2many_tags" domain="[('share', '=', False)]"/>
<field name="force_assignment"/>
</group>
<label for="duplicated_lead_ids" string="Leads with existing duplicates (for information)" help="Leads that you selected that have duplicates. If the list is empty, it means that no duplicates were found" attrs="{'invisible': [('deduplicate', '=', False)]}"/>
<group attrs="{'invisible': [('deduplicate', '=', False)]}">
<field name="duplicated_lead_ids" colspan="4" nolabel="1" readonly="1">
<tree create="false" delete="false">
<field name="create_date" widget="date"/>
<field name="name"/>
<field name="type"/>
<field name="contact_name"/>
<field name="country_id" invisible="context.get('invisible_country', True)" options="{'no_open': True, 'no_create': True}"/>
<field name="email_from"/>
<field name="stage_id"/>
<field name="user_id"/>
<field name="team_id"/>
</tree>
</field>
</group>
<group attrs="{'invisible': [('name', '!=', 'convert')]}" string="Customers" col="1">
<field name="action" class="oe_inline" widget="radio"/>
<group col="2">
<field name="partner_id"
widget="res_partner_many2one"
attrs="{'required': [('action', '=', 'exist')], 'invisible':[('action','!=','exist')]}"
context="{'show_vat': True}"
class="oe_inline"/>
</group>
</group>
<footer>
<button string="Convert to Opportunities" name="action_mass_convert" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
</footer>
</form>
</field>
</record>
<record id="action_crm_send_mass_convert" model="ir.actions.act_window">
<field name="name">Convert to opportunities</field>
<field name="res_model">crm.lead2opportunity.partner.mass</field>
<field name="view_mode">form</field>
<field name="view_id" ref="view_crm_lead2opportunity_partner_mass"/>
<field name="target">new</field>
<field name="context">{}</field>
<field name="binding_model_id" ref="model_crm_lead"/>
<field name="binding_view_types">list</field>
</record>
</odoo>

View file

@ -0,0 +1,53 @@
<?xml version="1.0"?>
<odoo>
<record id="view_crm_lead2opportunity_partner" model="ir.ui.view">
<field name="name">crm.lead2opportunity.partner.form</field>
<field name="model">crm.lead2opportunity.partner</field>
<field name="arch" type="xml">
<form string="Convert to Opportunity">
<group name="name">
<field name="name" widget="radio"/>
</group>
<group string="Assign this opportunity to">
<field name="user_id" domain="[('share', '=', False)]"/>
<field name="team_id" options="{'no_open': True, 'no_create': True}" kanban_view_ref="%(sales_team.crm_team_view_kanban)s"/>
</group>
<group string="Opportunities" attrs="{'invisible': [('name', '!=', 'merge')]}">
<field name="lead_id" invisible="1"/>
<field name="duplicated_lead_ids" colspan="2" nolabel="1">
<tree>
<field name="create_date" widget="date"/>
<field name="name"/>
<field name="type"/>
<field name="contact_name"/>
<field name="country_id" invisible="context.get('invisible_country', True)" options="{'no_open': True, 'no_create': True}"/>
<field name="email_from"/>
<field name="stage_id"/>
<field name="user_id"/>
<field name="team_id" kanban_view_ref="%(sales_team.crm_team_view_kanban)s"/>
</tree>
</field>
</group>
<group name="action" attrs="{'invisible': [('name', '!=', 'convert')]}" string="Customer" col="1">
<field name="action" nolabel="1" widget="radio"/>
<group col="2">
<field name="partner_id" widget="res_partner_many2one" context="{'res_partner_search_mode': 'customer', 'show_vat': True}" attrs="{'required': [('action', '=', 'exist')], 'invisible':[('action','!=','exist')]}"/>
</group>
</group>
<footer>
<button name="action_apply" string="Create Opportunity" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
</footer>
</form>
</field>
</record>
<record id="action_crm_lead2opportunity_partner" model="ir.actions.act_window">
<field name="name">Convert to opportunity</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">crm.lead2opportunity.partner</field>
<field name="view_mode">form</field>
<field name="view_id" ref="view_crm_lead2opportunity_partner"/>
<field name="target">new</field>
</record>
</odoo>

View file

@ -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', domain="[('share', '=', False)]")
team_id = fields.Many2one(
'crm.team', 'Sales Team',
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)

View file

@ -0,0 +1,45 @@
<?xml version="1.0"?>
<odoo>
<!-- Merge Opportunities -->
<record id="merge_opportunity_form" model="ir.ui.view">
<field name="name">crm.merge.opportunity.form</field>
<field name="model">crm.merge.opportunity</field>
<field name="arch" type="xml">
<form string="Merge Leads/Opportunities">
<group string="Assign opportunities to">
<field name="user_id" class="oe_inline"/>
<field name="team_id" class="oe_inline" kanban_view_ref="%(sales_team.crm_team_view_kanban)s"/>
</group>
<group string="Select Leads/Opportunities">
<field name="opportunity_ids" nolabel="1">
<tree>
<field name="create_date"/>
<field name="name"/>
<field name="type"/>
<field name="contact_name"/>
<field name="email_from" optional="hide"/>
<field name="phone" class="o_force_ltr" optional="hide"/>
<field name="stage_id"/>
<field name="user_id"/>
<field name="team_id"/>
</tree>
</field>
</group>
<footer>
<button name="action_merge" type="object" string="Merge" class="btn-primary" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
</footer>
</form>
</field>
</record>
<record id="action_merge_opportunities" model="ir.actions.act_window">
<field name="name">Merge</field>
<field name="res_model">crm.merge.opportunity</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="binding_model_id" ref="model_crm_lead"/>
<field name="binding_view_types">list</field>
</record>
</odoo>