19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:27 +01:00
parent d1963a3c3a
commit 2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions

View file

@ -17,7 +17,7 @@ class IrHttp(models.AbstractModel):
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]:
if url_parameter in request.params and request.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

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, api, SUPERUSER_ID
from odoo import fields, models, api
class UtmCampaign(models.Model):
@ -9,6 +9,7 @@ class UtmCampaign(models.Model):
_description = 'UTM Campaign'
_rec_name = 'title'
active = fields.Boolean('Active', default=True)
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)
@ -19,7 +20,7 @@ class UtmCampaign(models.Model):
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')
copy=False, group_expand='_group_expand_stage_ids')
tag_ids = fields.Many2many(
'utm.tag', 'utm_tag_rel',
'tag_id', 'campaign_id', string='Tags')
@ -27,9 +28,10 @@ class UtmCampaign(models.Model):
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'),
]
_unique_name = models.Constraint(
'UNIQUE(name)',
'The name must be unique',
)
@api.depends('title')
def _compute_name(self):
@ -51,9 +53,9 @@ class UtmCampaign(models.Model):
return super().create(vals_list)
@api.model
def _group_expand_stage_ids(self, stages, domain, order):
def _group_expand_stage_ids(self, stages, domain):
"""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)
stage_ids = stages.sudo()._search([], order=stages._order)
return stages.browse(stage_ids)

View file

@ -5,6 +5,8 @@
from odoo import _, api, fields, models
from odoo.exceptions import UserError
import re
class UtmMedium(models.Model):
_name = 'utm.medium'
@ -14,9 +16,10 @@ class UtmMedium(models.Model):
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'),
]
_unique_name = models.Constraint(
'UNIQUE(name)',
'The name must be unique',
)
@api.model_create_multi
def create(self, vals_list):
@ -25,12 +28,40 @@ class UtmMedium(models.Model):
vals['name'] = new_name
return super().create(vals_list)
@property
def SELF_REQUIRED_UTM_MEDIUMS_REF(self):
return {
'utm.utm_medium_email': 'Email',
'utm.utm_medium_direct': 'Direct',
'utm.utm_medium_website': 'Website',
'utm.utm_medium_twitter': 'X',
'utm.utm_medium_facebook': 'Facebook',
'utm.utm_medium_linkedin': 'LinkedIn'
}
@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
))
def _unlink_except_utm_medium_record(self):
for medium in self.SELF_REQUIRED_UTM_MEDIUMS_REF:
utm_medium = self.env.ref(medium, raise_if_not_found=False)
if utm_medium and utm_medium in self:
raise UserError(_(
"Oops, you can't delete the Medium '%s'.\n"
"Doing so would be like tearing down a load-bearing wall \u2014 not the best idea.",
utm_medium.name
))
def _fetch_or_create_utm_medium(self, name, module='utm'):
name_normalized = re.sub(r"[\s|.]", "_", name.lower())
try:
return self.env.ref(f'{module}.utm_medium_{name_normalized}')
except ValueError:
utm_medium = self.sudo().env['utm.medium'].create({
'name': self.SELF_REQUIRED_UTM_MEDIUMS_REF.get(f'{module}.utm_medium_{name_normalized}', name)
})
self.sudo().env['ir.model.data'].create({
'name': f'utm_medium_{name_normalized}',
'module': module,
'res_id': utm_medium.id,
'model': 'utm.medium',
})
return utm_medium

View file

@ -6,8 +6,8 @@ from collections import defaultdict
import itertools
from odoo import api, fields, models
from odoo.fields import Domain
from odoo.http import request
from odoo.osv import expression
class UtmMixin(models.AbstractModel):
@ -15,11 +15,11 @@ class UtmMixin(models.AbstractModel):
_name = 'utm.mixin'
_description = 'UTM Mixin'
campaign_id = fields.Many2one('utm.campaign', 'Campaign',
campaign_id = fields.Many2one('utm.campaign', 'Campaign', index='btree_not_null',
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',
source_id = fields.Many2one('utm.source', 'Source', index='btree_not_null',
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',
medium_id = fields.Many2one('utm.medium', 'Medium', index='btree_not_null',
help="This is the method of delivery, e.g. Postcard, Email, or Banner Ad")
@api.model
@ -30,13 +30,13 @@ class UtmMixin(models.AbstractModel):
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():
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)
value = request.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)
@ -59,15 +59,43 @@ class UtmMixin(models.AbstractModel):
('utm_medium', 'medium_id', 'odoo_utm_medium'),
]
def _tracking_models(self):
fnames = {fname for _, fname, _ in self.tracking_fields()}
return {
self._fields[fname].comodel_name
for fname in fnames
if fname in self._fields and self._fields[fname].type == "many2one"
}
@api.model
def find_or_create_record(self, model_name, name):
""" Version of ``_find_or_create_record`` used in frontend notably in
website_links. For UTM models it calls _find_or_create_record. For other
models (as through inheritance custom models could be used notably in
website links) it simply calls a create. In the end it relies on
standard ACLs, and is mainly a wrapper for UTM models.
:return: id of newly created or found record. As the magic of call_kw
for create is not called anymore we have to manually return an id
instead of a recordset.
"""
if model_name in self._tracking_models():
record = self._find_or_create_record(model_name, name)
else:
record = self.env[model_name].create({self.env[model_name]._rec_name: name})
return {'id': record.id, 'name': record.display_name}
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)
cleaned_name = name.strip()
if cleaned_name:
record = Model.with_context(active_test=False).search([('name', '=ilike', cleaned_name)], limit=1)
if not record:
# No record found, create a new one
record_values = {'name': name}
record_values = {'name': cleaned_name}
if 'is_auto_campaign' in record._fields:
record_values['is_auto_campaign'] = True
record = Model.create(record_values)
@ -97,12 +125,9 @@ class UtmMixin(models.AbstractModel):
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])
search_domain = Domain.OR(Domain('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
])
search_domain &= Domain('id', 'not in', skip_record_ids)
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

View file

@ -1,8 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models, tools
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class UtmSource(models.Model):
@ -11,9 +10,17 @@ class UtmSource(models.Model):
name = fields.Char(string='Source Name', required=True)
_sql_constraints = [
('unique_name', 'UNIQUE(name)', 'The name must be unique'),
]
_unique_name = models.Constraint(
'UNIQUE(name)',
'The name must be unique',
)
@api.ondelete(at_uninstall=False)
def _unlink_except_referral(self):
utm_source_referral = self.env.ref('utm.utm_source_referral', raise_if_not_found=False)
for record in self:
if record == utm_source_referral:
raise ValidationError(_("You cannot delete the 'Referral' UTM source record."))
@api.model_create_multi
def create(self, vals_list):
@ -31,12 +38,13 @@ class UtmSource(models.Model):
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)
create_date = record.create_date or fields.Datetime.today()
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,
content=content,
model_description=model_description,
create_date=fields.Date.to_string(create_date),
)
@ -51,9 +59,9 @@ class UtmSourceMixin(models.AbstractModel):
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"])
def default_get(self, fields):
# Exclude 'name' from fields to avoid retrieving it from context.
return super().default_get([field for field in fields if field != "name"])
@api.model_create_multi
def create(self, vals_list):
@ -80,23 +88,26 @@ class UtmSourceMixin(models.AbstractModel):
return super().create(vals_list)
def write(self, values):
if (values.get(self._rec_name) or values.get('name')) and len(self) > 1:
def write(self, vals):
if (vals.get(self._rec_name) or vals.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(
if vals.get(self._rec_name) and not vals.get('name'):
vals['name'] = self.env['utm.source']._generate_name(self, vals[self._rec_name])
if vals.get('name'):
vals['name'] = self.env['utm.mixin'].with_context(
utm_check_skip_record_ids=self.source_id.ids
)._get_unique_names("utm.source", [values['name']])[0]
)._get_unique_names("utm.source", [vals['name']])[0]
super().write(values)
return super().write(vals)
def copy(self, default=None):
def copy_data(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)
default_name = default.get('name')
vals_list = super().copy_data(default=default)
for source, vals in zip(self, vals_list):
vals['name'] = self.env['utm.mixin']._get_unique_names("utm.source", [default_name or source.name])[0]
return vals_list

View file

@ -21,6 +21,7 @@ class UtmTag(models.Model):
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 !"),
]
_name_uniq = models.Constraint(
'unique (name)',
'Tag name already exists!',
)