mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-21 07:32:04 +02:00
Initial commit: Core packages
This commit is contained in:
commit
12c29a983b
9512 changed files with 8379910 additions and 0 deletions
10
odoo-bringout-oca-ocb-utm/utm/models/__init__.py
Normal file
10
odoo-bringout-oca-ocb-utm/utm/models/__init__.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import utm_campaign
|
||||
from . import utm_medium
|
||||
from . import utm_mixin
|
||||
from . import utm_source
|
||||
from . import utm_stage
|
||||
from . import utm_tag
|
||||
from . import ir_http
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
26
odoo-bringout-oca-ocb-utm/utm/models/ir_http.py
Normal file
26
odoo-bringout-oca-ocb-utm/utm/models/ir_http.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
from odoo.http import request, Response
|
||||
|
||||
|
||||
class IrHttp(models.AbstractModel):
|
||||
_inherit = 'ir.http'
|
||||
|
||||
@classmethod
|
||||
def get_utm_domain_cookies(cls):
|
||||
return request.httprequest.host
|
||||
|
||||
@classmethod
|
||||
def _set_utm(cls, response):
|
||||
# Make sure response is an odoo Response.
|
||||
response = Response.load(response)
|
||||
domain = cls.get_utm_domain_cookies()
|
||||
for url_parameter, __, cookie_name in request.env['utm.mixin'].tracking_fields():
|
||||
if url_parameter in request.params and request.httprequest.cookies.get(cookie_name) != request.params[url_parameter]:
|
||||
response.set_cookie(cookie_name, request.params[url_parameter], max_age=31 * 24 * 3600, domain=domain, cookie_type='optional')
|
||||
|
||||
@classmethod
|
||||
def _post_dispatch(cls, response):
|
||||
cls._set_utm(response)
|
||||
super()._post_dispatch(response)
|
||||
59
odoo-bringout-oca-ocb-utm/utm/models/utm_campaign.py
Normal file
59
odoo-bringout-oca-ocb-utm/utm/models/utm_campaign.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, api, SUPERUSER_ID
|
||||
|
||||
|
||||
class UtmCampaign(models.Model):
|
||||
_name = 'utm.campaign'
|
||||
_description = 'UTM Campaign'
|
||||
_rec_name = 'title'
|
||||
|
||||
name = fields.Char(string='Campaign Identifier', required=True, compute='_compute_name',
|
||||
store=True, readonly=False, precompute=True, translate=False)
|
||||
title = fields.Char(string='Campaign Name', required=True, translate=True)
|
||||
|
||||
user_id = fields.Many2one(
|
||||
'res.users', string='Responsible',
|
||||
required=True, default=lambda self: self.env.uid)
|
||||
stage_id = fields.Many2one(
|
||||
'utm.stage', string='Stage', ondelete='restrict', required=True,
|
||||
default=lambda self: self.env['utm.stage'].search([], limit=1),
|
||||
group_expand='_group_expand_stage_ids')
|
||||
tag_ids = fields.Many2many(
|
||||
'utm.tag', 'utm_tag_rel',
|
||||
'tag_id', 'campaign_id', string='Tags')
|
||||
|
||||
is_auto_campaign = fields.Boolean(default=False, string="Automatically Generated Campaign", help="Allows us to filter relevant Campaigns")
|
||||
color = fields.Integer(string='Color Index')
|
||||
|
||||
_sql_constraints = [
|
||||
('unique_name', 'UNIQUE(name)', 'The name must be unique'),
|
||||
]
|
||||
|
||||
@api.depends('title')
|
||||
def _compute_name(self):
|
||||
new_names = self.env['utm.mixin'].with_context(
|
||||
utm_check_skip_record_ids=self.ids
|
||||
)._get_unique_names(self._name, [c.title for c in self])
|
||||
for campaign, new_name in zip(self, new_names):
|
||||
campaign.name = new_name
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if not vals.get('title') and vals.get('name'):
|
||||
vals['title'] = vals['name']
|
||||
new_names = self.env['utm.mixin']._get_unique_names(self._name, [vals.get('name') for vals in vals_list])
|
||||
for vals, new_name in zip(vals_list, new_names):
|
||||
if new_name:
|
||||
vals['name'] = new_name
|
||||
return super().create(vals_list)
|
||||
|
||||
@api.model
|
||||
def _group_expand_stage_ids(self, stages, domain, order):
|
||||
"""Read group customization in order to display all the stages in the
|
||||
Kanban view, even if they are empty.
|
||||
"""
|
||||
stage_ids = stages._search([], order=order, access_rights_uid=SUPERUSER_ID)
|
||||
return stages.browse(stage_ids)
|
||||
36
odoo-bringout-oca-ocb-utm/utm/models/utm_medium.py
Normal file
36
odoo-bringout-oca-ocb-utm/utm/models/utm_medium.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# -*- 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
|
||||
|
||||
|
||||
class UtmMedium(models.Model):
|
||||
_name = 'utm.medium'
|
||||
_description = 'UTM Medium'
|
||||
_order = 'name'
|
||||
|
||||
name = fields.Char(string='Medium Name', required=True, translate=False)
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
_sql_constraints = [
|
||||
('unique_name', 'UNIQUE(name)', 'The name must be unique'),
|
||||
]
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
new_names = self.env['utm.mixin']._get_unique_names(self._name, [vals.get('name') for vals in vals_list])
|
||||
for vals, new_name in zip(vals_list, new_names):
|
||||
vals['name'] = new_name
|
||||
return super().create(vals_list)
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_utm_medium_email(self):
|
||||
utm_medium_email = self.env.ref('utm.utm_medium_email', raise_if_not_found=False)
|
||||
if utm_medium_email and utm_medium_email in self:
|
||||
raise UserError(_(
|
||||
"The UTM medium '%s' cannot be deleted as it is used in some main "
|
||||
"functional flows, such as the recruitment and the mass mailing.",
|
||||
utm_medium_email.name
|
||||
))
|
||||
155
odoo-bringout-oca-ocb-utm/utm/models/utm_mixin.py
Normal file
155
odoo-bringout-oca-ocb-utm/utm/models/utm_mixin.py
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import re
|
||||
from collections import defaultdict
|
||||
import itertools
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.http import request
|
||||
from odoo.osv import expression
|
||||
|
||||
|
||||
class UtmMixin(models.AbstractModel):
|
||||
""" Mixin class for objects which can be tracked by marketing. """
|
||||
_name = 'utm.mixin'
|
||||
_description = 'UTM Mixin'
|
||||
|
||||
campaign_id = fields.Many2one('utm.campaign', 'Campaign',
|
||||
help="This is a name that helps you keep track of your different campaign efforts, e.g. Fall_Drive, Christmas_Special")
|
||||
source_id = fields.Many2one('utm.source', 'Source',
|
||||
help="This is the source of the link, e.g. Search Engine, another domain, or name of email list")
|
||||
medium_id = fields.Many2one('utm.medium', 'Medium',
|
||||
help="This is the method of delivery, e.g. Postcard, Email, or Banner Ad")
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
values = super(UtmMixin, self).default_get(fields)
|
||||
|
||||
# We ignore UTM for salesmen, except some requests that could be done as superuser_id to bypass access rights.
|
||||
if not self.env.is_superuser() and self.env.user.has_group('sales_team.group_sale_salesman'):
|
||||
return values
|
||||
|
||||
for url_param, field_name, cookie_name in self.env['utm.mixin'].tracking_fields():
|
||||
if field_name in fields:
|
||||
field = self._fields[field_name]
|
||||
value = False
|
||||
if request:
|
||||
# ir_http dispatch saves the url params in a cookie
|
||||
value = request.httprequest.cookies.get(cookie_name)
|
||||
# if we receive a string for a many2one, we search/create the id
|
||||
if field.type == 'many2one' and isinstance(value, str) and value:
|
||||
record = self._find_or_create_record(field.comodel_name, value)
|
||||
value = record.id
|
||||
if value:
|
||||
values[field_name] = value
|
||||
return values
|
||||
|
||||
def tracking_fields(self):
|
||||
# This function cannot be overridden in a model which inherit utm.mixin
|
||||
# Limitation by the heritage on AbstractModel
|
||||
# record_crm_lead.tracking_fields() will call tracking_fields() from module utm.mixin (if not overridden on crm.lead)
|
||||
# instead of the overridden method from utm.mixin.
|
||||
# To force the call of overridden method, we use self.env['utm.mixin'].tracking_fields() which respects overridden
|
||||
# methods of utm.mixin, but will ignore overridden method on crm.lead
|
||||
return [
|
||||
# ("URL_PARAMETER", "FIELD_NAME_MIXIN", "NAME_IN_COOKIES")
|
||||
('utm_campaign', 'campaign_id', 'odoo_utm_campaign'),
|
||||
('utm_source', 'source_id', 'odoo_utm_source'),
|
||||
('utm_medium', 'medium_id', 'odoo_utm_medium'),
|
||||
]
|
||||
|
||||
def _find_or_create_record(self, model_name, name):
|
||||
"""Based on the model name and on the name of the record, retrieve the corresponding record or create it."""
|
||||
Model = self.env[model_name]
|
||||
|
||||
record = Model.search([('name', '=', name)], limit=1)
|
||||
|
||||
if not record:
|
||||
# No record found, create a new one
|
||||
record_values = {'name': name}
|
||||
if 'is_auto_campaign' in record._fields:
|
||||
record_values['is_auto_campaign'] = True
|
||||
record = Model.create(record_values)
|
||||
|
||||
return record
|
||||
|
||||
@api.model
|
||||
def _get_unique_names(self, model_name, names):
|
||||
"""Generate unique names for the given model.
|
||||
|
||||
Take a list of names and return for each names, the new names to set
|
||||
in the same order (with a counter added if needed).
|
||||
|
||||
E.G.
|
||||
The name "test" already exists in database
|
||||
Input: ['test', 'test [3]', 'bob', 'test', 'test']
|
||||
Output: ['test [2]', 'test [3]', 'bob', 'test [4]', 'test [5]']
|
||||
|
||||
:param model_name: name of the model for which we will generate unique names
|
||||
:param names: list of names, we will ensure that each name will be unique
|
||||
:return: a list of new values for each name, in the same order
|
||||
"""
|
||||
# Avoid conflicting with itself, otherwise each check at update automatically
|
||||
# increments counters
|
||||
skip_record_ids = self.env.context.get("utm_check_skip_record_ids") or []
|
||||
# Remove potential counter part in each names
|
||||
names_without_counter = {self._split_name_and_count(name)[0] for name in names}
|
||||
|
||||
# Retrieve existing similar names
|
||||
search_domain = expression.OR([[('name', 'ilike', name)] for name in names_without_counter])
|
||||
if skip_record_ids:
|
||||
search_domain = expression.AND([
|
||||
[('id', 'not in', skip_record_ids)],
|
||||
search_domain
|
||||
])
|
||||
existing_names = {vals['name'] for vals in self.env[model_name].search_read(search_domain, ['name'])}
|
||||
|
||||
# Counter for each names, based on the names list given in argument
|
||||
# and the record names in database
|
||||
used_counters_per_name = {
|
||||
name: {
|
||||
self._split_name_and_count(existing_name)[1]
|
||||
for existing_name in existing_names
|
||||
if existing_name == name or existing_name.startswith(f'{name} [')
|
||||
} for name in names_without_counter
|
||||
}
|
||||
# Automatically incrementing counters for each name, will be used
|
||||
# to fill holes in used_counters_per_name
|
||||
current_counter_per_name = defaultdict(lambda: itertools.count(1))
|
||||
|
||||
result = []
|
||||
for name in names:
|
||||
if not name:
|
||||
result.append(False)
|
||||
continue
|
||||
|
||||
name_without_counter, asked_counter = self._split_name_and_count(name)
|
||||
existing = used_counters_per_name.get(name_without_counter, set())
|
||||
if asked_counter and asked_counter not in existing:
|
||||
count = asked_counter
|
||||
else:
|
||||
# keep going until the count is not already used
|
||||
for count in current_counter_per_name[name_without_counter]:
|
||||
if count not in existing:
|
||||
break
|
||||
existing.add(count)
|
||||
result.append(f'{name_without_counter} [{count}]' if count > 1 else name_without_counter)
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _split_name_and_count(name):
|
||||
"""
|
||||
Return the name part and the counter based on the given name.
|
||||
|
||||
e.g.
|
||||
"Medium" -> "Medium", 1
|
||||
"Medium [1234]" -> "Medium", 1234
|
||||
"""
|
||||
name = name or ''
|
||||
name_counter_re = r'(.*)\s+\[([0-9]+)\]'
|
||||
match = re.match(name_counter_re, name)
|
||||
if match:
|
||||
return match.group(1), int(match.group(2) or '1')
|
||||
return name, 1
|
||||
102
odoo-bringout-oca-ocb-utm/utm/models/utm_source.py
Normal file
102
odoo-bringout-oca-ocb-utm/utm/models/utm_source.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
|
||||
from odoo import _, api, fields, models, tools
|
||||
|
||||
|
||||
class UtmSource(models.Model):
|
||||
_name = 'utm.source'
|
||||
_description = 'UTM Source'
|
||||
|
||||
name = fields.Char(string='Source Name', required=True)
|
||||
|
||||
_sql_constraints = [
|
||||
('unique_name', 'UNIQUE(name)', 'The name must be unique'),
|
||||
]
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
new_names = self.env['utm.mixin']._get_unique_names(self._name, [vals.get('name') for vals in vals_list])
|
||||
for vals, new_name in zip(vals_list, new_names):
|
||||
vals['name'] = new_name
|
||||
return super().create(vals_list)
|
||||
|
||||
def _generate_name(self, record, content):
|
||||
"""Generate the UTM source name based on the content of the source."""
|
||||
if not content:
|
||||
return False
|
||||
|
||||
content = content.replace('\n', ' ')
|
||||
if len(content) >= 24:
|
||||
content = f'{content[:20]}...'
|
||||
|
||||
create_date = record.create_date or fields.date.today()
|
||||
create_date = fields.date.strftime(create_date, tools.DEFAULT_SERVER_DATE_FORMAT)
|
||||
model_description = self.env['ir.model']._get(record._name).name
|
||||
return _(
|
||||
'%(content)s (%(model_description)s created on %(create_date)s)',
|
||||
content=content, model_description=model_description, create_date=create_date,
|
||||
)
|
||||
|
||||
|
||||
class UtmSourceMixin(models.AbstractModel):
|
||||
"""Mixin responsible of generating the name of the source based on the content
|
||||
(field defined by _rec_name) of the record (mailing, social post,...).
|
||||
"""
|
||||
_name = 'utm.source.mixin'
|
||||
_description = 'UTM Source Mixin'
|
||||
|
||||
name = fields.Char('Name', related='source_id.name', readonly=False)
|
||||
source_id = fields.Many2one('utm.source', string='Source', required=True, ondelete='restrict', copy=False)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
# Exclude 'name' from fields_list to avoid retrieving it from context.
|
||||
return super().default_get([field for field in fields_list if field != "name"])
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
"""Create the UTM sources if necessary, generate the name based on the content in batch."""
|
||||
# Create all required <utm.source>
|
||||
utm_sources = self.env['utm.source'].create([
|
||||
{
|
||||
'name': values.get('name')
|
||||
or self.env.context.get('default_name')
|
||||
or self.env['utm.source']._generate_name(self, values.get(self._rec_name)),
|
||||
}
|
||||
for values in vals_list
|
||||
if not values.get('source_id')
|
||||
])
|
||||
|
||||
# Update "vals_list" to add the ID of the newly created source
|
||||
vals_list_missing_source = [values for values in vals_list if not values.get('source_id')]
|
||||
for values, source in zip(vals_list_missing_source, utm_sources):
|
||||
values['source_id'] = source.id
|
||||
|
||||
for values in vals_list:
|
||||
if 'name' in values:
|
||||
del values['name']
|
||||
|
||||
return super().create(vals_list)
|
||||
|
||||
def write(self, values):
|
||||
if (values.get(self._rec_name) or values.get('name')) and len(self) > 1:
|
||||
raise ValueError(
|
||||
_('You cannot update multiple records with the same name. The name should be unique!')
|
||||
)
|
||||
|
||||
if values.get(self._rec_name) and not values.get('name'):
|
||||
values['name'] = self.env['utm.source']._generate_name(self, values[self._rec_name])
|
||||
if values.get('name'):
|
||||
values['name'] = self.env['utm.mixin'].with_context(
|
||||
utm_check_skip_record_ids=self.source_id.ids
|
||||
)._get_unique_names("utm.source", [values['name']])[0]
|
||||
|
||||
super().write(values)
|
||||
|
||||
def copy(self, default=None):
|
||||
"""Increment the counter when duplicating the source."""
|
||||
default = default or {}
|
||||
default['name'] = self.env['utm.mixin']._get_unique_names("utm.source", [self.name])[0]
|
||||
return super().copy(default)
|
||||
16
odoo-bringout-oca-ocb-utm/utm/models/utm_stage.py
Normal file
16
odoo-bringout-oca-ocb-utm/utm/models/utm_stage.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class UtmStage(models.Model):
|
||||
"""Stage for utm campaigns."""
|
||||
|
||||
_name = 'utm.stage'
|
||||
_description = 'Campaign Stage'
|
||||
_order = 'sequence'
|
||||
|
||||
name = fields.Char(required=True, translate=True)
|
||||
sequence = fields.Integer(default=1)
|
||||
26
odoo-bringout-oca-ocb-utm/utm/models/utm_tag.py
Normal file
26
odoo-bringout-oca-ocb-utm/utm/models/utm_tag.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from random import randint
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class UtmTag(models.Model):
|
||||
"""Model of categories of utm campaigns, i.e. marketing, newsletter, ..."""
|
||||
|
||||
_name = 'utm.tag'
|
||||
_description = 'UTM Tag'
|
||||
_order = 'name'
|
||||
|
||||
def _default_color(self):
|
||||
return randint(1, 11)
|
||||
|
||||
name = fields.Char(required=True, translate=True)
|
||||
color = fields.Integer(
|
||||
string='Color Index', default=lambda self: self._default_color(),
|
||||
help='Tag color. No color means no display in kanban to distinguish internal tags from public categorization tags.')
|
||||
|
||||
_sql_constraints = [
|
||||
('name_uniq', 'unique (name)', "Tag name already exists !"),
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue