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,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import crm_lead
from . import crm_iap_lead_helpers
from . import crm_iap_lead_industry
from . import crm_iap_lead_role
from . import crm_iap_lead_seniority
from . import crm_iap_lead_mining_request

View file

@ -0,0 +1,70 @@
from math import floor, log10
from odoo import api, models
class CRMHelpers(models.Model):
_name = 'crm.iap.lead.helpers'
_description = 'Helper methods for crm_iap_mine modules'
@api.model
def notify_no_more_credit(self, service_name, model_name, notification_parameter):
"""
Notify about the number of credit.
In order to avoid to spam people each hour, an ir.config_parameter is set
"""
already_notified = self.env['ir.config_parameter'].sudo().get_param(notification_parameter, False)
if already_notified:
return
mail_template = self.env.ref('crm_iap_mine.lead_generation_no_credits')
iap_account = self.env['iap.account'].search([('service_name', '=', service_name)], limit=1)
# Get the email address of the creators of the records
res = self.env[model_name].search_read([], ['create_uid'])
uids = set(r['create_uid'][0] for r in res if r.get('create_uid'))
res = self.env['res.users'].search_read([('id', 'in', list(uids))], ['email'])
emails = set(r['email'] for r in res if r.get('email'))
email_values = {
'email_to': ','.join(emails)
}
mail_template.send_mail(iap_account.id, force_send=True, email_values=email_values)
self.env['ir.config_parameter'].sudo().set_param(notification_parameter, True)
@api.model
def lead_vals_from_response(self, lead_type, team_id, tag_ids, user_id, company_data, people_data):
country_id = self.env['res.country'].search([('code', '=', company_data['country_code'])]).id
website_url = 'https://www.%s' % company_data['domain'] if company_data['domain'] else False
lead_vals = {
# Lead vals from record itself
'type': lead_type,
'team_id': team_id,
'tag_ids': [(6, 0, tag_ids)],
'user_id': user_id,
'reveal_id': company_data['clearbit_id'],
# Lead vals from data
'name': company_data['name'] or company_data['domain'],
'partner_name': company_data['legal_name'] or company_data['name'],
'email_from': next(iter(company_data.get('email', [])), ''),
'phone': company_data['phone'] or (company_data['phone_numbers'] and company_data['phone_numbers'][0]) or '',
'website': website_url,
'street': company_data['location'],
'city': company_data['city'],
'zip': company_data['postal_code'],
'country_id': country_id,
'state_id': self._find_state_id(company_data['state_code'], country_id),
}
# If type is people then add first contact in lead data
if people_data:
lead_vals.update({
'contact_name': people_data[0]['full_name'],
'email_from': people_data[0]['email'],
'function': people_data[0]['title'],
})
return lead_vals
@api.model
def _find_state_id(self, state_code, country_id):
state_id = self.env['res.country.state'].search([('code', '=', state_code), ('country_id', '=', country_id)])
if state_id:
return state_id.id
return False

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class CrmIapLeadIndustry(models.Model):
""" Industry Tags of Acquisition Rules """
_name = 'crm.iap.lead.industry'
_description = 'CRM IAP Lead Industry'
_order = 'sequence,id'
name = fields.Char(string='Industry', required=True, translate=True)
reveal_ids = fields.Char(required=True) # The list of reveal_ids for this industry, separated with ','
color = fields.Integer(string='Color Index')
sequence = fields.Integer('Sequence')
_sql_constraints = [
('name_uniq', 'unique (name)', 'Industry name already exists!'),
]

View file

@ -0,0 +1,338 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from odoo import api, fields, models, _
from odoo.addons.iap.tools import iap_tools
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
DEFAULT_ENDPOINT = 'https://iap-services.odoo.com'
MAX_LEAD = 200
MAX_CONTACT = 5
CREDIT_PER_COMPANY = 1
CREDIT_PER_CONTACT = 1
class CRMLeadMiningRequest(models.Model):
_name = 'crm.iap.lead.mining.request'
_description = 'CRM Lead Mining Request'
def _default_lead_type(self):
if self.env.user.has_group('crm.group_use_lead'):
return 'lead'
else:
return 'opportunity'
def _default_country_ids(self):
return self.env.user.company_id.country_id
name = fields.Char(string='Request Number', required=True, readonly=True, default=lambda self: _('New'), copy=False)
state = fields.Selection([('draft', 'Draft'), ('error', 'Error'), ('done', 'Done')], string='Status', required=True, default='draft')
# Request Data
lead_number = fields.Integer(string='Number of Leads', required=True, default=3)
search_type = fields.Selection([('companies', 'Companies'), ('people', 'Companies and their Contacts')], string='Target', required=True, default='companies')
error_type = fields.Selection([
('credits', 'Insufficient Credits'),
('no_result', 'No Result'),
], string='Error Type', copy=False, readonly=True)
# Lead / Opportunity Data
lead_type = fields.Selection([('lead', 'Leads'), ('opportunity', 'Opportunities')], string='Type', required=True, default=_default_lead_type)
display_lead_label = fields.Char(compute='_compute_display_lead_label')
team_id = fields.Many2one(
'crm.team', string='Sales Team', ondelete="set null",
domain="[('use_opportunities', '=', True)]", readonly=False, compute='_compute_team_id', store=True)
user_id = fields.Many2one('res.users', string='Salesperson', default=lambda self: self.env.user)
tag_ids = fields.Many2many('crm.tag', string='Tags')
lead_ids = fields.One2many('crm.lead', 'lead_mining_request_id', string='Generated Lead / Opportunity')
lead_count = fields.Integer(compute='_compute_lead_count', string='Number of Generated Leads')
# Company Criteria Filter
filter_on_size = fields.Boolean(string='Filter on Size', default=False)
company_size_min = fields.Integer(string='Size', default=1)
company_size_max = fields.Integer(default=1000)
country_ids = fields.Many2many('res.country', string='Countries', default=_default_country_ids)
state_ids = fields.Many2many('res.country.state', string='States')
available_state_ids = fields.One2many('res.country.state', compute='_compute_available_state_ids')
industry_ids = fields.Many2many('crm.iap.lead.industry', string='Industries')
# Contact Generation Filter
contact_number = fields.Integer(string='Number of Contacts', default=10)
contact_filter_type = fields.Selection([('role', 'Role'), ('seniority', 'Seniority')], string='Filter on', default='role')
preferred_role_id = fields.Many2one('crm.iap.lead.role', string='Preferred Role')
role_ids = fields.Many2many('crm.iap.lead.role', string='Other Roles')
seniority_id = fields.Many2one('crm.iap.lead.seniority', string='Seniority')
# Fields for the blue tooltip
lead_credits = fields.Char(compute='_compute_tooltip', readonly=True)
lead_contacts_credits = fields.Char(compute='_compute_tooltip', readonly=True)
lead_total_credits = fields.Char(compute='_compute_tooltip', readonly=True)
@api.depends('lead_type', 'lead_number')
def _compute_display_lead_label(self):
selection_description_values = {
e[0]: e[1] for e in self._fields['lead_type']._description_selection(self.env)}
for request in self:
lead_type = selection_description_values[request.lead_type]
request.display_lead_label = '%s %s' % (request.lead_number, lead_type)
@api.onchange('lead_number', 'contact_number')
def _compute_tooltip(self):
for record in self:
company_credits = CREDIT_PER_COMPANY * record.lead_number
contact_credits = CREDIT_PER_CONTACT * record.contact_number
total_contact_credits = contact_credits * record.lead_number
record.lead_contacts_credits = _("Up to %d additional credits will be consumed to identify %d contacts per company.") % (contact_credits*company_credits, record.contact_number)
record.lead_credits = _('%d credits will be consumed to find %d companies.') % (company_credits, record.lead_number)
record.lead_total_credits = _("This makes a total of %d credits for this request.") % (total_contact_credits + company_credits)
@api.depends('lead_ids.lead_mining_request_id')
def _compute_lead_count(self):
if self.ids:
leads_data = self.env['crm.lead']._read_group(
[('lead_mining_request_id', 'in', self.ids)],
['lead_mining_request_id'], ['lead_mining_request_id'])
else:
leads_data = []
mapped_data = dict(
(m['lead_mining_request_id'][0], m['lead_mining_request_id_count'])
for m in leads_data)
for request in self:
request.lead_count = mapped_data.get(request.id, 0)
@api.depends('user_id', 'lead_type')
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 mining in self:
# setting user as void should not trigger a new team computation
if not mining.user_id:
continue
user = mining.user_id
if mining.team_id and user in mining.team_id.member_ids | mining.team_id.user_id:
continue
team_domain = [('use_leads', '=', True)] if mining.lead_type == 'lead' else [('use_opportunities', '=', True)]
team = self.env['crm.team']._get_default_team_id(user_id=user.id, domain=team_domain)
mining.team_id = team.id
@api.depends('country_ids')
def _compute_available_state_ids(self):
""" States for some specific countries should not be offered as filtering options because
they drastically reduce the amount of IAP reveal results.
For example, in Belgium, only 11% of companies have a defined state within the
reveal service while the rest of them have no state defined at all.
Meaning specifying states for that country will yield a lot less results than what you could
expect, which is not the desired behavior.
Obviously all companies are active within a state, it's just a lack of data in the reveal
service side.
To help users create meaningful iap searches, we only keep the states filtering for several
whitelisted countries (based on their country code).
The complete list and reasons for this change can be found on task-2471703. """
for lead_mining_request in self:
countries = lead_mining_request.country_ids.filtered(lambda country:
country.code in iap_tools._STATES_FILTER_COUNTRIES_WHITELIST)
lead_mining_request.available_state_ids = self.env['res.country.state'].search([
('country_id', 'in', countries.ids)
])
@api.onchange('available_state_ids')
def _onchange_available_state_ids(self):
self.state_ids -= self.state_ids.filtered(
lambda state: (state._origin.id or state.id) not in self.available_state_ids.ids
)
@api.onchange('lead_number')
def _onchange_lead_number(self):
if self.lead_number <= 0:
self.lead_number = 1
elif self.lead_number > MAX_LEAD:
self.lead_number = MAX_LEAD
@api.onchange('contact_number')
def _onchange_contact_number(self):
if self.contact_number <= 0:
self.contact_number = 1
elif self.contact_number > MAX_CONTACT:
self.contact_number = MAX_CONTACT
@api.onchange('country_ids')
def _onchange_country_ids(self):
self.state_ids = []
@api.onchange('company_size_min')
def _onchange_company_size_min(self):
if self.company_size_min <= 0:
self.company_size_min = 1
elif self.company_size_min > self.company_size_max:
self.company_size_min = self.company_size_max
@api.onchange('company_size_max')
def _onchange_company_size_max(self):
if self.company_size_max < self.company_size_min:
self.company_size_max = self.company_size_min
@api.model
def get_empty_list_help(self, help_string):
help_title = _('Create a Lead Mining Request')
sub_title = _('Generate new leads based on their country, industry, size, etc.')
return '<p class="o_view_nocontent_smiling_face">%s</p><p class="oe_view_nocontent_alias">%s</p>' % (help_title, sub_title)
def _prepare_iap_payload(self):
"""
This will prepare the data to send to the server
"""
self.ensure_one()
payload = {'lead_number': self.lead_number,
'search_type': self.search_type,
'countries': self.country_ids.mapped('code')}
if self.state_ids:
payload['states'] = self.state_ids.mapped('code')
if self.filter_on_size:
payload.update({'company_size_min': self.company_size_min,
'company_size_max': self.company_size_max})
if self.industry_ids:
# accumulate all reveal_ids (separated by ',') into one list
# eg: 3 records with values: "175,176", "177" and "190,191"
# will become ['175','176','177','190','191']
all_industry_ids = [
reveal_id.strip()
for reveal_ids in self.mapped('industry_ids.reveal_ids')
for reveal_id in reveal_ids.split(',')
]
payload['industry_ids'] = all_industry_ids
if self.search_type == 'people':
payload.update({'contact_number': self.contact_number,
'contact_filter_type': self.contact_filter_type})
if self.contact_filter_type == 'role':
payload.update({'preferred_role': self.preferred_role_id.reveal_id,
'other_roles': self.role_ids.mapped('reveal_id')})
elif self.contact_filter_type == 'seniority':
payload['seniority'] = self.seniority_id.reveal_id
return payload
def _perform_request(self):
"""
This will perform the request and create the corresponding leads.
The user will be notified if they don't have enough credits.
"""
self.error_type = False
server_payload = self._prepare_iap_payload()
reveal_account = self.env['iap.account'].get('reveal')
dbuuid = self.env['ir.config_parameter'].sudo().get_param('database.uuid')
params = {
'account_token': reveal_account.account_token,
'dbuuid': dbuuid,
'data': server_payload
}
try:
response = self._iap_contact_mining(params, timeout=300)
if not response.get('data'):
self.error_type = 'no_result'
return False
return response['data']
except iap_tools.InsufficientCreditError as e:
self.error_type = 'credits'
self.state = 'error'
return False
except Exception as e:
raise UserError(_("Your request could not be executed: %s", e))
def _iap_contact_mining(self, params, timeout=300):
endpoint = self.env['ir.config_parameter'].sudo().get_param('reveal.endpoint', DEFAULT_ENDPOINT) + '/iap/clearbit/1/lead_mining_request'
return iap_tools.iap_jsonrpc(endpoint, params=params, timeout=timeout)
def _create_leads_from_response(self, result):
""" This method will get the response from the service and create the leads accordingly """
self.ensure_one()
lead_vals_list = []
messages_to_post = {}
for data in result:
lead_vals_list.append(self._lead_vals_from_response(data))
template_values = data['company_data']
template_values.update({
'flavor_text': _("Opportunity created by Odoo Lead Generation"),
'people_data': data.get('people_data'),
})
messages_to_post[data['company_data']['clearbit_id']] = template_values
leads = self.env['crm.lead'].create(lead_vals_list)
for lead in leads:
if messages_to_post.get(lead.reveal_id):
lead.message_post_with_view('iap_mail.enrich_company', values=messages_to_post[lead.reveal_id], subtype_id=self.env.ref('mail.mt_note').id)
# Methods responsible for format response data into valid odoo lead data
@api.model
def _lead_vals_from_response(self, data):
self.ensure_one()
company_data = data.get('company_data')
people_data = data.get('people_data')
lead_vals = self.env['crm.iap.lead.helpers'].lead_vals_from_response(self.lead_type, self.team_id.id, self.tag_ids.ids, self.user_id.id, company_data, people_data)
lead_vals['lead_mining_request_id'] = self.id
return lead_vals
def action_draft(self):
self.ensure_one()
self.name = _('New')
self.state = 'draft'
def action_submit(self):
self.ensure_one()
if self.name == _('New'):
self.name = self.env['ir.sequence'].next_by_code('crm.iap.lead.mining.request') or _('New')
results = self._perform_request()
if results:
self._create_leads_from_response(results)
self.state = 'done'
if self.lead_type == 'lead':
return self.action_get_lead_action()
elif self.lead_type == 'opportunity':
return self.action_get_opportunity_action()
elif self.env.context.get('is_modal'):
# when we are inside a modal already, we re-open the same record
# that way, the form view is updated and the correct error message appears
# (sadly, there is no way to simply 'reload' a form view within a modal)
return {
'name': _('Generate Leads'),
'res_model': 'crm.iap.lead.mining.request',
'views': [[False, 'form']],
'target': 'new',
'type': 'ir.actions.act_window',
'res_id': self.id,
'context': dict(self.env.context, edit=True, form_view_initial_mode='edit')
}
else:
# will reload the form view and show the error message on top
return False
def action_get_lead_action(self):
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id("crm.crm_lead_all_leads")
action['domain'] = [('id', 'in', self.lead_ids.ids), ('type', '=', 'lead')]
return action
def action_get_opportunity_action(self):
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id("crm.crm_lead_opportunities")
action['domain'] = [('id', 'in', self.lead_ids.ids), ('type', '=', 'opportunity')]
return action
def action_buy_credits(self):
return {
'type': 'ir.actions.act_url',
'url': self.env['iap.account'].get_credits_url(service_name='reveal'),
}

View file

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class PeopleRole(models.Model):
""" CRM Reveal People Roles for People """
_name = 'crm.iap.lead.role'
_description = 'People Role'
name = fields.Char(string='Role Name', required=True, translate=True)
reveal_id = fields.Char(required=True)
color = fields.Integer(string='Color Index')
_sql_constraints = [
('name_uniq', 'unique (name)', 'Role name already exists!'),
]
@api.depends('name')
def name_get(self):
return [(role.id, role.name.replace('_', ' ').title()) for role in self]

View file

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class PeopleSeniority(models.Model):
""" Seniority for People Rules """
_name = 'crm.iap.lead.seniority'
_description = 'People Seniority'
name = fields.Char(string='Name', required=True, translate=True)
reveal_id = fields.Char(required=True)
_sql_constraints = [
('name_uniq', 'unique (name)', 'Name already exists!'),
]
@api.depends('name')
def name_get(self):
return [(seniority.id, seniority.name.replace('_', ' ').title()) for seniority in self]

View file

@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class Lead(models.Model):
_inherit = 'crm.lead'
lead_mining_request_id = fields.Many2one('crm.iap.lead.mining.request', string='Lead Mining Request', index='btree_not_null')
def _merge_get_fields(self):
return super(Lead, self)._merge_get_fields() + ['lead_mining_request_id']